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
95 changes: 92 additions & 3 deletions bin/fm-composer-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
# 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.
# 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.
# 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 All @@ -81,11 +87,21 @@
# otherwise-empty composer with de-emphasized ghost text - claude's rotating
# prompt suggestion, codex's idle suggestion, grok's placeholder, or cursor's
# idle placeholder - which a
# plain capture cannot tell apart from text a human typed.
# plain capture cannot tell apart from text a human typed. codex-cli 0.154.0
# draws its `Ask Codex to do anything` placeholder as SGR-2 dim text after the
# bare `›` glyph, which fm_composer_strip_ghost removes.
# fm_composer_strip_ghost is the ONE ANSI-aware extractor of "real typed
# content": it drops every de-emphasized run - dim/faint (SGR 2) AND a
# dark/muted TRUECOLOR foreground - and keeps only normal-intensity,
# normally-coloured text.
# Ghost stripping is a STYLE test, so it cannot see furniture a harness draws
# at normal intensity: codex-cli 0.154.0 animates a braille "starfield" around
# its idle composer in greys on both sides of the ghost luminance ceiling, so
# the brighter cells survive the strip and used to read as typed input. Those
# cells are recognised by SHAPE instead (fm_composer_strip_braille, declared
# next to the idle placeholders below), and only
# where a bare composer's furniture can sit: behind the glyph row's content
# and on the rows that bound its wrap region.
#
# UNICODE WHITESPACE (issue #1988; open PRs #1995/#2047 target the same
# defect and #1995's naming is adopted here so the implementations converge):
Expand Down Expand Up @@ -426,6 +442,43 @@ FM_COMPOSER_LEFTBAR_FOOTER_RE_DEFAULT='^(Build|Plan)[[:space:]]+·[[:space:]]+'
# a middle dot. It is consulted only as the boundary BELOW a bare composer,
# never on the composer row itself.
FM_COMPOSER_OMP_STATUS_RE_DEFAULT='^[[:space:]]*(π|󰵗)[[:space:]]+·[[:space:]]|^[[:space:]]*'"$FM_OMP_SPINNER_FRAMES_RE"'[[:space:]]+[0-9]+[smh]([[:space:]]|$)|[[:space:]]·[[:space:]].*[0-9]+(\.[0-9]+)?%/[0-9]+K'
# Braille-pattern cells (U+2800..U+28FF) are animation furniture: codex-cli
# 0.154.0 draws an idle "starfield" of them on the row above its `›` prompt
# row, on the `›` row itself after the dim `Ask Codex to do anything`
# placeholder, and on the row below it (verified live through Herdr on
# codex-cli 0.154.0, gpt-6-astra, fast mode). The cells are truecolor greys
# whose luminance straddles FM_COMPOSER_GHOST_LUMA_MAX, so the brighter ones
# survive ghost stripping. The rule, applied by shape rather than style:
# - a row whose non-whitespace content is entirely braille cells is screen
# furniture; it never counts as wrapped typed content and it bounds a bare
# composer's wrap region exactly as the status rows above do;
# - braille cells behind the glyph row's content are stripped before that
# row's emptiness decision when NOTHING else follows the glyph;
# - a row that mixes braille with any other non-whitespace text stays typed
# content, because a human can type a braille character.
# fm_composer_strip_braille is the ONE byte-exact remover: under LC_ALL=C awk
# walks bytes and drops every UTF-8 sequence E2 A0..A3 80..BF. It is
# deliberately not a grep bracket range over the block, for the reason
# FM_OMP_SPINNER_FRAMES_RE records (GNU grep rejects a range between multibyte
# endpoints). Reads stdin, prints the line with its braille cells removed.
fm_composer_strip_braille() {
LC_ALL=C awk '
{
line = $0; out = ""; n = length(line); i = 1
while (i <= n) {
c = substr(line, i, 1)
if (c == "\342" && i + 2 <= n) {
c2 = substr(line, i + 1, 1); c3 = substr(line, i + 2, 1)
if (c2 >= "\240" && c2 <= "\243" && c3 >= "\200" && c3 <= "\277") {
i += 3; continue
}
}
out = out c; i++
}
print out
}
'
}

# The bounded row window adapters should capture for a composer read. One
# shared policy (previously three per-backend variables that had drifted to
Expand Down Expand Up @@ -1001,6 +1054,8 @@ _fm_composer_classify_bare_row() { # <screen> <styled> <row>
raw=$(_fm_composer_screen_row "$row" "$screen")
content=$(_fm_composer_row_content "$raw" "$styled")
plain=$(_fm_composer_row_content "$raw" 0)
_fm_composer_bare_row_strip_furniture_var content
_fm_composer_bare_row_strip_furniture_var plain
state=$(fm_composer_classify_content 0 "$content" \
"${FM_COMPOSER_IDLE_RE:-$FM_COMPOSER_IDLE_RE_DEFAULT}" insensitive "$plain" 0 "$styled")
if [ "$styled" != 1 ] && [ "$state" = pending ]; then
Expand All @@ -1017,6 +1072,35 @@ _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_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
# bare composer's wrap region. A blank row is not furniture (the blank-row
# rules own it), and a row mixing braille with anything else is not either.
_fm_composer_row_is_braille_furniture() { # <row>
local row=$1 rest
fm_composer_normalize_trim_var row
[ -n "$row" ] || return 1
rest=$(printf '%s\n' "$row" | fm_composer_strip_braille)
fm_composer_normalize_trim_var rest
[ -z "$rest" ]
}

# _fm_composer_bare_row_strip_furniture_var: on a bare agent-glyph row, reduce
# the row to its glyph when everything behind the glyph is braille furniture,
# in place through the named variable; a row whose tail carries anything else,
# and a row with no agent glyph, are left untouched. This is the glyph-row half
# of the braille rule: codex 0.154's starfield cells behind its (stripped)
# placeholder must not stand in for typed input.
_fm_composer_bare_row_strip_furniture_var() { # <varname>
local __fmbf_name=$1 __fmbf_text=${!1} __fmbf_glyph='' __fmbf_body
fm_composer_leading_agent_glyph_var __fmbf_glyph "$__fmbf_text" || return 0
__fmbf_body=${__fmbf_text#*"$__fmbf_glyph"}
if _fm_composer_row_is_braille_furniture "$__fmbf_body"; then
printf -v "$__fmbf_name" '%s' "$__fmbf_glyph"
fi
}

# _fm_composer_wrap_region_ok: 0 when every row STRICTLY BELOW <glyph-row>
# through <cursor-row> is non-blank and carries no structural edge - the
# contiguity proof that those rows are the bare composer's wrapped input
Expand All @@ -1031,6 +1115,7 @@ _fm_composer_wrap_region_ok() { # <plain-screen> <glyph-row> <cursor-row>
[ -n "$trimmed" ] || return 1
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_leading_shell_glyph_var glyph "$trimmed"; then return 1; fi
row=$((row + 1))
done
Expand All @@ -1049,8 +1134,11 @@ _fm_composer_classify_bare_wrap() { # <screen> <styled> <glyph-row> <cursor-row
while [ "$row" -le "$cy" ]; do
raw=$(_fm_composer_screen_row "$row" "$screen")
content=$(_fm_composer_row_content "$raw" "$styled")
if [ "$row" -eq "$g" ] && fm_composer_leading_agent_glyph_var glyph "$content"; then
content=${content#*"$glyph"}
if [ "$row" -eq "$g" ]; then
_fm_composer_bare_row_strip_furniture_var content
if fm_composer_leading_agent_glyph_var glyph "$content"; then
content=${content#*"$glyph"}
fi
fi
fm_composer_normalize_trim_var content
[ -z "$content" ] || text_seen=1
Expand Down Expand Up @@ -1168,6 +1256,7 @@ _fm_composer_select_cursorless() {
[ -n "$trimmed" ] || break
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_SELECTED_LAST=$next
next=$((next + 1))
done
Expand Down
1 change: 1 addition & 0 deletions bin/fm-test-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ family_for_basename() {
fm-claude-stop-autoarm-live-e2e.test.sh|\
fm-cmux-claude-composer-live-e2e.test.sh|\
fm-composer-matrix-live-e2e.test.sh|\
fm-composer-codex-idle-live-e2e.test.sh|\
fm-codex-continuity-live-e2e.test.sh|fm-grok-continuity-live-e2e.test.sh|\
fm-cursor-primary-live-e2e.test.sh|\
fm-grok-stop-live-e2e.test.sh|fm-harness-adapter-instructions-live-e2e.test.sh|\
Expand Down
38 changes: 38 additions & 0 deletions docs/verification/runtime-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,43 @@ Cursor is deliberately outside this cursor-anchored empty-composer matrix becaus

`zellij action dump-screen --pane-id <id> --ansi` was verified at zellij 0.44.0 to preserve ANSI styling (real Claude Code rendered inside a zellij pane dumped `ESC[m` `❯` U+00A0 for its idle composer row), which is the capability the zellij composer classifier reads.

### 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`).
Idle, codex 0.154 animates a braille starfield on the row above its bold `›` prompt row, on the `›` row behind the SGR-2 dim `Ask Codex to do anything` placeholder, and on the row below it, then draws a status footer reading `gpt-6-astra high fast · ~/Projects/purser · Launch Purser desk brief`.
The starfield cells are truecolor greys whose luminance runs from roughly 66 to 165, so the cells above the 128 ghost ceiling survive ghost stripping, and the footer is bright, non-blank, and carries no structural edge.

The capture is a read-only `herdr pane read <pane> --format ansi` of the live pane; its 20-row tail is fed to the shared classifier with the descriptor above:

```sh
herdr pane read w4Z:p2 --format ansi > codex-0.154-idle-herdr.ansi
bash -c '. bin/fm-composer-lib.sh
caps=$(printf "styled=1\ncursor=0\nidentity=1\nrows=20")
fm_composer_classify_screen "$caps" "$(tail -n 20 codex-0.154-idle-herdr.ansi)"'
```

Observed output on the same capture before the fix (`bin/fm-composer-lib.sh` at b85e28b5) and then after it:

```text
pending
empty
```

Before the fix the bare `›` shape extended its wrap region over the two rows beneath the glyph (`kind=bare first=17 last=19` within the 20-row tail), read the surviving starfield cells and the footer as wrapped typed input, and answered `pending`.
The steering doorbell (`fm_task_inbox_ring` in `bin/fm-task-inbox-lib.sh`) defers on exactly that verdict, so every ring for the pane was recorded as skipped and the marked request was reported as a missed delivery.
After the fix, braille-only rows bound the wrap region (the status footer sits beneath the starfield row, so the region never reaches it), starfield cells behind the placeholder are stripped from the glyph row, and the same capture reads `empty` under the Herdr and Zellij styled profiles and with a tmux cursor on the glyph row, while a plain (`styled=0`) capture still reads `unknown`, never `pending`.
A second read-only capture of the same pane, taken during the fix with a bright starfield cell drawn between the `›` and the placeholder, read `pending` before and `empty` after as well.
`test_matrix_codex_idle_starfield_furniture` in `tests/fm-composer-lib.test.sh` carries both samples byte-for-byte, the divergence (the same screen with letters in place of the starfield reads `pending`), and the over-stripping negatives (wrapped typed input, braille mixed with text, a typed row with a middle dot, and the footer or a starfield row alone).

The live guard that refreshes this entry launches the installed codex idle in an isolated tmux server and asserts `empty` through both the cursor-anchored tmux read and the cursorless styled read Herdr and Zellij use, naming codex and `codex --version` on failure; it is default-on wherever codex and tmux are installed and spends no tokens:

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

The verification machine runs its fleet on Herdr and has no tmux installed, so on 2026-09-15 that guard reported `skip: live: tmux absent` there, and the Herdr capture above is this entry's live evidence.
The guard also notes whether the starfield and the placeholder were actually drawn during its read, because codex need not animate them under every model or mode; a refresh on a tmux host should record that note beside the verdict rather than assume the starfield was exercised.

## Steering-inbox doorbell

The steering channel's one behavioral assumption - a real worker agent follows the constant self-describing doorbell line (list the inbox, read and act on its records in numeric order, then `mv` each into `handled/`) - was verified on 2026-08-23 against every installed verified harness, on tmux 3.6a, macOS arm64, on an isolated private socket, driving the REAL `bin/fm-send.sh` end to end (durable record plus doorbell, with one mid-wait re-ring playing the watcher's role).
Expand Down Expand Up @@ -1154,6 +1191,7 @@ Real captures verified these active distinctions:
- Dim or faint suggestion text is ghost content, while normally styled text is pending input.
- Grok dark truecolor placeholders are ghost content, while bright truecolor typed input remains pending.
- A bare shell prompt has no safe agent-composer container and is unknown.
- Codex 0.154's idle braille starfield rows are composer furniture, with the dated Herdr evidence and refresh command in [Composer classification matrix](#composer-classification-matrix).

`tests/fm-composer-ghost.test.sh`, `tests/fm-composer-lib.test.sh`, and the Herdr composer cases pin the exact captured ANSI bytes.
The U+2063 operational and routed-request separators were exercised through a real Pi-on-Herdr path; the byte-exact active regression is:
Expand Down
Loading
Loading