Skip to content

fix: recognize wrapper-launched Claude Code composers and queue with Enter - #26

Merged
andrei-hasna merged 3 commits into
mainfrom
fix/cb60ace2-claude-code-composer-detection
Jul 31, 2026
Merged

fix: recognize wrapper-launched Claude Code composers and queue with Enter#26
andrei-hasna merged 3 commits into
mainfrom
fix/cb60ace2-claude-code-composer-detection

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Bug

dispatch send / dispatch triage refused prompt delivery to every Claude Code seat on the fleet:

"detail": "target is not a recognized agent composer (node); refusing prompt delivery",
"detection": {"targetKind":"unknown","agentKind":"unknown","canReceivePrompt":false,"paneCommand":"node"}

Measured on station01 (2026-07-31 ~09:08Z) against the hq tmux session — 7 windows, each a live Claude Code TUI launched via accounts launch … --tool claude. --force-active cannot override because detection never proves an agent at all. This blocks the CEO coordinator delivering directives into seat terminals.

Bug task: cb60ace2-fe5d-43e4-849b-d2df60d0e9c6 (dispatch project).

Root cause

Wrapper-launched panes (node) need process-tree proof AND live composer UI proof. The process tree already proves claude (the claude --dangerously-skip-permissions child). But liveUiProofForKind had wrapped-composer matchers only for Codewith and Codex; for claude it fell back to hasNamedAgentComposer, which requires a name-banner line plus model:/cwd: context lines — a synthetic shape the steady-state Claude Code TUI never renders (the existing wrapped-claude test passed only because its fixture invented that banner).

Fix

  • hasWrappedClaudeComposer (src/lib/exec-policy.ts), wired into liveUiProofForKind alongside the Codewith/Codex matchers: process-gated content proof from the real TUI signature — the composer glyph (plain or NBSP-padded) or the esc to interrupt busy marker, combined with the permission-mode footer (⏵⏵ bypass permissions on, accept edits on, plan mode on, ? for shortcuts) or the account/model status lines. A bare with no chrome stays refused (existing spoof-guard test still green), and the accounts launcher alone (no claude child) is not evidence.
  • Per-agent queue submit keys (AGENT_QUEUE_SUBMIT_KEYS): Codewith queues with Tab (unchanged); Claude Code queues with plain Enter. Claude panes no longer advertise Tab as a submit key — in Claude Code Tab is completion, not submission, so dispatch key Tab to a claude pane now correctly refuses.
  • Engine: --queue resolves the detection's proven queue key; Enter-queueing agents pass the idle-only Enter gates when queueing is explicitly requested and proven; queued delivery is single-shot for every queue key (avoids double-queueing).
  • Doc/description strings that hardcoded "queued Tab delivery" updated.

Regression tests (written failing-first)

  • recognizes wrapped idle Claude Code seats launched through the accounts wrapper — the headline regression: sanitized capture of a real hq seat + the real measured process tree must yield agentKind claude, composerState idle, canReceivePrompt true, submitKeys ["Enter"].
  • recognizes busy wrapped Claude Code seats and offers Enter queueing
  • recognizes the Claude Code composer glyph followed by a non-breaking space (the live TUI pads with U+00A0)
  • refuses Claude Code seat content without Claude process evidence (spoof tree + launcher-only tree)
  • engine: queues active wrapped Claude Code seats with Enter when queue is explicitly requested

Evidence

Full suite: 491 pass / 2 fail — the 2 failures are confirm-regression.integration.test.ts real-tmux tests that fail identically at clean base 27a5f75 (pre-existing, environment-dependent; out of scope). bun run typecheck and bun run build green.

Live dry-run with the built fix on station01:

hq:staff        -> {"agentKind":"claude","composerState":"idle","canReceivePrompt":true,"canQueuePrompt":false,"submitKeys":["Enter"],"recommendedSubmitKey":"Enter"}
hq:engineering  -> {"agentKind":"claude","composerState":"active","canReceivePrompt":false,"canQueuePrompt":true,"recommendedSubmitKey":"Enter"}

Fixtures are sanitized (synthetic workspace id, synthetic dollar/percent figures); profile slugs match the shapes already present in the repo's Codewith fixtures.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…Enter

