diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..91299fc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +# AGENTS.md — notes for Codex and other repository agents + +**Full project instructions live in [`CLAUDE.md`](CLAUDE.md); read it first.** It is the source of +truth for the file map, invariants, change workflow, and validation contract. Read +[`DESIGN.md`](DESIGN.md) before changing isolation, identity, leases, cleanup, tmux, or hooks. + +## This is a Jujutsu repository + +The repository uses **Jujutsu (`jj`) with a colocated Git backend**. Use `jj` for status, diffs, +history, commits, bookmarks, rewriting, and pushes. Do not use Git history commands just because +`.git/` exists. Never force-push or rewrite published work. + +Run these before editing: + +```sh +hostname +jj st +``` + +## Do not regress packaging or safety + +- Edit standalone sources, never a consumer repository’s vendored `.coord/` copy. +- `.coord/` is canonical; `.claude/coord/` is compatibility only. +- Keep the canonical skill in `skills/coord/` and install identical copies for Codex and Claude. +- Preflight every source, identity, wrapper, skill, and JSON input before replacing target files. +- Preserve unrelated hooks, skills, settings, `AGENTS.md`, and `CLAUDE.md` in target repositories. +- Keep runtime SQLite state outside repositories and preserve immutable project IDs. +- Use independent `jj git clone --colocate` instances for concurrent writable work. +- Treat full IDs as authority; short names and five-word handles are presentation/resolution aids. +- Never guess during workspace cleanup or target tmux objects by their names. + +Validate with: + +```sh +python3 -m unittest discover -s tests -v +python3 -m py_compile coord hook.py install session.py store.py workspace.py +python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py skills/coord +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7e270c5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,134 @@ +# CLAUDE.md — coord maintainer and agent guide + +This is the complete instruction source for agents working on `coord`. Read [`README.md`](README.md) +for the user workflow and [`DESIGN.md`](DESIGN.md) for the architecture, isolation boundary, and +proved failure modes. + +## Repository and VCS + +This is a **Jujutsu (`jj`) repository with a colocated Git backend**. Use `jj` for status, diffs, +history, descriptions, commits, bookmarks, rebases, and pushes. Do not use `git add`, `git commit`, +`git rebase`, or Git history mutation merely because `.git/` exists. + +Useful commands: + +```sh +jj st +jj diff +jj log +jj describe -m "type: concise change" +jj new +jj bookmark set master -r @ +jj git push +``` + +Never force-push or rewrite published changes. A remote is not part of a fresh checkout of this +standalone repository; inspect configuration before proposing publication. + +## Purpose and source map + +- `coord` — command parser and coordinator operations. +- `store.py` — SQLite schema, identities, leases, messaging, and audit records. +- `workspace.py` — independent Jujutsu clones, lifecycle, integration evidence, tmux, and cleanup. +- `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. +- `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. + +The standalone files above are authoritative. Installed `.coord/` copies and `.claude/coord/` +compatibility shims in consumer repositories are generated artifacts, not alternate sources. + +## Invariants + +Preserve these properties in every change: + +1. `.coord/` is the canonical installed runtime. `.claude/coord/` remains a compatible shim for + existing sessions, including argv, cwd, environment, exit status, and module imports. +2. `skills/coord/SKILL.md` is the repository skill source. Install byte-identical skill trees at + `.coord/skills/coord/`, `.agents/skills/coord/`, and `.claude/skills/coord/`; do not embed + repository-specific policy in the generic skill. +3. A project ID is explicit on first install, immutable thereafter, and shared by clones of one + logical project. Never silently generate or replace it. +4. Runtime SQLite state stays outside repositories. Install, upgrade, check, and cleanup must not + copy, delete, reset, or rewrite unrelated state. +5. Finish all fallible source reads, project-ID checks, wrapper generation, skill reads, and hook + JSON validation before the first target replacement. Invalid input must leave the target + untouched. +6. Replace only exact known coord hook endpoints. Preserve unrelated hooks, JSON keys, agent + instructions, skills, and repository files. +7. Writes use a complete write loop, fsync, and atomic replacement. Do not leave partial files. +8. Full opaque IDs remain canonical evidence. Exact five-word handles may resolve identities; + prefixes and display-only shortened VCS IDs never carry authority. +9. Managed writable work defaults to independent colocated `jj git clone` instances. A native + `jj workspace` is opt-in because it shares an operation log and object store. +10. Workspace deletion never guesses. Require registry/on-disk ownership agreement, clean state, + gate evidence, integration evidence where needed, and exact confirmation. +11. Tmux names are presentation only. Create concise `-` names, but inspect and mutate + sessions, windows, and panes only through recorded immutable tmux IDs. +12. `batch` accepts only the low-risk allowlist, inherits one already-resolved identity, executes + ordinary handlers sequentially, stops on failure, and performs no hidden rollback. +13. Hooks fail open when coordinator infrastructure cannot load, but a positively recognized + conflicting write fails closed. Do not claim shell-command parsing or security isolation the + hook does not provide. +14. Publishing, integration, activation, deployment, and other external side effects require + explicit resource scopes and authoritative before/after evidence. Never force. + +## Change workflow + +Before editing, run `hostname` and `jj st`. Preserve unrelated working-copy changes. In a shared +consumer repository, use its installed coord policy and an isolated managed clone; do not develop +the standalone source by editing a vendored copy. + +When changing runtime behavior: + +1. Update the authoritative module. +2. Add a behavioral regression test that observes the property, not merely a source string. +3. Run the focused test and the full standard-library suite. +4. If the installed interface or model changes, update README, DESIGN, and the coord skill. + +When changing installation or packaging: + +1. Keep `CORE_FILES` and `SKILL_FILES` explicit. +2. Build the entire install plan before the first replacement. +3. Test fresh install, legacy migration, invalid-input no-write behavior, drift detection, hook + 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`. + +When changing the skill: + +1. Keep the frontmatter trigger description specific and the body concise. +2. Put detailed architecture in DESIGN rather than duplicating it into model context. +3. Regenerate `agents/openai.yaml` when its UI text becomes stale. +4. Run the skill validator and the packaging tests. + +## Validation + +Run: + +```sh +python3 -m unittest discover -s tests -v +python3 -m py_compile coord hook.py install session.py store.py workspace.py +python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py skills/coord +./coord --help +./coord work --help +./install --help +jj st +``` + +Tests must use temporary directories and an explicit `COORD_DB`; never exercise cleanup against +an operator’s real registry. If a required external binary such as `jj` or `tmux` is unavailable, +report that portion as unverified rather than implying it passed. + +## Documentation ownership + +- README: installation, common operations, state locations, compatibility, and development entry. +- CLAUDE: complete maintainer/agent rules and validation contract. +- AGENTS: short high-risk front-load that points here. +- DESIGN: implementation-independent rationale, boundaries, and threat model. +- Skill: minimal operational procedure loaded into model context. + +Update or remove stale statements rather than appending contradictory history. Historical names +retained for compatibility must be labelled as such. diff --git a/DESIGN.md b/DESIGN.md index c0bcb1f..5563bf6 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,6 +1,7 @@ # Agent coordination design -Status: implemented. Packaging and installation: [`README.md`](README.md). +Status: implemented. Packaging and installation: [`README.md`](README.md). Maintainer invariants: +[`CLAUDE.md`](CLAUDE.md). ## Boundary that actually isolates work @@ -19,8 +20,10 @@ one repository is intentional, but it shares the operation log/object store and default isolation boundary. In this repository its secondary directory also lacks an ordinary `.git`, so `nix flake metadata .` does not discover the colocated Git flake normally. -Managed clones live under `/var/tmp/fleet-audit`, the large non-snapshotted scratch dataset. -`/tmp` and `/home` are on the nearly-full snapshotted dataset and must not receive agent clones. +The compatibility default for managed clones is `/var/tmp/fleet-audit`, the large +non-snapshotted scratch dataset in the original deployment. Other installations should select a +safe local dataset with `--scratch-root`; avoid any location whose capacity, snapshots, backup, +or mount semantics make disposable clones expensive. ## Identity diff --git a/README.md b/README.md index b467676..b90bcbe 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,177 @@ # coord -Harness-neutral coordination for Claude Code, Codex, and other repository agents. +`coord` is a small, harness-neutral coordinator for concurrent Claude Code, Codex, and other +repository agents. It combines: -The standalone project is the source of truth. Install a vendored copy into another repository: +- exact five-word agent and workspace handles; +- transactional resource leases and durable messages in host-local SQLite; +- isolated Jujutsu clones with recorded lifecycle, integration, and cleanup evidence; +- tmux launch, resume, attach, and safe-close operations using immutable tmux object IDs; +- shared lifecycle and write-conflict hooks for Claude Code and Codex; and +- a concise `coord` skill installed for both harnesses. + +The standalone project is the source of truth. [`DESIGN.md`](DESIGN.md) explains the isolation and +threat model. [`CLAUDE.md`](CLAUDE.md) is the complete maintainer and agent guide; [`AGENTS.md`](AGENTS.md) +front-loads its most important rules. + +## Requirements + +- Python 3 with the standard-library `sqlite3` module +- Jujutsu (`jj`) and Git for managed independent clones +- tmux plus `codex-direct` and/or `claude-direct` for managed interactive sessions + +The coordinator itself has no third-party Python dependencies. + +## Install or upgrade a repository + +Choose a stable project ID shared by every clone of the same logical project: ```sh ./install /path/to/repository --project-id my-logical-project ``` -An installation provides: - -- `.coord/` — canonical CLI, hooks, runtime modules, installer, design, and immutable project ID. -- `.claude/coord/` — compatibility shims for existing sessions and old instructions. -- merged `.claude/settings.json` and `.codex/hooks.json` entries pointing at `.coord/`. - -Upgrade an existing installation with the same command. Existing project IDs are preserved and a -conflicting requested ID is refused. Check drift without writing: +On later upgrades, omit `--project-id`; the installer preserves the existing value and refuses a +conflicting one: ```sh +./install /path/to/repository ./install /path/to/repository --check ``` -Runtime state remains outside repositories in the private host-local SQLite database described in -[`DESIGN.md`](DESIGN.md). Installing or upgrading never copies, deletes, or rewrites that state. +`--check` is read-only and exits nonzero on drift. Installation preflights all source files, +identity, wrappers, and hook JSON before replacing any target file. + +An installation provides: + +| Path | Purpose | +|---|---| +| `.coord/` | Canonical CLI, hook/runtime modules, installer, docs, skill source, and project ID | +| `.agents/skills/coord/` | Codex/project skill, including UI metadata | +| `.claude/skills/coord/` | Claude Code copy of the same skill | +| `.claude/coord/` | Compatibility shims for older sessions and instructions | +| `.codex/hooks.json` | Merged Codex lifecycle and write-conflict hooks | +| `.claude/settings.json` | Merged Claude lifecycle and write-conflict hooks | + +The installer replaces only hooks that invoke known coord endpoints. It preserves unrelated hooks +and unrelated settings. It does not alter the target repository’s `AGENTS.md`, `CLAUDE.md`, VCS +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. + +## Everyday use + +Hooks normally register the current runtime and export enough identity for commands to infer it. +Use the canonical entry point from the repository root: + +```sh +.coord/coord list +.coord/coord claim tests/checks.nix -p "add cache checks" +.coord/coord send "I am changing the cache schema" --to '*' +.coord/coord release tests/checks.nix +``` + +Use `append` only when simultaneous additions cannot overwrite one another: + +```sh +.coord/coord claim tests/checks.nix --mode append -p "append one independent check" +``` + +Compose low-risk coordinator actions with `:` separators: + +```sh +.coord/coord batch claim docs/plan.md -p "revise plan" : send "plan claimed" --to '*' +``` + +The batch stops at the first failure and is deliberately not atomic. Destructive workspace and +external publication actions are excluded. + +Create an isolated Jujutsu clone before writable concurrent work: + +```sh +.coord/coord work create cache-audit +.coord/coord work list +.coord/coord work gc +``` + +`work create` prints the exact five-word work handle and path. Then use that handle for lifecycle +operations: + +```sh +.coord/coord work start --harness codex +.coord/coord work attach --execute +.coord/coord work park --reason "waiting for review" +.coord/coord work resume +.coord/coord work inspect +``` + +Managed windows have short human-readable names, while mutations target immutable tmux IDs. +`work close` safely closes the recorded window without dropping the operator to an outer shell. +`work remove` is conservative: inspect its refusal, integration, gate, and confirmation +requirements before cleanup. + +Run `coord COMMAND --help` or `coord work COMMAND --help` for the complete option surface. Legacy +`--sid`, `--paths`, and similar spellings remain accepted for compatibility but are hidden; new +instructions should rely on inferred identity, positional operands, and readable optional flags. + +### Command map + +| Command | Role | +|---|---| +| `coord hello` | Register a live runtime; normally called by lifecycle hooks | +| `coord heartbeat` | Renew runtime presence and held leases | +| `coord bye` | End a runtime attachment without ending its durable conversation | +| `coord claim` / `coord renew` / `coord release` | Manage transactional resource leases | +| `coord list` | Show live agents, leases, and optionally work records | +| `coord send` / `coord inbox` / `coord ack` | Deliver and acknowledge durable messages | +| `coord check` | Ask whether a resource is blocked; used by write hooks | +| `coord audit` | Export deterministic audit events | +| `coord batch` | Compose guarded low-risk actions separated by `:` | +| `coord remote verify` | Read and verify an authoritative remote bookmark | +| `coord work …` | Create, inspect, run, integrate, recover, and clean managed work | + +## State, identity, and portability + +Runtime state defaults to: + +```text +$XDG_STATE_HOME/fleet-agent-coord/.sqlite3 +``` + +or `~/.local/state/fleet-agent-coord/…` when `XDG_STATE_HOME` is unset. The directory and SQLite +files are private to the user. `COORD_DB` selects a different database, and `COORD_PROJECT_ID` +overrides project discovery for controlled testing. The historical `fleet-agent-coord` directory +name is retained so existing deployments do not silently split their state. + +The managed-clone default is `/var/tmp/fleet-audit`, retained for compatibility with the original +deployment. Use `work create --scratch-root /safe/local/path` when that directory is inappropriate. +The cleanup registry never scans arbitrary directories or guesses ownership. + +Five-word handles are collision-checked aliases. Full UUIDs and full VCS object IDs remain the +authoritative stored evidence; prefixes and shortened IDs never authorize mutations. + +## Skills + +The canonical repository skill source is [`skills/coord/SKILL.md`](skills/coord/SKILL.md). An +installation retains it at `.coord/skills/coord/` and puts identical discovery copies under both +supported harness paths. Invoke it explicitly as `$coord`, or let its description trigger when +coordinating agents, claiming shared resources, creating isolated work, handling messages, +managing tmux sessions, or cleaning up managed work. + +The skill is intentionally concise. Detailed architecture stays in `.coord/DESIGN.md`, while +repository-specific policies belong in the target repository’s own agent instructions. + +## Develop and validate + +This repository is a colocated Jujutsu repository. Use `jj`, not Git, for status and history: + +```sh +jj st +jj diff +python3 -m unittest discover -s tests -v +python3 -m py_compile coord hook.py install session.py store.py workspace.py +python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py skills/coord +``` + +For an installer change, test both a fresh install and an in-place legacy upgrade. Do not publish +or force-update a remote without explicit authorization. No remote is configured by default. diff --git a/install b/install index 74ee748..fd2d656 100755 --- a/install +++ b/install @@ -17,6 +17,8 @@ from typing import Any HERE = Path(__file__).resolve().parent CORE_FILES = ( ".gitignore", + "AGENTS.md", + "CLAUDE.md", "DESIGN.md", "README.md", "coord", @@ -27,6 +29,10 @@ CORE_FILES = ( "workspace.py", ) EXECUTABLES = {"coord", "install"} +SKILL_FILES = ( + "SKILL.md", + "agents/openai.yaml", +) PROJECT_ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}") CLAUDE_COMMANDS = { @@ -233,6 +239,19 @@ def install(target: Path, *, requested_id: str, check: bool) -> list[str]: name in EXECUTABLES, ) ) + 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, + ) + ) planned.append( (destination / "project-id", f"{selected_id}\n".encode(), False) ) diff --git a/skills/coord/SKILL.md b/skills/coord/SKILL.md new file mode 100644 index 0000000..ebfdce3 --- /dev/null +++ b/skills/coord/SKILL.md @@ -0,0 +1,75 @@ +--- +name: coord +description: Coordinate concurrent repository agents with transactional resource leases, durable messages, isolated Jujutsu clones, managed tmux sessions, integration evidence, and ownership-checked cleanup. Use when an agent needs to inspect peer work, claim or release shared resources, message another session, create or resume isolated work, start or close an agent window, queue integration, or garbage-collect managed work. +--- + +# Coordinate repository agents + +Use the repository’s `.coord/coord` entry point. Prefer inferred identity from hooks or a managed +shell; use `-i ` only for manual recovery or selection. Never use prefixes. + +## Choose the boundary + +- For ordinary writable implementation, create an isolated clone with + `.coord/coord work create `. Work inside the returned path. +- For a shared external boundary, claim an explicit resource such as `integration/master`, + `push/origin/master`, or a deployment target. +- Claim files in a shared checkout only when isolation is unavailable. Use `--mode append` solely + when concurrent additions cannot overwrite one another. + +Inspect peers before coordinating: + +```sh +.coord/coord list +.coord/coord work list +``` + +## Claim and communicate + +Use positional resources and concise purposes: + +```sh +.coord/coord claim tests/checks.nix -p "add one cache check" +.coord/coord send "cache schema is changing" --to '*' +.coord/coord release tests/checks.nix +``` + +Treat a refused claim as authoritative. Message the exact holder handle or choose disjoint work; +do not bypass a lease. Acknowledge delivered messages after acting on them: + +```sh +.coord/coord inbox --peek +.coord/coord ack +``` + +Use `batch ACTION : ACTION...` only for its guarded low-risk commands. It is sequential, stops at +the first failure, and is not atomic. + +## Manage isolated work + +Use exact five-word work handles returned by the CLI: + +```sh +.coord/coord work start --harness codex +.coord/coord work attach --execute +.coord/coord work park --reason "waiting" +.coord/coord work resume +.coord/coord work inspect +.coord/coord work gc +``` + +Use `work close`, not raw pane/window killing, for a managed agent. Window names are human labels; +the coordinator mutates only recorded immutable tmux IDs. + +Before removal, inspect the work record. Never bypass dirty-tree, ownership, gate, or integration +refusals. Use `work adopt` only for a known independent clone with explicit recovery evidence. + +## Cross shared boundaries + +Acquire the exact resource before integration, push, activation, deployment, or another external +mutation. Verify the authoritative remote or target before and after the action. Never force-push. +Lock release alone is not integration proof; record the exact integrated target with +`work queue integrate --target `. + +For architecture, failure modes, and recovery rules, read the repository’s `.coord/DESIGN.md`. +Repository-specific instructions in `AGENTS.md` and `CLAUDE.md` always take precedence. diff --git a/skills/coord/agents/openai.yaml b/skills/coord/agents/openai.yaml new file mode 100644 index 0000000..23d0f23 --- /dev/null +++ b/skills/coord/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Agent Coordinator" + short_description: "Coordinate agents, leases, workspaces, and tmux" + default_prompt: "Use $coord to organize concurrent agent work safely." diff --git a/tests/test_docs.py b/tests/test_docs.py new file mode 100644 index 0000000..c55f130 --- /dev/null +++ b/tests/test_docs.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Keep entry documentation, skill packaging, and the CLI surface aligned.""" + +from __future__ import annotations + +import importlib.machinery +import importlib.util +from pathlib import Path +import subprocess +import sys +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + + +class DocumentationContract(unittest.TestCase): + def test_required_document_map_and_jj_policy(self) -> None: + readme = (ROOT / "README.md").read_text(encoding="utf-8") + claude = (ROOT / "CLAUDE.md").read_text(encoding="utf-8") + agents = (ROOT / "AGENTS.md").read_text(encoding="utf-8") + design = (ROOT / "DESIGN.md").read_text(encoding="utf-8") + + self.assertIn("[`CLAUDE.md`](CLAUDE.md)", readme) + self.assertIn("[`DESIGN.md`](DESIGN.md)", readme) + self.assertIn("[`CLAUDE.md`](CLAUDE.md)", agents) + self.assertIn("Jujutsu (`jj`)", claude) + self.assertIn("Jujutsu (`jj`)", agents) + self.assertIn("Status: implemented", design) + + def test_readme_names_every_public_top_level_command(self) -> None: + loader = importlib.machinery.SourceFileLoader("coord_docs_test", str(ROOT / "coord")) + spec = importlib.util.spec_from_loader(loader.name, loader) + assert spec is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + loader.exec_module(module) + parser = module.build_parser() + choices: set[str] = set() + for action in parser._actions: + if hasattr(action, "choices") and isinstance(action.choices, dict): + choices.update(action.choices) + + readme = (ROOT / "README.md").read_text(encoding="utf-8") + missing = sorted(name for name in choices if f"coord {name}" not in readme) + self.assertEqual(missing, [], f"README omits public commands: {missing}") + + def test_help_entrypoints_are_live(self) -> None: + for command in ( + [str(ROOT / "coord"), "--help"], + [str(ROOT / "coord"), "work", "--help"], + [str(ROOT / "install"), "--help"], + ): + result = subprocess.run( + command, + cwd=ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("usage:", result.stdout) + + def test_skill_is_concise_and_has_no_placeholders(self) -> None: + skill = (ROOT / "skills/coord/SKILL.md").read_text(encoding="utf-8") + self.assertNotIn("TODO", skill) + self.assertLess(len(skill.splitlines()), 120) + for operation in ("claim", "send", "work create", "work close", "work gc"): + self.assertIn(operation, skill) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_install.py b/tests/test_install.py index 0b586f6..ab42ddc 100755 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -41,6 +41,25 @@ class InstallBehavior(unittest.TestCase): (target / ".coord/project-id").read_text().strip(), "fresh-project", ) + codex_skill = target / ".agents/skills/coord/SKILL.md" + claude_skill = target / ".claude/skills/coord/SKILL.md" + canonical_skill = target / ".coord/skills/coord/SKILL.md" + self.assertEqual(codex_skill.read_bytes(), claude_skill.read_bytes()) + self.assertEqual(codex_skill.read_bytes(), canonical_skill.read_bytes()) + self.assertEqual( + codex_skill.read_bytes(), + (ROOT / "skills/coord/SKILL.md").read_bytes(), + ) + self.assertEqual( + (target / ".agents/skills/coord/agents/openai.yaml").read_bytes(), + ( + target / ".claude/skills/coord/agents/openai.yaml" + ).read_bytes(), + ) + self.assertTrue((target / ".coord/AGENTS.md").is_file()) + self.assertTrue((target / ".coord/CLAUDE.md").is_file()) + 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) @@ -50,6 +69,11 @@ class InstallBehavior(unittest.TestCase): legacy = target / ".claude/coord" legacy.mkdir(parents=True) (legacy / "project-id").write_text("legacy-project\n") + (target / "AGENTS.md").write_text("target agents\n") + (target / "CLAUDE.md").write_text("target claude\n") + unrelated_skill = target / ".claude/skills/local/SKILL.md" + unrelated_skill.parent.mkdir(parents=True) + unrelated_skill.write_text("target skill\n") (target / ".claude/settings.json").write_text( json.dumps( { @@ -112,6 +136,9 @@ class InstallBehavior(unittest.TestCase): (target / ".claude/coord/project-id").read_text().strip(), "legacy-project", ) + self.assertEqual((target / "AGENTS.md").read_text(), "target agents\n") + self.assertEqual((target / "CLAUDE.md").read_text(), "target claude\n") + self.assertEqual(unrelated_skill.read_text(), "target skill\n") settings = json.loads((target / ".claude/settings.json").read_text()) self.assertEqual(settings["effortLevel"], "high") commands = [ @@ -268,6 +295,9 @@ class InstallBehavior(unittest.TestCase): sentinel = legacy / "coord" sentinel.write_text("legacy-sentinel\n") (target / ".claude/settings.json").write_text("{ invalid") + unrelated_skill = target / ".agents/skills/local/SKILL.md" + unrelated_skill.parent.mkdir(parents=True) + unrelated_skill.write_text("local-sentinel\n") refused = self.run_install(target) self.assertEqual(refused.returncode, 2) @@ -275,6 +305,7 @@ class InstallBehavior(unittest.TestCase): self.assertEqual(sentinel.read_text(), "legacy-sentinel\n") self.assertFalse((target / ".coord").exists()) self.assertFalse((target / ".codex").exists()) + self.assertEqual(unrelated_skill.read_text(), "local-sentinel\n") if __name__ == "__main__":