From bec5e290995e6927a203fa0c36abcebe8cd05adc Mon Sep 17 00:00:00 2001 From: toki Date: Wed, 29 Jul 2026 09:42:17 +0900 Subject: [PATCH] =?UTF-8?q?fix(agent-ui):=20=EB=8F=99=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=86=8C=EC=9C=A0=EA=B6=8C=EC=9D=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 진행 중인 UI 작업과 직접 동기화가 같은 경로를 갱신하지 않도록 한다. --- agent-ops/rules/common/rules-agent-ui.md | 11 +++-- .../skills/common/sync-agent-ui/SKILL.md | 7 ++- .../sync-agent-ui/scripts/sync_state.py | 29 +++++++++++ .../sync-agent-ui/tests/test_sync_state.py | 49 ++++++++++++++++++- .../skills/common/update-roadmap/SKILL.md | 1 - .../skills/common/validate-agent-ui/SKILL.md | 21 +++++--- 6 files changed, 102 insertions(+), 16 deletions(-) diff --git a/agent-ops/rules/common/rules-agent-ui.md b/agent-ops/rules/common/rules-agent-ui.md index e9ff963..4742ab0 100644 --- a/agent-ops/rules/common/rules-agent-ui.md +++ b/agent-ops/rules/common/rules-agent-ui.md @@ -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하지 않는다. diff --git a/agent-ops/skills/common/sync-agent-ui/SKILL.md b/agent-ops/skills/common/sync-agent-ui/SKILL.md index dda13f2..7c5ce3f 100644 --- a/agent-ops/skills/common/sync-agent-ui/SKILL.md +++ b/agent-ops/skills/common/sync-agent-ui/SKILL.md @@ -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//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하지 않는다. diff --git a/agent-ops/skills/common/sync-agent-ui/scripts/sync_state.py b/agent-ops/skills/common/sync-agent-ui/scripts/sync_state.py index 5bfef43..4ef08f5 100644 --- a/agent-ops/skills/common/sync-agent-ui/scripts/sync_state.py +++ b/agent-ops/skills/common/sync-agent-ui/scripts/sync_state.py @@ -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 diff --git a/agent-ops/skills/common/sync-agent-ui/tests/test_sync_state.py b/agent-ops/skills/common/sync-agent-ui/tests/test_sync_state.py index 7aa006c..97de2a9 100644 --- a/agent-ops/skills/common/sync-agent-ui/tests/test_sync_state.py +++ b/agent-ops/skills/common/sync-agent-ui/tests/test_sync_state.py @@ -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( diff --git a/agent-ops/skills/common/update-roadmap/SKILL.md b/agent-ops/skills/common/update-roadmap/SKILL.md index 9fc14a8..0d4b173 100644 --- a/agent-ops/skills/common/update-roadmap/SKILL.md +++ b/agent-ops/skills/common/update-roadmap/SKILL.md @@ -12,7 +12,6 @@ description: 로드맵 업데이트, 로드맵에 추가, 마일스톤 추가· archive도 같은 Phase scaffold를 유지하며 `archive/phase//...` 아래에 둔다. 로드맵 전체를 매 작업마다 읽지 않도록 유지하면서, 브랜치별 로컬 `current.md`의 활성 Phase와 활성 Milestone 창이 실제 작업 후보 목록으로 동작하게 한다. `priority-queue.md`는 Phase를 가로지르는 실행 순서만 담당하며, Milestone 상세 정보는 복제하지 않는다. - Milestone은 구현 계획이 아니라 방향성, 범위, 위험, 확인 필요 사항을 기록하는 협업 문서로 유지한다. Epic과 Task는 별도 파일로 분리하지 않고 Milestone 문서의 `기능` 안에서 관리한다. 별도 `완료 기준` 섹션은 만들지 않고, 검증이 필요한 기능에만 같은 Task 안의 `검증:` 문구로 통합한다. diff --git a/agent-ops/skills/common/validate-agent-ui/SKILL.md b/agent-ops/skills/common/validate-agent-ui/SKILL.md index 6ef9904..22de7b2 100644 --- a/agent-ops/skills/common/validate-agent-ui/SKILL.md +++ b/agent-ops/skills/common/validate-agent-ui/SKILL.md @@ -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` - 같은 레벨의 `.md`와 `/` 혼재 여부를 확인한다. 2. **구조 검증과 자동 보정** @@ -62,15 +65,17 @@ definition, component, frame, visual source, USER_REVIEW, archive가 서로 뒤 - `ui_doc_type`이 경로와 맞는지 확인한다. 예: `definition/views//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와 frame-view의 region id가 `.` 또는 `..` 형식인지 확인한다. - 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 대상