feat(agent-ops): 마일스톤 준비 기본 실행을 정리한다

Milestone 준비 요청에서 workspace 위치를 명시하도록 하고, plan 생성과 재검토의 기본 실행을 Codex gpt-5.6-sol xhigh로 고정해 재현 가능한 준비 흐름을 제공한다. 다른 provider를 명시하면 provider 기본 모델을 보존한다.
This commit is contained in:
toki 2026-08-03 18:28:38 +09:00
parent 44678a7570
commit 7468e87185
11 changed files with 117 additions and 28 deletions

View file

@ -1 +1 @@
1.1.182
1.1.184

View file

@ -14,10 +14,10 @@ description: 현재 또는 지정 Milestone의 정확히 한 Epic을 작은 직
- `workspace`: 준비된 feature worktree 절대 경로 (필수)
- `target-milestone`: 활성 Milestone slug 또는 경로 (필수)
- `target-epic`: 정확한 Epic id 또는 이름 (필수)
- `planner-agent`: `codex`, `claude`, `gemini`, `pi` 중 하나 (필수)
- `planner-agent`: `codex`, `claude`, `gemini`, `pi` 중 하나 (생략 시 `codex`)
- `review-agent`: 생략하면 `planner-agent`와 같다. (선택)
- `planner-model`, `review-model`: provider별 model override (선택)
- `reasoning-effort`: 지원하는 provider의 reasoning/thinking override (선택)
- `planner-model`, `review-model`: provider별 model override. Codex 기본 사용 시 `planner-model` 생략 시 `gpt-5.6-sol`, 다른 provider는 해당 CLI 기본 모델을 사용한다. (선택)
- `reasoning-effort`: 지원하는 provider의 reasoning/thinking override. 생략 시 `xhigh` (선택)
- `pi-provider`: Pi provider override (선택)
- `retry`: terminal failure의 원인을 사용자가 해소한 뒤 같은 Epic 상태를 재개할 때만 사용한다. (선택)
@ -51,12 +51,11 @@ description: 현재 또는 지정 Milestone의 정확히 한 Epic을 작은 직
python3 agent-ops/skills/common/prepare-epic-work-items/scripts/run_epic_cycle.py \
--workspace "$WORKSPACE" \
--milestone "$MILESTONE" \
--epic "$EPIC" \
--planner-agent "$PLANNER_AGENT" \
--review-agent "$REVIEW_AGENT"
--epic "$EPIC"
```
- model, reasoning, Pi provider override와 `--retry`는 해당 입력이 있을 때만 전달한다.
- 기본값은 `codex / gpt-5.6-sol / xhigh`다. 다른 provider를 지정하면 모델을 별도로 주지 않는 한 해당 provider의 CLI 기본 모델을 사용한다.
- 다른 agent, model, reasoning, Pi provider override와 `--retry`는 해당 입력이 있을 때만 전달한다.
- 스크립트는 각 agent를 새 one-shot session으로 실행한다. Codex, Claude, Gemini(`agy` adapter), Pi를 같은 normalized runner 계약으로 지원한다.
- model stdout/stderr는 git common dir의 locator log에만 저장한다. caller stdout에는 lifecycle/attention event만 출력한다.

View file

@ -17,6 +17,9 @@ import uuid
AGENT_COMMAND = {"codex": "codex", "claude": "claude", "gemini": "agy", "pi": "pi"}
DEFAULT_AGENT = "codex"
DEFAULT_MODEL = "gpt-5.6-sol"
DEFAULT_REASONING_EFFORT = "xhigh"
LABEL_PATTERN = re.compile(r"^[A-Za-z0-9._-]+$")
PROBE_EXPECTED = "MILESTONE_AGENT_READY"
@ -198,13 +201,13 @@ def sanitized_command(command: list[str], prompt: str) -> list[str]:
def parser() -> argparse.ArgumentParser:
value = argparse.ArgumentParser(description=__doc__)
value.add_argument("--agent", choices=sorted(AGENT_COMMAND), required=True)
value.add_argument("--agent", choices=sorted(AGENT_COMMAND), default=DEFAULT_AGENT)
value.add_argument("--workspace", required=True)
prompt_group = value.add_mutually_exclusive_group()
prompt_group.add_argument("--prompt")
prompt_group.add_argument("--prompt-file")
value.add_argument("--model")
value.add_argument("--reasoning-effort")
value.add_argument("--reasoning-effort", default=DEFAULT_REASONING_EFFORT)
value.add_argument("--pi-provider")
value.add_argument("--label", default="one-shot")
value.add_argument("--probe", action="store_true")
@ -214,6 +217,8 @@ def parser() -> argparse.ArgumentParser:
def execute(args: argparse.Namespace) -> int:
workspace = workspace_root(args.workspace)
if args.model is None and args.agent == DEFAULT_AGENT:
args.model = DEFAULT_MODEL
if not LABEL_PATTERN.fullmatch(args.label):
raise AgentRunError("--label may contain only letters, digits, dot, underscore, and hyphen")
prompt = prompt_text(args)

View file

@ -16,6 +16,9 @@ from typing import Any, Iterable
STAGES = ("materialize", "initial-review", "refine", "final-review")
DEFAULT_PLANNER_AGENT = "codex"
DEFAULT_PLANNER_MODEL = "gpt-5.6-sol"
DEFAULT_REASONING_EFFORT = "xhigh"
PLAN_PATTERN = "PLAN-*-G??.md"
REVIEW_PATTERN = "CODE_REVIEW-*-G??.md"
HEADER = re.compile(r"^<!--\s+(?P<body>.*?)\s+-->$")
@ -525,17 +528,30 @@ def parser() -> argparse.ArgumentParser:
value.add_argument("--workspace", required=True)
value.add_argument("--milestone", required=True)
value.add_argument("--epic", required=True)
value.add_argument("--planner-agent", choices=("codex", "claude", "gemini", "pi"), required=True)
value.add_argument(
"--planner-agent",
choices=("codex", "claude", "gemini", "pi"),
default=DEFAULT_PLANNER_AGENT,
)
value.add_argument("--review-agent", choices=("codex", "claude", "gemini", "pi"))
value.add_argument("--planner-model")
value.add_argument("--review-model")
value.add_argument("--reasoning-effort")
value.add_argument("--reasoning-effort", default=DEFAULT_REASONING_EFFORT)
value.add_argument("--pi-provider")
value.add_argument("--retry", action="store_true")
return value
def apply_defaults(args: argparse.Namespace) -> argparse.Namespace:
if args.planner_model is None and args.planner_agent == DEFAULT_PLANNER_AGENT:
args.planner_model = DEFAULT_PLANNER_MODEL
if args.reasoning_effort is None:
args.reasoning_effort = DEFAULT_REASONING_EFFORT
return args
def cycle(args: argparse.Namespace) -> int:
apply_defaults(args)
workspace = resolve_workspace(args.workspace)
milestone_path, milestone_match = resolve_milestone(workspace, args.milestone)
milestone_text = milestone_path.read_text(encoding="utf-8")
@ -795,7 +811,7 @@ def cycle(args: argparse.Namespace) -> int:
def main(argv: Iterable[str] | None = None) -> int:
args = parser().parse_args(argv)
args = apply_defaults(parser().parse_args(argv))
state_path: Path | None = None
identity = "unknown"
try:

View file

@ -114,6 +114,8 @@ class AgentCommandTest(unittest.TestCase):
self.assertEqual(result, 0)
recorded = json.loads(result_file.read_text(encoding="utf-8"))
self.assertEqual(recorded["status"], "succeeded")
self.assertEqual(recorded["model"], "gpt-5.6-sol")
self.assertEqual(recorded["reasoning_effort"], "xhigh")
self.assertIn("agent_process_start_token", recorded)
locators = list((workspace / ".git" / "epic-work-preparation" / "runs").glob("*/locator.json"))
self.assertEqual(len(locators), 1)

View file

@ -31,6 +31,31 @@ def command(cwd: Path, *args: str) -> str:
class EpicCycleContractTest(unittest.TestCase):
def test_cycle_defaults_to_codex_top_model_and_reasoning(self) -> None:
args = MODULE.parser().parse_args(
["--workspace", "/workspace", "--milestone", "milestone.md", "--epic", "epic"]
)
MODULE.apply_defaults(args)
self.assertEqual(args.planner_agent, "codex")
self.assertEqual(args.planner_model, "gpt-5.6-sol")
self.assertEqual(args.reasoning_effort, "xhigh")
other = MODULE.parser().parse_args(
[
"--workspace",
"/workspace",
"--milestone",
"milestone.md",
"--epic",
"epic",
"--planner-agent",
"claude",
]
)
MODULE.apply_defaults(other)
self.assertIsNone(other.planner_model)
self.assertEqual(other.reasoning_effort, "xhigh")
def test_live_stage_result_requires_tracking_without_relaunch(self) -> None:
with tempfile.TemporaryDirectory() as raw:
root = Path(raw)

View file

@ -1,11 +1,11 @@
---
name: prepare-milestone-workspace
description: 계획 상태의 Milestone을 현재 작업현황과 동기화한 뒤 Git Flow feature branch를 Milestone slug로 생성·push하고 지정 workspace를 worktree로 준비해 첫 Epic 작업 사이클로 넘길 때 사용한다. "X 마일스톤 준비해", "X 마일스톤 작업준비해", "X 마일스톤 작업 준비해", "X 마일스톤 시작할게" 요청에서 사용한다.
description: 명시된 workspace에 계획 상태의 Milestone을 현재 작업현황과 동기화한 뒤 Git Flow feature branch를 Milestone slug로 생성·push하고 worktree로 준비해 첫 Epic 작업 사이클로 넘길 때 사용한다. "workspace에 milestone 작업 준비해" 요청에서 사용한다.
---
# Prepare Milestone Workspace
Milestone 작업을 시작한다는 요청(`X 마일스톤 준비해`, `X 마일스톤 작업준비해`, `X 마일스톤 작업 준비해`, `X 마일스톤 시작할게`)은 workspace만 만드는 요청이 아니라, 정합성 확인부터 첫 Epic 준비 사이클 연결까지 수행하는 전체 흐름으로 해석한다.
`<workspace>에 <milestone> 작업 준비해` 요청은 workspace만 추정하거나 만드는 요청이 아니라, 지정된 workspace에 대상 Milestone의 정합성 확인부터 첫 Epic 준비 사이클 연결까지 수행하는 전체 흐름으로 해석한다.
## 목적
@ -15,13 +15,15 @@ Milestone 작업을 시작한다는 요청(`X 마일스톤 준비해`, `X 마일
- `target-milestone`: 활성 Milestone 이름, id, slug 또는 경로 (필수)
- `workspace`: feature worktree로 사용할 절대 경로 (필수)
- `planner-agent`: `codex`, `claude`, `gemini`, `pi` 중 하나 (필수)
- `planner-agent`: `codex`, `claude`, `gemini`, `pi` 중 하나 (생략 시 `codex`)
- `review-agent`: 생략하면 `planner-agent`와 같다. (선택)
- `planner-model`, `review-model`: provider별 model override (선택)
- `reasoning-effort`: 지원하는 provider의 reasoning/thinking override (선택)
- `planner-model`, `review-model`: provider별 model override. Codex 기본 사용 시 `planner-model` 생략 시 `gpt-5.6-sol`, 다른 provider는 해당 CLI 기본 모델을 사용한다. (선택)
- `reasoning-effort`: 지원하는 provider의 reasoning/thinking override. 생략 시 `xhigh` (선택)
- `pi-provider`: Pi provider override (선택)
- `target-epic`: 준비 직후 실행할 정확한 Epic id 또는 이름. 생략하면 첫 미완료 Epic을 선택한다. (선택)
트리거의 첫 번째 위치 표현(`<workspace>`)은 workspace 절대 경로로, 두 번째 표현(`<milestone>`)은 대상 Milestone으로 각각 확정한다. workspace가 생략되거나 둘 이상의 경로로 해석되면 임의로 `current`나 기본 경로를 사용하지 않고 사용자에게 확인한다.
## 사전 조건
- 대상 checkout은 Git Flow develop branch이고 tracked/untracked 변경이 없어야 한다.
@ -45,12 +47,11 @@ Milestone 작업을 시작한다는 요청(`X 마일스톤 준비해`, `X 마일
python3 agent-ops/skills/common/prepare-milestone-workspace/scripts/prepare_workspace.py \
--repo "$REPO" \
--milestone "$MILESTONE" \
--workspace "$WORKSPACE" \
--planner-agent "$PLANNER_AGENT" \
--review-agent "$REVIEW_AGENT"
--workspace "$WORKSPACE"
```
- model override가 있으면 `--planner-model`, `--review-model`, reasoning override가 있으면 `--reasoning-effort`, Pi provider override가 있으면 `--pi-provider`로 전달한다.
- 기본값은 `codex / gpt-5.6-sol / xhigh`다. 다른 provider를 지정하면 모델을 별도로 주지 않는 한 해당 provider의 CLI 기본 모델을 사용한다.
- 다른 agent, model, reasoning, Pi provider override가 있으면 해당 인자를 전달한다.
- 스크립트는 develop HEAD와 remote develop의 일치, agent probe, branch 충돌, worktree 소유권을 mutation 전에 검사한다.
- branch는 Milestone id가 아니라 파일 basename을 사용한 `feature/<milestone-slug>`다.
- 기존 branch/worktree는 정확히 같은 branch·경로이고 clean할 때만 재개한다.

View file

@ -1,4 +1,4 @@
interface:
display_name: "Prepare Milestone Workspace"
short_description: "Prepare a Git Flow workspace for one Milestone"
default_prompt: "Use $prepare-milestone-workspace to prepare the named Milestone and start its first Epic."
default_prompt: "Use $prepare-milestone-workspace to prepare the named Milestone in the explicitly provided workspace and start its first Epic."

View file

@ -17,6 +17,9 @@ from typing import Any, Iterable
VALID_AGENTS = {"codex", "claude", "gemini", "pi"}
AGENT_COMMAND = {"codex": "codex", "claude": "claude", "gemini": "agy", "pi": "pi"}
DEFAULT_PLANNER_AGENT = "codex"
DEFAULT_PLANNER_MODEL = "gpt-5.6-sol"
DEFAULT_REASONING_EFFORT = "xhigh"
MILESTONE_PATTERN = re.compile(
r"^agent-roadmap/phase/(?P<phase>[a-z0-9-]+)/milestones/(?P<slug>[a-z0-9-]+)\.md$"
)
@ -248,11 +251,15 @@ def parser() -> argparse.ArgumentParser:
value.add_argument("--repo", required=True)
value.add_argument("--milestone", required=True)
value.add_argument("--workspace", required=True)
value.add_argument("--planner-agent", choices=sorted(VALID_AGENTS), required=True)
value.add_argument(
"--planner-agent",
choices=sorted(VALID_AGENTS),
default=DEFAULT_PLANNER_AGENT,
)
value.add_argument("--review-agent", choices=sorted(VALID_AGENTS))
value.add_argument("--planner-model")
value.add_argument("--review-model")
value.add_argument("--reasoning-effort")
value.add_argument("--reasoning-effort", default=DEFAULT_REASONING_EFFORT)
value.add_argument("--pi-provider")
value.add_argument("--remote", default="origin")
value.add_argument(
@ -264,7 +271,16 @@ def parser() -> argparse.ArgumentParser:
return value
def apply_defaults(args: argparse.Namespace) -> argparse.Namespace:
if args.planner_model is None and args.planner_agent == DEFAULT_PLANNER_AGENT:
args.planner_model = DEFAULT_PLANNER_MODEL
if args.reasoning_effort is None:
args.reasoning_effort = DEFAULT_REASONING_EFFORT
return args
def prepare(args: argparse.Namespace) -> int:
apply_defaults(args)
if args.skip_agent_probe and not (
args.dry_run or os.environ.get("AGENT_OPS_TESTING") == "1"
):
@ -423,7 +439,7 @@ def prepare(args: argparse.Namespace) -> int:
def main(argv: Iterable[str] | None = None) -> int:
args = parser().parse_args(argv)
args = apply_defaults(parser().parse_args(argv))
try:
return prepare(args)
except (OSError, PreparationError) as exc:

View file

@ -31,6 +31,31 @@ def command(cwd: Path, *args: str) -> str:
class PrepareWorkspaceTest(unittest.TestCase):
def test_workspace_defaults_to_codex_top_model_and_reasoning(self) -> None:
args = MODULE.parser().parse_args(
["--repo", "/repo", "--milestone", "milestone.md", "--workspace", "/workspace"]
)
MODULE.apply_defaults(args)
self.assertEqual(args.planner_agent, "codex")
self.assertEqual(args.planner_model, "gpt-5.6-sol")
self.assertEqual(args.reasoning_effort, "xhigh")
other = MODULE.parser().parse_args(
[
"--repo",
"/repo",
"--milestone",
"milestone.md",
"--workspace",
"/workspace",
"--planner-agent",
"claude",
]
)
MODULE.apply_defaults(other)
self.assertIsNone(other.planner_model)
self.assertEqual(other.reasoning_effort, "xhigh")
def test_agent_probe_bypass_is_test_only(self) -> None:
output = io.StringIO()
with contextlib.redirect_stdout(output), mock.patch.dict(

View file

@ -47,7 +47,7 @@
| 이 마일스톤은 X가 끝나야 가능해, A 전까지 B 잠가둬, 잠금 해제 조건은 X야, X 프로젝트 작업 뒤에 현재 마일스톤 진행, 의존성 설정해, 외부 의존 잠금 | `agent-ops/skills/common/update-roadmap/SKILL.md` |
| roadmap dependency 확인, locks.yaml 판별, 외부 의존 잠금 확인, unlock-ready 판별, 잠금 해제 조건 충족 여부 확인, roadmap-dependency-checker.sh | `agent-ops/skills/common/check-roadmap-dependency/SKILL.md` |
| 지금 작업이 뭐지?, 현재 작업 분석, 어디까지 했지?, 로드맵상 현 위치, 현재 마일스톤 위치, current 기준 breadcrumb | `agent-ops/skills/common/analyze-roadmap-position/SKILL.md` |
| X 마일스톤 준비해, X 마일스톤 작업준비해, X 마일스톤 작업 준비해, X 마일스톤 시작할게 | `agent-ops/skills/common/prepare-milestone-workspace/SKILL.md` |
| X에 Y 작업 준비해, X에 Y 작업준비해, X에 Y 마일스톤 작업 준비해 | `agent-ops/skills/common/prepare-milestone-workspace/SKILL.md` |
| 현 마일스톤 Epic 작업 준비해, 마일스톤 Epic 작업 준비해, 이 Epic의 작은 작업은 바로 처리하고 큰 작업은 plan으로 작성해, Epic 작업을 작은 작업과 plan으로 나눠 | `agent-ops/skills/common/prepare-epic-work-items/SKILL.md` |
| 계획 세워줘, 계획 작성해, 계획 만들어줘, 구현 계획, PLAN.md, plan, plan 작성해, plan 만들어줘 | `agent-ops/skills/common/plan/SKILL.md` |
| 현재 plan들 세분화해, 현재 plan 세분화, 기존 plan 더 나눠, task 세분화해, plan 분리해 | `agent-ops/skills/common/refine-plans/SKILL.md` |
@ -61,7 +61,7 @@
라우팅 우선순위:
- `X 마일스톤 준비해`, `X 마일스톤 작업준비해`, `X 마일스톤 작업 준비해`, `X 마일스톤 시작할게`처럼 Milestone 시작 의도가 포함되면 `prepare-milestone-workspace`를 선택하고, `WORKSPACE_READY` 뒤 정확히 한 Epic에 `prepare-epic-work-items`를 연결한다.
- `X에 Y 작업 준비해`처럼 workspace 위치와 대상 Milestone이 함께 명시되면 `prepare-milestone-workspace`를 선택하고, `WORKSPACE_READY` 뒤 정확히 한 Epic에 `prepare-epic-work-items`를 연결한다. workspace 위치가 없으면 이 흐름으로 라우팅하지 않고 확인을 요청한다.
- 한 Epic 안에서 작은 작업 직접 처리와 큰 작업 plan 작성을 함께 요청하면 `prepare-epic-work-items`를 선택한다. 이미 존재하는 plan만 세분화하는 요청과 새로운 plan만 작성하는 요청에는 이 스킬을 선택하지 않는다.
- 이미 생성된 미착수 pair의 분할만 요청하면 lane과 관계없이 `refine-plans`를 선택한다. 새 plan 작성이나 구현 범위 재분석이 포함되면 `plan`을 선택한다.
- `refine-plans` 대상이 아닌 PLAN/CODE_REVIEW 작성 또는 재작성이 요청 범위에 포함되면 `plan`을 선택한다. `plan`이 최종 단계에서 `finalize-task-routing`을 필수 호출한다.