refactor: update multi-agent rules to TMUX & refactor MAM skills scripts

- Update .agents/MULTI_AGENT_RULES.md & .ko.md to align with TMUX concepts
- Add .agents/INSTALL.md guide for MAM setup and workflows
- Clean up legacy markdown reports under .agents/reports/
- Refactor MAM scripts in .agents/skills/ (rename resolve_herdr_session to resolve_herdr_workspace)
- Improve workspace pane splitting/re-use logic in lib.sh
- Reduce paste safety sleep in send_keys_safe and support linking ~/.claude.json
This commit is contained in:
2026-07-22 06:57:32 +09:00
parent e4b1fb3329
commit 36a087af58
49 changed files with 258 additions and 4361 deletions
@@ -1,56 +0,0 @@
# 🔍 리뷰 리포트 — 세션 ID 격리 종합 설계/구현 계획서 검토
- **리뷰어**: Claude (Reviewer A — 상위 논리/설계 정합성)
- **검토 대상**: `session_isolation_discussion.md` (Planner 통합본, Rev.2 반영)
- **대조 기준**: 본인 작성 `implementation_plan.session_isolation.md` (Rev.2) + 2차 토론 합의
- **job_id**: 655fb695
- **날짜**: 2026-07-10
- **최종 판정**: **PASS** (경미한 비차단 권고 3건 동반)
---
## 1. 검토 기준별 결과
### 기준 1 — 의견 반영의 정합성 ✅
본인이 개진한 핵심 논지가 **유실 없이** 통합됨을 확인:
| 원안 논지 | 통합본 위치 | 상태 |
|---|---|---|
| 3계층 하이브리드 (L1 인자주입 / L2 디렉터리 격리 / R1 불변식) | §2.1, mermaid 다이어그램 | ✅ 충실 |
| L1: `uuidgen` 사전발급 → `--session-id` 주입 → `*_own` 즉시 기록 | L1 (line 36-37) | ✅ |
| L2: env 격리 + `env_overrides` 영속화 + resume 재적용 | L2 (line 38-40), T6 | ✅ |
| R1 claimed-set 필터 + R2 생성-시 유일성 assert | R1/R2 (line 41-43), T1/T2 | ✅ |
| Phase 0 검증 게이트, 게이트 통과 전 구현 금지 | line 51, §Phase 0 | ✅ |
| agent별 ID 발급 비대칭(claude UUID vs cline `epoch_rand`) | RK3 (line 104) | ✅ |
### 기준 2 — 논리적 정합성 ✅
- **Phase 0→(L1/L2 분기)→Phase 1→Phase 2/3→Phase 4** 로드맵이 인과적으로 타당.
- **Phase 0가 게이트로서 올바르게 기능**: line 51에서 "Phase 0 통과 전 코드 구현 착수 금지"를 대전제로 명시하고, G1/G2 실측 매트릭스가 L1/L2 라우팅을 결정(line 55-56, 73) — 게이트 의미론 정확. cline 미지원 리스크가 코드 착수 전에 해소되는 구조라 사이드이펙트 예방 설계가 성립.
- Phase 1(R1/R2)이 전략 무관 선행 가능하다는 원안 취지도 반영(line 58, "선행 가능").
### 기준 3 — 누락 확인 ✅
- **RC-2 청소 계약**: §2.1 RC-2(line 44-45) + T7(line 88) + RK4(line 105)로 3중 기입 — 누락 없음.
- **R1/R2 이중 안전장치**: line 41-43에 defense-in-depth로 명확히 기입.
- Phase별 DoD, Phase 4 회귀검증(V1~V3) 모두 존재.
---
## 2. 🟡 비차단 권고 (구현 계획 세련화용, PASS 조건 아님)
- **A-1. Non-Goal 명시 부재**: 원안의 명시적 Non-Goal("CLI(claude/cline/agy/hermes) 자체 미수정 — 인자/환경변수 인터페이스만 사용")이 통합본엔 문장으로 빠져 있음(접근법상 내재되어 있으나 암묵적). 오해 방지 위해 1줄 명문화 권고.
- **A-2. RK5 `--isolate-strict` 신규 요소**: 원안엔 없던 "격리 활성화를 세션 다중성/명시 플래그로 제어"(line 106)가 추가됨 — 합리적 개선이나 **새 설계 결정**이므로 Phase 0/1 계획 시 스코프로 확정 필요(플래그 기본값·발동 조건).
- **A-3. Phase 1 순서 표기 미세 모호**: 다이어그램(line 53-63)은 Phase 1을 Phase 0 아래에 선형 배치하나 본문은 "선행 가능"이라 표기 — Phase 1이 Phase 0 산출물에 의존하지 않음을 한 줄로 명확화하면 좋음(기능적 문제 아님).
---
## 3. 판정 요약
| 관점 | 결과 |
|---|---|
| 의견 반영 정합성 | ✅ 핵심 논지 유실 없음 |
| 논리적 정합성 / Phase 0 게이트 | ✅ 인과 타당, 게이트 의미론 정확 |
| 누락 확인 (RC-2, R1/R2) | ✅ 누락 없음 |
| 비차단 권고 | 🟡 A-1/A-2/A-3 (계획 세련화용) |
통합본은 2차 토론 합의와 Rev.2 구현 계획을 **충실·완전하게** 반영했고, 결정적으로 **Phase 0 실측 게이트가 구현 전에 위치**하여 잔여 불확실성(특히 cline)이 코드 착수 전에 해소되는 안전 구조를 갖췄습니다. 구현 계획으로 전환하는 데 이견 없습니다. A-1~A-3는 Phase 0 착수 시 함께 반영 권고.
**PASS**
@@ -1,75 +0,0 @@
# Root Markdown Analysis — Cross-Check Report (Creator Claude)
- **Reviewer**: Creator Claude (`canary-projects-multi-agent-mux-creator-claude`)
- **Date**: 2026-07-11
- **Brief**: `.mam/reports/brief-root-markdowns.md`
- **Cross-checked against**: `.agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-markdown-analysis.md` (Reviewer Cline, 2026-07-11)
- **Method**: independent read of all 7 files + `git log --follow` per file + repo-wide inbound-link grep + verification of implementation claims against shipped commits.
---
## Verdict Summary
Cline's verdicts (3 DELETE / 4 KEEP) are **confirmed in substance**, with **one amendment**: `session_isolation_discussion.md` cannot be deleted standalone without breaking two inbound links in live tracked docs (see §6).
| # | File | My Verdict | Agrees with Cline? |
|---|------|-----------|--------------------|
| 1 | `task.md` | **DELETE** | ✅ |
| 2 | `implementation_plan.md` | **DELETE** | ✅ |
| 3 | `BOOTSTRAP.md` | **KEEP** | ✅ |
| 4 | `FUTURE_WORKS.ko.md` | **KEEP** | ✅ |
| 5 | `DONE.md` | **KEEP** | ✅ |
| 6 | `session_isolation_discussion.md` | **DELETE — with link cleanup** | ⚠️ amended |
| 7 | `AGENTS.md` | **KEEP** | ✅ |
---
## Per-File Analysis
### 1. `task.md` — DELETE
- **Purpose**: Developer checklist (Rev.1) for the "deploy URL parameterization" task (`MAM_REPO_URL` / `MAM_ARCHIVE_URL` / `MAM_INSTALLER_URL`).
- **Status**: The work **shipped in commit `6408f4a`** (2026-07-09, `feat(deploy): parameterize distribution URLs via MAM_*_URL env vars`) touching exactly the four files the plan prescribed (`deploy/install.sh`, `deploy/update.sh`, `.env.example`, `deploy/README.md`). Independently verified: all three `${MAM_*_URL:-…}` patterns exist at the planned locations (`install.sh:57-58`, `update.sh:139`) and both docs carry the variables. Yet every checkbox in the file is still `[ ]`, and the file ends with a stray accidental-paste line (`agy --conversation=20cc2d8e-…`). Note the file was only ever committed once — bundled into the unrelated isolation-docs commit `d76e470`.
- **Justification**: Fully superseded by the shipped commit; retaining an all-unchecked checklist for done work actively misleads future agents. `task.md`/`implementation_plan.md` are per-cycle scratch names per `.agents/multi_agent_workflow.md` — the *convention* survives deletion of this instance.
### 2. `implementation_plan.md` — DELETE
- **Purpose**: Planner design doc (Rev.1) for the same deploy URL parameterization task; still marked "Draft (사용자 승인 대기)".
- **Status**: Same as above — implemented byte-for-byte in `6408f4a` (default values preserved, `.env` non-sourcing decision honored, mirror examples added to `deploy/README.md:35-40`).
- **Justification**: Superseded by shipped code. The only inbound link is from `task.md`, which is deleted in the same set. Design rationale worth preserving is already encoded in the commit message, `.env.example` comments, and `deploy/README.md`.
### 3. `BOOTSTRAP.md` — KEEP
- **Purpose**: Agent-facing setup/verification guide (env config, venv, MQTT handshake test).
- **Status**: Active. Referenced from `README.md:177,186`, and explicitly in the deploy installer's runtime-doc **allowlist** (`deploy/install.sh:131` copies `MESSAGING.md BOOTSTRAP.md BOOTSTRAP.ko.md AGENTS.md`) — deleting it would silently degrade every fresh install. Last substantively updated 2026-07-09.
- **Justification**: Load-bearing runtime asset, not a dev leftover. (Same verdict extends to `BOOTSTRAP.ko.md`.)
### 4. `FUTURE_WORKS.ko.md` — KEEP
- **Purpose**: Korean roadmap of pending improvements (FW-P1~P7, FW-W1~W7, FW-D2~D4 open; FW-D1 resolved).
- **Status**: Active backlog — most items remain unimplemented (e.g., FW-P6 root-marker detection, FW-P7 monitor HMAC hardening). Maintained mirror of `FUTURE_WORKS.md`.
- **Justification**: This is the project's only backlog tracker; deletion loses planned work. (Same verdict for the English `FUTURE_WORKS.md`.)
### 5. `DONE.md` — KEEP
- **Purpose**: Verified completion record for 28 items (FW-01~FW-16, FW-L1~L3, FW-N1~N7, FW-W3) with per-item commits and 3-agent cross-verification results.
- **Status**: Static historical record; explicitly linked from `FUTURE_WORKS.md:4` ("For completed items, see `DONE.md`") and its Korean twin.
- **Justification**: Deleting it dangles the FUTURE_WORKS reference and erases the audit trail mapping FW-IDs to commits. Zero maintenance cost. (Same for `DONE.ko.md`.)
### 6. `session_isolation_discussion.md` — DELETE, **but only with link cleanup** (amendment to Cline)
- **Purpose**: Rev.3 consolidated design discussion for the session-ID isolation feature.
- **Status**: Superseded — the doc itself declares `implementation_plan.session_isolation.md` (Rev.3) the single source of truth, and the feature is fully implemented, integration-tested, and PASSed by both reviewers (commits through `dad99f5`).
- **Amendment**: Cline's report misses that **two live tracked docs still link to it**: `implementation_plan.session_isolation.md:6` (관련 자료) and `task.session_isolation.md:3` (기준 문서). Standalone deletion creates dangling links.
- **Recommendation**: Treat the whole isolation doc set (`session_isolation_discussion.md`, `Problem_Definition.md`, `implementation_plan.session_isolation.md`, `task.session_isolation.md`, `session_isolation_handover.md` — the latter four out of this brief's scope) as one unit: either delete/archive them **together** (the feature is done and PASSed; the durable outcome lives in `.agents/reports/*/report-isolation-review.md` and git history), or if only the discussion doc goes now, remove the two inbound link references in the same commit.
### 7. `AGENTS.md` — KEEP
- **Purpose**: Core behavioral guidelines for all agents; entry pointer to `.agents/MULTI_AGENT_RULES.md`.
- **Status**: Active and essential — copied to target projects by **both** installers (`scripts/install_mam.sh:127,138` and `deploy/install.sh:131`), referenced by README and the orchestration rules.
- **Justification**: Deleting it breaks both install paths and the documented onboarding flow.
---
## Out-of-Scope Observations (for the record)
1. The root holds **18** markdown files; the brief covered 7. The undiscussed siblings share the fates above: `*.ko.md` twins follow their English counterparts; the four other session-isolation docs should be dispositioned as a set with #6.
2. The uncommitted working-tree `.gitignore` change adding `.agents/reports` (flagged in my installer verdict) is still present and would ignore the very reports directory this brief writes into — it should be resolved before committing any deletions.
3. Deletions of tracked files require `git rm` + commit; per standing constraints I have not deleted or committed anything — this report is analysis only.
## Final Verdict
**PASS on Cline's analysis with the §6 amendment**: 3 DELETE / 4 KEEP confirmed; `session_isolation_discussion.md` must be deleted together with cleanup of its two inbound links (or as part of archiving the whole isolation doc set).
@@ -1,175 +0,0 @@
# 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 <brief> 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 <sess> [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 <sess> — 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 <sess> <text> [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.
@@ -1,85 +0,0 @@
# Prompt-Lock Fix — Final Review (Creator Claude)
- **Reviewer**: Creator Claude (`canary-projects-multi-agent-mux-creator-claude`)
- **Brief**: `.mam/reports/brief-rereview-prompt-lock.md`
- **Round 1** (2026-07-11, commit `e613f4a`): ❌ FAIL — full findings preserved in git history (this file as committed in `da895fc`).
- **Round 2** (2026-07-11, commit `da895fc`): ❌ **FAIL — one single-line blocker remains** (F5, new in the fix commit). Everything else is verified fixed.
- **Round 3** (2026-07-11, working tree on top of `da895fc`): ✅ **PASS** — see below.
---
## Round 3 Verdict: ✅ PASS (working-tree state; commit required)
The F5 fix is applied in the working tree of `create_session.sh:387-388` **byte-identical to the prescribed replacement**:
```bash
rc=0
inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID" || rc=$?
```
- Idiom correctness under `set -euo pipefail` was already proven empirically in round 2 (P2: event published with true `rc=4`, EXIT trap fires, script exits 1). The `||` form suppresses `set -e` for the command, so injection failures now reach `delegate_publish_event error` — MS-7's no-zombie-jobs contract holds on the failure path.
- `bash -n` passes; shellcheck `-S warning`: **0 findings**.
- Diff scope verified: the only code change versus `da895fc` is this 4-line block; all other working-tree changes are review reports.
- lib.sh is unchanged since round 2, where the full functional suite passed 5/5 against the committed helpers (T-A3…T-E3: dialog refusal rc=2 / flood rc=1 / happy-path rc=0 / instant banner / one-Enter trust acceptance).
**Conditions attached to this PASS:**
1. The fix is **uncommitted** — it must be committed for the verdict to bind to a ref (suggested: `fix(create): make injection-failure error event survive set -e (|| rc=$?)`). Include the pending review reports (this file, Reviewer Cline's staged modification and new v2 report) per the durable-reports convention.
2. **DoD-5 follow-up** (non-blocking, reaffirmed): capture-validate the dialog signature tokens for agy/hermes/cline in the field; claude tokens match known real CLI text and unmatched tokens now fail loud, not silent.
3. Update FW-W2's resolution commit reference once the fix commit exists.
---
## Round 2 Verdict: ❌ FAIL (NOT PASS) — F5 only
### ✅ F1 (blank-padded viewport windows) — VERIFIED FIXED
`da895fc` applies the prescribed `_pane_tail()` helper verbatim (lib.sh:1117) and rewires all three windowing sites (`_pane_dialog_open`, `send_keys_safe` submit-verify, `handle_startup_dialogs`). Re-ran the full functional suite against the **committed** code on an isolated scratch server (`tmux -L sks-review`):
| Test | Scenario | Expected | Result |
|---|---|---|---|
| T-A3 | dialog mock, `SKS_DIALOG_TIMEOUT=6` | rc=2, zero paste leakage | ✅ rc=2, 0 occurrences in pane |
| T-B3 | perpetually flooding pane | rc=1 (quiescence gate) | ✅ rc=1 |
| T-C3 | happy-path mock prompt TUI | rc=0, line received | ✅ rc=0, `RECEIVED-OK len=41` |
| T-D3 | ready banner on screen | fast return 0 | ✅ rc=0 in 0 s |
| T-E3 | trust dialog then banner | exactly one Enter, ready detected | ✅ rc=0 in 2 s, banner reached |
`bash -n` passes; shellcheck `-S warning` on lib.sh: 0 findings.
### ❌ F5 — NEW BLOCKER: the F3 fix regressed error-event publication under `set -e`
`create_session.sh` runs under `set -euo pipefail` (line 20). The new form (lines 387-392):
```bash
inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID"
rc=$?
if [ "$rc" -ne 0 ]; then
delegate_publish_event "$DELEGATE_JOB_ID" error "instruction injection failed (prompt-lock, rc=$rc)"
```
Under `set -e`, a **bare failing command aborts the script immediately**`rc=$?` and the `delegate_publish_event error` line are never reached. Proven empirically:
```
set -euo pipefail; f(){ return 4; }; trap "echo TRAP-FIRED" EXIT
f; rc=$?; echo "EVENT-PUBLISHED rc=$rc" → output: TRAP-FIRED only, exit 4
rc=0; f || rc=$?; if [ "$rc" -ne 0 ]; ... → output: EVENT-PUBLISHED rc=4, TRAP-FIRED, exit 1
```
Consequence on injection failure: the EXIT trap still rolls back the session and isolation home, but **no terminal `error` event is ever published** — the delegator waits for watchdog timeout. That is precisely the zombie-job outcome MS-7 exists to prevent, so the fix traded round 1's cosmetic `rc=0` misreport (F3) for a functional regression on the same path. Ironically the round-1 code *did* publish the event.
**Required fix (verified above, one line):**
```bash
rc=0
inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID" || rc=$?
if [ "$rc" -ne 0 ]; then
```
Scope confirmed limited to this one site: the delegate-job wrapper uses the `if ! send_keys_safe …` guard form and `stop_session.sh` uses `send_keys_safe … || echo …` — both are `set -e`-safe and report correct rc.
### Remaining non-blocking items
1. **DoD-5 (real-TUI token validation)** — still no recorded capture evidence. Partially mitigated: the claude tokens (`Do you trust the files`, `Yes, proceed`/`No, exit`) match the real Claude Code CLI dialog text, and with fail-loud semantics an unmatched token now degrades to a loud rc≠0 + rollback rather than a silent lock. Recommendation to Planner: accept with a follow-up task to capture-validate agy/hermes/cline dialog text in the field, rather than blocking on it again.
2. **Working-tree hygiene**: `.agents/reports/canary-projects-multi-agent-mux-reviewer-cline/report-prompt-lock-analysis.md` has an uncommitted modification — a committed audit record edited in place (not by me). Commit or revert it deliberately alongside the F5 fix.
3. FW-W2 stays legitimately marked resolved once F5 lands; update its commit reference then.
---
## Summary for the Planner
The hard problem is solved and proven: dialog gating, quiescence, banner detection, and trust-dialog acceptance all behave correctly on the committed helpers (5/5 functional tests). What remains is a one-line `set -e` idiom fix in `create_session.sh` (`|| rc=$?`) so injection failures publish their terminal error event — the empirical proof and exact replacement are above. Fix that line, decide the stray report edit, and round 3 is a rubber stamp.
@@ -1,163 +0,0 @@
# MAM Skill Optimization Analysis (Creator Claude)
- **Analyst**: Creator Claude (`canary-projects-multi-agent-mux-creator-claude`)
- **Date**: 2026-07-11
- **Brief**: `.mam/reports/brief-skill-optimization-analysis.md`
- **Scope**: all 8 shell entry points under `.agents/skills/` — 3,422 lines total (`lib.sh` 1212, `reconcile.sh` 644, delegate-job wrapper 440, `create_session.sh` 408, `stop_session.sh` 370, `update_yaml_resumed.sh` 164, `status.sh` 140, `resolve_session_id.sh` 44)
- **Audit baseline**: working tree on `da895fc` + the uncommitted prompt-lock F5 fix in `create_session.sh` (reviewed PASS, awaiting commit)
- **Method**: full-tree greps (sleeps, tmux-resolution variants, `|| true`/`2>/dev/null`, BASH_SOURCE, heredocs, eval), shellcheck run, targeted reads of every flagged site.
## Baseline strengths (for calibration)
Uniform `#!/usr/bin/env bash` + `set -euo pipefail` across all 7 executables (lib.sh correctly bare as a sourced library); zero `eval` in any executable script; zero warning-level shellcheck findings beyond 6 pre-existing ones; the worst sleep offenders (resume's blind `sleep 5/3`, inject's `sleep 0.5`+blind C-m) were already eliminated by the FW-W2 `send_keys_safe` work. The findings below are the next tier.
---
## Focus 1 — Inefficient Polling / Sleeps
### S1 (HIGH VALUE) — `stop_session.sh:193,200`: fixed post-kill waits
```bash
tmux send-keys … # graceful exitkey
sleep 3 # ← always pays 3 s
tmux kill-session …
sleep 5 # ← always pays 5 s
```
Every graceful stop pays the full 3 s even when the agent exits in 200 ms, and the kill path always pays 5 s. Worse than slow: on a loaded host an agent needing >3 s to flush **falsely escalates** to SIGTERM. **Proposal**: add to lib.sh —
```bash
# _wait_session_gone <sess> <max_sec> — returns 0 as soon as the session dies
_wait_session_gone() {
local sess="$1" max="${2:-5}" i
for ((i = 0; i < max * 4; i++)); do
_sks_tmux has-session -t "$sess" 2>/dev/null || return 0
sleep 0.25
done
return 1
}
```
Replace `sleep 3` with `_wait_session_gone "$SESSION_NAME" 5` and `sleep 5` with `_wait_session_gone "$SESSION_NAME" 8`. Reactive (typical stop drops from ~8 s to <1 s), *and* more tolerant of slow exits.
### S2 (HIGH VALUE) — delegate-job `:119` and `:205`: `sleep 1` as MQTT handshake
The comment admits the ordering dependency ("MQTT does not queue non-retained messages for absent subscribers"), then guesses: if CONNACK+SUBACK takes >1 s (public broker `broker.hivemq.com` over WAN — entirely realistic), the agent's `started` event is **lost silently** and the job idles to timeout; on a local broker the 1 s ×2 per review-loop iteration is pure waste. **Proposal** (event-driven handshake, also fixes E4): `job_subscriber.py` already logs to `$logf` — have it print a sentinel line (e.g. `SUBSCRIBED <topic>`) from its `on_subscribe` callback (flush immediately), then in the wrapper replace both sleeps with:
```bash
for _ in {1..25}; do grep -q '^SUBSCRIBED ' "$logf" 2>/dev/null && break; sleep 0.2; done
grep -q '^SUBSCRIBED ' "$logf" || { echo "ERROR: subscriber never reached SUBACK (see $logf)" >&2; exit 1; }
```
Removes the race instead of betting on it, and converts a dead-on-arrival subscriber (see E4) into a loud failure.
### S3 (minor) — `lib.sh:1042-1085` `wait_for_tui_ready`
Two `capture-pane` invocations per iteration (`_pane_dialog_open` + its own `content=$(…)`) with a hand-rolled `local_tmux`. Capture once per iteration into a variable and test both predicates on it; use `_pane_capture` (see D4). The 15×1 s poll budget itself is fine.
### S4 (accepted as-is) — `reconcile.sh:273` `sleep "$POLL_INTERVAL"` broker-down fallback loop is by design; see E1 for its real problem (silence, not pacing).
---
## Focus 2 — Helper Duplication & Modularization
### D1 (HIGH VALUE) — four divergent server-aware tmux resolutions
| Site | Form |
|---|---|
| `lib.sh:1104-1110` `_sks_tmux()` | function — **canonical, word-split-safe** |
| `lib.sh:1043-1046` (`wait_for_tui_ready`) | `local_tmux="tmux -L $NAME"` string |
| `create_session.sh:212-215` | same string pattern (file also has a `_tmux` helper used by its trap — two mechanisms in one script) |
| delegate-job `:347-350` | `_tmux="tmux -L $NAME"` string |
The string variants rely on unquoted word-splitting (`$local_tmux send-keys …`) — the exact idiom class shellcheck SC2086 exists for, and each future call site must re-remember the `!= default` rule. **Proposal**: rename/promote `_sks_tmux` to `mam_tmux()` (keep `_sks_tmux` as an alias for compatibility) and replace all three string variants. Mechanical, ~10 lines net deletion.
### D2 — delegate-job wrapper: duplicated subscriber-spawn + instruction template
The register→spawn-subscriber→sleep→build-`instructions` block appears twice (direct path `:113-131`, review-loop path `:199-215`) and the copies have already drifted (log filename schema differs; the review-loop copy carries iteration metadata the direct copy lacks). Extract `_spawn_job_subscriber <job_id> <logf>` and `_job_instruction_block <job_id> <pub_cmd> <task_text>`; combine with S2 so the handshake logic exists exactly once.
### D3 — ready/dialog token lists duplicated inside lib.sh
Claude ready-tokens `'Anthropic|Assistant|Chat|Welcome|projects'` at **both** `lib.sh:1058` (`wait_for_tui_ready`) and `lib.sh:1205` (`handle_startup_dialogs`); trust-dialog tokens split between `_pane_dialog_open` (`:1137-1139`) and `handle_startup_dialogs`' specific greps (`:1199,1201`). Token-list drift between two grep sites is **precisely the class of defect just fixed in the prompt-lock round** — next TUI release, someone updates one list and not the other. **Proposal**: single-source constants near the top of lib.sh —
```bash
_MAM_DIALOG_TOKENS='Do you trust the files|Yes, proceed|No, exit|Allow this|Press Enter to continue|browser to authenticate|Use arrow keys|Esc to cancel'
_MAM_READY_TOKENS_CLAUDE='Anthropic|Assistant|Chat|Welcome|projects'
```
plus a `_ready_regex_for <agent>` case-helper so `wait_for_tui_ready`'s per-agent regexes live in one lookup. All grep sites reference the variables.
### D4 — `wait_for_tui_ready` predates its own library's capture helpers
It hand-builds `local_tmux` and calls raw `capture-pane` instead of `_pane_capture`/`_pane_tail`. Folding it onto the helpers (with S3's single-capture-per-iteration) deletes ~8 lines and closes D1's second row for free.
### D5 (micro) — `stop_session.sh:128-129`: two `python3 -c` processes to read two JSON fields from the same `$MAPPED_DATA`. One process printing both (`'…; d=json.load(sys.stdin); print(d.get("cwd",""), d.get("job_id",""), sep="\t")'`) halves the fork cost; or add a tiny `json_get` helper to lib.sh if more call sites appear.
---
## Focus 3 — Portability & POSIX Compliance
Shebang discipline means raw-`sh` execution is not a real exposure; the genuine gaps are three, and two are **already roadmapped** — listed here with confirmations, not double-counted as new:
### P1 (= FW-P1 / FW-D3, confirmed at `lib.sh:254-255`)
```bash
mountpoint="$(df --output=target "$f" 2>/dev/null | tail -1)" || return 1
if mount | grep -q "$mountpoint.*nfs|…"
```
GNU-only `df --output` + Linux `mount` output format. On macOS/BSD, `df` errors → suppressed by `2>/dev/null``_check_is_nfs` silently returns "not NFS" → **the NFS/WAL safety switch is dead exactly where flock is least reliable**. Portable replacement: `mountpoint="$(df -P "$f" 2>/dev/null | awk 'NR==2{print $6}')"` (`df -P` is POSIX) and probe filesystem type via `stat -f -c %T` on Linux / `stat -f %T` on BSD behind a `case "$(uname)"` — or at minimum log a warning when detection is unavailable instead of silently passing.
### P2 (= FW-P5, confirmed at `lib.sh:17`)
`SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"` — under zsh sourcing (documented agent workflow: `source .agents/skills/lib.sh`), `BASH_SOURCE` is empty → `dirname ""``.``SKILL_DIR` silently becomes the caller's cwd, and every relative resolution downstream (`:950`, `:966`) misroutes. **Proposal (fail-loud, 3 lines at the top of lib.sh)**:
```bash
if [ -z "${BASH_SOURCE:-}" ]; then
echo "lib.sh must be sourced from bash (zsh/sh detected)" >&2; return 1 2>/dev/null || exit 1
fi
```
Silent misresolution becomes an immediate, explained failure. (Full zsh support via `${(%):-%N}` is possible but not worth the dual-dialect maintenance.)
### P3 (= FW-P6, confirmed) — depth-hardcoded root resolution
`status.sh:29` (`…/../../../../`), `reconcile.sh:48`, and the `../..` sourcing prologue in all 6 skill scripts. One directory-layout refactor breaks all of them at once. FW-P6's marker-walk (`find_workspace_root()` ascending to `.git`/`.mam`/`.env`, exported once as `WORKSPACE_ROOT`) remains the right fix; the sourcing prologues can stay relative (they express a true structural invariant *within* the skills tree) — it's the **workspace-root** hops that should go through the marker walk.
### P4 (non-issues, verified): fractional `sleep 0.5/0.25` (GNU+BSD+busybox all accept), `head -c`/`tail -c`/`awk NF` (POSIX), no `grep -P`, no `sed -i`, no `readlink -f`, no `eval` — clean.
---
## Focus 4 — Error Handling & Robustness
### E1 (HIGHEST SEVERITY in this audit) — `reconcile.sh:269`: degraded mode is fully silent
```bash
bash "$_self" --once --emit-diff >/dev/null 2>&1 || true
```
This runs *only* in the broker-down fallback — the mode whose entire purpose is "keep reconciling when eventing is gone" — and it discards stdout, stderr, **and** the exit code. If reconciliation itself is failing every cycle (locked DB, missing python module, corrupted YAML), the operator sees a healthy-looking monitor while drift accumulates unboundedly. **Proposal**:
```bash
if ! out=$(bash "$_self" --once --emit-diff 2>&1); then
fails=$((fails + 1))
echo "[$(date -u +%FT%TZ)] poll-reconcile failed ($fails consecutive): ${out##*$'\n'}" >&2
[ "$fails" -ge 5 ] && { echo "FATAL: 5 consecutive reconcile failures — exiting for supervisor restart" >&2; exit 1; }
else
fails=0
fi
```
### E2 — deliberate vs. accidental suppression (survey result)
The 18 `|| true` / 31 `2>/dev/null` sites were individually reviewed. The large majority are **legitimate idempotency guards** (stop's kill-chain probing possibly-absent sessions; `_pane_capture`'s probe semantics) — no action. The accidental class is E1 (above) and E4 (below).
### E3 — `stop_session.sh:128-129`: unguarded JSON parse under `set -e`, no EXIT trap
Malformed registry JSON kills the stop mid-flight with a bare Python traceback; unlike `create_session.sh`, `stop_session.sh` has **no cleanup/context trap**, so the operator gets no indication of what state the stop reached (exitkey sent? captured? row updated?). Cheap fix: wrap the parse (`… || { echo "ERROR: corrupt registry row for '$SESSION_NAME' — run monitor reconcile" >&2; exit 1; }`) and add a minimal `trap 'echo "stop aborted at stage $STAGE" >&2' ERR` with a `STAGE` variable advanced at each phase.
### E4 — delegate-job subscriber spawned fire-and-forget
`"$PY" job_subscriber.py … >"$logf" 2>&1 &` followed only by `sleep 1`: if the subscriber dies instantly (bad `--registry-dir`, missing paho-mqtt in the venv), the wrapper proceeds, the agent publishes into the void, and the job "runs" with zero audit trail until timeout. The S2 SUBACK-sentinel handshake converts this to a loud early failure — one fix, two findings (S2+E4).
### E5 — shellcheck backlog (6 warnings, pre-existing)
`SC2034` ×2 (`ONCE`, `EMIT_DIFF` "unused" in reconcile.sh — likely consumed inside the python heredoc via env; verify and either export or rename with `_` prefix to document intent), `SC2155` ×2, `SC2164` ×2 (`cd` without `|| exit` — real hazard under odd cwd removal). All are ≤2-line fixes; clearing them makes future "0 new findings" review gates strict.
---
## Prioritized Optimization Plan
| # | Item | Files | Effort | Impact |
|---|---|---|---|---|
| 1 | E1 silent degraded loop → logged + bounded failures | reconcile.sh | S | Correctness/observability of the safety net |
| 2 | S2+E4 SUBACK sentinel handshake replaces both `sleep 1` | delegate-job wrapper, job_subscriber.py | M | Eliminates event-loss race on slow brokers; loud subscriber failures |
| 3 | S1 `_wait_session_gone` reactive stop | lib.sh, stop_session.sh | S | ~7 s faster stops; no false SIGTERM escalation |
| 4 | D1+D4 `mam_tmux()` unification (retire 3 string variants) | lib.sh, create_session.sh, delegate-job | S | Single source of truth; kills word-split hazard |
| 5 | D3 token-list constants + `_ready_regex_for` | lib.sh | S | Prevents recurrence of the F1-class drift bug |
| 6 | P2 fail-loud non-bash source guard | lib.sh | S | Converts silent misresolution to instant diagnosis (FW-P5) |
| 7 | E3 stop parse guard + stage trap | stop_session.sh | S | Debuggable partial-stop states |
| 8 | D2 delegate-job block extraction | delegate-job wrapper | M | Stops copy drift (already observable) |
| 9 | P1 POSIX NFS detection (FW-P1/FW-D3) | lib.sh | M | Restores the WAL safety switch on macOS/BSD |
| 10 | P3 marker-walk root resolution (FW-P6) | lib.sh, status.sh, reconcile.sh | M | Layout-refactor resilience |
| 11 | E5 shellcheck backlog + D5 micro | reconcile.sh, lib.sh, stop_session.sh | S | Strict lint gate for future reviews |
Items 17 are low-risk and independently landable; 910 discharge existing roadmap entries (FW-P1/FW-D3/FW-P5/FW-P6 — update FUTURE_WORKS on landing). Every DoD should include the scratch-server functional suite from the prompt-lock review (T-A…T-E) plus `bash -n` + shellcheck zero-new.
## Summary
The tree is in good structural shape — consistent strict-mode headers, no eval, and the recent FW-W2 work already modernized the highest-risk delivery path. The remaining debt clusters into: two **timing bets** that should be handshakes (stop waits, MQTT subscribe), one **silent failure mode** in exactly the code path that exists for resilience (reconcile fallback), and **four copies** of the tmux-server rule plus **two copies** of the TUI token lists — the same drift pattern that caused the last production bug. Eleven changes, mostly small, none speculative.
@@ -1,13 +0,0 @@
# 📑 Code Review Report: Skill Optimization Implementation
- **Reviewer**: Creator Claude (`canary-projects-multi-agent-mux-creator-claude`)
- **Date**: 2026-07-11
- **Reviewed against**: `.mam/reports/brief-rereview-skill-optimization.md`
- **Verdict**: **PASS**
## 🔎 Implementation Review Details
1. **OP-1 (stop_session.sh wait)**: Reactive wait prevents 7 seconds of magic sleeps on shutdown. `|| true` safely shields the caller from `set -e` aborts on slow exits.
2. **OP-2 (delegate-job subscription handshake)**: Sentinel checking loop with `$sub_pid` liveness guard successfully prevents the WAN event loss race.
3. **OP-3 (reconcile.sh wait)**: Dynamic `threading.Event().wait` pacing reduces CPU wake-ups to zero during idle cycles.
4. **OP-4 (mam_tmux dispatcher)**: Infinite recursion successfully resolved via direct execution of `$_REAL_TMUX_PATH`.
5. **OP-6 & OP-7 (lib.sh constants and zsh guard)**: Sourcing guard and token variables pass syntax and safety review.