feat(agent-ops): 마일스톤 작업 근거를 태그로 연결한다

분할된 계획과 완료 로그를 Task id 기준으로 집계해 마일스톤 동기화가 완료 작업을 누락하지 않도록 한다.
This commit is contained in:
toki 2026-08-01 19:58:16 +09:00
parent 7ae4be6ceb
commit 1debc7ada0
22 changed files with 914 additions and 243 deletions

View file

@ -59,6 +59,7 @@
- 테스트는 변경 범위에 맞춰 `go test ./...` 또는 대상 패키지 테스트를 실행한다.
- 사용자 실행 파이프라인에 닿는 작업을 한 경우, 작업 완료 후 `agent-ops/rules/project/domain/testing/rules.md`의 검증 기준을 따른다.
- 활성 `agent-task`의 dry-run, worker/review 실행, blocked retry와 상태 관찰은 사용자의 명시적 실행 요청이 있을 때만 `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` dispatcher로 수행한다. dispatcher는 이 프로젝트의 production orchestration 경로로 유지한다.
- 이 프로젝트에서는 `agent-ops/rules/common/rules-roadmap.md`의 기존 task-group-only 및 `Roadmap Completion` 단건 반영 문구를 legacy 호환 규칙으로 한정한다. 새 `m-*` PLAN/CODE_REVIEW/complete.log는 첫 줄의 `milestone-task=<id>[,<id>...]`로 Milestone Task 기여 범위를 보존한다. 이 metadata나 단건 PASS는 완료 선언이 아니며, `sync-milestone-workstate`가 같은 Milestone task group의 완료 로그를 id별로 집계해 현재 Task 설명·검증·SDD evidence가 모두 충족된 경우에만 체크한다. 기존 `Roadmap Completion`은 first-line metadata가 없는 archive 로그의 호환 evidence로만 취급한다.
- `iop-agent``agent-task` 밖의 격리된 unit/integration/compiled-binary test, parity·validation 검증에서만 허용한다. dispatcher, worker, self-check, official review와 PLAN/CODE_REVIEW final verification을 포함한 모든 활성 `agent-task` 실행 경로에서는 `iop-agent` 실행을 허용하지 않는다.
- field/bootstrap 작업은 `testing` domain rule을 따르고, 실제 local 환경값이 필요하면 `agent-test/local/rules.md`를 따른다.
- Node, specialized agent, domain agent, Control Plane enrollment 등 사용자가 대상 host에서 실행하는 bootstrap/install command 작업은 `agent-ops/rules/project/domain/testing/rules.md`의 one-line bootstrap UX 기준을 따른다.

View file

@ -39,7 +39,7 @@
- [ ] <SDD가 필요한 경우: SDD 잠금이 해제되어 있다>
- [ ] <SDD가 필요한 경우: SDD 사용자 리뷰가 없거나 승인/해결되었다>
- [ ] <SDD가 필요한 경우: Acceptance Scenario가 Milestone 기능 Task와 연결되어 있다>
- [ ] <SDD가 필요한 경우: Evidence Map이 완료 Roadmap Completion과 최종 검증 evidence로 검증 가능하게 연결되어 있다>
- [ ] <SDD가 필요한 경우: Evidence Map이 완료 complete.log의 milestone-task id별 집계와 최종 검증 evidence로 검증 가능하게 연결되어 있다>
- 결정 필요: <없음 | 아래 목록>
- <에이전트가 확정할 없는 제품/범위/우선순위/책임 경계 결정 항목>

View file

@ -57,7 +57,7 @@
| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | <test/smoke/search/user-review evidence> | `agent-task/m-<milestone-slug>/...` | <complete.log의 Roadmap Completion/최종 검증으로 남길 근거> |
| S01 | <test/smoke/search/user-review evidence> | `agent-task/m-<milestone-slug>/...` | <complete.log의 milestone-task id별 집계/최종 검증으로 확인할 근거> |
## Cross-repo Dependencies

View file

@ -38,7 +38,7 @@ Apply these rules:
- Compute `review-number` as `count(agent-task/{task_name}/code_review_*.log) + 1` before archiving the active review.
- Repeated `WARN`/`FAIL`, loop exhaustion, missing verification evidence, and a transient test failure do not trigger `USER_REVIEW.md` by themselves.
- For `milestone-lock`, resolve the selected Milestone from `Roadmap Targets` or another exact active Milestone path already fixed by the task. Read its current `구현 잠금 > 결정 필요` items and require one exact unresolved decision that blocks the next safe implementation step.
- For `milestone-lock`, resolve the selected Milestone from the first path segment of the task header (`m-<milestone-slug>`) or another exact active Milestone path already fixed by the task. Read its current `구현 잠금 > 결정 필요` items and require one exact unresolved decision that blocks the next safe implementation step.
- For `external-execution`, first resolve the repository-declared runner, transport, workdir, credentials source, and safe read-only preflight. Use an already authorized configured executor, including SSH or another declared remote runner, when it can perform the step. A current-host OS mismatch, missing local command, closed current-host localhost port, agent execution limit, or incomplete evidence is not enough while such an executor remains usable.
- Trigger `external-execution` only when the required target and attempted routing/preflight are concrete, the next verification step is required for the verdict, no authorized automatic route can perform it, and progress requires a user to grant access or authorization, prepare or operate a user-controlled environment, or supply the required evidence. Do not create another follow-up PLAN that repeats the same inaccessible preflight.
- Generic scope conflict, missing optional handoff evidence, arbitrary `상태` text, and repository-fixable setup remain normal WARN/FAIL follow-up inputs.
@ -91,9 +91,9 @@ Milestone task group contract:
- `agent-task/m-<milestone-slug>/` is reserved for Milestone-linked work created by the plan skill.
- Do not treat normal task groups that do not start with `m-` as runtime milestone completion targets.
- For a selected task path, parse only the first path segment as `{task_group}`. If it matches `^m-[a-z0-9][a-z0-9-]*$`, strip `m-` to get `<milestone-slug>`.
- Do not modify `agent-roadmap/**` for milestone routing during code-review finalization. Read only the Milestone path from `Roadmap Targets` and its SDD path when needed to verify SDD Evidence Map.
- Do not call `update-roadmap` from this skill. The runtime consumes the PASS completion event, checks current state, resolves the active Milestone, and calls `update-roadmap` if needed.
- For `m-<milestone-slug>` PASS tasks, report the original active task path, final archive path, complete log path, task group, and milestone slug so the runtime has deterministic event inputs.
- Do not modify `agent-roadmap/**` for milestone routing during code-review finalization. Resolve the active Milestone from the `m-<milestone-slug>` task group and read its SDD path only when needed to verify the first-line `milestone-task` ids against the SDD Evidence Map.
- Do not call `update-roadmap` from this skill. The runtime consumes the PASS completion event and invokes `sync-milestone-workstate`, which aggregates all same-group `complete.log` evidence before changing a Task checkbox.
- For `m-<milestone-slug>` PASS tasks, report the original active task path, final archive path, complete log path, task group, milestone slug, and `milestone-task` ids so the runtime has deterministic aggregation inputs.
Follow-up routing boundary:
@ -171,7 +171,7 @@ Before writing the verdict:
- Compare actual source files against every planned checklist item.
- Compare the plan `Implementation Checklist` and review stub `Implementation Checklist` (legacy: `구현 체크리스트`); repair non-behavioral drift when implementation remains judgeable.
- When the active artifacts have `Roadmap Targets`, check whether the referenced Milestone has `SDD: 필요`. When it does, read only that Milestone and its SDD, compare implementation evidence against the SDD Acceptance Scenarios/Evidence Map for the targeted task ids, and fail completeness or verification trust when evidence is insufficient. Do not require a separate SDD target section.
- When the active artifacts use an `m-*` task header, require identical non-empty `milestone-task` ids in PLAN and CODE_REVIEW, resolve the active Milestone by slug, and verify every id exists. If the Milestone has `SDD: 필요`, read only that Milestone and its SDD, compare implementation evidence against the SDD Acceptance Scenarios/Evidence Map for those ids, and fail completeness or verification trust when evidence is insufficient.
- Directly repair obvious non-behavioral source nits when safe: typos, stale comments, docs, or formatting only, with no behavior/test/API contract change.
- If a checklist item contains integrated verification for a feature, treat that feature item as incomplete until both implementation evidence and the matching verification output are present. Do not accept a separate unchecked completion-criteria item as a substitute.
- Confirm the implementation marked the matching checklist items in the active review file, including the mandatory `CODE_REVIEW-*-G??.md` evidence item; repair clear artifact drift when evidence supports completion.
@ -189,7 +189,7 @@ Append the review result to the active `CODE_REVIEW-*-G??.md`. For a canonical E
Required fields for canonical English active pairs:
- `Overall Verdict`: exactly `PASS`, `WARN`, or `FAIL`.
- `Dimension Assessment`: Pass/Warn/Fail for correctness, completeness, test coverage, API contract, code quality, implementation deviation, verification trust. If SDD Evidence Map applies through `Roadmap Targets`, also include spec conformance.
- `Dimension Assessment`: Pass/Warn/Fail for correctness, completeness, test coverage, API contract, code quality, implementation deviation, verification trust. If SDD Evidence Map applies through `milestone-task`, also include spec conformance.
- `Findings`: `None`, or bullets using `Required`, `Suggested`, or `Nit` with `file:line` and a concrete fix.
- `Routing Signals`: calculate once and append `review_rework_count=<N>` and `evidence_integrity_failure=true|false`. Set rework count to archived same-task `WARN|FAIL` verdicts plus one only when the current verdict is non-PASS. Set integrity failure to true only when a claimed test, command, exit code, or production path is absent, unexecuted, or contradicted by fresh reviewer evidence.
- `Next Step`: keep only the matching PASS, WARN/FAIL follow-up, or USER_REVIEW line.
@ -257,10 +257,11 @@ Complete log template:
- Template path: `agent-ops/skills/common/code-review/templates/complete-log-template.md`
- Copy the template's section order and fill every placeholder from the archived plan/review logs and final verdict.
- Do not leave placeholders in `complete.log`.
- Copy the archived PLAN's exact first-line generation header to the first line of `complete.log`. For `m-*`, this preserves the non-empty `milestone-task` ids; for non-milestone work it preserves the ordinary `task/plan/tag` header.
- If the task did not close through `USER_REVIEW.md`, remove the optional user-review row from the `루프 이력` table.
- If the archived plan or review log contains `Roadmap Targets`, copy it into `complete.log` as `Roadmap Completion`. Include the Milestone path, completed Task ids, archived plan/review log paths, and verification evidence. If there is no `Roadmap Targets` section, remove the optional `Roadmap Completion` template section entirely and do not invent roadmap targets.
- Use `없음` for empty `잔여 Nit` or `후속 작업`.
- A PASS `complete.log` must not contain unresolved Required or Suggested issues. Nit-only leftovers may be recorded under `잔여 Nit`.
- The `milestone-task` field is contribution scope, not a completion assertion. Do not write a new `Roadmap Completion` section or claim that any listed Task id is complete merely because this review passed.
For `WARN` or `FAIL`, materialize the next state prepared in Step 5 immediately after archive:
@ -279,8 +280,8 @@ After Step 6:
- If verdict is `PASS`, determine archive month from the current completion date as `YYYY/MM`, create the needed archive parent directories, then move the selected task artifacts from `agent-task/{task_name}/` to `agent-task/archive/YYYY/MM/{task_name}/`. For split work, move the selected subtask directory itself and preserve the task group path, e.g. `agent-task/refactoring/01_core/` moves to `agent-task/archive/YYYY/MM/refactoring/01_core/`.
- Do not overwrite an existing archive directory. If `agent-task/archive/YYYY/MM/{task_name}/` already exists, append the next numeric suffix to the final path segment: single-plan `agent-task/archive/YYYY/MM/{task_group}_1/`, split-plan `agent-task/archive/YYYY/MM/{task_group}/{subtask_dir}_1/`, and so on.
- After moving a split subtask, remove the active parent `agent-task/{task_group}/` only when it is empty.
- If verdict is `PASS` and `{task_group}` matches `m-<milestone-slug>`, do not resolve the roadmap target and do not call `update-roadmap`. Report completion event metadata after the task archive move: `origin-task=agent-task/{task_name}` from the original active task path, `task-group={task_group}`, `milestone-slug=<milestone-slug>`, final archive path, `complete.log` path, archived plan/review log paths, and `roadmap-completion=<Task ids from complete.log or none>`.
- The runtime consumes that completion event, checks current state, and calls `update-roadmap` if needed. `update-roadmap` only checks Milestone Task ids when `complete.log` contains `Roadmap Completion`; if the section is absent, roadmap Task completion is a no-op even for `m-*` task groups.
- If verdict is `PASS` and `{task_group}` matches `m-<milestone-slug>`, do not resolve the roadmap target and do not call `update-roadmap`. Report completion event metadata after the task archive move: `origin-task=agent-task/{task_name}` from the original active task path, `task-group={task_group}`, `milestone-slug=<milestone-slug>`, final archive path, `complete.log` path, archived plan/review log paths, and `milestone-task=<ids copied from the header>`.
- The runtime consumes that completion event and invokes `sync-milestone-workstate target-milestone=<milestone-slug> complete-log=<path>`. The sync skill scans every same-group active/archive `complete.log`, aggregates evidence by the listed ids, and checks only Tasks whose full current contract is satisfied.
- `WARN` and `FAIL` do not update the roadmap Milestone; the follow-up plan remains under the same `m-<milestone-slug>` task group when the original task was Milestone-linked.
- `USER_REVIEW` does not update the roadmap Milestone and does not produce PASS completion metadata. Keep the active task directory in place with `USER_REVIEW.md` and archived plan/review logs until its recorded user action or decision is resolved.
- If `USER_REVIEW.md` is later resolved as complete/PASS by the recorded action or decision and evidence, write `complete.log`, move the task artifacts to archive, and report `m-*` PASS completion metadata just like a normal `PASS`.
@ -324,8 +325,8 @@ Report Required/Suggested counts, archive names, the final task archive path for
- No active `PLAN-*.md`, `CODE_REVIEW-*.md`, or `USER_REVIEW.md` remains after PASS or user-review-resolved PASS.
- PASS or user-review-resolved PASS: `complete.log` written from `agent-ops/skills/common/code-review/templates/complete-log-template.md`, then task artifacts moved under `agent-task/archive/YYYY/MM/` with task-group path preserved for split work.
- PASS milestone task group: `m-<milestone-slug>` completion event metadata was reported for runtime; roadmap was not modified by code-review.
- PASS with `Roadmap Targets`: `complete.log` contains `Roadmap Completion` with Milestone path, Task ids, archived plan/review evidence, and verification evidence.
- PASS without `Roadmap Targets`: `complete.log` omits `Roadmap Completion` and reported metadata says `roadmap-completion=none`.
- PASS `complete.log` first line is byte-for-byte identical to the archived PLAN header. An `m-*` log contains non-empty `milestone-task` ids and reports them in completion metadata; a non-milestone log omits the field.
- PASS does not create `Roadmap Completion` or directly check a Milestone Task. Aggregated evaluation is deferred to `sync-milestone-workstate`.
- WARN/FAIL without user-review gate: the plan skill was invoked for the exact task path with verified `review_rework_count` and `evidence_integrity_failure`, completed `finalize-task-routing`, and created new active `PLAN-{build_lane}-GNN.md` and `CODE_REVIEW-{review_lane}-GNN.md` files matching the fresh routed output; no `complete.log`.
- WARN/FAIL prepared PLAN passed `python3 agent-ops/skills/common/orchestrate-agent-task-loop/scripts/dispatch.py --workspace <workspace> --validate-plan <candidate-plan>` before active-pair archive and again after byte-for-byte materialization. Invalid write claims leave the verdict-appended prior pair active.
- WARN/FAIL follow-up: the plan input omitted prior route fields, revalidated outcome/acceptance/exclusions from current evidence, used the completed in-memory PLAN as the packet, and copied identical `Archive Evidence Snapshot` sections into the new plan/review pair.

View file

@ -1,3 +1,5 @@
<!-- task={task_name} plan={plan_number} tag={TAG}{milestone_task_metadata_or_omit} -->
# Complete - {task_name}
## 완료 일시
@ -23,16 +25,6 @@
- `{command}` - {PASS/FAIL/BLOCKED}; {actual output summary or saved output path}
## Roadmap Completion
{optional; include only when archived plan/review had Roadmap Targets. Remove this entire section when there are no Roadmap Targets.}
- Milestone: `{agent-roadmap/phase/<phase-slug>/milestones/<milestone-slug>.md}`
- Milestone link: [Milestone 문서](agent-roadmap/phase/<phase-slug>/milestones/<milestone-slug>.md)
- Completed task ids:
- `{task-id}`: PASS; evidence=`{archived-plan-log}`, `{archived-review-log}`; verification=`{command or saved output path}`
- Not completed task ids: 없음
## 잔여 Nit
- 없음

