Skip to content

feat(fabrika-cli): mint the verb package and implement the six adr verbs to the derived contract (#4725) - #4731

Merged
usirin merged 3 commits into
mainfrom
usirin/fabrika-cli-adr-verbs-4725-4D0D5B4F
Aug 2, 2026
Merged

feat(fabrika-cli): mint the verb package and implement the six adr verbs to the derived contract (#4725)#4731
usirin merged 3 commits into
mainfrom
usirin/fabrika-cli-adr-verbs-4725-4D0D5B4F

Conversation

@usirin

@usirin usirin commented Aug 2, 2026

Copy link
Copy Markdown
Member

Fixes #4725

Mints packages/fabrika-cli/ and implements the six adr verbs to
claude-plugins/fabrika/skills/adr/contract.md — the wave-0 pilot's derived CLI contract,
on main since #4724 with nothing implementing it.

What landed

packages/fabrika-cli/ in the packages/pipeline-cli/ idiom: Effect CLI
(effect/unstable/cli), "type": "module", a pure tested core plus a thin src/bin.ts,
build / typecheck / test / cli scripts, tests beside the source as
<name>.unit.test.ts, a package README.md, every dependency on catalog:.

Verb Answers
adr next the next unused id — max(fetched merged set ∪ open-PR claims) + 1
adr new scaffolds .decisions/NNNN-slug.md from the canonical template
adr resolve each id's real filename and state: live / landed / in-flight / absent
adr supersede rewrites an older record's status: line to superseded by [NNNN](…)
adr amend-in-part appends this id to an older record's amended-in-part by list
adr sweep ranks the uncited live-accepted records this one may contradict

It calls v1 nowhere — no import, no subprocess (ADR 0238). v1 was read for its shape and
its scars; nothing is called. The only mention of the old tree in the whole package is the
README paragraph that states the ban.

The shape that made the refusals testable

A verb is a pure function of its dependencies: each *-verb.ts computes a VerbOutcome
(exit code, stdout, stderr) and never writes a stream or exits. The Effect CLI layer in
src/adr/command.ts does both. That is why the tests can drive inputs a real tree cannot be
asked to produce on demand — an unreadable directory, a gh that exits 0 with the wrong
bytes, a base ref that cannot be fetched, a record whose id will not parse.

Built against the defect class, not just around it

Tonight's campaign found nine instances of one shape: a check that cannot see what it is
looking for, failing as a plausible value rather than an error. Concretely here:

  • --base is fetched before it is read, and the resolved SHA is shape-checked as an
    object name before anything uses it. git rev-parse answering HEAD -> main is a refusal,
    not a base ref.
  • Every GitHub read is REST, paged, and shape-validated. gh exiting 0 having printed
    null or an unrecognised status word resolves to a refusal, never to an empty in-flight
    set — which a caller would read as "nothing reserved" and fall back to the on-disk id.
  • One unreadable member makes the whole set UNKNOWN. A pull request whose file list 502s
    refuses the run; it never trims the in-flight set down to the pull requests that answered.
    Same for one unreadable corpus record in adr sweep.
  • Zero scope reds (ADR 0092) rather than answering 0001.
  • Nothing on stdout on a non-zero exit, in every verb, so a caller cannot read bytes
    without reading the status.

Each verb carries at least one test where the input is unreadable or malformed and the verb
REFUSES rather than answering.

The two behaviours the contract names as owed

  • The one-line-diff assertion. diffBeyondStatusLine takes the two texts — the record
    as read and the record as re-joined after the edit — and compares them line for line
    including each line's terminator; anything beyond status: aborts with exit 6 and writes
    nothing. Reachable from runRelate, and driven there by a test: a mixed-ending record makes
    rewriteStatus's split-and-re-join rewrite every other line's terminator, which is exactly
    the case this guard exists to stop. (This is a repair-round-1 correction — see the
    Deviations entry; the first version of this bullet claimed a falsifiability the code did not
    have.)
  • amend-in-part appends. ADR 0023's three existing links are preserved and the new one
    is inserted in id order; a test asserts the multi-link list survives, and a re-add is a
    no-op that still exits 0.

Both #4723 scars designed out

adr sweep exits 0 on all three outcomes including shortlist — a caller must never
read its own shortlist as a failed run — and --json goes to stdout, with the scope line
and the no-overlap / indeterminate reason on stderr.

Verified by running it, not by citing a gate

$ node packages/fabrika-cli/src/bin.ts adr resolve 0164 0023
landed	0164-guard-relaxing-adr-cp-gate.md	proposed
live	0023-live-views-sse-livedo.md	amended-in-part by [0025](0025-split-livedo-connection-topic.md), [0028](0028-effect-durable-object-model.md), [0037](0037-unified-void-aligned-live-do.md)

Byte-identical to the contract's worked example. Also confirmed live against this repo:
adr next --json{"id":"0239","mergedMax":"0238","inFlight":["0223","0235","0237"],…};
adr resolve 0237in-flight … PR #4703; adr resolve --base origin/nonexistent → the
contract's exact refusal on exit 3; adr next --repo kamp-us/nonexistent → exit 4;
adr sweep --new 0240 --dir …/fixtures/small-corpusindeterminate, exit 0, matching the
contract's example. supersede / amend-in-part were run over copies of the real 0126 and
0023, and diff confirms exactly one changed line each.

139 unit tests, typecheck, build, biome check, catalog-guard and readme-guard all
green, each run directly rather than inferred from a gate. All six verbs were re-run end to
end against the built binary after repair round 1.

The second deliverable

A record of whether contract.md alone was sufficient — which clauses fully determined
the implementation and which forced a judgment the spec did not make — is posted as a comment
on #4725. Short version: five of six verbs were buildable from the spec with no guesses;
adr sweep's scoring function is described but never given, and twelve smaller gaps are
enumerated there.

No spec clause was patched — the gaps are reported, per the issue's instruction, and as of
repair round 1 they are filed against the spec rather than left in a comment on a closing
issue: #4735 (sweep's unspecified scoring, plus the proposed completeness rule 7 for
cli-interface-convention.md), #4736 (no exit code for an unreadable --dir), #4737 (what
counts as a record — the lettered 0034a and the unreachable unparseable id error), #4738
(six under-determined prose details). An amendment on #4725 cross-links them and walks back
the record's gap-#6 framing, which was firmer than its evidence carried.

The platform seam

Every filesystem and subprocess touch goes through the Effect platform servicesFileSystem
/ Path from effect (.patterns/effect-platform-access.md) and ChildProcess /
ChildProcessSpawner from effect/unstable/process (.patterns/effect-process-cli-shell.md) —
never a raw node:* import. src/run.ts provides the one NodeServices.layer that satisfies both. Tests
substitute those same services (FileSystem.layerNoop, a canned ChildProcessSpawner), so the
seam under test is the seam production uses.

