fix(fabrika): make the governance floor bind in CI (#5408) - #5494
Conversation
`ship gate` already derives the governance requirement from the diff, but it seats `blocked` at exit 0 and no workflow invoked it, so the floor bound on nothing but prose in the ship skill — #5293 and #5333 merged with no governance verdict after it shipped. Adds `fabrika ship floor`, a caller verb that asks `ship gate` for the one `governance` namespace and refuses on 18 when the verdict is absent, stale or fail, plus `.github/workflows/governance-floor.yml`, which relays that exit code and derives nothing (ADR 0228). `ship gate` is unchanged and stays the single merge authority. The mechanism choice, and why a new exit code on `ship gate` and a bash parse in the workflow were both rejected, is recorded in the ship contract.
No preview deploy
|
|
AC-5 evidence — the check is observed red, not asserted red. The This is a real PR touching One thing this run does not establish: the head carried a single non-marker comment, so |
|
review-code: advisory — blocking-set PR (§CP — approval-gated) PR #5494 is §CP — it touches Reviewed-head: @ 6e72288 Scope check first. The issue TITLE says "both halves", but the BODY’s Scope after split section narrows #5408 to the enforcement half only — "This issue is now the enforcement half only: make the required Verified PR #5494 against the acceptance criteria of #5408, one at a time:
Run-evidence bundle: PRESENT for head On the author’s UNVERIFIED platform claim — flagging was right, and the fail-closed claim holdsThe author flagged rather than asserted whether a workflow Two separate findings:
Bank-time notes for the approver (not charged against this PR)
Verdict-written: 2026-08-13T01:55:31Z |
|
review-skill: advisory — blocking-set PR (§CP — approval-gated) PR #5494 is §CP — Reviewed-head: @ 6e72288 Behavioral surface under review:
Operational note for the approver, not a criterion charge: once this lands, every PR touching Verdict-written: 2026-08-13T01:55:40Z |
|
governance: PASS @ 136dbdd — no contradiction with standing law, no gate invariant weakened; re-judged at this head after the merge of main Re-judged from scratch at the new head after the merge of Namespace. Self fence. Corpus half — no contradictionNo decision record is in this diff, so there was no subject to rank and no
Cited as One citation nit, not a finding. The Gate half — no weakening
Read by hand for the unanchored half, since an unanchored invariant is invisible to the scan:
No gate invariant in this diff's reach is removed or softened. The change is a strengthening: a What
|
The founder vetoed making fabrika's tree control-plane and substituted a control: a required
governanceverdict on any diff touching.decisions/,.claude/,.github/orclaude-plugins/. That control did not bind.ship gatederived the requirement correctly, but it reportsblockedat exit 0, no workflow ran it at all, and the only thing turningblockedinto a stop was a paragraph in the ship skill — so #5293 and #5333 both merged with no governance verdict after the floor shipped.This adds the missing caller.
fabrika ship floorasksship gatefor the onegovernancenamespace and refuses when the verdict is not a head-bound PASS; a new workflow runs it on every PR and relays its exit code.ship gateitself is untouched.Fixes #5408
What binds now, and on which condition
ship floor(the decision)governancenamespace at the given head resolves toabsent,staleorfail→ exit18. A verdict from an author without write+ resolvesabsentthrough the ADR 0055 ACL gate, so it reds too. Only a head-bound PASS from an authorized author issatisfied..github/workflows/governance-floor.yml(the enforcement)pull_request, nopaths:filter. Every non-zero exit reds the job:18means a human owes this PR a verdict;7/11/13mean the floor could not be resolved at all, which is UNKNOWN and never a pass (ADR 0092).Proving the unbound path is closed, not just that the happy path works
A guard that fires on ABSENT but stays quiet on WRONG is the defect class this repo keeps re-landing (#5416, #4887), so the mutation battery is the point of
src/ship/floor-verb.unit.test.ts. Each row is a PR that looks like it carries a governance verdict, and each one still reds on18:governance: FAIL @ <head>— present, head-bound, and negative.governance: PASS @ <other-head>— present and positive, bound to a tree nobody judged (ADR 0058).governance: PASS @ <head>from an author without write+ — present, positive, head-bound, unauthorized.review-skill: PASS @ <head>and nothing else — a PASS in the neighbouring namespace.Plus the fail-closed rows: an unreadable ACL, an unreadable file list, a short file list, and a zero-file diff all refuse rather than answering
n/a.Run against live PRs, not asserted. On PR #5481 (a real open
claude-plugins/**diff with no governance marker) the verb exits18and names the state; on PR #5237 (anapps/webdiff) it answersn/aat exit 0. Both transcripts are in the contract's Examples block, copied from the runs. This PR's owngovernance-floorcheck is the third data point: this diff touches.github/andclaude-plugins/, so the job is expected to be red here until a governance verdict is posted at head. That red is the acceptance criterion being met, not a build failure.The mechanism choice, and why the two alternatives lost
Recorded in full at
claude-plugins/fabrika/skills/ship/contract.md, section Why a caller verb, and not a new exit code onship gate. In short:blockedon the3+ band insideship gatebreaks the interface convention's answer/refusal split for every existing caller at once, and a refusal cannot report which namespace blocked, becauserefuse()hardcodes empty stdout.gate\tblockedin the workflow'srun:step puts the decision in bash, which ADR 0228 forbids, and needs a second step to decide whether the floor applies — a second copy ofGOVERNANCE_ROOTSthat nothing reds when it drifts (packages path filter excludes claude-plugins/skills, so its own corpus pins first run in the merge queue #4604).ship gateanswering as before, keeps one derivation of the verdict, and lets the workflow relay an exit code and derive nothing.ship flooris not a second conjunction and decides nothing about enqueue — it reads one namespace.ship gatestays the single merge authority.Deviations
1. Scope — the title's "both halves" is not what this PR delivers, by the issue's own instruction. Said: the title reads Control substituted for fabrika §CP does not bind (both halves). Did: delivered the enforcement half only. Why: the issue body's Scope after split section states this issue is now the enforcement half only and that the visibility half is #5409. Disposition: no action needed — #5409 owns the digest half.
2. The check is red-but-not-required, and the last step is an operator's. Said: AC 2 asks for a red required check. Did: shipped the job; it is not in the
main protectionruleset'srequired_status_checkslist, which today holds exactlyci-required,scan changed files for leaksandvalidate skill frontmatter(read live). Why: adding a context to a repository ruleset is a server-side control-plane change, not a diff, and not mine to make. What binds today: the red check makesship checksroll upredat the head, so the shipper refuses to enqueue — the autonomous merge path is closed. A human with admin can still override, which the ruleset entry is what would prevent. Disposition: for the founder, at bank time — add the contexta governance-root diff carries a governance verdict at its headto ruleset17377992. Flagged rather than done.3. One platform claim is not yet verified at the surface that matters. Said: the job runs with
permissions: contents: read, pull-requests: read. Did: verified the whole verb against live PRs with a localghtoken, not with a workflowGITHUB_TOKEN. Why: the ACL read (GET /repos/{o}/{r}/collaborators/{u}/permission) is the one call whose availability toGITHUB_TOKENat these permissions I could not establish from source. Disposition: for the reviewer to judge, and this PR's own run answers it — if the token cannot read the ACL, the job reds on11withcannot read the ACL for <login>rather than passing, which is the fail-closed direction; the fix is then a permissions line, and the check on this PR will say so plainly.4. Prose in the ship SKILL was changed, not just pointed at. Said: AC 6 asks that step 3 no longer stand alone as the enforcement. Did: added a paragraph to step 3 naming the workflow, and a row to the skill's repo-surface table with
degradeas the when-missing outcome. Why: the skill still refusesblockeditself, so a repo without the workflow is not broken — what is lost is enforcement against a run that never happened, and that is what the row says. Disposition: no action needed.