Claude Code seats launched through `accounts launch … --tool claude` run as a
bare `node` pane, so recognition takes the wrapper path: process-tree proof
plus live composer UI proof. The process tree already proved `claude`, but
liveUiProofForKind had no Claude Code content matcher — the generic
hasNamedAgentComposer requires a name banner plus model:/cwd: context lines
that the steady-state Claude Code TUI never renders. Every wrapped Claude
Code pane was therefore refused as "not a recognized agent composer (node)",
and --force-active could not override, because detection never proved an
agent at all.

- add hasWrappedClaudeComposer: process-gated content proof from the real TUI
  signature — the ❯ composer glyph (plain or NBSP-padded) or the
  `esc to interrupt` busy marker, combined with the permission-mode footer
  (`⏵⏵ bypass permissions on`, `accept edits on`, `plan mode on`,
  `? for shortcuts`) or the account/model status lines. A bare composer glyph
  without chrome stays refused, so copied transcripts are still not targets.
- make the queue submit key agent-specific (AGENT_QUEUE_SUBMIT_KEYS):
  Codewith queues with Tab, Claude Code queues with plain Enter. Claude
  panes no longer advertise Tab as a submit key — in Claude Code Tab is
  completion, not submission, so `dispatch key Tab` now correctly refuses.
- resolve --queue delivery through the detection's proven queue key and let
  Enter-queueing agents through the idle-only Enter gates; queued delivery is
  single-shot for every queue key to avoid double-queueing.
- fixtures are sanitized captures from the live hq seats measured on
  station01 (2026-07-31), including the real accounts-launch process tree.

Repro fixed: `dispatch send --to hq:staff -p test --dry-run --json` now
returns agentKind claude, composerState idle, canReceivePrompt true instead
of refusing with targetKind unknown.

Bug task: cb60ace2-fe5d-43e4-849b-d2df60d0e9c6

Agent: Augustus
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] NO_GO — #26 @ 5d5a820 — lens: correctness+security, reviewer Quintus (1 of 1)

Note: PR head 34a6808 = 5d5a820 + a one-line version bump (0.0.24→0.0.25, package.json only, verified by diff); all findings below apply to the PR head. npm still serves 0.0.24.

Verdict: NO_GO — the acceptance criteria themselves all pass, but the per-agent queue-key change missed one consumer, and that miss is a newly reachable wrong-key delivery into live Claude seats.

P1 (blocking): dispatch recover --apply presses Tab in active wrapped Claude Code seats

recommendRecoveryAction (src/lib/agent-recovery.ts:135-141) hardcodes submitKey: "Tab" for every canQueuePrompt target, and the engine's Tab gate (src/lib/engine.ts:133) checks canQueuePrompt generally — not that the agent's proven queue key IS Tab. This PR makes active wrapped Claude seats canQueuePrompt: true (queue key Enter), so both guards now pass for a key this PR's own comments call harmful ("Tab is completion, not a submit key... corrupt the composer").