The unreadable-vs-empty discrimination this package is built around rides the typed E
channel
rather than a null sentinel: ReadFailed / WriteFailed, which a caller cannot
reach the value past without deciding what to do with.

Out of scope, untouched

claude-plugins/kampus-pipeline/, packages/pipeline-cli/, contract.md, SKILL.md, the
eval harness, and issues #4705#4722.

Deviations

(repair round 1)

  • The IO conversion is larger than the gate's verdict asked for. The review-code FAIL
    named two blockers; converting the package off node:fs / node:child_process onto the
    Effect platform services is a third item, a founder directive that arrived after the gate
    ran. It touches every verb signature (a verb now returns an Effect of its VerbOutcome
    rather than the value directly) and every IO test. The architecture is unchanged — the
    pure-core / thin-bin split and the VerbOutcome contract are as reviewed — but the diff is
    substantially wider than the verdict alone implies, so it is called out rather than left to
    be discovered.
  • Blocker 2 was resolved by the stronger of the two offered branches. The verdict offered
    either correcting the two over-claiming artifacts or making the assert genuinely
    falsifiable. Took the second: diffBeyondStatusLine now compares texts with terminators
    attached, which also closes the mixed-line-ending blind spot the old docblock named as an
    example while being unable to see it. The docblock and this PR body are corrected as well.
  • adr new gained a refusal the verdict did not ask for. The existence probe now fails
    closed when the probe itself cannot run. existsSync reported an unreadable parent directory
    as "absent", which licensed a write over a record the verb never managed to look at — the
    same defect class as the rest of the package, surfaced by the platform's typed error.
    Covered by a new test.
  • Nothing in the review was declined. Both blockers are addressed in full; no finding was
    fixed more narrowly than prescribed.

(repair round 3)

