From b144480c6c2435fa372d767c3bcf014cc2d5ad1e Mon Sep 17 00:00:00 2001 From: slee029 Date: Sun, 20 Sep 2026 09:06:41 +0000 Subject: [PATCH 1/3] fix(composer): adopt upstream Muse 1.3 composer support Adapt composer and regression coverage from upstream PR #4946, head 942089ec142e4e37a4d04447efedfea406eeae84, including implementation commit 10bffd4e by cangele. The upstream PR remains unmerged; no upstream machine-specific verification claims are imported. --- bin/fm-composer-lib.sh | 120 +++++++++++++++++++--- tests/fm-composer-lib.test.sh | 108 +++++++++++++++++++ tests/fm-composer-matrix-live-e2e.test.sh | 18 +++- 3 files changed, 229 insertions(+), 17 deletions(-) diff --git a/bin/fm-composer-lib.sh b/bin/fm-composer-lib.sh index fbc86b17b9d..6bcf86022dc 100644 --- a/bin/fm-composer-lib.sh +++ b/bin/fm-composer-lib.sh @@ -55,15 +55,17 @@ # writes its model name there); a titled bottom border that # still starts and ends with the family's rule glyph is # tolerated, including Grok 1.0.5's three-column title overhang. -# bare - an agent prompt glyph row with no border at all (claude `❯`, -# codex `›`, muse `⟩`, cursor `→`). The agent glyph is itself the container -# proof; a bare SHELL glyph (`>` `$` `%` `#`) never is. +# bare - an agent prompt glyph row with no border at all (claude and +# muse 1.3 `❯`, codex `›`, muse 0.1.0 `⟩`, cursor `→`). The +# agent glyph is itself the container proof; a bare SHELL glyph +# (`>` `$` `%` `#`) never is. # A bare composer's WRAP region (typed input continuing on the # rows beneath the glyph row) is bounded by blank rows, by # structural edges, and by the FURNITURE rows a harness draws -# directly below its composer - omp's status row and -# braille-only animation rows (declared once below, next to -# the idle placeholders) - none of which is ever typed input. +# directly below its composer - omp's status row, +# braille-only animation rows, and a row that is nothing but +# one of the idle placeholder hints (all declared once below, +# next to each other) - none of which is ever typed input. # left-bar - opencode: rows prefixed by a heavy left bar `┃` with no # closing border, holding the idle hint, blank rows, and a # mode/model footer line. @@ -116,12 +118,29 @@ # glyph deliberately outside the agent set, so no opencode shape recorded here # can prove a left-bar envelope and open a zone under it. # +# Muse variant - content rows between two horizontal `─` rules, with no glyph +# of the container's own and no side border. The CLOSING rule +# is always solid; the OPENING rule may instead carry a title +# embedded in its own rule glyphs, which is how muse 1.3 draws +# its composer (`── Voice input (⌥ + v to start) ───…`, verified +# live on Muse Code 1.3.0-R3401.1). A titled rule only OPENS a +# region: it never closes one and never carries the staleness +# evidence a solid rule does, so a titled heading drawn below a +# composer cannot defer that composer. +# pi's region is blank, so it is provable only with a live +# agent identity reporting an idle/done pi (herdr `agent get`; +# the tmux foreground-process probe) - a blank region between +# two transcript rules is otherwise exactly the strict rule's +# unidentifiable blank row. muse's region holds a bare agent +# glyph, and that glyph is its own proof (the bare rules below). +# # THE SAFETY RULE for glyphs: a bare shell prompt glyph (`>` `$` `%` `#`) - # what a pane shows once its agent has exited to a plain login shell - is a # genuine empty agent composer ONLY inside a bordered container. On a bare row # it is a dead-shell prompt and classifies `unknown` (never a safe injection -# target). The AGENT glyphs `❯` (claude), `›` (codex), `⟩` (U+27E9, muse), -# and `→` (U+2192, cursor) are a genuine empty agent composer either way. +# target). The AGENT glyphs `❯` (claude, and muse from 1.3), `›` (codex), +# `⟩` (U+27E9, muse through 0.1.0), and `→` (U+2192, cursor) are a genuine +# empty agent composer either way. # Both glyph sets are declared # exactly once below; every decision reaches them through the declarations. # @@ -247,11 +266,16 @@ fm_composer_normalize_trim_var() { # # no fleet harness uses it for ghost text, so it is kept (real text wins: # under-stripping merely defers, which the max-defer alarm surfaces, while # over-stripping would inject over real input). -# Raising FM_COMPOSER_GHOST_LUMA_MAX is not free: muse draws its `⟩` prompt glyph -# in truecolor 38;2;90;160;255, luminance ~149.9 (verified, muse 0.1.0-R708.1), -# the tightest margin over the 128 default in the fleet. Above ~150 that glyph is -# stripped as ghost text, which is why the bare-glyph fallback below must also -# recognise every agent glyph from the UNSTRIPPED plain row. +# Raising FM_COMPOSER_GHOST_LUMA_MAX is not free: muse 0.1.0-R708.1 drew its `⟩` +# prompt glyph in truecolor 38;2;90;160;255, luminance ~149.9, the tightest +# margin over the 128 default ever measured in the fleet. Muse 1.3.0-R3401.1 +# draws `❯` in 38;2;251;191;36 (luminance ~191.3) instead, so the margin is +# wider on the current release, but the 0.1.0 measurement is what the ceiling +# was chosen against - and 1.3 recolours `❯` back to that exact +# 38;2;90;160;255 blue while its pane is UNFOCUSED, which is the state +# firstmate reads a worker in, so the tight margin is the live one. Above ~150 +# that glyph is stripped as ghost text, which is why the bare-glyph fallback +# below must also recognise every agent glyph from the UNSTRIPPED plain row. # The dim/faint and dark-foreground states are tracked together as "de-emphasis"; # codes are processed left to right within a sequence, so "ESC[0;2m" reads as dim. # LC_ALL=C makes awk walk bytes, so multibyte glyphs (e.g. ❯) and de-emphasised @@ -457,9 +481,18 @@ FM_COMPOSER_SHELL_PROMPT_GLYPHS=$(printf '%s\n' '>' '$' '%' '#') # hence the unanchored tail). cursor-agent renders # two, both anchored: `Plan, search, build anything` in a fresh session and # `Add a follow-up` once a turn has completed (verified live on cursor-agent -# 2026.08.11-e8db854). FM_COMPOSER_IDLE_RE overrides for an unverified harness; -# matching is case-insensitive. -FM_COMPOSER_IDLE_RE_DEFAULT='^Type a message\.\.\.$|^Ask anything(\.\.\.|…)|^Plan, search, build anything$|^Add a follow-up$' +# 2026.08.11-e8db854). Muse rotates hints from its own tip catalogue around an +# empty composer, and the two entries here are the ones seen unrung on a live +# muse mate; they are taken byte-for-byte from the installed Muse 1.3.0-R3401.1 +# binary's catalogue, which is the same source the pane renders from. That +# catalogue holds roughly twenty entries, so a hint outside these two can still +# be drawn - see docs/verification/runtime-backends.md. +# This set has two consumers: the idle-placeholder decisions below, and +# _fm_composer_row_is_idle_hint, which makes a row that is nothing but one of +# these hints bound a bare composer's wrap region instead of reading as typed +# input. FM_COMPOSER_IDLE_RE overrides for an unverified harness; matching is +# case-insensitive. +FM_COMPOSER_IDLE_RE_DEFAULT='^Type a message\.\.\.$|^Ask anything(\.\.\.|…)|^Plan, search, build anything$|^Add a follow-up$|^Type @ to search and insert workspace file paths$|^/loop 10m schedules a recurring prompt$' # Opencode draws a mode/model footer line INSIDE its left-bar composer # ("Build · GPT-5.5 Fast OpenAI · high"). It is composer furniture, not typed @@ -744,6 +777,37 @@ _fm_composer_pi_separator_row() { # return 1 } +# _fm_composer_titled_rule_row: a horizontal `─` rule that carries a TITLE +# embedded in its own rule glyphs - muse 1.3 opens its composer with +# `── Voice input (⌥ + v to start) ───…` (verified live on Muse Code +# 1.3.0-R3401.1 at 44, 60, and 100 columns). The proof is deliberately narrow, +# for the reason _fm_composer_titled_bottom_ok records about grok's titled +# bottom border: the row must OPEN and CLOSE with the family's own rule glyph, +# must still carry a full-width run of it, and must carry no other structural +# glyph, so a box border row or an arbitrary transcript line can never pass. +# The title itself is not parsed, because muse renders the keybind in it and a +# keybind is exactly the part a release may respell. +_fm_composer_titled_rule_row() { # + local row=$1 title + case "$row" in + ──*──) ;; + *) return 1 ;; + esac + case "$row" in + *│*|*┃*|*║*|*╭*|*╮*|*╰*|*╯*|*┌*|*┐*|*└*|*┘*|\ + *┏*|*┓*|*┗*|*┛*|*╔*|*╗*|*╚*|*╝*|*━*|*═*|*▀*|*▄*) return 1 ;; + esac + # The same eight-column run floor the solid rule above uses, so a titled row + # too short to be a composer rule stays ordinary transcript text. + case "$row" in + *────────*) ;; + *) return 1 ;; + esac + title=${row//─/} + fm_composer_normalize_trim_var title + [ -n "$title" ] +} + # Row-scan results are returned through FM_COMPOSER_SCAN_* globals (bash 3.2 # has no nameref); they are internal to this owner. _fm_composer_scan_screen() { # [extract-wrap] @@ -844,6 +908,12 @@ _fm_composer_scan_screen() { # [extract-wrap] pi_lines=0 pi_glyph_row=-1 pi_glyph='' + elif _fm_composer_titled_rule_row "$trimmed"; then + # A titled rule opens a region but never closes one or proves staleness. + pi_open=$row + pi_lines=0 + pi_glyph_row=-1 + pi_glyph='' else if [ "$pi_open" -ge 0 ]; then pi_lines=$((pi_lines + 1)) @@ -1182,6 +1252,22 @@ _fm_composer_row_is_omp_status() { # fm_composer_idle_matches "$1" "${FM_COMPOSER_OMP_STATUS_RE:-$FM_COMPOSER_OMP_STATUS_RE_DEFAULT}" sensitive } +# _fm_composer_row_is_idle_hint: 0 when the WHOLE trimmed row is one of the +# fleet idle placeholder hints (FM_COMPOSER_IDLE_RE_DEFAULT above, whose +# entries are anchored). A harness that rotates hints around its empty +# composer draws them on their own rows below the prompt glyph, where a bare +# composer's wrap region would otherwise swallow them and report an idle pane +# `pending` - the false verdict that skipped three doorbells on a live muse +# mate, the same defect omp's status row above was taught to bound. Those +# hints are drawn at normal intensity, so ghost stripping cannot see them and +# only this shape test can. +_fm_composer_row_is_idle_hint() { # + local row=$1 + fm_composer_normalize_trim_var row + [ -n "$row" ] || return 1 + fm_composer_idle_matches "$row" "${FM_COMPOSER_IDLE_RE:-$FM_COMPOSER_IDLE_RE_DEFAULT}" insensitive +} + # _fm_composer_row_is_braille_furniture: 0 when the row is non-blank and its # non-whitespace content is entirely braille cells (fm_composer_strip_braille # above) - an animation row that never counts as typed content and bounds a @@ -1226,6 +1312,7 @@ _fm_composer_wrap_region_ok() { # if fm_composer_row_has_edge "$trimmed"; then return 1; fi if _fm_composer_row_is_omp_status "$trimmed"; then return 1; fi if _fm_composer_row_is_braille_furniture "$trimmed"; then return 1; fi + if _fm_composer_row_is_idle_hint "$trimmed"; then return 1; fi if fm_composer_leading_shell_glyph_var glyph "$trimmed"; then return 1; fi row=$((row + 1)) done @@ -1473,6 +1560,7 @@ _fm_composer_select_cursorless() { fm_composer_row_has_edge "$trimmed" && break _fm_composer_row_is_omp_status "$trimmed" && break _fm_composer_row_is_braille_furniture "$trimmed" && break + _fm_composer_row_is_idle_hint "$trimmed" && break FM_COMPOSER_SELECTED_LAST=$next next=$((next + 1)) done diff --git a/tests/fm-composer-lib.test.sh b/tests/fm-composer-lib.test.sh index c7b4fc1bc9b..6f7c39db634 100755 --- a/tests/fm-composer-lib.test.sh +++ b/tests/fm-composer-lib.test.sh @@ -356,6 +356,112 @@ test_matrix_muse_truecolor_glyph_survives_signal_loss() { pass "matrix: muse's ⟩ reads empty everywhere and survives losing the styled-glyph signal" } +test_matrix_muse_13_titled_rule_composer() { + # Real idle Muse Code 1.3.0-R3401.1, captured byte-for-byte from a live pane + # at 100 and 44 columns: a TITLED opening rule, a truecolor `❯` + # (38;2;251;191;36, luminance ~191.3) on its own row, a solid closing rule, + # and the model/effort/cwd status row below it. Muse 0.1.0 drew an unbordered + # `⟩` with no rules at all, so 1.3's shape was unreadable: the lone closing + # rule read as a newer composer below the glyph row and every verdict was + # `unknown`, which refused every exit and relaunch of a live muse worker. + # The cwd cell is the one edit to the capture - it held a machine-local + # scratch path. + local rule glyph bottom statusrow idle idle_narrow typed out + rule="${ESC}[2;38;2;103;108;116m── ${ESC}[0m${ESC}[38;2;138;144;152mVoice input (⌥ + v to start)${ESC}[0m${ESC}[2;38;2;103;108;116m ────────────────────────────────────────────────────────────────────${ESC}[0m" + glyph="${ESC}[38;2;251;191;36m❯ ${ESC}[0m" + bottom="${ESC}[2;38;2;103;108;116m────────────────────────────────────────────────────────────────────────────────────────────────────${ESC}[0m" + statusrow="${ESC}[38;2;103;108;116m ${ESC}[0m${ESC}[38;2;90;160;255mmuse-spark-1.3-contributor${ESC}[0m${ESC}[38;2;138;144;152m · ${ESC}[0m${ESC}[38;2;90;160;255mmax${ESC}[0m${ESC}[38;2;138;144;152m · /…/muse-ws · ${ESC}[0m${ESC}[38;2;243;139;168mYOLO${ESC}[0m" + idle="$rule"$'\n'"$glyph"$'\n'"$bottom"$'\n'"$statusrow" + idle_narrow="${ESC}[2;38;2;103;108;116m── ${ESC}[0m${ESC}[38;2;138;144;152mVoice input (⌥ + v to start)${ESC}[0m${ESC}[2;38;2;103;108;116m ────────────${ESC}[0m"$'\n'"$glyph"$'\n'"${ESC}[2;38;2;103;108;116m────────────────────────────────────────────${ESC}[0m"$'\n'"$statusrow" + typed="$rule"$'\n'"${ESC}[38;2;251;191;36m❯ ${ESC}[0m${ESC}[38;2;204;211;219mfix the login bug${ESC}[0m"$'\n'"$bottom"$'\n'"$statusrow" + + assert_screen "muse 1.3 idle on tmux" empty "$CAPS_TMUX" "$idle" 1 "$(printf 'muse\tidle')" + assert_screen "muse 1.3 idle on herdr" empty "$CAPS_STYLED" "$idle" '' "$(printf 'muse\tidle')" + assert_screen "muse 1.3 idle on herdr with no identity probe result" empty \ + "$CAPS_STYLED" "$idle" '' probe-absent + assert_screen "muse 1.3 idle on zellij" empty "$CAPS_STYLED_NOID" "$idle" + assert_screen "muse 1.3 idle on cmux/orca" empty "$CAPS_PLAIN" \ + "$(printf '%s\n' "$idle" | fm_composer_strip_ansi)" + assert_screen "muse 1.3 idle at 44 columns" empty "$CAPS_STYLED_NOID" "$idle_narrow" + + # An UNFOCUSED pane is what firstmate actually reads, and muse recolours its + # glyph to 38;2;90;160;255 (luminance ~149.9) on focus-out - the tightest + # margin over the 128 ghost ceiling in the fleet. Drive that signal away too: + # with the ceiling raised past the glyph's luminance the ghost strip erases + # it, and the verdict must survive on the unstripped plain row alone. + local unfocused + unfocused="${ESC}[38;2;90;160;255m❯ ${ESC}[0m" + assert_screen "muse 1.3 idle in an unfocused pane" empty "$CAPS_STYLED_NOID" \ + "$rule"$'\n'"$unfocused"$'\n'"$bottom"$'\n'"$statusrow" + out=$(FM_COMPOSER_GHOST_LUMA_MAX=200 fm_composer_classify_screen "$CAPS_STYLED_NOID" \ + "$rule"$'\n'"$unfocused"$'\n'"$bottom"$'\n'"$statusrow") + [ "$out" = empty ] \ + || fail "an unfocused muse composer must stay empty when the ghost strip eats its glyph, got '$out'" + + # Typed text in the same geometry must stay pending, and must never read + # empty on a capture that cannot prove it real. + assert_screen "muse 1.3 typed on zellij" pending "$CAPS_STYLED_NOID" "$typed" + assert_screen "muse 1.3 typed on cmux/orca" unknown "$CAPS_PLAIN" \ + "$(printf '%s\n' "$typed" | fm_composer_strip_ansi)" + + # NON-VACUOUSNESS: the titled rule is what carries the verdict. Replace it + # with ordinary transcript text and the identical glyph row must fall back to + # `unknown`, because the closing rule below it is then a composer boundary + # with nothing it can close. + out=$(fm_composer_classify_screen "$CAPS_STYLED_NOID" \ + "transcript line"$'\n'"$glyph"$'\n'"$bottom"$'\n'"$statusrow") + [ "$out" = unknown ] \ + || fail "without its titled opening rule the muse glyph row must stay unknown, got '$out'" + + # A titled rule is an OPENING rule only: one drawn BELOW a bare composer is + # never the staleness evidence a solid rule is, so it cannot defer that + # composer. + assert_screen "a titled rule below a bare composer does not defer it" empty \ + "$CAPS_STYLED_NOID" "$glyph"$'\n'"$rule" + + # A dead shell parked in exactly this geometry must never read empty: the + # shell glyph is not a container proof and the separated shape has no agent + # identity to prove. + out=$(fm_composer_classify_screen "$CAPS_STYLED_NOID" \ + "$rule"$'\n''$'$'\n'"$bottom"$'\n'"$statusrow") + [ "$out" != empty ] \ + || fail "a dead shell inside muse 1.3's composer geometry must never read empty, got '$out'" + pass "matrix: muse 1.3's titled-rule composer reads empty, typed text stays pending, a dead shell never does" +} + +test_matrix_muse_idle_hint_row_is_furniture() { + # Muse rotates hints from its own tip catalogue around an empty composer. + # Drawn at normal intensity, they survive ghost stripping, so a bare + # composer's wrap region used to swallow one and report an idle pane + # `pending` - which is what skipped three doorbells on a live muse mate on + # 2026-09-18, leaving durable steers unrung until the mate's own cycle read + # them. + local rule glyph bottom statusrow hint out + rule="${ESC}[2;38;2;103;108;116m── ${ESC}[0m${ESC}[38;2;138;144;152mVoice input (⌥ + v to start)${ESC}[0m${ESC}[2;38;2;103;108;116m ────────────────────────────────────────────────────────────────────${ESC}[0m" + glyph="${ESC}[38;2;251;191;36m❯ ${ESC}[0m" + bottom="${ESC}[2;38;2;103;108;116m────────────────────────────────────────────────────────────────────────────────────────────────────${ESC}[0m" + statusrow="${ESC}[38;2;103;108;116m ${ESC}[0m${ESC}[38;2;90;160;255mmuse-spark-1.3-contributor${ESC}[0m${ESC}[38;2;138;144;152m · ${ESC}[0m${ESC}[38;2;90;160;255mmax${ESC}[0m${ESC}[38;2;138;144;152m · /…/muse-ws · ${ESC}[0m${ESC}[38;2;243;139;168mYOLO${ESC}[0m" + hint="${ESC}[38;2;138;144;152mType @ to search and insert workspace file paths${ESC}[0m" + + # NON-VACUOUSNESS: the hint really does survive ghost stripping, so the + # verdict below cannot be coming from an emptied row. + out=$(printf '%s\n' "$hint" | fm_composer_strip_ghost) + fm_composer_normalize_trim_var out + [ "$out" = 'Type @ to search and insert workspace file paths' ] \ + || fail "muse's hint must survive ghost stripping for this case to mean anything, got '$out'" + + assert_screen "a hint row below a bare composer is furniture" empty \ + "$CAPS_STYLED_NOID" "$glyph"$'\n'"$hint" + assert_screen "a hint row inside muse 1.3's composer is furniture" empty \ + "$CAPS_STYLED_NOID" "$rule"$'\n'"$glyph"$'\n'"$hint"$'\n'"$bottom"$'\n'"$statusrow" + + # The dangerous direction: a wrapped row that is NOT a hint is typed input + # and must still read pending. + assert_screen "a wrapped row that is not a hint stays pending" pending \ + "$CAPS_STYLED_NOID" "$glyph"$'\n'"and then rename the module" + pass "matrix: a row that is nothing but an idle hint bounds the wrap region; real wrapped input still reads pending" +} + test_matrix_cursor_reverse_video_placeholder_remnant() { # Real idle cursor-agent (2026.08.11-e8db854), captured byte-for-byte from a # live pane: the `→ ` glyph and the placeholder tail are dim (SGR 2), but the @@ -923,6 +1029,8 @@ test_composer_footer_zone_is_shape_independent test_composer_footer_zone_refuses_rather_than_allows test_matrix_codex_dim_hint_row test_matrix_muse_truecolor_glyph_survives_signal_loss +test_matrix_muse_13_titled_rule_composer +test_matrix_muse_idle_hint_row_is_furniture test_matrix_cursor_reverse_video_placeholder_remnant test_matrix_herdr_halfblock_rule_bounds_bare_wrap test_matrix_omp_status_row_bounds_bare_composer diff --git a/tests/fm-composer-matrix-live-e2e.test.sh b/tests/fm-composer-matrix-live-e2e.test.sh index bdd45b5773c..f319e8c0a38 100755 --- a/tests/fm-composer-matrix-live-e2e.test.sh +++ b/tests/fm-composer-matrix-live-e2e.test.sh @@ -160,9 +160,25 @@ check_harness_idle_cursorless() { # } # --- 1. Every installed verified harness must reach a proven-empty composer -- +# Each harness is launched the way bin/fm-spawn.sh launches it, minus the brief. +# muse is the one that needs a flag: it gates every workspace no operator has +# opened by hand behind its own trust dialog, which the strict classifier +# correctly refuses to read as a composer, so a bare `muse` here could only ever +# fail on that dialog and never exercise the composer at all. `--yolo` is the +# flag the real spawn passes for exactly that reason, and this guard submits no +# prompt, so nothing runs in the trusted workspace. +harness_launch() { # -> launch argv on stdout, one word per line + case "$1" in + muse) printf '%s\n' muse --yolo ;; + *) printf '%s\n' "$1" ;; + esac +} + for h in claude codex opencode pi grok kimi muse; do if command -v "$h" >/dev/null 2>&1; then - check_harness_idle_empty "$h" "$h" + launch=() + while IFS= read -r word; do launch+=("$word"); done < <(harness_launch "$h") + check_harness_idle_empty "$h" "${launch[@]}" else note "harness absent, not verified here: $h" fi From fff30a5bb258bc64bad02cac1b7423e111272745 Mon Sep 17 00:00:00 2001 From: slee029 Date: Sun, 20 Sep 2026 09:08:54 +0000 Subject: [PATCH 2/3] fix(composer): disambiguate stale Pi identity on Muse input Require a current single-row agent composer and adjacent structured Muse model/effort footer before preferring glyph classification over an idle or done Pi binding. Keep blocked/working Pi, actual pending input, missing structure, and dead-shell cases conservative. Exercise original escalated inbox recovery without re-enqueue or ladder resets; acknowledgement remains the only delivery proof. --- .../skills/stuck-crewmate-recovery/SKILL.md | 4 ++ bin/fm-composer-lib.sh | 37 +++++++++++++++++ tests/fm-composer-lib.test.sh | 24 +++++++++++ tests/fm-task-inbox.test.sh | 40 +++++++++++++++++++ 4 files changed, 105 insertions(+) diff --git a/.agents/skills/stuck-crewmate-recovery/SKILL.md b/.agents/skills/stuck-crewmate-recovery/SKILL.md index 3d7ac5e1d66..397e8fc1e42 100644 --- a/.agents/skills/stuck-crewmate-recovery/SKILL.md +++ b/.agents/skills/stuck-crewmate-recovery/SKILL.md @@ -71,6 +71,10 @@ Only positive socket refusal or absence is a daemon-down finding; escalate that Escalate in order: 1. Peek the pane, and check the task's steering inbox (`state/.inbox/`) for unhandled `*.msg` records - a stale wake naming an unread firstmate instruction means the worker never acknowledged a durable steer, and the record itself shows exactly what was intended. + If the endpoint is now proven alive and idle with an empty composer, retry the existing inbox doorbell once through `fm_task_inbox_ring` from `bin/fm-task-inbox-lib.sh`, using the recorded backend, endpoint, original oldest record, and expected label. + Preserve the original instruction and escalation marker: another enqueue duplicates the requested action, and resetting the watcher ladder gives an already escalated message a fresh retry budget. + Ringing is not acknowledgement or validation proof; inspect the existing record's move to `handled/` and the authoritative matching validation run before declaring progress or dispatching validation again. + If liveness, identity, or the composer is ambiguous, reconcile it before attempting this recovery; never ring a dead shell. 2. If the crewmate is waiting on a question its brief already answers, answer in one line via `FM_HOME= bin/fm-send.sh` from an active firstmate session unless `FM_HOME` is already set to the active firstmate home. 3. If the crewmate is confused or looping, interrupt with `FM_HOME= bin/fm-control.sh interrupt`, then redirect with one corrective line through `fm-send`. 4. If the crewmate is genuinely wedged after redirection, relaunch it with `FM_HOME= bin/fm-control.sh relaunch --note ''`, which stops the agent, carries the brief plus that note into a replacement in the same local copy, and restores the prior record if the replacement cannot start. diff --git a/bin/fm-composer-lib.sh b/bin/fm-composer-lib.sh index 6bcf86022dc..7769e3704d6 100644 --- a/bin/fm-composer-lib.sh +++ b/bin/fm-composer-lib.sh @@ -1843,6 +1843,35 @@ _fm_composer_classify_pi_rows() { # printf 'empty' } +# A native Pi binding can outlive its process after a Muse replacement. Only +# the current separated region's agent glyph AND adjacent Muse status footer +# can disambiguate that overlap; launch metadata or a model name in transcript +# cannot. This changes delivery classification, never native lifecycle state. +_fm_composer_muse_overlap() { # + local screen=$1 row=$2 plain glyph footer effort + [ "$FM_COMPOSER_SCAN_PI_PAIR_VALID" = 1 ] || return 1 + [ "$row" -eq "$((FM_COMPOSER_SCAN_PI_OPEN + 1))" ] || return 1 + [ "$row" -eq "$((FM_COMPOSER_SCAN_PI_CLOSE - 1))" ] || return 1 + plain=$(printf '%s\n' "$screen" | fm_composer_strip_ansi) + glyph=$(_fm_composer_screen_row "$row" "$plain") + fm_composer_normalize_trim_var glyph + case "$glyph" in ❯*) ;; *) return 1 ;; esac + footer=$(_fm_composer_screen_row "$((FM_COMPOSER_SCAN_PI_CLOSE + 1))" "$plain") + fm_composer_normalize_trim_var footer + # Two independent footer cells, in their rendered order. A single model + # mention is insufficient. Unknown future footer layouts fail closed. + case "$footer" in + muse-*' · '*' · '*) ;; + *) return 1 ;; + esac + effort=${footer#*' · '} + effort=${effort%%' · '*} + case "$effort" in + off|minimal|low|medium|high|xhigh|max) return 0 ;; + *) return 1 ;; + esac +} + _fm_composer_classify_bare_pi_overlap() { # local screen=$1 styled=$2 has_identity=$3 identity=$4 row=$5 agent if [ "$has_identity" != 1 ]; then @@ -1859,6 +1888,14 @@ _fm_composer_classify_bare_pi_overlap() { # (set mtime well past any grace under test) touch -t 202001010000 "$1" } +# The escalation marker transfers ownership to recovery, not another enqueue. +# Exercise the real classifier and existing ring API against the stale binding. +test_recovery_rings_original_escalated_record() ( + . "$ROOT/bin/fm-task-inbox-lib.sh" + . "$ROOT/bin/fm-composer-lib.sh" + local state rec screen log rc + state="$TMP_ROOT/recover-original/state" + mkdir -p "$state" + rec=$(fm_task_inbox_write "$state" t1 'start validation once') || fail "write failed" + age_path "$rec" + fm_task_inbox_record_escalated "$state" t1 "$rec" + [ "$(fm_task_inbox_due_action "$state" t1)" = quiet ] || fail "escalated record should stay quiet" + screen=$'────────────────────────────────────────────\n❯ \n────────────────────────────────────────────\nmuse-spark-1.3 · xhigh · project · YOLO' + log="$state/rings" + fm_backend_agent_state() { printf '%s' "${agent_state:-alive}"; } + fm_backend_composer_state() { + fm_composer_classify_screen $'styled=1\ncursor=0\nidentity=1\nrows=60' "$screen" '' $'pi\tdone' + } + fm_backend_send_text_submit() { printf '%s\n' "$3" >> "$log"; printf empty; } + fm_task_inbox_ring herdr 'session:pane' "$rec" || fail "corrected composer blocked original record" + [ "$(wc -l < "$log" | tr -d ' ')" = 1 ] || fail "recovery must ring once" + [ -f "$rec" ] || fail "ring is not acknowledgement" + [ "$(cat "$state/t1.inbox/.escalated")" = 001.msg ] || fail "ring reset escalation ownership" + [ "$(fm_task_inbox_due_action "$state" t1)" = quiet ] || fail "ring restarted an unbounded watcher ladder" + screen=${screen/❯ /❯ unfinished input} + rc=0; fm_task_inbox_ring herdr 'session:pane' "$rec" || rc=$? + [ "$rc" = 1 ] || fail "actual pending input was not protected" + agent_state=dead + rc=0; fm_task_inbox_ring herdr 'session:pane' "$rec" || rc=$? + [ "$rc" = 3 ] || fail "dead endpoint was not refused" + [ "$(wc -l < "$log" | tr -d ' ')" = 1 ] || fail "protected endpoint received another ring" + mkdir -p "$state/t1.inbox/handled" + mv "$rec" "$state/t1.inbox/handled/" + fm_task_inbox_oldest_unhandled "$state" t1 >/dev/null && fail "acknowledged validation would dispatch again" + [ "$(fm_task_inbox_due_action "$state" t1)" = quiet ] || fail "acknowledgement did not silence recovery" + [ "$(find "$state/t1.inbox" -name '*.msg' | wc -l | tr -d ' ')" = 1 ] || fail "recovery duplicated durable instruction" + pass "inbox: corrected composer re-rings original escalated record once, preserving acknowledgement and budget" +) + test_write_is_durable_and_exact() { local state rec rec2 doorbell doorbell2 doorbell3 expected actual expected2 actual2 text state="$TMP_ROOT/write/state"; mkdir -p "$state" @@ -697,6 +736,7 @@ test_watcher_dead_pane_ignores_stale_busy_state() { pass "watcher: dead-pane recovery overrides stale busy state" } +test_recovery_rings_original_escalated_record || exit 1 test_write_is_durable_and_exact test_doorbell_is_a_shell_noop test_doorbell_rejects_terminal_controls From 2e4eab2504340829e87627e3a0039e84b6c3535e Mon Sep 17 00:00:00 2001 From: slee029 Date: Sun, 20 Sep 2026 09:12:04 +0000 Subject: [PATCH 3/3] docs(verification): record Muse capture proof and live startup limits --- docs/verification/runtime-backends.md | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/verification/runtime-backends.md b/docs/verification/runtime-backends.md index cb152c4483a..180c235efec 100644 --- a/docs/verification/runtime-backends.md +++ b/docs/verification/runtime-backends.md @@ -653,6 +653,35 @@ FM_COMPOSER_MATRIX_LIVE=1 tests/fm-composer-matrix-live-e2e.test.sh On 2026-09-20 that guard could not reach its new arm for either installed harness, and the same failures reproduce on the unmodified library: bare `claude` 2.1.236 opens the session picker rather than a session, and the guard's mid-budget Escape then quits it, while codex-cli 0.147.0 parks on a hooks-trust modal the guard correctly refuses to confirm. The Herdr captures above are therefore this entry's live evidence, and the guard's claude arm owes a separate repair before it can refresh it. +### 2026-09-20 Muse 1.3 composer and stale Pi identity + +On Linux x86_64 with Muse Code 1.3.0-R3401.1, a captured idle Muse composer classified `pending` when the same screen was paired with stale native identity `pi/done`, and `empty` with `muse/idle`. +The portable `test_matrix_muse_stale_pi_identity` regression in `tests/fm-composer-lib.test.sh` retains the captured ANSI composer tail with the transcript omitted and workspace label sanitized. +It now reports `empty` with the stale idle/done Pi identity only when the single glyph row and adjacent structured Muse model/effort footer disambiguate the shape; actual text remains pending, unknown footer layouts preserve protection, and working/blocked Pi is not overridden. +The same suite covers the Muse 1.3 titled opening rule and rotating hint rows from upstream PR #4946; this records local verification, not that PR's merge state. + +Refresh the portable composer and original-record recovery evidence with: + +```sh +bin/fm-test-run.sh tests/fm-composer-lib.test.sh tests/fm-task-inbox.test.sh +bin/fm-test-run.sh tests/fm-composer-ghost.test.sh +``` + +Observed: two targeted suites passed (34.4s and 53.0s), and the ghost suite passed (6.5s). +The inbox regression proves that the existing ring helper can reuse an escalated original record without another enqueue or resetting its retry budget, with pending-input/dead-endpoint refusal and acknowledgement-based retirement. +It does not prove a live worker started validation. + +The current installed-harness guard was also run from an isolated fresh checkout in a restricted filesystem environment, with no model prompts submitted: + +```sh +FM_COMPOSER_MATRIX_LIVE=1 bin/fm-test-run.sh tests/fm-composer-matrix-live-e2e.test.sh +``` + +It exited 1 after 322.1s: Kimi 0.38.0 and the strict blank-shell posture passed; Claude 2.1.278 and Grok 1.0.34 remained at workspace trust prompts; Codex 0.154.0, OpenCode 1.18.30, Pi 0.85.1 and Muse 1.3.0-R3401.1 never exposed a readable composer and their failure capture tails were empty. +Zellij was absent. +No trust prompt was accepted, and these startup failures do not establish a composer regression or a successful current Muse startup. +A full live refresh remains required in a host context where the installed harnesses can start; the captured-screen and portable proofs must not be reported as a fleet-wide live pass. + ### 2026-09-15 codex-cli 0.154.0 idle starfield and status footer through Herdr Verified on 2026-09-15 on macOS arm64 (Darwin 25.5.0) against codex-cli 0.154.0 (model gpt-6-astra, fast mode) running as a Codex second mate inside a Herdr pane, read through Herdr's ANSI capture with its exact capability descriptor (`styled=1`, `cursor=0`, `identity=1`, `rows=20`).