Skip to content

feat(bin): distinguish firstmate's own pane from crewmate panes - #1084

Open
sbracewell64 wants to merge 6 commits into
kunchenguid:mainfrom
sbracewell64:fm/crew-pane-identity-guardrails
Open

feat(bin): distinguish firstmate's own pane from crewmate panes#1084
sbracewell64 wants to merge 6 commits into
kunchenguid:mainfrom
sbracewell64:fm/crew-pane-identity-guardrails

Conversation

@sbracewell64

Copy link
Copy Markdown

Intent

Make it impossible for the captain to mistake a crewmate pane for firstmate.

Incident this fixes (2026-07-26): the captain opened a crewmate's pane believing it was firstmate and issued cross-lane instructions there. Nothing was lost only because that crewmate volunteered its identity and asked before acting, and firstmate's own records still went stale for a day, because a crewmate cannot write another lane's state. Four mechanical contributors were identified and all four are addressed here. The task was explicitly scoped to the firstmate repo only.

Item 1 - the herdr crew workspace was labeled "firstmate" but never contains firstmate. fm_backend_herdr_workspace_label resolved the PRIMARY home to the constant "firstmate". That is the workspace firstmate SPAWNS CREWMATES INTO; firstmate itself is launched by the captain, never by fm-spawn.sh, so it is the one workspace firstmate is guaranteed NOT to be in. Live evidence at the time: herdr workspace wC labeled "firstmate" held two crewmate tabs and no supervisor, while firstmate sat in wN labeled "project-finance" from unrelated earlier use. The constant is now FM_BACKEND_HERDR_PRIMARY_LABEL='firstmate-crew'.

Item 1 migration - this was the constrained part of the task. The prior comment documented that "firstmate" was byte-identical to every pre-existing task's recorded label specifically so no forced migration was needed, so the brief required establishing from the code, not assumption, what actually reads the label and what happens to a task recorded under the old one, and required either a provably safe migration or an evidenced statement that a clean one is impossible. What the code shows: NO task metadata ever records the workspace LABEL. fm-spawn.sh records herdr_workspace_id and herdr_tab_id (ids) plus window=:, and every send/peek/capture/teardown path addresses the recorded pane id. The only label-keyed consumer is fm_backend_herdr_workspace_find, which serves both spawn-time container adoption and fm_backend_herdr_list_live recovery/orphan discovery. So the migration is deliberately two-sided: (a) fm_backend_herdr_workspace_find still ADOPTS a legacy-labeled workspace, so a home with tasks under the old label keeps both spawning and recovery working even if nothing is ever renamed; and (b) fm_backend_herdr_workspace_migrate_legacy_label renames that same workspace IN PLACE on the spawn path, so its workspace id, tabs, and panes never move and no recorded target changes. Both the adoption and the rename require the legacy-labeled workspace to hold at least one fm- tab. That condition is intentional and does double duty: it is exactly the case where migration matters (a workspace with no fm- tab holds no recoverable task by definition), and it is positive proof the workspace is firstmate's own container rather than a human's, because only firstmate ever creates an fm- tab. This additionally hardens the 2026-07-02 self-kill incident class: a captain's workspace whose label merely collides (herdr derives an unlabeled workspace's displayed label from its cwd basename) is now neither adopted nor renamed, which is strictly stronger than the pre-existing adopt-and-never-prune gate. The migration is best-effort by design and always returns 0: it prints nothing, never fails a spawn, and a failed rename costs only a stale label because find already adopts it. It also refuses when a canonically labeled workspace already exists, so two containers are never merged onto one label.

Item 1 secondmate form - the brief asked to keep the secondmate form parallel and consistent. I deliberately kept it as "2ndmate-" with NO -crew suffix, and the captain explicitly confirmed this decision after I raised it, so it is intentional and not an oversight. Reason: unlike the primary, a secondmate IS spawned by fm-spawn.sh into its own home's workspace (fm-spawn.sh's herdr arm shadows FM_HOME for exactly that call, and the existing real-herdr e2e asserts it), so that workspace genuinely contains the supervisor it names. Suffixing it would introduce the very mislabel this task removes. The rule encoded and documented is "a workspace label names what the workspace actually contains", which is the consistency the brief asked for.

Item 2 - firstmate's own tab was unlabeled (it carried the bare positional label "1"), so the supervisor had no visible front door. New bin/fm-label-self.sh, invoked once from bin/fm-session-start.sh's digest, labels the endpoint the caller is running in. It resolves the runtime with fm_backend_detect (the runtime this process is CURRENTLY executing inside) rather than fm_backend_name, because config/backend names the backend NEW TASKS spawn into, which can legitimately differ from the terminal the captain launched firstmate in. It always exits 0 and is silent on success, printing exactly one plain-English "note:" line otherwise - deliberately plain prose rather than a new diagnostic token, so it is never mistaken for an actionable bootstrap diagnostic that would send an agent to load the bootstrap-diagnostics skill.

Item 2 refusals are identity protections, not cosmetics, and are deliberate: (a) a SECONDMATE home is refused outright, because a secondmate's own endpoint was created by the parent's fm-spawn.sh and is labeled fm-, which is the parent's identity handle for it (fm_backend_expected_label_of_selector, and herdr's label-matched recovery in fm_backend_herdr_list_live), so renaming it would break the parent's send/peek/recovery path; (b) any label starting with fm- is refused because that prefix is the reserved task-endpoint namespace and herdr's recovery scan treats every fm-* tab in a home's workspace as a live task; (c) a runtime with no verified self-label operation reports the limitation instead of faking it - only tmux and herdr, the two runtime-auto-detected backends, implement an arm, and zellij/orca/cmux deliberately return an error rather than pretending.

Item 2 evidence that the label sticks - the brief required establishing whether the label survives the harness rewriting its own terminal title, and reporting a finding rather than shipping a label that silently does nothing. It sticks in both implemented runtimes, verified empirically: (herdr 0.7.5) a read-only herdr pane get on a live Claude crewmate pane showed terminal_title "the harness's live OSC title" as a field SEPARATE from the tab label, with the tab still carrying its spawn-time fm- label - the same property firstmate's whole label-based recovery already depends on; (real tmux 3.6a, private socket) a renamed window kept its name across an application OSC 2 title change under default options AND under a hostile allow-rename on plus global automatic-rename on config. The tmux arm still pins automatic-rename/allow-rename off exactly like fm_backend_tmux_create_task does, as defense in depth.

Hard constraint honored across items 1 and 2: crewmate TAB labels are load-bearing, not cosmetic. The fm- tab/window label format is UNCHANGED, and fm_backend_expected_label_of_selector still derives the same expected label, so endpoint identity verification is untouched. The brief stated that changing that format would be a needs-decision, not something to do.

Items 3 and 4 - bin/fm-brief.sh scaffold. Rule 8 (both ship and scout scaffolds) requires a crewmate addressed by a human about anything outside its assigned task to state plainly, BEFORE acting, that it is a worker on task and not firstmate, that merges, cross-lane work, other lanes' state, and fleet supervision belong to firstmate, and then to confirm the human still wants it to proceed. Today's crewmate did this on its own initiative and it is the single reason the incident ended cleanly; this makes it contract rather than luck. Rule 9 requires that an override actually carried out on the captain's explicit instruction be recorded with a status line, so firstmate's records self-heal instead of going stale.

The status verb in rule 9 is a deliberate, checked choice, not an arbitrary one. The brief required using the existing status protocol with no new state verb, and required checking bin/fm-classify-lib.sh so the line classifies correctly and does not read as an unresolved blocker forever. blocked: and needs-decision: OPEN a keyed decision in status_open_decisions that only an explicit resolution or a verified captain-held transfer closes, which is the wrong shape for an action that is already finished. A bare working: line with the default key is nonterminal, is folded by status_open_activities as a phase, and is closed naturally by the task's own later done: line. A colocated test drives a real two-line status stream through both folds to prove exactly that.

Deliberate scope decisions a reviewer reading only the diff would not know:

  • The tmux detached session named "firstmate" and zellij's shared session named "firstmate" have the same conceptual mislabel, but their names are embedded in every recorded task target (window=firstmate:fm-), so renaming them is a genuinely breaking migration. They were reported to the captain and recorded separately as out of scope for this task, not overlooked.
  • bin/fm-backend-hometag-lib.sh's "firstmate" prefix (zellij/cmux scoped task titles) is likewise load-bearing identity recorded in metadata and was intentionally left alone.
  • fm_backend_herdr_projection_order_best_effort's is_top_level_parent WAS updated to accept the new primary label (and the legacy one), because without it a migrated home's own workspace would stop being recognized as a top-level parent for every new projection, not just legacy shapes.
  • is_legacy_child / is_legacy_child_for were deliberately NOT aliased to the new label, and this is documented inline. That label format predates both the current projection-child format and this rename, so no legacy child can ever carry the new primary label; the only consequence is that a leftover legacy child of a renamed primary is not recognized as that primary's child, which degrades to skipped best-effort ordering and a flat-layout restart binding - both already the existing safe outcomes for any unrecognized shape, in an opt-in default-off feature.
  • The migration is called only from fm_backend_herdr_container_ensure (the spawn path), so read-only recovery and list paths keep their no-mutation contract. A projected spawn under presentation spaces therefore falls back to the flat layout once before the rename lands; that is documented, and it is the same safe fallback any unmatched projection binding already takes.
  • tests/lib.sh now exports an empty FM_SELF_LABEL suite-wide. This is a real isolation fix, not incidental: several suites drive the real bin/fm-session-start.sh, so without it a suite run from inside a real tmux window or herdr tab would relabel the developer's own window or tab. An explicitly empty label is the script's documented no-op (hence ${FM_SELF_LABEL-firstmate}, not :-), and the tests that need real behavior set it explicitly for exactly their own invocation.
  • Existing test fixtures that asserted the old label were updated rather than deleted, and the label-collision prune-safety fixtures were re-pointed at the new label so the structural created-vs-adopted gate is still exercised; a NEW test was added for the strictly stronger property that a legacy-labeled lookalike holding no task tab is neither adopted nor renamed.

Coverage added, colocated per repo convention (no new runner, existing suites extended): label resolution and the legacy/canonical precedence, adoption of a legacy workspace holding task tabs, refusal to adopt or rename a task-tab-less lookalike, in-place rename with the original workspace id preserved, no-op when a canonical workspace exists, zero herdr calls for a secondmate home, the full container_ensure migrate-then-adopt path, herdr self-label from the injected tab id plus the pane_get fallback plus refusal outside a herdr pane, the dispatcher's unsupported-backend reporting, the real-tmux self-label including the hostile-options terminal-title persistence check, both new brief rules in ship and scout scaffolds, the classify-lib fold proof, and the fm-label-self.sh refusals plus its session-start wiring.

Three test failures in this environment are pre-existing and were each confirmed against the base branch with the changes stashed, so they are not regressions from this work: tests/fm-backend-tmux-smoke.test.sh ("the tmux task shell did not become ready"), tests/fm-session-start.test.sh ("MISSING diagnostic did not appear at all", because /usr/bin/node is on the test base path so removing the fake node changes nothing), and tests/fm-watcher-lock.test.sh ("restart did not attach to the verified healthy peer"). The Pi extension suites fail here with node ESM module-format errors, also environmental.

Repo conventions followed: shellcheck-clean via bin/fm-lint.sh, one sentence per line in tracked Markdown, plain dashes, no agent commit co-author, mechanics kept in script headers rather than restated in AGENTS.md (AGENTS.md deliberately not touched - fm-brief.sh and fm-session-start.sh already own their scaffold and composition contracts under the one-owner rule), and docs/herdr-backend.md plus docs/tmux-backend.md updated as the classified owners for the label, the migration note, and the self-label behavior.

FOLLOW-UP CONTEXT for this re-run: an earlier run of this exact branch went green through lint and then failed at push with a 403, because that run was created BEFORE the fork push target was configured and so resolved the upstream URL (kunchenguid/firstmate) rather than the fork (sbracewell64/firstmate). That was an infrastructure timing issue, not a defect in the change, and a sibling run created after the config has since pushed successfully. Custody of that run's commits was returned with the documented 'no-mistakes axi sync --recover', so this branch already carries its three pipeline fix commits on top of the original: 50d4410 applied the two escalated ask-user fixes plus the auto-fix dedup (a current-label refusal so self-labeling can never rename a live fm- worker endpoint and fails closed when the current label cannot be read; a two-branch rule 9 so an override recorded after a task's own done:/failed: uses that same terminal verb instead of reopening a finished task; and a single shared helper for the herdr canonical/legacy/task-tab workspace precedence). f5526e8 fixed six hard-coded pre-rename 'firstmate' literals in tests/fm-backend-herdr-presentation-e2e.test.sh that the original sweep missed. 8cdb429 corrected stale label facts in docs/architecture.md and docs/configuration.md, added the FM_SELF_LABEL note, the docs/scripts.md row for the new script, and the tmux/herdr verification records. Those commits are intended and must be preserved. Expect a cross-fork pull request into kunchenguid/firstmate.

