feat: add versioned coord doctor and repair

This commit is contained in:
sid 2026-07-31 07:43:17 -06:00
parent c9a16229b1
commit c118c433c6
8 changed files with 171 additions and 6 deletions

View file

@ -33,6 +33,7 @@ standalone repository; inspect configuration before proposing publication.
- `hook.py` — pre-write lease enforcement and context delivery.
- `session.py` — harness lifecycle registration, heartbeat, and termination.
- `install` — preflighted vendoring, compatibility shims, hook merge, and skill installation.
- `VERSION` — semantic source/install release identifier exposed by both entry points.
- `skills/coord/` — canonical concise skill installed into both harness discovery locations.
- `tests/` — behavioral installer, CLI, documentation, and packaging tests.
- `DESIGN.md` — architectural rationale and security boundaries.
@ -96,6 +97,9 @@ When changing installation or packaging:
preservation, compatibility entry points, and identical dual-harness skills.
4. Confirm the installer vendors docs under `.coord/` without overwriting target-root `AGENTS.md`
or `CLAUDE.md`.
5. Bump `VERSION` for every shipped installed change: major for incompatibility, minor for new
backward-compatible behavior, and patch for compatible fixes. Drift checks remain content
based rather than trusting the version marker alone.
When changing the skill:

View file

@ -200,6 +200,13 @@ Behavioral checks cover:
## Implementation language and versioning
Each standalone release carries a semantic `VERSION`, vendored into `.coord/VERSION` and exposed
by `coord --version`. The installer doctor reports both source and installed versions but still
compares every managed byte and executable mode; equal version strings are not integrity evidence.
Legacy installations without the marker are explicitly `unversioned` and can be repaired through
the same preflighted atomic plan as any upgrade. This adds observability without coupling runtime
SQLite schema state to package release numbering.
Python plus the standard-library SQLite driver remains the current best fit: hooks can run before
a Nix activation or build bootstrap, deployment has no compiled-artifact handoff, and the
transaction boundary lives in SQLite rather than process memory. A Go rewrite would improve

View file

@ -41,6 +41,22 @@ conflicting one:
`--check` is read-only and exits nonzero on drift. Installation preflights all source files,
identity, wrappers, and hook JSON before replacing any target file.
For a version-aware diagnosis and an explicit repair workflow, use:
```sh
./install /path/to/repository --doctor
./install /path/to/repository --repair
.coord/coord --version
```
`--doctor` reports the source and installed versions, lists every stale or missing managed file,
and prints the exact repair command. An older installation without a version marker is reported as
`unversioned`. `--repair` uses the same fully preflighted, atomic replacement path as an ordinary
upgrade and preserves unrelated hooks, settings, skills, and repository content.
Run these commands from the authoritative standalone coord checkout whose release you want to
compare. A vendored `.coord/install . --doctor` can detect damage within its own installed release,
but it cannot discover a newer standalone release by itself.
An installation provides:
| Path | Purpose |
@ -59,6 +75,12 @@ history, or runtime database.
Codex pins hook source hashes and may prompt to trust a changed hook after an upgrade. Review and
approve it with `/hooks`; that prompt is an intentional harness security boundary.
The source release is recorded in `VERSION` using semantic `MAJOR.MINOR.PATCH` form and is vendored
as `.coord/VERSION`. Maintainers bump it whenever installed behavior or packaging changes: major
for incompatible changes, minor for backward-compatible functionality, and patch for compatible
fixes. Version equality is informational; byte-for-byte doctor checks remain authoritative for
detecting drift within a release.
## Everyday use
Hooks normally register the current runtime and export enough identity for commands to infer it.

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.1.0

2
coord
View file

@ -26,6 +26,7 @@ from typing import Any
HERE = pathlib.Path(__file__).resolve().parent
ROOT = HERE.parent if HERE.name == ".coord" else HERE
VERSION = (HERE / "VERSION").read_text(encoding="utf-8").strip()
SPEC = importlib.util.spec_from_file_location("coord_store", HERE / "store.py")
storelib = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
@ -1045,6 +1046,7 @@ def cmd_work_queue(args: argparse.Namespace) -> int:
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="coord")
parser.add_argument("--version", action="version", version=f"coord {VERSION}")
parser.add_argument(
"-i",
"--identity",

77
install
View file

@ -9,6 +9,7 @@ import os
from pathlib import Path
import re
import secrets
import shlex
import stat
import sys
from typing import Any
@ -21,6 +22,7 @@ CORE_FILES = (
"CLAUDE.md",
"DESIGN.md",
"README.md",
"VERSION",
"coord",
"hook.py",
"install",
@ -34,6 +36,9 @@ SKILL_FILES = (
"agents/openai.yaml",
)
PROJECT_ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}")
VERSION_RE = re.compile(
r"(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)"
)
CLAUDE_COMMANDS = {
"SessionStart": ('python3 "$CLAUDE_PROJECT_DIR/.coord/session.py"', 10),
@ -224,10 +229,32 @@ def project_id(target: Path, requested: str, *, check: bool) -> str:
return selected
def source_version() -> str:
try:
version = (HERE / "VERSION").read_text(encoding="utf-8").strip()
except OSError as exc:
raise ValueError(f"cannot read source VERSION: {exc}") from exc
if not VERSION_RE.fullmatch(version):
raise ValueError(f"refusing invalid source VERSION: {version!r}")
return version
def installed_version(target: Path) -> str:
marker = target / ".coord" / "VERSION"
if not marker.is_file():
return "unversioned"
try:
version = marker.read_text(encoding="utf-8").strip()
except OSError:
return "unreadable"
return version if VERSION_RE.fullmatch(version) else "invalid"
def install(target: Path, *, requested_id: str, check: bool) -> list[str]:
target = target.expanduser().resolve()
if not target.is_dir():
raise ValueError(f"target is not a directory: {target}")
source_version()
selected_id = project_id(target, requested_id, check=check)
destination = target / ".coord"
planned: list[tuple[Path, bytes, bool]] = []
@ -309,28 +336,66 @@ def install(target: Path, *, requested_id: str, check: bool) -> list[str]:
def main() -> int:
try:
release = source_version()
except ValueError as exc:
print(f"coord-install: {exc}", file=sys.stderr)
return 2
parser = argparse.ArgumentParser(prog="coord-install")
parser.add_argument("target", nargs="?", default=".")
parser.add_argument("--project-id", default="")
parser.add_argument("--check", action="store_true")
mode = parser.add_mutually_exclusive_group()
mode.add_argument("--check", action="store_true", help="check for drift")
mode.add_argument(
"--doctor",
action="store_true",
help="diagnose installation version and drift without changing files",
)
mode.add_argument(
"--repair",
action="store_true",
help="repair managed files and hooks while preserving unrelated content",
)
parser.add_argument(
"--version", action="version", version=f"coord-install {release}"
)
args = parser.parse_args()
target = Path(args.target).expanduser().resolve()
observed_version = installed_version(target)
try:
drift = install(
Path(args.target), requested_id=args.project_id, check=args.check
target,
requested_id=args.project_id,
check=args.check or args.doctor,
)
except ValueError as exc:
print(f"coord-install: {exc}", file=sys.stderr)
return 2
if args.check:
if args.check or args.doctor:
if args.doctor:
print(f"coord-install: source version {release}")
print(f"coord-install: installed version {observed_version}")
if drift:
print(f"coord-install: drift ({len(drift)} files)")
for path in drift:
print(f" {path}")
if args.doctor:
command = " ".join(
shlex.quote(value)
for value in (str(HERE / "install"), str(target), "--repair")
)
print(f"coord-install: repair with: {command}")
return 1
print("coord-install: installation matches")
print(f"coord-install: installation matches {release}")
return 0
print(f"coord-install: {'updated' if drift else 'already current'}")
print(f"target {Path(args.target).expanduser().resolve()}")
if args.repair and drift:
action = "repaired"
elif drift:
action = "updated"
else:
action = "already current"
print(f"coord-install: {action} {release}")
print(f"target {target}")
return 0

View file

@ -73,3 +73,8 @@ Lock release alone is not integration proof; record the exact integrated target
For architecture, failure modes, and recovery rules, read the repositorys `.coord/DESIGN.md`.
Repository-specific instructions in `AGENTS.md` and `CLAUDE.md` always take precedence.
When installation drift is suspected, run `<standalone-coord>/install "$PWD" --doctor` from the
target repository. It is read-only and prints source/installed versions plus an exact `--repair`
command. The vendored installer only knows its own release; use the authoritative standalone source
to detect upgrades, and review it before repairing because hook changes may require renewed trust.

View file

@ -15,6 +15,7 @@ import unittest
ROOT = Path(__file__).resolve().parents[1]
INSTALL = ROOT / "install"
RELEASE = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
class InstallBehavior(unittest.TestCase):
@ -58,10 +59,68 @@ class InstallBehavior(unittest.TestCase):
)
self.assertTrue((target / ".coord/AGENTS.md").is_file())
self.assertTrue((target / ".coord/CLAUDE.md").is_file())
self.assertEqual(
(target / ".coord/VERSION").read_text().strip(), RELEASE
)
self.assertFalse((target / "AGENTS.md").exists())
self.assertFalse((target / "CLAUDE.md").exists())
checked = self.run_install(target, "--check")
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn(f"matches {RELEASE}", checked.stdout)
def test_doctor_reports_unversioned_drift_and_repair_is_targeted(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
target = Path(temporary)
legacy = target / ".claude/coord"
legacy.mkdir(parents=True)
(legacy / "project-id").write_text("doctor-project\n")
stale = target / ".coord/coord"
stale.parent.mkdir(parents=True)
stale.write_text("stale managed file\n")
unrelated = target / ".agents/skills/local/SKILL.md"
unrelated.parent.mkdir(parents=True)
unrelated.write_text("operator content\n")
diagnosed = self.run_install(target, "--doctor")
self.assertEqual(diagnosed.returncode, 1, diagnosed.stderr)
self.assertIn(f"source version {RELEASE}", diagnosed.stdout)
self.assertIn("installed version unversioned", diagnosed.stdout)
self.assertIn("drift", diagnosed.stdout)
self.assertIn("--repair", diagnosed.stdout)
self.assertEqual(stale.read_text(), "stale managed file\n")
self.assertFalse((target / ".coord/VERSION").exists())
self.assertEqual(unrelated.read_text(), "operator content\n")
repaired = self.run_install(target, "--repair")
self.assertEqual(repaired.returncode, 0, repaired.stderr)
self.assertIn(f"repaired {RELEASE}", repaired.stdout)
self.assertEqual(unrelated.read_text(), "operator content\n")
healthy = self.run_install(target, "--doctor")
self.assertEqual(healthy.returncode, 0, healthy.stderr)
self.assertIn(f"installed version {RELEASE}", healthy.stdout)
self.assertIn(f"installation matches {RELEASE}", healthy.stdout)
version = subprocess.run(
[str(target / ".coord/coord"), "--version"],
cwd=target,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
self.assertEqual(version.returncode, 0, version.stderr)
self.assertEqual(version.stdout.strip(), f"coord {RELEASE}")
installer_version = subprocess.run(
[str(target / ".coord/install"), "--version"],
cwd=target,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
self.assertEqual(installer_version.returncode, 0, installer_version.stderr)
self.assertEqual(
installer_version.stdout.strip(), f"coord-install {RELEASE}"
)
def test_legacy_upgrade_preserves_hooks_identity_and_both_entrypoints(self) -> None:
with tempfile.TemporaryDirectory() as temporary: