fix(issue-25): 승격된 계획서 재사용을 --plan-file 로만 허용 — 암묵적 자동 로드 제거 #32

Merged
Godopu merged 6 commits from fix/issue-25 into main 2026-09-21 15:38:21 +00:00
Owner

개요

Closes #25.

run_loop.sh 를 --plan 없이 실행하면 러너가 planner 를 자동으로 고르고 .agents/reports/<planner>/plan-*.md 중 mtime 이 가장 최근인 파일을 "이미 수립된 계획서"로 creator 에게 넘겼다. 저장소에는 다른 이슈의 계획서 47개가 커밋돼 있어, 무관한 계획을 따라 범위 밖 변경이 일어날 수 있었다.

main 6fc9194 기준 5커밋, 4파일 +486/−31. 구현은 #21 운영 모델대로 워크트리(multi-agent-mux-wt/issue-25, fix/issue-25)의 전용 creator 가 했고, 리뷰는 --review-only 루프로 돌렸다.

변경 사항

커밋 내용
a28711c 승격된 계획서의 암묵적 자동 로드를 제거하고, 재사용은 --plan-file <path> 로만 허용
e551e3b 모든 계획 로드 경로에 계획 텍스트 검사를 넣고, 빈 planner 리포트면 루프를 멈추며 빈 refine 은 기존 계획을 유지
a334083 회귀 테스트 (tests/test_issue25_plan_reuse.py)
5fa5923 루프 스킬 문서에 --plan-file 과 빈 리포트 처리 반영
1e82651 리뷰 반영: 상대경로 --plan-file 을 사용법·문서대로 제어 루트 기준으로 해석

리뷰 (multi-agent-mux-loop, --worktree + --base + --review-only)

리뷰어 5명(claude·agy·grok·codex·opencode).

  • 1회차 (5fa5923): claude·agy·opencode PASS, grok NOT PASS(N1)·codex NOT PASS(R1) — 두 지적이 같은 결함을 가리켰다. --plan-file 의 상대경로가 실행 cwd 기준으로 해석되는데, 사용법·SKILL.md·테스트는 "제어 루트 기준"을 못 박고 있었다. 워크트리에서 루프를 돌리면 다른 파일을 읽을 수 있다.
  • creator 는 반론 없이 1e82651 로 수정했다.
  • 2회차 (1e82651): 5명 전원 PASS. 판정 identity 5건 모두 base 6fc9194 → head 1e82651, evidence: review-tree(각 리뷰어가 같은 커밋의 detached 사본에서 검증).

테스트

  • Creator: 전체 스위트를 브랜치와 base 양쪽에서 1회씩(각 약 28분). 새로 생긴 실패 0건. 이 호스트의 기준 실패는 12건(모두 tests/test_issue10_13_purge.py) 이며 base 6fc9194 에서도 동일하다. base archive 사본에 .venv 가 없어 생긴 4건은 링크 후 재실행에서 통과.
  • 오케스트레이터: tests/test_issue25_plan_reuse.py + tests/test_loop_cli.py + tests/test_issue5_phase3_loop.py → 38 passed (37.8초).

운영 메모

  • 첫 시도(--plan 전체 루프)의 creator job 은 등록 후 정확히 3600초에 deadline_timeout 으로 닫혔다. heartbeat 는 119회 연속 도착했고 에이전트는 정상이었다 — 검증 계획이 전체 스위트를 두 번 돌려 1시간 예산을 넘겼을 뿐이다. 커밋과 리포트는 그대로 남았고, 이후 --review-only 로 리뷰만 이어서 돌렸다. job 예산을 조절할 수단이 없는 문제는 별도 이슈로 등록했다.
  • 이 브랜치가 병합되기 전까지 본 체크아웃의 루프는 여전히 옛 동작(암묵적 계획 로드)을 한다. 그때까지는 --plan --plan-talk 0 또는 --review-only 를 쓴다.
  • 버전 bump 는 포함하지 않았다(병합 시 처리).

🤖 Generated with Claude Code