What Changed

  • Renamed the primary herdr crew workspace label from firstmate to firstmate-crew (FM_BACKEND_HERDR_PRIMARY_LABEL), since that workspace only ever holds crewmate/scout/secondmate tabs and never firstmate itself. Migration is two-sided and label-keyed only: fm_backend_herdr_workspace_find still adopts a legacy-labeled workspace, and a new fm_backend_herdr_workspace_migrate_legacy_label renames it in place on the spawn path, preserving workspace id, tabs, and panes. Both paths require the workspace to hold at least one fm-<id> tab, so a same-named human workspace is neither adopted nor renamed. The 2ndmate-<id> secondmate form and the fm-<task-id> tab label format are unchanged.
  • Added bin/fm-label-self.sh plus fm_backend_self_endpoint_id / fm_backend_label_self / fm_backend_current_self_label with tmux and herdr arms, wired into bin/fm-session-start.sh as a silent, always-exit-0 step that labels the endpoint firstmate is running in. The endpoint is resolved once from process-stable markers ($TMUX_PANE, HERDR_TAB_ID/HERDR_PANE_ID) and shared by the read and the rename; it fails closed and refuses secondmate homes, any fm- prefixed label, and backends with no verified self-label operation.
  • Added crewmate brief rules 8 and 9 to both the ship and scout scaffolds in bin/fm-brief.sh: declare worker-not-firstmate identity before acting on any off-brief human request, and record a carried-out override with a status line using working: before a terminal state or the same done:/failed: verb after one, never blocked:/needs-decision:. Test coverage was extended across the herdr, tmux, backend, brief, crew-state, and session-start suites, and tests/lib.sh now exports an empty FM_SELF_LABEL so suites driving the real session-start script cannot relabel a developer's own window or tab. Two suites (fm-backend-tmux-smoke, fm-session-start) have pre-existing failures reproduced on the base commit.

Risk Assessment

✅ Low: All three round-1 findings were fixed structurally rather than patched at the call site - the caller's own endpoint is now resolved exactly once through a new fm_backend_self_endpoint_id dispatcher and threaded into both the refusal and the rename, the tmux client-relative fallback that could name a bystander window was removed in favor of failing closed, and the guard value is now stdout-only - each with colocated coverage, leaving a well-bounded, backward-compatible change with no remaining substantiated defects.

Testing

I exercised the change end to end on real tmux 3.6 and real herdr 0.7.5 rather than only through fakes: firstmate's own pane relabels itself to "firstmate" at session start and the label survives a continuous harness OSC 2 title rewrite even under hostile allow-rename/automatic-rename options, a crewmate pane running the same script is refused with a plain-English note and keeps its fm-<id> name, a legacy herdr "firstmate" workspace holding a task tab is renamed in place to "firstmate-crew" with its workspace id and tabs intact while a task-tab-less lookalike is left untouched, and fm-brief.sh's new rules 8 and 9 render in a generated brief with the rule 9 verb proven to fold as a nonterminal phase instead of a permanent open decision. Twelve targeted suites ran, including four real-herdr gated e2e suites that actually executed here; all pass except two pre-existing failures (fm-session-start's MISSING diagnostic and fm-backend-tmux-smoke's interactive shell readiness) that I reproduced on the base commit, so they are not regressions. The tmux smoke failure sits ahead of the branch's own new self-label assertions, so I ran that section in isolation and confirmed all four pass. No PNG or GIF was possible because this environment has no Chrome, PIL, or ImageMagick; instead I captured the real ANSI tmux client screen and rendered it as an SVG image plus a self-contained HTML evidence page carrying every real capture.

