feat(bin): distinguish firstmate's own pane from crewmate panes - #1084
feat(bin): distinguish firstmate's own pane from crewmate panes#1084sbracewell64 wants to merge 6 commits into
Conversation
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.
|
CI verification available. Cross-fork PRs here do not run workflows without maintainer approval, so this PR shows no checks. The repository's own |
|
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 |
|
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. |
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 geton 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:
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
firstmatetofirstmate-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_findstill adopts a legacy-labeled workspace, and a newfm_backend_herdr_workspace_migrate_legacy_labelrenames it in place on the spawn path, preserving workspace id, tabs, and panes. Both paths require the workspace to hold at least onefm-<id>tab, so a same-named human workspace is neither adopted nor renamed. The2ndmate-<id>secondmate form and thefm-<task-id>tab label format are unchanged.bin/fm-label-self.shplusfm_backend_self_endpoint_id/fm_backend_label_self/fm_backend_current_self_labelwith tmux and herdr arms, wired intobin/fm-session-start.shas 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, anyfm-prefixed label, and backends with no verified self-label operation.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 usingworking:before a terminal state or the samedone:/failed:verb after one, neverblocked:/needs-decision:. Test coverage was extended across the herdr, tmux, backend, brief, crew-state, and session-start suites, andtests/lib.shnow exports an emptyFM_SELF_LABELso 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
/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-26Evidence: 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=0Evidence: 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 adoptedEvidence: 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-heronEvidence: 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.Evidence: fm-brief.sh rules 8 and 9 as a crewmate reads them
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=0Pipeline
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 totmux display-message -p '#{window_id}', 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 "$RUNTIME" 2>&1)merges stderr into the value that thecase "$CURRENT" 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 secondworkspace listresponse (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.
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.shbin/fm-test-run.sh tests/fm-backend.test.shbin/fm-test-run.sh tests/fm-brief.test.shbin/fm-test-run.sh tests/fm-crew-state.test.shbin/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.shbin/fm-test-run.sh tests/fm-instruction-owners.test.shRegression check: extracted base commit a5fe1bc withgit archiveand re-rantests/fm-session-start.test.shandtests/fm-backend-tmux-smoke.test.shthere - both fail identically, so neither is a regressionRan 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 checkManual e2e on real tmux 3.6 (private socket): captain-launched window named "1" plus two fm_backend_tmux_create_task crewmate windows; ran the realbin/fm-session-start.shinside the supervisor pane -> window renamed to "firstmate" silently; ranbin/fm-label-self.shinside an fm-<id> crewmate pane -> refused with a note, name unchanged; captured the real attached client status line via a nested tmux clientManual e2e on real herdr 0.7.5 (private throwaway sessions): legacyfirstmateworkspace holding an fm-<id> tab ->fm_backend_herdr_container_ensurerenamed it in place tofirstmate-crewwith the same workspace id and tab; a captain-lookalikefirstmateworkspace with no fm- tab -> neither adopted nor renamed, a fresh crew workspace created instead;fm_backend_herdr_workspace_labelresolves 2ndmate-<id> with no -crew suffix and an empty legacy label for a secondmate homeManual e2e on real herdr:bin/fm-label-self.shin 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 renamedGenerated a real brief withbin/fm-brief.sh demo-lane firstmateand captured rules 8 and 9 as a crewmate reads themDrove three real status streams throughstatus_open_decisions/status_open_activitiesinbin/fm-classify-lib.shto show the rule 9 working:/done: forms leave nothing open while the forbidden blocked: form leaves a permanent unresolved decisionVerified the developer's live herdr session (tabs and workspace labels) was unchanged before and after the run, and left the worktree clean (git status --porcelainempty)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.