sync: agent-ops from agentic-framework v1.1.194

This commit is contained in:
toki 2026-08-09 18:05:23 +09:00
parent 7b6d63376a
commit 7be8557dcc
5 changed files with 126 additions and 25 deletions

View file

@ -1 +1 @@
1.1.193
1.1.194

View file

@ -1,6 +1,6 @@
---
name: prepare-milestone-workspace
description: 계획 상태의 Milestone을 명시 workspace의 Git Flow feature worktree로 준비하거나, 이미 준비된 현재 feature workspace에서 선택한 한 개·범위·남은 모든 Epic을 검토된 작업으로 변환하고 전체 준비 배리어 뒤 dispatcher를 시작할 때 사용한다. "../sample-feature-worktree 위치에 X 작업 준비해", "현 마일스톤에 두 번째 에픽 작업 시작해", "X 마일스톤에 1,2번째 에픽까지 작업 시작해", "현 마일스톤에 남은 에픽 작업들 시작해" 요청에서 사용한다.
description: 계획 상태의 Milestone을 명시 workspace의 Git Flow feature worktree로 준비하거나, 사용자가 지정한 현재 workspace에서 대상 feature branch를 생성·재사용해 선택한 한 개·범위·남은 모든 Epic을 검토된 작업으로 변환하고 전체 준비 배리어 뒤 dispatcher를 시작할 때 사용한다. "../sample-feature-worktree 위치에 X 작업 준비해", "현재 워크스페이스에서 현 마일스톤 시작해", "현 마일스톤에 두 번째 에픽 작업 시작해", "X 마일스톤에 1,2번째 에픽까지 작업 시작해", "현 마일스톤에 남은 에픽 작업들 시작해" 요청에서 사용한다.
---
# Prepare Milestone Workspace
@ -9,7 +9,7 @@ description: 계획 상태의 Milestone을 명시 workspace의 Git Flow feature
## 목적
계획 가능한 Milestone 하나를 검증된 `feature/<milestone-slug>` workspace로 전환하거나 이미 준비된 동일 branch를 재사용하고, 선택 Epic 전체가 준비된 뒤에만 구현 dispatcher를 시작한다. 의미 정합성은 roadmap 스킬이, branch/worktree/current와 batch lifecycle은 번들 스크립트가 소유한다.
계획 가능한 Milestone 하나를 검증된 `feature/<milestone-slug>` workspace로 전환한다. 현재 workspace 실행 모드에서 사용자가 지정한 workspace가 Git Flow develop branch이면 그 자리에서 대상 feature branch를 생성·전환하고, 이미 대상 feature branch이면 재사용한다. 선택 Epic 전체가 준비된 뒤에만 구현 dispatcher를 시작한다. 의미 정합성은 roadmap 스킬이, branch/worktree/current와 batch lifecycle은 번들 스크립트가 소유한다.
## 입력
@ -21,12 +21,12 @@ description: 계획 상태의 Milestone을 명시 workspace의 Git Flow feature
- `target-epics`: `remaining`, `first-incomplete`, 정확한 Epic id/title의 comma list, 또는 문서 순서의 1-based inclusive range `N..M`. 생략하면 `first-incomplete`를 사용한다. (선택)
- `retry`: 기록된 attention/recovery 조건을 사용자가 해소한 뒤 batch를 재개할 때만 사용한다. (선택)
생성 모드의 첫 번째 위치 표현(`<workspace>`)은 workspace로, 두 번째 표현(`<milestone>`)은 대상 Milestone으로 각각 확정한다. 상대 workspace는 develop repository root 기준으로 해석한다. 현재 workspace 실행 모드의 `현 마일스톤``current.md`와 현재 feature branch가 함께 가리키는 Milestone으로, 이름을 지정하면 같은 workspace의 branch/current와 정확히 일치해야 한다. `두 번째 Epic``2..2`, `두 번째 Epic까지``1,2번째 Epic까지``1..2`, `세 번째부터 네 번째 Epic까지``3..4`, `남은 Epic`은 문서 순서의 미완료 Epic 전체를 선택하는 `remaining`으로 변환한다.
생성 모드의 첫 번째 위치 표현(`<workspace>`)은 workspace로, 두 번째 표현(`<milestone>`)은 대상 Milestone으로 각각 확정한다. 상대 workspace는 develop repository root 기준으로 해석한다. 현재 workspace 실행 모드의 `현 마일스톤`그 workspace의 `current.md`가 가리키는 단일 Milestone으로 확정하고, 이름을 지정하면 같은 `current.md`와 정확히 일치해야 한다. branch 이름으로 target을 선택하지 않는다. `두 번째 Epic``2..2`, `두 번째 Epic까지``1,2번째 Epic까지``1..2`, `세 번째부터 네 번째 Epic까지``3..4`, `남은 Epic`은 문서 순서의 미완료 Epic 전체를 선택하는 `remaining`으로 변환한다.
## 사전 조건
- 생성 모드는 clean Git Flow develop checkout과 정확히 `[계획]`인 Milestone을 요구한다.
- 현재 workspace 실행 모드는 target slug와 일치하는 `feature/<milestone-slug>` branch, 일치하는 local `current.md`, clean/upstream-synced workspace를 요구하고 Milestone `[계획]` 또는 `[진행중]`을 허용한다. 기록된 active batch 재개만 상태 소유 변경을 허용한다.
- 현재 workspace 실행 모드는 일치하는 local `current.md` clean/upstream-synced workspace를 요구하고 Milestone `[계획]` 또는 `[진행중]`을 허용한다. 현재 branch가 target slug와 일치하는 `feature/<milestone-slug>`이면 재사용하고, Git Flow develop branch이면 같은 workspace에서 target feature branch를 생성·전환한다. 기록된 active batch 재개만 상태 소유 변경을 허용한다.
- 두 모드 모두 `구현 잠금: 해제`, `결정 필요: 없음`이어야 한다.
- `sync-milestone-workstate mode=consistency-check``ready`여야 한다.
- remote와 `gitflow.branch.develop`, `gitflow.prefix.feature`를 확인할 수 있어야 한다.
@ -72,7 +72,8 @@ python3 agent-ops/skills/common/prepare-milestone-workspace/scripts/prepare_work
--review-target "$REVIEW_TARGET"
```
- 현재 workspace가 target feature branch/current와 다르면 다른 worktree를 탐색하거나 branch를 바꾸지 않고 `FAILED`로 멈춘다.
- 현재 workspace가 Git Flow develop branch이면 별도 worktree를 만들지 않고 그 workspace에서 target feature branch를 생성·전환한다.
- 현재 workspace가 target feature branch도 develop branch도 아니거나 `current.md` target이 다르면 다른 worktree를 탐색하지 않고 `FAILED`로 멈춘다.
- `remaining`에 미완료 Epic이 없으면 agent/dispatcher를 시작하지 않고 완료 event로 종료한다.
- 두 모드 모두 실행 중 caller LLM이 timer polling, `ps`, state 파일 검사 또는 중복 실행을 하지 않는다.
@ -101,7 +102,7 @@ python3 agent-ops/skills/common/prepare-milestone-workspace/scripts/prepare_work
## 실행 결과 검증
- [ ] branch가 `gitflow.prefix.feature + milestone file slug`인가
- [ ] 생성 모드와 현재 workspace 실행 모드 모두 최종 branch가 `gitflow.prefix.feature + milestone file slug`인가
- [ ] branch가 remote에 존재하고 upstream이 연결됐는가
- [ ] 요청 workspace가 정확히 해당 branch의 clean worktree인가
- [ ] workspace-local `agent-roadmap/current.md`가 대상 Milestone만 가리키는가
@ -128,7 +129,7 @@ Milestone workspace preparation
- `[계획]`이 아니거나 잠긴 Milestone의 branch를 만들지 않는다.
- consistency check의 `refresh-required``ready`로 간주하지 않는다.
- 사용자 소유 변경이 있는 develop checkout이나 기존 workspace를 덮어쓰지 않는다.
- 현재 workspace 실행 모드에서 target이 다른 branch/current를 자동 전환하지 않는다.
- 현재 workspace 실행 모드에서 Git Flow develop branch는 target feature branch로 전환한다. 그 외 target이 다른 feature/topic branch나 다른 `current.md`를 자동 전환하지 않는다.
- 선택 Epic 중 하나라도 attention/terminal failure 상태면 dispatcher를 시작하지 않는다.
- 복수 Epic batch에서 개별 `EPIC_WORK_ITEMS_READY`만 보고 dispatcher를 먼저 시작하지 않는다.
- `git push --force`, destructive rollback, branch/worktree 자동 삭제를 하지 않는다.

View file

@ -1013,7 +1013,7 @@ def parser() -> argparse.ArgumentParser:
value.add_argument(
"--existing-workspace",
action="store_true",
help="start selected Epic work in the current prepared feature workspace",
help="prepare or reuse the target feature branch in the current workspace and start selected Epic work",
)
value.add_argument("--execution-catalog", default=os.environ.get(CATALOG_ENV))
value.add_argument(
@ -1085,11 +1085,14 @@ def prepare_existing(args: argparse.Namespace) -> int:
feature_prefix = git(workspace, "config", "--get", "gitflow.prefix.feature")
branch = git(workspace, "branch", "--show-current")
expected_branch = f"{feature_prefix}{milestone_slug}" if feature_prefix else ""
if not develop or not feature_prefix or branch != expected_branch:
if not develop or not feature_prefix:
raise PreparationError("gitflow.branch.develop and gitflow.prefix.feature are required")
if branch not in {develop, expected_branch}:
raise PreparationError(
f"current workspace must use the target Milestone feature branch: "
f"expected={expected_branch or 'missing-gitflow-config'} actual={branch or 'detached'}"
"current workspace must use the Git Flow develop branch or target Milestone "
f"feature branch: expected={develop}|{expected_branch} actual={branch or 'detached'}"
)
start_from_develop = branch == develop
current_path = workspace / "agent-roadmap" / "current.md"
expected_current_target = f"phase/{phase_slug}/milestones/{milestone_slug}.md"
if (
@ -1121,8 +1124,10 @@ def prepare_existing(args: argparse.Namespace) -> int:
)
if batch_state and batch_state.get("status") != "completed" and not batch_matches:
raise PreparationError("another active Epic batch owns the current Milestone workspace")
if start_from_develop and resuming_batch:
raise PreparationError("an active Epic batch must resume on its target feature branch")
if not resuming_batch:
ensure_clean(workspace, "feature workspace")
ensure_clean(workspace, "current workspace")
upstream = git(
workspace,
"rev-parse",
@ -1132,7 +1137,7 @@ def prepare_existing(args: argparse.Namespace) -> int:
)
if not upstream.endswith(f"/{branch}"):
raise PreparationError(
f"feature branch upstream mismatch: branch={branch} upstream={upstream}"
f"workspace branch upstream mismatch: branch={branch} upstream={upstream}"
)
if not resuming_batch and not args.dry_run:
remote_name = upstream.split("/", 1)[0]
@ -1145,10 +1150,54 @@ def prepare_existing(args: argparse.Namespace) -> int:
cwd=workspace,
check=False,
).returncode != 0:
raise PreparationError("current feature branch is not synchronized with its upstream")
raise PreparationError("current workspace branch is not synchronized with its upstream")
if start_from_develop and not args.dry_run:
git(workspace, "fetch", args.remote)
local_ref = f"refs/heads/{expected_branch}"
remote_ref = f"refs/remotes/{args.remote}/{expected_branch}"
local_exists = ref_exists(workspace, local_ref)
remote_exists = ref_exists(workspace, remote_ref)
if start_from_develop:
if upstream != f"{args.remote}/{develop}":
raise PreparationError(
f"develop branch must track {args.remote}/{develop}: actual={upstream}"
)
remote_develop = f"refs/remotes/{args.remote}/{develop}"
if not ref_exists(workspace, remote_develop):
raise PreparationError(f"remote develop ref missing: {args.remote}/{develop}")
branch_worktrees = [
item for item in worktrees(workspace) if item.get("branch") == local_ref
]
if branch_worktrees:
raise PreparationError(
"target feature branch already belongs to another worktree: "
f"{branch_worktrees[0]['worktree']}"
)
if local_exists and run(
["git", "merge-base", "--is-ancestor", remote_develop, local_ref],
cwd=workspace,
check=False,
).returncode != 0:
raise PreparationError(
f"feature branch does not contain current {args.remote}/{develop}"
)
if not local_exists and remote_exists and run(
["git", "merge-base", "--is-ancestor", remote_develop, remote_ref],
cwd=workspace,
check=False,
).returncode != 0:
raise PreparationError(
f"remote feature branch does not contain current {args.remote}/{develop}"
)
if remote_exists and local_exists and git(
workspace, "rev-parse", local_ref
) != git(workspace, "rev-parse", remote_ref):
raise PreparationError("existing local and remote feature branches differ")
emit(
"PREFLIGHT_READY",
branch=branch,
branch=expected_branch,
source_branch=branch,
milestone=str(milestone_path),
workspace=str(workspace),
existing_workspace=True,
@ -1162,7 +1211,33 @@ def prepare_existing(args: argparse.Namespace) -> int:
args.planner_target,
args.review_target,
)
ensure_clean(workspace, "feature workspace after agent probe")
ensure_clean(workspace, "current workspace after agent probe")
if start_from_develop:
if not local_exists:
if remote_exists:
git(
workspace,
"branch",
"--track",
expected_branch,
f"{args.remote}/{expected_branch}",
)
else:
git(workspace, "branch", expected_branch, f"{args.remote}/{develop}")
git(workspace, "switch", expected_branch)
if not remote_exists:
git(workspace, "push", "--set-upstream", args.remote, expected_branch)
else:
git(
workspace,
"branch",
"--set-upstream-to",
f"{args.remote}/{expected_branch}",
expected_branch,
)
branch = expected_branch
emit("FEATURE_BRANCH_PUSHED", branch=branch, remote=args.remote)
ensure_clean(workspace, "feature workspace")
state = {
"status": "workspace-ready",
"milestone": str(milestone_path),

View file

@ -319,12 +319,18 @@ class PrepareWorkspaceTest(unittest.TestCase):
self.assertEqual(coordinate.call_args.kwargs["args"].epics, "2..2")
self.assertFalse(command(workspace, "git", "status", "--porcelain=v1"))
def test_existing_workspace_mode_refuses_non_target_branch(self) -> None:
def test_existing_workspace_mode_creates_feature_branch_from_develop(self) -> None:
with tempfile.TemporaryDirectory() as raw:
workspace = Path(raw) / "workspace"
command(workspace.parent, "git", "init", "-b", "dev", str(workspace))
root = Path(raw)
remote = root / "remote.git"
workspace = root / "workspace"
command(root, "git", "init", "--bare", str(remote))
command(root, "git", "init", "-b", "dev", str(workspace))
command(workspace, "git", "config", "user.name", "Test Agent")
command(workspace, "git", "config", "user.email", "agent@example.test")
command(workspace, "git", "config", "gitflow.branch.develop", "dev")
command(workspace, "git", "config", "gitflow.prefix.feature", "feature/")
command(workspace, "git", "remote", "add", "origin", str(remote))
milestone = (
workspace
/ "agent-roadmap"
@ -341,11 +347,21 @@ class PrepareWorkspaceTest(unittest.TestCase):
"## 기능\n\n### Epic: [first] First\n\n- [ ] [first-task] first\n",
encoding="utf-8",
)
(workspace / ".gitignore").write_text(
"agent-roadmap/current.md\n", encoding="utf-8"
)
command(workspace, "git", "add", ".gitignore", "agent-roadmap")
command(workspace, "git", "commit", "-m", "init")
command(workspace, "git", "push", "-u", "origin", "dev")
(workspace / "agent-roadmap" / "current.md").write_text(
"phase/phase-one/milestones/sample.md\n", encoding="utf-8"
)
output = io.StringIO()
with contextlib.redirect_stdout(output):
with (
contextlib.redirect_stdout(output),
mock.patch.dict(os.environ, {"AGENT_OPS_TESTING": "1"}),
mock.patch.object(MODULE, "coordinate_batch", return_value=0) as coordinate,
):
result = MODULE.main(
[
"--existing-workspace",
@ -355,12 +371,21 @@ class PrepareWorkspaceTest(unittest.TestCase):
str(milestone.relative_to(workspace)),
"--epics",
"1..1",
"--dry-run",
"--skip-agent-probe",
]
)
self.assertEqual(result, 2)
self.assertIn("current workspace must use", output.getvalue())
self.assertEqual(result, 0, output.getvalue())
self.assertIn('"event": "FEATURE_BRANCH_PUSHED"', output.getvalue())
self.assertEqual(
command(workspace, "git", "branch", "--show-current"),
"feature/sample",
)
self.assertEqual(
command(workspace, "git", "rev-parse", "feature/sample"),
command(workspace, "git", "rev-parse", "origin/feature/sample"),
)
self.assertEqual(coordinate.call_args.kwargs["workspace"], workspace)
self.assertFalse(command(workspace, "git", "status", "--porcelain=v1"))
def test_two_epic_batch_opens_dispatcher_barrier_once_after_both(self) -> None:
with tempfile.TemporaryDirectory() as raw:

View file

@ -62,7 +62,7 @@
라우팅 우선순위:
- `X에 Y 작업 준비해`처럼 workspace 위치와 대상 Milestone이 함께 명시되면 `prepare-milestone-workspace` 생성 모드를 선택한다. 상대 workspace는 develop repository root 기준으로 해석한다. 이 형식에서 workspace 위치가 없으면 확인을 요청한다.
- `현 마일스톤에 N번째 에픽 작업 시작해`, `Y 마일스톤에 1,2번째 에픽까지 작업 시작해` 또는 `현|Y 마일스톤에 남은 에픽 작업들 시작해`는 같은 스킬의 현재 workspace 실행 모드를 선택한다. `현 마일스톤`current/feature branch의 단일 일치 target, 이름 있는 Milestone은 현재 workspace branch/current와 정확히 일치하는 target만 허용한다. `N번째``N..N`, `N번째까지``1..N`, `1,2번째까지``1..2`, `남은 에픽`은 문서 순서의 미완료 Epic 전체를 뜻하는 `remaining`으로 해석한다. 현재 workspace가 준비되지 않았거나 target과 다르면 workspace를 추정·전환하지 않고 거부한다.
- `현 마일스톤에 N번째 에픽 작업 시작해`, `Y 마일스톤에 1,2번째 에픽까지 작업 시작해` 또는 `현|Y 마일스톤에 남은 에픽 작업들 시작해`는 같은 스킬의 현재 workspace 실행 모드를 선택한다. `현 마일스톤`현재 workspace의 `current.md` 단일 target, 이름 있는 Milestone은 같은 workspace의 `current.md`와 정확히 일치하는 target만 허용한다. 현재 branch가 대상 `feature/<milestone-slug>`이면 그대로 재사용하고, Git Flow develop branch이면 사용자가 지정한 그 workspace에서 대상 feature branch를 생성·전환한 뒤 실행한다. `N번째``N..N`, `N번째까지``1..N`, `1,2번째까지``1..2`, `남은 에픽`은 문서 순서의 미완료 Epic 전체를 뜻하는 `remaining`으로 해석한다. 다른 feature/topic branch이거나 target이 다르면 workspace를 추정·전환하지 않고 거부한다.
- 두 모드 모두 선택 Epic을 각각 `prepare-epic-work-items`로 준비하되 전체 `MILESTONE_WORK_ITEMS_READY` 전에는 dispatcher를 시작하지 않는다.
- 한 Epic 안에서 작은 작업 직접 처리와 큰 작업 plan 작성을 함께 요청하면 `prepare-epic-work-items`를 선택한다. 이미 존재하는 plan만 세분화하는 요청과 새로운 plan만 작성하는 요청에는 이 스킬을 선택하지 않는다.
- 이미 생성된 미착수 pair의 분할만 요청하면 lane과 관계없이 `refine-plans`를 선택한다. 새 plan 작성이나 구현 범위 재분석이 포함되면 `plan`을 선택한다.