fix(bin): deliver away-mode escalations into an idle Claude composer - #16
Open
BohnBawerick wants to merge 14 commits into
Open
fix(bin): deliver away-mode escalations into an idle Claude composer#16BohnBawerick wants to merge 14 commits into
BohnBawerick wants to merge 14 commits into
Conversation
…sk record Reads a project's registered "+hardened" annotation and carries it to the worker's instructions and the task's durable record, so the quality loop that bin/fm-quality.sh will drive has a posture and a fixed base commit to work from. That script is not part of this change; it is referenced by name only. - bin/fm-project-mode.sh: --quality prints one word, standard or hardened. The two-word stdout its three callers parse is untouched, so it gets its own output path. The bracket grammar is now position-tolerant: a "+"-prefixed token is a flag and never a mode, so "[+hardened local-only]" resolves the mode behind it instead of reading the flag as an unknown mode. Unrecognized flags are still ignored rather than refused. - bin/fm-brief.sh: --quality standard|hardened, defaulting to standard and refused on scout, dreamer, and secondmate scaffolds. A hardened brief records the sibling "Quality contract: quality=hardened" line and one short quality gate section; a standard brief records neither and stays byte-identical to the pre-quality scaffold. - bin/fm-spawn.sh: the brief's quality line must agree with --quality, the same check the delivery line already gets, in both directions. quality= and base_sha= land in the task record; the base commit is captured once at spawn and read back on relaunch, never recaptured, because the loop commits each round and a later capture would narrow the gate while still reporting success. - AGENTS.md: one sentence placing quality resolution at intake. Tests execute the real interfaces. The load-bearing ones prove a project without "+hardened" and a brief scaffolded without --quality behave exactly as before: the two-word stdout is pinned across every annotation form, the two scaffolds are compared byte for byte, and the task record's key set is pinned so only quality= and base_sha= are additive.
…ture and scripts inventory
The stage 0a pilot showed the receipt cannot express real findings and that a missing head_sha makes a drifted base report not-applicable and exit 0. This revises unpublished schema v1 in place: require head_sha, duration_ms, engine, threshold, and a stable finding id; replace survivors[] with per-phase findings[]; and make verify one envelope with phases[]. bounds.budget_minutes is the missing wall-clock bound.
A short herdr recent tail can drop Claude's opening rule and classify an idle composer unknown, so native-hosted away-mode never injected. Classify a glyph immediately under a closing rule as empty, read the visible viewport for composer capture, and let herdr native idle deliver when the composer is still unknown. Max-defer retries that path before alarming. The native-hosted daemon still injects into the captain pane. A dead shell has no idle agent registration and still defers.
… proven idle composers
…erdr composer read
BohnBawerick
force-pushed
the
fm/fm-afk-native-inject-wedge
branch
from
August 22, 2026 07:33
711cf8e to
bb5f2d0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Fix away-mode escalation delivery so an overnight away run actually reaches firstmate instead of piling up undelivered for hours.
Observed 2026-08-22 in the captain's home: state/.afk was set at 02:23, the return at 09:10 reported 24401s undelivered. Every escalation survived and was delivered in the return catch-up, so nothing was lost, but nothing reached firstmate during the night. Two workers sat parked on gate decisions from roughly 03:00 until 09:10, and two finished tasks waited to be landed. The max-defer alarm fired and the marker was durable, but a loud alarm nobody is awake to read is not a delivery path.
The overnight daemon log showed 1555 composer=unknown defers on herdr (not busy). Confirm against bin/fm-composer-lib.sh's verdict for an idle Claude pane whose session is waiting between turns rather than mid-turn. Reproduce before fixing. Do not fix a cause you have only reasoned about.
The core question: does the native-hosted away path need a different flush target than the pane hosting the daemon? Weigh at least: (1) make the composer verdict correct for an idle-between-turns Claude pane so the existing flush works; (2) give the native-hosted path a delivery target that does not depend on composer state; (3) make the max-defer escape an actual delivery escalation rather than only an alarm. If more than one is needed, do more than one. Fix this completely.
Chosen shape after reproduction: keep the captain pane as the flush target (a native background job inherits HERDR_PANE_ID of that pane). Fix the composer classifier so a clipped Claude idle pair (glyph immediately under a closing ─, opening ─ dropped from a short capture) reads empty. Point herdr composer capture at --source visible (the live viewport) instead of recent scrollback, and do not tail away the composer pair. On herdr, inject_msg may deliver when the composer is unknown if native agent-state is idle; pending still defers; a dead shell has no idle agent registration and still defers. Max-defer retries that recovery path before alarming.
Acceptance: an away run whose supervisor pane sits idle between turns delivers within the daemon cadence, proven by a test that failed before the change; a wedge that does occur has a bounded automatic recovery path, not only an alarm; catch-up on return still delivers anything genuinely held; regression tests live beside the code; AGENTS.md section 8's away-mode stub, the /afk skill, and any script header or doc whose contract changed must match. Do not leave a doc describing the old behaviour.
Constraints: keep it simple, no second daemon or broker; away mode must never expand approval authority; never broadly kill watchers; stay out of bin/fm-claude-stop-autoarm.sh, bin/fm-turnend-guard.sh, and bin/fm-session-start.sh (parallel task). Delivery mode is no-mistakes; complete only with a PR whose checks are green.
What Changed
bin/fm-composer-lib.sh): a bare agent glyph whose closing─sits on the very next row now readsemptyinstead of failing cursor-less selection as a clipped Pi pair, and a newfm_composer_screen_has_agent_containersplits "no container at all" from "a container I cannot judge" for callers that want to act onunknown.bin/backends/herdr.sh):fm_backend_herdr_capture_ansireads--source visible(the live viewport) instead of tailingrecentscrollback; capture/classify/identity collapse into onefm_backend_herdr_composer_read, andfm_backend_herdr_composer_unknown_deliverablereports a pane deliverable only when a styled re-read still saysunknown, the screen carries a proven agent container, and native agent-state is idle.bin/fm-backend.sh,bin/fm-supervise-daemon.sh):inject_msgdefers onpendingas before but now delivers through anunknowncomposer when the backend proves a live idle agent composer (herdr only; every other backend returns a hard defer), and the max-defer escape retries that same recovery flush before writing the wedge marker and firing the alarm. Regression tests cover the clipped-idle verdict, the visible-source capture, the degraded-unstyled and dead-shell/modal/unidentified-row refusals, the inject path, and max-defer recovery clearing the buffer without a wedge marker; the/afkskill and the architecture, configuration, herdr-backend, wedge-alarm and runtime-backends docs are updated to match.Risk Assessment
Testing
I ran the four changed test suites plus the away-mode return suite on the target commit (all pass), then proved the regression bites by replaying the same new tests against base-commit sources, where the clipped-idle-Claude classifier case and the herdr native-idle inject case both fail. For product-level evidence I drove the real away daemon process end to end against a scripted herdr CLI standing in for the captain's supervisor pane: on base commit the run reproduces the reported overnight wedge (nothing submitted, both escalations still buffered, wedge marker written, alarm repeating), and on the target commit the identical run delivers the digest to the pane within one housekeeping tick with an empty buffer and no alarm. The same harness confirms the safety edges still refuse - a dead login shell reporting agent_status=done, and a captain's half-typed draft, both keep deferring and alarm instead of typing - and that after an overnight wedge the return catch-up (bin/fm-afk-return.sh) still delivers everything held. The real-herdr e2e test skips here because no herdr server can run headless on this machine; CI owns it. No screenshot or rendered-HTML artifact applies: the change is a background daemon and a terminal-pane classifier, so the CLI transcript of daemon log lines, submitted pane text, and buffer state is the actual end-user surface.
Evidence: Away-mode delivery: before/after end-to-end transcript
Source: Away-mode delivery: before/after end-to-end transcript
############################################################### # BEFORE - base commit 672c6d0: the reported overnight wedge # ############################################################### === scenario: idle-claude === --- daemon log (state/.supervise-daemon.log) --- daemon starting; target=default:w1:p2; backend=herdr; afk=on; batch=2s ERROR: away-mode escalation undelivered 8s; inject could not confirm a submit (supervisor pane busy or wedged). Buffer + wake-queue preserved; alarm marker written. wedge alarm: ... durable marker .subsuper-inject-wedged is the only signal ERROR: away-mode escalation undelivered 16s; ... ERROR: away-mode escalation undelivered 24s; ... --- what actually reached the supervisor pane --- (nothing submitted) --- undelivered escalation buffer --- STILL BUFFERED: fm-c1 needs a gate decision: approve the schema drop? STILL BUFFERED: fm-c2 done: PR #15 wedge alarm marker --- PRESENT --- composer capture source used --- 52 pane read --source recent ############################################################### # AFTER - target commit e35768b # ############################################################### === scenario: idle-claude === --- daemon log (state/.supervise-daemon.log) --- daemon starting; target=default:w1:p2; backend=herdr; afk=on; batch=2s --- what actually reached the supervisor pane --- SUBMITTED: FIRSTMATE_OP: v1 away-supervisor: Supervisor escalate (2 event(s)): fm-c1 needs a gate decision: approve the schema drop? | fm-c2 done: PR #15 (pre-read; re-arm not needed — watcher daemon-managed) --- undelivered escalation buffer --- (empty - everything delivered) --- wedge alarm marker --- absent --- composer capture source used --- 1 pane read --source recent 1 pane read --source visible === scenario: dead-shell === (harness exited; herdr still reports agent_status=done -> native idle) --- what actually reached the supervisor pane --- (nothing submitted) --- undelivered escalation buffer --- STILL BUFFERED: fm-c1 needs a gate decision: approve the schema drop? STILL BUFFERED: fm-c2 done: PR #15 wedge alarm marker --- PRESENT === scenario: human-typing === (captain left 'land the parked workers' unsent) --- what actually reached the supervisor pane --- (nothing submitted) --- wedge alarm marker --- PRESENT === scenario: wedge-then-return === ERROR: away-mode escalation undelivered 8s; ... alarm marker written. [captain returns 09:10: harness restarted, away mode off] --- bin/fm-afk-return.sh (the return catch-up the captain sees) --- fm-afk-launch: away mode stopped; daemon terminal torn down and .afk cleared fm-afk-return: catch-up clear; ordinary captain work may proceed --- what actually reached the supervisor pane --- SUBMITTED: FIRSTMATE_OP: v1 away-supervisor: Supervisor escalate (2 event(s)): fm-c1 needs a gate decision: approve the schema drop? | fm-c2 done: PR #15 (pre-read; re-arm not needed — watcher daemon-managed) --- undelivered escalation buffer --- (empty - everything delivered) --- wedge alarm marker --- absentEvidence: Reproduction harness used to produce the transcript
Source: Reproduction harness used to produce the transcript
Evidence: Regression proof: new tests against base-commit sources
Known and deliberately not fixed
Two findings from this run's final review round are shipping unaddressed.
The review step closed at round 4 with no findings selected, so neither reached a fix round.
They are recorded here rather than as follow-up work, so the evidence sits with the change.
ansi-capture-has-no-source-fallbackbin/backends/herdr.sh:2616-fm_backend_herdr_capture_ansiissues exactly one command,pane read --source visible --lines N --format ansi, with no fallback.On a Herdr build that rejects
--source visiblethe CLI exits non-zero,fm_backend_herdr_composer_readdrops to the plain capture withstyled=0, an idle Claude row carrying its dim rotating hint degrades tounknown, andfm_backend_herdr_composer_unknown_deliverablerefuses atbin/backends/herdr.sh:2722precisely because the styled read failed.Every escalation would then defer for the whole away run, which is the overnight wedge this change exists to remove.
This is latent, not live.
The machine this was built and verified on runs Herdr 0.8.0, where
--source visibleis supported.The exposure is the older versions
docs/herdr-backend.md:4still lists as supported: 0.7.1, 0.7.3, 0.7.4 and 0.7.5.What it would need: keep
--source visibleas the primary styled read, and add a styled fallback of--source recent --lines 200 --format ansiwithout the localtail -n.The original clipping cause was that local tail, not
--source recent, so an untailed recent read is a correct styled capture.The fallback would still have to count as a successful styled capture so the unknown-deliverable path can proceed on it, while a genuinely failed styled read keeps refusing.
This would not widen the composer-unknown override: decision (A), limiting it to a successful styled capture and excluding bare-shell rows, stays as settled.
dead-shell-fixture-never-sets-shell-rowtests/fm-backend-herdr.test.sh:3246-test_unknown_deliverable_refuses_dead_shell_reporting_donefeeds the screenpaiva@box firstmate %.fm_composer_leading_shell_glyph_var(bin/fm-composer-lib.sh:457-469) strips leading whitespace and then requires the row to start with>,$,%or#.This row starts with
p, soFM_COMPOSER_SCAN_SHELL_ROWstays-1and_fm_composer_select_cursorlessrefuses through the empty-kind path instead.The test passes for the same reason as
test_unknown_deliverable_refuses_unidentified_rowand never executes the shell-row branch atbin/fm-composer-lib.sh:1091that it claims to pin.No behaviour changes either way; the fixture pins nothing.
What it would need: a fixture row that actually begins with a shell glyph, so the shell-row branch is the reason the test refuses, kept distinct from the unidentified-row case.
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-supervise-daemon.sh:1247- bin/fm-supervise-daemon.sh:1247 - the herdr native-idle override accepts everyunknownverdict. When fm_backend_herdr_composer_state falls back to the plain capture (styled=0), _fm_composer_classify_bare_wrap returnsunknownfor REAL typed text instead ofpending(bin/fm-composer-lib.sh:989), specifically so callers keep deferring. The change added a new way to reach that fallback:--source visibleon an older herdr makes the ANSI read fail. Sequence: captain returns and types an unsent line, ANSI capture fails, verdictunknown, busy_stateidle, line 1249 delivers and merges the digest into the captain's line. The intent's rule "pending still defers" is defeated because degraded-capture pending is spelledunknown. Gate the override on a styled capture, or have the adapter distinguish a degradedunknownfrom a shapeunknown.bin/fm-supervise-daemon.sh:1242- bin/fm-supervise-daemon.sh:1242 - the code, docs and skill all claim "a dead shell has no idle agent registration and still defers", but bin/backends/herdr.sh:2974 mapsdonetoidle, and docs/verification/runtime-backends.md:241 recordsdoneas a real herdr status. A pane whose harness exited while herdr still reportsdoneyields composer=unknown(bare shell glyph) plus busy_state=idle, so line 1249 types the digest into a login shell - exactly the hazard the strict blank-row captain decision prevents (bin/fm-composer-lib.sh:44-46). No verification evidence in the repo coversagent geton a dead pane, and the updated test at tests/fm-daemon.test.sh:1971 stubs fm_backend_busy_state tounknown, assuming the premise rather than proving it. Excludingdonefrom the override, or refusing it when the classifier selected a shell row, would close it..agents/skills/afk/SKILL.md:187- .agents/skills/afk/SKILL.md:187 still says "A blank or otherwise unidentified input row carries no positive container proof and defers injection, so a modal dialog or a mid-redraw pane is never an injection target." That is now false on herdr: those shapes classifyunknown, and with native agent-stateidlethe daemon injects. The intent requires the /afk skill to match and says "Do not leave a doc describing the old behaviour", and separately requires that away mode never expand approval authority - a digest plus Enter landing on a modal dialog selects its highlighted option. Either narrow the override so the sentence stays true, or rewrite the sentence to state the herdr exception.bin/fm-composer-lib.sh:1081- bin/fm-composer-lib.sh:1081 - the comment says "Claude's idle composer is a bare agent glyph immediately under its closing ─", but line 1085 requires the separator to be one row BELOW the glyph (and the branch is only reachable for a separator below the candidate, line 1079). The test fixture confirms the real order is glyph then rule. The comment inverts the geometry of a safety gate.bin/backends/herdr.sh:2613- bin/backends/herdr.sh:2613 - after dropping thetail, thelineslocal is set and validated but never used; the two-step lines/fetch dance is now just "clamp the argument up to 200" and can collapse to one line. Related: bin/fm-supervise-daemon.sh:1252 and :1256 are the same log line in two branches; the nesting flattens toif unknown && herdr && native=idle then deliver; else log; return 1.🔧 Fix: narrow herdr unknown-composer inject override to proven idle composers
4 issues (3 warnings, 1 info) still open:
bin/backends/herdr.sh:2689- bin/backends/herdr.sh:2689 states condition 4 as "Native agent-state is idle: positive proof a registered agent is waiting between turns rather than mid-turn", and bin/backends/herdr.sh:2709 implements it asfm_backend_herdr_busy_state == idle. The repo's own recorded evidence contradicts that premise: docs/verification/runtime-backends.md:241 records that "live Claude Code 2.1.236 on Herdr 0.8.0 keptagent_status=idlefor an entire landed turn, including a multi-second tool call", which is why the same file concludes "nativeidlecannot close a turn". bin/backends/herdr.sh:3047 also foldsblockedintoidle, so a pane parked on a permission dialog reads idle too. Concrete sequence: the captain's Claude pane is mid-generation,agent getreturns idle, the composer row is fully de-emphasised so the shared classifier answersunknownwith a proven bare-glyph container, and the override at bin/fm-supervise-daemon.sh:1252 fires. The only thing that actually stops delivery is the earlier rendered busy-footer guard inpane_is_busy(bin/fm-supervise-daemon.sh), which matches only the detected primary harness's signature and can be overridden by FM_BUSY_REGEX. That is a single rendered-text guard, not the "positive proof" the comment claims, and both bin/backends/herdr.sh:2689 and docs/herdr-backend.md:252 present native idle as the proof. Either state the real dependency (native idle plus the rendered busy guard) or add the rendered busy check inside the override so it does not rely on a caller ordering that a different consumer of fm_backend_composer_unknown_deliverable would not repeat.bin/backends/herdr.sh:2867- An override-driven delivery is unconfirmable by construction on the exact pane this change targets, so the digest can be retyped every cycle. Trace: inject_msg (bin/fm-supervise-daemon.sh:1250-1256) delivers because the composer readsunknown, then calls fm_backend_send_text_submit. In fm_backend_herdr_send_text_submit the pre-Enter baseline is idle, so after Enter it calls fm_backend_herdr_wait_for_working; per docs/verification/runtime-backends.md:241 Claude on herdr never flips agent_status toworking, so that returnsidleand control falls to bin/backends/herdr.sh:2867,verdict=$(fm_backend_herdr_composer_state "$target"). If that read still answersunknown- the same de-emphasised composer shape that triggered the override in the first place - the*)arm at bin/backends/herdr.sh:2871 returnsunknown, inject_msg logs "submit unconfirmed" and returns 1, and escalate_flush preserves the buffer even though the text was typed and Enter was sent. On the next housekeeping tick the composer readsunknownagain, the override fires again, and the same digest is typed into the captain's Claude a second time. Over a night that is repeated duplicate escalations, each starting a real turn. The override adds a delivery path whose success the herdr submit ack cannot report; it needs a confirmation signal that does not route back through the sameunknownverdict (for example accepting the rendered busy footer as post-submit proof on this path, as the non-idle-baseline branch at bin/backends/herdr.sh:2875-2880 already does).docs/verification/runtime-backends.md:240- The intent requires "any script header or doc whose contract changed must match. Do not leave a doc describing the old behaviour." docs/verification/runtime-backends.md:240 still records the verified capture contract asherdr pane read <pane> --source recent --lines Nwith "a 200-line request plus local trim was stable". bin/backends/herdr.sh:2612-2617 replaced exactly that for composer classification: the source is now--source visibleand the localtailis gone. No row records what--source visiblewas observed to return, which matters because the change rests on an unverified assumption. Specifically, ifvisiblefollows a scrolled viewport, a pane the captain left scrolled up overnight shows scrollback with no composer, every read answersunknownwith no container, the override refuses, and away mode wedges for the whole night again - fail-safe, but the same observed symptom this change exists to remove. Add the--source visiblerow (including the scrolled-pane behaviour) or correct line 240 so the table no longer describes the retired composer capture.bin/fm-composer-lib.sh:1344- bin/fm-composer-lib.sh:1332-1344 documents fm_composer_screen_has_agent_container as proving "a GENUINE agent composer shape" and says 1 covers "a modal or mid-redraw pane"; bin/fm-supervise-daemon.sh:1215 and .agents/skills/afk/SKILL.md:189 repeat that a modal "has no container". That is not what the code does: the function returns 0 for anyboxkind, and a Claude permission dialog is a bordered box. A real modal is actually refused one step earlier, because its bright dialog text makes fm_composer_classify_screen answerpending, notunknown. The residual gap is narrow but real: for a box, _fm_composer_classify_rows only answersunknownwhen geometry is ambiguous and every inner row ghost-strips to nothing, and herdr mapsagent_status=blocked(an agent parked on a dialog) to nativeidle(bin/backends/herdr.sh:3047), so such a screen would pass all four override conditions and the digest plus Enter would answer whatever the dialog has highlighted - the "away mode must never expand approval authority" line in the intent. Worth either tightening the function to the shapes it actually claims, or correcting the three comments so the modal refusal is attributed to the verdict rather than to a container check that does not perform it.🔧 Fix: record herdr visible-source composer capture in verification doc
2 issues (1 warning, 1 info) still open:
bin/backends/herdr.sh:2708- bin/backends/herdr.sh:2708 -fm_composer_screen_has_agent_containeraccepts thepiselection as container proof, but_fm_composer_pi_verdict(bin/fm-composer-lib.sh:1491) returnsunknownand RETURNS EARLY, before_fm_composer_classify_pi_rowsever inspects the region's text, whenever the native identity is not exactlypiorFM_COMPOSER_SCAN_PI_PAIR_VALIDis 0 (pair taller than FM_COMPOSER_PI_MAX_LINES, default 8). That reproduces the exact leak round 1 was asked to close - real typed text spelledunknowninstead ofpending- on a path the fix did not cover. Concrete reachable sequence: the captain's supervisor pane runs pi on herdr and holds a 9-row unsent draft inside the separator pair. fm_backend_herdr_composer_state -> classify_screen selects kind=pi (bin/fm-composer-lib.sh:1075-1081), verdictneed-identity, the adapter refetches identity,agent=pi but PI_PAIR_VALID=0, so line 1491 printsunknownwithout reading a single content row. inject_msg (bin/fm-supervise-daemon.sh:1249) seesunknown, calls the override; the fresh styled capture repeats the sameunknown,fm_composer_screen_has_agent_containerreturns 0 because SELECTED_KIND=pi,agent_status=idlemaps to native idle, and the digest plus Enter is typed into the captain's draft. The same path fires for any non-pi harness whose bottom-most shape is a bare─ ... ─pair (identity != pi at line 1491) with unsent text in it. Fix at the shared boundary: either excludepifromfm_composer_screen_has_agent_containerunless PI_PAIR_VALID=1 and the identity gate passed, or have the override additionally require the selected region's content to strip empty, sounknowncan only mean 'proven container, unjudgeable styling' and never 'proven container, unread text'.bin/backends/herdr.sh:2694- bin/backends/herdr.sh:2694-2707 copies the capture + caps + classify + need-identity-refetch block of fm_backend_herdr_composer_state (bin/backends/herdr.sh:2655-2670) verbatim, differing only in forcing the styled branch. The two must stay in lockstep for the override's condition 2 to mean what its header claims, and today they only do by hand. Factoring the shared block into one helper that returns the capture plus the resolved verdict (the override then passes a styled-only flag) removes the copy and lets the override reuse the capture it already holds instead offm_composer_screen_has_agent_containerre-stripping and re-scanning the same screen a third time.🔧 Fix: gate Pi container proof on identity and dedupe herdr composer read
2 warnings still open:
bin/backends/herdr.sh:2616- bin/backends/herdr.sh:2616 - fm_backend_herdr_capture_ansi is now the ONLY styled read, and it issues exactly one command:pane read --source visible --lines N --format ansi. There is no fallback to the--source recent --format ansiform that worked before this change. docs/verification/runtime-backends.md:241 and :253 verify--source visibleon Herdr 0.8.0 only, while docs/herdr-backend.md:51 and :118 still treat 0.7.5 as a supported floor. Concrete sequence on a herdr that rejects--source visible: the CLI exits non-zero, fm_backend_herdr_composer_read (bin/backends/herdr.sh:2668-2674) falls to the plain capture with styled=0, an idle Claude row carrying its dim rotating hint runs _fm_composer_classify_bare_row (bin/fm-composer-lib.sh:931-943), which degrades pending tounknown; inject_msg then calls fm_backend_herdr_composer_unknown_deliverable, which returns 1 at bin/backends/herdr.sh:2722 precisely because the styled read failed. Every escalation defers for the whole away run - the exact overnight wedge this change exists to remove, now reachable on any herdr withoutvisible. Note the original clipping cause was the localtail -n "$lines", not--source recent, so--source recent --lines 200 --format ansiWITHOUT the tail is a correct styled fallback that keepsvisibleas the primary source the intent requires.tests/fm-backend-herdr.test.sh:3246- tests/fm-backend-herdr.test.sh:3246 - test_unknown_deliverable_refuses_dead_shell_reporting_done feeds the screenpaiva@box firstmate %. fm_composer_leading_shell_glyph_var (bin/fm-composer-lib.sh:457-469) only strips LEADING whitespace and then requires the row to START with>/$/%/#; this row starts withp, so FM_COMPOSER_SCAN_SHELL_ROW stays -1 and FM_COMPOSER_SCAN_BARE_ROW stays -1. _fm_composer_select_cursorless therefore falls through every branch and returns 1 for the empty-SELECTED_KIND reason, i.e. the test passes through the same 'no container at all' path already covered by test_unknown_deliverable_refuses_unidentified_row. The shell-row branch it claims to pin (bin/fm-composer-lib.sh:1091) is never executed, so a regression that dropped that branch would leave this test green. The repo's canonical dead-shell fixtures are glyph-leading (tests/fm-composer-lib.test.sh:384 uses>, :488 uses$). Changing the fixture to a glyph-leading prompt row (e.g.%or$) while keeping agent_status=done exercises the real branch and still passes against current code.✅ **Test** - passed
✅ No issues found.
bash tests/fm-composer-lib.test.sh- includes the newtest_matrix_claude_clipped_closing_rule_is_emptybash tests/fm-backend-herdr.test.sh- includestest_composer_state_claude_clipped_closing_rule_is_empty,test_composer_capture_uses_visible_source, and the fivetest_unknown_deliverable_*contract casesbash tests/fm-backend.test.sh- includestest_composer_unknown_deliverable_default_is_refusalbash tests/fm-daemon.test.sh- includestest_inject_msg_herdr_unknown_native_idle_deliversandtest_max_defer_herdr_unknown_native_idle_flushesbash tests/fm-afk-return.test.sh- return catch-up gate still drains buffered escalations and wedge evidenceRed-before proof: extracted base commit 672c6d0 to a scratch tree, overlaid only the NEW test files onto the OLD bin/ sources, and reran all four suites -not ok - clipped claude idle on herdr: expected empty, got 'unknown',not ok - clipped idle Claude ... must read empty, got 'unknown',not ok - inject_msg should deliver when herdr native state is idle even if the composer reads unknownManual end-to-end away run (/home/paiva/.no-mistakes/evidence/01M0KRMA7WZXW050Z9EGDCFMYH/afk-wedge-e2e.sh): realbin/fm-supervise-daemon.shprocess,state/.afkset, two escalations buffered, scriptedherdrCLI modelling the captain's pane; scenariosidle-claude,dead-shell,human-typing,wedge-then-return, run against both base and target commitsbin/fm-afk-return.shexecuted inside thewedge-then-returnscenario to confirm the captain-visible return catch-up still delivers a digest that wedged overnighttimeout 600 bash tests/fm-afk-inject-herdr-e2e.test.sh- real-herdr e2e; skips in this environment (no herdr server can start headless)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.