fix: classify wrapped idle Claude panes for away-mode delivery - #109
Merged
Merged
Conversation
… in `bin/fm-composer-lib.sh`. This prevents Bash 5 nounset failures when no wrapped Claude status footer is present. Only that file changed. Verified with `tests/fm-composer-lib.test.sh`, `tests/fm-daemon.test.sh`, `bash -n`, and `git diff --check`
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 the away-mode defect where a Claude Code primary on Herdr, hosting the daemon as its native background bash job, has its genuinely idle pane classified unreadable and therefore receives no escalation digest. Reproduce the failure in the named isolated Herdr lab with a native background sleep loop and capture the actual wrapped Claude background-task footer. Fix shared shape handling only in bin/fm-composer-lib.sh, using bin/backends/herdr.sh only if capture or capability facts require it, with no adapter-local shape copies. A genuinely idle Claude composer with a background job must classify empty and rendered-idle, while pending composer text, an active mid-turn spinner, a dead shell, and unreadable or garbled captures must still defer. Commit captured idle and busy ANSI fixtures and behavioral regressions. Verify end to end that one escalation digest is delivered exactly once into the idle pane within one housekeeping tick while the background job remains running. Document current behavior in docs/herdr-backend.md Away-mode supervisor support and record dated evidence in docs/verification/supervision.md. Do not change the delivered-once journal contract or wedge alarm. Keep this to one fix. The broad changed-suite Muse harness detection failure is pre-existing on unchanged main cc6793f and filed separately as fm-muse-harness-detect-test-f1; do not modify tests/fm-muse-harness.test.sh or bin/fm-harness.sh for this PR, and treat that exact failure as pre-existing if the test gate asks.
What Changed
Risk Assessment
🚨 High: The wrapped-footer path can still allow injection during an active Claude turn or wedge a genuinely idle pane.
Testing
Focused composer, daemon, and documentation tests passed. The isolated real Herdr+Claude run launched Claude 2.1.259, executed
/afk, and captured the actual wrapped footer with composer=empty and rendered-idle classification, but Herdr reported agent_status=idle at the unchanged readiness gate, so the one-tick exactly-once delivery proof was not reached. Reviewer-visible terminal and classifier logs were preserved.Evidence: Live Herdr+Claude diagnostic transcript
Source: Live Herdr+Claude diagnostic transcript
Evidence: Wrapped Claude classifier behavior matrix
Source: Wrapped Claude classifier behavior matrix
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-composer-lib.sh:319- Required criterion: “unreadable or garbled captures must still defer.” The new primary regex validates only the prefix and accepts arbitrary suffixes; a capture ending with⏵⏵ bypass permissions on GARBLEDfollowed by/rcis stripped as a valid footer, then returns idle with an empty composer, allowing injection. Require a fully verified primary shape or an unreadable result; the added garbled regression does not cover this case.tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh:427- The one-tick timer starts only after the queue wait of up to 60 seconds, and stops when the token is merely visible, beforewait_for_single_deliveryconfirms submission. A delayed queue or Enter can therefore still reportdelivery-seconds=0/1; time the status creation through confirmed delivery instead.tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh:437- The live test checks nativeworkingonly before publishing the escalation. Its final delivery predicate checks token/ack/composer/buffer but not that the background job is still running, so the test can pass after the native sleep exits and cannot prove the required delivery occurred while it remained active.docs/verification/supervision.md:248- The documented live command hardcodes~/Projects/firstmate/bin/fm-herdr-lab.sh, so rerunning the evidence can use a different checkout than the reviewed source. Use the repository-root helper ($ROOT/bin/fm-herdr-lab.shorgit rev-parse --show-toplevel) instead.🔧 Fix: Hardened Claude footer parsing and delivery assertions
5 issues (2 errors, 3 warnings) still open:
bin/fm-composer-lib.sh:420- Required criterion: “pending composer text, an active mid-turn spinner, a dead shell, and unreadable or garbled captures must still defer.” With a current spinner, empty bare composer, valid primary footer, and/rc, the new split yieldsscreen_verdict=emptywhileactive_hint=0andactive_tool=0; the spinner is skipped and the function returns idle, allowing injection into a foreground turn. Recognize current spinner activity in this wrapped context and defer.docs/documentation-audiences.json:432- The newly trackedgarbled-suffix-background-narrow.ansi.txtis absent fromdocumentation-audiences.json. The audience checker classifies all tracked*.txtfiles and rejects any unclassified path, so this change deterministically fails that gate. Add the fixture asmaintainer-verification.tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh:262- The required end-to-end proof says delivery must occur “while the background job remains running,” but line 262 checks only aggregate Herdragent_status=working. If the native daemon exits just after submitting while Claude processes the acknowledgement, all predicates can still pass. Add an independent witness for that specific native job's liveness at confirmation.tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh:415- The one-housekeeping-tick requirement is not enforced:date +%struncates both endpoints andDELIVERY_ELAPSED <= 2allows delivery spanning two one-second cycles, potentially nearly three seconds, to pass. Use a monotonic subsecond bound matching the required one-second tick.tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh:275- The live footer witness independently searches pane history for a shell-status line and any standalone/rc. An earlier transcript/rcplus a current unwrapped status line can satisfy both checks, so the test can pass without reproducing an adjacent wrapped footer. Match the primary row and continuation as one bounded structure.🔧 Fix: Harden wrapped Claude spinner and footer witnesses
3 issues (2 errors, 1 warning) still open:
bin/fm-composer-lib.sh:439- Required criterion: “pending composer text, an active mid-turn spinner, a dead shell, and unreadable or garbled captures must still defer.” With a valid wrapped footer, empty composer, active✳ Effecting…row, and a non-edge tip row before the top rule, this loop overwrites the spinner with the tip. The new branch then misses busy activity and the later empty-composer path returns idle, allowing injection into the active turn. Preserve or scan every row in the preceding block for a busy match before allowing idle.bin/fm-composer-lib.sh:319- Required criterion: “unreadable or garbled captures must still defer.” The invalid-prefix regex only recognizesonwhen followed by whitespace or end-of-line. A malformed status such as⏵⏵ bypass permissions onGARBLEDbypasses both the exact and invalid matches, falls into the legacy footer fallback, passes the empty-composer checks, and returns idle. Treat status-looking rows that fail the exact primary shape as unreadable before fallback.bin/fm-composer-lib.sh:446- The wrapped-footer branch pipes intofm_busy_lines_match, which assignsFM_BUSY_MATCHED_ROWin a pipeline subshell. The parent therefore sees an empty value and recordsunknownat line 448 instead of the exact matched spinner row, breaking the rendered-busy diagnostic contract. Invoke the matcher with a here-string or otherwise return the matched row without a pipeline subshell.🔧 Fix: Prevent Claude spinner drops and malformed footer fallthrough
1 error still open:
bin/fm-composer-lib.sh:432- Required criterion: “pending composer text, an active mid-turn spinner, a dead shell, and unreadable or garbled captures must still defer,” while a genuinely idle background-job composer must be rendered-idle. The wrapped-footer branch only examines rows containing literal✳; a wrapped footer with an empty composer and an existing supported spinner such as✶or✢therefore falls through as idle and can allow injection. Conversely, any stale✳spinner in the preceding transcript tail is treated as current and wedges an idle pane. Bind the full Claude spinner signature to a structurally current row before merging.tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh:207- The current isolated Herdr+Claude run reached the real wrapped /rc footer and observed composer=empty with rendered_busy=0, but Herdr reported agent_status=idle at the readiness gate on line 207. The test stopped before escalation delivery, so exactly-once one-tick delivery while the native job remains running is unproven. This appears to be a live-runtime/test-witness mismatch rather than a shared classifier failure; decide whether to refresh the live environment or accept the prior dated evidence.bash tests/fm-composer-lib.test.shbash tests/fm-daemon.test.shbash tests/fm-documentation-audiences.test.shFM_AFK_HERDR_CLAUDE_LIVE=1 HERDR_LAB_HELPER="$(git rev-parse --show-toplevel)/bin/fm-herdr-lab.sh" tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.shDirect execution offm_claude_current_footer_busyandfm_composer_classify_screenover the six committed Claude/Herdr fixturesFinal HEAD, clean-worktree, evidence-presence, and lab-cleanup check✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.