fix(#34): 루프가 creator 의 하위 위임 잡까지 기다리고, 리뷰어에게 하위 잡 상태를 근거로 제공 #38

Merged
Godopu merged 9 commits from fix/issue-34 into main 2026-09-25 07:44:35 +00:00
Owner

Closes #34

creator 가 작업 일부를 delegate-job 으로 다른 에이전트에게 맡기면, 그 하위 잡이 아직 돌고 있어도 루프가 리뷰로 넘어가 미완성 결과물이 PASS 되던 문제를 고친다. 루프는 루프가 직접 만든 잡만 감시했고, 잡 레코드에는 부모·자식 관계가 없었다.

변경 요지

잡이 부모를 기록한다. delegate-job submit --parent-job <id> 로 이 잡이 어느 잡에서 위임됐는지 등록부에 남긴다. 부모 잡이 등록부에 없으면 등록을 거부한다. 기본값은 원격 러너가 넣어 주는 MAM_JOB_ID 이며, 그 잡이 이 등록부에 없으면 경고만 하고 부모 없이 등록한다. registry.py list --parent <id> 로 직계 자식을 볼 수 있다.

에이전트에게 부모 ID 를 전달한다. 로컬 에이전트는 herdr pane 에서 오래 떠 있는 프로세스라 잡마다 환경변수를 넣을 수 없다. 그래서 잡마다 새로 주입되는 안내문과 brief.md 에 "하위 위임 시 --parent-job <이 잡 ID> 를 붙여라"를 적는다.

루프가 자손 잡까지 기다린다. creator 잡(구현·수정·순응)이 끝난 뒤 그 자손 잡이 모두 종결될 때까지 기다린 다음 리뷰로 넘어간다. 자손은 폴링마다 다시 읽으므로 손자 잡도 잡힌다. 깊이 제한은 없고 상한은 둘이다. MAM_CHILD_WAIT_S 는 creator 잡 종결부터의 대기 상한으로 기본 3900초다. 각 하위 잡은 #33 의 3600초 예산으로 닫히므로 보통 그 전에 끝난다. MAM_JOB_TREE_MAX 는 잡 수 상한으로 기본 50개이며, 넘으면 기다리지 않는다. --wait-open-jobs 를 주면 --parent-job 없이 등록된 열린 잡까지 기다린다. 무관한 잡이 루프를 붙잡을 수 있어 기본은 꺼져 있다.

리뷰어가 하위 잡 상태를 본다. 리뷰 프롬프트에 해당 작업의 하위 잡 표(상태·종료 사유)를 근거로 넣고, 열린 하위 잡이나 실패한 하위 잡이 있으면 판정에 반영하게 한다. 수정 라운드에서도 앞선 creator 잡의 하위 잡이 표에 남는다.

하위 잡이 있을 때 리뷰 중 소스 변경은 승인을 무효로 한다. 하위 잡이 리뷰 도중 파일을 고칠 수 있기 때문이다. 워크트리 옵션 없이 도는 루프에도 적용하되, 하위 잡이 있는 실행에서만 켠다. 하위 잡이 없는 기존 루프의 동작은 바뀌지 않는다.

같은 서버의 다른 워크스페이스 에이전트에게 위임할 수 있다. herdr pane 은 자기 워크스페이스 ID(HERDR_WORKSPACE_ID)를 내보내고, 이 값이 스코프 필터로 쓰여 다른 워크스페이스의 에이전트가 보이지 않았다. 워크트리 모델에서는 creator 가 자기 워크스페이스에 따로 뜨므로, 워크트리 creator 의 하위 위임은 기본적으로 여기에 걸렸다. 이제 pane 안의 호출자가 이름으로 대상을 찾을 때(agent·has-session·kill-session) 그 ID 가 HERDR_PANE_ID 의 워크스페이스와 같으면 뗀다. herdr 문서에 따르면 살아 있는 에이전트 이름은 서버 단위로 유일하므로 다른 에이전트를 잘못 집을 수 없다. pane 밖에서 일부러 준 스코프, pane 과 어긋나는 스코프, pane·세션 생성 호출은 그대로다. 대상이 보이지 않아 취소된 잡은 session_busy 가 아니라 target_not_found 로 기록된다.

검증

항목 결과
전체 스위트 (ed293dc) 1130 통과 / 12 실패 / 19 건너뜀
리뷰어 5명 전체 루프 2회차 전원 PASS, 후속 델타(3fdbb62..ed293dc) 전원 PASS
tests/test_issue34_job_tree.py + test_issue23_24_startup_path.py 87 통과

실패 12건은 모두 이 호스트의 기준 실패(tests/test_issue10_13_purge.py)이며 base 에서도 같다. 새 테스트는 실제 delegate-job 래퍼로 하위 잡을 등록하고 실제 등록부를 읽는다. 첫 루프 시점의 새 테스트 14건은 수정 전 코드(7a4a0c2)에서 모두 실패한다.

실제 에이전트 시험. 테스트는 에이전트가 --parent-job 을 붙인다고 가정할 뿐 증명하지 못하므로, 실제 creator 에게 "작업 일부를 opencode 에게 위임하라"고만 지시하고 방법은 알려주지 않았다.

  • 1차(3fdbb62): creator 가 안내문을 읽고 스스로 --parent-job 을 붙였고, 하위 잡 레코드에 부모 ID 가 기록됐다. 다만 첫 시도가 취소됐다. 워크스페이스 w6 의 creator 가 w2 의 opencode 를 스코프 필터 때문에 보지 못한 것이다. creator 가 환경변수를 떼고 재시도해서 성공했다.
  • 2차(ed293dc): 환경변수를 건드리지 말라고 명시했다. 첫 호출에서 바로 위임에 성공했고 부모 ID 가 기록됐다.

리뷰와 검증에서 나온 지적

  • codex, 1차 리뷰. 수정 라운드에서 앞선 creator 잡의 하위 잡이 근거 표에서 빠졌다. 3fdbb62 로 고쳤다.
  • 오케스트레이터, 전체 스위트. 리뷰 중 소스 변경 규칙을 옵션 없는 루프에 무조건 적용하자 test_tier4_e2e.py::test_e2e_scenario5_multi_agent_review_loop 가 실패했다. 수정 전 코드에서는 통과하는 결정적 회귀였다. 샌드박스의 가짜 herdr 상태 파일이 매 라운드 바뀌어 라운드가 계속 무효가 됐다. 1d4f338 이 이 규칙을 하위 잡이 있을 때만 켠다.
  • 오케스트레이터, 실제 시험. 위의 같은 서버·다른 워크스페이스 문제. ed293dc 로 고쳤다.

범위 밖

멈춘 에이전트 감지(#30)와 잡 제한 시간 조절(#33)은 다루지 않는다. 하위 잡이 권한 확인 창에 멈추면 루프는 그 잡이 #33 의 예산으로 닫힐 때까지 기다린다.

🤖 Generated with Claude Code

Closes #34 creator 가 작업 일부를 `delegate-job` 으로 다른 에이전트에게 맡기면, 그 하위 잡이 아직 돌고 있어도 루프가 리뷰로 넘어가 미완성 결과물이 PASS 되던 문제를 고친다. 루프는 루프가 직접 만든 잡만 감시했고, 잡 레코드에는 부모·자식 관계가 없었다. ## 변경 요지 **잡이 부모를 기록한다.** `delegate-job submit --parent-job <id>` 로 이 잡이 어느 잡에서 위임됐는지 등록부에 남긴다. 부모 잡이 등록부에 없으면 등록을 거부한다. 기본값은 원격 러너가 넣어 주는 `MAM_JOB_ID` 이며, 그 잡이 이 등록부에 없으면 경고만 하고 부모 없이 등록한다. `registry.py list --parent <id>` 로 직계 자식을 볼 수 있다. **에이전트에게 부모 ID 를 전달한다.** 로컬 에이전트는 herdr pane 에서 오래 떠 있는 프로세스라 잡마다 환경변수를 넣을 수 없다. 그래서 잡마다 새로 주입되는 안내문과 `brief.md` 에 "하위 위임 시 `--parent-job <이 잡 ID>` 를 붙여라"를 적는다. **루프가 자손 잡까지 기다린다.** creator 잡(구현·수정·순응)이 끝난 뒤 그 자손 잡이 모두 종결될 때까지 기다린 다음 리뷰로 넘어간다. 자손은 폴링마다 다시 읽으므로 손자 잡도 잡힌다. 깊이 제한은 없고 상한은 둘이다. `MAM_CHILD_WAIT_S` 는 creator 잡 종결부터의 대기 상한으로 기본 3900초다. 각 하위 잡은 #33 의 3600초 예산으로 닫히므로 보통 그 전에 끝난다. `MAM_JOB_TREE_MAX` 는 잡 수 상한으로 기본 50개이며, 넘으면 기다리지 않는다. `--wait-open-jobs` 를 주면 `--parent-job` 없이 등록된 열린 잡까지 기다린다. 무관한 잡이 루프를 붙잡을 수 있어 기본은 꺼져 있다. **리뷰어가 하위 잡 상태를 본다.** 리뷰 프롬프트에 해당 작업의 하위 잡 표(상태·종료 사유)를 근거로 넣고, 열린 하위 잡이나 실패한 하위 잡이 있으면 판정에 반영하게 한다. 수정 라운드에서도 앞선 creator 잡의 하위 잡이 표에 남는다. **하위 잡이 있을 때 리뷰 중 소스 변경은 승인을 무효로 한다.** 하위 잡이 리뷰 도중 파일을 고칠 수 있기 때문이다. 워크트리 옵션 없이 도는 루프에도 적용하되, 하위 잡이 있는 실행에서만 켠다. 하위 잡이 없는 기존 루프의 동작은 바뀌지 않는다. **같은 서버의 다른 워크스페이스 에이전트에게 위임할 수 있다.** herdr pane 은 자기 워크스페이스 ID(`HERDR_WORKSPACE_ID`)를 내보내고, 이 값이 스코프 필터로 쓰여 다른 워크스페이스의 에이전트가 보이지 않았다. 워크트리 모델에서는 creator 가 자기 워크스페이스에 따로 뜨므로, 워크트리 creator 의 하위 위임은 기본적으로 여기에 걸렸다. 이제 pane 안의 호출자가 이름으로 대상을 찾을 때(`agent`·`has-session`·`kill-session`) 그 ID 가 `HERDR_PANE_ID` 의 워크스페이스와 같으면 뗀다. herdr 문서에 따르면 살아 있는 에이전트 이름은 서버 단위로 유일하므로 다른 에이전트를 잘못 집을 수 없다. pane 밖에서 일부러 준 스코프, pane 과 어긋나는 스코프, pane·세션 생성 호출은 그대로다. 대상이 보이지 않아 취소된 잡은 `session_busy` 가 아니라 `target_not_found` 로 기록된다. ## 검증 | 항목 | 결과 | |---|---| | 전체 스위트 (`ed293dc`) | 1130 통과 / 12 실패 / 19 건너뜀 | | 리뷰어 5명 | 전체 루프 2회차 전원 PASS, 후속 델타(`3fdbb62..ed293dc`) 전원 PASS | | `tests/test_issue34_job_tree.py` + `test_issue23_24_startup_path.py` | 87 통과 | 실패 12건은 모두 이 호스트의 기준 실패(`tests/test_issue10_13_purge.py`)이며 base 에서도 같다. 새 테스트는 실제 delegate-job 래퍼로 하위 잡을 등록하고 실제 등록부를 읽는다. 첫 루프 시점의 새 테스트 14건은 수정 전 코드(`7a4a0c2`)에서 모두 실패한다. **실제 에이전트 시험.** 테스트는 에이전트가 `--parent-job` 을 붙인다고 가정할 뿐 증명하지 못하므로, 실제 creator 에게 "작업 일부를 opencode 에게 위임하라"고만 지시하고 방법은 알려주지 않았다. - 1차(`3fdbb62`): creator 가 안내문을 읽고 스스로 `--parent-job` 을 붙였고, 하위 잡 레코드에 부모 ID 가 기록됐다. 다만 첫 시도가 취소됐다. 워크스페이스 `w6` 의 creator 가 `w2` 의 opencode 를 스코프 필터 때문에 보지 못한 것이다. creator 가 환경변수를 떼고 재시도해서 성공했다. - 2차(`ed293dc`): 환경변수를 건드리지 말라고 명시했다. 첫 호출에서 바로 위임에 성공했고 부모 ID 가 기록됐다. ## 리뷰와 검증에서 나온 지적 - **codex, 1차 리뷰.** 수정 라운드에서 앞선 creator 잡의 하위 잡이 근거 표에서 빠졌다. `3fdbb62` 로 고쳤다. - **오케스트레이터, 전체 스위트.** 리뷰 중 소스 변경 규칙을 옵션 없는 루프에 무조건 적용하자 `test_tier4_e2e.py::test_e2e_scenario5_multi_agent_review_loop` 가 실패했다. 수정 전 코드에서는 통과하는 결정적 회귀였다. 샌드박스의 가짜 herdr 상태 파일이 매 라운드 바뀌어 라운드가 계속 무효가 됐다. `1d4f338` 이 이 규칙을 하위 잡이 있을 때만 켠다. - **오케스트레이터, 실제 시험.** 위의 같은 서버·다른 워크스페이스 문제. `ed293dc` 로 고쳤다. ## 범위 밖 멈춘 에이전트 감지(#30)와 잡 제한 시간 조절(#33)은 다루지 않는다. 하위 잡이 권한 확인 창에 멈추면 루프는 그 잡이 #33 의 예산으로 닫힐 때까지 기다린다. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Godopu added 6 commits 2026-09-25 02:26:29 +00:00
- registry: parent_job field, register --parent-job fails closed on an
  unknown parent, children_of/descendants_of, list --parent
- wrapper: --parent-job; $MAM_JOB_ID default (ignored with a warning when
  this registry lacks it); brief line and one sentence at the end of the
  injected instructions
- create_session: the onboarding instructions carry the same sentence

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- wait_for_job_tree at the execution, correction and compliance waits;
  bounded by MAM_CHILD_WAIT_S (3900) and MAM_JOB_TREE_MAX (50)
- [하위 위임 잡] table: OPEN / CLOSED-UNCONFIRMED (deadline_timeout,
  agent_blocked, or error with no reason) / NOT-DISPATCHED / error /
  completed, plus unattributed open jobs; omitted when there are none so
  option-less prompts stay byte-identical
- --wait-open-jobs also waits for unattributed open jobs
- stale-review guard without --worktree/--base: head + tree (control
  plane excluded) before review, re-checked after verdicts and before
  promotion; off with a warning when the source cannot be fingerprinted
- tests/test_issue34_job_tree.py: real wrapper registrations, real
  publish_event / deadline close, real loop process

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
round_roots was replaced by the correction job, so an implementation
job's open or failed sub-job vanished from the next review and the loop
could pass (planner-reviewer-codex-01, P2). The run's creator jobs now
accumulate; each wait still covers only the new job's tree, so the wait
cap is not paid again per round. T9 covers open / error / completed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without --worktree/--base the guard fingerprinted the source on every
loop, so a loop whose source holds untracked files that change on every
job voided each round: test_tier4_e2e scenario 5 keeps the mock herdr's
state file (rewritten on each herdr call) in its repository and failed
with rc 1 on 3fdbb62 (passed on 7a4a0c2). A loop without sub-delegation
now behaves as before (issue #34 requirement 3); with sub-jobs the guard
protects against a deadline-closed sub-job still writing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- mam_herdr: for name lookups (agent / has-session / kill-session) a
  HERDR_WORKSPACE_ID that is the caller pane's own workspace (it matches
  the HERDR_PANE_ID prefix) is dropped. Live agent names are unique per
  server (herdr --skill), so this cannot address a same-named agent of
  another workspace; an id the caller chose, or one set outside a pane,
  still scopes (H-18/H-21/H-23). The generated shim is unchanged (S11).
- wrapper: a preflight that cannot see the target closes the job
  cancelled with detail target_not_found instead of session_busy; the
  loop's sub-job table counts it as NOT-DISPATCHED.
- tests: T10-T12 with the measured pane env (same-server socket, caller
  w6, target w2); T8 follows the sub-job-armed guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Godopu added 1 commit 2026-09-25 07:19:14 +00:00
- registry.get_feedback only returned the detail of the last completed or
  error event, so a sub-job the wrapper cancelled before injection
  (target_not_found, session_busy) reached the review table as a bare
  `cancelled` and its reason was lost (merge review, grok N1). get_feedback
  takes the event kinds to read (default unchanged); the table asks for
  cancelled too and shows NOT-DISPATCHED with the reason. Test T13, both
  reasons, fails without the fix.
- the [귀속되지 않은 열린 잡] line listed jobs registered without
  --parent-job but gave the reviewer no rule for them - the one case the
  table cannot show (claude K1). A rule sentence follows the line whenever
  such a job exists; without one the prompt is unchanged. T6 pins it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
Godopu added 1 commit 2026-09-25 07:23:09 +00:00
remote.py closes a job whose end it could not see with
`error --detail "deadline_timeout (remote runner)"`; the sub-job table
matched the reason exactly and filed it as a plain failure, losing the
unconfirmed count and the late-write warning (merge review, codex R2).
The kind is the first word of the reason now. Test T14 replays the
remote runner's close through the real registry and producer; it fails
without the fix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
Godopu added 1 commit 2026-09-25 07:44:21 +00:00
Issue #34: the loop waits for the creator job's sub-jobs, reviewers get
the sub-job table as evidence, a run with sub-jobs voids a round whose
source changed during review, and a pane caller reaches an agent of
another workspace on its own server. VERSIONS.md carries the entry; the
merge-side reports (round 1 @ed293dc and round 2 @e80186f with identity
sidecars, base ed293dc -> head e80186f, 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 8ea65bb238 into main 2026-09-25 07:44:35 +00:00
Author
Owner

병합 완료 — main (v4.7.4, Gitea merge 커밋)

병합 워크스페이스 재검토 (PR head 를 자체 venv 워크트리에 받아 --worktree --base --review-only --all-reviewer):

  • 1회차 @ed293dc: claude PASS(비차단 K1–K4), agy PASS, grok·codex NOT PASS. 지적은 세 갈래.
    • 래퍼가 투입 전에 cancelled --detail target_not_found|session_busy 로 닫은 하위 잡의 사유를 get_feedback 이 읽지 않아(마지막 completed/error 만) 표에 사유 없는 cancelled 로만 나오고 NOT-DISPATCHED 분류가 성립하지 않았다(grok N1·codex R1).
    • 원격 러너의 마감 문구 deadline_timeout (remote runner) 가 완전 일치 비교에 걸려 일반 실패로 집계됐다(codex R2, 실제 handle_dispatch 로 재현).
    • [귀속되지 않은 열린 잡] 줄에 판정 규칙이 없었다(claude K1).
  • 정정 8a9101e(get_feedback(events=…), STRAY_RULE), e80186f(사유의 첫 단어로 종류 판정). 회귀 T13·T14·T6 은 정정을 되돌리면 실패.
  • 2회차 @e80186f: claude·agy·grok·codex 4인 전원 PASS(identity 4건 base ed293dc → head e80186f, evidence review-tree).
  • 전체 스위트 @ed293dc: 1156 통과 / 3 skip / 실패 2 — 공개 브로커 no CONNACK 재시도로 11초 타이밍 한계를 넘긴 flake(단독 재실행 통과). @e80186f 관련 스위트 125 + 54 통과.
  • 릴리스 22586d2: 4.7.3 → 4.7.4 락스텝, 리포트 8건 + identity 4건 아카이브.

비차단 후속: MAM_JOB_TREE_MAX 초과 시 대기를 건너뛰고 표에 50행 넘게 붙을 수 있음(K2); target_not_found 도 래퍼 rc 5 라 루프의 busy 1회 재시도가 없는 대상에도 돎(K3, rc 분리 권고); descendants_of·귀속되지 않은 잡 조회가 폴링(3초)마다 등록부 전체를 읽음(K4; 자손 502개에서 약 0.01초 실측); 귀속되지 않은 열린 잡 목록은 시간 기반 휴리스틱이라 다른 루프의 잡이 섞일 수 있음(codex).

## 병합 완료 — main (v4.7.4, Gitea merge 커밋) **병합 워크스페이스 재검토** (PR head 를 자체 venv 워크트리에 받아 `--worktree --base --review-only --all-reviewer`): - 1회차 @ed293dc: claude PASS(비차단 K1–K4), agy PASS, grok·codex NOT PASS. 지적은 세 갈래. - 래퍼가 투입 전에 `cancelled --detail target_not_found|session_busy` 로 닫은 하위 잡의 사유를 `get_feedback` 이 읽지 않아(마지막 `completed`/`error` 만) 표에 사유 없는 `cancelled` 로만 나오고 `NOT-DISPATCHED` 분류가 성립하지 않았다(grok N1·codex R1). - 원격 러너의 마감 문구 `deadline_timeout (remote runner)` 가 완전 일치 비교에 걸려 일반 실패로 집계됐다(codex R2, 실제 `handle_dispatch` 로 재현). - `[귀속되지 않은 열린 잡]` 줄에 판정 규칙이 없었다(claude K1). - 정정 `8a9101e`(`get_feedback(events=…)`, `STRAY_RULE`), `e80186f`(사유의 첫 단어로 종류 판정). 회귀 T13·T14·T6 은 정정을 되돌리면 실패. - 2회차 @e80186f: claude·agy·grok·codex **4인 전원 PASS**(identity 4건 base ed293dc → head e80186f, evidence review-tree). - 전체 스위트 @ed293dc: 1156 통과 / 3 skip / 실패 2 — 공개 브로커 `no CONNACK` 재시도로 11초 타이밍 한계를 넘긴 flake(단독 재실행 통과). @e80186f 관련 스위트 125 + 54 통과. - 릴리스 `22586d2`: 4.7.3 → 4.7.4 락스텝, 리포트 8건 + identity 4건 아카이브. **비차단 후속**: `MAM_JOB_TREE_MAX` 초과 시 대기를 건너뛰고 표에 50행 넘게 붙을 수 있음(K2); `target_not_found` 도 래퍼 rc 5 라 루프의 busy 1회 재시도가 없는 대상에도 돎(K3, rc 분리 권고); `descendants_of`·귀속되지 않은 잡 조회가 폴링(3초)마다 등록부 전체를 읽음(K4; 자손 502개에서 약 0.01초 실측); 귀속되지 않은 열린 잡 목록은 시간 기반 휴리스틱이라 다른 루프의 잡이 섞일 수 있음(codex).
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#38