Files
Godopu e613f4aedb 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
2026-07-11 09:24:34 +09:00

20 KiB
Raw Permalink Blame History

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.

# ---------------------------------------------------------------------------
# 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 || truesend_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

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 -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 -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)

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

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.