Skip to content

v1.6.0

Choose a tag to compare

@renzrollon renzrollon released this 13 Oct 07:35
· 20 commits to main since this release

Added

  • 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.

    • 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.
  • 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).

  • 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.

  • 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.

  • 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 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.
    • verify exec is unchanged. The brief's verify exec coverage --write-baseline was not built, because verify exec is a pass-through.
  • ## 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.

    • --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.
    • 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.
    • A threshold-shaped list item is now rejected in both path sections, as a threshold-shaped key already was.
    • 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.
    • 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.
  • 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.

    • 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.
    • 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.
    • 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.
    • 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.
    • Two banners: DOMAIN REVIEW SKIPPED and DOMAIN REVIEW PARTIAL, each with its reason.
  • 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.

  • 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.

  • 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.

    • 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.
    • 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.
    • requirements-omitted flag 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 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.

  • 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.

  • 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.

  • 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.

    • 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.
    • .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, so review.ran can tell which dimensions ran. ran means dispatched with its rubric in a review the worker reported complete.
    • Tasks left unticked are a run.integrity fact. 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 ASSESSED and MERGE CONFIDENCE NOT WRITTEN. Each is documented in docs/04.
  • 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.

  • 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.

    • 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-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.
    • 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.
    • --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 --out and 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.
  • 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.

    • 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.
  • 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.

  • 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.

  • /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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

Changed

  • 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.
  • 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 --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.
  • 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.
  • 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.
  • 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.
  • 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.
  • A living list the size bound cut to nothing carries requirements-omitted. It read green with no flag.
  • 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.
  • 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.
  • verify coverage --json prints nine fields, not seven, and the published flag list gains unmeasured-source, which can lower a score.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.

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 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.
  • 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).
  • 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.
  • A close in a repository whose coverage report is not lcov raises MERGE EVIDENCE UNAVAILABLE every time, naming the format. Only lcov is read.
  • 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.
  • 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.
  • 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.