View file

@ -222,7 +222,7 @@ When recovering a KST-night `local-G07``local-G08` Laguna locator or a termin
- Persist `active_stage` for a running task. After dispatcher restart, exclude that task from candidates, restore or conservatively adopt its workspace write claim, and immediately dispatch every other dependency-ready task whose claim does not collide.
- **ABSOLUTE RULE:** Scan the complete candidate set only at initial entry and immediately after creating a verified `complete.log`. In that scan, exclude tasks shown as running by current-workspace state and native session/locator evidence, then atomically admit every dependency-ready task with a non-colliding write claim. An unmet dependency or write collision excludes only that task. Exit instead of polling when no candidate remains.
- Persist Pi worker success, Pi self-check success, and official review as separate stages. If restart state is `worker_done=true` and `selfcheck_done=false`, resume on the same Pi model, not with worker or review. Run the full pass when `selfcheck_incomplete=0`; otherwise resume the persisted successful self-check context locator with an unchecked-item retry. Never replace a missing or invalid persisted context with a fresh session.
- Key persistent state to the `task/plan/tag` generation at the start of PLAN. Checklist/body edits to the same PLAN do not reset the stage; a new plan number in a follow-up PLAN does.
- Key persistent state to the first-line `task/plan/tag` generation and, for `m-*`, its `milestone-task` scope. Checklist/body edits to the same PLAN do not reset the stage; a new plan number or changed Milestone Task scope does.
- Send an already completed review stub with no dispatcher execution record to review. Never send dispatcher-recorded Pi worker success to review before self-check completes.
- Start official review and worker/self-check together when they belong to different dependency-ready tasks with disjoint workspace claims. Wait for a claim owner to reach verified completion before admitting a colliding task.
- Let the dispatcher record every worker/self-check/review attempt start and finish in the task-group `WORK_LOG.md`.
@ -241,7 +241,7 @@ When recovering a KST-night `local-G07``local-G08` Laguna locator or a termin
4. **Converge review.**
- Run every official review in an independent Codex one-shot session with no separate numeric limit. Dispatch all ready reviews with disjoint workspace claims in parallel.
- For finalization recovery without an active PLAN, recover the review target and write claim from the archived plan log for the same task/plan/tag. Keep the claim until the completed archive is verified.
- For finalization recovery without an active PLAN, recover the review target and write claim from the archived plan log for the same first-line generation metadata, including `milestone-task` when present. Keep the claim until the completed archive is verified.
- Forbid collaboration/sub-agent tools in official review and finish inside the current one-shot session. If such a tool call appears, clean up that attempt's independent subprocess group and retry in a fresh review session. Count the failure toward the same stage's 10-consecutive-failure limit.
- Delegate PASS archive, WARN/FAIL follow-up pairs, and review-finalization recovery to the `code-review` file contract.
- Reclassify any remaining active pair and send it to worker or review.

View file

