Skip to content
Closed
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
82 changes: 81 additions & 1 deletion bin/fm-composer-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@
# different, self-proving thing: real claude 2.x draws exactly
# that (`─` rule, `❯`+NBSP, `─` rule), so the glyph inside the
# pair carries the shape and no identity is needed.
# A live pi whose identity STATUS is stale is the one admitted
# exception to the idle/done requirement (issue #5000): when the
# last non-blank row above the pair's solid opening rule is the
# fixed terminal provider-error banner pi draws once a turn has
# ended (FM_COMPOSER_PI_TERMINAL_ERROR_RE_DEFAULT below), the
# banner plus the solid rule plus the empty interior is the
# settled-turn proof the status failed to deliver. The identity
# must still name a live pi; probe-absent and foreign identities
# stay `unknown`.
#
# THE COMPOSER FOOTER ZONE (task firstmate-doorbell-vals-pending-p1): a
# harness draws its own furniture BELOW the composer - a user statusLine, a
Expand Down Expand Up @@ -471,6 +480,32 @@ FM_COMPOSER_SHELL_PROMPT_GLYPHS=$(printf '%s\n' '>' '$' '%' '#')
# matching is case-insensitive.
FM_COMPOSER_IDLE_RE_DEFAULT='^Type a message\.\.\.$|^Ask anything(\.\.\.|…)|^Plan, search, build anything$|^Add a follow-up$|^Ask Devin to build features, fix bugs, or work on your code$'

# The fixed terminal provider-error banner pi draws directly above its
# composer once a turn has ended on Codex's usage limit: pi's `Error: ` prefix
# followed by the exact message its Codex provider raises for that stream
# error event (verified live, pi 0.85.1 against a stub Codex endpoint; the
# live guard tests/fm-composer-pi-codex-banner-live-e2e.test.sh refreshes
# it). Why it may relax the separated shape's idle/done status requirement
# (issue #5000): herdr learns pi's status only from pi's own lifecycle
# integration, so a status that never followed the failed turn parks at
# `working` or at herdr's `unknown` placeholder for as long as the worker sits
# on the banner, and every lifecycle verb then refuses a composer that is
# provably empty. The banner is structural evidence that the turn ENDED: a
# running pi retitles its opening rule (`── ⠏ Working ──`), which is no
# longer a solid separator and dissolves the pair, and a new prompt pushes
# transcript rows between the banner and the rule. The match is exact and
# case-sensitive, so a similar message from another provider, a worker
# discussing this text, or a wrapped copy of it never qualifies.
# FM_COMPOSER_PI_TERMINAL_ERROR_RE overrides for an unverified rendering.
FM_COMPOSER_PI_TERMINAL_ERROR_RE_DEFAULT='^Error: Codex error: The usage limit has been reached$'
# Pi draws this fixed bug-report hint directly below EVERY error banner
# (verified live on pi 0.87.1), so it sits between the banner and the
# separator pair on current pi releases. It is vendor boilerplate attached to
# the banner itself, not a transcript row proving a new turn, so the scan
# below skips at most one occurrence of it before testing for the banner.
# FM_COMPOSER_PI_ERROR_HINT_RE overrides for an unverified rendering.
FM_COMPOSER_PI_ERROR_HINT_RE_DEFAULT='^If this looks like a pi bug, /bug sends a report to the developers\.$'

# 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
# text, and only the run's LAST row is ever matched against it.
Expand Down Expand Up @@ -1804,6 +1839,39 @@ _fm_composer_classify_bare_pi_overlap() { # <screen> <styled> <has-identity> <i
fi
}

# _fm_composer_pi_terminal_banner_above: 0 when the last non-blank row above
# the scanned pair's opening separator is pi's terminal provider-error banner
# (FM_COMPOSER_PI_TERMINAL_ERROR_RE_DEFAULT), tolerating at most one occurrence
# of pi's fixed bug-report hint row (FM_COMPOSER_PI_ERROR_HINT_RE_DEFAULT)
# directly beneath it. Rows are read plain, so the red styling pi gives the
# banner is neither required nor allowed to hide it, and each row is matched
# whole after trimming: any other transcript row, a menu, or a retitled rule
# between the banner and the pair means the turn did not end on this banner,
# and the caller keeps refusing.
_fm_composer_pi_terminal_banner_above() { # <screen>
local screen=$1 row raw trimmed hint_seen=0
row=$((FM_COMPOSER_SCAN_PI_OPEN - 1))
while [ "$row" -ge 0 ]; do
raw=$(_fm_composer_screen_row "$row" "$screen")
trimmed=$(_fm_composer_row_content "$raw" 0)
if [ -n "$trimmed" ]; then
if fm_composer_idle_matches "$trimmed" \
"${FM_COMPOSER_PI_TERMINAL_ERROR_RE:-$FM_COMPOSER_PI_TERMINAL_ERROR_RE_DEFAULT}" sensitive; then
return 0
fi
if [ "$hint_seen" = 0 ] && fm_composer_idle_matches "$trimmed" \
"${FM_COMPOSER_PI_ERROR_HINT_RE:-$FM_COMPOSER_PI_ERROR_HINT_RE_DEFAULT}" sensitive; then
hint_seen=1
row=$((row - 1))
continue
fi
return 1
fi
row=$((row - 1))
done
return 1
}

# The pi separated-shape verdict: identity + structure conjunction (herdr's
# rule, now fleet-wide). A missing identity capability keeps the shape
# unknown; an unfetched identity on an identity-capable backend asks the
Expand All @@ -1813,6 +1881,12 @@ _fm_composer_classify_bare_pi_overlap() { # <screen> <styled> <has-identity> <i
# is drawn above the separator pair, so the composer region looks free while the
# keys would answer the prompt instead of composing (issue #2797). Structure
# cannot disprove that, so a blocked pi defers rather than claiming empty.
# The one status the structure CAN disprove is a stale one: a live pi whose
# last non-blank row above the pair is its terminal provider-error banner
# (_fm_composer_pi_terminal_banner_above) has ended its turn on that banner
# whatever its integration last reported, so that shape reads empty on every
# registered status (issue #5000). A blocked pi's menu, a running pi's
# retitled rule, and a fresh prompt all displace the banner from that row.
_fm_composer_pi_verdict() { # <screen> <styled> <has_identity> <identity>
local screen=$1 styled=$2 has_identity=$3 identity=$4 agent agent_status state
if [ "$has_identity" != 1 ]; then
Expand Down Expand Up @@ -1840,6 +1914,12 @@ _fm_composer_pi_verdict() { # <screen> <styled> <has_identity> <identity>
fi
case "$agent_status" in
idle|done) printf 'empty' ;;
*) printf 'unknown' ;;
*)
if _fm_composer_pi_terminal_banner_above "$screen"; then
printf 'empty'
else
printf 'unknown'
fi
;;
esac
}
2 changes: 2 additions & 0 deletions bin/fm-test-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ family_for_basename() {
fm-cmux-claude-composer-live-e2e.test.sh|\
fm-composer-matrix-live-e2e.test.sh|\
fm-composer-codex-idle-live-e2e.test.sh|\
fm-composer-pi-codex-banner-live-e2e.test.sh|\
fm-codex-continuity-live-e2e.test.sh|fm-codex-hook-layer-live-e2e.test.sh|\
fm-grok-continuity-live-e2e.test.sh|\
fm-cursor-primary-live-e2e.test.sh|\
Expand Down Expand Up @@ -719,6 +720,7 @@ tests/fm-codex-continuity-live-e2e.test.sh 71
tests/fm-codex-hook-layer-live-e2e.test.sh 47
tests/fm-composer-codex-idle-live-e2e.test.sh 229
tests/fm-composer-matrix-live-e2e.test.sh 47
tests/fm-composer-pi-codex-banner-live-e2e.test.sh 60
tests/fm-contributions.test.sh 35676
tests/fm-control-relaunch.test.sh 137013
tests/fm-control.test.sh 39524
Expand Down
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2366,6 +2366,8 @@ FM_COMPOSER_IDLE_RE= # optional fleet-wide idle-placeholder regex override (b
FM_COMPOSER_CAPTURE_LINES=20 # fleet-wide bound for tail-capture composer reads; it no longer bounds the adapter composer state/content reads on tmux or herdr, which supply their bounded visible pane instead, while the cmux, orca, and Zellij adapters use this small window so stale scrollback banners stay out of the candidate set; it still bounds the shared inbox composer read (bin/fm-task-inbox-lib.sh) on every backend, and on herdr it also floors how many Ctrl+U presses a refused leftover may take
FM_COMPOSER_PI_MAX_LINES=8 # fleet-wide: maximum rows admitted between Pi's identity-corroborated separator pair; taller or ambiguous candidates stay unknown
FM_COMPOSER_GHOST_LUMA_MAX=128 # fleet-wide: max perceived luminance (0.299R+0.587G+0.114B, 0-255) for a TRUECOLOR foreground to count as de-emphasised ghost/placeholder text and be stripped; dim/faint (SGR 2) is stripped regardless. Assumes a dark terminal theme (bin/fm-composer-lib.sh's fm_composer_strip_ghost, used by styled tmux, herdr, and Zellij reads)
FM_COMPOSER_PI_TERMINAL_ERROR_RE= # optional override for Pi's fixed Codex usage-limit banner regex that fm-composer-lib.sh admits as proof a turn ended; defaults to FM_COMPOSER_PI_TERMINAL_ERROR_RE_DEFAULT ('^Error: Codex error: The usage limit has been reached$')
FM_COMPOSER_PI_ERROR_HINT_RE= # optional override for pi's fixed "/bug sends a report" hint-row regex that fm-composer-lib.sh tolerates at most one occurrence of between that banner and Pi's separator pair; defaults to FM_COMPOSER_PI_ERROR_HINT_RE_DEFAULT ('^If this looks like a pi bug, /bug sends a report to the developers\.$')
GROK_HOME= # optional Grok config home for firstmate's global grok turn-end hook; defaults to ~/.grok
FM_SEND_RETRIES=3 # fm-send typed-plane Enter-retry attempts after typing the line once; agy typed targets use a longer per-harness default owned by bin/fm-send.sh
FM_SEND_SLEEP=0.4 # seconds between fm-send typed-plane submit checks
Expand Down
1 change: 1 addition & 0 deletions docs/herdr-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ It hands the visible pane's ANSI viewport plus Herdr's capability facts to the f
A blocked Pi is parked on an interactive prompt, so its blank composer region is a menu's and not a free composer's.
That state defers instead of proving emptiness.
A working Pi, pending middle row, missing identity, incomplete separator pair, or over-tall candidate remains unknown or pending.
A live Pi whose last row above the pair is its fixed Codex usage-limit banner is admitted on every registered status, because Herdr learns Pi's status only from Pi's lifecycle integration and a status that never followed the failed turn would otherwise park every lifecycle verb on a provably empty composer; `bin/fm-composer-lib.sh` owns the banner and its bounds.
Identity stays a lazy second read, consulted only when a separator pair could change the verdict.

### Placeholder and ghost text
Expand Down
27 changes: 27 additions & 0 deletions docs/verification/runtime-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,33 @@ 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-25 Pi Codex usage-limit banner over an empty separator pair

Verified on 2026-09-25 on macOS arm64 (Darwin 25.5.0) against pi 0.85.1 with Herdr's Pi integration file at version 9, driving the installed Pi TUI in an isolated tmux server against a local stub Codex endpoint whose only answer is the `{"type":"error","message":"The usage limit has been reached"}` stream event, so no provider request leaves the machine and no model tokens are spent.
Pi renders that turn end as one red truecolor row, `Error: Codex error: The usage limit has been reached`, then a blank row, its solid opening rule, the empty composer row, its solid closing rule, and the path and model footer; while the turn is running the opening rule is retitled `── ⠏ Working ──`, which is no longer a solid separator.
The unmodified integration reported `working` at the prompt and `idle` once the error rendered, so on this pi and integration the status recovers; the fix covers the status that does not follow the failed turn, which is what issue #5000's two incidents left behind on the pi and integration installed before 2026-09-22.

The live guard is the command that refreshes this entry:

```sh
tests/fm-composer-pi-codex-banner-live-e2e.test.sh
```

Observed output:

```text
# pi (0.85.1): rendered banner row: Error: Codex error: The usage limit has been reached
ok - pi (0.85.1): banner screen classifies empty on the cursorless styled read with a working status
ok - pi (0.85.1): banner screen classifies empty on the cursorless styled read with a unknown status
ok - pi (0.85.1): banner screen classifies empty on the cursorless styled read with a idle status
ok - pi (0.85.1): the banner over the same screen with the identity probe absent stays unknown
ok - pi (0.85.1): banner screen classifies empty on the cursor-anchored tmux read
ok - live pi banner guard verified 5 live surface(s)
```

On the same capture before the fix, the Herdr profile (`styled=1`, `cursor=0`, `identity=1`, `rows=20`) read `unknown` with a `pi<TAB>working` or `pi<TAB>unknown` identity and `empty` only with `pi<TAB>idle`.
`test_matrix_pi_codex_usage_limit_banner_settles_a_stale_status` in `tests/fm-composer-lib.test.sh` pins the shape and every bound the banner does not cross, and `test_pi_parked_on_codex_usage_limit_banner_still_exits` in `tests/fm-control.test.sh` pins that `exit` types `/quit` over it while any other error text keeps the refusal.

### 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