Repository navigation
v1.6.0
Added
-
A merge verdict and score, computed by the CLI (
lib/merge-confidence.mjs).assessMergeConfidencetakes the facts a review and a run already produce and returnsMERGE,HUMANorBLOCK, 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 informationalplan.fidelity) and flags in one published list (FLAGS);combineSignalsis the formula and names no signal. The module is pure.- Every gating signal has a producer.
conformance.livingandspec.coveragewere registered with no producer and read unobserved; both now read the facts their rules name (see below), so a clean, reviewed, verified change can readMERGE. A signal reads unobserved only because a fact about the change is missing, and its reason says which.
- Every gating signal has a producer.
-
interlock merge-confidence [change] --changed <files...>. It reads the change's artifacts, its decision ledger, its delta-spec scenario count, drift andHEADfrom the repository, and--review,--verify,--handoff,--pipelineand--coveragefrom files the caller names.--base <ref>adds the diff shape fromgit diff --numstat, and every path that diff reports joins the changed paths, so a path left out of--changedstill reaches the risk classifier; a base that isHEADor ahead of it reads the diff shapeunobserved. It is offline and never callsgh,glabor the network. It exits 1 onBLOCK, or on anything butMERGEunder--require-merge.HUMANexits 0. An input it cannot read or parse also exits 1, with the reason on stderr and nothing on stdout, asgatedoes. The pipeline file and the coverage file are defined here and produced by later changes (E3, E8). -
MERGE_BANDSinlib/limits.mjsandMERGE_ALLOWED_CLASSESinlib/risk.mjs.MERGE_BANDSholds the band ceilings and every threshold the verdict applies. It is a separate export besideLANE_CAPS,interlock limitsprints one row per entry and per ceiling,--jsonpublishes it asmergeBands, and the cap-authority sweep covers it.MERGE_ALLOWED_CLASSESholds the risk classes the merge verdict reads green. It sits besideCONTINUITY_ALLOWED_CLASSESand holds the same classes today, so the two policies can diverge without one moving the other. -
The
verify judgeverdict carriestypecheck: { ran, exitCode, passed }, ornullwhen 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. -
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.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.interlock verify coverage --base <ref> [--report <lcov>] [--baseline <file>] [--compute-base]prints the coverage inputmerge-confidence --coveragereads 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 printsconfigured: false; a declared report that is missing or not lcov exits 1 with nothing on stdout.--compute-basemeasures the merge base in a temporary worktree that is removed whether the command passed or not.verify execis unchanged. The brief'sverify exec coverage --write-baselinewas not built, becauseverify execis a pass-through.
-
## Always Human ReviewinREVIEW.md, and policy read from the base (E4). The section is parsed with the## Do Not Reportgrammar and enforced only by the merge verdict; it never moves a finding or a gate.--base <ref>oninterlock reviewandinterlock review-policyreads both path lists from theREVIEW.mdcommitted 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.merge-confidence --basemakespolicy.humanPathsobservable: amber, needing a person, for a changed path under a listed path, for any edit toREVIEW.mditself, and for a committed section that could not be read in full; green otherwise; unobserved when no base was given.- A threshold-shaped list item is now rejected in both path sections, as a threshold-shaped key already was.
startShaon 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.- Two banners:
POLICY DIFFERS FROM BASEwhen the working copy differs from the start commit's, andREVIEW BASE NOT PINNEDwhen no start commit could be recorded. Both reach the close summary.
-
A
domainreview 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.LIMITS.maxDomainSpecCharsbounds the text at a requirement boundary and names what was left out.interlock limitsprints it asdomain spec text budget (chars), and--jsoncarries it.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--basethat names no commit exits 1.- A finding may carry
requirement: openspec/specs/<cap>/spec.md#<heading>. Adomainblocker 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, soBLOCKERandblockerare reported too. domain-skippedflag onreview.ranwhen the dimension was skipped for want of a graph. It leaves the band to the rest of the record.- Two banners:
DOMAIN REVIEW SKIPPEDandDOMAIN REVIEW PARTIAL, each with its reason.
-
interlock spec-audit [--json](E7). A non-gating report in thedriftfamily of what each living capability states: requirements, scenarios without aGIVEN, 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. -
docs/15-merge-confidence.md. The baseline recipe with the default-branch job,--compute-baseand its limits, the always-human-review section and why lists are read from the base, and how to read a spec audit. -
conformance.living: the handoff confirms the living requirements a change touches (E6). The signal compares the handoff'srequirementsCheckedandrequirementsUnconfirmedwith the number of entries in a living list the command counts itself; arequirementsRequiredin 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.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 thedomainreview's text is, so the two cannot disagree. It exits 0 in every case; a--basenaming no commit is the reason the list was not built. Without--livingthe output is unchanged.- The ship handoff asks about them under
--conformance, andHANDOFF_RESULT_SCHEMAcarries the optional countsrequirementsCheckedandrequirementsUnconfirmed, recorded on the manifest and printed as aliving requirements:row. requirements-omittedflag when the size bound left requirements out. It lowers the score and never moves a band.
-
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 thelegacy-unspeccedflag, with the repository's named-over-total figure. It reads spec text and git, never the graph, throughspecMentionedPathsinlib/spec-text.mjs(the rule the graph's extractor now shares) andlib/spec-coverage.mjs, so it reads the same in CI as on a developer's machine. Unobserved with no--base. -
One gather for the command and the close (
lib/merge-evidence.mjs).gatherMergeEvidenceassembles the assessment's input forinterlock merge-confidenceand 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. -
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 recordsHEADas that commit. Anything else is refused on stderr with nothing taken from it. An explicit--review,--verify,--handoffor--coveragereplaces the file's entry, and the verdict recorded in the file is never read. -
A ship run states its merge verdict once, at close. A completed
run closeassesses merge confidence from what the run observed and printsMERGE 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 readsHUMANorBLOCK; the verdict never changes the exit code.- A
mergeConfidenceblock on the receipt (verdict, score, worst band, counts, driving ids, flags; no reason or evidence text) andmergeVerdictin the outcome record's observed group. Nothing that decides reads either, andinterlock reportreads neither. .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.- A dimension record on
review.json(dimensions: { selected, ran, unavailable, completed, skipped }), written at every adjudication, soreview.rancan tell which dimensions ran.ranmeans dispatched with its rubric in a review the worker reported complete. - Tasks left unticked are a
run.integrityfact. A run that left a task of the change unticked reads amber, needing a person. - Five banners, none of which changes the exit code:
LIVING CONFORMANCE SKIPPED,LIVING CONFORMANCE PARTIAL,MERGE EVIDENCE UNAVAILABLE,MERGE CONFIDENCE NOT ASSESSEDandMERGE CONFIDENCE NOT WRITTEN. Each is documented indocs/04.
- A
-
Documentation.
docs/07anddocs/15cover the two signals,conformance --living,--run, the close's record and the evidence file;docs/04the banners and the summary block;docs/10anddocs/06the handoff's living requirements;docs/11anddocs/14the receipt block andmergeVerdict. -
interlock pipeline: the platform's checks become the pipeline input, in code (E3). A purelib/pipeline.mjsturns the platform's own output into the filemerge-confidence --pipelinereads, so no model and no person transcribes a check's state. It is offline: you pipe the document in.- 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. - GitLab:
interlock pipeline --gitlab-mrreads 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. - A result the platform did not give reads
pending: a cancelled or stale run and any value the state table does not name.pendingreadsunobservedat the verdict, never a pass and never a failure. --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.- A refused document removes the earlier file at
--outand says so, so an earlier moment's checks are never left for the next command. - 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.
- GitHub:
-
merge-confidencenames what it assessed and prints its labels.--jsongainshead({ sha }),base({ ref, sha }, the merge base) andlabels: the verdict label (interlock/merge,interlock/human-revieworinterlock/blocked) and the score label (interlock/score-<n>, unpadded), each with a colour and a description, withverdictNamesandscorePrefixfor recognising predecessors. The names are defined once, inMERGE_LABELS. The text output prints both on alabels:line.merge-confidence --label-namesprintsverdictNamesandscorePrefixand assesses nothing. It reads no input and no repository, so it answers when an assessment could not run.
-
merge-confidence --markdownprints 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--jsonit is a usage error. -
The
unmeasured-sourceflag.interlock verify coverageprints two more fields,unreportedFilesandunreportedAddedLines: the changed source files the report holds no record for, and the lines the diff adds in them. A greenverify.coveragereading carries the flag and names them; any other reading names them in its evidence. It lowers the score and moves no band. -
/interlock:mrdelivers the verdict. It fetches the target branch, pipes the platform's checks intointerlock pipeline, runsmerge-confidencewith--base,--pipelineand 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-commentposts 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 inputmerge-confidencecannot read) it applies no label and still removes the verdict and score labels an earlier assessment left, recognising them bymerge-confidence --label-names, so a merge label does not outlive a re-assessment that failed. -
One
DOMAIN REVIEW PARTIALline 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, citingLIMITS.maxDomainSpecCharsby its key.docs/04already said so; the code now does. A run with no graph and a cut states both in one line. -
REVIEW WORK FILE NOT CLEARED. Raised when the run cannot remove the findings or verdicts work file before its review (see "Changed"). Documented indocs/04. -
docs/15covers 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. -
The manifest declares the listing image.
.claude-plugin/plugin.jsonnames the bundled listing image undericon, 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.mjsnow fails when the key is absent, when its value does not begin./, whenfilesdoes 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. -
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.mjsmeasures both over the file setnpm packships and names each file at or over the size limit with its size, or the count. The largest today arebin/interlockat 233,141 bytes andlib/run.mjsat 225,951, in a bundle of 166 files. Nothing measured either before.
Changed
- BREAKING:
interlock gate,interlock reviewandinterlock remediaterefuse an input with no findings list. An object the shape reader did not recognise, such as{}, read as zero findings:gatepassed over surviving blockers,remediatesaid 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 recordinterlock review --jsonprints, whosesurvivinglistgateandremediatenow read. An empty list is how to say nothing was found.interlock readyreads a list-less findings file as an artifact review that has not run. - 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.
- BREAKING:
merge-confidence --rununder--basetakes 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--runwithout--basecannot readMERGE. The usage text anddocs/04now show--basewith--run. - BREAKING: a
--pipelinefile that is not well-formed is an input error. A missing or misspeltchecksread "the platform reports no checks", green, and a failed required check behind it readMERGE. The command now exits 1 naming the fault; inside the module such an input readsunobserved. An explicit empty list still reads green withno-pipeline. A hand-written file that was tolerated is refused with the reason. - BREAKING: the session-start hook returns the forward call and nothing else, so the band follows the first prompt. 1.5.1's
classic.SessionStarthook inhooks/mod.mjsreturned 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 inreturn next(e). The hook now resets the meter at a session boundary and returnsmeterSessionForward(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,/resumeor/branchthe module holds no report until the next prompt./interlock-preflightand/interlock-handoffread 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.mjsreads the hook as text and fails on any other return shape. run.integrityis 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 readsunobserved, 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 readsHUMANonrun.integrityat close where it read green; it could not readMERGEat a close before either.- A glob-shaped item in
REVIEW.mdis a reported problem.- src/**under## Always Human Reviewwas 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 readspolicy.humanPathsamber at the merge verdict when it sits in the always-human-review section at the base. Brackets and braces stay path characters. - A living list the size bound cut to nothing carries
requirements-omitted. It read green with no flag. - A cut living list keeps
requirements-omittedat 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. - A blocker the quality band removed holds
review.adversarialamber, needing a person. A blocker the skeptics' vote kept and their quality scores then removed is in no surviving list, soreview.blockersread green and the verdict could readMERGEwith nobody having looked at it. The signal now reads the review record'sdroppedByQualitylist: a blocker, or a severity outside the published set, holds the verdict atHUMANand is named in the evidence. A warning or a suggestion the band removed moves nothing, andinterlock gateis unchanged. verify coverage --jsonprints nine fields, not seven, and the published flag list gainsunmeasured-source, which can lower a score.merge-confidenceno longer saysMERGEis unreachable. The reason lines for the two producer-less signals, and the header's statement that no input can readMERGE, are gone with the producers' arrival.- A plugin option reaches the session through a hook of its own. The preflight used to copy the
ntfy topicandntfy serveroptions into the session's environment by reading the host's option variables. The manifest now hands each option to its ownSessionStartentry as a${user_config.…}argument, the form the plugin directory's checklist asks for, and the newhooks/notify-option.mjsappends the matchingINTERLOCK_NTFY_TOPICorINTERLOCK_NTFY_URLexport 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.mjsandlib/doctor.mjsread the twoINTERLOCK_NTFY_*variables only, and a test fails naming any shipped file that spells the host's option-variable prefix. - The doctor's
notifyrow says what it can see. Theinterlock doctorthat 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 readsskip, where 1.5.1 saidok; its detail names both ways to set a topic and says why.interlock doctortyped in the session afterwards reads the exported variable and reportsokwith the server. Askiprow draws no band. - The README says everything the plugin sends and has the model run. The network paragraph now names the
ntfy serveroption, says how an option reaches the session, and says/interlock:mrhas the model rungh,glabandgit fetchunder your own sign-in, with those requests being those tools'.docs/04,docs/07anddocs/13follow. - Left as they are, on purpose. The plugin directory's report on 1.5.1 also names the top-level
bin/, the manifest'stypeskey and a read of credentials from the environment.bin/stays: the bareinterlockcommand is how every skill, the workflow and the hooks module call the CLI.typesstays: it is a documented manifest field, and the engine refuses session state without the contract it names. The doctor still checks whetherANTHROPIC_API_KEY,CLAUDE_CODE_OAUTH_TOKEN,CODEX_API_KEYandOPENAI_API_KEYare set, by name and never by value. Each of the three is for a reviewer to confirm, and none is changed here.
If you never run merge-confidence: what is different since 1.5.1
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:
- The close summary prints a
MERGE CONFIDENCEblock on every completed run, and it can readBLOCKbesideSHIP 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. - Strict runs read
## Do Not Reportfrom the commit the run started on, not from the working tree. An edit toREVIEW.mdin the change under review does not take effect for that review, and the run says so (POLICY DIFFERS FROM BASE). - Strict runs may run a seventh review dimension,
domain, when the change touches a capability that has a living spec, and a run with--conformanceasks 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 thedomain spec text budget (chars)row ofinterlock limits. - A close in a repository whose coverage report is not lcov raises
MERGE EVIDENCE UNAVAILABLEevery time, naming the format. Only lcov is read. - A threshold-shaped item in a
REVIEW.mdpath list is reported as a problem and no longer read as a path, and now so is a glob-shaped one. - 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 nameslib/a.mjs, so a rebuilt graph can gain edges. - Strict runs whose domain text is cut by the bound gain a
DOMAIN REVIEW PARTIALline in the close summary. On a repository with large specs that is most strict runs.