2026-07-30 22:56:38 -06:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Install or verify a vendored, harness-neutral coordinator."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import re
|
|
|
|
|
import secrets
|
2026-07-31 07:43:17 -06:00
|
|
|
import shlex
|
2026-07-30 22:56:38 -06:00
|
|
|
import stat
|
|
|
|
|
import sys
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
|
|
|
CORE_FILES = (
|
|
|
|
|
".gitignore",
|
2026-07-30 23:23:53 -06:00
|
|
|
"AGENTS.md",
|
|
|
|
|
"CLAUDE.md",
|
2026-07-30 22:56:38 -06:00
|
|
|
"DESIGN.md",
|
|
|
|
|
"README.md",
|
2026-07-31 07:43:17 -06:00
|
|
|
"VERSION",
|
2026-07-30 22:56:38 -06:00
|
|
|
"coord",
|
|
|
|
|
"hook.py",
|
|
|
|
|
"install",
|
|
|
|
|
"session.py",
|
|
|
|
|
"store.py",
|
|
|
|
|
"workspace.py",
|
|
|
|
|
)
|
|
|
|
|
EXECUTABLES = {"coord", "install"}
|
2026-07-30 23:23:53 -06:00
|
|
|
SKILL_FILES = (
|
|
|
|
|
"SKILL.md",
|
|
|
|
|
"agents/openai.yaml",
|
|
|
|
|
)
|
2026-07-30 22:56:38 -06:00
|
|
|
PROJECT_ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}")
|
2026-07-31 07:43:17 -06:00
|
|
|
VERSION_RE = re.compile(
|
|
|
|
|
r"(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)"
|
|
|
|
|
)
|
2026-07-30 22:56:38 -06:00
|
|
|
|
|
|
|
|
CLAUDE_COMMANDS = {
|
|
|
|
|
"SessionStart": ('python3 "$CLAUDE_PROJECT_DIR/.coord/session.py"', 10),
|
|
|
|
|
"SessionEnd": ('python3 "$CLAUDE_PROJECT_DIR/.coord/session.py"', 5),
|
|
|
|
|
"PreToolUse": ('python3 "$CLAUDE_PROJECT_DIR/.coord/hook.py"', 10),
|
|
|
|
|
}
|
|
|
|
|
CODEX_DISCOVERY = (
|
|
|
|
|
'root="$PWD"; while [ "$root" != / ] && [ ! -f '
|
|
|
|
|
'"$root/.coord/{name}.py" ]; do root="${{root%/*}}"; '
|
|
|
|
|
'[ -n "$root" ] || root=/; done; '
|
|
|
|
|
'[ -f "$root/.coord/{name}.py" ] || exit 0; '
|
|
|
|
|
'python3 "$root/.coord/{name}.py"'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def legacy_wrapper(name: str) -> str:
|
|
|
|
|
if name == "coord":
|
|
|
|
|
return """#!/usr/bin/env python3
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
target = Path(__file__).resolve().parents[2] / ".coord" / "coord"
|
|
|
|
|
os.execv(str(target), [str(target), *sys.argv[1:]])
|
|
|
|
|
"""
|
|
|
|
|
if name in {"hook.py", "session.py"}:
|
|
|
|
|
return f"""#!/usr/bin/env python3
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import runpy
|
|
|
|
|
|
|
|
|
|
target = Path(__file__).resolve().parents[2] / ".coord" / "{name}"
|
|
|
|
|
runpy.run_path(str(target), run_name="__main__")
|
|
|
|
|
"""
|
|
|
|
|
if name in {"store.py", "workspace.py"}:
|
|
|
|
|
module = f"_coord_{name.removesuffix('.py')}_compat"
|
|
|
|
|
return f"""from importlib.util import module_from_spec, spec_from_file_location
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
target = Path(__file__).resolve().parents[2] / ".coord" / "{name}"
|
|
|
|
|
spec = spec_from_file_location("{module}", target)
|
|
|
|
|
assert spec is not None and spec.loader is not None
|
|
|
|
|
implementation = module_from_spec(spec)
|
|
|
|
|
sys.modules[spec.name] = implementation
|
|
|
|
|
spec.loader.exec_module(implementation)
|
|
|
|
|
globals().update({{
|
|
|
|
|
key: value for key, value in vars(implementation).items()
|
|
|
|
|
if key not in {{"__name__", "__loader__", "__package__", "__spec__"}}
|
|
|
|
|
}})
|
|
|
|
|
"""
|
|
|
|
|
raise ValueError(f"unsupported compatibility wrapper: {name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def read_json(path: Path) -> dict[str, Any]:
|
|
|
|
|
if not path.exists():
|
|
|
|
|
return {}
|
|
|
|
|
try:
|
|
|
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
|
|
|
raise ValueError(f"refusing invalid JSON at {path}: {exc}") from exc
|
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
|
raise ValueError(f"refusing non-object JSON at {path}")
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_coord_hook(item: object) -> bool:
|
2026-07-30 23:10:57 -06:00
|
|
|
if not isinstance(item, dict) or not isinstance(item.get("command"), str):
|
|
|
|
|
return False
|
|
|
|
|
command = item["command"]
|
|
|
|
|
return any(
|
|
|
|
|
endpoint in command
|
|
|
|
|
for endpoint in (
|
|
|
|
|
".coord/hook.py",
|
|
|
|
|
".coord/session.py",
|
|
|
|
|
".claude/coord/hook.py",
|
|
|
|
|
".claude/coord/session.py",
|
|
|
|
|
)
|
2026-07-30 22:56:38 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def merged_hooks(
|
|
|
|
|
current: dict[str, Any], desired: dict[str, tuple[str, int]]
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
result = json.loads(json.dumps(current))
|
|
|
|
|
hooks = result.setdefault("hooks", {})
|
|
|
|
|
if not isinstance(hooks, dict):
|
|
|
|
|
raise ValueError("refusing hook configuration whose 'hooks' value is not an object")
|
|
|
|
|
for event, (command, timeout) in desired.items():
|
|
|
|
|
existing = hooks.get(event, [])
|
|
|
|
|
if not isinstance(existing, list):
|
|
|
|
|
raise ValueError(f"refusing hook event {event!r} whose value is not a list")
|
|
|
|
|
retained = []
|
|
|
|
|
for group in existing:
|
|
|
|
|
if not isinstance(group, dict):
|
|
|
|
|
retained.append(group)
|
|
|
|
|
continue
|
|
|
|
|
entries = group.get("hooks")
|
|
|
|
|
if not isinstance(entries, list):
|
|
|
|
|
retained.append(group)
|
|
|
|
|
continue
|
|
|
|
|
kept_entries = [entry for entry in entries if not is_coord_hook(entry)]
|
|
|
|
|
if kept_entries:
|
|
|
|
|
kept_group = dict(group)
|
|
|
|
|
kept_group["hooks"] = kept_entries
|
|
|
|
|
retained.append(kept_group)
|
|
|
|
|
installed: dict[str, Any] = {
|
|
|
|
|
"hooks": [{"type": "command", "command": command, "timeout": timeout}]
|
|
|
|
|
}
|
|
|
|
|
if event == "PreToolUse":
|
|
|
|
|
installed["matcher"] = ".*"
|
|
|
|
|
retained.append(installed)
|
|
|
|
|
hooks[event] = retained
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def codex_hooks(current: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
desired = {
|
|
|
|
|
event: (
|
|
|
|
|
CODEX_DISCOVERY.format(
|
|
|
|
|
name="hook" if event == "PreToolUse" else "session"
|
|
|
|
|
),
|
|
|
|
|
timeout,
|
|
|
|
|
)
|
|
|
|
|
for event, (_command, timeout) in CLAUDE_COMMANDS.items()
|
|
|
|
|
}
|
|
|
|
|
return merged_hooks(current, desired)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_if_changed(
|
|
|
|
|
path: Path,
|
|
|
|
|
content: bytes,
|
|
|
|
|
*,
|
|
|
|
|
executable: bool = False,
|
|
|
|
|
check: bool,
|
|
|
|
|
drift: list[str],
|
|
|
|
|
) -> None:
|
|
|
|
|
observed = path.read_bytes() if path.is_file() else None
|
|
|
|
|
expected_mode = 0o755 if executable else 0o644
|
|
|
|
|
observed_executable = path.is_file() and bool(
|
|
|
|
|
stat.S_IMODE(path.stat().st_mode) & stat.S_IXUSR
|
|
|
|
|
)
|
|
|
|
|
mode_ok = path.is_file() and observed_executable == executable
|
|
|
|
|
if observed == content and mode_ok:
|
|
|
|
|
return
|
|
|
|
|
drift.append(str(path))
|
|
|
|
|
if check:
|
|
|
|
|
return
|
|
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
temporary = path.with_name(f".{path.name}.coord-new-{secrets.token_hex(6)}")
|
|
|
|
|
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, expected_mode)
|
|
|
|
|
try:
|
2026-07-30 23:10:57 -06:00
|
|
|
remaining = memoryview(content)
|
|
|
|
|
while remaining:
|
|
|
|
|
written = os.write(fd, remaining)
|
|
|
|
|
if written <= 0:
|
|
|
|
|
raise OSError(f"short write while creating {temporary}")
|
|
|
|
|
remaining = remaining[written:]
|
2026-07-30 22:56:38 -06:00
|
|
|
os.fsync(fd)
|
|
|
|
|
finally:
|
|
|
|
|
os.close(fd)
|
|
|
|
|
os.replace(temporary, path)
|
|
|
|
|
os.chmod(path, expected_mode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def project_id(target: Path, requested: str, *, check: bool) -> str:
|
|
|
|
|
canonical = target / ".coord" / "project-id"
|
|
|
|
|
legacy = target / ".claude" / "coord" / "project-id"
|
|
|
|
|
existing = []
|
|
|
|
|
for path in (canonical, legacy):
|
|
|
|
|
if path.is_file():
|
|
|
|
|
existing.append((path, path.read_text(encoding="utf-8").strip()))
|
|
|
|
|
values = {value for _path, value in existing}
|
|
|
|
|
if len(values) > 1:
|
|
|
|
|
detail = ", ".join(f"{path}={value!r}" for path, value in existing)
|
|
|
|
|
raise ValueError(f"refusing ambiguous existing project IDs: {detail}")
|
|
|
|
|
selected = next(iter(values), "")
|
|
|
|
|
if requested and selected and requested != selected:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"refusing project ID change from {selected!r} to {requested!r}"
|
|
|
|
|
)
|
|
|
|
|
selected = selected or requested
|
|
|
|
|
if not selected:
|
|
|
|
|
if check:
|
|
|
|
|
raise ValueError("installation has no project ID")
|
|
|
|
|
raise ValueError("new installation requires --project-id")
|
|
|
|
|
if not PROJECT_ID_RE.fullmatch(selected) or ".." in selected:
|
|
|
|
|
raise ValueError("project ID must be one safe immutable slug")
|
|
|
|
|
return selected
|
|
|
|
|
|
|
|
|
|
|
2026-07-31 07:43:17 -06:00
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
|
2026-07-30 22:56:38 -06:00
|
|
|
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}")
|
2026-07-31 07:43:17 -06:00
|
|
|
source_version()
|
2026-07-30 22:56:38 -06:00
|
|
|
selected_id = project_id(target, requested_id, check=check)
|
|
|
|
|
destination = target / ".coord"
|
2026-07-30 23:10:57 -06:00
|
|
|
planned: list[tuple[Path, bytes, bool]] = []
|
2026-07-30 22:56:38 -06:00
|
|
|
for name in CORE_FILES:
|
2026-07-30 23:10:57 -06:00
|
|
|
planned.append(
|
|
|
|
|
(
|
|
|
|
|
destination / name,
|
|
|
|
|
(HERE / name).read_bytes(),
|
|
|
|
|
name in EXECUTABLES,
|
|
|
|
|
)
|
2026-07-30 22:56:38 -06:00
|
|
|
)
|
2026-07-30 23:23:53 -06:00
|
|
|
for root in (
|
|
|
|
|
".coord/skills/coord",
|
|
|
|
|
".agents/skills/coord",
|
|
|
|
|
".claude/skills/coord",
|
|
|
|
|
):
|
|
|
|
|
for name in SKILL_FILES:
|
|
|
|
|
planned.append(
|
|
|
|
|
(
|
|
|
|
|
target / root / name,
|
|
|
|
|
(HERE / "skills" / "coord" / name).read_bytes(),
|
|
|
|
|
False,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-07-30 23:10:57 -06:00
|
|
|
planned.append(
|
|
|
|
|
(destination / "project-id", f"{selected_id}\n".encode(), False)
|
2026-07-30 22:56:38 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
legacy = target / ".claude" / "coord"
|
|
|
|
|
for name in ("coord", "hook.py", "session.py", "store.py", "workspace.py"):
|
2026-07-30 23:10:57 -06:00
|
|
|
planned.append(
|
|
|
|
|
(legacy / name, legacy_wrapper(name).encode(), name == "coord")
|
|
|
|
|
)
|
|
|
|
|
planned.extend(
|
|
|
|
|
(
|
|
|
|
|
(legacy / "project-id", f"{selected_id}\n".encode(), False),
|
|
|
|
|
(
|
|
|
|
|
legacy / "DESIGN.md",
|
|
|
|
|
b"Canonical design: [`../../.coord/DESIGN.md`](../../.coord/DESIGN.md).\n",
|
|
|
|
|
False,
|
|
|
|
|
),
|
|
|
|
|
(legacy / ".gitignore", b"state/\n__pycache__/\n", False),
|
2026-07-30 22:56:38 -06:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
settings = target / ".claude" / "settings.json"
|
|
|
|
|
expected_claude = (
|
|
|
|
|
json.dumps(
|
|
|
|
|
merged_hooks(read_json(settings), CLAUDE_COMMANDS),
|
|
|
|
|
indent=2,
|
|
|
|
|
sort_keys=True,
|
|
|
|
|
)
|
|
|
|
|
+ "\n"
|
|
|
|
|
).encode()
|
|
|
|
|
|
|
|
|
|
codex = target / ".codex" / "hooks.json"
|
|
|
|
|
expected_codex = (
|
|
|
|
|
json.dumps(codex_hooks(read_json(codex)), indent=2, sort_keys=True) + "\n"
|
|
|
|
|
).encode()
|
2026-07-30 23:10:57 -06:00
|
|
|
planned.extend(
|
|
|
|
|
(
|
|
|
|
|
(settings, expected_claude, False),
|
|
|
|
|
(codex, expected_codex, False),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# All source reads, identity checks, wrapper generation and hook JSON
|
|
|
|
|
# validation complete before the first target replacement.
|
|
|
|
|
drift: list[str] = []
|
|
|
|
|
for path, content, executable in planned:
|
|
|
|
|
write_if_changed(
|
|
|
|
|
path,
|
|
|
|
|
content,
|
|
|
|
|
executable=executable,
|
|
|
|
|
check=check,
|
|
|
|
|
drift=drift,
|
|
|
|
|
)
|
2026-07-30 22:56:38 -06:00
|
|
|
return drift
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
2026-07-31 07:43:17 -06:00
|
|
|
try:
|
|
|
|
|
release = source_version()
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
print(f"coord-install: {exc}", file=sys.stderr)
|
|
|
|
|
return 2
|
2026-07-30 22:56:38 -06:00
|
|
|
parser = argparse.ArgumentParser(prog="coord-install")
|
|
|
|
|
parser.add_argument("target", nargs="?", default=".")
|
|
|
|
|
parser.add_argument("--project-id", default="")
|
2026-07-31 07:43:17 -06:00
|
|
|
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}"
|
|
|
|
|
)
|
2026-07-30 22:56:38 -06:00
|
|
|
args = parser.parse_args()
|
2026-07-31 07:43:17 -06:00
|
|
|
target = Path(args.target).expanduser().resolve()
|
|
|
|
|
observed_version = installed_version(target)
|
2026-07-30 22:56:38 -06:00
|
|
|
try:
|
|
|
|
|
drift = install(
|
2026-07-31 07:43:17 -06:00
|
|
|
target,
|
|
|
|
|
requested_id=args.project_id,
|
|
|
|
|
check=args.check or args.doctor,
|
2026-07-30 22:56:38 -06:00
|
|
|
)
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
print(f"coord-install: {exc}", file=sys.stderr)
|
|
|
|
|
return 2
|
2026-07-31 07:43:17 -06:00
|
|
|
if args.check or args.doctor:
|
|
|
|
|
if args.doctor:
|
|
|
|
|
print(f"coord-install: source version {release}")
|
|
|
|
|
print(f"coord-install: installed version {observed_version}")
|
2026-07-30 22:56:38 -06:00
|
|
|
if drift:
|
|
|
|
|
print(f"coord-install: drift ({len(drift)} files)")
|
|
|
|
|
for path in drift:
|
|
|
|
|
print(f" {path}")
|
2026-07-31 07:43:17 -06:00
|
|
|
if args.doctor:
|
|
|
|
|
command = " ".join(
|
|
|
|
|
shlex.quote(value)
|
|
|
|
|
for value in (str(HERE / "install"), str(target), "--repair")
|
|
|
|
|
)
|
|
|
|
|
print(f"coord-install: repair with: {command}")
|
2026-07-30 22:56:38 -06:00
|
|
|
return 1
|
2026-07-31 07:43:17 -06:00
|
|
|
print(f"coord-install: installation matches {release}")
|
2026-07-30 22:56:38 -06:00
|
|
|
return 0
|
2026-07-31 07:43:17 -06:00
|
|
|
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}")
|
2026-07-30 22:56:38 -06:00
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|