Skip to content

fix: enforce acceptance and check guards before merging - #212

Merged
dnth merged 5 commits into
mainfrom
fm/fm-merge-guards
Oct 6, 2026
Merged

dnth merged 5 commits into
mainfrom
fm/fm-merge-guards

Conversation

@dnth

@dnth dnth commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Intent

Captain 2026-10-06 "go all of them" (follow-ups of the receipt-simplification review, after PR #211 merged as 1608b2c): add mechanical merge guards. Two rules lived only as instructions in AGENTS.md section 7: never auto-merge a task with any accepted-blocked acceptance criterion, and never merge a red PR. Make bin/fm-pr-merge.sh and bin/fm-merge-local.sh enforce them.

Constraints (from the captain/firstmate brief):

  • One-owner rule: each script's header owns its mechanics; AGENTS.md and skills get at most a one-line pointer.
  • Accepted-blocked: read from bin/fm-receipt-check.sh (accepted_blocked non-empty) for the task being merged.
  • Red checks: for fm-pr-merge.sh the PR's required/reported checks must all be passing (green); pending or failing refuses. fm-merge-local.sh has no forge checks, so it applies only the accepted-blocked rule.
  • Override: an explicit flag carrying the captain's concrete merge instruction verbatim (--captain-instruction ""), recorded in the task's durable record. Standing yolo authority alone never satisfies it for an accepted-blocked task or a red PR. Destructive or security-sensitive escalation rules are unchanged.
  • Refusals must name the exact reason and the override flag; existing merge behavior, metadata recording, merge-queue handling and post-merge CI arming stay unchanged on the green, no-accepted-blocked path.

Acceptance criteria:

  • AC1: fm-pr-merge.sh refuses a task whose receipts show any accepted-blocked criterion, naming the criterion ids and the override flag, and merges it when the captain-instruction flag carries non-empty concrete words, which are recorded in the task's durable record; proven by tests through the public interface with a fake forge.
  • AC2: fm-pr-merge.sh refuses a PR whose checks are failing or still pending, naming them and the override flag, and proceeds with the flag; tests with a fake forge for failing, pending, and all-green PRs.
  • AC3: fm-merge-local.sh refuses an accepted-blocked task without the flag and proceeds with it; proven by a test.
  • AC4: green, no-accepted-blocked path of both scripts unchanged: existing tests pass untouched; metadata, merge-queue handling and CI arming as before.
  • AC5: script headers document the guards and flag; AGENTS.md mention at most one line; fm-test-run --changed and fm-lint green.

Implementation decisions: shared bin/fm-merge-guard-lib.sh owns the accepted-blocked guard, flag validation (one non-blank line), and the durable override record (fm-merge-override.v1 JSONL appended to data//captain-merge-instructions.jsonl, which survives teardown unlike state/.meta) written before the merge only when the flag actually overrides a reason. Guard applies only to kind=ship tasks (mirrors fm-pr-check's evidence gate); unreadable evidence or unreadable checks refuse (overridable). Checks are read with gh pr checks --json name,bucket (pass/skipping green, pending pending, everything else failing), falling back to gh-axi pr checks when gh is absent or fails, mirroring the script's existing gh-primary/gh-axi-fallback outcome read; a PR with no reported checks ("no checks reported") is not red, so repos without CI still merge. Guards run before PR metadata recording so a refused merge records nothing. In fm-merge-local the guard runs after the fast-forward safety checks, just before the merge; task ids are now validated.

What Changed

  • Refuse PR and local merges for ship tasks with accepted-blocked criteria or unreadable acceptance evidence; also refuse PR merges with failing, pending, or unreadable checks before recording PR metadata.
  • Add --captain-instruction to both merge commands, validating one non-blank line and durably recording the verbatim instruction and overridden reasons before merging.
  • Document guard ownership and add regression coverage for refusals, overrides, green and check-less merges, and forge-reader fallback behavior.

Risk Assessment

✅ Low: Captain, the guards are bounded, conform to the accepted intent and subsequent decisions, and introduce no substantiated material defects.

Testing

Both targeted Bash test files passed. Real local merges demonstrated the guards and malformed-receipt fix; fake-forge tests covered PR checks and existing merge behavior. CLI and persisted-state evidence was captured, disposable data was removed, and no source changes were made. Live GitHub validation remains unavailable without isolated credentials.

  • Live validation: ⚠️ inconclusive - 7 of 11 scenarios driven live against the product
Scenario Result Live Evidence
Local merge refuses accepted-blocked AC1 despite standing yolo authority and leaves main unchanged ✅ pass live local-live.txt: blocked-refusal
Explicit local override fast-forwards main and records the captain's exact words ✅ pass live local-live.txt: blocked-override
Malformed receipt evidence refuses merging unless an explicit override is durably recorded ✅ pass live local-live.txt: malformed-refusal and malformed-override
Fully evidenced local task fast-forwards without creating an override record ✅ pass live local-live.txt: green
Blank or multiline instructions, removed equals-form alias, and unsafe task IDs refuse ✅ pass live local-live.txt: blank, multiline, removed-alias, and invalid-id
Local override cannot merge when its durable record cannot be written ✅ pass live local-live.txt: record-unwritable
Captain instruction does not bypass the local fast-forward safety requirement ✅ pass live local-live.txt: diverged-override
PR merge refuses accepted-blocked criteria and merges only with a recorded explicit instruction ⏸️ untested no Public-interface fake-forge tests passed. Isolated gh auth status confirmed no credentials. The GitHub-only interface cannot perform a real merge against a local Git repository. Provide a disposable G…
Failing, pending, or unreadable PR checks refuse before metadata recording; explicit instruction overrides ⏸️ untested no Fake-forge refusal and override tests passed. Live checks and merging require a disposable GitHub repository with controlled failing and pending checks and scoped credentials; isolated configuration h…
Green, check-less, and skipped-check PRs merge without an override, including both gh-axi fallback paths ⏸️ untested no Fake-forge tests passed for green, absent, skip, skipping, skipped, and neutral checks, including absent and failing gh readers. Real GitHub validation requires a disposable repository, controlled che…
Ordinary PR merges preserve metadata, merge-queue handling, outcome reporting, and poll registration ⏸️ untested no Existing public-interface fake-forge regression tests passed. Live verification requires an authenticated disposable GitHub repository with merge-queue configuration and controlled CI. No isolated cre…
Evidence: Live local merge outputs, branch state, and durable override records

SCENARIO blocked-refusal
receipt-check exit=0
{"schema":"fm-evidence-check.v2","task":"task-live","kind":"ship","status":"complete","required":["AC1"],"evidenced":[],"accepted_blocked":[{"criterion":"AC1","captain_exception":"Captain: hardware unavailable"}],"missing":[],"invalid":[]}

command: fm-merge-local.sh task-live
exit=1
error: refusing to merge fm/task-live for task task-live: acceptance criteria accepted as blocked: AC1 (captain exception: Captain: hardware unavailable)
error: standing merge authority does not cover this merge; only the captain's explicit instruction for it does: retry with --captain-instruction "<the captain's exact words>"
main before=879764bf2776d9c7a750e263a178f1ac61384440
main after=879764bf2776d9c7a750e263a178f1ac61384440
feature=01f04f22147eafb7284adbd7594b2b9e39c6d2e1
durable override=null
OBSERVATIONS VERIFIED

SCENARIO blocked-override
receipt-check exit=0
{"schema":"fm-evidence-check.v2","task":"task-live","kind":"ship","status":"complete","required":["AC1"],"evidenced":[],"accepted_blocked":[{"criterion":"AC1","captain_exception":"Captain: hardware unavailable"}],"missing":[],"invalid":[]}

command: fm-merge-local.sh task-live ['--captain-instruction', 'Captain: merge this disposable task despite AC1']
exit=0
merged fm/task-live into local main (e5b8665 -> 14169e6) in ~/.no-mistakes/worktrees/dd71c22cc6d7/01M489KPJKEY32SYFPQE74BPGS/.test-phase-tmp/manual/blocked-override/project
notice: merging fm/task-live for task task-live under the recorded captain instruction despite: acceptance criteria accepted as blocked: AC1 (captain exception: Captain: hardware unavailable)
main before=e5b866581d5d4ec9a7ba7e4c6c7e951efad02d0f
main after=14169e6f229e2802d9586726c6292472941b4183
feature=14169e6f229e2802d9586726c6292472941b4183
durable override={"schema": "fm-merge-override.v1", "task": "task-live", "script": "fm-merge-local", "target": "fm/task-live", "overridden": ["acceptance criteria accepted as blocked: AC1 (captain exception: Captain: hardware unavailable)"], "captain_instruction": "Captain: merge this disposable task despite AC1", "at": "2026-10-06T10:09:44Z"}
OBSERVATIONS VERIFIED

SCENARIO malformed-refusal
receipt-check exit=2
{"schema":"fm-evidence-check.v2","task":"task-live","kind":"ship","status":"invalid","required":["AC1"],"evidenced":[],"accepted_blocked":[],"missing":["AC1"],"invalid":["line 1: invalid receipt"]}

command: fm-merge-local.sh task-live
exit=1
error: refusing to merge fm/task-live for task task-live: acceptance evidence for task task-live could not be read, so an accepted-blocked criterion cannot be ruled out
error: standing merge authority does not cover this merge; only the captain's explicit instruction for it does: retry with --captain-instruction "<the captain's exact words>"
main before=8111811ab3c28bec452601b79bfeb64d9bda2888
main after=8111811ab3c28bec452601b79bfeb64d9bda2888
feature=892595a13508af982ac34e15351061010a74bb26
durable override=null
OBSERVATIONS VERIFIED

SCENARIO malformed-override
receipt-check exit=2
{"schema":"fm-evidence-check.v2","task":"task-live","kind":"ship","status":"invalid","required":["AC1"],"evidenced":[],"accepted_blocked":[],"missing":["AC1"],"invalid":["line 1: invalid receipt"]}

command: fm-merge-local.sh task-live ['--captain-instruction', 'Captain: merge this disposable task despite malformed receipts']
exit=0
merged fm/task-live into local main (8413ede -> 4a4bd1b) in ~/.no-mistakes/worktrees/dd71c22cc6d7/01M489KPJKEY32SYFPQE74BPGS/.test-phase-tmp/manual/malformed-override/project
notice: merging fm/task-live for task task-live under the recorded captain instruction despite: acceptance evidence for task task-live could not be read, so an accepted-blocked criterion cannot be ruled out
main before=8413ede4aad90f2605c70249d4cd1b82e6a3decc
main after=4a4bd1b2f57f7588c9e96ff14de26b01472b6552
feature=4a4bd1b2f57f7588c9e96ff14de26b01472b6552
durable override={"schema": "fm-merge-override.v1", "task": "task-live", "script": "fm-merge-local", "target": "fm/task-live", "overridden": ["acceptance evidence for task task-live could not be read, so an accepted-blocked criterion cannot be ruled out"], "captain_instruction": "Captain: merge this disposable task despite malformed receipts", "at": "2026-10-06T10:09:50Z"}
OBSERVATIONS VERIFIED

SCENARIO green
receipt-check exit=0
{"schema":"fm-evidence-check.v2","task":"task-live","kind":"ship","status":"complete","required":["AC1"],"evidenced":["AC1"],"accepted_blocked":[],"missing":[],"invalid":[]}

command: fm-merge-local.sh task-live
exit=0
merged fm/task-live into local main (c5a91f7 -> 3c2eb66) in ~/.no-mistakes/worktrees/dd71c22cc6d7/01M489KPJKEY32SYFPQE74BPGS/.test-phase-tmp/manual/green/project

main before=c5a91f78d6e32425c5bac5c4f2df68d01f467e96
main after=3c2eb66dfc7550de2b6cb7e0e0bb58ba5c73d783
feature=3c2eb66dfc7550de2b6cb7e0e0bb58ba5c73d783
durable override=null
OBSERVATIONS VERIFIED

SCENARIO blank
receipt-check exit=0
{"schema":"fm-evidence-check.v2","task":"task-live","kind":"ship","status":"complete","required":["AC1"],"evidenced":[],"accepted_blocked":[{"criterion":"AC1","captain_exception":"Captain: hardware unavailable"}],"missing":[],"invalid":[]}

command: fm-merge-local.sh task-live ['--captain-instruction', '  ']
exit=2
error: usage: fm-merge-local.sh <task-id> [--captain-instruction <words>], with the captain's exact words on one non-blank line
main before=caf57a0db3e456d6424336a5a48c3a52a2b53ec6
main after=caf57a0db3e456d6424336a5a48c3a52a2b53ec6
feature=544a7c8a27496e4d6f94501db1fcdde9e4b86d4b
durable override=null
OBSERVATIONS VERIFIED

SCENARIO multiline
receipt-check exit=0
{"schema":"fm-evidence-check.v2","task":"task-live","kind":"ship","status":"complete","required":["AC1"],"evidenced":[],"accepted_blocked":[{"criterion":"AC1","captain_exception":"Captain: hardware unavailable"}],"missing":[],"invalid":[]}

command: fm-merge-local.sh task-live ['--captain-instruction', 'merge\nthis']
exit=2
error: usage: fm-merge-local.sh <task-id> [--captain-instruction <words>], with the captain's exact words on one non-blank line
main before=341c723af5ae65f2411ee574eb956dd379c45e82
main after=341c723af5ae65f2411ee574eb956dd379c45e82
feature=c1cdd3dd681f096456e2018cc5128ab144c9cfdc
durable override=null
OBSERVATIONS VERIFIED

SCENARIO removed-alias
receipt-check exit=0
{"schema":"fm-evidence-check.v2","task":"task-live","kind":"ship","status":"complete","required":["AC1"],"evidenced":[],"accepted_blocked":[{"criterion":"AC1","captain_exception":"Captain: hardware unavailable"}],"missing":[],"invalid":[]}

command: fm-merge-local.sh task-live ['--captain-instruction=alias']
exit=2
error: usage: fm-merge-local.sh <task-id> [--captain-instruction <words>], with the captain's exact words on one non-blank line
main before=10d9835e6d4fb7576982d467a04b899374294da2
main after=10d9835e6d4fb7576982d467a04b899374294da2
feature=bc363b532aa669a13d2776b70c1c33210ac79451
durable override=null
OBSERVATIONS VERIFIED

SCENARIO record-unwritable
receipt-check exit=0
{"schema":"fm-evidence-check.v2","task":"task-live","kind":"ship","status":"complete","required":["AC1"],"evidenced":[],"accepted_blocked":[{"criterion":"AC1","captain_exception":"Captain: hardware unavailable"}],"missing":[],"invalid":[]}

command: fm-merge-local.sh task-live ['--captain-instruction', 'Captain: merge this disposable task despite AC1']
exit=1
error: refusing to merge fm/task-live for task task-live: the captain instruction could not be recorded at ~/.no-mistakes/worktrees/dd71c22cc6d7/01M489KPJKEY32SYFPQE74BPGS/.test-phase-tmp/manual/record-unwritable/data/task-live/captain-merge-instructions.jsonl
main before=cf96ebd2e7ce4a7eb07bda6e211b790c43f022a4
main after=cf96ebd2e7ce4a7eb07bda6e211b790c43f022a4
feature=eb2eaf82790fc21b0b24430ece3bf150cbdef674
durable override=null
OBSERVATIONS VERIFIED

SCENARIO diverged-override
receipt-check exit=0
{"schema":"fm-evidence-check.v2","task":"task-live","kind":"ship","status":"complete","required":["AC1"],"evidenced":[],"accepted_blocked":[{"criterion":"AC1","captain_exception":"Captain: hardware unavailable"}],"missing":[],"invalid":[]}

command: fm-merge-local.sh task-live ['--captain-instruction', 'Captain: merge this disposable task despite AC1']
exit=1
REFUSED: fm/task-live is not a fast-forward of main (it has diverged).
Have the crewmate rebase fm/task-live onto main, then retry.
main before=ca37096bdefc78c03de43dedaf9a560b6fae2cb5
main after=ca37096bdefc78c03de43dedaf9a560b6fae2cb5
feature=932bfea721f3c1057eec2b9c97b3258ac55df08b
durable override=null
OBSERVATIONS VERIFIED

SCENARIO invalid-id '../escape'
exit=2
error: invalid task id: ../escape
OBSERVATIONS VERIFIED

SCENARIO invalid-id 'bad/id'
exit=2
error: invalid task id: bad/id
OBSERVATIONS VERIFIED
Evidence: PR public-interface outputs with a fake forge; not live GitHub evidence
Public fm-pr-merge.sh outputs using a FAKE FORGE. Not live GitHub evidence.

CASE accepted-blocked-override

stdout
armed: state/task-x1.check.sh
verified: https://github.com/example/repo/pull/82 is merged (state=MERGED, merged=true, isInMergeQueue=false)
armed: state/task-x1-main-ci-82.check.sh

stderr
notice: merging https://github.com/example/repo/pull/82 for task task-x1 under the recorded captain instruction despite: acceptance criteria accepted as blocked: AC1 (captain exception: 2026-10-06 captain: ship without live creds)
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

gh-axi.log
pr merge 82 --repo example/repo --merge

data/task-x1/captain-merge-instructions.jsonl
{"schema":"fm-merge-override.v1","task":"task-x1","script":"fm-pr-merge","target":"https://github.com/example/repo/pull/82","overridden":["acceptance criteria accepted as blocked: AC1 (captain exception: 2026-10-06 captain: ship without live creds)"],"captain_instruction":"Captain 2026-10-06: merge PR 82 despite AC1 being blocked","at":"2026-10-06T10:11:58Z"}

CASE accepted-blocked-refused

stdout

stderr
error: refusing to merge https://github.com/example/repo/pull/81 for task task-x1: acceptance criteria accepted as blocked: AC1 (captain exception: 2026-10-06 captain: ship without live creds)
error: standing merge authority does not cover this merge; only the captain's explicit instruction for it does: retry with --captain-instruction "<the captain's exact words>"

gh-axi.log

CASE checks-green

stdout
armed: state/task-x1.check.sh
verified: https://github.com/example/repo/pull/87 is merged (state=MERGED, merged=true, isInMergeQueue=false)
armed: state/task-x1-main-ci-87.check.sh

stderr
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

gh-axi.log
pr merge 87 --repo example/repo --squash

CASE checks-none

stdout
armed: state/task-x1.check.sh
verified: https://github.com/example/repo/pull/87 is merged (state=MERGED, merged=true, isInMergeQueue=false)
armed: state/task-x1-main-ci-87.check.sh

stderr
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

gh-axi.log
pr merge 87 --repo example/repo --squash

CASE pending-checks-refused

stdout

stderr
error: refusing to merge https://github.com/example/repo/pull/85 for task task-x1: checks are still pending: e2e
error: standing merge authority does not cover this merge; only the captain's explicit instruction for it does: retry with --captain-instruction "<the captain's exact words>"

gh-axi.log

CASE red-checks-override

stdout
armed: state/task-x1.check.sh
verified: https://github.com/example/repo/pull/86 is merged (state=MERGED, merged=true, isInMergeQueue=false)
armed: state/task-x1-main-ci-86.check.sh

stderr
notice: merging https://github.com/example/repo/pull/86 for task task-x1 under the recorded captain instruction despite: checks are failing: e2e
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

gh-axi.log
pr merge 86 --repo example/repo --squash

data/task-x1/captain-merge-instructions.jsonl
{"schema":"fm-merge-override.v1","task":"task-x1","script":"fm-pr-merge","target":"https://github.com/example/repo/pull/86","overridden":["checks are failing: e2e"],"captain_instruction":"Captain 2026-10-06: merge 86 now, the e2e failure is a known flake","at":"2026-10-06T10:12:01Z"}

CASE red-checks-refused

stdout

stderr
error: refusing to merge https://github.com/example/repo/pull/84 for task task-x1: checks are failing: unit tests, deploy
error: standing merge authority does not cover this merge; only the captain's explicit instruction for it does: retry with --captain-instruction "<the captain's exact words>"

gh-axi.log

CASE red-checks-without-gh

stdout

stderr
error: refusing to merge https://github.com/example/repo/pull/89 for task task-x1: checks are failing: build, linux
error: refusing to merge https://github.com/example/repo/pull/89 for task task-x1: checks are still pending: e2e
error: standing merge authority does not cover this merge; only the captain's explicit instruction for it does: retry with --captain-instruction "<the captain's exact words>"

gh-axi.log
pr checks 89 --repo example/repo

CASE skipped-checks-gh-absent

stdout
armed: state/task-x1.check.sh
verified: https://github.com/example/repo/pull/90 is merged (state=MERGED, merged=true, isInMergeQueue=false)
armed: state/task-x1-main-ci-90.check.sh

stderr
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

gh-axi.log
pr checks 90 --repo example/repo
pr merge 90 --repo example/repo --squash
pr view 90 --repo example/repo

CASE skipped-checks-gh-failed

stdout
armed: state/task-x1.check.sh
verified: https://github.com/example/repo/pull/90 is merged (state=MERGED, merged=true, isInMergeQueue=false)
armed: state/task-x1-main-ci-90.check.sh

stderr
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

gh-axi.log
pr checks 90 --repo example/repo
pr merge 90 --repo example/repo --squash

CASE unreadable-checks

stdout

stderr
error: refusing to merge https://github.com/example/repo/pull/88 for task task-x1: the checks on https://github.com/example/repo/pull/88 could not be read, so a failing or pending check cannot be ruled out
error: standing merge authority does not cover this merge; only the captain's explicit instruction for it does: retry with --captain-instruction "<the captain's exact words>"

gh-axi.log
pr checks 88 --repo example/repo
Evidence: Isolated GitHub configuration has no credentials
You are not logged into any GitHub hosts. To log in, run: gh auth login
- Outcome: ⚠️ 1 warning across 2 runs (12m5s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 3 issues found → auto-fixed ✅
  • 🚨 bin/fm-pr-merge.sh:534 - With gh absent or its checks read failing, gh-axi reports a skipped job as skip. This classifier omits that value, so an otherwise green PR is refused as failing and unnecessarily requires a captain override. Recognize skip as green, preserving the same invariant as bin/fm-pr-merge.sh:506, and exercise that fallback result through the public interface.
  • ⚠️ bin/fm-pr-merge.sh:109 - Simplification: the added --captain-instruction=... alias is unnecessary for the required --captain-instruction &#34;&lt;exact words&gt;&#34; interface. Remove this alternate matching path, including bin/fm-pr-merge.sh:115 and bin/fm-merge-local.sh:39; use the documented spelling in tests/fm-pr-merge.test.sh:1826.
  • ⚠️ bin/fm-merge-guard-lib.sh:30 - Simplification: this introduces a second task-ID policy alongside fm_pr_task_id_valid in bin/fm-pr-lib.sh, with different treatment of leading underscores and hyphens. Validation is required, but a parallel definition is not. Remove this helper and reuse the existing owner at bin/fm-merge-local.sh:45; PR merges already use that owner at bin/fm-pr-merge.sh:98.

🔧 Fix applied.
✅ Re-checked - no issues remain.

⚠️ **Test** - 1 warning
  • 🚨 bin/fm-merge-guard-lib.sh:55 - The shared guard permits invalid receipt evidence: fm-receipt-check exits 2 with status=invalid and accepted_blocked=[], but the guard only rejects exit codes greater than 2. A real local merge with malformed receipt JSON exited 0 and advanced main without an override. Providing an instruction also landed without recording it. Reject invalid receipt accounting before trusting its empty accepted_blocked list, and add a public-interface regression.
  • 🚨 live validation verdict: no-go (5 of 9 scenarios were driven live against the product); failed: Merge with malformed receipt evidence: refuse without an instruction and record any explicit override
  • Live validation: ❌ no-go - 5 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Merge an accepted-blocked local task: refuse on standing authority, then land with a verbatim durable captain instruction ✅ pass live Local merge transcript
Merge with malformed receipt evidence: refuse without an instruction and record any explicit override ❌ fail live Adversarial local merges and Git commit state
Supply multiline instructions or the removed equals alias: reject without advancing main ✅ pass live Adversarial local merges and Git commit state
Override a blocked task with a symlinked instruction log: refuse without landing or writing through the symlink ✅ pass live Adversarial local merges and Git commit state
Land evidenced local work without an override record, while preserving non-ship scope and fast-forward safety ✅ pass live Local merge transcript; Adversarial local merges and Git commit state
Merge an accepted-blocked PR: refuse before metadata recording, then proceed with a recorded captain instruction ⏸️ untested no The prior payload did not establish a live result: the real public scripts were executed using a fake forge, and no real forge was driven. It does not identify available real-forge credentials or auth…
Merge failing or pending PR checks: name the checks, refuse, and proceed only with a durable explicit instruction ⏸️ untested no The prior payload did not establish a live result: forge responses and merge outcomes were mocked. It does not identify available real-forge credentials or authority needed to drive failing or pending…
Merge green, absent, or skipped checks through primary and fallback readers; refuse unreadable checks ⏸️ untested no The prior payload did not establish a live result: forge readers were mocked, including absent-gh and failed-gh fallback cases. It does not identify available real-forge access needed to exercise thes…
Preserve PR metadata, merge-queue outcomes, and CI watch arming after verified merges ⏸️ untested no The prior payload did not establish a live result: targeted public-interface tests used mocked forge outcomes. It does not identify available real-forge credentials or merge and CI authority needed fo…
  • TMPDIR=&#34;$PWD/.test-phase-tmp&#34; bash sourcing tests/fm-merge-local.test.sh, with product transcripts captured before cleanup
  • TMPDIR=&#34;$PWD/.test-phase-tmp&#34; bash sourcing tests/fm-pr-merge.test.sh, with fake-forge transcripts and durable records captured
  • TMPDIR="$PWD/.test-phase-tmp" bash tests/fm-main-ci-watch.test.sh
  • python3 .test-phase-tmp/adversarial.py: real local Git merges, receipt-check output, branch hashes, instruction validation, record failure, and divergence
  • TMPDIR="$PWD/.test-phase-tmp" bash .test-phase-tmp/pending-override.sh

🔧 Fix applied.
1 warning still open:

  • ⚠️ live validation verdict: inconclusive (7 of 11 scenarios were driven live against the product); untested: PR merge refuses accepted-blocked criteria and merges only with a recorded explicit instruction, Failing, pending, or unreadable PR checks refuse before metadata recording; explicit instruction overrides, Green, check-less, and skipped-check PRs merge without an override, including both gh-axi fallback paths, Ordinary PR merges preserve metadata, merge-queue handling, outcome reporting, and poll registration
  • Live validation: ⚠️ inconclusive - 7 of 11 scenarios driven live against the product
Scenario Result Live Evidence
Local merge refuses accepted-blocked AC1 despite standing yolo authority and leaves main unchanged ✅ pass live local-live.txt: blocked-refusal
Explicit local override fast-forwards main and records the captain's exact words ✅ pass live local-live.txt: blocked-override
Malformed receipt evidence refuses merging unless an explicit override is durably recorded ✅ pass live local-live.txt: malformed-refusal and malformed-override
Fully evidenced local task fast-forwards without creating an override record ✅ pass live local-live.txt: green
Blank or multiline instructions, removed equals-form alias, and unsafe task IDs refuse ✅ pass live local-live.txt: blank, multiline, removed-alias, and invalid-id
Local override cannot merge when its durable record cannot be written ✅ pass live local-live.txt: record-unwritable
Captain instruction does not bypass the local fast-forward safety requirement ✅ pass live local-live.txt: diverged-override
PR merge refuses accepted-blocked criteria and merges only with a recorded explicit instruction ⏸️ untested no Public-interface fake-forge tests passed. Isolated gh auth status confirmed no credentials. The GitHub-only interface cannot perform a real merge against a local Git repository. Provide a disposable G…
Failing, pending, or unreadable PR checks refuse before metadata recording; explicit instruction overrides ⏸️ untested no Fake-forge refusal and override tests passed. Live checks and merging require a disposable GitHub repository with controlled failing and pending checks and scoped credentials; isolated configuration h…
Green, check-less, and skipped-check PRs merge without an override, including both gh-axi fallback paths ⏸️ untested no Fake-forge tests passed for green, absent, skip, skipping, skipped, and neutral checks, including absent and failing gh readers. Real GitHub validation requires a disposable repository, controlled che…
Ordinary PR merges preserve metadata, merge-queue handling, outcome reporting, and poll registration ⏸️ untested no Existing public-interface fake-forge regression tests passed. Live verification requires an authenticated disposable GitHub repository with merge-queue configuration and controlled CI. No isolated cre…
  • TMPDIR="$PWD/.test-phase-tmp" bash tests/fm-merge-local.test.sh
  • TMPDIR="$PWD/.test-phase-tmp" bash tests/fm-pr-merge.test.sh
  • Python disposable-repository driver executed real bash bin/fm-receipt-check.sh and bash bin/fm-merge-local.sh, checking exit codes, branch SHAs, refusal messages, and persisted override JSON.
  • Public local interface checks for unsafe task IDs, invalid instructions, unwritable override records, and divergent branches.
  • gh auth status with an empty worktree-local GH_CONFIG_DIR and token environment variables removed.
  • Removed disposable repositories and test configuration; verified git status --short was clean.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

dnth added 5 commits October 6, 2026 15:12
fm-pr-merge.sh and fm-merge-local.sh now refuse a ship task with any
accepted-blocked acceptance criterion, and fm-pr-merge.sh refuses a PR whose
reported checks are failing, pending, or unreadable. Each refusal names the
reason and --captain-instruction, the only override; its verbatim words are
recorded in data/<id>/captain-merge-instructions.jsonl before the merge runs.
The green, no-accepted-blocked path is unchanged.
@dnth
dnth merged commit f9d2fa8 into main Oct 6, 2026
17 checks passed
@dnth
dnth deleted the fm/fm-merge-guards branch October 6, 2026 14:44
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.

1 participant