From 7b761be7b86e84fe88393b1d41d0743cf690b7db Mon Sep 17 00:00:00 2001 From: Godopu Date: Tue, 8 Sep 2026 10:39:54 +0900 Subject: [PATCH] docs(issue-6): establish phase 1-5 implementation plan and task checklist (Rev.2) --- .../issue_6_consolidated_review_report.md | 121 +++++++++ .../plan-f4873d82.md | 242 ++++++++++++++++++ .../reviewer-opencode-01/report-17f2e688.md | 67 +++++ .../reviewer-opencode-01/report-58fba2b1.md | 51 ++++ docs/contracts/issue-6/implementation_plan.md | 242 ++++++++++++++++++ docs/contracts/issue-6/task.md | 104 ++++++++ 6 files changed, 827 insertions(+) create mode 100644 .agents/reports/issue_6_consolidated_review_report.md create mode 100644 .agents/reports/planner-reviewer-claude-01/plan-f4873d82.md create mode 100644 .agents/reports/reviewer-opencode-01/report-17f2e688.md create mode 100644 .agents/reports/reviewer-opencode-01/report-58fba2b1.md create mode 100644 docs/contracts/issue-6/implementation_plan.md create mode 100644 docs/contracts/issue-6/task.md diff --git a/.agents/reports/issue_6_consolidated_review_report.md b/.agents/reports/issue_6_consolidated_review_report.md new file mode 100644 index 0000000..adf14f7 --- /dev/null +++ b/.agents/reports/issue_6_consolidated_review_report.md @@ -0,0 +1,121 @@ +# ๐Ÿ“‘ Issue #6 ์ข…ํ•ฉ ๋ฆฌ๋ทฐ ๋ฐ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์„  ํ†ตํ•ฉ ๋ณด๊ณ ์„œ (Rev. 2) + +- **์ž‘์„ฑ ์ฃผ์ฒด**: ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ (Orchestrator) +- **์ฐธ์—ฌ ๋ฆฌ๋ทฐ์–ด**: `planner-reviewer-claude-01` (Claude), `reviewer-creator-grok-01` (Grok), `planner-reviewer-codex-01` (Codex), `reviewer-opencode-01` (OpenCode) +- **๊ฒ€ํ†  ๋Œ€์ƒ**: Gitea [Issue #6](https://git.godopu.com/tmpl/multi-agent-mux/issues/6) โ€” `[Bug] delegate-job ์›Œ์ปค ์‹คํŒจ ์‹œ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋ฏธ๊ฐฑ์‹ (Fail-Silent) ๋ฐ TUI ํ„ด ๋งˆ๊ฐ ๋ ˆ์ด์Šค ์ปจ๋””์…˜ ํ•ด๊ฒฐ` +- **๊ฒ€ํ†  ๊ธฐ์ค€ ์ปค๋ฐ‹**: `f98b958` (`main`, `refactor`, v4.4.0) +- **๋ฌธ์„œ ๋ฒ„์ „**: Revision 2 (Rev. 2 โ€” 2์ฐจ ๋ผ์šด๋“œ ๋ฆฌ๋ทฐ์–ด ํ”ผ๋“œ๋ฐฑ ์ „๊ฑด ๋ฐ˜์˜) + +--- + +## 1. ๊ฐœ์š” ๋ฐ ์ดํ‰ + +์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ๋ฃจํ”„(`run_loop.sh` / `lib_py.loop`) ์‹คํ–‰ ์ค‘ ๋ฐœ์ƒํ–ˆ๋˜ 3,900์ดˆ ์˜๊ตฌ ๋Œ€๊ธฐ(hang) ๊ฒฐํ•จ(Issue #6)์— ๋Œ€ํ•ด 4๋ช…์˜ ๋ฆฌ๋ทฐ์–ด๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ์†Œ์Šค ์ฝ”๋“œ ์ถ”์ , ๋‹จ์œ„ ๊ฒฉ๋ฆฌ ์žฌํ˜„, ์‹ค์ธก ํฌ๋ Œ์‹, Herdr 0.8.2 ๋„ค์ดํ‹ฐ๋ธŒ API ๊ณ„์•ฝ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค. + +**๋ฆฌ๋ทฐ์–ด 4์ธ ์ „์›์˜ ๋งŒ์žฅ์ผ์น˜ ํŒ์ • ๊ฒฐ๊ณผ:** +> **"Issue #6์—์„œ ์ œ๊ธฐํ•œ ๊ฒฐํ•จ โ‘ ๊ณผ ๊ฒฐํ•จ โ‘ก๋Š” ๋ชจ๋‘ ์‹ค์ œ ์ฝ”๋“œ๋ฒ ์ด์Šค์— ์กด์žฌํ•˜๋Š” ์‹ฌ๊ฐํ•œ ์‹ค์žฌ ๋ฒ„๊ทธ ๋ฐ ์„ค๊ณ„ ๊ฒฐํ•จ์ด๋‹ค."** + +ํŠนํžˆ 2์ฐจ ๋ผ์šด๋“œ ๊ต์ฐจ ๊ฒ€ํ† ๋ฅผ ํ†ตํ•ด **๋ฃจํ”„ ์‹ค์‚ฌ์šฉ ๊ฒฝ๋กœ(`--type direct`)์˜ ์ˆจ์€ ๊ฒฐํ•จ**, **๊ธฐ์กด `deadline_timeout` CAS ์—”์ง„ ์žฌ์‚ฌ์šฉ ์›์น™**, **๊ตฌ๋…์ž ํƒ€์ž„์•„์›ƒ ์˜ˆ์‚ฐ ์ž๊ฐ€ํฌ์‹ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์‚ฌ์ „ ํ•ธ๋“œ์…ฐ์ดํฌ ์ˆœ์„œ**, **๋ธŒ๋กœ์ปค ์žฅ์• (`rc=3`) ๊ฒฉ๋ฆฌ** ๋“ฑ ํ”„๋กœ๋•์…˜ ์ˆ˜์ค€์˜ ์ •๋ฐ€ํ•œ ๊ฐ€๋“œ๋ ˆ์ผ์ด ํ†ตํ•ฉ ๋ณด๊ณ ์„œ Rev.2์— ์™„๋ฒฝํžˆ ๋ณด๊ฐ•๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + +--- + +## 2. ๊ฒฐํ•จ๋ณ„ ์ง„์œ„ ๊ฒ€ํ†  ๋ฐ ์‹ฌ์ธต ๋ถ„์„ + +### 2.1 ๊ฒฐํ•จ โ‘ : `multi-agent-mux-delegate-job` ์›Œ์ปค ์‹คํŒจ ์‹œ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋ฏธ๊ฐฑ์‹  (Fail-Silent ๋ฒ„๊ทธ) +- **ํŒ์ •**: **์‹ค์žฌํ•˜๋Š” ํ•ต์‹ฌ ๊ฒฐํ•จ (CONFIRMED REAL, P0/P1)** +- **์‹ค์ธก ๋ฐ ์žฌํ˜„ ๊ฒฐ๊ณผ**: + 1. **์ด์Šˆ ์ง€๋ชฉ ์œ„์น˜(`:358-362`)์˜ ์ง„์œ„**: + - `_phase == "worker"`์—์„œ `job_status != "completed"`์ผ ๋•Œ `break` ํ›„ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ(`registry.py status --set error`) ๊ฐฑ์‹  ์—†์ด ํ•จ์ˆ˜ ๋์—์„œ ๋กœ๊ทธ ๊ฒฝ๋กœ๋ฅผ ์ถœ๋ ฅํ•˜๋ฉฐ `exit 0`์œผ๋กœ ์ข…๋ฃŒ๋˜๋Š” ํ˜„์ƒ์ด ์ •ํ™•ํžˆ ํ™•์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + - ๋™์ผํ•œ ๊ฒฐํ•จ์ด ๋ฆฌ๋ทฐ์–ด ๋‹จ๊ณ„(`:375-378`) ์กฐ๊ธฐ ํƒˆ์ถœ ๋ถ„๊ธฐ์—๋„ ๋™์ผํ•˜๊ฒŒ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. + 2. **์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ์‹ค์‚ฌ์šฉ ๊ฒฝ๋กœ(`--type direct:227`)์˜ ์ˆจ์€ ๊ฒฐํ•จ (์ „์› ๊ณตํ†ต ๋ฐœ๊ฒฌ)**: + - ์ƒ์œ„ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ(`lib_py/loop/runner.py:271` ๋ฐ `run_loop.sh`)๋Š” ๋ฃจํ”„ ๋‚ด๋ถ€์—์„œ `--type loop`๊ฐ€ ์•„๋‹Œ **`--type direct`**๋กœ ์—์ด์ „ํŠธ ์ž‘์—…์„ ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค. + - direct ๊ฒฝ๋กœ(`:227`)๋Š” `run_agent` ์‹คํ–‰ ํ›„ `wait "$sub_pid" || true`๋กœ ๊ตฌ๋…์ž ์‹คํŒจ๋ฅผ ์‚ผ์ผœ๋ฒ„๋ฆฌ๊ณ , ์ƒํƒœ ์ „์ด๋ฅผ ์ „ํ˜€ ๊ฑฐ์น˜์ง€ ์•Š์€ ์ฑ„ `exit 0`์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + - ๋”ฐ๋ผ์„œ ์ด์Šˆ๊ฐ€ ์–ธ๊ธ‰ํ•œ ๋ฃจํ”„ ๋ถ„๊ธฐ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ **์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ๊ฐ€ ์‹ค์ œ๋กœ ํƒ€๋Š” direct ๊ฒฝ๋กœ ์—ญ์‹œ ์™„์ „ํžˆ ์นจ๋ฌต(Fail-Silent)**ํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. + 3. **ํŒŒ๊ธ‰ ํšจ๊ณผ (65๋ถ„ Hang)**: + - ์ƒ์œ„ ๋ฃจํ”„์˜ `wait_for_job`์€ `completed`, `error`, `cancelled` 3๊ฐœ ํ„ฐ๋ฏธ๋„ ์ƒํƒœ๋งŒ ์ข…๋ฃŒ ์กฐ๊ฑด์œผ๋กœ ์ธ์‹ํ•ฉ๋‹ˆ๋‹ค. + - ๊ตฌ๋…์ž๊ฐ€ 120์ดˆ idle timeout(`sub_rc=2`)์œผ๋กœ ์ฃฝ์—ˆ์Œ์—๋„ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๊ฐ€ `pending` ๋˜๋Š” `running`์œผ๋กœ ๋ฐฉ์น˜๋˜์–ด ์ƒ์œ„ ๋ฃจํ”„๊ฐ€ ๊ธฐ๋ณธ๊ฐ’์ธ **3,900์ดˆ(65๋ถ„) ๋™์•ˆ ๋ธ”๋กœํ‚น**๋ฉ๋‹ˆ๋‹ค. + - ์‹ค์ œ ์šด์˜ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ํฌ๋ Œ์‹ ๊ฒฐ๊ณผ `93a44347`, `2961b822`, `ee242671` 3๊ฑด์˜ ์‹ค์šด์˜ ์ž‘์—…์ด ์ •ํ™•ํžˆ ์ด ๊ฒฐํ•จ์œผ๋กœ ์žฅ์‹œ๊ฐ„ ๊ต์ฐฉ ์ƒํƒœ์— ๋ฐฉ์น˜๋˜์—ˆ๋˜ ์‚ฌ์‹ค์ด ์‹ค์ฆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + +### 2.2 ๊ฒฐํ•จ โ‘ก: ์—์ด์ „ํŠธ ์ด๋ฒคํŠธ ๋ฐœํ–‰๊ณผ TUI ๋ณต๊ท€ ๊ฐ„์˜ ๋ ˆ์ด์Šค ์ปจ๋””์…˜ +- **ํŒ์ •**: **์‹ค์žฌํ•˜๋Š” ์„ค๊ณ„ ๊ฒฐํ•จ (CONFIRMED REAL, P1)** +- **์‹ค์ธก ๋ฐ ์žฌํ˜„ ๊ฒฐ๊ณผ**: + 1. **๋น„๋™๊ธฐ ์ƒํƒœ ๋ถˆ์ผ์น˜ ๋ ˆ์ด์Šค**: + - ์—์ด์ „ํŠธ ํ”„๋กœ์„ธ์Šค๊ฐ€ MQTT๋กœ `completed` ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•œ ์งํ›„์—๋„ ํ„ฐ๋ฏธ๋„ ํ™”๋ฉด์€ ๋งˆํฌ๋‹ค์šด ๋ Œ๋”๋ง, ๋ฒ„ํผ ํ”Œ๋Ÿฌ์‹œ, ํˆด ํ˜ธ์ถœ ์ •๋ฆฌ ๋“ฑ์œผ๋กœ ์ธํ•ด ์—ฌ์ „ํžˆ `working/busy` ์ƒํƒœ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + - ์‹ค์ธก ๊ฒฐ๊ณผ Codex ๋“ฑ ๋Œ€ํ˜• ๋ชจ๋ธ์˜ ๊ฒฝ์šฐ `completed` ๋ฐœํ–‰ ํ›„ TUI๊ฐ€ `done`์œผ๋กœ ๋–จ์–ด์ง€๋Š” ๋ฐ ์ตœ๋Œ€ 31์ดˆ๊ฐ€ ์†Œ์š”๋˜๊ธฐ๋„ ํ–ˆ์Šต๋‹ˆ๋‹ค. + 2. **ํ”„๋กฌํ”„ํŠธ ๋ฌต์‚ด ๋ฐ ์œ ์‹ค**: + - ๋‹ค์Œ ๋‹จ๊ณ„(๋ฆฌ๋ทฐ์–ด ๋“ฑ)๊ฐ€ ์ฆ‰์‹œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ „์†กํ•˜๋ฉด TUI๊ฐ€ ์ž…๋ ฅ์„ ๋ฌด์‹œํ•˜๊ฑฐ๋‚˜ ์ฒซ ๊ธ€์ž/๋‹จ์–ด๊ฐ€ ์ž˜๋ฆฐ ์ฑ„ ์ฃผ์ž…๋˜์–ด ๋ฌธ๋งฅ์ด ๊นจ์ง„ ์ฑ„ ์‹คํ–‰๋˜๊ฑฐ๋‚˜ ๋ฌด์‘๋‹ต ์ƒํƒœ์— ๋น ์ง‘๋‹ˆ๋‹ค. + 3. **๋‘ ๊ฒฐํ•จ ๊ฐ„์˜ ์•…์ˆœํ™˜ ์ฆํญ**: + - ๊ฒฐํ•จ โ‘ก(TUI ๋ฏธ์ค€๋น„ ํ”„๋กฌํ”„ํŠธ ์œ ์‹ค) ๋ฐœ์ƒ โ†’ ์›Œ์ปค ๋ฌด์‘๋‹ต โ†’ ๊ตฌ๋…์ž 120์ดˆ ํƒ€์ž„์•„์›ƒ(`sub_rc=2`) โ†’ ๊ฒฐํ•จ โ‘ (Fail-Silent) ๋ฐœ๋™ โ†’ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `pending/running` ๋ฐฉ์น˜ โ†’ ์ƒ์œ„ ๋ฃจํ”„ 65๋ถ„ ์˜๊ตฌ ๊ต์ฐฉ(Hang). + +--- + +## 3. ์ˆ˜์ • ๋‚œ์ด๋„ ๋ฐ ๊ณต์ˆ˜ ์‚ฐ์ • (Consensus) + +| ์˜์—ญ | ๋‚œ์ด๋„ | ์˜ˆ์ƒ ๊ณต์ˆ˜ | ํ•ต์‹ฌ ๋ณ€๊ฒฝ ๋Œ€์ƒ | +| :--- | :---: | :---: | :--- | +| **๊ฒฐํ•จ โ‘  (Fail-Closed CAS)** | **์ค‘ํ•˜ (Low-Med)** | 1.0 ~ 1.5 MD | `skills/.../multi-agent-mux-delegate-job`, `lib_py/delegation/engine.py` | +| **๊ฒฐํ•จ โ‘ก (TUI ํ„ด ํ•ธ๋“œ์…ฐ์ดํฌ)** | **์ค‘ (Medium)** | 1.0 ~ 1.5 MD | `.agents/skills/multi-agent-mux-create/scripts/lib.sh` (`send_keys_safe`), Herdr API ์—ฐ๋™ | +| **ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ & ๋‹ค์ค‘ ์—์ด์ „ํŠธ ์‹ค์ธก** | **์ค‘ (Medium)** | 1.0 ~ 1.5 MD | 4๊ฐœ ์—์ด์ „ํŠธ(Claude/Grok/Codex/OpenCode) ์‹ค๊ธฐ๋™ E2E ๊ฒ€์ฆ, Mock ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | +| **์ด๊ณ„** | **์ค‘ (Medium)** | **3.0 ~ 4.5 Engineer-Days** | ์•ˆ์ „ํ•˜๊ณ  ํ†ต์ œ๋œ ์ ์ง„์  ๊ตฌํ˜„ ๊ฐ€๋Šฅ | + +--- + +## 4. ์ƒ์„ธ ๊ธฐ์ˆ  ์•„ํ‚คํ…์ฒ˜ ๋ฐ ๊ตฌํ˜„ ์‚ฌ์–‘ (Rev. 2 ๋ณด๊ฐ•) + +### 4.1 ๊ฒฐํ•จ โ‘  ํ•ด๊ฒฐ์ฑ…: ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ Fail-Closed ๋ฐ CAS ์—”์ง„ ์—ฐ๋™ +1. **ํ‘œ์ค€ CAS ์ „์ด(`deadline_timeout`) ํ™œ์šฉ**: + - ์ž„์˜์˜ ์‹ ๊ทœ CLI ํ”Œ๋ž˜๊ทธ(`--expect-status`)๋‚˜ ์šด์˜์ž ๊ฐ•์ œ ์˜ค๋ฒ„๋ผ์ด๋“œ๋ฅผ ๋ฐœ๋ช…ํ•˜์ง€ ์•Š๊ณ , ์ด๋ฏธ ๊ฒ€์ฆ๋œ `lib_py/delegation/engine.py:50`์˜ `deadline_timeout` ์ „์ด๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. + - `deadline_timeout`์€ ์žก์ด `pending` ๋˜๋Š” `running`์ผ ๋•Œ๋งŒ ์›์ž์ ์œผ๋กœ `error`๋กœ ์ „์ดํ•˜๋ฉฐ, ๊ตฌ๋…์ž ํƒ€์ž„์•„์›ƒ ์งํ›„ ๋ฐฑ์—”๋“œ์—์„œ ๋Šฆ๊ฒŒ ๋„์ฐฉํ•œ `completed` ์ด๋ฒคํŠธ๊ฐ€ ์ด๋ฏธ ์ปค๋ฐ‹๋œ ๊ฒฝ์šฐ `"conflicting error after terminal completed; refusing"`์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฑฐ๋ถ€(No-op)ํ•ฉ๋‹ˆ๋‹ค. + - `expected_dispatch_token`์„ ์ „๋‹ฌํ•˜์—ฌ ์ด์ „ ์‹œ๋„์˜ ๋Šฆ์€ ํƒ€์ž„์•„์›ƒ์ด ์žฌ์‹œ๋„ ์žก์„ ์˜ค์—ผ์‹œํ‚ค๋Š” ABA ๋ ˆ์ด์Šค๋ฅผ ์›์ฒœ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค. +2. **`multi-agent-mux-delegate-job` ๊ณตํ†ต ์ข…๋ฃŒ ํ—ฌํผ (`finalize_unfinished`) ๋„์ž…**: + - direct ๊ฒฝ๋กœ(`:227`) ๋ฐ loop/discuss ๊ฒฝ๋กœ(`:358`, `:375`)์— ๊ณตํ†ต ์ ์šฉ. + - ๊ตฌ๋…์ž ์ข…๋ฃŒ ์‹œ `wait "$sub_pid"` ์ˆ˜์ง‘ ๋ฐ ๋ฏธ์™„๋ฃŒ ์žก์— ๋Œ€ํ•ด `finalize_unfinished`๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ƒํƒœ๋ฅผ Fail-Closed ์ฒ˜๋ฆฌ. + - ๋ž˜ํผ ์ž์ฒด๋„ ์ž์‹ ๊ตฌ๋…์ž ํ”„๋กœ์„ธ์Šค๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ˆ˜๊ฑฐ(`kill "$sub_pid" 2>/dev/null; wait "$sub_pid"`)ํ•˜์—ฌ 3,600์ดˆ ๊ณ ์•„ ํ”„๋กœ์„ธ์Šค ๋ฐฉ์ง€. +3. **๋ธŒ๋กœ์ปค ์žฅ์• (`rc=3`)์™€ ์›Œ์ปค ์‹คํŒจ ๊ฒฉ๋ฆฌ (B-15 ๊ณ„์•ฝ ์ค€์ˆ˜)**: + - ๊ตฌ๋…์ž๊ฐ€ `rc=3`(`broker_unavailable`)์œผ๋กœ ์ข…๋ฃŒ๋˜์—ˆ์œผ๋‚˜ ๋””์Šคํฌ ์ƒ์— ์ด๋ฒคํŠธ๊ฐ€ ์‚ด์•„์žˆ์„ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š” ๊ฒฝ์šฐ, ์ฆ‰์‹œ ์‹คํŒจ๋กœ ๋‹จ์ •ํ•˜์ง€ ์•Š๊ณ  ์žก ๋ฐ๋“œ๋ผ์ธ๊นŒ์ง€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋ฅผ ์žฌํ™•์ธํ•˜์—ฌ ๋ธŒ๋กœ์ปค ์ผ์‹œ ์ˆœ๋‹จ์— ์˜ํ•œ ๊ฑฐ์ง“ ์‹คํŒจ๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. +4. **๋ฆฌํ…Œ์ธ ์ด๋ฒคํŠธ ์˜ค์—ผ ๋ฐฉ์ง€ (O-1)**: + - loop/discuss ๋ชจ๋“œ ์‹คํŒจ ์‹œ MQTT์— `error` ์ด๋ฒคํŠธ๋ฅผ retain ์ƒํƒœ๋กœ ๋ฐœํ–‰ํ•˜์ง€ ์•Š์•„, ์žฌ์‹œ๋„ ์‹œ ์ด์ „ ์—๋Ÿฌ ์ด๋ฒคํŠธ๊ฐ€ ์ฆ‰์‹œ ์žฌ์ˆ˜์‹ ๋˜๋Š” ํ˜„์ƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. +5. **์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ๋Ÿฌ๋„ˆ Fail-Fast ์—ฐ๊ฒฐ**: + - `lib_py/loop/runner.py:delegate_job`์ด ๋ž˜ํผ ์‹คํŒจ(rcโ‰ 0) ์‹œ์—๋„ `registered job:` ์ถœ๋ ฅ์„ ํŒŒ์‹ฑํ•˜์—ฌ `wait_for_job`์— ๋„˜๊ธฐ๋„๋ก ๋ณด์žฅ (๋””์Šคํฌ๊ฐ€ ์ด๋ฏธ `error`์ด๋ฏ€๋กœ 1์ดˆ ๋‚ด False ๋ฐ˜ํ™˜ํ•˜์—ฌ 65๋ถ„ ๊ต์ฐฉ ์›์ฒœ ์ฐจ๋‹จ). + +### 4.2 ๊ฒฐํ•จ โ‘ก ํ•ด๊ฒฐ์ฑ…: TUI ํ„ด ํ•ธ๋“œ์…ฐ์ดํฌ ์‹œํ€€์Šค ๋ฐ ์˜ˆ์‚ฐ ๋ณด๊ฐ• +1. **ํ•ธ๋“œ์…ฐ์ดํฌ ์‹คํ–‰ ์ˆœ์„œ (๊ตฌ๋…์ž ์‹œ์ž‘ ์ „ ๋ฐฐ์น˜)**: + - **์ค‘์š”**: `wait_for_agent_idle` ํ•ธ๋“œ์…ฐ์ดํฌ๋Š” ๋ฐ˜๋“œ์‹œ **๊ตฌ๋…์ž(`job_subscriber.py`) ๊ธฐ๋™ ์ „**์— ์™„๋ฃŒ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + - ๊ตฌ๋…์ž ์‹คํ–‰ ํ›„์— ๋Œ€๊ธฐํ•  ๊ฒฝ์šฐ, ์—์ด์ „ํŠธ์˜ ์œ ํœด ๋Œ€๊ธฐ ์‹œ๊ฐ„(15~30์ดˆ)์ด ๊ตฌ๋…์ž์˜ 120์ดˆ idle timeout ์˜ˆ์‚ฐ์„ ์ž ์‹(์ž๊ฐ€ ํฌ์‹)ํ•˜๋Š” ๊ฒฐํ•จ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. +2. **์œ ํœด ๋Œ€๊ธฐ ์˜ˆ์‚ฐ ํ™•๋Œ€ (15์ดˆ โ†’ 120์ดˆ)**: + - Codex ๋“ฑ ๋ณต์žกํ•œ ๋งˆํฌ๋‹ค์šด/๋„๊ตฌ ์ถœ๋ ฅ์„ ๋ Œ๋”๋งํ•˜๋Š” ๋Œ€ํ˜• ๋ชจ๋ธ์€ `completed` ์ด๋ฒคํŠธ ํ›„ TUI๊ฐ€ ์™„์ „ํžˆ `done`์œผ๋กœ ๋ณต๊ท€ํ•˜๋Š” ๋ฐ ์‹ค์ธก 31์ดˆ๊ฐ€ ๊ฑธ๋ ธ์Šต๋‹ˆ๋‹ค. + - ๋”ฐ๋ผ์„œ 15์ดˆ ํ•˜๋“œ์ฝ”๋”ฉ ๋Œ€์‹  ๊ธฐ๋ณธ 120์ดˆ(`SKS_IDLE_TIMEOUT_MS=120000`)๋ฅผ ์ ์šฉํ•˜๊ณ  ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์œ ์—ฐํ•˜๊ฒŒ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. +3. **Herdr ๋„ค์ดํ‹ฐ๋ธŒ ๋Œ€๊ธฐ API ์šฐ์„  ์‚ฌ์šฉ ๋ฐ Fallback ๊ฒฌ๊ณ ํ™”**: + - `herdr agent wait --until idle --until done --timeout "$SKS_IDLE_TIMEOUT_MS"`๋ฅผ ์šฐ์„  ํ˜ธ์ถœ. + - `lib_py.loop.watcher.wait_once` ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์žฌ์‚ฌ์šฉํ•˜์—ฌ ์‰˜ ์ค‘๋ณต ๊ตฌํ˜„์„ ๋ฐฉ์ง€. + - ํ—ค๋“œ๋ฆฌ์Šค ํ™˜๊ฒฝ ๋“ฑ `agent_status`๊ฐ€ `unknown`์ธ ํ™˜๊ฒฝ์—์„œ๋Š” ๊ธฐ์กด์˜ `_pane_quiescent` ์ •๋ฐ€ ํœด๋ฆฌ์Šคํ‹ฑ์œผ๋กœ ์šฐ์•„ํ•˜๊ฒŒ Fallback. +4. **์ˆ˜๋ฝ ํ™•์ธ ๋ฐ ์ด์ „ ํ„ด ์ž”์—ฌ ํ† ํฐ ์˜คํŒ ๋ฐฉ์ง€**: + - ํ‚ค ์ „์†ก ํ›„ `herdr agent prompt --wait --until working` ์ˆ˜๋ฝ ํ™•์ธ. + - Fallback ๊ฒฝ๋กœ์—์„œ ์ด์ „ ํ„ด์˜ ์ž”์—ฌ busy ํ† ํฐ(`esc to interrupt` ๋“ฑ)์ด ํ‚ค ์ „์†ก ์ „ ์บก์ฒ˜ ํ™”๋ฉด์— ์ด๋ฏธ ์กด์žฌํ–ˆ๋˜ ๊ฒฝ์šฐ, ์ด๋ฅผ ์ƒˆ ์ž…๋ ฅ์˜ ์ž‘์—… ์‹œ์ž‘ ๊ทผ๊ฑฐ๋กœ ์˜คํŒํ•˜์ง€ ์•Š๋„๋ก ์‚ฌ์ „ ์Šค๋ƒ…์ƒท๊ณผ ๋น„๊ต ๊ฒ€์ฆ. + +--- + +## 5. ๋‹จ๊ณ„๋ณ„ ์‹คํ–‰ ๊ณ„ํš (5-Phase Roadmap) + +- **Phase 1: ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ CAS ํ‘œ์ค€ ์—ฐ๋™ ๋ฐ delegate-job Fail-Closed ๊ตฌํ˜„** + - `engine.py` `deadline_timeout` ํ˜ธ์ถœ ๊ฒฝ๋กœ ํ™•๋ฆฝ + - `multi-agent-mux-delegate-job` ๋‚ด `direct`(:227) ๋ฐ `loop/discuss` ์ข…๋ฃŒ ์ •๋ฆฌ๋ถ€ ์ผ์›ํ™” +- **Phase 2: `send_keys_safe` ์‚ฌ์ „ ์œ ํœด ํ•ธ๋“œ์…ฐ์ดํฌ(Pre-Subscriber Idle Wait) ๊ตฌํ˜„** + - `SKS_IDLE_TIMEOUT_MS` ๊ธฐ๋ณธ 120์ดˆ ์ง€์› + - `herdr agent wait` 1์ฐจ ๊ฒŒ์ดํŠธ ๋ฐ ์ž”์—ฌ ํ† ํฐ ์˜คํŒ ๋ฐฉ์ง€ +- **Phase 3: ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ๋Ÿฌ๋„ˆ Fail-Fast ๋ฐฉ์–ด๋ง‰ ๋ณด๊ฐ•** + - `lib_py/loop/runner.py` ์˜ˆ์™ธ ๋ฐ ๋น„์ •์ƒ ์ข…๋ฃŒ ์‹œ ์กฐ๊ธฐ ์ˆ˜๊ฑฐ ์—ฐ๊ฒฐ +- **Phase 4: ๋‹จ์œ„/๊ณ„์•ฝ ๊ฒฉ๋ฆฌ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ์ŠˆํŠธ ๊ตฌ์ถ•** + - `tests/test_issue6_fail_closed.py` + - `tests/test_issue6_turn_handshake.py` + - ๊ธฐ์กด 750๊ฐœ pytest 100% ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ +- **Phase 5: 4๊ฐœ ์‹ค๊ธฐ๋™ ์—์ด์ „ํŠธ(Claude, Grok, Codex, OpenCode) ๋Œ€์ƒ E2E ๊ฒ€์ฆ** + - ํƒ€์ž„์•„์›ƒ ์ฃผ์ž… ๋ฐ TUI ์—ฐ์† ํ”„๋กฌํ”„ํŠธ ์ฃผ์ž… ์‹ค์ธก ํ†ต๊ณผ ํ™•์ธ + +--- + +## 6. ๊ฒฐ๋ก  + +Issue #6์€ ์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ๊ตฌ์กฐ์  ๊ฒฐํ•จ์ด๋ผ๊ธฐ๋ณด๋‹ค, **๋น„๋™๊ธฐ ์ด๋ฒคํŠธ ๋ถ„์‚ฐ ํ™˜๊ฒฝ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ƒํƒœ ๋ถˆ์ผ์น˜(Event vs TUI)์™€ ๋น„์ •์ƒ ์ข…๋ฃŒ ์‹œ์˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ˆ„๋ฝ(Fail-Silent)**์ด ๋งž๋ฌผ๋ ค ๋ฐœ์ƒํ•œ ์ „ํ˜•์ ์ธ ๋ถ„์‚ฐ ์‹œ์Šคํ…œ ๋ ˆ์ด์Šค ์ปจ๋””์…˜์ž…๋‹ˆ๋‹ค. + +๋ชจ๋“  ๋ฆฌ๋ทฐ์–ด๊ฐ€ ์ง„๋‹จ๊ณผ ํ•ด๊ฒฐ์ฑ…์— ์™„์ „ํžˆ ํ•ฉ์˜ํ•˜์˜€๊ณ , 2์ฐจ ๋ผ์šด๋“œ๋ฅผ ํ†ตํ•ด ์—ฃ์ง€์ผ€์ด์Šค๊นŒ์ง€ ๋ฐฉ์–ด ์‚ฌ์–‘์ด ์™„๋ฒฝํžˆ ์ˆ˜๋ฆฝ๋˜์—ˆ์œผ๋ฏ€๋กœ ๋ณธ Rev.2 ์‚ฌ์–‘์— ๋”ฐ๋ผ ์ฆ‰์‹œ ๊ตฌํ˜„์— ์ฐฉ์ˆ˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. diff --git a/.agents/reports/planner-reviewer-claude-01/plan-f4873d82.md b/.agents/reports/planner-reviewer-claude-01/plan-f4873d82.md new file mode 100644 index 0000000..8ef9f21 --- /dev/null +++ b/.agents/reports/planner-reviewer-claude-01/plan-f4873d82.md @@ -0,0 +1,242 @@ +# ๐Ÿ“‘ Issue #6 Implementation Plan (Phase 1โ€“5 Execution & Gate Specification) + +- **Document Version**: Rev.2 (Planner `planner-reviewer-claude-01`, job `f4873d82` โ€” Creator `creator-agy-01` ์ด์˜์ œ๊ธฐ 3๊ฑด ๋ฐ˜์˜; Rev.1์€ job `d2d70f57`) +- **Issue**: [Gitea Issue #6 โ€” `[Bug] delegate-job ์›Œ์ปค ์‹คํŒจ ์‹œ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋ฏธ๊ฐฑ์‹ (Fail-Silent) ๋ฐ TUI ํ„ด ๋งˆ๊ฐ ๋ ˆ์ด์Šค ์ปจ๋””์…˜ ํ•ด๊ฒฐ`](https://git.godopu.com/tmpl/multi-agent-mux/issues/6) +- **Source of Truth**: `.agents/reports/issue_6_consolidated_review_report.md` (Rev. 2) โ€” 4์ธ ๋ฆฌ๋ทฐ์–ด ํ•ฉ์˜ ์‚ฌ์–‘ +- **Baseline Commit**: `f98b958` (`main`, v4.4.0), Herdr `0.8.2`, Python 3.9.6, ์ „์ฒด ์Šค์œ„ํŠธ 750 passed +- **Status**: ์ฐฉ์ˆ˜ ์ „ (Phase 1 ๋Œ€๊ธฐ) + +--- + +## 1. ๊ฐœ์š” ๋ฐ ์„ค๊ณ„ ์›์น™ + +๋ณธ ๋ฌธ์„œ๋Š” Rev.2 ํ•ฉ์˜ ์‚ฌ์–‘(ยง4)์„ **ํŒŒ์ผยทํ•จ์ˆ˜ยท๋ผ์ธ ๋‹จ์œ„์˜ ์‹คํ–‰ ๊ณ„ํš**์œผ๋กœ ์ „๊ฐœํ•˜๊ณ , ๋‹จ๊ณ„๋ณ„ ์™„๋ฃŒ ๊ฒŒ์ดํŠธยท๋กค๋ฐฑยท๋น„๋ฒ”์œ„๋ฅผ ๊ทœ์ •ํ•œ๋‹ค. ๋ชจ๋“  ๋ผ์ธ ๋ฒˆํ˜ธ๋Š” ๊ธฐ์ค€ ์ปค๋ฐ‹ `f98b958` ๊ธฐ์ค€์ด๋ฉฐ, ์ฐฉ์ˆ˜ ์‹œ `grep -n`์œผ๋กœ ์žฌํ™•์ธํ•œ๋‹ค. + +### ๐Ÿ“Œ ํ•ต์‹ฌ ์›์น™ (Rev.2 ยง4 ํ•ฉ์˜) +1. **๊ธฐ์กด CAS ์—”์ง„ ์žฌ์‚ฌ์šฉ**: ํƒ€์ž„์•„์›ƒ fail-closed๋Š” `engine.py:50`์˜ `deadline_timeout` ์š”์ฒญ(๋น„-operator, terminal ์ดํ›„ ๊ฑฐ๋ถ€)์œผ๋กœ๋งŒ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์‹ ๊ทœ `--expect-status` ํ”Œ๋ž˜๊ทธ์™€ operator `error` ์˜ค๋ฒ„๋ผ์ด๋“œ๋Š” ๋„์ž…ํ•˜์ง€ ์•Š๋Š”๋‹ค. +2. **์‚ฌ์ „ ํ•ธ๋“œ์…ฐ์ดํฌ ์ˆœ์„œ**: TUI ์œ ํœด ๋Œ€๊ธฐ(`wait_for_agent_idle`)๋Š” **๊ตฌ๋…์ž(`job_subscriber.py`) ๊ธฐ๋™ ์ด์ „**์— ์™„๋ฃŒํ•œ๋‹ค. ๊ตฌ๋…์ž idle ์‹œ๊ณ„(`job_subscriber.py:265-267`, subscribe ์‹œ์  ๊ธฐ์‚ฐ)๋ฅผ ์ž ์‹ํ•˜์ง€ ์•Š๋Š”๋‹ค. +3. **์œ ํœด ์˜ˆ์‚ฐ 120์ดˆ**: `SKS_IDLE_TIMEOUT_MS` ๊ธฐ๋ณธ `120000`(์‹ค์ธก: codex `completed`โ†’`done` 31s). +4. **๋ธŒ๋กœ์ปค ์žฅ์•  ๊ฒฉ๋ฆฌ (B-15 ๊ณ„์•ฝ)**: ๊ตฌ๋…์ž `rc=3`์€ ์›Œ์ปค ์‹คํŒจ๊ฐ€ ์•„๋‹ˆ๋‹ค. ์žก ๋ฐ๋“œ๋ผ์ธ๊นŒ์ง€ ๋””์Šคํฌ๋ฅผ ์žฌ๊ด€์ธกํ•œ ๋’ค์—๋งŒ `deadline_timeout`์„ ์ ์šฉํ•œ๋‹ค. +5. **๋ฆฌํ…Œ์ธ ์˜ค์—ผ ๋ฐฉ์ง€ (O-1)**: loop/discuss ๊ฒฝ๋กœ์—์„œ๋Š” ๋ž˜ํผ๊ฐ€ MQTT `error` ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค(๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ „์ด๋งŒ). direct ๊ฒฝ๋กœ๋Š” ๊ฐ์‚ฌยท๋ชจ๋‹ˆํ„ฐ์šฉ์œผ๋กœ ๋ฐœํ–‰ํ•œ๋‹ค. +6. **๋กค๋ฐฑ ์Šค์œ„์น˜**: ๊ฐ ํ–‰์œ„ ๋ณ€๊ฒฝ์€ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ฆ‰์‹œ ์ด์ „ ๋™์ž‘์œผ๋กœ ๋ณต๊ท€ ๊ฐ€๋Šฅํ•ด์•ผ ํ•œ๋‹ค (Issue #5 ๊ด€๋ก€). +7. **๋ฏธ์ฃผ์ž… ์‹คํŒจ๋Š” `error`๊ฐ€ ์•„๋‹ˆ๋‹ค (Rev.2 Secondary)**: ํ”„๋กฌํ”„ํŠธ๊ฐ€ ํ•œ ๊ธ€์ž๋„ ์ „๋‹ฌ๋˜์ง€ ์•Š์€ ์‹คํŒจ(ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธยท์œ ํœด ๋Œ€๊ธฐ ํƒ€์ž„์•„์›ƒ)๋Š” `deadline_timeout`์ด ์•„๋‹ˆ๋ผ **`cancelled`**(detail `session_busy`)๋กœ ๋‹ซ๊ณ  ๋ž˜ํผ rc=5๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์žฌ์Šค์ผ€์ค„ ๊ฐ€๋Šฅ์„ฑ์„ ๋ณด์กดํ•˜๋˜ ๋ ˆ์ฝ”๋“œ๋Š” ๋น„์ข…๋ฃŒ ์ƒํƒœ๋กœ ๋‚จ๊ธฐ์ง€ ์•Š๋Š”๋‹ค. +8. **์…ธโ†’Python ๊ฒฝ๊ณ„์—์„œ herdr ๊ฒฉ๋ฆฌ ๋ช…์‹œ ์ „๋‹ฌ (Rev.2 Primary)**: Python ์ชฝ `herdr` ํ˜ธ์ถœ์€ ๋ฐ˜๋“œ์‹œ `--session "$HERDR_SESSION_NAME"`์„ ๋ช…์‹œํ•˜๊ณ , ์…ธ ์ชฝ ํ˜ธ์ถœ์€ `mam_herdr`/`_sks_herdr`๋งŒ ์‚ฌ์šฉํ•œ๋‹ค. ์ธํ„ฐํ”„๋ฆฌํ„ฐ๋Š” ํ•ญ์ƒ `"$(_delegate_py_bin)"`์ด๋‹ค. + +### ๐Ÿ“Œ ์‹ค์ธก์œผ๋กœ ํ™•์ •๋œ ์‚ฌ์‹ค (๊ณ„ํš์˜ ์ „์ œ) +| ์‚ฌ์‹ค | ๊ทผ๊ฑฐ | +|---|---| +| direct ๊ฒฝ๋กœ `wait "$sub_pid" \|\| true`๋Š” `:227`, loop ์›Œ์ปค/๋ฆฌ๋ทฐ์–ด ์กฐ๊ธฐ ํƒˆ์ถœ์€ `:359`/`:375` | `grep -n` (1์ฐจ ๋ณด๊ณ ์„œ์˜ `:213`์€ ์˜ค๊ธฐ) | +| ๋ช…์‹œ์  ์ฃผ์ž… ์‹คํŒจ(`send_keys_safe` rcโ‰ 0)๋Š” `set -e`๋กœ exit 1 + EXIT trap(`:523`)์ด `error` ๋ฐœํ–‰ โ†’ **๊ธฐ๋ณธ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์—์„œ๋Š” ์ด๋ฏธ fail-closed** | ์‹ค์ œ ๋ž˜ํผ์— rc=4 ์ฃผ์ž… ์‹ค์ธก | +| ๋‹จ, trap ๋ช…๋ น์— `--registry-dir`๊ฐ€ ์—†์–ด ์ปค์Šคํ…€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์—์„œ๋Š” `job record not found`๋กœ `pending` ์ž”์กด | ๋™์ผ ์‹ค์ธก | +| ๋ž˜ํผ exit ํ›„ ๊ตฌ๋…์ž ํ”„๋กœ์„ธ์Šค๊ฐ€ ๊ณ ์•„๋กœ ์ž”์กด | ๋™์ผ ์‹ค์ธก (`pgrep job_subscriber.py`) | +| `deadline_timeout`: pendingโ†’error ์ปค๋ฐ‹, completedโ†’`conflicting error after terminal completed; refusing` | `engine.transition` ์ง์ ‘ ํ˜ธ์ถœ ์‹ค์ธก | +| ์žก ์™„๋ฃŒ ํ›„ ์—์ด์ „ํŠธ ์ƒํƒœ๋Š” `done`(idle ์•„๋‹˜); working ์ค‘์—๋„ `interactive_ready: true`, `โฏ` ๋ Œ๋”๋ง | `herdr agent list`/ํŽ˜์ธ ์บก์ฒ˜ ์‹ค์ธก | +| `herdr agent wait`๋Š” ์ด๋ฏธ ๋ชฉํ‘œ ์ƒํƒœ๋ฉด ์ฆ‰์‹œ rc=0, ์•„๋‹ˆ๋ฉด timeout ์‹œ rc=1 + stderr JSON | ์‹ค์ธก, `lib_py/loop/watcher.py` ๋ชจ๋“ˆ docstring๊ณผ ์ผ์น˜ | +| `dispatch_token`์€ `claim`/`redispatch`์—์„œ๋งŒ ๋ฐœ๊ธ‰(`engine.py:87,297-300`) โ†’ direct ์žก์€ ํ† ํฐ ์—†์Œ, loop 2ํšŒ์ฐจ๋ถ€ํ„ฐ ์กด์žฌ | ์ฝ”๋“œ | +| **์‹ค์ œ `herdr` ๋ฐ”์ด๋„ˆ๋ฆฌ๋Š” `HERDR_SESSION`ยท`HERDR_SOCKET_PATH` ํ™˜๊ฒฝ๋ณ€์ˆ˜์™€ `--session ` ํ”Œ๋ž˜๊ทธ๋งŒ ์ธ์‹ํ•˜๋ฉฐ, MAM์˜ `HERDR_SESSION_NAME`์€ ์ธ์‹ํ•˜์ง€ ์•Š๋Š”๋‹ค.** `HERDR_SESSION_NAME`์€ `lib.sh`์˜ `herdr()`/`mam_herdr`/`_real_herdr`(`lib.sh:234-243`)๊ฐ€ `--session`์œผ๋กœ ๋ฒˆ์—ญํ•˜๋Š” ์…ธ ํ•จ์ˆ˜ ์ „์šฉ ๋ณ€์ˆ˜๋ผ **Python `subprocess`์—๋Š” ์ƒ์†๋˜์ง€ ์•Š๋Š”๋‹ค** (Rev.2 Primary-A) | `env -u โ€ฆ herdr agent get` 5์กฐํ•ฉ ์‹ค์ธก: `HERDR_SESSION_NAME`๋งŒ โ†’ `server_not_running`(๊ธฐ๋ณธ ์†Œ์ผ“), `HERDR_SESSION`/`HERDR_SOCKET_PATH`/`--session` โ†’ ์ •์ƒ | +| `lib.sh`๋ฅผ sourceํ•œ ์…ธ์—์„œ๋„ Python `subprocess.run(["herdr", โ€ฆ])`๋Š” `/opt/homebrew/bin/herdr`๋กœ ํ•ด์„๋˜์–ด ๊ธฐ๋ณธ ์†Œ์ผ“์„ ์กฐํšŒํ•œ๋‹ค | ๋™์ผ ์‹ค์ธก (f) | +| **loop ๋ชจ๋“œ ์›Œ์ปคโ†’๋ฆฌ๋ทฐ์–ด ์ „ํ™˜์€ v4.4.0์—์„œ ์ด๋ฏธ ๊นจ์ ธ ์žˆ๋‹ค**: `:276-283`์˜ `update --status pending --iteration $iteration`์ด ์›Œ์ปค `completed` ํ›„ ๊ฐ™์€ iteration์œผ๋กœ `redispatch`๋ฅผ ์š”์ฒญ โ†’ `redispatch iteration 1 does not advance past 1` rc=4 โ†’ `set -e`๋กœ ๋ž˜ํผ ์ข…๋ฃŒ (Rev.2 Tertiary) | `registry.py` CLI ์‹ค์ธก (iteration 2๋ฉด rc=0) | +| `cancelled` ์š”์ฒญ์€ ๋น„-operator ์†Œ์Šค๋กœ pendingโ†’cancelled ์ปค๋ฐ‹, completed ์ดํ›„ ๊ฑฐ๋ถ€, ์ดํ›„ operator `redispatch`(iteration ์ „์ง„)๋กœ ์žฌ๊ฐœ ๊ฐ€๋Šฅ | `engine.transition` ์‹ค์ธก | + +--- + +## 2. ๋‹จ๊ณ„๋ณ„ ๊ตฌํ˜„ ๋‹จ์œ„ ๋ฐ ํ•„์ˆ˜ ์™„๋ฃŒ ๊ธฐ์ค€ (Roadmap & Gates) + +| ๋‹จ๊ณ„ | ์ž‘์—… ๋ชฉํ‘œ | ์„ธ๋ถ€ ๊ตฌํ˜„ (ยง3 ์ฐธ์กฐ) | ํ•„์ˆ˜ ์™„๋ฃŒ ๊ฒŒ์ดํŠธ | ๋กค๋ฐฑ | +|---|---|---|---|---| +| **Phase 1** | **๋ ˆ์ง€์ŠคํŠธ๋ฆฌ CAS ํ‘œ์ค€ ์—ฐ๋™ ๋ฐ delegate-job Fail-Closed** | โ€ข `registry.py close --request deadline_timeout\|cancelled` ์„œ๋ธŒ์ปค๋งจ๋“œ
โ€ข ๋ž˜ํผ `finalize_unfinished`/`reap_subscriber` ํ—ฌํผ, direct(`:227`)ยทloop(`:359`,`:375`)ยท`run_agent` ์‹คํŒจยทํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์‹คํŒจ 5๊ฐœ ์ข…๋ฃŒ ๊ฒฝ๋กœ ์ผ์›ํ™”
โ€ข rc=3 ๋””์Šคํฌ ์žฌ๊ด€์ธก, trap `--registry-dir`, dry-run ์ˆœ์„œ, ์ข…๋ฃŒ ์ฝ”๋“œ ๊ณ„์•ฝ(rc 5 `session_busy` ํฌํ•จ)
โ€ข **loop ์—ญํ•  ์ „ํ™˜ `attempt_id` ๊ทœ์•ฝ** (ยง3.1.6) | โ€ข `--agent human` ๊ฒฐ์ •์  ์žฌํ˜„ 4์ข…(ยง5 T1-1~T1-4)์ด ์ˆ˜์ • ์ „ ์‹คํŒจยท์ˆ˜์ • ํ›„ ํ†ต๊ณผ
โ€ข T1-10(์›Œ์ปค ์™„๋ฃŒโ†’๋ฆฌ๋ทฐ์–ด ์žฌ๋””์ŠคํŒจ์น˜ ์„ฑ๊ณต) ์ˆ˜์ • ์ „ rc=4ยท์ˆ˜์ • ํ›„ rc=0
โ€ข `test_wrapper_exit_code_mapping_literals_are_frozen`ยทF-A 3์ข… ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ
โ€ข `MAM_DELEGATE_FAIL_CLOSED=0`์—์„œ ๊ธฐ์กด exit 0 ๋™์ž‘ ์žฌํ˜„ | `MAM_DELEGATE_FAIL_CLOSED=0` (ํ—ฌํผ no-op, `\|\| true` ๋ณต์›) | +| **Phase 2** | **์‚ฌ์ „ TUI ์œ ํœด ํ•ธ๋“œ์…ฐ์ดํฌ ๋ฐ ์ œ์ถœ ๊ฒ€์ฆ ๋ณด๊ฐ•** | โ€ข `lib_py/loop/watcher.py` CLI ์ง„์ž…์ (`"$(_delegate_py_bin)" -m lib_py.loop.watcher wait โ€ฆ --session "$HERDR_SESSION_NAME"`)
โ€ข `lib.sh: wait_for_agent_idle` (์‚ฌ์ „ `mam_herdr agent get` โ†’ `unknown`/`blocked` ๋ถ„๊ธฐ โ†’ watcher CLI, `SKS_IDLE_RETRIES` ๋ฐฑ์˜คํ”„ ์žฌ์‹œ๋„, ์ตœ์ข… ์‹คํŒจ rc 1 โ†’ ๋ž˜ํผ rc 5 `cancelled`)
โ€ข ๋ž˜ํผ `run_agent`๋ฅผ `preflight_agent`+`inject_agent`๋กœ ๋ถ„๋ฆฌ, ์ˆœ์„œ: preflight โ†’ idle wait โ†’ subscriber โ†’ inject
โ€ข `send_keys_safe` ํด๋ฐฑ ์ œ์ถœ ๊ฒ€์ฆ์—์„œ pre-submit busy ํ† ํฐ ๋ฐฐ์ œ, `agent prompt --wait --until working` ์ˆ˜๋ฝ ํ™•์ธ(์žฌ์ œ์ถœ ๊ธˆ์ง€) | โ€ข mock_herdr ์ƒํƒœ ์‹œํ€€์Šค ํ…Œ์ŠคํŠธ ยง5 T2-1~T2-11 ํ†ต๊ณผ
โ€ข `test_h16`(Enter ์ •ํ™•ํžˆ 1ํšŒ)ยท`test_b8`ยท`test_c1_*` ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ
โ€ข ๋ž˜ํผ ๋กœ๊ทธ์—์„œ `SUBSCRIBED` ์ด์ „์— idle ๋Œ€๊ธฐ ์™„๋ฃŒ ๋กœ๊ทธ๊ฐ€ ์ถœ๋ ฅ๋จ
โ€ข T2-10: watcher CLI argv์— `--session` ํฌํ•จ, T2-11: `lib.sh`์— bare `python`/`python3 -m lib_py.loop` ํ˜ธ์ถœ 0๊ฑด(์ •์  ๋ฆฐํŠธ) | `SKS_IDLE_HANDSHAKE=0` (๊ฒŒ์ดํŠธ ์Šคํ‚ต, ๊ธฐ์กด `_pane_quiescent`๋งŒ ์‚ฌ์šฉ), `SKS_PROMPT_WAIT=0` | +| **Phase 3** | **์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ๋Ÿฌ๋„ˆ Fail-Fast ์—ฐ๊ฒฐ** | โ€ข `runner.delegate_job`: rcโ‰ 0์—์„œ๋„ `registered job:` ํŒŒ์‹ฑ, `created_jobs`ยทcheckpoint ๊ธฐ๋ก, id ๋ฐ˜ํ™˜
โ€ข **rc=5(`session_busy`)๋Š” `MAM_SESSION_BUSY_RETRY_S`(๊ธฐ๋ณธ 30) ๋Œ€๊ธฐ ํ›„ 1ํšŒ ์žฌ์œ„์ž„(์ƒˆ job id)**, ์žฌ์‹คํŒจ ์‹œ None
โ€ข ํ˜ธ์ถœ๋ถ€๋Š” `wait_for_job`์ด ๋””์Šคํฌ `error`/`cancelled`๋กœ 1s ๋‚ด False
โ€ข `run_loop.sh` ๋ ˆ๊ฑฐ์‹œ `delegate_job_safe` ๋™์ผ ํŒŒ์‹ฑ(`MAM_LOOP_PY=0` ๊ฒฝ๋กœ ๋ณด์กด) | โ€ข `LoopRunner.wait_for_job`์ด fail-closed ์žก์—์„œ โ‰ค 2s ๋ฐ˜ํ™˜(T3-1)
โ€ข ์‹คํŒจ job id๊ฐ€ `.mam/loop-checkpoint.json`์— ๊ธฐ๋ก(T3-2)
โ€ข rc=5 1ํšŒ ์žฌ์œ„์ž„ ํ›„ ์„ฑ๊ณต/์‹คํŒจ ๋ถ„๊ธฐ(T3-3)
โ€ข `test_issue5_phase3_loop.py` ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ | ํ•จ์ˆ˜ ๋‹จ์œ„ revert (ํ”Œ๋ž˜๊ทธ ๋ถˆํ•„์š”: rcโ‰ 0 ๊ฒฝ๋กœ๋งŒ ๋ณ€๊ฒฝ) | +| **Phase 4** | **๋‹จ์œ„/๊ณ„์•ฝ ํšŒ๊ท€ ์Šค์œ„ํŠธ** | โ€ข `tests/test_issue6_fail_closed.py`, `tests/test_issue6_turn_handshake.py`
โ€ข `tests/conftest.py` mock_herdr์— `agent_status`ยท`agent wait`ยท`agent prompt --wait` ๋ชจํ‚น
โ€ข ๋ฌธ์„œ: SKILL.md ์ข…๋ฃŒ ์ฝ”๋“œ ๊ณ„์•ฝ, MESSAGING.md ยง5.1 ๋ณด๊ฐ• | โ€ข ์‹ ๊ทœ ํ…Œ์ŠคํŠธ ์ „๋ถ€ **์ˆ˜์ • ์ „ ์ฝ”๋“œ์—์„œ ์‹คํŒจํ•จ์„ ์—ญ๊ฒ€์ฆ**(Issue #5 ๊ด€๋ก€)
โ€ข ์ „์ฒด ์Šค์œ„ํŠธ 750 + ์‹ ๊ทœ โ‰ฅ 20, 0-Fail
โ€ข `git diff --check` ํด๋ฆฐ | ํ…Œ์ŠคํŠธ ํŒŒ์ผ ์‚ญ์ œ๋งŒ์œผ๋กœ ์›๋ณต | +| **Phase 5** | **์‹ค๊ธฐ๋™ 4๊ฐœ ์—์ด์ „ํŠธ E2E ๋ฐ ๋ฆด๋ฆฌ์Šค** | โ€ข Claude/Grok/Codex/OpenCode ๊ฐ 1ํšŒ: `completed` ์งํ›„ ๋™์ผ ์„ธ์…˜ ์—ฐ์† ์œ„์ž„ ํƒ€์ž„๋ผ์ธ ๊ธฐ๋ก
โ€ข ํƒ€์ž„์•„์›ƒ ์ฃผ์ž…(์—์ด์ „ํŠธ ๋ฌด์‘๋‹ต)์œผ๋กœ 65๋ถ„ ๋Œ€์‹  โ‰ค `IDLE_TIMEOUT`+5s ๋‚ด `error` ํ™•์ธ
โ€ข ์ž”์กด ๋ ˆ์ฝ”๋“œ 3๊ฑด ํ์‡„, IMPROVEMENTS.md ์›์žฅ, VERSIONS.md `v4.4.1` 4-way ๋ฝ์Šคํ… | โ€ข 4๊ฐœ ๋ฐฑ์—”๋“œ ์—ฐ์† ์œ„์ž„ ์ฃผ์ž… ์‹คํŒจ 0๊ฑด, ์ค‘๋ณต ์ œ์ถœ 0๊ฑด
โ€ข `test_version_consistency.py` ํ†ต๊ณผ
โ€ข ๋ฆฌ๋ทฐ์–ด ๊ต์ฐจ ๊ฒ€ํ†  `[VERDICT: PASS]` | ๋ฆด๋ฆฌ์Šค ํƒœ๊ทธ ๋ฏธ๋ถ€์—ฌ ์ƒํƒœ๋กœ `main` revert | + +--- + +## 3. ์ƒ์„ธ ๊ตฌํ˜„ ์‚ฌ์–‘ + +### 3.1 Phase 1 โ€” Fail-Closed + +**3.1.1 `lib_py/delegation/registry.py` โ€” `close` ์„œ๋ธŒ์ปค๋งจ๋“œ (์‹ ๊ทœ, Rev.2: `timeout`์„ ์ผ๋ฐ˜ํ™”)** +```text +registry.py [--registry-dir D] close --job --request {deadline_timeout,cancelled} [--detail ] [--expect-dispatch-token ] +``` +- ๊ตฌํ˜„: `engine.transition(job_id, request, registry_dir=rd, source="delegate_wrapper", fields={"detail": detail}, expected_dispatch_token=tok_or_None)`. ๋‘ ์š”์ฒญ ๋ชจ๋‘ ๋น„-operator์ด๋ฉฐ terminal ์ดํ›„ ๊ฑฐ๋ถ€๋œ๋‹ค(์‹ค์ธก). +- ์šฉ๋„ ๊ตฌ๋ถ„: `deadline_timeout` = ํ”„๋กฌํ”„ํŠธ๊ฐ€ **์ „๋‹ฌ๋œ ๋’ค** ์ข…๋ฃŒ ์ด๋ฒคํŠธ ์—†์ด ๋ฐ๋“œ๋ผ์ธ ์†Œ์ง„(์‹คํ–‰ ์‹คํŒจ). `cancelled` + detail `session_busy` = ํ”„๋กฌํ”„ํŠธ๊ฐ€ **์ „๋‹ฌ๋˜๊ธฐ ์ „** ์‹คํŒจ(ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธยท์œ ํœด ๋Œ€๊ธฐ ํƒ€์ž„์•„์›ƒ; Rev.2 Secondary). ํ›„์ž๋Š” `redispatch`๋กœ ์žฌ๊ฐœ ๊ฐ€๋Šฅํ•˜๋‹ค. +- ์ข…๋ฃŒ ์ฝ”๋“œ: ์ปค๋ฐ‹ โ†’ 0; ๊ฑฐ๋ถ€ ์‚ฌ์œ ๊ฐ€ `conflicting โ€ฆ after terminal` ๋˜๋Š” ๋ฉฑ๋“ฑ โ†’ **0**(stderr์— ์‚ฌ์œ  1์ค„; ์ด๋ฏธ ๋‹ซํžŒ ์žก์€ ์ •์ƒ); ๋ ˆ์ฝ”๋“œ ์—†์Œ โ†’ 1; ํ† ํฐ ๋ถˆ์ผ์น˜ โ†’ 4. +- `status --set`๋Š” ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค(`_operator_write` ๋ฐ F-C ๋ฉฑ๋“ฑ ๊ณ„์•ฝ ๋ณด์กด). + +**3.1.2 ๋ž˜ํผ `multi-agent-mux-delegate-job` โ€” ๊ณตํ†ต ํ—ฌํผ (์‹ ๊ทœ, `run_agent` ์œ„์— ์ •์˜)** +```bash +# reap_subscriber โ€” ์ž๊ธฐ ๊ตฌ๋…์ž๋งŒ ํšŒ์ˆ˜. ์›๋ž˜ rc๋Š” ํ˜ธ์ถœ์ž๊ฐ€ ๋ณด์กด. +reap_subscriber() { kill "$1" 2>/dev/null || true; wait "$1" 2>/dev/null || true; } + +# finalize_unfinished [] +# completed -> no-op (rc 0) +# broker_unavailable -> ๋‚จ์€ ๋ฐ๋“œ๋ผ์ธ(TIMEOUT - ๊ฒฝ๊ณผ) ๋™์•ˆ 3s ๊ฐ„๊ฒฉ ๋””์Šคํฌ ์žฌ๊ด€์ธก; +# completed/error ๋ฐœ๊ฒฌ ์‹œ ๊ทธ ์ƒํƒœ ์ฑ„ํƒ, ์•„๋‹ˆ๋ฉด deadline_timeout +# ๊ทธ ์™ธ(timeout/error/running) -> ์ฆ‰์‹œ registry.py close --request deadline_timeout +# phase == preflight|idle_wait -> registry.py close --request cancelled --detail session_busy (rc 5) +# (ํ”„๋กฌํ”„ํŠธ ๋ฏธ์ „๋‹ฌ: deadline_timeout ๊ธˆ์ง€ โ€” Rev.2 Secondary) +# direct ๊ฒฝ๋กœ์—์„œ๋งŒ publish_event --event error|cancelled ์ถ”๊ฐ€ ๋ฐœํ–‰ (loop/discuss ๊ธˆ์ง€: O-1) +# MAM_DELEGATE_FAIL_CLOSED=0 ์ด๋ฉด ์ „๋ถ€ no-op (๋กค๋ฐฑ) +``` +- `disk_st` ์žฌ๊ด€์ธก์€ ๊ธฐ์กด `:342-347`์˜ ์ธ๋ผ์ธ ํŒŒ์ด์ฌ์„ ํ•จ์ˆ˜ `_disk_status`๋กœ ์ถ”์ถœํ•ด ์žฌ์‚ฌ์šฉ. +- `expected_dispatch_token`์€ **๊ตฌ๋…์ž ๊ธฐ๋™ ์ง์ „** ๋ ˆ์ฝ”๋“œ์—์„œ ์ฝ์–ด ์ง€์—ญ๋ณ€์ˆ˜์— ๋ณด๊ด€(`_disp_tok`), ์กด์žฌํ•  ๋•Œ๋งŒ ์ „๋‹ฌ(direct ์žก์€ ์—†์Œ). + +**3.1.3 ์ข…๋ฃŒ ๊ฒฝ๋กœ 5๊ฐœ ์ผ์›ํ™”** +| # | ์œ„์น˜ (f98b958) | ๋ณ€๊ฒฝ | +|---|---|---| +| E1 | direct `:227` `wait "$sub_pid" \|\| true` | `wait "$sub_pid" \|\| sub_rc=$?` โ†’ ๊ธฐ์กด `:340-354` ๋งคํ•‘ ๋ธ”๋ก์„ ํ•จ์ˆ˜ `_map_sub_rc`๋กœ ์ถ”์ถœ(๋ฆฌํ„ฐ๋Ÿด ๋ฌธ์ž์—ด ๋ณด์กด) โ†’ `finalize_unfinished` โ†’ `exit_code=$(_rc_for "$job_status")` | +| E2 | loop ์›Œ์ปค `:359-362` | `break` ์ง์ „ `finalize_unfinished "$JOB_ID" "$job_status" "$sub_rc" worker` | +| E3 | loop ๋ฆฌ๋ทฐ์–ด `:375-378` | ๋™์ผ (`reviewer`) | +| E4 | `run_agent` ์‹คํŒจ (`:327` ๋ฐ direct `:204`) | `if ! run_agent โ€ฆ; then reap_subscriber "$sub_pid"; finalize_unfinished "$JOB_ID" error 1 inject; exit 1; fi`. trap(`:523`)์€ `--registry-dir "$REGISTRY_DIR"` ์ถ”๊ฐ€ ํ›„ ์ตœํ›„ ๋ณด๋ฃจ๋กœ๋งŒ ์œ ์ง€ | +| E5 | ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์‹คํŒจ(`:456-473` herdr/์„ธ์…˜ ๋ถ€์žฌ, `:511-513` strict-role) ๋ฐ ์œ ํœด ๋Œ€๊ธฐ ์ตœ์ข… ์‹คํŒจ | Phase 2์—์„œ `preflight_agent`๋กœ ์ด๋™. ์‹คํŒจ ์‹œ ์žก ๋“ฑ๋ก ํ›„๋ผ๋ฉด `finalize_unfinished โ€ฆ preflight\|idle_wait` โ†’ `cancelled`(session_busy) ํ›„ **`exit 5`** | + +**3.1.4 ์ข…๋ฃŒ ์ฝ”๋“œ ๊ณ„์•ฝ (SKILL.md์— ๋ฌธ์„œํ™”)** +| rc | ์˜๋ฏธ | +|---|---| +| 0 | `completed` | +| 1 | `error` (์—์ด์ „ํŠธ ๋ฐœํ–‰ / ์ฃผ์ž…ยทํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์‹คํŒจ) | +| 2 | ๊ตฌ๋…์ž ํƒ€์ž„์•„์›ƒ โ†’ `deadline_timeout` ์ ์šฉ๋จ | +| 3 | ๋ธŒ๋กœ์ปค ์žฅ์•  + ๋ฐ๋“œ๋ผ์ธ ๋‚ด ๋””์Šคํฌ ๋ฏธํ™•์ธ โ†’ `deadline_timeout` ์ ์šฉ๋จ | +| 5 | `session_busy`: ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ/์œ ํœด ๋Œ€๊ธฐ ์‹คํŒจ๋กœ ํ”„๋กฌํ”„ํŠธ ๋ฏธ์ „๋‹ฌ โ†’ `cancelled` ์ ์šฉ๋จ (์žฌ์Šค์ผ€์ค„ ๊ฐ€๋Šฅ) | +๋งˆ์ง€๋ง‰ stdout ์ค„(๊ฐ์‚ฌ ๋กœ๊ทธ ๊ฒฝ๋กœ)์€ ๋ชจ๋“  ๊ฒฝ๋กœ์—์„œ ์œ ์ง€ํ•œ๋‹ค(`runner.delegate_job`/`run_loop.sh:373` ํŒŒ์‹ฑ ๊ณ„์•ฝ). + +**3.1.5 dry-run ์ˆœ์„œ (๊ตฌ๋…์ž ๊ธฐ๋™ `:166` โ†’ dry-run `return 0` `:224`)**: `DRY_RUN=1`์ด๋ฉด ๊ตฌ๋…์ž๋ฅผ ๊ธฐ๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค(ํ˜„์žฌ๋Š” ๊ธฐ๋™ ํ›„ `return 0`์œผ๋กœ ๊ณ ์•„ ๋ฐœ์ƒ). + +**3.1.6 loop/discuss ์—ญํ•  ์ „ํ™˜์˜ `attempt_id` ๊ทœ์•ฝ (Rev.2 Tertiary โ€” ๊ธฐ์กด ๊ฒฐํ•จ ์ˆ˜์ •)** +- ๋ฌธ์ œ: `:276-283`์€ ์›Œ์ปคยท๋ฆฌ๋ทฐ์–ด phase ๋ชจ๋‘ ๊ฐ™์€ `$iteration`์œผ๋กœ `update --status pending`(=`redispatch`)์„ ์š”์ฒญํ•œ๋‹ค. `engine.py:227-240`์€ terminal์—์„œ์˜ `redispatch`์— `iteration` ์ „์ง„์„ ์š”๊ตฌํ•˜๋ฏ€๋กœ ์›Œ์ปค `completed` ์งํ›„ ๋ฆฌ๋ทฐ์–ด ์ „ํ™˜์ด rc=4๋กœ ๊ฑฐ๋ถ€๋˜๊ณ  `set -e`๋กœ ๋ž˜ํผ๊ฐ€ ์ฃฝ๋Š”๋‹ค(์‹ค์ธก). F-A ํ…Œ์ŠคํŠธ(`test_issue5_phase2a_loop.py:727`)๋Š” iteration 2๋ฅผ ๋„˜๊ฒจ ์ด ๊ฒฝ๋กœ๋ฅผ ๋น„์ผœ๊ฐ„๋‹ค. +- ์„ค๊ณ„: `iteration`์€ ๋ฃจํ”„ ํšŒ์ฐจ ๋ผ๋ฒจ๋กœ ์œ ์ง€ํ•˜๊ณ , **๋””์ŠคํŒจ์น˜ ๋‹จ์กฐ ์นด์šดํ„ฐ `attempt_id`**๋ฅผ ๋„์ž…ํ•œ๋‹ค(์—”์ง„ `:171`์ด ์ด๋ฏธ `record.get("attempt_id", record.get("iteration"))`์œผ๋กœ ์ฝ๋Š” ํ•„๋“œ). + 1. ๋ž˜ํผ: ์žก๋ณ„ `attempt=$((attempt+1))`์„ ๊ฐ phase ๋””์ŠคํŒจ์น˜ ์ง์ „์— ์ฆ๊ฐ€์‹œํ‚ค๊ณ  `update โ€ฆ --attempt-id "$attempt"`๋ฅผ ํ•จ๊ป˜ ๋„˜๊ธด๋‹ค(์›Œ์ปค r1=1, ๋ฆฌ๋ทฐ์–ด r1=2, ์›Œ์ปค r2=3 โ€ฆ). + 2. `registry.py update`์— `--attempt-id` ์ถ”๊ฐ€(`:296-302`). + 3. `engine._decide` redispatch ๊ทœ์น™: `fields`์— `attempt_id`๊ฐ€ ์žˆ์œผ๋ฉด **`attempt_id`๊ฐ€ `record.attempt_id`(์—†์œผ๋ฉด `iteration`)๋ณด๋‹ค ์ปค์•ผ** ์ปค๋ฐ‹; ์—†์œผ๋ฉด ๊ธฐ์กด `iteration` ๊ทœ์น™ ์œ ์ง€(F-A 3์ข… ๋ฌด๋ณ€๊ฒฝ). `attempt_id`๋Š” `redispatch`/`claim` ์ปค๋ฐ‹ ์‹œ ๋ ˆ์ฝ”๋“œ์— ๊ธฐ๋ก. + 4. `publish_event.py:250-255`์˜ `--finalize-report` ๊ธฐ๋ณธ `attempt_id`๋Š” `record.attempt_id`๋ฅผ ์šฐ์„ ํ•˜๊ณ  ์—†์œผ๋ฉด `iteration`. +- ๋ฒ”์œ„ ์ฃผ์˜: ์ด ์ˆ˜์ •์œผ๋กœ ๋ฆฌ๋ทฐ์–ด ์ „ํ™˜์˜ CAS ๊ฑฐ๋ถ€๋Š” ์‚ฌ๋ผ์ง€์ง€๋งŒ, **loop ๋ชจ๋“œ์˜ ๋ฆฌ๋ทฐ์–ด phase๋Š” O-1(๋ฆฌํ…Œ์ธ ์žฌ์ˆ˜๋ฝ, ยง4)์— ์˜ํ•ด ์—ฌ์ „ํžˆ ์ฆ‰์‹œ ์ข…๋ฃŒ๋œ๋‹ค.** ๋”ฐ๋ผ์„œ Phase 5 E2E๋Š” direct ๋ชจ๋“œ๋งŒ ์ˆ˜ํ–‰ํ•˜๊ณ , loop ๋ชจ๋“œ ์ข…๋‹จ ๊ฒ€์ฆ์€ O-1 ์ด์Šˆ ์™„๋ฃŒ ํ›„๋กœ ๋ช…์‹œํ•œ๋‹ค. + +### 3.2 Phase 2 โ€” ์‚ฌ์ „ TUI ํ•ธ๋“œ์…ฐ์ดํฌ + +**3.2.1 `lib_py/loop/watcher.py` CLI ์ง„์ž…์  (์‹ ๊ทœ `main`)** +```text +"$(_delegate_py_bin)" -m lib_py.loop.watcher wait --session [--until S]... [--timeout-ms N] + stdout: {"kind": "matched|timeout|agent_not_found|transport_error", "agent_status": "...", "error_code": "..."} + rc: matched 0 / timeout 2 / agent_not_found 3 / transport_error 4 +``` +- `wait_once`๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ(์…ธ ์ค‘๋ณต ๊ตฌํ˜„ ๊ธˆ์ง€). `lib_py.delegation`์€ `lib_py.loop`๋ฅผ ์ž„ํฌํŠธํ•˜์ง€ ์•Š๋Š”๋‹ค(ํŒจํ‚ค์ง€ ๊ทœ์น™ ์œ ์ง€) โ€” ํ˜ธ์ถœ์ž๋Š” ์…ธ(`lib.sh`)์ด๋‹ค. +- **๊ฒฉ๋ฆฌ ์ „๋‹ฌ (Rev.2 Primary-A)**: `build_argv(target, until, timeout_ms, session=None)`์— `session` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ๊ฐ’์ด ์žˆ๊ณ  `default`๊ฐ€ ์•„๋‹ˆ๋ฉด argv๋ฅผ `["herdr", "--session", session, "agent", "wait", โ€ฆ]`๋กœ ๊ตฌ์„ฑํ•œ๋‹ค. CLI `--session`์€ ๋ฏธ์ง€์ • ์‹œ `HERDR_SESSION_NAME` ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ฝ๋Š”๋‹ค. ์‹ค์ œ `herdr`๋Š” `HERDR_SESSION_NAME`์„ ์ธ์‹ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ(์‹ค์ธก) ์ด ๋ช…์‹œ ์ „๋‹ฌ์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ ์†Œ์ผ“(`~/.config/herdr/herdr.sock`)์„ ์กฐํšŒํ•ด `server_not_running`/`agent_not_found`๊ฐ€ ๋œ๋‹ค. ๊ธฐ์กด `wait_once` ํ˜ธ์ถœ์ž(`shadow.py` ๋“ฑ)๋Š” `session=None`์œผ๋กœ ๋™์ž‘ ๋ถˆ๋ณ€. +- **์ธํ„ฐํ”„๋ฆฌํ„ฐ (Rev.2 Primary-B)**: bare `python`/`python3` ๊ธˆ์ง€. `lib.sh`๋Š” `"$(_delegate_py_bin)"`(`lib.sh:1841`)๋งŒ ์‚ฌ์šฉํ•˜๋ฉฐ `PYTHONPATH`๋Š” `lib.sh:25,164`๊ฐ€ ์ด๋ฏธ ๋ณด์žฅํ•œ๋‹ค. T2-11 ์ •์  ๋ฆฐํŠธ๋กœ ๊ณ ์ •. + +**3.2.2 `lib.sh: wait_for_agent_idle [timeout_ms]` (์‹ ๊ทœ, `send_keys_safe` ์œ„)** +1. `SKS_IDLE_HANDSHAKE=0` โ†’ return 0 (๋กค๋ฐฑ). +2. `_sks_herdr agent get `(=`mam_herdr`, ๊ฒฉ๋ฆฌ ์ธ์ง€ ์…ธ ํ•จ์ˆ˜)๋กœ `agent_status` ์กฐํšŒ: `unknown`/์กฐํšŒ ์‹คํŒจ โ†’ return 3(ํœด๋ฆฌ์Šคํ‹ฑ ํด๋ฐฑ ์‹ ํ˜ธ, ํ˜ธ์ถœ์ž๋Š” ๊ธฐ์กด `_pane_quiescent` ๊ฒฝ๋กœ ์ง„ํ–‰); `blocked` โ†’ return 2(์ฃผ์ž… ๊ธˆ์ง€; `_pane_dialog_open` ์ฒ˜๋ฆฌ๋กœ ์œ„์ž„). +3. `idle`/`done` โ†’ return 0. `working` โ†’ `"$(_delegate_py_bin)" -m lib_py.loop.watcher wait --session "$HERDR_SESSION_NAME" --until idle --until done --until blocked --timeout-ms ${SKS_IDLE_TIMEOUT_MS:-120000}`: matched(idle/done) โ†’ 0, matched(blocked) โ†’ 2, timeout โ†’ ์žฌ์‹œ๋„, ๊ทธ ์™ธ โ†’ 3. +4. **์žฌ์‹œ๋„ (Rev.2 Secondary)**: timeout์€ `SKS_IDLE_RETRIES`(๊ธฐ๋ณธ 2)ํšŒ๊นŒ์ง€ `SKS_IDLE_RETRY_BACKOFF_S`(๊ธฐ๋ณธ 10)์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ๋ฐ˜๋ณตํ•œ๋‹ค(๊ธฐ๋ณธ ์ด ์˜ˆ์‚ฐ โ‰ˆ 3ร—120s + 20s). ์ตœ์ข… ์‹คํŒจ โ†’ return 1("agent still working"). ํ˜ธ์ถœ์ž(๋ž˜ํผ)๋Š” rc 1์„ `finalize_unfinished โ€ฆ idle_wait`(โ†’ `cancelled`/rc 5)๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉฐ **`deadline_timeout`์„ ์ ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค**. +- `interactive_ready`๋Š” ์–ด๋–ค ํŒ์ •์—๋„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค. + +**3.2.3 ๋ž˜ํผ ์‹คํ–‰ ์ˆœ์„œ ์žฌ๋ฐฐ์น˜ (`cmd_submit`)** +```text +register โ†’ brief ์ž‘์„ฑ โ†’ preflight_agent(herdr ์กด์žฌยท์„ธ์…˜ ์กด์žฌยท์—ญํ•  ๊ฒ€์‚ฌ) + โ†’ wait_for_agent_idle "$sess" (rc 1/2 โ†’ finalize_unfinished โ€ฆ idle_wait โ†’ cancelled/session_busy; exit 5) + โ†’ _disp_tok ์ฝ๊ธฐ โ†’ ๊ตฌ๋…์ž ๊ธฐ๋™ + SUBSCRIBED ํ•ธ๋“œ์…ฐ์ดํฌ + โ†’ inject_agent(send_keys_safe) โ†’ wait sub โ†’ finalize +``` +- loop/discuss ๋ฐ˜๋ณต๋งˆ๋‹ค ๋™์ผ ์ˆœ์„œ(`:286` ๊ตฌ๋…์ž ๊ธฐ๋™ ์•ž์— idle wait). + +**3.2.4 `send_keys_safe` ๋ณด๊ฐ• (`lib.sh:2231-2345`)** +- ์ œ์ถœ ๊ฒ€์ฆ(`:2330-2332`): `pre_submit`์— `esc to interrupt`/`โ— |โœฝ |[A-Za-z]+ing`๊ฐ€ **์ด๋ฏธ ์žˆ์—ˆ์œผ๋ฉด** ํ•ด๋‹น ํ† ํฐ์„ ์„ฑ๊ณต ๊ทผ๊ฑฐ์—์„œ ์ œ์™ธํ•˜๊ณ  marker ์†Œ๋ฉธ + ํ™”๋ฉด ๋ณ€ํ™”๋งŒ ์ธ์ •. +- ๋„ค์ดํ‹ฐ๋ธŒ ๊ฒฝ๋กœ(`:2279`): `SKS_PROMPT_WAIT=1`(๊ธฐ๋ณธ)์ด๋ฉด `agent prompt --wait --until working --timeout 5000`. ์„ฑ๊ณต โ†’ 0. `agent_prompt_stalled`/timeout โ†’ **์žฌ์ œ์ถœํ•˜์ง€ ์•Š๊ณ ** 2s ํ›„ `agent get` 1ํšŒ: `working` โ†’ 0, ๊ทธ ์™ธ โ†’ rc 4("delivery unknown"). `agent_blocked` โ†’ rc 2. +- ํด๋ฐฑ ๊ฒฝ๋กœ ์ง„์ž… ์ „ busy ํ† ํฐ ๊ฒŒ์ดํŠธ: `wait_for_agent_idle`์ด 3(unknown)์„ ๋ฐ˜ํ™˜ํ•œ ๊ฒฝ์šฐ์—๋งŒ `_pane_tail 8`์˜ busy ํ† ํฐ์„ `SKS_BUSY_TIMEOUT`(๊ธฐ๋ณธ 120s)๊นŒ์ง€ ๋Œ€๊ธฐ. + +### 3.3 Phase 3 โ€” ๋Ÿฌ๋„ˆ Fail-Fast +- `runner.py:279-289`: `res.returncode != 0`์ด์–ด๋„ stdout์—์„œ `registered job:` ๋งค์น˜ ์‹œ `created_jobs.append` + `_record_checkpoint()` ํ›„ **id ๋ฐ˜ํ™˜**(๋กœ๊ทธ๋Š” error ๋ ˆ๋ฒจ ์œ ์ง€). ๋งค์น˜ ์‹คํŒจ ์‹œ์—๋งŒ `None`. +- **rc=5 (`session_busy`, Rev.2 Secondary)**: `delegate_job`์ด `MAM_SESSION_BUSY_RETRY_S`(๊ธฐ๋ณธ 30)์ดˆ ๋Œ€๊ธฐ ํ›„ **1ํšŒ** ์žฌ์œ„์ž„(์ƒˆ job id ๋“ฑ๋ก)ํ•œ๋‹ค. ์žฌ์‹œ๋„๋„ rc=5๋ฉด ๋งˆ์ง€๋ง‰ id๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ํ˜ธ์ถœ๋ถ€๋Š” `wait_for_job`์ด `cancelled`๋กœ ์ฆ‰์‹œ False โ†’ ๊ธฐ์กด abort ๊ฒฝ๋กœ. ๋ฌดํ•œ ์žฌ์‹œ๋„๋Š” ๋‘์ง€ ์•Š๋Š”๋‹ค(๊ต์ฐฉ์˜ ์ƒˆ ํ˜•ํƒœ). +- `wait_for_job`์€ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š”๋‹ค(3900s๋Š” ์ตœํ›„ ์•ˆ์ „๋ง; `cancelled`๋Š” ์ด๋ฏธ False ์กฐ๊ฑด). +- `run_loop.sh:159-168, 373`: `delegate_job_safe` rcโ‰ 0์—์„œ๋„ ์ถœ๋ ฅ์—์„œ job id ์ถ”์ถœ(๋ ˆ๊ฑฐ์‹œ ๊ฒฝ๋กœ ๋™๋“ฑ์„ฑ). + +### 3.4 Phase 4 โ€” ํ…Œ์ŠคํŠธ ๋ฐ ๋ฌธ์„œ (ยง5 ์ฐธ์กฐ) +- `tests/conftest.py` mock_herdr: `agent get`/`list`์— `agent_status`(๊ธฐ๋ณธ `idle`) ์ถ”๊ฐ€; `agent wait`๋Š” ์ƒํƒœ ํŒŒ์ผ์˜ `status_sequence`(์˜ˆ: `["working","working","done"]`)๋ฅผ ํ˜ธ์ถœ๋งˆ๋‹ค ์†Œ๋น„, ์†Œ์ง„ ์‹œ ๋งˆ์ง€๋ง‰ ๊ฐ’ ์œ ์ง€, `--timeout`์€ ์‹œํ€€์Šค ์†Œ์ง„ ์ „ ์ข…๋ฃŒ ์‹œ `{"error":{"code":"timeout"}}` rc 1; `agent prompt --wait`๋Š” `prompt_wait_result` ํ•„๋“œ(`ok|agent_prompt_stalled|agent_blocked`)๋กœ ์ œ์–ด. +- ๋ฌธ์„œ: `SKILL.md` "Core Commands"์— ์ข…๋ฃŒ ์ฝ”๋“œ ํ‘œ(ยง3.1.4), `MESSAGING.md` ยง5.1 ๊ตฌ๋…์ž ํƒ€์ž„์•„์›ƒ์˜ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋ฐ˜์˜ ์„œ์ˆ , `BOOTSTRAP*.md` ์—์ด์ „ํŠธ ํ”„๋กœํ† ์ฝœ์— "completed ๋ฐœํ–‰ ํ›„ ๋„๊ตฌ ํ˜ธ์ถœ ์—†์ด ํ„ด ์ข…๋ฃŒ" 1๋ฌธ์žฅ. + +### 3.5 Phase 5 โ€” E2Eยท์ •๋ฆฌยท๋ฆด๋ฆฌ์Šค +- E2E ์ ˆ์ฐจ(**direct ๋ชจ๋“œ๋งŒ**; loop ๋ชจ๋“œ ์ข…๋‹จ์€ O-1 ์™„๋ฃŒ ํ›„): ๊ฐ ๋ฐฑ์—”๋“œ์— `--agent human`์ด ์•„๋‹Œ ์‹ค์„ธ์…˜์œผ๋กœ ์žก A ์œ„์ž„ โ†’ `completed` ์ˆ˜์‹  ์ฆ‰์‹œ ์žก B ์œ„์ž„ โ†’ ๋ž˜ํผ ๋กœ๊ทธ์˜ idle ๋Œ€๊ธฐ ์‹œ๊ฐ„, `herdr agent get` ํƒ€์ž„๋ผ์ธ, B์˜ `started` ์ง€์—ฐ์„ ๊ธฐ๋ก(`docs/contracts/issue-6/phase5-e2e.md`). +- ํƒ€์ž„์•„์›ƒ ์ฃผ์ž…: ์„ธ์…˜์— `/exit` ์—†์ด ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฌด์‹œํ•˜๋„๋ก `--idle-timeout 30`์œผ๋กœ ์œ„์ž„ โ†’ ๋ž˜ํผ rc=2, ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `error`, `wait_for_job` โ‰ค 2s. +- ์ž”์กด ๋ ˆ์ฝ”๋“œ `93a44347`, `2961b822`, `ee242671`: `registry.py close --job --request deadline_timeout --detail "Issue #6 backfill"`. +- ๋ฆด๋ฆฌ์Šค: `v4.4.1` (PATCH โ€” ๊ณต๊ฐœ ๊ณ„์•ฝ ์‚ญ์ œ ์—†์Œ, ์ข…๋ฃŒ ์ฝ”๋“œ๋Š” ์˜ค๋™์ž‘ ์ •์ •, `timeout` ์„œ๋ธŒ์ปค๋งจ๋“œ๋Š” ๋ž˜ํผ ๋‚ด๋ถ€ ๋ฐฐ๊ด€). 4-way ๋ฝ์Šคํ…: `lib.sh:32`, `eager.py:16`, `VERSIONS.md`(๊ฐœ์š”+๋งคํŠธ๋ฆญ์Šค+Changelog), 8๊ฐœ `SKILL.md` frontmatter. `IMPROVEMENTS.md` ยง5์— `Issue-6-FailClosed-TurnHandshake` ๋ธ”๋ก ์ถ”๊ฐ€(ยง6 ํ‘œยทํ—ค๋” ํ‚ค ๋ชฉ๋ก ๋™๊ธฐํ™”). + +--- + +## 4. ๋ช…์‹œ์  ๋น„๋ฒ”์œ„ (Out of Scope) โ€” ๋ณ„๋„ ์ด์Šˆ +1. **O-1 ๋ฆฌํ…Œ์ธ ํ„ฐ๋ฏธ๋„ ์žฌ์ˆ˜๋ฝ**: ์žฌ๋””์ŠคํŒจ์น˜ ํ›„ 2ํšŒ์ฐจ ๊ตฌ๋…์ž๊ฐ€ 1ํšŒ์ฐจ retained `completed`๋ฅผ ์ฆ‰์‹œ ์ˆ˜๋ฝ(์‹ค์ธก). loop ๋ชจ๋“œ ๊ธฐ์กด ๊ฒฐํ•จ. ๋ณธ ์ด์Šˆ์—์„œ๋Š” "loop ๊ฒฝ๋กœ retained ๋ฐœํ–‰ ๊ธˆ์ง€"๋กœ ํšŒํ”ผ๋งŒ ํ•œ๋‹ค. **Rev.2 ์ฃผ์˜: ยง3.1.6์˜ attempt_id ์ˆ˜์ • ํ›„์—๋„ loop ๋ชจ๋“œ ๋ฆฌ๋ทฐ์–ด phase๋Š” O-1 ๋•Œ๋ฌธ์— ์ข…๋‹จ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋ณ„๋„ ์ด์Šˆ๋ฅผ Issue #6 ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ์— "loop/discuss ๋ชจ๋“œ ์‚ฌ์šฉ ์ „ ํ•„์ˆ˜"๋กœ ๋ช…์‹œํ•œ๋‹ค.** +2. **๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์˜ค์—ผ**: `.mam/jobs`์— pytest ์„ธ์…˜๋ช… ๋ ˆ์ฝ”๋“œ 467๊ฑด ๋ˆ„์ถœ ์›์ธ. +3. **์„ธ์…˜๋ณ„ ์ „๋‹ฌ ์ง๋ ฌํ™” ๋ฝ**, **direct ์žก `dispatch_token` ๋ฐœ๊ธ‰**: ํ˜„ ๊ตฌ์กฐ(๋™๊ธฐ ์œ„์ž„ + loop lock)์—์„œ ๋ถˆํ•„์š”. +4. **`wait_for_job` 3900s ๋‹จ์ถ•**: ์ฆ์ƒ ์™„ํ™”์ด๋ฏ€๋กœ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š”๋‹ค. + +--- + +## 5. ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ํ–‰๋ ฌ (Phase 4 ์‚ฐ์ถœ๋ฌผ) + +### 5.1 `tests/test_issue6_fail_closed.py` (๋ธŒ๋กœ์ปค ๋ฌด๊ด€, `--agent human` + ์งง์€ ํƒ€์ž„์•„์›ƒ) +| ID | ์‹œ๋‚˜๋ฆฌ์˜ค | ๊ธฐ๋Œ€ | +|---|---|---| +| T1-1 | direct, ์ด๋ฒคํŠธ ์—†์Œ (`--timeout 6 --idle-timeout 2`) | rc=2, ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `error`, events.log์— wrapper `error` 1๊ฑด, ๊ตฌ๋…์ž pid ์†Œ๋ฉธ | +| T1-2 | loop, ์›Œ์ปค phase ์ด๋ฒคํŠธ ์—†์Œ | rc=2, `error`, **retained ๋ฐœํ–‰ ์—†์Œ** | +| T1-3 | ๊ตฌ๋…์ž ํƒ€์ž„์•„์›ƒ ์งํ›„ `publish_event completed` ๊ฒฝ์Ÿ | ์ตœ์ข… `completed` ์œ ์ง€, `timeout` ์„œ๋ธŒ์ปค๋งจ๋“œ rc 0(๊ฑฐ๋ถ€ ๋กœ๊ทธ) | +| T1-4 | `MQTT_HOST` ๋ถˆ๊ฐ€ + ๋””์Šคํฌ ๋น„์ข…๋ฃŒ (`--timeout 8`) | rc=3, ๋ฐ๋“œ๋ผ์ธ๊นŒ์ง€ ์žฌ๊ด€์ธก ๋กœ๊ทธ, ์ตœ์ข… `error` | +| T1-5 | rc=3 + ์žฌ๊ด€์ธก ์ค‘ ๋””์Šคํฌ `completed` ์ถœํ˜„ | rc=0, `completed` | +| T1-6 | `close --request deadline_timeout`์„ `completed` ๋ ˆ์ฝ”๋“œ์— ์ ์šฉ | rc=0, ์ƒํƒœ ๋ถˆ๋ณ€ | +| T1-10 | loop: ์›Œ์ปค `completed` ํ›„ ๋ฆฌ๋ทฐ์–ด `update --status pending --iteration 1 --attempt-id 2` | rc=0, `pending`, `attempt_id=2` (์ˆ˜์ • ์ „ rc=4 โ†’ ์—ญ๊ฒ€์ฆ) | +| T1-11 | ๊ฐ™์€ `--attempt-id 2` ์žฌ์š”์ฒญ(๋ฆฌํ”Œ๋ ˆ์ด) | rc=4 ๊ฑฐ๋ถ€ | +| T1-12 | `--attempt-id` ์—†์ด iteration 2 (F-A ๊ฒฝ๋กœ) | rc=0 (๊ธฐ์กด ๊ทœ์น™ ๋ณด์กด), F-A 3์ข… ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ | +| T1-13 | `close --request cancelled --detail session_busy`: pendingโ†’cancelled rc=0, completedโ†’ rc=0ยท์ƒํƒœ ๋ถˆ๋ณ€, ์ดํ›„ `redispatch` iteration 2 โ†’ pending | ์ „์ด ๊ทœ์น™ ๊ณ ์ • | +| T1-7 | ์ปค์Šคํ…€ `--registry-dir` + `send_keys_safe` ์‹คํŒจ(BASH_ENV ์˜ค๋ฒ„๋ผ์ด๋“œ) | rc=1, ํ•ด๋‹น ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `error` | +| T1-8 | `MAM_DELEGATE_FAIL_CLOSED=0` | T1-1 ์กฐ๊ฑด์—์„œ rc=0, `pending` (๊ตฌ ๋™์ž‘ ์žฌํ˜„) | +| T1-9 | dry-run direct | ๊ตฌ๋…์ž ํ”„๋กœ์„ธ์Šค ๋ฏธ๊ธฐ๋™ | +| ๊ณ ์ • | `test_wrapper_exit_code_mapping_literals_are_frozen` | ํ†ต๊ณผ | + +### 5.2 `tests/test_issue6_turn_handshake.py` (mock_herdr ์ƒํƒœ ์‹œํ€€์Šค) +| ID | ์‹œ๋‚˜๋ฆฌ์˜ค | ๊ธฐ๋Œ€ | +|---|---|---| +| T2-1 | `status_sequence=["working","done"]` | `agent wait` ํ˜ธ์ถœ ํ›„ `agent prompt` ํ˜ธ์ถœ(์ˆœ์„œ ๋‹จ์ •), Enter/C-m 0ํšŒ(๋„ค์ดํ‹ฐ๋ธŒ) | +| T2-2 | `working` ๊ณ ์ •, `SKS_IDLE_TIMEOUT_MS=1000` | rc=1, `agent prompt` ๋ฏธํ˜ธ์ถœ, ๋ž˜ํผ rc=1ยท`error` | +| T2-3 | `unknown` | `agent wait` ๋ฏธํ˜ธ์ถœ, `_pane_quiescent` ํด๋ฐฑ ๊ฒฝ๋กœ ์ง„์ž… | +| T2-4 | `blocked` | rc=2, ์ฃผ์ž… ์—†์Œ | +| T2-5 | `done` ์ฆ‰์‹œ | ๋Œ€๊ธฐ 0ํšŒ, ์ฆ‰์‹œ ์ฃผ์ž… | +| T2-6 | ํด๋ฐฑ ๊ฒฝ๋กœ, pre_submit์— `esc to interrupt` ์กด์žฌ + ํŽ˜์ด์ŠคํŠธ ๋“œ๋กญ | rc=4 (๊ธฐ์กด ์ฝ”๋“œ์—์„œ๋Š” 0 โ†’ ์—ญ๊ฒ€์ฆ) | +| T2-7 | `prompt_wait_result=agent_prompt_stalled` + ์ดํ›„ `working` | rc=0, prompt ํ˜ธ์ถœ **1ํšŒ** | +| T2-8 | ๋ž˜ํผ ์ˆœ์„œ: idle ๋Œ€๊ธฐ ๋กœ๊ทธ๊ฐ€ `subscriber pid:` ๋กœ๊ทธ๋ณด๋‹ค ์•ž | ๋กœ๊ทธ ์ˆœ์„œ ๋‹จ์ • | +| T2-9 | `working` ๊ณ ์ • + `SKS_IDLE_RETRIES=1`, `SKS_IDLE_TIMEOUT_MS=500` | `agent wait` 2ํšŒ ํ˜ธ์ถœ, ๋ž˜ํผ rc=5, ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `cancelled`(detail `session_busy`), `deadline_timeout` ๋ฏธํ˜ธ์ถœ, ๊ตฌ๋…์ž ๋ฏธ๊ธฐ๋™ | +| T2-10 | `HERDR_SESSION_NAME=iso-x`์—์„œ `wait_for_agent_idle` | mock ํ˜ธ์ถœ ๊ธฐ๋ก์— `herdr --session iso-x agent wait โ€ฆ` (argv ์ˆœ์„œ ๋‹จ์ •); `build_argv(session="default")`๋Š” `--session` ์ƒ๋žต | +| T2-11 | ์ •์  ๋ฆฐํŠธ: `lib.sh`ยท๋ž˜ํผ์—์„œ `^\s*python3? -m lib_py\.loop` ๋˜๋Š” bare `python -m` ํŒจํ„ด 0๊ฑด, watcher ํ˜ธ์ถœ์€ `_delegate_py_bin` ๊ฒฝ์œ  | grep ๋‹จ์ • | +| ํ˜ธํ™˜ | `test_h16`, `test_b8`, `test_c1_*`, `test_herdr_shim_contract` | ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ | + +### 5.3 Phase 3 (`tests/test_issue6_fail_closed.py` ๋‚ด) +| ID | ์‹œ๋‚˜๋ฆฌ์˜ค | ๊ธฐ๋Œ€ | +|---|---|---| +| T3-1 | `LoopRunner.delegate_job` rcโ‰ 0 + `registered job:` ์ถœ๋ ฅ | id ๋ฐ˜ํ™˜, `wait_for_job` โ‰ค 2s False | +| T3-2 | ๋™์ผ | `.mam/loop-checkpoint.json`์— id ๊ธฐ๋ก | +| T3-3 | ๋ž˜ํผ stub์ด 1ํšŒ์ฐจ rc=5, 2ํšŒ์ฐจ rc=0 (`MAM_SESSION_BUSY_RETRY_S=0`) | ์žฌ์œ„์ž„ 1ํšŒ ํ›„ 2ํšŒ์ฐจ id ๋ฐ˜ํ™˜; ๋‘ ๋ฒˆ ๋‹ค rc=5๋ฉด ๋งˆ์ง€๋ง‰ id ๋ฐ˜ํ™˜ + `wait_for_job` False | + +--- + +## 6. ๊ณต์ˆ˜ ๋ฐ ์ˆœ์„œ +| ๋‹จ๊ณ„ | ๊ณต์ˆ˜ | ์„ ํ–‰ | +|---|---|---| +| Phase 1 | 1.25 MD (+0.25 attempt_id) | โ€” | +| Phase 2 | 1.25โ€“1.75 MD (+0.25 ์„ธ์…˜ ์ „๋‹ฌยท์žฌ์‹œ๋„) | Phase 1 (finalize ํ—ฌํผ ์žฌ์‚ฌ์šฉ) | +| Phase 3 | 0.25 MD | Phase 1 | +| Phase 4 | 0.75 MD (Phase 1~3์™€ ๋ณ‘ํ–‰ ์ž‘์„ฑ, ์—ญ๊ฒ€์ฆ ํฌํ•จ) | โ€” | +| Phase 5 | 0.5โ€“1.0 MD | ์ „๋ถ€ | +| **ํ•ฉ๊ณ„** | **4.0โ€“5.0 MD** (ํ†ตํ•ฉ ๋ณด๊ณ ์„œ Rev.2 ยง3 ์ƒํ•œ ๋Œ€๋น„ +0.5 MD; ์‚ฌ์œ  ยง7) | | + +--- + +## 7. Creator ์ด์˜์ œ๊ธฐ ํŒ์ • (Rev.1 โ†’ Rev.2) + +| ์ด์˜ | ํŒ์ • | ์‹ค์ธก ๊ทผ๊ฑฐ | Rev.2 ๋ฐ˜์˜ | +|---|---|---|---| +| **Primary-A** watcher.py๊ฐ€ `HERDR_SESSION_NAME` ๊ฒฉ๋ฆฌ๋ฅผ ์žƒ๋Š”๋‹ค | **์ˆ˜์šฉ** (์„ธ๋ถ€ ์ •์ •: ์‹ค์ œ herdr ํ”Œ๋ž˜๊ทธ๋Š” `-L`์ด ์•„๋‹ˆ๋ผ `--session`; `-L`์€ `.mam/shim` ๋ฒˆ์—ญ ์ „์šฉ. ๊ฒฉ๋ฆฌ ๋ณ€์ˆ˜๋Š” ์…ธ ํ•จ์ˆ˜ `mam_herdr`/`_real_herdr`๊ฐ€ ๋ฒˆ์—ญํ•˜๋ฉฐ Python ์„œ๋ธŒํ”„๋กœ์„ธ์Šค์—๋Š” ์ƒ์†๋˜์ง€ ์•Š์Œ) | `HERDR_SESSION_NAME`๋งŒ ์žˆ๋Š” ํ™˜๊ฒฝ์—์„œ bare `herdr` โ†’ `server_not_running`; `--session`/`HERDR_SESSION`/`HERDR_SOCKET_PATH` โ†’ ์ •์ƒ. `lib.sh` source ํ›„ Python `subprocess`๋„ ๋™์ผ ์‹คํŒจ | ยง1 ์›์น™ 8, ยง3.2.1 `--session` ์ „๋‹ฌ, ยง3.2.2 `_sks_herdr` ์‚ฌ์šฉ, T2-10 | +| **Primary-B** bare `python -m` ํ˜ธ์ถœ์€ `ModuleNotFoundError` | **์ˆ˜์šฉ** (Rev.1 ํ‘œ๊ธฐ ์˜ค๋ฅ˜) | `lib.sh:1841 _delegate_py_bin`, `PYTHONPATH` export `lib.sh:25,164` | ยง3.2.1/3.2.2 `"$(_delegate_py_bin)"`, T2-11 ์ •์  ๋ฆฐํŠธ | +| **Secondary** ์ฃผ์ž… ์ „ ์œ ํœด ๋Œ€๊ธฐ ์‹คํŒจ๋ฅผ `deadline_timeout`(terminal `error`)๋กœ ๋‹ซ์œผ๋ฉด ๋ฏธ์ฃผ์ž… ์žก์ด ์‹คํ–‰ ์‹คํŒจ๋กœ ๋‚™์ธ + ๋Ÿฌ๋„ˆ ์ฆ‰์‹œ abort | **์ˆ˜์šฉ** (๋Œ€์•ˆ ์ฑ„ํƒ: ๋ž˜ํผ ๋‚ด bounded ์žฌ์‹œ๋„ โ†’ `cancelled`/`session_busy` rc 5 โ†’ ๋Ÿฌ๋„ˆ 1ํšŒ ์žฌ์œ„์ž„). "์žฌ์‹œ๋„ ๊ธฐํšŒ ์˜๊ตฌ ๋ฐ•ํƒˆ" ๋ถ€๋ถ„์€ ๋ถ€๋ถ„ ์ •์ •: `cancelled`๋„ operator `redispatch`๋กœ ์žฌ๊ฐœ ๊ฐ€๋Šฅ(์‹ค์ธก) | `engine.transition`: pendingโ†’cancelled ์ปค๋ฐ‹, completedโ†’cancelled ๊ฑฐ๋ถ€, cancelledโ†’redispatch ์„ฑ๊ณต | ยง1 ์›์น™ 7, ยง3.1.1 `close`, ยง3.1.2/3.1.4 rc 5, ยง3.2.2 ์žฌ์‹œ๋„, ยง3.3 ๋Ÿฌ๋„ˆ ์žฌ์œ„์ž„, T1-13/T2-9/T3-3 | +| **Tertiary** ์›Œ์ปคโ†’๋ฆฌ๋ทฐ์–ด ์ „ํ™˜ ์‹œ ๊ฐ™์€ iteration์˜ `redispatch` ๊ฑฐ๋ถ€ | **์ˆ˜์šฉ** (์ธ์šฉ ์ฝ”๋“œ๋Š” ์‹ค์ œ `engine.py:227-240`๊ณผ ๋‹ค๋ฅด๋‚˜ ๊ฒฐ๋ก ์€ ์ •ํ™•). v4.4.0์—์„œ **์ด๋ฏธ ๊นจ์ ธ ์žˆ๋Š” ๊ธฐ์กด ๊ฒฐํ•จ**์ž„์„ ํ™•์ธ | `registry.py update --status pending --iteration 1`(์›Œ์ปค completed ํ›„) โ†’ rc=4 `does not advance past 1`; iteration 2 โ†’ rc=0 | ยง3.1.6 `attempt_id` ๊ทœ์•ฝ, T1-10~T1-12 | +| (๋ถ€์ˆ˜) Rev.2 ํ†ตํ•ฉ ๋ณด๊ณ ์„œ ยง3์˜ `lib.sh` ๊ฒฝ๋กœ | Rev.1๊ณผ ๋™์ผํ•˜๊ฒŒ ์‹ค์ œ ๊ฒฝ๋กœ `.agents/skills/lib.sh` ์‚ฌ์šฉ | โ€” | โ€” | + +๊ณต์ˆ˜ +0.5 MD์˜ ์‚ฌ์œ : `attempt_id` ๊ทœ์•ฝ(์—”์ง„ ๊ทœ์น™ยทCLIยทpublish_event ๊ธฐ๋ณธ๊ฐ’ยทํ…Œ์ŠคํŠธ 3์ข…)๊ณผ ์„ธ์…˜ ์ „๋‹ฌยท์žฌ์‹œ๋„ ๋กœ์งยท๋Ÿฌ๋„ˆ ์žฌ์œ„์ž„(ํ…Œ์ŠคํŠธ 3์ข…)์ด Rev.1์— ์—†๋˜ ์‹ ๊ทœ ์ž‘์—…์ด๋‹ค. diff --git a/.agents/reports/reviewer-opencode-01/report-17f2e688.md b/.agents/reports/reviewer-opencode-01/report-17f2e688.md new file mode 100644 index 0000000..47a74a1 --- /dev/null +++ b/.agents/reports/reviewer-opencode-01/report-17f2e688.md @@ -0,0 +1,67 @@ +### [reviewer-opencode-01] Issue #6 ์ •๋ฐ€ ๊ฒ€ํ†  ๋ฐ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ ๋ณด๊ณ ์„œ + +- **Job**: `17f2e688` / **๊ฒ€ํ† ์ž**: `reviewer-opencode-01` (opencode) +- **๊ฒ€ํ†  ๋ฐฉ๋ฒ•**: ์ด์Šˆ๊ฐ€ ์ธ์šฉํ•œ ๋ชจ๋“  ์œ„์น˜๋ฅผ ๋ผ์ด๋ธŒ ์†Œ์Šค์—์„œ ์ง์ ‘ ์ฝ๊ณ , ๊ฒฐํ•จ ์ฒด์ธ ์ „์ฒด(worker break โ†’ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ โ†’ wait_for_job)๋ฅผ ์ฝ”๋“œ ๋ผ์ธ ๋‹จ์œ„๋กœ ์ถ”์ ํ•˜๋ฉฐ, ์…ธ ๋ฃจํ”„(`run_loop.sh`)์™€ Python ๋ฃจํ”„(`lib_py/loop/runner.py`) ์–‘์ชฝ ์†Œ๋น„์ž๋ฅผ ๋Œ€์กฐํ•˜๊ณ , ๊ธฐ์กด ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ํ™•์ธํ–ˆ๋‹ค. ์š”์•ฝ์„ ์‹ ๋ขฐํ•˜์ง€ ์•Š๊ณ  ์žฌํ˜„ ๊ฒฝ๋กœ๋ฅผ ์ง์ ‘ ๊ฒ€์ฆํ–ˆ๋‹ค. + +--- + +## 1. ๋ฒ„๊ทธ ์ง„์œ„ ์—ฌ๋ถ€ ๊ฒ€ํ†  ๊ฒฐ๊ณผ + +### ๊ฒฐํ•จ โ‘  (Fail-Silent on Worker Failure) โ€” **์‹ค์žฌ ํ™•์ธ (CONFIRMED REAL)** + +์ด์Šˆ์˜ ์ธ์šฉ์€ ์ •ํ™•ํ•˜๋‹ค. `multi-agent-mux-delegate-job`์˜ ๋ฐ˜๋ณต ๋ฃจํ”„: + +- **:351-361** โ€” `_phase == "worker"`์—์„œ `job_status != "completed"`์ธ ๊ฒฝ์šฐ(์ฃผ์ž… ์‹คํŒจ๋กœ ์ธํ•œ subscriber timeout, ํ”„๋กฌํ”„ํŠธ ๋ฏธ์ˆ˜์‹  idle timeout ๋“ฑ) `echo ... Terminating workflow.` ํ›„ `break`. +- ์ด `break` ๊ฒฝ๋กœ์—๋Š” **์–ด๋””์—๋„** `registry.py status --set` ํ˜ธ์ถœ์ด ์—†๋‹ค. ๋ฐ˜๋น„๊ต: ๋ฃจํ”„ ๋‚ด ๋‹ค๋ฅธ ์ข…๋ฃŒ ๊ฒฝ๋กœ๋Š” ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ฐฑ์‹ ํ•œ๋‹ค โ€” reviewer ์Šน์ธ ์‹œ `--set "completed"` (:394), ์ตœ๋Œ€ ๋ฐ˜๋ณต ๋„๋‹ฌ ์‹œ `--set "error"` (:400). +- ์ฆ‰ worker ์‹คํŒจ ์กฐ๊ธฐ ํƒˆ์ถœ์ด **์œ ์ผํ•˜๊ฒŒ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋ฅผ ๋ฐฉ์น˜ํ•˜๋Š” ์ข…๋ฃŒ ๊ฒฝ๋กœ**๋‹ค. ์žก์€ ์ƒ์„ฑ ์‹œ `status: "pending"`(`lib_py/delegation/registry.py:84`)์œผ๋กœ ๋“ฑ๋ก๋˜๋ฏ€๋กœ, ์ด ๊ฒฝ๋กœ๋ฅผ ๋ฐŸ์œผ๋ฉด `.json`์€ **์˜๊ตฌํžˆ `pending`**์œผ๋กœ ๋‚จ๋Š”๋‹ค. +- ํ•จ์ˆ˜๋Š” ์ดํ›„ validation hook์„ ๋Œ๊ณ  audit-log ๊ฒฝ๋กœ๋ฅผ stdout์œผ๋กœ ์ถœ๋ ฅํ•œ ๋’ค ์ •์ƒ ์ข…๋ฃŒํ•œ๋‹ค โ€” `exit 0`(fail-silent). ์ด์Šˆ ๊ธฐ์ˆ ๊ณผ ์ •ํ™•ํžˆ ์ผ์น˜. + +**์†Œ๋น„์ž ์ธก ๊ต์ฐฉ ํ™•์ธ**: ์…ธ `wait_for_job`(`run_loop.sh:270-306`)๊ณผ Python `wait_for_job`(`lib_py/loop/runner.py:291-323`) ๋ชจ๋‘ ์ข…๋ฃŒ ์กฐ๊ฑด์ด `completed`(โ†’rc 0), `error`/`cancelled`(โ†’rc 1)๋ฟ์ด๊ณ , ๊ทธ ์™ธ ์ƒํƒœ(`pending` ํฌํ•จ)๋Š” `sleep $check_interval` ํ›„ ์žฌํด๋งํ•œ๋‹ค. `pending`์€ ์–ด๋–ค ์ข…๋ฃŒ ์กฐ๊ฑด์—๋„ ํ•ด๋‹นํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ **`max_wait=3900s`๋ฅผ ์ „๋Ÿ‰ ์†Œ์ง„ํ•œ ๋’ค ํƒ€์ž„์•„์›ƒ์œผ๋กœ rc=1** โ€” ์ •ํ™•ํžˆ ์ด์Šˆ๊ฐ€ ๊ธฐ์ˆ ํ•œ 65๋ถ„ ์˜๊ตฌ ๋Œ€๊ธฐ. ๋‘ ๋Ÿฐํƒ€์ž„(v4.4.0์˜ Python ์ฝ”์–ด ํฌํ•จ) ๋ชจ๋‘ ๋™์ผํ•˜๊ฒŒ ๋…ธ์ถœ๋œ๋‹ค. + +### ๊ฒฐํ•จ โ‘ก (Race Condition on TUI Turn Handshake) โ€” **์‹ค์žฌ ํ™•์ธ (CONFIRMED REAL, ๊ทธ๋Ÿฌ๋‚˜ ์ด๋ฏธ ๋ถ€๋ถ„ ์™„ํ™” ์žฅ์น˜ ์กด์žฌ โ€” ์ž„๊ณ„๊ฐ’/๋Œ€๊ธฐ์‹œ๊ฐ„ ๋ฌธ์ œ)** + +- `send_keys_safe`(`lib.sh:2231-`)๋Š” ์ฃผ์ž… ์ „ `_pane_quiescent`(:2189)๋กœ **์—ฐ์† 2ํšŒ ๋™์ผ ์บก์ฒ˜**(๋ Œ๋”๋Ÿฌ ์ •์ง€)๋ฅผ ํ™•์ธํ•œ๋‹ค. ์ด๊ฒƒ์ด ๋ฐ”๋กœ ์ด์Šˆ๊ฐ€ ์š”๊ตฌํ•˜๋Š” settling ๋ธŒ๋ฆฌ๋ฏธํ‹ฐ๋ธŒ์˜ ์กด์žฌ ์ž์ฒด๋Š” ์ด๋ฏธ ๊ตฌํ˜„๋˜์–ด ์žˆ๋‹ค. +- ๋‹ค๋งŒ ์‹ค์ธก ํŒŒ๋ผ๋ฏธํ„ฐ: `SKS_QUIESCENT_TRIES=20` ร— `SKS_QUIESCENT_INTERVAL=0.5s` = **์ตœ๋Œ€ 10์ดˆ ๋Œ€๊ธฐ**. ์—์ด์ „ํŠธ๊ฐ€ ๋„๊ตฌ๋กœ `completed` ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•œ ์งํ›„(ํ„ด ๋‚ด๋ถ€) ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ๊ฐ€ ์ฆ‰์‹œ ๋‹ค์Œ ์žก์„ ์ƒ์„ฑํ•ด ์ฃผ์ž…ํ•˜๋ฉด, ์—์ด์ „ํŠธ TUI๋Š” ์•„์ง ํ„ด ์ถœ๋ ฅ์„ ๋ Œ๋”๋ง ์ค‘์ด๋ฏ€๋กœ ์บก์ฒ˜ ๋‚ด์šฉ์ด ๊ณ„์† ๋ณ€ํ•œ๋‹ค. ๋ Œ๋”๋ง์ด 10์ดˆ ์ด์ƒ ์ง€์†๋˜๋ฉด `_pane_quiescent`๊ฐ€ rc=1์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  `send_keys_safe`๋Š” "pane never quiesced"์™€ ํ•จ๊ป˜ **์ฃผ์ž… ์ž์ฒด๋ฅผ ๊ฑฐ๋ถ€**ํ•œ๋‹ค. +- **๊ฒฐํ•จ โ‘ก์™€ โ‘ ์˜ ์—ฐ์‡„(ํ•ต์‹ฌ)**: `run_agent`์˜ ์ฃผ์ž… ์‹คํŒจ๋Š” rc=1์„ ๋ฐ˜ํ™˜ํ•˜์ง€๋งŒ, ๋ฃจํ”„์˜ ํ˜ธ์ถœ๋ถ€(:327) `run_agent "$JOB_ID" ... `๋Š” **๋ฐ˜ํ™˜๊ฐ’์„ ๋ฌด์‹œํ•œ๋‹ค**(`if ! run_agent` ์•„๋‹˜). ์ดํ›„ `wait "$sub_pid"`๋Š” ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์ฃผ์ž…๋˜์ง€ ์•Š์€ ์—์ด์ „ํŠธ๋ฅผ ๋Œ€๊ธฐํ•˜๋‹ค idle timeout์œผ๋กœ rcโ‰ฅ2 โ†’ `job_status="timeout"` โ†’ worker break โ†’ ๊ฒฐํ•จ โ‘ ์˜ fail-silent โ†’ `pending` ๊ต์ฐฉ. ์ฆ‰ **โ‘ก๋Š” โ‘ ์˜ ์ฃผ์š” ํŠธ๋ฆฌ๊ฑฐ์ด๋ฉฐ, โ‘ ์€ โ‘ก์˜ ํ”ผํ•ด๋ฅผ ๊ต์ฐฉ์œผ๋กœ ์ฆํญ**ํ•œ๋‹ค. ๋‘ ๊ฒฐํ•จ์€ ๋…๋ฆฝ ํŒจ์น˜๊ฐ€ ์•„๋‹ˆ๋ผ ํ•˜๋‚˜์˜ ์ˆ˜์ • ๋‹จ์œ„๋กœ ๋‹ค๋ค„์•ผ ํ•œ๋‹ค. + +--- + +## 2. ๊ฒฐํ•จ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๋ฐ ํŒŒ๊ธ‰ ํšจ๊ณผ ๋ถ„์„ + +| ํ•ญ๋ชฉ | ๋ถ„์„ | +|---|---| +| **๋ฐœ์ƒ ๋นˆ๋„** | ๊ฒฐํ•จ โ‘ก๋Š” ๋ Œ๋”๋ง ์ง€์—ฐ > 10s์ธ ์—์ด์ „ํŠธ/๋Œ€ํ˜• ์ถœ๋ ฅ์—์„œ ํ˜„์‹ค์ ์œผ๋กœ ๋ฐœ์ƒ(์ด์Šˆ ์ž์ฒด๊ฐ€ ์‹ค๊ด€์ธก ์‚ฌ๋ก€). โ‘ก๊ฐ€ ํ„ฐ์งˆ ๋•Œ๋งˆ๋‹ค โ‘ ๋กœ ์ธํ•ด 65๋ถ„ ๊ต์ฐฉ์ด ๋’ค๋”ฐ๋ฅธ๋‹ค. | +| **๋ฃจํ”„ ๊ด€์  ํŒŒ๊ธ‰** | `run_loop.sh`์˜ `--max-loop` ์‚ฌ์ดํด ์ „์ฒด๊ฐ€ ๋‹จ์ผ worker ์‹คํŒจ ํ•˜๋‚˜๋กœ ์ตœ๋Œ€ 65๋ถ„ ์ •์ง€. ๋ณ‘๋ ฌ ๋ฆฌ๋ทฐ์–ด(`--all-reviewer`) ํŽœ๋”˜ํŠธ ์‹œ ๋ฆฌ์†Œ์Šค ์ ์œ  + ๋Œ€๊ธฐ ๋ˆ„์ . | +| **๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ** | `.json`์˜ `pending`์€ "๋ฏธ์ˆ˜ํ–‰"๊ณผ "์ˆ˜ํ–‰ ์‹คํŒจ"๋ฅผ ๊ตฌ๋ณ„ ๋ถˆ๊ฐ€ํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค โ€” ๊ฐ์‚ฌ ๋กœ๊ทธ(events.log)์— subscriber ๊ธฐ๋ก์€ ๋‚จ์ง€๋งŒ, ์ƒํƒœ ์›์žฅ ์ž์ฒด๊ฐ€ ๊ฑฐ์ง“("์•„์ง ์•ˆ ํ•จ"). ์ด๋Š” B-14(๋กœ์ปฌ ๋””์Šคํฌ 100% ๋™๊ธฐํ™”)์—์„œ ์ˆ˜๋ฆฝํ•œ "๋””์Šคํฌ ์ƒํƒœ๊ฐ€ ์ง„์‹ค"์ด๋ผ๋Š” ๊ณ„์•ฝ์˜ ์ž”์—ฌ ์˜ˆ์™ธ๋‹ค. | +| **์‹ฌ๊ฐ๋„** | โ‘ : **HIGH** (์˜๊ตฌ ๊ต์ฐฉ + ์›์žฅ ์˜ค์—ผ). โ‘ก: **MEDIUM-HIGH** (๊ต์ฐฉ์˜ ์ฃผ ํŠธ๋ฆฌ๊ฑฐ, ๋‹ค๋งŒ quiescent ๋ธŒ๋ฆฌ๋ฏธํ‹ฐ๋ธŒ๊ฐ€ ์žˆ์–ด ์™„์ „ ๋ฌด๋ฐฉ๋น„๋Š” ์•„๋‹˜). | + +## 3. ์ˆ˜์ • ๋‚œ์ด๋„ ๋ฐ ์œ„ํ—˜๋„ ํ‰๊ฐ€ + +| ๊ฒฐํ•จ | ๋‚œ์ด๋„ | ๊ณต์ˆ˜ | ํŒŒ๊ธ‰ ๋ฒ”์œ„ / ์œ„ํ—˜ | +|---|---|---|---| +| โ‘  ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ fail-closed ์ „์ด | **ํ•˜ (Low)** | ~0.5์ผ (ํ…Œ์ŠคํŠธ ํฌํ•จ) | worker/reviewer ์–‘ break ๊ฒฝ๋กœ์— `status --set error` 1์ค„์”ฉ ์ถ”๊ฐ€. ์†Œ๋น„์ž(`wait_for_job` 2๊ฐœ)๋Š” ์ด๋ฏธ `error`๋ฅผ ์ •์ƒ ์ข…๋ฃŒ ์กฐ๊ฑด์œผ๋กœ ๋‹ค๋ฃจ๋ฏ€๋กœ **์†Œ๋น„์ž ๋ณ€๊ฒฝ ๋ถˆํ•„์š”** โ€” ํŒŒ๊ธ‰ ์ตœ์†Œ. ์œ ์ผ ์œ„ํ—˜: break ์‹œ์ ์˜ ์ƒํƒœ๋ฅผ `error`๋กœ ๋ฎ์–ด์“ธ ๋•Œ ์ด๋ฏธ ๋””์Šคํฌ์— terminal ์ƒํƒœ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ(rc=3 broker_unavailable ๋ถ„๊ธฐ๊ฐ€ ์ด๋ฏธ ๋””์Šคํฌ ์šฐ์„  ๋ณ‘ํ•ฉ์„ ํ•˜๋ฏ€๋กœ ๊ทธ๋Œ€๋กœ ์žฌ์‚ฌ์šฉ). | +| โ‘ก TUI settling ์•ˆ์ „ํ™” | **์ค‘ (Medium)** | ~1์ผ | `run_agent` ๋ฐ˜ํ™˜๊ฐ’ ์ ‘์ˆ˜(๋ฃจํ”„ :327์˜ `if ! run_agent` ํ™”) + quiescent ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹/ํ™˜๊ฒฝ๋ณ€์ˆ˜ํ™”(`SKS_QUIESCENT_TRIES/INTERVAL`๋Š” ์ด๋ฏธ ํŠœ๋„ˆ๋ธ”). ์ฃผ์ž… ์‹คํŒจ๋ฅผ ์ฆ‰์‹œ `error` ์ „์ด๋กœ ์—ฐ๊ฒฐํ•˜๋ฉด โ‘ก๊ฐ€ ํ„ฐ๋ ค๋„ โ‘  ๋•๋ถ„์— ๋ฃจํ”„๊ฐ€ **๋น ๋ฅด๊ฒŒ ์‹คํŒจ ์ข…๋ฃŒ**๋œ๋‹ค(์‚ฌ์‹ค์ƒ โ‘ก์˜ ํญ๋ฐœ ๋ฐ˜๊ฒฝ์„ โ‘  ํŒจ์น˜๊ฐ€ ์ ˆ๋‹จ). ์œ„ํ—˜: quiescent ๋Œ€๊ธฐ ์—ฐ์žฅ์€ ๋ฃจํ”„ ์ง€์—ฐ์„ ๋Š˜๋ฆฌ๋ฏ€๋กœ ์ƒํ•œ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๋ด‰์ธ ํ•„์š”. | + +## 4. ๊ตฌ์ฒด์  ํ•ด๊ฒฐ ๋ฐฉ์•ˆ ๋ฐ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์„ ์•ˆ + +### ๊ฒฐํ•จ โ‘  โ€” fail-closed ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ „์ด +1. **worker break (:360) ์ง์ „**: `"$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" status --job "$JOB_ID" --set "error"` ์ถ”๊ฐ€ โ€” `job_status` ๋ณ€์ˆ˜๊ฐ€ ์ด๋ฏธ ์žˆ์œผ๋ฏ€๋กœ, ๋ณด์กด์„ ์›ํ•˜๋ฉด `--set "$job_status"` ๊ณ„์—ด๋กœ ์„ธ๋ถ„ํ™”ํ•ด๋„ ๋˜๋‚˜ `wait_for_job`์˜ ์ข…๋ฃŒ ์–ดํœ˜๊ฐ€ `error`์ด๋ฏ€๋กœ **์ •๊ทœํ™”ํ•ด์„œ `error`๋กœ ๊ธฐ๋ก** ๊ถŒ์žฅ. +2. **reviewer break (:365) ๋™์ผ ์ ์šฉ** (๋Œ€์นญ ๊ฒฐํ•จ, ์ด์Šˆ์—๋Š” ๋ฏธ๊ธฐ์žฌ์ง€๋งŒ ๋™์ผ ๋ผ์ธ ๊ตฌ์กฐ). +3. **`run_agent` ์‹คํŒจ ์ฆ‰์‹œ ์ „์ด(โ‘ก ์—ฐ์‡„ ์ ˆ๋‹จ)**: ๋ฃจํ”„ :327์„ `if ! run_agent ...; then ; break; fi`๋กœ ๋ณ€๊ฒฝ โ€” ์ฃผ์ž… ์‹คํŒจ๊ฐ€ subscriber 120์ดˆ ๋Œ€๊ธฐ๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ์ฆ‰์‹œ ์‹คํŒจ๋กœ ํ™•์ •. +4. **์ค‘๋ณต ๋ฐฉ์ง€**: rc=3 broker_unavailable ๋ถ„๊ธฐ(:342-347)์˜ ๋””์Šคํฌ ์šฐ์„  ๋ณ‘ํ•ฉ ๊ฒฐ๊ณผ(`completed`/`error`)๋ฅผ ๊ทธ๋Œ€๋กœ ์กด์ค‘ โ€” ์ด๋ฏธ terminal์ด๋ฉด ๋ฎ์–ด์“ฐ์ง€ ์•Š๋Š” ๊ฐ€๋“œ(`disk_st` ์ฒดํฌ ์žฌ์‚ฌ์šฉ). + +### ๊ฒฐํ•จ โ‘ก โ€” TUI settling ์•ˆ์ „ ์ฒ˜๋ฆฌ +1. **์ฆ‰์‹œ ์กฐ์น˜**: ์ฃผ์ž… ์‹คํŒจ์˜ ์ฆ‰์‹œ error ์ „์ด(์œ„ 4-3)๊ฐ€ ํ•ต์‹ฌ โ€” settling์€ ์™„ํ™” ์žฅ์น˜์ผ ๋ฟ, ์‹คํŒจ ์‹œ์˜ ์›์žฅ ์ •์ง์„ฑ์ด ๋ณธ์งˆ. +2. **quiescent ํŒŒ๋ผ๋ฏธํ„ฐ ํ™˜๊ฒฝ๋ณ€์ˆ˜ํ™” ํŠœ๋‹**: `SKS_QUIESCENT_TRIES`(๊ธฐ๋ณธ 20)๋ฅผ ์žก ์ฃผ์ž… ๋งฅ๋ฝ์—์„œ `MAM_SKS_INJECT_TRIES`(์˜ˆ: 40ํšŒร—0.5s=20s)๋กœ ์ƒํ–ฅํ•˜๋Š” ์˜ต์…˜ + **ํ”„๋กฌํ”„ํŠธ ๋ณต๊ท€(`>`) ๊ฐ์ง€ ์‹œ ์กฐ๊ธฐ ํƒˆ์ถœ** โ€” `_pane_quiescent`๊ฐ€ "๋™์ผ ์บก์ฒ˜"๋งŒ ๋ณด๋Š” ๊ฒƒ์„, adapter์˜ `input_prompt`/`input_placeholder` ์‚ฌ์‹ค(`lib_py.agents facts`)๊ณผ ๊ต์ฐจ ํ™•์ธํ•˜๋ฉด idle ๋ณต๊ท€๋ฅผ ์ •ํ™•ํžˆ ํฌ์ฐฉ(์‚ฌ์‹ค ๋ธŒ๋ฆฌ์ง€๊ฐ€ ์ด๋ฏธ ๋‚ด๋ณด๋‚ด๋Š” `MAM_INPUT_PROMPT`/`MAM_INPUT_PLACEHOLDER`/`MAM_INPUT_RULE_PATTERN`์„ `send_keys_safe`๊ฐ€ ์ด๋ฏธ ์ˆ˜์‹ ํ•˜๋ฏ€๋กœ, quiescent ์„ฑ๊ณต ํŒ์ •์— placeholder ํ”„๋ ˆ์ฆŒ์Šค ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ตœ์†Œ ๋ณ€๊ฒฝ). +3. **์„ ํƒ(์ค‘๊ธฐ)**: ์ฃผ์ž… ์žฌ์‹œ๋„ 1ํšŒ โ€” ์‹คํŒจ ์‹œ settle ์žฌ๋Œ€๊ธฐ ํ›„ 1ํšŒ ์žฌ์ฃผ์ž…, ์žฌ์‹คํŒจ ์‹œ error ์ „์ด. ๋ฌดํ•œ ์žฌ์‹œ๋„๋Š” ๊ธˆ์ง€(๊ต์ฐฉ์˜ ์ƒˆ ํ˜•ํƒœ). + +## 5. ๊ฒ€์ฆ ๋ฐ ํ…Œ์ŠคํŠธ ๊ณ„ํš + +1. **๊ฒฐํ•จ โ‘  ํšŒ๊ท€ ๊ฐ€๋“œ (์‹ ๊ทœ)**: mock herdr/agent ํ™˜๊ฒฝ์—์„œ worker ์ฃผ์ž… ์‹คํŒจ๋ฅผ ์œ ๋„(`run_agent`๊ฐ€ rc=1 ๋ฐ˜ํ™˜ํ•˜๋„๋ก) โ†’ ์žก `.json`์ด `error`๋กœ ๊ฐฑ์‹ ๋˜๊ณ  `wait_for_job`์ด `max_wait` ์†Œ์ง„ ์—†์ด rc=1๋กœ ์ฆ‰์‹œ ๋ณต๊ท€ํ•จ์„ ๋‹จ์–ธ. ๋ฐ˜๋ก€: broker_unavailable+๋””์Šคํฌ completed ์‹œ ๋ฎ์–ด์“ฐ๊ธฐ ์—†์Œ. +2. **๊ฒฐํ•จ โ‘ก ํšŒ๊ท€ ๊ฐ€๋“œ (์‹ ๊ทœ)**: `_pane_quiescent`๊ฐ€ ์—ฐ์† ๋ณ€ํ™” ์บก์ฒ˜(๋ Œ๋”๋ง ์ค‘)๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๋Š” mock์œผ๋กœ `send_keys_safe`์˜ ๊ฑฐ๋ถ€ โ†’ `run_agent` rc=1 โ†’ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `error` ์ „์ด ์—ฐ์‡„๋ฅผ ๋‹จ์–ธ. +3. **๊ธฐ์กด ์Šค์œ„ํŠธ**: `tests/test_o2_race_free_lock.py`(loop lock), Issue #5 Phase 2a/2b G-๊ฐ€๋“œ๋“ค, B-14/B-15์˜ G-1~G-10(`tests/test_b13*`, `test_issue5_*`)์ด ์ด๋ฒˆ ๋ณ€๊ฒฝ๊ณผ ์ถฉ๋Œ ์—†์Œ์„ ํ™•์ธ โ€” ํŠนํžˆ `test_g5_no_blocking_io_while_the_registry_lock_is_held`๊ฐ€ ์ƒˆ `status --set` ํ˜ธ์ถœ ์œ„์น˜(๋ฝ ๋ฐ–)์™€ ์ •ํ•ฉ. +4. **์ „์ฒด ์Šค์œ„ํŠธ**: ํ˜„์žฌ ๊ธฐ์ค€์„ (750 passed) ์œ ์ง€ ํ™•์ธ. +5. **๋ผ์ด๋ธŒ ์Šค๋ชจํฌ**: ์‹ค์ œ 2-์—์ด์ „ํŠธ ์œ„์ž„์—์„œ ์ผ๋ถ€๋Ÿฌ ์ฃผ์ž…์„ ๋ฐฉํ•ด(์„ธ์…˜ ์ผ์‹œ ์ •์ง€) โ†’ ๋ฃจํ”„๊ฐ€ 65๋ถ„์ด ์•„๋‹ˆ๋ผ ์ˆ˜์‹ญ ์ดˆ ๋‚ด์— `error`๋กœ ์ข…๋ฃŒ๋˜๋Š”์ง€ ์‹ค์ธก. + +### ์ดํ‰ +๋‘ ๊ฒฐํ•จ ๋ชจ๋‘ **์‹ค์žฌํ•˜๋ฉฐ ์ƒํ˜ธ ์ฆํญ ๊ด€๊ณ„**๋‹ค(โ‘ก๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ, โ‘ ์ด ๊ต์ฐฉ์œผ๋กœ ๋ณ€ํ™˜). โ‘ ์€ ์›์žฅ ์ •์ง์„ฑ ๊ณ„์•ฝ(B-14 ๊ณ„์—ด)์˜ ๋งˆ์ง€๋ง‰ ์ž”์—ฌ ๊ตฌ๋ฉ์ด๊ณ  ์ˆ˜์ • ๋น„์šฉ์ด ๋งค์šฐ ๋‚ฎ๋‹ค(๋‚œ์ด๋„ ํ•˜, ~0.5์ผ). โ‘ก๋Š” ์ด๋ฏธ ์กด์žฌํ•˜๋Š” quiescent ํ”„๋ฆฌ๋ฏธํ‹ฐ๋ธŒ๋ฅผ ์ฃผ์ž… ๋งฅ๋ฝ์— ๋งž๊ฒŒ ํŠœ๋‹ํ•˜๊ณ  `run_agent` ๋ฐ˜ํ™˜๊ฐ’์„ ๋ฃจํ”„๊ฐ€ ์ˆ˜์šฉํ•˜๋„๋ก ํ•˜๋Š” ์ค‘๊ฐ„ ๋‚œ์ด๋„ ์ž‘์—…(~1์ผ)์ด๋‹ค. ๋‘˜์„ ํ•œ ์ˆ˜์ • ๋‹จ์œ„๋กœ ๋ฌถ์–ด ์ง„ํ–‰ํ•  ๊ฒƒ์„ ๊ถŒ๊ณ ํ•˜๋ฉฐ, ์„ค๊ณ„ ๋ณ€๊ฒฝ ์ˆ˜์ค€์˜ ์žฌ๊ณ„ํš์€ ๋ถˆํ•„์š”ํ•˜๋‹ค. \ No newline at end of file diff --git a/.agents/reports/reviewer-opencode-01/report-58fba2b1.md b/.agents/reports/reviewer-opencode-01/report-58fba2b1.md new file mode 100644 index 0000000..39cb58c --- /dev/null +++ b/.agents/reports/reviewer-opencode-01/report-58fba2b1.md @@ -0,0 +1,51 @@ +### [reviewer-opencode-01] Issue #6 ํ†ตํ•ฉ ๋ณด๊ณ ์„œ ๊ฒ€ํ†  ์˜๊ฒฌ + +- **Job**: `58fba2b1` / **๊ฒ€ํ† ์ž**: `reviewer-opencode-01` (์› 1์ฐจ ๋ฆฌ๋ทฐ Job `17f2e688`์˜ ์ž‘์„ฑ์ž) +- **๊ฒ€ํ†  ๋Œ€์ƒ**: `.agents/reports/issue_6_consolidated_review_report.md` (์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ์ž‘์„ฑ, 4์ธ ๋ฆฌ๋ทฐ ์ข…ํ•ฉ) +- **๊ฒ€ํ†  ๋ฐฉ๋ฒ•**: ํ†ตํ•ฉ ๋ณด๊ณ ์„œ์˜ ๋ชจ๋“  ํ•ต์‹ฌ ์ฃผ์žฅ์„ ๋ผ์ด๋ธŒ ์†Œ์Šคยท์‹ค์ธก์œผ๋กœ ๋…๋ฆฝ ์žฌ๊ฒ€์ฆ โ€” direct ๊ฒฝ๋กœ(`:200-232`), ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ CLI/์—”์ง„ CAS ์ฝ”๋“œ, Herdr 0.8.2 CLI ์‹ค๋„์›€, ์ข€๋น„ ์žก ํฌ๋ Œ์‹, mock_herdr ์ปค๋ฒ„๋ฆฌ์ง€. ๋‚ด 1์ฐจ ๋ฆฌ๋ทฐ(`17f2e688`)์˜ ๊ถŒ๊ณ  ๋Œ€๋น„ ์ฆ๊ฐ๋„ ๋Œ€์กฐํ–ˆ๋‹ค. + +--- + +## 1. ์ดํ‰ ๋ฐ ํ†ตํ•ฉ ๋ณด๊ณ ์„œ ์ •ํ•ฉ์„ฑ ํ‰๊ฐ€ + +**์ •ํ•ฉ์„ฑ ๋†’์Œ โ€” ์ข…ํ•ฉ์ ์œผ๋กœ ์ถฉ์‹คํ•˜๊ณ  ์ •ํ™•ํ•œ ํ†ตํ•ฉ์ด๋‹ค.** ํ•ต์‹ฌ ํŒ์ •(โ‘ โ‘ก ๋ชจ๋‘ ์‹ค์žฌ, ์ƒํ˜ธ ์ฆํญ, ๊ตญ์†Œ ํŒจ์น˜๋กœ ํ•ด๊ฒฐ ๊ฐ€๋Šฅ)์€ ๋‚˜๋ฅผ ํฌํ•จํ•œ 4์ธ ๋ฆฌ๋ทฐ์˜ ์‹ค์ธก๊ณผ ์ผ์น˜ํ•˜๋ฉฐ, ํ†ตํ•ฉ ๊ณผ์ •์—์„œ ์™œ๊ณกยท์ถ•์†Œ๋Š” ๋ฐœ๊ฒฌ๋˜์ง€ ์•Š์•˜๋‹ค. ์˜คํžˆ๋ ค ๋‚ด 1์ฐจ ๋ฆฌ๋ทฐ๋ฅผ **์ดˆ์›”**ํ•˜๋Š” ๋‘ ๊ฐ€์ง€ ์‹ค์งˆ ๊ธฐ์—ฌ๊ฐ€ ์žˆ๋‹ค: + +1. **`--type direct` ์ˆจ์€ ๊ฒฐํ•จ ๋ฐœ๊ฒฌ (ยง2.1-2)**: ๋‚˜๋Š” loop/discuss ๊ฒฝ๋กœ(`:351-361`, `:375-378`)๋งŒ ์ถ”์ ํ–ˆ์œผ๋‚˜, ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ๊ฐ€ ์‹ค์ œ๋กœ ํƒ€๋Š” ๊ฒฝ๋กœ๋Š” `runner.py:269`์˜ `--type direct`์ด๊ณ  ์ด ๊ฒฝ๋กœ(:213 `run_agent` ๋ฌด๊ฐ€๋“œ, :227 `wait "$sub_pid" || true`, ์ƒํƒœ ์ „์ด ๋ถ€์žฌ, `exit 0`)๊ฐ€ ์™„์ „ ์นจ๋ฌต์ด๋ผ๋Š” ๋ฐœ๊ฒฌ โ€” **์ง์ ‘ ํ™•์ธํ–ˆ๋‹ค. ์ด๊ฒƒ์ด ํ†ตํ•ฉ ๋ณด๊ณ ์„œ์˜ ๊ฐ€์žฅ ํฐ ๊ธฐ์—ฌ๋‹ค.** +2. **CAS ์„ ํ–‰์กฐ๊ฑด (`expected_status in (pending, running)`)**: ๋‚ด 1์ฐจ ๋ฆฌ๋ทฐ๊ฐ€ "์ด๋ฏธ terminal์ด๋ฉด ๋ฎ์–ด์“ฐ์ง€ ์•Š๋Š” ๊ฐ€๋“œ"๋ฅผ ๋…ผ์ ์œผ๋กœ๋งŒ ์–ธ๊ธ‰ํ•œ ๋ฐ˜๋ฉด, ํ†ตํ•ฉ ๋ณด๊ณ ์„œ๋Š” ์ด๋ฅผ ๊ตฌ์ฒด์  ๋ฉ”์ปค๋‹ˆ์ฆ˜(์—”์ง„์˜ ๊ธฐ์กด CAS ๊ณ„์•ฝ ์žฌ์‚ฌ์šฉ + CLI ๋…ธ์ถœ)์œผ๋กœ ๋ฐœ์ „์‹œ์ผฐ๋‹ค. ์‹ค์ธก: `engine.py:188`์— `expected_status` ์„ ํ–‰์กฐ๊ฑด์ด ์ด๋ฏธ ์กด์žฌํ•˜๋‚˜ **`registry.py` status ์„œ๋ธŒ์ปค๋งจ๋“œ(:292-295)๋Š” `--job`/`--set`๋งŒ ๋ฐ›๊ณ  ์ด๋ฅผ ๋…ธ์ถœํ•˜์ง€ ์•Š๋Š”๋‹ค** โ€” ๋ณด๊ณ ์„œ์˜ ยง4.1-1์ด ์ •ํ™•ํžˆ ์ด ๋ˆ„๋ฝ ๋ฐฐ์„ ์„ ๋ฉ”์šฐ๋Š” ์ œ์•ˆ์ž„์„ ํ™•์ธ. + +**์‹ค์ฆ ํฌ๋ Œ์‹ ๊ฒ€์ฆ (๊ฐ•๋ ฅํ•œ 1์ฐจ ์ฆ๊ฑฐ)**: ๋ณด๊ณ ์„œ ยง2.1-3์ด ์ธ์šฉํ•œ ์ข€๋น„ ์žก 3๊ฑด์„ ๋‚ด๊ฐ€ ์ง์ ‘ ์žฌํ™•์ธ: `93a44347`์™€ `2961b822`๋Š” **์ง€๊ธˆ ์ด ์ˆœ๊ฐ„์—๋„ `status: "pending"`์œผ๋กœ ๋ฐฉ์น˜**๋˜์–ด ์žˆ๊ณ , `ee242671`(๋ฐ”๋กœ ๋‚ด๊ฐ€ ์ด์ „์— "์ธํ„ฐ๋ŸฝํŠธ๋œ ๋ผ์šด๋“œ"๋กœ ์ง„๋‹จํ–ˆ๋˜ ๋‚ด ์žก)์€ `running`์— `started` ์ด๋ฒคํŠธ ํ•˜๋‚˜๋งŒ ๋‚จ์€ ์ฑ„ ํ‘œ๋ฅ˜ ์ค‘ โ€” ์ฆ‰ ๊ทธ๋•Œ์˜ ์ค‘๋‹จ์€ ์ธํ„ฐ๋ŸฝํŠธ๊ฐ€ ์•„๋‹ˆ๋ผ **direct ๊ฒฝ๋กœ๊ฐ€ ๋‚ด subscriber ์‹คํŒจ๋ฅผ ์นจ๋ฌต์‹œํ‚จ ๊ฒƒ**์ด์—ˆ์„ ๊ฐœ์—ฐ์„ฑ์ด ๋†’๋‹ค. ๊ฒฐํ•จ โ‘ ์˜ ๋ผ์ด๋ธŒ ํ”ผํ•ด ์‚ฌ๋ก€๋กœ์„œ ์ด๋ณด๋‹ค ์ข‹์€ ์ฆ๊ฑฐ๋Š” ์—†๋‹ค. + +## 2. ๋ˆ„๋ฝ๋˜๊ฑฐ๋‚˜ ๋ณด๊ฐ•์ด ํ•„์š”ํ•œ ํ•ต์‹ฌ ์‚ฌํ•ญ (Missing) + +1. **`run_agent` ๋ฐ˜ํ™˜๊ฐ’ ์ ‘์ˆ˜์˜ ๋ช…๋ฌธํ™” ํ•„์š”** (๋‚ด 1์ฐจ ๋ฆฌ๋ทฐ 4-3์˜ ํ•ต์‹ฌ์ด ํ†ตํ•ฉ ๋ณด๊ณ ์„œ์— ์•”๋ฌต์ ): `finalize_unfinished`๋Š” "๊ตฌ๋…์ž rcโ‰ 0 + ๋น„ํ„ฐ๋ฏธ๋„ ๋””์Šคํฌ"์—์„œ ๋ฐœ๋™ํ•˜๋ฏ€๋กœ ์ฃผ์ž… ์‹คํŒจ๋ฅผ **๊ฐ„์ ‘์ ์œผ๋กœ๋งŒ**(์ฃผ์ž… ์‹คํŒจ โ†’ started ๋ฏธ๋ฐœํ–‰ โ†’ ๊ตฌ๋…์ž idle timeout โ†’ rc=2 โ†’ finalize) ํฌ์ฐฉํ•œ๋‹ค. ์ด๋Š” ๋™์ž‘ํ•˜๋‚˜ (a) ์ฃผ์ž… ์‹คํŒจ๋ฅผ ์žก๋Š” ๋ฐ ์ตœ๋Œ€ 120์ดˆ(๊ตฌ๋…์ž idle timeout)๊ฐ€ ๊ฑธ๋ฆฌ๊ณ , (b) `run_agent`๊ฐ€ ์ด๋ฏธ rc=1๋กœ ์‹คํŒจ ์‚ฌ์‹ค์„ ์ฆ‰์‹œ ์•Œ๊ณ  ์žˆ๋Š”๋ฐ ์ด๋ฅผ ๋ฒ„๋ฆฌ๋Š” ๊ตฌ์กฐ๋Š” ๊ทธ๋Œ€๋กœ๋‹ค. **๊ถŒ๊ณ **: `run_agent` ์‹คํŒจ ์‹œ ์ฆ‰์‹œ finalize ํ˜ธ์ถœ์„ ๋ช…์‹œ์ ์œผ๋กœ ์„ค๊ณ„์— ํฌํ•จํ•˜๋ผ โ€” ์‹คํŒจ ์ „ํŒŒ๊ฐ€ 120์ดˆ โ†’ ์ˆ˜ ์ดˆ๋กœ ๋‹จ์ถ•๋˜๊ณ , "์ฃผ์ž…์€ ์‹คํŒจํ–ˆ์œผ๋‚˜ ์—์ด์ „ํŠธ๊ฐ€ ๋Šฆ๊ฒŒ started๋ฅผ ๋ฐœํ–‰ํ•˜๋Š”" ์—ฃ์ง€์—์„œ ๋‘ ๊ฒฝ๋กœ์˜ ๊ฒฝํ•ฉ์„ ์—†์•ค๋‹ค. +2. **`herdr agent wait` ์‹คํŒจ ๋ชจ๋“œ ํ‘œ์ค€ํ™”**: wait๊ฐ€ ํƒ€์ž„์•„์›ƒ/๋Œ€์ƒ ๋ถ€์žฌ๋กœ ์‹คํŒจํ•  ๋•Œ์˜ rc์™€ ์ถœ๋ ฅ ๊ณ„์•ฝ(์„ฑ๊ณต๊ณผ ์‹คํŒจ๋ฅผ ๊ตฌ๋ณ„ํ•  ์ˆ˜ ์žˆ๋Š”๊ฐ€)์ด ยง4.2์— ๋ช…์‹œ๋˜์ง€ ์•Š์•˜๋‹ค. ํด๋ฐฑ ํœด๋ฆฌ์Šคํ‹ฑ ์ง„์ž… ์กฐ๊ฑด์ด ์ด rc์— ์˜์กดํ•˜๋ฏ€๋กœ, Phase 0 ์ŠคํŒŒ์ดํฌ์— "wait ์‹คํŒจ rc ์‹ค์ธก" ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค. +3. **`MAM_SKS_*` ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฌธ์„œํ™”**: ๊ธฐ์กด `SKS_QUIESCENT_TRIES/INTERVAL`๊ณผ ์‹ ๊ทœ ๋„ค์ดํ‹ฐ๋ธŒ ํƒ€์ž„์•„์›ƒ(15000/5000ms)์˜ ํŠœ๋‹ ๋ณ€์ˆ˜๊ตฐ์„ `.mam.env.example`์— ๋ฌธ์„œํ™”ํ•˜๋Š” ํ•ญ๋ชฉ์ด ยง5์— ๋น ์ ธ ์žˆ๋‹ค(๊ณผ๊ฑฐ v4.1.x ํŒจ์น˜์˜ ํ‘œ์ค€ ์ ˆ์ฐจ). + +## 3. ๋ถˆํ•„์š”ํ•˜๊ฑฐ๋‚˜ ๊ณผ๋„ํ•œ ์ œ์•ฝ/์„ค๊ณ„ (Unnecessary) + +**๊ณผ๋„ํ•œ ์„ค๊ณ„๋Š” ์—†๋‹ค.** ๋ช…์‹œ์ ์œผ๋กœ ๊ฒ€ํ† ํ•œ ๊ฒฐ๊ณผ: + +- `--expect-status` ์‹ ์„ค์ด ์•„๋‹Œ **๊ธฐ์กด CAS ์—”์ง„(:188)์˜ CLI ๋…ธ์ถœ** โ€” ์ƒˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ์•„๋‹ˆ๋ผ ๋ฐฐ์„ . ์ตœ์†Œ ๋น„์šฉ ์ตœ๋Œ€ ํšจ๊ณผ. +- `agent wait --until idle --until done`์˜ `done` ํฌํ•จ: ์—์ด์ „ํŠธ๊ฐ€ ํ„ด์ด ์•„๋‹ˆ๋ผ ๋Œ€ํ™” ์ž์ฒด๋ฅผ ์ข…๋ฃŒํ•˜๋Š” ์ƒํƒœ๋ฅผ ๋†“์น˜์ง€ ์•Š๋Š” ์ •๋‹นํ•œ ํฌํ•จ. +- 0.7.x ํ™”๋ฉด ํœด๋ฆฌ์Šคํ‹ฑ ํด๋ฐฑ ์œ ์ง€: ์ œ๊ฑฐ๊ฐ€ ์•„๋‹Œ 2์ฐจ ๊ณ„์ธต ์œ ์ง€ โ€” ํ˜ธํ™˜์„ฑ ๋ณดํ—˜์ด๋ฉฐ ๋น„์šฉ์ด ๋‚ฎ๋‹ค. +- ์œ ์ผํ•œ ์‚ฌ์†Œํ•œ ์ง€์ : ยง4.2-2์˜ "์ž”์—ฌ busy ํ† ํฐ ์‚ฌ์ „ ์บก์ฒ˜ ๋Œ€์กฐ" ๊ฐ€๋“œ๋Š” ํด๋ฐฑ ๊ฒฝ๋กœ์—์„œ๋งŒ ํ•„์š”ํ•˜๋‹ค. 1์ฐจ ๊ฒŒ์ดํŠธ(wait --until idle)๋ฅผ ํ†ต๊ณผํ•œ ๋’ค์—๋Š” ์ƒํƒœ๊ฐ€ ์ด๋ฏธ idle์ด๋ฏ€๋กœ ๋„ค์ดํ‹ฐ๋ธŒ ๊ฒฝ๋กœ์—์„œ ์ด ๊ฐ€๋“œ๋Š” ์ค‘๋ณต โ€” ๊ตฌํ˜„ ์‹œ **ํด๋ฐฑ ๊ฒฝ๋กœ์— ํ•œ์ •**ํ•ด ๋ฐฐ์น˜ํ•˜๋Š” ๊ฒƒ์ด ์ฝ”๋“œ๋ฅผ ์ค„์ธ๋‹ค (๋™์ž‘์—๋Š” ๋ฌดํ•ดํ•˜๋ฏ€๋กœ ์ฐจ๋‹จ ์‚ฌํ•ญ ์•„๋‹˜). + +## 4. ๊ฐ„๊ณผ๋œ ์ž ์žฌ์  ์œ„ํ—˜ ๋ฐ ์—ฃ์ง€ ์ผ€์ด์Šค (Overlooked) + +1. **`agent prompt --wait --until working`์˜ ํ„ด ๋น„์ถ”์ ์„ฑ**: ์‹ค์ธกํ•œ herdr ๋„์›€๋ง์— "It does not track turns: if the agent is already working, that active turn's completion may match" โ€” ์ฆ‰ working ์ƒํƒœ์—์„œ ์ œ์ถœํ•˜๋ฉด **์ด์ „ ํ„ด**์˜ ์™„๋ฃŒ๊ฐ€ ์ƒˆ ์ž…๋ ฅ์˜ ์ˆ˜๋ฝ ์ฆ๊ฑฐ๋กœ ์˜คํŒ๋  ์ˆ˜ ์žˆ๋‹ค. ํ†ตํ•ฉ ๋ณด๊ณ ์„œ์˜ 1์ฐจโ†’2์ฐจ ์ˆœ์„œ(wait๋กœ idle ํ™•๋ณด ํ›„ prompt)๊ฐ€ ์ด๋ฅผ ๊ตฌ์กฐ์ ์œผ๋กœ ๋ฐฉ์ง€ํ•˜์ง€๋งŒ, ์ด ๋ณด์žฅ์ด **์ˆœ์„œ์— ์˜์กด**ํ•œ๋‹ค๋Š” ์‚ฌ์‹ค์ด ๋ฌธ์„œ์— ๋ช…์‹œ๋˜์ง€ ์•Š์•˜๋‹ค. "prompt๋Š” ๋ฐ˜๋“œ์‹œ wait ์„ฑ๊ณต ํ›„์—๋งŒ ํ˜ธ์ถœ"์„ ๊ณ„์•ฝ ์กฐ๊ฑด์œผ๋กœ ๋ชป๋ฐ•์•„์•ผ ํ•œ๋‹ค. +2. **wait ๋ฌดํ•œ ๋Œ€๊ธฐ ํ•จ์ •**: `agent wait`๋Š” `--timeout` ์—†์œผ๋ฉด **๋ฌดํ•œ ๋Œ€๊ธฐ**ํ•œ๋‹ค(์‹ค์ธก). ยง4.2-1์ด 15000ms๋ฅผ ๋ช…์‹œํ•œ ๊ฒƒ์€ ์ •ํ™•ํ•˜๋‚˜, ํƒ€์ž„์•„์›ƒ ๋ˆ„๋ฝ ์‹œ ์ƒˆ๋กœ์šด ๊ต์ฐฉ ์œ ํ˜•์ด ์ƒ๊ธด๋‹ค โ€” ๊ตฌํ˜„ ๊ฐ€๋“œ(ํƒ€์ž„์•„์›ƒ ์ธ์ž ๊ฐ•์ œ)์™€ ํ…Œ์ŠคํŠธ(`--timeout` ์—†๋Š” ํ˜ธ์ถœ ๊ธˆ์ง€ lint์„ฑ ๋‹จ์–ธ)๊ฐ€ ํ•„์š”. +3. **๊ตฌ๋…์ž rc=3(broker_unavailable)๊ณผ finalize์˜ ์ƒํ˜ธ์ž‘์šฉ**: loop ๊ฒฝ๋กœ์˜ ๊ธฐ์กด ๋””์Šคํฌ ์šฐ์„  ๋ณ‘ํ•ฉ(:342-347)์ด finalize์™€ ๋งŒ๋‚  ๋•Œ, ๋ธŒ๋กœ์ปค ์žฅ์•  ์ค‘ ๋„์ฐฉํ•œ ๋Šฆ์€ completed ์ด๋ฒคํŠธ์™€์˜ ๊ฒฝํ•ฉ์ด ์ƒˆ ํŒจ์น˜๋กœ ์–ด๋–ป๊ฒŒ ์ •๋ ฌ๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ ๋งคํŠธ๋ฆญ์Šค์— ๋ช…์‹œ์  ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ํ•„์š”ํ•˜๋‹ค (ยง5-1์˜ CAS ๋ณด์กด ํ…Œ์ŠคํŠธ๊ฐ€ ์ด๋ฅผ ์ปค๋ฒ„ํ•˜๋‚˜ rc=3 ๋ณ€ํ˜• ํฌํ•จ ๊ถŒ๊ณ ). +4. **์ข€๋น„ ์žก ์ฒญ์†Œ**: `93a44347`/`2961b822`๋Š” ํŒจ์น˜ ํ›„์—๋„ `pending`์œผ๋กœ ๋‚จ๋Š”๋‹ค โ€” ํŒจ์น˜๋Š” **์‹ ๊ทœ** ์‹คํŒจ๋งŒ ๋ง‰๋Š”๋‹ค. ๊ตฌํ˜„ ์žก์— ๊ณผ๊ฑฐ ์ข€๋น„ ์žก ์ˆ˜๋™ ์ •๋ฆฌ(`status --set error` ๋˜๋Š” ์‚ญ์ œ)๋ฅผ ๋ถ€์ˆ˜ ์ ˆ์ฐจ๋กœ ํฌํ•จํ•  ๊ฒƒ. + +## 5. ๊ตฌํ˜„ ์ฐฉ์ˆ˜๋ฅผ ์œ„ํ•œ ์ตœ์ข… ์ œ์•ˆ ๋ฐ ๊ถŒ๊ณ ์‚ฌํ•ญ + +1. **์ฐฉ์ˆ˜ ์Šน์ธ**: ๊ณต์ˆ˜ ์‚ฐ์ •(2.0~3.5 ์ธ์ผ)์€ ํ„ฐ์น˜ํฌ์ธํŠธ ์‹ค์ธก(3๊ฐœ finalize ํ˜ธ์ถœ๋ถ€ + CLI ์ธ์ž ~15์ค„ + ๋Ÿฌ๋„ˆ ํŒŒ์‹ฑ ~10์ค„ + ํ•ธ๋“œ์…ฐ์ดํฌ ๊ฒŒ์ดํŠธ ~60์ค„ + mock ~50์ค„ + ํ…Œ์ŠคํŠธ 2ํŒŒ์ผ)๊ณผ ์ •ํ•ฉ โ€” ํƒ€๋‹นํ•˜๋‹ค. +2. **๊ตฌํ˜„ ์ˆœ์„œ ๊ถŒ๊ณ **: (1) `registry.py` CLI `--expect-status` ๋…ธ์ถœ(๋…๋ฆฝ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ, ๋ฌด์œ„ํ—˜) โ†’ (2) `finalize_unfinished` direct+loop ์ ์šฉ โ†’ (3) ๋Ÿฌ๋„ˆ rc-๊ด€์šฉ ํŒŒ์‹ฑ โ†’ (4) ๋„ค์ดํ‹ฐ๋ธŒ ํ•ธ๋“œ์…ฐ์ดํฌ + ํด๋ฐฑ โ†’ (5) mock/ํšŒ๊ท€ ํ…Œ์ŠคํŠธ. ๊ฐ ๋‹จ๊ณ„ ํ›„ 750 ์ „์ฒด ์Šค์œ„ํŠธ ์œ ์ง€ ํ™•์ธ. +3. **ํ…Œ์ŠคํŠธ ๋ณด๊ฐ•**: ยง5์˜ 2๊ฐœ ์‹ ๊ทœ ํŒŒ์ผ์— ์œ„ ยง4์˜ ์—ฃ์ง€ 3๊ฑด(wait ์‹คํŒจ rc, ํ„ด ๋น„์ถ”์ ์„ฑ ๋ฐฉ์ง€ ๊ณ„์•ฝ, rc=3 ๊ฒฝํ•ฉ)๊ณผ ๋‚ด 1์ฐจ ๋ฆฌ๋ทฐ ยง5-1(์ฃผ์ž… ์‹คํŒจ โ†’ ์ฆ‰์‹œ error ์ „์ด ์—ฐ์‡„)์„ ์‹œ๋‚˜๋ฆฌ์˜ค๋กœ ์ถ”๊ฐ€. +4. **๋ฌธ์„œ ๋™๊ธฐํ™”**: MESSAGING.md(ยง5.1 wapper ์ข…๋ฃŒ ๊ณ„์•ฝ ๊ฐฑ์‹ )์™€ `.mam.env.example`(ํƒ€์ž„์•„์›ƒ ๋ณ€์ˆ˜๊ตฐ)์„ ํŒจ์น˜ ์ปค๋ฐ‹์— ํฌํ•จ โ€” v4.4.0 ๋ฌธ์„œ ์›์žฅ ์ฒด๊ณ„์˜ ํ‘œ์ค€ ์ ˆ์ฐจ. + +## 6. ์ตœ์ข… ํŒ์ • + +ํ†ตํ•ฉ ๋ณด๊ณ ์„œ๋Š” 4์ธ ๋ฆฌ๋ทฐ๋ฅผ ์™œ๊ณก ์—†์ด ์ข…ํ•ฉํ–ˆ๊ณ , direct ๊ฒฝ๋กœ ๋ฐœ๊ฒฌ๊ณผ CAS ์„ ํ–‰์กฐ๊ฑด ์„ค๊ณ„๋กœ ์› ๋ฆฌ๋ทฐ๋“ค(๋‚ด ๊ฒƒ ํฌํ•จ)์„ ์‹ค์งˆ์ ์œผ๋กœ ์ดˆ์›”ํ–ˆ๋‹ค. ํฌ๋ Œ์‹ ์ฆ๊ฑฐ(์ข€๋น„ ์žก)๋Š” ๋ผ์ด๋ธŒ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์—์„œ ์žฌ๊ฒ€์ฆ๋˜์—ˆ์œผ๋ฉฐ, ํ•ด๊ฒฐ์•ˆ์€ ๊ธฐ์กด CAS ์—”์ง„๊ณผ Herdr 0.8.2 ๋„ค์ดํ‹ฐ๋ธŒ API์˜ ์ •ํ™•ํ•œ ์žฌ์‚ฌ์šฉ์œผ๋กœ ๊ณผ์„ค๊ณ„ ์—†์ด ์ตœ์†Œ ๋น„์šฉ์ด๋‹ค. ๋‚ด๊ฐ€ ๋ณด๊ฐ•ํ•œ 4๊ฐœ ํ•ญ๋ชฉ(run_agent ์ฆ‰์‹œ finalize, wait ์‹คํŒจ rc ๊ณ„์•ฝ, ํ„ด ๋น„์ถ”์ ์„ฑ ์ˆœ์„œ ๊ณ„์•ฝ ๋ช…๋ฌธํ™”, ์ข€๋น„ ์ฒญ์†Œ)์€ ๋ชจ๋“  ๊ฒƒ์ด ๊ตฌํ˜„ ์ฐฉ์ˆ˜ ์ „/์ค‘ ๋ฐ˜์˜ ๊ฐ€๋Šฅํ•œ ๊ถŒ๊ณ  ์‚ฌํ•ญ์ด์ง€ ํ†ตํ•ฉ ๋ณด๊ณ ์„œ์˜ ๊ฒฐํ•จ์ด ์•„๋‹ˆ๋‹ค. ๊ตฌํ˜„ ์ฐฉ์ˆ˜ ์ค€๋น„ ์™„๋ฃŒ ํŒ์ •์— ๋™์˜ํ•œ๋‹ค. + +[VERDICT: PASS] \ No newline at end of file diff --git a/docs/contracts/issue-6/implementation_plan.md b/docs/contracts/issue-6/implementation_plan.md new file mode 100644 index 0000000..8ef9f21 --- /dev/null +++ b/docs/contracts/issue-6/implementation_plan.md @@ -0,0 +1,242 @@ +# ๐Ÿ“‘ Issue #6 Implementation Plan (Phase 1โ€“5 Execution & Gate Specification) + +- **Document Version**: Rev.2 (Planner `planner-reviewer-claude-01`, job `f4873d82` โ€” Creator `creator-agy-01` ์ด์˜์ œ๊ธฐ 3๊ฑด ๋ฐ˜์˜; Rev.1์€ job `d2d70f57`) +- **Issue**: [Gitea Issue #6 โ€” `[Bug] delegate-job ์›Œ์ปค ์‹คํŒจ ์‹œ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋ฏธ๊ฐฑ์‹ (Fail-Silent) ๋ฐ TUI ํ„ด ๋งˆ๊ฐ ๋ ˆ์ด์Šค ์ปจ๋””์…˜ ํ•ด๊ฒฐ`](https://git.godopu.com/tmpl/multi-agent-mux/issues/6) +- **Source of Truth**: `.agents/reports/issue_6_consolidated_review_report.md` (Rev. 2) โ€” 4์ธ ๋ฆฌ๋ทฐ์–ด ํ•ฉ์˜ ์‚ฌ์–‘ +- **Baseline Commit**: `f98b958` (`main`, v4.4.0), Herdr `0.8.2`, Python 3.9.6, ์ „์ฒด ์Šค์œ„ํŠธ 750 passed +- **Status**: ์ฐฉ์ˆ˜ ์ „ (Phase 1 ๋Œ€๊ธฐ) + +--- + +## 1. ๊ฐœ์š” ๋ฐ ์„ค๊ณ„ ์›์น™ + +๋ณธ ๋ฌธ์„œ๋Š” Rev.2 ํ•ฉ์˜ ์‚ฌ์–‘(ยง4)์„ **ํŒŒ์ผยทํ•จ์ˆ˜ยท๋ผ์ธ ๋‹จ์œ„์˜ ์‹คํ–‰ ๊ณ„ํš**์œผ๋กœ ์ „๊ฐœํ•˜๊ณ , ๋‹จ๊ณ„๋ณ„ ์™„๋ฃŒ ๊ฒŒ์ดํŠธยท๋กค๋ฐฑยท๋น„๋ฒ”์œ„๋ฅผ ๊ทœ์ •ํ•œ๋‹ค. ๋ชจ๋“  ๋ผ์ธ ๋ฒˆํ˜ธ๋Š” ๊ธฐ์ค€ ์ปค๋ฐ‹ `f98b958` ๊ธฐ์ค€์ด๋ฉฐ, ์ฐฉ์ˆ˜ ์‹œ `grep -n`์œผ๋กœ ์žฌํ™•์ธํ•œ๋‹ค. + +### ๐Ÿ“Œ ํ•ต์‹ฌ ์›์น™ (Rev.2 ยง4 ํ•ฉ์˜) +1. **๊ธฐ์กด CAS ์—”์ง„ ์žฌ์‚ฌ์šฉ**: ํƒ€์ž„์•„์›ƒ fail-closed๋Š” `engine.py:50`์˜ `deadline_timeout` ์š”์ฒญ(๋น„-operator, terminal ์ดํ›„ ๊ฑฐ๋ถ€)์œผ๋กœ๋งŒ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์‹ ๊ทœ `--expect-status` ํ”Œ๋ž˜๊ทธ์™€ operator `error` ์˜ค๋ฒ„๋ผ์ด๋“œ๋Š” ๋„์ž…ํ•˜์ง€ ์•Š๋Š”๋‹ค. +2. **์‚ฌ์ „ ํ•ธ๋“œ์…ฐ์ดํฌ ์ˆœ์„œ**: TUI ์œ ํœด ๋Œ€๊ธฐ(`wait_for_agent_idle`)๋Š” **๊ตฌ๋…์ž(`job_subscriber.py`) ๊ธฐ๋™ ์ด์ „**์— ์™„๋ฃŒํ•œ๋‹ค. ๊ตฌ๋…์ž idle ์‹œ๊ณ„(`job_subscriber.py:265-267`, subscribe ์‹œ์  ๊ธฐ์‚ฐ)๋ฅผ ์ž ์‹ํ•˜์ง€ ์•Š๋Š”๋‹ค. +3. **์œ ํœด ์˜ˆ์‚ฐ 120์ดˆ**: `SKS_IDLE_TIMEOUT_MS` ๊ธฐ๋ณธ `120000`(์‹ค์ธก: codex `completed`โ†’`done` 31s). +4. **๋ธŒ๋กœ์ปค ์žฅ์•  ๊ฒฉ๋ฆฌ (B-15 ๊ณ„์•ฝ)**: ๊ตฌ๋…์ž `rc=3`์€ ์›Œ์ปค ์‹คํŒจ๊ฐ€ ์•„๋‹ˆ๋‹ค. ์žก ๋ฐ๋“œ๋ผ์ธ๊นŒ์ง€ ๋””์Šคํฌ๋ฅผ ์žฌ๊ด€์ธกํ•œ ๋’ค์—๋งŒ `deadline_timeout`์„ ์ ์šฉํ•œ๋‹ค. +5. **๋ฆฌํ…Œ์ธ ์˜ค์—ผ ๋ฐฉ์ง€ (O-1)**: loop/discuss ๊ฒฝ๋กœ์—์„œ๋Š” ๋ž˜ํผ๊ฐ€ MQTT `error` ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค(๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ „์ด๋งŒ). direct ๊ฒฝ๋กœ๋Š” ๊ฐ์‚ฌยท๋ชจ๋‹ˆํ„ฐ์šฉ์œผ๋กœ ๋ฐœํ–‰ํ•œ๋‹ค. +6. **๋กค๋ฐฑ ์Šค์œ„์น˜**: ๊ฐ ํ–‰์œ„ ๋ณ€๊ฒฝ์€ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ฆ‰์‹œ ์ด์ „ ๋™์ž‘์œผ๋กœ ๋ณต๊ท€ ๊ฐ€๋Šฅํ•ด์•ผ ํ•œ๋‹ค (Issue #5 ๊ด€๋ก€). +7. **๋ฏธ์ฃผ์ž… ์‹คํŒจ๋Š” `error`๊ฐ€ ์•„๋‹ˆ๋‹ค (Rev.2 Secondary)**: ํ”„๋กฌํ”„ํŠธ๊ฐ€ ํ•œ ๊ธ€์ž๋„ ์ „๋‹ฌ๋˜์ง€ ์•Š์€ ์‹คํŒจ(ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธยท์œ ํœด ๋Œ€๊ธฐ ํƒ€์ž„์•„์›ƒ)๋Š” `deadline_timeout`์ด ์•„๋‹ˆ๋ผ **`cancelled`**(detail `session_busy`)๋กœ ๋‹ซ๊ณ  ๋ž˜ํผ rc=5๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์žฌ์Šค์ผ€์ค„ ๊ฐ€๋Šฅ์„ฑ์„ ๋ณด์กดํ•˜๋˜ ๋ ˆ์ฝ”๋“œ๋Š” ๋น„์ข…๋ฃŒ ์ƒํƒœ๋กœ ๋‚จ๊ธฐ์ง€ ์•Š๋Š”๋‹ค. +8. **์…ธโ†’Python ๊ฒฝ๊ณ„์—์„œ herdr ๊ฒฉ๋ฆฌ ๋ช…์‹œ ์ „๋‹ฌ (Rev.2 Primary)**: Python ์ชฝ `herdr` ํ˜ธ์ถœ์€ ๋ฐ˜๋“œ์‹œ `--session "$HERDR_SESSION_NAME"`์„ ๋ช…์‹œํ•˜๊ณ , ์…ธ ์ชฝ ํ˜ธ์ถœ์€ `mam_herdr`/`_sks_herdr`๋งŒ ์‚ฌ์šฉํ•œ๋‹ค. ์ธํ„ฐํ”„๋ฆฌํ„ฐ๋Š” ํ•ญ์ƒ `"$(_delegate_py_bin)"`์ด๋‹ค. + +### ๐Ÿ“Œ ์‹ค์ธก์œผ๋กœ ํ™•์ •๋œ ์‚ฌ์‹ค (๊ณ„ํš์˜ ์ „์ œ) +| ์‚ฌ์‹ค | ๊ทผ๊ฑฐ | +|---|---| +| direct ๊ฒฝ๋กœ `wait "$sub_pid" \|\| true`๋Š” `:227`, loop ์›Œ์ปค/๋ฆฌ๋ทฐ์–ด ์กฐ๊ธฐ ํƒˆ์ถœ์€ `:359`/`:375` | `grep -n` (1์ฐจ ๋ณด๊ณ ์„œ์˜ `:213`์€ ์˜ค๊ธฐ) | +| ๋ช…์‹œ์  ์ฃผ์ž… ์‹คํŒจ(`send_keys_safe` rcโ‰ 0)๋Š” `set -e`๋กœ exit 1 + EXIT trap(`:523`)์ด `error` ๋ฐœํ–‰ โ†’ **๊ธฐ๋ณธ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์—์„œ๋Š” ์ด๋ฏธ fail-closed** | ์‹ค์ œ ๋ž˜ํผ์— rc=4 ์ฃผ์ž… ์‹ค์ธก | +| ๋‹จ, trap ๋ช…๋ น์— `--registry-dir`๊ฐ€ ์—†์–ด ์ปค์Šคํ…€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์—์„œ๋Š” `job record not found`๋กœ `pending` ์ž”์กด | ๋™์ผ ์‹ค์ธก | +| ๋ž˜ํผ exit ํ›„ ๊ตฌ๋…์ž ํ”„๋กœ์„ธ์Šค๊ฐ€ ๊ณ ์•„๋กœ ์ž”์กด | ๋™์ผ ์‹ค์ธก (`pgrep job_subscriber.py`) | +| `deadline_timeout`: pendingโ†’error ์ปค๋ฐ‹, completedโ†’`conflicting error after terminal completed; refusing` | `engine.transition` ์ง์ ‘ ํ˜ธ์ถœ ์‹ค์ธก | +| ์žก ์™„๋ฃŒ ํ›„ ์—์ด์ „ํŠธ ์ƒํƒœ๋Š” `done`(idle ์•„๋‹˜); working ์ค‘์—๋„ `interactive_ready: true`, `โฏ` ๋ Œ๋”๋ง | `herdr agent list`/ํŽ˜์ธ ์บก์ฒ˜ ์‹ค์ธก | +| `herdr agent wait`๋Š” ์ด๋ฏธ ๋ชฉํ‘œ ์ƒํƒœ๋ฉด ์ฆ‰์‹œ rc=0, ์•„๋‹ˆ๋ฉด timeout ์‹œ rc=1 + stderr JSON | ์‹ค์ธก, `lib_py/loop/watcher.py` ๋ชจ๋“ˆ docstring๊ณผ ์ผ์น˜ | +| `dispatch_token`์€ `claim`/`redispatch`์—์„œ๋งŒ ๋ฐœ๊ธ‰(`engine.py:87,297-300`) โ†’ direct ์žก์€ ํ† ํฐ ์—†์Œ, loop 2ํšŒ์ฐจ๋ถ€ํ„ฐ ์กด์žฌ | ์ฝ”๋“œ | +| **์‹ค์ œ `herdr` ๋ฐ”์ด๋„ˆ๋ฆฌ๋Š” `HERDR_SESSION`ยท`HERDR_SOCKET_PATH` ํ™˜๊ฒฝ๋ณ€์ˆ˜์™€ `--session ` ํ”Œ๋ž˜๊ทธ๋งŒ ์ธ์‹ํ•˜๋ฉฐ, MAM์˜ `HERDR_SESSION_NAME`์€ ์ธ์‹ํ•˜์ง€ ์•Š๋Š”๋‹ค.** `HERDR_SESSION_NAME`์€ `lib.sh`์˜ `herdr()`/`mam_herdr`/`_real_herdr`(`lib.sh:234-243`)๊ฐ€ `--session`์œผ๋กœ ๋ฒˆ์—ญํ•˜๋Š” ์…ธ ํ•จ์ˆ˜ ์ „์šฉ ๋ณ€์ˆ˜๋ผ **Python `subprocess`์—๋Š” ์ƒ์†๋˜์ง€ ์•Š๋Š”๋‹ค** (Rev.2 Primary-A) | `env -u โ€ฆ herdr agent get` 5์กฐํ•ฉ ์‹ค์ธก: `HERDR_SESSION_NAME`๋งŒ โ†’ `server_not_running`(๊ธฐ๋ณธ ์†Œ์ผ“), `HERDR_SESSION`/`HERDR_SOCKET_PATH`/`--session` โ†’ ์ •์ƒ | +| `lib.sh`๋ฅผ sourceํ•œ ์…ธ์—์„œ๋„ Python `subprocess.run(["herdr", โ€ฆ])`๋Š” `/opt/homebrew/bin/herdr`๋กœ ํ•ด์„๋˜์–ด ๊ธฐ๋ณธ ์†Œ์ผ“์„ ์กฐํšŒํ•œ๋‹ค | ๋™์ผ ์‹ค์ธก (f) | +| **loop ๋ชจ๋“œ ์›Œ์ปคโ†’๋ฆฌ๋ทฐ์–ด ์ „ํ™˜์€ v4.4.0์—์„œ ์ด๋ฏธ ๊นจ์ ธ ์žˆ๋‹ค**: `:276-283`์˜ `update --status pending --iteration $iteration`์ด ์›Œ์ปค `completed` ํ›„ ๊ฐ™์€ iteration์œผ๋กœ `redispatch`๋ฅผ ์š”์ฒญ โ†’ `redispatch iteration 1 does not advance past 1` rc=4 โ†’ `set -e`๋กœ ๋ž˜ํผ ์ข…๋ฃŒ (Rev.2 Tertiary) | `registry.py` CLI ์‹ค์ธก (iteration 2๋ฉด rc=0) | +| `cancelled` ์š”์ฒญ์€ ๋น„-operator ์†Œ์Šค๋กœ pendingโ†’cancelled ์ปค๋ฐ‹, completed ์ดํ›„ ๊ฑฐ๋ถ€, ์ดํ›„ operator `redispatch`(iteration ์ „์ง„)๋กœ ์žฌ๊ฐœ ๊ฐ€๋Šฅ | `engine.transition` ์‹ค์ธก | + +--- + +## 2. ๋‹จ๊ณ„๋ณ„ ๊ตฌํ˜„ ๋‹จ์œ„ ๋ฐ ํ•„์ˆ˜ ์™„๋ฃŒ ๊ธฐ์ค€ (Roadmap & Gates) + +| ๋‹จ๊ณ„ | ์ž‘์—… ๋ชฉํ‘œ | ์„ธ๋ถ€ ๊ตฌํ˜„ (ยง3 ์ฐธ์กฐ) | ํ•„์ˆ˜ ์™„๋ฃŒ ๊ฒŒ์ดํŠธ | ๋กค๋ฐฑ | +|---|---|---|---|---| +| **Phase 1** | **๋ ˆ์ง€์ŠคํŠธ๋ฆฌ CAS ํ‘œ์ค€ ์—ฐ๋™ ๋ฐ delegate-job Fail-Closed** | โ€ข `registry.py close --request deadline_timeout\|cancelled` ์„œ๋ธŒ์ปค๋งจ๋“œ
โ€ข ๋ž˜ํผ `finalize_unfinished`/`reap_subscriber` ํ—ฌํผ, direct(`:227`)ยทloop(`:359`,`:375`)ยท`run_agent` ์‹คํŒจยทํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์‹คํŒจ 5๊ฐœ ์ข…๋ฃŒ ๊ฒฝ๋กœ ์ผ์›ํ™”
โ€ข rc=3 ๋””์Šคํฌ ์žฌ๊ด€์ธก, trap `--registry-dir`, dry-run ์ˆœ์„œ, ์ข…๋ฃŒ ์ฝ”๋“œ ๊ณ„์•ฝ(rc 5 `session_busy` ํฌํ•จ)
โ€ข **loop ์—ญํ•  ์ „ํ™˜ `attempt_id` ๊ทœ์•ฝ** (ยง3.1.6) | โ€ข `--agent human` ๊ฒฐ์ •์  ์žฌํ˜„ 4์ข…(ยง5 T1-1~T1-4)์ด ์ˆ˜์ • ์ „ ์‹คํŒจยท์ˆ˜์ • ํ›„ ํ†ต๊ณผ
โ€ข T1-10(์›Œ์ปค ์™„๋ฃŒโ†’๋ฆฌ๋ทฐ์–ด ์žฌ๋””์ŠคํŒจ์น˜ ์„ฑ๊ณต) ์ˆ˜์ • ์ „ rc=4ยท์ˆ˜์ • ํ›„ rc=0
โ€ข `test_wrapper_exit_code_mapping_literals_are_frozen`ยทF-A 3์ข… ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ
โ€ข `MAM_DELEGATE_FAIL_CLOSED=0`์—์„œ ๊ธฐ์กด exit 0 ๋™์ž‘ ์žฌํ˜„ | `MAM_DELEGATE_FAIL_CLOSED=0` (ํ—ฌํผ no-op, `\|\| true` ๋ณต์›) | +| **Phase 2** | **์‚ฌ์ „ TUI ์œ ํœด ํ•ธ๋“œ์…ฐ์ดํฌ ๋ฐ ์ œ์ถœ ๊ฒ€์ฆ ๋ณด๊ฐ•** | โ€ข `lib_py/loop/watcher.py` CLI ์ง„์ž…์ (`"$(_delegate_py_bin)" -m lib_py.loop.watcher wait โ€ฆ --session "$HERDR_SESSION_NAME"`)
โ€ข `lib.sh: wait_for_agent_idle` (์‚ฌ์ „ `mam_herdr agent get` โ†’ `unknown`/`blocked` ๋ถ„๊ธฐ โ†’ watcher CLI, `SKS_IDLE_RETRIES` ๋ฐฑ์˜คํ”„ ์žฌ์‹œ๋„, ์ตœ์ข… ์‹คํŒจ rc 1 โ†’ ๋ž˜ํผ rc 5 `cancelled`)
โ€ข ๋ž˜ํผ `run_agent`๋ฅผ `preflight_agent`+`inject_agent`๋กœ ๋ถ„๋ฆฌ, ์ˆœ์„œ: preflight โ†’ idle wait โ†’ subscriber โ†’ inject
โ€ข `send_keys_safe` ํด๋ฐฑ ์ œ์ถœ ๊ฒ€์ฆ์—์„œ pre-submit busy ํ† ํฐ ๋ฐฐ์ œ, `agent prompt --wait --until working` ์ˆ˜๋ฝ ํ™•์ธ(์žฌ์ œ์ถœ ๊ธˆ์ง€) | โ€ข mock_herdr ์ƒํƒœ ์‹œํ€€์Šค ํ…Œ์ŠคํŠธ ยง5 T2-1~T2-11 ํ†ต๊ณผ
โ€ข `test_h16`(Enter ์ •ํ™•ํžˆ 1ํšŒ)ยท`test_b8`ยท`test_c1_*` ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ
โ€ข ๋ž˜ํผ ๋กœ๊ทธ์—์„œ `SUBSCRIBED` ์ด์ „์— idle ๋Œ€๊ธฐ ์™„๋ฃŒ ๋กœ๊ทธ๊ฐ€ ์ถœ๋ ฅ๋จ
โ€ข T2-10: watcher CLI argv์— `--session` ํฌํ•จ, T2-11: `lib.sh`์— bare `python`/`python3 -m lib_py.loop` ํ˜ธ์ถœ 0๊ฑด(์ •์  ๋ฆฐํŠธ) | `SKS_IDLE_HANDSHAKE=0` (๊ฒŒ์ดํŠธ ์Šคํ‚ต, ๊ธฐ์กด `_pane_quiescent`๋งŒ ์‚ฌ์šฉ), `SKS_PROMPT_WAIT=0` | +| **Phase 3** | **์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ๋Ÿฌ๋„ˆ Fail-Fast ์—ฐ๊ฒฐ** | โ€ข `runner.delegate_job`: rcโ‰ 0์—์„œ๋„ `registered job:` ํŒŒ์‹ฑ, `created_jobs`ยทcheckpoint ๊ธฐ๋ก, id ๋ฐ˜ํ™˜
โ€ข **rc=5(`session_busy`)๋Š” `MAM_SESSION_BUSY_RETRY_S`(๊ธฐ๋ณธ 30) ๋Œ€๊ธฐ ํ›„ 1ํšŒ ์žฌ์œ„์ž„(์ƒˆ job id)**, ์žฌ์‹คํŒจ ์‹œ None
โ€ข ํ˜ธ์ถœ๋ถ€๋Š” `wait_for_job`์ด ๋””์Šคํฌ `error`/`cancelled`๋กœ 1s ๋‚ด False
โ€ข `run_loop.sh` ๋ ˆ๊ฑฐ์‹œ `delegate_job_safe` ๋™์ผ ํŒŒ์‹ฑ(`MAM_LOOP_PY=0` ๊ฒฝ๋กœ ๋ณด์กด) | โ€ข `LoopRunner.wait_for_job`์ด fail-closed ์žก์—์„œ โ‰ค 2s ๋ฐ˜ํ™˜(T3-1)
โ€ข ์‹คํŒจ job id๊ฐ€ `.mam/loop-checkpoint.json`์— ๊ธฐ๋ก(T3-2)
โ€ข rc=5 1ํšŒ ์žฌ์œ„์ž„ ํ›„ ์„ฑ๊ณต/์‹คํŒจ ๋ถ„๊ธฐ(T3-3)
โ€ข `test_issue5_phase3_loop.py` ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ | ํ•จ์ˆ˜ ๋‹จ์œ„ revert (ํ”Œ๋ž˜๊ทธ ๋ถˆํ•„์š”: rcโ‰ 0 ๊ฒฝ๋กœ๋งŒ ๋ณ€๊ฒฝ) | +| **Phase 4** | **๋‹จ์œ„/๊ณ„์•ฝ ํšŒ๊ท€ ์Šค์œ„ํŠธ** | โ€ข `tests/test_issue6_fail_closed.py`, `tests/test_issue6_turn_handshake.py`
โ€ข `tests/conftest.py` mock_herdr์— `agent_status`ยท`agent wait`ยท`agent prompt --wait` ๋ชจํ‚น
โ€ข ๋ฌธ์„œ: SKILL.md ์ข…๋ฃŒ ์ฝ”๋“œ ๊ณ„์•ฝ, MESSAGING.md ยง5.1 ๋ณด๊ฐ• | โ€ข ์‹ ๊ทœ ํ…Œ์ŠคํŠธ ์ „๋ถ€ **์ˆ˜์ • ์ „ ์ฝ”๋“œ์—์„œ ์‹คํŒจํ•จ์„ ์—ญ๊ฒ€์ฆ**(Issue #5 ๊ด€๋ก€)
โ€ข ์ „์ฒด ์Šค์œ„ํŠธ 750 + ์‹ ๊ทœ โ‰ฅ 20, 0-Fail
โ€ข `git diff --check` ํด๋ฆฐ | ํ…Œ์ŠคํŠธ ํŒŒ์ผ ์‚ญ์ œ๋งŒ์œผ๋กœ ์›๋ณต | +| **Phase 5** | **์‹ค๊ธฐ๋™ 4๊ฐœ ์—์ด์ „ํŠธ E2E ๋ฐ ๋ฆด๋ฆฌ์Šค** | โ€ข Claude/Grok/Codex/OpenCode ๊ฐ 1ํšŒ: `completed` ์งํ›„ ๋™์ผ ์„ธ์…˜ ์—ฐ์† ์œ„์ž„ ํƒ€์ž„๋ผ์ธ ๊ธฐ๋ก
โ€ข ํƒ€์ž„์•„์›ƒ ์ฃผ์ž…(์—์ด์ „ํŠธ ๋ฌด์‘๋‹ต)์œผ๋กœ 65๋ถ„ ๋Œ€์‹  โ‰ค `IDLE_TIMEOUT`+5s ๋‚ด `error` ํ™•์ธ
โ€ข ์ž”์กด ๋ ˆ์ฝ”๋“œ 3๊ฑด ํ์‡„, IMPROVEMENTS.md ์›์žฅ, VERSIONS.md `v4.4.1` 4-way ๋ฝ์Šคํ… | โ€ข 4๊ฐœ ๋ฐฑ์—”๋“œ ์—ฐ์† ์œ„์ž„ ์ฃผ์ž… ์‹คํŒจ 0๊ฑด, ์ค‘๋ณต ์ œ์ถœ 0๊ฑด
โ€ข `test_version_consistency.py` ํ†ต๊ณผ
โ€ข ๋ฆฌ๋ทฐ์–ด ๊ต์ฐจ ๊ฒ€ํ†  `[VERDICT: PASS]` | ๋ฆด๋ฆฌ์Šค ํƒœ๊ทธ ๋ฏธ๋ถ€์—ฌ ์ƒํƒœ๋กœ `main` revert | + +--- + +## 3. ์ƒ์„ธ ๊ตฌํ˜„ ์‚ฌ์–‘ + +### 3.1 Phase 1 โ€” Fail-Closed + +**3.1.1 `lib_py/delegation/registry.py` โ€” `close` ์„œ๋ธŒ์ปค๋งจ๋“œ (์‹ ๊ทœ, Rev.2: `timeout`์„ ์ผ๋ฐ˜ํ™”)** +```text +registry.py [--registry-dir D] close --job --request {deadline_timeout,cancelled} [--detail ] [--expect-dispatch-token ] +``` +- ๊ตฌํ˜„: `engine.transition(job_id, request, registry_dir=rd, source="delegate_wrapper", fields={"detail": detail}, expected_dispatch_token=tok_or_None)`. ๋‘ ์š”์ฒญ ๋ชจ๋‘ ๋น„-operator์ด๋ฉฐ terminal ์ดํ›„ ๊ฑฐ๋ถ€๋œ๋‹ค(์‹ค์ธก). +- ์šฉ๋„ ๊ตฌ๋ถ„: `deadline_timeout` = ํ”„๋กฌํ”„ํŠธ๊ฐ€ **์ „๋‹ฌ๋œ ๋’ค** ์ข…๋ฃŒ ์ด๋ฒคํŠธ ์—†์ด ๋ฐ๋“œ๋ผ์ธ ์†Œ์ง„(์‹คํ–‰ ์‹คํŒจ). `cancelled` + detail `session_busy` = ํ”„๋กฌํ”„ํŠธ๊ฐ€ **์ „๋‹ฌ๋˜๊ธฐ ์ „** ์‹คํŒจ(ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธยท์œ ํœด ๋Œ€๊ธฐ ํƒ€์ž„์•„์›ƒ; Rev.2 Secondary). ํ›„์ž๋Š” `redispatch`๋กœ ์žฌ๊ฐœ ๊ฐ€๋Šฅํ•˜๋‹ค. +- ์ข…๋ฃŒ ์ฝ”๋“œ: ์ปค๋ฐ‹ โ†’ 0; ๊ฑฐ๋ถ€ ์‚ฌ์œ ๊ฐ€ `conflicting โ€ฆ after terminal` ๋˜๋Š” ๋ฉฑ๋“ฑ โ†’ **0**(stderr์— ์‚ฌ์œ  1์ค„; ์ด๋ฏธ ๋‹ซํžŒ ์žก์€ ์ •์ƒ); ๋ ˆ์ฝ”๋“œ ์—†์Œ โ†’ 1; ํ† ํฐ ๋ถˆ์ผ์น˜ โ†’ 4. +- `status --set`๋Š” ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค(`_operator_write` ๋ฐ F-C ๋ฉฑ๋“ฑ ๊ณ„์•ฝ ๋ณด์กด). + +**3.1.2 ๋ž˜ํผ `multi-agent-mux-delegate-job` โ€” ๊ณตํ†ต ํ—ฌํผ (์‹ ๊ทœ, `run_agent` ์œ„์— ์ •์˜)** +```bash +# reap_subscriber โ€” ์ž๊ธฐ ๊ตฌ๋…์ž๋งŒ ํšŒ์ˆ˜. ์›๋ž˜ rc๋Š” ํ˜ธ์ถœ์ž๊ฐ€ ๋ณด์กด. +reap_subscriber() { kill "$1" 2>/dev/null || true; wait "$1" 2>/dev/null || true; } + +# finalize_unfinished [] +# completed -> no-op (rc 0) +# broker_unavailable -> ๋‚จ์€ ๋ฐ๋“œ๋ผ์ธ(TIMEOUT - ๊ฒฝ๊ณผ) ๋™์•ˆ 3s ๊ฐ„๊ฒฉ ๋””์Šคํฌ ์žฌ๊ด€์ธก; +# completed/error ๋ฐœ๊ฒฌ ์‹œ ๊ทธ ์ƒํƒœ ์ฑ„ํƒ, ์•„๋‹ˆ๋ฉด deadline_timeout +# ๊ทธ ์™ธ(timeout/error/running) -> ์ฆ‰์‹œ registry.py close --request deadline_timeout +# phase == preflight|idle_wait -> registry.py close --request cancelled --detail session_busy (rc 5) +# (ํ”„๋กฌํ”„ํŠธ ๋ฏธ์ „๋‹ฌ: deadline_timeout ๊ธˆ์ง€ โ€” Rev.2 Secondary) +# direct ๊ฒฝ๋กœ์—์„œ๋งŒ publish_event --event error|cancelled ์ถ”๊ฐ€ ๋ฐœํ–‰ (loop/discuss ๊ธˆ์ง€: O-1) +# MAM_DELEGATE_FAIL_CLOSED=0 ์ด๋ฉด ์ „๋ถ€ no-op (๋กค๋ฐฑ) +``` +- `disk_st` ์žฌ๊ด€์ธก์€ ๊ธฐ์กด `:342-347`์˜ ์ธ๋ผ์ธ ํŒŒ์ด์ฌ์„ ํ•จ์ˆ˜ `_disk_status`๋กœ ์ถ”์ถœํ•ด ์žฌ์‚ฌ์šฉ. +- `expected_dispatch_token`์€ **๊ตฌ๋…์ž ๊ธฐ๋™ ์ง์ „** ๋ ˆ์ฝ”๋“œ์—์„œ ์ฝ์–ด ์ง€์—ญ๋ณ€์ˆ˜์— ๋ณด๊ด€(`_disp_tok`), ์กด์žฌํ•  ๋•Œ๋งŒ ์ „๋‹ฌ(direct ์žก์€ ์—†์Œ). + +**3.1.3 ์ข…๋ฃŒ ๊ฒฝ๋กœ 5๊ฐœ ์ผ์›ํ™”** +| # | ์œ„์น˜ (f98b958) | ๋ณ€๊ฒฝ | +|---|---|---| +| E1 | direct `:227` `wait "$sub_pid" \|\| true` | `wait "$sub_pid" \|\| sub_rc=$?` โ†’ ๊ธฐ์กด `:340-354` ๋งคํ•‘ ๋ธ”๋ก์„ ํ•จ์ˆ˜ `_map_sub_rc`๋กœ ์ถ”์ถœ(๋ฆฌํ„ฐ๋Ÿด ๋ฌธ์ž์—ด ๋ณด์กด) โ†’ `finalize_unfinished` โ†’ `exit_code=$(_rc_for "$job_status")` | +| E2 | loop ์›Œ์ปค `:359-362` | `break` ์ง์ „ `finalize_unfinished "$JOB_ID" "$job_status" "$sub_rc" worker` | +| E3 | loop ๋ฆฌ๋ทฐ์–ด `:375-378` | ๋™์ผ (`reviewer`) | +| E4 | `run_agent` ์‹คํŒจ (`:327` ๋ฐ direct `:204`) | `if ! run_agent โ€ฆ; then reap_subscriber "$sub_pid"; finalize_unfinished "$JOB_ID" error 1 inject; exit 1; fi`. trap(`:523`)์€ `--registry-dir "$REGISTRY_DIR"` ์ถ”๊ฐ€ ํ›„ ์ตœํ›„ ๋ณด๋ฃจ๋กœ๋งŒ ์œ ์ง€ | +| E5 | ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์‹คํŒจ(`:456-473` herdr/์„ธ์…˜ ๋ถ€์žฌ, `:511-513` strict-role) ๋ฐ ์œ ํœด ๋Œ€๊ธฐ ์ตœ์ข… ์‹คํŒจ | Phase 2์—์„œ `preflight_agent`๋กœ ์ด๋™. ์‹คํŒจ ์‹œ ์žก ๋“ฑ๋ก ํ›„๋ผ๋ฉด `finalize_unfinished โ€ฆ preflight\|idle_wait` โ†’ `cancelled`(session_busy) ํ›„ **`exit 5`** | + +**3.1.4 ์ข…๋ฃŒ ์ฝ”๋“œ ๊ณ„์•ฝ (SKILL.md์— ๋ฌธ์„œํ™”)** +| rc | ์˜๋ฏธ | +|---|---| +| 0 | `completed` | +| 1 | `error` (์—์ด์ „ํŠธ ๋ฐœํ–‰ / ์ฃผ์ž…ยทํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์‹คํŒจ) | +| 2 | ๊ตฌ๋…์ž ํƒ€์ž„์•„์›ƒ โ†’ `deadline_timeout` ์ ์šฉ๋จ | +| 3 | ๋ธŒ๋กœ์ปค ์žฅ์•  + ๋ฐ๋“œ๋ผ์ธ ๋‚ด ๋””์Šคํฌ ๋ฏธํ™•์ธ โ†’ `deadline_timeout` ์ ์šฉ๋จ | +| 5 | `session_busy`: ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ/์œ ํœด ๋Œ€๊ธฐ ์‹คํŒจ๋กœ ํ”„๋กฌํ”„ํŠธ ๋ฏธ์ „๋‹ฌ โ†’ `cancelled` ์ ์šฉ๋จ (์žฌ์Šค์ผ€์ค„ ๊ฐ€๋Šฅ) | +๋งˆ์ง€๋ง‰ stdout ์ค„(๊ฐ์‚ฌ ๋กœ๊ทธ ๊ฒฝ๋กœ)์€ ๋ชจ๋“  ๊ฒฝ๋กœ์—์„œ ์œ ์ง€ํ•œ๋‹ค(`runner.delegate_job`/`run_loop.sh:373` ํŒŒ์‹ฑ ๊ณ„์•ฝ). + +**3.1.5 dry-run ์ˆœ์„œ (๊ตฌ๋…์ž ๊ธฐ๋™ `:166` โ†’ dry-run `return 0` `:224`)**: `DRY_RUN=1`์ด๋ฉด ๊ตฌ๋…์ž๋ฅผ ๊ธฐ๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค(ํ˜„์žฌ๋Š” ๊ธฐ๋™ ํ›„ `return 0`์œผ๋กœ ๊ณ ์•„ ๋ฐœ์ƒ). + +**3.1.6 loop/discuss ์—ญํ•  ์ „ํ™˜์˜ `attempt_id` ๊ทœ์•ฝ (Rev.2 Tertiary โ€” ๊ธฐ์กด ๊ฒฐํ•จ ์ˆ˜์ •)** +- ๋ฌธ์ œ: `:276-283`์€ ์›Œ์ปคยท๋ฆฌ๋ทฐ์–ด phase ๋ชจ๋‘ ๊ฐ™์€ `$iteration`์œผ๋กœ `update --status pending`(=`redispatch`)์„ ์š”์ฒญํ•œ๋‹ค. `engine.py:227-240`์€ terminal์—์„œ์˜ `redispatch`์— `iteration` ์ „์ง„์„ ์š”๊ตฌํ•˜๋ฏ€๋กœ ์›Œ์ปค `completed` ์งํ›„ ๋ฆฌ๋ทฐ์–ด ์ „ํ™˜์ด rc=4๋กœ ๊ฑฐ๋ถ€๋˜๊ณ  `set -e`๋กœ ๋ž˜ํผ๊ฐ€ ์ฃฝ๋Š”๋‹ค(์‹ค์ธก). F-A ํ…Œ์ŠคํŠธ(`test_issue5_phase2a_loop.py:727`)๋Š” iteration 2๋ฅผ ๋„˜๊ฒจ ์ด ๊ฒฝ๋กœ๋ฅผ ๋น„์ผœ๊ฐ„๋‹ค. +- ์„ค๊ณ„: `iteration`์€ ๋ฃจํ”„ ํšŒ์ฐจ ๋ผ๋ฒจ๋กœ ์œ ์ง€ํ•˜๊ณ , **๋””์ŠคํŒจ์น˜ ๋‹จ์กฐ ์นด์šดํ„ฐ `attempt_id`**๋ฅผ ๋„์ž…ํ•œ๋‹ค(์—”์ง„ `:171`์ด ์ด๋ฏธ `record.get("attempt_id", record.get("iteration"))`์œผ๋กœ ์ฝ๋Š” ํ•„๋“œ). + 1. ๋ž˜ํผ: ์žก๋ณ„ `attempt=$((attempt+1))`์„ ๊ฐ phase ๋””์ŠคํŒจ์น˜ ์ง์ „์— ์ฆ๊ฐ€์‹œํ‚ค๊ณ  `update โ€ฆ --attempt-id "$attempt"`๋ฅผ ํ•จ๊ป˜ ๋„˜๊ธด๋‹ค(์›Œ์ปค r1=1, ๋ฆฌ๋ทฐ์–ด r1=2, ์›Œ์ปค r2=3 โ€ฆ). + 2. `registry.py update`์— `--attempt-id` ์ถ”๊ฐ€(`:296-302`). + 3. `engine._decide` redispatch ๊ทœ์น™: `fields`์— `attempt_id`๊ฐ€ ์žˆ์œผ๋ฉด **`attempt_id`๊ฐ€ `record.attempt_id`(์—†์œผ๋ฉด `iteration`)๋ณด๋‹ค ์ปค์•ผ** ์ปค๋ฐ‹; ์—†์œผ๋ฉด ๊ธฐ์กด `iteration` ๊ทœ์น™ ์œ ์ง€(F-A 3์ข… ๋ฌด๋ณ€๊ฒฝ). `attempt_id`๋Š” `redispatch`/`claim` ์ปค๋ฐ‹ ์‹œ ๋ ˆ์ฝ”๋“œ์— ๊ธฐ๋ก. + 4. `publish_event.py:250-255`์˜ `--finalize-report` ๊ธฐ๋ณธ `attempt_id`๋Š” `record.attempt_id`๋ฅผ ์šฐ์„ ํ•˜๊ณ  ์—†์œผ๋ฉด `iteration`. +- ๋ฒ”์œ„ ์ฃผ์˜: ์ด ์ˆ˜์ •์œผ๋กœ ๋ฆฌ๋ทฐ์–ด ์ „ํ™˜์˜ CAS ๊ฑฐ๋ถ€๋Š” ์‚ฌ๋ผ์ง€์ง€๋งŒ, **loop ๋ชจ๋“œ์˜ ๋ฆฌ๋ทฐ์–ด phase๋Š” O-1(๋ฆฌํ…Œ์ธ ์žฌ์ˆ˜๋ฝ, ยง4)์— ์˜ํ•ด ์—ฌ์ „ํžˆ ์ฆ‰์‹œ ์ข…๋ฃŒ๋œ๋‹ค.** ๋”ฐ๋ผ์„œ Phase 5 E2E๋Š” direct ๋ชจ๋“œ๋งŒ ์ˆ˜ํ–‰ํ•˜๊ณ , loop ๋ชจ๋“œ ์ข…๋‹จ ๊ฒ€์ฆ์€ O-1 ์ด์Šˆ ์™„๋ฃŒ ํ›„๋กœ ๋ช…์‹œํ•œ๋‹ค. + +### 3.2 Phase 2 โ€” ์‚ฌ์ „ TUI ํ•ธ๋“œ์…ฐ์ดํฌ + +**3.2.1 `lib_py/loop/watcher.py` CLI ์ง„์ž…์  (์‹ ๊ทœ `main`)** +```text +"$(_delegate_py_bin)" -m lib_py.loop.watcher wait --session [--until S]... [--timeout-ms N] + stdout: {"kind": "matched|timeout|agent_not_found|transport_error", "agent_status": "...", "error_code": "..."} + rc: matched 0 / timeout 2 / agent_not_found 3 / transport_error 4 +``` +- `wait_once`๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ(์…ธ ์ค‘๋ณต ๊ตฌํ˜„ ๊ธˆ์ง€). `lib_py.delegation`์€ `lib_py.loop`๋ฅผ ์ž„ํฌํŠธํ•˜์ง€ ์•Š๋Š”๋‹ค(ํŒจํ‚ค์ง€ ๊ทœ์น™ ์œ ์ง€) โ€” ํ˜ธ์ถœ์ž๋Š” ์…ธ(`lib.sh`)์ด๋‹ค. +- **๊ฒฉ๋ฆฌ ์ „๋‹ฌ (Rev.2 Primary-A)**: `build_argv(target, until, timeout_ms, session=None)`์— `session` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ๊ฐ’์ด ์žˆ๊ณ  `default`๊ฐ€ ์•„๋‹ˆ๋ฉด argv๋ฅผ `["herdr", "--session", session, "agent", "wait", โ€ฆ]`๋กœ ๊ตฌ์„ฑํ•œ๋‹ค. CLI `--session`์€ ๋ฏธ์ง€์ • ์‹œ `HERDR_SESSION_NAME` ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ฝ๋Š”๋‹ค. ์‹ค์ œ `herdr`๋Š” `HERDR_SESSION_NAME`์„ ์ธ์‹ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ(์‹ค์ธก) ์ด ๋ช…์‹œ ์ „๋‹ฌ์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ ์†Œ์ผ“(`~/.config/herdr/herdr.sock`)์„ ์กฐํšŒํ•ด `server_not_running`/`agent_not_found`๊ฐ€ ๋œ๋‹ค. ๊ธฐ์กด `wait_once` ํ˜ธ์ถœ์ž(`shadow.py` ๋“ฑ)๋Š” `session=None`์œผ๋กœ ๋™์ž‘ ๋ถˆ๋ณ€. +- **์ธํ„ฐํ”„๋ฆฌํ„ฐ (Rev.2 Primary-B)**: bare `python`/`python3` ๊ธˆ์ง€. `lib.sh`๋Š” `"$(_delegate_py_bin)"`(`lib.sh:1841`)๋งŒ ์‚ฌ์šฉํ•˜๋ฉฐ `PYTHONPATH`๋Š” `lib.sh:25,164`๊ฐ€ ์ด๋ฏธ ๋ณด์žฅํ•œ๋‹ค. T2-11 ์ •์  ๋ฆฐํŠธ๋กœ ๊ณ ์ •. + +**3.2.2 `lib.sh: wait_for_agent_idle [timeout_ms]` (์‹ ๊ทœ, `send_keys_safe` ์œ„)** +1. `SKS_IDLE_HANDSHAKE=0` โ†’ return 0 (๋กค๋ฐฑ). +2. `_sks_herdr agent get `(=`mam_herdr`, ๊ฒฉ๋ฆฌ ์ธ์ง€ ์…ธ ํ•จ์ˆ˜)๋กœ `agent_status` ์กฐํšŒ: `unknown`/์กฐํšŒ ์‹คํŒจ โ†’ return 3(ํœด๋ฆฌ์Šคํ‹ฑ ํด๋ฐฑ ์‹ ํ˜ธ, ํ˜ธ์ถœ์ž๋Š” ๊ธฐ์กด `_pane_quiescent` ๊ฒฝ๋กœ ์ง„ํ–‰); `blocked` โ†’ return 2(์ฃผ์ž… ๊ธˆ์ง€; `_pane_dialog_open` ์ฒ˜๋ฆฌ๋กœ ์œ„์ž„). +3. `idle`/`done` โ†’ return 0. `working` โ†’ `"$(_delegate_py_bin)" -m lib_py.loop.watcher wait --session "$HERDR_SESSION_NAME" --until idle --until done --until blocked --timeout-ms ${SKS_IDLE_TIMEOUT_MS:-120000}`: matched(idle/done) โ†’ 0, matched(blocked) โ†’ 2, timeout โ†’ ์žฌ์‹œ๋„, ๊ทธ ์™ธ โ†’ 3. +4. **์žฌ์‹œ๋„ (Rev.2 Secondary)**: timeout์€ `SKS_IDLE_RETRIES`(๊ธฐ๋ณธ 2)ํšŒ๊นŒ์ง€ `SKS_IDLE_RETRY_BACKOFF_S`(๊ธฐ๋ณธ 10)์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ๋ฐ˜๋ณตํ•œ๋‹ค(๊ธฐ๋ณธ ์ด ์˜ˆ์‚ฐ โ‰ˆ 3ร—120s + 20s). ์ตœ์ข… ์‹คํŒจ โ†’ return 1("agent still working"). ํ˜ธ์ถœ์ž(๋ž˜ํผ)๋Š” rc 1์„ `finalize_unfinished โ€ฆ idle_wait`(โ†’ `cancelled`/rc 5)๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉฐ **`deadline_timeout`์„ ์ ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค**. +- `interactive_ready`๋Š” ์–ด๋–ค ํŒ์ •์—๋„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค. + +**3.2.3 ๋ž˜ํผ ์‹คํ–‰ ์ˆœ์„œ ์žฌ๋ฐฐ์น˜ (`cmd_submit`)** +```text +register โ†’ brief ์ž‘์„ฑ โ†’ preflight_agent(herdr ์กด์žฌยท์„ธ์…˜ ์กด์žฌยท์—ญํ•  ๊ฒ€์‚ฌ) + โ†’ wait_for_agent_idle "$sess" (rc 1/2 โ†’ finalize_unfinished โ€ฆ idle_wait โ†’ cancelled/session_busy; exit 5) + โ†’ _disp_tok ์ฝ๊ธฐ โ†’ ๊ตฌ๋…์ž ๊ธฐ๋™ + SUBSCRIBED ํ•ธ๋“œ์…ฐ์ดํฌ + โ†’ inject_agent(send_keys_safe) โ†’ wait sub โ†’ finalize +``` +- loop/discuss ๋ฐ˜๋ณต๋งˆ๋‹ค ๋™์ผ ์ˆœ์„œ(`:286` ๊ตฌ๋…์ž ๊ธฐ๋™ ์•ž์— idle wait). + +**3.2.4 `send_keys_safe` ๋ณด๊ฐ• (`lib.sh:2231-2345`)** +- ์ œ์ถœ ๊ฒ€์ฆ(`:2330-2332`): `pre_submit`์— `esc to interrupt`/`โ— |โœฝ |[A-Za-z]+ing`๊ฐ€ **์ด๋ฏธ ์žˆ์—ˆ์œผ๋ฉด** ํ•ด๋‹น ํ† ํฐ์„ ์„ฑ๊ณต ๊ทผ๊ฑฐ์—์„œ ์ œ์™ธํ•˜๊ณ  marker ์†Œ๋ฉธ + ํ™”๋ฉด ๋ณ€ํ™”๋งŒ ์ธ์ •. +- ๋„ค์ดํ‹ฐ๋ธŒ ๊ฒฝ๋กœ(`:2279`): `SKS_PROMPT_WAIT=1`(๊ธฐ๋ณธ)์ด๋ฉด `agent prompt --wait --until working --timeout 5000`. ์„ฑ๊ณต โ†’ 0. `agent_prompt_stalled`/timeout โ†’ **์žฌ์ œ์ถœํ•˜์ง€ ์•Š๊ณ ** 2s ํ›„ `agent get` 1ํšŒ: `working` โ†’ 0, ๊ทธ ์™ธ โ†’ rc 4("delivery unknown"). `agent_blocked` โ†’ rc 2. +- ํด๋ฐฑ ๊ฒฝ๋กœ ์ง„์ž… ์ „ busy ํ† ํฐ ๊ฒŒ์ดํŠธ: `wait_for_agent_idle`์ด 3(unknown)์„ ๋ฐ˜ํ™˜ํ•œ ๊ฒฝ์šฐ์—๋งŒ `_pane_tail 8`์˜ busy ํ† ํฐ์„ `SKS_BUSY_TIMEOUT`(๊ธฐ๋ณธ 120s)๊นŒ์ง€ ๋Œ€๊ธฐ. + +### 3.3 Phase 3 โ€” ๋Ÿฌ๋„ˆ Fail-Fast +- `runner.py:279-289`: `res.returncode != 0`์ด์–ด๋„ stdout์—์„œ `registered job:` ๋งค์น˜ ์‹œ `created_jobs.append` + `_record_checkpoint()` ํ›„ **id ๋ฐ˜ํ™˜**(๋กœ๊ทธ๋Š” error ๋ ˆ๋ฒจ ์œ ์ง€). ๋งค์น˜ ์‹คํŒจ ์‹œ์—๋งŒ `None`. +- **rc=5 (`session_busy`, Rev.2 Secondary)**: `delegate_job`์ด `MAM_SESSION_BUSY_RETRY_S`(๊ธฐ๋ณธ 30)์ดˆ ๋Œ€๊ธฐ ํ›„ **1ํšŒ** ์žฌ์œ„์ž„(์ƒˆ job id ๋“ฑ๋ก)ํ•œ๋‹ค. ์žฌ์‹œ๋„๋„ rc=5๋ฉด ๋งˆ์ง€๋ง‰ id๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ํ˜ธ์ถœ๋ถ€๋Š” `wait_for_job`์ด `cancelled`๋กœ ์ฆ‰์‹œ False โ†’ ๊ธฐ์กด abort ๊ฒฝ๋กœ. ๋ฌดํ•œ ์žฌ์‹œ๋„๋Š” ๋‘์ง€ ์•Š๋Š”๋‹ค(๊ต์ฐฉ์˜ ์ƒˆ ํ˜•ํƒœ). +- `wait_for_job`์€ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š”๋‹ค(3900s๋Š” ์ตœํ›„ ์•ˆ์ „๋ง; `cancelled`๋Š” ์ด๋ฏธ False ์กฐ๊ฑด). +- `run_loop.sh:159-168, 373`: `delegate_job_safe` rcโ‰ 0์—์„œ๋„ ์ถœ๋ ฅ์—์„œ job id ์ถ”์ถœ(๋ ˆ๊ฑฐ์‹œ ๊ฒฝ๋กœ ๋™๋“ฑ์„ฑ). + +### 3.4 Phase 4 โ€” ํ…Œ์ŠคํŠธ ๋ฐ ๋ฌธ์„œ (ยง5 ์ฐธ์กฐ) +- `tests/conftest.py` mock_herdr: `agent get`/`list`์— `agent_status`(๊ธฐ๋ณธ `idle`) ์ถ”๊ฐ€; `agent wait`๋Š” ์ƒํƒœ ํŒŒ์ผ์˜ `status_sequence`(์˜ˆ: `["working","working","done"]`)๋ฅผ ํ˜ธ์ถœ๋งˆ๋‹ค ์†Œ๋น„, ์†Œ์ง„ ์‹œ ๋งˆ์ง€๋ง‰ ๊ฐ’ ์œ ์ง€, `--timeout`์€ ์‹œํ€€์Šค ์†Œ์ง„ ์ „ ์ข…๋ฃŒ ์‹œ `{"error":{"code":"timeout"}}` rc 1; `agent prompt --wait`๋Š” `prompt_wait_result` ํ•„๋“œ(`ok|agent_prompt_stalled|agent_blocked`)๋กœ ์ œ์–ด. +- ๋ฌธ์„œ: `SKILL.md` "Core Commands"์— ์ข…๋ฃŒ ์ฝ”๋“œ ํ‘œ(ยง3.1.4), `MESSAGING.md` ยง5.1 ๊ตฌ๋…์ž ํƒ€์ž„์•„์›ƒ์˜ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋ฐ˜์˜ ์„œ์ˆ , `BOOTSTRAP*.md` ์—์ด์ „ํŠธ ํ”„๋กœํ† ์ฝœ์— "completed ๋ฐœํ–‰ ํ›„ ๋„๊ตฌ ํ˜ธ์ถœ ์—†์ด ํ„ด ์ข…๋ฃŒ" 1๋ฌธ์žฅ. + +### 3.5 Phase 5 โ€” E2Eยท์ •๋ฆฌยท๋ฆด๋ฆฌ์Šค +- E2E ์ ˆ์ฐจ(**direct ๋ชจ๋“œ๋งŒ**; loop ๋ชจ๋“œ ์ข…๋‹จ์€ O-1 ์™„๋ฃŒ ํ›„): ๊ฐ ๋ฐฑ์—”๋“œ์— `--agent human`์ด ์•„๋‹Œ ์‹ค์„ธ์…˜์œผ๋กœ ์žก A ์œ„์ž„ โ†’ `completed` ์ˆ˜์‹  ์ฆ‰์‹œ ์žก B ์œ„์ž„ โ†’ ๋ž˜ํผ ๋กœ๊ทธ์˜ idle ๋Œ€๊ธฐ ์‹œ๊ฐ„, `herdr agent get` ํƒ€์ž„๋ผ์ธ, B์˜ `started` ์ง€์—ฐ์„ ๊ธฐ๋ก(`docs/contracts/issue-6/phase5-e2e.md`). +- ํƒ€์ž„์•„์›ƒ ์ฃผ์ž…: ์„ธ์…˜์— `/exit` ์—†์ด ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฌด์‹œํ•˜๋„๋ก `--idle-timeout 30`์œผ๋กœ ์œ„์ž„ โ†’ ๋ž˜ํผ rc=2, ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `error`, `wait_for_job` โ‰ค 2s. +- ์ž”์กด ๋ ˆ์ฝ”๋“œ `93a44347`, `2961b822`, `ee242671`: `registry.py close --job --request deadline_timeout --detail "Issue #6 backfill"`. +- ๋ฆด๋ฆฌ์Šค: `v4.4.1` (PATCH โ€” ๊ณต๊ฐœ ๊ณ„์•ฝ ์‚ญ์ œ ์—†์Œ, ์ข…๋ฃŒ ์ฝ”๋“œ๋Š” ์˜ค๋™์ž‘ ์ •์ •, `timeout` ์„œ๋ธŒ์ปค๋งจ๋“œ๋Š” ๋ž˜ํผ ๋‚ด๋ถ€ ๋ฐฐ๊ด€). 4-way ๋ฝ์Šคํ…: `lib.sh:32`, `eager.py:16`, `VERSIONS.md`(๊ฐœ์š”+๋งคํŠธ๋ฆญ์Šค+Changelog), 8๊ฐœ `SKILL.md` frontmatter. `IMPROVEMENTS.md` ยง5์— `Issue-6-FailClosed-TurnHandshake` ๋ธ”๋ก ์ถ”๊ฐ€(ยง6 ํ‘œยทํ—ค๋” ํ‚ค ๋ชฉ๋ก ๋™๊ธฐํ™”). + +--- + +## 4. ๋ช…์‹œ์  ๋น„๋ฒ”์œ„ (Out of Scope) โ€” ๋ณ„๋„ ์ด์Šˆ +1. **O-1 ๋ฆฌํ…Œ์ธ ํ„ฐ๋ฏธ๋„ ์žฌ์ˆ˜๋ฝ**: ์žฌ๋””์ŠคํŒจ์น˜ ํ›„ 2ํšŒ์ฐจ ๊ตฌ๋…์ž๊ฐ€ 1ํšŒ์ฐจ retained `completed`๋ฅผ ์ฆ‰์‹œ ์ˆ˜๋ฝ(์‹ค์ธก). loop ๋ชจ๋“œ ๊ธฐ์กด ๊ฒฐํ•จ. ๋ณธ ์ด์Šˆ์—์„œ๋Š” "loop ๊ฒฝ๋กœ retained ๋ฐœํ–‰ ๊ธˆ์ง€"๋กœ ํšŒํ”ผ๋งŒ ํ•œ๋‹ค. **Rev.2 ์ฃผ์˜: ยง3.1.6์˜ attempt_id ์ˆ˜์ • ํ›„์—๋„ loop ๋ชจ๋“œ ๋ฆฌ๋ทฐ์–ด phase๋Š” O-1 ๋•Œ๋ฌธ์— ์ข…๋‹จ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋ณ„๋„ ์ด์Šˆ๋ฅผ Issue #6 ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ์— "loop/discuss ๋ชจ๋“œ ์‚ฌ์šฉ ์ „ ํ•„์ˆ˜"๋กœ ๋ช…์‹œํ•œ๋‹ค.** +2. **๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์˜ค์—ผ**: `.mam/jobs`์— pytest ์„ธ์…˜๋ช… ๋ ˆ์ฝ”๋“œ 467๊ฑด ๋ˆ„์ถœ ์›์ธ. +3. **์„ธ์…˜๋ณ„ ์ „๋‹ฌ ์ง๋ ฌํ™” ๋ฝ**, **direct ์žก `dispatch_token` ๋ฐœ๊ธ‰**: ํ˜„ ๊ตฌ์กฐ(๋™๊ธฐ ์œ„์ž„ + loop lock)์—์„œ ๋ถˆํ•„์š”. +4. **`wait_for_job` 3900s ๋‹จ์ถ•**: ์ฆ์ƒ ์™„ํ™”์ด๋ฏ€๋กœ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š”๋‹ค. + +--- + +## 5. ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ํ–‰๋ ฌ (Phase 4 ์‚ฐ์ถœ๋ฌผ) + +### 5.1 `tests/test_issue6_fail_closed.py` (๋ธŒ๋กœ์ปค ๋ฌด๊ด€, `--agent human` + ์งง์€ ํƒ€์ž„์•„์›ƒ) +| ID | ์‹œ๋‚˜๋ฆฌ์˜ค | ๊ธฐ๋Œ€ | +|---|---|---| +| T1-1 | direct, ์ด๋ฒคํŠธ ์—†์Œ (`--timeout 6 --idle-timeout 2`) | rc=2, ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `error`, events.log์— wrapper `error` 1๊ฑด, ๊ตฌ๋…์ž pid ์†Œ๋ฉธ | +| T1-2 | loop, ์›Œ์ปค phase ์ด๋ฒคํŠธ ์—†์Œ | rc=2, `error`, **retained ๋ฐœํ–‰ ์—†์Œ** | +| T1-3 | ๊ตฌ๋…์ž ํƒ€์ž„์•„์›ƒ ์งํ›„ `publish_event completed` ๊ฒฝ์Ÿ | ์ตœ์ข… `completed` ์œ ์ง€, `timeout` ์„œ๋ธŒ์ปค๋งจ๋“œ rc 0(๊ฑฐ๋ถ€ ๋กœ๊ทธ) | +| T1-4 | `MQTT_HOST` ๋ถˆ๊ฐ€ + ๋””์Šคํฌ ๋น„์ข…๋ฃŒ (`--timeout 8`) | rc=3, ๋ฐ๋“œ๋ผ์ธ๊นŒ์ง€ ์žฌ๊ด€์ธก ๋กœ๊ทธ, ์ตœ์ข… `error` | +| T1-5 | rc=3 + ์žฌ๊ด€์ธก ์ค‘ ๋””์Šคํฌ `completed` ์ถœํ˜„ | rc=0, `completed` | +| T1-6 | `close --request deadline_timeout`์„ `completed` ๋ ˆ์ฝ”๋“œ์— ์ ์šฉ | rc=0, ์ƒํƒœ ๋ถˆ๋ณ€ | +| T1-10 | loop: ์›Œ์ปค `completed` ํ›„ ๋ฆฌ๋ทฐ์–ด `update --status pending --iteration 1 --attempt-id 2` | rc=0, `pending`, `attempt_id=2` (์ˆ˜์ • ์ „ rc=4 โ†’ ์—ญ๊ฒ€์ฆ) | +| T1-11 | ๊ฐ™์€ `--attempt-id 2` ์žฌ์š”์ฒญ(๋ฆฌํ”Œ๋ ˆ์ด) | rc=4 ๊ฑฐ๋ถ€ | +| T1-12 | `--attempt-id` ์—†์ด iteration 2 (F-A ๊ฒฝ๋กœ) | rc=0 (๊ธฐ์กด ๊ทœ์น™ ๋ณด์กด), F-A 3์ข… ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ | +| T1-13 | `close --request cancelled --detail session_busy`: pendingโ†’cancelled rc=0, completedโ†’ rc=0ยท์ƒํƒœ ๋ถˆ๋ณ€, ์ดํ›„ `redispatch` iteration 2 โ†’ pending | ์ „์ด ๊ทœ์น™ ๊ณ ์ • | +| T1-7 | ์ปค์Šคํ…€ `--registry-dir` + `send_keys_safe` ์‹คํŒจ(BASH_ENV ์˜ค๋ฒ„๋ผ์ด๋“œ) | rc=1, ํ•ด๋‹น ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `error` | +| T1-8 | `MAM_DELEGATE_FAIL_CLOSED=0` | T1-1 ์กฐ๊ฑด์—์„œ rc=0, `pending` (๊ตฌ ๋™์ž‘ ์žฌํ˜„) | +| T1-9 | dry-run direct | ๊ตฌ๋…์ž ํ”„๋กœ์„ธ์Šค ๋ฏธ๊ธฐ๋™ | +| ๊ณ ์ • | `test_wrapper_exit_code_mapping_literals_are_frozen` | ํ†ต๊ณผ | + +### 5.2 `tests/test_issue6_turn_handshake.py` (mock_herdr ์ƒํƒœ ์‹œํ€€์Šค) +| ID | ์‹œ๋‚˜๋ฆฌ์˜ค | ๊ธฐ๋Œ€ | +|---|---|---| +| T2-1 | `status_sequence=["working","done"]` | `agent wait` ํ˜ธ์ถœ ํ›„ `agent prompt` ํ˜ธ์ถœ(์ˆœ์„œ ๋‹จ์ •), Enter/C-m 0ํšŒ(๋„ค์ดํ‹ฐ๋ธŒ) | +| T2-2 | `working` ๊ณ ์ •, `SKS_IDLE_TIMEOUT_MS=1000` | rc=1, `agent prompt` ๋ฏธํ˜ธ์ถœ, ๋ž˜ํผ rc=1ยท`error` | +| T2-3 | `unknown` | `agent wait` ๋ฏธํ˜ธ์ถœ, `_pane_quiescent` ํด๋ฐฑ ๊ฒฝ๋กœ ์ง„์ž… | +| T2-4 | `blocked` | rc=2, ์ฃผ์ž… ์—†์Œ | +| T2-5 | `done` ์ฆ‰์‹œ | ๋Œ€๊ธฐ 0ํšŒ, ์ฆ‰์‹œ ์ฃผ์ž… | +| T2-6 | ํด๋ฐฑ ๊ฒฝ๋กœ, pre_submit์— `esc to interrupt` ์กด์žฌ + ํŽ˜์ด์ŠคํŠธ ๋“œ๋กญ | rc=4 (๊ธฐ์กด ์ฝ”๋“œ์—์„œ๋Š” 0 โ†’ ์—ญ๊ฒ€์ฆ) | +| T2-7 | `prompt_wait_result=agent_prompt_stalled` + ์ดํ›„ `working` | rc=0, prompt ํ˜ธ์ถœ **1ํšŒ** | +| T2-8 | ๋ž˜ํผ ์ˆœ์„œ: idle ๋Œ€๊ธฐ ๋กœ๊ทธ๊ฐ€ `subscriber pid:` ๋กœ๊ทธ๋ณด๋‹ค ์•ž | ๋กœ๊ทธ ์ˆœ์„œ ๋‹จ์ • | +| T2-9 | `working` ๊ณ ์ • + `SKS_IDLE_RETRIES=1`, `SKS_IDLE_TIMEOUT_MS=500` | `agent wait` 2ํšŒ ํ˜ธ์ถœ, ๋ž˜ํผ rc=5, ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `cancelled`(detail `session_busy`), `deadline_timeout` ๋ฏธํ˜ธ์ถœ, ๊ตฌ๋…์ž ๋ฏธ๊ธฐ๋™ | +| T2-10 | `HERDR_SESSION_NAME=iso-x`์—์„œ `wait_for_agent_idle` | mock ํ˜ธ์ถœ ๊ธฐ๋ก์— `herdr --session iso-x agent wait โ€ฆ` (argv ์ˆœ์„œ ๋‹จ์ •); `build_argv(session="default")`๋Š” `--session` ์ƒ๋žต | +| T2-11 | ์ •์  ๋ฆฐํŠธ: `lib.sh`ยท๋ž˜ํผ์—์„œ `^\s*python3? -m lib_py\.loop` ๋˜๋Š” bare `python -m` ํŒจํ„ด 0๊ฑด, watcher ํ˜ธ์ถœ์€ `_delegate_py_bin` ๊ฒฝ์œ  | grep ๋‹จ์ • | +| ํ˜ธํ™˜ | `test_h16`, `test_b8`, `test_c1_*`, `test_herdr_shim_contract` | ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ | + +### 5.3 Phase 3 (`tests/test_issue6_fail_closed.py` ๋‚ด) +| ID | ์‹œ๋‚˜๋ฆฌ์˜ค | ๊ธฐ๋Œ€ | +|---|---|---| +| T3-1 | `LoopRunner.delegate_job` rcโ‰ 0 + `registered job:` ์ถœ๋ ฅ | id ๋ฐ˜ํ™˜, `wait_for_job` โ‰ค 2s False | +| T3-2 | ๋™์ผ | `.mam/loop-checkpoint.json`์— id ๊ธฐ๋ก | +| T3-3 | ๋ž˜ํผ stub์ด 1ํšŒ์ฐจ rc=5, 2ํšŒ์ฐจ rc=0 (`MAM_SESSION_BUSY_RETRY_S=0`) | ์žฌ์œ„์ž„ 1ํšŒ ํ›„ 2ํšŒ์ฐจ id ๋ฐ˜ํ™˜; ๋‘ ๋ฒˆ ๋‹ค rc=5๋ฉด ๋งˆ์ง€๋ง‰ id ๋ฐ˜ํ™˜ + `wait_for_job` False | + +--- + +## 6. ๊ณต์ˆ˜ ๋ฐ ์ˆœ์„œ +| ๋‹จ๊ณ„ | ๊ณต์ˆ˜ | ์„ ํ–‰ | +|---|---|---| +| Phase 1 | 1.25 MD (+0.25 attempt_id) | โ€” | +| Phase 2 | 1.25โ€“1.75 MD (+0.25 ์„ธ์…˜ ์ „๋‹ฌยท์žฌ์‹œ๋„) | Phase 1 (finalize ํ—ฌํผ ์žฌ์‚ฌ์šฉ) | +| Phase 3 | 0.25 MD | Phase 1 | +| Phase 4 | 0.75 MD (Phase 1~3์™€ ๋ณ‘ํ–‰ ์ž‘์„ฑ, ์—ญ๊ฒ€์ฆ ํฌํ•จ) | โ€” | +| Phase 5 | 0.5โ€“1.0 MD | ์ „๋ถ€ | +| **ํ•ฉ๊ณ„** | **4.0โ€“5.0 MD** (ํ†ตํ•ฉ ๋ณด๊ณ ์„œ Rev.2 ยง3 ์ƒํ•œ ๋Œ€๋น„ +0.5 MD; ์‚ฌ์œ  ยง7) | | + +--- + +## 7. Creator ์ด์˜์ œ๊ธฐ ํŒ์ • (Rev.1 โ†’ Rev.2) + +| ์ด์˜ | ํŒ์ • | ์‹ค์ธก ๊ทผ๊ฑฐ | Rev.2 ๋ฐ˜์˜ | +|---|---|---|---| +| **Primary-A** watcher.py๊ฐ€ `HERDR_SESSION_NAME` ๊ฒฉ๋ฆฌ๋ฅผ ์žƒ๋Š”๋‹ค | **์ˆ˜์šฉ** (์„ธ๋ถ€ ์ •์ •: ์‹ค์ œ herdr ํ”Œ๋ž˜๊ทธ๋Š” `-L`์ด ์•„๋‹ˆ๋ผ `--session`; `-L`์€ `.mam/shim` ๋ฒˆ์—ญ ์ „์šฉ. ๊ฒฉ๋ฆฌ ๋ณ€์ˆ˜๋Š” ์…ธ ํ•จ์ˆ˜ `mam_herdr`/`_real_herdr`๊ฐ€ ๋ฒˆ์—ญํ•˜๋ฉฐ Python ์„œ๋ธŒํ”„๋กœ์„ธ์Šค์—๋Š” ์ƒ์†๋˜์ง€ ์•Š์Œ) | `HERDR_SESSION_NAME`๋งŒ ์žˆ๋Š” ํ™˜๊ฒฝ์—์„œ bare `herdr` โ†’ `server_not_running`; `--session`/`HERDR_SESSION`/`HERDR_SOCKET_PATH` โ†’ ์ •์ƒ. `lib.sh` source ํ›„ Python `subprocess`๋„ ๋™์ผ ์‹คํŒจ | ยง1 ์›์น™ 8, ยง3.2.1 `--session` ์ „๋‹ฌ, ยง3.2.2 `_sks_herdr` ์‚ฌ์šฉ, T2-10 | +| **Primary-B** bare `python -m` ํ˜ธ์ถœ์€ `ModuleNotFoundError` | **์ˆ˜์šฉ** (Rev.1 ํ‘œ๊ธฐ ์˜ค๋ฅ˜) | `lib.sh:1841 _delegate_py_bin`, `PYTHONPATH` export `lib.sh:25,164` | ยง3.2.1/3.2.2 `"$(_delegate_py_bin)"`, T2-11 ์ •์  ๋ฆฐํŠธ | +| **Secondary** ์ฃผ์ž… ์ „ ์œ ํœด ๋Œ€๊ธฐ ์‹คํŒจ๋ฅผ `deadline_timeout`(terminal `error`)๋กœ ๋‹ซ์œผ๋ฉด ๋ฏธ์ฃผ์ž… ์žก์ด ์‹คํ–‰ ์‹คํŒจ๋กœ ๋‚™์ธ + ๋Ÿฌ๋„ˆ ์ฆ‰์‹œ abort | **์ˆ˜์šฉ** (๋Œ€์•ˆ ์ฑ„ํƒ: ๋ž˜ํผ ๋‚ด bounded ์žฌ์‹œ๋„ โ†’ `cancelled`/`session_busy` rc 5 โ†’ ๋Ÿฌ๋„ˆ 1ํšŒ ์žฌ์œ„์ž„). "์žฌ์‹œ๋„ ๊ธฐํšŒ ์˜๊ตฌ ๋ฐ•ํƒˆ" ๋ถ€๋ถ„์€ ๋ถ€๋ถ„ ์ •์ •: `cancelled`๋„ operator `redispatch`๋กœ ์žฌ๊ฐœ ๊ฐ€๋Šฅ(์‹ค์ธก) | `engine.transition`: pendingโ†’cancelled ์ปค๋ฐ‹, completedโ†’cancelled ๊ฑฐ๋ถ€, cancelledโ†’redispatch ์„ฑ๊ณต | ยง1 ์›์น™ 7, ยง3.1.1 `close`, ยง3.1.2/3.1.4 rc 5, ยง3.2.2 ์žฌ์‹œ๋„, ยง3.3 ๋Ÿฌ๋„ˆ ์žฌ์œ„์ž„, T1-13/T2-9/T3-3 | +| **Tertiary** ์›Œ์ปคโ†’๋ฆฌ๋ทฐ์–ด ์ „ํ™˜ ์‹œ ๊ฐ™์€ iteration์˜ `redispatch` ๊ฑฐ๋ถ€ | **์ˆ˜์šฉ** (์ธ์šฉ ์ฝ”๋“œ๋Š” ์‹ค์ œ `engine.py:227-240`๊ณผ ๋‹ค๋ฅด๋‚˜ ๊ฒฐ๋ก ์€ ์ •ํ™•). v4.4.0์—์„œ **์ด๋ฏธ ๊นจ์ ธ ์žˆ๋Š” ๊ธฐ์กด ๊ฒฐํ•จ**์ž„์„ ํ™•์ธ | `registry.py update --status pending --iteration 1`(์›Œ์ปค completed ํ›„) โ†’ rc=4 `does not advance past 1`; iteration 2 โ†’ rc=0 | ยง3.1.6 `attempt_id` ๊ทœ์•ฝ, T1-10~T1-12 | +| (๋ถ€์ˆ˜) Rev.2 ํ†ตํ•ฉ ๋ณด๊ณ ์„œ ยง3์˜ `lib.sh` ๊ฒฝ๋กœ | Rev.1๊ณผ ๋™์ผํ•˜๊ฒŒ ์‹ค์ œ ๊ฒฝ๋กœ `.agents/skills/lib.sh` ์‚ฌ์šฉ | โ€” | โ€” | + +๊ณต์ˆ˜ +0.5 MD์˜ ์‚ฌ์œ : `attempt_id` ๊ทœ์•ฝ(์—”์ง„ ๊ทœ์น™ยทCLIยทpublish_event ๊ธฐ๋ณธ๊ฐ’ยทํ…Œ์ŠคํŠธ 3์ข…)๊ณผ ์„ธ์…˜ ์ „๋‹ฌยท์žฌ์‹œ๋„ ๋กœ์งยท๋Ÿฌ๋„ˆ ์žฌ์œ„์ž„(ํ…Œ์ŠคํŠธ 3์ข…)์ด Rev.1์— ์—†๋˜ ์‹ ๊ทœ ์ž‘์—…์ด๋‹ค. diff --git a/docs/contracts/issue-6/task.md b/docs/contracts/issue-6/task.md new file mode 100644 index 0000000..670a5ed --- /dev/null +++ b/docs/contracts/issue-6/task.md @@ -0,0 +1,104 @@ +# ๐Ÿ“‘ Issue #6 Task Checklist & Verification Evidence + +- **Document Version**: Rev.2 (Planner `planner-reviewer-claude-01`, job `f4873d82` โ€” Creator ์ด์˜์ œ๊ธฐ 3๊ฑด ๋ฐ˜์˜) +- **Issue**: [Gitea Issue #6](https://git.godopu.com/tmpl/multi-agent-mux/issues/6) +- **Plan**: `docs/contracts/issue-6/implementation_plan.md` (Rev.2) +- **Baseline**: `f98b958` (v4.4.0), 750 passed +- **Status**: Phase 1 ๋Œ€๊ธฐ + +> ๊ฐ ํ•ญ๋ชฉ์€ ์™„๋ฃŒ ์‹œ `[x]`๋กœ ๋ฐ”๊พธ๊ณ  ๋ฐ”๋กœ ์•„๋ž˜ `*์ฆ๊ฑฐ*:` ์ค„์— ์ปค๋ฐ‹ ํ•ด์‹œยทํ…Œ์ŠคํŠธ IDยท์‹ค์ธก ์ˆ˜์น˜๋ฅผ ๊ธฐ๋กํ•œ๋‹ค(Issue #5 ๊ด€๋ก€). ์‹ ๊ทœ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ๋Š” **์ˆ˜์ • ์ „ ์ฝ”๋“œ์—์„œ ์‹คํŒจํ•จ์„ ์—ญ๊ฒ€์ฆ**ํ•œ ๋’ค ํ†ต๊ณผ ์ฆ๊ฑฐ๋ฅผ ๋‚จ๊ธด๋‹ค. + +--- + +## Phase 1: ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ CAS ํ‘œ์ค€ ์—ฐ๋™ ๋ฐ delegate-job Fail-Closed + +- [ ] **P1-01** `registry.py close --job --request {deadline_timeout,cancelled} [--detail] [--expect-dispatch-token]` ์„œ๋ธŒ์ปค๋งจ๋“œ ์ถ”๊ฐ€ (`engine.transition(..., request, source="delegate_wrapper")`). rc: ์ปค๋ฐ‹/๋ฉฑ๋“ฑ/terminal ๊ฑฐ๋ถ€ 0, ๋ ˆ์ฝ”๋“œ ์—†์Œ 1, ํ† ํฐ ๋ถˆ์ผ์น˜ 4. + - *์ฆ๊ฑฐ*: +- [ ] **P1-02** ๋ž˜ํผ์— `reap_subscriber`, `_disk_status`, `_map_sub_rc`(๊ธฐ์กด `:340-354` ๋ฆฌํ„ฐ๋Ÿด ๋ณด์กด), `_rc_for`, `finalize_unfinished` ํ—ฌํผ ์ถ”๊ฐ€. phase `preflight|idle_wait`๋Š” `cancelled`/`session_busy`(rc 5), ๊ทธ ์™ธ๋Š” `deadline_timeout`. `MAM_DELEGATE_FAIL_CLOSED=0` ๋กค๋ฐฑ ๋ถ„๊ธฐ ํฌํ•จ. + - *์ฆ๊ฑฐ*: +- [ ] **P1-03** direct ๊ฒฝ๋กœ `:227` `|| true` ์ œ๊ฑฐ โ†’ `sub_rc` ๋งคํ•‘ โ†’ `finalize_unfinished` โ†’ ์ข…๋ฃŒ ์ฝ”๋“œ 0/1/2/3/5. ๋งˆ์ง€๋ง‰ stdout ์ค„(๊ฐ์‚ฌ ๊ฒฝ๋กœ) ์œ ์ง€. + - *์ฆ๊ฑฐ*: +- [ ] **P1-04** loop/discuss ์›Œ์ปค(`:359`)ยท๋ฆฌ๋ทฐ์–ด(`:375`) `break` ์ง์ „ `finalize_unfinished` ํ˜ธ์ถœ. **loop ๊ฒฝ๋กœ์—์„œ `publish_event --event error` ๋ฐœํ–‰ ๊ธˆ์ง€**(O-1). + - *์ฆ๊ฑฐ*: +- [ ] **P1-05** `run_agent` ์‹คํŒจ ๋ถ„๊ธฐ ๋ช…์‹œํ™”(`if ! run_agent`): `reap_subscriber` + `finalize_unfinished โ€ฆ inject` + `exit 1`. trap(`:523`)์— `--registry-dir "$REGISTRY_DIR"` ์ถ”๊ฐ€. + - *์ฆ๊ฑฐ*: +- [ ] **P1-06** rc=3 ์ฒ˜๋ฆฌ: ๋‚จ์€ ๋ฐ๋“œ๋ผ์ธ ๋™์•ˆ 3s ๊ฐ„๊ฒฉ ๋””์Šคํฌ ์žฌ๊ด€์ธก, terminal ๋ฐœ๊ฒฌ ์‹œ ์ฑ„ํƒ, ์•„๋‹ˆ๋ฉด `deadline_timeout`(rc=3). + - *์ฆ๊ฑฐ*: +- [ ] **P1-07** dry-run direct์—์„œ ๊ตฌ๋…์ž ๋ฏธ๊ธฐ๋™ (ํ˜„์žฌ `:166` ๊ธฐ๋™ ํ›„ `:224` `return 0`). + - *์ฆ๊ฑฐ*: +- [ ] **P1-08** ๊ฒฐ์ •์  ์žฌํ˜„ 4์ข…(T1-1, T1-2, T1-4, T1-7)์ด ์ˆ˜์ • ์ „ ์‹คํŒจยท์ˆ˜์ • ํ›„ ํ†ต๊ณผ. `test_wrapper_exit_code_mapping_literals_are_frozen` ํ†ต๊ณผ. + - *์ฆ๊ฑฐ*: +- [ ] **P1-09** ๋กค๋ฐฑ ๋“œ๋ฆด: `MAM_DELEGATE_FAIL_CLOSED=0`์—์„œ T1-8 ์žฌํ˜„(rc=0, `pending`). + - *์ฆ๊ฑฐ*: +- [ ] **P1-10** (Rev.2 Tertiary) `attempt_id` ๊ทœ์•ฝ: `registry.py update --attempt-id`, `engine._decide` redispatch๊ฐ€ `attempt_id` ๊ณต๊ธ‰ ์‹œ ์ด๋ฅผ ์ „์ง„ ์กฐ๊ฑด์œผ๋กœ ์‚ฌ์šฉ(๋ฏธ๊ณต๊ธ‰ ์‹œ ๊ธฐ์กด iteration ๊ทœ์น™), `redispatch`/`claim` ์ปค๋ฐ‹ ์‹œ ๋ ˆ์ฝ”๋“œ ๊ธฐ๋ก, `publish_event --finalize-report` ๊ธฐ๋ณธ attempt_id๊ฐ€ `record.attempt_id` ์šฐ์„ . + - *์ฆ๊ฑฐ*: +- [ ] **P1-11** ๋ž˜ํผ loop ๊ฒฝ๋กœ: phase ๋””์ŠคํŒจ์น˜๋งˆ๋‹ค `attempt` ์ฆ๊ฐ€ ํ›„ `update โ€ฆ --attempt-id "$attempt"`. T1-10(์ˆ˜์ • ์ „ rc=4 ์—ญ๊ฒ€์ฆ)/T1-11/T1-12, F-A 3์ข… ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ. + - *์ฆ๊ฑฐ*: +- [ ] **P1-12** T1-13 `close --request cancelled` ์ „์ด ๊ทœ์น™ ๊ณ ์ •. + - *์ฆ๊ฑฐ*: + +--- + +## Phase 2: ์‚ฌ์ „ TUI ์œ ํœด ํ•ธ๋“œ์…ฐ์ดํฌ ๋ฐ ์ œ์ถœ ๊ฒ€์ฆ ๋ณด๊ฐ• + +- [ ] **P2-01** `lib_py/loop/watcher.py`์— `main()`/`__main__` ์ถ”๊ฐ€: `wait --session S --until โ€ฆ --timeout-ms N`, JSON 1์ค„ stdout, rc matched 0 / timeout 2 / agent_not_found 3 / transport_error 4. `build_argv(..., session=None)`๊ฐ€ `default`๊ฐ€ ์•„๋‹Œ ์„ธ์…˜์ด๋ฉด `herdr --session S agent wait โ€ฆ`๋กœ ๊ตฌ์„ฑ(Rev.2 Primary-A). `lib_py.delegation`โ†’`lib_py.loop` ์ž„ํฌํŠธ 0๊ฑด ์œ ์ง€(`test_delegation_package_imports_no_sibling_lib_py_modules`). + - *์ฆ๊ฑฐ*: +- [ ] **P2-02** `lib.sh: wait_for_agent_idle`: `SKS_IDLE_HANDSHAKE`, ์‚ฌ์ „ `_sks_herdr agent get`(`unknown`โ†’3, `blocked`โ†’2, `idle|done`โ†’0), `working`โ†’`"$(_delegate_py_bin)" -m lib_py.loop.watcher wait --session "$HERDR_SESSION_NAME" --until idle --until done --until blocked --timeout-ms ${SKS_IDLE_TIMEOUT_MS:-120000}`, timeout์€ `SKS_IDLE_RETRIES`(2)ร—`SKS_IDLE_RETRY_BACKOFF_S`(10) ์žฌ์‹œ๋„ ํ›„ rc 1. bare `python` ๊ธˆ์ง€. `interactive_ready` ๋ฏธ์‚ฌ์šฉ. + - *์ฆ๊ฑฐ*: +- [ ] **P2-03** ๋ž˜ํผ `run_agent`๋ฅผ `preflight_agent` + `inject_agent`๋กœ ๋ถ„๋ฆฌ. ์ˆœ์„œ: register โ†’ preflight โ†’ idle wait โ†’ `_disp_tok` ์ฝ๊ธฐ โ†’ ๊ตฌ๋…์ž ๊ธฐ๋™ โ†’ inject โ†’ wait โ†’ finalize. directยทloop ์–‘ ๊ฒฝ๋กœ ์ ์šฉ. ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ/idle ์‹คํŒจ ์‹œ `finalize_unfinished โ€ฆ preflight|idle_wait`(โ†’ `cancelled`/`session_busy`) + `exit 5`. + - *์ฆ๊ฑฐ*: +- [ ] **P2-04** `send_keys_safe` ์ œ์ถœ ๊ฒ€์ฆ: pre_submit์— ์กด์žฌํ•œ busy ํ† ํฐ ๋ฐฐ์ œ(`:2330-2332`). + - *์ฆ๊ฑฐ*: +- [ ] **P2-05** `send_keys_safe` ๋„ค์ดํ‹ฐ๋ธŒ ๊ฒฝ๋กœ `agent prompt --wait --until working --timeout 5000` (`SKS_PROMPT_WAIT=1` ๊ธฐ๋ณธ). stalled/timeout โ†’ ์žฌ์ œ์ถœ ์—†์ด `agent get` 1ํšŒ ํ™•์ธ, `agent_blocked` โ†’ rc 2, ๋ถˆ๋ช… โ†’ rc 4. + - *์ฆ๊ฑฐ*: +- [ ] **P2-06** ํด๋ฐฑ(`unknown`) ๊ฒฝ๋กœ busy ํ† ํฐ ๊ฒŒ์ดํŠธ(`SKS_BUSY_TIMEOUT` ๊ธฐ๋ณธ 120s). + - *์ฆ๊ฑฐ*: +- [ ] **P2-07** T2-1~T2-11 ํ†ต๊ณผ, `test_h16`/`test_b8`/`test_c1_*`/`test_herdr_shim_contract` ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ. ๋กค๋ฐฑ ๋“œ๋ฆด `SKS_IDLE_HANDSHAKE=0`. + - *์ฆ๊ฑฐ*: +- [ ] **P2-08** (Rev.2 Primary) T2-10 `--session` argv ๋‹จ์ •, T2-11 bare `python` ์ •์  ๋ฆฐํŠธ 0๊ฑด. + - *์ฆ๊ฑฐ*: + +--- + +## Phase 3: ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ๋Ÿฌ๋„ˆ Fail-Fast ์—ฐ๊ฒฐ + +- [ ] **P3-01** `runner.delegate_job`: rcโ‰ 0์—์„œ๋„ `registered job:` ํŒŒ์‹ฑ โ†’ `created_jobs`ยทcheckpoint ๊ธฐ๋ก โ†’ id ๋ฐ˜ํ™˜. ๋ฏธ๋งค์น˜ ์‹œ๋งŒ `None`. + - *์ฆ๊ฑฐ*: +- [ ] **P3-02** `run_loop.sh` `delegate_job_safe`/`:373` ํŒŒ์‹ฑ ๋™๋“ฑ์„ฑ(`MAM_LOOP_PY=0`). + - *์ฆ๊ฑฐ*: +- [ ] **P3-03** T3-1(`wait_for_job` โ‰ค 2s False), T3-2(checkpoint ๊ธฐ๋ก), `test_issue5_phase3_loop.py` ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ. + - *์ฆ๊ฑฐ*: +- [ ] **P3-04** (Rev.2 Secondary) `delegate_job` rc=5 โ†’ `MAM_SESSION_BUSY_RETRY_S`(30) ๋Œ€๊ธฐ ํ›„ 1ํšŒ ์žฌ์œ„์ž„. T3-3. + - *์ฆ๊ฑฐ*: + +--- + +## Phase 4: ๋‹จ์œ„/๊ณ„์•ฝ ํšŒ๊ท€ ์Šค์œ„ํŠธ ๋ฐ ๋ฌธ์„œ + +- [ ] **P4-01** `tests/conftest.py` mock_herdr ํ™•์žฅ: `agent get/list`์— `agent_status`(๊ธฐ๋ณธ `idle`), `agent wait`(`status_sequence` ์†Œ๋น„, timeout ์‹œ stderr JSON rc 1), `agent prompt --wait`(`prompt_wait_result`), ์ „์—ญ `--session ` ์ธ์ž ์ˆ˜์šฉยทํ˜ธ์ถœ ๊ธฐ๋ก. ๊ธฐ์กด ํ…Œ์ŠคํŠธ ๋ฌด๋ณ€๊ฒฝ ํ†ต๊ณผ. + - *์ฆ๊ฑฐ*: +- [ ] **P4-02** `tests/test_issue6_fail_closed.py` T1-1~T1-13, T3-1~T3-3 ์ž‘์„ฑ ๋ฐ ์—ญ๊ฒ€์ฆ. + - *์ฆ๊ฑฐ*: +- [ ] **P4-03** `tests/test_issue6_turn_handshake.py` T2-1~T2-11 ์ž‘์„ฑ ๋ฐ ์—ญ๊ฒ€์ฆ. + - *์ฆ๊ฑฐ*: +- [ ] **P4-04** ๋ฌธ์„œ: `multi-agent-mux-delegate-job/SKILL.md` ์ข…๋ฃŒ ์ฝ”๋“œ ํ‘œ, `MESSAGING.md` ยง5.1 ๊ตฌ๋…์ž ํƒ€์ž„์•„์›ƒโ†’๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋ฐ˜์˜, `BOOTSTRAP.md`/`BOOTSTRAP.ko.md` "completed ๋ฐœํ–‰ ํ›„ ๋„๊ตฌ ํ˜ธ์ถœ ์—†์ด ํ„ด ์ข…๋ฃŒ" 1๋ฌธ์žฅ. `test_deploy_freshness.py`(`MQTT_*` 10์ข…)ยท`test_o1_rebuttal.py` ํ†ต๊ณผ. + - *์ฆ๊ฑฐ*: +- [ ] **P4-05** ์ „์ฒด ์Šค์œ„ํŠธ 0-Fail (750 + ์‹ ๊ทœ โ‰ฅ 20), `git diff --check` ํด๋ฆฐ, ์ˆ˜์ง‘ ์ˆ˜ยทํ†ต๊ณผ ์ˆ˜ยท์†Œ์š” ์‹œ๊ฐ„ ๊ธฐ๋ก. + - *์ฆ๊ฑฐ*: + +--- + +## Phase 5: ์‹ค๊ธฐ๋™ E2E, ์ •๋ฆฌ, ๋ฆด๋ฆฌ์Šค + +- [ ] **P5-01** 4๊ฐœ ๋ฐฑ์—”๋“œ(Claude/Grok/Codex/OpenCode) ์—ฐ์† ์œ„์ž„ E2E(**direct ๋ชจ๋“œ**; loop ๋ชจ๋“œ ์ข…๋‹จ์€ O-1 ์ด์Šˆ ์™„๋ฃŒ ํ›„): `completed`โ†’๋‹ค์Œ ์žก `started` ํƒ€์ž„๋ผ์ธ, idle ๋Œ€๊ธฐ ์‹œ๊ฐ„, ์ฃผ์ž… ์‹คํŒจยท์ค‘๋ณต 0๊ฑด. `docs/contracts/issue-6/phase5-e2e.md` ์ž‘์„ฑ. + - *์ฆ๊ฑฐ*: +- [ ] **P5-02** ํƒ€์ž„์•„์›ƒ ์ฃผ์ž… E2E(`--idle-timeout 30`, ํ”„๋กฌํ”„ํŠธ ๋ฌด์‹œ): ๋ž˜ํผ rc=2, `error`, `wait_for_job` โ‰ค 2s. + - *์ฆ๊ฑฐ*: +- [ ] **P5-03** ์ž”์กด ๋ ˆ์ฝ”๋“œ `93a44347`, `2961b822`, `ee242671` `registry.py close --request deadline_timeout โ€ฆ --detail "Issue #6 backfill"`๋กœ ํ์‡„. + - *์ฆ๊ฑฐ*: +- [ ] **P5-04** `IMPROVEMENTS.md` ยง5 `Issue-6-FailClosed-TurnHandshake` ๋ธ”๋ก ์ถ”๊ฐ€(ยง6 ํ‘œยทํ—ค๋” ํ‚ค ๋ชฉ๋กยท๊ฑด์ˆ˜ ๋™๊ธฐํ™”). ๋ณ„๋„ ์ด์Šˆ ๋“ฑ๋ก: O-1 ๋ฆฌํ…Œ์ธ ์žฌ์ˆ˜๋ฝ(**loop/discuss ๋ชจ๋“œ ์‚ฌ์šฉ ์ „ ํ•„์ˆ˜**๋กœ ํ‘œ๊ธฐ), ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์˜ค์—ผ. + - *์ฆ๊ฑฐ*: +- [ ] **P5-05** `v4.4.1` 4-way ๋ฝ์Šคํ…(`lib.sh:32`, `eager.py:16`, `VERSIONS.md` ๊ฐœ์š”ยท๋งคํŠธ๋ฆญ์ŠคยทChangelog, 8๊ฐœ `SKILL.md`), `test_version_consistency.py` ํ†ต๊ณผ. + - *์ฆ๊ฑฐ*: +- [ ] **P5-06** ๋ฆฌ๋ทฐ์–ด ๊ต์ฐจ ๊ฒ€ํ†  `[VERDICT: PASS]` ํ›„ `main` ๋ณ‘ํ•ฉ. + - *์ฆ๊ฑฐ*: