@renzrollon/interlock 1.5.1 → 1.6.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/plugin.json +12 -1
- package/CHANGELOG.md +93 -0
- package/README.md +5 -6
- package/bin/interlock +1655 -29
- package/hooks/mod.mjs +20 -23
- package/hooks/notify-option.mjs +69 -0
- package/hooks/preflight.mjs +6 -42
- package/lib/conformance.mjs +75 -0
- package/lib/coverage.mjs +387 -0
- package/lib/doctor.mjs +17 -13
- package/lib/drift.mjs +36 -12
- package/lib/findings.mjs +40 -3
- package/lib/graph/extract/openspec.mjs +6 -15
- package/lib/limits.mjs +78 -1
- package/lib/merge-confidence.mjs +1302 -0
- package/lib/merge-evidence.mjs +527 -0
- package/lib/notify.mjs +10 -20
- package/lib/outcomes.mjs +26 -5
- package/lib/pipeline.mjs +380 -0
- package/lib/prompts/dimensions.mjs +24 -2
- package/lib/prompts/domain-context.mjs +271 -0
- package/lib/prompts/handoff.mjs +32 -1
- package/lib/prompts/index.mjs +57 -0
- package/lib/prompts/remediate.mjs +29 -5
- package/lib/prompts/review.mjs +21 -7
- package/lib/prompts/schemas.mjs +2 -0
- package/lib/ready.mjs +13 -1
- package/lib/receipt.mjs +72 -2
- package/lib/review-core.mjs +224 -21
- package/lib/risk.mjs +8 -0
- package/lib/run-log.mjs +44 -0
- package/lib/run.mjs +859 -27
- package/lib/spec-audit.mjs +232 -0
- package/lib/spec-coverage.mjs +105 -0
- package/lib/spec-text.mjs +257 -0
- package/lib/verify.mjs +17 -4
- package/package.json +1 -1
- package/shared/SESSION-SHAPES.md +1 -1
- package/shared/TEST-PROFILE.md +39 -2
- package/skills/mr/SKILL.md +161 -11
- package/skills/review-code/REVIEW.template.md +24 -6
- package/skills/review-code/SKILL.md +16 -6
- package/skills/review-code/dimensions/domain.md +21 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "interlock",
|
|
3
3
|
"displayName": "Interlock",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.0",
|
|
5
5
|
"description": "Spec-driven development for Claude Code, layered on OpenSpec. You read one spec; parallel agents ship it to a tested commit.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Carl Rollon",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"code-review",
|
|
19
19
|
"knowledge-graph"
|
|
20
20
|
],
|
|
21
|
+
"icon": "./assets/icon.png",
|
|
21
22
|
"userConfig": {
|
|
22
23
|
"ntfy_topic": {
|
|
23
24
|
"type": "string",
|
|
@@ -41,6 +42,16 @@
|
|
|
41
42
|
{
|
|
42
43
|
"type": "command",
|
|
43
44
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/preflight.mjs\""
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"type": "command",
|
|
48
|
+
"command": "node",
|
|
49
|
+
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/notify-option.mjs", "topic", "${user_config.ntfy_topic}"]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "node",
|
|
54
|
+
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/notify-option.mjs", "url", "${user_config.ntfy_url}"]
|
|
44
55
|
}
|
|
45
56
|
]
|
|
46
57
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,99 @@ this project adheres to Semantic Versioning 2.0.0.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## 1.6.0 — 2026-10-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **A merge verdict and score, computed by the CLI (`lib/merge-confidence.mjs`).** `assessMergeConfidence` takes the facts a review and a run already produce and returns `MERGE`, `HUMAN` or `BLOCK`, a score, and one entry per registered signal with its band (`green`, `amber`, `red`, `unobserved`), its flags, its evidence and its reason. The verdict is the worst band over the gating signals, and an unobserved signal ranks as amber. A flag lowers the score and never moves a band. The score sits inside the worst band's range, so a red never outscores an amber. Signals live in one frozen registry (`SIGNALS`, seventeen gating signals and the informational `plan.fidelity`) and flags in one published list (`FLAGS`); `combineSignals` is the formula and names no signal. The module is pure.
|
|
15
|
+
- **Every gating signal has a producer.** `conformance.living` and `spec.coverage` were registered with no producer and read unobserved; both now read the facts their rules name (see below), so a clean, reviewed, verified change can read `MERGE`. A signal reads unobserved only because a fact about the change is missing, and its reason says which.
|
|
16
|
+
- **`interlock merge-confidence [change] --changed <files...>`.** It reads the change's artifacts, its decision ledger, its delta-spec scenario count, drift and `HEAD` from the repository, and `--review`, `--verify`, `--handoff`, `--pipeline` and `--coverage` from files the caller names. `--base <ref>` adds the diff shape from `git diff --numstat`, and every path that diff reports joins the changed paths, so a path left out of `--changed` still reaches the risk classifier; a base that is `HEAD` or ahead of it reads the diff shape `unobserved`. It is offline and never calls `gh`, `glab` or the network. It exits 1 on `BLOCK`, or on anything but `MERGE` under `--require-merge`. `HUMAN` exits 0. An input it cannot read or parse also exits 1, with the reason on stderr and nothing on stdout, as `gate` does. The pipeline file and the coverage file are defined here and produced by later changes (E3, E8).
|
|
17
|
+
- **`MERGE_BANDS` in `lib/limits.mjs` and `MERGE_ALLOWED_CLASSES` in `lib/risk.mjs`.** `MERGE_BANDS` holds the band ceilings and every threshold the verdict applies. It is a separate export beside `LANE_CAPS`, `interlock limits` prints one row per entry and per ceiling, `--json` publishes it as `mergeBands`, and the cap-authority sweep covers it. `MERGE_ALLOWED_CLASSES` holds the risk classes the merge verdict reads green. It sits beside `CONTINUITY_ALLOWED_CLASSES` and holds the same classes today, so the two policies can diverge without one moving the other.
|
|
18
|
+
- **The `verify judge` verdict carries `typecheck: { ran, exitCode, passed }`,** or `null` when no type-check result was reported, in every context. A passing type check used to leave no trace, so "passed" could not be told from "never reported". No key is renamed and no halting set moves: a failed type check at the final gate still prints its non-blocking banner and does not halt.
|
|
19
|
+
|
|
20
|
+
- **Coverage figures the CLI computes (E8).** A pure lcov reader gives a project total and changed-line coverage on canonical repository paths, whether the report wrote a file absolute, relative or with a leading `./`; a record outside the repository is left out and counted.
|
|
21
|
+
- **`interlock verify baseline [--report <lcov>] [--baseline <file>]`** records `{ schema, sha, ref, totalPct, generatedAt }` for the current commit, and refuses (exit 1, previous baseline untouched) when the report has no project total. It writes to the working root's testing directory, never the state home.
|
|
22
|
+
- **`interlock verify coverage --base <ref> [--report <lcov>] [--baseline <file>] [--compute-base]`** prints the coverage input `merge-confidence --coverage` reads and no band: `configured`, `headTotalPct`, `baseTotalPct`, `baseSha`, `baseIsAncestor`, `diffPct`, `changedLines`. A baseline is usable only when its commit is an ancestor of the head, which is read from git and never from an age. A profile with no coverage command prints `configured: false`; a declared report that is missing or not lcov exits 1 with nothing on stdout. `--compute-base` measures the merge base in a temporary worktree that is removed whether the command passed or not.
|
|
23
|
+
- **`verify exec` is unchanged.** The brief's `verify exec coverage --write-baseline` was not built, because `verify exec` is a pass-through.
|
|
24
|
+
- **`## Always Human Review` in `REVIEW.md`, and policy read from the base (E4).** The section is parsed with the `## Do Not Report` grammar and enforced only by the merge verdict; it never moves a finding or a gate.
|
|
25
|
+
- **`--base <ref>` on `interlock review` and `interlock review-policy`** reads both path lists from the `REVIEW.md` committed at the ref and the prose from the working tree, and says when the two copies differ. A ref that names no commit exits 1 before a review is decided. Without it both commands read the working tree as before.
|
|
26
|
+
- **`merge-confidence --base`** makes `policy.humanPaths` observable: amber, needing a person, for a changed path under a listed path, for any edit to `REVIEW.md` itself, and for a committed section that could not be read in full; green otherwise; unobserved when no base was given.
|
|
27
|
+
- **A threshold-shaped list item is now rejected** in both path sections, as a threshold-shaped key already was.
|
|
28
|
+
- **`startSha` on the run manifest.** A ship run records the commit it started on and reads the lists from it. A manifest written before this change has none and reads the working tree.
|
|
29
|
+
- **Two banners:** `POLICY DIFFERS FROM BASE` when the working copy differs from the start commit's, and `REVIEW BASE NOT PINNED` when no start commit could be recorded. Both reach the close summary.
|
|
30
|
+
- **A `domain` review dimension (E5).** A seventh rubric, `skills/review-code/dimensions/domain.md`, whose reviewer is handed the living requirements in force for the capabilities a change touches, quoted as data. Capabilities are selected from reliable spec-to-file edges and from the capabilities the change's own delta specs modify.
|
|
31
|
+
- **`LIMITS.maxDomainSpecChars`** bounds the text at a requirement boundary and names what was left out. `interlock limits` prints it as `domain spec text budget (chars)`, and `--json` carries it.
|
|
32
|
+
- **`interlock domain-context [change] --changed <files...> [--base <ref>] [--json]`** serves the same block to the review skill and exits 0 on a completed report; a `--base` that names no commit exits 1.
|
|
33
|
+
- **A finding may carry `requirement: openspec/specs/<cap>/spec.md#<heading>`.** A `domain` blocker with no citation is a problem; the CLI checks the citation against the spec at the base and reports a failed check without dropping the finding. "Blocker" is read as the gate reads it, so `BLOCKER` and ` blocker ` are reported too.
|
|
34
|
+
- **`domain-skipped` flag** on `review.ran` when the dimension was skipped for want of a graph. It leaves the band to the rest of the record.
|
|
35
|
+
- **Two banners:** `DOMAIN REVIEW SKIPPED` and `DOMAIN REVIEW PARTIAL`, each with its reason.
|
|
36
|
+
- **`interlock spec-audit [--json]` (E7).** A non-gating report in the `drift` family of what each living capability states: requirements, scenarios without a `GIVEN`, failure and edge-case scenarios, bound and prohibition lines, numeric literals in requirement prose for a person to classify, and with a graph the broken file links and the coverable source no spec names. A measure it cannot take is stated with its reason and never printed as zero. It always exits 0. The suite pins the structural invariants of this repository's committed specs and never their counts.
|
|
37
|
+
- **`docs/15-merge-confidence.md`.** The baseline recipe with the default-branch job, `--compute-base` and its limits, the always-human-review section and why lists are read from the base, and how to read a spec audit.
|
|
38
|
+
|
|
39
|
+
- **`conformance.living`: the handoff confirms the living requirements a change touches (E6).** The signal compares the handoff's `requirementsChecked` and `requirementsUnconfirmed` with the number of entries in a living list the command counts itself; a `requirementsRequired` in the input is ignored. Green when every listed requirement was checked and none is unconfirmed, amber (needing more evidence) when any is unconfirmed, a count is missing or the counts differ, unobserved when the list has entries and no handoff was given.
|
|
40
|
+
- **`interlock conformance --living [--base <ref>]`** lists, after the scenarios, the living requirements in force for the capabilities the change touches (`L1..Ln`, spec path, line and heading, never the body), selected, read from the base and bounded exactly as the `domain` review's text is, so the two cannot disagree. It exits 0 in every case; a `--base` naming no commit is the reason the list was not built. Without `--living` the output is unchanged.
|
|
41
|
+
- **The ship handoff asks about them** under `--conformance`, and `HANDOFF_RESULT_SCHEMA` carries the optional counts `requirementsChecked` and `requirementsUnconfirmed`, recorded on the manifest and printed as a `living requirements:` row.
|
|
42
|
+
- **`requirements-omitted` flag** when the size bound left requirements out. It lowers the score and never moves a band.
|
|
43
|
+
- **`spec.coverage`: is the source a change adds named in a spec? (E6)** Amber, needing a fix, for a coverable source file the diff adds that no living spec and none of the change's delta specs names, and for a kept file whose only naming spec the diff changed or deleted. An edited file nothing ever named is the `legacy-unspecced` flag, with the repository's named-over-total figure. It reads spec text and git, never the graph, through `specMentionedPaths` in `lib/spec-text.mjs` (the rule the graph's extractor now shares) and `lib/spec-coverage.mjs`, so it reads the same in CI as on a developer's machine. Unobserved with no `--base`.
|
|
44
|
+
- **One gather for the command and the close (`lib/merge-evidence.mjs`).** `gatherMergeEvidence` assembles the assessment's input for `interlock merge-confidence` and for a run's close, so both read the same facts; a fact it cannot read is named (on stderr, or as a banner) and leaves its signal unobserved.
|
|
45
|
+
- **`interlock merge-confidence --run <file>`.** Takes the review, verification, handoff, coverage and run-record inputs from the evidence file a run wrote, only when the file names this change, records that the run committed and records `HEAD` as that commit. Anything else is refused on stderr with nothing taken from it. An explicit `--review`, `--verify`, `--handoff` or `--coverage` replaces the file's entry, and the verdict recorded in the file is never read.
|
|
46
|
+
- **A ship run states its merge verdict once, at close.** A completed `run close` assesses merge confidence from what the run observed and prints `MERGE CONFIDENCE <verdict> — score <n> (...)` with the signals that need a fix, a person or more evidence, the flags and the evidence file. A halted close assesses nothing. No pipeline is supplied inside a run, so a close reads `HUMAN` or `BLOCK`; the verdict never changes the exit code.
|
|
47
|
+
- **A `mergeConfidence` block on the receipt** (verdict, score, worst band, counts, driving ids, flags; no reason or evidence text) and **`mergeVerdict` in the outcome record's observed group.** Nothing that decides reads either, and `interlock report` reads neither.
|
|
48
|
+
- **`.claude/ship/merge-confidence.json`**, the run's evidence file, and **`.claude/ship/verify-final.json`**, the final verification verdict stamped with the run's id, so the close reads only what this run wrote. A review or verification left by an earlier run is not read.
|
|
49
|
+
- **A dimension record on `review.json`** (`dimensions: { selected, ran, unavailable, completed, skipped }`), written at every adjudication, so `review.ran` can tell which dimensions ran. `ran` means dispatched with its rubric in a review the worker reported complete.
|
|
50
|
+
- **Tasks left unticked are a `run.integrity` fact.** A run that left a task of the change unticked reads amber, needing a person.
|
|
51
|
+
- **Five banners,** none of which changes the exit code: `LIVING CONFORMANCE SKIPPED`, `LIVING CONFORMANCE PARTIAL`, `MERGE EVIDENCE UNAVAILABLE`, `MERGE CONFIDENCE NOT ASSESSED` and `MERGE CONFIDENCE NOT WRITTEN`. Each is documented in `docs/04`.
|
|
52
|
+
- **Documentation.** `docs/07` and `docs/15` cover the two signals, `conformance --living`, `--run`, the close's record and the evidence file; `docs/04` the banners and the summary block; `docs/10` and `docs/06` the handoff's living requirements; `docs/11` and `docs/14` the receipt block and `mergeVerdict`.
|
|
53
|
+
|
|
54
|
+
- **`interlock pipeline`: the platform's checks become the pipeline input, in code (E3).** A pure `lib/pipeline.mjs` turns the platform's own output into the file `merge-confidence --pipeline` reads, so no model and no person transcribes a check's state. It is offline: you pipe the document in.
|
|
55
|
+
- **GitHub:** `gh pr view <n> --json headRefOid,statusCheckRollup | interlock pipeline --github - --required <name> --out <file>`. The head commit and the checks come from one document; `--required`, repeatable, marks checks by exact name, and a required name the platform has not reported becomes a pending entry.
|
|
56
|
+
- **GitLab:** `interlock pipeline --gitlab-mr` reads the merge request and writes an input with no checks, or a pending record holding the head commit and the head pipeline's id; `interlock pipeline --gitlab --mr <record>` reads the pipeline. Nobody types the commit. **This reader is written from GitLab's and glab's documentation and has not been run against a live project.**
|
|
57
|
+
- **A result the platform did not give reads `pending`:** a cancelled or stale run and any value the state table does not name. `pending` reads `unobserved` at the verdict, never a pass and never a failure.
|
|
58
|
+
- **`--exclude <name>`**, repeatable and exact, leaves out a check whose own result waits on the verdict. The name is recorded in the file and shown in the signal's evidence and the description block.
|
|
59
|
+
- **A refused document removes the earlier file at `--out`** and says so, so an earlier moment's checks are never left for the next command.
|
|
60
|
+
- **A re-run with no start time never hides a failure.** A check GitHub reports more than once is read from the run that started last. When a start time among them cannot be read (absent, not a date, or the zero time of a run that has not started), the check reads the least favourable state among them (`fail`, `pending`, `skipped`, `pass`, in that order) and stderr names it. The order the runs are listed in decides nothing.
|
|
61
|
+
- **`merge-confidence` names what it assessed and prints its labels.** `--json` gains `head` (`{ sha }`), `base` (`{ ref, sha }`, the merge base) and `labels`: the verdict label (`interlock/merge`, `interlock/human-review` or `interlock/blocked`) and the score label (`interlock/score-<n>`, unpadded), each with a colour and a description, with `verdictNames` and `scorePrefix` for recognising predecessors. The names are defined once, in `MERGE_LABELS`. The text output prints both on a `labels:` line.
|
|
62
|
+
- **`merge-confidence --label-names`** prints `verdictNames` and `scorePrefix` and assesses nothing. It reads no input and no repository, so it answers when an assessment could not run.
|
|
63
|
+
- **`merge-confidence --markdown`** prints the description block for a merge request and nothing else. Every reason, evidence line and excluded name is an inline code span, so a check, a finding or a ledger question named by someone else cannot render as a mention, a link or markup. With `--json` it is a usage error.
|
|
64
|
+
- **The `unmeasured-source` flag.** `interlock verify coverage` prints two more fields, `unreportedFiles` and `unreportedAddedLines`: the changed source files the report holds no record for, and the lines the diff adds in them. A green `verify.coverage` reading carries the flag and names them; any other reading names them in its evidence. It lowers the score and moves no band.
|
|
65
|
+
- **`/interlock:mr` delivers the verdict.** It fetches the target branch, pipes the platform's checks into `interlock pipeline`, runs `merge-confidence` with `--base`, `--pipeline` and the run's evidence file when one exists, pastes the block under `## Merge confidence`, and applies one verdict label and one score label after removing their predecessors, **only when the platform's head is still the commit that was assessed.** New flags: `--confidence-comment` posts the block as a comment, `--gate-check <name>` excludes a check. The skill no longer asks a model for a confidence level, carries no label name of its own, and never merges, approves or requests changes. **It now creates and applies labels on the platform**, an outward action it did not take before; it remains invocable only by a person. When the assessment stops (a refused pipeline document, an input `merge-confidence` cannot read) it applies no label and still removes the verdict and score labels an earlier assessment left, recognising them by `merge-confidence --label-names`, so a merge label does not outlive a re-assessment that failed.
|
|
66
|
+
- **One `DOMAIN REVIEW PARTIAL` line for a size cut.** When the bound leaves requirements out of the domain reviewer's text, a strict run raises the banner with how many were left out, citing `LIMITS.maxDomainSpecChars` by its key. `docs/04` already said so; the code now does. A run with no graph and a cut states both in one line.
|
|
67
|
+
- **`REVIEW WORK FILE NOT CLEARED`.** Raised when the run cannot remove the findings or verdicts work file before its review (see "Changed"). Documented in `docs/04`.
|
|
68
|
+
- **`docs/15` covers delivery.** One row per signal and per flag, each table held equal to the registry by a test; the labels; the pipeline input; three platform recipes (a GitHub label-keyed approval, a Mergify queue rule, a GitLab pipeline job) and a CI job, each saying who merges, which step clears the labels on new commits, and that it was written from the platform's documentation and not exercised; and how far a label can be trusted.
|
|
69
|
+
- **The manifest declares the listing image.** `.claude-plugin/plugin.json` names the bundled listing image under `icon`, the key the plugin directory reads a listing image from. An earlier commit removed the key and pinned its absence on the claim that the directory finds the file itself, which no document says. `test/spine/package.test.mjs` now fails when the key is absent, when its value does not begin `./`, when `files` does not cover the path, or when the file is not a complete, square PNG of 512 to 2048 pixels. The manifest is the one shipped file that spells the image's path, and the same test fails naming any other that does: the directory's scan counts each such file as a reference to an image it cannot read as code.
|
|
70
|
+
- **The bundle is held to the plugin directory's two file limits.** The directory holds a version for a reviewer when a shipped file that is not an image or a font reaches 256 KiB (262,144 bytes), or when the bundle holds more than 512 files. `test/spine/package.test.mjs` measures both over the file set `npm pack` ships and names each file at or over the size limit with its size, or the count. The largest today are `bin/interlock` at 233,141 bytes and `lib/run.mjs` at 225,951, in a bundle of 166 files. Nothing measured either before.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- **BREAKING: `interlock gate`, `interlock review` and `interlock remediate` refuse an input with no findings list.** An object the shape reader did not recognise, such as `{}`, read as zero findings: `gate` passed over surviving blockers, `remediate` said there was nothing to fix. Each now exits 1 with the reason on stderr and nothing on stdout. A findings input is a list of findings, per-dimension records, or the record `interlock review --json` prints, whose `surviving` list `gate` and `remediate` now read. **An empty list is how to say nothing was found.** `interlock ready` reads a list-less findings file as an artifact review that has not run.
|
|
75
|
+
- **BREAKING: a strict run's review that leaves no findings list is not a clean review.** The run removes the findings and verdicts work files before the review, so nothing an earlier run left is adjudicated as this one's. A findings file that is absent, not JSON or list-less records the review as not completed, whatever the step reported, and the run continues to final verification. In a remediation round the same file no longer clears blockers: the round keeps the last adjudication's surviving findings and is recorded as not re-reviewed, so such a run halts at its verdict where it used to commit; when the last adjudication cannot be read back either, it halts naming both files. The review and remediation briefings now ask for an empty list when nothing is found.
|
|
76
|
+
- **BREAKING: `merge-confidence --run` under `--base` takes nothing from an evidence file whose run started somewhere other than the merge base.** A run's evidence covers the diff that run reviewed; a review of the last commit of a five-commit branch read green for the whole branch. No base is taken from the file, so `--run` without `--base` cannot read `MERGE`. The usage text and `docs/04` now show `--base` with `--run`.
|
|
77
|
+
- **BREAKING: a `--pipeline` file that is not well-formed is an input error.** A missing or misspelt `checks` read "the platform reports no checks", green, and a failed required check behind it read `MERGE`. The command now exits 1 naming the fault; inside the module such an input reads `unobserved`. An explicit empty list still reads green with `no-pipeline`. A hand-written file that was tolerated is refused with the reason.
|
|
78
|
+
- **BREAKING: the session-start hook returns the forward call and nothing else, so the band follows the first prompt.** 1.5.1's `classic.SessionStart` hook in `hooks/mod.mjs` returned the forward call continued with `.then`, to read the preflight's report once the chain had resolved. The plugin directory still held that version at the same line, and its own example of this hook ends in `return next(e)`. The hook now resets the meter at a session boundary and returns `meterSessionForward(meterSessionInput)`, with nothing bound to the call, awaited on it or chained after it, so the event's result, the session's first message included, is the chain's own. The read after the chain is removed, because it needs the chain's promise and cannot move ahead of it: the settings hook that writes the report runs inside the chain. On a host that raises the classic event to a hooks module, the band above the prompt now appears at the first prompt you submit, where it used to appear before it, and after `/clear`, `/resume` or `/branch` the module holds no report until the next prompt. `/interlock-preflight` and `/interlock-handoff` read the file themselves and are unchanged. On Claude Code 2.1.291, which raised no classic event to a module, nothing changes. `test/spine/mod-pins.test.mjs` reads the hook as text and fails on any other return shape.
|
|
79
|
+
- **`run.integrity` is unobserved for an observation the run record does not carry.** An empty record, or one whose permission-prompt and auto-mode-denial counts were never observed, read "no degradation". It now reads `unobserved`, naming the missing observations by their record keys; a recorded degradation beside them still reads amber. A run on a host with no recorder therefore reads `HUMAN` on `run.integrity` at close where it read green; it could not read `MERGE` at a close before either.
|
|
80
|
+
- **A glob-shaped item in `REVIEW.md` is a reported problem.** `- src/**` under `## Always Human Review` was read as a literal path that matched nothing, so the listed paths read green. An item holding `*` or `?` is now reported wherever policy problems are, added to neither list, and reads `policy.humanPaths` amber at the merge verdict when it sits in the always-human-review section at the base. Brackets and braces stay path characters.
|
|
81
|
+
- **A living list the size bound cut to nothing carries `requirements-omitted`.** It read green with no flag.
|
|
82
|
+
- **A cut living list keeps `requirements-omitted` at an amber reading.** The flag and the asked-and-left-out evidence were attached only when the answers read green, so an unconfirmed requirement on a cut list said nothing about the cut.
|
|
83
|
+
- **A blocker the quality band removed holds `review.adversarial` amber, needing a person.** A blocker the skeptics' vote kept and their quality scores then removed is in no surviving list, so `review.blockers` read green and the verdict could read `MERGE` with nobody having looked at it. The signal now reads the review record's `droppedByQuality` list: a blocker, or a severity outside the published set, holds the verdict at `HUMAN` and is named in the evidence. A warning or a suggestion the band removed moves nothing, and `interlock gate` is unchanged.
|
|
84
|
+
- **`verify coverage --json` prints nine fields, not seven,** and the published flag list gains `unmeasured-source`, which can lower a score.
|
|
85
|
+
- **`merge-confidence` no longer says `MERGE` is unreachable.** The reason lines for the two producer-less signals, and the header's statement that no input can read `MERGE`, are gone with the producers' arrival.
|
|
86
|
+
- **A plugin option reaches the session through a hook of its own.** The preflight used to copy the `ntfy topic` and `ntfy server` options into the session's environment by reading the host's option variables. The manifest now hands each option to its own `SessionStart` entry as a `${user_config.…}` argument, the form the plugin directory's checklist asks for, and the new `hooks/notify-option.mjs` appends the matching `INTERLOCK_NTFY_TOPIC` or `INTERLOCK_NTFY_URL` export to the session's environment file. The host does not run a hook whose arguments name an option with no saved value (probed on Claude Code 2.1.296 and 2.1.161), so each entry runs only for a person who saved that option; the preflight's entry names none and runs for everyone, and it no longer reads or writes anything for the push. A variable you already set still wins. `lib/notify.mjs` and `lib/doctor.mjs` read the two `INTERLOCK_NTFY_*` variables only, and a test fails naming any shipped file that spells the host's option-variable prefix.
|
|
87
|
+
- **The doctor's `notify` row says what it can see.** The `interlock doctor` that session start runs sits beside the option hooks and cannot see what they are about to export. With only a plugin option set, the row in the session-start report reads `skip`, where 1.5.1 said `ok`; its detail names both ways to set a topic and says why. `interlock doctor` typed in the session afterwards reads the exported variable and reports `ok` with the server. A `skip` row draws no band.
|
|
88
|
+
- **The README says everything the plugin sends and has the model run.** The network paragraph now names the `ntfy server` option, says how an option reaches the session, and says `/interlock:mr` has the model run `gh`, `glab` and `git fetch` under your own sign-in, with those requests being those tools'. `docs/04`, `docs/07` and `docs/13` follow.
|
|
89
|
+
- **Left as they are, on purpose.** The plugin directory's report on 1.5.1 also names the top-level `bin/`, the manifest's `types` key and a read of credentials from the environment. `bin/` stays: the bare `interlock` command is how every skill, the workflow and the hooks module call the CLI. `types` stays: it is a documented manifest field, and the engine refuses session state without the contract it names. The doctor still checks whether `ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, `CODEX_API_KEY` and `OPENAI_API_KEY` are set, by name and never by value. Each of the three is for a reviewer to confirm, and none is changed here.
|
|
90
|
+
|
|
91
|
+
#### If you never run `merge-confidence`: what is different since 1.5.1
|
|
92
|
+
|
|
93
|
+
The merge-confidence work also changed things a ship run does on its own. Each of these is in the code as released, not only in a command you would have to call:
|
|
94
|
+
|
|
95
|
+
- **The close summary prints a `MERGE CONFIDENCE` block** on every completed run, and it can read `BLOCK` beside `SHIP COMPLETE`: a type check that failed at the final gate does not halt the run and does read the verdict red. The verdict never changes the exit code.
|
|
96
|
+
- **Strict runs read `## Do Not Report` from the commit the run started on,** not from the working tree. An edit to `REVIEW.md` in the change under review does not take effect for that review, and the run says so (`POLICY DIFFERS FROM BASE`).
|
|
97
|
+
- **Strict runs may run a seventh review dimension, `domain`,** when the change touches a capability that has a living spec, and a run with `--conformance` asks its handoff about the living requirements in force as well as the change's own scenarios. Both cost tokens: the reviewer is handed the requirement text, up to the `domain spec text budget (chars)` row of `interlock limits`.
|
|
98
|
+
- **A close in a repository whose coverage report is not lcov raises `MERGE EVIDENCE UNAVAILABLE`** every time, naming the format. Only lcov is read.
|
|
99
|
+
- **A threshold-shaped item in a `REVIEW.md` path list is reported as a problem** and no longer read as a path, and now so is a glob-shaped one.
|
|
100
|
+
- **A spec mention written with a leading `./` links in the graph.** `` `./lib/a.mjs` `` in a living spec used to produce no spec-to-file edge; it now names `lib/a.mjs`, so a rebuilt graph can gain edges.
|
|
101
|
+
- **Strict runs whose domain text is cut by the bound gain a `DOMAIN REVIEW PARTIAL` line** in the close summary. On a repository with large specs that is most strict runs.
|
|
102
|
+
|
|
10
103
|
## 1.5.1 — 2026-10-11
|
|
11
104
|
|
|
12
105
|
### Changed
|
package/README.md
CHANGED
|
@@ -13,9 +13,7 @@
|
|
|
13
13
|
</p>
|
|
14
14
|
<p align="center"><sub>One real run, cut to 34 s. <code>spec</code> asks one question, writes 11 specs and 12 tasks, then stops. <code>ship</code> runs three agents in parallel across two waves and ends in a commit with the suite green, 180 of 180.</sub></p>
|
|
15
15
|
<p align="center"><code>12 tasks · 2 waves · 19 agents · 13m 54s · 57 files · 2,881 lines · 180 of 180 pass · 1 commit</code></p>
|
|
16
|
-
|
|
17
|
-
<p align="center"><sub>The whole run, uncut and captioned: <a href="https://github.com/renzrollon/interlock/releases/download/v1.4.2/interlock-spec-and-ship-full.mp4">2:34, mp4, 9 MB</a>.</sub></p>
|
|
18
|
-
-->
|
|
16
|
+
<p align="center"><sub>The whole run, uncut and captioned: <a href="https://github.com/renzrollon/interlock/releases/download/v1.5.0/interlock-spec-and-ship-full.mp4">2:34, mp4, 9 MB</a>.</sub></p>
|
|
19
17
|
|
|
20
18
|
```bash
|
|
21
19
|
/interlock:spec "<idea>" # explores your repo, writes a reviewed spec, then stops
|
|
@@ -58,7 +56,7 @@ interlock doctor # preflight: prints the exact allowlist an unattended
|
|
|
58
56
|
| `/interlock:spec "<idea>"` | Idea → explored, reviewed, implementation-ready change. Asks you what the repo cannot answer, then stops | skill |
|
|
59
57
|
| **You read the spec** | The checkpoint. [Ten minutes, with a checklist](docs/02-the-checkpoint.md) | you |
|
|
60
58
|
| `/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** |
|
|
61
|
-
| `/interlock:mr` | Change → merge request | skill |
|
|
59
|
+
| `/interlock:mr` | Change → merge request, with the CLI's merge-confidence verdict in the description and as labels. It never merges | skill |
|
|
62
60
|
|
|
63
61
|
<p align="center">
|
|
64
62
|
<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">
|
|
@@ -79,7 +77,7 @@ Decisions that have a correct answer are moved out of prose and into code, one a
|
|
|
79
77
|
|
|
80
78
|
- **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)
|
|
81
79
|
- **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.
|
|
82
|
-
- **Reviews you can actually read.** On `--strict`, up to
|
|
80
|
+
- **Reviews you can actually read.** On `--strict`, up to seven review dimensions run in parallel (the seventh, `domain`, when the change touches a capability that has a living spec), 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)
|
|
83
81
|
- **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)
|
|
84
82
|
- **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)
|
|
85
83
|
- **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)
|
|
@@ -118,7 +116,7 @@ Three zero-dependency Node binaries land on your `PATH`, and they work without t
|
|
|
118
116
|
| `interlock-graph` | A local, deterministic code knowledge graph. No vector store, no network |
|
|
119
117
|
| `interlock-run` | The experimental runner above |
|
|
120
118
|
|
|
121
|
-
Everything runs without a model and without the network, with one exception: an opt-in push for a run that stops while nobody is watching. `run close --notify` sends the close summary's first line, the change name and the run id; the spec checkpoint sends the change name; an `interlock notify` you type sends the title and body you give it. The request goes to the [ntfy](https://ntfy.sh) server named by `INTERLOCK_NTFY_URL`, the public `https://ntfy.sh` by default, under the topic you chose in the plugin's `ntfy topic` option or in `INTERLOCK_NTFY_TOPIC`.
|
|
119
|
+
Everything runs without a model and without the network, with one exception: an opt-in push for a run that stops while nobody is watching. `run close --notify` sends the close summary's first line, the change name and the run id; the spec checkpoint sends the change name; an `interlock notify` you type sends the title and body you give it. The request goes to the [ntfy](https://ntfy.sh) server named by the plugin's `ntfy server` option or by `INTERLOCK_NTFY_URL`, the public `https://ntfy.sh` by default, under the topic you chose in the plugin's `ntfy topic` option or in `INTERLOCK_NTFY_TOPIC`. Each option has a session-start hook of its own, which the host runs only when that option is saved; the hook writes the matching variable into the session's environment for the session's commands, unless you already set it, because the CLI call that pushes is not a hook and an option reaches hooks only. No account, key or token goes with the request: the topic is the only thing in it that identifies you, so treat it as a secret. With no topic set, no request is ever made. Interlock makes no other request. `/interlock:mr` has the model run `gh`, `glab` and `git fetch` under your own sign-in to read and write a merge request; those requests are those tools'. `interlock doctor` reports whether `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN` is set, by name and never by value, and the vendor CLIs a run spawns inherit the environment as any child process does; what they send is theirs. Reference and every environment variable: [07 — CLI and configuration](docs/07-cli-and-configuration.md).
|
|
122
120
|
|
|
123
121
|
## Docs
|
|
124
122
|
|
|
@@ -140,6 +138,7 @@ New here? Start with **[01 — The first hour](docs/01-first-hour.md)**. Only ev
|
|
|
140
138
|
| [12 — Repository review policy](docs/12-repository-review-policy.md) | The optional `REVIEW.md`: what it can change and what it cannot |
|
|
141
139
|
| [13 — The guards](docs/13-the-guards.md) | The hooks, the stage marker's lifecycle, and the fail-open rule |
|
|
142
140
|
| [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 |
|
|
141
|
+
| [15 — Merge confidence](docs/15-merge-confidence.md) | What the merge verdict reads, signal by signal and flag by flag; how it reaches a merge request (the pipeline input, the description block, the labels, three platform recipes and how far a label can be trusted); recording a coverage baseline, computing the base on demand, reading the policy from the base, reading a spec audit |
|
|
143
142
|
|
|
144
143
|
## Development
|
|
145
144
|
|