810 lines
34 KiB
Markdown
810 lines
34 KiB
Markdown
# 3-플랫폼 동기화 Phase 0 — 생성 엔진 + agents Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Claude의 9개 agent를 Codex CLI(`.codex/agents/*.toml`)와 Antigravity CLI(`.agents/agents/<name>/agent.json`)의 native 포맷으로 생성하는 멱등 스크립트 `scripts/sync-automation.py`를 만들고, 누락된 2개 agent(`branch-depth-auditor`·`coverage-auditor`)를 양 플랫폼에 추가한다.
|
||
|
||
**Architecture:** Claude `.claude/agents/<name>.md` frontmatter의 `tools:`를 **권한 SSOT**로 삼아 codex `sandbox_mode`와 antigravity `toolNames`를 기계적으로 파생한다. 프롬프트 본문은 플랫폼별 SSOT(`.codex/agents/<name>.md` = codex 일반화 본문, `.agents/plugins/wiki-superpowers/agents/<name>.md` = antigravity gated 본문)에서 가져와 native 머신 포맷으로 포장한다. 스크립트는 순수 함수(권한 파생·포맷 직렬화)와 I/O를 분리하고, `--check` 모드로 SSOT↔생성물 drift를 0으로 검증한다.
|
||
|
||
**Tech Stack:** Python 3.12 (stdlib only — `tomllib` for TOML 검증, `json`, `pathlib`, `argparse`), pytest.
|
||
|
||
---
|
||
|
||
## 배경 / 현재 상태 (실측)
|
||
|
||
- Claude agents (SSOT identity + 권한): `.claude/agents/*.md` — frontmatter `name`/`description`/`tools`(콤마 목록)/`model: sonnet`. 9개 존재.
|
||
- Codex agents: `.codex/agents/*.md` — frontmatter `name`/`description`(tools/model 없음) + 일반화 본문. **7개** (branch-depth-auditor·coverage-auditor 누락). codex가 실제 읽는 native 포맷은 `*.toml`인데 **하나도 없음**.
|
||
- Antigravity agents: `.agents/plugins/wiki-superpowers/agents/*.md` — frontmatter `name`/`description` + G1~G4 gate 본문. **7개** (동일 2개 누락). antigravity가 실제 읽는 `.agents/agents/<name>/agent.json` **하나도 없음** (디스크의 `~/.gemini/.../agent.json`은 stale·오류 생성물 — read-only agent에 write 도구가 들어있고 content가 요약됨. 신뢰 금지).
|
||
|
||
**확정된 agent.json 스키마** (실 디스크 `~/.gemini/antigravity-cli/brain/.../wiki-research-lane/agent.json`에서 키 구조만 채택):
|
||
```json
|
||
{
|
||
"name": "<name>",
|
||
"description": "<desc>",
|
||
"hidden": true,
|
||
"config": {
|
||
"customAgent": {
|
||
"systemPromptSections": [ { "title": "Agent System Instructions", "content": "<body>" } ],
|
||
"toolNames": [ ... ],
|
||
"systemPromptConfig": {
|
||
"includeSections": ["user_information","mcp_servers","skills","subagent_reminder","messaging","artifacts","user_rules"]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**권한 파생 규칙** (Claude `tools:` → 플랫폼):
|
||
|
||
| Claude tool | Antigravity `toolNames` | 비고 |
|
||
|---|---|---|
|
||
| (모든 agent 공통 baseline) | `send_message`, `view_file`, `find_by_name`, `grep_search`, `list_dir` | 9개 모두 Read+Grep+Glob 보유 |
|
||
| `Bash` | `run_command` | |
|
||
| `Edit` | `replace_file_content`, `multi_replace_file_content` | |
|
||
| `Write` | `write_to_file` | |
|
||
| `WebFetch` | `read_url_content` | |
|
||
| `WebSearch` | `search_web` | |
|
||
|
||
- Codex `sandbox_mode` = `workspace-write` (Claude tools에 `Edit` 또는 `Write` 포함 시) / else `read-only`.
|
||
- 9개 agent 권한 (Claude frontmatter 실측):
|
||
- read-only: `branch-depth-auditor`(Read,Grep,Glob), `coverage-auditor`·`wiki-adversarial-reviewer`·`wiki-diagram-reviewer`·`wiki-link-verifier`·`wiki-research-lane`(Read,Grep,Glob,Bash)
|
||
- workspace-write: `wiki-doc-author`(+Edit,Write), `wiki-source-summarizer`(+Edit,Write,WebFetch), `wiki-decision-researcher`(+Write,WebSearch,WebFetch)
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
- Create: `scripts/sync-automation.py` — 단일 진입점 CLI. 순수 함수 구역(파싱·권한 파생·직렬화) + I/O 구역(파일 read/write) + `argparse` CLI.
|
||
- Create: `scripts/test_sync_automation.py` — pytest 단위 테스트 (순수 함수 + 생성물 유효성).
|
||
- Create: `.codex/agents/branch-depth-auditor.md`, `.codex/agents/coverage-auditor.md` — codex 일반화 본문 SSOT (Claude에서 적응).
|
||
- Create: `.agents/plugins/wiki-superpowers/agents/branch-depth-auditor.md`, `.agents/plugins/wiki-superpowers/agents/coverage-auditor.md` — antigravity gated 본문 SSOT.
|
||
- Generate (스크립트 출력): `.codex/agents/<name>.toml` ×9, `.agents/agents/<name>/agent.json` ×9.
|
||
- Modify (Phase 0 말미, 최소): `.codex/agents/README.md`, `.agents/plugins/wiki-superpowers/README.md` — 생성 스크립트 사용법 1단락. (전체 문서 정리는 Phase 2.)
|
||
|
||
> 책임 경계: `sync-automation.py`는 *권한 파생 + 포맷 포장*만 한다. 본문 프로즈의 플랫폼 적응(일반화·gate 작성)은 사람이 SSOT `.md`에 직접 한다. codex는 `.toml`만 로드하므로 `.codex/agents/*.md`는 우리 SSOT로 남고 codex는 무시한다.
|
||
|
||
---
|
||
|
||
## Task 1: 스크립트 골격 + frontmatter 파싱 + 권한 파생 (순수 함수)
|
||
|
||
**Files:**
|
||
- Create: `scripts/sync-automation.py`
|
||
- Test: `scripts/test_sync_automation.py`
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```python
|
||
# scripts/test_sync_automation.py
|
||
import json
|
||
import tomllib
|
||
|
||
import sync_automation as s
|
||
|
||
|
||
def test_parse_frontmatter_extracts_fields():
|
||
md = (
|
||
"---\n"
|
||
"name: wiki-link-verifier\n"
|
||
"description: Audit the wiki for orphans.\n"
|
||
"tools: Read, Grep, Glob, Bash\n"
|
||
"model: sonnet\n"
|
||
"---\n\n"
|
||
"You are the Wiki Link Verifier.\n"
|
||
)
|
||
fm, body = s.parse_frontmatter(md)
|
||
assert fm["name"] == "wiki-link-verifier"
|
||
assert fm["description"] == "Audit the wiki for orphans."
|
||
assert fm["tools"] == "Read, Grep, Glob, Bash"
|
||
assert body == "You are the Wiki Link Verifier.\n"
|
||
|
||
|
||
def test_parse_tools_list():
|
||
assert s.parse_tools("Read, Grep, Glob, Bash") == ["Read", "Grep", "Glob", "Bash"]
|
||
assert s.parse_tools("Read,Edit,Write") == ["Read", "Edit", "Write"]
|
||
|
||
|
||
def test_sandbox_mode_read_only_when_no_write_tools():
|
||
assert s.codex_sandbox_mode(["Read", "Grep", "Glob", "Bash"]) == "read-only"
|
||
|
||
|
||
def test_sandbox_mode_workspace_write_when_edit_or_write():
|
||
assert s.codex_sandbox_mode(["Read", "Edit", "Write", "Bash"]) == "workspace-write"
|
||
assert s.codex_sandbox_mode(["Read", "Write", "WebSearch"]) == "workspace-write"
|
||
|
||
|
||
def test_antigravity_toolnames_read_only_agent():
|
||
# Read,Grep,Glob,Bash -> baseline + run_command, no write tools
|
||
assert s.antigravity_tool_names(["Read", "Grep", "Glob", "Bash"]) == [
|
||
"send_message", "view_file", "find_by_name", "grep_search", "list_dir",
|
||
"run_command",
|
||
]
|
||
|
||
|
||
def test_antigravity_toolnames_write_agent_with_web():
|
||
# source-summarizer: Read,Edit,Write,Bash,Grep,Glob,WebFetch
|
||
assert s.antigravity_tool_names(
|
||
["Read", "Edit", "Write", "Bash", "Grep", "Glob", "WebFetch"]
|
||
) == [
|
||
"send_message", "view_file", "find_by_name", "grep_search", "list_dir",
|
||
"write_to_file", "replace_file_content", "multi_replace_file_content",
|
||
"run_command", "read_url_content",
|
||
]
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -v`
|
||
Expected: FAIL with `ModuleNotFoundError: No module named 'sync_automation'`.
|
||
|
||
> 참고: 파일명이 `sync-automation.py`(하이픈)라 `import sync_automation`이 안 된다. 테스트 상단에서 모듈을 로드하도록 `conftest.py`로 별칭을 만든다(아래 Step 3에 포함). 또는 파일명을 `sync_automation.py`로 하고 CLI는 `python3 scripts/sync_automation.py`로 부른다. **결정: 파일명을 `scripts/sync_automation.py`(언더스코어)로 한다** — import 가능 + CLI 호출에 지장 없음. 본 계획의 이후 모든 경로에서 `sync_automation.py`로 읽는다.
|
||
|
||
- [ ] **Step 3: Write minimal implementation**
|
||
|
||
```python
|
||
# scripts/sync_automation.py
|
||
"""Generate Codex/Antigravity native agent files from Claude SSOT.
|
||
|
||
Permission SSOT : .claude/agents/<name>.md frontmatter `tools:`
|
||
Codex body SSOT : .codex/agents/<name>.md
|
||
Antigravity SSOT : .agents/plugins/wiki-superpowers/agents/<name>.md
|
||
Outputs : .codex/agents/<name>.toml , .agents/agents/<name>/agent.json
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
REPO = Path(__file__).resolve().parent.parent
|
||
|
||
CLAUDE_AGENTS = REPO / ".claude" / "agents"
|
||
CODEX_AGENTS = REPO / ".codex" / "agents"
|
||
ANTIGRAV_SSOT = REPO / ".agents" / "plugins" / "wiki-superpowers" / "agents"
|
||
ANTIGRAV_OUT = REPO / ".agents" / "agents"
|
||
|
||
AGENT_NAMES = [
|
||
"branch-depth-auditor",
|
||
"coverage-auditor",
|
||
"wiki-adversarial-reviewer",
|
||
"wiki-decision-researcher",
|
||
"wiki-diagram-reviewer",
|
||
"wiki-doc-author",
|
||
"wiki-link-verifier",
|
||
"wiki-research-lane",
|
||
"wiki-source-summarizer",
|
||
]
|
||
|
||
ANTIGRAV_INCLUDE_SECTIONS = [
|
||
"user_information", "mcp_servers", "skills",
|
||
"subagent_reminder", "messaging", "artifacts", "user_rules",
|
||
]
|
||
|
||
_ANTIGRAV_BASELINE = ["send_message", "view_file", "find_by_name", "grep_search", "list_dir"]
|
||
_ANTIGRAV_EXTRA_ORDER = [
|
||
("Write", ["write_to_file"]),
|
||
("Edit", ["replace_file_content", "multi_replace_file_content"]),
|
||
("Bash", ["run_command"]),
|
||
("WebFetch", ["read_url_content"]),
|
||
("WebSearch", ["search_web"]),
|
||
]
|
||
|
||
|
||
def parse_frontmatter(md: str) -> tuple[dict[str, str], str]:
|
||
lines = md.splitlines(keepends=True)
|
||
if not lines or lines[0].strip() != "---":
|
||
raise ValueError("missing frontmatter open")
|
||
fm: dict[str, str] = {}
|
||
i = 1
|
||
while i < len(lines) and lines[i].strip() != "---":
|
||
line = lines[i].rstrip("\n")
|
||
if ":" in line:
|
||
key, _, val = line.partition(":")
|
||
fm[key.strip()] = val.strip()
|
||
i += 1
|
||
if i >= len(lines):
|
||
raise ValueError("missing frontmatter close")
|
||
body = "".join(lines[i + 1:]).lstrip("\n")
|
||
return fm, body
|
||
|
||
|
||
def parse_tools(tools_csv: str) -> list[str]:
|
||
return [t.strip() for t in tools_csv.split(",") if t.strip()]
|
||
|
||
|
||
def codex_sandbox_mode(tools: list[str]) -> str:
|
||
return "workspace-write" if ({"Edit", "Write"} & set(tools)) else "read-only"
|
||
|
||
|
||
def antigravity_tool_names(tools: list[str]) -> list[str]:
|
||
names = list(_ANTIGRAV_BASELINE)
|
||
tset = set(tools)
|
||
for claude_tool, mapped in _ANTIGRAV_EXTRA_ORDER:
|
||
if claude_tool in tset:
|
||
names.extend(mapped)
|
||
return names
|
||
```
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -v`
|
||
Expected: PASS (6 tests).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/sync_automation.py scripts/test_sync_automation.py
|
||
git commit -m "feat(sync): frontmatter parse + permission derivation pure functions"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: Codex TOML 직렬화
|
||
|
||
**Files:**
|
||
- Modify: `scripts/sync_automation.py`
|
||
- Test: `scripts/test_sync_automation.py`
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```python
|
||
def test_codex_toml_is_valid_and_roundtrips():
|
||
body = "You are the Wiki Link Verifier.\nLine two with `backticks` and 'quotes'.\n"
|
||
out = s.render_codex_toml(
|
||
name="wiki-link-verifier",
|
||
description='Audit "the wiki" for orphans.',
|
||
sandbox_mode="read-only",
|
||
body=body,
|
||
)
|
||
parsed = tomllib.loads(out)
|
||
assert parsed["name"] == "wiki-link-verifier"
|
||
assert parsed["description"] == 'Audit "the wiki" for orphans.'
|
||
assert parsed["sandbox_mode"] == "read-only"
|
||
assert parsed["developer_instructions"].strip() == body.strip()
|
||
|
||
|
||
def test_codex_toml_rejects_triple_single_quote_body():
|
||
import pytest
|
||
with pytest.raises(ValueError):
|
||
s.render_codex_toml("n", "d", "read-only", "bad ''' body")
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k codex_toml -v`
|
||
Expected: FAIL with `AttributeError: module 'sync_automation' has no attribute 'render_codex_toml'`.
|
||
|
||
- [ ] **Step 3: Write minimal implementation**
|
||
|
||
```python
|
||
def _toml_basic_string(value: str) -> str:
|
||
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
||
return f'"{escaped}"'
|
||
|
||
|
||
def render_codex_toml(name: str, description: str, sandbox_mode: str, body: str) -> str:
|
||
if "'''" in body:
|
||
raise ValueError("body contains ''' which breaks TOML literal multiline string")
|
||
return (
|
||
f"name = {_toml_basic_string(name)}\n"
|
||
f"description = {_toml_basic_string(description)}\n"
|
||
f"sandbox_mode = {_toml_basic_string(sandbox_mode)}\n"
|
||
f"developer_instructions = '''\n"
|
||
f"{body.rstrip()}\n"
|
||
f"'''\n"
|
||
)
|
||
```
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k codex_toml -v`
|
||
Expected: PASS (2 tests).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/sync_automation.py scripts/test_sync_automation.py
|
||
git commit -m "feat(sync): codex TOML serialization with literal multiline body"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: Antigravity agent.json 직렬화
|
||
|
||
**Files:**
|
||
- Modify: `scripts/sync_automation.py`
|
||
- Test: `scripts/test_sync_automation.py`
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```python
|
||
def test_antigravity_agent_json_schema():
|
||
obj = s.build_antigravity_agent(
|
||
name="wiki-link-verifier",
|
||
description="Audit the wiki.",
|
||
body="You are the Wiki Link Verifier.\n",
|
||
tools=["Read", "Grep", "Glob", "Bash"],
|
||
)
|
||
assert obj["name"] == "wiki-link-verifier"
|
||
assert obj["description"] == "Audit the wiki."
|
||
assert obj["hidden"] is True
|
||
ca = obj["config"]["customAgent"]
|
||
assert ca["systemPromptSections"][0]["title"] == "Agent System Instructions"
|
||
assert ca["systemPromptSections"][0]["content"] == "You are the Wiki Link Verifier.\n"
|
||
assert "write_to_file" not in ca["toolNames"] # read-only agent
|
||
assert ca["toolNames"][0] == "send_message"
|
||
assert ca["systemPromptConfig"]["includeSections"] == s.ANTIGRAV_INCLUDE_SECTIONS
|
||
|
||
|
||
def test_antigravity_json_render_is_valid_json():
|
||
obj = s.build_antigravity_agent("n", "d", "body\n", ["Read", "Grep", "Glob"])
|
||
text = s.render_json(obj)
|
||
assert json.loads(text) == obj
|
||
assert text.endswith("\n")
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k antigravity_agent_json -v`
|
||
Expected: FAIL with `AttributeError: ... 'build_antigravity_agent'`.
|
||
|
||
- [ ] **Step 3: Write minimal implementation**
|
||
|
||
```python
|
||
def build_antigravity_agent(name: str, description: str, body: str, tools: list[str]) -> dict:
|
||
return {
|
||
"name": name,
|
||
"description": description,
|
||
"hidden": True,
|
||
"config": {
|
||
"customAgent": {
|
||
"systemPromptSections": [
|
||
{"title": "Agent System Instructions", "content": body}
|
||
],
|
||
"toolNames": antigravity_tool_names(tools),
|
||
"systemPromptConfig": {"includeSections": list(ANTIGRAV_INCLUDE_SECTIONS)},
|
||
}
|
||
},
|
||
}
|
||
|
||
|
||
def render_json(obj: dict) -> str:
|
||
return json.dumps(obj, indent=2, ensure_ascii=False) + "\n"
|
||
```
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k antigravity_agent_json -v`
|
||
Expected: PASS (2 tests).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/sync_automation.py scripts/test_sync_automation.py
|
||
git commit -m "feat(sync): antigravity agent.json builder + stable JSON render"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: CLI 배선 (generate / check) + 기존 7개 검증
|
||
|
||
**Files:**
|
||
- Modify: `scripts/sync_automation.py`
|
||
- Test: `scripts/test_sync_automation.py`
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
```python
|
||
def test_load_agent_inputs_for_existing_agent():
|
||
inp = s.load_agent_inputs("wiki-link-verifier")
|
||
assert inp.tools == ["Read", "Grep", "Glob", "Bash"]
|
||
assert inp.codex_description
|
||
assert inp.antigrav_description
|
||
assert "Wiki Link Verifier" in inp.codex_body
|
||
assert inp.antigrav_body
|
||
|
||
|
||
def test_generate_one_writes_both_outputs(tmp_path, monkeypatch):
|
||
monkeypatch.setattr(s, "CODEX_AGENTS", tmp_path / "codex")
|
||
monkeypatch.setattr(s, "ANTIGRAV_OUT", tmp_path / "antigrav")
|
||
(tmp_path / "codex").mkdir()
|
||
s.generate_one("wiki-link-verifier", check=False)
|
||
toml_path = tmp_path / "codex" / "wiki-link-verifier.toml"
|
||
json_path = tmp_path / "antigrav" / "wiki-link-verifier" / "agent.json"
|
||
assert toml_path.exists()
|
||
assert json_path.exists()
|
||
assert tomllib.loads(toml_path.read_text())["sandbox_mode"] == "read-only"
|
||
assert json.loads(json_path.read_text())["hidden"] is True
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k "load_agent_inputs or generate_one" -v`
|
||
Expected: FAIL with `AttributeError: ... 'load_agent_inputs'`.
|
||
|
||
- [ ] **Step 3: Write minimal implementation**
|
||
|
||
```python
|
||
from dataclasses import dataclass
|
||
|
||
|
||
@dataclass
|
||
class AgentInputs:
|
||
name: str
|
||
tools: list[str]
|
||
codex_description: str
|
||
codex_body: str
|
||
antigrav_description: str
|
||
antigrav_body: str
|
||
|
||
|
||
def load_agent_inputs(name: str) -> AgentInputs:
|
||
claude_fm, _ = parse_frontmatter((CLAUDE_AGENTS / f"{name}.md").read_text())
|
||
codex_fm, codex_body = parse_frontmatter((CODEX_AGENTS / f"{name}.md").read_text())
|
||
ag_fm, ag_body = parse_frontmatter((ANTIGRAV_SSOT / f"{name}.md").read_text())
|
||
return AgentInputs(
|
||
name=name,
|
||
tools=parse_tools(claude_fm["tools"]),
|
||
codex_description=codex_fm["description"],
|
||
codex_body=codex_body,
|
||
antigrav_description=ag_fm["description"],
|
||
antigrav_body=ag_body,
|
||
)
|
||
|
||
|
||
def _write_or_check(path: Path, content: str, check: bool, drift: list[str]) -> None:
|
||
if check:
|
||
current = path.read_text() if path.exists() else None
|
||
if current != content:
|
||
drift.append(str(path.relative_to(REPO)))
|
||
else:
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text(content)
|
||
|
||
|
||
def generate_one(name: str, check: bool, drift: list[str] | None = None) -> None:
|
||
drift = drift if drift is not None else []
|
||
inp = load_agent_inputs(name)
|
||
toml_text = render_codex_toml(
|
||
inp.name, inp.codex_description, codex_sandbox_mode(inp.tools), inp.codex_body
|
||
)
|
||
json_text = render_json(
|
||
build_antigravity_agent(inp.name, inp.antigrav_description, inp.antigrav_body, inp.tools)
|
||
)
|
||
_write_or_check(CODEX_AGENTS / f"{name}.toml", toml_text, check, drift)
|
||
_write_or_check(ANTIGRAV_OUT / name / "agent.json", json_text, check, drift)
|
||
|
||
|
||
def main(argv: list[str] | None = None) -> int:
|
||
ap = argparse.ArgumentParser(description="Sync Claude agents -> Codex/Antigravity native files")
|
||
ap.add_argument("target", choices=["agents"], help="what to sync")
|
||
ap.add_argument("--check", action="store_true", help="fail (exit 2) if outputs drift from SSOT")
|
||
ap.add_argument("--only", help="single agent name (default: all)")
|
||
args = ap.parse_args(argv)
|
||
|
||
names = [args.only] if args.only else AGENT_NAMES
|
||
drift: list[str] = []
|
||
for name in names:
|
||
generate_one(name, check=args.check, drift=drift)
|
||
if args.check and drift:
|
||
print("DRIFT detected in:\n " + "\n ".join(drift), file=sys.stderr)
|
||
return 2
|
||
action = "checked" if args.check else "generated"
|
||
print(f"{action} {len(names)} agents (codex .toml + antigravity agent.json)")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|
||
```
|
||
|
||
- [ ] **Step 4: Run unit tests (still only 7 agents exist, so use --only)**
|
||
|
||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -v`
|
||
Expected: PASS (all tests). `load_agent_inputs`/`generate_one` use `wiki-link-verifier` which exists.
|
||
|
||
- [ ] **Step 5: Smoke-run on one existing agent**
|
||
|
||
Run:
|
||
```bash
|
||
cd /home/donghyeon/dev/llm-wiki-private
|
||
python3 scripts/sync_automation.py agents --only wiki-link-verifier
|
||
python3 -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('.codex/agents/wiki-link-verifier.toml').read_text())['sandbox_mode'])"
|
||
python3 -c "import json,pathlib; o=json.loads(pathlib.Path('.agents/agents/wiki-link-verifier/agent.json').read_text()); print(o['hidden'], 'write_to_file' in o['config']['customAgent']['toolNames'])"
|
||
```
|
||
Expected output:
|
||
```
|
||
generated 1 agents (codex .toml + antigravity agent.json)
|
||
read-only
|
||
True False
|
||
```
|
||
(`True False` = hidden true + write_to_file absent for read-only agent — confirms the stale-disk bug is fixed.)
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add scripts/sync_automation.py scripts/test_sync_automation.py .codex/agents/wiki-link-verifier.toml .agents/agents/wiki-link-verifier/agent.json
|
||
git commit -m "feat(sync): CLI generate/check + AgentInputs loader; verify on link-verifier"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: 누락 agent 2개의 codex SSOT 본문 작성 (`branch-depth-auditor`, `coverage-auditor`)
|
||
|
||
> codex 본문 = Claude 본문의 일반화: frontmatter에서 `tools:`/`model:` 제거(name+description만 유지), 본문의 `Read tool`/`Edit tool`/`Write tool`/`Bash tool` → `Read`/`Edit`/`Write`/`shell`, `CLAUDE.md` 단독 언급 → `CLAUDE.md (또는 AGENTS.md)`.
|
||
|
||
**Files:**
|
||
- Create: `.codex/agents/branch-depth-auditor.md`
|
||
- Create: `.codex/agents/coverage-auditor.md`
|
||
|
||
- [ ] **Step 1: Learn the generalization convention**
|
||
|
||
Read these two pairs side by side to learn the exact convention:
|
||
- `.claude/agents/wiki-link-verifier.md` vs `.codex/agents/wiki-link-verifier.md`
|
||
- Then Read `.claude/agents/branch-depth-auditor.md` and `.claude/agents/coverage-auditor.md` in full.
|
||
|
||
- [ ] **Step 2: Create `.codex/agents/branch-depth-auditor.md`**
|
||
|
||
Structure:
|
||
```markdown
|
||
---
|
||
name: branch-depth-auditor
|
||
description: <verbatim copy of the description line from .claude/agents/branch-depth-auditor.md>
|
||
---
|
||
|
||
<Claude body copied verbatim, then these substitutions applied:
|
||
"Read tool"->"Read", "Grep tool"->"Grep", "Glob tool"->"Glob", "Bash tool"->"shell",
|
||
"Edit tool"->"Edit", "Write tool"->"Write";
|
||
standalone "CLAUDE.md" reference -> "CLAUDE.md (또는 AGENTS.md)".
|
||
Leave all rules/ and templates/ references, axes, and depth-ladder logic byte-identical.>
|
||
```
|
||
|
||
- [ ] **Step 3: Create `.codex/agents/coverage-auditor.md`** identically (verbatim description + generalized body).
|
||
|
||
- [ ] **Step 4: Verify frontmatter parses for both**
|
||
|
||
Run:
|
||
```bash
|
||
cd /home/donghyeon/dev/llm-wiki-private/scripts && python3 -c "
|
||
import sync_automation as s
|
||
for n in ['branch-depth-auditor','coverage-auditor']:
|
||
fm,body=s.parse_frontmatter(open(f'../.codex/agents/{n}.md').read())
|
||
assert fm['name']==n and fm['description'] and body.strip(), n
|
||
assert 'tool' not in fm and 'model' not in fm, 'frontmatter must be name+description only'
|
||
print('ok', n)
|
||
"
|
||
```
|
||
Expected:
|
||
```
|
||
ok branch-depth-auditor
|
||
ok coverage-auditor
|
||
```
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add .codex/agents/branch-depth-auditor.md .codex/agents/coverage-auditor.md
|
||
git commit -m "feat(codex): add branch-depth-auditor + coverage-auditor SSOT bodies"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: 누락 agent 2개의 antigravity gated SSOT 본문 작성
|
||
|
||
> antigravity 본문 = codex 본문 + G1~G4 Gemini hard gate. 기존 read-only gated agent를 템플릿으로 사용한다. tool 참조는 antigravity 어휘(`view_file`/`grep_search`/`run_command`)로 쓴다.
|
||
|
||
**Files:**
|
||
- Create: `.agents/plugins/wiki-superpowers/agents/branch-depth-auditor.md`
|
||
- Create: `.agents/plugins/wiki-superpowers/agents/coverage-auditor.md`
|
||
|
||
- [ ] **Step 1: Study the gate template**
|
||
|
||
Read `.agents/plugins/wiki-superpowers/agents/wiki-link-verifier.md` in full (read-only agent — closest analog). Extract the exact section structure of: G1 Pre-Read Proof, G2 Post-Write Validator, G3 Output Schema + V Counter, G4 Enumerated STOP Conditions. Also read `.agents/plugins/wiki-superpowers/agents/wiki-research-lane.md` if a longer analytical example helps.
|
||
|
||
- [ ] **Step 2: Create `.agents/plugins/wiki-superpowers/agents/branch-depth-auditor.md`**
|
||
|
||
```markdown
|
||
---
|
||
name: branch-depth-auditor
|
||
description: <verbatim copy from .claude/agents/branch-depth-auditor.md frontmatter>
|
||
---
|
||
|
||
<body = the codex branch-depth-auditor body content (audit axes + L0~L3 depth ladder
|
||
+ Ready/Not-ready verdict logic), PLUS the four gates:
|
||
- G1 Pre-Read Proof: table quoting the first line of each Mandatory First Read
|
||
(CLAUDE.md/AGENTS.md, rules/branch-depth-gate.md, the target branch note).
|
||
- G2 Post-Write Validator: read-only -> state "read-only agent, no file writes;
|
||
verification is the grep/sed verbatim-quote proofs inside the gap report (G3 V counter)".
|
||
- G3 Output Schema + V Counter: {{ }} placeholder schema for the Ready/Not-ready
|
||
verdict + per-axis gap rows; V = number of quote-grep commands actually run.
|
||
- G4 Enumerated STOP Conditions: numbered list -> return NEEDS_CONTEXT
|
||
(1: branch-note path missing/unreadable; 2: linked raw sources unreadable;
|
||
3: wiki_structure_lint not yet passed; 4: target is not a feature-*.md branch note).
|
||
Use Antigravity tool names in any tool reference: view_file, grep_search, run_command.>
|
||
```
|
||
Mirror the axes/ladder language from `.claude/agents/branch-depth-auditor.md`; do NOT invent new audit criteria.
|
||
|
||
- [ ] **Step 3: Create `.agents/plugins/wiki-superpowers/agents/coverage-auditor.md`**
|
||
|
||
Same structure. Coverage = completeness (covered-here / delegated / missing per required concern) with a 3-tier verdict and optional project mode. G4 STOP conditions: governing_docs missing, `## Coverage` section absent, links unresolved, target not a branch note. tool refs use `run_command` for grep-based concern classification + `view_file` for governing docs.
|
||
|
||
- [ ] **Step 4: Verify frontmatter parses + all four gates present**
|
||
|
||
Run:
|
||
```bash
|
||
cd /home/donghyeon/dev/llm-wiki-private
|
||
for n in branch-depth-auditor coverage-auditor; do
|
||
f=.agents/plugins/wiki-superpowers/agents/$n.md
|
||
echo "== $n =="
|
||
grep -c "G1 Pre-Read Proof" "$f"
|
||
grep -c "G4" "$f"
|
||
done
|
||
```
|
||
Expected: each `grep -c` prints `1` or more (gates present). If `0`, the gate section is missing — add it.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add .agents/plugins/wiki-superpowers/agents/branch-depth-auditor.md .agents/plugins/wiki-superpowers/agents/coverage-auditor.md
|
||
git commit -m "feat(antigravity): add branch-depth-auditor + coverage-auditor gated SSOT bodies"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: 전체 9개 생성 + 스키마 검증 + drift 0 확인
|
||
|
||
**Files:**
|
||
- Generate: `.codex/agents/*.toml` ×9, `.agents/agents/<name>/agent.json` ×9
|
||
- Test: `scripts/test_sync_automation.py` (add a full-suite generation guard)
|
||
|
||
- [ ] **Step 1: Add a test asserting all 9 generate and pass schema checks**
|
||
|
||
```python
|
||
def test_all_nine_agents_generate_valid_artifacts(tmp_path, monkeypatch):
|
||
monkeypatch.setattr(s, "CODEX_AGENTS", tmp_path / "codex")
|
||
monkeypatch.setattr(s, "ANTIGRAV_OUT", tmp_path / "antigrav")
|
||
# copy SSOT bodies the generator reads from real repo (CODEX_AGENTS is also the source dir),
|
||
# so point the source dir explicitly: regenerate using real source via a fresh load.
|
||
(tmp_path / "codex").mkdir()
|
||
# NOTE: generate_one reads codex body from s.CODEX_AGENTS; for this test we only verify
|
||
# the antigravity side (json) which reads from ANTIGRAV_SSOT (unchanged real dir).
|
||
for name in s.AGENT_NAMES:
|
||
obj = s.build_antigravity_agent(
|
||
name, "d",
|
||
s.parse_frontmatter((s.ANTIGRAV_SSOT / f"{name}.md").read_text())[1],
|
||
s.parse_tools(s.parse_frontmatter((s.CLAUDE_AGENTS / f"{name}.md").read_text())[0]["tools"]),
|
||
)
|
||
assert json.loads(s.render_json(obj))["name"] == name
|
||
# read-only agents must NOT carry write tools
|
||
ro = {"branch-depth-auditor", "coverage-auditor", "wiki-adversarial-reviewer",
|
||
"wiki-diagram-reviewer", "wiki-link-verifier", "wiki-research-lane"}
|
||
if name in ro:
|
||
assert "write_to_file" not in obj["config"]["customAgent"]["toolNames"], name
|
||
```
|
||
|
||
> 위 테스트는 `CODEX_AGENTS`/`ANTIGRAV_SSOT`의 9개 `.md`가 모두 존재해야 통과한다(Task 5·6 완료 후). codex toml 본문은 실 디렉토리에서 직접 읽으므로 통합 스모크(Step 3)로 검증한다.
|
||
|
||
- [ ] **Step 2: Run the unit suite**
|
||
|
||
Run: `cd scripts && python3 -m pytest test_sync_automation.py -v`
|
||
Expected: PASS (all, including the new 9-agent guard).
|
||
|
||
- [ ] **Step 3: Generate all 9 and validate every artifact**
|
||
|
||
Run:
|
||
```bash
|
||
cd /home/donghyeon/dev/llm-wiki-private
|
||
python3 scripts/sync_automation.py agents
|
||
echo "--- validate codex toml (9) ---"
|
||
python3 -c "
|
||
import tomllib, pathlib
|
||
ns=['branch-depth-auditor','coverage-auditor','wiki-adversarial-reviewer','wiki-decision-researcher','wiki-diagram-reviewer','wiki-doc-author','wiki-link-verifier','wiki-research-lane','wiki-source-summarizer']
|
||
for n in ns:
|
||
o=tomllib.loads(pathlib.Path(f'.codex/agents/{n}.toml').read_text())
|
||
assert o['name']==n and o['sandbox_mode'] in ('read-only','workspace-write') and o['developer_instructions'].strip()
|
||
print('codex toml ok:', len(ns))
|
||
"
|
||
echo "--- validate antigravity json (9) ---"
|
||
python3 -c "
|
||
import json, pathlib
|
||
ns=['branch-depth-auditor','coverage-auditor','wiki-adversarial-reviewer','wiki-decision-researcher','wiki-diagram-reviewer','wiki-doc-author','wiki-link-verifier','wiki-research-lane','wiki-source-summarizer']
|
||
for n in ns:
|
||
o=json.loads(pathlib.Path(f'.agents/agents/{n}/agent.json').read_text())
|
||
ca=o['config']['customAgent']
|
||
assert o['name']==n and o['hidden'] is True
|
||
assert ca['systemPromptSections'][0]['content'].strip()
|
||
assert ca['toolNames'][0]=='send_message'
|
||
print('antigravity json ok:', len(ns))
|
||
"
|
||
```
|
||
Expected:
|
||
```
|
||
generated 9 agents (codex .toml + antigravity agent.json)
|
||
--- validate codex toml (9) ---
|
||
codex toml ok: 9
|
||
--- validate antigravity json (9) ---
|
||
antigravity json ok: 9
|
||
```
|
||
|
||
- [ ] **Step 4: Confirm idempotency / drift-0**
|
||
|
||
Run:
|
||
```bash
|
||
cd /home/donghyeon/dev/llm-wiki-private
|
||
python3 scripts/sync_automation.py agents --check && echo "DRIFT-FREE"
|
||
```
|
||
Expected:
|
||
```
|
||
checked 9 agents (codex .toml + antigravity agent.json)
|
||
DRIFT-FREE
|
||
```
|
||
(exit 0). If it prints `DRIFT detected`, re-run without `--check` and re-commit the generated files.
|
||
|
||
- [ ] **Step 5: Commit generated artifacts**
|
||
|
||
```bash
|
||
cd /home/donghyeon/dev/llm-wiki-private
|
||
git add scripts/test_sync_automation.py .codex/agents/*.toml .agents/agents/
|
||
git commit -m "feat(sync): generate all 9 agents for codex (.toml) + antigravity (agent.json)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: 생성 스크립트 사용법 문서 1단락 (codex + antigravity README)
|
||
|
||
> 전체 문서 정리(CLAUDE.md의 구식 '수동 cat' 서술 제거 등)는 Phase 2. 여기서는 Phase 0가 만든 스크립트/산출물이 고아가 되지 않도록 최소 포인터만 추가한다.
|
||
|
||
**Files:**
|
||
- Modify: `.codex/agents/README.md`
|
||
- Modify: `.agents/plugins/wiki-superpowers/README.md`
|
||
|
||
- [ ] **Step 1: Add a "Native generation" note to `.codex/agents/README.md`**
|
||
|
||
기존 README 상단(또는 "Pattern" 섹션 위)에 다음 취지의 1단락 추가 (정확한 문구는 기존 톤에 맞춰 작성):
|
||
- codex는 이제 native subagent를 `.codex/agents/*.toml`로 자동 등록한다(`developer_instructions`+`sandbox_mode`). `.md`는 사람이 편집하는 SSOT이고 `.toml`은 `python3 scripts/sync_automation.py agents`로 생성된다.
|
||
- `.md` 편집 후 반드시 `sync_automation.py agents`를 다시 돌려야 `.toml`에 반영된다. CI/hook에서는 `--check`로 drift를 검사한다.
|
||
|
||
- [ ] **Step 2: Add the same note to `.agents/plugins/wiki-superpowers/README.md`**
|
||
|
||
기존 "⚠️ Loading model" 박스의 "Sync command" 줄을 실제 스크립트로 교체:
|
||
- 구: `python3 .agents/scripts/convert-wiki-agents.py` (존재하지 않음)
|
||
- 신: `python3 scripts/sync_automation.py agents` — `.agents/plugins/.../agents/*.md`(gated SSOT) + `.claude/agents/*.md`(권한 SSOT) → `.agents/agents/<name>/agent.json` 생성. `--check`로 drift 검사.
|
||
|
||
- [ ] **Step 3: Verify the dead reference is gone**
|
||
|
||
Run:
|
||
```bash
|
||
cd /home/donghyeon/dev/llm-wiki-private
|
||
grep -rn "convert-wiki-agents.py" .agents/ .codex/ CLAUDE.md || echo "no dead reference remaining in README scope"
|
||
```
|
||
Expected: `no dead reference remaining in README scope` (CLAUDE.md 본문의 언급은 Phase 2에서 처리하므로, 여기서 grep이 CLAUDE.md만 남기면 그 줄은 Phase 2 TODO로 남겨도 됨 — 단 README 2개에는 남지 않아야 함).
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add .codex/agents/README.md .agents/plugins/wiki-superpowers/README.md
|
||
git commit -m "docs(sync): point READMEs at scripts/sync_automation.py (replaces missing convert script)"
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 0 완료 기준 (Definition of Done)
|
||
|
||
- `scripts/sync_automation.py` + 테스트 통과(pytest green).
|
||
- `.codex/agents/*.toml` 9개 + `.agents/agents/<name>/agent.json` 9개 생성·검증.
|
||
- `sync_automation.py agents --check` 가 exit 0 (drift 0).
|
||
- 누락 2개 agent(branch-depth-auditor·coverage-auditor)가 codex·antigravity 양쪽 SSOT + 생성물에 존재.
|
||
- README 2개가 실제 생성 스크립트를 가리킴.
|
||
- 검증된 부채 수정: read-only agent의 `agent.json`에 write 도구 없음(stale 디스크 버그 해소).
|
||
|
||
이후: **Phase 1**(commands → codex `.agents/skills/` + antigravity `.agents/workflows/`)는 별도 plan으로 작성한다. Phase 0의 `sync_automation.py`에 `commands` 타깃을 확장한다.
|
||
|
||
---
|
||
|
||
## Self-Review (작성자 체크)
|
||
|
||
- **Spec coverage**: 설계 §4 Phase 0(엔진+agents)의 모든 항목 — 생성 엔진, 9개 agent 생성, 누락 2개 추가, antigravity 로딩 경로 복구(agent.json+스크립트), codex toml 현대화 — 각각 Task 1~8에 매핑됨. Phase 1·2는 범위 밖(별도 plan).
|
||
- **Placeholder scan**: 코드 스텝은 실제 코드 포함. Task 5·6의 본문 작성은 "기존 파일을 템플릿으로 verbatim 적응"이라는 결정론적 절차 + 검증 grep을 제공(프로즈 자체는 SSOT 적응이라 코드처럼 박제 불가하나, 입력 파일·치환 규칙·검증 명령을 명시).
|
||
- **Type consistency**: 함수 시그니처 일관 — `parse_frontmatter`→`(fm,body)`, `antigravity_tool_names(tools)`, `render_codex_toml(name,description,sandbox_mode,body)`, `build_antigravity_agent(name,description,body,tools)`, `generate_one(name,check,drift)`. 모든 Task에서 동일 이름 사용. 모듈명 `sync_automation`(언더스코어)로 통일.
|
||
- **알려진 한계**: Task 7 Step 1 테스트는 codex toml 본문을 실디렉토리에서 읽어 통합 스모크(Step 3)로 보완. Task 6 Step 4의 grep 카운트는 게이트 "존재"만 보장하고 의미적 정확성은 보장하지 않음 → 실제 antigravity 실행 스모크는 Phase 1 검증과 함께 수행 권장.
|