modify gemini template

This commit is contained in:
NINI
2026-04-28 01:51:20 +09:00
parent 949e0ab13c
commit 38291723f0
32 changed files with 523 additions and 320 deletions
+37 -36
View File
@@ -1,15 +1,14 @@
#!/usr/bin/env python3
"""
Codex Harness Step Executor — phase 내 step을 순차 실행하고 자가 교정한다.
Gemini Harness Step Executor — phase 내 step을 순차 실행하고 자가 교정한다.
Usage:
python3 scripts/execute.py <phase-dir> [--push]
python scripts/execute.py <phase-dir> [--push]
"""
import argparse
import contextlib
import json
import os
import subprocess
import sys
import threading
@@ -51,7 +50,7 @@ def progress_indicator(label: str):
class StepExecutor:
"""Phase 디렉토리 안의 step들을 Codex로 순차 실행하는 하네스."""
"""Phase 디렉토리 안의 step들을 Gemini CLI로 순차 실행하는 하네스."""
MAX_RETRIES = 3
FEAT_MSG = "feat({phase}): step {num}{name}"
@@ -115,7 +114,7 @@ class StepExecutor:
r = self._run_git("rev-parse", "--abbrev-ref", "HEAD")
if r.returncode != 0:
print(f" ERROR: git을 사용할 수 없거나 git repo가 아닙니다.")
print(" ERROR: git을 사용할 수 없거나 git repo가 아닙니다.")
print(f" {r.stderr.strip()}")
sys.exit(1)
@@ -128,7 +127,7 @@ class StepExecutor:
if r.returncode != 0:
print(f" ERROR: 브랜치 '{branch}' checkout 실패.")
print(f" {r.stderr.strip()}")
print(f" Hint: 변경사항을 stash하거나 commit한 후 다시 시도하세요.")
print(" Hint: 변경사항을 stash하거나 commit한 후 다시 시도하세요.")
sys.exit(1)
print(f" Branch: {branch}")
@@ -176,9 +175,9 @@ class StepExecutor:
def _load_guardrails(self) -> str:
sections = []
agents_md = ROOT / "AGENTS.md"
if agents_md.exists():
sections.append(f"## 프로젝트 규칙 (AGENTS.md)\n\n{agents_md.read_text(encoding='utf-8')}")
gemini_md = ROOT / "GEMINI.md"
if gemini_md.exists():
sections.append(f"## 프로젝트 규칙 (GEMINI.md)\n\n{gemini_md.read_text(encoding='utf-8')}")
docs_dir = ROOT / "docs"
if docs_dir.is_dir():
for doc in sorted(docs_dir.glob("*.md")):
@@ -201,29 +200,29 @@ class StepExecutor:
retry_section = ""
if prev_error:
retry_section = (
f"\n## 이전 시도 실패 — 아래 에러를 반드시 참고하여 수정하라\n\n"
"\n## 이전 시도 실패 — 아래 에러를 반드시 참고하여 수정하라\n\n"
f"{prev_error}\n\n---\n\n"
)
return (
f"당신은 {self._project} 프로젝트의 Codex 문서 작성 에이전트입니다. 아래 step을 수행하세요.\n\n"
f"당신은 {self._project} 프로젝트의 Gemini CLI 문서 작성 에이전트입니다. 아래 step을 수행하세요.\n\n"
f"{guardrails}\n\n---\n\n"
f"{step_context}{retry_section}"
f"## 작업 규칙\n\n"
f"1. 이전 step에서 작성된 문서와 메모를 확인하고 일관성을 유지하라.\n"
f"2. 이 step에 명시된 작업만 수행하라. 추가 산출물이나 임의 요구사항을 만들지 마라.\n"
f"3. 기존 문서 구조와 피드백 기록을 깨뜨리지 마라.\n"
f"4. AC(Acceptance Criteria) 검증을 직접 실행하라.\n"
"## 작업 규칙\n\n"
"1. 이전 step에서 작성된 문서와 메모를 확인하고 일관성을 유지하라.\n"
"2. 이 step에 명시된 작업만 수행하라. 추가 산출물이나 임의 요구사항을 만들지 마라.\n"
"3. 기존 문서 구조와 피드백 기록을 깨뜨리지 마라.\n"
"4. AC(Acceptance Criteria) 검증을 직접 실행하라.\n"
f"5. /phases/{self._phase_dir_name}/index.json의 해당 step status를 업데이트하라:\n"
f" - AC 통과 \"completed\" + \"summary\" 필드에 이 step의 산출물을 한 줄로 요약\n"
f" - {self.MAX_RETRIES}회 수정 시도 후에도 실패 \"error\" + \"error_message\" 기록\n"
f" - 사용자 개입이 필요한 경우 (API 키, 인증, 수동 설정 등) → \"blocked\" + \"blocked_reason\" 기록 후 즉시 중단\n"
f"6. 직접 git commit하지 마라. commit은 scripts/execute.py가 step 완료 후 수행한다.\n"
f"7. 병렬 조사나 독립 리뷰가 필요하고 step에서 허용했다면 .codex/agents의 custom agent 역할을 활용하라.\n\n---\n\n"
" - AC 통과 -> \"completed\" + \"summary\" 필드에 이 step의 산출물을 한 줄로 요약\n"
f" - {self.MAX_RETRIES}회 수정 시도 후에도 실패 -> \"error\" + \"error_message\" 기록\n"
" - 사용자 개입이 필요한 경우 -> \"blocked\" + \"blocked_reason\" 기록 후 즉시 중단\n"
"6. 직접 git commit하지 마라. commit은 scripts/execute.py가 step 완료 후 수행한다.\n"
"7. 병렬 조사나 독립 리뷰가 필요하고 step에서 허용했다면 .gemini/agents의 subagent 역할을 활용하라.\n\n---\n\n"
)
# --- Codex 호출 ---
# --- Gemini CLI invocation ---
def _invoke_codex(self, step: dict, preamble: str) -> dict:
def _invoke_gemini(self, step: dict, preamble: str) -> dict:
step_num, step_name = step["step"], step["name"]
step_file = self._phase_dir / f"step{step_num}.md"
@@ -232,7 +231,7 @@ class StepExecutor:
sys.exit(1)
prompt = preamble + step_file.read_text(encoding="utf-8")
cmd = ["codex", "exec", "--skip-git-repo-check", "--full-auto", "--json", "-"]
cmd = ["gemini", "--output-format", "json", "--approval-mode", "yolo"]
try:
result = subprocess.run(
@@ -246,18 +245,20 @@ class StepExecutor:
timeout=1800,
)
except FileNotFoundError:
print("\n ERROR: Codex CLI를 찾을 수 없습니다. `codex --version`이 실행되는지 확인하세요.")
print("\n ERROR: Gemini CLI를 찾을 수 없습니다. `gemini --version`이 실행되는지 확인하세요.")
sys.exit(1)
if result.returncode != 0:
print(f"\n WARN: Codex가 비정상 종료됨 (code {result.returncode})")
print(f"\n WARN: Gemini CLI가 비정상 종료됨 (code {result.returncode})")
if result.stderr:
print(f" stderr: {result.stderr[:500]}")
output = {
"step": step_num, "name": step_name,
"step": step_num,
"name": step_name,
"exitCode": result.returncode,
"stdout": result.stdout, "stderr": result.stderr,
"stdout": result.stdout,
"stderr": result.stderr,
}
out_path = self._phase_dir / f"step{step_num}-output.json"
with open(out_path, "w", encoding="utf-8") as f:
@@ -269,10 +270,10 @@ class StepExecutor:
def _print_header(self):
print(f"\n{'='*60}")
print(f" Codex Harness Step Executor")
print(" Gemini Harness Step Executor")
print(f" Phase: {self._phase_name} | Steps: {self._total}")
if self._auto_push:
print(f" Auto-push: enabled")
print(" Auto-push: enabled")
print(f"{'='*60}")
def _check_blockers(self):
@@ -281,12 +282,12 @@ class StepExecutor:
if s["status"] == "error":
print(f"\n ✗ Step {s['step']} ({s['name']}) failed.")
print(f" Error: {s.get('error_message', 'unknown')}")
print(f" Fix and reset status to 'pending' to retry.")
print(" Fix and reset status to 'pending' to retry.")
sys.exit(1)
if s["status"] == "blocked":
print(f"\n ⏸ Step {s['step']} ({s['name']}) blocked.")
print(f" Reason: {s.get('blocked_reason', 'unknown')}")
print(f" Resolve and reset status to 'pending' to retry.")
print(" Resolve and reset status to 'pending' to retry.")
sys.exit(2)
if s["status"] != "pending":
break
@@ -315,7 +316,7 @@ class StepExecutor:
tag += f" [retry {attempt}/{self.MAX_RETRIES}]"
with progress_indicator(tag) as pi:
self._invoke_codex(step, preamble)
self._invoke_gemini(step, preamble)
elapsed = int(pi.elapsed)
index = self._read_json(self._index_file)
@@ -368,7 +369,7 @@ class StepExecutor:
self._update_top_index("error")
sys.exit(1)
return False # unreachable
return False
def _execute_all_steps(self, guardrails: str):
while True:
@@ -414,8 +415,8 @@ class StepExecutor:
def main():
parser = argparse.ArgumentParser(description="Codex Harness Step Executor")
parser.add_argument("phase_dir", help="Phase directory name (e.g. 0-mvp)")
parser = argparse.ArgumentParser(description="Gemini Harness Step Executor")
parser.add_argument("phase_dir", help="Phase directory name (e.g. 0-document)")
parser.add_argument("--push", action="store_true", help="Push branch after completion")
args = parser.parse_args()
+19 -19
View File
@@ -24,12 +24,12 @@ import execute as ex
@pytest.fixture
def tmp_project(tmp_path):
"""phases/, AGENTS.md, docs/ 를 갖춘 임시 프로젝트 구조."""
"""phases/, GEMINI.md, docs/ 를 갖춘 임시 프로젝트 구조."""
phases_dir = tmp_path / "phases"
phases_dir.mkdir()
agents_md = tmp_path / "AGENTS.md"
agents_md.write_text("# Rules\n- rule one\n- rule two", encoding="utf-8")
gemini_md = tmp_path / "GEMINI.md"
gemini_md.write_text("# Rules\n- rule one\n- rule two", encoding="utf-8")
docs_dir = tmp_path / "docs"
docs_dir.mkdir()
@@ -146,7 +146,7 @@ class TestJsonHelpers:
# ---------------------------------------------------------------------------
class TestLoadGuardrails:
def test_loads_agents_md_and_docs(self, executor, tmp_project):
def test_loads_gemini_md_and_docs(self, executor, tmp_project):
with patch.object(ex, "ROOT", tmp_project):
result = executor._load_guardrails()
assert "# Rules" in result
@@ -166,11 +166,11 @@ class TestLoadGuardrails:
guide_pos = result.index("guide")
assert arch_pos < guide_pos
def test_no_agents_md(self, executor, tmp_project):
(tmp_project / "AGENTS.md").unlink()
def test_no_gemini_md(self, executor, tmp_project):
(tmp_project / "GEMINI.md").unlink()
with patch.object(ex, "ROOT", tmp_project):
result = executor._load_guardrails()
assert "AGENTS.md" not in result
assert "GEMINI.md" not in result
assert "Architecture" in result
def test_no_docs_dir(self, executor, tmp_project):
@@ -420,24 +420,24 @@ class TestCommitStep:
# ---------------------------------------------------------------------------
# _invoke_codex (mocked)
# _invoke_gemini (mocked)
# ---------------------------------------------------------------------------
class TestInvokeCodex:
def test_invokes_codex_with_correct_args(self, executor):
class TestInvokeGemini:
def test_invokes_gemini_with_correct_args(self, executor):
mock_result = MagicMock(returncode=0, stdout='{"result": "ok"}', stderr="")
step = {"step": 2, "name": "ui"}
preamble = "PREAMBLE\n"
with patch("subprocess.run", return_value=mock_result) as mock_run:
output = executor._invoke_codex(step, preamble)
output = executor._invoke_gemini(step, preamble)
cmd = mock_run.call_args[0][0]
assert cmd[:2] == ["codex", "exec"]
assert "--skip-git-repo-check" in cmd
assert "--full-auto" in cmd
assert "--json" in cmd
assert cmd[-1] == "-"
assert cmd[0] == "gemini"
assert "--output-format" in cmd
assert "json" in cmd
assert "--approval-mode" in cmd
assert "yolo" in cmd
assert "PREAMBLE" in mock_run.call_args[1]["input"]
assert "UI를 구현하세요" in mock_run.call_args[1]["input"]
@@ -446,7 +446,7 @@ class TestInvokeCodex:
step = {"step": 2, "name": "ui"}
with patch("subprocess.run", return_value=mock_result):
executor._invoke_codex(step, "preamble")
executor._invoke_gemini(step, "preamble")
output_file = executor._phase_dir / "step2-output.json"
assert output_file.exists()
@@ -458,7 +458,7 @@ class TestInvokeCodex:
def test_nonexistent_step_file_exits(self, executor):
step = {"step": 99, "name": "nonexistent"}
with pytest.raises(SystemExit) as exc_info:
executor._invoke_codex(step, "preamble")
executor._invoke_gemini(step, "preamble")
assert exc_info.value.code == 1
def test_timeout_is_1800(self, executor):
@@ -466,7 +466,7 @@ class TestInvokeCodex:
step = {"step": 2, "name": "ui"}
with patch("subprocess.run", return_value=mock_result) as mock_run:
executor._invoke_codex(step, "preamble")
executor._invoke_gemini(step, "preamble")
assert mock_run.call_args[1]["timeout"] == 1800
+57 -19
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Basic validation for the Markdown document harness template.
Basic validation for the Gemini CLI Markdown document harness template.
This check is intentionally lightweight: it verifies that the template files
exist and keep the sections that later Harness steps depend on.
@@ -20,7 +20,7 @@ ROOT = Path(__file__).resolve().parent.parent
REQUIRED_FILES = [
"README.md",
"AGENTS.md",
"GEMINI.md",
"docs/PRD.md",
"docs/ResearchNote.md",
"docs/DraftFeedback.md",
@@ -31,14 +31,19 @@ REQUIRED_FILES = [
".agents/skills/document-harness/SKILL.md",
".agents/skills/document-harness/references/phase-templates.md",
".agents/skills/document-review/SKILL.md",
".codex/config.toml",
".codex/hooks.json",
".codex/hooks/pre_tool_guard.py",
".codex/hooks/stop_validate.py",
".codex/agents/doc_researcher.toml",
".codex/agents/doc_drafter.toml",
".codex/agents/doc_reviewer.toml",
".codex/agents/evidence_checker.toml",
".gemini/settings.json",
".gemini/hooks/pre_tool_guard.py",
".gemini/hooks/validate_docs_after_agent.py",
".gemini/agents/doc-researcher.md",
".gemini/agents/doc-drafter.md",
".gemini/agents/doc-reviewer.md",
".gemini/agents/evidence-checker.md",
".gemini/commands/harness/plan.toml",
".gemini/commands/harness/research.toml",
".gemini/commands/harness/draft.toml",
".gemini/commands/harness/final.toml",
".gemini/commands/harness/review.toml",
".gemini/commands/harness/status.toml",
]
REQUIRED_DIRS = [
@@ -48,18 +53,23 @@ REQUIRED_DIRS = [
".agents/skills",
".agents/skills/document-harness",
".agents/skills/document-review",
".codex",
".codex/hooks",
".codex/agents",
".gemini",
".gemini/commands",
".gemini/commands/harness",
".gemini/hooks",
".gemini/agents",
]
REQUIRED_SECTIONS = {
"README.md": [
"## 핵심 아이디어",
"## Codex 구성",
"## Gemini CLI 구성",
"## 빠른 시작",
"## 자동 실행 방식",
"## 피드백 게이트",
"## Gemini CLI Skills",
"## Gemini CLI Subagents",
"## Hooks",
"## 검증",
],
"docs/PRD.md": [
@@ -83,15 +93,23 @@ REQUIRED_SECTIONS = {
"## 쟁점과 상반된 주장",
"## 확인 필요",
],
"AGENTS.md": [
"GEMINI.md": [
"## 목적",
"## Codex 구성",
"## Gemini CLI 구성",
"## 기본 산출물",
"## 문서 작성 규칙",
"## Codex 작업 규칙",
"## Gemini CLI 작업 규칙",
"## 권장 워크플로우",
"## 명령어",
],
"docs/ARCHITECTURE.md": [
"## 디렉토리 구조",
"## 데이터 흐름",
"## Step 설계 패턴",
"## Gemini CLI 구성 책임",
"## 상태 관리",
"## 파일 책임",
],
".agents/skills/document-harness/SKILL.md": [
"# Document Harness Skill",
"## Operating Rules",
@@ -107,11 +125,24 @@ REQUIRED_SECTIONS = {
}
REQUIRED_JSON_FILES = [
".codex/hooks.json",
".gemini/settings.json",
]
REQUIRED_TOML_FILES = [
".gemini/commands/harness/plan.toml",
".gemini/commands/harness/research.toml",
".gemini/commands/harness/draft.toml",
".gemini/commands/harness/final.toml",
".gemini/commands/harness/review.toml",
".gemini/commands/harness/status.toml",
]
FORBIDDEN_FILES = [
"AGENTS.md",
".codex/config.toml",
".codex/hooks.json",
".codex/hooks/pre_tool_guard.py",
".codex/hooks/stop_validate.py",
".codex/agents/doc_researcher.toml",
".codex/agents/doc_drafter.toml",
".codex/agents/doc_reviewer.toml",
@@ -132,6 +163,8 @@ def markdown_file_has_valid_start(path: Path) -> bool:
return True
if first == "---" and path.name == "SKILL.md":
return True
if first == "---" and path.parent.name == "agents":
return True
return False
@@ -151,7 +184,12 @@ def main() -> int:
if path.suffix == ".md":
if not markdown_file_has_valid_start(path):
errors.append(f"markdown file must start with a level-1 heading or Skill frontmatter: {rel}")
errors.append(f"markdown file must start with a level-1 heading, Skill frontmatter, or agent frontmatter: {rel}")
for rel in FORBIDDEN_FILES:
path = ROOT / rel
if path.exists():
errors.append(f"forbidden legacy file remains: {rel}")
for rel, sections in REQUIRED_SECTIONS.items():
path = ROOT / rel