Skip to content

feat(bin): merge upstream's 76 commits and reconcile the fork's divergences - #29

Merged
rub-a-dub-dub merged 104 commits into
mainfrom
fm/firstmate-reconcile-fork-with-upstream
Sep 27, 2026
Merged

rub-a-dub-dub merged 104 commits into
mainfrom
fm/firstmate-reconcile-fork-with-upstream

Conversation

@rub-a-dub-dub

@rub-a-dub-dub rub-a-dub-dub commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Intent

Reconcile this fork with its upstream in BOTH directions. The captain's standing policy, in his own words on 2026-09-22: "the update process should be to incorporate upstream as well as my changes. Where my changes do/don't make sense, adjust/discard them. That's the standing way this should work."

So this is a reconciliation, not a one-way pull and not a refusal. Take upstream's work. Keep his changes where they still make sense against it. Adjust or discard his own where they do not - he authorised that explicitly and in advance.

CURRENT DIVERGENCE, measured 2026-09-22: origin/main (rub-a-dub-dub/firstmate) is 11 ahead and 76 behind upstream/main (kunchenguid/firstmate). The local checkout is a further 4 behind its own origin.

HIS 11 COMMITS UPSTREAM DOES NOT HAVE:
2e9903c derive fm-crew-state's passed-outcome PR detail from pr_state (#13)
7ce3cda report a retain replay's absent backlog row instead of silently retiring it (#9)
fc20562 refuse a merge when a PR's CI event may have been dropped (#12)
126eaec annotate pre-existing shellcheck findings that fail CI lint (#10)
6339e2d Merge the fork's landed fixes into the upstream line
00b1613 stop appended metadata from disarming the PR merge poll (#6)
3de1d01 fail the update when a remote secondmate's fork sync fails (#5)
7ac2246 treat a never-asked external-imports prompt as not a decline (#4)
6f4c112 synchronize GitHub forks before checking origin (#3)
d233f53 clarify acknowledgement means receipt, not completion (#2)
eb845e4 read current state from the newest state-bearing line, not the last line (#1)

FIRSTMATE RECOMMENDED THE OPPOSITE AND WAS OVERRULED - recorded so you do not re-litigate it. The recommendation was to take only his own four unpulled commits and leave upstream alone, on the grounds that 76 unreviewed commits land in the code that supervises the fleet, and that upstream carries open issue 5269 warning of a suspicious PR cluster targeting supervision code from external accounts. He overruled it in one line. Reconciliation is the standing way; do not propose isolation.

Later, when the worker escalated one upstream commit (69d660a, "feat(bin): add opt-in typed dispatch resolution") matching the pattern warned about in upstream issue 5269, with options to include it as-is with prominent disclosure, strip it, or hold the PR, the captain answered: "syncin with firstmate fork: approve including the commit".

When review repeatedly raised findings about telling a secondmate's own terminal line apart from relayed child lines, firstmate recommended one final bounded fix round with an automatic fallback to upstream's secondmate behavior if the theme recurs; the captain answered: "I agree with your rec".

What Changed

  • Merges the 76 upstream commits the fork was behind, on top of its own 11: new bin/fm-dispatch-resolve.sh (opt-in typed dispatch resolution, gated on TYPESAFE_API_KEY), bin/fm-pr-state.sh and bin/fm-pr-reviewers.sh (read-only PR blocker and reviewer discovery), bin/fm-contributions.sh plus bin/fm-contributions.jq, bin/fm-env-lib.sh, a rewritten bin/fm-inbox.sh capture/reply/receipt surface, the flag-gated Claude Code Calm mod under .claude/mods/firstmate-calm/, and fixes across the supervision scripts, docs, CI workflows, and test suite.
  • Reconciles the fork's status-reading divergence against upstream's rewritten readers: bin/fm-crew-state.sh now reports the current declared state from one status_current_line read with a three-tier priority (declared paused:/captain-held: wait first, then the still-open decision fold, then the newest state-bearing line), and both the remote-secondmate branch and the log fallback reuse that single result instead of re-deriving it via status_current_state_line.
  • Discards the fork-only behaviours that no longer hold against upstream in bin/fm-classify-lib.sh: secondmate relay discrimination reverts to the kind-blind terminal rule, decision-origin preservation reverts to upstream's unconditional reset, and a plain done: becomes the sole retirement route for the fold's default bucket while a paused: line no longer retires it (decision-fold version bumped to 11), so a worker that appended blocked: then paused: stays visible to the watcher.

Risk Assessment

✅ Low: This round's changes are a comment and documentation sweep plus one behavior alignment that I confirmed by direct library execution to make the whole-file fold, the cursor-backed fold, and the watcher's origins map mutually consistent - resolving a genuine reader divergence the merge had introduced - leaving only a cosmetic inaccuracy in a migration note.

Testing

  • ⏭️ Test - skipped

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

⏭️ **Rebase** - skipped
  • ⚠️ .agents/skills/harness-adapters/references/harness/claude.md - merge conflict rebasing onto origin/main
  • ⚠️ bin/fm-claude-trust.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-claude-trust.test.sh - merge conflict rebasing onto origin/main
⚠️ **Review** - 1 info
  • ⚠️ bin/fm-classify-lib.sh:2173 - c5ce30a claims a durable fix ("keep paused decisions visible to watcher"), but the same failure is still reachable after one more appended line. The exemption only holds while the paused: line is the log's LAST line.

The guard is if [ -z "$after" ] && [ "$verb" != "$pause" ]; then origins=''; fi. after is empty on EVERY line once the fold has emptied - including lines _fm_decision_fold_line ignores outright (bare continuation prose, note:, working:), which return $open unchanged and therefore retire nothing. So the blanket reset re-fires on the first such line after the pause and discards the origin the pause just preserved.

Traced by executing the real library (sourced bin/fm-classify-lib.sh at c5ce30a, _fm_status_open_decision_origins + status_span_first_actionable_record <f> 0 rec needs):

  • blocked: cannot reach the registry + paused: waiting for the window -> origins=[default\t1], rc=0, event=blocked: cannot reach the registry (the fixed case)
  • same two lines + note: still waiting -> origins=[], rc=1, needs=0, no event
  • same two lines + Still waiting on the window. (no colon, not an event at all) -> origins=[], rc=1, needs=0, no event
  • needs-decision: should I deploy to prod? + paused: waiting for the window + note: bumping -> origins=[], rc=1, needs=0

That third line is the ordinary away-mode shape: a worker that declares a blocker, declares the wait, then keeps narrating. The consequence is exactly what the comment at lines 2145-2159 says must not happen - and it is total, because the other two surfaces are already silent by design: the OPEN DECISIONS fold retires the unkeyed bucket on the plain paused: line (origin/main #19, now also on the whole-file path via this same commit), and fm-crew-state.sh reports state: paused because status_current_state_line walks back past the note: to the pause. Once origins is cleared, the watcher's actionable-event classifier is the last surface and it reports rc=1 / needs=0, so the captain's question is invisible everywhere.

The new test at tests/fm-classify-decision-key.test.sh:101 passes only because it writes exactly two lines and stops; it never appends anything after the pause, which is why the general case ships unnoticed.

Smallest honest remedy, inside the condition this commit just wrote and adding no new state: reset only when the line is one the fold actually acted on - i.e. gate the reset on the verb being in the fold's own vocabulary (needs-decision|blocked|done|failed|$resolve|$held) rather than on after merely being empty, keeping the existing pause exclusion. A line the fold ignores cannot retire anything and must not clear the map. I verified that candidate against the same inputs: origins survives note:/prose (rc=0, event surfaced) and is still correctly dropped by a later done: (pause-then-done -> origins=[], event=done: shipped) and by resolved: (pause-then-resolved -> origins=[], rc=1). One judgment call is left for the author: under that candidate a later working: line also stops clearing the origin, so a worker that resumes keeps the blocked line actionable until a real terminal/resolve lands - pick whether working belongs in the resetting set.

Not a regression against the merge parent (929a366 cleared origins in every pause case, including pause-as-last-line), so this is an incomplete fix rather than new breakage - but the commit's stated invariant does not hold as written.

  • ℹ️ bin/fm-classify-lib.sh:2159 - The comment block that is now the authoritative statement of this commit's invariant points at coverage that does not exist: "see tests/fm-watch-triage.test.sh's 'hidden behind a current wait' case". grep -rn "hidden behind a current" tests/ matches only tests/fm-classify-decision-key.test.sh:109-114 - the assertions c5ce30a itself just added. There is no such case in tests/fm-watch-triage.test.sh (the string was introduced by origin/main's 102d710 alongside code that did not yet implement the exemption, so the reference was never live).

The stale pointer is pre-existing text, but c5ce30a is what makes the comment's claim true and adds the test that actually backs it, so the one-line correction belongs here: retarget the reference at tests/fm-classify-decision-key.test.sh's test_plain_pause_retires_the_default_fold_without_hiding_its_origin. Worth fixing because this comment is the only place the paused-exclusion invariant is explained, and a maintainer following the pointer to verify it finds nothing.

🔧 Fix: narrow origin reset to fold transitions after pause
2 issues (1 warning, 1 info) still open:

  • ⚠️ bin/fm-classify-lib.sh:2180 - de9456c narrows the wholesale origins='' reset to the fold's transition vocabulary, but four of the six verbs it kept in that list can only ever fire when the fold is ALREADY empty - i.e. exactly the pause-preserved state c5ce30a exists to protect - so the same failure is still reachable. The list needs-decision|blocked|done|failed|"$resolve"|"$held" is applied under [ -z "$after" ] only, which is NOT the "did THIS line empty the fold" test the new comment claims.

Concrete production sequence, traced by sourcing bin/fm-classify-lib.sh at de9456c and calling _fm_status_open_decision_origins + status_span_first_actionable_record <f> 0 rec needs (kind=code):

  1. needs-decision [key=deploy]: ship it? -> open={deploy}, origins=[deploy>1]
  2. blocked: cannot reach the registry -> open={deploy,default}, origins=[deploy>1, default>2]
  3. paused: waiting for the window -> fold retires default; after={deploy} is non-empty so no reset; origins still holds default>2 (the c5ce30a exemption working as designed)
  4. resolved [key=deploy]: yes, ship -> fold drops deploy, after=''; verb=resolved is in the list -> origins='', wiping the default origin the pause preserved.
    Measured result at step 4: origins=[], rc=1, needs=0, no event - the worker's live blocker is invisible to the watcher. Drop the pause (same 3 lines without paused:) and the blocker survives: origins=[default>2], rc=0, record=blocked: cannot reach the registry. So the regression is specific to the pause path the commit claims to have fixed.

Same outcome, verified, for captain-held [key=...] after a pause (origins=[], rc=1) and for a bare resolved [key=other] after a pause. Step 4's writers are ordinary production paths: bin/fm-send.sh:682 emits resolved [key=$k]: ... when the captain answers a keyed decision, bin/fm-captain-hold.sh:1689 emits captain-held [key=$key]: ... on a verified transfer. The two tests de9456c added never open a second keyed decision, which is why this ships unnoticed.

The defect lives inside the component the fix round introduced, and the smallest honest remedy is to REMOVE the part of it that is not required rather than harden it: needs-decision|blocked can reach the reset only when the fold ignored the line and the open set was already empty, and "$resolve"|"$held" are already handled correctly per-key by the explicit arm at line 2199 (_fm_open_set_has "$after" "$key" || _fm_decision_origin_drop). All four are no-ops except when they destroy a pause-preserved origin. Narrowing the arm to done|failed) origins='' ;; is a one-line edit that adds no state. I patched a copy of the library with exactly that and re-ran every input: the keyed-resolve, keyed-held and unrelated-key cases now keep origins=[default] and report the blocker, while all three assertions of the new test_a_terminal_after_a_pause_still_retires_the_preserved_origin still hold (done: shipped it anyway, failed: gave up on the registry and unkeyed resolved: registry is back each retire the origin, via the explicit done)/"$resolve" arms and the ship/scout wholesale wipe), and the three narration cases plus a pause-then-resume-then-reblock sequence are unaffected.

Not a regression against the merge parent (929a366 cleared origins on every pause), so this is an incomplete fix rather than new breakage - but the commit's stated invariant still does not hold.

  • ℹ️ bin/fm-classify-lib.sh:2163 - The comment block de9456c added is now the authoritative statement of this invariant, and its central claim is false: "The wholesale reset below asks whether THIS line emptied the fold, not merely whether the fold is empty". The condition at line 2179 is still [ -z "$after" ] - it never inspects $open, so it cannot distinguish "this line emptied the fold" from "the fold was already empty". Verified: a resolved [key=other] line that folds to no change at all still triggers the reset, because after was already empty.

That gap is precisely the mechanism of the finding above, and a maintainer trusting this comment would conclude the pause-preserved origin is safe when it is not. Correcting the arm to done|failed also requires rewriting this sentence to say what the code does test (the verb, not a fold transition); the two edits belong together.

🔧 Fix: reset origins only on wholesale terminal fold wipe
2 issues (1 warning, 1 info) still open:

  • ⚠️ bin/fm-classify-lib.sh:2182 - 03f2590's narrowed reset does close the round-2 defect I verified (keyed resolve/held after a pause now leave another key's blocker actionable), but the SAME failure class is still reachable, because the condition tests $verb alone and never $kind - which is exactly the gate its own commit message ("reset origins only on wholesale terminal fold wipe") and its new comment at line 2163 claim.

Concrete production sequence, traced by sourcing bin/fm-classify-lib.sh at HEAD and calling _fm_status_open_decision_origins + status_span_first_actionable_record <f> 0 rec needs with a sibling .meta holding kind=secondmate:

  1. needs-decision: should I deploy to prod? -> open={default}, origins=[default>1]
  2. paused: waiting for the window -> fold retires the OPEN DECISIONS row, after=''; c5ce30a's exemption keeps origins (working as designed)
  3. done [key=child-outcome-w1-done-abcd1234]: child w1 done: landed -> _fm_decision_fold_line does NOT wholesale-wipe (kind=secondmate fails its done:ship|done:scout|failed:ship|failed:scout arm) and _fm_decision_line_retires_default is false (the key is not default), so it returns $open unchanged = '' -> after=''. verb=done hits line 2182 -> origins=''.
    Measured at step 3: origins=[], needs=0, and the span carries only the relayed child line - the captain's unanswered question is invisible to the watcher, which is precisely what the comment at lines 2145-2160 says must not happen. Drop the pause and it survives (origins=[default>1], the question in the span), so the loss is specific to the path c5ce30a claims to fix.

Step 3's writers are ordinary production paths, not hypotheticals: bin/fm-inactive-reconcile.sh:458-465 publishes <state> [key=child-outcome-<child>-<state>-<fp8>]: child <id> <state>: <note> onto the secondmate's parent channel via fm_parent_channel_report (state is done or failed), and bin/fm-secondmate-report.sh:104 writes done [corr=<16hex>]: <note> (via-helper) onto the same file. I measured both, plus failed [key=child-outcome-...]: all three give origins=[] after a pause and origins=[default>1] without one. This also falsifies the standing promise at lines 2146-2151 that a correlation-marked done "never prunes an origin here either", and it re-opens the secondmate own-line-vs-relayed-child-line theme the round-6 decision settled with "treat every keyed secondmate terminal as a relay, not its own".

Not a regression against upstream: _fm_status_open_decision_origins at upstream 6f0f139, at the merge 650dfd1 and at the merge parent 929a366 all carry the unconditional [ -n "$after" ] || origins='', which wipes in strictly MORE cases. So this is an incomplete fix, not new breakage - but the commit's stated invariant does not hold.

Two materially different remedies, which is why this is ask-user rather than a third mechanical repair:
(a) Gate the reset on the wholesale wipe the comment already describes - read the unstamped copy (as _fm_decision_fold_line itself does) and fire only for done:ship|done:scout|failed:ship|failed:scout with a colon present, ideally via a shared predicate both readers call, the way _fm_decision_line_retires_default is already shared between them. I patched a copy with exactly that and re-measured every case: the three relay cases above now keep origins and surface the blocker/question; round 3's new resolved [key=deploy]/captain-held [key=deploy] assertions, round 2's three narration cases, round 1's pause-only case, and done:/resolved: after a pause all keep their current asserted outcomes. One fixture needs adjusting: tests/fm-classify-decision-key.test.sh:165 test_a_terminal_after_a_pause_still_retires_the_preserved_origin writes no .meta, so _fm_status_kind returns unknown and its failed: gave up on the registry case currently passes only because the fold happened to be empty; give that fixture a kind=ship meta and all three of its trailing lines pass under the gate (verified). Note the same coincidence already makes today's behavior inconsistent: add one open keyed decision and failed: stops retiring the origin at HEAD too.
(b) Take the fallback the intent already authorizes. The User intent records: "firstmate recommended one final bounded fix round with an automatic fallback to upstream's secondmate behavior if the theme recurs; the captain answered: 'I agree with your rec'." This is the third round on this mechanism (c5ce30a, de9456c, 03f2590) and the theme has recurred, so reverting all three to upstream's unconditional [ -n "$after" ] || origins='' is an authorized, in-scope option - and the round-6 decision established that "the reconciliation target is upstream's behavior, not this fork's pre-merge behavior". The cost is that a paused worker's blocker goes back to being invisible to the watcher, the harm c5ce30a was written to stop.

My recommendation is (a): it is a one-line correction to the condition already written plus one test fixture, it adds no state, it makes the code match both comments, and it preserves the captain-facing behavior the user selected in rounds 1 and 2. ask-user because either direction changes user-visible watcher reporting and (b) would reverse two recorded decisions.

  • ℹ️ bin/fm-classify-lib.sh:2163 - The comment block 03f2590 added is now the authoritative statement of this invariant, and its central claim is false: it says the reset "exists for the one retirement the per-key arms further down cannot model: _fm_decision_fold_line discards the WHOLE open set on a ship's or scout's done/failed declaration". The condition at lines 2181-2183 inspects $verb only - it never reads $kind, and never checks that the line carries the colon the fold's wholesale arm requires - so it also fires for a secondmate's or unknown-kind done:/failed: line, including the keyed and correlation-marked relays the fold deliberately refuses to let retire anything. Verified by execution: done [key=child-outcome-w1-done-abcd1234]: ... on a kind=secondmate log clears the whole map.

The older comment directly above (lines 2146-2151) is falsified the same way - it promises "this origins map only drops on resolve/held and on a done line that _fm_decision_line_retires_default accepts - so a correlation-marked done ... never prunes an origin here either" - and a maintainer trusting either sentence would conclude the pause-preserved origin is safe when it is not.

This is the mechanism of the finding above, so the two belong together: taking remedy (a) there makes both sentences true as written and needs no comment edit; if that remedy is declined, both sentences must instead be rewritten to say what the code does test (the verb alone, for every kind).

🔧 Fix: revert origin preservation to upstream's unconditional reset
3 issues (1 error, 2 warnings) still open:

  • 🚨 bin/fm-classify-lib.sh:2167 - This round's revert (f177b1d) restores upstream's unconditional [ -n "$after" ] || origins='' while the fork's paused retirement arm in _fm_decision_fold_line (line 786) stays in place. The combination loses an unkeyed blocked: followed by a plain paused: from BOTH readers - a state neither merge input has.

Measured by sourcing each library version and folding blocked: cannot reach the release host + paused: waiting for release access:

  • upstream 6f0f139: open_decisions=[default\tblocked\t...], origins=[default\t1], status_span_has_actionable=YES
  • base 6ad419d (the fork's own tip, 929a366's second parent): open_decisions=[], origins=[default\t1], actionable=YES
  • c5ce30a (the author's fix in this run): open_decisions=[], origins=[default\t1], actionable=YES - i.e. exactly base behavior
  • HEAD f177b1d: open_decisions=[], origins=[], actionable=NO

So HEAD is strictly worse than upstream, than the fork's base, and than the commit this round reverted. The concrete harm: status_span_first_actionable_record returns needs=0 with no event for that span, so the watcher classifies a stuck-and-waiting worker as routine and never escalates it; paused: is deliberately not captain-relevant, so nothing is shown as the event that replaced the blocker. bin/fm-brief.sh:335 and bin/fm-dod-lib.sh:260 instruct workers to write exactly this plain unkeyed paused [at=...]: {why} shape, so this is the documented away-mode path, not a hypothetical.

The intent's authorized fallback is "an automatic fallback to upstream's secondmate behavior if the theme recurs". As executed this revert does not reach upstream's behavior - it reaches something worse than upstream's - because only one half of the collision was reverted.

Three remedies, which is why this is ask-user:
(a) Round 3's own recommended remedy, which the user selected: gate the wholesale reset on the same wholesale wipe _fm_decision_fold_line performs (unstamped line, colon present, done:ship|done:scout|failed:ship|failed:scout), via a predicate both readers share the way _fm_decision_line_retires_default already is. I patched a copy with exactly that and measured: blocked:+paused: -> origins=[default\t1], actionable=YES; needs-decision: + paused: + done [key=child-outcome-w1-done-abcd1234]: ... on a kind=secondmate log -> origins=[default\t1] (the captain's question survives the relay, closing round 3's finding too); needs-decision:+done: -> origins=[] unchanged. This fixes both the paused case and the secondmate-relay case, and needs no test rewrite.
(b) Revert f177b1d, restoring c5ce30a's pause exemption. Restores base and upstream's watcher behavior, but leaves round 3's relay case open.
(c) Go fully to upstream by DELETING paused from _fm_decision_fold_line's retirement arm at line 786 (and then the "$pause" gate addition at line 825 becomes unnecessary - removal of that component rather than hardening it). This restores upstream behavior in both readers, but contradicts fork PR #19's accepted intent, which explicitly named "a later terminal or paused line" as the retirement trigger, and invalidates the assertion in test_plain_pause_retires_the_default_fold_in_both_folds that this same fix round added.

I recommend (a): it is the remedy the user already chose in round 3, it is the only one that closes both failure paths, and it keeps every current assertion green.

  • ⚠️ docs/architecture.md:194 - This line is the repository's authoritative statement of the invariant the finding above shows is violated, and it is now false: "the supervisors' actionable-event map (bin/fm-classify-lib.sh's _fm_status_open_decision_origins) retires that bucket on the crew's own later plain unkeyed done: line but never on a paused one ... which would otherwise make a worker that appended blocked: and then paused disappear from supervision the moment the wait was declared, while OPEN DECISIONS still retires the row."

At HEAD the map does retire on a paused line (measured above), and the worker does disappear from supervision - the doc describes the harm as the reason for a guard that no longer exists. It was true one commit earlier at c5ce30a and false again after f177b1d. The doc also states the OPEN DECISIONS half correctly, so it is the clearest existing record of which of the two readers is meant to retire on pause.

Taking remedy (a) or (b) above makes this sentence true again with no doc edit. Taking remedy (c) requires rewriting it, because under (c) OPEN DECISIONS would no longer retire the row either. ask-user for the same reason as the finding above: the remedy depends on which behavior the author wants, not on a mechanical correction.

  • ⚠️ tests/fm-watch-triage.test.sh:235 - The only assertion covering the invariant in the first finding was rewritten inside this branch's merge resolution, so nothing now fails when the paused blocker is dropped.

Both inputs to merge 929a366 assert the paused: form:

  • base 6ad419d (second parent): printf 'blocked: cannot reach the release host\npaused: waiting for release access\n' ... || fail "a blocked: event hidden behind a current wait was classified routine"
  • branch tip 9871bf2 / upstream 6f0f139 / merge 650dfd1: identical assertion

HEAD carries the resolution's rewrite instead - the input changed to working: waiting for release access and the failure message to "hidden behind routine work", with a new comment ("Use a working line: a plain paused declaration intentionally retires the shared default bucket") justifying it. git log -S'working: waiting for release access' -- tests/fm-watch-triage.test.sh finds no non-merge commit, so the rewrite exists only in 929a366's conflict resolution; it converted a failing behavior into a passing weaker test rather than resolving the collision between upstream's unconditional origins reset and the fork's pause retirement arm. The working: variant it was replaced with is already covered by the needs-decision + working: case 18 lines above, so the rewrite is pure coverage loss.

Remedy: restore the paused: input and its original failure message once the first finding is resolved - under remedies (a), (b) or (c) it passes. ask-user because it reverses a deliberate resolution decision made in the merge and is coupled to which remedy is chosen above.

🔧 Fix: drop paused retirement to match upstream in both readers
3 issues (1 error, 2 warnings) still open:

  • 🚨 bin/fm-brief.sh:345 - The worker-facing brief still states the rule this commit deleted. Lines 345, 454 and 549 all tell every dispatched worker: "An UNKEYED one folds under a shared default bucket naming no particular decision, so it retires the moment you append any later plain unkeyed done: or $PAUSED_VERB: line - your own later status supersedes it silently". After 1eb7076 a paused line retires nothing in either reader.

Provenance: git log -S puts that sentence at 102d710, "fix(bin): give unkeyed open decisions a retirement route and a printed resolve key (#19)" - the same fork PR whose paused-retires behavior the round-4 decision dropped in favour of upstream. It is absent from upstream 6f0f139 and from merge 650dfd1, so it is fork text that the remedy missed. The fix round's own new test comment (tests/fm-classify-decision-key.test.sh, "bin/fm-brief.sh and bin/fm-dod-lib.sh both instruct a worker going away to append exactly this plain unkeyed paused [at=...]: {why} line") cites fm-brief.sh as the authority for the paused shape while leaving fm-brief.sh asserting the opposite rule.

Concrete reachable sequence, measured against HEAD by sourcing the library: a worker appends blocked: cannot reach the release host; the blocker later clears on its own and the worker goes away with paused [at=...]: waiting for release access, believing per line 454 that its unkeyed row has retired. At HEAD status_open_decisions still returns default\tblocked\tcannot reach the release host and status_span_first_actionable_record returns the blocked record - so the captain's OPEN DECISIONS keeps the stale row and the watcher keeps escalating the worker as actionable, indefinitely, until someone appends a resolved: or a plain unkeyed done: line that the brief no longer identifies as the only route out.

Remedy: delete or \$PAUSED_VERB`/or `$PAUSED_VERB:`from the three paragraphs (345, 454, 549) so the plain unkeyeddone:` line is the only retirement route named, matching the code. No test asserts the sentence, so nothing else moves. auto-fix: this corrects a statement the same change falsified, in the direction the round-4 decision already settled; it raises no new question about author intent.

  • ⚠️ docs/architecture.md:107 - The fix round rewrote line 194 exactly as instructed but left line 107, so docs/architecture.md now contradicts itself on the same rule. Line 107 still says the shared unkeyed bucket "names no specific decision, so it also retires when the crew's own later plain unkeyed done: or paused line lands"; line 194 now says it is retired "by the crew's own later plain unkeyed done: line in both readings ... and by a paused: line in neither". Line 194 is the one that matches the code I executed.

Line 107 is the more load-bearing of the two for a reader: it documents the OPEN DECISIONS section that fm-wake-drain.sh prints on every presentation and the --resolve-key default footer a captain uses to close such a row, i.e. exactly the surface whose behavior changed. Remedy: drop or paused from line 107.

  • ⚠️ bin/fm-classify-lib.sh:1205 - Six comment sites in the changed file still state the deleted rule, two of them documented contracts rather than incidental prose:

  • Line 1205 is the FM_OPEN_DECISIONS_FOLD_VERSION=10 rationale: "10: a plain (no correlation token) done/paused line now also retires the shared &fix(bin): report wake drain presentation failures on stdout #34;default&fix(bin): report wake drain presentation failures on stdout #34; bucket, so a cursor persisted under version 9 must be discarded". That numbered list is the documented meaning of the persisted cursor version, and item 10's stated rule is now half wrong. No cursor is actually mis-trusted - I checked the value at every relevant revision (upstream 6f0f139 = 9, fork base 6ad419d = 6, introduced as 10 by the merge 929a366 itself), so version 10 exists only on this unmerged branch and 10 invalidates both 6 and 9 - and the bump is still warranted, because HEAD's fold still retires on a plain done: where upstream 9 does not (measured: kind=task needs-decision: + done: yields an empty set at HEAD, an open default row upstream). Only the stated reason is stale.

  • Line 1032 is status_key_closing_verb's own contract: "For the shared &fix(bin): report wake drain presentation failures on stdout #34;default&fix(bin): report wake drain presentation failures on stdout #34; key alone the closing verb may also be a plain done or the configured paused verb, because a plain unkeyed line of either retires that bucket". Paused no longer does.

  • Lines 435, 438, 440 and 468 repeat "done/paused" in the key-grammar block that the code's own retirement arm at 787 now explicitly contradicts ("A paused line retires nothing").

Remedy: drop /paused and or the configured paused verb at 435, 438, 440, 468, 1032 and 1205, leaving the plain done line as the sole retirement route. Mechanical, no behavior change.

🔧 Fix: name plain done as sole default-bucket retirement route
2 issues (1 warning, 1 info) still open:

  • ⚠️ bin/fm-classify-lib.sh:1207 - The fix round changed _fm_decision_fold_line semantics (1eb7076 removed the paused verb from both the accepted-verb gate at line 765 and the retirement arm at line 783) but left FM_OPEN_DECISIONS_FOLD_VERSION=10. The block directly above states the rule this breaks: "FM_OPEN_DECISIONS_FOLD_VERSION must be bumped whenever _fm_decision_fold_line semantics change, so persisted state from an older interpretation is discarded and rebuilt from byte 0" (line 1139). Instead of bumping, 9c87acc rewrote item 10's prose to describe the NEW rule, which silently reinterprets cursors already persisted under version 10's OLD rule.

Reproduced concretely, not inferred. In a temp state dir with kind=secondmate, status log blocked: cannot reach the release host + paused: waiting for release access:

  • Sourced the branch's pre-fix library (git show 929a366:bin/fm-classify-lib.sh) and called status_open_decisions_incremental. Under the old rule the paused line retired the default bucket, so it wrote .t.open-decisions-cursor = version=10:secondmate / offset=74 / ident=strong:... with an EMPTY open set.
  • Appended working: retrying the release host and ran HEAD's library on the same file:
    status_open_decisions -> default\tblocked\tcannot reach the release host
    status_open_decisions_incremental -> `` (empty)
    The version matches, the ident matches, the file only grew, so no invalidation signal fires and the stale empty set is trusted forward forever; no later append can reopen the row because the blocked line is behind the cursor.

That is the exact surface the change is about: bin/fm-wake-drain.sh:444 builds the captain-facing OPEN DECISIONS section through scan_open_decisions_snapshot -> status_open_decisions_incremental (line 1841), so a real blocker stays invisible to the captain on every presentation while the whole-file fold says it is open. It also breaks the invariant the library documents at line 1129 ("the two strategies can never disagree on what is open") and that assert_fold pins in tests/fm-classify-decision-key.test.sh.

Reachability is narrow but real: it needs a state directory folded by this branch's code between merge 929a366 and 1eb7076. It is also a direct violation of the file's own stated invalidation contract, independent of who holds such a cursor today.

Remedy: set FM_OPEN_DECISIONS_FOLD_VERSION=11 and add an item-11 line recording that a paused line no longer retires the shared "default" bucket, so cursors folded under version 10's paused-retiring reading are discarded and rebuilt from byte 0. Revert item 10's prose to describe what version 10 actually meant. auto-fix: this corrects an omission in the fix round's own change, adds no new state or machinery (the versioning mechanism already exists), and is invisible to users beyond one extra full re-fold.

status_key_closing_verb's per-line filter (lines 1062-1070, taken verbatim from upstream 6f0f139) lets a line reach the fold only when event:kind is one of done:ship|done:scout|failed:ship|failed:scout, or when the verb is needs-decision|blocked|$resolve|$held. A plain done: on any other kind hits *) continue, so the fold never sees it and default never closes in this reader.

Measured on needs-decision: ship without migration? + done: shipped, varying only the sibling .meta kind:
ship -> status_open_decisions=[] status_key_closing_verb default=[done]
secondmate -> status_open_decisions=[] status_key_closing_verb default=[needs-decision]
task -> status_open_decisions=[] status_key_closing_verb default=[needs-decision]
Upstream's own test pins the secondmate answer: tests/fm-classify-decision-key.test.sh:693 asserts expected=blocked for kind=secondmate / want=default / terminal=done. And the one case that does report done (ship) is the ship/scout terminal rule closing EVERY key, not the "default key alone" exception the sentence credits.

No behavior consequence today: the sole production caller, bin/fm-captain-hold.sh:1834, only tests = "$resolve", and both done and needs-decision fail that test identically. This is a stated function contract that is false, in the exact comment the last commit authored, so a later reader using it to decide whether default is still open would be misled.

Remedy: replace the sentence with one that matches the code - the reported closing verb is resolved or the captain-held verb, plus a ship/scout terminal done/failed which closes every key; the fold's plain-done default-bucket retirement is deliberately NOT reflected here, because the per-line filter drops plain done on other kinds.

🔧 Fix: bump fold version 11 and correct closing-verb contract
4 issues (1 warning, 3 infos) still open:

  • ⚠️ .agents/skills/afk/SKILL.md:70 - The round-5/6 sweep removed paused-retirement promises from the three bin/fm-brief.sh worker paragraphs and docs/architecture.md lines 107/194, but missed a fourth site, and this one is a live operational instruction rather than prose: "An UNKEYED blocker is retired by that fold on the worker's own later plain unkeyed done: or paused line rather than by any proof it cleared ... so an away-window blocker that must hold the return needs its own [key=<slug>]."

1eb7076 removed the paused verb from both the accepted-verb gate (bin/fm-classify-lib.sh:765) and the retirement arm (:783), so the paused half is now false. Verified by executing the real library: blocked: cannot reach the release host + paused: waiting for release access yields default\tblocked\tcannot reach the release host from status_open_decisions AND status_open_decisions_incremental at HEAD.

This is not a comment about internals. The sentence describes the exact gate it sits next to, and bin/fm-afk-return.sh:186 implements that gate by reading status_open_decisions and keeping every row whose verb is blocked (:191). So the away agent is told that a pause retires an unkeyed blocker and therefore that keying is needed to hold the return, when in fact the unkeyed blocker now survives a pause and holds the return by itself.

Provenance: the sentence is the fork's own (absent from upstream 6f0f139, whose text reads "keeps every open blocked: event until that blocker's own resolution is proven"), present at base 6ad419d:72 and carried through the merge at 929a366:70. Remedy: drop or paused from the sentence, leaving the plain unkeyed done: line as the sole retirement route, exactly as the three fm-brief.sh paragraphs were changed. Mechanical, no behavior change.

  • ℹ️ bin/fm-classify-lib.sh:2152 - The comment 1eb7076 rewrote opens with "Deliberately narrower than _fm_decision_fold_line's own retirement arm", but after paused was removed from the fold the two retirement conditions are equivalent, so the stated reason for the whole comment no longer exists, and the same comment's next paragraph now says the opposite ("Neither reader retires anything on a paused line").

Traced both arms over every relevant case. The fold (:790) drops default when _fm_decision_line_retires_default "$key" "$line". The origins map (:2194) drops the origin when that same predicate holds AND ! _fm_open_set_has "$after" "$key". Case by case: keyed done -> predicate false in both, no drop; correlation-marked done -> predicate false in both, no drop; plain done with default open -> fold drops the row, after therefore lacks default, origins drops the entry; plain done with default not open -> fold no-ops, origins' extra guard is satisfied but there is no entry to drop. The extra _fm_open_set_has test never changes an outcome; it only re-confirms what the fold just did on the same line. The asymmetry the word "narrower" named was the paused verb, which only the fold used to accept.

No behavior consequence - this is comment text only. Remedy: replace the "deliberately narrower" framing with a statement that the two readers now retire the shared bucket on exactly the same predicate (a plain, correlation-token-free, unkeyed done line) and neither retires on a paused line, which is what the rest of the comment already says.

  • ℹ️ bin/fm-classify-lib.sh:1210 - The item-11 note 3c81e1c added reads "a paused line no longer retires that bucket - only a plain done one does, matching upstream". The paused clause is true, but "matching upstream" sits immediately after the plain-done clause, and upstream does not retire the shared "default" bucket on a plain done line at all.

Measured, not inferred. Sourced git show 6f0f139:bin/fm-classify-lib.sh and folded needs-decision: should I deploy? + done: shipped: upstream reports default\tneeds-decision\tshould I deploy? for kind=task AND kind=secondmate, i.e. still open. HEAD reports an empty set for the same input. Structurally, upstream's _fm_decision_fold_line accepts only needs-decision|blocked|$resolve|$held (its sole done handling is the ship/scout terminal wipe, which closes every key rather than the default bucket); the done) retirement arm at :783 and the |done in the verb gate at :765 exist only on the fork side, inherited from base 6ad419d.

That matters for this file specifically: the numbered version list is the record a future reconciliation reads to decide which fold rules are the fork's own and which came from upstream, and this entry credits upstream with the one fold rule that is still fork-only. No behavior consequence. Remedy: attach "matching upstream" to the paused clause alone, and say that the plain-done retirement of the shared bucket remains the fork's own divergence from upstream's fold.

  • ℹ️ tests/fm-classify-decision-key.test.sh:122 - test_a_plain_pause_retires_nothing_in_either_reader (added by 1eb7076) is invoked on the line immediately after its own definition, while every other test in the file is invoked from the single list further down (:443 onward), which 1eb7076 did edit in the same hunk to rename test_unkeyed_blocked_retires_after_a_later_terminal_line.

The test does run and its assertions are correct - I reproduced its expected outcome directly against the library (both folds report default\tblocked\t…; status_span_first_actionable_record returns rc 0 with the needs-decision marker set). So this is wiring, not coverage: the file's convention is one invocation block, and the next person who notices the new test is missing from that block will add it there and silently run it twice. Remedy: delete the inline call at :122 and add the name to the invocation list beside the test it was written to complement.

🔧 Fix: correct stale paused-retirement claims and test wiring
1 info still open:

  • ℹ️ bin/fm-classify-lib.sh:1211 - The version-11 migration note states the stale-cursor direction backwards. It reads: "a cursor folded under version 10's paused-retiring reading holds a bucket this reading keeps open and must be discarded." A version-10 cursor cannot hold that bucket - version 10 is precisely the reading that RETIRED it on the paused line, so its persisted open-set has already dropped the row. Version 11 is the reading that keeps it open, so the stale cursor is MISSING a row, not holding an extra one.

Measured, not inferred. Sourced git show 929a366:bin/fm-classify-lib.sh (the version-10 reading) and folded needs-decision: should I deploy? + paused: waiting for kind=task: status_open_decisions_incremental returned the empty set, i.e. the persisted cursor dropped default. HEAD returns default\tneeds-decision\tshould I deploy? for the same input. So the discard is needed because the old cursor UNDER-reports, which is also the more consequential direction: an open captain-facing decision would stay invisible until the cursor is rebuilt, whereas the sentence describes a harmless extra row.

No behavior consequence - the bump itself is correct and I confirmed both status_open_decisions_incremental (line 1294) and status_open_decisions_cursor_offset (line 1892) compare against FM_OPEN_DECISIONS_FOLD_VERSION:$kind and reset to offset 0 on mismatch. The defect is only in the record a future reconciliation reads to understand why each version was spent, which is the same record round 7 corrected for its upstream-attribution error. Remedy: state that a version-10 cursor has already dropped a bucket this reading keeps open, so it must be discarded and rebuilt from byte 0. The rest of the entry (the paused/upstream attribution and the fork-only plain-done divergence) is accurate as written - I verified upstream 6f0f139's fold accepts no done verb outside its ship/scout terminal rule.

⏭️ **Test** - skipped

Step was skipped.

⚠️ **Document** - 1 info
  • ℹ️ docs/architecture.md:107 - Judgment call left unchanged: the unkeyed default-bucket retirement rule now appears twice in this document - as a clause inside the wake-drain OPEN DECISIONS paragraph (:107) and as the full cross-reader rule in the shared-classification-library section (:194, the owner). This run's fix rounds had to update both copies when paused retirement was dropped, which is the duplication signal. I left :107 in place rather than reducing it to a pointer, because removing the clause would make its own sentence ("keeps surfacing until it is explicitly resolved") misleading for the captain-facing reader, and a pointer-plus-summary would restate nearly the same words. Both copies are correct against HEAD today. If a follow-up wants one owner, the right move is to trim :107 to the --resolve-key/default fact and let :194 own retirement, which is a small consolidation rather than anything this change requires.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Rangezi and others added 30 commits September 14, 2026 11:23
… asked, not declined (kunchenguid#4387)

* fix(bin): read Claude Code's default external-imports flags as never asked, not declined (kunchenguid#4378)

fm-claude-trust.sh refused the whole trust registration whenever the project-root entry
carried hasClaudeMdExternalIncludesApproved === false, on the premise that Claude Code
writes that value only on an explicit "No, disable". Claude Code's default project
entry carries Approved and WarningShown both false before the dialog is ever shown, so
every such project refused every spawn.

Only Approved === false with WarningShown === true — the pair the dialog writes on a
decline — now counts as a decline. false/false behaves like an absent flag: trust is
registered and no import consent is manufactured.

New case test_project_root_entry_default_import_flags_are_not_a_decline fails on
b182d0f with the refusal and passes with the fix; tests/fm-claude-trust.test.sh 31/31,
bin/fm-lint.sh clean with pinned ShellCheck 0.11.0 and actionlint 1.7.12.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* no-mistakes(review): Correct harness doc's external-imports decline predicate

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…chenguid#4445)

* fix(brief): keep operator address out of composed intent

Teach raw-word authoring for intent sections and mid-task relays, with a neutral [captain] provenance marker for legacy mixed tasks. Keep headings and contract prose outside the serialized intent body.

The legacy selector already excluded the old speaker labels from its output; preserve that read compatibility. The reproduced leak comes from adding labels inside a modern intent body, not from the legacy selector. Do not scrub actual request content.

Add exact serialized-input and generated-contract regressions, retaining refusal of unmarked legacy tasks and coverage of scout promotion.

Fixes kunchenguid#3882

* no-mistakes(review): Refuse operator-address lines in Captain's intent body

* no-mistakes(document): Document operator-address refusal in intent contract comments
…as a proven empty composer (kunchenguid#4455)

* fix(composer): accept Grok title overhang

* no-mistakes(review): summary: named Grok overhang constant, doc caveat, restored tmux typed-title coverage
…ailure (kunchenguid#4474)

* fix(bin): recover Claude auto-arm after timeout

* no-mistakes(document): Add host-timeout signal coverage to autoarm test-coverage list
* fix(spawn): establish Claude task channel authority

* no-mistakes(document): Document Claude task-worker control-channel trust in harness-adapters reference
…or pending text (kunchenguid#4458)

* fix: guard relaunch exit against pending input

* no-mistakes(review): Verifying test run in progress

* no-mistakes(document): docs(agent-control): document exit's composer-empty fail-safe guard

* no-mistakes(ci): fixed 2 tests broken by approved do_exit fail-safe change (empty-only composer gate). herdr-smoke test's sleep-stand-in never renders a real composer -> updated assertion to expect "not proven empty" refusal instead of stale "did not stop" msg. secondmate-restart fake tmux capture-pane returned bare '> ' glyph (never valid empty proof) -> changed to bordered empty box matching fm-control-relaunch fixture. all 4 related suites pass locally now
…unchenguid#4460)

* fix: reconcile diverged secondmate updates

* no-mistakes(document): Fix stale fm-update.sh/fm-ff-lib.sh purpose lines in docs/scripts.md

* no-mistakes(document): docs: reflect secondmate divergence reconcile in README/SKILL.md
…d#4497)

* fix(dispatch): support Codex Luna max effort

* no-mistakes(review): use portable CODEX_HOME path in codex effort reference
kunchenguid#4498)

* feat(calm): render smooth Unicode swell

* feat(calm): make sails asymmetric

* feat(calm): use quarter sail glyph

* no-mistakes(review): docs: sync calm feasibility sprite passage with approved renderer

* no-mistakes(document): docs: sync calm wave phase doc comment

* no-mistakes(ci): CI の Lint 失敗は tests/fm-calm-pi-extension.test.sh の test_interactive_terminal_e2e 関数で `boat_narrow_sails` が local 宣言に残っていたことによる ShellCheck SC2034 でした。関数内での参照を確認したところ、狭幅端末の検査は boat_narrow_previous / boat_narrow_direction / boat_narrow_reversed に移行済みで、boat_narrow_sails は代入も参照も一切ありませんでした。そのため local 宣言からこの 1 語のみを削除しました(3315 行目)。Calm の描画実装、他のテストアサーション、ドキュメントは変更していません。検証: bin/fm-lint.sh(ローカル変更ファイルモード)exit 0、CI 相当の `shellcheck --norc --external-sources tests/fm-calm-pi-extension.test.sh` exit 0(SC2034 解消)、`bash -n` 構文チェック通過、actionlint 1.7.12 でワークフロー 3 件 valid。
kunchenguid#4491)

* fix: supersede scout delivery brief on promotion

* fix: preserve ship safety contract after promotion

* no-mistakes(document): Document fm-promote.sh now supersedes brief.md on relaunch
…d stop cleanup dropping accents from a held body (kunchenguid#4471)

* fix(bin): let captain holds work on hosts with an older JSON::PP

Holding a task for the captain, and the cleanup that keeps a captain-held row
open, both fail outright on any host whose JSON::PP defaults allow_nonref off -
2.27202 on a Linux desk is one. Both read a task's body back with `decode_json`,
but tasks-axi shows a scalar field as a JSON-encoded bare string, and an older
library rejects that whole value with "must be object or array".

The consequence is fleet-wide on such a host, not one broken command: a worker
there cannot formally record a decision for the captain at all. It can only
mention the decision in passing in a status line, where it can be missed - which
is how a real decision goes unrecorded. The hold reports that the task lost its
hold-set stamp; the cleanup cannot return the row to Queued.

Both call sites now ask for allow_nonref explicitly rather than inheriting
whatever the installed library defaults to. The second one is worth naming: its
`/\A"/` guard reads as deliberate, but a leading quote is exactly the bare-string
case that fails, so the guard selects for the failing input rather than
protecting against it.

The regression case forces the older default back off for every perl the commands
spawn, then drives both paths - holding a task that carries a body, and tearing
down a captain-held row whose deliverable must still be appended. It also probes
that the simulation genuinely rejects a bare scalar, so the case cannot pass
vacuously on a lenient host. Each half was verified failing on its own unfixed
call site with that site's real error message. Suites: fm-captain-hold-lifecycle
51 cases, fm-backlog-atomicity 99 cases, 0 failures.

Verification limit: the mechanism is reproduced and tested, but neither fix is
verified against a real JSON::PP 2.27202 host, because none is in the loop. This
laptop runs 4.06, where the bug does not manifest.

`bin/fm-procevent-lavish.sh:471` was checked and left alone - it matches a
brace-delimited object before decoding, so allow_nonref never applies.

* fix(bin): stop cleanup silently dropping accented characters from a held body

Cleanup rewrites a captain-held row's body to append the finished work's
deliverable, and the decoder it reads that body with printed decoded characters
to a stream with no `:raw` layer. A character at or below U+00FF then came out
as one latin-1 byte instead of two UTF-8 ones, so a body reading "café" lost the
accent. `fm_backlog_retain` writes that body straight back through
`--body-file`, and nothing reported an error - the character was simply gone
from a row still waiting on the captain.

The decoder now writes bytes, the same `binmode STDOUT, ":raw"` plus
`utf8::encode` that the sibling decoder in `bin/fm-captain-hold.sh` already
used.

Review of the parent commit found this on one of the lines that commit already
changed. It predates that change.

The test asserts bytes rather than decoded strings, because comparing strings
cannot tell latin-1 from UTF-8. It uses two separate rows on purpose: any
character above U+00FF makes perl print the whole string as UTF-8, so one body
carrying both an accent and an em dash passes even unfixed and proves nothing.
Verified failing before the fix on the accented row, passing after. Suites:
fm-captain-hold-lifecycle 52 cases, fm-backlog-atomicity 99 cases, 0 failures.

* no-mistakes(document): record body-decode regression proofs in captain-hold lifecycle doc

* no-mistakes(review): drop whole-file UTF-8 check from retained-body test

* no-mistakes(review): correct stale JSON::PP fleet-host claim in lifecycle doc

* no-mistakes(review): anchor native-reproduction claims per defect in lifecycle doc
…furniture (kunchenguid#4532)

* fix(composer): read codex 0.154's idle starfield and status footer as furniture

codex-cli 0.154.0 animates a braille "starfield" around its idle composer:
on the row above the bold `›` prompt row, on the `›` row behind the SGR-2
dim `Ask Codex to do anything` placeholder, and on the row below it, then
draws a bright status footer (`<model> <effort>[ fast] · <path> · <title>`).
The cells are truecolor greys on both sides of the ghost luminance ceiling,
so the brighter ones survive ghost stripping, and the rows below the glyph
carry no structural edge. The shared classifier selected the bare `›` shape,
extended its wrap region over the two rows beneath the glyph, read the
survivors and the footer as wrapped typed input, and answered `pending`;
the steering doorbell defers on exactly that verdict, so no doorbell ever
reached an idle codex 0.154 pane.

bin/fm-composer-lib.sh now recognises that furniture by shape, declared
once next to the idle placeholders and reached from the two wrap-region
boundary points:
- a row whose non-whitespace content is entirely braille cells
  (U+2800..U+28FF, detected byte-exactly under LC_ALL=C) is furniture: it
  never counts as wrapped typed content and bounds a bare composer's wrap
  region; braille behind the glyph row's content is stripped before the
  emptiness decision when nothing else follows the glyph; a row mixing
  braille with other text stays typed content;
- the codex status footer bounds the wrap region exactly as omp's status
  row does, anchored on the effort token, a spaced middle dot, and a `~` or
  `/` path cell, so a typed `fix · tests` stays composer input;
- `^Ask Codex to do anything$` joins the verified idle-placeholder set; the
  ghost strip remains what proves that row empty, and the bare-row rule that
  bright placeholder text is real input is unchanged.

Unchanged: the strict blank-row rule, the styled=0 degradation (a plain
cmux/orca capture of this screen still reads `unknown`, never `pending`),
FM_COMPOSER_GHOST_LUMA_MAX, and every other harness's shape.

tests/fm-composer-lib.test.sh carries both live Herdr samples byte-for-byte
with the divergence (letters in place of the starfield read `pending`) and
the over-stripping negatives; tests/fm-composer-codex-idle-live-e2e.test.sh
is the default-on live guard (token-free, skips explicitly without codex or
tmux) that launches the installed codex idle and asserts `empty` through
both the tmux and the cursorless styled reads, naming codex --version on
failure. docs/verification/runtime-backends.md records the dated Herdr
evidence: `pending` before, `empty` after, on the captured screen.

* no-mistakes(review): drop unreachable codex footer rule and inert placeholder entry

---------

Co-authored-by: Todd Billings <todd@usdvcapital.com>
* fix(bin): refuse empty text steers in fm-send

A marked secondmate request sent with an empty message delivered only
marker and correlation bytes and minted a pending-reply expectation the
parent could never see resolved, stalling the fleet with no loud error
(kunchenguid#4255). Fail closed on an empty or whitespace-only message on the text
path, mirroring the existing --resolve-key refusal.

* chore: retain ambient Pi-lens autoformat as its own commit

Formatting-only edits produced by ambient Pi-lens autoformat during the
msg-loss investigation, kept separate from the behavioural change in
c23acba so the fix stays reviewable on its own.

AGENTS.md is deliberately excluded: its only autoformat edit stripped the
trailing space from the documented FM_OPERATIONAL_PREFIX value, which
bin/fm-operational-input.sh:28 defines as "FIRSTMATE_OP: " and line 11
records as permanent compatibility. Documenting that constant without its
trailing space makes the doc wrong about the contract, so that one line was
restored rather than retained.
…chenguid#4554)

On rose-pine-moon the two-color water (cyan crests over blue troughs) read as
a pink stripe over aqua, the yellow left sail and mast clashed with the red
right sail, and the hull carried a blue interior run. Every water cell is now
blue so the swell reads through glyph height alone, and both sail halves, the
mast, and the whole hull are one yellow run. Geometry, cadence, animation,
direction flip, resize clamping, and the narrow fallback are unchanged.

Update the unit and real-TUI color assertions to the new palette and the Calm
docs that described the old one.
…chenguid#4270)

* fix(watch): stop aging a second mate's active turn from its launch

The parent watcher's second-mate wake-loop stall check exempts a mate that
is demonstrably inside an active turn, but secondmate_in_active_turn asked
busy_turn_over_age first and returned "not in a turn" whenever that said
the bound was crossed.

busy_turn_over_age ages from state/<task>.turn-ended, falling back to
state/<task>.meta. A second mate's turns end in its own home, so the
parent never gets a turn-ended mark for it and the fallback ages the
mate's last launch. Every mate launched more than BUSY_TURN_MAX_SECS ago
was therefore permanently "over age", the busy pane was never consulted,
and any turn outstripping FM_SECONDMATE_WAKE_STALL_SECS raised a false
wake-loop stall.

The gate now bounds the busy exemption by <idle> - how long the queue's
drain position has not moved - which is evidence this home actually
holds. A busy mate stays exempt while the queue has been frozen for less
than BUSY_TURN_MAX_SECS, and a mate stuck busy forever still alarms, so
the bound that stops a busy pane from proving liveness forever is kept
rather than removed. busy_turn_over_age is untouched; its remaining
callers are the ordinary crew busy-pane bound.

The regression pins the case that actually broke: a mate whose launch
record predates BUSY_TURN_MAX_SECS and which is demonstrably mid-turn
must not escalate, while the same mate with its queue frozen past the
bound still publishes exactly one notification. The existing coverage
only exercised a freshly launched mate, which passes either way.

Reaching that alert now costs a pane capture inside the gate, so the
three checkpoints in this suite that assert an alert move from a 1s to a
4s bound - the value the neighbouring active-turn cases already use. The
bound is a ceiling, not a wait: the checkpoint returns on the first
actionable wake. On a loaded machine a 1s bound missed the alert
repeatedly; at 4s it did not miss in 20 runs under the same load.

* no-mistakes(review): scope the second-mate active-turn regression test's coverage claim

* no-mistakes(document): fix stale second-mate active-turn comments in fm-watch
…unchenguid#4278)

* feat(bin): add read-only PR blocker and reviewer-discovery commands

Two focused, opt-in commands that read GitHub and never write to it.

fm-pr-state.sh reports what still blocks one pull request from the
author's side: a closed or merged state, draft state, unknown or
conflicting mergeability, absent or failing required checks, and a
blocking CHANGES_REQUESTED decision explained by each reviewer's latest
verdict, marked STALE when it was left at a superseded head. A pull
request that only awaits an approval is not reported as blocked, and
advisory checks are omitted. Every reading is taken against one exact
head; a push that lands mid-read invalidates the whole result rather
than mixing two snapshots.

fm-pr-reviewers.sh suggests reviewers from the most recent commits to
the pull request's exact changed paths, counting each commit once,
resolving handles through GitHub's own commit author.login mapping, and
excluding the author and Bot accounts.

Both stay read-only: no review request, no approval, no merge.
Unresolved review-thread state is left unreported because the REST API
does not expose it and unattended commands may not use GraphQL.

Closes kunchenguid#3731

* no-mistakes(review): accept only PR URLs and stop at terminal state

* no-mistakes(review): report unconfirmed required checks; make URL-only guards discriminate

* no-mistakes(review): stop attributing readings to unverified heads

* no-mistakes(review): narrow readiness contract to checks that have reported

* no-mistakes(review): read the pull request once, drop the head guard

* no-mistakes(document): scope pr-forge isolation proof to its measured members

* no-mistakes(document): record uncovered pr-forge members and their pending proof

* docs(isolation-proof): re-prove pr-forge at its full membership

tests/fm-pr-state.test.sh and tests/fm-pr-reviewers.test.sh joined the
pr-forge family in this branch, and script_allows_concurrency grants
four workers by family membership alone, so both ran concurrently on a
proof measured before they existed.

Re-proved the family at all eight members: two consecutive runs, 0
failures, each begun with the one-minute load average below 6.0 so the
result measures isolation rather than contention. A third run taken
between them is disclosed rather than recorded, because it started
while the previous run's workers were still decaying.

The new durations are not comparable with the six-member measurement
above them, so they are not presented as evidence about the two new
members, and that record's 1.72x four-worker figure is left as a
statement about its own run rather than restated as current.

* no-mistakes(review): disclose gh error-text coupling at its matching site and tests
…uid#2752)

* fix(bin): teach validation-round pauses in briefs

* no-mistakes(document): Point classifier comments to authoritative pause examples
…guid#4510)

* fix(teardown): refuse a cleanup whose endpoint close failed

bin/fm-teardown.sh discarded both the exit status and the stderr of every
fm_backend_kill call, so a close that genuinely failed was indistinguishable
from one that succeeded. Teardown continued past it, deleted the task's durable
records, returned its worktree, and reported the cleanup as completed. The
deleted metadata is the only record of which endpoint belongs to the task, so
such a close did not merely leave a stray session behind, it stranded one:
nothing was left on disk naming it.

The adapters could not carry that signal either. Driven against the real code,
every backend arm returned 0 for a genuine failure exactly as it did for an
already-exited endpoint, so there was nothing for the four call sites to
propagate even once they stopped swallowing it.

The tmux arm now resolves a close that did not succeed against the window's
exact recorded identity, since kill-window fails the same way for a window that
is gone and one that is still there. The Orca arm reports a close its missing
CLI never attempted. Both stay silent for an endpoint that is already
legitimately gone, and the remaining arms are unchanged: their close-command
timing cannot be established without the real Zellij, Orca, and cmux binaries,
and a gate that refused ordinary cleanup of an already-exited session would be
worse than the defect. docs/verification/runtime-backends.md records what each
backend can prove.

A reported close failure now reaches teardown's existing retain-and-stop
refusal before the records naming the endpoint are removed, matching where the
Herdr confirmed-gone gates already sit for the same hazard, and the retained
records let a rerun finish once the close works.

* no-mistakes(review): refuse unreadable tmux close re-read; honor --force override

* no-mistakes(review): drop unreachable Orca force arm; prove CLI-absent close

* no-mistakes(document): document endpoint-close refusal in its backend and retirement owners

* no-mistakes(ci): The two reported failing checks are NOT code defects. Both "CI" (run 34935529184) and "Require no-mistakes" (run 34935529206) returned conclusion=action_required with zero jobs and 0s duration (run_started_at == updated_at), which is this repo's workflow-approval gate holding the run before any job starts. No job executed, so nothing in the diff could have caused them; two unrelated branches (fm/captain-hold-json-nonref, fm/presenter-core-l1) show the identical shape in the same time window. Verified the change locally instead: bin/fm-lint.sh clean, bin/fm-test-run.sh --check-coverage ok, and all suites the diff touches pass (fm-teardown-endpoint-safety 25/25 including the five new endpoint-close cases, fm-backend-orca, fm-backend, fm-backend-tmux-smoke, fm-backend-cmux, fm-backend-zellij, fm-backend-herdr). Separately, I found and fixed a genuinely flaky test that the phase rules require me to make deterministic: tests/fm-tmux-agent-liveness.test.sh intermittently failed "an idle shell pane must classify dead" (verdict ambiguous, comms=[bash sleep]). It is selected by --changed for this diff, so it would run against this PR once CI is approved. Root cause, established by instrumenting the pane's process group: the idle window was created by `new-session` with no command, so it inherited tmux's default-shell, i.e. whoever runs the suite. ps on the pane tty showed `-zsh` -> `bash` -> `sleep`, all sharing pgid==tpgid, i.e. the host operator's shell configuration spawning a periodic helper directly into the pane's FOREGROUND process group, which is the one surface the classifier reads. `sleep` classifies as `other`, so fg_other=1 and the verdict became `ambiguous` instead of `dead` whenever that helper overlapped the 10s poll window. Every other window in the suite runs an explicit command via new_window; the idle case was the only one whose process group the host defined. Fix (smallest root-cause, test-only, 1 line + explanatory comment): create the idle window with an explicit bare `/bin/sh` (`-- /bin/sh`), the same shell the neighbouring background case already execs. Its foreground group is now exactly one process (verified: `/bin/sh` alone), so no host configuration can inject into it. This flake is pre-existing and NOT caused by this PR: an interleaved A/B showed base commit da5e658 failing the identical case (2/6 runs) alongside head (3/7 runs), and the diff only extracted the tmux inventory read into a helper with identical semantics while never touching fm_backend_tmux_foreground_comms. After the fix: 8/8 consecutive passes, with lint and the coverage guard still clean. Change left uncommitted in the working tree
* feat(calm): ship the Claude Code Calm and sailboat mod behind the function-hooks flag

Add .claude/mods/firstmate-calm, a Claude Code mod (function-hooks plugin) that
brings Calm to Claude Code: the sailboat replaces the stock working row through a
Raster repainted on the sprite's own tick, and tool, tool-group, mid-turn narration,
and canonically classified operational user rows draw at zero height. /calm is
registered by the hooks module itself and toggles the same per-home config/calm
preference the Pi extension uses, so one choice applies on either harness; rows
redraw retroactively on toggle and stay hidden across claude --continue.

The mod loads only while Claude Code's default-off CLAUDE_CODE_ENABLE_FUNCTION_HOOKS
flag is on. Nothing sets that flag in any settings file, and the plugin carries no
command file, skill, agent, or classic hook, so it is a complete no-op while the
flag is off. The trusted project auto-loads it through an .agents/skills symlink,
the only path Claude Code scans for project plugins.

Extract the working-ship geometry, bounce track, cadences, and freeze/resume state
into a harness-neutral sprite core inside the mod (Claude Code refuses hooks-module
imports from outside the plugin folder) and have the Pi widget paint that core's
frames as standard ANSI, byte for byte as before; the Pi suite stays green. Classify
operational rows through a port of bin/fm-operational-input.sh's classify command
guarded by a corpus parity test against the shell owner.

Tests: portable Node checks (plugin shape, sprite parity with Pi's rendering,
Raster packing, policy, classifier parity), the mod's own claude plugin test suites
behind a default-on wrapper, and an opt-in live TUI guard proving the flag-off no-op,
the moving boat, hidden rows, the persisted toggle, and resume on Claude Code 2.1.272.

Docs: record the version-scoped Claude Code evidence and the three bounded gaps in
docs/calm-mode-feasibility.md, describe the Claude Code contract in docs/calm.md,
and make the shared preference, layout, and contributor notes harness-neutral.

* no-mistakes(review): Preserve colliding final replies and strengthen parser parity

* no-mistakes(review): Preserve final replies and strengthen canonical parity checks

* no-mistakes(review): Require exact function-hooks opt-in before Calm activation

* no-mistakes(review): Clarify Calm module loading and activation boundaries

* no-mistakes(review): Reset Calm presentation state across session starts

* no-mistakes(document): Refresh Calm session lifecycle documentation

* feat(calm): paint the Claude Code working ship in Claude's own theme colors

The captain picked the "Claude native" palette for the Claude Code mod's Raster:
every water cell takes the spinner blue of the active theme family (#93a5ff dark,
#5769f7 light) and the whole boat takes the Claude orange of the stock spinner
(#d77757), one water color and one boat color. The family follows the `theme`
setting's prefix, read at load through $.config.list and re-read on a
config.set of that row, with `auto` and custom themes falling back to the dark
set. The Pi extension keeps its standard ANSI blue and yellow, byte for byte.

Rename the shared sprite's color classes from hue names to `water` and `boat`,
since each harness now maps them to its own colors; geometry, motion, cadence,
and the activation gate are untouched.

Tests cover both palettes' packing and the family rule under Node, and the
plugin kit drives every theme value, a theme change mid-session, the Calm-off
pass-through, and inertness of the menu read while the flag is off. The docs
describe the Claude Code colors and record the guard passing on 2.1.273.

* no-mistakes(review): Use light palette for unresolved Claude themes

* no-mistakes(document): Refresh Claude Calm verification evidence
…kunchenguid#4586)

* fix(watch): honour a declared wait before wedge-escalating a quiet pane

wedge_timer_check escalated on elapsed idle time alone. Nothing asked
whether the worker had already said why its pane was quiet, so a lane
that declared a bounded external wait climbed the escalation ladder for
as long as the wait lasted, and past FM_WEDGE_DEMAND_INSPECT_COUNT every
repeat carried demand-deep-inspection - which by its own wording forbids
re-absorbing on the run-step or pane state, so the supervisor could not
use the evidence that was there either.

The generated brief promises that declaring `paused:` buys the long
recheck cadence instead of a wedge, but the timer was still reachable
while that declaration stood: a crew that declares a wait and then has an
active run or busy pane attributed to it is handed to the timer as
provably-working. The declaration is what the worker said about its own
silence, so it now outranks a liveness verdict that only says something
is running.

The consult runs in the at-threshold branch that was about to escalate,
beside the worktree walk already there, and costs one status-line read.
Either status-line record defers to the same FM_PAUSE_RESURFACE_SECS
recheck the declared-wait absorber already uses, so the wait is still
rechecked and cannot rot invisibly. Which verb declared it decides the
wording, because the two block on different people: a `paused:` wait is
owed by an external dependency and asks the reader to confirm it still
holds, while a `captain-held:` transfer is owed by the captain reading
the recheck and asks them to answer or release the hold. A hold is not
rechecked at all while the away-posture record exists, as on every other
captain-held path, and that absorb arms no throttle so the recheck is
owed in full on return.

A declared clearing time that has already passed stops counting, and a
lane that never declared one keeps the identical escalation schedule,
reason, count and demand-deep-inspection wording, so detection and its
worst-case time are unchanged. The deferral restarts the idle timer
rather than cancelling it, so a lane that stops waiting escalates again
within one threshold.

A lane quiet because its own validation run is parked at a gate awaiting
a human decision is deliberately out of scope: reading that state needs a
signal carrying who the wait is on and what clears it, rather than one
inferred from a parked verdict that also covers gates awaiting the
crewmate itself.

Tests pin both directions for each case and were each confirmed to fail
with the consult removed.

* no-mistakes(document): docs: honour declared waits in stale-escalation docs
* fix(bin): derive passed PR state from PR record

A completed no-mistakes run with outcome=passed does not prove the associated pull request merged or closed. A parked gate can be approved on other evidence, so the old crew-state label could report an open PR as merged and make teardown look safe when unlanded work still exists.

For passed runs, derive the crew-state detail from the run or task PR identity, accept a matching merge-poll retirement receipt as local merged evidence, and otherwise perform a bounded forge read. If the identity is absent or unreadable, report the run as passed with unknown PR state instead of inventing a merged claim.

Fixes kunchenguid#4607

* no-mistakes(review): Add bounded GitLab merge-request state reads

* no-mistakes(review): Preserve network-free inactive crew-state scans

* no-mistakes(document): Document PR record readers in shared library
kunchenguid#4627)

* fix: restore published contribution follow-up (Fixes kunchenguid#4469)

* fix(review): Fix contribution freshness and merge actor routing

* fix(review): Restore issue triage and scope contribution follow-up

* fix(test): test: assert one wake per contribution signal

* fix(document): Document contribution follow-up

* fix: restore truthful terminal delivery evidence

* fix(review): Disclose unsupported contributions and deduplicate watcher wakes

* fix(review): Preserve unmeasured unsupported contributions across Bearings

* fix(review): Deduplicate shared contribution wakes and isolate diagnostics

* fix(ci): Captain, fixed the CI failure by updating the PR-security fake GitHub interface to support the contribution observer’s API reads. Verified with shellcheck, git diff --check, the full contribution suite, and a focused merged-poll retirement reproduction. The full PR-security script was not allowed to complete locally after its expanded observer path made it substantially slower
…nguid#4658)

* fix(bin): make a remote-reply document gap self-clearing and re-attemptable

A remote mate's undelivered document raised a keyed `blocked` decision that
nothing could ever resolve, and any `data/*.md` substring in any mirrored line
was an unconditional fetch instruction. A mate announcing a report it had not
written yet therefore manufactured a permanent, factually false blocker, and
its own explanation of the false alarm manufactured more.

The reader has no permanence vocabulary: a report still being written refuses
exactly like a path that will never exist. So an undelivered document is now a
durable, re-attemptable obligation under `state/remote-replies/<id>.pending-docs`,
re-attempted on the next delta and on the channel's own quiet poll, and retired
with a matching `resolved` line naming the local copy once it arrives. The
cursor still advances and no delta stalls on one bad pointer.

Only a structured `report=data/....md` pointer now offers a document, so a path
merely mentioned in prose - including one under another home's mirror tree,
which is provably not that mate's to serve - is never fetched. Offers are
deduplicated across the whole delta, the escalation names each missing document
once and carries the reader's own reason instead of discarding it, and a
strictly increasing notice ordinal keeps a later escalation from being
swallowed as duplicate bytes. A mirrored line still lands once whichever
pointer form it was first written under.

* no-mistakes(review): Require structured pointer token boundaries

* no-mistakes(review): Unify boundary-safe pointer extraction and rewriting

* fix(bin): identify a mirrored line independently of its delivery state

Two defects in the boundary-safe pointer work.

The at-most-once check compared only the all-remote and all-local renderings
of a line, so it could not recognize a mixed one. A line offering two documents
where only the first was deliverable mirrored as local-plus-remote; once the
second arrived, a cursor-loss whole-log recapture rendered the same line
all-local, matched neither alternate, and mirrored a second time. A line's
identity is now the canonical form every boundary-valid pointer would take once
delivered, derived by the same parser that does extraction and rewriting, so it
no longer depends on which documents happened to be deliverable at the time.

The pointer map was passed to awk through the process environment. A delta may
carry up to the configured 1 MiB bound, and an expanded map of delivered
pointers can exceed the platform's exec argument limit, so awk would fail to
start; because no caller checked, the empty result would have been appended as
blank lines while the cursor advanced past dropped status content. The map now
travels in a file, and every call site checks the exit status and stops the
ingest rather than committing a delta it could not render.

Both passes now run once per stream instead of twice per line.

* no-mistakes(review): Abort ingest when document pointer extraction fails

* no-mistakes(review): Exclude structured cross-home pointers from document transfer

* fix(bin): fail open on an undeliverable remote document instead of tracking it

Narrow the remote-reply document fix to the scope the diagnosis actually
requires, as decided after measuring a simpler alternative.

A document the reader cannot deliver now fails open. The mate's line is
mirrored with its own pointer, the cursor advances, and one unkeyed note
carries the reader's reason. A note never enters the open-decision fold, so it
cannot stand open the way the original keyed block did - which removes the
never-clearing false blocker by construction rather than by resolving it.

That makes the durable self-clearing obligation unnecessary, so it goes: the
per-mate pending-documents record, its notice ordinal and resolved
announcements, and the poll-side retry. Canonical line identity goes too, and
with it a way to silently drop a genuine status line; mirroring is back to
at-most-once on exact bytes. The cross-home exclusion goes as well: under
fail-open a cross-home report= either fails harmlessly or is a nested remote
report this mate genuinely holds, which is now relayed again.

Kept: fetching only on a structured report= pointer, the boundary-correct
parser, the file-based rewrite map, and checked extraction and rewrite exit
status. The parser now scans behind a sentinel byte so a rejected candidate can
no longer give the text right after it a false leading boundary.

The reported incident is covered end to end: a report path announced in prose
before it exists raises no decision, and the report still arrives through the
ledger publisher's structured offer once written.

* no-mistakes(review): Preserve source-line identity across remote reply replays

* no-mistakes(document): Document remote reply transfer and replay semantics

* no-mistakes(lint): Fix staging truncation lint checks
* Preserve substantive Calm mid-turn text

* no-mistakes(review): Distinguish newline-preserved replies from short narration

* no-mistakes(document): Document Calm mid-turn preservation boundaries

* no-mistakes(ci): Fixed the flaky contribution watcher test by increasing its bounded checkpoint from 5 to 15 seconds, allowing diagnostics to surface under slower CI load. Verified with `bash tests/fm-contributions.test.sh` and `git diff --check`
…#4656)

* fix(bin): re-record PR poll identity after a volume device renumber (Fixes kunchenguid#4260)

A volume remount can renumber the state filesystem's st_dev while every
inode and byte stays the same; APFS does this across a reboot. A poll
registration records its sidecar and check as device:inode, so every poll
armed before the remount failed strict validation and the watcher refused
all of them as unauthenticated state checks until each was re-armed by hand.

There are two device comparisons. fm_pr_private_file_valid compares a live
file's device with the state directory's device read in the same invocation:
it refuses a file that is not on the state directory's own filesystem and
already survives a renumber, so it is unchanged. The registration's recorded
identity versus the live identity (from kunchenguid#556, reused by the kunchenguid#932 retirement
receipt) binds the registration to the exact files published in its own
transaction; its device part is what breaks.

When strict capture fails, the watcher now proves the device is the only
difference: every other artifact check passes (template bytes, both hashes,
private mode, single link, live device, metadata), both recorded identities
name one device, and each recorded inode equals its live inode. Only then,
under the task's control lock, does it rewrite the two identity lines,
repeating the whole proof and comparing the registration's file identity and
bytes just before the rename, and then capture strictly again. A swapped,
altered, re-moded, relinked, split-device, or foreign-device artifact still
fails a proof and is still refused, and a pending retirement receipt blocks
the rewrite.

Reproduction: on macOS a poll armed on an APFS disk image that was detached
and re-attached behind another image moved st_dev 16777239 -> 16777243 with
inodes, bytes, mode, and link count unchanged; the real watcher refused it on
main and reports its merge with this change. The portable regression test
rewrites a real registration's recorded device and drives the watcher.

Not changed here: the status presentation cursor keys rows by its own
device:inode identity in bin/fm-classify-lib.sh, a different helper that
needs its own fix; a retirement receipt left by a reboot between its
publication and removal still names the old device and stays refused; custom
check trust binds only a content hash and is unaffected.

* fix(review): Serialize PR poll publication writers

* fix(review): Bound PR poll publication lock scope
…llow-up to kunchenguid#4627) (kunchenguid#4661)

A budget that expires partway through an observation no longer records an
error or prints the unavailable wake; the URL keeps its prior record and is
observed first next poll. forge() flags budget exhaustion at the point it
refuses, or when a read is killed at the budget's own deadline, so a genuine
forge failure still records the error and wakes. Each distinct URL is now
observed once per poll and applied to every owning task.
…kunchenguid#4680)

* fix(bin): clear parent pending-replies on local secondmate retirement

Local secondmate teardown left resolved parent pending-reply records behind
after home removal (seen after papa-hdds / pxmx retirement). Refuse non-forced
retirement while any reply for that id is still unresolved, and delete every
matching record plus its delivery confirmation after a successful local or
remote retirement, matching the remote cleanup path.

* no-mistakes(document): Align secondmate retirement docs with pending-reply cleanup

* no-mistakes(review): Lokale Pending-replies-Sicherheitsprüfung vor Home-Entfernung

* no-mistakes(review): Pending-replies-corr_id auf 16-Hex absichern

* no-mistakes(review): Pending-replies Basename und corr_id abgleichen

* no-mistakes(document): Clarify forced retirement pending-reply cleanup

---------

Co-authored-by: ladwein <ladwein@firstmate.bost8.thelad.loc>
…ile-fork-with-upstream

# Conflicts:
#	.agents/skills/harness-adapters/references/harness/claude.md
#	.agents/skills/updatefirstmate/SKILL.md
#	AGENTS.md
#	bin/fm-brief.sh
#	bin/fm-classify-lib.sh
#	bin/fm-claude-trust.sh
#	bin/fm-crew-state.sh
#	bin/fm-ff-lib.sh
#	bin/fm-inactive-reconcile.sh
#	docs/architecture.md
#	docs/configuration.md
#	tests/fm-classify-decision-key.test.sh
#	tests/fm-claude-trust.test.sh
#	tests/fm-crew-state.test.sh
#	tests/fm-inactive-reconcile.test.sh
… open-decision fold

Fixes gaps found reconciling upstream's status_current_line (fold-based
current-state read) with the fork's status_current_state_line (backward-walk
that survives a trailing note:/resolved: for a still-standing paused:/
captain-held: or open decision), surfaced by upstream's own new crew-state
test suite once both were merged together:

- A secondmate's own status log also carries done:/failed: child-outcome
  lines relayed upward by the ledger path, so those must not be read as the
  secondmate's own terminal state; ship/scout crews carry no such relayed
  lines, so done:/failed: there still win immediately.
- A declared wait (paused:/captain-held:) must outrank even a still-open
  keyed decision positioned earlier in the log, not just an unrelated
  working:/done:/failed: - a worker that moves from an unresolved escalation
  to a declared pause is doing the pause now.
- A ship/scout terminal declaration (done:/failed:) must be the log's
  literal newest recognized event to count as current: once anything - even
  a mere note: - trails it, something happened since, and it must not be
  resurrected as still-current over an even older decision beneath it.
- A colonless continuation line has no verb at all (status_line_verb returns
  the whole line when there is no colon), so it must never be mistaken for a
  recognized event.

All under fm-classify-lib.sh's status_current_line/status_current_state_line
and fm-crew-state.sh's reuse of the resulting LOG_LINE, which now also skips
emitting a status-log source when the mapped state is still unknown.
…e-fork-with-upstream

# Conflicts:
#	.agents/skills/afk/SKILL.md
#	AGENTS.md
#	bin/backends/herdr.sh
#	bin/fm-brief.sh
#	bin/fm-classify-lib.sh
#	bin/fm-control.sh
#	bin/fm-merge-authority-lib.sh
#	bin/fm-merge-outcome-lib.sh
#	bin/fm-pr-lib.sh
#	bin/fm-pr-merge.sh
#	bin/fm-send.sh
#	bin/fm-spawn.sh
#	bin/fm-supervise-daemon.sh
#	bin/fm-watch.sh
#	docs/agent-control.md
#	docs/architecture.md
#	docs/configuration.md
#	docs/scripts.md
#	tests/fm-captain-hold-lifecycle.test.sh
#	tests/fm-control-relaunch.test.sh
#	tests/fm-pr-check-security.test.sh
#	tests/fm-watch-triage.test.sh
@rub-a-dub-dub rub-a-dub-dub changed the title feat(bin): reconcile the fork with upstream main and settle the current-state read feat(bin): merge upstream's 76 commits and reconcile the fork's divergences Sep 27, 2026
rub-a-dub-dub and others added 2 commits September 27, 2026 04:36
Base advanced by three commits (#31, #30, #32) since this branch last took
origin/main at 929a366. Only 4e5c8ee (#31, pin Codex workers to the standard
service tier) overlapped the reconciliation, in three files:

- .agents/skills/harness-adapters/references/harness/codex.md: keep upstream's
  Effort flag row, which now advertises `max` for gpt-5.6-luna, and add the
  base's new Service tier row alongside it.
- bin/fm-spawn.sh: carry `-c 'service_tier="default"'` on both codex launch
  templates, keeping upstream's `--disable hooks` on the crewmate template, and
  take the base's service-tier rationale into the launch contract header.
- tests/fm-spawn-dispatch-profile.test.sh: register the base's new
  test_codex_scout_uses_standard_service_tier next to upstream's restructured
  max-effort and hook-layer cases, and thread the service-tier flag through
  upstream's own Luna max-effort launch assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e-fork-with-upstream

# Conflicts:
#	docs/architecture.md
@rub-a-dub-dub
rub-a-dub-dub merged commit 655f222 into main Sep 27, 2026
18 of 19 checks passed
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.