## 개요 Closes #25. `run_loop.sh` 를 `--plan` 없이 실행하면 러너가 planner 를 자동으로 고르고 `.agents/reports/<planner>/plan-*.md` 중 mtime 이 가장 최근인 파일을 "이미 수립된 계획서"로 creator 에게 넘겼다. 저장소에는 다른 이슈의 계획서 47개가 커밋돼 있어, 무관한 계획을 따라 범위 밖 변경이 일어날 수 있었다. main `6fc9194` 기준 5커밋, 4파일 +486/−31. 구현은 #21 운영 모델대로 워크트리(`multi-agent-mux-wt/issue-25`, `fix/issue-25`)의 전용 creator 가 했고, 리뷰는 `--review-only` 루프로 돌렸다. ## 변경 사항 | 커밋 | 내용 | |---|---| | `a28711c` | 승격된 계획서의 암묵적 자동 로드를 제거하고, 재사용은 `--plan-file <path>` 로만 허용 | | `e551e3b` | 모든 계획 로드 경로에 계획 텍스트 검사를 넣고, 빈 planner 리포트면 루프를 멈추며 빈 refine 은 기존 계획을 유지 | | `a334083` | 회귀 테스트 (`tests/test_issue25_plan_reuse.py`) | | `5fa5923` | 루프 스킬 문서에 `--plan-file` 과 빈 리포트 처리 반영 | | `1e82651` | 리뷰 반영: 상대경로 `--plan-file` 을 사용법·문서대로 **제어 루트 기준**으로 해석 | ## 리뷰 (multi-agent-mux-loop, `--worktree` + `--base` + `--review-only`) 리뷰어 5명(claude·agy·grok·codex·opencode). - **1회차** (`5fa5923`): claude·agy·opencode PASS, **grok NOT PASS(N1)·codex NOT PASS(R1)** — 두 지적이 같은 결함을 가리켰다. `--plan-file` 의 상대경로가 실행 cwd 기준으로 해석되는데, 사용법·SKILL.md·테스트는 "제어 루트 기준"을 못 박고 있었다. 워크트리에서 루프를 돌리면 다른 파일을 읽을 수 있다. - creator 는 반론 없이 `1e82651` 로 수정했다. - **2회차** (`1e82651`): **5명 전원 PASS**. 판정 identity 5건 모두 `base 6fc9194 → head 1e82651`, `evidence: review-tree`(각 리뷰어가 같은 커밋의 detached 사본에서 검증). ## 테스트 - Creator: 전체 스위트를 브랜치와 base 양쪽에서 1회씩(각 약 28분). 새로 생긴 실패 0건. 이 호스트의 기준 실패는 **12건(모두 `tests/test_issue10_13_purge.py`)** 이며 base `6fc9194` 에서도 동일하다. base archive 사본에 `.venv` 가 없어 생긴 4건은 링크 후 재실행에서 통과. - 오케스트레이터: `tests/test_issue25_plan_reuse.py` + `tests/test_loop_cli.py` + `tests/test_issue5_phase3_loop.py` → **38 passed** (37.8초). ## 운영 메모 - 첫 시도(`--plan` 전체 루프)의 creator job 은 등록 후 정확히 3600초에 `deadline_timeout` 으로 닫혔다. heartbeat 는 119회 연속 도착했고 에이전트는 정상이었다 — 검증 계획이 전체 스위트를 두 번 돌려 1시간 예산을 넘겼을 뿐이다. 커밋과 리포트는 그대로 남았고, 이후 `--review-only` 로 리뷰만 이어서 돌렸다. job 예산을 조절할 수단이 없는 문제는 별도 이슈로 등록했다. - 이 브랜치가 병합되기 전까지 본 체크아웃의 루프는 여전히 옛 동작(암묵적 계획 로드)을 한다. 그때까지는 `--plan --plan-talk 0` 또는 `--review-only` 를 쓴다. - 버전 bump 는 포함하지 않았다(병합 시 처리). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Godopu added 5 commits 2026-09-21 14:34:18 +00:00
Without --plan, Phase 1 resolved a planner session automatically and globbed
<workspace_root>/.agents/reports/<planner>/plan-*.md for the newest file by
mtime, handing it to the Creator as "the plan already agreed on". Both keys are
wrong: the report directory belongs to a session, not to this task, so other
issues' plans pile up in it, and right after a clone or checkout mtime is
checkout order. 47 such plans are committed in this repository, so an ordinary
run could silently implement against an unrelated design.

