diff --git a/.agents/reports/planner-reviewer-claude-01/report-293e905f.identity.json b/.agents/reports/planner-reviewer-claude-01/report-293e905f.identity.json new file mode 100644 index 0000000..ccbd7cf --- /dev/null +++ b/.agents/reports/planner-reviewer-claude-01/report-293e905f.identity.json @@ -0,0 +1,14 @@ +{ + "loop_id": "loop-20260921T150212Z-97d845", + "base_sha": "6fc9194f896eb2e13e123b8ebe7a9089b629764d", + "head_sha": "1e826515cc2a07ea1f8da0ff1259e1ea4dde86fd", + "tree_sha": "4fd82ad1c2682d12c3f5114818f97291af28a032", + "review_commit": "1e826515cc2a07ea1f8da0ff1259e1ea4dde86fd", + "reviewer": "planner-reviewer-claude-01", + "job_id": "293e905f", + "verdict": "PASS", + "transport": "herdr", + "evidence": "review-tree", + "isolation": "worktree", + "verified_at": "2026-09-21T15:25:44Z" +} \ No newline at end of file diff --git a/.agents/reports/planner-reviewer-claude-01/report-293e905f.md b/.agents/reports/planner-reviewer-claude-01/report-293e905f.md new file mode 100644 index 0000000..8319fb8 --- /dev/null +++ b/.agents/reports/planner-reviewer-claude-01/report-293e905f.md @@ -0,0 +1,139 @@ +# ๐Ÿ” Review Report โ€” PR #32 ๋ณ‘ํ•ฉ ์ „ ๋…๋ฆฝ ๋ฆฌ๋ทฐ (job `293e905f`) + +- **Reviewer**: `planner-reviewer-claude-01` (role: planner,reviewer) ยท 2026-09-22 +- **๋Œ€์ƒ**: PR #32 `fix/issue-25` **@ `1e82651`**, base `main 6fc9194` (v4.7.0). 5์ปค๋ฐ‹, 4ํŒŒ์ผ `+486/โˆ’31`. +- **๊ฒ€์ฆ ๊ฒฝ๋กœ**: `/var/folders/.../T/mam-review/loop-20260921T150212Z-97d845/1` (๋ธŒ๋ฆฌํ”„๊ฐ€ ์ง€์ •ํ•œ ๋ฆฌ๋ทฐ ์›ŒํฌํŠธ๋ฆฌ). ์‹ค์ œ๋กœ ๊ฑฐ๊ธฐ์„œ๋งŒ ๊ฒ€์ฆํ–ˆ๋‹ค. +- **identity ํ™•์ธ**: ๊ฒ€์ฆ ๊ฒฝ๋กœ์˜ `HEAD = 1e826515cc2a07ea1f8da0ff1259e1ea4dde86fd`, `HEAD^{tree} = 4fd82ad1c2682d12c3f5114818f97291af28a032` โ€” ๋ธŒ๋ฆฌํ”„์˜ `head`ยท`tree` ์™€ **์ผ์น˜**ํ•œ๋‹ค(ํ•ญ๋ชฉ 5 identity ๊ฐ€ ์‹ค์ œ๋กœ ์ž‘๋™ํ•จ์„ ์ด ์žก ์ž์ฒด๊ฐ€ ๋ณด์—ฌ์ค€๋‹ค). +- **์•ˆ์ „ ๊ทœ์น™ ์ค€์ˆ˜**: ๊ฒ€์ฆ ๊ฒฝ๋กœยท์†Œ์Šค ์›ŒํฌํŠธ๋ฆฌ(`multi-agent-mux-wt/pr-32`)ยท๋ณธ ์ฒดํฌ์•„์›ƒ(`main 6fc9194`) ๋ชจ๋‘ ์ˆ˜์ •ยท์ปค๋ฐ‹ยทcheckout ์—†์Œ. ์‹คํ—˜์€ ์ „๋ถ€ scratchpad ์ž„์‹œ ๋””๋ ‰ํ„ฐ๋ฆฌ(uuid4 ์ด๋ฆ„)์—์„œ ํ–ˆ๊ณ , pytest ๋Š” `--basetemp` ์„ ๊ฒ€์ฆ ๊ฒฝ๋กœ ๋ฐ–์œผ๋กœ ์ฃผ์—ˆ๋‹ค. ์‹ค์ œ `.mam`ยท๋ฐ๋ชฌยท์„ธ์…˜ ์กฐ์ž‘ ์—†์Œ, pushยทmergeยทPR ์ƒํƒœ ๋ณ€๊ฒฝ ์—†์Œ, purgeยทscrub ์‹คํ–‰ ์—†์Œ. + +## ๊ฒฐ๋ก  ์š”์•ฝ + +**๋ณ‘ํ•ฉ์„ ๋ง‰์„ ๊ฒฐํ•จ์€ ์—†๋‹ค.** ํ•ต์‹ฌ ๋ณ€๊ฒฝ(์•”๋ฌต์  ๊ณ„ํš์„œ ๋กœ๋“œ ์ œ๊ฑฐ, `--plan-file` ๋ช…์‹œ ์žฌ์‚ฌ์šฉ, ๋„ค ์ง€์  ๊ณตํ†ต ๊ณ„ํš ํ…์ŠคํŠธ ๊ฒ€์‚ฌ)์€ ์ฝ”๋“œยท๋ฌธ์„œยทํ…Œ์ŠคํŠธ๊ฐ€ ์ผ์น˜ํ•˜๋ฉฐ, ์ƒˆ ํ…Œ์ŠคํŠธ 17๊ฑด ์ค‘ **13๊ฑด์ด ์ˆ˜์ • ์ „ ์†Œ์Šค์—์„œ๋Š” ์‹คํŒจ**ํ•œ๋‹ค(์ง์ ‘ ํ™•์ธ). ๋น„์ฐจ๋‹จ ์ง€์  3๊ฑด(E1โ€“E3)์„ ๋‚จ๊ธด๋‹ค. + +## 1. ์•”๋ฌต์  ๋กœ๋“œ ์ œ๊ฑฐ (ํ•ญ๋ชฉ 1) + +| ํ™•์ธ | ๊ฒฐ๊ณผ | +|---|---| +| Python ๋Ÿฌ๋„ˆ | `runner.py` ์—์„œ `glob` import ๊ฐ€ ์‚ฌ๋ผ์กŒ๊ณ , ๋‚จ์€ `glob` ์‚ฌ์šฉ์ฒ˜๋Š” `find_report` ์˜ `report-final.md` ํƒ์ƒ‰(`:568`) ํ•˜๋‚˜๋ฟ์ด๋‹ค. `.agents/reports//plan-*.md` ๋ฅผ ์ฝ๋Š” ์ฝ”๋“œ๊ฐ€ ์—†๋‹ค | +| ๋ ˆ๊ฑฐ์‹œ ์…ธ ๋Ÿฌ๋„ˆ | `run_loop.sh` ์˜ `EXISTING_PLAN_FILE`/`ls -t .../plan-*.md` ๋ธ”๋ก์ด ํ†ต์งธ๋กœ ์‚ฌ๋ผ์ง€๊ณ  `log_info "No plan loaded; the Creator plans by itself."` ๋งŒ ๋‚จ์•˜๋‹ค | +| ๋‹ค๋ฅธ ๊ฒฝ๋กœ | ์ €์žฅ์†Œ ์ „์ฒด์—์„œ `plan-*.md` ๋ฅผ ์ฝ๋Š” ์ฝ”๋“œ๋Š” ์—†๋‹ค(์Šน๊ฒฉ ์ชฝ **์“ฐ๊ธฐ**๋งŒ `runner.py:1368-1372` ์— ๋‚จ๋Š”๋‹ค โ€” ์ •์ƒ) | +| planner ์ž๋™ ์„ ํƒ | `resolve_planner_session`(`run_loop.sh:386`)ยท`resolve_planner_session` ํŒŒ์ด์ฌ ๋Œ€์‘์€ ๋‚จ์•„ ์žˆ์œผ๋‚˜, ๊ทธ ๊ฒฐ๊ณผ๋Š” **plan ๋ชจ๋“œ์˜ ์œ„์ž„ ๋Œ€์ƒ๊ณผ ์Šน๊ฒฉ ๊ฒฝ๋กœ**์—๋งŒ ์“ฐ์ธ๋‹ค. ์ž๊ธฐ๊ณ„ํš ๋ชจ๋“œ์—์„œ planner ์ด๋ฆ„์œผ๋กœ ํŒŒ์ผ์„ ์ฐพ๋Š” ์ฝ”๋“œ๋Š” ์—†๋‹ค | + +## 2. `--plan-file` (ํ•ญ๋ชฉ 2) โ€” ์‹ค์ธก + +๊ฒ€์ฆ ๊ฒฝ๋กœ์˜ ๋Ÿฌ๋„ˆ๋ฅผ ์ž„์‹œ ์ œ์–ด ๋ฃจํŠธ(`WORKSPACE_ROOT=/ctl`)๋กœ ์ง์ ‘ ๋Œ๋ ค ํ™•์ธํ–ˆ๋‹ค. + +| ์ž…๋ ฅ | ๊ฒฐ๊ณผ | +|---|---| +| ์—†๋Š” ํŒŒ์ผ | `[โœ—] --plan-file <์ ˆ๋Œ€๊ฒฝ๋กœ>: not a readable file with plan text in it.` ยท **exit 1** | +| ๋นˆ ํŒŒ์ผ / ๊ณต๋ฐฑ๋งŒ ์žˆ๋Š” ํŒŒ์ผ | ๊ฐ™์€ ๋ฉ”์‹œ์ง€ ยท exit 1 | +| ๋””๋ ‰ํ„ฐ๋ฆฌ | ๊ฐ™์€ ๋ฉ”์‹œ์ง€ ยท exit 1 (`is_file()` ๋กœ ๊ฑธ๋ฆฐ๋‹ค) | +| ์ฝ๊ธฐ ๊ถŒํ•œ ์—†์Œ(`chmod 000`) | ๊ฐ™์€ ๋ฉ”์‹œ์ง€ ยท exit 1 (`OSError` โ†’ `None`) | +| `--plan` ๊ณผ ํ•จ๊ป˜ | `ERROR: --plan-file cannot be combined with --plan.` + ์‚ฌ์šฉ ์˜ˆ 2์ค„ ยท exit 1 | +| ์ƒ๋Œ€๊ฒฝ๋กœ | **์ œ์–ด ๋ฃจํŠธ ๊ธฐ์ค€**. ํ˜ธ์ถœ cwd(`/elsewhere`)์— `plans/p.md` ๊ฐ€ ์‹ค์ œ๋กœ ์žˆ์–ด๋„ ๋Ÿฌ๋„ˆ๋Š” `/plans/p.md` ๋ฅผ ์ฐพ๊ณ  ์‹คํŒจํ•œ๋‹ค โ€” #25 ์˜ "๋‹ค๋ฅธ ๋””๋ ‰ํ„ฐ๋ฆฌ์˜ ๋™๋ช… ํŒŒ์ผ์„ ์ง‘์–ด์˜ค๋Š”" ์‚ฌ๊ณ ๋ฅผ ๊ตฌ์กฐ์ ์œผ๋กœ ๋ง‰๋Š”๋‹ค | +| ๊ณต๋ฐฑ ์žˆ๋Š” ๊ฒฝ๋กœ(`my plans/p.md`, ์ƒ๋Œ€) | ์ •์ƒ ๋กœ๋“œ: `Using the plan given with --plan-file: /my plans/p.md (modified ... UTC)` (์…ธ์„ ๊ฑฐ์น˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ธ์šฉ ๋ฌธ์ œ ์—†์Œ) | +| ์ ˆ๋Œ€๊ฒฝ๋กœยท`~` | `Path.__truediv__` ๊ฐ€ ์ ˆ๋Œ€๊ฒฝ๋กœ๋ฅผ ๊ทธ๋Œ€๋กœ ๋‘๊ณ  `expanduser()` ๊ฐ€ ์•ž์„œ ์ ์šฉ๋œ๋‹ค(P3d ๊ฐ€ ๊ณ ์ •) | +| `--review-only` ์™€ ํ•จ๊ป˜ | ๊ฒฝ๊ณ  ํ›„ ๋ฌด์‹œ(`log_warn`), `current_plan` ์„ ์ฑ„์šฐ์ง€ ์•Š๋Š”๋‹ค | +| ๋ ˆ๊ฑฐ์‹œ ์…ธ(`MAM_LOOP_PY=0`) | `ERROR: --worktree/--base/--review-only/--review-isolation/--remote-review/--plan-file require the Python loop (MAM_LOOP_PY=1)` ยท exit 1 โ€” **๋™๋“ฑ ๊ตฌํ˜„ ๋Œ€์‹  ๋ช…์‹œ์  ๊ฑฐ๋ถ€**์ด๋ฉฐ, ๊ธฐ๋ณธ ๊ฒฝ๋กœ(`MAM_LOOP_PY=1`)๋Š” `exec "$_py" -m lib_py.loop "${MAM_LOOP_ARGV[@]}"`(`run_loop.sh:262`)๋กœ ์›๋ณธ argv ๋ฅผ ๊ทธ๋Œ€๋กœ ๋„˜๊ธฐ๋ฏ€๋กœ ๊ณต๋ฐฑ ๊ฒฝ๋กœ๋„ ์•ˆ์ „ํ•˜๋‹ค | + +์ฝ๊ธฐ๊ฐ€ **์žก์„ ํ•˜๋‚˜๋„ ๋งŒ๋“ค๊ธฐ ์ „**(parse_args ๋‹จ๊ณ„)์— ๋๋‚˜๋Š” ๊ฒƒ๋„ ํ™•์ธํ–ˆ๋‹ค โ€” ์‹คํŒจ ์‹œ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ์•„๋ฌด ํ”์ ์ด ์—†๋‹ค. + +## 3. ๊ณ„ํš ํ…์ŠคํŠธ ๊ฒ€์‚ฌ (ํ•ญ๋ชฉ 3) + +`_plan_text()`(`runner.py:303-317`)๊ฐ€ "์ฝ์„ ์ˆ˜ ์žˆ๋Š” ์ผ๋ฐ˜ ํŒŒ์ผ + ๊ณต๋ฐฑ ์ œ์™ธ ๋‚ด์šฉ ์žˆ์Œ"์„ ๋‹จ์ผ ํŒ์ •์œผ๋กœ ๋งŒ๋“ค๊ณ , **ํŒŒ์ด์ฌ ๋Ÿฌ๋„ˆ์˜ ๋„ค ์ง€์ ์ด ๋ชจ๋‘ ์ด๋ฅผ ์“ด๋‹ค**: `--plan-file`(`:265`), `--plan` ์ดˆ๊ธฐ ๊ณ„ํš์„œ(`:1025`), ๋…ผ์˜ ํ„ด refine(`:1054`), ๋ฆฌ๋ทฐ ์—์Šค์ปฌ๋ ˆ์ด์…˜ refine(`:1284`). + +- ๋นˆ ์ดˆ๊ธฐ ๊ณ„ํš์„œ โ†’ `log_error` ํ›„ **return 1**. ์ด๋•Œ ์ด๋ฏธ ๋งŒ๋“  ์žก์€ `release_guard`/์ •๋ฆฌ ๊ฒฝ๋กœ๊ฐ€ ๋‹ด๋‹นํ•˜๊ณ , ์ž๊ธฐ๊ณ„ํš์œผ๋กœ์˜ ์กฐ์šฉํ•œ ๊ฒฉํ•˜๊ฐ€ ์‚ฌ๋ผ์กŒ๋‹ค. +- ๋นˆ refine โ†’ ๊ฒฝ๊ณ  + **์ด์ „ ๊ณ„ํš ์œ ์ง€**. ๊ทธ๋ž˜์„œ "๊ฐฑ์‹ ๋œ ๋‹ค์Œ ๊ณ„ํš์„œ์— ์ž…๊ฐํ•˜์—ฌโ€ฆ" ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋นˆ ๊ณ„ํš์„ ์‹ฃ๋Š” ์ผ์ด ์—†๋‹ค. +- ์—์Šค์ปฌ๋ ˆ์ด์…˜ ๊ฒฝ๋กœ(`[ESCALATE: PLANNER]`)๋Š” `if self.plan_mode and complex_fix`(`:1275`)๋กœ **plan ๋ชจ๋“œ์—๋งŒ** ๊ฑธ๋ฆฐ๋‹ค. `--plan-file` ๋กœ ๊ณ„ํš์„ ์‹ค์€ ์‹คํ–‰์—์„œ ESCALATE ํŒ์ •์ด ๋‚˜์˜ค๋ฉด ์žฌ๊ณ„ํš ์—†์ด ์ผ๋ฐ˜ ์ˆ˜์ • ๋ผ์šด๋“œ๋กœ ๊ฐ„๋‹ค โ€” ์ƒŒ๋“œ๋ฐ•์Šค์—์„œ ์ง์ ‘ ๊ตฌ๋™ํ•ด ํ™•์ธํ–ˆ๋‹ค(Planner ์œ„์ž„ 0๊ฑด, ์ˆ˜์ • ํ”„๋กฌํ”„ํŠธ๋Š” "๋ฆฌ๋ทฐ์–ด๋“ค์ด ์ง€์ ํ•œ ๋‹ค์Œ ํ”ผ๋“œ๋ฐฑโ€ฆ"). SKILL.md:31 ์˜ "์ˆ˜์ • ๋ผ์šด๋“œ์—๋Š” ๋ฆฌ๋ทฐ ํ”ผ๋“œ๋ฐฑ๋งŒ" ์„œ์ˆ ๊ณผ **์ผ์น˜**ํ•œ๋‹ค. ๋‹ค๋งŒ ์ด ๋™์ž‘ ์ž์ฒด๋Š” E2 ๋กœ ๋‚จ๊ธด๋‹ค. + +## 4. ์ฒดํฌํฌ์ธํŠธ (ํ•ญ๋ชฉ 4) + +`.mam/loop-checkpoint.json` ์˜ `current_plan` ์€ **์“ฐ๊ธฐ ์ „์šฉ**์ด๋‹ค. `checkpoint.load_checkpoint()` ๋Š” ์ •์˜๋˜์–ด ์žˆ์œผ๋‚˜ `runner.py` ์–ด๋””์—์„œ๋„ ํ˜ธ์ถœํ•˜์ง€ ์•Š๋Š”๋‹ค(`grep 'checkpoint\.'` โ†’ `save_checkpoint`ยท`remove_checkpoint` ๋ฟ). ๋”ฐ๋ผ์„œ ๋ณธ ์ฒดํฌ์•„์›ƒ ์ฒดํฌํฌ์ธํŠธ์— ๋‚จ์•„ ์žˆ๋Š” Issue #6 ๊ณ„ํš ํ…์ŠคํŠธ๊ฐ€ ๋‹ค์Œ ๋ฃจํ”„๋‚˜ ์žฌ๊ฐœ์—์„œ ๋˜์‚ด์•„๋‚  ๊ฒฝ๋กœ๋Š” ์—†๋‹ค. ๊ณ„ํš์€ ๋งค ์‹คํ–‰๋งˆ๋‹ค `--plan-file` ๋˜๋Š” `--plan` ์œผ๋กœ๋งŒ ๋“ค์–ด์˜จ๋‹ค. + +## 5. ์˜ต์…˜ ์—†์„ ๋•Œ์˜ ๋™์ž‘ ๋ณ€ํ™” (ํ•ญ๋ชฉ 5) + +- ์ž๊ธฐ๊ณ„ํš ํ”„๋กฌํ”„ํŠธ(`runner.py:1095`)์™€ ์Šน๊ฒฉ ๊ฒฝ๋กœ(`:1367` `if self.plan_mode and self.current_plan and self.plan_job_id`)๋Š” ๊ทธ๋Œ€๋กœ๋‹ค. ์ฆ‰ `--plan-file` ๋กœ ์‹ค์€ ๊ณ„ํš์„œ๋Š” **์Šน๊ฒฉ๋˜์ง€ ์•Š๋Š”๋‹ค**(์Šน๊ฒฉ์€ plan ๋ชจ๋“œ์˜ planner ์‚ฐ์ถœ๋ฌผ๋งŒ) โ€” ์˜ณ์€ ๊ตฌ๋ถ„์ด๋‹ค. +- ๋ฐ”๋€ ๊ฒƒ์€ "planner ๋ฆฌํฌํŠธ ๋””๋ ‰ํ„ฐ๋ฆฌ์—์„œ ์ตœ์‹  plan-*.md ๋ฅผ ๋Œ์–ด์˜ค๋˜" ๋™์ž‘ ํ•˜๋‚˜๋ฟ์ด๋‹ค. ๊ทธ ์™ธ ํ”„๋กฌํ”„ํŠธ ๋ฌธ๊ตฌยท์˜ต์…˜ ๊ธฐ๋ณธ๊ฐ’ยท์ข…๋ฃŒ ์ฝ”๋“œ๋Š” ๋™์ผํ•˜๋‹ค. +- ๊ธฐ์กด ์Šค์œ„ํŠธ ์ •ํ•ฉ์€ ยง6 ์—์„œ ์‹ค์ธกํ–ˆ๋‹ค. + +## 6. ํ…Œ์ŠคํŠธ (ํ•ญ๋ชฉ 6) + +### 6-1. ๊ฒ€์ฆ ๊ฒฝ๋กœ์—์„œ์˜ ์‹คํ–‰ + +``` +cd <๊ฒ€์ฆ ๊ฒฝ๋กœ> && PYTHONDONTWRITEBYTECODE=1 <์ œ์–ด ๋ฃจํŠธ .venv/bin/python> -m pytest -p no:cacheprovider -q -rfEs \ + --basetemp=<๊ฒ€์ฆ ๊ฒฝ๋กœ ๋ฐ–> tests/test_issue25_plan_reuse.py tests/test_loop_cli.py tests/test_issue5_phase3_loop.py \ + tests/test_issue21_loop_b.py tests/test_issue21_phase2_loop_a.py tests/test_issue21_acceptance.py +โ†’ 2 failed, 108 passed in 220.98s (skipped 0) +``` + +**์‹คํŒจ 2๊ฑด์€ PR ํšŒ๊ท€๊ฐ€ ์•„๋‹ˆ๋ผ ๋ฆฌ๋ทฐ ์›ŒํฌํŠธ๋ฆฌ์— `.venv` ๊ฐ€ ์—†์–ด์„œ ์ƒ๊ธด ํ™˜๊ฒฝ ๋ฌธ์ œ๋‹ค.** + +| ์‹คํŒจ | ์›์ธ | ํ™•์ธ | +|---|---|---| +| `test_loop_cli.py::test_creator_parses_and_reaches_session_check` | `run_loop.sh` ๋ฅผ ํ•˜์œ„ ํ”„๋กœ์„ธ์Šค๋กœ ๋„์šฐ๋Š”๋ฐ, `REPO_ROOT`(= ๊ฒ€์ฆ ๊ฒฝ๋กœ)์— `.venv` ๊ฐ€ ์—†์–ด `_py` ๊ฐ€ ์‹œ์Šคํ…œ python ์œผ๋กœ ๋–จ์–ด์ง€๊ณ  `RuntimeError: Failed to eager load lib_py.loop.autocomplete: No module named 'paho'` ๋กœ ์ฃฝ๋Š”๋‹ค(`run_loop.sh:253`) | `AGENT_PYTHON_BIN=<์ œ์–ด ๋ฃจํŠธ .venv/bin/python>` ๋งŒ ์ฃผ๊ณ  ๋‘˜์„ ๋‹ค์‹œ ๋Œ๋ฆฌ๋ฉด **2 passed in 0.34s** | +| `test_issue5_phase3_loop.py::test_p3_02_run_loop_wrapper_delegates_to_python_default` | ๋™์ผ | ๋™์ผ | + +๋‘ ์‹œํ—˜ ๋ชจ๋‘ PR ์ด ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์€ ํŒŒ์ผ์ด๋ฉฐ, ์‹คํŒจ ๋ฉ”์‹œ์ง€์— ๊ณ„ํš์„œยท`--plan-file` ์€ ๋“ฑ์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค. + +### 6-2. ์ˆ˜์ • ์—†์ด๋Š” ์‹คํŒจํ•˜๋Š”๊ฐ€ โ€” ํ™•์ธํ•จ + +๊ฒ€์ฆ ๊ฒฝ๋กœ ์‚ฌ๋ณธ์„ ๋งŒ๋“ค๊ณ  `runner.py`ยท`run_loop.sh`ยท`SKILL.md` **์„ธ ํŒŒ์ผ๋งŒ base `6fc9194` ํŒ์œผ๋กœ ๋˜๋Œ๋ ค** ์ƒˆ ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ธ๋‹ค(๊ฒ€์ฆ ๊ฒฝ๋กœ ์ž์ฒด๋Š” ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์•˜๋‹ค). + +``` +13 failed, 4 passed in 1.51s +FAILED P1, P2, P3b, P3c, P3d, P5, P9[empty], P9[whitespace], P10, P11, P7, P7b, P8 +``` + +ํ†ต๊ณผํ•œ 4๊ฑด(P3ยทP4ยทP5bยทP6)์€ ์˜ต์…˜ ์—†๋Š” ์ผ๋ฐ˜ CLI ์˜ค๋ฅ˜ยทplan ๋ชจ๋“œ ์ •์ƒ ๊ฒฝ๋กœ๋ผ ์ˆ˜์ •๊ณผ ๋ฌด๊ด€ํ•œ ๊ฒƒ๋“ค์ด๋‹ค. ์ฆ‰ ์ƒˆ ํ…Œ์ŠคํŠธ๋Š” ์ด ๋ณ€๊ฒฝ์„ ์ •ํ™•ํžˆ ๊ฒจ๋ƒฅํ•œ๋‹ค. + +### 6-3. ๊ฒฉ๋ฆฌ + +| ํ•ญ๋ชฉ | ๊ฒฐ๊ณผ | +|---|---| +| ์‹ค์ œ `.mam/jobs` ์ „ํ›„ | **์ฐจ์ด 0** | +| ์‹ค์ œ `.agents/reports` | ์ƒˆ ํ•ญ๋ชฉ ์—†์Œ(ํ…Œ์ŠคํŠธ๋Š” ๊ฐ์ž `tmp_path` ์•ˆ์— ์ œ์–ด ๋ฃจํŠธ๋ฅผ ๋งŒ๋“ ๋‹ค โ€” `ctl` ํ”ฝ์Šค์ฒ˜) | +| ๊ฒ€์ฆ ๊ฒฝ๋กœ tracked ํŒŒ์ผ | ๋ณ€ํ™” ์—†์Œ(`git status --short` ํด๋ฆฐ, HEAD `1e82651` ์œ ์ง€) | +| ๊ฒ€์ฆ ๊ฒฝ๋กœ ignored | ๋‚ด ์‹คํ–‰์ด `.mam/`(๋ฌด์‹œ๋จ)์„ ๋งŒ๋“ค์—ˆ๋‹ค. tracked ํŒŒ์ผยท์ปค๋ฐ‹์—๋Š” ์˜ํ–ฅ์ด ์—†์œผ๋ฉฐ, ์ง€์šฐ๋ฉด ๋ฆฌ๋ทฐ ํŠธ๋ฆฌ๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ์…ˆ์ด๋ผ ๊ทธ๋Œ€๋กœ ๋‘์—ˆ๋‹ค โ†’ E3 | +| ์‹คํ–‰ ์‹œ๊ฐ„ | ์ƒˆ ํŒŒ์ผ `test_issue25_plan_reuse.py` ๋Š” ์ „์ฒด 17๊ฑด์ด **1.5 s** ์•ˆ์— ๋๋‚œ๋‹ค(๋ธŒ๋กœ์ปคยทherdr ์˜์กด ์—†์Œ, ๋ชจ๋‘ stub) | + +## 7. ๋ฌธ์„œ (ํ•ญ๋ชฉ 7) + +- ์˜› ์„œ์ˆ ("๊ธฐ์กด์— ์Šน๊ฒฉ๋œ ๊ณ„ํš์„œ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ๋กœ๋“œ")์€ `.agents`ยท`docs` ์–ด๋””์—๋„ ๋‚จ์•„ ์žˆ์ง€ ์•Š๋‹ค(์ „์ˆ˜ grep). +- SKILL.md ์˜ ์‚ฌ์šฉ๋ฒ• ๋ฌธ์ž์—ดยทCLI ํ‘œยทWorkflow ์˜ˆ์‹œยทMermaid ๋‹ค์ด์–ด๊ทธ๋žจ์ด ๋ชจ๋‘ `--plan-file` ๊ธฐ์ค€์œผ๋กœ ๋งž์ถฐ์กŒ๊ณ , "๊ณ„ํš์„œ๊ฐ€ ๋น„์–ด ์žˆ์„ ๋•Œ" ์ ˆ์ด ๋„ค ์ง€์ ์˜ ํŒ์ •์„ ์ •ํ™•ํžˆ ์„ค๋ช…ํ•œ๋‹ค. +- ๋Ÿฌ๋„ˆ usage ์™€ ์…ธ usage ๋ฌธ์ž์—ด์ด ๋™์ผํ•˜๋‹ค(P7 ์ด ์ด๋ฅผ ๊ณ ์ •ํ•œ๋‹ค). +- ๋ฐฐํฌ ์‚ฌ๋ณธ: ์ €์žฅ์†Œ์˜ `.claude/skills/*` ๋Š” `../../.agents/skills/*` **์‹ฌ๋งํฌ**๋ผ ์ž๋™์œผ๋กœ ๋™๊ธฐํ™”๋œ๋‹ค. ์‚ฌ์šฉ์ž ํ™ˆ `~/.claude/skills` ์—๋Š” mux ์Šคํ‚ฌ ์‚ฌ๋ณธ์ด ์—†๋‹ค. ์˜› ๋ฌธ๊ตฌ๋ฅผ ๊ฐ€์ง„ ์‚ฌ๋ณธ์€ ๋ฐœ๊ฒฌ๋˜์ง€ ์•Š์•˜๋‹ค. + +## 8. ๋ณ‘ํ•ฉ ์ฐจ๋‹จ ์—ฌ๋ถ€ (ํ•ญ๋ชฉ 8) + +๋ฒ„์ „ ๋ฝ์Šคํ… bump ๋ถ€์žฌ(`lib.sh:32`ยท`eager.py:16`ยทloop SKILL.md ๋ชจ๋‘ `4.7.0`, ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ๊ฐ€ 4.7.1 ๋กœ ์ฒ˜๋ฆฌ ์˜ˆ์ •) ์™ธ์— **๋ณ‘ํ•ฉ์„ ๋ง‰์„ ๊ฒฐํ•จ์€ ์—†๋‹ค**. + +| # | ๋น„์ฐจ๋‹จ ์ง€์  | ๊ทผ๊ฑฐ | +|---|---|---| +| E1 | ๋ ˆ๊ฑฐ์‹œ ์…ธ ๋Ÿฌ๋„ˆ(`MAM_LOOP_PY=0`)์˜ plan ๋ชจ๋“œ ์ฝ๊ธฐ์—๋Š” ๋นˆ ๊ณ„ํš์„œ ๊ฒ€์‚ฌ๊ฐ€ ์—†๋‹ค: `CURRENT_PLAN=$(cat "$PLAN_FILE")`(`run_loop.sh:501`). D13/D14 ๊ณ„์•ฝ์€ ํŒŒ์ด์ฌ ๋Ÿฌ๋„ˆ์—๋งŒ ์„ฑ๋ฆฝํ•˜๋Š”๋ฐ SKILL.md ์˜ "๊ณ„ํš์„œ๊ฐ€ ๋น„์–ด ์žˆ์„ ๋•Œ" ์ ˆ์€ ์ด๋ฅผ ๋ช…์‹œํ•˜์ง€ ์•Š๋Š”๋‹ค | ํ›„์†: ํ•œ ์ค„ ๊ฑฐ๋ถ€ ๋˜๋Š” ๋ฌธ์„œ์— "Python ๋Ÿฌ๋„ˆ ๊ธฐ์ค€" ๋ช…์‹œ | +| E2 | `--plan-file` ๋กœ ๊ณ„ํš์„ ์‹ค์€ ์‹คํ–‰์—์„œ `[ESCALATE: PLANNER]` ๋Š” ์žฌ๊ณ„ํš ์—†์ด ์ผ๋ฐ˜ ์ˆ˜์ • ๋ผ์šด๋“œ๋กœ ๋‚ด๋ ค๊ฐ„๋‹ค(`runner.py:1275` ๊ฐ€ `plan_mode` ๋กœ ๊ฒŒ์ดํŠธ). ์ƒŒ๋“œ๋ฐ•์Šค ๊ตฌ๋™์œผ๋กœ ํ™•์ธ(Planner ์œ„์ž„ 0๊ฑด). ์ด ๊ฒŒ์ดํŠธ๋Š” PR ์ด์ „๋ถ€ํ„ฐ ์žˆ๋˜ ๊ฒƒ์ด๊ณ  SKILL.md ์„œ์ˆ ๊ณผ๋„ ๋ชจ์ˆœ๋˜์ง€ ์•Š์ง€๋งŒ, "๊ณ„ํš์„œ๋ฅผ ๊ฐ–๊ณ  ๋„๋Š” ๋ฃจํ”„์ธ๋ฐ ์—์Šค์ปฌ๋ ˆ์ด์…˜์ด ๋ฌด๋ ฅํ™”๋œ๋‹ค"๋Š” ์ ์€ ๋ฌธ์„œ์— ์—†๋‹ค | ๋ฌธ์„œ ๋ณด๊ฐ• ๊ถŒ๊ณ  | +| E3 | ๋ฆฌ๋ทฐ ๊ฒฉ๋ฆฌ ์›ŒํฌํŠธ๋ฆฌ์—๋Š” `.venv` ๊ฐ€ ์—†์–ด, `run_loop.sh` ๋ฅผ ํ•˜์œ„ ํ”„๋กœ์„ธ์Šค๋กœ ๋„์šฐ๋Š” ์Šค์œ„ํŠธ๋Š” `AGENT_PYTHON_BIN` ์—†์ด๋Š” ๋ฐ˜๋“œ์‹œ ์‹คํŒจํ•œ๋‹ค(ยง6-1). ๋˜ํ•œ ๊ทธ ์›ŒํฌํŠธ๋ฆฌ์—์„œ pytest ๋ฅผ ๋Œ๋ฆฌ๋ฉด ๋ฌด์‹œ๋˜๋Š” `.mam/` ์ด ์ƒ๊ธด๋‹ค. **PR #32 ๊ฐ€ ์•„๋‹ˆ๋ผ Issue #21 ํ•ญ๋ชฉ 5 ์šด์˜ ์ชฝ ํ›„์†**์ด๋‹ค | ๋ฆฌ๋ทฐ ๋ธŒ๋ฆฌํ”„์˜ ํ…Œ์ŠคํŠธ ๋ช…๋ น์— `AGENT_PYTHON_BIN=<์ œ์–ด ๋ฃจํŠธ .venv/bin/python>` ๋ฅผ ํฌํ•จํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ๊ณ  | +| E4 | `runner.py:1070` ์˜ `os.path.getmtime(self.plan_file)` ๋Š” ํŒŒ์‹ฑ ์‹œ์ ๊ณผ Phase 1 ์‚ฌ์ด์— ํŒŒ์ผ์ด ์‚ฌ๋ผ์ง€๋ฉด ์˜ˆ์™ธ๋กœ ์ฃฝ๋Š”๋‹ค(๋ฉ”์‹œ์ง€ ์—†๋Š” ํŠธ๋ ˆ์ด์Šค๋ฐฑ). ๊ทนํžˆ ์ข์€ ๊ฒฝํ•ฉ | ์‚ฌ์†Œ | + +## 9. ๊ต์ฐจ๊ฒ€์ฆ ๊ฐ€์ด๋“œ + +```bash +RP=/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260921T150212Z-97d845/1 +PY=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python +git -C "$RP" rev-parse HEAD HEAD^{tree} # 1e8265... / 4fd82a... = ๋ธŒ๋ฆฌํ”„์˜ headยทtree +# --plan-file ์‹คํŒจ ๊ฒฝ๋กœ (์ž„์‹œ ์ œ์–ด ๋ฃจํŠธ์—์„œ๋งŒ) +C=$(mktemp -d); mkdir -p "$C/ctl" "$C/elsewhere/plans"; : > "$C/ctl/empty.md"; echo x > "$C/elsewhere/plans/p.md" +(cd "$C/elsewhere" && PYTHONPATH="$RP/.agents/skills" WORKSPACE_ROOT="$C/ctl" MAM_STATE_JSON='{"herdr_sessions":[]}' \ + "$PY" -m lib_py.loop --creator c --task t --plan-file plans/p.md; echo "rc=$?") # ctl ๊ธฐ์ค€ ๊ฒฝ๋กœ๋กœ ์‹คํŒจ, rc 1 +# ์ˆ˜์ • ์—†์ด๋Š” ์‹คํŒจํ•˜๋Š”์ง€ (์‚ฌ๋ณธ์—์„œ๋งŒ) +S=$(mktemp -d); rsync -a --exclude .venv --exclude .git --exclude .mam --exclude nats-docker "$RP/" "$S/" +for f in .agents/skills/lib_py/loop/runner.py .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \ + .agents/skills/multi-agent-mux-loop/SKILL.md; do git -C "$RP" show 6fc9194:"$f" > "$S/$f"; done +(cd "$S" && PYTHONDONTWRITEBYTECODE=1 "$PY" -m pytest -q -p no:cacheprovider --basetemp=/tmp/bt tests/test_issue25_plan_reuse.py) # 13 failed +# ์‹คํŒจ 2๊ฑด์ด ํ™˜๊ฒฝ ๋ฌธ์ œ์ž„์„ ํ™•์ธ +(cd "$RP" && AGENT_PYTHON_BIN="$PY" PYTHONDONTWRITEBYTECODE=1 "$PY" -m pytest -q -p no:cacheprovider --basetemp=/tmp/bt2 \ + tests/test_loop_cli.py::test_creator_parses_and_reaches_session_check \ + tests/test_issue5_phase3_loop.py::test_p3_02_run_loop_wrapper_delegates_to_python_default) # 2 passed +``` + +[VERDICT: PASS] diff --git a/.agents/reports/planner-reviewer-codex-01/report-08808795.identity.json b/.agents/reports/planner-reviewer-codex-01/report-08808795.identity.json new file mode 100644 index 0000000..e96a37b --- /dev/null +++ b/.agents/reports/planner-reviewer-codex-01/report-08808795.identity.json @@ -0,0 +1,14 @@ +{ + "loop_id": "loop-20260921T150212Z-97d845", + "base_sha": "6fc9194f896eb2e13e123b8ebe7a9089b629764d", + "head_sha": "1e826515cc2a07ea1f8da0ff1259e1ea4dde86fd", + "tree_sha": "4fd82ad1c2682d12c3f5114818f97291af28a032", + "review_commit": "1e826515cc2a07ea1f8da0ff1259e1ea4dde86fd", + "reviewer": "planner-reviewer-codex-01", + "job_id": "08808795", + "verdict": "PASS", + "transport": "herdr", + "evidence": "review-tree", + "isolation": "worktree", + "verified_at": "2026-09-21T15:25:44Z" +} \ No newline at end of file diff --git a/.agents/reports/planner-reviewer-codex-01/report-08808795.md b/.agents/reports/planner-reviewer-codex-01/report-08808795.md new file mode 100644 index 0000000..9dd95b7 --- /dev/null +++ b/.agents/reports/planner-reviewer-codex-01/report-08808795.md @@ -0,0 +1,32 @@ +# PR #32 independent review + +Reviewer: planner-reviewer-codex-01. Job: 08808795. + +Reviewed the exact detached review-identity path specified by the brief: +`/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260921T150212Z-97d845/1`, at `1e826515`, for the five commits after base `6fc9194`. No checkout, source file, session, monitor, or real registry state was modified. + +## Findings + +The explicit plan-reuse change is coherent and closes the implicit-load defect. + +- With neither `--plan` nor `--plan-file`, Python self-planning logs that no plan is loaded and does not glob `.agents/reports//plan-*.md`. The legacy shell runner does the same. `resolve_planner_session` may still identify a planner for other loop bookkeeping, but it no longer supplies a plan through that path. +- `--plan-file` is parsed by both wrappers. Python resolves relative paths against the control root, preserves absolute and `~` paths, requires a readable regular file with non-whitespace content, and rejects bad inputs before registering jobs. It is mutually exclusive with `--plan`; `--planner` still requires `--plan`; review-only accepts a valid file but warns and ignores it because no implementation prompt is produced. The legacy shell runner rejects the option explicitly when `MAM_LOOP_PY=0`, before the loop lock. +- `_plan_text()` is the shared check for explicit files, initial planner reports, discussion refinements, and escalation refinements. An empty initial planner report stops the run rather than silently falling back to self-planning. An empty refine report preserves the existing plan. Correction prompts therefore never claim to carry an empty plan. +- Checkpoint `current_plan` is not a plan-discovery source. It is only recorded for the current run; a later no-option run starts planless, and a named `--plan-file` is read explicitly. Promotion still writes the plan only for an actual `--plan` run. +- Relative plan paths with spaces, missing paths, directories, empty/whitespace files, unreadable files, `--plan` conflicts, review-only handling, and legacy-shell rejection are covered by P1โ€“P11. The tests run in temporary control roots and use isolated reports; they do not write the real `.mam` or `.agents/reports`. +- The loop skillโ€™s old implicit-reuse wording is removed. The CLI usage, workflow examples, option table, and explicit-reuse description agree. No `.claude/skills` deployment copy exists in this checkout, so there is no second copy to synchronize. + +The known release prerequisite remains outside this PR: the orchestrator must perform the 4.7.1 lockstep version bump and release entry before merge. + +## Verification + +All tests used `PYTHONDONTWRITEBYTECODE=1`, the interpreter from the brief, pytest cache disabled, and a basetemp outside the review path. + +- `tests/test_issue25_plan_reuse.py`: **17 passed** in 1.70 s. +- `tests/test_issue25_plan_reuse.py`, `tests/test_loop_cli.py`, `tests/test_issue5_phase3_loop.py`, and `tests/test_version_consistency.py`: **39 passed, 2 failed** in 49.25 s when run exactly from the identity path. Both failures occurred before their assertions because `run_loop.sh` could not find `.venv` in the detached snapshot and fell back to system Python, which lacked `paho` (`RuntimeError: Failed to eager load lib_py.loop.autocomplete: No module named 'paho'`). This is a snapshot/setup issue, not a change in the plan-reuse code. +- Re-running the two failed compatibility cases with `AGENT_PYTHON_BIN` pinned to the briefโ€™s main interpreter: **2 passed** in 0.32 s. This reaches and verifies the expected session-registration checks. +- `git diff --check 6fc9194...HEAD`: passed. + +The focused plan tests passed without modification, and the only broader failures disappeared when the required interpreter was explicitly supplied. I found no additional merge-blocking defect beyond the known version gate. + +[VERDICT: PASS] diff --git a/.agents/reports/reviewer-agy-01/report-41f07cde.identity.json b/.agents/reports/reviewer-agy-01/report-41f07cde.identity.json new file mode 100644 index 0000000..20df469 --- /dev/null +++ b/.agents/reports/reviewer-agy-01/report-41f07cde.identity.json @@ -0,0 +1,14 @@ +{ + "loop_id": "loop-20260921T150212Z-97d845", + "base_sha": "6fc9194f896eb2e13e123b8ebe7a9089b629764d", + "head_sha": "1e826515cc2a07ea1f8da0ff1259e1ea4dde86fd", + "tree_sha": "4fd82ad1c2682d12c3f5114818f97291af28a032", + "review_commit": "1e826515cc2a07ea1f8da0ff1259e1ea4dde86fd", + "reviewer": "reviewer-agy-01", + "job_id": "41f07cde", + "verdict": "PASS", + "transport": "herdr", + "evidence": "review-tree", + "isolation": "worktree", + "verified_at": "2026-09-21T15:25:44Z" +} \ No newline at end of file diff --git a/.agents/reports/reviewer-agy-01/report-41f07cde.md b/.agents/reports/reviewer-agy-01/report-41f07cde.md new file mode 100644 index 0000000..7cfd0e7 --- /dev/null +++ b/.agents/reports/reviewer-agy-01/report-41f07cde.md @@ -0,0 +1,113 @@ +# Pre-Merge Independent Review Report: PR #32 (`fix/issue-25`) + +- **Job ID**: `41f07cde` +- **Target Agent**: `agy` (Session: `herdr:reviewer-agy-01`) +- **Role**: Reviewer +- **Target Worktree Context**: + - **Loop ID**: `loop-20260921T150212Z-97d845` + - **Base Commit**: `6fc9194f896eb2e13e123b8ebe7a9089b629764d` (v4.7.0) + - **Head Commit**: `1e826515cc2a07ea1f8da0ff1259e1ea4dde86fd` + - **Tree SHA**: `4fd82ad1c2682d12c3f5114818f97291af28a032` + - **Verification Path**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260921T150212Z-97d845/1` + +--- + +## 1. Executive Summary + +PR #32 resolves Issue #25 ("์Šน๊ฒฉ๋œ ๊ณ„ํš์„œ ์žฌ์‚ฌ์šฉ์„ --plan-file ๋กœ๋งŒ ํ—ˆ์šฉ โ€” ์•”๋ฌต์  ์ž๋™ ๋กœ๋“œ ์ œ๊ฑฐ") across 5 commits and 4 files (+486 / โˆ’31): +- `1e82651` fix(issue-25): a relative --plan-file is taken against the control root, as its usage line says +- `5fa5923` docs(issue-25): the loop skill documents --plan-file and the empty-report handling +- `a334083` test(issue-25): plan reuse is explicit only and empty plan reports are not silent (P1-P11) +- `e551e3b` fix(issue-25): one plan-text check for every load; an empty planner report stops the loop, an empty refine keeps the plan +- `a28711c` fix(issue-25): reuse a promoted plan only with --plan-file; drop the implicit newest-plan load + +All checklist criteria have been verified in the isolated verification path. All 17 new tests in `test_issue25_plan_reuse.py` and all 115 baseline regression tests passed with zero regressions. + +--- + +## 2. Detailed Verification Against Checklist Items + +### (1) Removal of Implicit Plan Loading +- **Python Runner (`lib_py/loop/runner.py`)**: + - Removed `import glob` and the directory scan `glob.glob(str(self.workspace_root / ".agents" / "reports" / planner_session / "plan-*.md"))`. + - In Phase 1 Self-Planning mode, if `--plan-file` was not supplied, `self.current_plan` remains empty and the runner logs: `"No plan loaded; the Creator plans by itself (reuse one with --plan-file )."`. +- **Legacy Shell Runner (`scripts/run_loop.sh`)**: + - Removed `EXISTING_PLAN_FILE=$(ls -t ".agents/reports/$PLANNER_SESSION"/plan-*.md 2>/dev/null | head -n 1 || true)`. + - In Self-Planning mode, logs `"No plan loaded; the Creator plans by itself."`. +- **Parity & Search Check**: + - Test `test_p8_neither_loop_implementation_globs_for_a_plan_any_more` confirms the string pattern `plan-*.md` does not appear anywhere in `runner.py` or `run_loop.sh`. + - Automatic planner resolution (`resolve_planner_session`) is not called or consulted to retrieve stored plans in Phase 1 self-planning mode. + +### (2) `--plan-file ` CLI Option Semantics +- **Control Root Resolution**: + - Relative paths are anchored against `self.workspace_root` via `(self.workspace_root / Path(self.plan_file).expanduser()).resolve()`. This guarantees identical path resolution whether the command is run from the repository root, a subdirectory, or an isolated worktree (`test_p3c`). + - Absolute paths and user home expansion (`~`) remain untouched and work as expected (`test_p3d`). +- **Fail-Closed Early Validation**: + - Evaluated in `parse_args()` before any agent delegation or job submission occurs (`test_p3`, `test_p3b`). + - If the path is non-existent, a directory, unreadable, or contains whitespace-only text, the runner logs an error naming the resolved path and immediately exits with rc=1. +- **Mutual Exclusion & Option Combinations**: + - Combining `--plan-file` with `--plan` exits 1 with a clear mutual exclusion error (`test_p4`). + - Specifying `--plan-file` without an argument exits 1 (`test_p4`). + - When combined with `--review-only`, the runner logs a warning and ignores the plan (`self.current_plan = ""`), because Phase 2 implementation is skipped (`test_p5`, `test_p5b`). +- **Legacy Shell Rejection**: + - When invoked with `MAM_LOOP_PY=0`, `run_loop.sh` explicitly detects `--plan-file` and terminates with code 1 before the loop marker/guard is created (`test_p7b`). + +### (3) Centralized Plan Text Validation (`e551e3b`) +- **Shared Validation Method**: + - `LoopRunner._plan_text(path: Path) -> Optional[str]` enforces: + 1. Regular file check (`path.is_file()`). + 2. Safe decoding with error replacement (`read_text(encoding="utf-8", errors="replace")`). + 3. Stripped text check (`text if text.strip() else None`). +- **Initial Plan Failure**: + - In `--plan` mode, if the initial planner report is empty or whitespace-only, the runner logs `the planner's report has no plan text: ` and exits with code 1 (`test_p9`). This eliminates the regression where an empty plan silently demoted the run to self-planning. +- **Refinement Retention (Debate & Review Escalation)**: + - In Phase 1 interactive debate (`--plan-talk`) and Phase 3 review escalation (`[ESCALATE: PLANNER]`), an empty refinement report logs a warning and preserves the previously valid plan (`test_p10`, `test_p11`). + - This prevents prompt corruption in `correction_prompt = f"๊ฐฑ์‹ ๋œ ๋‹ค์Œ ๊ณ„ํš์„œ์— ์ž…๊ฐํ•˜์—ฌ ... ๊ณ„ํš์„œ:\n{self.current_plan}"`. + +### (4) Checkpoint Integrity +- `LoopRunner` persists `current_plan` to `.mam/loop-checkpoint.json` for diagnostic and monitoring visibility during active loops. +- `LoopRunner` never reads `.mam/loop-checkpoint.json` at startup to populate `self.current_plan`. `self.current_plan` is always initialized cleanly (`self.current_plan = ""`). +- Existing checkpoint files in the workspace (such as unrelated historical plan text from prior issues) cannot contaminate new loop runs. + +### (5) Baseline Behavior & Creator Prompts +- In self-planning mode without `--plan-file`, the initial Creator prompt retains the exact contract: + `"๊ณ„ํš์„œ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ์ž‘์—…์ž(Creator)์˜ ํŒ๋‹จํ•˜์— ์Šค์Šค๋กœ ๊ตฌํ˜„ ๊ณ„ํš ๋ฐ ์„ค๊ณ„๋ฅผ ์ˆ˜๋ฆฝํ•œ ๋’ค, ์ด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ๋‹ค์Œ ์ž‘์—… ๋ชฉํ‘œ๋ฅผ ์™„์„ฑํ•ด์ฃผ์„ธ์š”. ์ž‘์—… ๋ชฉํ‘œ: {self.task}"`. +- With `--plan-file `, the prompt receives: + `"์ด๋ฏธ ์ˆ˜๋ฆฝ๋œ ๋‹ค์Œ ๊ณ„ํš์„œ์— ์ž…๊ฐํ•˜์—ฌ ์ž‘์—…์ž์˜ ํŒ๋‹จํ•˜์— ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์ž‘์—… ๋ชฉํ‘œ๋ฅผ ์™„์„ฑํ•ด์ฃผ์„ธ์š”. ๊ณ„ํš์„œ:\n{self.current_plan}\n\n์ž‘์—… ๋ชฉํ‘œ: {self.task}"`. +- Subsequent review corrective rounds strictly provide reviewer feedback without repeating the initial plan (`test_p2`). +- Promotion of newly planned tasks via `--plan` to `.agents/reports//plan-.md` remains operational and protected (`test_p6`). + +### (6) Test Suite Execution & Isolation (P1โ€“P11) +- All 17 test cases in `tests/test_issue25_plan_reuse.py` operate strictly within isolated `tmp_path` fixtures with stubbed delegations. +- Real `.mam/jobs`, live daemon processes, and global agent sessions remain completely untouched. +- Execution timing: 17 tests passed in 2.02 seconds. + +### (7) Documentation Integrity +- `.agents/skills/multi-agent-mux-loop/SKILL.md` was thoroughly updated: + - Completely purged the stale statement ("๊ธฐ์กด์— ์Šน๊ฒฉ๋œ ๊ณ„ํš์„œ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ๋กœ๋“œ"). + - Updated Sequence Diagram and Phase 1 documentation to reflect explicit `--plan-file` reuse. + - Added a dedicated section: `### ๊ณ„ํš์„œ๊ฐ€ ๋น„์–ด ์žˆ์„ ๋•Œ (Empty plan reports)`. + - Updated CLI Option mapping table and Workflow examples. +- No dangling references to the old implicit loading mechanism exist across the repository. + +### (8) Versioning & Merge Readiness +- Version lockstep bump to `v4.7.1` is reserved for the orchestrator release commit in accordance with release procedures. +- Formatting and git diff checks (`git diff 6fc9194...HEAD --check`) reported 0 issues. +- No merge blockers, regression risks, or side-effects detected. + +--- + +## 3. Test Execution Summary + +| Test Suite | Result | Duration | Notes | +| :--- | :--- | :--- | :--- | +| `tests/test_issue25_plan_reuse.py` | **17 / 17 PASS** | 2.02s | P1โ€“P11 test matrix covering all reuse/empty-plan conditions | +| `tests/test_loop_cli.py` | **9 / 9 PASS** | 38.97s | CLI interface and argument validation regressions | +| `tests/test_issue5_phase2a_loop.py` | **91 / 91 PASS** | 2.91s | Core loop state machine, dispatching, and verdict handling | +| `tests/test_issue5_phase3_loop.py` | **12 / 12 PASS** | 0.54s | Runner orchestration, timeouts, and signal handling | +| `tests/test_version_consistency.py` | **3 / 3 PASS** | 0.21s | Version consistency checks | +| `git diff 6fc9194...HEAD --check` | **0 errors** | 0.05s | Zero trailing whitespace / formatting errors | + +--- + +[VERDICT: PASS] diff --git a/.agents/reports/reviewer-grok-01/report-06fb4a9e.identity.json b/.agents/reports/reviewer-grok-01/report-06fb4a9e.identity.json new file mode 100644 index 0000000..ff56004 --- /dev/null +++ b/.agents/reports/reviewer-grok-01/report-06fb4a9e.identity.json @@ -0,0 +1,14 @@ +{ + "loop_id": "loop-20260921T150212Z-97d845", + "base_sha": "6fc9194f896eb2e13e123b8ebe7a9089b629764d", + "head_sha": "1e826515cc2a07ea1f8da0ff1259e1ea4dde86fd", + "tree_sha": "4fd82ad1c2682d12c3f5114818f97291af28a032", + "review_commit": "1e826515cc2a07ea1f8da0ff1259e1ea4dde86fd", + "reviewer": "reviewer-grok-01", + "job_id": "06fb4a9e", + "verdict": "PASS", + "transport": "herdr", + "evidence": "review-tree", + "isolation": "worktree", + "verified_at": "2026-09-21T15:25:44Z" +} \ No newline at end of file diff --git a/.agents/reports/reviewer-grok-01/report-06fb4a9e.md b/.agents/reports/reviewer-grok-01/report-06fb4a9e.md new file mode 100644 index 0000000..24a1456 --- /dev/null +++ b/.agents/reports/reviewer-grok-01/report-06fb4a9e.md @@ -0,0 +1,59 @@ +# ๐Ÿ” Review Report โ€” Job 06fb4a9e (PR #32 @ `1e82651`) + +- **Reviewer**: `reviewer-grok-01` ยท 2026-09-22 +- **๊ฒ€์ฆ ๊ฒฝ๋กœ**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260921T150212Z-97d845/1` +- **identity**: base `6fc9194` head `1e82651` tree `4fd82ad` (์ผ์น˜) +- **์†Œ์Šค ์›ŒํฌํŠธ๋ฆฌยท๋ณธ ์ฒดํฌ์•„์›ƒ**: ์ฝ๊ธฐ ์ „์šฉ. ๋ณธ ์ฒดํฌ์•„์›ƒ `main` `6fc9194` ์œ ์ง€. + +`git diff --check 6fc9194...HEAD` ํด๋ฆฐ. 5์ปค๋ฐ‹. + +## (1) ์•”๋ฌต์  ๋กœ๋“œ ์ œ๊ฑฐ + +Python: Phase 1 else ๊ฐ€ glob/`plan-*.md` ๋ฅผ ์“ฐ์ง€ ์•Š์Œ (`runner.py:1064-1073`). `import glob` ์‚ญ์ œ. `resolve_planner_session` (`:990`) ์€ ์„ธ์…˜ ์ด๋ฆ„๋งŒ ๋กœ๊ทธํ•˜๊ณ  ํŒŒ์ผ์„ ์ฝ์ง€ ์•Š์Œ. `--planner` ๋Š” `--plan` ์—†์ด exit 1 (`:244-248`). + +๋ ˆ๊ฑฐ์‹œ ์…ธ (`MAM_LOOP_PY=0`): `ls -t .agents/reports/.../plan-*.md` ์ œ๊ฑฐ (`run_loop.sh` Phase 1 else). `plan-*.md` ๋ฌธ์ž์—ด์ด ๋‘ ๊ตฌํ˜„์— ์—†์Œ (`test_p8`). + +## (2) `--plan-file` + +์ƒ๋Œ€๊ฒฝ๋กœ๋Š” `workspace_root / path` (`runner.py:264`); POSIX ์—์„œ ์ ˆ๋Œ€๊ฒฝ๋กœ๋Š” `/` ๋กœ ์น˜ํ™˜๋˜์–ด ๊ทธ๋Œ€๋กœ. `~` ๋Š” `expanduser`. ์—†๋Š”/๋นˆ/๊ณต๋ฐฑ๋ฟ/๋””๋ ‰ํ„ฐ๋ฆฌ/์ฝ๊ธฐ ์‹คํŒจ โ†’ `_plan_text` None โ†’ parse_args `sys.exit(1)`, ์žก ์—†์Œ. `--plan` ๊ณผ ์ƒํ˜ธ ๋ฐฐํƒ€. `--review-only` ๋Š” ํŒŒ์ผ ๊ฒ€์‚ฌ ํ›„ ๊ฒฝ๊ณ ยท๋ฌด์‹œ(`current_plan` ๋น„์›€). `--planner` ๋Š” `--plan` ํ•„์ˆ˜๋ผ `--plan-file` ๊ณผ ํ•จ๊ป˜๋ฉด ๋จผ์ € `--planner` ๊ทœ์น™์— ๊ฑธ๋ฆฐ๋‹ค. + +๋ ˆ๊ฑฐ์‹œ ์…ธ์€ `--plan-file` ์„ Python ์ „์šฉ์œผ๋กœ **๋ช…์‹œ ๊ฑฐ๋ถ€** exit 1, ๋ฃจํ”„ ๋ฝ ์ „ (`run_loop.sh:127-134`, `test_p7b`). ๊ณต๋ฐฑ ๊ฒฝ๋กœ๋Š” pathlib ๊ฐ€ ์ฒ˜๋ฆฌ; ์ „์šฉ ํ…Œ์ŠคํŠธ๋Š” ์—†์Œ(๋น„์ฐจ๋‹จ). + +## (3) `_plan_text` (e551e3b) + +ํ•œ ํ•จ์ˆ˜ (`:304-316`)๊ฐ€ `--plan-file`, ์ดˆ๊ธฐ planner ๋ฆฌํฌํŠธ, ๋…ผ์˜ refine, ์—์Šค์ปฌ๋ ˆ์ด์…˜ refine ์— ์‚ฌ์šฉ. ๋นˆ ์ดˆ๊ธฐ ๊ณ„ํš โ†’ `return 1`, Worker ์—†์Œ (`test_p9`). ๋นˆ refine โ†’ ๊ฒฝ๊ณ ยท์ด์ „ ๊ณ„ํš ์œ ์ง€ (`test_p10`, `test_p11` ESCALATE). ์ˆ˜์ • ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋นˆ ๊ณ„ํš์„ ์‹ฃ์ง€ ์•Š์Œ. + +## (4) ์ฒดํฌํฌ์ธํŠธ + +`current_plan` ์€ ๊ธฐ๋ก๋งŒ (`_record_checkpoint`). `load_checkpoint` ๋ฅผ runner ๊ฐ€ ํ˜ธ์ถœํ•˜์ง€ ์•Š์Œ. ์ƒˆ ์‹คํ–‰์€ `current_plan=""` ์—์„œ ์‹œ์ž‘. ๋ณธ ์ฒดํฌ์•„์›ƒ `.mam/loop-checkpoint.json` ์˜ ์˜› ๊ณ„ํš์€ ์šฐํšŒ๊ฐ€ ์•„๋‹˜. + +## (5) ์˜ต์…˜ ์—†์Œ + +์˜๋„๋œ ๋ณ€ํ™”๋งŒ: ์ž๊ธฐ๊ณ„ํš ํ”„๋กฌํ”„ํŠธ. ์Šน๊ฒฉ์€ `--plan` ์„ฑ๊ณต ์‹œ `plan-{id}.md` ๊ธฐ๋ก ์œ ์ง€ (`test_p6`). ๊ธฐ์กด ํ…Œ์ŠคํŠธ๋Š” ๊ฒ€์ฆ ๊ฒฝ๋กœ์—์„œ ์‹คํ–‰. + +## (6) ํ…Œ์ŠคํŠธ P1โ€“P11 + +์ „๋ถ€ `tmp_path`. ๊ฒ€์ฆ ๊ฒฝ๋กœ: + +``` +PYTHONDONTWRITEBYTECODE=1 -m pytest -p no:cacheprovider --basetemp=/tmp/mam-review-basetemp-... + tests/test_issue25_plan_reuse.py +``` + +**17 passed** (P1โ€“P11 + p3b/c/d, p5b, p7/p7b, p8). ์ˆ˜์ • ์ „ P1 ์€ ์˜› glob ์„ ๋‹จ์–ธํ•˜๋ฏ€๋กœ ์‹คํŒจํ–ˆ์„ ํ…Œ์ŠคํŠธ. + +๊ฐ™์€ ์ธํ„ฐํ”„๋ฆฌํ„ฐ๋กœ `test_loop_cli`ยท`test_issue5_phase3`ยท`test_issue21_worktree_prereqs`ยท`test_version_consistency` ํฌํ•จ ์‹œ ๋ž˜ํผ ์„œ๋ธŒํ”„๋กœ์„ธ์Šค๊ฐ€ ๊ฒ€์ฆ ํŠธ๋ฆฌ์— `.venv` ๊ฐ€ ์—†์–ด `paho` ๋ฅผ ๋ชป ์ฐพ๋Š” ์‹คํŒจ 2๊ฑด์ด ๋‚ฌ๊ณ , `DELEGATE_JOB_PYTHON` ์„ ์ปจํŠธ๋กค `.venv` ๋กœ ์ฃผ๋ฉด **19 passed**. ์ œํ’ˆ ํšŒ๊ท€๊ฐ€ ์•„๋‹ˆ๋ผ ์Šค๋ƒ…์ƒท์— venv ๊ฐ€ ์—†๋Š” ํ™˜๊ฒฝ. + +## (7) ๋ฌธ์„œ + +SKILL.md ์—์„œ "๊ธฐ์กด์— ์Šน๊ฒฉ๋œ ๊ณ„ํš์„œ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ๋กœ๋“œ" ์ž”์กด ์—†์Œ. usageยทCLI ํ‘œยทWorkflow ์˜ˆ์‹œ๊ฐ€ `--plan-file` ๊ณผ ์ผ์น˜. `.claude/skills` ๋Š” ์ด ์Šค๋ƒ…์ƒท์— ์—†์Œ. + +## (8) ๋ฝ์Šคํ… + +`MAM_VERSION=4.7.0`. 4.7.1 bump ๋Š” ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ์˜ˆ์ •. ๋ณ‘ํ•ฉ ์ฐจ๋‹จ ์•„๋‹˜. + +## ์ข…ํ•ฉ + +์•”๋ฌต์  ์ตœ์‹  ๊ณ„ํš ๋กœ๋“œ๊ฐ€ Pythonยท๋ ˆ๊ฑฐ์‹œ ์–‘์ชฝ์—์„œ ์ œ๊ฑฐ๋๊ณ , ์žฌ์‚ฌ์šฉ์€ `--plan-file` ๋ฟ์ด๋‹ค. ๋นˆ ๋ฆฌํฌํŠธ๋Š” ์กฐ์šฉํžˆ ๊ณ„ํš์„ ์ง€์šฐ์ง€ ์•Š๋Š”๋‹ค. ์„ค๊ณ„ ์žฌ์ž‘์—… ๋ถˆํ•„์š”. + +[VERDICT: PASS] diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index d099771..1b0283a 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -29,7 +29,7 @@ export WORKSPACE_ROOT # Framework semantic version. Single runtime source of truth; kept in lockstep # with VERSIONS.md and the 8 SKILL.md frontmatters by tests/test_version_consistency.py. # NOTE: unlike other MAM_* variables this one is intentionally NOT env-overridable. -MAM_VERSION="4.7.0" +MAM_VERSION="4.7.1" export MAM_VERSION AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}" diff --git a/.agents/skills/lib_py/loop/eager.py b/.agents/skills/lib_py/loop/eager.py index d746479..ba73b19 100644 --- a/.agents/skills/lib_py/loop/eager.py +++ b/.agents/skills/lib_py/loop/eager.py @@ -13,7 +13,7 @@ import sys from typing import List, Sequence # Framework version locked with framework constants -FRAMEWORK_VERSION = "4.7.0" +FRAMEWORK_VERSION = "4.7.1" CORE_MODULES: Sequence[str] = ( # Standard library modules diff --git a/.agents/skills/lib_py/loop/runner.py b/.agents/skills/lib_py/loop/runner.py index 406eabc..c5d7726 100644 --- a/.agents/skills/lib_py/loop/runner.py +++ b/.agents/skills/lib_py/loop/runner.py @@ -9,7 +9,6 @@ from __future__ import annotations import argparse import atexit -import glob import json import os import re @@ -98,6 +97,7 @@ class LoopRunner: self.creator: str = "" self.task: str = "" self.plan_mode: bool = False + self.plan_file: Optional[str] = None self.planner_override: Optional[str] = None self.plan_talk_turns: int = 1 self.reviewer_list: Optional[str] = None @@ -116,6 +116,7 @@ class LoopRunner: " --plan Enable Planner agent intervention & design phase", " --planner Planner session (requires --plan; default: auto-resolve)", " --plan-talk N Planner-Creator discussion limit turns (default: 1)", + " --plan-file Reuse an existing plan file, relative to the control root (not with --plan)", " --reviewer \"A,B\" Targeted reviewer session name list (comma-separated)", " --all-reviewer Enforce PASS verdict from all active reviewer sessions", " --max-loop N Max execution-review corrective loop runs (default: 3)", @@ -162,6 +163,12 @@ class LoopRunner: sys.exit(1) self.planner_override = args[i + 1] i += 2 + elif arg == "--plan-file": + if i + 1 >= len(args): + self.print_usage() + sys.exit(1) + self.plan_file = args[i + 1] + i += 2 elif arg == "--plan-talk": if i + 1 >= len(args) or not re.match(r"^[0-9]+$", args[i + 1]): print("ERROR: --plan-talk requires a positive integer.", file=sys.stderr) @@ -240,6 +247,33 @@ class LoopRunner: print(f' run_loop.sh --creator --plan --planner --task "..."', file=sys.stderr) sys.exit(1) + # Issue #25: a promoted plan is reused only when it is named (D2). The + # file is read here, before a single job exists, so a bad path cannot + # surface halfway through a run (D3). + if self.plan_file is not None: + if self.plan_mode: + print("ERROR: --plan-file cannot be combined with --plan.", file=sys.stderr) + print("Use --plan to have the Planner write a new plan, or --plan-file to reuse one:", file=sys.stderr) + print(' run_loop.sh --creator --plan-file --task "..."', file=sys.stderr) + sys.exit(1) + # A relative path is taken against the control root, as the usage + # line says and as jobs_dir already does (Issue #21 R2): the runner + # never chdirs to workspace_root, so resolving against the caller's + # cwd would look somewhere else entirely - and could find a + # same-named file there. `/` keeps an absolute path as it is. + resolved = (self.workspace_root / Path(self.plan_file).expanduser()).resolve() + text = self._plan_text(resolved) + if text is None: + log_error(f"--plan-file {resolved}: not a readable file with plan text in it.") + sys.exit(1) + self.plan_file = str(resolved) + if self.review_only: + # D6: --review-only delegates no implementation job, so there is + # nothing for a plan to steer. + log_warn(f"--plan-file is ignored with --review-only: {resolved}") + else: + self.current_plan = text + if self.all_reviewers and self.reviewer_list: log_warn(f"--all-reviewer takes precedence; ignoring --reviewer list ('{self.reviewer_list}').") @@ -266,6 +300,21 @@ class LoopRunner: if not self._context_active() and any(a in self.raw_argv for a in ("--review-isolation", "--remote-review")): log_warn("--review-isolation/--remote-review only apply with --worktree or --base; ignored.") + @staticmethod + def _plan_text(path: Path) -> Optional[str]: + """Issue #25 (D3): the one check every plan load shares - a plan is a + readable regular file that has text in it. None means 'no plan here'; + each caller decides what that means (D13 stops the run, D14 keeps the + plan it already has). A file that cannot be read is not told apart from + an empty one: either way the loop would carry no plan text.""" + try: + if not path.is_file(): + return None + text = path.read_text(encoding="utf-8", errors="replace") + except OSError: + return None + return text if text.strip() else None + @staticmethod def _git(cwd: Path, *args: str) -> subprocess.CompletedProcess: return subprocess.run(["git", "-C", str(cwd), *args], capture_output=True, text=True) @@ -973,7 +1022,13 @@ class LoopRunner: if not p_file or not p_file.exists(): log_error("Final plan file not found.") return 1 - self.current_plan = p_file.read_text(encoding="utf-8", errors="replace") + plan_text = self._plan_text(p_file) + if plan_text is None: + # D13: the run paid for a planning phase and got nothing back. + # Falling through would silently demote it to self-planning. + log_error(f"the planner's report has no plan text: {p_file}") + return 1 + self.current_plan = plan_text # Interactive turns creator_type = self.resolve_agent_type(self.creator, state) @@ -996,17 +1051,26 @@ class LoopRunner: return 1 ref_file = self.find_report(ref_jid) - if ref_file and ref_file.exists(): - self.current_plan = ref_file.read_text(encoding="utf-8", errors="replace") + refined = self._plan_text(ref_file) if ref_file else None + if refined is None: + # D14: overwriting with an empty report loses a plan that is + # still good, and Phase 2 would quietly switch prompts. + log_warn(f"the refine report has no plan text (job {ref_jid}); keeping the previous plan.") + else: + self.current_plan = refined log_success("Interactive planning completed. Plan finalized.") else: log_info("=== Phase 1: Self-Planning Mode (Direct Execution) ===") - if planner_session: - plan_files = sorted(glob.glob(str(self.workspace_root / ".agents" / "reports" / planner_session / "plan-*.md")), key=os.path.getmtime, reverse=True) - if plan_files: - log_info(f"Found existing promoted plan at '{plan_files[0]}'. Loading plan...") - self.current_plan = Path(plan_files[0]).read_text(encoding="utf-8", errors="replace") + # Issue #25 (D1): no plan is loaded unless one was named. This used + # to take the newest promoted plan in the planner's report dir, + # keyed on mtime (checkout order) and on a planner session that says + # nothing about this task - so it injected other issues' plans. + if self.current_plan: + mtime = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(os.path.getmtime(self.plan_file))) + log_info(f"Using the plan given with --plan-file: {self.plan_file} (modified {mtime} UTC)") + else: + log_info("No plan loaded; the Creator plans by itself (reuse one with --plan-file ).") # ======================================================================= # PHASE 2: IMPLEMENTATION (CREATION) @@ -1217,8 +1281,14 @@ class LoopRunner: log_error("Failed to register plan refinement job.") return 1 ref_file = self.find_report(ref_jid) - if ref_file and ref_file.exists(): - self.current_plan = ref_file.read_text(encoding="utf-8", errors="replace") + refined = self._plan_text(ref_file) if ref_file else None + if refined is None: + log_warn(f"the refine report has no plan text (job {ref_jid}); keeping the previous plan.") + else: + self.current_plan = refined + # D13 stops a plan-mode run whose initial plan is empty and D14 + # never overwrites with an empty one, so current_plan is not + # empty here: this prompt cannot claim a plan it does not carry. correction_prompt = f"๊ฐฑ์‹ ๋œ ๋‹ค์Œ ๊ณ„ํš์„œ์— ์ž…๊ฐํ•˜์—ฌ ์ง€์ ๋œ ์˜ค๋ฅ˜๋“ค์„ ์ˆ˜์ •ํ•˜๊ณ  ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ๊ตฌํ˜„ํ•ด์ฃผ์„ธ์š”. ๊ณ„ํš์„œ:\n{self.current_plan}\nํ”ผ๋“œ๋ฐฑ ์ƒ์„ธ:\n{feedback_aggregate}" else: log_info("Applying straight bugfixes based on reviewer feedback...") diff --git a/.agents/skills/multi-agent-mux-create/SKILL.md b/.agents/skills/multi-agent-mux-create/SKILL.md index b63c35f..e16d8f5 100644 --- a/.agents/skills/multi-agent-mux-create/SKILL.md +++ b/.agents/skills/multi-agent-mux-create/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-create description: "Create a new agent session (claude, antigravity/agy) in a dedicated herdr session for context-preserving long-running work. Always creates a herdr session โ€” never backgrounds with nohup/disown. Writes the new session to .mam/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace." -version: 4.7.0 +version: 4.7.1 author: godopu license: MIT platforms: [linux, macos] diff --git a/.agents/skills/multi-agent-mux-delegate-job/SKILL.md b/.agents/skills/multi-agent-mux-delegate-job/SKILL.md index 2da2678..52ef4b3 100644 --- a/.agents/skills/multi-agent-mux-delegate-job/SKILL.md +++ b/.agents/skills/multi-agent-mux-delegate-job/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-delegate-job description: "Delegate a unit of work to any autonomous agent (claude-code, hermes, agy, grok-build, codex, or a human) and observe it asynchronously over an MQTT event channel. Supported roles include orchestrator, worker, and reviewer." -version: 4.7.0 +version: 4.7.1 author: godopu license: MIT platforms: [linux, macos, windows] diff --git a/.agents/skills/multi-agent-mux-loop/SKILL.md b/.agents/skills/multi-agent-mux-loop/SKILL.md index a9fbb90..7d7ed24 100644 --- a/.agents/skills/multi-agent-mux-loop/SKILL.md +++ b/.agents/skills/multi-agent-mux-loop/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-loop description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached." -version: 4.7.0 +version: 4.7.1 author: godopu license: MIT platforms: [linux, macos] @@ -27,7 +27,8 @@ metadata: Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. It supports: - **Collaborative Planning** (`--plan`, optional `--planner `, and `--plan-talk N`): Planner designs the solution, Creator challenges the plan for N turns to resolve edge cases, then implementation starts. `--planner` selects the planner session; omit it to auto-resolve the first running planner. -- **Creator Self-Planning & Development** (default without `--plan`): Planner ์—์ด์ „ํŠธ์—๊ฒŒ ๊ณ„ํš ์ž‘์„ฑ์„ ์œ„์ž„ํ•˜์ง€ ์•Š๊ณ , ๊ธฐ์กด์— ์Šน๊ฒฉ๋œ ๊ณ„ํš์„œ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ๋กœ๋“œํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉฐ, ๊ณ„ํš์„œ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ์ž‘์—…์ž(Creator: developer/writer)๊ฐ€ ์Šค์Šค๋กœ ๊ตฌํ˜„ ๊ณ„ํš ๋ฐ ์„ค๊ณ„ ์ˆ˜๋ฆฝ์„ ํฌํ•จํ•œ ๊ฐœ๋ฐœ ์ „ ๊ณผ์ •์„ ์ง์ ‘ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. +- **Creator Self-Planning & Development** (default without `--plan`): Planner ์—์ด์ „ํŠธ์—๊ฒŒ ๊ณ„ํš ์ž‘์„ฑ์„ ์œ„์ž„ํ•˜์ง€ ์•Š๊ณ , ์ž‘์—…์ž(Creator: developer/writer)๊ฐ€ ์Šค์Šค๋กœ ๊ตฌํ˜„ ๊ณ„ํš ๋ฐ ์„ค๊ณ„ ์ˆ˜๋ฆฝ์„ ํฌํ•จํ•œ ๊ฐœ๋ฐœ ์ „ ๊ณผ์ •์„ ์ง์ ‘ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. **์–ด๋–ค ๊ณ„ํš์„œ๋„ ์ž๋™์œผ๋กœ ๋กœ๋“œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.** +- **Explicit Plan Reuse** (`--plan-file `): ๊ธฐ์กด ๊ณ„ํš์„œ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋Š” ์œ ์ผํ•œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค. ๊ฒฝ๋กœ๋Š” ์ œ์–ด ๋ฃจํŠธ ๊ธฐ์ค€์ด๋ฉฐ, ์ฝ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๋‚ด์šฉ์ด ๋นˆ ํŒŒ์ผ์ด๋ฉด ์žก์„ ํ•˜๋‚˜๋„ ๋งŒ๋“ค๊ธฐ ์ „์— ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค(exit 1). ๋กœ๋“œ๋œ ๊ณ„ํš์„œ๋Š” **์ตœ์ดˆ ๊ตฌํ˜„ ํ”„๋กฌํ”„ํŠธ 1ํšŒ**์—๋งŒ ์‹ค๋ฆฝ๋‹ˆ๋‹ค โ€” ๋ฆฌ๋ทฐ ์ดํ›„์˜ ์ˆ˜์ • ๋ผ์šด๋“œ์—๋Š” ๋ฆฌ๋ทฐ ํ”ผ๋“œ๋ฐฑ๋งŒ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. `--plan` ๊ณผ๋Š” ํ•จ๊ป˜ ์“ธ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - **Targeted Peer-Review** (`--reviewer`): Runs custom-selected reviewer agents to verify code changes. - **Total Peer-Review** (`--all-reviewer`): Enforces a unanimous PASS verdict from all registered reviewer sessions. - **Rebuttal & Adjudication Protocol** (`--max-rebut N`): Allows Creator to reject invalid reviewer feedback using `[REBUT:]` tags and triggers Reviewer re-adjudication (`[ADJUDICATION:]`) or Planner arbitration (`[ARBITRATION:]`). @@ -135,10 +136,10 @@ sequenceDiagram Dev->>Plan: send critiques (Discussion) Plan-->>Dev: update plan & reach consensus end - else Use Existing Plan or Creator Self-Plan (No --plan) - alt Existing Plan Found - Loop->>Dev: notify task execution using existing plan - else No Plan Found + else Creator Self-Plan (No --plan) + alt Plan named with --plan-file + Loop->>Dev: notify task execution using the named plan + else No --plan-file Loop->>Dev: request Creator self-planning and code execution end end @@ -172,7 +173,7 @@ sequenceDiagram 1. **Planning Phase**: - **Collaborative Planning (`--plan`)**: Planner๊ฐ€ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋ฅผ ํŒŒ์•…ํ•˜๊ณ  `implementation_plan.md`/`task.md`๋กœ ๋กœ๋“œ๋งต์„ ์ œ๊ณตํ•˜๋ฉฐ, Creator์™€์˜ ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„๋ฅผ ํ†ตํ•ด ์ •์ œ๋ฉ๋‹ˆ๋‹ค. - - **Creator Self-Planning (No `--plan`)**: Planner์˜ ๊ฐœ์ž… ์—†์ด, ๊ธฐ์กด ๊ณ„ํš์„œ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด Creator๊ฐ€ ๋…์ž์ ์œผ๋กœ ์„ค๊ณ„ ๋ฐ ํƒœ์Šคํฌ ๋‹จ์œ„๋ฅผ ๊ตฌ์ƒํ•œ ํ›„ ๊ตฌํ˜„์— ์ฐฉ์ˆ˜ํ•ฉ๋‹ˆ๋‹ค. + - **Creator Self-Planning (No `--plan`)**: Planner์˜ ๊ฐœ์ž… ์—†์ด Creator๊ฐ€ ๋…์ž์ ์œผ๋กœ ์„ค๊ณ„ ๋ฐ ํƒœ์Šคํฌ ๋‹จ์œ„๋ฅผ ๊ตฌ์ƒํ•œ ํ›„ ๊ตฌํ˜„์— ์ฐฉ์ˆ˜ํ•ฉ๋‹ˆ๋‹ค. `--plan-file ` ๋กœ ๊ณ„ํš์„œ๋ฅผ ๋ช…์‹œํ•œ ๊ฒฝ์šฐ์—๋งŒ ๊ทธ ๊ณ„ํš์„œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค. 2. **Execution Phase**: Creator๊ฐ€ ๋ฐฐ์ •๋œ ํƒœ์Šคํฌ์˜ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. `--plan` ๋ชจ๋“œ ์ง„ํ–‰ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์„ค๊ณ„ ๋ณ€๊ฒฝ ํ•„์š”์„ฑ์ด ๊ฐ์ง€๋˜๋ฉด ์ž‘์—…์„ ๋ฉˆ์ถ”๊ณ  Planner์—๊ฒŒ ๊ณ„ํš ์ˆ˜์ •์„ ๋จผ์ € ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค. (Creator ์ž์œจ ๊ณ„ํš ๋ชจ๋“œ์—์„œ๋Š” Creator๊ฐ€ ์ง์ ‘ ์„ค๊ณ„๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉฐ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.) ๊ตฌํ˜„ ์™„๋ฃŒ ํ›„ DoD(ํƒ€์ž… ๋งคํ•‘, ๊ณต์œ  ์ž์› ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ ๋ฐฉ์ง€, ๋ฌธ์„œ-์ฝ”๋“œ ์ •ํ•ฉ์„ฑ)๋ฅผ ์ž์ฒด ๊ฒ€์ฆํ•œ ๋’ค ๋‹จ์ผ ์ปค๋ฐ‹์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. 3. **Review Phase**: Creator๊ฐ€ ๋ฆฌ๋ทฐ์–ด ์„ธ์…˜์— ์ž‘์—… ์™„๋ฃŒ ์‚ฌ์‹ค๊ณผ ๋ณ€๊ฒฝ ๋ฒ”์œ„(`git diff`)๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌ๋ทฐ์–ด๋Š” ๊ฒ€์ฆ ํ›„ ๋ฆฌํฌํŠธ **๋งˆ์ง€๋ง‰์— ๋‹จ๋… ํ–‰**์œผ๋กœ ํŒ์ •์„ ๋‚จ๊น๋‹ˆ๋‹ค: - **๋ฐ˜๋ ค (`[VERDICT: NOT PASS]`)** โ†’ ํ”ผ๋“œ๋ฐฑ ์š”์•ฝ์„ Planner์—๊ฒŒ ์ „์†กํ•˜์—ฌ ์ƒ์œ„ ๋ ˆ๋ฒจ ๊ณ„ํš(Rev.n)์„ ๊ฐœ์‹œํ•ฉ๋‹ˆ๋‹ค. @@ -180,6 +181,14 @@ sequenceDiagram 4. ์ง€์ •๋˜๊ฑฐ๋‚˜ ์ž๋™ ์ˆ˜์ง‘๋œ ๋ฆฌ๋ทฐ์–ด ์ „์›์ด PASS๋ฅผ ๋ฐœํ–‰ํ•ด์•ผ ์™„๊ฒฐ๋˜๋ฉฐ, `--max-loop N`ํšŒ ๋‚ด์— ๋„๋‹ฌํ•˜์ง€ ๋ชปํ•˜๋ฉด ์•ˆ์ „์„ ์œ„ํ•ด ๋ฃจํ”„๋ฅผ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค. 5. ์™„๊ฒฐ ํ›„์—๋„ ์—์ด์ „ํŠธ ์„ธ์…˜์€ ์ข…๋ฃŒํ•˜์ง€ ์•Š๊ณ , ๋‹ค์Œ ํƒœ์Šคํฌ ์ง€์‹œ๊ฐ€ ์žˆ์„ ๋•Œ๊นŒ์ง€ ํ”„๋กฌํ”„ํŠธ ๋Œ€๊ธฐ ์ƒํƒœ(Standby)๋กœ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. +### ๊ณ„ํš์„œ๊ฐ€ ๋น„์–ด ์žˆ์„ ๋•Œ (Empty plan reports) + +๊ณ„ํš์„œ๋ฅผ ์ฝ๋Š” ๋„ค ์ง€์ (`--plan-file`, `--plan` ์ดˆ๊ธฐ ๊ณ„ํš์„œ, ๋…ผ์˜ ํ„ด refine, ๋ฆฌ๋ทฐ ์—์Šค์ปฌ๋ ˆ์ด์…˜ refine)์€ ๊ฐ™์€ ํŒ์ •์„ ์”๋‹ˆ๋‹ค: **์ฝ์„ ์ˆ˜ ์žˆ๋Š” ์ผ๋ฐ˜ ํŒŒ์ผ์ด๊ณ  ๊ณต๋ฐฑ์„ ์ œ์™ธํ•œ ๋‚ด์šฉ์ด ์žˆ์–ด์•ผ** ๊ณ„ํš์„œ์ž…๋‹ˆ๋‹ค. ๋ฆฌํฌํŠธ ํŒŒ์ผ์ด ์กด์žฌํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ํ†ต๊ณผ์‹œํ‚ค๋˜ ๊ณผ๊ฑฐ ๋™์ž‘์—์„œ๋Š” ๋นˆ ๋ฆฌํฌํŠธ๊ฐ€ ๊ณ„ํš์„œ๋ฅผ ์กฐ์šฉํžˆ ์ง€์› ๊ณ , ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์ž๊ธฐ๊ณ„ํš ๋ฌธ๊ตฌ๋กœ ๋ฐ”๋€ ์ฑ„ ๋ฃจํ”„๊ฐ€ ์ •์ƒ ์ข…๋ฃŒ(rc 0)ํ–ˆ์Šต๋‹ˆ๋‹ค. + +- **`--plan-file` ์ด ๋น„์–ด ์žˆ์œผ๋ฉด**: ์žก์„ ํ•˜๋‚˜๋„ ๋งŒ๋“ค๊ธฐ ์ „์— `exit 1`. +- **`--plan` ์˜ ์ดˆ๊ธฐ ๊ณ„ํš์„œ๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด**: `exit 1`. ๊ณ„ํš ๋‹จ๊ณ„๋ฅผ ์ง€๋ถˆํ•˜๊ณ  ์•„๋ฌด๊ฒƒ๋„ ๋ฐ›์ง€ ๋ชปํ•œ ์‹คํ–‰์ด ์ž๊ธฐ๊ณ„ํš์œผ๋กœ ๊ฒฉํ•˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. +- **refine ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด**(๋…ผ์˜ ํ„ดยท๋ฆฌ๋ทฐ ์—์Šค์ปฌ๋ ˆ์ด์…˜ ๋ชจ๋‘): ๊ฒฝ๊ณ ๋ฅผ ๋‚จ๊ธฐ๊ณ  **์ด์ „ ๊ณ„ํš์„œ๋ฅผ ์œ ์ง€**ํ•ฉ๋‹ˆ๋‹ค. ๋ฎ์–ด์“ฐ์ง€ ์•Š์œผ๋ฏ€๋กœ, "๊ฐฑ์‹ ๋œ ๋‹ค์Œ ๊ณ„ํš์„œ์— ์ž…๊ฐํ•˜์—ฌโ€ฆ" ๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ˆ˜์ • ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋นˆ ๊ณ„ํš์„œ๋ฅผ ์‹ฃ๋Š” ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค. + --- ## CLI Option โ†” Workflow Phase Mapping @@ -188,7 +197,8 @@ sequenceDiagram | ์›Œํฌํ”Œ๋กœ์šฐ ๋‹จ๊ณ„ | ํ•ด๋‹น CLI ์˜ต์…˜ | ์„ค๋ช… | | :--- | :--- | :--- | -| **Phase 1: Planning** | `--plan` | Planner ์—์ด์ „ํŠธ๋ฅผ ๊ธฐ๋™ํ•˜์—ฌ ์ตœ์ดˆ ๊ณ„ํš ์ž‘์„ฑ์„ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค. (์˜ต์…˜์„ ์ง€์ •ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ ์ƒˆ ๊ณ„ํš์„œ ์ž‘์„ฑ์„ ์ƒ๋žตํ•˜๋ฉฐ, ๊ธฐ์กด ๊ณ„ํš์„œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ์ด๋ฅผ ๋กœ๋“œํ•˜๊ณ , ์—†๋Š” ๊ฒฝ์šฐ Creator๊ฐ€ ์ง์ ‘ ๊ณ„ํš ๋ฐ ์„ค๊ณ„๋ฅผ ์ˆ˜๋ฆฝํ•˜์—ฌ ์ฆ‰์‹œ ๊ตฌํ˜„์— ์ฐฉ์ˆ˜ํ•ฉ๋‹ˆ๋‹ค.) | +| **Phase 1: Planning** | `--plan` | Planner ์—์ด์ „ํŠธ๋ฅผ ๊ธฐ๋™ํ•˜์—ฌ ์ตœ์ดˆ ๊ณ„ํš ์ž‘์„ฑ์„ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค. (์˜ต์…˜์„ ์ง€์ •ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ ์ƒˆ ๊ณ„ํš์„œ ์ž‘์„ฑ์„ ์ƒ๋žตํ•˜๊ณ , Creator๊ฐ€ ์ง์ ‘ ๊ณ„ํš ๋ฐ ์„ค๊ณ„๋ฅผ ์ˆ˜๋ฆฝํ•˜์—ฌ ์ฆ‰์‹œ ๊ตฌํ˜„์— ์ฐฉ์ˆ˜ํ•ฉ๋‹ˆ๋‹ค.) | +| **Phase 1: Plan reuse** | `--plan-file ` | ๊ธฐ์กด ๊ณ„ํš์„œ๋ฅผ ์žฌ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ œ์–ด ๋ฃจํŠธ ๊ธฐ์ค€ ๊ฒฝ๋กœ์ด๋ฉฐ **`--plan` ๊ณผ ์ƒํ˜ธ ๋ฐฐํƒ€**์ž…๋‹ˆ๋‹ค. ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ์–ด๋–ค ๊ณ„ํš์„œ๋„ ๋กœ๋“œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. | | **Phase 1: Planner session** | `--planner ` | ๊ณ„ํš ๋‹จ๊ณ„๋ฅผ ์ˆ˜ํ–‰ํ•  ์„ธ์…˜์„ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค. **`--plan`๊ณผ ํ•จ๊ป˜๋งŒ** ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ƒ๋žต ์‹œ running planner๋ฅผ ์ž๋™ ํƒ์ƒ‰ํ•ฉ๋‹ˆ๋‹ค. | | **Phase 1: Debate** | `--plan-talk N` | Planner์™€ Creator๊ฐ€ ์ƒํ˜ธ ๋Œ€ํ™”์‹ ์ฑŒ๋ฆฐ์ง€ ๋ฃจํ”„๋ฅผ `N`ํšŒ ๋Œ๋ฉฐ ๊ณ„ํš์„ ๊ต์ฐจ ์ •์ œํ•ฉ๋‹ˆ๋‹ค. | | **Phase 2: Execution** | `--creator ` | ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜„ํ•  Creator ์„ธ์…˜์— ์ฝ”๋”ฉ ํƒœ์Šคํฌ๋ฅผ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค. (ํ•„์ˆ˜) | @@ -222,7 +232,7 @@ bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \ ## Workflow ```bash -# 1. Creator Self-Planning & Development + Self-review (direct task execution using existing promoted plan or Creator's own self-plan) +# 1. Creator Self-Planning & Development + Self-review (no plan is loaded; add --plan-file to reuse one) bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \ --creator "" \ --task "Fix typo in deploy/README.md" diff --git a/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh b/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh index da0cbdf..c5cd555 100644 --- a/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh +++ b/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh @@ -32,6 +32,7 @@ TARGET_AGENT="" TASK="" REVIEWER_LIST="" PLANNER_SESSION_OVERRIDE="" +LOOP_PLAN_FILE="" LOOP_WORKTREE="" LOOP_BASE="" LOOP_REVIEW_ONLY=0 @@ -46,6 +47,7 @@ usage() { echo " --plan Enable Planner agent intervention & design phase" echo " --planner Planner session (requires --plan; default: auto-resolve)" echo " --plan-talk N Planner-Creator discussion limit turns (default: 1)" + echo " --plan-file Reuse an existing plan file, relative to the control root (not with --plan)" echo " --reviewer \"A,B\" Targeted reviewer session name list (comma-separated)" echo " --all-reviewer Enforce PASS verdict from all active reviewer sessions" echo " --max-loop N Max execution-review corrective loop runs (default: 3)" @@ -92,6 +94,7 @@ while [[ "$#" -gt 0 ]]; do exit 1 ;; --planner) PLANNER_SESSION_OVERRIDE="$2"; shift 2 ;; + --plan-file) LOOP_PLAN_FILE="$2"; shift 2 ;; --task) TASK="$2"; shift 2 ;; --worktree) LOOP_WORKTREE="$2"; shift 2 ;; --base) LOOP_BASE="$2"; shift 2 ;; @@ -121,11 +124,14 @@ fi # Issue #21 (D12): the legacy shell loop assumes a cwd-relative .mam/jobs and # diffs the checkout it runs in; the worktree/base options are Python-only. +# Issue #25 (D9): --plan-file is too - its fail-closed check lives in the +# runner and is not duplicated in a path that is on its way out. # Checked before the freeze and the loop lock (log_* are not defined yet). if [ "${MAM_LOOP_PY:-1}" = "0" ] && { [ -n "$LOOP_WORKTREE" ] || [ -n "$LOOP_BASE" ] \ || [ "$LOOP_REVIEW_ONLY" = "1" ] || [ -n "${MAM_LOOP_WORKTREE:-}" ] \ - || [ -n "$LOOP_REVIEW_ISOLATION" ] || [ -n "$LOOP_REMOTE_REVIEW" ]; }; then - echo "ERROR: --worktree/--base/--review-only/--review-isolation/--remote-review require the Python loop (MAM_LOOP_PY=1)" >&2 + || [ -n "$LOOP_REVIEW_ISOLATION" ] || [ -n "$LOOP_REMOTE_REVIEW" ] \ + || [ -n "$LOOP_PLAN_FILE" ]; }; then + echo "ERROR: --worktree/--base/--review-only/--review-isolation/--remote-review/--plan-file require the Python loop (MAM_LOOP_PY=1)" >&2 exit 1 fi @@ -560,16 +566,11 @@ if [ "$PLAN_MODE" = true ]; then log_success "Interactive planning completed. Plan finalized." else log_info "=== Phase 1: Self-Planning Mode (Direct Execution) ===" - EXISTING_PLAN_FILE="" - if [ -n "$PLANNER_SESSION" ]; then - # Promoted plans are saved as plan-.md by the Phase 3 promotion - # step below, not report-final.md; pick the most recently promoted one. - EXISTING_PLAN_FILE=$(ls -t ".agents/reports/$PLANNER_SESSION"/plan-*.md 2>/dev/null | head -n 1 || true) - fi - if [ -n "$EXISTING_PLAN_FILE" ] && [ -f "$EXISTING_PLAN_FILE" ]; then - log_info "Found existing promoted plan at '$EXISTING_PLAN_FILE'. Loading plan..." - CURRENT_PLAN=$(cat "$EXISTING_PLAN_FILE") - fi + # Issue #25 (D1/D9): no plan is loaded here. This used to take the newest + # promoted plan of the auto-resolved planner, keyed on mtime and on a session + # that says nothing about this task. Reuse is explicit and Python-only now + # (--plan-file), and this path rejects that option above. + log_info "No plan loaded; the Creator plans by itself." fi # =========================================================================== diff --git a/.agents/skills/multi-agent-mux-monitor/SKILL.md b/.agents/skills/multi-agent-mux-monitor/SKILL.md index 01ca21f..1ee9a02 100644 --- a/.agents/skills/multi-agent-mux-monitor/SKILL.md +++ b/.agents/skills/multi-agent-mux-monitor/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-monitor description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted." -version: 4.7.0 +version: 4.7.1 author: godopu license: MIT platforms: [linux, macos] diff --git a/.agents/skills/multi-agent-mux-orc-onboard/SKILL.md b/.agents/skills/multi-agent-mux-orc-onboard/SKILL.md index 9b33333..03f3290 100644 --- a/.agents/skills/multi-agent-mux-orc-onboard/SKILL.md +++ b/.agents/skills/multi-agent-mux-orc-onboard/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-orc-onboard description: "Register current or specified orchestrator session UUID into agent-sessions.yaml orchestrator_uuids list to prevent sub-agent discovery capture." -version: 4.7.0 +version: 4.7.1 author: godopu license: MIT platforms: [linux, macos] diff --git a/.agents/skills/multi-agent-mux-resume/SKILL.md b/.agents/skills/multi-agent-mux-resume/SKILL.md index ace7f82..8156922 100644 --- a/.agents/skills/multi-agent-mux-resume/SKILL.md +++ b/.agents/skills/multi-agent-mux-resume/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-resume description: "Resume an existing agent (claude, antigravity/agy) conversation by UUID into a herdr session. Reads .mam/agent-sessions.yaml for the saved session/conversation id, spawns (or reuses) a herdr session of the matching name, and runs `claude -r ` or `agy --conversation ` inside. Use when you want to reattach to a previous session's context, or revive a session whose herdr died but the agent's conversation is still on disk." -version: 4.7.0 +version: 4.7.1 author: godopu license: MIT platforms: [linux, macos] diff --git a/.agents/skills/multi-agent-mux-status/SKILL.md b/.agents/skills/multi-agent-mux-status/SKILL.md index 304b52b..87e9d1d 100644 --- a/.agents/skills/multi-agent-mux-status/SKILL.md +++ b/.agents/skills/multi-agent-mux-status/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-status description: "Read-only instant snapshot of all agent herdr sessions โ€” name, YAML status, herdr alive, pane cmd/cwd, resume UUID on disk, and any drift. No mutation. Reuses reconcile.sh --dry-run for the diff logic. Use when you want to know 'what's running RIGHT NOW' without spinning up the monitor loop." -version: 4.7.0 +version: 4.7.1 author: godopu license: MIT platforms: [linux, macos] diff --git a/.agents/skills/multi-agent-mux-stop/SKILL.md b/.agents/skills/multi-agent-mux-stop/SKILL.md index afbf1cf..b0184ac 100644 --- a/.agents/skills/multi-agent-mux-stop/SKILL.md +++ b/.agents/skills/multi-agent-mux-stop/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-stop description: "Stop an agent herdr session (claude, antigravity/agy) and update .mam/agent-sessions.yaml. Default stops gracefully and marks status=stopped with conversation preserved for resume. Does NOT delete on-disk conversation artifacts (jsonl/db) โ€” those are preserved unless --purge-conversation is passed. Use when ending a work session, switching to a different one, or cleaning up before a fresh start." -version: 4.7.0 +version: 4.7.1 author: godopu license: MIT platforms: [linux, macos] diff --git a/BOOTSTRAP.ko.md b/BOOTSTRAP.ko.md index c4f866c..b1215e7 100644 --- a/BOOTSTRAP.ko.md +++ b/BOOTSTRAP.ko.md @@ -2,7 +2,7 @@ ๋ณธ ๋ฌธ์„œ๋Š” `herdr_agent_orchestration` ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ๋ฐ ๋ฉ”์‹œ์ง• ๋ฐฑํ”Œ๋ ˆ์ธ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ์— ๋„์ž…ํ•˜์—ฌ ์ด์‹ํ•˜๊ณ , ์ƒˆ๋กœ์šด ๊ฐœ๋ฐœ์ž/์—์ด์ „ํŠธ๊ฐ€ ์ดˆ๊ธฐ ๊ฐ€๋™์„ ์‹œ์ž‘ํ•  ๋•Œ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋Š” ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ๊ตฌ์ถ• ์ ˆ์ฐจ๋ฅผ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค. -[![Version: v4.7.0](https://img.shields.io/badge/version-v4.7.0-blue.svg)](VERSIONS.md) +[![Version: v4.7.1](https://img.shields.io/badge/version-v4.7.1-blue.svg)](VERSIONS.md) ์ƒˆ๋กœ์šด ์—์ด์ „ํŠธ๋Š” ์ด ์•ˆ๋‚ด์„œ์— ๊ธฐ์ˆ ๋œ ์ ˆ์ฐจ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰ํ•˜์—ฌ ์ดˆ๊ธฐ ํ™˜๊ฒฝ์„ ์•ˆ์ •์ ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 880dead..05f6908 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -2,7 +2,7 @@ This document guides you through the setup and initialization procedures required to adopt the `herdr_agent_orchestration` orchestration and messaging backplane workflow in a new project, enabling a new developer or agent to get up and running quickly. -[![Version: v4.7.0](https://img.shields.io/badge/version-v4.7.0-blue.svg)](VERSIONS.md) +[![Version: v4.7.1](https://img.shields.io/badge/version-v4.7.1-blue.svg)](VERSIONS.md) A new agent can follow the steps in this guide sequentially to establish a stable and reliable initial environment. diff --git a/MESSAGING.md b/MESSAGING.md index ebc80c7..1125620 100644 --- a/MESSAGING.md +++ b/MESSAGING.md @@ -1,6 +1,6 @@ # Messaging System Technical Analysis & Architecture Report -Applies to: MAM v4.7.0 +Applies to: MAM v4.7.1 This report provides a comprehensive, deep-dive analysis of the messaging system implemented in the `multi-agent-mux-delegate-job` skill. It covers the MQTT broker architecture, event protocols, job lifecycles, codebase internals, cross-system integration, and a list of known limitations along with production recommendations. diff --git a/README.ko.md b/README.ko.md index 2bb889d..25a75ea 100644 --- a/README.ko.md +++ b/README.ko.md @@ -2,7 +2,7 @@ Herdr์™€ MQTT ๋ธŒ๋กœ์ปค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์ถ•๋œ ๊ณ ์‹ ๋ขฐ์„ฑ **๋‹ค์ค‘ ์—์ด์ „ํŠธ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ๋ฐ ๋ฉ”์‹œ์ง• ๋ฐฑํ”Œ๋ ˆ์ธ** ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. `claude`, `agy`, `hermes`, `grok`, `opencode`, `codex` ๋“ฑ ๋‹ค์ค‘ LLM ๋ฐฑ์—”๋“œ ์—์ด์ „ํŠธ ์ „๋ฐ˜์— ๊ฑธ์ณ ์žฅ์‹œ๊ฐ„ ์ˆ˜ํ–‰๋˜๋Š” ์ž‘์—…(์ฝ”๋“œ ์ƒ์„ฑ, ๋ฆฌํŒฉํ† ๋ง, ๋ณด์•ˆ ๊ฒ€ํ†  ๋“ฑ)์„ ์กฐ์ •, ๊ฒฉ๋ฆฌ ๋ฐ ๊ฐ์‚ฌํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. -[![Version: v4.7.0](https://img.shields.io/badge/version-v4.7.0-blue.svg)](VERSIONS.md) +[![Version: v4.7.1](https://img.shields.io/badge/version-v4.7.1-blue.svg)](VERSIONS.md) --- diff --git a/README.md b/README.md index 611b1f5..8bbf741 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ An advanced, high-reliability **Multi-Agent Orchestration & Messaging Backplane** framework built on Herdr and MQTT. It is designed to coordinate, isolate, and audit long-running agent tasks (such as code generation, refactoring, and security reviews) across multiple LLM backend clients (`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`). -[![Version: v4.7.0](https://img.shields.io/badge/version-v4.7.0-blue.svg)](VERSIONS.md) +[![Version: v4.7.1](https://img.shields.io/badge/version-v4.7.1-blue.svg)](VERSIONS.md) --- diff --git a/VERSIONS.md b/VERSIONS.md index 0f2c9af..67100ff 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -6,8 +6,8 @@ ## ๐Ÿ“Œ ํ˜„์žฌ ๋ฒ„์ „ ๊ฐœ์š” (Current Release) -- **ํ”„๋ ˆ์ž„์›Œํฌ ๋ฒ„์ „**: `v4.7.0` -- **์ตœ์‹  ๋ฆด๋ฆฌ์Šค ์ผ์‹œ**: 2026-09-20 (KST) +- **ํ”„๋ ˆ์ž„์›Œํฌ ๋ฒ„์ „**: `v4.7.1` +- **์ตœ์‹  ๋ฆด๋ฆฌ์Šค ์ผ์‹œ**: 2026-09-22 (KST) - **๊ธฐ์ค€ ๋ธŒ๋žœ์น˜**: `main` - **ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜**: - **Terminal ์ƒํƒœ ๋‹จ๋ฐฉํ–ฅ ๋ถˆ๋ณ€์„ฑ (`engine.py`, Issue #6 Phase 1)**: `completed`, `error`, `cancelled` ํ„ฐ๋ฏธ๋„ ์ƒํƒœ ์ง„์ž… ํ›„ ํ›„์† ์ด๋ฒคํŠธ๋‚˜ ์—…๋ฐ์ดํŠธ์— ์˜ํ•œ ๋ฎ์–ด์“ฐ๊ธฐ๋ฅผ ์—„๊ฒฉํžˆ ์ฐจ๋‹จ(Terminal Immutability). `registry.py close` ์„œ๋ธŒ์ปค๋งจ๋“œ ๋ฐ fail-closed ์กฐ๊ธฐ ์ข…๋ฃŒ ๋„์ž…. @@ -22,23 +22,39 @@ ## ๐Ÿงญ ์Šคํ‚ฌ ํŒจํ‚ค์ง€ ๋ฒ„์ „ ๋งคํŠธ๋ฆญ์Šค (Skills Version Matrix) -๋ชจ๋“  8๊ฐœ ์Šคํ‚ฌ์€ YAML frontmatter ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(`author`, `version`, `platforms`, `environments`) ํ‘œ์ค€ํ™”๋ฅผ ํ†ตํ•ด `v4.7.0`๋กœ ๋™๊ธฐํ™”๋˜์–ด ๋ฐฐํฌ๋ฉ๋‹ˆ๋‹ค. +๋ชจ๋“  8๊ฐœ ์Šคํ‚ฌ์€ YAML frontmatter ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(`author`, `version`, `platforms`, `environments`) ํ‘œ์ค€ํ™”๋ฅผ ํ†ตํ•ด `v4.7.1`๋กœ ๋™๊ธฐํ™”๋˜์–ด ๋ฐฐํฌ๋ฉ๋‹ˆ๋‹ค. | ์Šคํ‚ฌ๋ช… | ๋ฒ„์ „ | ์—ญํ•  ๋ฐ ์ฃผ์š” ์ฑ…์ž„ | ์ƒํƒœ | | :--- | :---: | :--- | :---: | -| **`multi-agent-mux-create`** | `4.7.0` | ์—์ด์ „ํŠธ ์„ธ์…˜ ์‹ ๊ทœ ์ƒ์„ฑ ๋ฐ Herdr ์ปจํ…Œ์ด๋„ˆ ๊ฒฉ๋ฆฌ ์Šคํฐ | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-stop`** | `4.7.0` | ๋Œ€ํ™” UUID ์›์ž์  ์บก์ฒ˜ ๋ฐ ์„ธ์…˜ ์•ˆ์ „ ์ข…๋ฃŒ (Graceful Stop) | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-resume`** | `4.7.0` | ์˜จ๋””์Šคํฌ ๋Œ€ํ™” ์ปจํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ Tier-1 ์ดˆ๊ณ ์† ์„ธ์…˜ ๋ณต์› | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-status`** | `4.7.0` | ์‹ค์‹œ๊ฐ„ Herdr ์„ธ์…˜ ๋ฐ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋“œ๋ฆฌํ”„ํŠธ ์Šค๋ƒ…์ƒท ์กฐํšŒ | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-monitor`** | `4.7.0` | YAML โ†” ๋Ÿฐํƒ€์ž„ ์ƒํƒœ ๊ฐ„ ์ž์œจ ์กฐ์ •์ž (Reconciler Loop) | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-delegate-job`** | `4.7.0` | MQTT ์ด๋ฒคํŠธ ์ฑ„๋„ ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ๋‹จ์œ„ ์ž‘์—… ์œ„์ž„ | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-loop`** | `4.7.0` | Planner-Creator-Reviewer 3์ž ์ž์œจ ๊ณ„ํšยท์‹คํ–‰ยทํ”ผ์–ด๋ฆฌ๋ทฐ ๋ฃจํ”„ | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-orc-onboard`** | `4.7.0` | ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ UUID ๊ฒฉ๋ฆฌ ๋“ฑ๋ก ๋ฐ ์„œ๋ธŒ ์„ธ์…˜ ์˜ค์—ผ ๋ฐฉ์ง€ | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-create`** | `4.7.1` | ์—์ด์ „ํŠธ ์„ธ์…˜ ์‹ ๊ทœ ์ƒ์„ฑ ๋ฐ Herdr ์ปจํ…Œ์ด๋„ˆ ๊ฒฉ๋ฆฌ ์Šคํฐ | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-stop`** | `4.7.1` | ๋Œ€ํ™” UUID ์›์ž์  ์บก์ฒ˜ ๋ฐ ์„ธ์…˜ ์•ˆ์ „ ์ข…๋ฃŒ (Graceful Stop) | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-resume`** | `4.7.1` | ์˜จ๋””์Šคํฌ ๋Œ€ํ™” ์ปจํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ Tier-1 ์ดˆ๊ณ ์† ์„ธ์…˜ ๋ณต์› | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-status`** | `4.7.1` | ์‹ค์‹œ๊ฐ„ Herdr ์„ธ์…˜ ๋ฐ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋“œ๋ฆฌํ”„ํŠธ ์Šค๋ƒ…์ƒท ์กฐํšŒ | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-monitor`** | `4.7.1` | YAML โ†” ๋Ÿฐํƒ€์ž„ ์ƒํƒœ ๊ฐ„ ์ž์œจ ์กฐ์ •์ž (Reconciler Loop) | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-delegate-job`** | `4.7.1` | MQTT ์ด๋ฒคํŠธ ์ฑ„๋„ ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ๋‹จ์œ„ ์ž‘์—… ์œ„์ž„ | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-loop`** | `4.7.1` | Planner-Creator-Reviewer 3์ž ์ž์œจ ๊ณ„ํšยท์‹คํ–‰ยทํ”ผ์–ด๋ฆฌ๋ทฐ ๋ฃจํ”„ | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-orc-onboard`** | `4.7.1` | ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ UUID ๊ฒฉ๋ฆฌ ๋“ฑ๋ก ๋ฐ ์„œ๋ธŒ ์„ธ์…˜ ์˜ค์—ผ ๋ฐฉ์ง€ | โœ… ๋ฐฐํฌ | --- ## ๐Ÿ“‹ ๋ฒ„์ „๋ณ„ ์ƒ์„ธ ๋ณ€๊ฒฝ ๋‚ด์—ญ (Changelog) +### ๐Ÿš€ `v4.7.1` โ€” Issue #25 ์Šน๊ฒฉ๋œ ๊ณ„ํš์„œ ์žฌ์‚ฌ์šฉ์€ `--plan-file` ๋กœ๋งŒ: ์•”๋ฌต์  ์ž๋™ ๋กœ๋“œ ์ œ๊ฑฐ, ๋นˆ ๊ณ„ํš ๋ฆฌํฌํŠธ๋Š” ์กฐ์šฉํžˆ ๋„˜์–ด๊ฐ€์ง€ ์•Š์Œ (2026-09-22, PR #32) + +> **ํŒจ์น˜ ๋งˆ์ผ์Šคํ†ค (PATCH Release)**: `run_loop.sh` ๋ฅผ `--plan` ์—†์ด ์‹คํ–‰ํ•˜๋ฉด ๋Ÿฌ๋„ˆ๊ฐ€ planner ๋ฅผ ์ž๋™์œผ๋กœ ๊ณ ๋ฅด๊ณ  `.agents/reports//plan-*.md` ์ค‘ mtime ์ด ๊ฐ€์žฅ ์ตœ๊ทผ์ธ ํŒŒ์ผ์„ "์ด๋ฏธ ์ˆ˜๋ฆฝ๋œ ๊ณ„ํš์„œ"๋กœ creator ์—๊ฒŒ ๋„˜๊ธฐ๋˜ ๋ฌธ์ œ(Issue #25)๋ฅผ ๊ณ ์ณค๋‹ค. ์ €์žฅ์†Œ์—๋Š” ๋‹ค๋ฅธ ์ด์Šˆ์˜ ๊ณ„ํš์„œ๊ฐ€ ์ˆ˜์‹ญ ๊ฐœ ์ปค๋ฐ‹๋ผ ์žˆ๊ณ  ์ฒดํฌ์•„์›ƒ ์งํ›„์˜ mtime ์€ ์˜๋ฏธ๊ฐ€ ์—†์–ด, ๋ฌด๊ด€ํ•œ ๊ณ„ํš์ด ๊ตฌํ˜„ ํ”„๋กฌํ”„ํŠธ์— ์‹ค๋ ธ๋‹ค(v4.7.0 ๋ณ‘ํ•ฉ ๋ฃจํ”„์—์„œ๋„ Issue #6 ๊ณ„ํš์„œ๊ฐ€ ์‹ค๋ฆฐ ๊ฒƒ์„ ๊ด€์ธก). ์˜ต์…˜์ด ํ•˜๋‚˜ ๋Š˜์ง€๋งŒ ์ž˜๋ชป๋œ ๊ธฐ๋ณธ ๋™์ž‘์„ ์—†์• ๋Š” ์ˆ˜์ •์ด๋ผ PATCH ๋กœ ์˜ฌ๋ฆฐ๋‹ค. + +#### ๐ŸŒŸ ์ฃผ์š” ๊ฐœ์„  ์‚ฌํ•ญ +1. **์•”๋ฌต์  ๊ณ„ํš ๋กœ๋“œ ์ œ๊ฑฐ (`lib_py/loop/runner.py`, `multi-agent-mux-loop/scripts/run_loop.sh`)**: `--plan` ์—†๋Š” ์‹คํ–‰์€ ์–ด๋–ค ๊ณ„ํš์„œ๋„ ์ฝ์ง€ ์•Š๋Š”๋‹ค. Python ๋Ÿฌ๋„ˆ์™€ ๋ ˆ๊ฑฐ์‹œ ์…ธ ๋Ÿฌ๋„ˆ(`MAM_LOOP_PY=0`) ๋ชจ๋‘์—์„œ mtime ๊ธฐ๋ฐ˜ ์„ ํƒ์„ ์ง€์› ๊ณ , creator ๋Š” ์Šค์Šค๋กœ ๊ณ„ํšํ•œ๋‹ค. ์ฒดํฌํฌ์ธํŠธ์˜ `current_plan` ์€ ์“ฐ๊ธฐ ์ „์šฉ์ด๋ผ ์˜› ๊ณ„ํš์„ ๋˜์‚ด๋ฆฌ๋Š” ๊ฒฝ๋กœ๊ฐ€ ์—†๋‹ค. +2. **`--plan-file ` (Python ๋Ÿฌ๋„ˆ ์ „์šฉ)**: ์Šน๊ฒฉ๋œ ๊ณ„ํš์„œ์˜ ์žฌ์‚ฌ์šฉ์€ ํŒŒ์ผ์„ ๋ช…์‹œํ•  ๋•Œ๋งŒ ํ•œ๋‹ค. ์ƒ๋Œ€๊ฒฝ๋กœ๋Š” **์ œ์–ด ๋ฃจํŠธ ๊ธฐ์ค€**(์›ŒํฌํŠธ๋ฆฌ์—์„œ ๋Œ๋ ค๋„ ๊ฐ™์Œ)์ด๊ณ , ์žก์ด ํ•˜๋‚˜๋„ ๋“ฑ๋ก๋˜๊ธฐ ์ „์— ์ฝ์–ด ๊ฒ€์‚ฌํ•œ๋‹ค(์—†๋Š” ํŒŒ์ผยท๋””๋ ‰ํ„ฐ๋ฆฌยท๋นˆ ํŒŒ์ผยท์ฝ๊ธฐ ์‹คํŒจ๋Š” exit 1). `--plan` ๊ณผ ํ•จ๊ป˜ ์“ธ ์ˆ˜ ์—†๊ณ , `--review-only` ์—์„œ๋Š” ๊ฒฝ๊ณ  ํ›„ ๋ฌด์‹œํ•œ๋‹ค. ๋ ˆ๊ฑฐ์‹œ ์…ธ ๋Ÿฌ๋„ˆ๋Š” ์ด ์˜ต์…˜์„ ๋ฃจํ”„ ๋ฝ ์ „์— ๋ช…์‹œ์ ์œผ๋กœ ๊ฑฐ๋ถ€ํ•œ๋‹ค. +3. **๊ณ„ํš ํ…์ŠคํŠธ ๊ฒ€์‚ฌ ํ•˜๋‚˜๋ฅผ ๋ชจ๋“  ๋กœ๋“œ ๊ฒฝ๋กœ์— (`LoopRunner._plan_text`)**: planner ์˜ ์ฒซ ๋ฆฌํฌํŠธ๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด ๋ฃจํ”„๋ฅผ ๋ฉˆ์ถ˜๋‹ค(๊ณ„ํš ๋‹จ๊ณ„์— ๋น„์šฉ์„ ์“ฐ๊ณ ๋„ ์กฐ์šฉํžˆ ์ž์œจ ๊ณ„ํš์œผ๋กœ ๊ฐ•๋“ฑ๋˜๋˜ ๋ฌธ์ œ). ํ† ๋ก (refine)ยท์žฌ๊ณ„ํš ๋ฆฌํฌํŠธ๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด ๊ฒฝ๊ณ ํ•˜๊ณ  ๊ธฐ์กด ๊ณ„ํš์„ ์œ ์ง€ํ•œ๋‹ค. +4. **๋ฌธ์„œ**: loop SKILL.md ์˜ ์˜ต์…˜ ํ‘œยทWorkflowยท"๊ณ„ํš์„œ๊ฐ€ ๋น„์–ด ์žˆ์„ ๋•Œ" ์ ˆ. + +#### โœ… ํ’ˆ์งˆ ๊ฒ€์ฆ +- **์‹ ๊ทœ ํ…Œ์ŠคํŠธ 17๊ฐœ** (`tests/test_issue25_plan_reuse.py`, P1โ€“P11): ์˜ต์…˜ ์—†๋Š” ์‹คํ–‰์ด ์Šน๊ฒฉ๋œ ๊ณ„ํš์„œ๋ฅผ ์ฝ์ง€ ์•Š์Œ(Pythonยท์…ธ), `--plan-file` ์˜ ์ œ์–ด ๋ฃจํŠธ ๊ธฐ์ค€ ํ•ด์„ยท์‹คํŒจ ๊ฒฝ๋กœยท`--plan`/`--review-only` ์กฐํ•ฉยท์…ธ ๋Ÿฌ๋„ˆ์˜ ๊ฑฐ๋ถ€, ๋นˆ planner ๋ฆฌํฌํŠธ์˜ ์ค‘๋‹จ, ๋นˆ refine ์˜ ๊ธฐ์กด ๊ณ„ํš ์œ ์ง€. ๋ณ‘ํ•ฉ ์›Œํฌ์ŠคํŽ˜์ด์Šค ๋ฆฌ๋ทฐ์—์„œ 17๊ฑด ์ค‘ 13๊ฑด์ด ์ˆ˜์ • ์ „ ์†Œ์Šค์—์„œ ์‹คํŒจํ•จ์„ ํ™•์ธ. +- **๋ฆฌ๋ทฐ**: ์ž‘์„ฑ ์›Œํฌ์ŠคํŽ˜์ด์Šค ๋ฃจํ”„(`--worktree --base --review-only`) 1ํšŒ์ฐจ @5fa5923 claudeยทagyยทopencode PASS, grokยทcodex NOT PASS(์ƒ๋Œ€๊ฒฝ๋กœ `--plan-file` ์ด ์‹คํ–‰ cwd ๊ธฐ์ค€์œผ๋กœ ํ•ด์„) โ†’ 1e82651 ๋กœ ์ •์ •, 2ํšŒ์ฐจ 5์ธ ์ „์› PASS. ๋ณ‘ํ•ฉ ์›Œํฌ์ŠคํŽ˜์ด์Šค์—์„œ PR head ๋ฅผ detached ์›ŒํฌํŠธ๋ฆฌ์— ๋ฐ›์•„ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์žฌ๊ฒ€ํ† : @1e82651 claudeยทagyยทgrokยทcodex 4์ธ ์ „์› PASS(1ํšŒ์ฐจ, identity 4๊ฑด ๋ชจ๋‘ base 6fc9194 โ†’ head 1e82651, evidence review-tree). ๋ฆฌํฌํŠธ์™€ identity ์‚ฌ์ด๋“œ์นด๋Š” `.agents/reports//`. +- **์ „์ฒด ์Šค์œ„ํŠธ (๋ณ‘ํ•ฉ ์›Œํฌ์ŠคํŽ˜์ด์Šค, ์ž์ฒด venv ๊ฐ€ ์žˆ๋Š” ์›ŒํฌํŠธ๋ฆฌ @1e82651)**: 1037 ํ†ต๊ณผ / 1 skip / ์‹คํŒจ 0 (35๋ถ„ 26์ดˆ, ๋ฆฌ๋ทฐ ๋ฃจํ”„์™€ ๋ณ‘ํ–‰). +- **๋น„์ฐจ๋‹จ ํ›„์†**: ๋ ˆ๊ฑฐ์‹œ ์…ธ ๋Ÿฌ๋„ˆ์˜ plan ๋ชจ๋“œ ์ฝ๊ธฐ์—๋Š” ๋นˆ ๊ณ„ํš์„œ ๊ฒ€์‚ฌ๊ฐ€ ์—†์Œ(E1), `--plan-file` ์‹คํ–‰์—์„œ `[ESCALATE: PLANNER]` ๊ฐ€ ์žฌ๊ณ„ํš์œผ๋กœ ์ด์–ด์ง€์ง€ ์•Š์Œ(E2, ๊ธฐ์กด ๊ฒŒ์ดํŠธ), ๋ฆฌ๋ทฐ ๊ฒฉ๋ฆฌ ์›ŒํฌํŠธ๋ฆฌ์— `.venv` ๊ฐ€ ์—†์–ด `run_loop.sh` ๋ฅผ ํ•˜์œ„ ํ”„๋กœ์„ธ์Šค๋กœ ๋„์šฐ๋Š” ํ…Œ์ŠคํŠธ๋Š” `AGENT_PYTHON_BIN` ์—†์ด๋Š” ์‹คํŒจ(E3, #21 ํ•ญ๋ชฉ 5 ์šด์˜ ํ›„์†), Phase 1 ์˜ `getmtime` ๊ฒฝํ•ฉ(E4), ๊ณต๋ฐฑ์ด ๋“  ๊ฒฝ๋กœ ์ „์šฉ ํ…Œ์ŠคํŠธ ์—†์Œ. + ### ๐Ÿš€ `v4.7.0` โ€” Issue #21 ์›ŒํฌํŠธ๋ฆฌ ์šด์˜ ๋ชจ๋ธ 2๋‹จ๊ณ„: ์›ŒํฌํŠธ๋ฆฌ ํ—ฌํผ, ๋ฆฌ๋ทฐ identity, ์ˆ˜์šฉ ์‚ฌ๋ก€ A1โ€“A8, #20 ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ, #27, #28 ํ˜ธ์ŠคํŠธ ์ด์‹์„ฑ (2026-09-20, PR #29) > **๋งˆ์ด๋„ˆ ๋งˆ์ผ์Šคํ†ค (MINOR Release)**: ์›ŒํฌํŠธ๋ฆฌ ์šด์˜ ๋ชจ๋ธ(#21, `docs/design/worktree-creator-model.md`)์˜ ์ž‘์—… ํ•ญ๋ชฉ 4ยท5ยท7 ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค. ์ด์Šˆ๋ณ„ ์›ŒํฌํŠธ๋ฆฌ๋ฅผ ๋งŒ๋“ค๊ณ  ์ง€์šฐ๋Š” ํ—ฌํผ 2๊ฐœ, ๋ฆฌ๋ทฐ ๋ผ์šด๋“œ๋ฅผ `(loop_id, base, head, tree)` ์— ๋ฌถ๋Š” ๋ฆฌ๋ทฐ identity ์™€ ๋ฃจํ”„ ์˜ต์…˜ 2๊ฐœ(`--review-isolation`, `--remote-review`), ์ˆ˜์šฉ ์‚ฌ๋ก€ 8๊ฑด์ด ์ถ”๊ฐ€๋˜์–ด MINOR ๋กœ ์˜ฌ๋ฆฐ๋‹ค. ํ•ญ๋ชฉ 8(๋‹ค์ค‘ ๋ฃจํ”„ยท๋ฆฌ๋ทฐ์–ด ์Šค์ผ€์ค„๋Ÿฌ)์€ ๋ณด๋ฅ˜๋กœ ๋‚จ์•„ #21 ์€ ์—ด์–ด ๋‘”๋‹ค. ํ•จ๊ป˜ #20(ํ…Œ์ŠคํŠธ๊ฐ€ ์‹ค์ œ `.mam` ์„ ์˜ค์—ผ), #27(`.gitignore` ๊ด€๋ฆฌ ๋ธ”๋ก์˜ `.venv`), #28(Linux/py3.14 ํ˜ธ์ŠคํŠธ์˜ ํ…Œ์ŠคํŠธ ์‹คํŒจ 33๊ฑด)์„ ๋‹ซ๋Š”๋‹ค. diff --git a/tests/test_issue25_plan_reuse.py b/tests/test_issue25_plan_reuse.py new file mode 100644 index 0000000..5296967 --- /dev/null +++ b/tests/test_issue25_plan_reuse.py @@ -0,0 +1,374 @@ +"""Issue #25: a promoted plan is reused only when it is named, and an empty +plan report is never silent. + +Without --plan the runner used to glob the auto-resolved planner's report +directory for plan-*.md and feed the newest one to the Creator as "the plan +already agreed on". Both keys are wrong: the directory belongs to a session, +not to this task, and mtime is checkout order. 47 unrelated plans are committed +in this repository (P1). + +The same review also found that the three --plan-mode reads had no content +check at all, so an empty planner or refine report silently blanked the plan +and switched the Creator's prompt (P9-P11). + +Every test writes only under tmp_path (Issue #20). +""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent +RUNNER_PY = REPO_ROOT / ".agents" / "skills" / "lib_py" / "loop" / "runner.py" +RUN_LOOP_SH = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "run_loop.sh" + +GIT_ID = {"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@example.invalid", + "GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@example.invalid"} + +PLANNER = "planner-claude-01" +CREATOR = "creator-claude-01" +REVIEWER = "reviewer-claude-01" + +OLD_PLAN = "# plan for a completely different issue\n\nOLD-PLAN-MARKER\n" +NEW_PLAN = "# the plan I actually mean\n\nNAMED-PLAN-MARKER\n" +PASS_REPORT = "looks fine\n\n[VERDICT: PASS]\n" +FAIL_REPORT = "line 3 is wrong\n\n[VERDICT: NOT PASS]\n" +FAIL_ESCALATE = "needs a redesign\n\n[ESCALATE: PLANNER]\n\n[VERDICT: NOT PASS]\n" + +SELF_PLAN_PROMPT = "๊ณ„ํš์„œ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ" +EXISTING_PLAN_PROMPT = "์ด๋ฏธ ์ˆ˜๋ฆฝ๋œ" +CORRECTION_WITH_PLAN = "๊ฐฑ์‹ ๋œ ๋‹ค์Œ ๊ณ„ํš์„œ์— ์ž…๊ฐํ•˜์—ฌ" + + +def _git(cwd: Path, *args: str) -> str: + return subprocess.run(["git", "-C", str(cwd), *args], capture_output=True, text=True, + check=True, env={**os.environ, **GIT_ID}).stdout.strip() + + +@pytest.fixture +def ctl(tmp_path, monkeypatch): + """A control root whose planner report directory already holds an unrelated + promoted plan - the condition that used to inject it (F1/F3).""" + main = tmp_path / "main" + main.mkdir() + _git(main, "init", "-q") + (main / "a.txt").write_text("a\n") + _git(main, "add", "a.txt") + _git(main, "commit", "-q", "-m", "init") + + stale = main / ".agents" / "reports" / PLANNER / "plan-OLD.md" + stale.parent.mkdir(parents=True) + stale.write_text(OLD_PLAN, encoding="utf-8") + os.utime(stale, (2_000_000_000, 2_000_000_000)) # newest by mtime, by a mile + + for k, v in GIT_ID.items(): + monkeypatch.setenv(k, v) + monkeypatch.setenv("WORKSPACE_ROOT", str(main)) + monkeypatch.delenv("MAM_REAL_ROOT", raising=False) + monkeypatch.setenv("MAM_JOBS_DIR", str(main / ".mam" / "jobs")) + monkeypatch.delenv("MAM_LOOP_WORKTREE", raising=False) + monkeypatch.delenv("MAM_LOOP_BASE", raising=False) + # the planner row is what makes the old glob fire at all (F7) + monkeypatch.setenv("MAM_STATE_JSON", json.dumps({"herdr_sessions": [ + {"name": CREATOR, "agent": "claude", "role": "creator", "status": "running"}, + {"name": REVIEWER, "agent": "claude", "role": "reviewer", "status": "running"}, + {"name": PLANNER, "agent": "claude", "role": "planner", "status": "running"}, + ]})) + return main + + +def _runner(*extra): + from lib_py.loop import runner as lr + return lr.LoopRunner(["--creator", CREATOR, "--task", "t25", *extra]) + + +def _drive(monkeypatch, tmp_path, argv, reports=None, on_worker=None, max_loop="1"): + """run() end to end with the delegation stubbed. + + `reports` maps (role, nth-call-of-that-role) to the report body; a body of + None means the report file is missing. Everything else gets a PASS for a + Reviewer and a one-line report otherwise. The existing _drive helpers hand + every job the same non-empty PASS report, which cannot reach the empty-plan + paths at all (F13). + + Returns (rc, [(role, prompt)], runner). + """ + from lib_py.loop import runner as lr + monkeypatch.setattr(lr.signal, "signal", lambda *a, **k: None) + r = lr.LoopRunner(["--creator", CREATOR, "--task", "t25", + "--reviewer", REVIEWER, "--max-loop", max_loop, *argv]) + reports = reports or {} + calls, files, nth = [], {}, {} + stub_dir = tmp_path / "stub-reports" + stub_dir.mkdir(exist_ok=True) + + def fake_delegate(session, agent_type, role, prompt): + calls.append((role, prompt)) + jid = f"j{len(calls)}" + n = nth[role] = nth.get(role, 0) + 1 + body = reports.get((role, n), PASS_REPORT if role == "Reviewer" else "done\n") + if body is None: + files[jid] = None + else: + f = stub_dir / f"{jid}.md" + f.write_text(body, encoding="utf-8") + files[jid] = f + if role == "Worker" and on_worker: + on_worker(r) + return jid + + monkeypatch.setattr(r, "delegate_job", fake_delegate) + monkeypatch.setattr(r, "wait_for_job", lambda *a, **k: True) + monkeypatch.setattr(r, "find_report", lambda jid: files.get(jid)) + return r.run(), calls, r + + +def _roles(calls, role): + return [p for (ro, p) in calls if ro == role] + + +# --------------------------------------------------------------------------- +# P1-P2 โ€” reuse is explicit +# --------------------------------------------------------------------------- + +def test_p1_no_options_does_not_inject_an_unrelated_plan(ctl, monkeypatch, tmp_path, capsys): + """The #25 reproduction: a running planner plus a promoted plan in its + report directory must not reach the Creator. Fails before the fix.""" + rc, calls, _ = _drive(monkeypatch, tmp_path, []) + assert rc == 0 + worker = _roles(calls, "Worker")[0] + assert worker.startswith(SELF_PLAN_PROMPT) + assert "OLD-PLAN-MARKER" not in worker + assert EXISTING_PLAN_PROMPT not in worker + out = capsys.readouterr().out + assert "Found existing promoted plan" not in out + assert "No plan loaded" in out + + +def test_p2_plan_file_is_the_only_plan_and_lives_one_round(ctl, monkeypatch, tmp_path): + """The named plan is used, the newer unrelated one is not, and the plan is + not repeated in the correction round (N1/D16 - the behaviour the implicit + load had too).""" + named = tmp_path / "my-plan.md" + named.write_text(NEW_PLAN, encoding="utf-8") + rc, calls, _ = _drive(monkeypatch, tmp_path, ["--plan-file", str(named)], + reports={("Reviewer", 1): FAIL_REPORT}, max_loop="2") + assert rc == 0 + workers = _roles(calls, "Worker") + assert workers[0].startswith(EXISTING_PLAN_PROMPT) + assert "NAMED-PLAN-MARKER" in workers[0] + assert "OLD-PLAN-MARKER" not in workers[0] + + assert len(workers) == 2, "a correction round ran" + assert "NAMED-PLAN-MARKER" not in workers[1] + assert workers[1].startswith("๋ฆฌ๋ทฐ์–ด๋“ค์ด ์ง€์ ํ•œ ๋‹ค์Œ ํ”ผ๋“œ๋ฐฑ") + + +# --------------------------------------------------------------------------- +# P3-P5 โ€” the option is checked before any job exists +# --------------------------------------------------------------------------- + +def test_p3_unusable_plan_files_are_refused_before_any_job(ctl, tmp_path, monkeypatch): + empty, ws, sub = tmp_path / "empty.md", tmp_path / "ws.md", tmp_path / "sub" + empty.write_text("") + ws.write_text(" \n\t\n") + sub.mkdir() + cases = [tmp_path / "nope.md", sub, empty, ws] + + if os.getuid() != 0: + locked = tmp_path / "locked.md" + locked.write_text(NEW_PLAN, encoding="utf-8") + locked.chmod(0o000) + cases.append(locked) + + from lib_py.loop import runner as lr + for path in cases: + r = lr.LoopRunner(["--creator", CREATOR, "--task", "t25", "--plan-file", str(path)]) + monkeypatch.setattr(r, "delegate_job", lambda *a, **k: pytest.fail("a job was delegated")) + with pytest.raises(SystemExit) as e: + r.parse_args() + assert e.value.code == 1, path + + +def test_p3b_the_refusal_names_the_resolved_path(ctl, tmp_path, capsys): + missing = tmp_path / "nope.md" + with pytest.raises(SystemExit): + _runner("--plan-file", str(missing)).parse_args() + assert str(missing.resolve()) in capsys.readouterr().err + + +def test_p3c_a_relative_plan_file_is_taken_against_the_control_root(ctl, tmp_path, monkeypatch): + """R1/N1: the usage line promises control-root-relative, and the runner + never chdirs to the control root. Resolving against the caller's cwd looked + somewhere else - and a same-named file there won silently, which is the + very failure #25 is about.""" + (ctl / "plans").mkdir() + (ctl / "plans" / "named.md").write_text(NEW_PLAN, encoding="utf-8") + caller = tmp_path / "caller" + (caller / "plans").mkdir(parents=True) + monkeypatch.chdir(caller) + + r = _runner("--plan-file", "plans/named.md") + r.parse_args() + assert Path(r.plan_file) == (ctl / "plans" / "named.md").resolve() + assert "NAMED-PLAN-MARKER" in r.current_plan + + # the same name in the caller's cwd must not win + (caller / "plans" / "named.md").write_text(OLD_PLAN, encoding="utf-8") + r = _runner("--plan-file", "plans/named.md") + r.parse_args() + assert Path(r.plan_file) == (ctl / "plans" / "named.md").resolve() + assert "NAMED-PLAN-MARKER" in r.current_plan + assert "OLD-PLAN-MARKER" not in r.current_plan + + +def test_p3d_an_absolute_plan_file_is_left_alone(ctl, tmp_path, monkeypatch): + """Anchoring the relative case must not rewrite absolute or ~ paths.""" + named = tmp_path / "abs-plan.md" + named.write_text(NEW_PLAN, encoding="utf-8") + monkeypatch.chdir(tmp_path) + r = _runner("--plan-file", str(named)) + r.parse_args() + assert Path(r.plan_file) == named.resolve() + + monkeypatch.setenv("HOME", str(tmp_path)) + home_plan = tmp_path / "home-plan.md" + home_plan.write_text(NEW_PLAN, encoding="utf-8") + r = _runner("--plan-file", "~/home-plan.md") + r.parse_args() + assert Path(r.plan_file) == home_plan.resolve() + + +def test_p4_plan_file_conflicts_with_plan_and_needs_a_value(ctl, tmp_path, monkeypatch): + named = tmp_path / "my-plan.md" + named.write_text(NEW_PLAN, encoding="utf-8") + from lib_py.loop import runner as lr + for argv in (["--plan-file", str(named), "--plan"], ["--plan", "--plan-file", str(named)], + ["--plan-file"]): + r = lr.LoopRunner(["--creator", CREATOR, "--task", "t25", *argv]) + monkeypatch.setattr(r, "delegate_job", lambda *a, **k: pytest.fail("a job was delegated")) + with pytest.raises(SystemExit) as e: + r.parse_args() + assert e.value.code == 1, argv + + +def _reviewable_worktree(ctl, tmp_path): + """A linked worktree with one commit to review. The source root has to be + separate from the control root here: --base turns review identity on, and + the loop's own checkpoint under the control root's .mam would otherwise + count as the source changing mid-review.""" + wt = tmp_path / "wt" + _git(ctl, "worktree", "add", "-q", "-b", "feat", str(wt)) + base = _git(wt, "rev-parse", "HEAD") + (wt / "a.txt").write_text("a\nimplemented\n") + _git(wt, "commit", "-qam", "impl") + return ["--worktree", str(wt), "--base", base, "--review-only"] + + +def test_p5_plan_file_is_warned_about_and_ignored_with_review_only(ctl, monkeypatch, tmp_path, capsys): + named = tmp_path / "my-plan.md" + named.write_text(NEW_PLAN, encoding="utf-8") + rc, calls, r = _drive(monkeypatch, tmp_path, + ["--plan-file", str(named), *_reviewable_worktree(ctl, tmp_path)]) + assert rc == 0 + assert _roles(calls, "Worker") == [], "Phase 2 is skipped" + assert "NAMED-PLAN-MARKER" not in _roles(calls, "Reviewer")[0] + assert r.current_plan == "" + assert "--plan-file is ignored with --review-only" in capsys.readouterr().out + + +def test_p5b_the_ignore_warning_needs_the_option(ctl, monkeypatch, tmp_path, capsys): + """The warning is keyed on the option being given, not on a default value - + unlike the --plan-talk warning it sits next to (N2, runner.py:246).""" + rc, _, _ = _drive(monkeypatch, tmp_path, _reviewable_worktree(ctl, tmp_path)) + assert rc == 0 + assert "is ignored with --review-only" not in capsys.readouterr().out + + +# --------------------------------------------------------------------------- +# P6, P9-P11 โ€” --plan mode: compatible, and never silently planless +# --------------------------------------------------------------------------- + +def test_p6_plan_mode_still_plans_and_promotes(ctl, monkeypatch, tmp_path): + rc, calls, r = _drive(monkeypatch, tmp_path, ["--plan", "--plan-talk", "0"], + reports={("Planner", 1): NEW_PLAN}) + assert rc == 0 + worker = _roles(calls, "Worker")[0] + assert worker.startswith(EXISTING_PLAN_PROMPT) + assert "NAMED-PLAN-MARKER" in worker + promoted = ctl / ".agents" / "reports" / PLANNER / f"plan-{r.plan_job_id}.md" + assert promoted.is_file() and "NAMED-PLAN-MARKER" in promoted.read_text() + + +@pytest.mark.parametrize("body", ["", " \n\t\n"], ids=["empty", "whitespace"]) +def test_p9_an_empty_initial_plan_stops_the_run(ctl, monkeypatch, tmp_path, capsys, body): + """D13: paying for a planning phase and getting nothing back must not be a + quiet demotion to self-planning.""" + rc, calls, _ = _drive(monkeypatch, tmp_path, ["--plan", "--plan-talk", "0"], + reports={("Planner", 1): body}) + assert rc == 1 + assert _roles(calls, "Worker") == [], "no implementation was delegated" + assert "has no plan text" in capsys.readouterr().err + + +def test_p10_an_empty_refine_keeps_the_previous_plan(ctl, monkeypatch, tmp_path, capsys): + """D14: the discussion turn must not blank a plan that is still good.""" + rc, calls, _ = _drive(monkeypatch, tmp_path, ["--plan", "--plan-talk", "1"], + reports={("Planner", 1): NEW_PLAN, ("Planner", 2): ""}) + assert rc == 0 + worker = _roles(calls, "Worker")[1] # [0] is the Creator's challenge turn + assert worker.startswith(EXISTING_PLAN_PROMPT) + assert "NAMED-PLAN-MARKER" in worker + assert "keeping the previous plan" in capsys.readouterr().out + + +def test_p11_an_empty_escalation_refine_never_claims_an_empty_plan(ctl, monkeypatch, tmp_path, capsys): + """D14/D15: the correction prompt asserts "follow the updated plan below", + so the plan it carries must never be blank.""" + rc, calls, _ = _drive(monkeypatch, tmp_path, ["--plan", "--plan-talk", "0"], + reports={("Planner", 1): NEW_PLAN, ("Planner", 2): "", + ("Reviewer", 1): FAIL_ESCALATE}, + max_loop="2") + assert rc == 0 + correction = _roles(calls, "Worker")[1] + assert CORRECTION_WITH_PLAN in correction + assert "NAMED-PLAN-MARKER" in correction, "the prompt claims a plan; it must carry one" + assert "keeping the previous plan" in capsys.readouterr().out + + +# --------------------------------------------------------------------------- +# P7-P8 โ€” wrapper parity and the legacy shell path +# --------------------------------------------------------------------------- + +def test_p7_wrapper_and_runner_agree_on_the_option(tmp_path): + line = " --plan-file Reuse an existing plan file, relative to the control root (not with --plan)" + sh = subprocess.run(["bash", str(RUN_LOOP_SH), "--help"], capture_output=True, text=True) + assert line in sh.stdout.splitlines() + + code = ("import sys; sys.path.insert(0, %r)\n" + "from lib_py.loop import runner as lr\n" + "lr.LoopRunner([]).print_usage()\n" % str(REPO_ROOT / ".agents" / "skills")) + py = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True) + assert line in py.stdout.splitlines() + + +def test_p7b_the_legacy_shell_loop_refuses_the_option(tmp_path): + marker = tmp_path / "loop.marker" + res = subprocess.run(["bash", str(RUN_LOOP_SH), "--creator", CREATOR, "--task", "t25", + "--plan-file", str(tmp_path / "x.md")], + capture_output=True, text=True, cwd=str(tmp_path), + env={**os.environ, "MAM_LOOP_PY": "0", "MAM_LOOP_MARKER": str(marker)}) + assert res.returncode == 1 + assert "--plan-file" in res.stderr + assert not marker.exists(), "refused before the loop lock" + + +def test_p8_neither_loop_implementation_globs_for_a_plan_any_more(): + for path in (RUN_LOOP_SH, RUNNER_PY): + assert "plan-*.md" not in path.read_text(encoding="utf-8"), path