Measured (fixtures identical to the PR's own tests, MockRunner harness):

  • HEAD 5d5a820: recovery action = {"kind":"queue","submitKey":"Tab","safeToApply":true}; engine result delivered, prompt typed into the live pane, Tab sent.
  • Base 27a5f75: same inputs → refuse / nothing typed (wrapped Claude was undetected).

Reachable today: dispatch recover <target> -p ... --apply (queue is ON by default; --no-queue is the opt-out) and the MCP recover tool — and recovering busy seats is this verb's primary use case. The record can read "delivered" while the seat's composer was polluted and nothing queued.

Remedy (small, named): make recommendRecoveryAction use detection.recommendedSubmitKey — the exported queueSubmitKeyFor helper added by this PR is currently dead code, which is the tell that this consumer was missed — and harden the engine Tab gate to require the proven queue key to be Tab. Regression test: recover against the active wrapped Claude fixture must plan/apply Enter, never Tab.

P2 (follow-up): --queue --submit-key Enter on an active Codewith pane now delivers

queuedDelivery (engine.ts:122) doesn't pin the resolved submit key to the proven queue key. Measured: HEAD delivers Enter into an active Codewith composer; base refused without --force-active. Both flags are accepted together by the CLI. Remedy: queuedDelivery should also require submitKey === detection.recommendedSubmitKey.

P3 (non-blocking)

  • queueSubmitKeyFor is exported and unconsumed.
  • Scrollback text in a genuine claude-process pane quoting "esc to interrupt" + footer chrome classifies active/queue-capable. Process-gated, so bounded to real Claude panes and the delivered key is Enter — same class as the pre-existing codewith/codex exposure, not materially worse.

Verified (all acceptance criteria pass)

  1. All 5 named regression tests exist and pass; failing-first proven: copied onto clean base source, 4 new tests + the amended direct-pane test fail (5 fail / 100 pass).
  2. Full suite 491 pass / 2 fail — exactly the two claimed integration tests; reproduced identically at clean base 27a5f75 from an independent clone (mechanism: 80-col pane wraps the long cwd, toContain(process.cwd()) can never match — environmental, pre-existing). bun run typecheck rc=0, bun run build rc=0.
  3. Live dry-run send --to hq:staff --dry-run --json: agentKind: "claude", canReceivePrompt: true, recognized claude wrapper from process tree and live composer UI.
  4. Security posture: "does not accept bare Claude prompt glyph" passes; spoofed node trees and accounts-launcher-without-claude-child refused (tested); codewith/codex matchers untouched in the diff; shell refusal intact; dispatch key Tab now correctly refuses Claude panes (submitKeys=["Enter"]).
  5. Secrets scan over the diff: 0 hits; fixture ids are synthetic (account005/006, wks_c0ffee...).

One remediation cycle should clear this: the P1 remedy is two small changes plus one regression test.

Review findings on #26 (cycle 1):

- P1: recommendRecoveryAction hardcoded Tab for every canQueuePrompt target,
  so `dispatch recover --apply` would press Tab in an active Claude Code
  seat — the key the queue-key map exists to avoid. Recovery now uses the
  detection's proven queue key, and the engine Tab gate additionally
  requires Tab to BE the proven queue key, so a stale Tab recommendation
  can no longer reach a Claude composer.
- P2: --queue --submit-key Enter on an active Codewith pane rode the
  queuedDelivery exemption with the wrong key. queuedDelivery now requires
  the resolved submit key to equal the proven queue key; mismatched
  explicit keys fall back to the strict idle-only gates, as at base.
- P3: removed the unused queueSubmitKeyFor export; consumers read the
  proven key from detection.recommendedSubmitKey.

Regression tests written failing-first:
- agent-recovery: "recover plans queued Enter recovery for active Claude
  Code seats, never Tab"
- engine: "refuses --queue --submit-key Tab against active Claude Code
  seats" and "refuses --queue --submit-key Enter against active Codewith
  panes"

Agent: Augustus
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] GO — #26 @ 92f4829 — lens: correctness+security, reviewer Quintus (1 of 1)

Remediation cycle 1 re-review, scoped to my named defects and their direct regressions per the bounded-review policy. All findings are fixed and verified by re-running the original repros against the new head.

P1 fixed and verified. recommendRecoveryAction now returns the detection's proven queue key, and the engine Tab gate requires Tab to BE the proven queue key. Re-ran my original repro at 92f4829:

  • Recovery action for the active wrapped Claude fixture: {"kind":"queue","submitKey":"Enter","safeToApply":true} (was "Tab" at 5d5a820).
  • Engine end-to-end with that action: delivered with Enter, no Tab sent to the Claude pane.
  • Defense in depth confirmed: even a forced --queue --submit-key Tab against the active Claude fixture is now skipped with zero keystrokes reaching the pane (new engine test asserts no send-keys/paste-buffer at all).

P2 fixed and verified. queuedDelivery now requires the resolved submit key to equal the proven queue key. Re-ran my repro: --queue --submit-key Enter against an active Codewith pane is skipped, no Enter sent — restored to base behavior. --queue with the matching explicit key (Enter on Claude, Tab on Codewith) still queues, single-shot.

P3 fixed. Dead queueSubmitKeyFor export removed.

Regression surface of the fix itself checked: legitimate Codewith Tab queueing ("queues active wrapped Codewith panes with Tab", "recover ... plans queued Tab recovery for active Codewith panes") and the no-queue-support refusal all pass in the full run.

Failing-first verified for the three new tests: copied onto pre-fix source 5d5a820, all three fail (3 fail / 66 pass); at 92f4829 all pass.

