fix(agent-ui): 동기화 경로 소유권을 검증한다

진행 중인 UI 작업과 직접 동기화가 같은 경로를 갱신하지 않도록 한다.
This commit is contained in:
toki 2026-07-29 09:42:17 +09:00
parent a52c73bc2a
commit bec5e29099
6 changed files with 102 additions and 16 deletions

View file

@ -64,18 +64,20 @@ agent-ui/
- component 문서는 `component_id`, `status`, `source_evidence`를 둔다.
- view 문서의 `frame`은 frame-view 문서가 있으면 경로, 없으면 `null`로 둔다.
- frame-view 문서는 visual source가 있을 때만 만들며 `view_id`, `definition`, `visual_source`, `regions`를 둔다.
- `source_evidence`는 list이며 각 항목은 `type`, `path`, `notes`를 둔다.
- `source_evidence.type``code`, `docs`, `user` 중 하나다.
- definition-index/view/component의 `source_evidence`는 list이며 각 항목은 `type`, `path`, `notes`를 둔다.
- 이 문서들의 `source_evidence.type``code`, `docs`, `user` 중 하나다.
- 실제 파일 근거가 없으면 `path: null`을 사용한다. placeholder 문자열을 현재 근거처럼 남기지 않는다.
- view에 visual source가 없으면 view 문서의 `frame``null`로 두고 frame-view 문서를 만들지 않는다.
- frame-view를 만들 때 `visual_source`는 실제 존재하는 visual source 경로여야 한다. 없는 `wire.excalidraw`를 기본 근거처럼 쓰지 않는다.
- `status` 값은 `구현됨`, `계획`, `가정`, `불명확` 중 하나다.
- view/component의 `status` 값은 `구현됨`, `계획`, `가정`, `불명확` 중 하나다. frame-view와 index 문서에는 `status`를 두지 않는다.
- `구현됨`은 코드 반영 완료 상태다. 코드 근거가 있으면 존재하는 `code` evidence path를 함께 둔다.
- `계획`은 정합화된 UI 정의지만 아직 코드 반영 전이거나 사용자 확인 후 재작업 대상인 상태다.
- `가정`은 사용자 입력 또는 추정만 있고 확정 근거가 부족한 상태다.
- `불명확`은 근거가 부족하거나 판단할 수 없는 상태다.
- `가정``불명확`은 코드 동기화 대상이 아니며, sync 전에 `agent-ui/USER_REVIEW.md`로 분리한다.
- frontmatter의 `status`, `source_evidence`, `regions`와 본문 `Status`, `Source Evidence`, `Regions`는 같은 기준을 말해야 한다.
- view는 frontmatter의 `status`, `source_evidence`, `regions`와 본문 `Status`, `Source Evidence`, `Regions`가 같은 기준을 말해야 한다.
- component는 frontmatter의 `status`, `source_evidence`와 본문 `Status`, `Source Evidence`가 같은 기준을 말해야 한다.
- frame-view는 frontmatter `regions`와 본문 `Required Regions`가 같은 기준을 말해야 하며, `status``Source Evidence`를 요구하지 않는다.
- frontmatter는 기계적 검증을 위한 최소 schema이고, 본문 Markdown은 사람과 agent가 읽는 설명을 유지한다.
## 운영 동기화 흐름
@ -112,6 +114,7 @@ agent-ui/
- schema version 4의 `pending_code_work`/`reconciled_code_work`는 `plan-required` task/UI 매핑과 완료 근거고, `pending_milestone_work`/`reconciled_milestone_work`는 `milestone-required` Milestone/UI 매핑과 완료 근거다. 일반 plan/review/complete.log/Milestone 문서는 이 schema를 알 필요가 없다.
- `status_paths`에는 status schema가 있는 view/component만, `frame_paths`에는 validation-only frame-view만 기록한다.
- pending entry의 `status_paths``frame_paths`는 task와 Milestone을 통틀어 중복 소유할 수 없다. refinement/reindex에서는 helper의 `previous-task-path` rebind를 사용하고 scope/evidence 변경이 있을 때만 검증 후 replace한다.
- direct sync의 agent-ui 경로 트리는 code/Milestone pending entry가 소유한 `status_paths`/`frame_paths`와 겹칠 수 없다. 코드 변경 전과 commit 전 확인하고, `record-sync`도 lock 안에서 동일·상위·하위 경로 겹침을 거부한다.
- pending entry는 `prepare-code-work`만 생성/갱신하고 `reconcile-completion` 검증 통과 시에만 `reconciled_code_work`로 이동한다. 실패, ambiguous completion, evidence 부족에서는 유지한다.
- Milestone pending entry는 `prepare-milestone-work`만 생성/갱신하고 `complete-milestone check-only`와 UI 검증이 모두 통과한 `reconcile-milestone-completion`에서만 `reconciled_milestone_work`로 이동한다. 실패하면 Milestone close를 진행하지 않는다.
- `sync-agent-ui`의 prepare, reconcile, direct/baseline 기록은 모두 `sync-agent-ui/scripts/sync_state.py`의 agent-ui directory lock과 inspection 시점 SHA-256 검증으로 수행한다. shared state를 수동 read-modify-write하지 않는다.

