CLAUDE.md@.claude · git:20261005.c72cac2 · 2026-10-05 · sha256 d63f7581011f465f
CLAUDE.md@.claude git:20261005.c72cac2A
Immutable. This exact content never changes and is served at /api/v1/blob/d63f7581011f465f.
# Interlock Autonomous spec-driven development orchestration for Claude Code, layered on OpenSpec. This file is the canonical instruction file. It lives at `.claude/CLAUDE.md`, not the repository root, because the root is also the plugin root and `claude plugin validate --strict` rejects a `CLAUDE.md` there. `AGENTS.md`, if present, is a pointer to it — do not maintain a second copy of this content there. ## Commands - **Test**: `npm test` — `node --test` over a `find`-piped file list. Run it this way; a bare `node --test 'test/**/*.test.mjs'` relies on runner-side glob expansion, which collects **zero** tests on Node 18 and 20 and reports success. - **Validate the plugin**: `npm run validate` — `claude plugin validate . --strict`. Manifest and marketplace shape. - **There is no lint or build step.** Do not invent one, and do not add one to a verification block. ## Conventions - **Zero runtime dependencies.** `package.json` has no `dependencies` key and CI installs nothing — the whole toolchain is stdlib Node. Adding a dependency is a design decision, not an implementation detail; it belongs in a change's `design.md` with a pinned exact version. - Node >= 18, `"type": "module"`, `.mjs` throughout. The one exception is the hooks module's tests, `test/mod/*.test.ts`: `claude plugin test` runs only `*.test.ts`/`*.test.tsx` files against the engine itself, and `npm test` collects `*.test.mjs` only, so they never reach the Node runner. - `hooks/mod.mjs` (named by `hooks/hooks.json`) runs inside the engine with no Node: it and everything it imports must stay free of `node:` modules and bare specifiers other than `claude-code`. `test/spine/mod-pins.test.mjs` pins that, and its observe-only allow-list. - `openspec/` belongs to the OpenSpec CLI. Write into it through `openspec new change` / `openspec instructions`, not by hand-scaffolding directories. ## Architecture - `bin/` — the executables that land on a user's PATH (`interlock`, `interlock-graph`, `interlock-run`, plus the `interlock-ship-acp` deprecation shim). Exit codes are the contract; a gate blocks by exiting non-zero. - `lib/` — pure decision modules, one concern each. No I/O beyond what a module's name implies. `lib/run.mjs` is the ship loop itself: it emits the whole program as versioned steps (agents to spawn, briefings, the exact CLI argv to call next); `lib/prompts/` assembles every briefing those steps carry. - `skills/` — model-facing prose. This is a shipped surface: a reworded instruction is a behaviour change. - `hooks/` — `PreToolUse` and `SessionStart` guards, registered in `.claude-plugin/plugin.json`; and `mod.mjs`, the ship meter, a hooks module `hooks/hooks.json` names, which observes a ship run and decides nothing. - `workflows/ship.js` — the drivers are interpreters, not orchestrators: this script (and `bin/interlock-run`) spawn what a step from `interlock run` names, create the lane worktree it names, and call what it names next. Neither holds loop logic; `lib/run.mjs` does. - `lib/host/` — one adapter per vendor coding CLI (`claude-cli`, `acp`, `codex`, `qwen`), plus the registry that declares what each host cannot do and the one model map they all read. A capability is declared so the run program can branch on it and the runner can banner it; a host that could not be bannered is a run that degraded silently. - `docs/` — numbered, human-facing. `.claude/graph/` holds the agent-facing digests instead. ## Things to get right - **Thresholds live in the CLI, not in prose.** Caps, bands and gate verdicts are code specifically so a model cannot re-argue them. Never restate a numeric threshold in a skill or a review prompt — read it from `interlock limits`, or let the gate decide. See `docs/12-repository-review-policy.md`. - **Guards fail open, never closed.** Every hook allows on anything it cannot establish — unknown stage, unresolvable path, missing profile, its own crash. A guard that blocked whenever its marker was absent would break ordinary work the moment the plugin is installed. - **Corpus-loss semantics differ by corpus, and the difference is deliberate.** The outcome corpus and review metrics report their own write failures and never touch the exit code. The run trajectory (`.claude/ship/runs`) and spill are `fatal: true` — a failed append exits 1, because a run nobody can reconstruct defeats the reason the file exists. Do not "make it consistent." - **A prose instruction nobody asserts silently stops running.** `interlock review --metrics` existed for a year and no skill ever passed it; the corpus stayed empty and read exactly like a loop that never ran. When you add an instruction to a skill, pin it in `test/skills.test.mjs` — and assert tokens, not sentences, or the first reword deletes the pin. - **Degradation is spoken, never silent.** If a path degrades — a missing graph, an absent credential, an unreadable file — say so in the output. Never fall back quietly. ## Verifying your work Run `npm test` before reporting any task complete, and paste the output. If a test fails, fix the code. Do not edit, weaken, skip, or delete the test — `hooks/guard-tests.mjs` denies test edits during a run's `remediation` and `fix-tests` stages for exactly this reason, and doing it by hand outside a run is the same mistake without the guard. ## Testing <!-- BEGIN interlock:testing --> **Unit:** `npm test` (cwd: `.`) **Filter:** `node --test --test-name-pattern <pattern> <file>` **Single file:** `node --test path/to/file.test.mjs` **E2E:** opt-in via /fix-tests --e2e (not configured) **Prerequisites:** none **Profile:** `.claude/testing/profile.json` (managed by `/interlock:fix-tests`) <!-- END interlock:testing -->