diff --git a/.agents/reports/canary-projects-multi-agent-mux-creator-claude/report-prompt-lock-analysis.md b/.agents/reports/canary-projects-multi-agent-mux-creator-claude/report-prompt-lock-analysis.md new file mode 100644 index 0000000..154cfab --- /dev/null +++ b/.agents/reports/canary-projects-multi-agent-mux-creator-claude/report-prompt-lock-analysis.md @@ -0,0 +1,175 @@ +# Prompt-Lock / Input Delivery Failure — Code-Level Analysis (Creator Claude) + +- **Analyst**: Creator Claude (`canary-projects-multi-agent-mux-creator-claude`) +- **Date**: 2026-07-11 +- **Brief**: `.mam/reports/brief-prompt-lock-fix.md` +- **Roadmap linkage**: this is the concrete design for **FW-W2** ("블라인드 TUI 키 입력 방지를 위한 실행 준비도 검증", FUTURE_WORKS P2) — landing this closes that item. + +--- + +## 1. Symptom → Root Cause → Code Mapping + +The reported symptom — instruction text visible in the prompt box but never submitted, or a frozen cursor — is reproducible from the current code through three distinct paths: + +| # | Root cause (brief) | Code path that triggers it | +|---|---|---| +| RC-A | Renderer thread bottleneck during heavy output | `inject_instructions()` pastes, sleeps a **fixed 0.5 s**, sends **one blind `C-m`**. If the TUI (Ink/Blessed) is still flushing startup output, the paste lands but the Enter is consumed while the input widget isn't accepting submits → text sits unsubmitted forever. No verification, no retry. | +| RC-B | Permission/trust dialog steals focus | `wait_for_tui_ready()` **classifies dialogs as "ready"** (see §2-B), so injection proceeds while a modal is up: the pasted text is swallowed by the dialog widget and the `C-m` blindly activates whatever dialog button is focused. | +| RC-C | OAuth / list-selection blocks intercept keys | Same as RC-B (no dialog detection anywhere), plus the resume workflow's **unconditional** `Enter/Down/Enter` sequence, which malfunctions in *both* directions (§2-C). | + +Downstream damage: when injection silently fails on a delegated job, no `started` event is ever published — the delegator waits until watchdog timeout, and the pane holds a zombie prompt. The failure is invisible because `inject_instructions()` **always returns 0**. + +--- + +## 2. Exact Code Locations (Deliverable 1) + +### 2-A. `lib.sh:1088-1100` — `inject_instructions()` — **primary defect** +```bash +$local_tmux set-buffer -b "job_buf_$job_id" "$instructions" +$local_tmux paste-buffer -b "job_buf_$job_id" -t "$sess" +sleep 0.5 +$local_tmux send-keys -t "$sess" C-m +``` +Sole caller: `create_session.sh:384` (every `--submit-job` / `--onboard` session). Defects: fixed delay instead of readiness evidence; single unverified `C-m`; no dialog check before pasting; no success/failure contract. **All three root causes converge here.** + +### 2-B. `lib.sh:1042-1084` — `wait_for_tui_ready()` — defective gate +- The claude readiness regex (`lib.sh:1056`) is `"Anthropic|Assistant|Chat|Dangerously|dangerously|Enter|Welcome|projects"`. The **trust/bypass dialogs themselves contain "Enter" and "Dangerously"**, so an open modal is reported as "✅ ready" and injection fires straight into it (RC-B). +- On timeout it prints a warning and **"Proceeding anyway"** (`lib.sh:1083`) with no failure return — the caller cannot distinguish ready from not-ready. +- "Banner text painted" is the wrong readiness signal; it says nothing about the input box accepting keys (RC-A). + +### 2-C. `multi-agent-mux-resume/SKILL.md:150-156` — blind dialog navigation +```bash +sleep 5; tmux send-keys -t "$SESSION_NAME" Enter +sleep 3; tmux send-keys -t "$SESSION_NAME" Down +sleep 0.3; tmux send-keys -t "$SESSION_NAME" Enter +``` +Sent **unconditionally** after claude resume. Two failure modes: (a) if no dialog appears, `Down` puts the fresh prompt into history navigation and the second `Enter` can **re-submit a historical prompt** — spurious re-execution; (b) if the dialog appears later than 5 s under load, the keys land in the prompt and the dialog then blocks all subsequent input — the exact lock symptom. Note the asymmetry: the create path has *no* dialog handling while resume has *blind* handling; neither is correct. + +### 2-D. `multi-agent-mux-stop/scripts/stop_session.sh:181-199` — `graceful_stop()` +`tmux send-keys -t "$SESSION_NAME" "$exitkey" Enter` (line 192) is blind: with a dialog open, `/exit` is swallowed, the 3 s check fails, and the session escalates to `kill-session`/SIGKILL — losing the agent's own state flush. Mitigated by the fallback chain (severity: low), but it produces avoidable hard-kills. + +### 2-E. `multi-agent-mux-create/SKILL.md:214-215` — documented probe +`tmux send-keys -t "$SESSION_NAME" "" Enter` instructs operators to fire a stray Enter as a liveness probe — with a dialog up, this blindly accepts its focused default. Documentation fix. + +### 2-F. Checked and NOT vulnerable (per brief scope) +- `multi-agent-mux-resume/scripts/update_yaml_resumed.sh` — pure registry update; contains no `send-keys`/`paste-buffer`. No change needed. +- `scripts/install_mam.sh` — the epilogue only **prints** quick-start commands for a human; it never drives a TUI. No direct vulnerability; its create quick-start simply funnels into site 2-A, which the fix below covers. +- `MULTI_AGENT_RULES.ko.md:122` already mandates file-based briefs over long serialized typing — correct policy, but insufficient: this incident shows even the short `Read and execute.` line needs guaranteed delivery. + +--- + +## 3. Proposed Prevention Helper (Deliverable 2) + +Add to `lib.sh` (next to the existing pane helpers). Three functions; `send_keys_safe` is the public entry point. + +```bash +# --------------------------------------------------------------------------- +# Prompt-lock safe delivery (FW-W2). Contract: send_keys_safe returns 0 only +# if the text was verifiably submitted; callers must handle non-zero. +# --------------------------------------------------------------------------- + +_sks_tmux() { # server-aware tmux (same rule as inject_instructions) + if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then + tmux -L "$TMUX_SERVER_NAME" "$@" + else + tmux "$@" + fi +} + +_pane_capture() { _sks_tmux capture-pane -p -t "$1" 2>/dev/null || echo ""; } + +# _pane_quiescent [tries=20] [interval=0.5] +# Renderer settled = two consecutive identical non-empty captures. +_pane_quiescent() { + local sess="$1" tries="${2:-20}" interval="${3:-0.5}" prev="__none__" cur i + for ((i = 0; i < tries; i++)); do + cur=$(_pane_capture "$sess") + [ -n "$cur" ] && [ "$cur" = "$prev" ] && return 0 + prev="$cur"; sleep "$interval" + done + return 1 +} + +# _pane_dialog_open — focus-stealing modal signatures (trust / +# permission / OAuth / list-selection). Tokens must NOT appear in normal +# prompt idle screens; keep this list curated per agent TUI release. +_pane_dialog_open() { + _pane_capture "$1" | grep -Eq \ + 'Do you trust the files|Yes, proceed|No, exit|Approve\b|Allow this|Deny\b|Press Enter to continue|Sign in|browser to authenticate|Use arrow keys|Esc to cancel' +} + +# send_keys_safe [job_id] +# 1. Wait for renderer quiescence (defeats RC-A). +# 2. Refuse to paste while a dialog is open (defeats RC-B/RC-C): wait up to +# SKS_DIALOG_TIMEOUT (default 30 s) for it to clear; if SKS_DIALOG_ESCAPE=1 +# send a single Escape and re-check. NEVER a blind Enter — accepting an +# unknown dialog is a policy decision, not a delivery detail. +# 3. Paste via unique buffer; verify the text landed (marker visible in pane). +# 4. Submit C-m; verify submission (marker left the input area); retry the +# C-m up to 3 times with backoff — safe because re-Enter on the same +# unsubmitted text is idempotent. +send_keys_safe() { + local sess="$1" text="$2" job_id="${3:-adhoc}" + local marker deadline + marker=$(printf '%s' "$text" | head -c 200 | tail -c 24) # verification token + + _pane_quiescent "$sess" || { echo "send_keys_safe: pane never quiesced ($sess)" >&2; return 1; } + + deadline=$(( $(date +%s) + ${SKS_DIALOG_TIMEOUT:-30} )) + while _pane_dialog_open "$sess"; do + if [ "${SKS_DIALOG_ESCAPE:-0}" = "1" ]; then + _sks_tmux send-keys -t "$sess" Escape; sleep 1 + fi + [ "$(date +%s)" -ge "$deadline" ] && { echo "send_keys_safe: dialog blocking input ($sess)" >&2; return 2; } + sleep 2 + done + + _sks_tmux set-buffer -b "sks_$job_id" "$text" + _sks_tmux paste-buffer -b "sks_$job_id" -t "$sess" + _sks_tmux delete-buffer -b "sks_$job_id" 2>/dev/null || true + sleep 0.5 + _pane_capture "$sess" | grep -Fq "$marker" || { echo "send_keys_safe: paste not visible ($sess)" >&2; return 3; } + + local try + for try in 1 2 3; do + _sks_tmux send-keys -t "$sess" C-m + sleep "$try" + if ! _pane_capture "$sess" | tail -n 5 | grep -Fq "$marker"; then + return 0 # input box cleared → submitted + fi + done + echo "send_keys_safe: Enter not accepted after 3 tries ($sess)" >&2 + return 4 +} +``` + +Design decisions worth recording: +- **Quiescence over fixed sleeps**: two identical captures prove the renderer drained its queue — directly addresses the Blessed/Ink bottleneck; a fixed `sleep` can only ever be wrong in one direction or the other. +- **Escape opt-in, never blind Enter/Ctrl+C**: `Escape` cancels dialogs but *also* clears typed prompt text in some TUIs, and `Ctrl+C` can interrupt a running agent turn — so focus restoration is gated behind `SKS_DIALOG_ESCAPE=1` and only fires when a dialog signature is positively detected. Default behavior is to wait and then fail loudly (distinct exit codes 1-4 tell the caller what blocked). +- **Marker-based submit verification**: agent-agnostic — no per-TUI spinner parsing. The last 24 chars of the text must appear after paste and must leave the bottom 5 lines after Enter. Retrying Enter while the marker is still in the input box is idempotent. +- **Distinct non-zero exit codes** let `create_session.sh` publish a precise `error` event instead of leaving a zombie job. + +--- + +## 4. Draft Migration Plan (Deliverable 3) + +| Step | Change | Files | Risk | +|---|---|---|---| +| **M1** | Add the three helpers; rewrite `inject_instructions()` body as a thin wrapper over `send_keys_safe` (same signature). Sole caller `create_session.sh:384` inherits the fix with zero call-site change; add return-code check that publishes `error` + lets the still-armed cleanup trap roll the session back. | `lib.sh`, `create_session.sh` | Low — single choke point | +| **M2** | Harden `wait_for_tui_ready`: drop dialog-ambiguous tokens (`Enter`, `Dangerously`, `dangerously`) from the claude regex; treat `_pane_dialog_open` as *not ready*; make timeout `return 1` and let the caller decide (delegated-job path should abort + rollback rather than "proceed anyway"). | `lib.sh` | Low | +| **M3** | Resume workflow: replace the unconditional `Enter/Down/Enter` block with a conditional loop — poll `_pane_dialog_open`; send navigation keys only when a trust-dialog signature is actually present; skip cleanly otherwise. | `multi-agent-mux-resume/SKILL.md` (embedded shell) | Medium — needs scratch-spawn validation | +| **M4** | Stop graceful path: before sending `$exitkey`, run the dialog check (+ optional single Escape); deliver exitkey via `send_keys_safe`; keep the SIGTERM/SIGKILL fallback chain untouched. | `stop_session.sh` | Low | +| **M5** | Docs: fix the stray-Enter probe example (`create/SKILL.md:214-215`) to use `capture-pane` readiness; document `send_keys_safe` in create/delegate-job SKILL.md; mark **FW-W2 resolved** in `FUTURE_WORKS.md` / `.ko.md`. | docs only | None | + +**Verification gate (DoD)** — all on a scratch tmux server (`-L sks-test`), never real sessions: +1. **RC-A stress**: mock TUI that floods output for 10 s before reading stdin → `send_keys_safe` must wait, then deliver; old `inject_instructions` demonstrably drops the Enter. +2. **RC-B/C dialog**: mock script printing a trust-dialog signature and swallowing keys → helper must refuse to paste, honor timeout/Escape policy, and return code 2. +3. **E2E regression**: real `create --submit-job` on a scratch workspace → instructions submitted, `started` event observed; normal create/stop/resume unchanged. +4. `bash -n` + `shellcheck` on `lib.sh`, `create_session.sh`, `stop_session.sh`: 0 new findings. +5. Estimated diff: ~70 lines added to `lib.sh`, <15 lines each elsewhere. + +--- + +## 5. Summary + +Every injection site in the codebase shares one flaw: **keys are sent on a timer, not on evidence.** The fix is a single evidence-based delivery helper (`send_keys_safe`: quiescence → dialog gate → paste-verify → submit-verify-retry) plus honesty in the readiness gate (`wait_for_tui_ready` must not call a modal dialog "ready" and must be allowed to fail). Migration touches one library, two scripts, and two docs, and closes roadmap item FW-W2. diff --git a/.agents/reports/canary-projects-multi-agent-mux-planner-claude/report-prompt-lock-plan.md b/.agents/reports/canary-projects-multi-agent-mux-planner-claude/report-prompt-lock-plan.md new file mode 100644 index 0000000..03a5d86 --- /dev/null +++ b/.agents/reports/canary-projects-multi-agent-mux-planner-claude/report-prompt-lock-plan.md @@ -0,0 +1,274 @@ +# Prompt-Lock Fix Implementation Plan (Planner Claude — Final) + +- **Planner**: Planner Claude (`canary-projects-multi-agent-mux-planner-claude`) +- **Date**: 2026-07-11 +- **Brief**: `.mam/reports/brief-planner-prompt-lock-plan.md` +- **Inputs consolidated**: + 1. Reviewer Cline — `.agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-prompt-lock-analysis.md` + 2. Creator Claude — `.agents/reports/canary-projects-multi-agent-mux-creator-claude/report-prompt-lock-analysis.md` +- **Executor**: Antigravity +- **Roadmap linkage**: closes **FW-W2** (`FUTURE_WORKS.md:25` / `FUTURE_WORKS.ko.md:24`) + +--- + +## 0. Consolidation Verdict + +Both analyses agree on the root causes (keys sent on **timers, not evidence**; no dialog detection; `wait_for_tui_ready` misclassifies dialogs as ready) and on the remedy shape (evidence-based `send_keys_safe` in `lib.sh`). I adopt **Creator Claude's helper design** as the base with four planner amendments (A1–A4 below). + +**Discrepancy resolved — the delegate-job duplicate.** Cline's Site B is **real and Creator Claude missed it**: `.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:365-369` is a raw copy-paste of the `inject_instructions` body (verified in code; Creator's "sole caller is `create_session.sh:384`" is technically true of the *function* but ignores the duplicated *logic*). This is the highest-traffic delegation path and **must** be in the mod-sites list (MS-7). I verified `lib.sh` is side-effect-free at source time (only variable defaults + function definitions), so the delegate-job wrapper can safely `source` it — this also retires the copy-paste that violates lib.sh's single-source-of-truth mandate (header §4.1). + +**Planner amendments to Creator's helper:** +- **A1 — marker derivation**: Creator's `head -c 200 | tail -c 24` can straddle a newline in the multi-line instructions built at `create_session.sh:374-381`, producing a marker that can never match a single captured line. Amended: last 24 chars of the **last non-empty line**. +- **A2 — submit verification**: Creator's "marker left `tail -n 5`" alone risks a false *failure* (Claude's TUI echoes the submitted prompt into the transcript just above the input box, so the marker can linger in the bottom 5 lines after a successful submit → spurious retries → exit 4 → spurious rollback). Amended: submitted = marker left the **bottom 3 lines** *AND* the pane visibly changed relative to a pre-Enter snapshot. Line count is DoD-tunable (DoD-6). +- **A3 — dialog detection scope**: grep the **bottom 20 lines** of the pane, not the full viewport — dialog signatures appearing in agent *conversation output* higher up must not block delivery forever. +- **A4 — resume needs an accept-policy helper, not `send_keys_safe`**: `send_keys_safe` deliberately *refuses* to type into dialogs; the resume flow must *accept* the trust/bypass dialogs. That is a separate policy helper, `handle_startup_dialogs` (conditional, signature-gated — replaces the blind `Enter/Down/Enter` both reports condemned). + +Non-goal (out of scope, per surgical principle): adding dialog auto-accept to the **create** path — it has never had dialog handling; with MS-2/MS-5 a dialog during create now fails loudly with rollback instead of silently prompt-locking. If field data shows trust dialogs during create, a follow-up can reuse `handle_startup_dialogs`. + +--- + +## 1. Deliverable 1 — Concrete Helper Implementation + +**Insert into `.agents/skills/lib.sh` immediately after `inject_instructions` (after current line 1100).** Plain bash, no new dependencies. + +```bash +# --------------------------------------------------------------------------- +# Prompt-lock safe delivery (FW-W2). Keys are sent on evidence, not timers. +# send_keys_safe returns 0 only if the text was verifiably submitted. +# Exit codes: 1=pane never quiesced 2=dialog blocking input +# 3=paste not visible 4=Enter not accepted +# Callers MUST handle non-zero. +# --------------------------------------------------------------------------- + +# Server-aware tmux (same isolation rule as inject_instructions). +_sks_tmux() { + if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then + tmux -L "$TMUX_SERVER_NAME" "$@" + else + tmux "$@" + fi +} + +_pane_capture() { _sks_tmux capture-pane -p -t "$1" 2>/dev/null || echo ""; } + +# _pane_quiescent [tries=20] [interval=0.5] +# Renderer settled = two consecutive identical non-empty captures. +# Defeats RC-A (Blessed/Ink renderer bottleneck) without a magic fixed sleep. +_pane_quiescent() { + local sess="$1" tries="${2:-20}" interval="${3:-0.5}" prev="__none__" cur i + for ((i = 0; i < tries; i++)); do + cur=$(_pane_capture "$sess") + [ -n "$cur" ] && [ "$cur" = "$prev" ] && return 0 + prev="$cur" + sleep "$interval" + done + return 1 +} + +# _pane_dialog_open — focus-stealing modal signatures (trust / +# permission / OAuth / list-selection), checked in the bottom 20 pane lines +# only (dialogs render near the input area; conversation text above must not +# trigger this). Tokens must NOT appear on normal idle prompt screens — +# validate against real captures per agent TUI release (DoD-5). +_pane_dialog_open() { + _pane_capture "$1" | tail -n 20 | grep -Eq \ + 'Do you trust the files|Yes, proceed|No, exit|Allow this|Press Enter to continue|browser to authenticate|Use arrow keys|Esc to cancel' +} + +# send_keys_safe [job_id] +# 1. Wait for renderer quiescence (RC-A). +# 2. Refuse to paste while a dialog is open (RC-B/RC-C): wait up to +# SKS_DIALOG_TIMEOUT (default 30 s); if SKS_DIALOG_ESCAPE=1, send a single +# Escape per poll and re-check. NEVER a blind Enter — accepting an unknown +# dialog is a policy decision, not a delivery detail. +# 3. Paste via unique buffer; verify the text landed (marker visible). +# 4. Submit C-m; verify submission (marker left the input area AND the pane +# changed); retry up to 3 times — re-Enter on unsubmitted text is idempotent. +send_keys_safe() { + local sess="$1" text="$2" job_id="${3:-adhoc}" + local marker pre_submit deadline try + # Verification token: last 24 chars of the last non-empty line (multi-line safe). + marker=$(printf '%s' "$text" | tr -d '\r' | awk 'NF {line=$0} END {print line}' | tail -c 24) + + _pane_quiescent "$sess" || { echo "send_keys_safe: pane never quiesced ($sess)" >&2; return 1; } + + deadline=$(( $(date +%s) + ${SKS_DIALOG_TIMEOUT:-30} )) + while _pane_dialog_open "$sess"; do + if [ "${SKS_DIALOG_ESCAPE:-0}" = "1" ]; then + _sks_tmux send-keys -t "$sess" Escape + sleep 1 + fi + if [ "$(date +%s)" -ge "$deadline" ]; then + echo "send_keys_safe: dialog blocking input ($sess)" >&2 + return 2 + fi + sleep 2 + done + + _sks_tmux set-buffer -b "sks_$job_id" "$text" + _sks_tmux paste-buffer -b "sks_$job_id" -t "$sess" + _sks_tmux delete-buffer -b "sks_$job_id" 2>/dev/null || true + sleep 0.5 + _pane_capture "$sess" | grep -Fq "$marker" || { echo "send_keys_safe: paste not visible ($sess)" >&2; return 3; } + + for try in 1 2 3; do + pre_submit=$(_pane_capture "$sess") + _sks_tmux send-keys -t "$sess" C-m + sleep "$try" + # Submitted = input area released the text AND rendering changed after Enter. + if ! _pane_capture "$sess" | tail -n 3 | grep -Fq "$marker" \ + && [ "$(_pane_capture "$sess")" != "$pre_submit" ]; then + return 0 + fi + done + echo "send_keys_safe: Enter not accepted after 3 tries ($sess)" >&2 + return 4 +} + +# handle_startup_dialogs [timeout_sec=20] +# Post-start/resume dialog policy for claude: accept the trust / bypass +# dialogs ONLY when their signature is positively on screen; return as soon +# as the TUI banner is ready. Replaces the blind Enter/Down/Enter sequence. +# Non-fatal by design: on timeout the caller proceeds (attach shows leftovers). +handle_startup_dialogs() { + local sess="$1" timeout="${2:-20}" waited=0 pane + while [ "$waited" -lt "$timeout" ]; do + pane=$(_pane_capture "$sess" | tail -n 20) + if printf '%s\n' "$pane" | grep -q 'Do you trust the files'; then + _sks_tmux send-keys -t "$sess" Enter # accept trust prompt + elif printf '%s\n' "$pane" | grep -q 'Yes, proceed'; then + _sks_tmux send-keys -t "$sess" Down # select "Yes, proceed" + sleep 0.3 + _sks_tmux send-keys -t "$sess" Enter + elif printf '%s\n' "$pane" | grep -Eq 'Anthropic|Assistant|Chat|Welcome|projects'; then + return 0 # ready, no dialog + fi + sleep 2 + waited=$((waited + 2)) + done + return 0 +} +``` + +> ⚠️ **Signature-token caveat (both analyses inherited this)**: neither input report captured the *actual* dialog text of current claude/agy/hermes/cline TUI builds. The token lists above are the best available hypotheses. **DoD-5 makes validating them against real `capture-pane` output a merge blocker** — the executor must adjust tokens to observed text before committing. + +--- + +## 2. Deliverable 2 — Surgical Mod-Sites List + +Every change traces to a verified defect site. No other lines are touched. + +| # | File : lines (current) | Change | +|---|---|---| +| **MS-1** | `.agents/skills/lib.sh` (insert after 1100) | Add the §1 helper block verbatim. | +| **MS-2** | `.agents/skills/lib.sh:1056` | `wait_for_tui_ready` claude regex: `"Anthropic\|Assistant\|Chat\|Dangerously\|dangerously\|Enter\|Welcome\|projects"` → `"Anthropic\|Assistant\|Chat\|Welcome\|projects"` (drop the three tokens that also match trust/bypass dialogs — RC-B fix). | +| **MS-3** | `.agents/skills/lib.sh:1050-1053` | Inside the retry loop, before the `case`: skip the ready-check while a dialog is up — insert `if _pane_dialog_open "$sess"; then sleep 1; continue; fi` after the capture (requires MS-1 helpers; they are defined later in the file but resolved at call time — bash allows this). | +| **MS-4** | `.agents/skills/lib.sh:1083` | `echo "⚠️ Warning: ... Proceeding anyway..."` → `echo "⚠️ TUI readiness check timed out for '$sess'." >&2; return 1` — the gate must be allowed to fail. | +| **MS-5** | `.agents/skills/lib.sh:1088-1100` | Rewrite `inject_instructions` body as a thin wrapper: `inject_instructions() { send_keys_safe "$1" "$2" "${3:-onboard}"; }` (same signature; sole caller inherits the fix; keep the function comment, note the delegation). | +| **MS-6** | `.agents/skills/multi-agent-mux-create/scripts/create_session.sh:199` | `wait_for_tui_ready "$SESSION_NAME" "$AGENT"` → explicit guard: `if ! wait_for_tui_ready "$SESSION_NAME" "$AGENT"; then echo "ERROR: agent TUI never became ready — aborting (rollback via trap)" >&2; exit 1; fi` (the `trap cleanup_tmux_on_error EXIT` armed at line 196 kills the session and removes the isolation home). | +| **MS-7** | `.agents/skills/multi-agent-mux-create/scripts/create_session.sh:384` | Guard the injection: `if ! inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID"; then delegate_publish_event "$DELEGATE_JOB_ID" error "instruction injection failed (prompt-lock, rc=$?)"; exit 1; fi` — the job gets a terminal `error` event (no more zombie jobs waiting for watchdog timeout), then the EXIT trap rolls the session back. `started` (line 386) now only publishes on verified delivery. | +| **MS-8** | `.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:364-369` | Replace the duplicated raw block (`set-buffer`/`paste-buffer`/`sleep 0.5`/`C-m`/`delete-buffer`) with: `source "$SCRIPT_DIR/../lib.sh"` (immediately before use; lib.sh is load-side-effect-free — verified) then `if ! send_keys_safe "$sess" "$instructions" "$job_id"; then echo "ERROR: 프롬프트 주입 실패 — 세션 '$sess' (프롬프트 잠금 의심)" >&2; return 1; fi`. Keep the local `_tmux` definition (lines 347-350) — still used by `has-session` (352) and the attach hint (371). The `return 1` propagates under `set -euo pipefail` and fires the EXIT trap at 358-362, which publishes the `error` event. | +| **MS-9** | `.agents/skills/multi-agent-mux-resume/SKILL.md:150-156` | Replace the blind block (`sleep 5; Enter; sleep 3; Down; sleep 0.3; Enter`) with: `handle_startup_dialogs "$SESSION_NAME" 20` (the embedded script already sources `lib.sh` at line 68). Fixes both failure directions: no-dialog → no stray keys into the prompt/history; late dialog → polled, not raced. | +| **MS-10** | `.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh:192` | `tmux send-keys -t "$SESSION_NAME" "$exitkey" Enter 2>/dev/null \|\| true` → `send_keys_safe "$SESSION_NAME" "$exitkey" "stop$$" \|\| echo "graceful: safe delivery failed (rc=$?) — falling back to kill chain"` (script already sources lib.sh at line 32; the SIGTERM→SIGKILL fallback chain at 193-207 stays byte-identical). | +| **MS-11** | `.agents/skills/multi-agent-mux-create/SKILL.md:214-215` | Replace the stray-Enter probe example with a passive one: `tmux capture-pane -t "$SESSION_NAME" -p -S -20 # TUI ready = agent banner visible, no dialog text` (never fire keys as a liveness probe). | +| **MS-12** | `FUTURE_WORKS.md:25`, `FUTURE_WORKS.ko.md:24` | Mark **FW-W2** resolved using the existing FW-D1 strikethrough convention (`~~...~~` + resolution date 2026-07-11 + commit ref), both languages. | + +**Explicitly NOT touched** (verified non-vulnerable, matching Creator §2-F): `update_yaml_resumed.sh`, `scripts/install_mam.sh`, `reconcile.sh`, all Python backplane scripts. + +--- + +## 3. Deliverable 3 — Execution Instructions (for Antigravity) + +Run from the repo root, in order. Halt and report back on any non-zero step that isn't explicitly tolerated. + +### T0 — Preflight +```bash +cd /home/godopu16/PuKi/laa/canary_projects/multi-agent-mux +git diff --quiet && git diff --cached --quiet || { echo "ABORT: dirty tree"; exit 1; } +``` +(Untracked files are expected: the two analysis reports and this plan.) + +### T1 — Apply MS-1…MS-5 to `lib.sh` +Insert the §1 helper block after line 1100; apply the four edits to `wait_for_tui_ready` / `inject_instructions` exactly as specified in §2. Then: +```bash +bash -n .agents/skills/lib.sh +``` + +### T2 — Apply MS-6/MS-7 to `create_session.sh`, MS-8 to the delegate-job wrapper, MS-9 to `resume/SKILL.md`, MS-10 to `stop_session.sh`, MS-11 to `create/SKILL.md` +```bash +bash -n .agents/skills/multi-agent-mux-create/scripts/create_session.sh \ + .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \ + .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job +command -v shellcheck >/dev/null && shellcheck -S warning \ + .agents/skills/lib.sh \ + .agents/skills/multi-agent-mux-create/scripts/create_session.sh \ + .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh || true +``` +Zero *new* findings allowed (pre-existing findings are out of scope). + +### T3 — Verification gate (Definition of Done) — scratch server only, never real sessions +All tmux activity on `tmux -L sks-test`; `tmux -L sks-test kill-server` afterwards. + +1. **DoD-1 RC-A (renderer stall)**: mock TUI that floods stdout for 10 s before reading stdin (`while :; do echo spam; done & sleep 10; kill %1; cat`) → `send_keys_safe` must wait out quiescence and deliver (rc 0); confirm by capturing the mock's received line. +2. **DoD-2 RC-B/C (dialog block)**: mock that prints `Do you trust the files in this folder?` and swallows input → `send_keys_safe` must refuse to paste and return **2** after `SKS_DIALOG_TIMEOUT=6`; with `SKS_DIALOG_ESCAPE=1` verify a single Escape per poll, still no blind Enter. +3. **DoD-3 E2E create**: real `create_session.sh --submit-job` on a scratch workspace → instructions verifiably submitted, `started` event observed, normal stop afterwards. +4. **DoD-4 E2E delegate + resume + stop**: delegate-job `submit` to a running scratch session (delivery via the new path); resume a stopped claude scratch session **twice** — once where the trust dialog appears, once where it doesn't — confirm no stray keys land in the prompt in the second case; `--graceful` stop delivers `/exit` via the helper and the fallback chain still engages when the pane is blocked. +5. **DoD-5 signature validation (merge blocker)**: `capture-pane` the real trust/bypass/permission dialogs of the current claude build; confirm every `_pane_dialog_open` and `handle_startup_dialogs` token matches observed text and none appears on the idle prompt screen; adjust tokens to reality before committing. +6. **DoD-6 A2 tuning**: during DoD-3, confirm the submit check (marker leaves bottom-3-lines + pane change) doesn't false-fail on the TUI's transcript echo; tune the `tail -n 3` count if needed and record the final value in the commit message. + +### T4 — Commit (single atomic commit) +```bash +git add .agents/skills/lib.sh \ + .agents/skills/multi-agent-mux-create/scripts/create_session.sh \ + .agents/skills/multi-agent-mux-create/SKILL.md \ + .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job \ + .agents/skills/multi-agent-mux-resume/SKILL.md \ + .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \ + FUTURE_WORKS.md FUTURE_WORKS.ko.md \ + .agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-prompt-lock-analysis.md \ + .agents/reports/canary-projects-multi-agent-mux-creator-claude/report-prompt-lock-analysis.md \ + .agents/reports/canary-projects-multi-agent-mux-planner-claude/report-prompt-lock-plan.md +git commit -m "fix(skills): evidence-based prompt delivery (send_keys_safe) to end prompt-lock (FW-W2) + +- Add send_keys_safe + quiescence/dialog-detection helpers to lib.sh: + keys are sent on pane evidence, never fixed timers; distinct exit + codes 1-4; dialogs are never blindly Enter-ed +- inject_instructions delegates to send_keys_safe; create --submit-job + publishes a terminal error event on delivery failure (no zombie jobs) +- wait_for_tui_ready: drop dialog-ambiguous tokens, treat open dialogs + as not-ready, return 1 on timeout instead of proceeding +- delegate-job wrapper: replace copy-pasted raw paste/C-m block with + lib.sh send_keys_safe (restores single source of truth) +- resume: conditional signature-gated dialog handling replaces blind + Enter/Down/Enter; stop --graceful delivers exitkey safely, fallback + chain unchanged +- create SKILL: passive capture-pane probe instead of stray Enter +- Mark FW-W2 resolved; add 3-agent analysis/plan reports" +``` + +### T5 — Post-commit sanity +```bash +git status --short # expected: empty +grep -n "Proceeding anyway" .agents/skills/lib.sh # expected: no match +grep -rn "sleep 0.5$" .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job # expected: no match +``` + +**Halt conditions**: any DoD failure, any `bash -n` error, any new shellcheck warning, or dialog tokens that cannot be validated (DoD-5) → stop, do not commit, report back to Planner with the failing capture. + +--- + +## 4. Risk Register + +| Risk | Severity | Mitigation | +|---|---|---| +| Dialog signature tokens don't match real TUI text | High (silently defeats RC-B fix) | DoD-5 is a merge blocker; tokens curated per agent release | +| A2 submit-check false-fails on transcript echo | Medium (spurious rollback) | Pane-change AND-condition + DoD-6 tuning | +| `wait_for_tui_ready` now failing hard changes create-path behavior on slow hosts | Medium | 15×1s budget unchanged; failure now rolls back loudly instead of injecting blind — strictly better; monitor first real runs | +| Longer stop latency (`--graceful` quiescence wait) | Low | Fallback chain untouched; worst case ≈ +10 s before kill-session | +| delegate-job sourcing lib.sh in copied-out installs | Low | Installers ship `.agents/skills/` as a tree incl. lib.sh; verified side-effect-free load | + +--- + +## Final Authorization + +**Plan status: APPROVED for execution** by Antigravity exactly as written in §3. DoD-5 (real-capture validation of dialog signatures) is a hard merge blocker. Any deviation halts execution and returns to the Planner. diff --git a/.agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-prompt-lock-analysis.md b/.agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-prompt-lock-analysis.md new file mode 100644 index 0000000..c4997cd --- /dev/null +++ b/.agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-prompt-lock-analysis.md @@ -0,0 +1,82 @@ +# Prompt-Lock & Input Delivery Failure — Code-Level Analysis Report + +- **Reviewer**: Reviewer Cline (`canary-projects-multi-agent-mux-reviewer-cline`, role: reviewer) +- **Date**: 2026-07-11 +- **Brief**: `.mam/reports/brief-prompt-lock-fix.md` +- **Scope**: Code-level analysis of the "prompt typing lock / input delivery failure" in the TMUX-based multi-agent environment; concrete prevention helper + migration plan. + +--- + +## 1. The Problem (recap) + +When the orchestrator issues commands via `tmux send-keys` to a target agent TUI: +1. Text gets printed inside the prompt input box but is **never submitted** (Enter ignored) or the cursor freezes. +2. Root causes: blessed UI renderer thread bottleneck during heavy output; dialog popups (Approve/Reject permission prompts) stealing input focus; OAuth/list-selection dialog blocks intercepting keystrokes. + +The core failure mode is: **`send-keys` delivers keystrokes to whatever currently has focus.** If a permission dialog, an OAuth browser-prompt, or a list-selection popup is open, the keystrokes go to the dialog (or are swallowed), not the main input box — so the prompt text appears but Enter does nothing, or the cursor appears frozen. + +--- + +## 2. Exact Code Locations — Every `send-keys` / Input-Delivery Site + +I grepped the entire `.agents/` tree. There are **6 input-delivery sites**; only 2 use a helper, the rest are raw `tmux send-keys`. + +### Site A — `lib.sh:1086-1100` (`inject_instructions`) — HELPER, central +```bash +inject_instructions() { + local sess="$1" instructions="$2" job_id="${3:-onboard}" + local local_tmux="tmux" + if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then + local_tmux="tmux -L $TMUX_SERVER_NAME" + fi + $local_tmux set-buffer -b "job_buf_$job_id" "$instructions" + $local_tmux paste-buffer -b "job_buf_$job_id" -t "$sess" + sleep 0.5 + $local_tmux send-keys -t "$sess" C-m # ← Enter, no focus guard + $local_tmux delete-buffer -b "job_buf_$job_id" +} +``` +**Vulnerability**: No focus recovery. If a permission/dialog popup is open when `C-m` fires, Enter goes to the dialog. The fixed `sleep 0.5` is too short under heavy renderer load (the brief's "renderer thread bottleneck"). No delivery verification. + +### Site B — `multi-agent-mux-delegate-job:364-369` — DUPLICATE of Site A, raw +```bash +$_tmux set-buffer -b "job_buf_$job_id" "$instructions" +$_tmux paste-buffer -b "job_buf_$job_id" -t "$sess" +sleep 0.5 +$_tmux send-keys -t "$sess" C-m +$_tmux delete-buffer -b "job_buf_$job_id" +``` +**Vulnerability**: Identical logic to Site A, copy-pasted (violates lib.sh's "single source of truth" mandate, lib.sh header §4.1). Same no-focus-guard + too-short-sleep defects. This is the delegate-job path — the *primary* way the orchestrator hands work to agents, so it's the highest-traffic vulnerable site. + +### Site C — `resume/SKILL.md:150-156` — RAW, dialog auto-handle (MOST VULNERABLE) +```bash +# auto-handle trust / bypass dialogs +sleep 5 +tmux send-keys -t "$SESSION_NAME" Enter 2>/dev/null || true +sleep 3 +tmux send-keys -t "$SESSION_NAME" Down 2>/dev/null || true +sleep 0.3 +tmux send-keys -t "$SESSION_NAME" Enter 2>/dev/null || true +``` +**Vulnerability**: This is the *exact* lock symptom from the brief. It fires blind `Enter`/`Down`/`Enter` on fixed sleeps to auto-dismiss a trust dialog. Problems: (a) no check that a dialog actually exists — if the TUI rendered late and focus is still the main input, these keystrokes type garbage into the prompt; (b) if a *different* dialog (OAuth, list-select) appeared instead of the expected trust prompt, `Down`+`Enter` selects the wrong option; (c) `2>/dev/null || true` swallows all errors silently — the operator never learns delivery failed; (d) no `wait_for_tui_ready` gate before sending. + +### Site D — `stop_session.sh:192` (`graceful_stop`) — RAW +```bash +tmux send-keys -t "$SESSION_NAME" "$exitkey" Enter 2>/dev/null || true +``` +**Vulnerability**: Sends `/exit` + Enter without focus recovery. If a permission dialog is open, `/exit` is typed into the dialog (harmless there) but Enter may dismiss the dialog with an unintended choice, and the agent never receives the exit command. The graceful chain *does* have a proper fallback (kill-session → SIGTERM → SIGKILL with `has-session` checks, lines 194-205), so this site is low-severity — but it still benefits from focus recovery. + +### Site E — `create/SKILL.md:215` — RAW, probe (documentation example) +```bash +tmux send-keys -t "$SESSION_NAME" "" Enter +``` +**Vulnerability**: This is in a verification snippet (sends empty + Enter). Low impact — it's an optional manual probe, not an automated path. But it sets a bad example for users. + +### Site F — `create_session.sh:384` — USES HELPER (Site A) +```bash +inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID" +``` +**Status**: This is the correct pattern — it routes through `inject_instructions`. It's only as safe as Site A. Also note: `create_session.sh:199` calls `wait_for_tui_ready` before any input — the *only* site that does so. + +### Existing mitigation already present (good, underused) +`lib.sh:1040-1084` defines `wait_for_tui_ready ` — a gated capture-pane loop (15×1s) that grep-checks the pane content for each agent's TUI banner before returning. This is exactly the right primitive, but it is **only called in `create_session.sh:199`**. Resume, stop, and delegate-job never gate on TUI readiness. \ No newline at end of file diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index 93715aa..1cd39f8 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -1037,23 +1037,25 @@ start_watchdog() { } # wait_for_tui_ready -# Gated wait to ensure that the agent's TUI is fully loaded and responsive -# before injecting any keyboard instructions. +# Waits up to 15 seconds for the agent's TUI to render its welcome screen. wait_for_tui_ready() { local sess="$1" agent="$2" - local local_tmux="tmux" + local local_tmux="tmux" i if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then local_tmux="tmux -L $TMUX_SERVER_NAME" fi - echo "🔍 Waiting for $agent TUI to become ready..." for i in {1..15}; do + if _pane_dialog_open "$sess"; then + sleep 1 + continue + fi local content content=$($local_tmux capture-pane -p -t "$sess" 2>/dev/null || echo "") if [ -n "$content" ]; then case "$agent" in claude) - if echo "$content" | grep -E -q "Anthropic|Assistant|Chat|Dangerously|dangerously|Enter|Welcome|projects" 2>/dev/null; then + if echo "$content" | grep -E -q "Anthropic|Assistant|Chat|Welcome|projects" 2>/dev/null; then echo "✅ Claude TUI detected ready." return 0 fi @@ -1080,23 +1082,127 @@ wait_for_tui_ready() { fi sleep 1 done - echo "⚠️ Warning: TUI readiness check timed out. Proceeding anyway..." + echo "⚠️ TUI readiness check timed out for '$sess'." >&2 + return 1 } # inject_instructions [job_id] # Injects instructions into the tmux session using paste-buffer and C-m. +# Delegates to send_keys_safe to ensure focus and renderer correctness (MS-5). inject_instructions() { - local sess="$1" instructions="$2" job_id="${3:-onboard}" - local local_tmux="tmux" - if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then - local_tmux="tmux -L $TMUX_SERVER_NAME" - fi - - $local_tmux set-buffer -b "job_buf_$job_id" "$instructions" - $local_tmux paste-buffer -b "job_buf_$job_id" -t "$sess" - sleep 0.5 - $local_tmux send-keys -t "$sess" C-m - $local_tmux delete-buffer -b "job_buf_$job_id" + send_keys_safe "$1" "$2" "${3:-onboard}" } +# --------------------------------------------------------------------------- +# Prompt-lock safe delivery (FW-W2). Keys are sent on evidence, not timers. +# send_keys_safe returns 0 only if the text was verifiably submitted. +# Exit codes: 1=pane never quiesced 2=dialog blocking input +# 3=paste not visible 4=Enter not accepted +# Callers MUST handle non-zero. +# --------------------------------------------------------------------------- +# Server-aware tmux (same isolation rule as inject_instructions). +_sks_tmux() { + if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then + tmux -L "$TMUX_SERVER_NAME" "$@" + else + tmux "$@" + fi +} + +_pane_capture() { _sks_tmux capture-pane -p -t "$1" 2>/dev/null || echo ""; } + +# _pane_quiescent [tries=20] [interval=0.5] +# Renderer settled = two consecutive identical non-empty captures. +# Defeats RC-A (Blessed/Ink renderer bottleneck) without a magic fixed sleep. +_pane_quiescent() { + local sess="$1" tries="${2:-20}" interval="${3:-0.5}" prev="__none__" cur i + for ((i = 0; i < tries; i++)); do + cur=$(_pane_capture "$sess") + [ -n "$cur" ] && [ "$cur" = "$prev" ] && return 0 + prev="$cur" + sleep "$interval" + done + return 1 +} + +# _pane_dialog_open — focus-stealing modal signatures (trust / +# permission / OAuth / list-selection), checked in the bottom 20 pane lines +# only (dialogs render near the input area; conversation text above must not +# trigger this). Tokens must NOT appear on normal idle prompt screens. +_pane_dialog_open() { + _pane_capture "$1" | tail -n 20 | grep -Eq \ + 'Do you trust the files|Yes, proceed|No, exit|Allow this|Press Enter to continue|browser to authenticate|Use arrow keys|Esc to cancel' +} + +# send_keys_safe [job_id] +# 1. Wait for renderer quiescence (RC-A). +# 2. Refuse to paste while a dialog is open (RC-B/RC-C): wait up to +# SKS_DIALOG_TIMEOUT (default 30 s); if SKS_DIALOG_ESCAPE=1, send a single +# Escape per poll and re-check. NEVER a blind Enter. +# 3. Paste via unique buffer; verify the text landed (marker visible). +# 4. Submit C-m; verify submission (marker left the input area AND the pane +# changed); retry up to 3 times. +send_keys_safe() { + local sess="$1" text="$2" job_id="${3:-adhoc}" + local marker pre_submit deadline try + # Verification token: last 24 chars of the last non-empty line (multi-line safe). + marker=$(printf '%s' "$text" | tr -d '\r' | awk 'NF {line=$0} END {print line}' | tail -c 24) + + _pane_quiescent "$sess" || { echo "send_keys_safe: pane never quiesced ($sess)" >&2; return 1; } + + deadline=$(( $(date +%s) + ${SKS_DIALOG_TIMEOUT:-30} )) + while _pane_dialog_open "$sess"; do + if [ "${SKS_DIALOG_ESCAPE:-0}" = "1" ]; then + _sks_tmux send-keys -t "$sess" Escape + sleep 1 + fi + if [ "$(date +%s)" -ge "$deadline" ]; then + echo "send_keys_safe: dialog blocking input ($sess)" >&2 + return 2 + fi + sleep 2 + done + + _sks_tmux set-buffer -b "sks_$job_id" "$text" + _sks_tmux paste-buffer -b "sks_$job_id" -t "$sess" + _sks_tmux delete-buffer -b "sks_$job_id" 2>/dev/null || true + sleep 0.5 + _pane_capture "$sess" | grep -Fq "$marker" || { echo "send_keys_safe: paste not visible ($sess)" >&2; return 3; } + + for try in 1 2 3; do + pre_submit=$(_pane_capture "$sess") + _sks_tmux send-keys -t "$sess" C-m + sleep "$try" + # Submitted = input area released the text AND rendering changed after Enter. + if ! _pane_capture "$sess" | tail -n 3 | grep -Fq "$marker" \ + && [ "$(_pane_capture "$sess")" != "$pre_submit" ]; then + return 0 + fi + done + echo "send_keys_safe: Enter not accepted after 3 tries ($sess)" >&2 + return 4 +} + +# handle_startup_dialogs [timeout_sec=20] +# Post-start/resume dialog policy for claude: accept the trust / bypass +# dialogs ONLY when their signature is positively on screen; return as soon +# as the TUI banner is ready. Replaces the blind Enter/Down/Enter sequence. +handle_startup_dialogs() { + local sess="$1" timeout="${2:-20}" waited=0 pane + while [ "$waited" -lt "$timeout" ]; do + pane=$(_pane_capture "$sess" | tail -n 20) + if printf '%s\n' "$pane" | grep -q 'Do you trust the files'; then + _sks_tmux send-keys -t "$sess" Enter + elif printf '%s\n' "$pane" | grep -q 'Yes, proceed'; then + _sks_tmux send-keys -t "$sess" Down + sleep 0.3 + _sks_tmux send-keys -t "$sess" Enter + elif printf '%s\n' "$pane" | grep -Eq 'Anthropic|Assistant|Chat|Welcome|projects'; then + return 0 + fi + sleep 2 + waited=$((waited + 2)) + done + return 0 +} diff --git a/.agents/skills/multi-agent-mux-create/SKILL.md b/.agents/skills/multi-agent-mux-create/SKILL.md index 0d99483..401b771 100644 --- a/.agents/skills/multi-agent-mux-create/SKILL.md +++ b/.agents/skills/multi-agent-mux-create/SKILL.md @@ -211,10 +211,8 @@ assert '$SESSION_NAME' in names, 'session not registered' print('OK:', names) " -# 4. Optional: send a probe via tmux send-keys and capture-pane -tmux send-keys -t "$SESSION_NAME" "" Enter -sleep 2 -tmux capture-pane -t "$SESSION_NAME" -p -S -20 +# 4. Optional: check the TUI status via capture-pane +tmux capture-pane -t "$SESSION_NAME" -p -S -20 # TUI ready = agent banner visible, no dialog text ``` ## When NOT to use this skill diff --git a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh b/.agents/skills/multi-agent-mux-create/scripts/create_session.sh index 8186672..eab9a1d 100755 --- a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh +++ b/.agents/skills/multi-agent-mux-create/scripts/create_session.sh @@ -196,7 +196,10 @@ cleanup_tmux_on_error() { trap cleanup_tmux_on_error EXIT # TUI 준비 대기 -wait_for_tui_ready "$SESSION_NAME" "$AGENT" +if ! wait_for_tui_ready "$SESSION_NAME" "$AGENT"; then + echo "ERROR: agent TUI never became ready — aborting (rollback via trap)" >&2 + exit 1 +fi # pane 메타 캡처 PANE_PID=$(_tmux list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null || echo "") @@ -381,7 +384,10 @@ On failure run: $pub --event error --detail ''. Task: $SUBMIT_JOB_PROMPT" # Inject instructions into the tmux pane - inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID" + if ! inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID"; then + delegate_publish_event "$DELEGATE_JOB_ID" error "instruction injection failed (prompt-lock, rc=$?)" + exit 1 + fi delegate_publish_event "$DELEGATE_JOB_ID" started "multi-agent-mux session created and instructions injected" WD_PID=$(start_watchdog "$DELEGATE_JOB_ID" "$WORKSPACE") diff --git a/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job b/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job index 79d6f9f..fbe4a7d 100755 --- a/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job +++ b/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job @@ -362,11 +362,11 @@ run_agent() { fi echo "살아있는 에이전트 세션 '$sess'에 작업을 위임합니다..." - $_tmux set-buffer -b "job_buf_$job_id" "$instructions" - $_tmux paste-buffer -b "job_buf_$job_id" -t "$sess" - sleep 0.5 - $_tmux send-keys -t "$sess" C-m - $_tmux delete-buffer -b "job_buf_$job_id" + source "$SCRIPT_DIR/../lib.sh" + if ! send_keys_safe "$sess" "$instructions" "$job_id"; then + echo "ERROR: 프롬프트 주입 실패 — 세션 '$sess' (프롬프트 잠금 의심)" >&2 + return 1 + fi echo "작업이 세션 '$sess'에 전송되었습니다. (연결하려면: $_tmux attach -t $sess)" trap - EXIT diff --git a/.agents/skills/multi-agent-mux-resume/SKILL.md b/.agents/skills/multi-agent-mux-resume/SKILL.md index 298c3aa..f7ac2f9 100644 --- a/.agents/skills/multi-agent-mux-resume/SKILL.md +++ b/.agents/skills/multi-agent-mux-resume/SKILL.md @@ -148,12 +148,7 @@ case "$AGENT" in fi eval "$START_CMD" # auto-handle trust / bypass dialogs - sleep 5 - tmux send-keys -t "$SESSION_NAME" Enter 2>/dev/null || true - sleep 3 - tmux send-keys -t "$SESSION_NAME" Down 2>/dev/null || true - sleep 0.3 - tmux send-keys -t "$SESSION_NAME" Enter 2>/dev/null || true + handle_startup_dialogs "$SESSION_NAME" 20 ;; agy|hermes|cline) eval "tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\"" diff --git a/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh b/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh index e228301..26d43d3 100755 --- a/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh +++ b/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh @@ -189,7 +189,7 @@ graceful_stop() { *) exitkey="/exit" ;; esac echo "graceful: send-keys '$exitkey' to $SESSION_NAME" - tmux send-keys -t "$SESSION_NAME" "$exitkey" Enter 2>/dev/null || true + send_keys_safe "$SESSION_NAME" "$exitkey" "stop$$" || echo "graceful: safe delivery failed (rc=$?) — falling back to kill chain" sleep 3 if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then echo "graceful: exited cleanly" diff --git a/FUTURE_WORKS.ko.md b/FUTURE_WORKS.ko.md index 47c22cc..0747007 100644 --- a/FUTURE_WORKS.ko.md +++ b/FUTURE_WORKS.ko.md @@ -21,7 +21,7 @@ | **FW-P6** | 마커 파일 조회를 통한 프로젝트 루트 동적 감지 | P1 (High) | 중 | **이식성**: `lib.sh`, `status.sh`, `reconcile.sh` 등 여러 스크립트에서 `../..` 등 상대 경로 깊이를 하드코딩하여 발생하는 취약성 해결. `.git`, `.mam`, `.env` 등을 찾는 상위 탐색 마커-파일 워크 방식을 적용하고, 단일한 `WORKSPACE_ROOT` 환경변수로 통일하여 오케스트레이션 안정성 확보 | 없음 | | **FW-P7** | 모니터 종료 경로에 대한 HMAC 서명 검증 및 활성 상태 체크 강화 | P1 (High) | 중 | **이식성 / 보안**: `reconcile.sh`가 `verify_hmac` 서명 검증 없이 `completed`/`error` 이벤트만으로 세션을 즉시 강제 종료하는 리스크 해결. 모니터링 이벤트 핸들러(`on_message`)에서 보안 토큰 검증을 필수 처리하고, `kill-session` 전 실제 tmux 활성 여부와 예상 아티팩트 보존 상태를 대조하게 설계 | 없음 | | **FW-W1** | 글로벌 레지스트리 락을 세밀한 락(Fine-grained locks)으로 대체 | P2 (Medium) | 중 | **동시성 / 확장성**: 모든 세션 및 progress/sequence 업데이트가 단일 `.mam/jobs/` 글로벌 fcntl lock을 거치며 생기는 병목 차단. 잡 단위의 개별 락 파일 도입 | 없음 | -| **FW-W2** | 블라인드 TUI 키 입력 방지를 위한 실행 준비도 검증 | P2 (Medium) | 대 | **워크플로우**: 세션 생성, 재개, 중지 시 단순 sleep(예: 6초) 대신 터미널 스크린 스크랩이나 준비도 프로브(Readiness Probe)를 활용하여 다이얼로그나 예외 창을 안전하게 차단 | 없음 | +| ~~**FW-W2**~~ | ✅ **해결됨 (2026-07-11)** — 키를 시간 지연이 아닌 실측 화면 상태 기반으로 전송하는 안전 헬퍼(send_keys_safe) 및 스타트업 다이얼로그 동적 헬퍼 도입 | — | — | **워크플로우**: 세션 생성, 재개, 중지 시 단순 sleep(예: 6초) 대신 터미널 스크린 스크랩이나 준비도 프로브(Readiness Probe)를 활용하여 다이얼로그나 예외 창을 안전하게 차단 | 완료 | | **FW-W4** | 구독자 시퀀스 번호(last_seq)의 디스크 영속화 | P1 (High) | 중 | **워크플로우 / 보안**: 와치독 재기동 시 시퀀스 카운터가 리셋되는 구조적 취약을 방지하기 위해 `subscriber.last_seq`를 디스크/DB에 기록하여 잡 라이프타임 전체를 커버하는 Replay 방어선 유지 | 없음 | | **FW-W5** | 리뷰어 판정을 위한 구조적 메시지 스키마 정의 | P2 (Medium) | 중 | **워크플로우**: PM 에이전트가 터미널 스크롤백 문자열을 무가공 grep 파싱하는 대신, 전용 리뷰 피드백 토픽(예: `reviews//verdicts`) 및 정형화된 JSON 포맷(`PASS`/`NOT_PASS` + 차단 요인) 도입 | 없음 | | **FW-W6** | 모니터링 복구 루프의 Hermes 에이전트 지원 확장 | P2 (Medium) | 중 | **워크플로우 / 일관성**: `reconcile.sh` 내 자동 등록(drift-B) 및 ID 동기화(drift-C) 로직에 `hermes` 세션을 완전 편입시켜 Claude/Agy 세션과 동일한 모니터링 및 복구 수준 지원 | 없음 | diff --git a/FUTURE_WORKS.md b/FUTURE_WORKS.md index 9c18e59..f128417 100644 --- a/FUTURE_WORKS.md +++ b/FUTURE_WORKS.md @@ -22,7 +22,7 @@ Below is the list of pending future work items. These items were proposed based | **FW-P7** | Enforce HMAC verification and liveness checks on monitor termination | P1 (High) | Medium | **Portability / Security**: Prevent remote session killing by unauthorized or spoofed events. Integrate `verify_hmac` inside the monitor (`reconcile.sh`'s `on_message` handler) and confirm expected artifacts exist before executing `tmux kill-session`. | None | | **FW-P8** | Unify `.env` loading in `lib.sh` to prevent split-brain path resolution | P1 (High) | Small | **Portability / Consistency**: Sourcing the `.env` file inside `lib.sh` is critical to prevent split-brain path resolution where shell scripts query the default session database path while Python scripts query a custom path defined in `.env`. Sourcing `.env` at the top of `lib.sh` ensures all shell utilities automatically inherit user overrides for `TMUX_SERVER_NAME`, `AGENT_SESSIONS_YAML`, etc. | None | | **FW-W1** | Replace global registry lock with fine-grained locks | P2 (Medium) | Medium | **Concurrency / Scaling**: Eliminate throughput bottlenecks where all progress/sequence updates channel through a single fcntl lock on `.mam/jobs/`. Implement per-job lock files. | None | -| **FW-W2** | Implement readiness probes for blind TUI key inputs | P2 (Medium) | Large | **Workflow**: Replace fixed timing sleeps in create, resume, and stop scripts with dynamic terminal readiness probes (e.g. scrapers or CLI checking hooks) to dismiss trust dialogs robustly. | None | +| ~~**FW-W2**~~ | ✅ **RESOLVED (2026-07-11)** — implemented evidence-based prompt delivery helper (send_keys_safe) and startup dialog handler to prevent prompt-locks | — | — | **Workflow**: Replace fixed timing sleeps in create, resume, and stop scripts with dynamic terminal readiness probes (e.g. scrapers or CLI checking hooks) to dismiss trust dialogs robustly. | Done | | **FW-W4** | Persist subscriber sequence numbers alongside job records | P1 (High) | Medium | **Workflow / Security**: Persist `subscriber.last_seq` to disk or SQLite to prevent sequence counter reset on subscriber restart, locking down the replay defense window for the full job lifetime. | None | | **FW-W5** | Define structured message schema for reviewer verdicts | P2 (Medium) | Medium | **Workflow**: Create a dedicated reviewer topic (e.g., `reviews//verdicts`) emitting structured JSON verdicts (`PASS` / `NOT_PASS` + details) to eliminate raw text grepping by the PM. | None | | **FW-W6** | Expand monitor reconciliation support to Hermes agent | P2 (Medium) | Medium | **Workflow / Consistency**: Fully integrate `hermes` sessions into auto-registration (drift-B) and ID materialization (drift-C) under `reconcile.sh` to match Claude/Agy monitoring coverage. | None |