Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ tests/fm-pr-merge.test.sh # fm-pr-merge.sh records pr= and avail
tests/fm-crew-state.test.sh # fm-crew-state.sh current-state reconciliation: run-step authority including closed panes and ci log-tail checks-green detection, stale checks-green and needs-decision/blocked superseded by resumed work, genuine-parked, cross-branch runs-list attribution, pane/status-log fallback, scout skip, torn-down/missing-meta graceful
tests/fm-backend.test.sh # runtime-backend abstraction: fm-backend.sh selection/meta/dispatch helpers, shell-portable sourced backend matching, blocked codex-app refusal, and old-vs-new fake-tool command-log conformance for fm-send/fm-peek/fm-spawn/fm-teardown
tests/fm-backend-tmux-smoke.test.sh # real (private-socket) tmux smoke test for the tmux adapter: create/duplicate-refuse, send text + Enter, send literal + key, bounded capture, live-window resolve, kill
tests/fm-backend-herdr.test.sh # fake herdr CLI unit tests for the experimental herdr adapter, including version/tool gates, target parsing, send/capture, structural composer-state verification, slash-submit retry regression coverage, native busy state, per-home workspace-label resolution, default-tab prune safety, restored-layout husk replacement, and verified CLI bug workarounds
tests/fm-backend-herdr.test.sh # fake herdr CLI unit tests for the experimental herdr adapter, including version/tool gates, target parsing, send/capture, structural composer-state verification with bordered and unbordered prompt fixtures, slash-submit retry regression coverage, native busy state, per-home workspace-label resolution, default-tab prune safety, restored-layout husk replacement, and verified CLI bug workarounds
tests/fm-backend-herdr-smoke.test.sh # real herdr adapter smoke test, skipped when herdr or jq is unavailable, using an isolated throwaway HERDR_SESSION and guarded session cleanup, including live-agent duplicate refusal and no-agent husk replacement
tests/fm-backend-autodetect-smoke.test.sh # real herdr auto-detection smoke test, skipped when herdr, jq, or treehouse is unavailable, using the same guarded session cleanup
tests/fm-backend-herdr-workspace-per-home-e2e.test.sh # mandatory isolated E2E for workspace-per-home: primary and secondmate-shaped homes, a crewmate spawned from a secondmate home, teardown, list-live recovery
Expand Down
20 changes: 10 additions & 10 deletions bin/backends/cmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -521,16 +521,16 @@ fm_backend_cmux_capture() { # <target> <lines> [expected-label]
}

# fm_backend_cmux_composer_state: classify the composer's own row as
# empty|pending|unknown. Adapted directly from herdr's structural border-row
# classifier (fm_backend_herdr_composer_state, bin/backends/herdr.sh:598-665)
# per the build task's explicit direction - this is the highest-risk piece of
# a new backend's send-and-verify logic, and cmux's `read-screen` gives the
# same kind of plain-text capture with no cursor-row primitive that herdr's
# `pane read` does, so the same structural approach applies unchanged: locate
# the composer row as the only captured line whose TRIMMED content both
# STARTS and ENDS with the same border glyph (│, ┃, or a plain ASCII |),
# scanning forward and keeping the LAST match so an earlier border-shaped line
# (scrollback, a popup) never outranks the real bottom-anchored composer row.
# empty|pending|unknown. Adapted from the bordered-row branch of herdr's
# structural classifier (fm_backend_herdr_composer_state) per the build task's
# explicit direction - this is the highest-risk piece of a new backend's
# send-and-verify logic, and cmux's `read-screen` gives the same kind of
# plain-text capture with no cursor-row primitive that herdr's `pane read`
# does. The cmux classifier intentionally remains border-row based: locate the
# composer row as the only captured line whose TRIMMED content both STARTS and
# ENDS with the same border glyph (│, ┃, or a plain ASCII |), scanning forward
# and keeping the LAST match so an earlier border-shaped line (scrollback, a
# popup) never outranks the real bottom-anchored composer row.
FM_BACKEND_CMUX_COMPOSER_LINES=${FM_BACKEND_CMUX_COMPOSER_LINES:-20}
FM_BACKEND_CMUX_IDLE_RE=${FM_BACKEND_CMUX_IDLE_RE:-'^Type a message\.\.\.$'}

Expand Down
102 changes: 73 additions & 29 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -595,19 +595,38 @@ fm_backend_herdr_capture() { # <target> <lines>
printf '%s' "$out" | tail -n "$lines"
}