View file

@ -52,6 +52,7 @@ description: 정합화된 agent-ui 변경분을 코드에 직접 반영하거나
- [ ] `agent-ui/.sync-state.json`을 읽는다. 없고 `mode=incremental|prepare-code-work|reconcile-completion|prepare-milestone-work|reconcile-milestone-completion`이면 legacy baseline migration 필요로 보고 중단한다.
- [ ] `schema_version: 1|2|3`이면 bundled state helper가 기존 필드와 pending 매핑을 보존·정규화하고 `schema_version: 4`로 올리게 한다.
- [ ] `.sync-state.json`을 갱신하는 모든 모드에서 `agent-ops/skills/common/sync-agent-ui/scripts/sync_state.py`를 사용한다. shared state를 수동 read-modify-write하지 않는다.
- [ ] `mode=incremental|full`이면 현재 sync 대상 agent-ui 경로 또는 그 상·하위 경로가 code/Milestone pending entry의 `status_paths` 또는 `frame_paths`와 겹치지 않는지 확인한다. 겹치면 direct sync를 시작하지 않고 기존 pending owner 흐름으로 돌린다.
- [ ] `mode=incremental|full`이면 수정할 UI 코드 경로에 해당하는 project/domain rule을 먼저 읽는다.
- [ ] `mode=incremental|full`이고 코드 변경 검증이 필요하면 작업 환경의 `agent-test/<env>/rules.md`를 읽는다. 환경 미지정은 `local`로 본다.
- [ ] `mode=prepare-code-work`이면 exact active `PLAN-*-G??.md`와 matching `CODE_REVIEW-*-G??.md``task-path`에 존재하는지 확인한다.
@ -211,6 +212,7 @@ python3 agent-ops/skills/common/sync-agent-ui/scripts/sync_state.py reconcile-mi
- `mode=full`에서 `.sync-state.json`이 없으면 code sync 성공 후 새 sync state를 만든다.
- `가정` 또는 `불명확` 상태 항목은 코드 반영 대상에서 제외하고 USER_REVIEW로 남긴다.
- 현재 범위에 미해결 USER_REVIEW가 있으면 중단한다.
- 확정된 sync 대상 agent-ui 경로 트리가 `pending_code_work` 또는 `pending_milestone_work``status_paths`/`frame_paths`와 하나라도 겹치면 direct sync 대상으로 가져오지 않는다. 해당 pending task/Milestone의 완료 정합화 흐름을 사용한다.
2. **코드 반영 방식 판정**
- 이 판정은 agent-ui 문서 작업의 크기가 아니라, 정합화된 agent-ui 변경분을 코드에 반영하는 구현 작업의 크기와 위험만 대상으로 한다.
@ -250,10 +252,11 @@ python3 agent-ops/skills/common/sync-agent-ui/scripts/sync_state.py reconcile-mi
5. **sync state와 commit/push**
- 이 단계는 `execution-route=direct-sync`에서 코드 반영, 검증, agent-ui status/code evidence 갱신이 모두 통과한 경우에만 수행한다.
- commit 전에 state를 다시 읽어 direct sync 대상이 새 pending entry의 소유 경로와 겹치지 않는지 재확인한다. 겹치면 commit하지 않고 충돌 경로와 pending owner를 보고한다.
- 검증이 통과하면 agent-ui/code 변경을 먼저 commit한다.
- `.sync-state.json`이 없으면 `agent-ops/skills/common/_templates/agent-ui/sync-state-template.json` 기준으로 새로 만든다.
- 방금 만든 sync 결과 commit hash와 `.sync-state.json`의 최신 SHA-256을 bundled helper `record-sync`에 넘긴다.
- helper가 `last_sync_mode`, `last_synced_head`, `last_synced_at`, `agent_ui_paths`, `code_paths`, `notes`를 갱신하고 기존 code/Milestone pending·reconciled 기록을 보존하게 한다.
- helper가 lock 안에서 pending 소유 경로와의 겹침을 다시 거부한 뒤 `last_sync_mode`, `last_synced_head`, `last_synced_at`, `agent_ui_paths`, `code_paths`, `notes`를 갱신하고 기존 code/Milestone pending·reconciled 기록을 보존하게 한다.
```bash
python3 agent-ops/skills/common/sync-agent-ui/scripts/sync_state.py record-sync \
@ -290,6 +293,7 @@ python3 agent-ops/skills/common/sync-agent-ui/scripts/sync_state.py record-sync
- [ ] `plan-required` 판정에서 직접 코드 변경, status 구현됨 전환, baseline sync state 갱신, commit/push를 하지 않고 pending entry만 기록했는가
- [ ] `milestone-required` 판정에서 직접 코드 변경, status 구현됨 전환, baseline sync state 갱신, commit/push를 하지 않고 exact Milestone pending mapping만 기록했는가
- [ ] `direct-sync` 판정에서 코드 반영과 검증이 통과한 항목은 같은 실행 안에서 agent-ui 문서 status를 `구현됨`으로 바꾸고 code evidence를 남겼는가
- [ ] `direct-sync` 대상이 code/Milestone pending owner의 status/frame path와 겹치지 않는지 코드 변경 전과 commit 전 확인했는가
- [ ] `prepare-code-work`가 exact task path와 활성 agent-ui 문서를 unique pending entry로 기록했는가
- [ ] refinement/reindex된 pending task는 전체 결과를 닫는 child 하나로 rebind되어 이전 task path가 남지 않았는가
- [ ] view/component는 `status_paths`, frame-view는 validation-only `frame_paths`로 분리했는가
@ -346,6 +350,7 @@ python3 agent-ops/skills/common/sync-agent-ui/scripts/sync_state.py record-sync
- `plan-required` 또는 `milestone-required`로 판정된 작업을 같은 `sync-agent-ui` 실행에서 직접 구현하지 않는다.
- 일반 plan/review/complete.log에 agent-ui 전용 completion schema나 status 전환 책임을 추가하지 않는다.
- 일반 Milestone 문서나 roadmap 스킬에 agent-ui 전용 완료 필드나 status 전환 책임을 추가하지 않는다.
- code/Milestone pending entry가 소유한 status/frame path를 direct sync로 반영하거나 `record-sync`하지 않는다.
- matching pending entry 없이 `complete.log`만 보고 agent-ui 문서를 추측해 갱신하지 않는다.
- frame-view 문서에 `status`를 추가하거나 `구현됨` 전환을 적용하지 않는다.
- `.sync-state.json`을 bundled helper 밖에서 수동 read-modify-write하지 않는다.

View file

@ -61,6 +61,16 @@ def _stable_unique(values: list[str]) -> list[str]:
return list(dict.fromkeys(values))
def _path_trees_overlap(left: str, right: str) -> bool:
left_path = PurePosixPath(left)
right_path = PurePosixPath(right)
return (
left_path == right_path
or left_path in right_path.parents
or right_path in left_path.parents
)
def _require_relative_path(value: str, *, prefix: str | None = None) -> str:
path = PurePosixPath(value)
if not value or path.is_absolute() or ".." in path.parts or "." in path.parts:
@ -760,6 +770,25 @@ def _record_sync(args: argparse.Namespace) -> dict[str, Any]:
raise StateError("incremental/full record-sync requires code_paths")
if any(path.startswith(("agent-ui/", "agent-task/")) for path in code_paths):
raise StateError("record-sync code_paths must not point to agent-ui or agent-task artifacts")
if args.sync_mode in {"incremental", "full"}:
pending_paths = {
path
for entry in state["pending_code_work"] + state["pending_milestone_work"]
for path in entry["status_paths"] + entry["frame_paths"]
}
overlapping_paths = sorted(
pending_path
for pending_path in pending_paths
if any(
_path_trees_overlap(pending_path, sync_path)
for sync_path in agent_ui_paths
)
)
if overlapping_paths:
raise StateError(
"incremental/full record-sync overlaps pending work paths: "
+ ", ".join(overlapping_paths)
)
state["last_sync_mode"] = args.sync_mode
state["last_synced_head"] = args.sync_result_head

View file

@ -398,7 +398,13 @@ class SyncStateTests(unittest.TestCase):
digest = write_json(self.state_path, self.base_state)
prepared = sync_state._prepare(self.prepare_args(digest))
recorded = sync_state._record_sync(
self.record_sync_args(prepared["after_sha256"])
self.record_sync_args(
prepared["after_sha256"],
agent_ui_path=[
"agent-ui/definition/views/other-dashboard/index.md"
],
code_path=["src/other_dashboard.py"],
)
)
state = json.loads(self.state_path.read_text())
self.assertEqual("sync_recorded", recorded["status"])
@ -417,15 +423,54 @@ class SyncStateTests(unittest.TestCase):
prepared = sync_state._prepare_milestone(
self.prepare_milestone_args(digest)
)
state_before = self.state_path.read_bytes()
with self.assertRaisesRegex(
sync_state.StateError,
"overlaps pending work paths",
):
sync_state._record_sync(
self.record_sync_args(prepared["after_sha256"])
)
self.assertEqual(state_before, self.state_path.read_bytes())
recorded = sync_state._record_sync(
self.record_sync_args(prepared["after_sha256"])
self.record_sync_args(
prepared["after_sha256"],
agent_ui_path=[
"agent-ui/definition/views/other-dashboard/index.md"
],
code_path=["src/other_dashboard.py"],
)
)
state = json.loads(self.state_path.read_text())
self.assertEqual("sync_recorded", recorded["status"])
self.assertEqual(1, len(state["pending_milestone_work"]))
self.assertEqual([], state["reconciled_milestone_work"])
def test_record_sync_rejects_paths_owned_by_pending_work(self):
digest = write_json(self.state_path, self.base_state)
prepared = sync_state._prepare(self.prepare_args(digest))
state_before = self.state_path.read_bytes()
for agent_ui_path in (
"agent-ui/definition/views/dashboard/index.md",
"agent-ui/definition/views/dashboard",
):
with self.subTest(agent_ui_path=agent_ui_path):
with self.assertRaisesRegex(
sync_state.StateError,
"overlaps pending work paths",
):
sync_state._record_sync(
self.record_sync_args(
prepared["after_sha256"],
agent_ui_path=[agent_ui_path],
)
)
self.assertEqual(state_before, self.state_path.read_bytes())
def test_record_migration_allows_no_code_paths(self):
digest = write_json(self.state_path, self.base_state)
recorded = sync_state._record_sync(

View file

@ -12,7 +12,6 @@ description: 로드맵 업데이트, 로드맵에 추가, 마일스톤 추가·
archive도 같은 Phase scaffold를 유지하며 `archive/phase/<phase-slug>/...` 아래에 둔다.
로드맵 전체를 매 작업마다 읽지 않도록 유지하면서, 브랜치별 로컬 `current.md`의 활성 Phase와 활성 Milestone 창이 실제 작업 후보 목록으로 동작하게 한다.
`priority-queue.md`는 Phase를 가로지르는 실행 순서만 담당하며, Milestone 상세 정보는 복제하지 않는다.
Milestone은 구현 계획이 아니라 방향성, 범위, 위험, 확인 필요 사항을 기록하는 협업 문서로 유지한다.
Epic과 Task는 별도 파일로 분리하지 않고 Milestone 문서의 `기능` 안에서 관리한다. 별도 `완료 기준` 섹션은 만들지 않고, 검증이 필요한 기능에만 같은 Task 안의 `검증:` 문구로 통합한다.

View file

@ -1,6 +1,5 @@
---
name: validate-agent-ui
version: 1.1.0
description: agent-ui scaffold, definition/frame/component/wireframe/source evidence/status 정합성을 맞추고 필요한 USER_REVIEW와 code sync intent를 분리하는 스킬
---
@ -46,7 +45,11 @@ definition, component, frame, visual source, USER_REVIEW, archive가 서로 뒤
- `definition/views/**/index.md`에서 view 목록을 만든다.
- `definition/components/**/index.md`에서 component 목록을 만든다.
- `frame/views/**/index.md`에서 frame 목록을 만든다. frame이 없는 view는 visual source가 없는 정상 상태로 본다.
- 활성 Markdown 문서의 frontmatter에서 `ui_doc_type`, id, status, source_evidence, regions를 수집한다.
- 활성 Markdown 문서의 frontmatter에서 `ui_doc_type`과 문서 유형별 필드를 수집한다.
- view: `view_id`, `status`, `frame`, `source_evidence`, `regions`
- component: `component_id`, `status`, `source_evidence`
- frame-view: `view_id`, `definition`, `visual_source`, `regions`
- definition-index: `surface_type`, `source_evidence`
- 같은 레벨의 `<name>.md``<name>/` 혼재 여부를 확인한다.
2. **구조 검증과 자동 보정**
@ -62,15 +65,17 @@ definition, component, frame, visual source, USER_REVIEW, archive가 서로 뒤
- `ui_doc_type`이 경로와 맞는지 확인한다. 예: `definition/views/<view-id>/index.md``view`다.
- view/component/frame id가 경로 id와 일치하는지 확인한다.
- view/component 문서에 `Source Evidence``Status`가 있는지 확인한다.
- frontmatter `source_evidence`는 list이고 각 항목은 `type`, `path`, `notes`를 가져야 한다.
- `source_evidence.type``code`, `docs`, `user` 중 하나여야 한다.
- view/component/definition-index의 frontmatter `source_evidence`는 list이고 각 항목은 `type`, `path`, `notes`를 가져야 한다.
- 이 문서들의 `source_evidence.type``code`, `docs`, `user` 중 하나여야 한다.
- 근거 파일이 없으면 `path: null`이어야 하며, placeholder 문자열을 근거로 보지 않는다.
- frontmatter의 `status`, `source_evidence`, `regions`와 본문 `Status`, `Source Evidence`, `Regions`가 충돌하는지 확인한다.
- `Status``구현됨`, `계획`, `가정`, `불명확` 중 하나여야 한다.
- view는 frontmatter의 `status`, `source_evidence`, `regions`와 본문 `Status`, `Source Evidence`, `Regions`가 충돌하는지 확인한다.
- component는 frontmatter의 `status`, `source_evidence`와 본문 `Status`, `Source Evidence`가 충돌하는지 확인한다.
- frame-view는 frontmatter `regions`와 본문 `Required Regions`가 충돌하는지 확인한다. frame-view에는 `status``Source Evidence`를 요구하지 않는다.
- view/component의 `Status``구현됨`, `계획`, `가정`, `불명확` 중 하나여야 한다.
- `구현됨` 항목은 코드 반영 완료 상태여야 한다. 코드 근거가 있으면 실제 존재하는 code evidence path를 가져야 한다.
- `구현됨`의 code evidence 경로가 명시되어 있고 실제 존재하지 않으면 `repair=true`일 때 `계획`으로 낮추고 USER_REVIEW를 남길 수 있다.
- `가정`, `불명확` 항목이 코드 동기화 가능한 정의처럼 설명되면 자동 의미 변경하지 않고 USER_REVIEW로 남긴다.
- region id가 `<view-id>.<region>` 또는 `<view-id>.<region>.<subregion>` 형식인지 확인한다.
- view와 frame-view의 region id가 `<view-id>.<region>` 또는 `<view-id>.<region>.<subregion>` 형식인지 확인한다.
- view 문서에서 참조한 component id가 component 목록에 있는지 확인한다.
- 없는 component가 단순 누락이고 생성 의도가 명확하면 `repair=true`일 때 component skeleton을 만들고 `Status``Source Evidence`를 채운다.
- 새 component로 만들지 기존 component로 바꿀지 판단이 필요하면 USER_REVIEW로 남긴다.
@ -133,7 +138,7 @@ definition, component, frame, visual source, USER_REVIEW, archive가 서로 뒤
- 명확한 단일 후보 상대 링크 보정
- visual source가 있는 frame index의 region 누락 보완
- 빈 archive/user-review 디렉터리 생성
- 템플릿 필수 섹션과 frontmatter 누락 보완. `ui_doc_type`, `Source Evidence`, `Status` 누락 포함
- 템플릿 필수 섹션과 frontmatter 누락 보완. 모든 문서의 `ui_doc_type`, view/component의 `Source Evidence``Status`, 문서 유형별 필수 필드 누락 포함
- 명시된 code evidence 경로가 실제 존재하지 않는 `구현됨``계획`으로 낮추고 USER_REVIEW를 남기는 보정
## USER_REVIEW 대상