fix(composer): unify safe composer detection across backends - #2066
Open
kunchenguid wants to merge 28 commits into
Open
fix(composer): unify safe composer detection across backends#2066kunchenguid wants to merge 28 commits into
kunchenguid wants to merge 28 commits into
Conversation
…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.
|
Superseded by #2102, which carries the same composer classification work on a validated branch with green checks. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Follow-up commit on the already-validated composer thin-adapter branch (PR #2066, checks green): one docs-only change updating docs/verification/runtime-backends.md 'Composer classification matrix' with the genuine trusted-checkout live guard rerun performed after checks passed - the already-agreed post-green step resolving the test step's test-live-trust warning. The pipeline's untrusted gate worktree could not verify claude/grok/muse (first-launch trust dialogs, which the guard refuses to confirm by design); the rerun from the trusted checkout at the final validated head verified all six installed harnesses (claude 2.1.226, codex 0.146.0, opencode 1.14.46, pi 0.84.0, grok 1.0.0, muse 0.1.0-R708.1), the strict blank-row deferral, and the hardened zellij false-positive probe live, with no prompt submitted to any harness. Honesty contract: the record states only what actually ran; kimi remains explicitly absent with no live result claimed. Everything else on the branch is already validated; no code changes in this commit.
What Changed
fm-composer-lib.sh, leaving backend adapters responsible only for capture capabilities.Risk Assessment
🚨 High: The shared classifier has a source-proven false-empty path that can overwrite unsubmitted user input on three supported backends and requires an explicit product tradeoff to resolve.
Testing
Confirmed the commit changes only the verification document, the installed harness versions and explicit Kimi absence match its recorded output, the live guard remains safely opt-in, and the updated end-user documentation renders correctly. The live run was not repeated because this gate worktree is intentionally untrusted; the change records the supplied trusted-checkout rerun.
Evidence: Rendered Composer classification matrix documentation
Evidence: Commit scope, installed-version inventory, and guard opt-in transcript
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-composer-lib.sh:1221- A Pi draft containing only❯between separator rows is classifiedemptyon identity-less backends (Zellij, cmux, Orca), allowing injection to overwrite real input or submit verification to falsely succeed. Tests explicitly demonstrate this at tests/fm-composer-lib.test.sh:249-253. Please decide the intended tradeoff: the earliest shared safety boundary is_fm_composer_classify_bare_pi_overlap, where an overlap without identity should defer asunknown, though that requires another signal to preserve empty Claude detection on identity-less backends.✅ **Test** - passed
✅ No issues found.
git diff-tree --no-commit-id --name-status -r 59eb8f461693e02814da03f732248533230f0d84Queried installed versions forclaude,codex,opencode,pi,grok,kimi,muse,tmux, andzellijtests/fm-composer-matrix-live-e2e.test.shwithout opt-in, confirming the guard safely skipsgit diff --check 59eb8f461693e02814da03f732248533230f0d84^ 59eb8f461693e02814da03f732248533230f0d84 -- docs/verification/runtime-backends.mdRendered the updated Composer classification matrix section to reviewer-visible HTML✅ **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/rowsas 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).* 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
pendingcame 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
ba885d0— 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 readpendingunderLC_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 readunknownforever 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 aspendingcomposer text); (4) tmux tolerates grok's titled bottom border (every grok steer faileddelivery 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 realdump-screen --ansiclassifier (the fleet's only FALSE-POSITIVE: any pane change - a clock, a spinner - confirmed delivery and could close a--resolve-keydecision 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 ba885d0..568d205, oldest first):c69e0e9— prevent Pi identity-proof bypass: a lone agent-glyph draft between Pi rules reademptyafter 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.9b6917f— 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 staysemptyon tmux; removed the test fixture that masked the regression with an impossible identity.f8eb6f7— harden box structure and titled-border geometry against the widened shape set.9adb9bb— 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.29b0844— prevent a box's bottom border from ever being read as an input row.612ba2e— prove the zellij probe actually typed (zellij actions exit 0 while doing nothing): observation before Enter,send-failedfails the guard.e06a12d— preserve Pi identity uncertainty and scan full left-bar drafts (a multi-row opencode draft below the hint row was missed).2c2678d— verify zellij text landed before submitting (typing proof by observation, not exit codes).11fc737— 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).0b1d28c— 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").e1a9072— prevent falsesend-failedon wrapped pastes: cursorless extraction includes the bare composer's wrap region.0b83c7f— prevent dead-shell injection: a stale❯above an exited agent's live$prompt reademptycursorlessly; a lower shell-prompt row now invalidates the candidate.8b68139— handle bordered shell-glyph placeholders in extraction (a│ > Type a message... │baseline broke the paste proof).cf63973— prevent injection over streaming output: a bare glyph row with contiguous output directly below no longer readsemptycursorlessly (blank/rule-separated harness footers stay inert).9b44a85— the same claimed-region rule for box and left-bar candidates (opencode's╹▀▀▀floor counted as the shape's own boundary).d1b2a29— preserve user bytes: prompt-glyph stripping applies only to the shape's actual prompt row, never wrapped continuation rows (steers quoting$ cmdlines were stranded assend-failed).dd32ccf— 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.3b7822e— 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).174c3b7— 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.75dd1d4— 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).d25a77b— align the zellij paste proof with those final semantics (boxed placeholder baselines; superseded fixture reworked).3cea3ae/568d205(docs alignment),81cb5df(lint),74d0ede(CI fixes),59eb8f4(trusted-checkout live rerun record) complete the branch.Away-mode validation under STRICT (captain decision blank-row-injection-posture)
Path:
inject_msg→pane_input_pending→fm_backend_composer_state→ thin capture →fm_composer_classify_screen(strict rule applied once, in the shared owner).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.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.emptyconfirms; 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.shfrom the trusted checkout at this branch's final head (2026-08-10): claude 2.1.226, 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 indocs/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.