Reuse is explicit now. --plan-file <path> is the only way to load an existing
plan; it is resolved against the control root, checked before a single job
exists, and refused if the file is missing, not a regular file, unreadable or
empty (an empty file would quietly turn into a "no plan" prompt). It cannot be
combined with --plan, and --review-only warns and ignores it because no
implementation job is delegated there. The implementation prompt itself is
unchanged, so the Creator's contract does not move.

Phase 1 now says which plan it loaded and where from, or that it loaded none.
The legacy shell loop (MAM_LOOP_PY=0) loses the same auto-load and rejects
--plan-file rather than duplicating the check on a path that is on its way out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The --plan-file check landed on the one path a human is watching. The three
--plan-mode loads read a report written by another agent with nobody watching,
and had no content check at all: find_report only tests that the path exists.

An empty or whitespace-only report therefore blanked current_plan in silence.
Phase 2's `if self.current_plan` then flipped the prompt to self-planning and
the run finished rc 0, having paid for a planner job, a Creator challenge and a
refine job and thrown the result away. The escalation refine was worse: it has
no guard, so the correction prompt asserted "follow the updated plan below" and
carried nothing.

All four loads share _plan_text() now - a plan is a readable regular file with
text in it - and each caller says what None means. An empty initial plan stops
the run instead of demoting it, and an empty refine warns and keeps the plan it
already has, which is still good. Those two together leave the escalation
prompt unable to claim a plan it does not carry, so it needs no new branch.

Found by the Creator's challenge to the plan (job 0f63ac1d), sustained by the
Planner as revision 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
P1 is the reproduction: a running planner plus a promoted plan in its report
directory must not reach the Creator. P2 pins that a named plan is used, that
the newer unrelated one is not, and that the plan rides only the first
implementation prompt - the correction round carries feedback alone, as the
implicit load did too. P3-P5 keep the option's checks in front of any job.
P6 keeps --plan mode and its promotion working.

P9-P11 cover the loads the review widened: an empty initial plan stops the run,
an empty discussion refine keeps the previous plan, and an empty escalation
refine leaves the "follow the updated plan below" prompt carrying the real one.
The existing _drive helpers hand every job the same non-empty PASS report and
so cannot reach these paths at all, so this module drives the loop with its own
stub that scripts a report per job. P7-P8 pin wrapper/runner parity, the legacy
shell refusal, and that neither implementation globs for a plan any more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The skill described the implicit load as a feature in five places. Replace them
with what the loop does now: nothing is loaded without --plan-file, the path is
relative to the control root, and the plan rides the first implementation
prompt only. Adds the CLI table row and a section on what an empty plan report
means at each of the four load points.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review R1/N1 (reviewer-grok-01, planner-reviewer-codex-01), reproduced before
fixing. The usage line and SKILL.md both promise "relative to the control
root", but parse_args did Path(...).expanduser().resolve(), which is relative
to the caller's cwd. The runner never chdirs to workspace_root, so the two are
routinely different - the documented recipe runs the loop from the control root
while the source root is a worktree.

Two ways it went wrong: a plan that exists only under the control root was not
found at all (exit 1), and a same-named file in the caller's cwd was loaded
instead, quietly putting an unrelated plan into the first implementation
prompt. That second one is the very failure #25 is about, reintroduced through
the option meant to replace it.

Resolve against workspace_root the way jobs_dir already does (Issue #21 R2);
pathlib's `/` leaves an absolute or expanded-home path untouched. Every earlier
test passed an absolute tmp_path, which is why none of them caught this: P3c
covers both failure modes with the control root away from the caller's cwd, and
P3d pins that absolute and ~ paths are not rewritten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Godopu added 1 commit 2026-09-21 15:38:11 +00:00
Issue #25: a promoted plan is reused only with --plan-file; the implicit
newest-plan load is gone in both runners, and an empty planner report no
longer passes silently. VERSIONS.md carries the entry; the four merge-side
review reports and their identity sidecars (base 6fc9194 -> head 1e82651,
evidence review-tree) are archived under .agents/reports/<session>/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
Godopu merged commit 1e78dae6eb into main 2026-09-21 15:38:21 +00:00
Author
Owner

병합 완료 — main 1e78dae (v4.7.1, Gitea merge 커밋)

병합 워크스페이스 재검토: PR head 1e82651 을 자체 venv 가 있는 detached 워크트리에 받아 --worktree --base 6fc9194 --review-only --all-reviewer 로 돌렸다. claude·agy·grok·codex 4인 전원 PASS(1회차), identity 4건 모두 base 6fc9194 → head 1e82651, evidence review-tree. claude 리뷰어는 새 테스트 17건 중 13건이 수정 전 소스에서 실패함을 직접 확인했다. 전체 스위트 @1e82651: 1037 통과 / 1 skip / 실패 0.

릴리스 커밋 6b7198f 로 4.7.0 → 4.7.1 락스텝 bump, 리포트·identity 사이드카 8개를 .agents/reports/<session>/ 에 아카이브했다. #25 는 자동으로 닫혔다.

비차단 후속(리뷰어 지적)

  • E1: 레거시 셸 러너(MAM_LOOP_PY=0)의 plan 모드 읽기(run_loop.sh 의 CURRENT_PLAN=$(cat …))에는 빈 계획서 검사가 없다. SKILL.md 의 "계획서가 비어 있을 때" 절에 "Python 러너 기준"을 명시하거나 한 줄 거부를 넣는다.
  • E2: --plan-file 로 계획을 실은 실행에서 [ESCALATE: PLANNER] 는 재계획 없이 일반 수정 라운드로 내려간다(plan_mode 게이트, PR 이전부터의 동작). 문서에 없다.
  • E3: 리뷰 격리 워크트리에는 .venv 가 없어 run_loop.sh 를 하위 프로세스로 띄우는 테스트는 AGENT_PYTHON_BIN 없이는 실패한다. #21 항목 5 의 운영 후속으로 #21 에 기록했다.
  • E4: Phase 1 의 os.path.getmtime(self.plan_file) 는 파싱과 Phase 1 사이에 파일이 사라지면 트레이스백으로 죽는다(극히 좁은 경합). 공백이 든 --plan-file 경로 전용 테스트도 없다.
## 병합 완료 — main `1e78dae` (v4.7.1, Gitea merge 커밋) **병합 워크스페이스 재검토**: PR head `1e82651` 을 자체 venv 가 있는 detached 워크트리에 받아 `--worktree --base 6fc9194 --review-only --all-reviewer` 로 돌렸다. claude·agy·grok·codex **4인 전원 PASS(1회차)**, identity 4건 모두 base `6fc9194` → head `1e82651`, evidence `review-tree`. claude 리뷰어는 새 테스트 17건 중 13건이 수정 전 소스에서 실패함을 직접 확인했다. 전체 스위트 @1e82651: **1037 통과 / 1 skip / 실패 0**. 릴리스 커밋 `6b7198f` 로 4.7.0 → 4.7.1 락스텝 bump, 리포트·identity 사이드카 8개를 `.agents/reports/<session>/` 에 아카이브했다. #25 는 자동으로 닫혔다. **비차단 후속(리뷰어 지적)** - E1: 레거시 셸 러너(`MAM_LOOP_PY=0`)의 plan 모드 읽기(`run_loop.sh` 의 `CURRENT_PLAN=$(cat …)`)에는 빈 계획서 검사가 없다. SKILL.md 의 "계획서가 비어 있을 때" 절에 "Python 러너 기준"을 명시하거나 한 줄 거부를 넣는다. - E2: `--plan-file` 로 계획을 실은 실행에서 `[ESCALATE: PLANNER]` 는 재계획 없이 일반 수정 라운드로 내려간다(`plan_mode` 게이트, PR 이전부터의 동작). 문서에 없다. - E3: 리뷰 격리 워크트리에는 `.venv` 가 없어 `run_loop.sh` 를 하위 프로세스로 띄우는 테스트는 `AGENT_PYTHON_BIN` 없이는 실패한다. #21 항목 5 의 운영 후속으로 #21 에 기록했다. - E4: Phase 1 의 `os.path.getmtime(self.plan_file)` 는 파싱과 Phase 1 사이에 파일이 사라지면 트레이스백으로 죽는다(극히 좁은 경합). 공백이 든 `--plan-file` 경로 전용 테스트도 없다.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tmpl/multi-agent-mux#32