Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .agents/skills/stuck-crewmate-recovery/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>.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=<this-firstmate-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=<this-firstmate-home> bin/fm-control.sh <task-id> interrupt`, then redirect with one corrective line through `fm-send`.
4. If the crewmate is genuinely wedged after redirection, relaunch it with `FM_HOME=<this-firstmate-home> bin/fm-control.sh <task-id> relaunch --note '<progress so far>'`, 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.
Expand Down
157 changes: 141 additions & 16 deletions bin/fm-composer-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
#
Expand Down Expand Up @@ -247,11 +266,16 @@ fm_composer_normalize_trim_var() { # <varname>
# 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
Expand Down Expand Up @@ -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 <prompt> 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
Expand Down Expand Up @@ -744,6 +777,37 @@ _fm_composer_pi_separator_row() { # <trimmed-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() { # <trimmed-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() { # <plain-screen> <cursor-or-empty> [extract-wrap]
Expand Down Expand Up @@ -844,6 +908,12 @@ _fm_composer_scan_screen() { # <plain-screen> <cursor-or-empty> [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))
Expand Down Expand Up @@ -1182,6 +1252,22 @@ _fm_composer_row_is_omp_status() { # <trimmed-row>
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() { # <row>
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
Expand Down Expand Up @@ -1226,6 +1312,7 @@ _fm_composer_wrap_region_ok() { # <plain-screen> <glyph-row> <cursor-row>
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1755,6 +1843,35 @@ _fm_composer_classify_pi_rows() { # <screen> <styled>
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() { # <screen> <glyph-row>
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() { # <screen> <styled> <has-identity> <identity> <bare-row>
local screen=$1 styled=$2 has_identity=$3 identity=$4 row=$5 agent
if [ "$has_identity" != 1 ]; then
Expand All @@ -1771,6 +1888,14 @@ _fm_composer_classify_bare_pi_overlap() { # <screen> <styled> <has-identity> <i
fi
agent=${identity%%$'\t'*}
if [ "$agent" = pi ]; then
case "${identity#*$'\t'}" in
idle|done)
if _fm_composer_muse_overlap "$screen" "$row"; then
_fm_composer_classify_bare_row "$screen" "$styled" "$row"
return 0
fi
;;
esac
_fm_composer_pi_verdict "$screen" "$styled" "$has_identity" "$identity"
else
_fm_composer_classify_bare_row "$screen" "$styled" "$row"
Expand Down
29 changes: 29 additions & 0 deletions docs/verification/runtime-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
Loading