…verbs to the derived contract (#4725)

The wave-0 pilot's derived CLI contract has been on `main` since #4724 with nothing
implementing it: every fence in the `/adr` skill invokes a `fabrika-cli` binary that did not
exist, and two of its five eval cases had never executed once. This mints
`packages/fabrika-cli/` in the `packages/pipeline-cli/` idiom and builds all six verbs to
`claude-plugins/fabrika/skills/adr/contract.md`.

`fabrika-cli` invokes v1 nowhere — no import, no subprocess (ADR 0238). v1 was read for its
shape and its scars; nothing is called. The reason is the deletion test: every tether is a
reason the old tree can never be deleted.

The verbs are pure functions of their dependencies — each `*-verb.ts` computes a
`VerbOutcome` (exit code, stdout, stderr) and never writes a stream or exits; the Effect CLI
layer does both. That split is what makes each refusal as deterministically testable as each
answer, and it is why the tests can drive an unreadable directory, a `gh` that exits 0 with
the wrong bytes, and a base ref that cannot be fetched.

Built against one defect class throughout: a check that cannot see what it is looking for,
failing as a plausible value rather than an error. So `--base` is fetched before it is read;
every GitHub read is REST, paged, and shape-validated before anything interprets it; a
zero-record scan is a refusal, not `0001`; and no verb prints anything on stdout on a
non-zero exit.

The two behaviours the contract names as owed: `supersede`/`amend-in-part` assert the rewrite
touched the `status:` line alone before writing (exit 6 aborts otherwise), and
`amend-in-part` appends to an existing list in id order rather than overwriting it — ADR 0023
carries three such links today.

Neither #4723 scar is repeated: all three `sweep` outcomes exit 0, including the shortlist,
and `--json` goes to stdout.

135 deterministic unit tests, at least one unreadable-or-malformed-input refusal per verb.

Fixes #4725

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview deployed

  • web — Stage pr-4731 torn down.

@usirin

usirin commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

review-code: FAIL @ d42e411

Gated against issue #4725's eleven acceptance criteria. Class: has-code (single namespace — every changed path is under packages/fabrika-cli/ plus pnpm-lock.yaml). Not control plane; ordinary auto-merge lane. Every file under review was sourced from the PR head, never from the launch tree.

Ten of eleven criteria pass, the implementation is strong, and the failure is cheap to repair — but it is a real failure, not a nit, because the criterion it fails is the one carrying this PR's second deliverable into the other eighteen specs.


The two blockers

1. AC 11 — the spec defects were documented, but never filed

Any ambiguity or unbuildable clause found in contract.md was filed as a defect against the spec, not worked around silently

The contract-sufficiency record (#4725 comment 5155527469) names thirteen gaps. Nothing was worked around silently — that half is discharged well. But the positive clause is unmet: no issue exists against the spec. I listed every issue created since 2026-08-01T21:00Z; the only new ones are #4726, #4727, #4730, #4732, #4733, and none is a contract.md defect (#4733 is a founder-directed IO-idiom issue, not a spec defect).

That is material, not procedural. The record's own headline finding — adr sweep's scoring function is never specified, so any two implementers ship a different verb — comes with a concrete structural fix it proposes for cli-interface-convention.md:

7. Every value an example prints is derivable from the spec.

Eighteen more skills will be specified in this format. That rule currently lives in a comment on an issue that closes when this PR merges, and nobody is routed to make the change. CLAUDE.md's own rule — a follow-up that lives only in the conversation is a follow-up that dies there — applies one step up: a comment on a closing issue is not a tracked defect.

Repair: file the defects against claude-plugins/fabrika/skills/adr/contract.md and claude-plugins/fabrika/docs/cli-interface-convention.md — at minimum (a) the sweep-scoring structural gap plus the proposed completeness rule 7, (b) the indeterminate / no-overlap wording fork, (c) the missing exit code for an unreadable --dir in next / resolve, (d) the lettered-id scan. Then link them from the record.

2. The one-line-diff assertion is true by construction, and two artifacts claim it is not

packages/fabrika-cli/src/adr/status-line.ts:116-133:

const rewritten = [...lines];
rewritten[index] = `status: ${statusAfter}`;
const diff = diffBeyondStatusLine(lines, rewritten, index);

Both operands are the same array with exactly one index replaced, so diffBeyondStatusLine can only return null here. MultiLineDiff — and therefore exit 6 — is unreachable from runRelate. No test drives runRelate to exit 6; relate-verb.unit.test.ts:173-177 asserts only that the constant MULTI_LINE_DIFF equals 6.

That much is defensible on its own — a defensive invariant assert is a regression trap, and being unreachable today is what "invariant" means. The defect is that two artifacts state the opposite as a load-bearing claim:

  • status-line.ts:107-114"compares operands from two independent origins — the original text as read, and the text as re-joined after the edit — so it can genuinely fail: a rewrite that dropped a line, normalised a line ending, or lost the trailing newline shows up as a diff count other than 1"
  • The PR body — "Unit-tested with a genuinely two-line diff and with a dropped line, so the assertion can actually fail rather than being true by construction."

Neither holds at the call site. The unit tests exercise diffBeyondStatusLine directly on hand-built arrays; the call site compares an array to a copy of itself.

And the docblock's own named example is a genuine blind spot: rewriteStatus splits on /\r?\n/ and re-joins with a single newline chosen by text.includes("\r\n"), so a mixed-ending file has every line ending rewritten — and the guard, comparing post-split arrays, cannot see it.

This is the exact defect class this PR's body cites as the thing it was built against — a check that cannot see what it is looking for — asserted in defense against that class. Recorded here because a comment that overstates a guard is worse than no comment: it stops the next reader from checking.

Repair (either): correct both claims to say the assert is a construction-time invariant trap; or make it true by diffing the original text against the re-joined text (which also closes the line-ending blind spot) and add a runRelate test that reaches exit 6.


Criterion table

Criterion Evidence
PASS package in the pipeline-cli idiom, fabrika-cli binary, Effect CLI, "type": "module", pure core + thin bin, build/typecheck/test/cli packages/fabrika-cli/package.json; tsc -p tsconfig.build.json, tsgo and vitest run all clean at this head
PASS six verbs under adr, matching inputs/flags/output/exit table/error taxonomy all six exercised against the built binary. adr resolve 0164 and adr resolve 0023 came out byte-identical to the contract's worked examples. adr supersede 0126 --by 9999 printed the contract's exact string at exit 4
PASS adr sweep native, no pipeline-cli invocation anywhere in the package searched src/, package.json, both tsconfigs, vitest.config.ts, README.md: one hit, README.md:16, the paragraph stating the ban. No import, no subprocess. The isolation rule holds
PASS sweep exits 0 on all three outcomes; --json on stdout runSweep routes all three through answer (code 0); a live adr sweep --new 0238 --limit 3 printed a shortlist at exit 0. Tests sweep-verb.unit.test.ts:27,47,56. Both #4723 scars designed out
PASS supersede/amend-in-part refuse a diff beyond the status line, unit-tested diffBeyondStatusLine is tested with a genuine second-line edit and with a dropped line (status-line.unit.test.ts:61-70). Passing — blocker 2 is about the claims made about this guard, which is a separate defect
PASS amend-in-part appends in id order, multi-link list preserved status-line.unit.test.ts:16-29; relate-verb.unit.test.ts:112-121 asserts ADR 0023's three links survive, and an out-of-order append reorders by id
PASS per-verb <name>.unit.test.ts covering success, documented errors, zero scope 12 files, 135 tests, 135 passed. Zero-scope refusals (ADR 0092) tested for next/resolve/sweep; every write-refusal also asserts io.written.size === 0
PASS README.md present; every dep catalog:/workspace: both guards green at head. pnpm-workspace.yaml is correctly not in the diff — every dep the package declares already had a catalog entry on main, so no new entry was needed
PASS --help one-liners for group/verb/flag; verb index derived from the registry src/adr/command.ts — every flag, argument, verb and the group carries a description; src/registry.ts is the derivation seam
PASS every fabrika-cli fence in the skill runs against the built binary all seven invocations in claude-plugins/fabrika/skills/adr/SKILL.md exercised against dist/bin.js
FAIL spec ambiguities filed as a defect against the spec blocker 1

Conjunctive: one FAIL means the gate fails.


The record, gated as a deliverable

I sampled four of its claims against the spec and the diff independently rather than taking them on trust. All four hold.

1. adr sweep's scoring is genuinely unspecified — confirmed, and this is the most important finding in the PR. The entire ranking specification in contract.md is the verb-inventory cell ("deterministic — scan, score, sort") plus one Grounding line ("a lexical/rarity score over decision-bearing text, capped at 8, excluding the subject's own citations"). Not given: what decision-bearing text is, the tokenizer, stopwords, the weighting, the rarity denominator, the tie-break, the rounding. The claim that the spec's 15.11 / 13.46 are unreproducible by construction reproduces independently — my own run against the live corpus scored 93.77 / 78.92 / 75.01. Not a near miss; a different function. Correctly weighted as the headline, and the proposed rule 7 is the right shape of fix.

2. Lettered ids — true, with one over-claim. .decisions/0034a-live-fan-out-options-considered.md is on main (contents API at ref=main). But "this makes the spec-as-written unrunnable" is contingent on the author's own invented candidate/non-record distinction (their gap #7): under a purely literal reading a non-matching filename is simply not a record, and adr next would ignore 0034a rather than refuse. The record discloses gap #7 immediately after, so it is not concealing the contingency — but #6's framing ("a spec bug, not a preference") is firmer than the evidence carries. A calibration note, not dishonesty.

3. No exit code for an unreadable --dir in next/resolve — true. Both exit tables are 0/1/3/4/5; 3 is an unfetchable base and 5 is read and empty. Nothing covers read failed. Routed to 1 at next-verb.ts:54-56, and the self-criticism is correct: that seats a proven refusal on the code reserved for a failure to invoke, against the convention the spec inherits (#4208/#4219). Least-bad given the codes available — and flagging it was the right call over silently minting a new code.

4. The indeterminate / no-overlap fork — the shipped branch is the one the spec specifies. SKILL.md:56-59 defines no-overlap as "nothing mechanically adjacent was left to open", explicitly covering an ADR "sharing no distinctive vocabulary"; indeterminate is "your draft yielded no distinctive terms." The shipped mapping — empty idf set to indeterminate, empty scored list to no-overlap — matches. The flip was correct.

Verdict on the record: honest, specific, falsifiable, and its unflattering half is the load-bearing half. It earns its place as a deliverable. Its one gap is the guard-honesty defect in blocker 2, which it does not mention and which the PR body inverts.


Checks run at this head, not cited from a gate

  • pnpm -C packages/fabrika-cli typecheck (tsgo) — clean
  • pnpm -C packages/fabrika-cli build (tsc -p tsconfig.build.json) — clean
  • pnpm -C packages/fabrika-cli test12 files, 135 tests, 135 passed
  • biome check packages/fabrika-cli — 40 files, no diagnostics
  • All six verbs run end-to-end against the built binary (the live corpus plus a scratch fixture)
  • CI at head: green. catalog-guard, readme-guard, packages unit tests and produce run-evidence bundle all success; the skipped runs are change-detection, not failures

On the refusal tests specifically — the thing this campaign keeps finding broken — they are real. next-verb.unit.test.ts:68 and :77 drive a gh that exits 0 with the wrong bytes and assert the proven refusal code together with stdout === ""; io/github.unit.test.ts:20,67 cover the parsers directly. verb.ts's refuse makes a non-empty stdout on a non-zero exit structurally unrepresentable. The one exception is exit 6 — blocker 2.


Disclosure: this gate ran as a subagent of the same top-level session that produced the implementation (session fcd74bd3-8872-4023-b100-a81ac870eb43). It carried none of the implementation's reasoning in: the spec was read from main first-hand, every check was re-run, each of the four author claims was re-derived from the sources, and the verdict is a FAIL. Flagged so a reader can weight it.

…e one-line-diff guard falsifiable (#4725)

Repair round 1 on the review-code FAIL.

IO through Effect Platform, never raw `node:*`
----------------------------------------------
`src/io/fs.ts` hand-rolled a `FileSystemLike` over `node:fs` and `src/io/exec.ts`
used `execFileSync`, while `run.ts` already provided `NodeServices.layer` — the
package provisioned the platform layer and then reached around it, rebuilding the
service abstraction and test-substitutability the platform ships.

Grounded in `.patterns/effect-platform-access.md` (v4: `FileSystem`/`Path` from
`effect`, Node impls via `NodeServices.layer` — not the v3 `NodeContext` names) and
`.patterns/effect-process-cli-shell.md` (v4: `ChildProcess`/`ChildProcessSpawner`
from `effect/unstable/process` — not `@effect/platform`'s `Command`).

The architecture is unchanged: a verb stays a pure function of its dependencies
computing a `VerbOutcome`; only what it depends on moved. `fakeFs`/`fakeExec` are
replaced by substituted platform layers (`FileSystem.layerNoop`, a canned
`ChildProcessSpawner`), so the seam a test replaces is the seam production uses.

The unreadable-vs-empty discrimination is STRENGTHENED, not lost: the `null`
sentinel becomes a typed `ReadFailed`/`WriteFailed` on the `E` channel, so a caller
cannot reach the value without deciding what to do with the failure. `adr new`'s
existence probe gains a refusal it did not have — `existsSync` reported an
unreadable parent as "absent", which licensed a write over a record it never looked
at.

The one-line-diff assert was true by construction
-------------------------------------------------
`diffBeyondStatusLine` compared `lines` against a copy of `lines` with one index
replaced, so `MultiLineDiff`/exit 6 was unreachable from `runRelate` while the
docblock and the PR body both claimed the opposite. Made it real rather than
correcting the claim down: it now takes the two TEXTS and compares them line for
line INCLUDING each terminator, which closes the blind spot the docblock itself
named — `rewriteStatus` splits on the newline regex and re-joins with one chosen
newline, so a mixed-ending file had every line's terminator rewritten invisibly. A
`runRelate` test drives exit 6 through that path and asserts nothing was written.

139 tests pass; typecheck, build and biome clean; all six verbs re-run end to end
against the built binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@usirin

usirin commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

review-code: FAIL @ 27c6b86 — not merge-ready

Round-2 gate against issue #4725's eleven acceptance criteria, at the new head. Class: has-code, single namespace — every changed path is under packages/fabrika-cli/ plus pnpm-lock.yaml. §CP: CONTROL_PLANE_TOUCHED='', GUARD_TOUCHING='', 42 files scanned, 0 ADRs content-probed — ordinary auto-merge lane, no approval gate. Every file under review was sourced from the PR head via the per-run ref and throwaway review tree, never from the launch tree.

All four round-1 items landed clean, and both prior blockers are genuinely closed. AC 11 now passes. One blocker remains, and it is new to this round because the round-1 gate did not run the check: the glossary-freshness sub-gate fires on this PR and is unmet. It is a one-row repair.


The remaining blocker — glossary-freshness (Step 3c)

glossary-freshness: scanned new surfaces ⇒ packages/fabrika-cli
glossary-freshness: FAIL — new surface added but .glossary/TERMS.md untouched (§ZS relevant-but-zero-match)

packages/fabrika-cli is a new public package — the second detector's exact surface — and .glossary/TERMS.md is untouched by the diff. I checked the register on main rather than trusting the detector: it already carries fabrika, derived CLI contract (contract spec), authoring brief, eval case, eval set and incident corpus, so the campaign has been naming its concepts as it ships them. fabrika-cli appears zero times. The one noun that every fabrika skill's fences invoke, and that eighteen more derived contracts will be implemented into, is the one that never entered the vocabulary register — and it is already being reached for under four different phrasings across the board ("the verb package", "the first verb package", "the seed package", "fabrika-cli"). That is precisely the one-concept-named-four-ways drift this gate exists to stop, and the argument is the same one this PR's own round-1 blocker rested on: a convention that lands only in code does not reach the seventeen sessions that come after.

Repair: add a fabrika-cli row to .glossary/TERMS.md — what the package is (fabrika's deterministic verb package, packages/fabrika-cli/, binary fabrika-cli, verbs grouped per skill), and the distinction that earns it a row (not pipeline-cli — ADR 0238 makes it a copied idiom, never a dependency; not the derived CLI contract — that is the spec, this is the implementation). One row, in this PR.

Also worth routing separately (does not block this PR): the extracted gate script errored on its third detector while producing the verdict above —

awk: nonterminated character class

review-code/scripts/glossary-freshness.sh:56 escapes / inside an awk ERE character class, so the new public export detector silently matched nothing on this run. Detector 2 had already fired, so the outcome here is sound — but on a PR whose only new surface is an added export from an existing package, that detector is a silent no-op and the gate would read green. Not this PR's defect; naming it because it was observed here.


The four repairs — verified, one at a time

Item 1 — IO through the Effect platform: landed, and the load-bearing property is stronger

The pattern-doc claims check out against the docs, not just against the body. I read both from main:

  • .patterns/effect-platform-access.md — v4: FileSystem / Path from the effect package, Node impls via NodeServices.layer. src/io/fs.ts:14 is import {Effect, FileSystem, Path} from "effect"; src/run.ts:11,24 provides the one NodeServices.layer. Correct v4 idiom; no NodeContext.layer / NodeFileSystem.layer / @effect/platform import path anywhere.
  • .patterns/effect-process-cli-shell.mdsrc/io/exec.ts:40-65 is the doc's canonical shape nearly line for line: ChildProcess.make, all three channels read concurrently, branch on the exit code, Effect.scoped, PlatformError caught and folded.
  • The named gotcha specifically. The doc: "ChildProcessSpawner.string hides the exit code — spawn the handle directly." exec.ts never calls .string; it builds the handle and reads handle.exitCode itself. Followed.

A search for node: across packages/fabrika-cli/src/ at this head returns one hit — io/fs.ts:52, a prose mention of existsSync inside a comment explaining why the probe now fails closed. Zero imports. Corroborated across 36 files.

The property that must not have been lost — it wasn't, and it is now unrepresentable to lose. The old seam's null meant "I could not read this"; unreadable and empty take opposite branches. It is now a typed E: ReadFailed / WriteFailed (io/fs.ts:18-27), raised by readDir, readFile, exists and writeFile off Effect.catchTag("PlatformError", …). I searched the whole non-test source for anything that could swallow it — orElseSucceed, catchAll, Effect.option, Effect.either, orElse, ignore. Nothing catches ReadFailed or WriteFailed anywhere. The only orElseSucceed in the package is exec.ts:36, on stream text decoding, which is the pattern doc's own collect. A conversion that swallowed a platform error into a default or an empty array is not present.

The added test exists and genuinely asserts refusal. src/adr/new-verb.unit.test.ts:71-77:

it("refuses an existence probe that FAILED, rather than reading it as 'absent' and writing", async () => {
    const fs = fakeFs({unprobeable: [".decisions/0240-only-landed-adrs-may-be-cited.md"]});
    const out = await run(fs);
    expect(out.code).toBe(1);
    expect(out.stdout).toBe("");
    expect(fs.written.size).toBe(0);
});

unprobeable is a distinct axis from files in the fake (fakes.test-support.ts:34-35,63-66) — it makes fs.exists fail with a PlatformError, which is not the same input as a path that is absent. The verb's call site (new-verb.ts:46-51) reads the probe through Effect.result and refuses on Result.isFailure, with the invariant stated at its enforcement site. The assertion covers all three things that matter: the proven code, empty stdout, and nothing written. Real test, real refusal.

verb.ts's refuse still makes a non-empty stdout on a non-zero exit structurally unrepresentableverb.ts:51-55 returns {code, stdout: "", stderr: […]} with stdout a literal, not a parameter. There is no argument a caller could pass to seat bytes on stdout beside a refusal. Confirmed by running it: every live refusal below printed 0 bytes.

Test coverage did not thin in the swap. I counted per-file across the two heads rather than trusting the total:

file prior head this head
new-verb.unit.test.ts 7 8
relate-verb.unit.test.ts 17 18
status-line.unit.test.ts 18 19
io/git.unit.test.ts 8 9
the other 8 files 96 96

No file lost a test; four gained one. 135 → 139. And the substitution is the stronger kind: fakeFs is now built on FileSystem.layerNoop and fakeShell on ChildProcessSpawner.make + makeHandle, so the seam a test replaces is the same platform service production resolves — not a hand-rolled interface only the tests know about. (The helper names fakeFs/fakeShell survive; what changed underneath them is the substance.)

The disclosed judgment call — keeping Attempt<A> in io/git.ts / io/github.ts — is principled, not a convenient stopping point. Three reasons, in order of weight:

  1. Attempt was never the sentinel. It is a tagged sum (Ok / Failure, git.ts:19-24) that a caller cannot reach the value past without discriminating _tag — structurally the same forcing function the E channel gives. The things that actually were null-shaped sentinels are exactly the ones that moved. The stated rule — the E-channel change was made only where the sentinel actually was — is the correct rule, and it was applied correctly.
  2. There is a positive reason to keep it. Shell<A> has E = never, so each Failure is mapped at its call site onto a distinct proven exit code — 3 for an unfetchable base, 4 for an unknown in-flight set, 5 for zero scope. On a shared E channel those would have to be re-discriminated by tag anyway, and the mapping would sit further from the verb that owns it.
  3. It is on the record at the site (git.ts:10-13), with the reasoning, rather than being an unexamined leftover.

One soft spot, not a blocker. git.ts:58-66remotes returns [] when the remote listing fails, which is the empty-on-failure shape this package refuses everywhere else. It is contained: an empty remote list makes splitRemoteRef return null, which takes the wholesale-fetch branch, whose own failure refuses (git.ts:91), and the resolved SHA is still shape-checked (git.ts:100-102). So it cannot produce a false answer today. Worth a follow-up, not a repair round.

Item 2 — the spec defects are filed, and rule 7 is not a footnote

Four issues exist, all open, all status:needs-triage, all created between 2026-08-02T05:39Z and 05:41Z, all filed against the spec rather than the implementation:

issue carries
#4735 sweep's unspecified scoring · the proposed completeness rule 7 · the indeterminate/no-overlap fork · the rarity-floor population · the unspecified reason string (record gaps 1–4)
#4736 no exit code for an unreadable --dir in next/resolve (gap 8)
#4737 what counts as a record — the lettered 0034a and the unreachable unparseable id error (gaps 6, 7)
#4738 six under-determined prose details (gaps 5, 9–13)

Rule 7 is §2 of #4735, under its own heading ("The structural fix — a seventh completeness rule"), quoted verbatim, and closed with "This is the load-bearing half of this issue: the rule is what stops the same gap reaching the other eighteen specs." It is filed against cli-interface-convention.md, which is the right target — the doc that governs every verb, not the one spec that surfaced the gap. That is the campaign-level deliverable landed as a tracked artifact, which is exactly what round 1 said was missing.

contract.md is still not patched. No claude-plugins/ path appears anywhere in the 42-file diff — verified structurally off the files endpoint, not from the body's claim.

Item 3 — the guard is real, and I drove it end to end

diffBeyondStatusLine (status-line.ts:95-108) now takes the two texts and compares them through linesWithEndings (:69-82), which retains each line's terminator — the byte a plain split discards. rewriteStatus (:159) calls it with the original text as read against the re-joined result, not an array against a copy of itself.

Reachability, proven by running the real binary rather than by reading the code. A mixed-ending record (LF frontmatter, CRLF body), adr supersede through the CLI:

EXIT=6
STDOUT_BYTES=0
adr supersede: rewrite would have changed 6 line(s) beyond status: — aborted, nothing written.
FILE: UNCHANGED

And the control, so this is a guard and not a brick — the same command on a uniform-LF record:

0126-ambient-adr-discovery.md	superseded by [0240](0240-only-landed-adrs-may-be-cited.md)
EXIT=0

The unit test that drives it through runRelate is relate-verb.unit.test.ts:177-195, asserting code === MULTI_LINE_DIFF, stdout === "", the stderr reason, and io.written.size === 0. Exactly what round 1 asked for.

The two artifacts no longer overstate. status-line.ts:134-143 now says the assertion runs on the original text against the re-joined result and names the mixed-ending file as the reachable case — which I have just reached. The PR body's bullet says "Reachable from runRelate, and driven there by a test", and flags itself as a repair-round-1 correction. Both claims are now true, and the round-1 defect — two artifacts asserting a falsifiability the code did not have — is gone.

Item 4 — the over-claim, amended not rewritten

The contract-sufficiency record (#4725 comment 5155527469) has created_at == updated_at == 2026-08-02T05:04:54Zunedited — and still carries its original words at line 114 ("Lettered ids are unhandled, and this makes the spec-as-written unrunnable") and line 120 ("the clearest case of 'the spec is the bug'").

The correction is a separate, later comment (5155713401, 05:41:43Z) that opens "Appended rather than edited into the record above, so the original claim and its correction both stand", states plainly that gap #6's conclusion is contingent on the record's own invented gap-#7 distinction, and gives the calibrated replacement. Both stand. Amend, never rewrite — honored exactly.


Standing constraints — re-verified at this head, not assumed

  • No pipeline-cli tether. Searched packages/fabrika-cli/ across *.ts, *.json, *.md: one hit, README.md:16, the paragraph stating the ban. No import, no subprocess, no dependency entry. ADR 0238 holds through a refactor that touched every verb signature.
  • Every dep catalog:/workspace:; README.md present. catalog-guard checkall deps in 29 workspace manifests are on catalog:/workspace:. readme-guard checkall 25 packages/* workspace members carry a README.md. Both run here, not cited from CI.
  • Stdout carries the answer only; a non-zero exit prints nothing permissive. Live, four refusal paths, stdout measured in bytes each time: unfetchable base → exit 3, 0 bytes; unknown repo → exit 4, 0 bytes; unreadable corpus → exit 3, 0 bytes; mixed-ending rewrite → exit 6, 0 bytes. Structurally guaranteed by verb.ts:51-55.
  • --base fetched before read. git.ts:85-103 — fetch, then resolve the base to a commit, then a shape check that the result is an object name; a fetch failure refuses rather than falling back to a stale local ref.
  • REST only, never GraphQL; paginated on every list read. github.ts:79-85 and :100-106 are the only two GitHub reads, both gh api --paginate … per_page=100. No GraphQL path in the package.
  • Every verb keeps a refusal test asserting the proven code and stdout === "". Count of expect(out.stdout).toBe("") per verb file: next 6, resolve 4, sweep 4, new 3, relate 3.
  • The unreadable-corpus refusal fired for real during this review. adr sweep pointed at the review tree's .decisions — which this gate's own instruction denylist removes — came back exit 3, 0 bytes, corpus unreadable, rather than no-overlap. An unplanned live confirmation of the exact property this package is built around.

Criterion table

Criterion Evidence
PASS package in the pipeline-cli idiom, fabrika-cli binary, Effect CLI, "type": "module", pure core + thin bin, build/typecheck/test/cli package.json; tsgo, the build and vitest run all clean at this head. The pure-core/thin-bin split survived the IO conversion — a verb now returns an Effect of its VerbOutcome, still computing and never writing a stream
PASS six verbs under adr, matching inputs/flags/output/exit table/error taxonomy all six re-run at this head. adr resolve 0164 0023 is byte-identical to the contract's worked example; adr next --json{"id":"0239","mergedMax":"0238","inFlight":["0223","0235","0237"],…}
PASS adr sweep native, no pipeline-cli invocation anywhere in the package one mention in the whole package: README.md:16, the ban paragraph
PASS sweep exits 0 on all three outcomes; --json on stdout live: a 3-entry shortlist over the 235-record corpus at exit 0; the --json payload on stdout with the scope line alone on stderr; indeterminate on the small-corpus fixture at exit 0. Both #4723 scars designed out
PASS supersede/amend-in-part refuse a diff beyond the status line, unit-tested now genuinely falsifiable and driven to exit 6 through runRelate — item 3 above
PASS amend-in-part appends in id order, multi-link list preserved status-line.ts:44-59; relate-verb.unit.test.ts asserts ADR 0023's three links survive and an out-of-order append reorders by id. Live adr resolve 0023 still prints all three
PASS per-verb <name>.unit.test.ts covering success, documented errors, zero scope 12 files, 139 tests, 139 passed. Per-file counts confirm no file lost coverage in the IO conversion
PASS README.md present; every dep catalog:/workspace: both guards run here, both green
PASS --help one-liners for group/verb/flag; verb index derived from the registry src/adr/command.ts — every flag, argument, verb and the group carries a description; src/registry.ts is the derivation seam
PASS every fabrika-cli fence in the skill runs against the built binary the six verbs exercised end to end at this head
PASS spec ambiguities filed as a defect against the spec #4735 / #4736 / #4737 / #4738 — item 2 above. The round-1 blocker is discharged
FAIL glossary-freshness (standing sub-gate, Step 3c) new public package packages/fabrika-cli, .glossary/TERMS.md untouched, fabrika-cli absent from the register on main

Conjunctive: one FAIL means the gate fails.

Sub-gates that did not fire, stated rather than omitted: containment marker none → flag-gating not applicable. session-caching gate: not applicable — 3 keyword candidates, all prose false positives, no session-caching path. unresolved-threads: not applicable — the review-thread read ran and returned no threads. comment-discipline: PASS — 556 comment lines over 3610 source lines, sampled against the deslop-comments rubric; the standalone notes are KEEP-category (a local invariant at its enforcement site, a workaround plus its forcing constraint, a deliberate-looking-wrong guard), with no narration, name-restatement or ADR re-derivation slop. deviation-disclosure: PASS — the body's ## Deviations discloses all four departures, including the one that matters (the IO conversion being wider than the verdict asked for), and nothing in the review was declined.


Checks run at this head, not cited from a gate

  • pnpm -C packages/fabrika-cli typecheck (tsgo) — clean
  • pnpm -C packages/fabrika-cli build — clean
  • pnpm -C packages/fabrika-cli test12 files, 139 tests, 139 passed (was 135)
  • biome check packages/fabrika-cli — 40 files, no diagnostics
  • catalog-guard check, readme-guard check — both green
  • all six verbs end to end against the live 235-record corpus plus scratch fixtures, including four refusal paths and the exit-6 guard
  • CI at head: green — 41 success, 4 skipped (change-detection), 0 failures. ci-required success

Run-evidence bundle: PRESENT for head 27c6b866 — producer run 30734689151, artifact 8829186896, manifest.commit == head, schemaVersion 1; checks 2/2 pass; tests 2420/2420 passed, 0 failed, 0 skipped.

Read the PR head (§HEAD): all files under review sourced from 27c6b866111dbfe84dc5983f421a98b433d89379 via the per-run ref and throwaway review tree, never the launched checkout's working copy.


One criterion fails. The PR stays open and unmerged; #4725 stays open and assigned. The failing row is a single .glossary/TERMS.md addition — every other criterion, both round-1 blockers, the founder's IO directive and every standing constraint pass at this head. Re-request review once the glossary row lands.

Disclosure: this gate ran as a subagent of the same top-level session that produced the implementation. It carried none of the implementation's reasoning in: the two pattern docs were read from main first-hand, every check was re-run here, the exit-6 guard was driven through the real binary rather than read, and the per-file test counts were compared across the two heads rather than taken from the total. Flagged so a reader can weight it.

Verdict-written: 2026-08-02T06:03:51Z

The deterministic verb package the fabrika skills call had no row in the
vocabulary register while the board already reached for it under four
phrasings — the one-concept-named-four-ways drift .glossary exists to stop,
and this package is the target eighteen further derived contracts will name.

The row's load-bearing half is the isolation: fabrika-cli calls pipeline-cli
nowhere by construction (ADR 0238), which is the whole reason it is a
separate package rather than a group in the v1 router.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@usirin

usirin commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

review-code: PASS @ 762c64e — merge-ready

Round-3 gate against issue #4725's eleven acceptance criteria, at head 762c64e3. Class: has-code, single namespace — I re-derived it with pipeline-cli class-probe classify --namespaces over the live 43-file set (class-probe: 43 changed file(s) → has-codereview-code), specifically because a .glossary/** path entered the diff this round and a new path class can pull in a second required gate. It did not: .glossary/** rides HAS_CODE_RE, and the docs carve-out excludes both .glossary/ and packages/, so review-code remains the one required namespace. cp-classify classifynot-control-plane [path-clear-no-content-source] — proven ordinary, no §CP gate, ordinary auto-merge lane. No path under apps/web/src/, so review-design is not dispatched.

Every file under review was sourced from the PR head via a per-run ref and a throwaway review worktree — never the launch tree (§HEAD).


1. The delta is exactly one line, and I verified it rather than accepting it

The stat between the round-2 head and this one is .glossary/TERMS.md | 1 + — one file changed, one insertion. Nothing else in the tree moved. Three corroborations, because a stat alone does not rule out a rewritten history:

  • No rebase. An ancestry test of 27c6b866 against 762c64e3 returns true. The prior head is a genuine ancestor, so the round-2 review's tree is this tree minus one line, not a re-landed approximation of it.
  • One new commit, 762c64e3 docs(glossary): name fabrika-cli in TERMS.md (#4725).
  • Still level with main. The merge base of the head and origin/main equals origin/main itself, 61c17a90. The body's "no rebase was taken" claim is true and the branch is current.

Because every packages/fabrika-cli/** byte is identical to the head the round-2 gate walked criterion by criterion, I did not re-derive that eleven-criterion walk from scratch. I did independently re-verify the standing hard constraints and re-run every check at this head — below.

2. glossary-freshness — the round-2 blocker, cleared on substance

glossary-freshness: scanned new surfaces ⇒ packages/fabrika-cli
glossary-freshness: PASS — new surface ships with a .glossary/TERMS.md touch

The gate fired for real rather than going quiet: detector 2 still names packages/fabrika-cli as the new surface, so this is a positive match against a non-empty scope, not an empty scan reading green. (Detector 3 still errors with awk: nonterminated character class, which is #4700 — already filed with an independent reproduction. Not this PR's defect, no duplicate filed, and it does not weaken this outcome: detector 2 carried the match.)

I checked the row, not just the detector's silence.

  • It says what fabrika-cli is. The deterministic verb package fabrika's skills call, packages/fabrika-cli/, binary fabrika-cli, invoked fabrika-cli <group> <verb> …, one registered group per skill with adr first — and why it exists: the deterministic half of fabrika's two-layer split, where what a skill would otherwise re-derive per session becomes a verb and the skill fence is one invocation of it. That is the concept, not a path restatement.
  • It carries the non-obvious property, and the property is true. The row states, in bold, that it calls pipeline-cli nowhere — no import, no subprocess (ADR 0238), and then the reason a reader needs in order not to misuse the term: a fabrika tethered to the pipeline it exists to replace can never replace it, so v1 is a reference for semantics and scars, never a dependency. I verified that claim at this head rather than trusting it — a recursive search for pipeline-cli across packages/fabrika-cli/ returns exactly one hit, README.md:16, the paragraph that states the ban. Zero imports, zero subprocesses.
  • Shape matches the file. Three columns (Term | Definition | Not), same as the header at line 19 and every neighbour. The Not column does the disambiguation work the file's own preamble asks of it, and does it against the three terms actually confusable here: pipeline-cli (the frozen baseline, never an upstream), the derived CLI contract (that is the spec; this is the implementation built to it), and a general-purpose CLI. Tone matches the neighbouring fabrika rows — dense, issue-cited, "not" clauses that name a real drift.
  • Both links resolve at this head: ../.decisions/0238-fabrika-reimplements-v1-never-calls-it.md and ../packages/fabrika-cli/.

On the placement, which the author flagged as a judgment call: it is the better of the two options and I agree with it. The row went after derived CLI contract (contract spec) rather than beside fabrika. A name-prefix match would have put it one row earlier — but this block is demonstrably not alphabetical (authoring brief, fabrika, derived CLI contract, … — d after f breaks it), so there is no ordering invariant to violate. What the block does follow is concept dependency, and under that rule the spec and the implementation built to it now read adjacently, which is exactly where the Not column's second clause ("that is the per-skill spec; this is the implementation built to it") wants a reader's eye to already be. Placing it beside fabrika on a name match would have separated the two terms that most need reading together. Sound call.

3. Nothing regressed — re-run at this head, not inherited

A glossary row should not move 139 tests, and it did not. Every one of these ran against the review worktree at 762c64e3:

Check Result
pnpm -C packages/fabrika-cli typecheck clean, exit 0
pnpm -C packages/fabrika-cli build clean, exit 0
pnpm -C packages/fabrika-cli test 12 files, 139 tests, 139 passed
root pnpm typecheck (turbo) 30/30 successful, @kampus/fabrika-cli:typecheck in the graph
root pnpm lint (biome) exit 0, 2009 files, 9 warnings + 1 info — zero of them in packages/fabrika-cli
pipeline-cli catalog-guard check all deps in 29 workspace manifests are on catalog:/workspace:
pipeline-cli readme-guard check all 25 packages/* workspace members carry a README.md

The two guards are non-vacuous here: pnpm ls -r confirms @kampus/fabrika-cli@0.1.0 is a real workspace member at packages/fabrika-cli, so it is inside both guards' scope rather than passing them by absence (ADR 0092).

CI at head: every check-run on 762c64e3 is successci-required, lint / format / typecheck, packages unit tests, unit + client tests, integration tests, e2e (reads + authed + flows, blocking), CodeQL, the commit secret scan, and each guard job. The only two non-successes are skipped by design (lint workflow YAML, report no preview deploy).

4. The standing hard constraints, re-verified at this head

  • No tether (ADR 0238). One pipeline-cli occurrence in the whole package: README.md:16, the ban paragraph. No import, no subprocess. (AC 3)
  • Every dep catalog:. package.json@effect/platform-node, effect, @effect/tsgo, @types/node, @typescript/native-preview, typescript, vitest — all catalog:, confirmed independently by catalog-guard. (AC 8)
  • stdout carries the answer only. verb.ts:51-55's refuse() returns {code, stdout: "", …} with stdout a literal, not a parameter — a non-empty stdout beside a non-zero exit is structurally unrepresentable, not merely avoided. (AC 4, AC 7)
  • --base is fetched before it is read. io/git.ts's fetchAndResolve fetches, then resolves with rev-parse --verify --quiet <base>^{commit}, then shape-checks the result with isObjectName — so a resolution answering something that is not an object name is a refusal, never a permissive default. Every downstream read takes the resolved SHA, never a ref name, so nothing silently re-resolves mid-run. (AC 2)
  • REST only, --paginate on list reads. io/github.ts:80-81 and :101-102 are the only two GitHub calls; both are api + --paginate. Zero graphql in the package. (AC 2)
  • Every verb keeps a refusal test asserting the proven exit code AND stdout === "". 20 such assertions across the five verb test files (next-verb 6, resolve-verb 4, sweep-verb 4, new-verb 3, relate-verb 3), each pairing a named proven code (BASE_UNFETCHABLE, IN_FLIGHT_UNKNOWN, ZERO_SCOPE, …) with expect(out.stdout).toBe(""). (AC 5, AC 7)
  • The adr-sweep shortlist --json writes its payload to stderr, not stdout #4723 scars stay designed out. sweep-verb.ts returns answer(...) on both the --json and the rendered path — so all three outcomes, shortlist included, exit 0 — and the JSON payload is the answer's stdout while the scope line and the no-overlap / indeterminate reason go to stderr. (AC 4)
  • The one-line-diff assertion is reachable and armed. status-line.ts:159rewriteStatus computes diffBeyondStatusLine(text, rewrittenText, index) after the rewrite and returns MultiLineDiff (nothing written) on any hit, with the mixed-line-ending case named at its enforcement site. Driven by status-line.unit.test.ts:52-92. (AC 5)
  • --help is registry-derived. registry.tsregisteredGroups is the single array the root command and its --help index both read; a group appears under --help by being registered and nowhere else. (AC 9)
  • Out-of-scope surfaces still untouched. The changed-path set against origin/main, minus packages/fabrika-cli/, .glossary/ and pnpm-lock.yaml, is empty. claude-plugins/ has a zero-line diff, so claude-plugins/fabrika/skills/adr/contract.md is unpatched and stays owned by spec defect: adr sweep's scoring is unspecified — the contract prints example scores it cannot derive #4735spec defect: six under-determined details in contract.md that each forced an invented answer #4738 — all four of which I confirmed open and real, as is review-code's glossary-freshness detector never compiles — its awk regex is malformed, so the check reads as a clean skip #4700. (AC 11)

The per-criterion table

  • [PASS] AC 1 — packages/fabrika-cli/ exists with bin.fabrika-cli, "type": "module", build/typecheck/test/cli scripts, pure core + thin src/bin.ts, in the packages/pipeline-cli/ idiom — verified at head, all four scripts run clean
  • [PASS] AC 2 — all six verbs under the adr group (next, new, resolve, supersede, amend-in-part, sweep), fetched-then-read base ref with a shape-checked SHA, REST + --paginate reads, reserved exit codes in verb.ts
  • [PASS] AC 3 — adr sweep native; one pipeline-cli string in the package and it is README.md:16's ban paragraph (ADR 0238)
  • [PASS] AC 4 — sweep returns answer(...) on all three outcomes ⇒ exit 0 including shortlist; --json on stdout, diagnostics on stderr (both adr-sweep shortlist --json writes its payload to stderr, not stdout #4723 scars)
  • [PASS] AC 5 — rewriteStatus runs diffBeyondStatusLine post-rewrite and refuses with MultiLineDiff, writing nothing; status-line.unit.test.ts:52-92 asserts it
  • [PASS] AC 6 — amend-in-part appends in id order, an existing multi-link list is preserved, and a re-add is a no-op that still exits 0 (relate-verb.unit.test.ts)
  • [PASS] AC 7 — 12 <name>.unit.test.ts files beside their source, 139 tests, success + documented-error + zero-scope paths, 20 refusal assertions pairing a proven code with empty stdout
  • [PASS] AC 8 — README.md present (readme-guard: 25/25) and every dep catalog: (catalog-guard: 29 manifests clean)
  • [PASS] AC 9 — the --help index is derived from registry.ts's registeredGroups, never hand-maintained
  • [PASS] AC 10 — every fabrika-cli fence in the skill runs against the built binary; build clean at head and the six verbs were exercised end to end
  • [PASS] AC 11 — spec gaps filed as defects against the spec, not worked around: spec defect: adr sweep's scoring is unspecified — the contract prints example scores it cannot derive #4735, spec defect: adr next/resolve have no exit code for an unreadable --dir, forcing a proven refusal onto exit 1 #4736, spec defect: contract.md never defines what counts as a record — the lettered id 0034a and the unreachable 'unparseable id' error #4737, spec defect: six under-determined details in contract.md that each forced an invented answer #4738 all open; contract.md byte-untouched
  • [PASS] glossary-freshness — new surface packages/fabrika-cli ships with its .glossary/TERMS.md row; row checked on substance (definition, the ADR-0238 no-call property verified true, 3-column shape, resolving links, coherent placement)
  • [PASS] comment-discipline — 3241 added lines scanned on comment-bearing files; the added docblocks are KEEP-category (module invariants at their enforcement site, the reserved-exit-code table in verb.ts, the any-in-registry.ts rationale, the fetch-before-read constraint in io/git.ts), with no narration, name-restatement or ADR re-derivation slop (deslop-comments rubric)
  • [PASS] deviation-disclosure — see below

Sub-gates that did not fire, stated rather than omitted: containment marker none → flag-gating not applicable. session-caching gate: not applicable — 3 keyword candidates, all prose false positives inside docblocks, no session-caching path. unresolved-threads: not applicable — the review-thread read ran (exit 0) and returned no threads.

The two disclosures from the repair lane — checked, not accepted

Amend-never-rewrite: honored. The ## Deviations section now carries a **(repair round 3)** block appended below the untouched **(repair round 1)** block. The round-2 verdict, posted against 27c6b866 when only the round-1 block existed, recorded that the body "discloses all four departures" — and all four are still present, with their original framing and in their original order. Nothing was rewritten in place; the new round is a labelled append.

Its content is accurate on both counts. The glossary-freshness.sh awk bug is recorded as declined and routed, not silently dropped — which is the correct disposition: it is a §CP surface this PR must not touch, and #4700 is open carrying an independent reproduction, so declining here is right and filing a duplicate would have been wrong. "Nothing else moved" and "the branch was already level with origin/main" both check out against the evidence in §1.

The un-released lane claim: noted, not blocking, and correctly surfaced rather than taken silently. The departure from the skill's R3 claim-release step leaves no debt on main, embodies nothing in the diff, and is a lane-hygiene matter for the dispatching session that owns the claim — not a code finding. It does not bear on this verdict.

Run-evidence bundle: PRESENT for head 762c64e3 — producer run 30735448231, artifact 8829448638, manifest.commit == head, schemaVersion 1; checks 2/2 pass; tests 2420/2420 passed, 0 failed, 0 skipped.

Read the PR head (§HEAD): all files under review sourced from 762c64e3d6cbc92631b39f708c8216be86bf0068 via the per-run ref and the throwaway review worktree, never the launched checkout's working copy.

All criteria pass. This PR is merge-ready. review-code does not mergeship-it is the authorized merge step; merging will auto-close #4725 via Fixes #4725.

Verdict-written: 2026-08-02T06:26:26Z

@usirin
usirin added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit 042e469 Aug 2, 2026
46 checks passed
@usirin
usirin deleted the usirin/fabrika-cli-adr-verbs-4725-4D0D5B4F branch August 2, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement packages/fabrika-cli — the six adr verbs from the wave-0 pilot's derived contract

1 participant