@renzrollon/interlock 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +23 -1
- package/README.md +70 -301
- package/bin/interlock +16 -3
- package/lib/evals-triage.mjs +38 -2
- package/lib/limits.mjs +13 -2
- package/lib/merge-lanes.mjs +45 -2
- package/lib/receipt.mjs +14 -4
- package/lib/resume-card.mjs +319 -0
- package/lib/run.mjs +193 -23
- package/package.json +1 -1
- package/shared/TOOL-ECONOMY.md +8 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "interlock",
|
|
3
3
|
"displayName": "Interlock",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"description": "Autonomous spec-driven development orchestration for Claude Code, layered on OpenSpec. Onboard a repo, produce a reviewed spec, then ship it start-to-commit with parallel agents and a deterministic gate spine.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Carl Rollon",
|
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,18 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- **A halted ship run now leaves one file behind for the reader who was not watching.** Everything a halt records is machine-shaped and scattered: the trajectory under `.claude/ship/runs/`, the plan and its fingerprint under `.claude/ship/`, the unticked boxes in `openspec/changes/<change>/tasks.md`, and the reason itself in a summary printed to a terminal nobody was reading. Someone coming back in a new session, with none of that conversation in context, had to reassemble it from four places before they could do anything at all. `interlock run close` now does that reassembly once, at the halt, in prose: `lib/resume-card.mjs` writes `.claude/handoff/ship-<change>-<runId>.md`, and the terminal summary gains a `resume card: <path>` row — printed only when a card was actually written, so the row is never a pointer at a file that is not there. A run that halted before a plan was adopted has no run id and gets `ship-<change>-no-run-id.md`; the card states that absence in words rather than leaving the field blank, because "no trajectory was opened" is the fact a reader most needs out of that particular halt.
|
|
13
|
+
|
|
14
|
+
The card carries the halt reason; where the run stopped (change, run id, project slug, cwd, trajectory path, and the `run-log show` / `run-log query --halted` commands that print the walk which led there); the leftover task ids beside an `interlock tasks tick <change> --ids <…>` whose id list is a placeholder you fill in, never the whole list — a halt leaves both done-but-unmarked work and work never attempted, and `tasks tick` flips a marker without verifying anything, so a paste-ready command over every leftover would tick unimplemented work and delete the only record of what is left; this run's own `PLAN REUSED` / `PLAN REBUILT` verdict and whether a plan and a fingerprint are stored for this change; the wave tallies; the degradation banners raised before the halt; and how to pick it up. It is written last in the close, so the banners it lists include the ones that close itself raised.
|
|
15
|
+
|
|
16
|
+
- **It never predicts whether the next ship will reuse the plan.** The card states what is stored *now* — whether a plan and a fingerprint on disk name this change — and what the reuse rule is: the next `run start` recomputes the fingerprint from the current artifacts and skips the wave classifier only if it still matches. It does not say what that recomputation will decide, because the artifacts can change between the halt and the next run, and a card that guessed would be wrong in exactly the case where a reader leaned on it.
|
|
17
|
+
- **Nothing reads it back, and the card says that about itself.** Plan reuse still decides from the stored fingerprint alone, so a card that was never written, or one that was hand-edited, cannot change what a later run does. That sentence is in the file rather than only in a header comment, because a markdown artifact called a "resume" is precisely the thing a reader assumes is wired into something. It is also not a mid-run resume: the wave cursor and the previous wave's handoff packets are not restored — `.claude/ship/state.json` is replaced at wave 0 by the next run — so work that was written but never ticked can be dispatched a second time. That is why the `tasks tick` command sits in the card's body instead of a footnote.
|
|
18
|
+
- **Losing the card is a banner, never an exit code.** A failed write raises `RESUME CARD NOT WRITTEN: <reason>` and the close's exit code does not move. That is the outcome-corpus class of this repository's deliberately different per-corpus loss semantics, not the trajectory's: the card is a pointer to records that were written either way, and a read-only checkout or a full disk must not change *how* a run halted.
|
|
19
|
+
- **Every list is bounded, and the bound is published rather than restated.** Leftover task ids, wave tallies and degradation banners are each capped by a new `LIMITS.resumeCardListRows`, printed by `interlock limits` as `resume card list rows (per list)`. The card is a pointer somebody reads in one sitting, not a second copy of the trajectory — but a silent head would make it lie, so it always says how many rows it left out and names the command that prints them all.
|
|
20
|
+
- **The suite cannot drop a card into your live tree.** `test/spine/root-isolation.test.mjs` now guards `.claude/handoff/` alongside `.claude/ship/` and `.claude/metrics/`, so a test that reaches the binary without a pinned root fails there rather than leaving a card in the developer's checkout for a later read to mistake for a real halt's.
|
|
21
|
+
|
|
22
|
+
See [when it stops](docs/04-when-it-stops.md) for where the card sits in the halt path, and `interlock limits` for the cap every one of its lists is printed under.
|
|
23
|
+
|
|
12
24
|
- **Prompt-cache cost is named, measured, and fixed where it was ours to fix.** A ship run repeatedly re-writes prefixes it could have read back, and until now nothing in Interlock could see it: the trajectory recorded exactly one token field (`outputTokens`) and nothing on the input side, so a cache miss was invisible by construction. Wave gaps measured in this repository's own trajectory are **363s and 1017s**, both past the short default lifetime, and each boundary re-writes its ~30–40k spawn prefix cold.
|
|
13
25
|
|
|
14
26
|
A plugin cannot set either prompt-cache lifetime — the component model has no settings component — so this does the three things that *are* Interlock's:
|
|
@@ -24,6 +36,16 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
24
36
|
|
|
25
37
|
### Fixed
|
|
26
38
|
|
|
39
|
+
- **A failed write to the run trajectory now halts the run it was recording, instead of warning and shipping anyway.** `.claude/ship/runs` has been `fatal: true` in `interlock doctor` since it existed, and `wave-state` and `verify judge` have always exited non-zero when their append did not land. The live `interlock run` path was the one writer that did not: it called `ctx.warn` and walked on — into the task tick, and into the commit. The outcome was precisely the artifact the corpus exists to prevent: a run that ticked its boxes, committed, and cannot be replayed. It now halts at every site that owes the trajectory a line — `run-start`, `wave-state create` / `record-batch` / `record-verify` / `replan`, `agent-spawn` (implementer lanes included, which go through the same writer `wave-state next` uses) and `cli-exit` — **before** any tick and before any commit, with the reason `trajectory append failed: <site>: <cause>`. The cause is the filesystem's own, threaded out of the writer rather than left on a stderr the Workflow host does not relay; a full disk now says so by name. See [when it stops](docs/04-when-it-stops.md#the-loud-halts).
|
|
40
|
+
|
|
41
|
+
What this costs you: a run that previously finished green on a machine that could not write its own record now stops. That green finish was the bug. Nothing is ticked and nothing is committed, so the re-run starts from a truthful `tasks.md`. The close still runs, so the receipt, the outcome record and the halt resume card all still land wherever the filesystem allows.
|
|
42
|
+
|
|
43
|
+
The split this repository deliberately keeps is unchanged and was not "made consistent": the trajectory is evidence and is fatal; `.claude/learning/outcomes.jsonl`, the review metrics and the resume card are pointers to evidence written elsewhere, so losing one is reported and never moves the exit code. One consequence is now documented rather than surprising — when the **close** cannot write its receipt or terminal line, the summary still reads `SHIP COMPLETE` (the run did complete and commit) while the exit code moves to `1` (it cannot be replayed), with a `TRAJECTORY APPEND FAILED` banner naming the lost line.
|
|
44
|
+
|
|
45
|
+
- **`merge-lanes` stopped treating a rename and an edit of the same file as disjoint work.** Under `--isolate-waves` each lane runs in its own git worktree and the folds are checked for collisions on the paths the lanes wrote. A rename was keyed on its **destination** only — so lane A renaming `lib/a.mjs` to `lib/b.mjs` and lane B editing `lib/a.mjs` looked like two lanes touching different files, and the fold then restored or deleted that source depending on which lane happened to apply last. Silent last-writer-wins, in the one mechanism whose entire purpose is to refuse it. Contention is now keyed on every path a fold will **mutate**: a rename contributes its destination *and* the source it deletes (`foldMutationPaths` in `lib/merge-lanes.mjs`). A copy's source is not a mutation — the fold does not delete it — and is deliberately excluded, so a genuinely disjoint batch is not halted for it.
|
|
46
|
+
|
|
47
|
+
What this costs you: a batch that used to fold and silently lose one lane's edit now halts, names the contended source path and both lanes, and leaves both worktrees on disk to read. `mergeDecision` is unchanged — it is still a path-set intersection, and `canonicalizePath` is still the single normalization site. The reported `folds[].files` of a clean fold now include rename sources, which is accurate: they are paths the lane's fold mutates.
|
|
48
|
+
|
|
27
49
|
- **The outcome eval was reporting a cache tally it never asked for.** `evals/ship/agent/model.mjs` is the repository's only raw Messages API caller, and it sent **no `cache_control` block at all** while faithfully tallying `cache_read_input_tokens` — so it re-paid full base input on up to 60 turns of every loop and reported a permanent zero that read as "caching unavailable" rather than "no breakpoint sent". A cost bug and a measurement bug in one object.
|
|
28
50
|
|
|
29
51
|
The loop now marks its stable prefix — `tools` then `system`, the portion byte-identical on every turn — with one breakpoint on the prefix's last block, computed once outside the loop so every turn sends the same bytes; a breakpoint inside the growing message list would re-write the entry each turn instead of reading it. The existing `emptyUsage()`/`addUsage()` tallies are untouched: they were always correct and were only ever reporting a structural zero.
|
|
@@ -37,7 +59,7 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
37
59
|
- **A clean completion reminds you to archive.** When every task is ticked and the run did not halt, the summary prints `ARCHIVE PENDING — <change>: after merge, run openspec archive <change>` — plus `also unarchived: <n> completed change(s) — run interlock drift` when other changes are also sitting there — right before the closing "do not start another run" line. It is a reminder, never a gate: the exit code never moves, because the shipped change satisfies the unarchived check by construction at every clean close.
|
|
38
60
|
- **`interlock doctor` stops reporting green for a run whose first ping prompts.** `REQUIRED_COMMANDS` gains `test`, `printenv` and `mkdir` — the commands the environment probe actually shells out to — each with a `why`, and a new `notify` row (`ok` naming the server, `skip` naming `INTERLOCK_NTFY_TOPIC`, never the value). A repo-fact test now checks this repository's own `.claude/settings.json` against the derived list, and a drift test extracts every instructed command out of `workflows/ship.js` and `lib/prompts/*.mjs` and fails, naming the file and the command, the moment one falls outside `REQUIRED_COMMANDS`, the runner commands, or the documented host read-only set (`HOST_READ_ONLY`).
|
|
39
61
|
|
|
40
|
-
All four are additive: unset `INTERLOCK_NTFY_TOPIC` and a run behaves exactly as before, plus the three new identity rows and, on a clean ship, the archive line. See [when it stops](docs/04-when-it-stops.md) for `ARCHIVE PENDING` and `PUSH FAILED`, and
|
|
62
|
+
All four are additive: unset `INTERLOCK_NTFY_TOPIC` and a run behaves exactly as before, plus the three new identity rows and, on a clean ship, the archive line. See [when it stops](docs/04-when-it-stops.md) for `ARCHIVE PENDING` and `PUSH FAILED`, and [push notifications](docs/07-cli-and-configuration.md#the-one-network-call-push-notifications) for the two environment variables.
|
|
41
63
|
|
|
42
64
|
- **The CLIs install without Claude Code.** `package.json` was `private: true`, so the only way onto a PATH was a git checkout or the plugin install — even though the policy engine, the graph and the runner are stdlib Node with no dependencies and each is useful outside a Claude Code session. The package publishes as **`@renzrollon/interlock`** (`npm i -g @renzrollon/interlock`, or `npx -p @renzrollon/interlock interlock ...`), and the plugin install path is untouched.
|
|
43
65
|
|
package/README.md
CHANGED
|
@@ -1,358 +1,127 @@
|
|
|
1
1
|
# Interlock
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Spec-driven development for Claude Code. You read one spec; parallel agents ship it to a tested commit.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@renzrollon/interlock)
|
|
6
|
+
[](https://github.com/renzrollon/interlock/actions/workflows/ci.yml)
|
|
7
|
+
[](docs/01-first-hour.md#before-you-start)
|
|
8
|
+
[](./LICENSE)
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
/
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
Then, in a repo, onboard once:
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
/interlock:bootstrap # code → understanding → graph
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Every change after that:
|
|
10
|
+
<p align="center">
|
|
11
|
+
<img src="docs/assets/interlock-flow-wide.png" alt="Interlock flow: bootstrap once, then spec, you read the spec, ship, and optionally mr. Two commands, one human checkpoint." width="900">
|
|
12
|
+
</p>
|
|
19
13
|
|
|
20
14
|
```bash
|
|
21
|
-
/interlock:spec "<idea>"
|
|
22
|
-
|
|
23
|
-
/interlock:ship
|
|
24
|
-
/interlock:ship --solo # one agent implements the whole change in order
|
|
25
|
-
/interlock:ship --waves # force the parallel plan
|
|
15
|
+
/interlock:spec "<idea>" # explores your repo, writes a reviewed spec, then stops
|
|
16
|
+
# ← you read it. The only place a human is required.
|
|
17
|
+
/interlock:ship # parallel agents implement → your tests pass → commit. Nobody is asked.
|
|
26
18
|
```
|
|
27
19
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|---|---|
|
|
32
|
-
| [Claude Code](https://claude.com/claude-code) **v2.1.154+** | `/interlock:ship` launches a [dynamic workflow](https://code.claude.com/docs/en/workflows). Known-good on 2.1.229 |
|
|
33
|
-
| Dynamic workflows **enabled** | Off via `disableWorkflows`, org policy, or `CLAUDE_CODE_DISABLE_WORKFLOWS` means no `ship`. On Pro, enable it in `/config` |
|
|
34
|
-
| `CLAUDE_CODE_SUBAGENT_MODEL` **unset** | If it is set it overrides every per-tier model the planner assigns, so `ship` runs entirely on that model. It banners this rather than hiding it — see [when it stops](docs/04-when-it-stops.md) |
|
|
35
|
-
| [`openspec`](https://github.com/Fission-AI/OpenSpec) CLI | Interlock drives it; it does not replace it |
|
|
36
|
-
| Node.js ≥ 18 | For the three bundled CLIs |
|
|
37
|
-
|
|
38
|
-
### Installing the OpenSpec dependency
|
|
20
|
+
- **One human checkpoint.** A spec is the cheapest place to catch a wrong idea, so that is the one place you must look. Everything after it is automatic.
|
|
21
|
+
- **Zero-touch by construction.** `ship` is a Claude Code workflow, and the workflow runtime has no channel for mid-run input. It cannot ask you anything, so it never does.
|
|
22
|
+
- **Caps and gates are code, not prose.** Parallelism, retry budgets and review thresholds are CLI exit codes a model cannot argue with, and during remediation an agent cannot even edit a test. Re-run any decision yourself, with no model and no network.
|
|
39
23
|
|
|
40
|
-
|
|
24
|
+
## Install
|
|
41
25
|
|
|
42
|
-
|
|
43
|
-
npm install -g @fission-ai/openspec@latest
|
|
44
|
-
```
|
|
26
|
+
Interlock is a [Claude Code](https://claude.com/claude-code) plugin layered on [OpenSpec](https://github.com/Fission-AI/OpenSpec), which owns the spec format. It needs Claude Code **v2.1.154+** with dynamic workflows enabled, the `openspec` CLI, and Node.js ≥ 18. The full requirements table is in [the first hour](docs/01-first-hour.md#before-you-start).
|
|
45
27
|
|
|
46
28
|
```bash
|
|
29
|
+
npm install -g @fission-ai/openspec@latest # OpenSpec first
|
|
47
30
|
cd your-project && openspec init
|
|
48
31
|
```
|
|
49
32
|
|
|
50
|
-
OpenSpec itself requires **Node.js 20.19.0+** (higher than Interlock's own ≥ 18) and also installs via pnpm, yarn, bun or nix. `openspec init` creates `openspec/` and installs its stock skills — Interlock composes with those rather than replacing them.
|
|
51
|
-
|
|
52
|
-
**Interlock is a Claude Code plugin.** It relies on Claude Code's skill frontmatter, plugin `bin/` PATH injection, subagent fan-out, and the workflow runtime — Cursor and Copilot are not supported in 0.x. Claude Code is the **default and supported host**, and `/interlock:ship` launches the workflow there or halts; it never falls back to anything else. There is an experimental **runner** you invoke yourself — `interlock-run`, which drives the same loop over the Claude Code CLI, an [ACP](https://agentclientprotocol.com) agent, OpenAI Codex or Qwen Code, described under [Experimental](#experimental) — and no slash command starts it for you.
|
|
53
|
-
|
|
54
|
-
Before a long `ship` run, allowlist the commands its agents use — run `interlock doctor` to get the exact list (derived from what the flow shells out to, plus your own test profile) rather than one enumerated here. Workflow agents inherit your permission settings, so a command that is not allowlisted stops the run on an approval prompt — which is exactly what a zero-touch run should never do.
|
|
55
|
-
|
|
56
|
-
Also decide once whether this repository keeps its run corpora. If you will run `/interlock:ship` against your own product, the trajectory, outcome record and review metrics are your audit trail and belong in git; if you are developing a harness, they are exhaust and belong in `.gitignore`. Both `.gitignore` blocks and the two caveats that bite when committing are in [11 — whether to keep them](docs/11-the-indicators.md#whether-to-keep-them).
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
interlock doctor
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
That is the preflight: it checks the allowlist against the commands the flow actually shells out to (including the one your own `.claude/testing/profile.json` names), plus the Node version, the installed plugin's workflow and agent types, the OpenSpec CLI, git, and whether the run-state directories can be written at all. It exits 1 when a check would stop an unattended run, prints the settings snippet that fixes it, and changes nothing itself. Every condition it names is one you would otherwise meet three waves in. The plugin also runs this preflight automatically at session start (a `SessionStart` hook) so a missing allowlist entry surfaces before a run rather than three waves in.
|
|
63
|
-
|
|
64
|
-
The plugin ships three `PreToolUse` guards as well — deterministic deny rules that stop an in-run agent from editing a test during remediation, hand-ticking `tasks.md`, or committing outside the commit stage. **They bind only agents running inside an Interlock ship run and are inert outside one:** they fail open whenever no active run marker is present, so installing the plugin does not change how your own editing or committing behaves. See [13 — The guards](docs/13-the-guards.md).
|
|
65
|
-
|
|
66
|
-
### Install the CLIs without the plugin
|
|
67
|
-
|
|
68
|
-
The bundled CLIs are ordinary Node with zero dependencies, and each is useful on its own — `interlock` gates a CI job, `interlock-graph` indexes a repo for any agent, `interlock-run` drives the same ship loop over a non-Claude host. They ship as an npm package as well as a plugin:
|
|
69
|
-
|
|
70
33
|
```bash
|
|
71
|
-
|
|
34
|
+
/plugin marketplace add renzrollon/interlock
|
|
35
|
+
/plugin install interlock@interlock
|
|
72
36
|
```
|
|
73
37
|
|
|
74
|
-
|
|
38
|
+
Then, once per repo. The plugin puts the `interlock` CLI on your `PATH`:
|
|
75
39
|
|
|
76
40
|
```bash
|
|
77
|
-
|
|
41
|
+
interlock doctor # preflight: prints the exact allowlist an unattended run needs
|
|
42
|
+
/interlock:bootstrap # reads the code, writes what it learned, builds the graph agents navigate
|
|
78
43
|
```
|
|
79
44
|
|
|
80
|
-
|
|
81
|
-
|---|---|
|
|
82
|
-
| **Works from the package alone** | `interlock` (the policy engine: `limits`, `gate`, `surface`, `waves`, `report`, `ledger`, `doctor`), `interlock-graph` (build and query the codebase graph), `interlock-run` (drive the loop over the Claude Code CLI, an ACP agent, Codex or Qwen) |
|
|
83
|
-
| **Needs the Claude Code plugin** | `/interlock:spec`, `/interlock:ship`, `/interlock:bootstrap` and every other slash command; the `SessionStart` preflight and the `PreToolUse` guards. The package ships the skill and hook files so a package checkout is still a complete plugin, but nothing runs them without the plugin host |
|
|
84
|
-
|
|
85
|
-
**Releasing:** push a `v<MAJOR.MINOR.PATCH>` tag (the version `package.json` carries) and `.github/workflows/release.yml` runs the suite and publishes with npm provenance. It uses trusted publishing, which has to be configured once on the registry for this repository and that workflow file name — until it is, the publish step fails with the registry's authentication error rather than skipping.
|
|
86
|
-
|
|
87
|
-
New here? Start with **[the first hour](docs/01-first-hour.md)**. If you have only ever prompted a coding agent — no skills, no specs, no gates — read **[09 — From prompt to workflow](docs/09-from-prompt-to-workflow.md)** first: every term defined once, ending at why `ship` is a script and not a prompt. Then **[10](docs/10-agentic-workflow-ship-and-spec.md)** for this repo's loop reviewed in depth.
|
|
88
|
-
|
|
89
|
-
| Doc | |
|
|
90
|
-
|---|---|
|
|
91
|
-
| [01 — The first hour](docs/01-first-hour.md) | Install to first shipped change |
|
|
92
|
-
| [02 — The checkpoint](docs/02-the-checkpoint.md) | How to read a spec in ten minutes |
|
|
93
|
-
| [03 — OpenSpec vs Interlock](docs/03-openspec-vs-interlock.md) | What composes with what |
|
|
94
|
-
| [04 — When it stops](docs/04-when-it-stops.md) | Every halt and banner, and what to do |
|
|
95
|
-
| [05 — Continuity](docs/05-continuity.md) | When `--continue` may skip the human read |
|
|
96
|
-
| [06 — Why it works](docs/06-why-it-works.md) | The mechanisms, low-level, with the costs stated |
|
|
97
|
-
| [08 — The harness landscape](docs/08-harness-landscape.md) | OpenClaw, Hermes Agent and DeepSeek Harness, and which layer each sits at |
|
|
98
|
-
| [09 — From prompt to workflow](docs/09-from-prompt-to-workflow.md) | New to agentic workflows? Every term defined, then why `ship` is a script |
|
|
99
|
-
| [10 — Ship and spec for prompt-only engineers](docs/10-agentic-workflow-ship-and-spec.md) | Agentic-workflow primer, review of spec+ship, token and quality tactics |
|
|
100
|
-
| [11 — The indicators](docs/11-the-indicators.md) | What `interlock report` measures, each denominator, and why it gates nothing |
|
|
101
|
-
| [12 — Repository review policy](docs/12-repository-review-policy.md) | The optional `REVIEW.md`: what it can change (scope, advice) and what it cannot (the band, nit cap) |
|
|
102
|
-
| [13 — The guards](docs/13-the-guards.md) | The four hooks, the stage marker's lifecycle, and the fail-open rule. The guards bind only agents inside a ship run and are inert outside one |
|
|
103
|
-
| [14 — Evals and the consumer posture](docs/14-evals.md) | What your run is checked by, what it records, why no model evals run in your CI, and how to file a failure |
|
|
104
|
-
|
|
105
|
-
---
|
|
45
|
+
`doctor` exits 1 on anything that would stop a zero-touch run and prints the settings snippet that fixes it. The plugin runs it at every session start too. Your first shipped change, step by step: **[01 — The first hour](docs/01-first-hour.md)**.
|
|
106
46
|
|
|
107
47
|
## The flow
|
|
108
48
|
|
|
109
|
-
One human stop. Everything else is automatic.
|
|
110
|
-
|
|
111
|
-
<p align="center">
|
|
112
|
-
<img src="docs/assets/interlock-flow-wide.png" alt="Interlock flow: bootstrap once, then spec → you read the spec → ship → mr" width="900">
|
|
113
|
-
</p>
|
|
114
|
-
|
|
115
|
-
**The gap between `spec` and `ship` is the product.** A spec is the cheapest place to catch a wrong idea, so that is the one place a person is *required* to look.
|
|
116
|
-
|
|
117
|
-
`/interlock:ship` is the one that truly asks nothing, and it is structurally incapable of it: ship is a **dynamic workflow**, and the workflow runtime takes no mid-run user input at all. The zero-touch contract is a property of the runtime rather than a promise in a prompt. Default ship is waves → unit verify → commit. Pass `--strict` when you want the adversarial review and handoff tail. `commit` and `mr` set `disable-model-invocation: true` for the same reason — Claude cannot decide on its own that now is a good time to commit.
|
|
118
|
-
|
|
119
|
-
The steps before it are conversational where they have to be: `spec` asks about intent, bug-fix evidence, and dependency versions it refuses to guess — questions with no correct answer available in the repo. That is also why every decision that could need a human has to be settled *before* ship starts. Once the workflow is running, there is nobody to ask.
|
|
120
|
-
|
|
121
|
-
---
|
|
122
|
-
|
|
123
|
-
## Why this and not a folder of prompts
|
|
124
|
-
|
|
125
|
-
Decisions that have a correct answer are moved out of prose and into code, one at a time. The plugin ships three CLIs on your `PATH` — the deterministic spine, the knowledge graph, and the experimental [runner](#experimental):
|
|
126
|
-
|
|
127
|
-
**`interlock`** — the deterministic spine. Each subcommand replaces a judgement the model used to re-derive in prose on every run, usually inconsistently:
|
|
128
|
-
|
|
129
|
-
| Command | Decides |
|
|
130
|
-
|---|---|
|
|
131
|
-
| `interlock waves` | Wave order, per-task model, a **hard cap on parallel agents**, and whether two tasks in one wave would edit the same file |
|
|
132
|
-
| `interlock surface` | Whether a diff touches UI, and therefore needs a manual test plan |
|
|
133
|
-
| `interlock gate` | Whether a review blocks, which findings are too weak to report, and how the rest partition for parallel fixers |
|
|
134
|
-
| `interlock review` | Which findings survive two skeptics, and how many were dismissed versus dropped as too weak |
|
|
135
|
-
| `interlock remediate` | What gets fixed, what gets deferred, and when the round budget is spent |
|
|
136
|
-
| `interlock verify` | What to run, what a red result means, and which failures share a root cause |
|
|
137
|
-
| `interlock wave-state` | What happens next in the wave loop, and when to stop |
|
|
138
|
-
| `interlock risk` | How dangerous a change is, from its paths and artifacts |
|
|
139
|
-
| `interlock drift` | Which completed changes were never archived, which specs cite files that are gone, and which changed files no spec describes |
|
|
140
|
-
| `interlock conformance` | Which spec scenarios a change must be checked against — the questions, never the verdicts |
|
|
141
|
-
| `interlock ready` | Whether a change may skip the human checkpoint — fail-closed |
|
|
142
|
-
| `interlock ledger` | Whether the decision ledger still holds an unanswered product question |
|
|
143
|
-
| `interlock validate` | Whether a change is actually implementable |
|
|
144
|
-
| `interlock tasks` | Whether the wave plan covers every unchecked box, and which ids may be ticked |
|
|
145
|
-
| `interlock run-log` | Whether a finished run's trajectory can actually be replayed |
|
|
146
|
-
| `interlock run` | The whole ship loop, as steps: every briefing and every branch a driver obeys next |
|
|
147
|
-
| `interlock limits` | Every cap the loop obeys, so nothing restates one |
|
|
148
|
-
|
|
149
|
-
Every one of them runs without a model and without the network, so you can check any decision the loop made yourself — with one exception: `interlock notify` and `run close --notify` open a network connection, and only when `INTERLOCK_NTFY_TOPIC` is set. See [Configuration: push notifications](#configuration-push-notifications) below.
|
|
150
|
-
|
|
151
|
-
### Configuration: push notifications
|
|
152
|
-
|
|
153
|
-
A ship run that halts or completes while nobody is watching can push you a message. It is off by default and reads only from the environment — nothing is read from or written to the repo tree for this:
|
|
154
|
-
|
|
155
|
-
| Variable | Meaning |
|
|
156
|
-
|---|---|
|
|
157
|
-
| `INTERLOCK_NTFY_TOPIC` | The [ntfy](https://ntfy.sh) topic to post to. Unset (the default) means `run close` posts nothing, and the suite never makes a request. Treat the value as a secret — anyone who knows it reads every message, since it is the only authentication the public server offers. |
|
|
158
|
-
| `INTERLOCK_NTFY_URL` | The ntfy server, defaulting to the public `https://ntfy.sh`. Point it at a self-hosted server if the public relay is not an acceptable trust boundary for your halt reasons. |
|
|
159
|
-
|
|
160
|
-
With a topic set, both `workflows/ship.js` and `bin/interlock-run` pass `--notify` on every close, so one message goes out per terminal outcome — `high` priority on `SHIP HALTED`, default priority otherwise — naming only the summary's first line, the change and the run id, never the topic, the working directory or the project slug. A failed push shows up as `push: failed — <reason>` in the summary and a `PUSH FAILED: <reason>` banner, and never changes the run's exit code. See [when it stops](docs/04-when-it-stops.md#push-failed) for the failure modes, or run a one-off yourself:
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
interlock notify --title "<title>" --body "<body>"
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
**`interlock-graph`** — a local, deterministic code knowledge graph. No vector store, no network. Agents navigate with token-budgeted subgraphs instead of re-grepping:
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
interlock-graph build .
|
|
170
|
-
interlock-graph consumers normalizeEmail
|
|
171
|
-
interlock-graph path lib/auth app/api
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Everything genuinely requiring judgement — classification, implementation, review, synthesis — stays with the model. The split is the point: **the script holds the loop, the CLI holds the rules, the agents do the work.**
|
|
175
|
-
|
|
176
|
-
The wave loop, the halt conditions and the verification order live in `lib/run.mjs`, which emits the whole program as steps — the agents to spawn, with their briefings, and the exact `interlock` argv to call once they return. `workflows/ship.js` and the experimental `bin/interlock-run` are interpreters of that program, not two copies of it: each spawns what a step names and calls what it names next, and branches on nothing — not a flag, not a mode, not a count, not a verdict. Control flow written as prose is control flow the model can talk itself out of; control flow written twice in two drivers is control flow that drifts, which is what moving it into one CLI removes. Default `ship` is that loop through to a green unit suite and a commit. Adversarial review and handoff artifacts are `--strict` (or `--review` / `--handoff` on their own), not the execute loop itself.
|
|
177
|
-
|
|
178
|
-
That leaves one thing worth calling out because it took the longest to close: tasks in a wave run in parallel **in one working tree**, and their independence used to be asserted by the classifier and checked by nothing. The planner now takes each task's predicted file list and moves any task that would collide with a sibling into a later batch of the same wave — ordering inside a wave is free, while a new wave is a checkpoint. Collision is compared on the **canonical** path, so `src/a.ts` and `./src/a.ts` are one file rather than two keys; a path that is absolute or escapes the repo root is reported as unusable rather than rewritten into scope. The prediction is still a model's — but with `--isolate-waves`, each lane in a batch runs in its own git worktree, so the race is **closed within a batch** rather than merely narrowed: a mis-predicted shared write can no longer overwrite a sibling lane. Their worktrees fold back into the shared tree afterward (`interlock merge-lanes`); a prediction miss — two lanes that actually wrote the same file — surfaces as a named halt at merge time, never as a silently discarded write. Without the flag, the race is narrowed exactly as before.
|
|
179
|
-
|
|
180
|
-
---
|
|
181
|
-
|
|
182
|
-
## Reviews you can actually read
|
|
183
|
-
|
|
184
|
-
`/interlock:review-code` fans out up to six dimensions in parallel — language, architecture, QA and technical-lead always; devops and security when the diff earns them — then **puts two skeptics on every blocker and warning and tries to refute it.** Findings that don't survive are never shown to you.
|
|
185
|
-
|
|
186
|
-
An unverified review reports everything it notices, so you learn to skim it. A review where every finding survived two adversaries is one you read line by line. The report always tells you how many findings were dismissed — that number is the evidence it's worth trusting.
|
|
187
|
-
|
|
188
|
-
**A skeptic must cite what it read to dismiss a finding.** A verdict of "not real" has to carry a `file:line` (or `file:start-end`) span naming a path that is actually in the reviewed diff — a shape a machine can check, not a judgement call handed to another model. A dismissal that fails either half does not dismiss anything: it is recorded, its quality score still counts, and the finding survives to you. The report says how many refutations were refused, so you can see the rule fire. Voting a finding *real* needs no citation, because that direction already ends with a human reading it — the cheap error. Only the dismissing direction is gated, because a wrongly dismissed finding is *invisible*, and nobody can catch a mistake they never see. [Research on adversarial review](https://arxiv.org/pdf/2604.19049) documents where uncited refutation ends: eighty-plus agents, dedicated skeptics among them, unanimously endorsing an OpenSSL vulnerability that did not exist. Confident prose is the one thing an LLM produces reliably, so it is the one thing a dismissal must not rest on.
|
|
189
|
-
|
|
190
|
-
Surviving is not sufficient. `interlock gate` also applies a quality band: a finding the skeptics scored too low for how well-grounded and actionable it is gets dropped before the gate counts blockers, so a vague blocker cannot hold up a change. That threshold lives in the CLI rather than in the review prose, which is what stops it from being quietly re-argued on each run.
|
|
191
|
-
|
|
192
|
-
A repo can own its review policy in an optional root **`REVIEW.md`**, read on every review. It can change **scope and advice**: declare `## Do Not Report` paths the CLI drops findings on, and prose — the local definition of "Important", who owns the bar — injected into the reviewer as clearly-delimited advice. It **cannot** change the band or the nit cap: those stay in the CLI, and a threshold-shaped key in the file is reported and ignored, never adopted — a path exclusion a model could talk past is not an exclusion, and a band a file could edit is not a gate. No `REVIEW.md` changes nothing; a malformed one is reported and the run proceeds under default policy. See [repository review policy](docs/12-repository-review-policy.md).
|
|
193
|
-
|
|
194
|
-
---
|
|
195
|
-
|
|
196
|
-
## Commands
|
|
197
|
-
|
|
198
|
-
`bootstrap` once per repo. Then `spec` and `ship` on every change. `mr` when you want the merge request.
|
|
199
|
-
|
|
200
49
|
| | | |
|
|
201
50
|
|---|---|---|
|
|
202
|
-
|
|
|
203
|
-
|
|
|
204
|
-
|
|
|
205
|
-
| `
|
|
206
|
-
|
|
207
|
-
`ship` is the odd one out on purpose: a skill is instructions Claude follows, a workflow is a script a runtime executes. `/interlock:ship` is a thin skill that only launches `workflows/ship.js`, so the Skill tool can find it in any repo where the plugin is installed. The loop stays in the script.
|
|
208
|
-
|
|
209
|
-
<details>
|
|
210
|
-
<summary><b>Advanced surface</b> — mostly called by the four above; reach for them directly only when you know why</summary>
|
|
211
|
-
|
|
212
|
-
<br>
|
|
213
|
-
|
|
214
|
-
| | |
|
|
215
|
-
|---|---|
|
|
216
|
-
| `explore` | Parallel read-only reconnaissance, durable brief |
|
|
217
|
-
| `review-code` · `review-artifacts` | The adversarial gates, run standalone. Default `ship` does not run `review-code`; pass `--review` or `--strict`. |
|
|
218
|
-
| `graph` · `docs-digest` | Build and query the local code graph and docs digest |
|
|
219
|
-
| `fix-tests` | Discover the test setup, then repair failures by root cause |
|
|
220
|
-
| `manual-test-plan` · `explain-code` · `commit` | Individual `ship` stages, run on their own |
|
|
221
|
-
| `dispatch` | One batched pre-flight, then routes you to the right skill |
|
|
222
|
-
|
|
223
|
-
None of these are part of a first loop — see [the first hour](docs/01-first-hour.md).
|
|
224
|
-
|
|
225
|
-
Live-session retro (`session-retro`) now ships from [shippable-skills](https://github.com/renzrollon/shippable-skills) so it can run on Cursor, Copilot, Codex, and Claude Code. Install with `npx skills add renzrollon/shippable-skills`.
|
|
226
|
-
|
|
227
|
-
</details>
|
|
228
|
-
|
|
229
|
-
---
|
|
230
|
-
|
|
231
|
-
## It composes OpenSpec — it doesn't replace it
|
|
232
|
-
|
|
233
|
-
`openspec init` installs its own `openspec-propose`, `openspec-explore` and `openspec-apply-change` skills. Interlock does **not** fork them. `/interlock:spec` drives the `openspec` CLI directly — `openspec new change`, `openspec status --json`, `openspec instructions` — because the CLI is the stable contract and a forked skill drifts on every OpenSpec release.
|
|
234
|
-
|
|
235
|
-
What Interlock adds around it: parallel exploration with a durable brief, an evidence gate for bug fixes, invariant sweeps, wave execution with mechanical caps, optional adversarial review (`--strict` or `/interlock:review-code`), and the deterministic spine above.
|
|
236
|
-
|
|
237
|
-
Both sets of skills coexist. Plugin skills are namespaced, so `/openspec-propose` and `/interlock:spec` both stay available. Use `/interlock:spec` when you want the gates; use the stock skills when you want the plain artifact loop.
|
|
238
|
-
|
|
239
|
-
### Specs that don't quietly rot
|
|
240
|
-
|
|
241
|
-
Spec drift is the standing criticism of every tool in this category, and the usual answers are to delete the spec after shipping or to leave it to discipline. OpenSpec's `openspec archive` merges a completed change's deltas back into the living specs — Interlock never archives for you, it just stops the step being forgotten:
|
|
242
|
-
|
|
243
|
-
```bash
|
|
244
|
-
interlock drift --changed <files>
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
Four findings, deliberately kept at **different confidence** rather than averaged into one number: changes that finished but were never archived (certain — read off the filesystem); specs citing files that no longer exist (evidence — the file was there when the graph was built); changed source files no spec describes, always reported with a repo-wide coverage figure so the count means something; and specs older than code they cite (an inference from dates, printed last and labelled as such).
|
|
248
|
-
|
|
249
|
-
`interlock conformance` is the other half: it lists the scenarios a change's delta specs promised, so each can be checked against what was actually built. It emits questions, never verdicts.
|
|
250
|
-
|
|
251
|
-
**Neither blocks.** Every other subcommand exits non-zero when it blocks; these two never do. A gate built on regex-inferred spec→file links would be wrong often enough to get switched off, and a gate everyone disables protects nothing.
|
|
51
|
+
| `/interlock:bootstrap` | Onboard a repo — once | skill |
|
|
52
|
+
| `/interlock:spec "<idea>"` | Idea → explored, reviewed, implementation-ready change. Asks you what the repo cannot answer, then stops | skill |
|
|
53
|
+
| **You read the spec** | The checkpoint. [Ten minutes, with a checklist](docs/02-the-checkpoint.md) | you |
|
|
54
|
+
| `/interlock:ship` | Reviewed change → waves (parallel batches of file-disjoint tasks) → your test suite → commit. No review by default; `--strict` adds adversarial review and handoff artifacts. `--solo` / `--waves` force the shape | **workflow** |
|
|
55
|
+
| `/interlock:mr` | Change → merge request | skill |
|
|
252
56
|
|
|
253
|
-
|
|
57
|
+
`ship` is the odd one out on purpose. A skill is instructions Claude follows; a workflow is a script a runtime executes, and this runtime has no channel for a question. That is why every decision that could need a human is settled *before* ship starts, and why `spec` is conversational where it has to be. The plan preview names the shape and the agent count before anything is spawned.
|
|
254
58
|
|
|
255
|
-
##
|
|
256
|
-
|
|
257
|
-
Structural graph indexing — import and symbol edges — covers **JavaScript/TypeScript, Python, and shell**. Other languages (Go, Rust, Java, Ruby) get everything else: docs and OpenSpec indexing, spec→file links, prose retrieval, and the full workflow. When `interlock-graph build` finds nothing to index it says so and explains why, rather than reporting an empty graph as success.
|
|
59
|
+
## Why this and not a folder of prompts
|
|
258
60
|
|
|
259
|
-
|
|
61
|
+
Decisions that have a correct answer are moved out of prose and into code, one at a time. The script holds the loop, the CLI holds the rules, the agents do the work.
|
|
260
62
|
|
|
261
|
-
|
|
63
|
+
- **Thresholds are code.** Wave order, the parallel-agent cap, the remediation round budget, the review quality band: each is an `interlock` subcommand with an exit code, not markdown a model can talk itself past. [Every subcommand →](docs/07-cli-and-configuration.md)
|
|
64
|
+
- **Parallel agents cannot overwrite each other.** The planner separates tasks that would touch the same file; with `--isolate-waves`, every parallel task runs in its own git worktree and a collision is a named halt, never a lost write.
|
|
65
|
+
- **Reviews you can actually read.** On `--strict`, up to six review dimensions run in parallel, then two skeptics attack every finding. A dismissal must cite a `file:line` in the diff or it dismisses nothing. You see only what survived, and how many did not. [How and why →](docs/06-why-it-works.md)
|
|
66
|
+
- **A guard, not a request.** During remediation an agent cannot edit a test, tick a task box, or commit outside the commit stage. They are `PreToolUse` hooks, inert outside a run. [The guards →](docs/13-the-guards.md)
|
|
67
|
+
- **Specs that don't quietly rot.** `interlock drift` reports unarchived changes, specs citing missing files, and code no spec describes. Each at its own confidence level, and it never blocks. [OpenSpec vs Interlock →](docs/03-openspec-vs-interlock.md)
|
|
68
|
+
- **Degradation is spoken, never silent.** A missing graph, an overridden model, a failed push, a host that cannot report tokens: each is a named banner in the summary. [When it stops →](docs/04-when-it-stops.md)
|
|
262
69
|
|
|
263
70
|
## How it compares
|
|
264
71
|
|
|
265
72
|
Most of the category competes on how much structure you write before coding — Spec Kit adds phases, BMAD adds roles, Kiro adds an IDE. Interlock competes on a different axis: **how many decisions the model is not allowed to make.**
|
|
266
73
|
|
|
267
|
-
|
|
268
|
-
- **The zero-touch contract is the runtime's, not a prompt's.** `ship` is a workflow, so there is nobody to ask. Everyone else promises autonomy in prose. Default ship is waves → verify → commit; `--strict` is the review/handoff tail.
|
|
269
|
-
- **A dismissal must cite evidence; a report needn't.** On `--review` / `--strict` or `/interlock:review-code`, findings are attacked before you see them, dismissal counts are printed, and a refutation that cites nothing refutes nothing.
|
|
270
|
-
- **The invariant sweep is the licensed exception to the diff leash** — a value canonicalized in one place and still read raw in three others is the one bug class every diff-scoped review is structurally blind to.
|
|
271
|
-
- **Spec drift is measured, not hand-waved** — and reported at three separate confidence levels rather than one misleading number.
|
|
272
|
-
|
|
273
|
-
The trade is portability. Spec Kit runs on thirty agents; Interlock runs on one, because the guarantees above come from Claude Code's workflow runtime and its plugin surface. A portable version of this would be a folder of prompts, which is the thing it exists not to be.
|
|
74
|
+
It composes OpenSpec rather than replacing it. `openspec init` installs its own skills, and `/interlock:spec` drives the `openspec` CLI directly, so both stay available. Use `/interlock:spec` when you want the gates, and the stock skills when you want the plain artifact loop. What Interlock adds, and when plain OpenSpec is the right call: [03](docs/03-openspec-vs-interlock.md). Where it sits against OpenClaw, Hermes Agent and DeepSeek Harness: [08](docs/08-harness-landscape.md).
|
|
274
75
|
|
|
275
|
-
|
|
76
|
+
The trade is portability. Claude Code is the **default and supported host**; Cursor and Copilot are not supported. Everything the loop decides lives in a CLI any host can shell out to, so portability means a second host adapter, not thirty prompt templates.
|
|
276
77
|
|
|
277
|
-
|
|
78
|
+
## Beyond Claude Code (experimental)
|
|
278
79
|
|
|
279
|
-
|
|
80
|
+
`interlock-run` drives the same loop over the Claude Code CLI, any [Agent Client Protocol (ACP)](https://agentclientprotocol.com) agent, OpenAI Codex or Qwen Code. You start it yourself. No slash command does, and `/interlock:ship` never falls back to it.
|
|
280
81
|
|
|
281
|
-
|
|
82
|
+
Two things to know before you do. `claude -p`, the Agent SDK and ACP are the usage Anthropic flagged for separate metered credit; the interactive Workflow runtime that `/interlock:ship` uses is the exempted path, and the runner prints which path it is on. And Codex and Qwen do not understand the planner's model tiers, so map them per host with `INTERLOCK_MODEL_MAP`; an unmapped tier is bannered, never silently run on your default.
|
|
282
83
|
|
|
283
|
-
|
|
84
|
+
Host table, banners and configuration: [07](docs/07-cli-and-configuration.md#interlock-run--the-experimental-runner). The older `interlock-ship-acp` still works, prints a deprecation line, and is removed in the next minor. Code Mode is out of scope.
|
|
284
85
|
|
|
285
|
-
|
|
86
|
+
## The CLIs
|
|
286
87
|
|
|
287
|
-
|
|
88
|
+
Three zero-dependency Node binaries land on your `PATH`, and they work without the plugin too (`npm install -g @renzrollon/interlock`):
|
|
288
89
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
**The runner: the same loop over a vendor coding CLI.** `lib/host.mjs` states the whole host contract — spawn one labeled agent, spawn a batch, run `interlock` and branch on its exit code — and forbids a host from reimplementing wave ordering, verify judgement, limits or the gate. `bin/interlock-run` is the second implementation of it, and it is not welded to one vendor: an adapter under `lib/host/` is a transport plus a declaration of what that host cannot do, and the run program reads the declaration rather than branching on a name. It lands on your `PATH` with the other two CLIs.
|
|
296
|
-
|
|
297
|
-
```bash
|
|
298
|
-
interlock-run <change-name> --host claude
|
|
299
|
-
INTERLOCK_ACP_COMMAND="<your-acp-agent>" interlock-run <change-name> --host acp
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
| `--host` | Drives | Result schema | Model selection | Plugin hooks | Token usage | Billing path |
|
|
303
|
-
|---|---|---|---|---|---|---|
|
|
304
|
-
| `claude` | `claude -p` | enforced by the CLI | the planner's slugs, passed through | yes (`--plugin-dir`) | yes | Anthropic, programmatic |
|
|
305
|
-
| `acp` | your `INTERLOCK_ACP_COMMAND` | recovered from text | negotiated on `session/new` | only over the Claude binary | no | whatever the agent is |
|
|
306
|
-
| `codex` | `codex exec` | enforced by the CLI | mapped, or unrouted | **no** | yes | ChatGPT plan or API key |
|
|
307
|
-
| `qwen` | `qwen -p` | enforced by the CLI | mapped, or unrouted | **no** | no | whatever you configured |
|
|
308
|
-
|
|
309
|
-
`interlock-ship-acp` still works and prints a deprecation line; it is removed in the next minor version.
|
|
310
|
-
|
|
311
|
-
- **The whole loop, including `--strict`.** Adversarial review, bounded remediation, the verdict and the handoff artifacts are steps `interlock run` emits, exactly as the waves are, so every host interprets them the way it interprets a batch. Adjudication, the round budget and the dimension selection never leave the CLI, so a strict run here halts on the same terms as one on Claude Code.
|
|
312
|
-
- **Isolation is the runner's, on every host.** Under `--isolate-waves` the step names one worktree path per lane, the runner creates it from the batch's merge base, and `interlock run record-batch` folds the clean lanes, removes their worktrees, and **halts naming the path and both lanes** when two of them wrote the same file despite a disjoint prediction. That is the same guarantee the Workflow host has, and it was the gap that was blocked on the host.
|
|
313
|
-
- **Model routing comes from one published map.** `INTERLOCK_MODEL_MAP` maps the planner's tier slugs to each host's model ids. Claude accepts `haiku`/`sonnet`/`opus` unmapped; Codex and Qwen do not, and an unmapped spawn there gets **no model flag** and is named in a `MODEL ROUTING UNAVAILABLE (<host>)` banner with its reason — never quietly run on your default and reported as the tier the planner asked for. `INTERLOCK_ACP_MODEL_MAP` is an alias of the `acp` entry.
|
|
314
|
-
|
|
315
|
-
```bash
|
|
316
|
-
export INTERLOCK_MODEL_MAP='{"codex":{"haiku":"gpt-5-mini","sonnet":"gpt-5","opus":"gpt-5-pro"}}'
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
- **The runner names the billing path it is on.** Every summary prints `RUNNER HOST: <id> (experimental)`. A run over the Claude binary — directly, or through ACP — prints `SUBSCRIPTION PATH: programmatic`, because `claude -p`, the Agent SDK and ACP are the usage Anthropic flagged for separate metered credit; the interactive Workflow runtime is the path that change exempted, which is why **`/interlock:ship` stays the default and this runner is not started for you.** A Codex run with neither `CODEX_API_KEY` nor `OPENAI_API_KEY` prints `CHATGPT PLAN PATH`. A host with no hooks prints `HOOKS NOT IN FORCE (<host>)`. The receipt records the host, its billing path and its hook availability.
|
|
320
|
-
- **What a host cannot do is declared, not discovered.** Codex and Qwen have no equivalent of this repository's `PreToolUse` guards, so nothing stops a repair step from weakening a test there except the CLI's own unit-suite shrink check. Qwen reports no token accounting, so every wave and the run total are recorded as `unknown` — never as zero.
|
|
321
|
-
- **The zero-touch contract is weaker.** On Claude Code nobody can interrupt a run because the runtime has no channel for it. Here the driver just declines to ask — a policy in a file, not a property of a runtime.
|
|
322
|
-
- **`/interlock:ship` is untouched.** No flag, no auto-detect, no fallback: when the Workflow tool is missing the trampoline still halts, and the runner refuses `--host workflow` from the other side. See [when it stops](docs/04-when-it-stops.md).
|
|
323
|
-
|
|
324
|
-
**Evals over the model-facing surface.** The deterministic spine is densely unit-tested; the prompts, skills and shared contracts that steer a model are not, and the repo's own archived proposals record failures where the prompt bytes were correct and a model did the wrong thing anyway. An `evals/` case suite regression-tests that surface against a real model — tier read-scope, cited-cap resolution, lane partial-failure reporting, handoff enum conformance, control-plane action invention, trampoline halt, skill routing, and evidence-locator fabrication — each case citing the reproduced failure it encodes.
|
|
90
|
+
| | |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `interlock` | The policy engine: `limits`, `gate`, `waves`, `verify`, `doctor`, `report` and the rest. Every gating command exits 1 when it blocks |
|
|
93
|
+
| `interlock-graph` | A local, deterministic code knowledge graph. No vector store, no network |
|
|
94
|
+
| `interlock-run` | The experimental runner above |
|
|
325
95
|
|
|
326
|
-
|
|
327
|
-
export CLAUDE_CODE_WALNUT_SPIRE=1 # early-access enablement — env only, never committed
|
|
328
|
-
claude plugin eval . --tag smoke --no-publish --json evals-results.json
|
|
329
|
-
interlock evals triage --results evals-results.json # regression / variance / no signal — exit code is the verdict
|
|
330
|
-
```
|
|
96
|
+
Everything runs without a model and without the network, with one exception. `interlock notify` and `run close --notify` push a message when a run halts or completes, and only when `INTERLOCK_NTFY_TOPIC` is set; `INTERLOCK_NTFY_URL` points it at a self-hosted server. Reference and every environment variable: [07 — CLI and configuration](docs/07-cli-and-configuration.md).
|
|
331
97
|
|
|
332
|
-
|
|
333
|
-
- **The verdict is model-free.** `interlock evals triage` classifies a results file without a model or the network; its exit code is the verdict, so the one gate a model could otherwise re-argue is on the deterministic spine like every other decision.
|
|
334
|
-
- **Advisory, pending a baseline.** The CI eval job reports and does not block. No baseline scores exist yet, so any blocking threshold would be a guess; promotion needs observed variance across more than one run. The offline structural gate `test/evals.test.mjs` runs in `npm test` and *does* gate every pull request.
|
|
335
|
-
- **CI skips until access is provisioned.** The eval job ([.github/workflows/evals.yml](.github/workflows/evals.yml)) skips fork pull requests and skips cleanly when no model credential is present, so the suite lands and is maintained before paid access exists. Provisioning a credential is a later configuration action, not a prerequisite for the change.
|
|
98
|
+
## Docs
|
|
336
99
|
|
|
337
|
-
|
|
100
|
+
New here? Start with **[01 — The first hour](docs/01-first-hour.md)**. Only ever prompted a coding agent? **[09](docs/09-from-prompt-to-workflow.md)** defines every term once, ending at why `ship` is a script and not a prompt.
|
|
338
101
|
|
|
339
|
-
|
|
102
|
+
| | |
|
|
103
|
+
|---|---|
|
|
104
|
+
| [01 — The first hour](docs/01-first-hour.md) | Install to first shipped change |
|
|
105
|
+
| [02 — The checkpoint](docs/02-the-checkpoint.md) | How to read a spec in ten minutes |
|
|
106
|
+
| [03 — OpenSpec vs Interlock](docs/03-openspec-vs-interlock.md) | What composes with what |
|
|
107
|
+
| [04 — When it stops](docs/04-when-it-stops.md) | Every halt and banner, and what to do |
|
|
108
|
+
| [05 — Continuity](docs/05-continuity.md) | When `--continue` may skip the human read |
|
|
109
|
+
| [06 — Why it works](docs/06-why-it-works.md) | The mechanisms, low-level, with the costs stated |
|
|
110
|
+
| [07 — CLI and configuration](docs/07-cli-and-configuration.md) | Every subcommand, environment variable and host |
|
|
111
|
+
| [08 — The harness landscape](docs/08-harness-landscape.md) | OpenClaw, Hermes Agent, DeepSeek Harness, and the spec-driven neighbours |
|
|
112
|
+
| [09 — From prompt to workflow](docs/09-from-prompt-to-workflow.md) | New to agentic workflows? Every term defined, then why `ship` is a script |
|
|
113
|
+
| [10 — Ship and spec for prompt-only engineers](docs/10-agentic-workflow-ship-and-spec.md) | Primer, review of spec+ship, token and quality tactics |
|
|
114
|
+
| [11 — The indicators](docs/11-the-indicators.md) | What `interlock report` measures and why it gates nothing; whether to commit the run corpora |
|
|
115
|
+
| [12 — Repository review policy](docs/12-repository-review-policy.md) | The optional `REVIEW.md`: what it can change and what it cannot |
|
|
116
|
+
| [13 — The guards](docs/13-the-guards.md) | The hooks, the stage marker's lifecycle, and the fail-open rule |
|
|
117
|
+
| [14 — Evals](docs/14-evals.md) | What a consumer's run is checked by, why no model evals run in your CI, how to file a failure |
|
|
340
118
|
|
|
341
119
|
## Development
|
|
342
120
|
|
|
343
121
|
```bash
|
|
344
122
|
git clone https://github.com/renzrollon/interlock && cd interlock
|
|
345
123
|
npm test # no dependencies to install first
|
|
346
|
-
claude plugin validate . --strict
|
|
347
124
|
claude --plugin-dir . # load it without installing
|
|
348
125
|
```
|
|
349
126
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
---
|
|
353
|
-
|
|
354
|
-
## Credits
|
|
355
|
-
|
|
356
|
-
Built on [OpenSpec](https://github.com/Fission-AI/OpenSpec) by Fission AI, and on the wave-execution pattern for parallel task application.
|
|
357
|
-
|
|
358
|
-
[MIT](./LICENSE)
|
|
127
|
+
More in [CONTRIBUTING.md](./CONTRIBUTING.md). Built on [OpenSpec](https://github.com/Fission-AI/OpenSpec) by Fission AI, and on the wave-execution pattern for parallel task application. [MIT](./LICENSE).
|
package/bin/interlock
CHANGED
|
@@ -82,7 +82,7 @@ import {
|
|
|
82
82
|
} from '../lib/verify.mjs'
|
|
83
83
|
import { spillBytes } from '../lib/spill.mjs'
|
|
84
84
|
import { classifyRisk, formatRisk } from '../lib/risk.mjs'
|
|
85
|
-
import { mergeDecision } from '../lib/merge-lanes.mjs'
|
|
85
|
+
import { mergeDecision, foldMutationPaths } from '../lib/merge-lanes.mjs'
|
|
86
86
|
import { analyzeDrift, formatDrift } from '../lib/drift.mjs'
|
|
87
87
|
import { buildChecklist, formatChecklist } from '../lib/conformance.mjs'
|
|
88
88
|
import { readLedger, summarize as summarizeLedger, formatLedger } from '../lib/ledger.mjs'
|
|
@@ -1152,7 +1152,17 @@ function logWaveMutation(root, { source, change, state }) {
|
|
|
1152
1152
|
if (!exit.written) {
|
|
1153
1153
|
process.stderr.write(`interlock: cli-exit not recorded: ${exit.reason}\n`)
|
|
1154
1154
|
}
|
|
1155
|
-
|
|
1155
|
+
// `reason` is additive, and it is what a HALT can say out loud. stderr reaches
|
|
1156
|
+
// a terminal on the runner host and nothing at all on the Workflow host, which
|
|
1157
|
+
// relays stdout only — so a live-run halt that carried the bare fact of the
|
|
1158
|
+
// failure could never name the full disk or the unwritable directory that
|
|
1159
|
+
// caused it. Both reasons when both appends failed: they are different files'
|
|
1160
|
+
// worth of the same corpus and a reader wants whichever one is informative.
|
|
1161
|
+
const failed = [
|
|
1162
|
+
!wave.written ? `wave-action: ${wave.reason}` : null,
|
|
1163
|
+
!exit.written ? `cli-exit: ${exit.reason}` : null
|
|
1164
|
+
].filter(Boolean)
|
|
1165
|
+
return { step, ok: wave.written && exit.written, reason: failed.join('; ') || null }
|
|
1156
1166
|
}
|
|
1157
1167
|
|
|
1158
1168
|
/**
|
|
@@ -1333,7 +1343,10 @@ function runMergeLanes(root, laneEntries, base) {
|
|
|
1333
1343
|
const diff = laneDiff(path, base)
|
|
1334
1344
|
if (diff === null) continue // git could not diff this worktree — unresolved
|
|
1335
1345
|
diffsByLabel.set(label, diff)
|
|
1336
|
-
|
|
1346
|
+
// Contention keys are every path this lane's fold will mutate, not only the
|
|
1347
|
+
// destination git named — a rename deletes its source, so an edit of that
|
|
1348
|
+
// source by another lane is a collision, not a disjoint write.
|
|
1349
|
+
changedByLane[label] = foldMutationPaths(diff)
|
|
1337
1350
|
}
|
|
1338
1351
|
|
|
1339
1352
|
const decision = mergeDecision({ lanes: lanesForDecision, changedByLane, base })
|