Skip to content

fix(bin): sync upstream captain-hold, codex composer, and empty-steer fixes - #1

Merged
brentsec merged 3 commits into
mainfrom
fm/upstream-sync-b85e28b5
Sep 15, 2026
Merged

brentsec merged 3 commits into
mainfrom
fm/upstream-sync-b85e28b5

Conversation

@brentsec

@brentsec brentsec commented Sep 15, 2026 •

Copy link
Copy Markdown
Owner

Intent

make sure to also sync the firstmate repo with the upstream firstmate repo before adding my customizations to my fork.. its currently 1 commit behind firstmate's upstream currently.

What Changed

  • Captain holds: bin/fm-captain-hold.sh and bin/fm-backlog-transition-lib.sh decode a shown task body with JSON::PP->new->utf8->allow_nonref instead of decode_json, and the retention decoder now writes raw bytes via binmode STDOUT, ":raw" plus utf8::encode, so holds and cleanup work where JSON::PP defaults allow_nonref off and a retained body keeps its non-ASCII characters; tests/fm-captain-hold-lifecycle.test.sh adds cases for both paths and docs/captain-hold-lifecycle.md records them.
  • Composer classification: bin/fm-composer-lib.sh adds fm_composer_strip_braille and treats braille-only rows as furniture - they bound a bare composer's wrap region and are stripped from behind the glyph row - so codex-cli 0.154's idle starfield screens classify as empty rather than pending; covered by new cases in tests/fm-composer-lib.test.sh, the new tests/fm-composer-codex-idle-live-e2e.test.sh (registered in bin/fm-test-run.sh), and dated evidence in docs/verification/runtime-backends.md.
  • bin/fm-send.sh refuses an empty or whitespace-only text steer before anything is marked, recorded, or typed, with coverage in tests/fm-send-inbox.test.sh; bin/fm-spawn.sh and bin/fm-backlog-handoff.sh change only in autoformat output (statement splitting, case-pattern and redirect spacing), with no behavior difference.

Risk Assessment

✅ Low: A clean linear upstream sync whose bulk is a verified semantics-neutral reformat, with three narrow behavioral fixes that each carry an executing regression test, and no fork-local changes mixed in.

Testing

I first confirmed the sync itself against the real upstream remote: the branch head resolves to exactly kunchenguid/firstmate main 2da3c5e, the fork's old main is an ancestor, the range holds only the three official commits, and there are no merge commits or conflict markers. I then drove each commit's behaviour against the running product in isolated environments - real fm-send against a live Herdr endpoint, real fm-captain-hold and fm-teardown against a real tasks-axi backlog, and real codex-cli 0.154.0 in a real Herdr pane - and for each defect I re-ran the identical scenario against the pre-sync tree extracted at base aa92177, so every fix has a reproduced failure beside it. The empty-steer stall, the allow_nonref hold failure and the dropped accent all reproduce before and are gone after. The targeted suites for the touched areas pass, and the reformatted fm-spawn.sh still spawns and tears down a real Herdr pane end to end. Two real-Herdr suites failed on the first pass because the Powerlevel10k wizard blocks a fresh pane's shell on this host; I disabled that wizard for the run and both passed, so it is an environment blocker rather than a product one. The one thing I could not drive live is the braille starfield itself: codex 0.154.0 never animated it here across three model and mode combinations including a real attached Herdr viewer, so I report that scenario untested. Visual evidence is a rendered HTML page plus screenshot showing the real codex idle pane and every before/after transcript side by side.

  • Live validation: ✅ go - 10 of 11 scenarios driven live against the product