Evidence: Rendered evidence page - all real captures in one surface
<!doctype html>
<html><head><meta charset="utf-8"><title>Crew pane identity guardrails - evidence</title>
<style>
 body { background:#12141a; color:#e6e6e6; font-family:-apple-system,Segoe UI,Roboto,sans-serif; margin:0; padding:32px 40px 60px; }
 h1 { font-size:22px; margin:0 0 4px; letter-spacing:.2px }
 .sub { color:#9aa0aa; font-size:13px; margin-bottom:28px }
 h2 { font-size:15px; margin:34px 0 6px; color:#ffd866; font-weight:600 }
 .cap { color:#9aa0aa; font-size:12.5px; margin:0 0 10px }
 pre { background:#0a0c10; border:1px solid #262b34; border-radius:6px; padding:14px 16px; overflow-x:auto;
        font-family:"DejaVu Sans Mono",ui-monospace,Menlo,Consolas,monospace; font-size:12px; line-height:1.45; color:#d5d9de; margin:0 }
 pre.screen { font-size:11.5px; line-height:1.3; background:#000; border-color:#3ea34a }
 .grid { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:18px }
 .tag { display:inline-block; font-size:11px; padding:2px 8px; border-radius:99px; background:#1d2430; color:#79b8ff; margin-left:8px; vertical-align:middle }
</style></head><body>
<h1>Make a crewmate pane unmistakable for firstmate <span class="tag">real tmux 3.6 &amp; herdr 0.7.5</span></h1>
<div class="sub">Evidence captured on branch <code>fm/crew-pane-identity-guardrails</code> at 8bab78f. Every panel below is real command output, not a mock.</div>

<h2>1. The captain's actual tmux screen after firstmate labels its own window</h2>
<p class="cap">A real attached tmux client. The status line reads the supervisor window as <b>firstmate</b>, sitting beside the two <code>fm-&lt;task-id&gt;</code> crewmate windows. On the right, the live harness terminal title (<code>claude - firstmate - ...</code>) is a separate field, so the OSC 2 rewrite never steals the window name back - captured under a hostile <code>allow-rename on</code> + <code>automatic-rename on</code> global config.</p>
<pre class="screen">firstmate - supervising 2 crewmates

$ bin/fm-label-self.sh   # silent: this window is now labeled &quot;firstmate&quot;












<span style="color:#1b1b1b"></span><span style="color:#1b1b1b;background:#3ea34a">[firstmate0:firstmate* 1:fm-brisk-heron  2:fm-vast-otter                                                                 &quot;claude - firstmate - &quot; 12:57 26-Jul-26</span></pre>

<h2>2. Full tmux walk-through: before, after, and the crewmate that is left alone</h2>
<pre>
=== STEP 1 - what the captain sees BEFORE this change ===
The supervisor window is the bare positional &quot;1&quot;. Nothing in the window
list says which window is firstmate.

  0: 1
  1: fm-brisk-heron
  2: fm-vast-otter

=== STEP 2 - firstmate runs bin/fm-session-start.sh in its own window ===
Session-start lines that mention the self-label step (silent on success):
6:LOCK
10:●  READ-ONLY SESSION - FLEET LOCK OWNERSHIP WAS NOT VERIFIED
  (no &quot;note:&quot; line above means the endpoint was labeled successfully)

=== STEP 3 - what the captain sees AFTER this change ===
  0: firstmate
  1: fm-brisk-heron
  2: fm-vast-otter

Same window, read back individually:
  window_name    = firstmate
  automatic-rename = off / allow-rename = off (pinned by the adapter)

=== STEP 4 - the harness keeps rewriting its terminal title ===
  live pane_title  = claude - firstmate - supervising 2 crewmates
  window_name      = firstmate   &lt;- the label STICKS through the OSC 2 rewrite

=== STEP 5 - a CREWMATE runs the very same session start ===
A crewmate working in a firstmate-repo worktree runs bin/fm-session-start.sh
too. Without the current-label refusal it would rename its own fm-&lt;id&gt;
window to &quot;firstmate&quot; - the incident, inverted.

What the crewmate pane printed:
  note: this terminal tab is already a worker endpoint called &#x27;fm-vast-otter&#x27;, so it was left alone: that is the name firstmate reaches its crew by, and this pane is not firstmate.
  exit=0

Window list after the crewmate ran it:
  0: firstmate
  1: fm-brisk-heron
  2: fm-vast-otter

=== STEP 6 - the final window list the captain reads ===
--------------------------------------------------------------------------------
  0: firstmate
  1: fm-brisk-heron
  2: fm-vast-otter
--------------------------------------------------------------------------------
firstmate has a front door; every other window is a fm-&lt;task-id&gt; worker.

=== STEP 7 - the attached client status line ===
The real attached tmux client, exactly as the captain sees it (status line at the bottom):
--------------------------------------------------------------------------------























[firstmate0:firstmate* 1:fm-brisk-heron  2:fm-vast-otter-                                                                                                        &quot;claude - firstmate - &quot; 12:53 26-Jul-26
--------------------------------------------------------------------------------</pre>

<div class="grid">
<div>
<h2>3. herdr: firstmate labels its own tab, refuses a crewmate's</h2>
<p class="cap">Real herdr server, private throwaway session.</p>
<pre>=== BEFORE - what the captain sees in herdr&#x27;s tab strip ===
  tab w1:t1  label=1
  tab w1:t2  label=fm-brisk-heron

firstmate is running in w1:t1, whose label is the bare positional &quot;1&quot;.
Nothing distinguishes it from a crewmate tab except the fm- prefix.

=== firstmate runs bin/fm-label-self.sh in its own tab ===
exit=0  output=&lt;silent - the success contract&gt;

=== a CREWMATE runs the very same script in its own tab ===
exit=0
output: note: this terminal tab is already a worker endpoint called &#x27;fm-brisk-heron&#x27;, so it was left alone: that is the name firstmate reaches its crew by, and this pane is not firstmate.

=== genuinely outside any herdr pane (HERDR_TAB_ID and HERDR_PANE_ID unset) ===
exit=0
output: note: could not work out which herdr terminal tab this firstmate is running in, so nothing was renamed. error: not running inside a herdr pane (neither HERDR_TAB_ID nor HERDR_PANE_ID is set) 

=== fail-closed: a tab whose CURRENT label cannot be read ===
(a tab id that does not exist in this session - an unreadable label is not
evidence the tab is safe to rename, so nothing is renamed)
exit=0
output: note: could not read what this terminal tab is currently called in herdr, so it was left alone rather than risk renaming a worker endpoint. error: could not read the current label of herdr tab w9:t9 

=== AFTER - what the captain sees in herdr&#x27;s tab strip ===
  tab w1:t1  label=firstmate
  tab w1:t2  label=fm-brisk-heron

The supervisor tab now reads &quot;firstmate&quot;; every crewmate tab is untouched.</pre>
</div>
<div>
<h2>4. herdr: the crew workspace label and its migration</h2>
<p class="cap">The spaces sidebar the captain reads.</p>
<pre>=== The label the adapter resolves for each kind of home ===
  primary home            -&gt; firstmate-crew
  its legacy label        -&gt; firstmate
  secondmate home         -&gt; 2ndmate-harbor-pilot   (no -crew suffix: fm-spawn.sh DOES put
                                          the secondmate itself in there)
  secondmate legacy label -&gt; &quot;&quot;   (nothing to migrate)


=== CASE A - a home whose tasks were recorded under the OLD label ===
Set up exactly what such a captain has today: a workspace labeled
&quot;firstmate&quot; holding a live fm-&lt;id&gt; crewmate tab.

BEFORE - the spaces sidebar:
  w1  label=firstmate
  tabs inside w1:
    tab w1:t2  label=fm-legacy-lane

The captain sees a space called &quot;firstmate&quot; that contains only workers.

Now firstmate spawns a crewmate (bin/fm-spawn.sh -&gt; container_ensure):
  container_ensure resolved -&gt; w1

AFTER - the spaces sidebar:
  w1  label=firstmate-crew
  tabs inside w1:
    tab w1:t2  label=fm-legacy-lane

  same workspace id?  before=w1  after=w1  -&gt; YES - renamed IN PLACE, no task target moved
  recovery still finds it? fm_backend_herdr_workspace_find -&gt; w1


=== CASE B - a captain&#x27;s OWN workspace whose label merely collides ===
herdr shows an unlabeled workspace under its cwd basename, so a captain
working in a directory called &quot;firstmate&quot; gets a space labeled exactly
that. It holds no fm-&lt;id&gt; tab, so it is not firstmate&#x27;s container.

BEFORE - the spaces sidebar:
  w1  label=firstmate
  tabs inside w1 (the captain&#x27;s own, no fm- tab):
    tab w1:t1  label=1

Now firstmate spawns a crewmate into this home:
  container_ensure resolved -&gt; w2

AFTER - the spaces sidebar:
  w1  label=firstmate
  w2  label=firstmate-crew

  the captain&#x27;s &quot;firstmate&quot; workspace w1: left untouched, NOT renamed, NOT adopted
  firstmate got its own fresh crew workspace instead: w2
--------------------------------------------------------------------------------</pre>
</div>
</div>

<h2>5. fm-brief.sh rule 9: the status verb a crewmate records an override under</h2>
<p class="cap">Driven through the real <code>bin/fm-classify-lib.sh</code> folds that firstmate reads state with.</p>
<pre>=== fm-brief.sh rule 9: the verb a crewmate uses for a captain override ===
The crewmate acted outside its brief on the captain&#x27;s explicit word and
must leave a record so firstmate&#x27;s own state self-heals.

A. RULE 9, mid-task branch (working:, then the task&#x27;s own done: closes it)
  status stream:
    working: acted outside this brief on captain instruction - merged PR 42 in another lane
    done: shipped the lane deliverable
  firstmate reads it as:
    open decisions  : 
    open activities : 

B. RULE 9, already-finished branch (same terminal verb, nothing reopened)
  status stream:
    done: reproduced the bug and wrote the report
    done: acted outside this brief on captain instruction - merged PR 42 in another lane; task deliverable unchanged
  firstmate reads it as:
    open decisions  : 
    open activities : 

C. WHAT RULE 9 FORBIDS - the same record written as blocked:
  status stream:
    blocked: acted outside this brief on captain instruction - merged PR 42 in another lane
    done: shipped the lane deliverable
  firstmate reads it as:
    open decisions  : default	blocked	acted outside this brief on captain instruction - merged PR 42 in another lane
    open activities : 

  ^ the finished task still shows an unresolved decision to the captain,
    forever, which is why rule 9 names working:/done: instead.</pre>

<h2>6. fm-brief.sh rule 8: the crewmate says what it is before it acts</h2>
<p class="cap">Generated by a real <code>bin/fm-brief.sh demo-lane firstmate</code> run - this is the text a crewmate actually reads in its brief.</p>
<pre>bin/fm-brief.sh demo-lane firstmate -&gt; data/demo-lane/brief.md, Rules 8 and 9 as a crewmate reads them:

8. You are not firstmate, and a human typing into your pane may believe you are. If a human asks you
   for anything outside this task, say so plainly BEFORE you act: you are a worker on task `demo-lane`,
   not firstmate, and merges, cross-lane work, other lanes&#x27; state, and fleet supervision belong to
   firstmate. Then ask whether they still want you to proceed, and wait for their answer.
9. If you do act outside this brief on the captain&#x27;s explicit instruction, leave a durable record.
   You cannot write another lane&#x27;s records, so that line is the only way firstmate learns of it and
   keeps its own records accurate. Firstmate reconciles your state from the LAST status line, so
   which verb you use depends on where this task already is:
   - You have NOT reported a terminal state yet: append
     `working: acted outside this brief on captain instruction - {what you did, and where}`.
     Your own later `done:` closes that phase.
   - You have ALREADY reported `done:` or `failed:`: append the record under that SAME terminal
     verb, e.g. `done: acted outside this brief on captain instruction - {what you did, and where}; task deliverable unchanged`.
     A captain dropping into an already-idle, already-done pane is the COMMON case, so this is
     usually the branch you want. A bare `working:` line here would reopen a task that is really
     finished, and leave a phase open that you have no further verb to close.
   Never record this with `blocked:` or `needs-decision:`: those open a decision that stays open
   until it is explicitly closed, and a completed action is neither.</pre>

<h2>7. The change's own new real-tmux assertions</h2>
<p class="cap">tests/fm-backend-tmux-smoke.test.sh's self-label section, run against real tmux 3.6 with the pre-existing interactive-shell section (which fails identically on the base commit in this sandbox) omitted.</p>
<pre>@1
ok - real tmux: fm_backend_tmux_create_task creates a window and refuses a duplicate
ok - real tmux: fm_backend_tmux_label_self labels firstmate&#x27;s own window, refuses outside tmux, and the label survives a harness terminal-title rewrite
ok - real tmux: fm_backend_tmux_current_self_label reads the real window name, and fm-label-self.sh leaves an fm-&lt;id&gt; worker window alone
ok - real tmux: with $TMUX_PANE unset the self operations fail closed and the client&#x27;s current window keeps its name
ok - real tmux: kill removes the window and the readable session inventory authoritatively classifies it missing</pre>
</body></html>
- Evidence: The captain's real tmux status line, rendered as an image (local file: /tmp/no-mistakes-evidence/01KYFKMYCC97RTQ26HTH3NJMR9/tmux-screen.svg)
Evidence: Real attached tmux client status line (raw text capture)

[firstmate0:firstmate* 1:fm-brisk-heron 2:fm-vast-otter "claude - firstmate - " 12:57 26-Jul-26

The real attached tmux client, exactly as the captain sees it (status line at the bottom):
--------------------------------------------------------------------------------























[firstmate0:firstmate* 1:fm-brisk-heron  2:fm-vast-otter-                                                                                                        "claude - firstmate - " 12:53 26-Jul-26
--------------------------------------------------------------------------------
Evidence: Real-tmux walk-through: before, after, crewmate refusal, title persistence

=== STEP 1 - what the captain sees BEFORE this change === 0: 1 1: fm-brisk-heron 2: fm-vast-otter === STEP 3 - what the captain sees AFTER this change === 0: firstmate 1: fm-brisk-heron 2: fm-vast-otter === STEP 4 - the harness keeps rewriting its terminal title === live pane_title = claude - firstmate - supervising 2 crewmates window_name = firstmate <- the label STICKS through the OSC 2 rewrite === STEP 5 - a CREWMATE runs the very same session start === note: this terminal tab is already a worker endpoint called 'fm-vast-otter', so it was left alone: that is the name firstmate reaches its crew by, and this pane is not firstmate. exit=0


=== STEP 1 - what the captain sees BEFORE this change ===
The supervisor window is the bare positional "1". Nothing in the window
list says which window is firstmate.

  0: 1
  1: fm-brisk-heron
  2: fm-vast-otter

=== STEP 2 - firstmate runs bin/fm-session-start.sh in its own window ===
Session-start lines that mention the self-label step (silent on success):
6:LOCK
10:●  READ-ONLY SESSION - FLEET LOCK OWNERSHIP WAS NOT VERIFIED
  (no "note:" line above means the endpoint was labeled successfully)

=== STEP 3 - what the captain sees AFTER this change ===
  0: firstmate
  1: fm-brisk-heron
  2: fm-vast-otter

Same window, read back individually:
  window_name    = firstmate
  automatic-rename = off / allow-rename = off (pinned by the adapter)

=== STEP 4 - the harness keeps rewriting its terminal title ===
  live pane_title  = claude - firstmate - supervising 2 crewmates
  window_name      = firstmate   <- the label STICKS through the OSC 2 rewrite

=== STEP 5 - a CREWMATE runs the very same session start ===
A crewmate working in a firstmate-repo worktree runs bin/fm-session-start.sh
too. Without the current-label refusal it would rename its own fm-<id>
window to "firstmate" - the incident, inverted.

What the crewmate pane printed:
  note: this terminal tab is already a worker endpoint called 'fm-vast-otter', so it was left alone: that is the name firstmate reaches its crew by, and this pane is not firstmate.
  exit=0

Window list after the crewmate ran it:
  0: firstmate
  1: fm-brisk-heron
  2: fm-vast-otter

=== STEP 6 - the final window list the captain reads ===
--------------------------------------------------------------------------------
  0: firstmate
  1: fm-brisk-heron
  2: fm-vast-otter
--------------------------------------------------------------------------------
firstmate has a front door; every other window is a fm-<task-id> worker.

=== STEP 7 - the attached client status line ===
The real attached tmux client, exactly as the captain sees it (status line at the bottom):
--------------------------------------------------------------------------------























[firstmate0:firstmate* 1:fm-brisk-heron  2:fm-vast-otter-                                                                                                        "claude - firstmate - " 12:53 26-Jul-26
--------------------------------------------------------------------------------
Evidence: Real-herdr crew workspace label and in-place legacy migration

=== CASE A - a home whose tasks were recorded under the OLD label === BEFORE: w1 label=firstmate tab w1:t2 label=fm-legacy-lane AFTER: w1 label=firstmate-crew tab w1:t2 label=fm-legacy-lane same workspace id? before=w1 after=w1 -> YES - renamed IN PLACE, no task target moved recovery still finds it? fm_backend_herdr_workspace_find -> w1 === CASE B - a captain's OWN workspace whose label merely collides === BEFORE: w1 label=firstmate tab w1:t1 label=1 (no fm- tab) AFTER: w1 label=firstmate w2 label=firstmate-crew the captain's "firstmate" workspace w1: left untouched, NOT renamed, NOT adopted

=== The label the adapter resolves for each kind of home ===
  primary home            -> firstmate-crew
  its legacy label        -> firstmate
  secondmate home         -> 2ndmate-harbor-pilot   (no -crew suffix: fm-spawn.sh DOES put
                                          the secondmate itself in there)
  secondmate legacy label -> ""   (nothing to migrate)


=== CASE A - a home whose tasks were recorded under the OLD label ===
Set up exactly what such a captain has today: a workspace labeled
"firstmate" holding a live fm-<id> crewmate tab.

BEFORE - the spaces sidebar:
  w1  label=firstmate
  tabs inside w1:
    tab w1:t2  label=fm-legacy-lane

The captain sees a space called "firstmate" that contains only workers.

Now firstmate spawns a crewmate (bin/fm-spawn.sh -> container_ensure):
  container_ensure resolved -> w1

AFTER - the spaces sidebar:
  w1  label=firstmate-crew
  tabs inside w1:
    tab w1:t2  label=fm-legacy-lane

  same workspace id?  before=w1  after=w1  -> YES - renamed IN PLACE, no task target moved
  recovery still finds it? fm_backend_herdr_workspace_find -> w1


=== CASE B - a captain's OWN workspace whose label merely collides ===
herdr shows an unlabeled workspace under its cwd basename, so a captain
working in a directory called "firstmate" gets a space labeled exactly
that. It holds no fm-<id> tab, so it is not firstmate's container.

BEFORE - the spaces sidebar:
  w1  label=firstmate
  tabs inside w1 (the captain's own, no fm- tab):
    tab w1:t1  label=1

Now firstmate spawns a crewmate into this home:
  container_ensure resolved -> w2

AFTER - the spaces sidebar:
  w1  label=firstmate
  w2  label=firstmate-crew

  the captain's "firstmate" workspace w1: left untouched, NOT renamed, NOT adopted
  firstmate got its own fresh crew workspace instead: w2
--------------------------------------------------------------------------------
Evidence: Real-herdr self-label: success, crewmate refusal, and fail-closed paths

BEFORE: tab w1:t1 label=1 tab w1:t2 label=fm-brisk-heron firstmate runs bin/fm-label-self.sh in its own tab -> exit=0, silent (the success contract) a CREWMATE runs it -> note: this terminal tab is already a worker endpoint called 'fm-brisk-heron', so it was left alone... outside any herdr pane -> note: could not work out which herdr terminal tab this firstmate is running in, so nothing was renamed. unreadable current label -> note: could not read what this terminal tab is currently called in herdr, so it was left alone rather than risk renaming a worker endpoint. AFTER: tab w1:t1 label=firstmate tab w1:t2 label=fm-brisk-heron

=== BEFORE - what the captain sees in herdr's tab strip ===
  tab w1:t1  label=1
  tab w1:t2  label=fm-brisk-heron

firstmate is running in w1:t1, whose label is the bare positional "1".
Nothing distinguishes it from a crewmate tab except the fm- prefix.

=== firstmate runs bin/fm-label-self.sh in its own tab ===
exit=0  output=<silent - the success contract>

=== a CREWMATE runs the very same script in its own tab ===
exit=0
output: note: this terminal tab is already a worker endpoint called 'fm-brisk-heron', so it was left alone: that is the name firstmate reaches its crew by, and this pane is not firstmate.

=== genuinely outside any herdr pane (HERDR_TAB_ID and HERDR_PANE_ID unset) ===
exit=0
output: note: could not work out which herdr terminal tab this firstmate is running in, so nothing was renamed. error: not running inside a herdr pane (neither HERDR_TAB_ID nor HERDR_PANE_ID is set) 

=== fail-closed: a tab whose CURRENT label cannot be read ===
(a tab id that does not exist in this session - an unreadable label is not
evidence the tab is safe to rename, so nothing is renamed)
exit=0
output: note: could not read what this terminal tab is currently called in herdr, so it was left alone rather than risk renaming a worker endpoint. error: could not read the current label of herdr tab w9:t9 

=== AFTER - what the captain sees in herdr's tab strip ===
  tab w1:t1  label=firstmate
  tab w1:t2  label=fm-brisk-heron

The supervisor tab now reads "firstmate"; every crewmate tab is untouched.
Evidence: Rule 9 status verb folded through the real classify-lib

A. RULE 9 mid-task (working: then done:) -> open decisions: <none> open activities: <none> B. RULE 9 already-finished (done: + done:) -> open decisions: <none> open activities: <none> C. FORBIDDEN (blocked: then done:) -> open decisions: default\tblocked\tacted outside this brief on captain instruction - merged PR 42 in another lane ^ the finished task still shows an unresolved decision to the captain, forever.

=== fm-brief.sh rule 9: the verb a crewmate uses for a captain override ===
The crewmate acted outside its brief on the captain's explicit word and
must leave a record so firstmate's own state self-heals.

A. RULE 9, mid-task branch (working:, then the task's own done: closes it)
  status stream:
    working: acted outside this brief on captain instruction - merged PR 42 in another lane
    done: shipped the lane deliverable
  firstmate reads it as:
    open decisions  : 
    open activities : 

B. RULE 9, already-finished branch (same terminal verb, nothing reopened)
  status stream:
    done: reproduced the bug and wrote the report
    done: acted outside this brief on captain instruction - merged PR 42 in another lane; task deliverable unchanged
  firstmate reads it as:
    open decisions  : 
    open activities : 

C. WHAT RULE 9 FORBIDS - the same record written as blocked:
  status stream:
    blocked: acted outside this brief on captain instruction - merged PR 42 in another lane
    done: shipped the lane deliverable
  firstmate reads it as:
    open decisions  : default	blocked	acted outside this brief on captain instruction - merged PR 42 in another lane
    open activities : 

  ^ the finished task still shows an unresolved decision to the captain,
    forever, which is why rule 9 names working:/done: instead.
Evidence: fm-brief.sh rules 8 and 9 as a crewmate reads them
bin/fm-brief.sh demo-lane firstmate -> data/demo-lane/brief.md, Rules 8 and 9 as a crewmate reads them:

8. You are not firstmate, and a human typing into your pane may believe you are. If a human asks you
   for anything outside this task, say so plainly BEFORE you act: you are a worker on task `demo-lane`,
   not firstmate, and merges, cross-lane work, other lanes' state, and fleet supervision belong to
   firstmate. Then ask whether they still want you to proceed, and wait for their answer.
9. If you do act outside this brief on the captain's explicit instruction, leave a durable record.
   You cannot write another lane's records, so that line is the only way firstmate learns of it and
   keeps its own records accurate. Firstmate reconciles your state from the LAST status line, so
   which verb you use depends on where this task already is:
   - You have NOT reported a terminal state yet: append
     `working: acted outside this brief on captain instruction - {what you did, and where}`.
     Your own later `done:` closes that phase.
   - You have ALREADY reported `done:` or `failed:`: append the record under that SAME terminal
     verb, e.g. `done: acted outside this brief on captain instruction - {what you did, and where}; task deliverable unchanged`.
     A captain dropping into an already-idle, already-done pane is the COMMON case, so this is
     usually the branch you want. A bare `working:` line here would reopen a task that is really
     finished, and leave a phase open that you have no further verb to close.
   Never record this with `blocked:` or `needs-decision:`: those open a decision that stays open
   until it is explicitly closed, and a completed action is neither.
Evidence: The branch's new real-tmux self-label assertions, run in isolation

ok - real tmux: fm_backend_tmux_label_self labels firstmate's own window, refuses outside tmux, and the label survives a harness terminal-title rewrite ok - real tmux: fm_backend_tmux_current_self_label reads the real window name, and fm-label-self.sh leaves an fm-<id> worker window alone ok - real tmux: with $TMUX_PANE unset the self operations fail closed and the client's current window keeps its name ok - real tmux: kill removes the window and the readable session inventory authoritatively classifies it missing EXIT=0

@1
ok - real tmux: fm_backend_tmux_create_task creates a window and refuses a duplicate
ok - real tmux: fm_backend_tmux_label_self labels firstmate's own window, refuses outside tmux, and the label survives a harness terminal-title rewrite
ok - real tmux: fm_backend_tmux_current_self_label reads the real window name, and fm-label-self.sh leaves an fm-<id> worker window alone
ok - real tmux: with $TMUX_PANE unset the self operations fail closed and the client's current window keeps its name
ok - real tmux: kill removes the window and the readable session inventory authoritatively classifies it missing
- Outcome: ⚠️ 1 info across 1 run (18m30s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 3 issues found → auto-fixed ✅
  • ⚠️ bin/backends/tmux.sh:131 - bin/backends/tmux.sh:131 - fm-label-self.sh resolves the caller's tmux window twice (once via fm_backend_tmux_current_self_label to read the name, once via fm_backend_tmux_label_self to rename it), and when $TMUX_PANE is unset fm_backend_tmux_self_window_id falls back to tmux display-message -p &#39;#{window_id}&#39;, which returns the attached client's CURRENT window rather than the caller's own. The two resolutions can therefore return different windows, so the fm-<id> worker refusal can pass on window A while the rename lands on window B - which may be a live fm-<task-id> worker window, exactly the outcome the guard exists to prevent. Resolve the window id once and pass it to both the read and the rename (the herdr arm is already deterministic because it resolves from the process-stable HERDR_TAB_ID/HERDR_PANE_ID).
  • ℹ️ bin/fm-label-self.sh:99 - bin/fm-label-self.sh:99 - CURRENT=$(fm_backend_current_self_label &#34;$RUNTIME&#34; 2&gt;&amp;1) merges stderr into the value that the case &#34;$CURRENT&#34; in fm-*) refusal tests. No current success path writes to stderr, but any future warning emitted alongside a successful label read would prepend text to $CURRENT, break the prefix match, and let the step rename a live worker endpoint - defeating the documented fail-closed contract. Capture stdout separately and use stderr only for the failure note.
  • ℹ️ tests/fm-backend-herdr.test.sh:642 - tests/fm-backend-herdr.test.sh:642 - test_container_ensure_reuses_existing_workspace still stages a second workspace list response (4.out) commented as "workspace_find's own list", but the dedup in 50d4410 made fm_backend_herdr_container_ensure share ONE resolution between the migration and the ensure, so 4.out is never consumed and the comment describes the pre-dedup call shape. The unused fixture would also silently absorb an unexpected extra herdr call rather than failing, unlike the sibling migrate-then-adopt test which asserts the list count is exactly 1.

🔧 Fix: resolve self endpoint once and fail closed without TMUX_PANE
✅ Re-checked - no issues remain.

⚠️ **Test** - 1 info
  • ℹ️ tests/fm-backend-tmux-smoke.test.sh:92 - tests/fm-backend-tmux-smoke.test.sh fails in this environment at its pre-existing interactive-shell readiness assertion (line 92, byte-identical to the base commit and confirmed failing there too). That assertion sits BEFORE the branch's four new real-tmux self-label assertions, so a normal run of this suite never reaches the new coverage here. I verified the new section separately and it passes, but the reviewer should know the suite's exit code does not currently exercise it on a machine with this sandbox's shell startup behavior.
  • bin/fm-test-run.sh tests/fm-backend-herdr.test.sh
  • bin/fm-test-run.sh tests/fm-backend.test.sh
  • bin/fm-test-run.sh tests/fm-brief.test.sh
  • bin/fm-test-run.sh tests/fm-crew-state.test.sh
  • bin/fm-test-run.sh tests/fm-session-start.test.sh (1 pre-existing failure, reproduced on base)
  • bin/fm-test-run.sh tests/fm-backend-herdr-smoke.test.sh (real herdr 0.7.5, isolated lab session)
  • bin/fm-test-run.sh tests/fm-backend-herdr-prune-safety-e2e.test.sh (real herdr)
  • bin/fm-test-run.sh tests/fm-backend-herdr-workspace-per-home-e2e.test.sh (real herdr)
  • bin/fm-test-run.sh tests/fm-backend-herdr-presentation-e2e.test.sh (real herdr)
  • bin/fm-test-run.sh tests/fm-backend-tmux-smoke.test.sh (1 pre-existing failure, reproduced on base)
  • bin/fm-test-run.sh tests/fm-documentation-audiences.test.sh
  • bin/fm-test-run.sh tests/fm-instruction-owners.test.sh
  • Regression check: extracted base commit a5fe1bc with git archive and re-ran tests/fm-session-start.test.sh and tests/fm-backend-tmux-smoke.test.sh there - both fail identically, so neither is a regression
  • Ran the branch's NEW real-tmux self-label assertions in isolation (tests/fm-backend-tmux-smoke.test.sh lines 159-end, skipping the pre-existing blocker section): all 4 pass, including the hostile allow-rename/automatic-rename OSC 2 persistence check
  • Manual e2e on real tmux 3.6 (private socket): captain-launched window named "1" plus two fm_backend_tmux_create_task crewmate windows; ran the real bin/fm-session-start.sh inside the supervisor pane -> window renamed to "firstmate" silently; ran bin/fm-label-self.sh inside an fm-<id> crewmate pane -> refused with a note, name unchanged; captured the real attached client status line via a nested tmux client
  • Manual e2e on real herdr 0.7.5 (private throwaway sessions): legacy firstmate workspace holding an fm-<id> tab -> fm_backend_herdr_container_ensure renamed it in place to firstmate-crew with the same workspace id and tab; a captain-lookalike firstmate workspace with no fm- tab -> neither adopted nor renamed, a fresh crew workspace created instead; fm_backend_herdr_workspace_label resolves 2ndmate-<id> with no -crew suffix and an empty legacy label for a secondmate home
  • Manual e2e on real herdr: bin/fm-label-self.sh in the supervisor tab (label "1") -> silent success, tab relabeled "firstmate"; in an fm-<id> crewmate tab -> refused; with no HERDR_TAB_ID/HERDR_PANE_ID -> refused; with an unresolvable tab id -> fails closed, nothing renamed
  • Generated a real brief with bin/fm-brief.sh demo-lane firstmate and captured rules 8 and 9 as a crewmate reads them
  • Drove three real status streams through status_open_decisions/status_open_activities in bin/fm-classify-lib.sh to show the rule 9 working:/done: forms leave nothing open while the forbidden blocked: form leaves a permanent unresolved decision
  • Verified the developer's live herdr session (tabs and workspace labels) was unchanged before and after the run, and left the worktree clean (git status --porcelain empty)
⚠️ **Document** - 1 info
  • ℹ️ AGENTS.md:136 - AGENTS.md's numbered session-start list (steps 1-6) does not mention the new self-label step that fm-session-start.sh runs between lock and bootstrap. Left unchanged deliberately: AGENTS.md states the script header is the single owner of composed commands and ordering, the step emits no digest section and is silent on success, and its one failure line is plain prose rather than an actionable diagnostic token, so nothing in the AGENTS.md diagnostics list is contradicted. Adding a step 1b there would synchronize a second copy of ordering the script header owns.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

On 2026-07-26 the captain opened a crewmate pane believing it was
firstmate and issued cross-lane instructions there. Nothing was lost only
because that crewmate volunteered its identity and asked first, and
firstmate's own records still went stale for a day, because a crewmate
cannot write another lane's state.

Four mechanical contributors, all fixed here.

The primary home's herdr crew workspace was labeled "firstmate" - the one
workspace firstmate is guaranteed NOT to be in, since firstmate is
launched by the captain and never by fm-spawn.sh. It is now
"firstmate-crew". The secondmate form stays "2ndmate-<id>" on purpose: a
secondmate IS spawned into its own home workspace alongside its children,
so that label already names what the workspace contains, and a "-crew"
suffix there would introduce the very mislabel this removes.

The rename is safe for a home that already has tasks under the old label.
No task metadata ever recorded the label - fm-spawn.sh records
herdr_workspace_id/herdr_tab_id and a window=<session>:<pane-id> target -
so the only label-keyed consumer is fm_backend_herdr_workspace_find, which
also feeds fm_backend_herdr_list_live recovery. That function still adopts
a legacy-labeled workspace while it holds at least one fm-<id> tab, and
the spawn path renames that exact workspace in place, so its id, tabs, and
panes never move. A legacy-labeled workspace with no task tab is neither
adopted nor renamed, which additionally protects a personal workspace that
merely shares the old name.

Firstmate's own tab carried no label at all. bin/fm-label-self.sh, run
once from the session-start digest, labels it "firstmate" under tmux and
herdr. It refuses in a secondmate home, whose fm-<secondmate-id> endpoint
label is the parent's identity handle, and refuses any fm- label, since
that prefix is the task-endpoint namespace. fm-<task-id> labels are
untouched, so endpoint identity verification is unchanged.

The label sticks in both runtimes. Herdr tracks a pane's terminal_title
separately from its tab label (verified read-only against a live Claude
pane: terminal_title rewritten by the harness, tab label still the
spawn-time fm-<id>). Real tmux 3.6a keeps a renamed window name across an
application OSC 2 title change, under default and hostile options alike.

Finally, every crewmate scaffold now carries the pane-identity pair: state
plainly that you are a worker on this task and not firstmate before acting
on anything a human asks outside the brief, then confirm; and record an
override actually carried out with a nonterminal "working:" line, so
firstmate's records self-heal. That verb is deliberate - a blocked: or
needs-decision: line would open a keyed decision that only an explicit
resolution closes.
@sbracewell64

Copy link
Copy Markdown
Author

CI verification available. Cross-fork PRs here do not run workflows without maintainer approval, so this PR shows no checks. The repository's own CI and no-mistakes-required workflows were run on the identical commit via a mirror PR in the author's fork: sbracewell64#4 — both green. No action requested; posting so the absence of checks here is not read as unverified.

@kunchenguid

kunchenguid commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Automated reminder: thanks for the PR! This branch currently has a merge conflict with the base branch.

When you get a chance, please rebase onto (or merge) the latest base branch, resolve the conflict, and push. After that, checks will re-run and the PR will get looked at again.

Noted for firstmate#1084 at 0e4aaf8f.

@kunchenguid

Copy link
Copy Markdown
Owner

Automated reminder: this PR still looks blocked on a rebase or merge conflict fix.

If you are still interested, please rebase onto the current base branch, resolve the conflict, and push.

If I do not hear back, I may close this as inactive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants