Skip to content

fix(composer): unify safe classification across backends - #2102

Merged
kunchenguid merged 33 commits into
mainfrom
fm/fm-composer-thin-adapter-refactor-r2
Aug 11, 2026
Merged

fix(composer): unify safe classification across backends#2102
kunchenguid merged 33 commits into
mainfrom
fm/fm-composer-thin-adapter-refactor-r2

Conversation

@kunchenguid

@kunchenguid kunchenguid commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Intent

Docs-only follow-up on the validated superseding branch (PR #2102, checks green; supersedes PR #2066 after a custody deadlock): refresh docs/verification/runtime-backends.md 'Composer classification matrix' with the genuine trusted-checkout live guard rerun performed at the branch's FINAL validated head - the already-agreed post-green step replaying the recorded test-live-trust decision. All six installed harnesses verified live (claude 2.1.227 - auto-updated since the audit, so the shipped classifier is additionally proven on the newer release - codex 0.146.0, opencode 1.14.46, pi 0.84.0, grok 1.0.0, muse 0.1.0-R708.1), plus the strict blank-row deferral and the hardened zellij false-positive probe; kimi remains honestly absent with no live result claimed. No code changes; everything else on the branch is already validated. The captain's finalized decisions stand and are not to be re-litigated.

What Changed

  • Centralize composer shape, placeholder, and content classification in fm-composer-lib.sh, with tmux, Herdr, cmux, Orca, and Zellij reduced to capture/capability adapters and Kimi readiness using the shared classifier.
  • Fail closed on ambiguous, blank, stale, and dead-shell screens, and replace Zellij’s content-diff delivery acknowledgement with composer-scoped typing verification and shared submit retries.
  • Expand classifier and backend regression coverage and document the final-head trusted live matrix for all six installed harnesses, with Kimi explicitly recorded as absent.

Risk Assessment

✅ Low: The final follow-up accurately records the requested trusted-checkout rerun, versions, absent Kimi status, blank-row deferral, and Zellij false-positive probe without introducing new source risk.

Testing

Confirmed the final follow-up is docs-only, exercised the referenced portable classifier behavior, corroborated the installed harness/version inventory and Kimi absence, and visually reviewed the rendered matrix. All targeted checks passed; the historical live guard was not replayed because this isolated worktree is not the already-trusted checkout whose final-head run the documentation records.

  • Evidence: Rendered Composer classification matrix (local file: /var/folders/0k/bf8mwt2n5qddzk24r20gfk0c0000gn/T/no-mistakes-evidence/01KZQ18YH4WK55V52FNE3YHCA6/composer-classification-matrix.html.png)
Evidence: Reviewable rendered HTML
<!doctype html>
<html><head><meta charset="utf-8"><title>Composer classification matrix — rendered verification</title>
<style>
:root{color-scheme:light} body{margin:0;background:#f6f8fa;color:#1f2328;font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.page{max-width:980px;margin:36px auto;padding:42px 52px;background:white;border:1px solid #d0d7de;border-radius:10px;box-shadow:0 3px 16px #1f23281a}h1{font-size:2em;border-bottom:1px solid #d8dee4;padding-bottom:.3em}code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#eff1f3;border-radius:4px;padding:.15em .35em}pre{overflow:auto;background:#f6f8fa;border:1px solid #d8dee4;border-radius:6px;padding:16px}pre code{padding:0;background:transparent}a{color:#0969da}.badge{display:inline-block;color:#1a7f37;background:#dafbe1;border:1px solid #82e596;border-radius:999px;padding:4px 10px;font-weight:600;font-size:13px;margin-bottom:12px}h1 span{display:none}
</style></head><body><main class="page"><div class="badge">Rendered from target commit bb089684</div>
<h2 id="label-Composer+classification+matrix">Composer classification matrix<span><a href="#label-Composer+classification+matrix">&para;</a> <a href="#top">&uarr;</a></span></h2>

<p>The shared composer classifier (<code>bin/fm-composer-lib.sh</code>, <code>fm_composer_classify_screen</code>) owns every composer shape fleet-wide; each backend contributes only a capture and a capability descriptor. The live half of that guarantee was verified on 2026-08-10 from an already-trusted checkout at the branch&#39;s final validated head, against every installed harness on tmux 3.6a, macOS arm64, on an isolated private socket, with no prompt submitted to any harness. An earlier untrusted-worktree run left Claude, Grok, and Muse unverified because the guard treats first-launch trust dialogs as an unreadable-composer state and never confirms them; this trusted-checkout rerun supersedes those missing results.</p>

<pre>FM_COMPOSER_MATRIX_LIVE=1 tests/fm-composer-matrix-live-e2e.test.sh</pre>

<p>Observed output:</p>

<pre>ok - claude (2.1.227 (Claude Code)): real idle composer classifies empty
ok - codex (codex-cli 0.146.0): real idle composer classifies empty
ok - opencode (1.14.46): real idle composer classifies empty
ok - pi (0.84.0): real idle composer classifies empty
ok - grok (grok 1.0.0 (3cd0d0cbcebe)): real idle composer classifies empty
# harness absent, not verified here: kimi
ok - muse (Muse Code 0.1.0 (0.1.0-R708.1)): real idle composer classifies empty
ok - strict posture live: a blank shell row classifies unknown and injection defers
ok - zellij (zellij 0.44.0): unrelated pane change never confirms delivery (verdict: unknown)
ok - live composer-matrix guard verified 8 live surface(s)</pre>

<p>All six installed harnesses&#39; real idle composers reached a proven <code>empty</code> (Claude auto-updated to 2.1.227 between the audit and this rerun, so the shipped classifier is proven against the newer release as well), including Pi through the tmux foreground-process identity probe, Grok through the titled-bottom-border tolerance, and OpenCode through the left-bar shape; Codex and OpenCode first parked on vendor update-available modals that the strict classifier correctly refused until the guard&#39;s single non-submitting Escape dismissed them. The strict blank-row posture held live (a blank shell row deferred injection), and a zellij pane changing for reasons unrelated to submission never confirmed a delivery, replacing the retired content-diff heuristic&#39;s false positive. Kimi was not installed on the verification machine; its bordered shape is pinned by the portable byte-capture regressions in <code>tests/fm-composer-lib.test.sh</code>, which also carry the other five adapters&#39; capability profiles for every harness under both a UTF-8 locale and <code>LC_ALL=C</code>. This guard is the refresh command after any harness upgrade; rerun it and update the versions above rather than trusting this table across releases.</p>

<p><code>zellij action dump-screen --pane-id &lt;id&gt; --ansi</code> was verified at zellij 0.44.0 to preserve ANSI styling (real Claude Code rendered inside a zellij pane dumped <code>ESC[m</code> <code>❯</code> U+00A0 for its idle composer row), which is the capability the zellij composer classifier reads.</p>
</main></body></html>
Evidence: Installed harness inventory and final commit scope
Installed live-guard inventory observed during targeted validation:
tmux 3.6a
claude    installed  2.1.227 (Claude Code)
codex     installed  codex-cli 0.146.0
opencode  installed  1.14.46
pi        installed  0.84.0
grok      installed  grok 1.0.0 (3cd0d0cbcebe) [stable]
kimi      absent
muse      installed  Muse Code 0.1.0 (0.1.0-R708.1)
zellij    installed  zellij 0.44.0

Final follow-up commit scope:
M	docs/verification/runtime-backends.md

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • git diff --name-status HEAD^..HEAD &amp;&amp; git diff --check HEAD^..HEAD — confirmed the follow-up commit changes only docs/verification/runtime-backends.md and has no whitespace errors.
  • tests/fm-composer-lib.test.sh — exercised the portable composer matrix, strict blank-row behavior, all harness shapes including Kimi, and Zellij ANSI handling.
  • Queried --version for the live-guard harness inventory plus tmux -V; versions matched the documented matrix and Kimi was absent.
  • Rendered the Composer classification matrix from the target commit to HTML and generated/reviewed a 1400×1400 PNG thumbnail.
  • Validated the rendered artifact includes the final-head wording, Claude 2.1.227, six installed harness results, honest Kimi absence, strict deferral, and Zellij false-positive result.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.


Reviewer companion: hazard-by-hazard map and evidence

What this is

One comprehensive, captain-approved refactor of the composer-classification architecture (audit data/fm-composer-consolidation-audit-s1, decisions corr=a66f17eb1dffbe7f): every composer shape now lives in ONE shared owner (bin/fm-composer-lib.sh, fm_composer_classify_screen), the five backend adapters are capture-only plus a declarative capability descriptor (styled/cursor/identity/rows as data), fm-spawn's kimi regex (a fourth shape copy) is retired, the whole 30-cell correctness matrix is fixed, and the captain's STRICT blank-row posture applies fleet-wide. A new harness shape is teachable in one place and every backend learns it in the same commit.

Matrix before/after (idle-empty composer; correct verdict = empty)

Before = production code at 85e750a, measured by the audit. After = this branch (portable byte-capture regressions under UTF-8 AND LC_ALL=C; live guard for the tmux column).

harness tmux (UTF-8) tmux (LC_ALL=C) cmux orca herdr zellij
claude empty → empty pending → empty empty → empty unknown → empty empty (C: pending) → empty unknown → empty
codex empty → empty empty → empty unknown → unknown* pending(banner!) → unknown* empty → empty unknown → empty
pi empty(permissive) → empty(identity-proven) same unknown → unknown* unknown → unknown* empty → empty unknown → unknown*
muse empty → empty empty → empty unknown → empty unknown → empty unknown → empty unknown → empty
opencode unknown → empty unknown → empty unknown → empty unknown → empty unknown → empty unknown → empty
grok unknown → empty unknown → empty empty → empty empty → empty empty → empty unknown → empty

* the CORRECT reachable verdict for that capability profile (approved design, report §4.3 pt 7): a plain capture cannot distinguish codex's idle hint from typed text (the old orca pending came from a startup banner, a confidently wrong answer), and a backend without an identity probe cannot prove pi's blank separated row (the strict rule). Net: 13/30 correct before (2 locale-dependent, 1 permissive artifact, 1 false pending) → 25/30 proven-empty + 5 principled defers, 0 false pending, 0 false empty.

The discrete safety fixes, each its own commit

The base commit plus 18 review-round fix commits each prevent a concrete failure. Review rounds were driven gate-by-gate with captain decisions (yolo off); the review agent's adversarial passes found real hazards in both the original code and its own fixes, and each is separately reviewable:

Base commit 05a8bfb — the consolidation itself plus the audit's six matrix fixes: (1) locale-safe Unicode-space normalization closing #1988 for tmux/herdr (claude's +U+00A0 read pending under LC_ALL=C, deferring every away-mode escalation in daemon contexts; cmux's adapter-local NBSP case deleted); (2) muse's in the shared glyph set (empty muse composers read unknown forever on herdr/cmux/orca); (3) orca learns the borderless bare shape and drops backward paging (every steer to a borderless harness exited unconfirmed; a codex startup banner classified as pending composer text); (4) tmux tolerates grok's titled bottom border (every grok steer failed delivery unconfirmed; away mode never injected); (5) opencode's left-bar shape (unreadable on every backend); (6) zellij's content-diff submit heuristic replaced by a real dump-screen --ansi classifier (the fleet's only FALSE-POSITIVE: any pane change - a clock, a spinner - confirmed delivery and could close a --resolve-key decision for a message never received). Plus the strict posture, the pi identity probe for tmux, and the tri-state groundwork.

Review fix commits (git log origin/main..HEAD, oldest first):

  1. f7cf335 — prevent Pi identity-proof bypass: a lone agent-glyph draft between Pi rules read empty after a swallowed Enter; identity now reroutes it to the separated shape (pending) where the capability exists. On identity-less backends bare-wins is the captain-decided tradeoff (byte-identical to Claude's real composer; Pi delivery there is never confirmed anyway; the zellij delta independently protects sends). Also extends the matrix tests toward the full capture x profile x locale cross-product.
  2. 3b1438d — prevent the fix above regressing the fix(cmux): classify borderless Claude composers #2029 cell: absent/non-Pi identity keeps the bare verdict, so Claude's +NBSP row stays empty on tmux; removed the test fixture that masked the regression with an impossible identity.
  3. a222dff — harden box structure and titled-border geometry against the widened shape set.
  4. a787ed0 — prevent false delivery confirmation on an unreadable baseline: the submit core's idle baseline is now tri-state and affirmative (an unreadable pre-typing read disables the unknown→empty turn-started conversion entirely); plus the zellij live-guard fails loudly on setup/source/pane-discovery failures instead of passing vacuously.
  5. ebe7dc9 — prevent a box's bottom border from ever being read as an input row.
  6. 12af7a5 — prove the zellij probe actually typed (zellij actions exit 0 while doing nothing): observation before Enter, send-failed fails the guard.
  7. b11aa2e — preserve Pi identity uncertainty and scan full left-bar drafts (a multi-row opencode draft below the hint row was missed).
  8. 777254a — verify zellij text landed before submitting (typing proof by observation, not exit codes).
  9. 337ca19 — scope that observation to the classified composer (a screen-wide substring passed on transcript echoes while an unrelated draft sat in the composer, and rejected legitimate wrapped sends).
  10. d19458e — prove THIS paste landed by composer-scoped pre/post delta (containment passed when the text was already present, or when furniture like opencode's footer matched a short steer such as "high").
  11. 0ef8b73 — prevent false send-failed on wrapped pastes: cursorless extraction includes the bare composer's wrap region.
  12. d459dc8 — prevent dead-shell injection: a stale above an exited agent's live $ prompt read empty cursorlessly; a lower shell-prompt row now invalidates the candidate.
  13. 790d80d — handle bordered shell-glyph placeholders in extraction (a │ > Type a message... │ baseline broke the paste proof).
  14. ba55d5a — prevent injection over streaming output: a bare glyph row with contiguous output directly below no longer reads empty cursorlessly (blank/rule-separated harness footers stay inert).
  15. 1ca98e4 — the same claimed-region rule for box and left-bar candidates (opencode's ╹▀▀▀ floor counted as the shape's own boundary).
  16. eb0addd — preserve user bytes: prompt-glyph stripping applies only to the shape's actual prompt row, never wrapped continuation rows (steers quoting $ cmd lines were stranded as send-failed).
  17. 40c3e5f — prevent steer text executing at a live shell: extraction can never absorb a shell-prompt-shaped row as wrap continuation (a paste landing AT the shell could delta-pass, and Enter would have executed it); agent-glyph continuations stay preserved.
  18. 0d2023a — keep wrapped glyph continuations through submit retries (transcript prompt echoes re-anchor to the live bottom prompt; wrap regions are non-glyph-leading rows only).
  19. 4a5e860 — prevent typed placeholder text confirming delivery: the idle pattern proves emptiness only in a shape's own placeholder position; styled text surviving ghost-stripping elsewhere stays pending; plain-capture regex collisions on glyph rows fail safe.
  20. 64788c6 — restore the two cells the previous fix over-tightened (plain glyph-bearing boxed placeholder stays empty - the grok cell; row-leading agent glyphs re-anchor so normal transcripts select the live bottom prompt instead of blocking away-mode injection indefinitely).
  21. fd18c8a — align the zellij paste proof with those final semantics (boxed placeholder baselines; superseded fixture reworked).

347211d/a3492d3 (docs alignment), a3e93b9 (lint), 6d4b6e2 (CI fixes), 73b931b (trusted-checkout live rerun record) complete the branch.

Away-mode validation under STRICT (captain decision blank-row-injection-posture)

Path: inject_msgpane_input_pendingfm_backend_composer_state → thin capture → fm_composer_classify_screen (strict rule applied once, in the shared owner).

  • Defers on everything not positively proven: blank cursor rows, dead-shell prompts, bare busy-footer rows, unidentified text, modal dialogs (live-observed: codex/opencode update modals correctly refuse classification), stale composers above live activity. Pinned by tests/fm-daemon.test.sh, tests/fm-composer-lib.test.sh (test_strict_blank_row_divergence - the posture cannot silently regress), tests/fm-composer-ghost.test.sh, and live.
  • Still injects when proven: real-tmux end-to-end (tests/fm-afk-inject-e2e.test.sh) - defers on human partial input then delivers cleanly after idle; retries a swallowed Enter (wrapped digests included) with exactly one submission; delivers exactly one clean digest into a proven-empty composer. Escalation buffers survive every deferral.
  • Send path: only exact empty confirms; the tmux turn-started conversion requires an affirmatively proven idle baseline; zellij requires a composer-scoped typed-text delta - a changed pane is never again a delivery confirmation.

Live verification

FM_COMPOSER_MATRIX_LIVE=1 tests/fm-composer-matrix-live-e2e.test.sh from the trusted checkout at this branch's final head (2026-08-10, rerun at the final r2 head): claude 2.1.227 (auto-updated since the audit; the shipped classifier is proven on the newer release), codex 0.146.0, opencode 1.14.46, pi 0.84.0, grok 1.0.0, muse 0.1.0-R708.1 all classify empty live; strict blank-row deferral held; the hardened zellij probe (typing observed before Enter) confirmed the false-positive dead. Kimi is not installed here - no live result exists or is claimed; its bordered shape is pinned by the portable regressions. Recorded in docs/verification/runtime-backends.md "Composer classification matrix".

Sequencing vs open PRs #1995 / #2047

Both target issue #1988 in the shared owner and were open at intake. This branch implements the locale-safe normalization itself using #1995's exact function names (fm_composer_normalize_spaces_var / fm_composer_normalize_trim_var) so the implementations converge: if this merges first, #1995's normalization hunks and #2047's fm-composer-lib hunk become redundant (their doc/test additions may still be worth cherry-picking); if either merges first, this branch rebases onto the same names. Captain sequencing call.

r2 additions (this superseding PR)

This PR supersedes #2066 identically in content after a validation-custody deadlock (nothing was force-pushed or hand-cleared; the old PR's gate record could not reconcile with its legitimately advanced branch). The rebase onto current main reconciles .agents/skills/afk/SKILL.md with #2065: its durable wake acknowledgement content is untouched and this branch's strict away-mode composer-guard section applies on top. Two further captain-decided fixes landed during r2 revalidation:

  1. 9a63b77 - restore proven-box classification when the cursor sits ON the bottom border (grok parks its cursor there in a pristine composer; fix ebe7dc9 had over-tightened containment and left grok sends/injections unknown in that state).
  2. b569442 - styled box rows obey the placeholder-position rule faithfully: bright text surviving ghost-stripping stays pending even when it matches Type a message... / Ask anything... (the real placeholder is de-emphasized and already stripped); plain-capture box rows keep the grok-cell decision.

One reviewer proposal was explicitly DECLINED by the captain as a finalized tradeoff: plain-capture bare-composer text degrades to unknown (loud refusal, no blind Enter retry on cmux/orca) - deliberately NOT extended with delta-proof machinery.

73b931b/bb08968 record the trusted-checkout live guard reruns honestly (final r2 head: all six installed harnesses live-green including claude 2.1.227 and grok; kimi absent, no live result claimed).

…le matrix fixed

Consolidate every composer shape - bordered boxes (all families, geometry,
titled bottom borders), bare agent-glyph rows and their wrap regions,
opencode's left bar, and pi's identity-gated separator pair - into
fm_composer_classify_screen in bin/fm-composer-lib.sh. Adapters now
contribute only a capture and a declarative capability descriptor
(styled/cursor/identity/rows); capability differences change how confidently
a shape is judged, never what the shapes are, so a new harness shape is
teachable in exactly one place.

Correctness fixes landed as part of the consolidation (audit
data/fm-composer-consolidation-audit-s1):
- locale-safe Unicode-space normalization in the shared owner (closes the
  fleet-wide half of #1988; cmux's local byte-exact NBSP case deleted;
  naming converges with PR #1995's normalization primitive)
- muse's bare glyph joins the shared set, unbreaking muse on herdr/cmux/orca
- orca learns the borderless bare shape, drops its backward-paged composer
  window, and can no longer classify a stale startup banner as the composer
- tmux tolerates a titled bottom border, unbreaking grok steering
- the left-bar shape makes opencode readable on every backend
- zellij gets a real classifier through dump-screen --ansi, replacing the
  content-diff submit heuristic that could confirm an undelivered message
  and close a --resolve-key decision (the fleet's only false positive)
- fm-spawn's kimi launch-readiness regex (the fourth shape copy) now routes
  through the shared classifier

The strict blank-row posture applies fleet-wide (captain decision
blank-row-injection-posture): no positive container proof = unknown = defer,
replacing tmux's permissive blank-cursor-row rule. Away-mode injection was
re-validated end to end on real tmux (defer on partial input and unproven
rows, clean delivery with swallowed-Enter retry into proven-empty
composers). The tmux submit core gains a baseline-idle turn-started
conversion so pi steering stays confirmed while its working screen hides
the composer; busy conversion without that baseline remains forbidden.

Plain-capture backends now degrade a glyph row carrying trailing text to
unknown instead of a false pending, per the approved capability rule.

Portable regressions pin the full byte-capture matrix from the audit under
a UTF-8 locale and LC_ALL=C, the strict-vs-permissive divergence, and
deliberate signal separation; the opt-in live guard
(tests/fm-composer-matrix-live-e2e.test.sh) verified every installed
harness against the real classifier, recorded in
docs/verification/runtime-backends.md.
The pipeline's isolated gate worktree is untrusted, so claude, grok, and
muse stopped at first-launch trust dialogs there (the guard refuses to
confirm them by design). This rerun from the trusted checkout at the final
validated head verified all six installed harnesses, the strict blank-row
deferral, and the hardened zellij false-positive probe live.
…ed rerun

The post-validation rerun from the trusted checkout verified all six
installed harnesses at the branch's final head, including Claude 2.1.227
(auto-updated since the audit's captures) and Grok, which the untrusted
gate worktree could not verify past their first-launch trust dialogs.
@kunchenguid
kunchenguid force-pushed the fm/fm-composer-thin-adapter-refactor-r2 branch from a2b278e to bb08968 Compare August 10, 2026 23:54
@kunchenguid kunchenguid changed the title fix: harden composer detection across terminal backends fix(composer): unify safe classification across backends Aug 10, 2026
@kunchenguid
kunchenguid merged commit 7f05100 into main Aug 11, 2026
37 of 39 checks passed
@kunchenguid
kunchenguid deleted the fm/fm-composer-thin-adapter-refactor-r2 branch August 11, 2026 01:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant