fix(snapshot): read captain-actionability off the hold kind, not the record kind - #83
Merged
Freudator86 merged 4 commits intoAug 9, 2026
Conversation
…record kind AGENTS.md section 10 files a captain-gated thread with `tasks-axi hold <id> --reason "<reason>" --kind captain` and says nothing about the record kind, but the captain-actionable predicate also demanded `kind == "captain"`. A hold filed exactly as instructed was therefore invisible to /decisionboard, to bin/fm-decision-inventory.sh, and to every bearings read. Measured on the live home 2026-08-09: 35 records carried `hold-kind: captain` and the surface returned 32. The three missing were a production deploy filed as kind ship, a thread carrying no record kind at all, and one blocked record. Widening the predicate to the hold kind returns the first two; `hold --kind` is a closed vocabulary, so it admits only records someone deliberately held for the captain. The blocked one is untouched on purpose: a captain hold behind an unresolved blocker still leaves decisions_open and lands in gates carrying no kind. That is a separate, separately filed gap, and the new test pins it so this change cannot quietly move it. Cross-references that claimed the record kind was what kept a record off the captain's surface are corrected to name the hold kind: AGENTS.md section 10, bin/fm-chart-kinds-lib.sh, bin/fm-sea-chart.sh, bin/fm-to-backlog.sh, .agents/skills/sea-chart, and docs/to-backlog-provenance.md. fog and out-of-course stay safe by construction because both are held as `future`. The sea chart's own reconciliation baseline stays kind-based and is documented as deliberately narrower. Regression coverage in tests/fm-bearings-snapshot.test.sh asserts the reported surface, not the predicate: verified failing before the change and passing after.
…cked captain holds
Freudator86
deleted the
fm/firstmate-captain-hold-invisible-to-decision-surface-r2
branch
August 9, 2026 14:10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
A captain-gated decision must reach the captain even when it is filed on a record whose own kind is not
captain.THE DEFECT. AGENTS.md section 10 tells firstmate to file a captain-gated thread with
tasks-axi hold <id> --reason "<reason>" --kind captainand says nothing about the record's own kind. But the captain-actionable predicate in bin/fm-fleet-snapshot.sh also demanded.kind == "captain", so a hold filed exactly as the instructions describe was invisible to /decisionboard, to bin/fm-decision-inventory.sh, and to every bearings read.MEASURED, NOT ARGUED. On the live home 2026-08-09: 35 records carried
hold-kind: captainand the surface returned 32. The three missing were a production deploy filed as kind ship, a thread carrying no record kind at all, and one record blocked by an unresolved blocker. After the change the surface returns 34.THE DECISION AND WHY IT WENT THIS WAY. The captain's standing preference, recorded 2026-07-31, is to widen the mechanism so the instruction becomes true rather than narrow the instruction to match the mechanism. It was a genuine choice, not an order: the two fields do mean different things, but not in a way that justifies narrowing.
kindsays what the work IS (ship, scout, fog);hold_kindsays WHO is being asked. Captain-actionability is a property of the hold, so the predicate now readshold_kindalone.tasks-axi hold --kindis a closed vocabulary (captain, external, load, parked, future) enforced by tasks-axi, so widening admits only records someone deliberately held for the captain - it is not an open door.DELIBERATELY OUT OF SCOPE, AND PINNED SO IT CANNOT MOVE. A record blocked by another record also leaves decisions_open entirely with no footnote. That is a separate, already-filed gap and the brief explicitly forbade folding it in. The change does not touch it, and the new test asserts the blocked record STILL stays out of decisions_open and STILL lands in gates - so this change cannot quietly alter that behaviour later.
ONE REAL CONSEQUENCE, WRITTEN DOWN RATHER THAN LEFT IMPLICIT. bin/fm-chart-kinds-lib.sh claimed fog and out-of-course were safe by construction BECAUSE the predicate required
kind: captain. That guarantee now rests on the hold kind instead: both are filed withhold --kind future, so both still read captain_actionable false. But a fog record misfiled with a captain hold would now surface as a captain decision. That is the recoverable direction on purpose - a misfiled question the captain can see is recoverable, one he cannot is not - and the header now says so.CROSS-REFERENCES CORRECTED (the repo's one-owner rule requires it). Six places claimed the record kind was what kept a record off the captain's surface and are now corrected to name the hold kind: AGENTS.md section 10, bin/fm-chart-kinds-lib.sh, bin/fm-sea-chart.sh (three comment blocks), bin/fm-to-backlog.sh, .agents/skills/sea-chart/SKILL.md, and docs/to-backlog-provenance.md. bin/fm-to-backlog.sh never calls
tasks-axi holdat all, so its kind refusal was never what protected that surface; its header said otherwise and now says the truth. bin/fm-sea-chart.sh's own reconciliation baseline stays kind-based on purpose and is now documented as deliberately narrower than the predicate - it is not an oversight. A stale comment in tests/fm-sea-chart.test.sh is corrected for the same reason.COVERAGE. The brief required a test that asserts the SURFACE REPORTS the hold, not that a jq expression parses, and explicitly said a test that would have passed before the fix is not coverage. test_captain_hold_on_a_non_captain_kind_record_surfaces in tests/fm-bearings-snapshot.test.sh drives bearings end-to-end and asserts decisions_open. It was verified failing against the old predicate and passing against the new one by stashing the fix and re-running.
VERIFIED ON THIS EXACT BASE. bin/fm-lint.sh green. Eight affected suites green: fm-bearings-snapshot, fm-fleet-snapshot-view, fm-sea-chart, fm-backlog-lint, fm-instruction-owners, fm-to-backlog, fm-decision-inventory, fm-captain-translation-contract.
BRANCH SHAPE AND NAME, BOTH DELIBERATE. Work was done in a disposable worktree of the admiralty fleet repository, where bin/fm-fleet-snapshot.sh and AGENTS.md are vendored paths that cannot change there. An earlier run correctly flagged that the branch was based on admiralty main, 113 commits ahead of the fork main this pull request targets, which would have bundled 52 unrelated files into the pull request. The captain authorised rebasing onto fork/main, which was done: the branch is now exactly one commit on top of fork/main touching nine files, all byte-identical between the two bases, so the replay dropped nothing. The branch then carries an -r2 suffix only because the local gate still held the pre-rebase tip and rejected the rewritten history; a fresh name was taken rather than force-replacing anything. No pin bump is proposed or performed - that is a separate act by whoever curates the fleet repository.
What Changed
bin/fm-fleet-snapshot.shnow derivescaptain_actionablefromhold_kind == "captain"alone instead of also requiringkind == "captain", so a thread held exactly as AGENTS.md section 10 instructs (tasks-axi hold <id> --reason "…" --kind captain) reaches/decisionboard,bin/fm-decision-inventory.sh, and every bearings read regardless of the record's own kind. Records with unresolved blockers stay withheld — unchanged and deliberately out of scope.tests/fm-bearings-snapshot.test.shgainstest_captain_hold_on_a_non_captain_kind_record_surfaces, driving bearings end-to-end to assert the hold appears indecisions_openwhile a blocked record still stays out of it and lands in gates; the Test phase bisect-proved it fails against the base predicate.AGENTS.md,bin/fm-chart-kinds-lib.sh(fog/out-of-course safety now rests onhold --kind future),bin/fm-sea-chart.sh(its reconciliation baseline stays kind-based and is documented as deliberately narrower),bin/fm-to-backlog.sh,docs/to-backlog-provenance.md, thedecisionboard/sea-chart/to-backlogskills, and a stale comment intests/fm-sea-chart.test.sh.Risk Assessment
✅ Low: The follow-up commit changes only comments and one Markdown line - zero executable lines in bin/ - leaves the deliberately narrow reconciliation baseline at bin/fm-sea-chart.sh:514 untouched as the brief requires, and both reworded claims were verified against the code they describe.
Testing
I ran the bearings snapshot suite plus the seven other suites the change touches, all green, and proved the new test is real coverage by swapping in the base-commit
fm-fleet-snapshot.shand watching it fail, then restoring a clean worktree. For product-level evidence I built a demo home whose backlog files captain holds exactly as AGENTS.md section 10 describes and ran the actualfm-bearings-snapshot.shandfm-decision-inventory.shCLIs on both predicates: before the fix the surface returned one decision and pushed the ship-kind production deploy and the kindless fleet notice into gates, after the fix it returns all three while the blocked record still stays out of decisions_open and still lands in gates, and the fog record held--kind futurestays non-actionable in both. I also rendered the captain-facing decision board through the repo's ownbin/fm-board.shand screenshotted the before/after side by side. No failures, no flakiness, and no transient files left in the working tree./tmp/no-mistakes-evidence/01KZJGA6X57S5QEHX409R91M5S/decision-surface-before-after.png)Evidence: Rendered board HTML (built with bin/fm-board.sh, self-contained, no network)
Evidence: CLI transcript — bearings and decision-inventory on the same home, both predicates
### Demo home backlog (holds filed exactly as AGENTS.md s10 instructs) - [ ] deploy-prod - Deploy the stack to production (kind: ship) (hold: production action needs his live word) (hold-kind: captain) - [ ] allships-notice - Tell the fleet an update exists (hold: captain chose to hold it until close-out) (hold-kind: captain) - [ ] pick-approach - Which retry shape do we take (kind: captain) (hold: two viable shapes) (hold-kind: captain) - [ ] blocked-deploy - Clear the residue rows blocked-by: ship-task (kind: ship) (hold: production data change) (hold-kind: captain) - [ ] fog-thing - Unclear area (kind: fog) (hold: parked until later) (hold-kind: future) ### BEFORE - bin/fm-bearings-snapshot.sh at base e6762c5 decisions_open[1]{id,key,verb,summary,owner}: pick-approach,pick-approach,captain-hold,"Which retry shape do we take: two viable shapes",(main) gates[4]{id,title,blocked_by,reason,owner}: (main-inventory),in-flight backlog item has no child metadata,"-",main inventory,(main) deploy-prod,Deploy the stack to production,"-",production action needs his live word,(main) allships-notice,Tell the fleet an update exists,"-",captain chose to hold it until close-out,(main) blocked-deploy,Clear the residue rows,ship-task,production data change,(main) ### AFTER - bin/fm-bearings-snapshot.sh at bfb4c4b decisions_open[3]{id,key,verb,summary,owner}: deploy-prod,deploy-prod,captain-hold,"Deploy the stack to production: production action needs his live word",(main) allships-notice,allships-notice,captain-hold,"Tell the fleet an update exists: captain chose to hold it until close-out",(main) pick-approach,pick-approach,captain-hold,"Which retry shape do we take: two viable shapes",(main) gates[2]{id,title,blocked_by,reason,owner}: (main-inventory),in-flight backlog item has no child metadata,"-",main inventory,(main) blocked-deploy,Clear the residue rows,ship-task,production data change,(main) ### /decisionboard feed - bin/fm-decision-inventory.sh BEFORE: {"records":1,"decisions":1,"ids":["pick-approach"]} AFTER : {"records":3,"decisions":3,"ids":["allships-notice","deploy-prod","pick-approach"]} ### fog record held with --kind future stays non-actionable in both "sea-chart items (fog, out-of-course) kept out of gates: 1","--all-queued"Evidence: New test fails against the base-commit predicate (bisect proof)
Evidence: Demo home fixture used for the end-to-end run
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-sea-chart.sh:84- The new header claims that after this change "Only a BLOCKED one is still lost, and that is the blocked-decision gap this header opens with, not a second flank" (repeated at line 502: "it loses to the blocked-decision gap in the header, not to this line"). But the gap the header opens with (lines 59-72) is the one this chart RECOVERS into withheld[], and that recovery is built from $own_decision_records at line 504, which filters.kind == "captain". A blocked record of another kind carrying a captain hold is therefore in neither decisions_open (predicate fails on the blocker) nor withheld[] (baseline fails on the kind); it falls through to unplaced[], where unplaced_reason emits cause "blocked" with why "kind ship is none this chart places as fog, as a boundary, or as a decision, and it is held back by X, so it is not takeable either" - a sentence that never says the captain is being asked. So this class is not covered by the recovery the header points it at. This is not a regression (it behaved identically before) and the narrower baseline is deliberate per the intent, but the two new comment blocks describe a safety net that provably excludes this case. Resolve either by rewording both to say this class is recovered nowhere as a decision, or by widening the baseline to.kind == "captain" or .hold_kind == "captain"- the latter is a behavior change and the author explicitly documented the narrow baseline as deliberate, hence ask-user..agents/skills/to-backlog/SKILL.md:114- Missed cross-reference of exactly the class this change corrects elsewhere. The line still reads "this skill refuses to file a captain-actionable record so it cannot become a second owner by accident" - true only while captain-actionability requiredkind: captain, the kind this skill refuses. The intent enumerates six corrected places, and the author already rewrote the identical claim in bin/fm-to-backlog.sh:36-40 ("This script never callstasks-axi hold, and a hold is what bin/fm-fleet-snapshot.sh reads for captain-actionability, so the refusal is about ownership rather than the surface") and in docs/to-backlog-provenance.md:142-143. This SKILL.md is the surface firstmate actually reads, so it is the one most likely to mislead a future filer into believing the kind refusal protects the captain surface. Same correction as the other two: the refusal guards ownership, not actionability.AGENTS.md:488- AGENTS.md and .agents/skills/sea-chart/SKILL.md:157 keep the absolute "can never be mistaken for a captain decision" / "Neither can ever be mistaken", while bin/fm-chart-kinds-lib.sh:35-38 - edited in the same commit - was deliberately made honest that the guarantee is now filing discipline rather than structure ("a fog record filed with a captain hold would now surface as a captain decision"). The three files edited together now hedge differently on the same claim. The AGENTS.md sentence does state its own condition ("and both are held asfuture"), so a careful reader gets it; noting the divergence rather than asking for a change, since the intent explicitly chose the recoverable direction and recorded it in the lib header.🔧 Fix: correct sea-chart and to-backlog comments on blocked captain holds
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
bash tests/fm-bearings-snapshot.test.sh— full suite green, including the newtest_captain_hold_on_a_non_captain_kind_record_surfacesBisect proof: replacedbin/fm-fleet-snapshot.shwith the base-commit (e6762c5) version, re-rantests/fm-bearings-snapshot.test.sh, confirmed the new test emitsnot ok, thengit checkout -- bin/fm-fleet-snapshot.sh(worktree verified clean)bash tests/fm-instruction-owners.test.shbash tests/fm-decision-inventory.test.shbash tests/fm-sea-chart.test.shbash tests/fm-to-backlog.test.shbash tests/fm-fleet-snapshot-view.test.shbash tests/fm-backlog-lint.test.shbash tests/fm-captain-translation-contract.test.shManual end-to-end: built a demoFM_HOMEwith holds filed per AGENTS.md section 10 (kind ship, no kind, kind captain, a blocked one, plus a fog record held--kind future) and ranFM_HOME=... bin/fm-bearings-snapshot.shagainst both the base and target predicatesManual end-to-end:FM_HOME=... bin/fm-decision-inventory.sh | jq '{records,decisions,ids:[.groups[].decisions[].id]}'against both predicatesRendered the captain-facing board withbin/fm-board.sh --title ... --body ... --out ...from the two bearings JSON outputs and captured a full-page screenshot viachrome-devtools-axibin/fm-sea-chart.sh:82- Follow-up, deliberately not done here. The new fm-sea-chart.sh header now states a real captain-facing consequence: a BLOCKED captain hold carried on a record whose own kind is notcaptainreaches neither surface — the predicate fails it on the blocker, the chart's kind-based baseline fails it on the kind, so it is recovered nowhere as a decision and lands inunplaced[]under causeblockedwithout ever saying the captain is being asked. That fact currently lives only in a code header; no captain-facing surface discloses it (the assembler'slimits[]does not carry it, and the sea-chart skill's withheld-cause guidance does not cover it). The behaviour is unchanged by this change and folding it in was explicitly forbidden by the brief, so I left it alone rather than adding a limit string (which would be executable behaviour) or duplicating the header's prose into the skill. Worth attaching to the already-filed blocked-record gap: when that gap is closed, decide whether this class earns an entry inlimits[].✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.