@ -110,8 +110,19 @@ VERDICT_SCHEMA_MATCHERS = tuple(
)
for heading, label in CODE_REVIEW_RESULT_SCHEMAS
)
MILESTONE_TASK_ID_PATTERN = r"[A-Za-z0-9]+(?:[-_+=][A-Za-z0-9]+){0,3}"
MILESTONE_TASK_ID_RE = re.compile(rf"\A{MILESTONE_TASK_ID_PATTERN}\Z")
PLAN_IDENTITY_RE = re.compile(
r"<!--\s+task=(?P<task>\S+)\s+plan=(?P<plan>\d+)\s+tag=(?P<tag>\S+)\s+-->"
r"\A<!--\s+task=(?P<task>\S+)\s+plan=(?P<plan>\d+)\s+tag=(?P<tag>\S+)"
r"(?:\s+milestone-task=(?P<milestone_task>[^,\s]+(?:,[^,\s]+)*))?"
r"\s+-->[ \t]*(?:\r?\n|\Z)"
)
MILESTONE_ITEM_RE = re.compile(
rf"^-\s+\[[ xX]\]\s+\[({MILESTONE_TASK_ID_PATTERN})\]", re.MULTILINE
)
MILESTONE_FEATURE_SECTION_RE = re.compile(
r"^##[ \t]+기능[ \t]*\r?\n(?P<body>.*?)(?=^##[ \t]+|\Z)",
re.MULTILINE | re.DOTALL,
)
IMPLEMENTATION_CHECKBOX_RE = re.compile(
r"^-\s+\[([^\]\r\n]*)\]", re.MULTILINE
@ -278,10 +289,90 @@ def plan_identity(path: Path | None) -> str:
match = PLAN_IDENTITY_RE.search(text)
if not match:
return sha256_file(path)
identity = "\0".join(match.group(name) for name in ("task", "plan", "tag"))
fields = [match.group(name) for name in ("task", "plan", "tag")]
if match.group("milestone_task"):
fields.append(match.group("milestone_task"))
identity = "\0".join(fields)
return "meta:" + hashlib.sha256(identity.encode()).hexdigest()
def milestone_task_ids(metadata: re.Match[str]) -> tuple[str, ...]:
value = metadata.group("milestone_task")
return tuple(value.split(",")) if value else ()
def milestone_feature_task_ids(text: str) -> set[str]:
feature_section = MILESTONE_FEATURE_SECTION_RE.search(text)
if feature_section is None:
return set()
return set(MILESTONE_ITEM_RE.findall(feature_section.group("body")))
def metadata_work_unit_id(metadata: re.Match[str]) -> str:
work_unit_id = (
f"{metadata.group('task')}::plan-{metadata.group('plan')}::"
f"tag-{metadata.group('tag')}"
)
if metadata.group("milestone_task"):
work_unit_id += f"::milestone-task-{metadata.group('milestone_task')}"
return work_unit_id
def validate_plan_metadata(path: Path, workspace: Path) -> list[str]:
try:
head = path.read_text(encoding="utf-8", errors="replace")[:1024]
except OSError as exc:
return [f"PLAN metadata를 읽을 수 없다: {exc}"]
metadata = PLAN_IDENTITY_RE.search(head)
if metadata is None:
return [
"첫 줄 generation header를 판별할 수 없다: "
"<!-- task=... plan=N tag=... [milestone-task=id[,id...]] -->"
]
task_group = metadata.group("task").split("/", 1)[0]
task_ids = milestone_task_ids(metadata)
invalid_ids = [
task_id
for task_id in task_ids
if MILESTONE_TASK_ID_RE.fullmatch(task_id) is None
]
if invalid_ids:
return [
"milestone-task id 문법이 Milestone item-id 계약과 다르다: "
+ ", ".join(invalid_ids)
]
if len(task_ids) != len(set(task_ids)):
return ["milestone-task에 중복 Task id가 있다"]
if not task_group.startswith("m-"):
return ["비마일스톤 task에는 milestone-task를 둘 수 없다"] if task_ids else []
if not task_ids:
return ["m-* PLAN 첫 줄에는 milestone-task=<id[,id...]>가 필요하다"]
slug = task_group[2:]
candidates = sorted(
path
for path in (workspace / "agent-roadmap" / "phase").glob(
f"*/milestones/{slug}.md"
)
if path.is_file()
)
if len(candidates) != 1:
return [
f"milestone-task target은 활성 Milestone과 정확히 하나 매칭되어야 한다: "
f"slug={slug!r}, matches={len(candidates)}"
]
milestone_text = candidates[0].read_text(encoding="utf-8", errors="replace")
known_ids = milestone_feature_task_ids(milestone_text)
unknown_ids = [task_id for task_id in task_ids if task_id not in known_ids]
if unknown_ids:
return [
"milestone-task가 활성 Milestone 기능 Task id와 일치하지 않는다: "
+ ", ".join(unknown_ids)
]
return []
def write_json(path: Path, value: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
@ -1418,11 +1509,14 @@ def read_task_directory(workspace: Path, directory: Path) -> Task | None:
plan.read_text(encoding="utf-8", errors="replace")[:1024]
)
if metadata is None:
errors.append("PLAN task/plan/tag metadata를 판별할 수 없다")
errors.append("PLAN 첫 줄 generation metadata를 판별할 수 없다")
elif metadata.group("task") != name:
errors.append(
f"PLAN task metadata가 디렉터리와 다르다: {metadata.group('task')}"
)
errors.extend(validate_plan_metadata(plan, workspace))
if review is not None and plan_identity(plan) != plan_identity(review):
errors.append("PLAN/CODE_REVIEW generation metadata가 다르다")
return Task(
name=name,
directory=directory,
@ -1819,7 +1913,7 @@ def work_unit_id_from_file(path: Path) -> str | None:
head = path.read_text(encoding="utf-8", errors="replace")[:1024]
match = PLAN_IDENTITY_RE.search(head)
if match:
return f"{match.group('task')}::plan-{match.group('plan')}::tag-{match.group('tag')}"
return metadata_work_unit_id(match)
except Exception:
pass
return None
@ -1860,10 +1954,7 @@ def official_review_source_identity(task: Task) -> tuple[str, int, str]:
raise ExecutionDecisionError(
f"official review PLAN work-unit identity를 복구할 수 없다: {source}"
)
work_unit_id = (
f"{metadata.group('task')}::plan-{metadata.group('plan')}::"
f"tag-{metadata.group('tag')}"
)
work_unit_id = metadata_work_unit_id(metadata)
return route_match.group(1), int(route_match.group(2)), work_unit_id
@ -7035,6 +7126,11 @@ def main() -> int:
candidate = Path(validate_plan)
if not candidate.is_absolute():
candidate = (Path.cwd() / candidate).resolve()
metadata_diagnostics = validate_plan_metadata(candidate, workspace)
if metadata_diagnostics:
for diagnostic in metadata_diagnostics:
print(f"plan metadata error: {diagnostic}", file=sys.stderr)
return 2
write_set, diagnostics = inspect_write_set(candidate, workspace)
if diagnostics:
for diagnostic in diagnostics:

View file

@ -2,7 +2,7 @@
"""Deterministic execution-target selector CLI over the pure route policy.
The selector consumes a static routing task file (``PLAN-*`` or
``CODE_REVIEW-*``), its ``task/plan/tag`` generation header and optional prior
``CODE_REVIEW-*``), its ``task/plan/tag/milestone-task`` generation header and optional prior
decision / quota snapshot, and returns a stable JSON contract that the
dispatcher can persist. This module exposes the schema/invalid-input,
worker/review grade matrix, resume, failover, and policy-owned promotion
@ -28,7 +28,13 @@ TIMEZONE_NAME = "Asia/Seoul"
DEFAULT_QUOTA_PROBE_COMMAND = "iop-node quota-probe"
_FILENAME_RE = re.compile(r"^(PLAN|CODE_REVIEW)-(local|cloud)-G(\d{2})\.md$")
_HEADER_RE = re.compile(r"<!--\s*task=(\S+)\s+plan=(\d+)\s+tag=(\S+)\s*-->")
_MILESTONE_TASK_ID_PATTERN = r"[A-Za-z0-9]+(?:[-_+=][A-Za-z0-9]+){0,3}"
_MILESTONE_TASK_ID_RE = re.compile(rf"\A{_MILESTONE_TASK_ID_PATTERN}\Z")
_HEADER_RE = re.compile(
r"\A<!--\s*task=(?P<task>\S+)\s+plan=(?P<plan>\d+)\s+tag=(?P<tag>\S+)"
r"(?:\s+milestone-task=(?P<milestone_task>[^,\s]+(?:,[^,\s]+)*))?"
r"\s*-->[ \t]*(?:\r?\n|\Z)"
)
_STAGE_BY_KIND = {"PLAN": "worker", "CODE_REVIEW": "review"}
_VALID_TRANSITIONS = {"initial", "resume", "failover", "promotion"}
_VALID_EXECUTION_CLASSES = {"local_model", "cloud_model"}
@ -92,7 +98,7 @@ def _parse_filename(task_file: Path) -> tuple[str, str, int]:
return kind, lane, grade
def _parse_header(task_file: Path) -> tuple[str, int, str]:
def _parse_header(task_file: Path) -> tuple[str, int, str, str | None]:
try:
with Path(task_file).open("rb") as handle:
head = handle.read(1024)
@ -103,14 +109,47 @@ def _parse_header(task_file: Path) -> tuple[str, int, str]:
if match is None:
raise SelectorInputError(
"malformed_header",
"first 1KiB must contain <!-- task=... plan=N tag=... -->",
"first line must contain <!-- task=... plan=N tag=... "
"[milestone-task=id[,id...]] -->",
)
return match.group(1), int(match.group(2)), match.group(3)
task = match.group("task")
milestone_task = match.group("milestone_task")
task_ids = tuple(milestone_task.split(",")) if milestone_task else ()
invalid_ids = [
task_id
for task_id in task_ids
if _MILESTONE_TASK_ID_RE.fullmatch(task_id) is None
]
if invalid_ids:
raise SelectorInputError(
"invalid_milestone_task",
"milestone-task ids must follow the Milestone item-id grammar: "
+ ", ".join(invalid_ids),
)
if len(task_ids) != len(set(task_ids)):
raise SelectorInputError(
"duplicate_milestone_task",
"milestone-task must contain unique comma-separated Task ids",
)
if task.split("/", 1)[0].startswith("m-") and not milestone_task:
raise SelectorInputError(
"missing_milestone_task",
"m-* task headers require milestone-task=id[,id...]",
)
if not task.split("/", 1)[0].startswith("m-") and milestone_task:
raise SelectorInputError(
"unexpected_milestone_task",
"non-milestone task headers must omit milestone-task",
)
return task, int(match.group("plan")), match.group("tag"), milestone_task
def _work_unit_id(header: tuple[str, int, str]) -> str:
task, plan, tag = header
return f"{task}::plan-{plan}::tag-{tag}"
def _work_unit_id(header: tuple[str, int, str, str | None]) -> str:
task, plan, tag, milestone_task = header
work_unit_id = f"{task}::plan-{plan}::tag-{tag}"
if milestone_task:
work_unit_id += f"::milestone-task-{milestone_task}"
return work_unit_id
def _validate_evaluated_at(evaluated_at: datetime) -> None:

View file

@ -1930,7 +1930,8 @@ class WorkLogInvokeIntegrationTest(unittest.IsolatedAsyncioTestCase):
review = task_directory / "CODE_REVIEW-cloud-G07.md"
plan.write_text(
"<!-- task=m-principal-provider-credential-slot-routing/"
"02+01_credential_catalog plan=1 tag=TEST -->\n",
"02+01_credential_catalog plan=1 tag=TEST "
"milestone-task=credential-catalog -->\n",
encoding="utf-8",
)
review.write_text("review\n", encoding="utf-8")
@ -5626,6 +5627,7 @@ class WriteSetTest(unittest.TestCase):
workspace = Path(temporary)
plan = workspace / "PLAN-cloud-G10.md"
plan.write_text(
"<!-- task=validate-plan plan=0 tag=TEST -->\n\n"
"## Modified Files Summary\n\n"
"| File | Items |\n|---|---|\n"
"| `agent-test/runs/output-filter-recovery/**` | TEST-1 |\n",
@ -5649,6 +5651,91 @@ class WriteSetTest(unittest.TestCase):
stderr.getvalue(),
)
def test_validate_plan_requires_known_milestone_task_scope(self):
with tempfile.TemporaryDirectory() as temporary:
workspace = Path(temporary)
milestone = (
workspace
/ "agent-roadmap"
/ "phase"
/ "security"
/ "milestones"
/ "secret-at-rest.md"
)
milestone.parent.mkdir(parents=True)
milestone.write_text(
"# Milestone\n\n## 기능\n\n"
"- [ ] [secret-at-rest] Encrypt stored secrets\n"
"- [ ] [validation-tests] Verify ciphertext handling\n\n"
"## 구현 잠금\n\n"
"- [ ] [decision-only] Select a user-owned policy\n",
encoding="utf-8",
)
claimed = workspace / "src" / "secret.go"
claimed.parent.mkdir(parents=True)
plan = workspace / "PLAN-cloud-G10.md"
def validate(header: str) -> tuple[int, str]:
plan.write_text(
header
+ "\n\n## Modified Files Summary\n\n"
+ "| File | Items |\n|---|---|\n"
+ "| `src/secret.go` | API-1 |\n",
encoding="utf-8",
)
with mock.patch.object(
sys,
"argv",
[
str(SCRIPT),
"--workspace",
str(workspace),
"--validate-plan",
str(plan),
],
), mock.patch("sys.stderr", new_callable=io.StringIO) as stderr:
result = dispatch.main()
return result, stderr.getvalue()
missing_result, missing_error = validate(
"<!-- task=m-secret-at-rest/01_storage plan=0 tag=API -->"
)
self.assertEqual(missing_result, 2)
self.assertIn("milestone-task=<id[,id...]>", missing_error)
unknown_result, unknown_error = validate(
"<!-- task=m-secret-at-rest/01_storage plan=0 tag=API "
"milestone-task=unknown -->"
)
self.assertEqual(unknown_result, 2)
self.assertIn("unknown", unknown_error)
non_feature_result, non_feature_error = validate(
"<!-- task=m-secret-at-rest/01_storage plan=0 tag=API "
"milestone-task=decision-only -->"
)
self.assertEqual(non_feature_result, 2)
self.assertIn("decision-only", non_feature_error)
invalid_result, invalid_error = validate(
"<!-- task=m-secret-at-rest/01_storage plan=0 tag=API "
"milestone-task=secret.at.rest -->"
)
self.assertEqual(invalid_result, 2)
self.assertIn("item-id 계약", invalid_error)
valid_result, valid_error = validate(
"<!-- task=m-secret-at-rest/01_storage plan=0 tag=API "
"milestone-task=secret-at-rest,validation-tests -->"
)
self.assertEqual(valid_result, 0)
self.assertEqual(valid_error, "")
self.assertEqual(
dispatch.work_unit_id_from_file(plan),
"m-secret-at-rest/01_storage::plan-0::tag-API::"
"milestone-task-secret-at-rest,validation-tests",
)
def test_workspace_claims_persist_replace_wait_and_release_on_completion(self):
with tempfile.TemporaryDirectory() as temporary:
workspace = Path(temporary)
@ -7455,6 +7542,7 @@ class OrchestrationPersistenceTest(unittest.TestCase):
plan = workspace / "PLAN-cloud-G10.md"
target = workspace / "src" / "target.go"
plan.write_text(
"<!-- task=validate-child plan=0 tag=TEST -->\n\n"
"## Modified Files Summary\n\n"
"| File | Items |\n|---|---|\n"
f"| `{target}` | TEST-1 |\n",
@ -12083,6 +12171,52 @@ class ArtifactLanguageContractTest(unittest.TestCase):
dispatch.DISPATCHER_CHILD_BOUNDARY_PROMPT,
)
def test_milestone_task_metadata_and_aggregation_contract_is_shared(self):
skills_root = Path(__file__).resolve().parents[3]
plan_skill = (skills_root / "common" / "plan" / "SKILL.md").read_text(
encoding="utf-8"
)
review_skill = (
skills_root / "common" / "code-review" / "SKILL.md"
).read_text(encoding="utf-8")
refine_skill = (
skills_root / "common" / "refine-plans" / "SKILL.md"
).read_text(encoding="utf-8")
sync_skill = (
skills_root / "common" / "sync-milestone-workstate" / "SKILL.md"
).read_text(encoding="utf-8")
update_skill = (
skills_root / "common" / "update-roadmap" / "SKILL.md"
).read_text(encoding="utf-8")
project_rules = (
skills_root.parent / "rules" / "project" / "rules.md"
).read_text(encoding="utf-8")
complete_template = (
skills_root
/ "common"
/ "code-review"
/ "templates"
/ "complete-log-template.md"
).read_text(encoding="utf-8")
self.assertIn("milestone-task=<task-id>[,<task-id>...]", plan_skill)
self.assertIn("exact first-line generation header", review_skill)
self.assertTrue(
complete_template.startswith(
"<!-- task={task_name} plan={plan_number} tag={TAG}"
"{milestone_task_metadata_or_omit} -->"
)
)
self.assertNotIn("## Roadmap Completion", complete_template)
self.assertIn("합집합은 parent id 집합과 정확히 같아야", refine_skill)
self.assertIn("evidence routing 범위", sync_skill)
self.assertIn("모든 완료 로그를 id별로", sync_skill)
self.assertIn("sync-milestone-workstate", update_skill)
self.assertIn(
"task-group-only 및 `Roadmap Completion` 단건 반영 문구를 legacy",
project_rules,
)
class ParallelLimitSchedulingTest(unittest.IsolatedAsyncioTestCase):
"""Deterministic regressions for the workspace-global --max-parallel cap."""

View file

@ -70,11 +70,16 @@ def write_task_file(
task: str = "grp/01_unit",
plan: int = 0,
tag: str = "API",
milestone_task: str | None = None,
body: str = "body\n",
) -> Path:
path = Path(directory) / f"{kind}-{lane}-G{grade:02d}.md"
milestone_metadata = (
f" milestone-task={milestone_task}" if milestone_task else ""
)
path.write_text(
f"<!-- task={task} plan={plan} tag={tag} -->\n\n# title\n\n{body}",
f"<!-- task={task} plan={plan} tag={tag}{milestone_metadata} -->\n\n"
f"# title\n\n{body}",
encoding="utf-8",
)
return path
@ -206,6 +211,86 @@ class SelectorContractTests(unittest.TestCase):
with self.assertRaises(selector.SelectorInputError) as ctx:
selector.select_execution_target(path, evaluated_at=kst(12))
self.assertEqual(ctx.exception.code, "malformed_header")
path.write_text(
"# preamble\n<!-- task=grp/01_unit plan=0 tag=API -->\n",
encoding="utf-8",
)
with self.assertRaises(selector.SelectorInputError) as ctx:
selector.select_execution_target(path, evaluated_at=kst(12))
self.assertEqual(ctx.exception.code, "malformed_header")
def test_milestone_task_scope_is_required_and_part_of_identity(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
missing = write_task_file(
root,
"PLAN",
"cloud",
5,
task="m-secret-at-rest/01_storage",
)
with self.assertRaises(selector.SelectorInputError) as ctx:
selector.select_execution_target(missing, evaluated_at=kst(12))
self.assertEqual(ctx.exception.code, "missing_milestone_task")
scoped = write_task_file(
root,
"PLAN",
"cloud",
5,
task="m-secret-at-rest/01_storage",
milestone_task="secret-at-rest,validation-tests",
)
result = selector.select_execution_target(scoped, evaluated_at=kst(12))
self.assertEqual(
result["work_unit_id"],
"m-secret-at-rest/01_storage::plan-0::tag-API::"
"milestone-task-secret-at-rest,validation-tests",
)
def test_milestone_task_scope_rejects_duplicates_and_non_m_tasks(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
duplicate = write_task_file(
root,
"PLAN",
"cloud",
5,
task="m-secret-at-rest/01_storage",
milestone_task="secret-at-rest,secret-at-rest",
)
with self.assertRaises(selector.SelectorInputError) as duplicate_ctx:
selector.select_execution_target(duplicate, evaluated_at=kst(12))
self.assertEqual(
duplicate_ctx.exception.code, "duplicate_milestone_task"
)
unexpected = write_task_file(
root,
"PLAN",
"cloud",
5,
milestone_task="secret-at-rest",
)
with self.assertRaises(selector.SelectorInputError) as unexpected_ctx:
selector.select_execution_target(unexpected, evaluated_at=kst(12))
self.assertEqual(
unexpected_ctx.exception.code, "unexpected_milestone_task"
)
malformed_id = write_task_file(
root,
"PLAN",
"cloud",
5,
task="m-secret-at-rest/01_storage",
milestone_task="secret.at.rest",
)
with self.assertRaises(selector.SelectorInputError) as malformed_ctx:
selector.select_execution_target(malformed_id, evaluated_at=kst(12))
self.assertEqual(
malformed_ctx.exception.code, "invalid_milestone_task"
)
def test_work_unit_id_stable_across_body_changes(self):
with TemporaryDirectory() as tmp:

View file

@ -1,6 +1,6 @@
---
name: plan
description: Analyze the current repository and write a detailed PLAN-{build_lane}-GNN.md plus CODE_REVIEW-{review_lane}-GNN.md stub for implementation work. Use for every feature, refactor, bug fix, and code-review WARN/FAIL follow-up that enters the plan-code-review loop. Every initial or follow-up pair must run finalize-task-routing after analysis and before routed filenames are chosen. Milestone-linked work uses a reserved m-prefixed task group so runtime can route PASS completion events to update-roadmap.
description: Analyze the current repository and write a detailed PLAN-{build_lane}-GNN.md plus CODE_REVIEW-{review_lane}-GNN.md stub for implementation work. Use for every feature, refactor, bug fix, and code-review WARN/FAIL follow-up that enters the plan-code-review loop. Every initial or follow-up pair must run finalize-task-routing after analysis and before routed filenames are chosen. Milestone-linked work uses an m-prefixed task group and first-line milestone-task ids so PASS logs can be aggregated by sync-milestone-workstate.
---
# Plan
@ -13,7 +13,7 @@ Create the planning artifacts for the implementation loop:
plan skill -> analysis -> finalize-task-routing -> PLAN-{build_lane}-GNN.md + CODE_REVIEW-{review_lane}-GNN.md stub
implementation -> code changes + filled implementation evidence in CODE_REVIEW-{review_lane}-GNN.md
code-review skill -> verdict + archive, complete.log and task-directory archive move, USER_REVIEW.md, or mandatory plan-skill follow-up
runtime -> for m-prefixed PASS completion events, state check and optional update-roadmap call
runtime -> for m-prefixed PASS completion events, aggregate complete.log evidence with sync-milestone-workstate
```
`code-review` may stop the automatic loop with `USER_REVIEW.md` for either a selected Milestone `구현 잠금 > 결정 필요` item (`milestone-lock`) or required external verification that cannot proceed without a user-controlled runner, device, credential, interactive session, evidence handoff, or explicit authorization (`external-execution`). A current-host mismatch or missing command is not enough when a repository-declared runner or authorized executor can perform the step automatically. Repeated non-PASS reviews and missing evidence are not user-review reasons by themselves. Plan creation after `USER_REVIEW.md` requires its recorded user action or decision to be resolved. If that resolution closes the task as complete/PASS, code-review writes `complete.log` and archives the task instead of creating a new plan.
@ -80,7 +80,7 @@ Task directory naming rules:
- A normal single-plan task uses `agent-task/{task_group}/` with a short snake_case category name, e.g. `agent-task/refactoring/`.
- If the plan is based on a selected active Milestone, use `agent-task/m-<milestone-slug>/` as the task group. Do not include the Phase slug, Epic id, Task id, or a separate task slug in the task group.
- `m-<milestone-slug>` is a reserved top-level task group namespace for Milestone-linked work. Non-roadmap tasks must not use `m-`.
- Runtime completion-event routing for `m-*` reads only the top-level `{task_group}` name. It resolves `<milestone-slug>` by matching exactly one active file at `agent-roadmap/phase/*/milestones/<milestone-slug>.md`; archive paths are not target candidates.
- Runtime completion-event routing for `m-*` reads the top-level `{task_group}` name and the first-line `milestone-task` ids preserved in `complete.log`. It resolves `<milestone-slug>` by matching exactly one active file at `agent-roadmap/phase/*/milestones/<milestone-slug>.md`; archive paths are not target candidates.
- When split gates require decomposition, create one shared category folder and multiple subtask directories under it. Each subtask directory owns exactly one normal active plan file and one normal active review stub.
- Multi-plan output is a set of independent `PLAN-{build_lane}-GNN.md` + `CODE_REVIEW-{review_lane}-GNN.md` pairs across `agent-task/{task_group}/{subtask_dir}/` folders, not multiple plan files inside one folder.
- Multi-plan subtask directory names must start with a stable two-digit task index. The index must increase across sibling subtask directories for sorting, but it is not a serial execution dependency.
@ -177,8 +177,10 @@ If the selected review already has an appended verdict, accept it only in `prepa
- 기능 Task에 `검증:`이 있으면 구현 계획의 같은 plan item 안에 해당 검증을 포함한다. 검증이 명시되지 않은 기능 Task에는 억지 검증 항목을 만들지 말고, 필요한 일반 빌드/회귀 확인만 최종 검증에 둔다.
- 선택한 활성 Milestone 범위에 속하는 구현 계획이면 `{task_group}``m-<milestone-slug>`로 정한다. `<milestone-slug>`는 선택한 Milestone 경로의 파일명에서 `.md`를 제거한 값이다.
- 같은 Milestone에서 split work가 필요하면 기존 split 규칙 그대로 `agent-task/m-<milestone-slug>/<subtask_dir>/` 아래에 계획 파일을 만든다.
- Milestone 기능 Task 완료를 목표로 하는 계획이면 `Roadmap Targets` 섹션에 활성 Milestone 경로와 완료 대상 Task id를 고정한다. 이 섹션은 `complete.log``Roadmap Completion` 근거로 복사되어 `update-roadmap`이 해당 Task만 체크하는 anchor가 된다. 이 섹션은 `{task_group}`이 해당 Milestone slug의 `m-<milestone-slug>`일 때만 쓴다.
- Milestone 작업이 아니거나, Milestone 안의 조사/하위 구현처럼 특정 기능 Task 완료를 주장하지 않는 계획이면 `Roadmap Targets` 섹션을 쓰지 않는다. 섹션이 없으면 PASS 후에도 roadmap Task 체크를 하지 않는다.
- Milestone 작업 계획은 첫 줄에 `milestone-task=<task-id>[,<task-id>...]`를 넣어 이 작업이 기여하는 기존 기능 Task id를 고정한다. id는 선택한 활성 Milestone `기능` 섹션에 실제로 존재해야 하고, `rules-roadmap.md`의 item-id 문법을 따르며, 중복 없이 쉼표로 구분하고 공백을 넣지 않는다.
- `milestone-task`는 PASS 즉시 체크할 완료 주장이나 plan 하나당 Task 하나라는 뜻이 아니다. 여러 plan이 같은 id에 기여할 수 있고 한 plan이 여러 id에 기여할 수 있다. 이후 `sync-milestone-workstate`가 같은 Milestone task group의 모든 `complete.log`를 id별로 모아 Task 설명·검증·SDD evidence 충족 여부를 평가한다.
- Milestone 범위의 하위 구현이나 조사도 관련 기능 Task id가 명확하면 같은 id를 기록한다. 관련 id를 정할 수 없다면 `m-*` task group으로 계획하지 말고, 먼저 Milestone 기능 Task를 보강하거나 비마일스톤 task group으로 분리한다.
- WARN/FAIL follow-up은 범위가 그대로면 이전 `milestone-task` id 목록을 정확히 유지한다. 범위를 바꾸는 경우에만 현재 Milestone과 SDD mapping을 다시 확인해 id를 명시적으로 교정하며, id가 조용히 누락되거나 다른 id로 바뀌면 안 된다.
- `agent-roadmap/` 디렉터리가 없으면 기존 task routing 규칙대로 진행한다.
Use short snake_case task group names for non-roadmap work, e.g. `api_refactor`.
@ -230,10 +232,19 @@ Set `plan_number=post_archive_plan_log_count`. The new pair's future archive suf
Render the complete plan in memory first. In `write` mode, write it to the routed plan basename. In `prepare-follow-up` mode, return the exact rendered body as `prepared_plan` and do not write it.
Header line must be exactly:
Header line must be exactly one of these forms:
```markdown
<!-- task={task_name} plan={plan_number} tag={TAG} -->
<!-- task=m-<milestone-slug>[/<subtask_dir>] plan={plan_number} tag={TAG} milestone-task=<task-id>[,<task-id>...] -->
```
Use the second form for every `m-*` task and the first form for every non-milestone task. The PLAN and review stub first lines must be identical.
Example:
```markdown
<!-- task=m-principal-provider-credential-slot-routing/07+01,02,05_secret_material plan=0 tag=API milestone-task=secret-at-rest -->
```
Required sections:
@ -242,20 +253,9 @@ Required sections:
- `For the Implementing Agent`: warn that filling implementation-owned `CODE_REVIEW-*-G??.md` sections is mandatory. Tell the implementer to run verification, fill actual notes/output, keep active files in place, and report ready for review; finalization is code-review-skill only. If blocked, the implementer records only exact blocker evidence, attempted commands/output, and resume conditions in implementation-owned evidence fields. It must not ask the user, call user-input tools, create control-plane stop files, classify the next state, archive logs, or write `complete.log`.
- `Background`: 2-4 sentences explaining why the work is needed.
- `Archive Evidence Snapshot`: include this section only when the plan resumes from `USER_REVIEW.md`, a prior archived review, or any archive evidence. Omit it for first-pass plans with no archive evidence. The section must contain only the archive facts needed to implement without rereading archive by default: prior task/archive paths, verdict, Required/Suggested/Nit summary, affected files, verification evidence, and any roadmap carryover. If exact prior context is still required, cite the specific archive file paths allowed to read; do not ask the implementer to search `agent-task/archive/**` broadly.
- `Roadmap Targets`: include this section only when the plan is intended to complete one or more existing Milestone 기능 Task ids. Omit the section entirely for non-roadmap work or Milestone-adjacent work that should not check a Task on PASS. Format exactly:
```markdown
## Roadmap Targets
- Milestone: `agent-roadmap/phase/<phase-slug>/milestones/<milestone-slug>.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/<phase-slug>/milestones/<milestone-slug>.md)
- Task ids:
- `<task-id>`: <Task text or concise label>
- Completion mode: check-on-pass
```
- `Analysis`: record the findings from Step 2 and the final routed output from Step 3. This section is the written output of the analysis — not a summary, but the actual findings that justify the plan's scope and decisions. Must include all of the following subsections:
- `Files Read`: list every source and test file read during analysis, with path. List verification-context source files only when they were actually present and read.
- `SDD Criteria`: for `SDD: 필요` Milestones, list the SDD path, status, targeted Acceptance Scenario ids, their Milestone Task ids, and the Evidence Map rows that drive the plan. State explicitly how those rows shaped the implementation checklist and final verification. If the selected Milestone has `SDD: 불필요`, state the recorded reason. If the work is not Milestone-linked, state "not applicable".
- `SDD Criteria`: for `SDD: 필요` Milestones, list the SDD path, status, first-line `milestone-task` ids, targeted Acceptance Scenario ids, and the Evidence Map rows that drive the plan. State explicitly how those rows shaped the implementation checklist and final verification. If the selected Milestone has `SDD: 불필요`, state the recorded reason. If the work is not Milestone-linked, state "not applicable".
- `Verification Context`: state whether a handoff was supplied, every source path actually read, concrete commands/criteria applied, preconditions, constraints, gaps, confidence, and repository-native fallback evidence. If required verification leaves the current checkout, include an `External Verification Preflight` record with runner, repo root/workdir, branch/HEAD/dirty state, source sync status, binary/artifact paths, required command help/version output, config path, runtime identity, ports/process state, external hosts, OS/arch assumptions, and the exact setup/sync/rebuild step or blocker derived from mismatches.
- `Test Coverage Gaps`: list each behavior change and whether existing tests cover it; explicitly note gaps.
- `Symbol References`: list renamed/removed symbols and every call site found, or state "none" if no symbols were changed.
@ -323,8 +323,8 @@ Verification fidelity rules:
Read `agent-ops/skills/common/plan/templates/review-stub-template.md` in full only after Step 3 returns `status: routed`.
Replace every occurrence of each token below:
- Scalar tokens: `{date}`, `{task_group}`, `{task_name}`, `{plan_number}`, `{TAG}`, `{build_lane}`, `{build_grade}`, `{review_lane}`, `{review_grade}`, `{plan_log_number}`, `{review_log_number}`.
- Plan-copy tokens: `{roadmap_targets_or_omit}`, `{archive_evidence_snapshot_or_omit}`, `{implementation_checklist}`, `{review_checkpoints}`.
- Scalar tokens: `{date}`, `{task_group}`, `{task_name}`, `{plan_number}`, `{TAG}`, `{milestone_task_metadata_or_omit}`, `{build_lane}`, `{build_grade}`, `{review_lane}`, `{review_grade}`, `{plan_log_number}`, `{review_log_number}`. Set `{milestone_task_metadata_or_omit}` to ` milestone-task=<ids>` for `m-*` and to an empty string otherwise.
- Plan-copy tokens: `{archive_evidence_snapshot_or_omit}`, `{implementation_checklist}`, `{review_checkpoints}`.
- Generated row/section tokens: `{implementation_completion_rows}` contains one row for every plan item, and `{verification_result_sections}` contains the fixed verification instructions plus every intermediate/final command from the plan.
Use the routed build/review grades independently. Remove optional plan-copy content by replacing its token with an empty string, not by leaving template instructions.
@ -349,12 +349,12 @@ Do not write or return a prepared pair when either routing target is not `routed
- Split work, if any, uses one shared `agent-task/{task_group}/` parent and one subtask directory per plan/review pair with names like `01_core`, `02+01_edge_integration`, `03+01_node_integration`; dependency details live in the subtask directory name as `NN+PP[,QQ...]_subtask_name`.
- Split sibling indices follow topological dependency order: every predecessor is lower than its consumer, and every gap is explained by an unchanged existing predecessor or an occupied active/archive index.
- Milestone-linked work uses `agent-task/m-<milestone-slug>/` as the task group; non-roadmap task groups do not start with `m-`.
- Both first lines match `<!-- task={task_name} plan={plan_number} tag={TAG} -->`.
- Both first lines are identical. Non-milestone pairs match `<!-- task={task_name} plan={plan_number} tag={TAG} -->`; `m-*` pairs append exactly ` milestone-task=<task-id>[,<task-id>...]` before ` -->`.
- The review stub was rendered from `agent-ops/skills/common/plan/templates/review-stub-template.md` after routing and has no unresolved known template token.
- In `write` mode, previous active files, if any, were archived with lane/grade parsed from their own basenames and the correct current archive suffixes. In `prepare-follow-up` mode, those archive names were only predicted.
- In `write` mode when resuming from `USER_REVIEW.md`, it was archived to the calculated `current_user_review_archive_name` and the resolved user action or decision was recorded in the new plan.
- `Roadmap Targets` exists only when PASS should check explicit Milestone Task ids, the task group is `m-<milestone-slug>` for the listed Milestone path, and every listed Task id exists in the selected active Milestone.
- If `Roadmap Targets` exists in the plan, the review stub contains the identical section. If it does not exist in the plan, the review stub omits it too.
- Every `m-*` pair has a non-empty, duplicate-free `milestone-task` list whose ids exist in the selected active Milestone; non-milestone pairs omit the field.
- `milestone-task` ids describe evidence contribution scope, not check-on-PASS completion. Split/follow-up pairs preserve their declared scope under the refinement and follow-up rules.
- If the selected Milestone has `SDD: 필요`, the plan's `Analysis > SDD Criteria` (legacy: `분석 결과 > SDD 기준`) proves that the implementation checklist and final verification were derived from the approved SDD Acceptance Scenarios and Evidence Map. Missing SDD mapping blocks plan creation.
- If the plan is a follow-up or resumes from prior archive evidence, it has `Archive Evidence Snapshot` and the review stub contains the identical section.
- `Analysis > Verification Context` records supplied handoff facts, source paths actually read, external preflight, gaps, confidence, and repository-native fallback evidence.

View file

@ -1,4 +1,4 @@
<!-- task={task_name} plan={plan_number} tag={TAG} -->
<!-- task={task_name} plan={plan_number} tag={TAG}{milestone_task_metadata_or_omit} -->
# Code Review Reference - {TAG}
@ -16,7 +16,6 @@
date={date}
task={task_name}, plan={plan_number}, tag={TAG}
{roadmap_targets_or_omit}
{archive_evidence_snapshot_or_omit}
## For the Review Agent
@ -29,7 +28,7 @@ Review completion means the following steps are finished:
1. Append verdict and `review_rework_count` / `evidence_integrity_failure` routing signals.
2. Archive `CODE_REVIEW-{review_lane}-{review_grade}.md``code_review_{review_lane}_{review_grade}_{review_log_number}.log` and `PLAN-{build_lane}-{build_grade}.md``plan_{build_lane}_{build_grade}_{plan_log_number}.log`.
3. If PASS, write `complete.log` and move active task directory to `agent-task/archive/YYYY/MM/{task_name}/`. If WARN/FAIL, fully write the next filesystem state required by the code-review skill.
4. If PASS and task group is `m-<milestone-slug>`, report completion event metadata. Roadmap state check and `update-roadmap` calls are runtime responsibilities.
4. If PASS and task group is `m-<milestone-slug>`, preserve the first-line `milestone-task` metadata in `complete.log` and report it for the runtime aggregation event. Roadmap state evaluation belongs to `sync-milestone-workstate`.
5. Check applicable `Review-Only Checklist` items at the final `.log` location before reporting.
---
@ -56,7 +55,7 @@ Review completion means the following steps are finished:
- [ ] Verify that the Agent-Ops managed block in `.gitignore` unignores `agent-task/**/*.md` and `agent-task/**/*.log` and ignores `agent-roadmap/current.md`.
- [ ] If PASS, write `complete.log` based on `agent-ops/skills/common/code-review/templates/complete-log-template.md` and leave no active `.md` files.
- [ ] If PASS, move active task directory `agent-task/{task_name}/` to `agent-task/archive/YYYY/MM/{task_name}/` and update this checklist at the final archive path.
- [ ] If PASS and task group is `m-<milestone-slug>`, report completion event metadata for runtime, without modifying roadmap or directly calling `update-roadmap`.
- [ ] If PASS and task group is `m-<milestone-slug>`, preserve and report `milestone-task` metadata for runtime aggregation, without modifying roadmap or directly calling `update-roadmap`.
- [ ] If PASS for split work, remove empty active parent `agent-task/{task_group}/` or verify it was kept due to remaining siblings/files.
- [ ] If WARN/FAIL, write the next filesystem state matching code-review verdict and do not write `complete.log`.
@ -87,7 +86,6 @@ _Record key design decisions here._
| Section | Owner | Note |
|---------|-------|------|
| Header comment, Overview, Review Agent Instructions | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
| Implementation Item Completion (item names) | Fixed at stub creation | Implementing agent checks `[ ]``[x]` only |
| Implementation Checklist (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]``[x]` only |

View file

@ -45,13 +45,13 @@ description: 현재 plan들 세분화해, 현재 plan 세분화, 기존 plan 더
4. **현재 PLAN/CODE_REVIEW 형식을 유지한다**
- 각 child는 기존 PLAN을 복제한 뒤 자신의 scope만 남긴다. Header/task, title, background, `구현 체크리스트`, plan item, `수정 파일 요약`, `최종 검증`을 child 경계에 맞게 갱신한다. 기존 PLAN에 `분석 결과`가 있으면 읽은 파일·테스트 공백·심볼 참조·분할 판단·범위 결정 근거도 child 범위로 줄이고 기존 `최종 라우팅`은 제거한다.
- `Roadmap Targets`는 전체 결과를 닫는 closure child 하나에만 둔다.
- `m-*` parent의 `milestone-task` id 집합은 child들에 보존한다. 각 child는 자신의 범위가 기여하는 parent id의 비어 있지 않은 부분집합을 첫 줄에 기록하고, 여러 child가 같은 id에 기여하면 중복 배치를 허용한다. 모든 child id의 합집합은 parent id 집합과 정확히 같아야 하며 parent 밖 id를 추가하거나 closure child 하나에만 몰아넣지 않는다.
- 각 child PLAN body가 완성되면 parent와 sibling의 이전 lane/G, 점수, route 사유, filename을 입력에서 제거하고 child packet과 기존 PLAN에 이미 있는 사실만 사용해 `finalize-task-routing``evaluation_mode=isolated-reassessment`로 정확히 한 번 실행한다. Routing 때문에 source/test/log를 다시 읽지 않는다.
- `review_rework_count``evidence_integrity_failure`만 route-free 운영 이력으로 전달한다. Child 중 하나라도 `status=routed`가 아니면 원본 pair와 sibling 경로를 바꾸지 않고 중단 사유만 보고한다.
- 각 child의 `최종 라우팅`, build/review lane, G, canonical basename은 finalizer 출력으로 갱신한다. Lane, G, boundary, filename을 수작업으로 만들거나 parent 값을 복사하지 않는다.
- 각 review는 기존 CODE_REVIEW를 복제한 뒤 header/task, 완료표, 구현 checklist, checkpoint, 검증 section을 matching PLAN과 맞춘다. 고정 안내와 review 전용 section의 문구는 유지하되 child task path와 future archive suffix 참조는 갱신한다.
- PLAN과 review는 입력에 이미 있는 section 구조를 유지하며 없는 section을 새로 만들지 않는다. 첫 task header 외의 HTML metadata comment는 출력에서 제거한다.
- PLAN과 review의 첫 줄은 동일한 `task`, `plan`, `tag`를 사용한다. Checklist 문구·순서와 검증 명령을 서로 일치시킨다.
- PLAN과 review의 첫 줄은 동일한 `task`, `plan`, `tag`, `milestone-task`를 사용한다. 비마일스톤 pair에는 `milestone-task`를 추가하지 않는다. Checklist 문구·순서와 검증 명령을 서로 일치시킨다.
5. **최종 pair로 직접 교체한다**
- 모든 child pair, finalizer 출력, sibling reindex, directory rename map을 먼저 메모리에서 완성하고 최종 경로·archive log 충돌을 확인한다.

View file

@ -1,6 +1,5 @@
---
name: roadmap-sdd
version: 1.0.0
description: 로드맵 Milestone에 녹아 있는 SDD 설계 게이트를 판정, 생성, 갱신, 사용자 리뷰 대기, 잠금 해제, archive 처리할 때 사용한다. 사용자가 SDD, spec gate, 설계 게이트, SDD 필요 여부, SDD 승인 준비, SDD 사용자 리뷰, SDD 잠금 해제, SDD archive를 요청하거나, 큰 Milestone의 구현 잠금이 SDD 필요 상태일 때 사용한다.
---
@ -89,7 +88,7 @@ SDD가 필요한 Milestone은 `구현 잠금`에 아래 필드를 둔다.
- [ ] SDD 잠금이 해제되어 있다
- [ ] SDD 사용자 리뷰가 없거나 승인/해결되었다
- [ ] Acceptance Scenario가 Milestone 기능 Task와 연결되어 있다
- [ ] Evidence Map이 완료 시 `Roadmap Completion`과 최종 검증 evidence로 검증 가능하게 연결되어 있다
- [ ] Evidence Map이 완료 시 `complete.log` 첫 줄의 `milestone-task` id별 집계와 최종 검증 evidence로 검증 가능하게 연결되어 있다
- 결정 필요: 없음
```
@ -164,7 +163,7 @@ SDD 문서는 자체 잠금을 가진다.
5. SDD 상태가 `[승인됨]`이 아니거나 `SDD 잠금``잠금`이면 `blocked`로 보고한다.
6. `USER_REVIEW.md`가 있으면 `blocked`로 보고한다.
7. Acceptance Scenario가 Milestone 기능 Task id와 연결되어 있는지 확인한다.
8. Evidence Map이 완료 시 `Roadmap Completion`과 최종 검증 evidence로 검증될 수 있도록 scenario, task, evidence가 매핑되어 있는지 확인한다.
8. Evidence Map이 완료 시 같은 Milestone task group의 `complete.log``milestone-task` id별로 집계하고 최종 검증 evidence와 대조할 수 있도록 scenario, task, evidence가 매핑되어 있는지 확인한다.
9. 모두 충족하면 `pass`로 보고한다.
### review-ready

View file

@ -10,7 +10,7 @@
- "마일스톤 완료해도 될지 검토", "현 마일스톤 종료 검토", "현재 마일스톤 닫고 다음 마일스톤 지정"처럼 종료 판단, spec sync, roadmap archive, 다음 Milestone 지정을 함께 요구하는 요청은 `complete-milestone`으로 보낸다. 이 흐름 안에서 `update-spec`을 필수 gate로 수행한다. 사용할 테스트 환경 규칙이 있으면 먼저 `update-test mode=resolve-context`의 중립 `Verification Context`를 전달하고, 없거나 불완전하면 complete-milestone의 repository-native fallback을 사용한다.
- 구현 계획 요청에서 선택 Milestone의 구현 잠금이 남아 있으면 `plan`은 구현 계획을 만들지 않고 잠금 차단을 보고한다.
- SDD 생성/갱신/잠금 해제는 `roadmap-sdd` 또는 `update-roadmap` 요청으로 처리한다.
- 런타임이 `origin-task`/`complete-log` 단건 완료 이벤트를 전달한 경우는 `update-roadmap`으로 처리한다.
- 런타임이 새 형식의 `origin-task`/`complete-log` 완료 이벤트를 전달하고 첫 줄에 `milestone-task`가 있으면 `sync-milestone-workstate`로 처리해 같은 Milestone task group의 evidence를 집계한다. first-line metadata가 없고 legacy `Roadmap Completion`만 있는 과거 단건 이벤트는 `update-roadmap` 호환 흐름으로 처리할 수 있다.
- active/archive `complete.log`, 관련 파일, git history를 종합해 Milestone 작업 상태를 복구하거나 확인하는 요청은 `sync-milestone-workstate`로 처리한다.
- plan 요청에 사용할 테스트 환경 규칙이 있으면 `update-test mode=resolve-context`로 read-only `Verification Context`를 만든 뒤 `plan`에 전달한다. 규칙이 없거나 매칭되지 않으면 파일을 생성하지 않고 plan의 repository-native fallback을 사용한다.
- `sync-agent-ui``plan-required`로 라우팅한 작업은 plan pair 생성 뒤 `sync-agent-ui mode=prepare-code-work`로 task/UI 매핑을 기록한다. 일반 code-review PASS와 exact `complete.log` 생성 뒤에는 원래 `task-path``completion-log``sync-agent-ui mode=reconcile-completion`에 전달해 해당 매핑만 정합화한다.
@ -37,7 +37,7 @@
| README 작성해줘, README 만들어줘, 프로젝트 설명 문서 만들어줘 | `agent-ops/skills/common/create-readme/SKILL.md` |
| 핸즈오프 남겨, handoff 작성, 인수인계 작성, 다른 세션에서 이어가게 정리, 작업을 이어받도록 기록 | `agent-ops/skills/common/create-handoff/SKILL.md` |
| 로드맵 만들어줘, roadmap 생성, 마일스톤 설계, goal/phase 구조 잡아줘 | `agent-ops/skills/common/create-roadmap/SKILL.md` |
| 현 마일스톤과 작업현황 동기화, 현재 마일스톤 작업현황 동기화, 마일스톤 작업현황 동기화, 마일스톤 완료내역 동기화, agent-task 완료를 마일스톤에 반영, complete.log 후보 스캔, 누락된 Roadmap Completion 복구, 파일/git 기준 작업 상태 확인, 작은 작업 완료 반영, 마일스톤 체크박스 재동기화, 로드맵 작업 완료 상태 동기화 | `agent-ops/skills/common/sync-milestone-workstate/SKILL.md` |
| 현 마일스톤과 작업현황 동기화, 현재 마일스톤 작업현황 동기화, 마일스톤 작업현황 동기화, 마일스톤 완료내역 동기화, agent-task 완료를 마일스톤에 반영, complete.log 후보 스캔, milestone-task id별 evidence 집계, 누락된 Roadmap Completion 복구, 파일/git 기준 작업 상태 확인, 작은 작업 완료 반영, 마일스톤 체크박스 재동기화, 로드맵 작업 완료 상태 동기화 | `agent-ops/skills/common/sync-milestone-workstate/SKILL.md` |
| 마일스톤 완료해도 될지 검토해봐, 현 마일스톤 종료 검토, 현재 마일스톤 닫고 다음 마일스톤 지정, 마일스톤 종료해, 마일스톤 완료 검토, 종료 검토 | `agent-ops/skills/common/complete-milestone/SKILL.md` |
| 로드맵 업데이트, roadmap 갱신, 로드맵에 추가, 로드맵 작업 추가, 로드맵 기능 추가, 로드맵 Epic 추가, 로드맵 에픽 추가, 로드맵 Task 추가, 로드맵 태스크 추가, 로드맵 테스크 추가, 로드맵 TODO 추가, 마일스톤에 추가, 마일스톤 추가, 마일스톤 갱신, 마일스톤 아카이브, phase 추가, phase 변경, 페이즈 추가, 페이즈 변경, 현재 마일스톤 변경, 로드맵 한국어 전환, 로드맵 번역 | `agent-ops/skills/common/update-roadmap/SKILL.md` |
| SDD 작성, SDD 생성, SDD 갱신, SDD 필요 여부, SDD gate 확인, SDD 사용자 리뷰, SDD 잠금 해제, SDD 승인 준비, SDD archive, spec gate, 설계 게이트 | `agent-ops/skills/common/roadmap-sdd/SKILL.md` |

View file

@ -1,182 +1,154 @@
---
name: sync-milestone-workstate
version: 1.1.0
description: 현 마일스톤과 작업현황 동기화, 현재 마일스톤 작업현황 동기화, 마일스톤 완료내역 동기화, agent-task 완료를 마일스톤에 반영, active/archive complete.log 후보 스캔, 누락된 Roadmap Completion 복구, 작은 작업처럼 agent-task 기록이 없는 완료 내역을 관련 파일과 git 기록까지 종합 확인, 마일스톤 체크박스 재동기화 요청에서 Milestone Task, 상태, Phase/current 라벨을 실제 evidence와 맞추는 절차
description: 현 마일스톤과 작업현황 동기화, 현재 마일스톤 작업현황 동기화, 마일스톤 완료내역 동기화, agent-task 완료를 마일스톤에 반영, complete.log의 milestone-task id별 증거 집계, active/archive 완료 로그 스캔, legacy Roadmap Completion 복구, 파일/git 기준 작업 상태 확인, 마일스톤 체크박스 재동기화 요청에서 Milestone Task와 상태를 실제 evidence에 맞추는 절차
---
# sync-milestone-workstate
## 목적
현재 또는 지정 Milestone의 기능 Task 상태를 실제 작업 evidence와 동기화한다.
새 작업을 배치하거나 구현 계획을 수정하지 않는다.
`complete.log``Roadmap Completion`은 가장 강한 직접 근거로 사용하되, 그 파일이 있더라도 관련 파일과 git history로 최소 sanity pass를 수행한다.
`complete.log`가 없거나 불완전해도 완료가 없다고 단정하지 않는다.
작은 작업이나 수동 수정처럼 `agent-task` 기록이 없을 수 있으므로 Milestone의 관련 경로, 실제 파일 내용, git history, 테스트/검증 흔적을 함께 확인한다.
런타임이 `origin-task``complete-log`를 단건 완료 이벤트로 전달한 일반 반영은 `update-roadmap`을 사용한다.
현재 또는 지정 Milestone의 기능 Task 상태를 실제 작업 evidence와 동기화한다. 새 작업을 배치하거나 구현 계획을 수정하지 않는다.
새 계약의 `complete.log` 첫 줄 `milestone-task=<id>[,<id>...]`는 해당 완료 작업의 evidence가 어느 Milestone Task에 기여하는지 나타내는 인덱스다. 이 값만으로 Task 완료를 선언하지 않는다. 같은 Milestone task group의 모든 완료 로그를 id별로 모은 뒤 현재 Task 설명, Task 안의 `검증:`, 관련 파일/git evidence, 필요한 SDD Acceptance Scenario와 Evidence Map을 함께 평가해 계약 전체가 충족된 Task만 `[x]`로 바꾼다.
한 plan이 여러 Task id에 기여하거나 여러 plan이 같은 Task id에 기여할 수 있다. 따라서 plan 하나의 PASS와 Milestone Task 하나의 완료를 1:1로 가정하지 않는다.
## 언제 호출할지
- 사용자가 "현 마일스톤과 작업현황 동기화", "현재 마일스톤 작업현황 동기화", "마일스톤 작업현황 동기화"라고 요청할 때
- 사용자가 "마일스톤 완료내역 동기화", "agent-task 완료를 마일스톤에 반영", "complete.log 후보 스캔"이라고 요청할
- 사용자가 "누락된 Roadmap Completion 복구", "마일스톤 체크박스 재동기화", "로드맵 작업 완료 상태 동기화"라고 요청할 때
- 사용자가 agent-task에는 없지만 실제 파일/git 기준으로는 완료된 것 같다고 지적할 때
- code-review PASS/archive 이후 runtime completion event 반영이 누락되었는지 확인하고 file-based fallback으로 복구해야 할 때
- 사용자가 현 마일스톤과 작업현황 동기화, 마일스톤 완료내역 반영, 체크박스 재동기화를 요청할 때
- code-review가 `m-*` PASS completion event와 `complete-log`를 전달했을
- `complete.log``milestone-task` id별 evidence를 모아 현재 Task 계약을 평가해야 할 때
- 과거 `Roadmap Completion` 또는 task metadata가 없는 완료 기록을 새 계약과 함께 복구해야 할 때
- agent-task 기록이 없지만 실제 파일/git 기준 완료 가능성을 감사해야 할 때
## 입력
- `target-milestone`: 동기화할 Milestone 이름, slug, 또는 경로. 없으면 `agent-roadmap/current.md`의 활성 Milestone 단일 후보를 사용한다. (선택)
- `complete-log`: 특정 `complete.log` 경로. 지정되면 이 파일을 우선 검증하되, 같은 Milestone slug의 active/archive 후보도 함께 확인한다. (선택)
- `mode`: `sync` 또는 `check-only`. 기본값은 `sync`다. (선택)
- `check-only`: 어떤 파일도 수정하지 않는다. Milestone, Phase, `current.md`, `.agent-roadmap-sync/locks.yaml` 모두 쓰기 금지이며 반영 후보만 보고한다.
- `target-milestone`: 활성 Milestone 이름, slug, 또는 경로. 없으면 `agent-roadmap/current.md`의 단일 활성 Milestone을 사용한다. (선택)
- `complete-log`: 방금 완료된 exact `complete.log` 경로. 이 파일을 우선 검증하되 같은 Milestone task group의 다른 완료 로그도 집계한다. (선택)
- `mode`: `sync` 또는 `check-only`. 기본값은 `sync`다. `check-only`에서는 어떤 파일도 수정하지 않는다. (선택)
## 먼저 확인할 것
## first-line metadata 계약
- [ ] `agent-roadmap/current.md`를 읽어 활성 Milestone 후보를 확인한다.
- [ ] 대상 Milestone 문서의 `상태`, `구현 잠금`, `기능`, `완료 리뷰`, `작업 컨텍스트`를 확인한다.
- [ ] 대상 Phase `PHASE.md``Milestone 흐름`에 대상 Milestone 항목이 있는지 확인한다.
- [ ] 대상 Milestone의 `SDD: 필요` 여부와 `SDD 문서`, `USER_REVIEW.md` 존재 여부를 확인한다.
- [ ] 같은 slug의 active task와 archive task `complete.log` 후보를 모두 탐색한다. active만 보고 no-op으로 끝내지 않는다.
- [ ] 관련 파일과 git history를 최소 확인한다. `complete.log`가 없거나 일부 Task만 설명하면 더 깊게 감사한다.
`m-*` 완료 로그의 첫 줄은 다음 형식이다.
```markdown
<!-- task=m-<milestone-slug>[/<subtask_dir>] plan=<N> tag=<TAG> milestone-task=<task-id>[,<task-id>...] -->
```
- 주석은 파일 첫 줄에 있어야 한다.
- `task`의 첫 path segment는 집계 대상 `m-<milestone-slug>`와 정확히 같아야 한다.
- `milestone-task`는 비어 있지 않은 쉼표 구분 목록이며 공백과 중복 id를 허용하지 않는다. 각 id는 `rules-roadmap.md`의 item-id 문법과 일치해야 한다.
- 모든 id는 대상 활성 Milestone `기능`에 존재해야 한다.
- PLAN, CODE_REVIEW, `complete.log`는 같은 generation header를 보존한다.
- metadata는 evidence routing 범위다. PASS 또는 id 존재만으로 `[x]` 처리하지 않는다.
## 실행 절차
1. **대상 Milestone 확정**
- `target-milestone`이 있으면 활성 `agent-roadmap/phase/*/milestones/*.md`에서 정확히 하나를 찾는다.
- `target-milestone`이 없으면 `agent-roadmap/current.md`의 활성 Milestone이 정확히 하나인지 확인한다.
- 대상이 없거나 둘 이상이면 Milestone을 수정하지 않고 target 불명확으로 보고한다.
- 대상 경로가 `agent-roadmap/archive/**`이면 수정하지 않고 archive target 불가로 보고한다.
- 없으면 `agent-roadmap/current.md`의 활성 Milestone 단일 후보를 사용한다.
- 대상이 없거나 둘 이상이거나 archive 경로이면 어떤 상태도 수정하지 않고 target 불명확으로 보고한다.
- 대상 Phase `PHASE.md``current.md`의 현재 라벨도 함께 기록한다.
2. **Milestone Task와 evidence scope 읽기**
- Milestone `기능` 섹션의 Task id만 완료 후보로 본다.
- Task id는 `- [ ] [item-id]` 또는 `- [x] [item-id]` 형식에서 추출한다.
- 각 Task의 설명과 `검증:` 문구를 기록한다.
- `구현 잠금`의 상태, `결정 필요`, `SDD: 필요|불필요`, SDD 문서 링크/경로를 확인한다.
- `작업 컨텍스트`의 관련 경로, Milestone 범위, Task 설명의 코드/문서 키워드를 evidence scope로 삼는다.
- 관련 경로가 전혀 없으면 Task 설명에서 검색어를 만들되, 후보가 넓거나 모호하면 Task를 자동 완료하지 않고 scope 불명확으로 보고한다.
2. **현재 Task 계약 읽기**
- 대상 Milestone `기능``- [ ] [id]``- [x] [id]`만 Task 후보로 추출한다.
- 각 Task 설명, 같은 Task 안의 `검증:`, 관련 Epic 범위, `작업 컨텍스트` 관련 경로를 기록한다.
- `구현 잠금`, `결정 필요`, `SDD: 필요|불필요`, SDD 경로, SDD `USER_REVIEW.md` 존재 여부를 확인한다.
- 동기화 기준은 과거 plan 문구가 아니라 현재 Milestone Task 계약이다. 계약이 변경되어 evidence가 부족해졌으면 자동 완료하지 않는다.
3. **complete.log 후보 수집**
- 대상 Milestone slug를 `<milestone-slug>`로 두고 task group은 `m-<milestone-slug>`로 고정한다.
- active 후보를 찾는다: `agent-task/m-<milestone-slug>/complete.log`, `agent-task/m-<milestone-slug>/**/complete.log`
- archive 후보를 찾는다: `agent-task/archive/*/*/m-<milestone-slug>/complete.log`, `agent-task/archive/*/*/m-<milestone-slug>/**/complete.log`
- `complete-log` 입력이 있으면 그 파일도 후보에 포함하되, `Roadmap Completion`의 Milestone 경로가 대상과 일치해야 직접 반영한다.
- 일반 `agent-task/archive/**` 전체를 훑지 말고 위 패턴에 맞는 같은 milestone task group만 읽는다.
3. **같은 task group의 완료 로그 수집**
- task group을 `m-<milestone-slug>`로 고정한다.
- active 후보: `agent-task/m-<milestone-slug>/complete.log`, `agent-task/m-<milestone-slug>/**/complete.log`
- archive 후보: `agent-task/archive/*/*/m-<milestone-slug>/complete.log`, `agent-task/archive/*/*/m-<milestone-slug>/**/complete.log`
- 전달된 `complete-log`도 포함하되 resolved path가 위 task group과 일치해야 한다.
- 다른 slug의 `agent-task/archive/**`는 탐색하지 않는다.
- 동일 resolved path는 한 번만 센다.
4. **Roadmap Completion 직접 근거 검증**
- 각 `complete.log``Roadmap Completion` 섹션이 없으면 직접 Task 체크 근거로 쓰지 않는다. 파일/git evidence를 찾기 위한 힌트로만 사용하고 no-op 사유에 남긴다.
- `Milestone:` 경로가 대상 Milestone 경로와 정확히 일치하지 않으면 해당 파일을 직접 반영하지 않고 mismatch로 보고한다.
- `Completed task ids`의 id가 대상 Milestone의 기존 Task id와 정확히 일치하지 않으면 직접 반영하지 않고 unknown task id로 보고한다.
- 완료 근거는 `PASS` 또는 동등한 완료 판정과 검증 evidence가 있는 항목만 직접 인정한다.
- 같은 Task id에 여러 `complete.log`가 있으면 PASS 근거가 있는 항목을 모으고, 서로 충돌하는 `Not completed task ids`가 있으면 충돌을 보고한다.
4. **로그 분류와 id별 인덱스 구성**
- canonical 로그는 first-line metadata를 파싱하고 task group, id 문법, 중복, 대상 Milestone의 기존 id 여부를 검증한다.
- 유효한 canonical 로그를 각 `milestone-task` id bucket에 모두 넣는다. 한 로그가 여러 id를 가지면 각 bucket에 기여한다.
- unknown id, 다른 task group, PLAN/review header 불일치, PASS가 아닌 terminal 결과, unresolved Required/Suggested, 상충하는 검증 결과가 있으면 해당 로그를 자동 완료 evidence에서 제외하고 이유를 보고한다.
- 같은 id에 로그가 여러 개면 어느 하나를 대표로 고르지 말고 모두 보존한다.
- first-line metadata가 없고 legacy `Roadmap Completion`이 있는 로그는 명시 완료 주장과 연결 evidence를 호환 근거로 분류한다. 현재 Task 계약과 SDD gate를 다시 평가하며 섹션만 보고 즉시 체크하지 않는다.
- metadata와 `Roadmap Completion`이 모두 없는 legacy 로그는 관련 파일/git 탐색을 위한 힌트로만 사용한다.
5. **파일/git evidence 감사**
- 항상 대상 Milestone의 관련 파일과 git history를 최소 확인한다.
- `Roadmap Completion`으로 확인되지 않은 Task가 있거나 사용자가 실제 구현 완료를 지적하면 Task별 상세 감사를 수행한다.
- 관련 파일을 `rg --files <관련 경로>`와 Task 키워드 `rg`로 찾고, 필요한 파일 본문을 읽어 Task 설명과 직접 대응되는 구현/문서/테스트 변경을 확인한다.
- `git log --oneline -- <관련 경로>`와 필요한 경우 `git show --stat --name-only <commit>`로 Milestone 관련 커밋을 확인한다.
- 커밋 메시지만으로 Task를 완료 처리하지 않는다. 커밋이 변경한 파일과 현재 파일 내용이 Task 설명을 충족해야 한다.
- Task 완료 인정 기준:
- 구현/산출물 evidence가 Task 설명과 직접 대응한다.
- Task에 `검증:`이 있으면 해당 검증 명령의 기록, 현재 테스트 실행 결과, 또는 같은 범위를 검증하는 명시 evidence가 있다.
- `validation-tests` 같은 테스트 Task는 테스트 코드와 검증 실행 evidence가 모두 있어야 한다.
- evidence가 의미상 유사하지만 Task id와 연결이 불분명하면 `[x]` 처리하지 않고 `검토 필요`로 보고한다.
5. **Task별 evidence 집계**
- 각 Task id마다 bucket의 모든 `complete.log`에서 `구현/정리 내용`, `최종 검증`, archived plan/review 포인터, final verdict를 모은다.
- 필요한 경우 같은 완료 디렉터리의 exact `plan_*.log``code_review_*.log`만 읽어 metadata 일치와 구체적인 구현·검증 evidence를 확인한다. sibling archive task group 밖으로 확장하지 않는다.
- Milestone의 관련 경로를 `rg --files`와 Task 키워드로 확인하고, 현재 파일 내용이 Task 설명의 각 요구를 실제로 제공하는지 대조한다.
- `git log --oneline -- <관련 경로>`와 필요한 `git show --stat --name-only <commit>`으로 provenance를 보조 확인한다. 커밋 메시지만으로 완료 처리하지 않는다.
- 한 로그가 Task 계약 전체를 충족하면 단독으로 완료 evidence가 될 수 있다. 여러 로그가 각각 세분화된 하위 범위를 맡았다면 합집합이 계약 전체와 검증을 충족할 때 완료 evidence가 된다.
- 로그 수, plan 수, 특정 tag 존재, 파일명 유사성은 완료 기준이 아니다.
6. **SDD Evidence Map 확인**
- `SDD: 필요`인 Milestone은 direct evidence와 file/git evidence 모두 SDD gate를 통과해야 한다.
- SDD `Acceptance Scenarios``Evidence Map`에서 각 완료 후보 Task id와 연결된 scenario가 있는지 확인한다.
- 연결된 scenario의 evidence가 확인한 complete log, 파일 변경, git commit, verification 중 하나로 설명 가능해야 한다.
- SDD 파일이 없거나 `USER_REVIEW.md`가 남아 있거나 Evidence Map 연결이 비어 있으면 Task 체크 또는 `[검토중]` 전환을 하지 않고 차단 사유로 보고한다.
6. **검증과 SDD gate 평가**
- Task에 `검증:`이 있으면 집계된 실제 실행 결과, 현재 재실행 결과, 또는 같은 범위를 검증하는 명시 evidence가 있어야 한다.
- 테스트 자체가 산출물인 Task는 테스트 코드와 실행 evidence를 모두 요구한다.
- `SDD: 필요`이면 해당 Task id에 연결된 모든 필요한 Acceptance Scenario와 Evidence Map row를 찾는다. 집계한 로그·파일·검증 evidence가 그 mapping을 충족해야 한다.
- SDD 파일 부재, 미승인/잠금 상태, 남은 SDD `USER_REVIEW.md`, Task mapping 부재는 해당 Task 자동 체크를 차단한다.
- evidence가 의미상 유사하지만 id와의 연결 또는 요구 범위가 불명확하면 `검토 필요`로 남긴다.
7. **Milestone 문서 반영**
- `mode=check-only`이면 어떤 파일도 수정하지 않고 반영 후보만 보고한다.
- 검증된 완료 Task id만 `[x]`로 바꾼다. 이미 `[x]`인 항목은 유지한다.
- 일부 Task만 완료되었고 미완료 Task가 남으면 Milestone 상태는 `[진행중]`으로 둔다. 단, 기존 상태가 `[검토중]`, `[보류]`, `[폐기]`이면 자동으로 낮추지 않고 차이만 보고한다.
- 모든 기능 Task가 `[x]`이고 `구현 잠금``해제`, `결정 필요: 없음`, SDD 사용자 리뷰 없음, SDD evidence 충족이면 Milestone 상태를 `[검토중]`으로 바꾼다.
- `[검토중]`으로 바꾸면 `완료 리뷰` 섹션을 만들거나 갱신하고, 사용한 `complete.log`, 파일/git evidence 요약, 완료 Task id, 남은 차단 항목 없음 또는 요약을 1~3줄로 남긴다.
- 모든 Task가 `[x]`여도 구현 잠금이나 SDD gate가 남으면 `[검토중]`으로 바꾸지 않고 `완료 리뷰` 또는 `작업 컨텍스트`에 차단 항목을 남긴다.
- 이 스킬은 Milestone을 `[완료]`로 바꾸거나 archive로 이동하지 않는다.
7. **완료 판정**
- 다음이 모두 참인 Task만 `[x]` 후보로 판정한다.
- 현재 Task 설명의 capability와 산출물이 모두 확인된다.
- 명시 `검증:`이 충족된다.
- 필요한 SDD mapping과 evidence가 충족된다.
- 집계 evidence 사이에 미완료 선언, 실패, scope 충돌이 없다.
- canonical bucket이 비어 있어도 파일/git 감사로 계약 전체가 명확히 충족되면 완료 후보가 될 수 있으나, 어떤 evidence가 각 요구를 충족했는지 보고한다.
- canonical 로그가 하나 이상 있어도 계약 일부만 충족하면 `[x]` 처리하지 않는다.
8. **Phase와 current 라벨 동기화**
- `mode=check-only`이면 이 단계를 쓰기 없이 확인만 한다.
- 대상 Phase `PHASE.md``Milestone 흐름`에서 대상 Milestone 상태 라벨을 Milestone 본문 상태와 맞춘다.
- `agent-roadmap/current.md`에 대상 Milestone이 있으면 상태 라벨을 Milestone 본문 상태와 맞춘다.
- `current.md`에는 `[완료]` 또는 `[폐기]`를 남기지 않는다. 이 스킬은 `[검토중]`까지 유지할 수 있다.
8. **Milestone/Phase/current 반영**
- `mode=check-only`이면 후보만 보고하고 파일을 수정하지 않는다.
- 새로 검증된 Task만 `[x]`로 바꾸고 기존 `[x]`는 유지한다. evidence가 상실된 기존 `[x]`는 자동으로 되돌리지 않고 불일치로 보고한다.
- 미완료 Task가 남으면 일반적으로 `[진행중]`을 유지한다. 기존 `[검토중]`, `[보류]`, `[폐기]`는 자동 하향하지 않고 차이를 보고한다.
- 모든 Task가 `[x]`이고 구현 잠금 해제, `결정 필요: 없음`, SDD gate 충족이면 Milestone을 `[검토중]`으로 바꾸고 `완료 리뷰`에 id별 집계 로그와 파일/git evidence를 1~3줄로 요약한다.
- 이 스킬은 `[완료]` 전환이나 archive 이동을 하지 않는다.
- 대상 Phase `PHASE.md``agent-roadmap/current.md`의 대상 라벨을 Milestone 본문과 맞춘다. `current.md`에는 `[완료]` 또는 `[폐기]`를 남기지 않는다.
9. **workspace lock 확인**
- `mode=check-only`이면 관련 lock 여부와 필요한 동기화 후보만 보고하고 `locks.yaml`을 수정하지 않는다.
- `.agent-roadmap-sync/locks.yaml`이 있으면 대상 Milestone identity로 `agent-ops/bin/roadmap-dependency-checker.sh --find-milestone "<project>:<milestone-path>" both "<locks-file>"`를 실행한다.
- 관련 lock이 없으면 결과에 `Workspace 잠금: 관련 lock 없음`을 남긴다.
- 대상 Milestone identity가 어느 entry의 `rely-on.target`과 일치하면 대상 Milestone 상태 기준으로 해당 `rely-on.status`를 동기화한다. `[검토중]` 또는 `[완료]`이면 `enable`, 그 외 상태면 `disable`이다.
- 대상 Milestone identity가 어느 entry의 `locked`와 일치하면 모든 `rely-on.status``enable`인지 결과에 남긴다.
- 이 스킬에서 새 lock을 만들거나 다른 Milestone의 구현 잠금을 직접 해제하지 않는다.
- `.agent-roadmap-sync/locks.yaml`이 있으면 `agent-ops/bin/roadmap-dependency-checker.sh --find-milestone "<project>:<milestone-path>" both "<locks-file>"`를 실행한다.
- 대상이 `rely-on.target`이면 `[검토중]` 또는 `[완료]`에서 `enable`, 그 외에는 `disable`로 동기화한다. `check-only`에서는 쓰지 않는다.
- 새 lock을 만들거나 다른 Milestone 잠금을 직접 해제하지 않는다.
10. **결과 보고**
- 수정 파일과 변경 전/후 상태를 보고한다.
- 읽은 active/archive `complete.log` 후보 수, 확인한 관련 파일/git 범위, 반영한 Task id를 보고한다.
- 반영하지 않은 `complete.log`나 Task가 있으면 이유를 보고한다.
- SDD gate, 완료 리뷰, Workspace lock, 남은 미완료 Task, 검토 필요 Task를 보고한다.
- 대상 Milestone, 수정 파일, `complete.log`, SDD, 사용자 리뷰 같은 문서/산출물 포인터는 raw path만 쓰지 말고 `[표시 제목](상대경로)` Markdown 링크로 보고한다.
10. **검증과 보고**
- `git diff --check`를 실행한다.
- active/archive 후보 수, canonical/legacy/제외 수, Task id별 연결 로그와 판정, 파일/git 범위, SDD gate, 상태 변경, 남은 차단을 보고한다.
- 문서와 산출물 포인터는 Markdown 링크로 쓴다.
## 실행 결과 검증
- [ ] 대상 Milestone이 활성 경로에서 정확히 하나로 확정되었는가
- [ ] 같은 `m-<milestone-slug>`의 active/root/nested `complete.log`와 archive/root/nested `complete.log` 후보를 모두 확인했는가
- [ ] archive 확인을 생략하고 active task만 근거로 no-op 처리하지 않았는가
- [ ] `complete.log` 부재만으로 완료 Task 없음이라고 단정하지 않았는가
- [ ] 관련 파일과 git history를 확인했거나, scope 불명확 사유를 보고했는가
- [ ] `Roadmap Completion`의 Milestone 경로와 Task id가 대상 Milestone과 exact match였는가
- [ ] `SDD: 필요`인 경우 SDD 파일, `USER_REVIEW.md` 부재, Acceptance Scenario, Evidence Map 연결을 확인했는가
- [ ] 완료 근거가 있는 Task만 `[x]`로 바꾸었는가
- [ ] 모든 Task 완료와 구현 잠금 해제 조건이 충족된 경우에만 `[검토중]`으로 전환했는가
- [ ] `[검토중]`으로 전환했다면 `완료 리뷰`에 complete.log, 파일/git evidence, 남은 차단 항목이 남았는가
- [ ] Phase `PHASE.md``agent-roadmap/current.md`의 상태 라벨이 Milestone 본문과 일치하는가
- [ ] `[검토중]` 전환만 수행하고 `[완료]` 전환 또는 archive 이동을 하지 않았는가
- [ ] `.agent-roadmap-sync/locks.yaml`이 있으면 관련 lock 여부와 필요한 `rely-on.status` 동기화를 결과에 반영했는가
- [ ] 결과 보고의 문서/산출물 포인터가 raw path만 남지 않고 Markdown 링크로 작성되었는가
- [ ] `git diff --check`를 실행했는가
- 검증 실패 시: 파일을 추가로 추정 수정하지 말고 실패한 항목, 차단 사유, 필요한 evidence 경로를 보고한다.
## 출력 형식
## 판정 보고 형식
```markdown
## 동기화 완료
- 대상 Milestone: [<milestone-name>](agent-roadmap/phase/<phase-slug>/milestones/<milestone-slug>.md)
- 모드: <sync | check-only>
- 수정 파일:
- <없음 | [문서명](path)>
- 상태: <변경 없음 | 이전 -> 이후>
- complete.log 후보: active <N>개, archive <N>개, canonical <N>개, legacy <N>개, 제외 <N>
## Task별 집계
- `<task-id>`: <완료 | 미완료 | 검토 필요>
- 연결 로그: <N>
- 충족 evidence: <요약 또는 없음>
- 미충족/충돌: <요약 또는 없음>
- SDD gate: <불필요 | 충족 | 차단>
## 반영 내용
- 상태: <변경 없음 | 이전 -> 이후>
- 완료 Task: <id 목록 또는 없음>
- 미완료 Task: <id 목록 또는 없음>
- 검토 필요 Task: <id 목록 또는 없음>
- complete.log 후보: active <N>개, archive <N>
- 반영한 complete.log:
- <[complete.log](path)>
- 반영 제외 complete.log:
- <[complete.log](path)> - <사유>
- 파일/git evidence:
- <task-id 또는 범위> - <파일/커밋/검증 요약>
- SDD gate: <불필요 | 충족 | 차단: 사유>
- 완료 리뷰: <변경 없음 | 검토중 갱신 | 잠금 차단 기록>
- 새로 완료 처리한 Task: <id 목록 또는 없음>
- 남은 Task: <id 목록 또는 없음>
- 수정 파일: <Markdown 링크 목록 또는 없음>
- Workspace 잠금: <관련 lock 없음 | 상태 요약 | 미확인 사유>
## TODO 항목
- <남은 차단 항목 또는 없음>
- TODO: <남은 차단 항목 또는 없음>
```
## 금지 사항
- active `agent-task/m-<milestone-slug>`만 확인하고 archive `complete.log` 확인 없이 no-op 처리하지 않는다.
- `complete.log` 부재만으로 Task 미완료를 단정하지 않는다.
- 대상 slug와 다른 `agent-task/archive/**` 문서를 일반 탐색하지 않는다.
- 커밋 메시지, 파일명, plan/review log만으로 Task를 `[x]` 처리하지 않는다.
- `complete.log`의 Task id를 의미 유사도, 순서, 파일명으로 보정하지 않는다.
- 파일/git evidence가 있어도 Task 설명과 직접 대응되지 않으면 완료 처리하지 않는다.
- SDD gate가 필요한데 Evidence Map 연결을 확인하지 않고 `[검토중]`으로 전환하지 않는다.
- 구현 잠금이 남아 있거나 `결정 필요`가 있으면 `[검토중]`, `[완료]`, archive를 수행하지 않는다.
- 이 스킬에서 새 Milestone/Epic/Task를 만들거나 기존 id를 바꾸지 않는다.
- 이 스킬에서 Milestone을 `[완료]`로 전환하거나 archive 이동하지 않는다.
- `milestone-task` id 존재, plan PASS, 로그 개수만으로 Task를 `[x]` 처리하지 않는다.
- plan 하나와 Task 하나를 1:1로 가정하거나, 같은 id의 여러 로그 중 하나만 임의 선택하지 않는다.
- 새 canonical 로그에 `Roadmap Completion` 작성을 요구하지 않는다.
- legacy `Roadmap Completion`도 현재 Task 계약과 SDD gate 재평가 없이 즉시 반영하지 않는다.
- active task만 보고 archive 후보를 생략하거나 `complete.log` 부재만으로 미완료를 단정하지 않는다.
- 커밋 메시지, 파일명, plan/review log만으로 완료 처리하지 않는다.
- 의미 유사도, 순서, 파일명으로 unknown id를 보정하지 않는다.
- 구현 잠금이나 SDD gate가 남은 상태에서 `[검토중]`, `[완료]`, archive를 수행하지 않는다.
- 새 Milestone/Epic/Task를 만들거나 기존 id를 바꾸지 않는다.

View file

@ -22,7 +22,7 @@ Epic과 Task는 별도 파일로 분리하지 않고 Milestone 문서의 `기능
- Milestone 완료, 보류, 폐기, 신규 추가가 필요할 때
- Phase 완료, 보류, 폐기, 신규 추가가 필요할 때
- 완료 또는 폐기된 Phase/Milestone을 archive로 이동해야 할 때
- 런타임이 `m-<milestone-slug>` task group의 PASS 완료 이벤트를 Milestone에 반영해야 할 때
- 런타임이 first-line `milestone-task`가 없는 legacy `m-<milestone-slug>` PASS 완료 이벤트를 Milestone에 반영해야 할 때. 새 metadata 이벤트는 `sync-milestone-workstate`로 라우팅한다.
- 특정 기능이나 작업을 새 Milestone, 기존 Milestone의 Epic, 기존 Epic의 Task 중 적절한 위치에 추가해야 할 때
- 활성 Phase/Milestone 창에 포함할 목록이 달라졌을 때
- 기존 로드맵을 `phase/<phase-slug>/PHASE.md` scaffold로 마이그레이션하거나 표준화해야 할 때
@ -48,7 +48,7 @@ Epic과 Task는 별도 파일로 분리하지 않고 Milestone 문서의 `기능
- `sdd-path`: SDD 문서 경로. 기본값은 `agent-roadmap/sdd/<phase-slug>/<milestone-slug>/SDD.md` (선택)
- `sdd-review`: SDD 사용자 리뷰 상태. `없음` / `요청됨` / `해결됨` 중 하나 (선택)
- `evidence`: 완료 판단에 사용할 파일, PR, 테스트, 커밋, 사용자 설명 (선택)
- `complete-log`: 런타임 완료 이벤트가 전달한 `complete.log` 경로. `Roadmap Completion` 섹션이 있을 때만 Milestone 기능 Task 체크에 사용한다 (선택)
- `complete-log`: 런타임 완료 이벤트가 전달한 `complete.log` 경로. 첫 줄에 `milestone-task`가 있으면 이 스킬에서 직접 체크하지 않고 `sync-milestone-workstate`로 라우팅한다. metadata가 없는 legacy 로그는 `Roadmap Completion` 호환 검증에만 사용한다 (선택)
- `review-state`: 완료 리뷰 상태. `검토중` / `통과` / `보완 필요` / `보류` / `폐기` 중 하나 (선택)
- `review-comment`: 완료 리뷰에 남길 보완, 보류, 폐기 방향성 또는 근거 메모 (선택)
- `origin-task`: 런타임 완료 이벤트가 전달한 `agent-task/m-<milestone-slug>` 또는 `agent-task/m-<milestone-slug>/<subtask_dir>` 형식의 원래 active task 경로. 이벤트가 최종 archive 경로만 갖고 있으면 런타임이 이 형식으로 정규화해 전달한다 (선택)
@ -222,10 +222,10 @@ agent-roadmap/
- 런타임 완료 이벤트의 `origin-task`에서 `agent-task/` 다음 첫 path segment가 `m-<milestone-slug>`이면 Milestone 기반 plan/review 완료에서 온 요청으로 본다. `origin-task`는 archive 이동 전 active task 경로 또는 런타임이 그 형태로 정규화한 경로를 사용한다.
- `<milestone-slug>`는 활성 `agent-roadmap/phase/*/milestones/<milestone-slug>.md`에서 정확히 하나만 찾아야 한다. archive Milestone은 target 후보가 아니다.
- target이 없거나 둘 이상이면 Milestone 내용을 추정해 수정하지 말고 target 불명확으로 보고한다.
- target이 확정되어도 `complete-log` 입력이 없거나 해당 파일에 `Roadmap Completion` 섹션이 없으면 Milestone 기능 Task를 체크하지 않고 no-op으로 보고한다. 일반 `m-*` 완료 이벤트만으로 Task를 추정해 체크하지 않는다.
- `Roadmap Completion` 섹션이 있으면 Milestone 경로가 target과 일치하는지, Completed task ids의 각 id가 해당 Milestone의 기존 기능 Task id 하나와 정확히 일치하는지 확인한다. 하나라도 일치하지 않으면 수정하지 말고 target 불일치로 보고한다.
- target Milestone이 `SDD: 필요`이면 런타임 완료 이벤트의 `complete-log`에 있는 `Roadmap Completion`과 최종 검증 evidence가 SDD `Evidence Map`을 충족해야 한다. 사용자가 `update-roadmap` 요청에 별도 evidence를 명시해 수동 반영을 요구한 경우에만 Evidence Map 충족 근거를 보조 근거로 사용할 수 있다. 근거가 없으면 `Roadmap Completion`이 있어도 Task를 체크하지 않고 SDD evidence 부족으로 보고한다.
- 일치하면 PASS evidence, `complete.log`, final archive path, archived plan/review log 경로, code-review 결과 요약을 근거로 `Roadmap Completion`에 적힌 기능 Task만 `[x]`로 갱신한다. target routing 자체는 완료 이벤트의 `m-<milestone-slug>` task group과 `complete.log``Roadmap Completion` 섹션으로 결정한다.
- `complete-log` 첫 줄에 `milestone-task`가 있으면 새 계약 이벤트다. 여기서 단건 PASS를 Task 완료로 해석하거나 체크하지 말고 `sync-milestone-workstate target-milestone=<milestone-slug> complete-log=<path>`로 라우팅한다. 그 스킬이 같은 task group의 모든 완료 로그를 id별로 집계한다.
- first-line metadata가 없는 legacy 로그만 `Roadmap Completion` 호환 흐름을 사용할 수 있다. 섹션이 없으면 no-op이며 일반 `m-*` 완료 이벤트만으로 Task를 추정하지 않는다.
- legacy `Roadmap Completion`이 있으면 Milestone 경로와 Completed task ids를 exact match하고 PASS/검증/SDD Evidence Map을 확인한다. 현재 Task 계약을 충족하지 않거나 충돌 evidence가 있으면 체크하지 않는다.
- 사용자가 이 스킬에 별도 evidence를 명시한 수동 갱신은 일반 완료 리뷰 규칙으로 평가할 수 있지만, `milestone-task` 단건 이벤트를 check-on-pass로 바꾸는 근거로 사용하지 않는다.
- 갱신 후 모든 기능 Task와 Task 안에 명시된 검증이 충족되어도 `구현 잠금`이 해제되어 있지 않으면 `[검토중]` 전환을 하지 않고 잠금 차단으로 보고한다. 기능 Task와 구현 잠금이 모두 충족될 때만 `[검토중]` 전환과 `완료 리뷰` 요청 규칙을 적용한다.
- target Milestone이 `[스케치]`이면 완료 이벤트를 반영하지 말고 상태 불일치로 보고한다. `[스케치]`는 Milestone 기반 `agent-task` 완료 이벤트의 target이 될 수 없다.
@ -325,7 +325,7 @@ target 없는 신규 추가 요청은 append가 아니라 upsert로 처리한다
1. **갱신 범위 결정**
- 요청에서 mode, 대상 Phase/Milestone, placement, placement-unit을 추론한다.
- 런타임 완료 이벤트의 `origin-task` task group이 `m-<milestone-slug>`이면 `target-milestone`을 활성 Milestone 경로 매칭으로 확정한다.
- 런타임 완료 이벤트가 `complete-log`를 전달하면 파일을 읽고 `Roadmap Completion` 섹션 유무와 Completed task ids를 확인한다. 섹션이 없으면 Milestone 기능 Task 체크는 no-op이다. SDD 대상 Milestone이면 SDD `Evidence Map` 충족 여부도 확인한다.
- 런타임 완료 이벤트가 `complete-log`를 전달하면 첫 줄 metadata를 먼저 확인한다. `milestone-task`가 있으면 직접 수정하지 않고 `sync-milestone-workstate`로 라우팅한다. 없는 legacy 로그만 `Roadmap Completion`과 Completed task ids, 필요한 SDD `Evidence Map` 확인한다.
- 구조 전환, 템플릿 보정, current 동기화는 `sync`로 본다.
- `priority-queue.md` 생성, 순서 조정, 깨진 링크 복구, archive/폐기/경로 변경/split/merge 후 큐 정리는 `sync` 또는 `replan`으로 본다.
- 완료/폐기 근거가 충족된 이동은 `archive`로 본다.
@ -441,7 +441,7 @@ target 없는 신규 추가 요청은 append가 아니라 upsert로 처리한다
- 로컬 current.md 활성 창 변경 사항
- 완료 리뷰 상태와 남은 차단 항목
- SDD gate 상태와 사용자 리뷰 필요 여부
- 런타임 완료 이벤트의 `origin-task``m-<milestone-slug>`이면 원래 active task 경로와 매칭된 target Milestone, `Roadmap Completion` Task ids 또는 no-op 사유
- 런타임 완료 이벤트의 `origin-task``m-<milestone-slug>`이면 원래 active task 경로와 매칭된 target Milestone, `milestone-task` 집계 라우팅 또는 legacy `Roadmap Completion` Task ids/no-op 사유
- archive 모드이면 이동 경로와 남긴 링크
- 확인 필요로 남긴 항목
@ -468,7 +468,7 @@ target 없는 신규 추가 요청은 append가 아니라 upsert로 처리한다
- SDD gate: <불필요 | 필요-작성 | 필요-잠금 | 필요-사용자 리뷰 | 필요-승인됨 | 변경 없음>
- 승격 조건: <해당 없음 | 추가/수정/미충족 유지/충족 요약>
- 완료 리뷰: <변경 없음 | 검토중 | 통과 | 보완 필요 | 보류 | 폐기>
- runtime m-task 라우팅: <해당 없음 | origin-task -> target Milestone | target 불명확>
- runtime m-task 라우팅: <해당 없음 | milestone-task -> sync-milestone-workstate | legacy Roadmap Completion -> target Milestone | target 불명확>
- Workspace 잠금: <변경 없음 | 관련 lock 없음 | entry 생성/갱신 | rely-on enable | rely-on disable | 미충족 | 런타임 해제 대기>
- 활성 항목: <변경 없음 | Phase/Milestone 추가/제거 요약>
- 아카이브: <변경 없음 | 이동 링크와 남긴 링크>

View file

@ -222,7 +222,7 @@ When recovering a KST-night `local-G07``local-G08` Laguna locator or a termin
- Persist `active_stage` for a running task. After dispatcher restart, exclude that task from candidates, restore or conservatively adopt its workspace write claim, and immediately dispatch every other dependency-ready task whose claim does not collide.
- **ABSOLUTE RULE:** Scan the complete candidate set only at initial entry and immediately after creating a verified `complete.log`. In that scan, exclude tasks shown as running by current-workspace state and native session/locator evidence, then atomically admit every dependency-ready task with a non-colliding write claim. An unmet dependency or write collision excludes only that task. Exit instead of polling when no candidate remains.
- Persist Pi worker success, Pi self-check success, and official review as separate stages. If restart state is `worker_done=true` and `selfcheck_done=false`, resume on the same Pi model, not with worker or review. Run the full pass when `selfcheck_incomplete=0`; otherwise resume the persisted successful self-check context locator with an unchecked-item retry. Never replace a missing or invalid persisted context with a fresh session.
- Key persistent state to the `task/plan/tag` generation at the start of PLAN. Checklist/body edits to the same PLAN do not reset the stage; a new plan number in a follow-up PLAN does.
- Key persistent state to the first-line `task/plan/tag` generation and, for `m-*`, its `milestone-task` scope. Checklist/body edits to the same PLAN do not reset the stage; a new plan number or changed Milestone Task scope does.
- Send an already completed review stub with no dispatcher execution record to review. Never send dispatcher-recorded Pi worker success to review before self-check completes.
- Start official review and worker/self-check together when they belong to different dependency-ready tasks with disjoint workspace claims. Wait for a claim owner to reach verified completion before admitting a colliding task.
- Let the dispatcher record every worker/self-check/review attempt start and finish in the task-group `WORK_LOG.md`.
@ -241,7 +241,7 @@ When recovering a KST-night `local-G07``local-G08` Laguna locator or a termin
4. **Converge review.**
- Run every official review in an independent Codex one-shot session with no separate numeric limit. Dispatch all ready reviews with disjoint workspace claims in parallel.
- For finalization recovery without an active PLAN, recover the review target and write claim from the archived plan log for the same task/plan/tag. Keep the claim until the completed archive is verified.
- For finalization recovery without an active PLAN, recover the review target and write claim from the archived plan log for the same first-line generation metadata, including `milestone-task` when present. Keep the claim until the completed archive is verified.
- Forbid collaboration/sub-agent tools in official review and finish inside the current one-shot session. If such a tool call appears, clean up that attempt's independent subprocess group and retry in a fresh review session. Count the failure toward the same stage's 10-consecutive-failure limit.
- Delegate PASS archive, WARN/FAIL follow-up pairs, and review-finalization recovery to the `code-review` file contract.
- Reclassify any remaining active pair and send it to worker or review.

View file

@ -110,8 +110,19 @@ VERDICT_SCHEMA_MATCHERS = tuple(
)
for heading, label in CODE_REVIEW_RESULT_SCHEMAS
)
MILESTONE_TASK_ID_PATTERN = r"[A-Za-z0-9]+(?:[-_+=][A-Za-z0-9]+){0,3}"
MILESTONE_TASK_ID_RE = re.compile(rf"\A{MILESTONE_TASK_ID_PATTERN}\Z")
PLAN_IDENTITY_RE = re.compile(
r"<!--\s+task=(?P<task>\S+)\s+plan=(?P<plan>\d+)\s+tag=(?P<tag>\S+)\s+-->"
r"\A<!--\s+task=(?P<task>\S+)\s+plan=(?P<plan>\d+)\s+tag=(?P<tag>\S+)"
r"(?:\s+milestone-task=(?P<milestone_task>[^,\s]+(?:,[^,\s]+)*))?"
r"\s+-->[ \t]*(?:\r?\n|\Z)"
)
MILESTONE_ITEM_RE = re.compile(
rf"^-\s+\[[ xX]\]\s+\[({MILESTONE_TASK_ID_PATTERN})\]", re.MULTILINE
)
MILESTONE_FEATURE_SECTION_RE = re.compile(
r"^##[ \t]+기능[ \t]*\r?\n(?P<body>.*?)(?=^##[ \t]+|\Z)",
re.MULTILINE | re.DOTALL,
)
IMPLEMENTATION_CHECKBOX_RE = re.compile(
r"^-\s+\[([^\]\r\n]*)\]", re.MULTILINE
@ -278,10 +289,90 @@ def plan_identity(path: Path | None) -> str:
match = PLAN_IDENTITY_RE.search(text)
if not match:
return sha256_file(path)
identity = "\0".join(match.group(name) for name in ("task", "plan", "tag"))
fields = [match.group(name) for name in ("task", "plan", "tag")]
if match.group("milestone_task"):
fields.append(match.group("milestone_task"))
identity = "\0".join(fields)
return "meta:" + hashlib.sha256(identity.encode()).hexdigest()
def milestone_task_ids(metadata: re.Match[str]) -> tuple[str, ...]:
value = metadata.group("milestone_task")
return tuple(value.split(",")) if value else ()
def milestone_feature_task_ids(text: str) -> set[str]:
feature_section = MILESTONE_FEATURE_SECTION_RE.search(text)
if feature_section is None:
return set()
return set(MILESTONE_ITEM_RE.findall(feature_section.group("body")))
def metadata_work_unit_id(metadata: re.Match[str]) -> str:
work_unit_id = (
f"{metadata.group('task')}::plan-{metadata.group('plan')}::"
f"tag-{metadata.group('tag')}"
)
if metadata.group("milestone_task"):
work_unit_id += f"::milestone-task-{metadata.group('milestone_task')}"
return work_unit_id
def validate_plan_metadata(path: Path, workspace: Path) -> list[str]:
try:
head = path.read_text(encoding="utf-8", errors="replace")[:1024]
except OSError as exc:
return [f"PLAN metadata를 읽을 수 없다: {exc}"]
metadata = PLAN_IDENTITY_RE.search(head)
if metadata is None:
return [
"첫 줄 generation header를 판별할 수 없다: "
"<!-- task=... plan=N tag=... [milestone-task=id[,id...]] -->"
]
task_group = metadata.group("task").split("/", 1)[0]
task_ids = milestone_task_ids(metadata)
invalid_ids = [
task_id
for task_id in task_ids
if MILESTONE_TASK_ID_RE.fullmatch(task_id) is None
]
if invalid_ids:
return [
"milestone-task id 문법이 Milestone item-id 계약과 다르다: "
+ ", ".join(invalid_ids)
]
if len(task_ids) != len(set(task_ids)):
return ["milestone-task에 중복 Task id가 있다"]
if not task_group.startswith("m-"):
return ["비마일스톤 task에는 milestone-task를 둘 수 없다"] if task_ids else []
if not task_ids:
return ["m-* PLAN 첫 줄에는 milestone-task=<id[,id...]>가 필요하다"]
slug = task_group[2:]
candidates = sorted(
path
for path in (workspace / "agent-roadmap" / "phase").glob(
f"*/milestones/{slug}.md"
)
if path.is_file()
)
if len(candidates) != 1:
return [
f"milestone-task target은 활성 Milestone과 정확히 하나 매칭되어야 한다: "
f"slug={slug!r}, matches={len(candidates)}"
]
milestone_text = candidates[0].read_text(encoding="utf-8", errors="replace")
known_ids = milestone_feature_task_ids(milestone_text)
unknown_ids = [task_id for task_id in task_ids if task_id not in known_ids]
if unknown_ids:
return [
"milestone-task가 활성 Milestone 기능 Task id와 일치하지 않는다: "
+ ", ".join(unknown_ids)
]
return []
def write_json(path: Path, value: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
@ -1418,11 +1509,14 @@ def read_task_directory(workspace: Path, directory: Path) -> Task | None:
plan.read_text(encoding="utf-8", errors="replace")[:1024]
)
if metadata is None:
errors.append("PLAN task/plan/tag metadata를 판별할 수 없다")
errors.append("PLAN 첫 줄 generation metadata를 판별할 수 없다")
elif metadata.group("task") != name:
errors.append(
f"PLAN task metadata가 디렉터리와 다르다: {metadata.group('task')}"
)
errors.extend(validate_plan_metadata(plan, workspace))
if review is not None and plan_identity(plan) != plan_identity(review):
errors.append("PLAN/CODE_REVIEW generation metadata가 다르다")
return Task(
name=name,
directory=directory,
@ -1819,7 +1913,7 @@ def work_unit_id_from_file(path: Path) -> str | None:
head = path.read_text(encoding="utf-8", errors="replace")[:1024]
match = PLAN_IDENTITY_RE.search(head)
if match:
return f"{match.group('task')}::plan-{match.group('plan')}::tag-{match.group('tag')}"
return metadata_work_unit_id(match)
except Exception:
pass
return None
@ -1860,10 +1954,7 @@ def official_review_source_identity(task: Task) -> tuple[str, int, str]:
raise ExecutionDecisionError(
f"official review PLAN work-unit identity를 복구할 수 없다: {source}"
)
work_unit_id = (
f"{metadata.group('task')}::plan-{metadata.group('plan')}::"
f"tag-{metadata.group('tag')}"
)
work_unit_id = metadata_work_unit_id(metadata)
return route_match.group(1), int(route_match.group(2)), work_unit_id
@ -7035,6 +7126,11 @@ def main() -> int:
candidate = Path(validate_plan)
if not candidate.is_absolute():
candidate = (Path.cwd() / candidate).resolve()
metadata_diagnostics = validate_plan_metadata(candidate, workspace)
if metadata_diagnostics:
for diagnostic in metadata_diagnostics:
print(f"plan metadata error: {diagnostic}", file=sys.stderr)
return 2
write_set, diagnostics = inspect_write_set(candidate, workspace)
if diagnostics:
for diagnostic in diagnostics:

View file

@ -2,7 +2,7 @@
"""Deterministic execution-target selector CLI over the pure route policy.
The selector consumes a static routing task file (``PLAN-*`` or
``CODE_REVIEW-*``), its ``task/plan/tag`` generation header and optional prior
``CODE_REVIEW-*``), its ``task/plan/tag/milestone-task`` generation header and optional prior
decision / quota snapshot, and returns a stable JSON contract that the
dispatcher can persist. This module exposes the schema/invalid-input,
worker/review grade matrix, resume, failover, and policy-owned promotion
@ -28,7 +28,13 @@ TIMEZONE_NAME = "Asia/Seoul"
DEFAULT_QUOTA_PROBE_COMMAND = "iop-node quota-probe"
_FILENAME_RE = re.compile(r"^(PLAN|CODE_REVIEW)-(local|cloud)-G(\d{2})\.md$")
_HEADER_RE = re.compile(r"<!--\s*task=(\S+)\s+plan=(\d+)\s+tag=(\S+)\s*-->")
_MILESTONE_TASK_ID_PATTERN = r"[A-Za-z0-9]+(?:[-_+=][A-Za-z0-9]+){0,3}"
_MILESTONE_TASK_ID_RE = re.compile(rf"\A{_MILESTONE_TASK_ID_PATTERN}\Z")
_HEADER_RE = re.compile(
r"\A<!--\s*task=(?P<task>\S+)\s+plan=(?P<plan>\d+)\s+tag=(?P<tag>\S+)"
r"(?:\s+milestone-task=(?P<milestone_task>[^,\s]+(?:,[^,\s]+)*))?"
r"\s*-->[ \t]*(?:\r?\n|\Z)"
)
_STAGE_BY_KIND = {"PLAN": "worker", "CODE_REVIEW": "review"}
_VALID_TRANSITIONS = {"initial", "resume", "failover", "promotion"}
_VALID_EXECUTION_CLASSES = {"local_model", "cloud_model"}
@ -92,7 +98,7 @@ def _parse_filename(task_file: Path) -> tuple[str, str, int]:
return kind, lane, grade
def _parse_header(task_file: Path) -> tuple[str, int, str]:
def _parse_header(task_file: Path) -> tuple[str, int, str, str | None]:
try:
with Path(task_file).open("rb") as handle:
head = handle.read(1024)
@ -103,14 +109,47 @@ def _parse_header(task_file: Path) -> tuple[str, int, str]:
if match is None:
raise SelectorInputError(
"malformed_header",
"first 1KiB must contain <!-- task=... plan=N tag=... -->",
"first line must contain <!-- task=... plan=N tag=... "
"[milestone-task=id[,id...]] -->",
)
return match.group(1), int(match.group(2)), match.group(3)
task = match.group("task")
milestone_task = match.group("milestone_task")
task_ids = tuple(milestone_task.split(",")) if milestone_task else ()
invalid_ids = [
task_id
for task_id in task_ids
if _MILESTONE_TASK_ID_RE.fullmatch(task_id) is None
]
if invalid_ids:
raise SelectorInputError(
"invalid_milestone_task",
"milestone-task ids must follow the Milestone item-id grammar: "
+ ", ".join(invalid_ids),
)
if len(task_ids) != len(set(task_ids)):
raise SelectorInputError(
"duplicate_milestone_task",
"milestone-task must contain unique comma-separated Task ids",
)
if task.split("/", 1)[0].startswith("m-") and not milestone_task:
raise SelectorInputError(
"missing_milestone_task",
"m-* task headers require milestone-task=id[,id...]",
)
if not task.split("/", 1)[0].startswith("m-") and milestone_task:
raise SelectorInputError(
"unexpected_milestone_task",
"non-milestone task headers must omit milestone-task",
)
return task, int(match.group("plan")), match.group("tag"), milestone_task
def _work_unit_id(header: tuple[str, int, str]) -> str:
task, plan, tag = header
return f"{task}::plan-{plan}::tag-{tag}"
def _work_unit_id(header: tuple[str, int, str, str | None]) -> str:
task, plan, tag, milestone_task = header
work_unit_id = f"{task}::plan-{plan}::tag-{tag}"
if milestone_task:
work_unit_id += f"::milestone-task-{milestone_task}"
return work_unit_id
def _validate_evaluated_at(evaluated_at: datetime) -> None:

View file

@ -1930,7 +1930,8 @@ class WorkLogInvokeIntegrationTest(unittest.IsolatedAsyncioTestCase):
review = task_directory / "CODE_REVIEW-cloud-G07.md"
plan.write_text(
"<!-- task=m-principal-provider-credential-slot-routing/"
"02+01_credential_catalog plan=1 tag=TEST -->\n",
"02+01_credential_catalog plan=1 tag=TEST "
"milestone-task=credential-catalog -->\n",
encoding="utf-8",
)
review.write_text("review\n", encoding="utf-8")
@ -5626,6 +5627,7 @@ class WriteSetTest(unittest.TestCase):
workspace = Path(temporary)
plan = workspace / "PLAN-cloud-G10.md"
plan.write_text(
"<!-- task=validate-plan plan=0 tag=TEST -->\n\n"
"## Modified Files Summary\n\n"
"| File | Items |\n|---|---|\n"
"| `agent-test/runs/output-filter-recovery/**` | TEST-1 |\n",
@ -5649,6 +5651,91 @@ class WriteSetTest(unittest.TestCase):
stderr.getvalue(),
)
def test_validate_plan_requires_known_milestone_task_scope(self):
with tempfile.TemporaryDirectory() as temporary:
workspace = Path(temporary)
milestone = (
workspace
/ "agent-roadmap"
/ "phase"
/ "security"
/ "milestones"
/ "secret-at-rest.md"
)
milestone.parent.mkdir(parents=True)
milestone.write_text(
"# Milestone\n\n## 기능\n\n"
"- [ ] [secret-at-rest] Encrypt stored secrets\n"
"- [ ] [validation-tests] Verify ciphertext handling\n\n"
"## 구현 잠금\n\n"
"- [ ] [decision-only] Select a user-owned policy\n",
encoding="utf-8",
)
claimed = workspace / "src" / "secret.go"
claimed.parent.mkdir(parents=True)
plan = workspace / "PLAN-cloud-G10.md"
def validate(header: str) -> tuple[int, str]:
plan.write_text(
header
+ "\n\n## Modified Files Summary\n\n"
+ "| File | Items |\n|---|---|\n"
+ "| `src/secret.go` | API-1 |\n",
encoding="utf-8",
)
with mock.patch.object(
sys,
"argv",
[
str(SCRIPT),
"--workspace",
str(workspace),
"--validate-plan",
str(plan),
],
), mock.patch("sys.stderr", new_callable=io.StringIO) as stderr:
result = dispatch.main()
return result, stderr.getvalue()
missing_result, missing_error = validate(
"<!-- task=m-secret-at-rest/01_storage plan=0 tag=API -->"
)
self.assertEqual(missing_result, 2)
self.assertIn("milestone-task=<id[,id...]>", missing_error)
unknown_result, unknown_error = validate(
"<!-- task=m-secret-at-rest/01_storage plan=0 tag=API "
"milestone-task=unknown -->"
)
self.assertEqual(unknown_result, 2)
self.assertIn("unknown", unknown_error)
non_feature_result, non_feature_error = validate(
"<!-- task=m-secret-at-rest/01_storage plan=0 tag=API "
"milestone-task=decision-only -->"
)
self.assertEqual(non_feature_result, 2)
self.assertIn("decision-only", non_feature_error)
invalid_result, invalid_error = validate(
"<!-- task=m-secret-at-rest/01_storage plan=0 tag=API "
"milestone-task=secret.at.rest -->"
)
self.assertEqual(invalid_result, 2)
self.assertIn("item-id 계약", invalid_error)
valid_result, valid_error = validate(
"<!-- task=m-secret-at-rest/01_storage plan=0 tag=API "
"milestone-task=secret-at-rest,validation-tests -->"
)
self.assertEqual(valid_result, 0)
self.assertEqual(valid_error, "")
self.assertEqual(
dispatch.work_unit_id_from_file(plan),
"m-secret-at-rest/01_storage::plan-0::tag-API::"
"milestone-task-secret-at-rest,validation-tests",
)
def test_workspace_claims_persist_replace_wait_and_release_on_completion(self):
with tempfile.TemporaryDirectory() as temporary:
workspace = Path(temporary)
@ -7455,6 +7542,7 @@ class OrchestrationPersistenceTest(unittest.TestCase):
plan = workspace / "PLAN-cloud-G10.md"
target = workspace / "src" / "target.go"
plan.write_text(
"<!-- task=validate-child plan=0 tag=TEST -->\n\n"
"## Modified Files Summary\n\n"
"| File | Items |\n|---|---|\n"
f"| `{target}` | TEST-1 |\n",
@ -12083,6 +12171,52 @@ class ArtifactLanguageContractTest(unittest.TestCase):
dispatch.DISPATCHER_CHILD_BOUNDARY_PROMPT,
)
def test_milestone_task_metadata_and_aggregation_contract_is_shared(self):
skills_root = Path(__file__).resolve().parents[3]
plan_skill = (skills_root / "common" / "plan" / "SKILL.md").read_text(
encoding="utf-8"
)
review_skill = (
skills_root / "common" / "code-review" / "SKILL.md"
).read_text(encoding="utf-8")
refine_skill = (
skills_root / "common" / "refine-plans" / "SKILL.md"
).read_text(encoding="utf-8")
sync_skill = (
skills_root / "common" / "sync-milestone-workstate" / "SKILL.md"
).read_text(encoding="utf-8")
update_skill = (
skills_root / "common" / "update-roadmap" / "SKILL.md"
).read_text(encoding="utf-8")
project_rules = (
skills_root.parent / "rules" / "project" / "rules.md"
).read_text(encoding="utf-8")
complete_template = (
skills_root
/ "common"
/ "code-review"
/ "templates"
/ "complete-log-template.md"
).read_text(encoding="utf-8")
self.assertIn("milestone-task=<task-id>[,<task-id>...]", plan_skill)
self.assertIn("exact first-line generation header", review_skill)
self.assertTrue(
complete_template.startswith(
"<!-- task={task_name} plan={plan_number} tag={TAG}"
"{milestone_task_metadata_or_omit} -->"
)
)
self.assertNotIn("## Roadmap Completion", complete_template)
self.assertIn("합집합은 parent id 집합과 정확히 같아야", refine_skill)
self.assertIn("evidence routing 범위", sync_skill)
self.assertIn("모든 완료 로그를 id별로", sync_skill)
self.assertIn("sync-milestone-workstate", update_skill)
self.assertIn(
"task-group-only 및 `Roadmap Completion` 단건 반영 문구를 legacy",
project_rules,
)
class ParallelLimitSchedulingTest(unittest.IsolatedAsyncioTestCase):
"""Deterministic regressions for the workspace-global --max-parallel cap."""

View file

@ -70,11 +70,16 @@ def write_task_file(
task: str = "grp/01_unit",
plan: int = 0,
tag: str = "API",
milestone_task: str | None = None,
body: str = "body\n",
) -> Path:
path = Path(directory) / f"{kind}-{lane}-G{grade:02d}.md"
milestone_metadata = (
f" milestone-task={milestone_task}" if milestone_task else ""
)
path.write_text(
f"<!-- task={task} plan={plan} tag={tag} -->\n\n# title\n\n{body}",
f"<!-- task={task} plan={plan} tag={tag}{milestone_metadata} -->\n\n"
f"# title\n\n{body}",
encoding="utf-8",
)
return path
@ -206,6 +211,86 @@ class SelectorContractTests(unittest.TestCase):
with self.assertRaises(selector.SelectorInputError) as ctx:
selector.select_execution_target(path, evaluated_at=kst(12))
self.assertEqual(ctx.exception.code, "malformed_header")
path.write_text(
"# preamble\n<!-- task=grp/01_unit plan=0 tag=API -->\n",
encoding="utf-8",
)
with self.assertRaises(selector.SelectorInputError) as ctx:
selector.select_execution_target(path, evaluated_at=kst(12))
self.assertEqual(ctx.exception.code, "malformed_header")
def test_milestone_task_scope_is_required_and_part_of_identity(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
missing = write_task_file(
root,
"PLAN",
"cloud",
5,
task="m-secret-at-rest/01_storage",
)
with self.assertRaises(selector.SelectorInputError) as ctx:
selector.select_execution_target(missing, evaluated_at=kst(12))
self.assertEqual(ctx.exception.code, "missing_milestone_task")
scoped = write_task_file(
root,
"PLAN",
"cloud",
5,
task="m-secret-at-rest/01_storage",
milestone_task="secret-at-rest,validation-tests",
)
result = selector.select_execution_target(scoped, evaluated_at=kst(12))
self.assertEqual(
result["work_unit_id"],
"m-secret-at-rest/01_storage::plan-0::tag-API::"
"milestone-task-secret-at-rest,validation-tests",
)
def test_milestone_task_scope_rejects_duplicates_and_non_m_tasks(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
duplicate = write_task_file(
root,
"PLAN",
"cloud",
5,
task="m-secret-at-rest/01_storage",
milestone_task="secret-at-rest,secret-at-rest",
)
with self.assertRaises(selector.SelectorInputError) as duplicate_ctx:
selector.select_execution_target(duplicate, evaluated_at=kst(12))
self.assertEqual(
duplicate_ctx.exception.code, "duplicate_milestone_task"
)
unexpected = write_task_file(
root,
"PLAN",
"cloud",
5,
milestone_task="secret-at-rest",
)
with self.assertRaises(selector.SelectorInputError) as unexpected_ctx:
selector.select_execution_target(unexpected, evaluated_at=kst(12))
self.assertEqual(
unexpected_ctx.exception.code, "unexpected_milestone_task"
)
malformed_id = write_task_file(
root,
"PLAN",
"cloud",
5,
task="m-secret-at-rest/01_storage",
milestone_task="secret.at.rest",
)
with self.assertRaises(selector.SelectorInputError) as malformed_ctx:
selector.select_execution_target(malformed_id, evaluated_at=kst(12))
self.assertEqual(
malformed_ctx.exception.code, "invalid_milestone_task"
)
def test_work_unit_id_stable_across_body_changes(self):
with TemporaryDirectory() as tmp: