docs(decisions): 0250 — a fabrika hook whose verb cannot run fails open, loudly (#5079) - #5250
Conversation
…en, loudly (#5079) Records the founder ruling on #5079 and flips the one reversible policy point in the fabrika hook surface from INTERIM to RULED. The polarity: a hook whose verb never ran produced no evidence, so it may never deny. The teeth: silence is the banned state — the cannot-run state owes a visible degraded notice. Two things checked first-hand because the epic body carried a stale reading: #3743 is closed not_planned and was never ruled (guard.sh L32's header is stale), and the shared .git/hooks all-lanes blast radius does not reach fabrika's harness hooks, so Horn A loses its strongest support and survives on the bootstrap deadlock instead. Also records what the ruling does not buy: the degraded notice has no fabrika-side owner for the exit-127 state, because fabrika is what failed to resolve and the session-start seam is graded RETIRE.
No preview deploy
|
|
ADR number collision resolved (engine note, 2026-08-10). This PR and #5252 both authored themselves as ADR 0250 — a concurrent-author race on the next number, caught by live enumeration before CI. Resolution: #5250 keeps 0250. It opened first and was already at the gate. #5252 renumbered to 0251 (head No action needed here. Enumerate |
|
review-doc: advisory — blocking-set PR (§CP — approval-gated) This verdict is advisory only and authorizes no merge. §CP membership here is by CONTENT, not path (ADR 0164): Reviewed-head: @ 2cbfce9 Linked issue: #5079. Graded against the founder ruling (comment §CP re-derivation — both axes
The not-control-plane report from the coder was right on the path axis and incomplete: adding a Acceptance criteria — 7/7
The substance — is the fail-open reasoning sound?
The UNKNOWN-announced requirement — required, not merely permitted
The two candid claims from the coder — both verified
Closing-keyword audit — the closing set is exactly {#5079}Case-insensitive
Every other issue reference in the ADR is either a bare markdown link or carries the keyword after the ref ("which #4791 closes"), which does not trigger. #5251, #5078, #4927 and #3411 are NOT in the closing set. Clean. Doc-hygiene checklist
Step 4a — ADR contradiction sweep
CI at headAll check-runs at Merge-order hazard — a live ADR-0250 number collision (not a defect in this PR)Re-enumerated at review time across
Per ADR 0074, Verified read-only against the PR head through a per-run ref (§RO/§HEAD); no working tree was switched. The |
|
review-code: advisory — blocking-set PR (§CP — approval-gated) This verdict is advisory only and authorizes no merge. §CP by CONTENT (ADR 0164) — Reviewed-head: @ 2cbfce9 Linked issue: #5079 (graded against founder ruling Why this namespace is required at all
Regexes re-resolved live from Verification
Notes carried from the doc-class passFull acceptance-criteria verification (7/7), the substance judgment on the fail-open reasoning, the two verified coder claims, the doc-hygiene checklist, and the ADR contradiction sweep are in the Verified read-only against the PR head through a per-run ref (§RO/§HEAD); no working tree was switched. |
Fixes #5079
Records the founder ruling on the one founder-seat question inside epic #4927, and flips the single reversible policy point it owns.
What the ruling is
A fabrika hook whose verb cannot run fails OPEN — and the silence is what is banned. A verb that never started (a bare
fabrikaat exit127, or the cross-checkout refusal at exit2) produced no evidence, so it may never deny; but the cannot-run state owes a visible degraded notice. Fail-open-and-loud, never fail-open-and-forgotten.The boundary holds: a verb that runs and returns a deny still fails closed exactly as designed.
fabrika hook spawndenying an off-allowlist model is untouched.Why that polarity — the two families, stated explicitly
"Could not execute" resolves in opposite directions depending on which family the check belongs to, and this ADR names which one applies:
pipeline-cli checks read --expect greenis the canonical instance: exit1= read-OK-but-not-green (definite), exit2= UNREADABLE, deliberately distinct (packages/pipeline-cli/src/tools/checks/command.tsL19–22, ship-it Step 3 readsgh pr checks, which reports completed CI as in-progress #3999). Refusing costs one stalled PR.claude-plugins/pipeline-crew/PROBES.md; Agent liveness probes wrapping in uninstalledtimeoutfail closed → false 'API down' stalled EM conductor ~5h #3411, the ~5h false outage from atimeoutthat was not on PATH).The fabrika cannot-run state is the second family, and the discriminator is which thing failed. A verification guard that fails closed has run and is holding a decision it owns. A hook whose verb never started produced no evidence at all — a deny there asserts a violation nobody observed. So it may not deny; what does not follow is that it may pass, hence the third outcome: UNKNOWN, announced.
Two things checked first-hand, because the epic body carried a stale reading
closed,state_reasonnot_planned, closed 2026-08-01 — swept in the v1 backlog kill batch as fabrika-superseded.claude-plugins/kampus-pipeline/hooks/guard.shL32 still calls it "an open founder ruling"; that header is stale, and this is a fresh ruling against fabrika, not a revival. (guard.sh is v1 and frozen per ADR 0238, so this PR does not edit it.).git/hooksblast radius does not reach fabrika — Horn A loses its strongest support. The Harness worktree-agent spawns fail: post-checkout lefthook can't resolve sh in stripped PATH (blocks both lanes) #787/fix(lefthook): rc-restore PATH so post-checkout survives stripped-PATH worktree spawns (#787) #788/bootstrap-deps still aborts worktree creation on missing pnpm in stripped harness env (follow-up to #787) #789 all-lanes-down radius is a property of a git hook (.git/hooksshared across linked worktrees,git worktree addexec'ing with a stripped PATH —create-worktree.shL52–55). fabrika ships no git hook: zero.git/hookshits underclaude-plugins/fabrika/, and everything it declares inhooks.jsonis a per-session Claude Code harness hook. v1's ownguard.shcited that radius for what is likewise a harness hook, so the v1 rationale conflated the two surfaces. Horn A survives on different grounds instead — the bootstrap deadlock (failing closed at exit127bricks the session you would need to install fabrika) and the fact that rule 5 admits no wrapper, so there is nowhere to fail closed from.Both horns are preserved intact in the ADR, with their incidents (#1050 + #787/#788/#789 for A; #3742's stale-build and the masking case for B).
What this PR does not claim
The ruling's teeth are only half-implementable today, and that is recorded rather than papered over: on exit
2fabrika speaks for itself (resolve.ts's foreign-checkout refusal), but on exit127fabrika cannot speak — it is what failed to resolve — and the session-start seam the ruling points at (#5078) is closed with a RETIRE verdict. So the notice for the127state has no owner; its only structural cure is the publish-plus-install at #4791. Also recorded as UNKNOWN: what Claude Code itself does when a declared hook's command is unresolvable, which could not be checked first-hand.Implementation of the chosen horn is gated on an adversarial review / threat-model in either direction. This ADR is the ruling, not an implementation licence.
Diff
.decisions/0250-fabrika-hook-cannot-run-fails-open.md— new. The ruling.claude-plugins/fabrika/docs/hook-surface.md— the dispatch-failure policy point flips from INTERIM, owned by Decide: does a fabrika hook fail open or fail closed when its verb cannot run? (re-seats #1050 / #3743) #5079 to RULED, plus the notice wording; four downstream record cross-references that described the question as open are corrected, including thepin.shrecord's dangling Owner (this decision child ruled the cannot-run polarity only, so the lost version-gated refusal is still unowned).Acceptance criteria
.git/hooksblast radius established as not applying to fabrika's hook surface, and Horn A re-weighed accordingly.#the-dispatch-failure-policy-pointsection ofclaude-plugins/fabrika/docs/hook-surface.md.Scope
Not control-plane: neither changed path matches a live CODEOWNERS row, and
claude-plugins/fabrika/**is founder-ruled non-§CP (#5036). No file underclaude-plugins/kampus-pipeline/is touched. Docs-only — no code, no user-facing surface (the issue's containment isexempt).