Scenario Result Live Evidence
The fork's branch is exactly upstream firstmate main, with no extra or lost commits ✅ pass live git ls-remote https://github.com/kunchenguid/firstmate.git refs/heads/main returned 2da3c5e, identical to local HEAD; git merge-base --is-ancestor aa92177 HEAD suc…
An operator steering a secondmate with no message is refused, and nothing is recorded, minted, or typed ✅ pass live bin/fm-send.sh domain against a real kind=secondmate task whose endpoint is a live Herdr pane: exit 1, "a text steer requires a nonempty message; nothing was sent", state/domain.inbox absent, no sta…
Adversarial: empty-string, whitespace-only and tab+newline arguments are all refused the same way ✅ pass live Three further live sends (domain "", domain " ", domain $'\t' $'\n') each exited 1 with the same refusal and left state/domain.inbox and state/pending-replies absent - evidence/04-fm-send-live-h…
Regression reproduction: the pre-sync tree stalls the fleet on that same empty steer ✅ pass live The identical live drive against the base tree extracted at aa92177 exited 0, wrote state/domain.inbox/001.msg whose body is only [fm-from-firstmate]corr=e5812b920d04b848 (marker + correlation bytes…
A real steer still delivers durably and the --key lifecycle path is untouched ✅ pass live fm-send.sh domain please re-read the brief and confirm exited 0, wrote state/domain.inbox/001.msg with body [fm-from-firstmate]corr=798a14d993d7d0f5 please re-read the brief and confirm and one pe…
A worker can record a captain decision on a host whose JSON::PP defaults allow_nonref off ✅ pass live With the older default forced back off (and probed non-vacuous: decode_json rejects a bare scalar), real bin/fm-captain-hold.sh hold exited 1 on the base tree with the library's own "must be an obje…
Cleanup keeps an accented captain-held body's exact UTF-8 bytes while appending the deliverable ✅ pass live Real bin/fm-teardown.sh on a real tasks-axi row bodied "The café decision is still open.": base tree produced The caf� decision (accent lost, no c3 a9), HEAD produced The café decision with byte…
A live idle codex 0.154 pane classifies as an empty composer, so the steering doorbell is not deferred ✅ pass live Real codex-cli 0.154.0 launched in an isolated Herdr pane; fm_backend_herdr_composer_state (the production cursorless styled profile, styled=1 cursor=0 identity=1 rows=20) answered empty - evidenc…
Adversarial: codex 0.154's braille starfield rows read as furniture (empty) rather than wrapped typed input (pending) ⏸️ untested no codex-cli 0.154.0 does not draw the idle braille starfield (nor the grey composer background it accompanies) on this Linux host, so there is no live screen that exercises the braille rule. The upstrea…
The shfmt reformat of fm-spawn.sh is behaviour-preserving: a task still spawns onto and tears down from a real Herdr backend ✅ pass live tests/fm-backend-autodetect-smoke.test.sh against the real installed Herdr: real bin/fm-spawn.sh auto-detected herdr, recorded backend=herdr with herdr_session/workspace/tab/pane in meta, its launch…
The shfmt reformat of fm-backlog-handoff.sh is behaviour-preserving: backlog items still move, wake, and stay idempotent ✅ pass live tests/fm-backlog-handoff.test.sh drives the real bin/fm-backlog-handoff.sh executable against real backlog files (tmux/gh/treehouse stubbed): all cases pass including crash-then-retry durability, co…

Live validation review page (rendered HTML, all before/after panels + the real codex pane)

Evidence: Live validation review page (source HTML)

Source: Live validation review page (source HTML)

<!doctype html><meta charset="utf-8">
<title>Live validation - firstmate upstream sync aa92177..2da3c5e</title>
<style>
 :root{--bg:#0f1115;--panel:#161a21;--line:#252b35;--ink:#dfe3ea;--mute:#98a1b0;
        --bad:#ff7b72;--good:#56d364;--accent:#79b8ff}
 *{box-sizing:border-box;min-width:0}
 body{margin:0;padding:32px;background:var(--bg);color:var(--ink);
       font:14px/1.55 -apple-system,Segoe UI,Roboto,Helvetica,sans-serif}
 header{max-width:1500px;margin:0 auto 26px}
 h1{font-size:21px;margin:0 0 6px;letter-spacing:-.2px}
 header p{margin:0;color:var(--mute);max-width:95ch}
 .wrap{max-width:1500px;margin:0 auto;display:grid;gap:20px}
 .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px 20px}
 h2{font-size:15px;margin:0 0 4px}
 .sub{margin:0 0 14px;color:var(--mute);font-size:13px;max-width:110ch}
 .split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:14px}
 .col{border:1px solid var(--line);border-radius:9px;overflow:hidden;background:#0b0d11;min-width:0}
 .tag{padding:7px 12px;font-size:12px;font-weight:650;letter-spacing:.3px;
       border-bottom:1px solid var(--line)}
 .bad .tag{color:var(--bad);background:rgba(255,123,114,.08)}
 .good .tag{color:var(--good);background:rgba(86,211,100,.08)}
 pre{margin:0;padding:12px 14px;overflow-x:auto;white-space:pre-wrap;word-break:break-word;
      font:12px/1.5 "JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace;color:#cdd3dc}
 .single pre{border:1px solid var(--line);border-radius:9px;background:#0b0d11}
 .pane{border:1px solid var(--line);border-radius:9px;background:#07090c;padding:14px 16px}
 .dots{display:flex;gap:6px;margin-bottom:10px} .dots i{width:10px;height:10px;border-radius:50%;display:block}
 .verdicts{display:flex;gap:10px;margin-top:12px;flex-wrap:wrap}
 .v{font:12px/1 ui-monospace,monospace;padding:7px 11px;border-radius:7px;border:1px solid var(--line)}
 .v b{font-weight:700}
 .v.ok{color:var(--good);border-color:rgba(86,211,100,.4)}
 .v.info{color:var(--accent);border-color:rgba(121,184,255,.35)}
</style>
<header>
  <h1>Live validation &mdash; firstmate fork synced to upstream <code>2da3c5e</code></h1>
  <p>Every panel below was produced in this run by driving the real commands: real <code>codex-cli 0.154.0</code>
     in a real isolated Herdr pane, the real <code>bin/fm-send.sh</code> against a live Herdr endpoint, and the real
     <code>bin/fm-captain-hold.sh</code> / <code>bin/fm-teardown.sh</code> against a real tasks-axi backlog.
     "Before" runs the same scenario against the pre-sync tree at base <code>aa92177</code>.</p>
</header>
<div class="wrap">

<section class="card single">
  <h2>1. The sync itself</h2>
  <p class="sub">The fork's branch head resolves to exactly upstream <code>kunchenguid/firstmate</code> <code>main</code>; the fork's old main is an ancestor, the range holds only the three official commits, no merge commits, no conflict residue.</p>
  <pre>### upstream ref
2da3c5e2193cb725bf173b7dfcbf8b094c4b862d	refs/heads/main

\### local HEAD
2da3c5e2193cb725bf173b7dfcbf8b094c4b862d

\### tree of local HEAD
2f0d099653525273ecf810204ff145880b30a4cf

\### base is ancestor of HEAD?
yes: aa92177 is an ancestor of HEAD

\### commits base..HEAD (exactly the upstream commits, no extras)
2da3c5e2193cb725bf173b7dfcbf8b094c4b862d fix(bin): refuse empty text steers in fm-send (#4259)
8b10b61e3feace8f275c6d0b3e490cdf7ab1f67d fix(bin): read codex 0.154&#x27;s idle braille starfield rows as composer furniture (#4532)
b85e28b5f8aad91a553e33d461da9f238bbdac38 fix(bin): make captain holds work on hosts with an older JSON::PP, and stop cleanup dropping accents from a held body (#4471)

\### merge commits in range (expect none)
(end)

\### working tree clean / no conflict markers in changed files
(end status)
(end conflict scan)</pre>
</section>

<section class="card">
  <h2>2. Steering a secondmate with an empty message (#4255 stall)</h2>
  <p class="sub">A marked secondmate request sent with no message used to deliver only marker and correlation bytes and mint a pending-reply expectation the parent could never see resolved. Driven end to end against a live Herdr endpoint.</p>
  <div class="split">
    <div class="col bad"><div class="tag">BEFORE &mdash; pre-sync tree aa92177</div><pre>$ fm-send.sh domain          # a marked secondmate steer with NO message
fm-send: doorbell not typed because the agent in &lt;pane&gt; has exited;
         the steer is durably recorded at state/domain.inbox/001.msg
exit: 0

--- durable state left behind by the PRE-FIX empty steer ---
inbox records: 1
  001.msg body -&gt; [fm-from-firstmate]corr=e5812b920d04b848
                  (marker and correlation bytes only - no message)
pending-reply expectations minted: 1
  state/pending-replies/e5812b920d04b848
                  (the parent waits on a reply to nothing)</pre></div>
    <div class="col good"><div class="tag">AFTER &mdash; synced tree 2da3c5e</div><pre>$ fm-send.sh domain          # a marked secondmate steer with NO message
exit: 1
stderr:
  error: a text steer requires a nonempty message; nothing was sent
  (an empty marked request would deliver only marker and correlation bytes
   and leave the parent waiting on a reply to nothing)

--- durable state after the empty steer ---
inbox records: none (state/domain.inbox does not exist)
pending-reply expectations: none (no state/pending-replies directory)
--- live herdr pane w1:p1 after the empty steer ---
 (unchanged - nothing was typed)

... and identically for &quot;&quot; , &quot;   &quot; and a tab+newline argument list.

$ fm-send.sh domain please re-read the brief and confirm
exit: 0
inbox records: 1  -&gt; state/domain.inbox/001.msg
body: [fm-from-firstmate]corr=798a14d993d7d0f5 please re-read the brief and confirm
pending-reply expectations: 1</pre></div>
  </div>
</section>

<section class="card">
  <h2>3. Recording a captain decision where JSON::PP defaults allow_nonref off</h2>
  <p class="sub">On such a host a worker could not formally record a decision for the captain at all &mdash; the hold reported that the task lost its hold-set stamp.</p>
  <div class="single"><pre># Defect 1: recording a captain decision on an older-JSON::PP host  #
#####################################################################

===== PRE-FIX tree (base aa92177) =====
$ fm-captain-hold.sh hold sample-nonref-body --reason &quot;captain go needed&quot;   (JSON::PP allow_nonref default OFF)
exit: 1
stderr:
  JSON text must be an object or array (but found number, string, true, false or null, use allow_nonref to allow this) at -e line 3.
  JSON text must be an object or array (but found number, string, true, false or null, use allow_nonref to allow this) at -e line 3.
  fm-captain-hold: task sample-nonref-body did not retain its hold-set stamp
row state:
    hold_kind: &quot;-&quot;
    body: &quot;Captain hold set: 2026-09-15T18:09:36Z\n\nFirst line of the plan.&quot;
  -&gt; hold_kind captain     : NO
  -&gt; hold-set stamp present: yes
  -&gt; original body kept    : yes

===== POST-FIX tree (HEAD 2da3c5e) =====
$ fm-captain-hold.sh hold sample-nonref-body --reason &quot;captain go needed&quot;   (JSON::PP allow_nonref default OFF)
exit: 0
stdout:
  sample-nonref-body
row state:
    hold_kind: captain
    body: &quot;Captain hold set: 2026-09-15T18:09:36Z\n\nFirst line of the plan.&quot;
  -&gt; hold_kind captain     : yes
  -&gt; hold-set stamp present: yes
  -&gt; original body kept    : yes</pre></div>
</section>

<section class="card">
  <h2>4. An accented body surviving cleanup's rewrite</h2>
  <p class="sub">Cleanup rewrites a captain-held row's body to append the finished work's deliverable. Asserted on bytes: <code>c3 a9</code> (UTF-8 &eacute;) rather than a single latin-1 byte.</p>
  <div class="single"><pre>===== PRE-FIX tree (base aa92177) =====
body written into the row (UTF-8 bytes):  54 68 65 20 63 61 66 c3 a9 20 64 65 63 69 73 69
 6f 6e 20 69 73 20 73 74 69 6c 6c 20 6f 70 65 6e
$ fm-teardown.sh sample-accent-scout   (cleanup retains the captain-held row and appends the deliverable)
exit: 0
stderr:
  ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ●  WATCHER DOWN - SUPERVISION IS OFF
  ●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
  ●  Trust the emitted supervision protocol for this harness; do not use shell &amp; for watcher repair.
  ●  This is a supervision warning only; the guarded operation WILL still run.
retained row body line:
    body: &quot;Captain hold set: 2026-09-15T18:10:13Z\n\nThe caf� decision is still open.\n\nDeliverable of the finished work: report data/sample-accent-scout/report.md&quot;
deliverable appended        : yes
accent survived as UTF-8 (c3 a9)   : NO
raw bytes around the accent        :  5c 6e 54 68 65 20 63 61 66 ef bf bd 20 64 65 63 69 73 69 0a 
row body renders as               : The caf� decision

===== POST-FIX tree (HEAD 2da3c5e) =====
body written into the row (UTF-8 bytes):  54 68 65 20 63 61 66 c3 a9 20 64 65 63 69 73 69
 6f 6e 20 69 73 20 73 74 69 6c 6c 20 6f 70 65 6e
$ fm-teardown.sh sample-accent-scout   (cleanup retains the captain-held row and appends the deliverable)
exit: 0
stderr:
  ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ●  WATCHER DOWN - SUPERVISION IS OFF
  ●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
  ●  Trust the emitted supervision protocol for this harness; do not use shell &amp; for watcher repair.
  ●  This is a supervision warning only; the guarded operation WILL still run.
retained row body line:
    body: &quot;Captain hold set: 2026-09-15T18:10:15Z\n\nThe café decision is still open.\n\nDeliverable of the finished work: report data/sample-accent-scout/report.md&quot;
deliverable appended        : yes
accent survived as UTF-8 (c3 a9)   : yes
raw bytes around the accent        :  5c 6e 54 68 65 20 63 61 66 c3 a9 20 64 65 63 69 73 69 0a 
row body renders as               : The café decision</pre></div>
</section>

<section class="card">
  <h2>5. A live idle codex 0.154 pane reads as an empty composer</h2>
  <p class="sub">Captured from a real codex-cli 0.154.0 sitting idle in an isolated Herdr pane and classified through the production adapter <code>fm_backend_herdr_composer_state</code> (styled=1, cursor=0, identity=1, rows=20) &mdash; the cursorless profile the fix targets. The pane is what the steering doorbell must reach.</p>
  <div class="pane"><div class="dots"><i style="background:#ff5f57"></i><i style="background:#febc2e"></i><i style="background:#28c840"></i></div><pre>WARNING: proceeding, even though we could not create PATH aliases: Refusing to create helper bina
ries under temporary dir &quot;/tmp&quot; (codex_home: AbsolutePathBuf(&quot;/tmp/fm-codex-idle-live/codex-home3
&quot;))
╭────────────────────────────────────────────────╮
│ &gt;_ OpenAI Codex (v0.154.0)                     │
│                                                │
│ model:     gpt-6-astra high   /model to change │
│ directory: /tmp/fm-codex-idle-live/project     │
╰────────────────────────────────────────────────╯

  Tip: This is GPT-6, a new generation of intelligence. Astra is state-of-the-art in coding,
  computer use, science, and professional work. Give it a hard problem, a half-formed idea, or
  anything you&#x27;ve been meaning to build. See where it takes you.

• You have 3 usage limit resets available. Run /usage to use one.
 
 
› Ask Codex to do anything
 
  gpt-6-astra high · /tmp/fm-codex-idle-live/project</pre></div>
  <div class="verdicts">
    <span class="v ok">herdr cursorless styled read &rarr; <b>empty</b></span>
    <span class="v info">codex --version &rarr; <b>codex-cli 0.154.0</b></span>
    <span class="v info">braille starfield drawn on this host &rarr; <b>no</b> (see testing notes)</span>
  </div>
</section>

</div>
![Real codex-cli 0.154.0 idle pane as captured through the Herdr backend (rendered)](https://github.com/user-attachments/assets/31c03845-7c95-4192-bd5d-e64925890a6e) - Evidence: [Real codex-cli 0.154.0 idle pane as captured through the Herdr backend (rendered)](https://github.com/brentsec/firstmate/blob/57f2caa68d1c72b9035b8e40bce58892aabf1565/.no-mistakes/evidence/fm/upstream-sync-b85e28b5/codex-idle-live.png)
Evidence: Raw ANSI bytes of the classified live codex idle frame

Source: Raw ANSI bytes of the classified live codex idle frame

WARNING: proceeding, even though we could not create PATH aliases: Refusing to create helper bina
ries under temporary dir "/tmp" (codex_home: AbsolutePathBuf("/tmp/fm-codex-idle-live/codex-home3
"))
�[0m�[2m╭────────────────────────────────────────────────╮�[0m
�[0m�[2m│ >_ �[0m�[1mOpenAI Codex�[0m�[2m (v0.154.0)                     │�[0m
�[0m�[2m│                                                │�[0m
�[0m�[2m│ model:     �[0mgpt-6-astra high�[0m�[2m   �[0m�[38;5;6m/model�[0m�[2m to change │�[0m
�[0m�[2m│ directory: �[0m/tmp/fm-codex-idle-live/project�[0m�[2m     │�[0m
�[0m�[2m╰────────────────────────────────────────────────╯�[0m

  �[0m�[1mTip:�[0m This is GPT-6, a new generation of intelligence. Astra is state-of-the-art in coding,
  computer use, science, and professional work. Give it a hard problem, a half-formed idea, or
  anything you've been meaning to build. See where it takes you.

�[0m�[2m• �[0mYou have 3 usage limit resets available. Run /usage to use one.
 
 
�[0m�[1m›�[0m �[0m�[2mAsk Codex to do anything�[0m
 
  �[0m�[38;2;246;226;183mgpt-6-astra high�[0m�[2m · �[0m�[38;2;171;223;167m/tmp/fm-codex-idle-live/project�[0m
Evidence: Sync fidelity against the real upstream remote

Source: Sync fidelity against the real upstream remote

### upstream ref 2da3c5e2193cb725bf173b7dfcbf8b094c4b862d refs/heads/main ### local HEAD 2da3c5e2193cb725bf173b7dfcbf8b094c4b862d ### base is ancestor of HEAD? yes: aa92177 is an ancestor of HEAD ### commits base..HEAD (exactly the upstream commits, no extras) 2da3c5e2 fix(bin): refuse empty text steers in fm-send (#4259) 8b10b61e fix(bin): read codex 0.154's idle braille starfield rows as composer furniture (#4532) b85e28b5 fix(bin): make captain holds work on hosts with an older JSON::PP, and stop cleanup dropping accents from a held body (#4471) ### merge commits in range (expect none) (end) ### working tree clean / no conflict markers in changed files (end status) (end conflict scan)

\### upstream ref
2da3c5e2193cb725bf173b7dfcbf8b094c4b862d	refs/heads/main

\### local HEAD
2da3c5e2193cb725bf173b7dfcbf8b094c4b862d

\### tree of local HEAD
2f0d099653525273ecf810204ff145880b30a4cf

\### base is ancestor of HEAD?
yes: aa92177 is an ancestor of HEAD

\### commits base..HEAD (exactly the upstream commits, no extras)
2da3c5e2193cb725bf173b7dfcbf8b094c4b862d fix(bin): refuse empty text steers in fm-send (#4259)
8b10b61e3feace8f275c6d0b3e490cdf7ab1f67d fix(bin): read codex 0.154's idle braille starfield rows as composer furniture (#4532)
b85e28b5f8aad91a553e33d461da9f238bbdac38 fix(bin): make captain holds work on hosts with an older JSON::PP, and stop cleanup dropping accents from a held body (#4471)

\### merge commits in range (expect none)
(end)

\### working tree clean / no conflict markers in changed files
(end status)
(end conflict scan)
Evidence: fm-send live drive against a real Herdr endpoint (refusals + happy path)

Source: fm-send live drive against a real Herdr endpoint (refusals + happy path)

herdr lab session : fm-lab-fmsend-603813-11610
live pane         : w1:p1

#############################################################
# 1. ADVERSARIAL: a marked secondmate steer with NO message  #
#############################################################

===== empty marked secondmate steer =====
$ fm-send.sh domain
exit: 1
stderr:
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the requested message WILL still be sent.
●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
error: a text steer requires a nonempty message; nothing was sent (an empty marked request would deliver only marker and correlation bytes and leave the parent waiting on a reply to nothing)
--- durable state after the empty steer ---
inbox records: none (state/domain.inbox does not exist)
pending-reply expectations: none (no state/pending-replies directory)
--- live herdr pane w1:p1 after the empty steer ---
1: mux@muxtop002  /tmp/fm-send-live/home                                     ✔  32  11:07:35

#############################################################
# 2. ADVERSARIAL: an explicit empty-string message           #
#############################################################

===== explicit empty-string message =====
$ fm-send.sh domain 
exit: 1
stderr:
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.
error: a text steer requires a nonempty message; nothing was sent (an empty marked request would deliver only marker and correlation bytes and leave the parent waiting on a reply to nothing)
--- durable state after the empty-string steer ---
inbox records: none (state/domain.inbox does not exist)
pending-reply expectations: none (no state/pending-replies directory)

#############################################################
# 3. ADVERSARIAL: a whitespace-only message                  #
#############################################################

===== whitespace-only message =====
$ fm-send.sh domain    
exit: 1
stderr:
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.
error: a text steer requires a nonempty message; nothing was sent (an empty marked request would deliver only marker and correlation bytes and leave the parent waiting on a reply to nothing)
--- durable state after the whitespace-only steer ---
inbox records: none (state/domain.inbox does not exist)
pending-reply expectations: none (no state/pending-replies directory)

#############################################################
# 4. ADVERSARIAL: whitespace-only across multiple arguments  #
#############################################################

===== tab + newline only =====
$ fm-send.sh domain 	 

exit: 1
stderr:
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.
error: a text steer requires a nonempty message; nothing was sent (an empty marked request would deliver only marker and correlation bytes and leave the parent waiting on a reply to nothing)
--- durable state after the tab/newline steer ---
inbox records: none (state/domain.inbox does not exist)
pending-reply expectations: none (no state/pending-replies directory)

#############################################################
# 5. HAPPY PATH: a real message still delivers               #
#############################################################

===== real message =====
$ fm-send.sh domain please re-read the brief and confirm
exit: 0
stderr:
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.
fm-send: doorbell not typed because the agent in fm-lab-fmsend-603813-11610:w1:p1 has exited; the steer is durably recorded at /tmp/fm-send-live/home/state/domain.inbox/001.msg for recovery (stuck-crewmate-recovery), and the watcher will not re-ring a dead pane
--- durable state after the real steer ---
inbox records: 1
  state/domain.inbox/001.msg
pending-reply expectations: 1
--- live herdr pane w1:p1 after the real steer ---
1: mux@muxtop002  /tmp/fm-send-live/home                                     ✔  32  11:07:35
--- recorded inbox body ---
[fm-from-firstmate]⁣corr=798a14d993d7d0f5 please re-read the brief and confirm
#############################################################
# 6. The --key lifecycle path takes no text and is unaffected#
#############################################################

===== --key Enter =====
$ fm-send.sh domain --key Enter
exit: 0
stderr:
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.
--- live herdr pane w1:p1 after --key Enter ---
1: mux@muxtop002  /tmp/fm-send-live/home                                     ✔  32  11:07:35
Evidence: Pre-fix reproduction of the kunchenguid#4255 empty-steer stall

Source: Pre-fix reproduction of the #4255 empty-steer stall

=== PRE-FIX (base aa92177): empty marked secondmate steer === exit: 0 inbox records: 1 001.msg body -> [fm-from-firstmate]M-bM-^AM-#corr=e5812b920d04b848 pending-reply expectations minted: 1 state/pending-replies/e5812b920d04b848

pre-fix tree extracted at base commit: aa92177

=== PRE-FIX (base aa92177): empty marked secondmate steer ===
fm-send: doorbell not typed because the agent in fm-lab-fmsendpre-610094-20154:w1:p1 has exited; the steer is durably recorded at /tmp/fm-send-live/home-prefix/state/domain.inbox/001.msg for recovery (stuck-crewmate-recovery), and the watcher will not re-ring a dead pane
exit: 0
--- durable state left behind by the PRE-FIX empty steer ---
inbox records: 1
  001.msg body -> [fm-from-firstmate]M-bM-^AM-#corr=e5812b920d04b848 
pending-reply expectations minted: 1
  state/pending-replies/e5812b920d04b848
Evidence: Captain hold on an older-JSON::PP host, pre-fix vs HEAD

Source: Captain hold on an older-JSON::PP host, pre-fix vs HEAD

===== PRE-FIX tree (base aa92177) ===== exit: 1 JSON text must be an object or array (but found number, string, true, false or null, use allow_nonref to allow this) at -e line 3. fm-captain-hold: task sample-nonref-body did not retain its hold-set stamp -> hold_kind captain : NO ===== POST-FIX tree (HEAD 2da3c5e) ===== exit: 0 -> hold_kind captain : yes

installed JSON::PP on this host: 4.16
with the older default forced back off, decode_json on a bare scalar: rejects

#####################################################################
# Defect 1: recording a captain decision on an older-JSON::PP host  #
#####################################################################

===== PRE-FIX tree (base aa92177) =====
$ fm-captain-hold.sh hold sample-nonref-body --reason "captain go needed"   (JSON::PP allow_nonref default OFF)
exit: 1
stderr:
  JSON text must be an object or array (but found number, string, true, false or null, use allow_nonref to allow this) at -e line 3.
  JSON text must be an object or array (but found number, string, true, false or null, use allow_nonref to allow this) at -e line 3.
  fm-captain-hold: task sample-nonref-body did not retain its hold-set stamp
row state:
    hold_kind: "-"
    body: "Captain hold set: 2026-09-15T18:09:36Z\n\nFirst line of the plan."
  -> hold_kind captain     : NO
  -> hold-set stamp present: yes
  -> original body kept    : yes

===== POST-FIX tree (HEAD 2da3c5e) =====
$ fm-captain-hold.sh hold sample-nonref-body --reason "captain go needed"   (JSON::PP allow_nonref default OFF)
exit: 0
stdout:
  sample-nonref-body
row state:
    hold_kind: captain
    body: "Captain hold set: 2026-09-15T18:09:36Z\n\nFirst line of the plan."
  -> hold_kind captain     : yes
  -> hold-set stamp present: yes
  -> original body kept    : yes
Evidence: Accented captain-held body through cleanup, pre-fix vs HEAD

Source: Accented captain-held body through cleanup, pre-fix vs HEAD

===== PRE-FIX tree (base aa92177) ===== row body renders as : The caf� decision accent survived as UTF-8 (c3 a9) : NO ===== POST-FIX tree (HEAD 2da3c5e) ===== row body renders as : The café decision accent survived as UTF-8 (c3 a9) : yes

#####################################################################
# Defect 2: an accented body surviving cleanup's rewrite            #
#####################################################################

===== PRE-FIX tree (base aa92177) =====
body written into the row (UTF-8 bytes):  54 68 65 20 63 61 66 c3 a9 20 64 65 63 69 73 69
 6f 6e 20 69 73 20 73 74 69 6c 6c 20 6f 70 65 6e
$ fm-teardown.sh sample-accent-scout   (cleanup retains the captain-held row and appends the deliverable)
exit: 0
stderr:
  ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ●  WATCHER DOWN - SUPERVISION IS OFF
  ●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
  ●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
  ●  This is a supervision warning only; the guarded operation WILL still run.
retained row body line:
    body: "Captain hold set: 2026-09-15T18:10:13Z\n\nThe caf� decision is still open.\n\nDeliverable of the finished work: report data/sample-accent-scout/report.md"
deliverable appended        : yes
accent survived as UTF-8 (c3 a9)   : NO
raw bytes around the accent        :  5c 6e 54 68 65 20 63 61 66 ef bf bd 20 64 65 63 69 73 69 0a 
row body renders as               : The caf� decision

===== POST-FIX tree (HEAD 2da3c5e) =====
body written into the row (UTF-8 bytes):  54 68 65 20 63 61 66 c3 a9 20 64 65 63 69 73 69
 6f 6e 20 69 73 20 73 74 69 6c 6c 20 6f 70 65 6e
$ fm-teardown.sh sample-accent-scout   (cleanup retains the captain-held row and appends the deliverable)
exit: 0
stderr:
  ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ●  WATCHER DOWN - SUPERVISION IS OFF
  ●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
  ●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
  ●  This is a supervision warning only; the guarded operation WILL still run.
retained row body line:
    body: "Captain hold set: 2026-09-15T18:10:15Z\n\nThe café decision is still open.\n\nDeliverable of the finished work: report data/sample-accent-scout/report.md"
deliverable appended        : yes
accent survived as UTF-8 (c3 a9)   : yes
raw bytes around the accent        :  5c 6e 54 68 65 20 63 61 66 c3 a9 20 64 65 63 69 73 69 0a 
row body renders as               : The café decision
Evidence: Live codex idle read + starfield hunt

Source: Live codex idle read + starfield hunt

lab session: fm-lab-codexstar-477444-11993   pane: w1:p1
codex version: codex-cli 0.154.0
frames sampled: 400; frames carrying braille furniture: 0
classified frame: /tmp/fm-codex-idle-live/codex-last.ansi

=== verdict on the captured LIVE codex 0.154 idle frame (herdr cursorless styled profile) ===
pre-fix  library (base aa92177): empty
post-fix library (HEAD 2da3c5e): empty

=== the frame as codex drew it (plain text) ===
1:WARNING: proceeding, even though we could not create PATH aliases: Refusing to create helper bina
2:ries under temporary dir "/tmp" (codex_home: AbsolutePathBuf("/tmp/fm-codex-idle-live/codex-home2
3:"))
4:╭──────────────────────────────────────────────────────────╮
5:│ >_ OpenAI Codex (v0.154.0)                               │
6:│                                                          │
7:│ model:     gpt-daybreak-blue-latest high   /model to ch… │
8:│ directory: /tmp/fm-codex-idle-live/project               │
9:╰──────────────────────────────────────────────────────────╯
11:  Tip: This is GPT-6, a new generation of intelligence. Astra is state-of-the-art in coding,
12:  computer use, science, and professional work. Give it a hard problem, a half-formed idea, or
13:  anything you've been meaning to build. See where it takes you.
15:• You have 3 usage limit resets available. Run /usage to use one.
18:› Ask Codex to do anything
20:  gpt-daybreak-blue-latest high · /tmp/fm-codex-idle-live/project
Evidence: Real-Herdr smoke suites re-driven with the host shell blocker removed

Source: Real-Herdr smoke suites re-driven with the host shell blocker removed

ok - real herdr: fm-spawn.sh auto-detects herdr from HERDR_ENV=1 (no explicit config) and prints the loud notice ok - real herdr: auto-detected spawn records backend=herdr and herdr_session/workspace/tab/pane fields in meta ok - real herdr: the auto-detected spawn's launch command actually ran in the herdr pane ok - real herdr: teardown completes the auto-detected spawn/teardown cycle (meta cleared, pane closed) ok - real herdr: isolated lab session removed and default fleet session unchanged FM_TEST_SUMMARY total=2 failed=0 skipped_gate=0

FM_TEST_BEGIN 2026-09-15T18:13:36Z tests/fm-backend-herdr-smoke.test.sh family=real-herdr-gated expected_gate_skip=herdr
ok - real herdr: version_check accepts the installed binary's protocol
ok - real herdr: container_ensure starts the isolated session's server, creates the firstmate workspace (fm-lab-backend-smoke-799346:w1), and reports its seeded default tab id (w1:t1)
ok - real herdr: session status normalizes running and compatible
ok - real herdr: container_ensure is idempotent (reuses/adopts the existing firstmate workspace, reports no seeded default tab on adoption)
ok - real herdr: create_task prunes the freshly-created workspace's seeded default tab, leaving exactly one clean fm-<id> task tab
ok - real herdr: create_task refuses a same-labeled tab whose pane hosts a genuinely live registered agent (unchanged behavior)
ok - real herdr: create_task closes and replaces a same-labeled tab whose pane hosts no registered agent (the restored-husk shape), leaving the workspace intact
ok - real herdr: a secondmate-shaped home (.fm-secondmate-home) gets its OWN herdr workspace, distinct from the primary's, in the SAME session
ok - real herdr: the secondmate-shaped home's workspace is labeled 2ndmate-<secondmate-id> in herdr itself
ok - real herdr: a task spawned into the secondmate-shaped home lands as a tab inside the secondmate's OWN workspace
ok - real herdr: list_live stays scoped to each home's own workspace - neither home sees the other's tasks
ok - real herdr: BOTH workspace ids/labels AND both tasks' pane ids survive a session stop + fresh server restart (multi-workspace shape)
ok - real herdr: send_text_line runs a command atomically (pane run) and its output is capturable
ok - real herdr: send_literal + send_key Enter submit as two separate steps (verified: send-text does NOT auto-submit)
ok - real herdr: current_path reads the pane's live cwd
note: FM_HERDR_SMOKE_REAL_CLAUDE=1 not set; skipping the real-agent busy_state check
ok - real herdr: kill removes the pane and is idempotent/best-effort
ok - real herdr: list_live discovers a live task tab by fm-<id> label
FM_TEST_END 2026-09-15T18:13:40Z tests/fm-backend-herdr-smoke.test.sh exit=0 duration_ms=3666 gate_skip=false
FM_TEST_BEGIN 2026-09-15T18:13:40Z tests/fm-backend-autodetect-smoke.test.sh family=real-herdr-gated expected_gate_skip=herdr
ok - real herdr: fm-spawn.sh auto-detects herdr from HERDR_ENV=1 (no explicit config) and prints the loud notice
ok - real herdr: auto-detected spawn records backend=herdr and herdr_session/workspace/tab/pane fields in meta
ok - real herdr: the auto-detected spawn's launch command actually ran in the herdr pane
ok - real herdr: teardown completes the auto-detected spawn/teardown cycle (meta cleared, pane closed)
ok - real herdr: isolated lab session removed and default fleet session unchanged
FM_TEST_END 2026-09-15T18:13:47Z tests/fm-backend-autodetect-smoke.test.sh exit=0 duration_ms=7439 gate_skip=false
FM_TEST_SUMMARY total=2 failed=0 skipped_gate=0 duration_ms=11140
FM_TEST_SUMMARY_FAMILY family=real-herdr-gated count=2 duration_ms=11105 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-backend-autodetect-smoke.test.sh duration_ms=7439
FM_TEST_SLOWEST rank=2 script=tests/fm-backend-herdr-smoke.test.sh duration_ms=3666
EXIT=0
- Outcome: ⚠️ 1 info across 1 run (27m5s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ bin/fm-send.sh:756 - The intent states the fork is "1 commit behind firstmate's upstream", but this branch syncs 3 upstream commits (b85e28b, 8b10b61, 2da3c5e) - ~2,100 lines across 13 files. This still satisfies the stated goal (sync the fork with upstream before adding customizations) and the branch is a strict linear descendant of origin/main with no fork-local commits mixed in, so it is not an intent contradiction. It is worth knowing what lands: the bulk is a semantics-neutral autoformat of bin/fm-spawn.sh and bin/fm-backlog-handoff.sh (verified: both files are identical to their pre-change versions once whitespace, ';' and '&feat(bin): recurring automated review sweep across the fleet kunchenguid/firstmate#39; are normalized away, and a token-level diff shows only case-pattern splits and redirection respacing - no string or heredoc content changed), but one user-visible behavior change rides along: fm-send.sh now exits 1 on an empty or whitespace-only text steer instead of delivering marker-only bytes. Every in-repo caller passes a non-empty constant, so nothing in-tree regresses, but any steer automation you add on top of this fork must send a non-empty message. No action needed on the sync itself.
⚠️ **Test** - 1 info
  • ℹ️ tests/fm-backend-herdr-smoke.test.sh - On this host both real-Herdr suites (tests/fm-backend-herdr-smoke.test.sh, tests/fm-backend-autodetect-smoke.test.sh) fail on a first run because the Powerlevel10k configuration wizard blocks the freshly-created pane's zsh before any command reaches it - the pane sits on "Does this look like >< but taller and fatter? Choice [ynrq]:" so send_text_line never echoes and treehouse never enters a worktree. Unrelated to this sync; both suites pass when the pane shell is launched with POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true. Worth configuring p10k (or exporting that variable) on this machine so local real-Herdr runs are not misread as product failures.
  • Live validation: ✅ go - 10 of 11 scenarios driven live against the product
Scenario Result Live Evidence
The fork's branch is exactly upstream firstmate main, with no extra or lost commits ✅ pass live git ls-remote https://github.com/kunchenguid/firstmate.git refs/heads/main returned 2da3c5e, identical to local HEAD; git merge-base --is-ancestor aa92177 HEAD suc…
An operator steering a secondmate with no message is refused, and nothing is recorded, minted, or typed ✅ pass live bin/fm-send.sh domain against a real kind=secondmate task whose endpoint is a live Herdr pane: exit 1, "a text steer requires a nonempty message; nothing was sent", state/domain.inbox absent, no sta…
Adversarial: empty-string, whitespace-only and tab+newline arguments are all refused the same way ✅ pass live Three further live sends (domain &#34;&#34;, domain &#34; &#34;, domain $&#39;\t&#39; $&#39;\n&#39;) each exited 1 with the same refusal and left state/domain.inbox and state/pending-replies absent - evidence/04-fm-send-live-h…
Regression reproduction: the pre-sync tree stalls the fleet on that same empty steer ✅ pass live The identical live drive against the base tree extracted at aa92177 exited 0, wrote state/domain.inbox/001.msg whose body is only [fm-from-firstmate]corr=e5812b920d04b848 (marker + correlation bytes…
A real steer still delivers durably and the --key lifecycle path is untouched ✅ pass live fm-send.sh domain please re-read the brief and confirm exited 0, wrote state/domain.inbox/001.msg with body [fm-from-firstmate]corr=798a14d993d7d0f5 please re-read the brief and confirm and one pe…
A worker can record a captain decision on a host whose JSON::PP defaults allow_nonref off ✅ pass live With the older default forced back off (and probed non-vacuous: decode_json rejects a bare scalar), real bin/fm-captain-hold.sh hold exited 1 on the base tree with the library's own "must be an obje…
Cleanup keeps an accented captain-held body's exact UTF-8 bytes while appending the deliverable ✅ pass live Real bin/fm-teardown.sh on a real tasks-axi row bodied "The café decision is still open.": base tree produced The caf� decision (accent lost, no c3 a9), HEAD produced The café decision with byte…
A live idle codex 0.154 pane classifies as an empty composer, so the steering doorbell is not deferred ✅ pass live Real codex-cli 0.154.0 launched in an isolated Herdr pane; fm_backend_herdr_composer_state (the production cursorless styled profile, styled=1 cursor=0 identity=1 rows=20) answered empty - evidenc…
Adversarial: codex 0.154's braille starfield rows read as furniture (empty) rather than wrapped typed input (pending) ⏸️ untested no codex-cli 0.154.0 does not draw the idle braille starfield (nor the grey composer background it accompanies) on this Linux host, so there is no live screen that exercises the braille rule. The upstrea…
The shfmt reformat of fm-spawn.sh is behaviour-preserving: a task still spawns onto and tears down from a real Herdr backend ✅ pass live tests/fm-backend-autodetect-smoke.test.sh against the real installed Herdr: real bin/fm-spawn.sh auto-detected herdr, recorded backend=herdr with herdr_session/workspace/tab/pane in meta, its launch…
The shfmt reformat of fm-backlog-handoff.sh is behaviour-preserving: backlog items still move, wake, and stay idempotent ✅ pass live tests/fm-backlog-handoff.test.sh drives the real bin/fm-backlog-handoff.sh executable against real backlog files (tmux/gh/treehouse stubbed): all cases pass including crash-then-retry durability, co…
  • git ls-remote https://github.com/kunchenguid/firstmate.git refs/heads/main compared against local HEAD, git merge-base --is-ancestor aa92177 HEAD, git log aa92177..HEAD, merge-commit and conflict-marker scan over every changed file
  • Live: real bin/fm-send.sh against a live Herdr pane (isolated bin/fm-herdr-lab.sh session) with a real kind=secondmate task - empty, empty-string, whitespace-only, tab+newline, a real message, and --key Enter
  • Live regression: the same empty marked secondmate steer against the pre-sync tree extracted from base aa92177 (git archive aa92177 | tar -x), showing exit 0 + marker-only record + minted pending-reply
  • Live: real bin/fm-captain-hold.sh hold on a real tasks-axi backlog with JSON::PP allow_nonref forced off via a PERL5OPT shim, pre-sync tree vs HEAD
  • Live: real bin/fm-teardown.sh retaining a captain-held row whose body carries café, asserting bytes (c3 a9) not decoded strings, pre-sync tree vs HEAD
  • Live: real codex-cli 0.154.0 launched in an isolated Herdr pane, captured with fm_backend_herdr_capture_ansi and classified with fm_backend_herdr_composer_state (styled=1 cursor=0 identity=1 rows=20); same frame re-classified with the base-commit composer library for a before/after
  • Starfield hunt: 750+ sampled frames of live idle codex across gpt-daybreak-blue-latest, gpt-6-astra, and gpt-6-astra + fast mode with a real attached Herdr TUI viewer (fm-herdr-lab.sh viewer start)
  • bin/fm-test-run.sh tests/fm-send-inbox.test.sh tests/fm-captain-hold-lifecycle.test.sh tests/fm-composer-lib.test.sh tests/fm-composer-ghost.test.sh
  • bin/fm-test-run.sh tests/fm-backend-herdr-smoke.test.sh tests/fm-backend-autodetect-smoke.test.sh tests/fm-spawn-batch.test.sh tests/fm-spawn-dispatch-profile.test.sh tests/fm-spawn-worktree-settle.test.sh tests/fm-backlog-handoff.test.sh tests/fm-backlog-atomicity.test.sh
  • POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true bin/fm-test-run.sh tests/fm-backend-herdr-smoke.test.sh tests/fm-backend-autodetect-smoke.test.sh (re-drive after fixing the host shell blocker)
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 1)
✅ **Push** - passed

✅ No issues found.

…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
@brentsec brentsec closed this Sep 15, 2026
@brentsec brentsec reopened this Sep 15, 2026
tbillings28 and others added 2 commits September 15, 2026 03:50
…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.
@brentsec brentsec changed the title Sync main with official firstmate main b85e28b5 Sync main with official firstmate main 2da3c5e2 Sep 15, 2026
@brentsec brentsec changed the title Sync main with official firstmate main 2da3c5e2 fix(bin): sync upstream captain-hold, codex composer, and empty-steer fixes Sep 15, 2026
@brentsec
brentsec merged commit 8ff3a80 into main Sep 15, 2026
14 of 16 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.

4 participants