modify gemini template
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user