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
This commit is contained in:
2026-07-11 09:24:34 +09:00
parent 7fc6c8c7b9
commit e613f4aedb
11 changed files with 673 additions and 37 deletions
@@ -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 (A1A4 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 <sess> [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 <sess> — 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 <sess> <text> [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 <sess> [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.