# fm_backend_herdr_composer_state: classify the composer's own row - the
# interior line of its rounded-corner box - as empty|pending|unknown, scanning
# a generous tail-window capture of <target>. herdr's CLI exposes no
# cursor-row primitive (unlike tmux's #{cursor_y}), so this locates the
# composer row structurally: it is the only captured line whose TRIMMED
# content both STARTS and ENDS with the same border glyph (│, ┃, or a plain
# ASCII |). The box's own top/bottom rows use rounded corners (╭─…─╮ / ╰─…─╯),
# which never match; popup item rows and horizontal separator rows carry no
# border glyph at all; the footer help line ("Enter:send │ … │ …", verified
# grok 0.2.82) uses │ only as an INTERIOR separator and does not start with
# one, so it never matches either. Scans forward and keeps the LAST match, so
# a border-shaped line earlier in scrollback/a popup can never outrank the
# real (bottom-anchored) composer row.
# fm_backend_herdr_composer_state: classify the composer's own row as
# empty|pending|unknown, scanning a generous tail-window capture of <target>.
# herdr's CLI exposes no cursor-row primitive (unlike tmux's #{cursor_y}), so
# this locates the composer row structurally, recognizing TWO row shapes and
# keeping whichever match comes LAST (scanning forward), so a shape earlier in
# scrollback/a popup can never outrank the real (bottom-anchored) composer row:
#
# bordered - a boxed composer (verified grok 0.2.82): the row's TRIMMED
# content both STARTS and ENDS with the same border glyph (│, ┃,
# or a plain ASCII |). The box's own top/bottom rows use rounded
# corners (╭─…─╮ / ╰─…─╯), which never match; popup item rows and
# horizontal separator rows carry no border glyph at all; the
# footer help line ("Enter:send │ … │ …") uses │ only as an
# INTERIOR separator and does not start with one, so it never
# matches either.
# bare - an UNBORDERED composer (verified real claude 2.x and codex
# 0.142.x, both under herdr 0.7.1, docs/herdr-backend.md
# "Incident (2026-07-07)"): the row's TRIMMED content starts with
# one of the verified agent-specific prompt glyphs but carries no
# closing border at all - claude's own live input row is a bare
# "❯ …" with no surrounding │, and codex's is a bare "› …". Both
# harnesses ALSO render bordered decorative boxes elsewhere (a
# startup welcome banner, an update-available notice) that
# satisfy the bordered shape above; requiring a match on EITHER
# shape and keeping the last (bottom-most) one is what keeps the
# live composer winning over a stale decorative box still sitting
# in the same capture window - a bordered box is only ever
# followed later on screen by the actual live composer, never the
# reverse, in every harness observed so far. The bare shape is
# deliberately narrower than the bordered content classifier so a
# no-agent shell fallback prompt (`>`, `$`, `%`, or `#`) falls
# through to `unknown` instead of being misread as delivered.
#
# empty - blank, a bare prompt glyph, or known ghost/placeholder text
# ("Type a message...", verified grok 0.2.82's empty-composer
Expand All @@ -618,42 +637,67 @@ fm_backend_herdr_capture() { # <target> <lines>
# composer (e.g. "/compact" -> "/compact compaction
# instructions", verified live against real grok 0.2.82) - that
# first Enter is a SELECTION, not a submission.
# unknown - the pane could not be read, or no composer row was found in the
# captured window.
# unknown - the pane could not be read, or no composer row (of either shape)
# was found in the captured window.
#
# KNOWN REMAINING GAP (docs/herdr-backend.md "Incident (2026-07-07)"): codex's
# idle composer shows dynamic tip/hint text ("Use /skills to list available
# skills") rather than blank or a fixed placeholder string, so it cannot be
# told apart from real pending input by pattern matching alone - a genuinely
# idle codex composer under herdr classifies as "pending", not "empty". This
# makes injection defer forever rather than redeliver, which is a narrower,
# already-safe failure mode (the buffer is preserved, never silently lost, and
# the max-defer wedge alarm still fires) - not fixed here; a real fix needs
# either an upstream herdr cursor-row/style primitive or a codex-specific
# signal, neither available today.
FM_BACKEND_HERDR_COMPOSER_LINES=${FM_BACKEND_HERDR_COMPOSER_LINES:-20}
# Known ghost/placeholder composer text. Extend this if another
# herdr-verified harness needs its own idle placeholder recognized.
FM_BACKEND_HERDR_IDLE_RE=${FM_BACKEND_HERDR_IDLE_RE:-'^Type a message\.\.\.$'}
# Known bare (unbordered) prompt glyphs a composer row may start with: ❯
# (claude) and › (codex) only. Generic shell-style glyphs > $ % # are still
# recognized after a bordered composer row has already been structurally found.
FM_BACKEND_HERDR_BARE_PROMPT_RE=${FM_BACKEND_HERDR_BARE_PROMPT_RE:-'^[❯›]'}

fm_backend_herdr_composer_state() { # <target> -> empty|pending|unknown
local target=$1 cap line trimmed stripped="" found=0
local target=$1 cap line trimmed stripped="" found=0 shape=""
cap=$(fm_backend_herdr_capture "$target" "$FM_BACKEND_HERDR_COMPOSER_LINES") || { printf 'unknown'; return 0; }
while IFS= read -r line; do
trimmed="${line#"${line%%[![:space:]]*}"}"
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
[ -n "$trimmed" ] || continue
case "$trimmed" in
'│'*'│'|'┃'*'┃'|'|'*'|') : ;;
*) continue ;;
'│'*'│'|'┃'*'┃'|'|'*'|')
stripped=$trimmed
shape=bordered
found=1
;;
*)
if printf '%s' "$trimmed" | grep -qE "$FM_BACKEND_HERDR_BARE_PROMPT_RE"; then
stripped=$trimmed
shape=bare
found=1
fi
;;
esac
stripped=$trimmed
found=1
done < <(printf '%s\n' "$cap")
[ "$found" -eq 1 ] || { printf 'unknown'; return 0; }
# Strip the border glyphs, then trim again.
stripped=${stripped//│/}
stripped=${stripped//┃/}
stripped=${stripped//|/}
stripped="${stripped#"${stripped%%[![:space:]]*}"}"
stripped="${stripped%"${stripped##*[![:space:]]}"}"
if [ "$shape" = bordered ]; then
# Strip the border glyphs, then trim again.
stripped=${stripped//│/}
stripped=${stripped//┃/}
stripped=${stripped//|/}
stripped="${stripped#"${stripped%%[![:space:]]*}"}"
stripped="${stripped%"${stripped##*[![:space:]]}"}"
fi
# A bare prompt glyph = empty composer.
case "$stripped" in
'❯'|'>'|'$'|'%'|'#') printf 'empty'; return 0 ;;
'❯'|'›'|'>'|'$'|'%'|'#') printf 'empty'; return 0 ;;
esac
# Strip a leading prompt glyph before judging what remains.
case "$stripped" in
'❯ '*|'> '*|'$ '*|'% '*|'# '*) stripped=${stripped#??} ;;
'❯'*|'>'*|'$'*|'%'*|'#'*) stripped=${stripped#?} ;;
'❯ '*|'› '*|'> '*|'$ '*|'% '*|'# '*) stripped=${stripped#??} ;;
'❯'*|'›'*|'>'*|'$'*|'%'*|'#'*) stripped=${stripped#?} ;;
esac
stripped="${stripped#"${stripped%%[![:space:]]*}"}"
stripped="${stripped%"${stripped##*[![:space:]]}"}"
Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ FM_BACKEND= # optional runtime backend override for new spawns; tmux
HERDR_SESSION=default # herdr-only: named session for normal backend ops; not enough for destructive cleanup (docs/herdr-backend.md)
FM_BACKEND_HERDR_COMPOSER_LINES=20 # herdr-only: tail lines scanned to locate the composer row for submit verification
FM_BACKEND_HERDR_IDLE_RE='^Type a message\.\.\.$' # herdr-only: empty-composer placeholder regex after border/prompt stripping
FM_BACKEND_HERDR_BARE_PROMPT_RE='^[❯›]' # herdr-only: verified agent glyphs recognized as an UNBORDERED (bare) composer row, e.g. claude's ❯ or codex's › (docs/herdr-backend.md "Incident (2026-07-07)")
FM_BACKEND_ORCA_COMPOSER_LINES=200 # orca-only: terminal-read lines scanned to locate the composer row for submit verification
FM_BACKEND_ORCA_IDLE_RE='^Type a message\.\.\.$' # orca-only: empty-composer placeholder regex after border/prompt stripping
FM_ZELLIJ_SESSION=firstmate # zellij-only: named session for normal backend ops and test isolation (docs/zellij-backend.md)
Expand Down
Loading