Gates: full suite 494 pass / 2 fail — the same two pre-existing environmental confirm-regression.integration failures already reproduced identically at clean base 27a5f75 (80-col pane wraps the long cwd). bun run typecheck rc=0, bun run build rc=0. Live dry-run against hq:staff still detects claude / canReceivePrompt: true.

Scope check: the remediation diff (34a6808..92f4829) touches only agent-recovery.ts, engine.ts, exec-policy.ts (dead-export removal) and the two test files — nothing outside the named defects.

Clear to merge.

@andrei-hasna
andrei-hasna merged commit d68b9cf into main Jul 31, 2026
2 checks passed
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] GO — #26 @ 92f4829 — lens: correctness+security, reviewer Quintus (1 of 1)

Re-review of remediation cycle 1. Scope limited to my three named defects and their direct regressions, per the bounded-review policy. All three are fixed and verified by re-running my original repros, not by reading the diff.

P1 (blocking, FIXED) — Tab into an active Claude Code seat.
recommendRecoveryAction now returns detection.recommendedSubmitKey ?? "Tab" (src/lib/agent-recovery.ts:136), and the engine Tab gate now requires the proven key, not merely queue capability (src/lib/engine.ts:138). My saved repro against the active wrapped-Claude fixture:

A1 recovery action: {"kind":"queue","submitKey":"Enter","safeToApply":true,
                     "reason":"active agent composer advertises queued Enter prompt support"}
A2 engine result: delivered | typed prompt into live pane: true | SENT TAB TO CLAUDE PANE: false

At 5d5a820 this returned submitKey:"Tab" and drove a Tab into the composer. Fail-closed check: when detection carries no recommendedSubmitKey, resolveSubmitKey still falls back to "Tab", but the tightened gate then refuses it — an unproven target is skipped, never submitted.

P2 (FIXED) — --queue --submit-key Enter riding the queuedDelivery exemption on active Codewith.
queuedDelivery now additionally requires submitKey === detection.recommendedSubmitKey (src/lib/engine.ts:126-131). Repro:

B  --queue --submit-key Enter vs active codewith: skipped | SENT ENTER: false
   detail: target cannot receive an Enter prompt safely (... active composer supports queued Tab
   prompt delivery); pass --queue for supported active agents or --force-active to override

P3 (FIXED) — dead queueSubmitKeyFor export removed. grep -rn queueSubmitKeyFor src/ → no matches.

Regression tests present and passing (all failing-first claims accepted on the basis that they encode the exact refuted behaviour):

  • agent-recovery.test.ts:188 "recover plans queued Enter recovery for active Claude Code seats, never Tab" — asserts {kind:"queue",submitKey:"Enter"} and that no send-keys/paste-buffer is issued.
  • engine.test.ts:661 "refuses --queue --submit-key Tab against active Claude Code seats"
  • engine.test.ts:687 "refuses --queue --submit-key Enter against active Codewith panes"
    Each run under a name filter: 0 fail.

Direct regressions — none found.

  • engine.test.ts:604 "queues active wrapped Codewith panes with Tab when queue is explicitly requested" still green: Codewith's recommendedSubmitKey is Tab, so plain --queue resolves to Tab, clears the Tab gate, and rides the exemption unchanged.
  • Idle-target recovery untouched (canReceivePrompt path unchanged).
  • Diff confined to 6 files; no source file outside agent-recovery.ts / engine.ts / exec-policy.ts changed.

Gates. Full suite 496 pass / 0 fail, exit 0 (Ran 496 tests across 39 files). bun run typechecktsc --noEmit, exit 0. Note this is better than the 494/2 baseline I predicted: the two failures I had recorded as pre-existing environmental integration failures pass in a clean checkout, so they were environment artefacts of my original worktree, not code.

Provenance / caveat, stated because it affects what this verdict certifies. My review worktree was deleted mid-run (PR already merged; worktree cleanup). The first full-suite attempt is therefore garbage — 30 fail / 28 errors, all ENOENT plus getcwd: cannot access parent directories — and I discarded it rather than report it. The numbers above come from a fresh worktree at the squash-merge commit d68b9cf, whose tree is byte-identical to the reviewed 92f4829 (git diff --stat 92f4829 d68b9cf is empty), so they certify exactly the reviewed content. Worktree HEAD reviewed = 92f4829 = the PR head on GitHub.

No concrete, currently-reachable P0/P1 remains in the remediated code or its direct regressions. GO.

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