From a1aea6a449e73eab2715ab6a3d6142c6cff858a9 Mon Sep 17 00:00:00 2001 From: NINI Date: Sat, 9 May 2026 02:20:01 +0900 Subject: [PATCH] modify coding template --- Coding/Codex/.agents/plugins/marketplace.json | 20 - .../.agents/skills/harness-review/SKILL.md | 67 +-- .../skills/harness-review/agents/openai.yaml | 4 +- .../.agents/skills/harness-workflow/SKILL.md | 163 +++-- .../harness-workflow/agents/openai.yaml | 4 +- .../Codex/.codex/agents/harness-reviewer.toml | 11 - Coding/Codex/.codex/agents/phase-planner.toml | 12 - Coding/Codex/.codex/config.toml | 7 +- Coding/Codex/.codex/hooks.json | 18 +- .../pre_tool_use_policy.cpython-312.pyc | Bin 1562 -> 0 bytes .../__pycache__/stop_continue.cpython-312.pyc | Bin 2018 -> 0 bytes .../Codex/.codex/hooks/pre_commit_checks.py | 111 ++++ .../Codex/.codex/hooks/pre_tool_use_policy.py | 47 -- Coding/Codex/.codex/hooks/stop_continue.py | 55 -- Coding/Codex/.codex/hooks/tdd-guard.py | 189 ++++++ Coding/Codex/.gitignore | 9 + Coding/Codex/AGENTS.md | 31 +- Coding/Codex/README.md | 2 - .../.codex-plugin/plugin.json | 22 - .../harness-engineering/agents/openai.yaml | 4 - .../harness-engineering/commands/harness.md | 43 -- .../harness-engineering/commands/review.md | 38 -- .../__pycache__/execute.cpython-312.pyc | Bin 25704 -> 0 bytes .../validate_workspace.cpython-312.pyc | Bin 3922 -> 0 bytes Coding/Codex/scripts/execute.py | 29 +- Coding/Codex/scripts/test_execute.py | 562 ------------------ Coding/Codex/scripts/validate_workspace.py | 91 --- .../Gemini/.agents/plugins/marketplace.json | 20 - .../.agents/skills/harness-review/SKILL.md | 57 -- .../skills/harness-review/agents/openai.yaml | 4 - .../.agents/skills/harness-workflow/SKILL.md | 145 ----- .../harness-workflow/agents/openai.yaml | 4 - .../.codex/agents/harness-reviewer.toml | 11 - .../Gemini/.codex/agents/phase-planner.toml | 12 - Coding/Gemini/.codex/config.toml | 9 - Coding/Gemini/.codex/hooks.json | 28 - .../pre_tool_use_policy.cpython-312.pyc | Bin 1562 -> 0 bytes .../__pycache__/stop_continue.cpython-312.pyc | Bin 2018 -> 0 bytes .../.codex/hooks/pre_tool_use_policy.py | 47 -- Coding/Gemini/.codex/hooks/stop_continue.py | 55 -- Coding/Gemini/AGENTS.md | 40 -- Coding/Gemini/README.md | 2 - Coding/Gemini/docs/ADR.md | 21 - Coding/Gemini/docs/ARCHITECTURE.md | 24 - Coding/Gemini/docs/PRD.md | 21 - Coding/Gemini/docs/UI_GUIDE.md | 76 --- .../.codex-plugin/plugin.json | 22 - .../harness-engineering/agents/openai.yaml | 4 - .../harness-engineering/commands/harness.md | 43 -- .../harness-engineering/commands/review.md | 38 -- .../__pycache__/execute.cpython-312.pyc | Bin 25704 -> 0 bytes .../validate_workspace.cpython-312.pyc | Bin 3922 -> 0 bytes Coding/Gemini/scripts/execute.py | 424 ------------- Coding/Gemini/scripts/test_execute.py | 562 ------------------ Coding/Gemini/scripts/validate_workspace.py | 91 --- 55 files changed, 437 insertions(+), 2862 deletions(-) delete mode 100644 Coding/Codex/.agents/plugins/marketplace.json delete mode 100644 Coding/Codex/.codex/agents/harness-reviewer.toml delete mode 100644 Coding/Codex/.codex/agents/phase-planner.toml delete mode 100644 Coding/Codex/.codex/hooks/__pycache__/pre_tool_use_policy.cpython-312.pyc delete mode 100644 Coding/Codex/.codex/hooks/__pycache__/stop_continue.cpython-312.pyc create mode 100644 Coding/Codex/.codex/hooks/pre_commit_checks.py delete mode 100644 Coding/Codex/.codex/hooks/pre_tool_use_policy.py delete mode 100644 Coding/Codex/.codex/hooks/stop_continue.py create mode 100644 Coding/Codex/.codex/hooks/tdd-guard.py create mode 100644 Coding/Codex/.gitignore delete mode 100644 Coding/Codex/README.md delete mode 100644 Coding/Codex/plugins/harness-engineering/.codex-plugin/plugin.json delete mode 100644 Coding/Codex/plugins/harness-engineering/agents/openai.yaml delete mode 100644 Coding/Codex/plugins/harness-engineering/commands/harness.md delete mode 100644 Coding/Codex/plugins/harness-engineering/commands/review.md delete mode 100644 Coding/Codex/scripts/__pycache__/execute.cpython-312.pyc delete mode 100644 Coding/Codex/scripts/__pycache__/validate_workspace.cpython-312.pyc delete mode 100644 Coding/Codex/scripts/test_execute.py delete mode 100644 Coding/Codex/scripts/validate_workspace.py delete mode 100644 Coding/Gemini/.agents/plugins/marketplace.json delete mode 100644 Coding/Gemini/.agents/skills/harness-review/SKILL.md delete mode 100644 Coding/Gemini/.agents/skills/harness-review/agents/openai.yaml delete mode 100644 Coding/Gemini/.agents/skills/harness-workflow/SKILL.md delete mode 100644 Coding/Gemini/.agents/skills/harness-workflow/agents/openai.yaml delete mode 100644 Coding/Gemini/.codex/agents/harness-reviewer.toml delete mode 100644 Coding/Gemini/.codex/agents/phase-planner.toml delete mode 100644 Coding/Gemini/.codex/config.toml delete mode 100644 Coding/Gemini/.codex/hooks.json delete mode 100644 Coding/Gemini/.codex/hooks/__pycache__/pre_tool_use_policy.cpython-312.pyc delete mode 100644 Coding/Gemini/.codex/hooks/__pycache__/stop_continue.cpython-312.pyc delete mode 100644 Coding/Gemini/.codex/hooks/pre_tool_use_policy.py delete mode 100644 Coding/Gemini/.codex/hooks/stop_continue.py delete mode 100644 Coding/Gemini/AGENTS.md delete mode 100644 Coding/Gemini/README.md delete mode 100644 Coding/Gemini/docs/ADR.md delete mode 100644 Coding/Gemini/docs/ARCHITECTURE.md delete mode 100644 Coding/Gemini/docs/PRD.md delete mode 100644 Coding/Gemini/docs/UI_GUIDE.md delete mode 100644 Coding/Gemini/plugins/harness-engineering/.codex-plugin/plugin.json delete mode 100644 Coding/Gemini/plugins/harness-engineering/agents/openai.yaml delete mode 100644 Coding/Gemini/plugins/harness-engineering/commands/harness.md delete mode 100644 Coding/Gemini/plugins/harness-engineering/commands/review.md delete mode 100644 Coding/Gemini/scripts/__pycache__/execute.cpython-312.pyc delete mode 100644 Coding/Gemini/scripts/__pycache__/validate_workspace.cpython-312.pyc delete mode 100644 Coding/Gemini/scripts/execute.py delete mode 100644 Coding/Gemini/scripts/test_execute.py delete mode 100644 Coding/Gemini/scripts/validate_workspace.py diff --git a/Coding/Codex/.agents/plugins/marketplace.json b/Coding/Codex/.agents/plugins/marketplace.json deleted file mode 100644 index 056725e..0000000 --- a/Coding/Codex/.agents/plugins/marketplace.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "local-harness-engineering", - "interface": { - "displayName": "Local Harness Engineering" - }, - "plugins": [ - { - "name": "harness-engineering", - "source": { - "source": "local", - "path": "./plugins/harness-engineering" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "Productivity" - } - ] -} diff --git a/Coding/Codex/.agents/skills/harness-review/SKILL.md b/Coding/Codex/.agents/skills/harness-review/SKILL.md index c0fcf8d..75d026e 100644 --- a/Coding/Codex/.agents/skills/harness-review/SKILL.md +++ b/Coding/Codex/.agents/skills/harness-review/SKILL.md @@ -1,57 +1,42 @@ --- name: harness-review -description: Review a Harness Engineering repository against its persistent rules and design docs. Use when Codex is asked to review local changes, generated phase files, or implementation output against `AGENTS.md`, `docs/ARCHITECTURE.md`, `docs/ADR.md`, `docs/UI_GUIDE.md`, testing expectations, and Harness step acceptance criteria. +description: Use when reviewing this Harness repository: local changes, generated phase files, step outputs, implementation diffs, missing tests, build readiness, or compliance with AGENTS.md, docs/ARCHITECTURE.md, docs/ADR.md, and Harness acceptance criteria. --- # Harness Review -Use this skill when the user wants a repository-grounded review instead of generic commentary. +## Overview -## Review input set +Use this skill to review Harness work against the repository's persistent rules, architecture docs, and executable verification requirements. Prioritize bugs, regressions, missing tests, and rule violations. -Read these first: +## Review Process -- `/AGENTS.md` -- `/docs/ARCHITECTURE.md` -- `/docs/ADR.md` -- `/docs/UI_GUIDE.md` -- the changed files or generated `phases/` files under review - -If the user explicitly asks for delegated review, prefer the repo custom agent `harness_reviewer` or built-in read-only explorers. +1. Read `/AGENTS.md`, `/docs/ARCHITECTURE.md`, and `/docs/ADR.md`. +2. Inspect the changed files with `git status --short` and `git diff`. +3. Check architecture, stack choices, tests, critical rules, and build readiness. +4. Run relevant verification commands when feasible. If a command cannot be run, report that as residual risk. +5. Lead with actionable findings. Keep summaries secondary. ## Checklist -Evaluate the patch against these questions: +| Item | Question | +| --- | --- | +| Architecture | Does the change follow `docs/ARCHITECTURE.md` directory and module boundaries? | +| Stack | Does the change stay within choices documented in `docs/ADR.md`? | +| Tests | Are new or changed behaviors covered by tests? | +| Critical Rules | Does the change violate any `AGENTS.md` CRITICAL rule? | +| Build | Do relevant build/test/lint commands pass? | -1. Does it follow the architecture described in `docs/ARCHITECTURE.md`? -2. Does it stay within the technology choices documented in `docs/ADR.md`? -3. Are new or changed behaviors covered by tests or other explicit validation? -4. Does it violate any CRITICAL rule in `AGENTS.md`? -5. Do generated `phases/` files remain self-contained, executable, and internally consistent? -6. If the user expects verification, does `python scripts/validate_workspace.py` succeed or is the failure explained? +## Output Format -## Output rules +If there are findings, list them first in severity order with file and line references when possible. Then include this table: -- Lead with findings, ordered by severity. -- Include file references for each finding. -- Explain the concrete risk or regression, not just the rule name. -- If there are no findings, say so explicitly and mention residual risks or missing evidence. -- Keep summaries brief after the findings. +| 항목 | 결과 | 비고 | +| --- | --- | --- | +| 아키텍처 준수 | PASS/FAIL | {상세} | +| 기술 스택 준수 | PASS/FAIL | {상세} | +| 테스트 존재 | PASS/FAIL | {상세} | +| CRITICAL 규칙 | PASS/FAIL | {상세} | +| 빌드 가능 | PASS/FAIL | {상세} | -## Preferred review table - -When the user asks for a checklist-style review, use this table: - -| Item | Result | Notes | -|------|------|------| -| Architecture compliance | PASS/FAIL | {details} | -| Tech stack compliance | PASS/FAIL | {details} | -| Test coverage | PASS/FAIL | {details} | -| CRITICAL rules | PASS/FAIL | {details} | -| Build and validation | PASS/FAIL | {details} | - -## What not to do - -- Do not approve changes just because they compile. -- Do not focus on style-only issues when correctness, architecture drift, or missing validation exists. -- Do not assume a passing hook means the implementation is acceptable; review the actual diff and docs. +When there are no findings, say that clearly, then mention any commands not run or remaining risk. diff --git a/Coding/Codex/.agents/skills/harness-review/agents/openai.yaml b/Coding/Codex/.agents/skills/harness-review/agents/openai.yaml index 555439e..9af296a 100644 --- a/Coding/Codex/.agents/skills/harness-review/agents/openai.yaml +++ b/Coding/Codex/.agents/skills/harness-review/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Harness Review" - short_description: "Review changes against Harness project rules" - default_prompt: "Use Harness review to check architecture, tests, and rules." + short_description: "Review Harness changes safely" + default_prompt: "Use $harness-review to review Harness repository changes." diff --git a/Coding/Codex/.agents/skills/harness-workflow/SKILL.md b/Coding/Codex/.agents/skills/harness-workflow/SKILL.md index 6d6f4c2..872ddb1 100644 --- a/Coding/Codex/.agents/skills/harness-workflow/SKILL.md +++ b/Coding/Codex/.agents/skills/harness-workflow/SKILL.md @@ -1,47 +1,36 @@ --- name: harness-workflow -description: Plan and run the Harness Engineering workflow for this repository. Use when Codex needs to read `AGENTS.md` and `docs/*.md`, discuss implementation scope, draft phase plans, or create/update `phases/index.json`, `phases/{phase}/index.json`, and `phases/{phase}/stepN.md` files for staged execution. +description: Use when planning or running this Harness framework: reading AGENTS.md and docs/*.md, discussing implementation scope, creating or updating phases/index.json, phases/{task}/index.json, phases/{task}/stepN.md, or invoking scripts/execute.py for staged Codex execution. --- # Harness Workflow -Use this skill when the user is working in the Harness template and wants structured planning or phase-file generation. +## Overview + +Use this skill to turn a user-approved task into small, self-contained Harness steps that another Codex session can execute reliably. Keep the workflow grounded in repository docs and executable acceptance criteria. ## Workflow -### 1. Explore first +1. Read `AGENTS.md` and relevant files under `docs/`, especially `docs/PRD.md`, `docs/ARCHITECTURE.md`, and `docs/ADR.md`. +2. Discuss unresolved product or technical decisions with the user before writing phase files. +3. When the user asks for an implementation plan, draft steps and get approval before creating files. +4. Create or update `phases/index.json`, `phases/{task-name}/index.json`, and one `phases/{task-name}/stepN.md` per step. +5. Run the phase with `python scripts/execute.py {task-name}` when asked to execute it. Use `--push` only when the user asks to push. -Read these files before proposing steps: +## Step Design Rules -- `/AGENTS.md` -- `/docs/PRD.md` -- `/docs/ARCHITECTURE.md` -- `/docs/ADR.md` -- `/docs/UI_GUIDE.md` +- Scope each step to one layer or module. Split steps when multiple modules would otherwise change together. +- Make every step self-contained. Do not rely on prior conversation; include all required context and file paths. +- Force context gathering. Each step must tell Codex which docs and previous outputs to read before editing. +- Specify interfaces and signatures, not full implementations, unless exact code is required for a constraint. +- Put core invariants directly in the step: idempotency, security, data integrity, API contracts, or other non-negotiables. +- Use executable acceptance criteria such as `npm run build && npm test`, not abstract statements. +- Write cautions concretely: "Do not do X. Reason: Y." +- Name steps with kebab-case slugs such as `project-setup`, `api-layer`, or `auth-flow`. -If the user explicitly asks for parallel exploration, use built-in Codex subagents such as `explorer`, or the repo-scoped custom agent `phase_planner`. +## Phase Files -### 2. Discuss before locking the plan - -If scope, sequencing, or architecture choices are still ambiguous, surface the decision points before creating `phases/` files. - -### 3. Design steps with strict boundaries - -When drafting a phase plan: - -1. Keep scope minimal. One step should usually touch one layer or one module. -2. Make each step self-contained. Every `stepN.md` must work in an isolated Codex session. -3. List prerequisite files explicitly. Never rely on "as discussed above". -4. Specify interfaces or invariants, not line-by-line implementations. -5. Use executable acceptance commands, not vague success criteria. -6. Write concrete warnings in "do not do X because Y" form. -7. Use kebab-case step names. - -## Files to generate - -### `phases/index.json` - -Top-level phase registry. Append to `phases[]` when the file already exists. +Create or update `phases/index.json`: ```json { @@ -54,16 +43,12 @@ Top-level phase registry. Append to `phases[]` when the file already exists. } ``` -- `dir`: phase directory name. -- `status`: `pending`, `completed`, `error`, or `blocked`. -- Timestamp fields are written by `scripts/execute.py`; do not seed them during planning. - -### `phases/{phase}/index.json` +Create `phases/{task-name}/index.json`: ```json { "project": "", - "phase": "", + "phase": "", "steps": [ { "step": 0, "name": "project-setup", "status": "pending" }, { "step": 1, "name": "core-types", "status": "pending" }, @@ -72,74 +57,68 @@ Top-level phase registry. Append to `phases[]` when the file already exists. } ``` -- `project`: from `AGENTS.md`. -- `phase`: directory name. -- `steps[].step`: zero-based integer. -- `steps[].name`: kebab-case slug. -- `steps[].status`: initialize to `pending`. +Rules: -### `phases/{phase}/stepN.md` +- `project` comes from `AGENTS.md`. +- `phase` matches the task directory name. +- `steps[].step` starts at `0`. +- Initial status is always `pending`. +- Do not add timestamps when creating files. `scripts/execute.py` records `created_at`, `started_at`, `completed_at`, `failed_at`, and `blocked_at`. -Each step file should contain: - -1. A title. -2. A "read these files first" section. -3. A concrete task section. -4. Executable acceptance criteria. -5. Verification instructions. -6. Explicit prohibitions. - -Recommended structure: +## Step Template ```markdown # Step {N}: {name} -## Read First -- /AGENTS.md -- /docs/ARCHITECTURE.md -- /docs/ADR.md -- {files from previous steps} +## 읽어야 할 파일 -## Task -{specific instructions} +먼저 아래 파일들을 읽고 프로젝트의 아키텍처와 설계 의도를 파악하라: + +- `/AGENTS.md` +- `/docs/ARCHITECTURE.md` +- `/docs/ADR.md` +- {previously created or modified files} + +이전 step에서 만들어진 코드를 꼼꼼히 읽고, 설계 의도를 이해한 뒤 작업하라. + +## 작업 + +{Concrete instructions with file paths, interfaces, signatures, and rules.} ## Acceptance Criteria -```bash -python scripts/validate_workspace.py -``` - -## Verification -1. Run the acceptance commands. -2. Check AGENTS and docs for rule drift. -3. Update the matching step in phases/{phase}/index.json: - - completed + summary - - error + error_message - - blocked + blocked_reason - -## Do Not -- {concrete prohibition} -``` -``` - -## Execution - -Run the generated phase with: ```bash -python scripts/execute.py -python scripts/execute.py --push +npm run build +npm test ``` -`scripts/execute.py` handles: +## 검증 절차 -- `feat-{phase}` branch checkout/creation -- guardrail injection from `AGENTS.md` and `docs/*.md` -- accumulation of completed-step summaries into later prompts -- up to 3 retries with prior error feedback -- two-phase commit of code changes and metadata updates -- timestamps such as `created_at`, `started_at`, `completed_at`, `failed_at`, and `blocked_at` +1. 위 AC 커맨드를 실행한다. +2. 아키텍처 체크리스트를 확인한다: + - ARCHITECTURE.md 디렉토리 구조를 따르는가? + - ADR 기술 스택을 벗어나지 않았는가? + - AGENTS.md CRITICAL 규칙을 위반하지 않았는가? +3. 결과에 따라 `phases/{task-name}/index.json`의 해당 step을 업데이트한다: + - 성공: `"status": "completed"`, `"summary": "산출물 한 줄 요약"` + - 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"` + - 사용자 개입 필요: `"status": "blocked"`, `"blocked_reason": "구체적 사유"` 후 중단 -## Recovery rules +## 금지사항 -- If a step is `error`, reset its status to `pending`, remove `error_message`, then rerun. -- If a step is `blocked`, resolve the blocker, reset to `pending`, remove `blocked_reason`, then rerun. +- {Do not do X. Reason: Y.} +- 기존 테스트를 깨뜨리지 마라. +``` + +## Execution And Recovery + +Run: + +```bash +python scripts/execute.py {task-name} +python scripts/execute.py {task-name} --push +``` + +`scripts/execute.py` creates or checks out `feat-{task-name}`, injects `AGENTS.md` and `docs/*.md` into each prompt, carries completed step summaries forward, retries failed steps up to three times, separates code and metadata commits, and records timestamps. + +If a step is `error`, set it back to `pending` and remove `error_message` after fixing the cause. If a step is `blocked`, resolve `blocked_reason`, set it back to `pending`, remove `blocked_reason`, and rerun. diff --git a/Coding/Codex/.agents/skills/harness-workflow/agents/openai.yaml b/Coding/Codex/.agents/skills/harness-workflow/agents/openai.yaml index 890daa1..3a671bf 100644 --- a/Coding/Codex/.agents/skills/harness-workflow/agents/openai.yaml +++ b/Coding/Codex/.agents/skills/harness-workflow/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Harness Workflow" - short_description: "Guide Codex through Harness phase planning" - default_prompt: "Use the Harness workflow to plan phases and step files." + short_description: "Plan staged Harness workflow steps" + default_prompt: "Use $harness-workflow to plan Harness phases and step files." diff --git a/Coding/Codex/.codex/agents/harness-reviewer.toml b/Coding/Codex/.codex/agents/harness-reviewer.toml deleted file mode 100644 index 95263fd..0000000 --- a/Coding/Codex/.codex/agents/harness-reviewer.toml +++ /dev/null @@ -1,11 +0,0 @@ -name = "harness_reviewer" -description = "Read-only reviewer for Harness projects, focused on architecture drift, critical rule violations, and missing validation." -model = "gpt-5.4" -model_reasoning_effort = "high" -sandbox_mode = "read-only" -developer_instructions = """ -Review changes like a repository owner. -Prioritize correctness, architecture compliance, behavior regressions, and missing tests over style. -Always compare the patch against AGENTS.md, docs/ARCHITECTURE.md, docs/ADR.md, and the requested acceptance criteria. -Lead with concrete findings and file references. If no material issues are found, say so explicitly and mention residual risks. -""" diff --git a/Coding/Codex/.codex/agents/phase-planner.toml b/Coding/Codex/.codex/agents/phase-planner.toml deleted file mode 100644 index 35d5389..0000000 --- a/Coding/Codex/.codex/agents/phase-planner.toml +++ /dev/null @@ -1,12 +0,0 @@ -name = "phase_planner" -description = "Read-heavy Harness planner that decomposes docs into minimal, self-contained phase and step files." -model = "gpt-5.4" -model_reasoning_effort = "high" -sandbox_mode = "read-only" -developer_instructions = """ -Plan before implementing. -Read AGENTS.md and the docs directory, identify the smallest coherent phase boundaries, and draft self-contained steps. -Keep each step scoped to one layer or one module when possible. -Do not make code changes unless the parent agent explicitly asks you to write files. -Return concrete file paths, acceptance commands, and blocking assumptions. -""" diff --git a/Coding/Codex/.codex/config.toml b/Coding/Codex/.codex/config.toml index 39ca33a..b75aa36 100644 --- a/Coding/Codex/.codex/config.toml +++ b/Coding/Codex/.codex/config.toml @@ -1,9 +1,4 @@ -# Project-scoped Codex defaults for the Harness template. -# As of 2026-04-15, hooks are experimental and disabled on native Windows. +#:schema https://developers.openai.com/codex/config-schema.json [features] codex_hooks = true - -[agents] -max_threads = 6 -max_depth = 1 diff --git a/Coding/Codex/.codex/hooks.json b/Coding/Codex/.codex/hooks.json index 166e5b9..0229d62 100644 --- a/Coding/Codex/.codex/hooks.json +++ b/Coding/Codex/.codex/hooks.json @@ -2,24 +2,24 @@ "hooks": { "PreToolUse": [ { - "matcher": "Bash", + "matcher": "^Bash$", "hooks": [ { "type": "command", - "command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_policy.py\"", - "statusMessage": "Checking risky shell command" + "command": "python -c \"import pathlib, runpy, subprocess; root = pathlib.Path(subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], text=True).strip()); runpy.run_path(str(root / '.codex' / 'hooks' / 'pre_commit_checks.py'), run_name='__main__')\"", + "timeout": 600, + "statusMessage": "Running pre-commit checks" } ] - } - ], - "Stop": [ + }, { + "matcher": "^(apply_patch|Edit|Write)$", "hooks": [ { "type": "command", - "command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/stop_continue.py\"", - "statusMessage": "Running Harness validation", - "timeout": 300 + "command": "python -c \"import pathlib, runpy, subprocess; root = pathlib.Path(subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], text=True).strip()); runpy.run_path(str(root / '.codex' / 'hooks' / 'tdd-guard.py'), run_name='__main__')\"", + "timeout": 30, + "statusMessage": "Checking TDD guard" } ] } diff --git a/Coding/Codex/.codex/hooks/__pycache__/pre_tool_use_policy.cpython-312.pyc b/Coding/Codex/.codex/hooks/__pycache__/pre_tool_use_policy.cpython-312.pyc deleted file mode 100644 index 2f329e6f4e007d549479fa87944ec67f2ab67aac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1562 zcmZuxOH3O_7@mFj{Qx@#B^6P&fu?M!O;jZZq9_p$E1``58QLn=R;yj_fY)B{S~IhN zEszlE0i;S4^*|~`sw%Z=D=sP?n;ww;L}hI~afTQ-oz zBAuE!R+yY(u9@MHJ9*~GXeK+YTQUe_USgOIWZv_dS%5Wf9%jc0tZ8QM4b99JV97_A zhm)FVK}R#$ib1TIrDJ3cMVN7kW8Y*rZGi|N$9Mry5lE!sXSTuM#+QxbrEP+bw zblehcuzg(hf8&xRx8utKiMK0^fe)quSQbe~+f(UmYcJ)JxFnEd+peVB+Dp03I^~3g zkJJ-=W{$ikxfQVZY=y1JeaRM1blrV2>qu{(SO5ZO2r~P60r@Ab!V&%t^#LRa;4aHt z0E^OHupp?>3B-Y+QQ~=yZo74#(9rQDQb5yX&#O+?VacAgOOrm~;ph~CGc5aE45@5E zx6!of4q6uC(KM!3pVbmZ^*CDFe7_*@s zW4m!l&z|-b*z|g?8mDrFNh&*@VNnS;Fb!a0*)HvPdwOz$RbX!?kC2C`GzSSyj@+3X zeMg@fo|(xPCZ?%~APr(@pi-Ge$8Swc7V@LR(|Oj8U#qhSu{@u!Lx$=`gKvTO&~b|p zh+NFCQ&E%`y*a+mqQ+anS{*_C5JewDz3w@7sgbKUs9ZB_7oBHI_#NUe8CwA@Q3|B? z(%oC?rh0$$v++;IchZ;ELai|9=-#NVSMR;~wYZZUc$^%3lpK5z`Sz`y;ZGn0QcpTMTN0BmaEhID zwRfZsJnTC&a!qow2v9e4x1&P@K3z}FdL zXB32A!R_C{#b+S)6hxkaXe$^_iz~_f?k*wGx(wv_TJs0#+>>Z#bLNxxcA`Ds%RMa# K$giMFtia#py^Mwc diff --git a/Coding/Codex/.codex/hooks/__pycache__/stop_continue.cpython-312.pyc b/Coding/Codex/.codex/hooks/__pycache__/stop_continue.cpython-312.pyc deleted file mode 100644 index e706fdf7384c15e6c56105f196c9b4d25c64f458..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2018 zcmZ`4O>YxNbk@7}I=lW@CjmkN$(EFlMaU9}s%=zNsz_R;hqU6WU?El;&xEXF@47Q% zVjD*av=YgQpdLsCRq3T4N)P=J?WvbIMv56|MGHM}b4v~tr_MOp7}0jDo%iO=dvD&i ze~!mBz*p+`U)`1hz+-U;7C(z6J%fC|1t7=(D8W!(Dugm2N#Jr`&d6=3WRx}xXTlsR z`A9*{sGLWI;ZK!J6m`MK%pm}9JOq%&zvxO^HvlWrCUQ-x$X2v^?qta`F)I4Rr9Q5h zmz}%|9qRg?IXjI!(=ku@5S2|@!k$T}UnHjEK`!xe2@z`g9x@9)MrP3=#B`@jmzqRpoT^4SC9TIML0lH-(069*1dC{#f@!QDD zcOCcUKn>*df8WDU5CAMyBpMGm{)bx*xXTg|DNurQE*VI6U=ZRtsV2d2mA?)nVxJ4u zLb+6jFTetRA}r|%sJY$%>%bR$-r1oB7~LIUwQcV%g}4nW|3$y=S_yC7&&f48*B|h9 zAe7pr!mZ%=arB)aUPS(8p9bMuKK=9J63nx94U9}$>mW_^#k zUJ0c&%?hz7L|K>cNx&i)ImGv@UZ&(YWJ=z5Ad^XjurQ&}^;q)#Qy)x>qAZ^kV;K9G z$(IntqEnfQ5%KdpG7(gE2_;M;rO6`pvxpET)MJuH-~#bN_ieqQYsW2Tp^+* zOtGHtO^bKR7eo@tg?2u-<4~+HrQo<8KFrx;{3CDh3Y-V6kQ`1u?B03Hx@p}x@#XmE zaYld|<+t3fM>&NQ)v4(zp zeza-yE>-G=xjH;jH%8{in|klMK2X;O?&*7le0@SqK2f6A z$FGh*>Kl4woNM;FO6~nqjdW=n3BWDW7PXC3S2)t@1+nPqQY3&rQd;PoE>0G z2u0s2_^^~mZ{yeaQH&PyCf~TRlJpz6_y^ec80Zf`>H*Mx0e#OR2c>xHv;>scLUm2v aeP8RndHS=n4Q Path: + try: + root = subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], + cwd=cwd, + text=True, + stderr=subprocess.DEVNULL, + ).strip() + except (subprocess.CalledProcessError, FileNotFoundError): + return cwd + return Path(root) + + +def _load_scripts(root: Path) -> dict[str, str]: + package_json = root / "package.json" + if not package_json.exists(): + return {} + + try: + package = json.loads(package_json.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + _deny(f"Invalid package.json: {exc}") + raise SystemExit(0) from exc + + scripts = package.get("scripts", {}) + if not isinstance(scripts, dict): + return {} + return {str(name): str(command) for name, command in scripts.items()} + + +def _is_git_commit(command: str) -> bool: + return re.search(r"\bgit(?:\s+(?:-[A-Za-z]\s+\S+|--[A-Za-z0-9-]+(?:=\S+)?))*\s+commit\b", command) is not None + + +def _deny(reason: str) -> None: + print( + json.dumps( + { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": reason, + } + } + ) + ) + + +def _tail(text: str, limit: int = 1200) -> str: + text = text.strip() + if len(text) <= limit: + return text + return text[-limit:] + + +def _run_checks(root: Path, scripts: dict[str, str]) -> str | None: + npm = shutil.which("npm") or shutil.which("npm.cmd") + if npm is None: + return "npm was not found, so pre-commit checks could not run." + + for check in CHECKS: + if check not in scripts: + continue + result = subprocess.run( + [npm, "run", check], + cwd=root, + capture_output=True, + text=True, + ) + + if result.returncode != 0: + details = _tail(result.stdout + "\n" + result.stderr) + if details: + return f"npm run {check} failed:\n{details}" + return f"npm run {check} failed with exit code {result.returncode}." + + return None + + +def main() -> int: + try: + payload = json.load(sys.stdin) + except json.JSONDecodeError: + return 0 + + command = payload.get("tool_input", {}).get("command", "") + if not isinstance(command, str) or not _is_git_commit(command): + return 0 + + cwd = Path(payload.get("cwd") or Path.cwd()) + root = _repo_root(cwd) + failure = _run_checks(root, _load_scripts(root)) + if failure: + _deny(f"PRE-COMMIT CHECKS: {failure}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Coding/Codex/.codex/hooks/pre_tool_use_policy.py b/Coding/Codex/.codex/hooks/pre_tool_use_policy.py deleted file mode 100644 index e64fa90..0000000 --- a/Coding/Codex/.codex/hooks/pre_tool_use_policy.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -"""Block obviously destructive shell commands before Codex runs them.""" - -from __future__ import annotations - -import json -import re -import sys - - -BLOCK_PATTERNS = ( - r"\brm\s+-rf\b", - r"\bgit\s+push\s+--force(?:-with-lease)?\b", - r"\bgit\s+reset\s+--hard\b", - r"\bDROP\s+TABLE\b", - r"\btruncate\s+table\b", - r"\bRemove-Item\b.*\b-Recurse\b", - r"\bdel\b\s+/s\b", -) - - -def main() -> int: - try: - payload = json.load(sys.stdin) - except json.JSONDecodeError: - return 0 - - command = payload.get("tool_input", {}).get("command", "") - for pattern in BLOCK_PATTERNS: - if re.search(pattern, command, re.IGNORECASE): - json.dump( - { - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "deny", - "permissionDecisionReason": "Harness guardrail blocked a risky shell command.", - } - }, - sys.stdout, - ) - return 0 - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/Coding/Codex/.codex/hooks/stop_continue.py b/Coding/Codex/.codex/hooks/stop_continue.py deleted file mode 100644 index e61f2ae..0000000 --- a/Coding/Codex/.codex/hooks/stop_continue.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -"""Run repository validation when a Codex turn stops and request one more pass if it fails.""" - -from __future__ import annotations - -import json -import subprocess -import sys -from pathlib import Path - - -def main() -> int: - try: - payload = json.load(sys.stdin) - except json.JSONDecodeError: - return 0 - - if payload.get("stop_hook_active"): - return 0 - - root = Path(payload.get("cwd") or ".").resolve() - validator = root / "scripts" / "validate_workspace.py" - if not validator.exists(): - return 0 - - result = subprocess.run( - [sys.executable, str(validator)], - cwd=root, - capture_output=True, - text=True, - timeout=240, - ) - - if result.returncode == 0: - return 0 - - summary = (result.stdout or result.stderr or "workspace validation failed").strip() - if len(summary) > 1200: - summary = summary[:1200].rstrip() + "..." - - json.dump( - { - "decision": "block", - "reason": ( - "Validation failed. Review the output, fix the repo, then continue.\n\n" - f"{summary}" - ), - }, - sys.stdout, - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/Coding/Codex/.codex/hooks/tdd-guard.py b/Coding/Codex/.codex/hooks/tdd-guard.py new file mode 100644 index 0000000..8f7e23c --- /dev/null +++ b/Coding/Codex/.codex/hooks/tdd-guard.py @@ -0,0 +1,189 @@ +import json +import subprocess +import sys +from pathlib import Path + + +SOURCE_SUFFIXES = {".ts", ".tsx", ".js", ".jsx"} +TEST_SUFFIXES = ("ts", "tsx", "js", "jsx") +CONFIG_SUFFIXES = {".json", ".css", ".scss", ".md", ".yml", ".yaml"} +NEXT_SPECIAL_FILES = { + "layout.ts", + "layout.tsx", + "page.ts", + "page.tsx", + "loading.tsx", + "error.tsx", + "not-found.tsx", + "globals.css", +} + + +def _repo_root(cwd: Path) -> Path: + try: + root = subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], + cwd=cwd, + text=True, + stderr=subprocess.DEVNULL, + ).strip() + except (subprocess.CalledProcessError, FileNotFoundError): + return cwd + return Path(root) + + +def _extract_patch_paths(command: str) -> list[str]: + prefixes = ( + "*** Add File: ", + "*** Update File: ", + "*** Delete File: ", + "*** Move to: ", + ) + paths: list[str] = [] + for raw_line in command.splitlines(): + line = raw_line.strip() + for prefix in prefixes: + if line.startswith(prefix): + paths.append(line[len(prefix) :].strip()) + break + return paths + + +def _touched_paths(payload: dict) -> list[str]: + tool_input = payload.get("tool_input", {}) + if not isinstance(tool_input, dict): + return [] + + file_path = tool_input.get("file_path") + if isinstance(file_path, str) and file_path: + return [file_path] + + command = tool_input.get("command") + if isinstance(command, str): + return _extract_patch_paths(command) + + return [] + + +def _normalize(path_text: str) -> str: + return path_text.replace("\\", "/").lower() + + +def _is_test_path(path_text: str) -> bool: + normalized = _normalize(path_text) + name = normalized.rsplit("/", 1)[-1] + return ( + "__tests__/" in normalized + or ".test." in name + or ".spec." in name + or "test" in name + or "spec" in name + ) + + +def _is_exempt(path_text: str) -> bool: + normalized = _normalize(path_text) + path = Path(path_text) + name = path.name.lower() + + if _is_test_path(path_text): + return True + if name in NEXT_SPECIAL_FILES: + return True + if path.suffix.lower() in CONFIG_SUFFIXES: + return True + if ".env" in name or ".config." in name: + return True + if any(token in name for token in ("tailwind", "postcss", "next.config", "tsconfig")): + return True + if "/types/" in normalized or name in {"types.ts", "types.d.ts"}: + return True + + return False + + +def _resolve_path(path_text: str, cwd: Path) -> Path: + path = Path(path_text) + if path.is_absolute(): + return path + return (cwd / path).resolve() + + +def _base_name(path: Path) -> str: + for suffix in (".tsx", ".ts", ".jsx", ".js"): + if path.name.endswith(suffix): + return path.name[: -len(suffix)] + return path.stem + + +def _has_existing_test(path: Path, root: Path) -> bool: + directory = path.parent + parent = directory.parent + base = _base_name(path) + + for ext in TEST_SUFFIXES: + if (directory / f"{base}.test.{ext}").exists(): + return True + if (directory / f"{base}.spec.{ext}").exists(): + return True + + for ext in TEST_SUFFIXES: + if (parent / "__tests__" / f"{base}.test.{ext}").exists(): + return True + if (directory / "__tests__" / f"{base}.test.{ext}").exists(): + return True + + for ext in TEST_SUFFIXES: + if (root / "src" / "__tests__" / f"{base}.test.{ext}").exists(): + return True + + return False + + +def _guarded_paths(paths: list[str], cwd: Path, root: Path) -> list[str]: + missing_tests: list[str] = [] + for path_text in paths: + if _is_exempt(path_text): + continue + + path = _resolve_path(path_text, cwd) + if path.suffix.lower() not in SOURCE_SUFFIXES: + continue + if not _has_existing_test(path, root): + missing_tests.append(_base_name(path)) + + return missing_tests + + +def main() -> int: + try: + payload = json.load(sys.stdin) + except json.JSONDecodeError: + return 0 + + cwd = Path(payload.get("cwd") or Path.cwd()) + root = _repo_root(cwd) + missing_tests = _guarded_paths(_touched_paths(payload), cwd, root) + if not missing_tests: + return 0 + + names = ", ".join(sorted(set(missing_tests))) + print( + json.dumps( + { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": ( + "TDD GUARD: missing test file for " + f"{names}. Write or add the test first." + ), + } + } + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Coding/Codex/.gitignore b/Coding/Codex/.gitignore new file mode 100644 index 0000000..19f6219 --- /dev/null +++ b/Coding/Codex/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +.next/ +out/ +next-env.d.ts +tsconfig.tsbuildinfo + +# phase execution outputs +phases/**/phase*-output.json +phases/**/step*-output.json diff --git a/Coding/Codex/AGENTS.md b/Coding/Codex/AGENTS.md index 37a55bf..cd56f06 100644 --- a/Coding/Codex/AGENTS.md +++ b/Coding/Codex/AGENTS.md @@ -1,11 +1,4 @@ -# Project: {프로젝트명} - -## Repository Role -- This repository is a Codex-first Harness Engineering template. -- Persistent repository instructions live in this `AGENTS.md`. -- Reusable repo-scoped workflows live in `.agents/skills/`. -- Project-scoped custom agents live in `.codex/agents/`. -- Experimental hooks live in `.codex/hooks.json`. +# 프로젝트: {프로젝트명} ## 기술 스택 - {프레임워크 (예: Next.js 15)} @@ -17,24 +10,12 @@ - CRITICAL: {절대 지켜야 할 규칙 2 (예: 클라이언트 컴포넌트에서 직접 외부 API를 호출하지 말 것)} - {일반 규칙 (예: 컴포넌트는 components/ 폴더에, 타입은 types/ 폴더에 분리)} -## Harness Workflow -- 먼저 `docs/PRD.md`, `docs/ARCHITECTURE.md`, `docs/ADR.md`, `docs/UI_GUIDE.md`를 읽고 기획/설계 의도를 파악할 것 -- 단계별 실행 계획이 필요하면 repo skill `harness-workflow`를 사용해 `phases/` 아래 파일을 설계할 것 -- 변경사항 리뷰가 필요하면 repo skill `harness-review` 또는 Codex의 `/review`를 사용할 것 -- `phases/{phase}/index.json`은 phase 진행 상태의 단일 진실 공급원으로 취급할 것 -- 각 `stepN.md`는 독립된 Codex 세션에서도 실행 가능하도록 자기완결적으로 작성할 것 - ## 개발 프로세스 - CRITICAL: 새 기능 구현 시 반드시 테스트를 먼저 작성하고, 테스트가 통과하는 구현을 작성할 것 (TDD) -- 커밋 메시지는 conventional commits 형식을 따를 것 (`feat:`, `fix:`, `docs:`, `refactor:`) -- `scripts/execute.py`는 step 완료 후 코드/메타데이터 커밋을 정리하므로, step 프롬프트 안에서 별도 커밋을 만들 필요는 없음 - -## 검증 -- 기본 검증 스크립트는 `python scripts/validate_workspace.py` -- Node 프로젝트면 `package.json`의 `lint`, `build`, `test` 스크립트를 자동 탐지해 순서대로 실행 -- 다른 스택이면 `HARNESS_VALIDATION_COMMANDS` 환경 변수에 줄바꿈 기준으로 검증 커맨드를 지정 +- 커밋 메시지는 conventional commits 형식을 따를 것 (feat:, fix:, docs:, refactor:) ## 명령어 -- `python scripts/execute.py `: Codex 기반 phase 순차 실행 -- `python scripts/execute.py --push`: phase 완료 후 브랜치 push -- `python scripts/validate_workspace.py`: 저장소 검증 +npm run dev # 개발 서버 +npm run build # 프로덕션 빌드 +npm run lint # ESLint +npm run test # 테스트 diff --git a/Coding/Codex/README.md b/Coding/Codex/README.md deleted file mode 100644 index cad8f9e..0000000 --- a/Coding/Codex/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Agentic-AI-Template - diff --git a/Coding/Codex/plugins/harness-engineering/.codex-plugin/plugin.json b/Coding/Codex/plugins/harness-engineering/.codex-plugin/plugin.json deleted file mode 100644 index 60b9951..0000000 --- a/Coding/Codex/plugins/harness-engineering/.codex-plugin/plugin.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "harness-engineering", - "version": "1.0.0", - "description": "Repo-local Harness Engineering slash commands for Codex.", - "interface": { - "displayName": "Harness Engineering", - "shortDescription": "Harness planning and review prompts for this repo", - "longDescription": "Optional local plugin that exposes Harness Engineering slash commands while the core workflow remains in repo-native AGENTS, skills, custom agents, and hooks.", - "developerName": "Local Repository", - "category": "Productivity", - "capabilities": [ - "Interactive", - "Read", - "Write" - ], - "defaultPrompt": [ - "Use Harness Engineering to plan a new phase for this repository.", - "Review my changes against the Harness docs and rules." - ], - "brandColor": "#2563EB" - } -} diff --git a/Coding/Codex/plugins/harness-engineering/agents/openai.yaml b/Coding/Codex/plugins/harness-engineering/agents/openai.yaml deleted file mode 100644 index 2671cba..0000000 --- a/Coding/Codex/plugins/harness-engineering/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Harness Engineering" - short_description: "Use Harness slash commands in this repository" - default_prompt: "Use Harness Engineering to plan a phase or review changes in this repository." diff --git a/Coding/Codex/plugins/harness-engineering/commands/harness.md b/Coding/Codex/plugins/harness-engineering/commands/harness.md deleted file mode 100644 index e4bc9df..0000000 --- a/Coding/Codex/plugins/harness-engineering/commands/harness.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -description: Run the Harness Engineering planning workflow for this repository. ---- - -# /harness - -## Preflight - -- Read `/AGENTS.md`, `/docs/PRD.md`, `/docs/ARCHITECTURE.md`, `/docs/ADR.md`, and `/docs/UI_GUIDE.md` if they exist. -- Confirm whether the user wants discussion only, a draft plan, or file generation under `phases/`. -- Note whether the user explicitly asked for subagents; only then consider `phase_planner` or built-in explorers/workers. - -## Plan - -- State what will be created or updated before editing files. -- If a plan already exists under `phases/`, say whether you are extending it or replacing part of it. -- Keep each proposed step small, self-contained, and independently executable. - -## Commands - -- Invoke `$harness-workflow` and follow it. -- When file generation is requested, create or update: - - `phases/index.json` - - `phases/{phase}/index.json` - - `phases/{phase}/stepN.md` -- Use `python scripts/execute.py ` as the runtime target when you need to reference execution. - -## Verification - -- Re-read the generated phase files for consistency. -- Check that step numbering, phase names, and acceptance commands line up. -- If the repo has a validator, prefer `python scripts/validate_workspace.py` as the default acceptance command unless the user specified a narrower command. - -## Summary - -## Result -- **Action**: planned or generated Harness phase files -- **Status**: success | partial | failed -- **Details**: phase name, step count, and any blockers - -## Next Steps - -- Suggest the next natural command, usually `python scripts/execute.py ` or a focused edit to one generated step. diff --git a/Coding/Codex/plugins/harness-engineering/commands/review.md b/Coding/Codex/plugins/harness-engineering/commands/review.md deleted file mode 100644 index 568d6e6..0000000 --- a/Coding/Codex/plugins/harness-engineering/commands/review.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -description: Review local changes against Harness repository rules and docs. ---- - -# /review - -## Preflight - -- Read `/AGENTS.md`, `/docs/ARCHITECTURE.md`, `/docs/ADR.md`, and `/docs/UI_GUIDE.md` if they exist. -- Identify the changed files or generated `phases/` artifacts that need review. -- If the user wants a delegated review, use the read-only custom agent `harness_reviewer` only when they explicitly asked for subagents. - -## Plan - -- State what evidence will be checked: docs, changed files, generated phase files, and validation output if available. -- Prioritize correctness, architecture drift, CRITICAL rule violations, and missing tests over style commentary. - -## Commands - -- Invoke `$harness-review`. -- Use Codex built-in `/review` when the user specifically wants a code-review style pass over the working tree or git diff. -- If validation is relevant, run `python scripts/validate_workspace.py` or explain why it was not run. - -## Verification - -- Confirm that every finding is tied to a file and an actual rule or behavioral risk. -- If no findings remain, say so explicitly and mention residual risks or missing evidence. - -## Summary - -## Result -- **Action**: reviewed Harness changes -- **Status**: success | partial | failed -- **Details**: findings, docs checked, and validation status - -## Next Steps - -- Suggest the smallest follow-up: fix the top finding, rerun validation, or execute a pending phase. diff --git a/Coding/Codex/scripts/__pycache__/execute.cpython-312.pyc b/Coding/Codex/scripts/__pycache__/execute.cpython-312.pyc deleted file mode 100644 index b92b9852e168e2e19d202ca8480ab07b6d54e620..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25704 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

nC+R#~s?dzi}Nm^^;4JuLGa|<=c~? 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 [--push] @@ -9,6 +9,7 @@ Usage: import argparse import contextlib import json +import os import subprocess import sys import threading @@ -53,7 +54,7 @@ class StepExecutor: """Phase 디렉토리 안의 step들을 순차 실행하는 하네스.""" MAX_RETRIES = 3 - FEAT_MSG = "feat({phase}): step {num} - {name}" + FEAT_MSG = "feat({phase}): step {num} — {name}" CHORE_MSG = "chore({phase}): step {num} output" TZ = timezone(timedelta(hours=9)) @@ -177,9 +178,7 @@ class StepExecutor: sections = [] agents_md = ROOT / "AGENTS.md" if agents_md.exists(): - sections.append( - f"## 프로젝트 규칙 (AGENTS.md)\n\n{agents_md.read_text(encoding='utf-8')}" - ) + sections.append(f"## 프로젝트 규칙 (AGENTS.md)\n\n{agents_md.read_text(encoding='utf-8')}") docs_dir = ROOT / "docs" if docs_dir.is_dir(): for doc in sorted(docs_dir.glob("*.md")): @@ -199,6 +198,9 @@ class StepExecutor: def _build_preamble(self, guardrails: str, step_context: str, prev_error: Optional[str] = None) -> str: + commit_example = self.FEAT_MSG.format( + phase=self._phase_name, num="N", name="" + ) retry_section = "" if prev_error: retry_section = ( @@ -218,7 +220,8 @@ class StepExecutor: f" - AC 통과 → \"completed\" + \"summary\" 필드에 이 step의 산출물을 한 줄로 요약\n" f" - {self.MAX_RETRIES}회 수정 시도 후에도 실패 → \"error\" + \"error_message\" 기록\n" f" - 사용자 개입이 필요한 경우 (API 키, 인증, 수동 설정 등) → \"blocked\" + \"blocked_reason\" 기록 후 즉시 중단\n" - f"6. 변경사항은 워킹 트리에 남겨라. step 완료 후 커밋은 execute.py가 정리한다.\n\n---\n\n" + f"6. 모든 변경사항을 커밋하라:\n" + f" {commit_example}\n\n---\n\n" ) # --- Codex 호출 --- @@ -232,14 +235,9 @@ class StepExecutor: sys.exit(1) prompt = preamble + step_file.read_text(encoding="utf-8") - last_message_path = self._phase_dir / f"step{step_num}-last-message.txt" result = subprocess.run( - ["codex", "exec", "--full-auto", "--json", "-C", self._root, "-o", str(last_message_path)], - cwd=self._root, - input=prompt, - capture_output=True, - text=True, - timeout=1800, + ["codex", "exec", "--dangerously-bypass-approvals-and-sandbox", "--json", prompt], + cwd=self._root, capture_output=True, text=True, timeout=1800, ) if result.returncode != 0: @@ -247,14 +245,9 @@ class StepExecutor: if result.stderr: print(f" stderr: {result.stderr[:500]}") - final_message = None - if last_message_path.exists(): - final_message = last_message_path.read_text(encoding="utf-8") - output = { "step": step_num, "name": step_name, "exitCode": result.returncode, - "finalMessage": final_message, "stdout": result.stdout, "stderr": result.stderr, } out_path = self._phase_dir / f"step{step_num}-output.json" diff --git a/Coding/Codex/scripts/test_execute.py b/Coding/Codex/scripts/test_execute.py deleted file mode 100644 index 1039319..0000000 --- a/Coding/Codex/scripts/test_execute.py +++ /dev/null @@ -1,562 +0,0 @@ -"""execute.py Codex migration safety-net tests.""" - -import json -import sys -from datetime import datetime, timedelta -from pathlib import Path -from unittest.mock import patch, MagicMock - -import pytest - -sys.path.insert(0, str(Path(__file__).parent)) -import execute as ex - - -# --------------------------------------------------------------------------- -# Fixtures -# --------------------------------------------------------------------------- - -@pytest.fixture -def tmp_project(tmp_path): - """phases/, AGENTS.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") - - docs_dir = tmp_path / "docs" - docs_dir.mkdir() - (docs_dir / "arch.md").write_text("# Architecture\nSome content") - (docs_dir / "guide.md").write_text("# Guide\nAnother doc") - - return tmp_path - - -@pytest.fixture -def phase_dir(tmp_project): - """step 3개를 가진 phase 디렉토리.""" - d = tmp_project / "phases" / "0-mvp" - d.mkdir() - - index = { - "project": "TestProject", - "phase": "mvp", - "steps": [ - {"step": 0, "name": "setup", "status": "completed", "summary": "프로젝트 초기화 완료"}, - {"step": 1, "name": "core", "status": "completed", "summary": "핵심 로직 구현"}, - {"step": 2, "name": "ui", "status": "pending"}, - ], - } - (d / "index.json").write_text(json.dumps(index, indent=2, ensure_ascii=False)) - (d / "step2.md").write_text("# Step 2: UI\n\nUI를 구현하세요.") - - return d - - -@pytest.fixture -def top_index(tmp_project): - """phases/index.json (top-level).""" - top = { - "phases": [ - {"dir": "0-mvp", "status": "pending"}, - {"dir": "1-polish", "status": "pending"}, - ] - } - p = tmp_project / "phases" / "index.json" - p.write_text(json.dumps(top, indent=2)) - return p - - -@pytest.fixture -def executor(tmp_project, phase_dir): - """테스트용 StepExecutor 인스턴스. git 호출은 별도 mock 필요.""" - with patch.object(ex, "ROOT", tmp_project): - inst = ex.StepExecutor("0-mvp") - # 내부 경로를 tmp_project 기준으로 재설정 - inst._root = str(tmp_project) - inst._phases_dir = tmp_project / "phases" - inst._phase_dir = phase_dir - inst._phase_dir_name = "0-mvp" - inst._index_file = phase_dir / "index.json" - inst._top_index_file = tmp_project / "phases" / "index.json" - return inst - - -# --------------------------------------------------------------------------- -# _stamp (= 이전 now_iso) -# --------------------------------------------------------------------------- - -class TestStamp: - def test_returns_kst_timestamp(self, executor): - result = executor._stamp() - assert "+0900" in result - - def test_format_is_iso(self, executor): - result = executor._stamp() - dt = datetime.strptime(result, "%Y-%m-%dT%H:%M:%S%z") - assert dt.tzinfo is not None - - def test_is_current_time(self, executor): - before = datetime.now(ex.StepExecutor.TZ).replace(microsecond=0) - result = executor._stamp() - after = datetime.now(ex.StepExecutor.TZ).replace(microsecond=0) + timedelta(seconds=1) - parsed = datetime.strptime(result, "%Y-%m-%dT%H:%M:%S%z") - assert before <= parsed <= after - - -# --------------------------------------------------------------------------- -# _read_json / _write_json -# --------------------------------------------------------------------------- - -class TestJsonHelpers: - def test_roundtrip(self, tmp_path): - data = {"key": "값", "nested": [1, 2, 3]} - p = tmp_path / "test.json" - ex.StepExecutor._write_json(p, data) - loaded = ex.StepExecutor._read_json(p) - assert loaded == data - - def test_save_ensures_ascii_false(self, tmp_path): - p = tmp_path / "test.json" - ex.StepExecutor._write_json(p, {"한글": "테스트"}) - raw = p.read_text() - assert "한글" in raw - assert "\\u" not in raw - - def test_save_indented(self, tmp_path): - p = tmp_path / "test.json" - ex.StepExecutor._write_json(p, {"a": 1}) - raw = p.read_text() - assert "\n" in raw - - def test_load_nonexistent_raises(self, tmp_path): - with pytest.raises(FileNotFoundError): - ex.StepExecutor._read_json(tmp_path / "nope.json") - - -# --------------------------------------------------------------------------- -# _load_guardrails -# --------------------------------------------------------------------------- - -class TestLoadGuardrails: - def test_loads_agents_md_and_docs(self, executor, tmp_project): - with patch.object(ex, "ROOT", tmp_project): - result = executor._load_guardrails() - assert "# Rules" in result - assert "rule one" in result - assert "# Architecture" in result - assert "# Guide" in result - - def test_sections_separated_by_divider(self, executor, tmp_project): - with patch.object(ex, "ROOT", tmp_project): - result = executor._load_guardrails() - assert "---" in result - - def test_docs_sorted_alphabetically(self, executor, tmp_project): - with patch.object(ex, "ROOT", tmp_project): - result = executor._load_guardrails() - arch_pos = result.index("arch") - guide_pos = result.index("guide") - assert arch_pos < guide_pos - - def test_no_agents_md(self, executor, tmp_project): - (tmp_project / "AGENTS.md").unlink() - with patch.object(ex, "ROOT", tmp_project): - result = executor._load_guardrails() - assert "AGENTS.md" not in result - assert "Architecture" in result - - def test_no_docs_dir(self, executor, tmp_project): - import shutil - shutil.rmtree(tmp_project / "docs") - with patch.object(ex, "ROOT", tmp_project): - result = executor._load_guardrails() - assert "Rules" in result - assert "Architecture" not in result - - def test_empty_project(self, tmp_path): - with patch.object(ex, "ROOT", tmp_path): - # executor가 필요 없는 static-like 동작이므로 임시 인스턴스 - phases_dir = tmp_path / "phases" / "dummy" - phases_dir.mkdir(parents=True) - idx = {"project": "T", "phase": "t", "steps": []} - (phases_dir / "index.json").write_text(json.dumps(idx)) - inst = ex.StepExecutor.__new__(ex.StepExecutor) - result = inst._load_guardrails() - assert result == "" - - -# --------------------------------------------------------------------------- -# _build_step_context -# --------------------------------------------------------------------------- - -class TestBuildStepContext: - def test_includes_completed_with_summary(self, phase_dir): - index = json.loads((phase_dir / "index.json").read_text()) - result = ex.StepExecutor._build_step_context(index) - assert "Step 0 (setup): 프로젝트 초기화 완료" in result - assert "Step 1 (core): 핵심 로직 구현" in result - - def test_excludes_pending(self, phase_dir): - index = json.loads((phase_dir / "index.json").read_text()) - result = ex.StepExecutor._build_step_context(index) - assert "ui" not in result - - def test_excludes_completed_without_summary(self, phase_dir): - index = json.loads((phase_dir / "index.json").read_text()) - del index["steps"][0]["summary"] - result = ex.StepExecutor._build_step_context(index) - assert "setup" not in result - assert "core" in result - - def test_empty_when_no_completed(self): - index = {"steps": [{"step": 0, "name": "a", "status": "pending"}]} - result = ex.StepExecutor._build_step_context(index) - assert result == "" - - def test_has_header(self, phase_dir): - index = json.loads((phase_dir / "index.json").read_text()) - result = ex.StepExecutor._build_step_context(index) - assert result.startswith("## 이전 Step 산출물") - - -# --------------------------------------------------------------------------- -# _build_preamble -# --------------------------------------------------------------------------- - -class TestBuildPreamble: - def test_includes_project_name(self, executor): - result = executor._build_preamble("", "") - assert "TestProject" in result - - def test_includes_guardrails(self, executor): - result = executor._build_preamble("GUARD_CONTENT", "") - assert "GUARD_CONTENT" in result - - def test_includes_step_context(self, executor): - ctx = "## 이전 Step 산출물\n\n- Step 0: done" - result = executor._build_preamble("", ctx) - assert "이전 Step 산출물" in result - - def test_mentions_executor_commits(self, executor): - result = executor._build_preamble("", "") - assert "커밋은 execute.py가 정리한다" in result - - def test_includes_rules(self, executor): - result = executor._build_preamble("", "") - assert "작업 규칙" in result - assert "AC" in result - - def test_no_retry_section_by_default(self, executor): - result = executor._build_preamble("", "") - assert "이전 시도 실패" not in result - - def test_retry_section_with_prev_error(self, executor): - result = executor._build_preamble("", "", prev_error="타입 에러 발생") - assert "이전 시도 실패" in result - assert "타입 에러 발생" in result - - def test_includes_max_retries(self, executor): - result = executor._build_preamble("", "") - assert str(ex.StepExecutor.MAX_RETRIES) in result - - def test_includes_index_path(self, executor): - result = executor._build_preamble("", "") - assert "/phases/0-mvp/index.json" in result - - -# --------------------------------------------------------------------------- -# _update_top_index -# --------------------------------------------------------------------------- - -class TestUpdateTopIndex: - def test_completed(self, executor, top_index): - executor._top_index_file = top_index - executor._update_top_index("completed") - data = json.loads(top_index.read_text()) - mvp = next(p for p in data["phases"] if p["dir"] == "0-mvp") - assert mvp["status"] == "completed" - assert "completed_at" in mvp - - def test_error(self, executor, top_index): - executor._top_index_file = top_index - executor._update_top_index("error") - data = json.loads(top_index.read_text()) - mvp = next(p for p in data["phases"] if p["dir"] == "0-mvp") - assert mvp["status"] == "error" - assert "failed_at" in mvp - - def test_blocked(self, executor, top_index): - executor._top_index_file = top_index - executor._update_top_index("blocked") - data = json.loads(top_index.read_text()) - mvp = next(p for p in data["phases"] if p["dir"] == "0-mvp") - assert mvp["status"] == "blocked" - assert "blocked_at" in mvp - - def test_other_phases_unchanged(self, executor, top_index): - executor._top_index_file = top_index - executor._update_top_index("completed") - data = json.loads(top_index.read_text()) - polish = next(p for p in data["phases"] if p["dir"] == "1-polish") - assert polish["status"] == "pending" - - def test_nonexistent_dir_is_noop(self, executor, top_index): - executor._top_index_file = top_index - executor._phase_dir_name = "no-such-dir" - original = json.loads(top_index.read_text()) - executor._update_top_index("completed") - after = json.loads(top_index.read_text()) - for p_before, p_after in zip(original["phases"], after["phases"]): - assert p_before["status"] == p_after["status"] - - def test_no_top_index_file(self, executor, tmp_path): - executor._top_index_file = tmp_path / "nonexistent.json" - executor._update_top_index("completed") # should not raise - - -# --------------------------------------------------------------------------- -# _checkout_branch (mocked) -# --------------------------------------------------------------------------- - -class TestCheckoutBranch: - def _mock_git(self, executor, responses): - call_idx = {"i": 0} - def fake_git(*args): - idx = call_idx["i"] - call_idx["i"] += 1 - if idx < len(responses): - return responses[idx] - return MagicMock(returncode=0, stdout="", stderr="") - executor._run_git = fake_git - - def test_already_on_branch(self, executor): - self._mock_git(executor, [ - MagicMock(returncode=0, stdout="feat-mvp\n", stderr=""), - ]) - executor._checkout_branch() # should return without checkout - - def test_branch_exists_checkout(self, executor): - self._mock_git(executor, [ - MagicMock(returncode=0, stdout="main\n", stderr=""), - MagicMock(returncode=0, stdout="", stderr=""), - MagicMock(returncode=0, stdout="", stderr=""), - ]) - executor._checkout_branch() - - def test_branch_not_exists_create(self, executor): - self._mock_git(executor, [ - MagicMock(returncode=0, stdout="main\n", stderr=""), - MagicMock(returncode=1, stdout="", stderr="not found"), - MagicMock(returncode=0, stdout="", stderr=""), - ]) - executor._checkout_branch() - - def test_checkout_fails_exits(self, executor): - self._mock_git(executor, [ - MagicMock(returncode=0, stdout="main\n", stderr=""), - MagicMock(returncode=1, stdout="", stderr=""), - MagicMock(returncode=1, stdout="", stderr="dirty tree"), - ]) - with pytest.raises(SystemExit) as exc_info: - executor._checkout_branch() - assert exc_info.value.code == 1 - - def test_no_git_exits(self, executor): - self._mock_git(executor, [ - MagicMock(returncode=1, stdout="", stderr="not a git repo"), - ]) - with pytest.raises(SystemExit) as exc_info: - executor._checkout_branch() - assert exc_info.value.code == 1 - - -# --------------------------------------------------------------------------- -# _commit_step (mocked) -# --------------------------------------------------------------------------- - -class TestCommitStep: - def test_two_phase_commit(self, executor): - calls = [] - def fake_git(*args): - calls.append(args) - if args[:2] == ("diff", "--cached"): - return MagicMock(returncode=1) - return MagicMock(returncode=0, stdout="", stderr="") - executor._run_git = fake_git - - executor._commit_step(2, "ui") - - commit_calls = [c for c in calls if c[0] == "commit"] - assert len(commit_calls) == 2 - assert "feat(mvp):" in commit_calls[0][2] - assert "chore(mvp):" in commit_calls[1][2] - - def test_no_code_changes_skips_feat_commit(self, executor): - call_count = {"diff": 0} - calls = [] - def fake_git(*args): - calls.append(args) - if args[:2] == ("diff", "--cached"): - call_count["diff"] += 1 - if call_count["diff"] == 1: - return MagicMock(returncode=0) - return MagicMock(returncode=1) - return MagicMock(returncode=0, stdout="", stderr="") - executor._run_git = fake_git - - executor._commit_step(2, "ui") - - commit_msgs = [c[2] for c in calls if c[0] == "commit"] - assert len(commit_msgs) == 1 - assert "chore" in commit_msgs[0] - - -# --------------------------------------------------------------------------- -# _invoke_codex (mocked) -# --------------------------------------------------------------------------- - -class TestInvokeCodex: - def test_invokes_codex_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) - - cmd = mock_run.call_args[0][0] - kwargs = mock_run.call_args[1] - assert cmd[0] == "codex" - assert cmd[1] == "exec" - assert "--full-auto" in cmd - assert "--json" in cmd - assert "-o" in cmd - assert "PREAMBLE" in kwargs["input"] - assert "UI를 구현하세요" in kwargs["input"] - assert output["finalMessage"] is None - - def test_saves_output_json(self, executor): - def fake_run(*args, **kwargs): - cmd = args[0] - last_message_path = Path(cmd[cmd.index("-o") + 1]) - last_message_path.write_text("completed", encoding="utf-8") - return MagicMock(returncode=0, stdout='{"ok": true}', stderr="") - - step = {"step": 2, "name": "ui"} - - with patch("subprocess.run", side_effect=fake_run): - executor._invoke_codex(step, "preamble") - - output_file = executor._phase_dir / "step2-output.json" - assert output_file.exists() - data = json.loads(output_file.read_text()) - assert data["step"] == 2 - assert data["name"] == "ui" - assert data["exitCode"] == 0 - assert data["finalMessage"] == "completed" - - 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") - assert exc_info.value.code == 1 - - def test_timeout_is_1800(self, executor): - mock_result = MagicMock(returncode=0, stdout="{}", stderr="") - step = {"step": 2, "name": "ui"} - - with patch("subprocess.run", return_value=mock_result) as mock_run: - executor._invoke_codex(step, "preamble") - - assert mock_run.call_args[1]["timeout"] == 1800 - - -# --------------------------------------------------------------------------- -# progress_indicator (= 이전 Spinner) -# --------------------------------------------------------------------------- - -class TestProgressIndicator: - def test_context_manager(self): - import time - with ex.progress_indicator("test") as pi: - time.sleep(0.15) - assert pi.elapsed >= 0.1 - - def test_elapsed_increases(self): - import time - with ex.progress_indicator("test") as pi: - time.sleep(0.2) - assert pi.elapsed > 0 - - -# --------------------------------------------------------------------------- -# main() CLI 파싱 (mocked) -# --------------------------------------------------------------------------- - -class TestMainCli: - def test_no_args_exits(self): - with patch("sys.argv", ["execute.py"]): - with pytest.raises(SystemExit) as exc_info: - ex.main() - assert exc_info.value.code == 2 # argparse exits with 2 - - def test_invalid_phase_dir_exits(self): - with patch("sys.argv", ["execute.py", "nonexistent"]): - with patch.object(ex, "ROOT", Path("/tmp/fake_nonexistent")): - with pytest.raises(SystemExit) as exc_info: - ex.main() - assert exc_info.value.code == 1 - - def test_missing_index_exits(self, tmp_project): - (tmp_project / "phases" / "empty").mkdir() - with patch("sys.argv", ["execute.py", "empty"]): - with patch.object(ex, "ROOT", tmp_project): - with pytest.raises(SystemExit) as exc_info: - ex.main() - assert exc_info.value.code == 1 - - -# --------------------------------------------------------------------------- -# _check_blockers (= 이전 main() error/blocked 체크) -# --------------------------------------------------------------------------- - -class TestCheckBlockers: - def _make_executor_with_steps(self, tmp_project, steps): - d = tmp_project / "phases" / "test-phase" - d.mkdir(exist_ok=True) - index = {"project": "T", "phase": "test", "steps": steps} - (d / "index.json").write_text(json.dumps(index)) - - with patch.object(ex, "ROOT", tmp_project): - inst = ex.StepExecutor.__new__(ex.StepExecutor) - inst._root = str(tmp_project) - inst._phases_dir = tmp_project / "phases" - inst._phase_dir = d - inst._phase_dir_name = "test-phase" - inst._index_file = d / "index.json" - inst._top_index_file = tmp_project / "phases" / "index.json" - inst._phase_name = "test" - inst._total = len(steps) - return inst - - def test_error_step_exits_1(self, tmp_project): - steps = [ - {"step": 0, "name": "ok", "status": "completed"}, - {"step": 1, "name": "bad", "status": "error", "error_message": "fail"}, - ] - inst = self._make_executor_with_steps(tmp_project, steps) - with pytest.raises(SystemExit) as exc_info: - inst._check_blockers() - assert exc_info.value.code == 1 - - def test_blocked_step_exits_2(self, tmp_project): - steps = [ - {"step": 0, "name": "ok", "status": "completed"}, - {"step": 1, "name": "stuck", "status": "blocked", "blocked_reason": "API key"}, - ] - inst = self._make_executor_with_steps(tmp_project, steps) - with pytest.raises(SystemExit) as exc_info: - inst._check_blockers() - assert exc_info.value.code == 2 diff --git a/Coding/Codex/scripts/validate_workspace.py b/Coding/Codex/scripts/validate_workspace.py deleted file mode 100644 index f7c0526..0000000 --- a/Coding/Codex/scripts/validate_workspace.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -"""Run repository validation commands for the Harness template.""" - -from __future__ import annotations - -import json -import os -import subprocess -import sys -from pathlib import Path - - -DEFAULT_NPM_ORDER = ("lint", "build", "test") - - -def load_env_commands() -> list[str]: - raw = os.environ.get("HARNESS_VALIDATION_COMMANDS", "") - return [line.strip() for line in raw.splitlines() if line.strip()] - - -def load_npm_commands(root: Path) -> list[str]: - package_json = root / "package.json" - if not package_json.exists(): - return [] - - try: - payload = json.loads(package_json.read_text(encoding="utf-8")) - except json.JSONDecodeError: - return [] - - scripts = payload.get("scripts", {}) - if not isinstance(scripts, dict): - return [] - - commands = [] - for name in DEFAULT_NPM_ORDER: - value = scripts.get(name) - if isinstance(value, str) and value.strip(): - commands.append(f"npm run {name}") - return commands - - -def discover_commands(root: Path) -> list[str]: - env_commands = load_env_commands() - if env_commands: - return env_commands - return load_npm_commands(root) - - -def run_command(command: str, root: Path) -> subprocess.CompletedProcess: - return subprocess.run( - command, - cwd=root, - shell=True, - capture_output=True, - text=True, - ) - - -def emit_stream(prefix: str, content: str, *, stream) -> None: - text = content.strip() - if not text: - return - print(prefix, file=stream) - print(text, file=stream) - - -def main() -> int: - root = Path(__file__).resolve().parent.parent - commands = discover_commands(root) - - if not commands: - print("No validation commands configured.") - print("Set HARNESS_VALIDATION_COMMANDS or add npm scripts for lint/build/test.") - return 0 - - for command in commands: - print(f"$ {command}") - result = run_command(command, root) - emit_stream("[stdout]", result.stdout, stream=sys.stdout) - emit_stream("[stderr]", result.stderr, stream=sys.stderr) - if result.returncode != 0: - print(f"Validation failed: {command}", file=sys.stderr) - return result.returncode - - print("Validation succeeded.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/Coding/Gemini/.agents/plugins/marketplace.json b/Coding/Gemini/.agents/plugins/marketplace.json deleted file mode 100644 index 056725e..0000000 --- a/Coding/Gemini/.agents/plugins/marketplace.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "local-harness-engineering", - "interface": { - "displayName": "Local Harness Engineering" - }, - "plugins": [ - { - "name": "harness-engineering", - "source": { - "source": "local", - "path": "./plugins/harness-engineering" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "Productivity" - } - ] -} diff --git a/Coding/Gemini/.agents/skills/harness-review/SKILL.md b/Coding/Gemini/.agents/skills/harness-review/SKILL.md deleted file mode 100644 index c0fcf8d..0000000 --- a/Coding/Gemini/.agents/skills/harness-review/SKILL.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: harness-review -description: Review a Harness Engineering repository against its persistent rules and design docs. Use when Codex is asked to review local changes, generated phase files, or implementation output against `AGENTS.md`, `docs/ARCHITECTURE.md`, `docs/ADR.md`, `docs/UI_GUIDE.md`, testing expectations, and Harness step acceptance criteria. ---- - -# Harness Review - -Use this skill when the user wants a repository-grounded review instead of generic commentary. - -## Review input set - -Read these first: - -- `/AGENTS.md` -- `/docs/ARCHITECTURE.md` -- `/docs/ADR.md` -- `/docs/UI_GUIDE.md` -- the changed files or generated `phases/` files under review - -If the user explicitly asks for delegated review, prefer the repo custom agent `harness_reviewer` or built-in read-only explorers. - -## Checklist - -Evaluate the patch against these questions: - -1. Does it follow the architecture described in `docs/ARCHITECTURE.md`? -2. Does it stay within the technology choices documented in `docs/ADR.md`? -3. Are new or changed behaviors covered by tests or other explicit validation? -4. Does it violate any CRITICAL rule in `AGENTS.md`? -5. Do generated `phases/` files remain self-contained, executable, and internally consistent? -6. If the user expects verification, does `python scripts/validate_workspace.py` succeed or is the failure explained? - -## Output rules - -- Lead with findings, ordered by severity. -- Include file references for each finding. -- Explain the concrete risk or regression, not just the rule name. -- If there are no findings, say so explicitly and mention residual risks or missing evidence. -- Keep summaries brief after the findings. - -## Preferred review table - -When the user asks for a checklist-style review, use this table: - -| Item | Result | Notes | -|------|------|------| -| Architecture compliance | PASS/FAIL | {details} | -| Tech stack compliance | PASS/FAIL | {details} | -| Test coverage | PASS/FAIL | {details} | -| CRITICAL rules | PASS/FAIL | {details} | -| Build and validation | PASS/FAIL | {details} | - -## What not to do - -- Do not approve changes just because they compile. -- Do not focus on style-only issues when correctness, architecture drift, or missing validation exists. -- Do not assume a passing hook means the implementation is acceptable; review the actual diff and docs. diff --git a/Coding/Gemini/.agents/skills/harness-review/agents/openai.yaml b/Coding/Gemini/.agents/skills/harness-review/agents/openai.yaml deleted file mode 100644 index 555439e..0000000 --- a/Coding/Gemini/.agents/skills/harness-review/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Harness Review" - short_description: "Review changes against Harness project rules" - default_prompt: "Use Harness review to check architecture, tests, and rules." diff --git a/Coding/Gemini/.agents/skills/harness-workflow/SKILL.md b/Coding/Gemini/.agents/skills/harness-workflow/SKILL.md deleted file mode 100644 index 6d6f4c2..0000000 --- a/Coding/Gemini/.agents/skills/harness-workflow/SKILL.md +++ /dev/null @@ -1,145 +0,0 @@ ---- -name: harness-workflow -description: Plan and run the Harness Engineering workflow for this repository. Use when Codex needs to read `AGENTS.md` and `docs/*.md`, discuss implementation scope, draft phase plans, or create/update `phases/index.json`, `phases/{phase}/index.json`, and `phases/{phase}/stepN.md` files for staged execution. ---- - -# Harness Workflow - -Use this skill when the user is working in the Harness template and wants structured planning or phase-file generation. - -## Workflow - -### 1. Explore first - -Read these files before proposing steps: - -- `/AGENTS.md` -- `/docs/PRD.md` -- `/docs/ARCHITECTURE.md` -- `/docs/ADR.md` -- `/docs/UI_GUIDE.md` - -If the user explicitly asks for parallel exploration, use built-in Codex subagents such as `explorer`, or the repo-scoped custom agent `phase_planner`. - -### 2. Discuss before locking the plan - -If scope, sequencing, or architecture choices are still ambiguous, surface the decision points before creating `phases/` files. - -### 3. Design steps with strict boundaries - -When drafting a phase plan: - -1. Keep scope minimal. One step should usually touch one layer or one module. -2. Make each step self-contained. Every `stepN.md` must work in an isolated Codex session. -3. List prerequisite files explicitly. Never rely on "as discussed above". -4. Specify interfaces or invariants, not line-by-line implementations. -5. Use executable acceptance commands, not vague success criteria. -6. Write concrete warnings in "do not do X because Y" form. -7. Use kebab-case step names. - -## Files to generate - -### `phases/index.json` - -Top-level phase registry. Append to `phases[]` when the file already exists. - -```json -{ - "phases": [ - { - "dir": "0-mvp", - "status": "pending" - } - ] -} -``` - -- `dir`: phase directory name. -- `status`: `pending`, `completed`, `error`, or `blocked`. -- Timestamp fields are written by `scripts/execute.py`; do not seed them during planning. - -### `phases/{phase}/index.json` - -```json -{ - "project": "", - "phase": "", - "steps": [ - { "step": 0, "name": "project-setup", "status": "pending" }, - { "step": 1, "name": "core-types", "status": "pending" }, - { "step": 2, "name": "api-layer", "status": "pending" } - ] -} -``` - -- `project`: from `AGENTS.md`. -- `phase`: directory name. -- `steps[].step`: zero-based integer. -- `steps[].name`: kebab-case slug. -- `steps[].status`: initialize to `pending`. - -### `phases/{phase}/stepN.md` - -Each step file should contain: - -1. A title. -2. A "read these files first" section. -3. A concrete task section. -4. Executable acceptance criteria. -5. Verification instructions. -6. Explicit prohibitions. - -Recommended structure: - -```markdown -# Step {N}: {name} - -## Read First -- /AGENTS.md -- /docs/ARCHITECTURE.md -- /docs/ADR.md -- {files from previous steps} - -## Task -{specific instructions} - -## Acceptance Criteria -```bash -python scripts/validate_workspace.py -``` - -## Verification -1. Run the acceptance commands. -2. Check AGENTS and docs for rule drift. -3. Update the matching step in phases/{phase}/index.json: - - completed + summary - - error + error_message - - blocked + blocked_reason - -## Do Not -- {concrete prohibition} -``` -``` - -## Execution - -Run the generated phase with: - -```bash -python scripts/execute.py -python scripts/execute.py --push -``` - -`scripts/execute.py` handles: - -- `feat-{phase}` branch checkout/creation -- guardrail injection from `AGENTS.md` and `docs/*.md` -- accumulation of completed-step summaries into later prompts -- up to 3 retries with prior error feedback -- two-phase commit of code changes and metadata updates -- timestamps such as `created_at`, `started_at`, `completed_at`, `failed_at`, and `blocked_at` - -## Recovery rules - -- If a step is `error`, reset its status to `pending`, remove `error_message`, then rerun. -- If a step is `blocked`, resolve the blocker, reset to `pending`, remove `blocked_reason`, then rerun. diff --git a/Coding/Gemini/.agents/skills/harness-workflow/agents/openai.yaml b/Coding/Gemini/.agents/skills/harness-workflow/agents/openai.yaml deleted file mode 100644 index 890daa1..0000000 --- a/Coding/Gemini/.agents/skills/harness-workflow/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Harness Workflow" - short_description: "Guide Codex through Harness phase planning" - default_prompt: "Use the Harness workflow to plan phases and step files." diff --git a/Coding/Gemini/.codex/agents/harness-reviewer.toml b/Coding/Gemini/.codex/agents/harness-reviewer.toml deleted file mode 100644 index 95263fd..0000000 --- a/Coding/Gemini/.codex/agents/harness-reviewer.toml +++ /dev/null @@ -1,11 +0,0 @@ -name = "harness_reviewer" -description = "Read-only reviewer for Harness projects, focused on architecture drift, critical rule violations, and missing validation." -model = "gpt-5.4" -model_reasoning_effort = "high" -sandbox_mode = "read-only" -developer_instructions = """ -Review changes like a repository owner. -Prioritize correctness, architecture compliance, behavior regressions, and missing tests over style. -Always compare the patch against AGENTS.md, docs/ARCHITECTURE.md, docs/ADR.md, and the requested acceptance criteria. -Lead with concrete findings and file references. If no material issues are found, say so explicitly and mention residual risks. -""" diff --git a/Coding/Gemini/.codex/agents/phase-planner.toml b/Coding/Gemini/.codex/agents/phase-planner.toml deleted file mode 100644 index 35d5389..0000000 --- a/Coding/Gemini/.codex/agents/phase-planner.toml +++ /dev/null @@ -1,12 +0,0 @@ -name = "phase_planner" -description = "Read-heavy Harness planner that decomposes docs into minimal, self-contained phase and step files." -model = "gpt-5.4" -model_reasoning_effort = "high" -sandbox_mode = "read-only" -developer_instructions = """ -Plan before implementing. -Read AGENTS.md and the docs directory, identify the smallest coherent phase boundaries, and draft self-contained steps. -Keep each step scoped to one layer or one module when possible. -Do not make code changes unless the parent agent explicitly asks you to write files. -Return concrete file paths, acceptance commands, and blocking assumptions. -""" diff --git a/Coding/Gemini/.codex/config.toml b/Coding/Gemini/.codex/config.toml deleted file mode 100644 index 39ca33a..0000000 --- a/Coding/Gemini/.codex/config.toml +++ /dev/null @@ -1,9 +0,0 @@ -# Project-scoped Codex defaults for the Harness template. -# As of 2026-04-15, hooks are experimental and disabled on native Windows. - -[features] -codex_hooks = true - -[agents] -max_threads = 6 -max_depth = 1 diff --git a/Coding/Gemini/.codex/hooks.json b/Coding/Gemini/.codex/hooks.json deleted file mode 100644 index 166e5b9..0000000 --- a/Coding/Gemini/.codex/hooks.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "hooks": { - "PreToolUse": [ - { - "matcher": "Bash", - "hooks": [ - { - "type": "command", - "command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_policy.py\"", - "statusMessage": "Checking risky shell command" - } - ] - } - ], - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/stop_continue.py\"", - "statusMessage": "Running Harness validation", - "timeout": 300 - } - ] - } - ] - } -} diff --git a/Coding/Gemini/.codex/hooks/__pycache__/pre_tool_use_policy.cpython-312.pyc b/Coding/Gemini/.codex/hooks/__pycache__/pre_tool_use_policy.cpython-312.pyc deleted file mode 100644 index 2f329e6f4e007d549479fa87944ec67f2ab67aac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1562 zcmZuxOH3O_7@mFj{Qx@#B^6P&fu?M!O;jZZq9_p$E1``58QLn=R;yj_fY)B{S~IhN zEszlE0i;S4^*|~`sw%Z=D=sP?n;ww;L}hI~afTQ-oz zBAuE!R+yY(u9@MHJ9*~GXeK+YTQUe_USgOIWZv_dS%5Wf9%jc0tZ8QM4b99JV97_A zhm)FVK}R#$ib1TIrDJ3cMVN7kW8Y*rZGi|N$9Mry5lE!sXSTuM#+QxbrEP+bw zblehcuzg(hf8&xRx8utKiMK0^fe)quSQbe~+f(UmYcJ)JxFnEd+peVB+Dp03I^~3g zkJJ-=W{$ikxfQVZY=y1JeaRM1blrV2>qu{(SO5ZO2r~P60r@Ab!V&%t^#LRa;4aHt z0E^OHupp?>3B-Y+QQ~=yZo74#(9rQDQb5yX&#O+?VacAgOOrm~;ph~CGc5aE45@5E zx6!of4q6uC(KM!3pVbmZ^*CDFe7_*@s zW4m!l&z|-b*z|g?8mDrFNh&*@VNnS;Fb!a0*)HvPdwOz$RbX!?kC2C`GzSSyj@+3X zeMg@fo|(xPCZ?%~APr(@pi-Ge$8Swc7V@LR(|Oj8U#qhSu{@u!Lx$=`gKvTO&~b|p zh+NFCQ&E%`y*a+mqQ+anS{*_C5JewDz3w@7sgbKUs9ZB_7oBHI_#NUe8CwA@Q3|B? z(%oC?rh0$$v++;IchZ;ELai|9=-#NVSMR;~wYZZUc$^%3lpK5z`Sz`y;ZGn0QcpTMTN0BmaEhID zwRfZsJnTC&a!qow2v9e4x1&P@K3z}FdL zXB32A!R_C{#b+S)6hxkaXe$^_iz~_f?k*wGx(wv_TJs0#+>>Z#bLNxxcA`Ds%RMa# K$giMFtia#py^Mwc diff --git a/Coding/Gemini/.codex/hooks/__pycache__/stop_continue.cpython-312.pyc b/Coding/Gemini/.codex/hooks/__pycache__/stop_continue.cpython-312.pyc deleted file mode 100644 index e706fdf7384c15e6c56105f196c9b4d25c64f458..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2018 zcmZ`4O>YxNbk@7}I=lW@CjmkN$(EFlMaU9}s%=zNsz_R;hqU6WU?El;&xEXF@47Q% zVjD*av=YgQpdLsCRq3T4N)P=J?WvbIMv56|MGHM}b4v~tr_MOp7}0jDo%iO=dvD&i ze~!mBz*p+`U)`1hz+-U;7C(z6J%fC|1t7=(D8W!(Dugm2N#Jr`&d6=3WRx}xXTlsR z`A9*{sGLWI;ZK!J6m`MK%pm}9JOq%&zvxO^HvlWrCUQ-x$X2v^?qta`F)I4Rr9Q5h zmz}%|9qRg?IXjI!(=ku@5S2|@!k$T}UnHjEK`!xe2@z`g9x@9)MrP3=#B`@jmzqRpoT^4SC9TIML0lH-(069*1dC{#f@!QDD zcOCcUKn>*df8WDU5CAMyBpMGm{)bx*xXTg|DNurQE*VI6U=ZRtsV2d2mA?)nVxJ4u zLb+6jFTetRA}r|%sJY$%>%bR$-r1oB7~LIUwQcV%g}4nW|3$y=S_yC7&&f48*B|h9 zAe7pr!mZ%=arB)aUPS(8p9bMuKK=9J63nx94U9}$>mW_^#k zUJ0c&%?hz7L|K>cNx&i)ImGv@UZ&(YWJ=z5Ad^XjurQ&}^;q)#Qy)x>qAZ^kV;K9G z$(IntqEnfQ5%KdpG7(gE2_;M;rO6`pvxpET)MJuH-~#bN_ieqQYsW2Tp^+* zOtGHtO^bKR7eo@tg?2u-<4~+HrQo<8KFrx;{3CDh3Y-V6kQ`1u?B03Hx@p}x@#XmE zaYld|<+t3fM>&NQ)v4(zp zeza-yE>-G=xjH;jH%8{in|klMK2X;O?&*7le0@SqK2f6A z$FGh*>Kl4woNM;FO6~nqjdW=n3BWDW7PXC3S2)t@1+nPqQY3&rQd;PoE>0G z2u0s2_^^~mZ{yeaQH&PyCf~TRlJpz6_y^ec80Zf`>H*Mx0e#OR2c>xHv;>scLUm2v aeP8RndHS=n4Q int: - try: - payload = json.load(sys.stdin) - except json.JSONDecodeError: - return 0 - - command = payload.get("tool_input", {}).get("command", "") - for pattern in BLOCK_PATTERNS: - if re.search(pattern, command, re.IGNORECASE): - json.dump( - { - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "deny", - "permissionDecisionReason": "Harness guardrail blocked a risky shell command.", - } - }, - sys.stdout, - ) - return 0 - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/Coding/Gemini/.codex/hooks/stop_continue.py b/Coding/Gemini/.codex/hooks/stop_continue.py deleted file mode 100644 index e61f2ae..0000000 --- a/Coding/Gemini/.codex/hooks/stop_continue.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -"""Run repository validation when a Codex turn stops and request one more pass if it fails.""" - -from __future__ import annotations - -import json -import subprocess -import sys -from pathlib import Path - - -def main() -> int: - try: - payload = json.load(sys.stdin) - except json.JSONDecodeError: - return 0 - - if payload.get("stop_hook_active"): - return 0 - - root = Path(payload.get("cwd") or ".").resolve() - validator = root / "scripts" / "validate_workspace.py" - if not validator.exists(): - return 0 - - result = subprocess.run( - [sys.executable, str(validator)], - cwd=root, - capture_output=True, - text=True, - timeout=240, - ) - - if result.returncode == 0: - return 0 - - summary = (result.stdout or result.stderr or "workspace validation failed").strip() - if len(summary) > 1200: - summary = summary[:1200].rstrip() + "..." - - json.dump( - { - "decision": "block", - "reason": ( - "Validation failed. Review the output, fix the repo, then continue.\n\n" - f"{summary}" - ), - }, - sys.stdout, - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/Coding/Gemini/AGENTS.md b/Coding/Gemini/AGENTS.md deleted file mode 100644 index 37a55bf..0000000 --- a/Coding/Gemini/AGENTS.md +++ /dev/null @@ -1,40 +0,0 @@ -# Project: {프로젝트명} - -## Repository Role -- This repository is a Codex-first Harness Engineering template. -- Persistent repository instructions live in this `AGENTS.md`. -- Reusable repo-scoped workflows live in `.agents/skills/`. -- Project-scoped custom agents live in `.codex/agents/`. -- Experimental hooks live in `.codex/hooks.json`. - -## 기술 스택 -- {프레임워크 (예: Next.js 15)} -- {언어 (예: TypeScript strict mode)} -- {스타일링 (예: Tailwind CSS)} - -## 아키텍처 규칙 -- CRITICAL: {절대 지켜야 할 규칙 1 (예: 모든 API 로직은 app/api/ 라우트 핸들러에서만 처리)} -- CRITICAL: {절대 지켜야 할 규칙 2 (예: 클라이언트 컴포넌트에서 직접 외부 API를 호출하지 말 것)} -- {일반 규칙 (예: 컴포넌트는 components/ 폴더에, 타입은 types/ 폴더에 분리)} - -## Harness Workflow -- 먼저 `docs/PRD.md`, `docs/ARCHITECTURE.md`, `docs/ADR.md`, `docs/UI_GUIDE.md`를 읽고 기획/설계 의도를 파악할 것 -- 단계별 실행 계획이 필요하면 repo skill `harness-workflow`를 사용해 `phases/` 아래 파일을 설계할 것 -- 변경사항 리뷰가 필요하면 repo skill `harness-review` 또는 Codex의 `/review`를 사용할 것 -- `phases/{phase}/index.json`은 phase 진행 상태의 단일 진실 공급원으로 취급할 것 -- 각 `stepN.md`는 독립된 Codex 세션에서도 실행 가능하도록 자기완결적으로 작성할 것 - -## 개발 프로세스 -- CRITICAL: 새 기능 구현 시 반드시 테스트를 먼저 작성하고, 테스트가 통과하는 구현을 작성할 것 (TDD) -- 커밋 메시지는 conventional commits 형식을 따를 것 (`feat:`, `fix:`, `docs:`, `refactor:`) -- `scripts/execute.py`는 step 완료 후 코드/메타데이터 커밋을 정리하므로, step 프롬프트 안에서 별도 커밋을 만들 필요는 없음 - -## 검증 -- 기본 검증 스크립트는 `python scripts/validate_workspace.py` -- Node 프로젝트면 `package.json`의 `lint`, `build`, `test` 스크립트를 자동 탐지해 순서대로 실행 -- 다른 스택이면 `HARNESS_VALIDATION_COMMANDS` 환경 변수에 줄바꿈 기준으로 검증 커맨드를 지정 - -## 명령어 -- `python scripts/execute.py `: Codex 기반 phase 순차 실행 -- `python scripts/execute.py --push`: phase 완료 후 브랜치 push -- `python scripts/validate_workspace.py`: 저장소 검증 diff --git a/Coding/Gemini/README.md b/Coding/Gemini/README.md deleted file mode 100644 index cad8f9e..0000000 --- a/Coding/Gemini/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Agentic-AI-Template - diff --git a/Coding/Gemini/docs/ADR.md b/Coding/Gemini/docs/ADR.md deleted file mode 100644 index 216ad6d..0000000 --- a/Coding/Gemini/docs/ADR.md +++ /dev/null @@ -1,21 +0,0 @@ -# Architecture Decision Records - -## 철학 -{프로젝트의 핵심 가치관 (예: MVP 속도 최우선. 외부 의존성 최소화. 작동하는 최소 구현을 선택.)} - ---- - -### ADR-001: {결정 사항 (예: Next.js App Router 선택)} -**결정**: {뭘 선택했는지} -**이유**: {왜 선택했는지} -**트레이드오프**: {뭘 포기했는지} - -### ADR-002: {결정 사항} -**결정**: {뭘 선택했는지} -**이유**: {왜 선택했는지} -**트레이드오프**: {뭘 포기했는지} - -### ADR-003: {결정 사항} -**결정**: {뭘 선택했는지} -**이유**: {왜 선택했는지} -**트레이드오프**: {뭘 포기했는지} diff --git a/Coding/Gemini/docs/ARCHITECTURE.md b/Coding/Gemini/docs/ARCHITECTURE.md deleted file mode 100644 index 2ff9891..0000000 --- a/Coding/Gemini/docs/ARCHITECTURE.md +++ /dev/null @@ -1,24 +0,0 @@ -# 아키텍처 - -## 디렉토리 구조 -``` -src/ -├── app/ # 페이지 + API 라우트 -├── components/ # UI 컴포넌트 -├── types/ # TypeScript 타입 정의 -├── lib/ # 유틸리티 + 헬퍼 -└── services/ # 외부 API 래퍼 -``` - -## 패턴 -{사용하는 디자인 패턴 (예: Server Components 기본, 인터랙션이 필요한 곳만 Client Component)} - -## 데이터 흐름 -``` -{데이터가 어떻게 흐르는지 (예: -사용자 입력 → Client Component → API Route → 외부 API → 응답 → UI 업데이트 -)} -``` - -## 상태 관리 -{상태 관리 방식 (예: 서버 상태는 Server Components, 클라이언트 상태는 useState/useReducer)} diff --git a/Coding/Gemini/docs/PRD.md b/Coding/Gemini/docs/PRD.md deleted file mode 100644 index b1950bb..0000000 --- a/Coding/Gemini/docs/PRD.md +++ /dev/null @@ -1,21 +0,0 @@ -# PRD: {프로젝트명} - -## 목표 -{이 프로젝트가 해결하려는 문제를 한 줄로 요약} - -## 사용자 -{누가 이 제품을 쓰는지} - -## 핵심 기능 -1. {기능 1} -2. {기능 2} -3. {기능 3} - -## MVP 제외 사항 -- {안 만들 것 1} -- {안 만들 것 2} -- {안 만들 것 3} - -## 디자인 -- {디자인 방향 (예: 다크모드 고정, 미니멀)} -- {색상 (예: 무채색 + 포인트 1가지)} diff --git a/Coding/Gemini/docs/UI_GUIDE.md b/Coding/Gemini/docs/UI_GUIDE.md deleted file mode 100644 index c3d280f..0000000 --- a/Coding/Gemini/docs/UI_GUIDE.md +++ /dev/null @@ -1,76 +0,0 @@ -# UI 디자인 가이드 - -## 디자인 원칙 -1. {원칙 1 — 예: "도구처럼 보여야 한다. 마케팅 페이지가 아니라 매일 쓰는 대시보드."} -2. {원칙 2} -3. {원칙 3} - -## AI 슬롭 안티패턴 — 하지 마라 -| 금지 사항 | 이유 | -|-----------|------| -| backdrop-filter: blur() | glass morphism은 AI 템플릿의 가장 흔한 징후 | -| gradient-text (배경 그라데이션 텍스트) | AI가 만든 SaaS 랜딩의 1번 특징 | -| "Powered by AI" 배지 | 기능이 아니라 장식. 사용자에게 가치 없음 | -| box-shadow 글로우 애니메이션 | 네온 글로우 = AI 슬롭 | -| 보라/인디고 브랜드 색상 | "AI = 보라색" 클리셰 | -| 모든 카드에 동일한 rounded-2xl | 균일한 둥근 모서리는 템플릿 느낌 | -| 배경 gradient orb (blur-3xl 원형) | 모든 AI 랜딩 페이지에 있는 장식 | - -## 색상 -### 배경 -| 용도 | 값 | -|------|------| -| 페이지 | {예: #0a0a0a} | -| 카드 | {예: #141414} | - -### 텍스트 -| 용도 | 값 | -|------|------| -| 주 텍스트 | {예: text-white} | -| 본문 | {예: text-neutral-300} | -| 보조 | {예: text-neutral-400} | -| 비활성 | {예: text-neutral-500} | - -### 데이터/시맨틱 색상 -| 용도 | 값 | -|------|------| -| {긍정/성공} | {예: #22c55e} | -| {부정/에러} | {예: #ef4444} | -| {중립/기본} | {예: #525252} | - -## 컴포넌트 -### 카드 -``` -{예: rounded-lg bg-[#141414] border border-neutral-800 p-6} -``` - -### 버튼 -``` -Primary: {예: rounded-lg bg-white text-black hover:bg-neutral-200} -Text: {예: text-neutral-500 hover:text-neutral-300} -``` - -### 입력 필드 -``` -{예: rounded-lg bg-neutral-900 border border-neutral-800 px-4 py-3} -``` - -## 레이아웃 -- 전체 너비: {예: max-w-5xl} -- 정렬: {예: 좌측 정렬 기본. 중앙 정렬 금지} -- 간격: {예: gap-3~4, 섹션 간 space-y-8} - -## 타이포그래피 -| 용도 | 스타일 | -|------|--------| -| 페이지 제목 | {예: text-4xl font-semibold text-white} | -| 카드 제목 | {예: text-sm font-medium text-neutral-400} | -| 본문 | {예: text-sm text-neutral-300 leading-relaxed} | - -## 애니메이션 -- {허용할 애니메이션만 나열. 예: fade-in (0.4s), slide-up (0.5s)} -- {그 외 모든 애니메이션 금지} - -## 아이콘 -- {예: SVG 인라인, strokeWidth 1.5} -- {예: 아이콘 컨테이너(둥근 배경 박스)로 감싸지 않는다} diff --git a/Coding/Gemini/plugins/harness-engineering/.codex-plugin/plugin.json b/Coding/Gemini/plugins/harness-engineering/.codex-plugin/plugin.json deleted file mode 100644 index 60b9951..0000000 --- a/Coding/Gemini/plugins/harness-engineering/.codex-plugin/plugin.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "harness-engineering", - "version": "1.0.0", - "description": "Repo-local Harness Engineering slash commands for Codex.", - "interface": { - "displayName": "Harness Engineering", - "shortDescription": "Harness planning and review prompts for this repo", - "longDescription": "Optional local plugin that exposes Harness Engineering slash commands while the core workflow remains in repo-native AGENTS, skills, custom agents, and hooks.", - "developerName": "Local Repository", - "category": "Productivity", - "capabilities": [ - "Interactive", - "Read", - "Write" - ], - "defaultPrompt": [ - "Use Harness Engineering to plan a new phase for this repository.", - "Review my changes against the Harness docs and rules." - ], - "brandColor": "#2563EB" - } -} diff --git a/Coding/Gemini/plugins/harness-engineering/agents/openai.yaml b/Coding/Gemini/plugins/harness-engineering/agents/openai.yaml deleted file mode 100644 index 2671cba..0000000 --- a/Coding/Gemini/plugins/harness-engineering/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Harness Engineering" - short_description: "Use Harness slash commands in this repository" - default_prompt: "Use Harness Engineering to plan a phase or review changes in this repository." diff --git a/Coding/Gemini/plugins/harness-engineering/commands/harness.md b/Coding/Gemini/plugins/harness-engineering/commands/harness.md deleted file mode 100644 index e4bc9df..0000000 --- a/Coding/Gemini/plugins/harness-engineering/commands/harness.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -description: Run the Harness Engineering planning workflow for this repository. ---- - -# /harness - -## Preflight - -- Read `/AGENTS.md`, `/docs/PRD.md`, `/docs/ARCHITECTURE.md`, `/docs/ADR.md`, and `/docs/UI_GUIDE.md` if they exist. -- Confirm whether the user wants discussion only, a draft plan, or file generation under `phases/`. -- Note whether the user explicitly asked for subagents; only then consider `phase_planner` or built-in explorers/workers. - -## Plan - -- State what will be created or updated before editing files. -- If a plan already exists under `phases/`, say whether you are extending it or replacing part of it. -- Keep each proposed step small, self-contained, and independently executable. - -## Commands - -- Invoke `$harness-workflow` and follow it. -- When file generation is requested, create or update: - - `phases/index.json` - - `phases/{phase}/index.json` - - `phases/{phase}/stepN.md` -- Use `python scripts/execute.py ` as the runtime target when you need to reference execution. - -## Verification - -- Re-read the generated phase files for consistency. -- Check that step numbering, phase names, and acceptance commands line up. -- If the repo has a validator, prefer `python scripts/validate_workspace.py` as the default acceptance command unless the user specified a narrower command. - -## Summary - -## Result -- **Action**: planned or generated Harness phase files -- **Status**: success | partial | failed -- **Details**: phase name, step count, and any blockers - -## Next Steps - -- Suggest the next natural command, usually `python scripts/execute.py ` or a focused edit to one generated step. diff --git a/Coding/Gemini/plugins/harness-engineering/commands/review.md b/Coding/Gemini/plugins/harness-engineering/commands/review.md deleted file mode 100644 index 568d6e6..0000000 --- a/Coding/Gemini/plugins/harness-engineering/commands/review.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -description: Review local changes against Harness repository rules and docs. ---- - -# /review - -## Preflight - -- Read `/AGENTS.md`, `/docs/ARCHITECTURE.md`, `/docs/ADR.md`, and `/docs/UI_GUIDE.md` if they exist. -- Identify the changed files or generated `phases/` artifacts that need review. -- If the user wants a delegated review, use the read-only custom agent `harness_reviewer` only when they explicitly asked for subagents. - -## Plan - -- State what evidence will be checked: docs, changed files, generated phase files, and validation output if available. -- Prioritize correctness, architecture drift, CRITICAL rule violations, and missing tests over style commentary. - -## Commands - -- Invoke `$harness-review`. -- Use Codex built-in `/review` when the user specifically wants a code-review style pass over the working tree or git diff. -- If validation is relevant, run `python scripts/validate_workspace.py` or explain why it was not run. - -## Verification - -- Confirm that every finding is tied to a file and an actual rule or behavioral risk. -- If no findings remain, say so explicitly and mention residual risks or missing evidence. - -## Summary - -## Result -- **Action**: reviewed Harness changes -- **Status**: success | partial | failed -- **Details**: findings, docs checked, and validation status - -## Next Steps - -- Suggest the smallest follow-up: fix the top finding, rerun validation, or execute a pending phase. diff --git a/Coding/Gemini/scripts/__pycache__/execute.cpython-312.pyc b/Coding/Gemini/scripts/__pycache__/execute.cpython-312.pyc deleted file mode 100644 index b92b9852e168e2e19d202ca8480ab07b6d54e620..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25704 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

nC+R#~s?dzi}Nm^^;4JuLGa|<=c~? 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 [--push] -""" - -import argparse -import contextlib -import json -import subprocess -import sys -import threading -import time -import types -from datetime import datetime, timezone, timedelta -from pathlib import Path -from typing import Optional - -ROOT = Path(__file__).resolve().parent.parent - - -@contextlib.contextmanager -def progress_indicator(label: str): - """터미널 진행 표시기. with 문으로 사용하며 .elapsed 로 경과 시간을 읽는다.""" - frames = "◐◓◑◒" - stop = threading.Event() - t0 = time.monotonic() - - def _animate(): - idx = 0 - while not stop.wait(0.12): - sec = int(time.monotonic() - t0) - sys.stderr.write(f"\r{frames[idx % len(frames)]} {label} [{sec}s]") - sys.stderr.flush() - idx += 1 - sys.stderr.write("\r" + " " * (len(label) + 20) + "\r") - sys.stderr.flush() - - th = threading.Thread(target=_animate, daemon=True) - th.start() - info = types.SimpleNamespace(elapsed=0.0) - try: - yield info - finally: - stop.set() - th.join() - info.elapsed = time.monotonic() - t0 - - -class StepExecutor: - """Phase 디렉토리 안의 step들을 순차 실행하는 하네스.""" - - MAX_RETRIES = 3 - FEAT_MSG = "feat({phase}): step {num} - {name}" - CHORE_MSG = "chore({phase}): step {num} output" - TZ = timezone(timedelta(hours=9)) - - def __init__(self, phase_dir_name: str, *, auto_push: bool = False): - self._root = str(ROOT) - self._phases_dir = ROOT / "phases" - self._phase_dir = self._phases_dir / phase_dir_name - self._phase_dir_name = phase_dir_name - self._top_index_file = self._phases_dir / "index.json" - self._auto_push = auto_push - - if not self._phase_dir.is_dir(): - print(f"ERROR: {self._phase_dir} not found") - sys.exit(1) - - self._index_file = self._phase_dir / "index.json" - if not self._index_file.exists(): - print(f"ERROR: {self._index_file} not found") - sys.exit(1) - - idx = self._read_json(self._index_file) - self._project = idx.get("project", "project") - self._phase_name = idx.get("phase", phase_dir_name) - self._total = len(idx["steps"]) - - def run(self): - self._print_header() - self._check_blockers() - self._checkout_branch() - guardrails = self._load_guardrails() - self._ensure_created_at() - self._execute_all_steps(guardrails) - self._finalize() - - # --- timestamps --- - - def _stamp(self) -> str: - return datetime.now(self.TZ).strftime("%Y-%m-%dT%H:%M:%S%z") - - # --- JSON I/O --- - - @staticmethod - def _read_json(p: Path) -> dict: - return json.loads(p.read_text(encoding="utf-8")) - - @staticmethod - def _write_json(p: Path, data: dict): - p.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8") - - # --- git --- - - def _run_git(self, *args) -> subprocess.CompletedProcess: - cmd = ["git"] + list(args) - return subprocess.run(cmd, cwd=self._root, capture_output=True, text=True) - - def _checkout_branch(self): - branch = f"feat-{self._phase_name}" - - r = self._run_git("rev-parse", "--abbrev-ref", "HEAD") - if r.returncode != 0: - print(f" ERROR: git을 사용할 수 없거나 git repo가 아닙니다.") - print(f" {r.stderr.strip()}") - sys.exit(1) - - if r.stdout.strip() == branch: - return - - r = self._run_git("rev-parse", "--verify", branch) - r = self._run_git("checkout", branch) if r.returncode == 0 else self._run_git("checkout", "-b", branch) - - if r.returncode != 0: - print(f" ERROR: 브랜치 '{branch}' checkout 실패.") - print(f" {r.stderr.strip()}") - print(f" Hint: 변경사항을 stash하거나 commit한 후 다시 시도하세요.") - sys.exit(1) - - print(f" Branch: {branch}") - - def _commit_step(self, step_num: int, step_name: str): - output_rel = f"phases/{self._phase_dir_name}/step{step_num}-output.json" - index_rel = f"phases/{self._phase_dir_name}/index.json" - - self._run_git("add", "-A") - self._run_git("reset", "HEAD", "--", output_rel) - self._run_git("reset", "HEAD", "--", index_rel) - - if self._run_git("diff", "--cached", "--quiet").returncode != 0: - msg = self.FEAT_MSG.format(phase=self._phase_name, num=step_num, name=step_name) - r = self._run_git("commit", "-m", msg) - if r.returncode == 0: - print(f" Commit: {msg}") - else: - print(f" WARN: 코드 커밋 실패: {r.stderr.strip()}") - - self._run_git("add", "-A") - if self._run_git("diff", "--cached", "--quiet").returncode != 0: - msg = self.CHORE_MSG.format(phase=self._phase_name, num=step_num) - r = self._run_git("commit", "-m", msg) - if r.returncode != 0: - print(f" WARN: housekeeping 커밋 실패: {r.stderr.strip()}") - - # --- top-level index --- - - def _update_top_index(self, status: str): - if not self._top_index_file.exists(): - return - top = self._read_json(self._top_index_file) - ts = self._stamp() - for phase in top.get("phases", []): - if phase.get("dir") == self._phase_dir_name: - phase["status"] = status - ts_key = {"completed": "completed_at", "error": "failed_at", "blocked": "blocked_at"}.get(status) - if ts_key: - phase[ts_key] = ts - break - self._write_json(self._top_index_file, top) - - # --- guardrails & context --- - - 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')}" - ) - docs_dir = ROOT / "docs" - if docs_dir.is_dir(): - for doc in sorted(docs_dir.glob("*.md")): - sections.append(f"## {doc.stem}\n\n{doc.read_text(encoding='utf-8')}") - return "\n\n---\n\n".join(sections) if sections else "" - - @staticmethod - def _build_step_context(index: dict) -> str: - lines = [ - f"- Step {s['step']} ({s['name']}): {s['summary']}" - for s in index["steps"] - if s["status"] == "completed" and s.get("summary") - ] - if not lines: - return "" - return "## 이전 Step 산출물\n\n" + "\n".join(lines) + "\n\n" - - def _build_preamble(self, guardrails: str, step_context: str, - prev_error: Optional[str] = None) -> str: - retry_section = "" - if prev_error: - retry_section = ( - f"\n## ⚠ 이전 시도 실패 — 아래 에러를 반드시 참고하여 수정하라\n\n" - f"{prev_error}\n\n---\n\n" - ) - return ( - f"당신은 {self._project} 프로젝트의 개발자입니다. 아래 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" - 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. 변경사항은 워킹 트리에 남겨라. step 완료 후 커밋은 execute.py가 정리한다.\n\n---\n\n" - ) - - # --- Codex 호출 --- - - def _invoke_codex(self, step: dict, preamble: str) -> dict: - step_num, step_name = step["step"], step["name"] - step_file = self._phase_dir / f"step{step_num}.md" - - if not step_file.exists(): - print(f" ERROR: {step_file} not found") - sys.exit(1) - - prompt = preamble + step_file.read_text(encoding="utf-8") - last_message_path = self._phase_dir / f"step{step_num}-last-message.txt" - result = subprocess.run( - ["codex", "exec", "--full-auto", "--json", "-C", self._root, "-o", str(last_message_path)], - cwd=self._root, - input=prompt, - capture_output=True, - text=True, - timeout=1800, - ) - - if result.returncode != 0: - print(f"\n WARN: Codex가 비정상 종료됨 (code {result.returncode})") - if result.stderr: - print(f" stderr: {result.stderr[:500]}") - - final_message = None - if last_message_path.exists(): - final_message = last_message_path.read_text(encoding="utf-8") - - output = { - "step": step_num, "name": step_name, - "exitCode": result.returncode, - "finalMessage": final_message, - "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: - json.dump(output, f, indent=2, ensure_ascii=False) - - return output - - # --- 헤더 & 검증 --- - - def _print_header(self): - print(f"\n{'='*60}") - print(f" Harness Step Executor") - print(f" Phase: {self._phase_name} | Steps: {self._total}") - if self._auto_push: - print(f" Auto-push: enabled") - print(f"{'='*60}") - - def _check_blockers(self): - index = self._read_json(self._index_file) - for s in reversed(index["steps"]): - 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.") - 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.") - sys.exit(2) - if s["status"] != "pending": - break - - def _ensure_created_at(self): - index = self._read_json(self._index_file) - if "created_at" not in index: - index["created_at"] = self._stamp() - self._write_json(self._index_file, index) - - # --- 실행 루프 --- - - def _execute_single_step(self, step: dict, guardrails: str) -> bool: - """단일 step 실행 (재시도 포함). 완료되면 True, 실패/차단이면 False.""" - step_num, step_name = step["step"], step["name"] - done = sum(1 for s in self._read_json(self._index_file)["steps"] if s["status"] == "completed") - prev_error = None - - for attempt in range(1, self.MAX_RETRIES + 1): - index = self._read_json(self._index_file) - step_context = self._build_step_context(index) - preamble = self._build_preamble(guardrails, step_context, prev_error) - - tag = f"Step {step_num}/{self._total - 1} ({done} done): {step_name}" - if attempt > 1: - tag += f" [retry {attempt}/{self.MAX_RETRIES}]" - - with progress_indicator(tag) as pi: - self._invoke_codex(step, preamble) - elapsed = int(pi.elapsed) - - index = self._read_json(self._index_file) - status = next((s.get("status", "pending") for s in index["steps"] if s["step"] == step_num), "pending") - ts = self._stamp() - - if status == "completed": - for s in index["steps"]: - if s["step"] == step_num: - s["completed_at"] = ts - self._write_json(self._index_file, index) - self._commit_step(step_num, step_name) - print(f" ✓ Step {step_num}: {step_name} [{elapsed}s]") - return True - - if status == "blocked": - for s in index["steps"]: - if s["step"] == step_num: - s["blocked_at"] = ts - self._write_json(self._index_file, index) - reason = next((s.get("blocked_reason", "") for s in index["steps"] if s["step"] == step_num), "") - print(f" ⏸ Step {step_num}: {step_name} blocked [{elapsed}s]") - print(f" Reason: {reason}") - self._update_top_index("blocked") - sys.exit(2) - - err_msg = next( - (s.get("error_message", "Step did not update status") for s in index["steps"] if s["step"] == step_num), - "Step did not update status", - ) - - if attempt < self.MAX_RETRIES: - for s in index["steps"]: - if s["step"] == step_num: - s["status"] = "pending" - s.pop("error_message", None) - self._write_json(self._index_file, index) - prev_error = err_msg - print(f" ↻ Step {step_num}: retry {attempt}/{self.MAX_RETRIES} — {err_msg}") - else: - for s in index["steps"]: - if s["step"] == step_num: - s["status"] = "error" - s["error_message"] = f"[{self.MAX_RETRIES}회 시도 후 실패] {err_msg}" - s["failed_at"] = ts - self._write_json(self._index_file, index) - self._commit_step(step_num, step_name) - print(f" ✗ Step {step_num}: {step_name} failed after {self.MAX_RETRIES} attempts [{elapsed}s]") - print(f" Error: {err_msg}") - self._update_top_index("error") - sys.exit(1) - - return False # unreachable - - def _execute_all_steps(self, guardrails: str): - while True: - index = self._read_json(self._index_file) - pending = next((s for s in index["steps"] if s["status"] == "pending"), None) - if pending is None: - print("\n All steps completed!") - return - - step_num = pending["step"] - for s in index["steps"]: - if s["step"] == step_num and "started_at" not in s: - s["started_at"] = self._stamp() - self._write_json(self._index_file, index) - break - - self._execute_single_step(pending, guardrails) - - def _finalize(self): - index = self._read_json(self._index_file) - index["completed_at"] = self._stamp() - self._write_json(self._index_file, index) - self._update_top_index("completed") - - self._run_git("add", "-A") - if self._run_git("diff", "--cached", "--quiet").returncode != 0: - msg = f"chore({self._phase_name}): mark phase completed" - r = self._run_git("commit", "-m", msg) - if r.returncode == 0: - print(f" ✓ {msg}") - - if self._auto_push: - branch = f"feat-{self._phase_name}" - r = self._run_git("push", "-u", "origin", branch) - if r.returncode != 0: - print(f"\n ERROR: git push 실패: {r.stderr.strip()}") - sys.exit(1) - print(f" ✓ Pushed to origin/{branch}") - - print(f"\n{'='*60}") - print(f" Phase '{self._phase_name}' completed!") - print(f"{'='*60}") - - -def main(): - parser = argparse.ArgumentParser(description="Harness Step Executor") - parser.add_argument("phase_dir", help="Phase directory name (e.g. 0-mvp)") - parser.add_argument("--push", action="store_true", help="Push branch after completion") - args = parser.parse_args() - - StepExecutor(args.phase_dir, auto_push=args.push).run() - - -if __name__ == "__main__": - main() diff --git a/Coding/Gemini/scripts/test_execute.py b/Coding/Gemini/scripts/test_execute.py deleted file mode 100644 index 1039319..0000000 --- a/Coding/Gemini/scripts/test_execute.py +++ /dev/null @@ -1,562 +0,0 @@ -"""execute.py Codex migration safety-net tests.""" - -import json -import sys -from datetime import datetime, timedelta -from pathlib import Path -from unittest.mock import patch, MagicMock - -import pytest - -sys.path.insert(0, str(Path(__file__).parent)) -import execute as ex - - -# --------------------------------------------------------------------------- -# Fixtures -# --------------------------------------------------------------------------- - -@pytest.fixture -def tmp_project(tmp_path): - """phases/, AGENTS.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") - - docs_dir = tmp_path / "docs" - docs_dir.mkdir() - (docs_dir / "arch.md").write_text("# Architecture\nSome content") - (docs_dir / "guide.md").write_text("# Guide\nAnother doc") - - return tmp_path - - -@pytest.fixture -def phase_dir(tmp_project): - """step 3개를 가진 phase 디렉토리.""" - d = tmp_project / "phases" / "0-mvp" - d.mkdir() - - index = { - "project": "TestProject", - "phase": "mvp", - "steps": [ - {"step": 0, "name": "setup", "status": "completed", "summary": "프로젝트 초기화 완료"}, - {"step": 1, "name": "core", "status": "completed", "summary": "핵심 로직 구현"}, - {"step": 2, "name": "ui", "status": "pending"}, - ], - } - (d / "index.json").write_text(json.dumps(index, indent=2, ensure_ascii=False)) - (d / "step2.md").write_text("# Step 2: UI\n\nUI를 구현하세요.") - - return d - - -@pytest.fixture -def top_index(tmp_project): - """phases/index.json (top-level).""" - top = { - "phases": [ - {"dir": "0-mvp", "status": "pending"}, - {"dir": "1-polish", "status": "pending"}, - ] - } - p = tmp_project / "phases" / "index.json" - p.write_text(json.dumps(top, indent=2)) - return p - - -@pytest.fixture -def executor(tmp_project, phase_dir): - """테스트용 StepExecutor 인스턴스. git 호출은 별도 mock 필요.""" - with patch.object(ex, "ROOT", tmp_project): - inst = ex.StepExecutor("0-mvp") - # 내부 경로를 tmp_project 기준으로 재설정 - inst._root = str(tmp_project) - inst._phases_dir = tmp_project / "phases" - inst._phase_dir = phase_dir - inst._phase_dir_name = "0-mvp" - inst._index_file = phase_dir / "index.json" - inst._top_index_file = tmp_project / "phases" / "index.json" - return inst - - -# --------------------------------------------------------------------------- -# _stamp (= 이전 now_iso) -# --------------------------------------------------------------------------- - -class TestStamp: - def test_returns_kst_timestamp(self, executor): - result = executor._stamp() - assert "+0900" in result - - def test_format_is_iso(self, executor): - result = executor._stamp() - dt = datetime.strptime(result, "%Y-%m-%dT%H:%M:%S%z") - assert dt.tzinfo is not None - - def test_is_current_time(self, executor): - before = datetime.now(ex.StepExecutor.TZ).replace(microsecond=0) - result = executor._stamp() - after = datetime.now(ex.StepExecutor.TZ).replace(microsecond=0) + timedelta(seconds=1) - parsed = datetime.strptime(result, "%Y-%m-%dT%H:%M:%S%z") - assert before <= parsed <= after - - -# --------------------------------------------------------------------------- -# _read_json / _write_json -# --------------------------------------------------------------------------- - -class TestJsonHelpers: - def test_roundtrip(self, tmp_path): - data = {"key": "값", "nested": [1, 2, 3]} - p = tmp_path / "test.json" - ex.StepExecutor._write_json(p, data) - loaded = ex.StepExecutor._read_json(p) - assert loaded == data - - def test_save_ensures_ascii_false(self, tmp_path): - p = tmp_path / "test.json" - ex.StepExecutor._write_json(p, {"한글": "테스트"}) - raw = p.read_text() - assert "한글" in raw - assert "\\u" not in raw - - def test_save_indented(self, tmp_path): - p = tmp_path / "test.json" - ex.StepExecutor._write_json(p, {"a": 1}) - raw = p.read_text() - assert "\n" in raw - - def test_load_nonexistent_raises(self, tmp_path): - with pytest.raises(FileNotFoundError): - ex.StepExecutor._read_json(tmp_path / "nope.json") - - -# --------------------------------------------------------------------------- -# _load_guardrails -# --------------------------------------------------------------------------- - -class TestLoadGuardrails: - def test_loads_agents_md_and_docs(self, executor, tmp_project): - with patch.object(ex, "ROOT", tmp_project): - result = executor._load_guardrails() - assert "# Rules" in result - assert "rule one" in result - assert "# Architecture" in result - assert "# Guide" in result - - def test_sections_separated_by_divider(self, executor, tmp_project): - with patch.object(ex, "ROOT", tmp_project): - result = executor._load_guardrails() - assert "---" in result - - def test_docs_sorted_alphabetically(self, executor, tmp_project): - with patch.object(ex, "ROOT", tmp_project): - result = executor._load_guardrails() - arch_pos = result.index("arch") - guide_pos = result.index("guide") - assert arch_pos < guide_pos - - def test_no_agents_md(self, executor, tmp_project): - (tmp_project / "AGENTS.md").unlink() - with patch.object(ex, "ROOT", tmp_project): - result = executor._load_guardrails() - assert "AGENTS.md" not in result - assert "Architecture" in result - - def test_no_docs_dir(self, executor, tmp_project): - import shutil - shutil.rmtree(tmp_project / "docs") - with patch.object(ex, "ROOT", tmp_project): - result = executor._load_guardrails() - assert "Rules" in result - assert "Architecture" not in result - - def test_empty_project(self, tmp_path): - with patch.object(ex, "ROOT", tmp_path): - # executor가 필요 없는 static-like 동작이므로 임시 인스턴스 - phases_dir = tmp_path / "phases" / "dummy" - phases_dir.mkdir(parents=True) - idx = {"project": "T", "phase": "t", "steps": []} - (phases_dir / "index.json").write_text(json.dumps(idx)) - inst = ex.StepExecutor.__new__(ex.StepExecutor) - result = inst._load_guardrails() - assert result == "" - - -# --------------------------------------------------------------------------- -# _build_step_context -# --------------------------------------------------------------------------- - -class TestBuildStepContext: - def test_includes_completed_with_summary(self, phase_dir): - index = json.loads((phase_dir / "index.json").read_text()) - result = ex.StepExecutor._build_step_context(index) - assert "Step 0 (setup): 프로젝트 초기화 완료" in result - assert "Step 1 (core): 핵심 로직 구현" in result - - def test_excludes_pending(self, phase_dir): - index = json.loads((phase_dir / "index.json").read_text()) - result = ex.StepExecutor._build_step_context(index) - assert "ui" not in result - - def test_excludes_completed_without_summary(self, phase_dir): - index = json.loads((phase_dir / "index.json").read_text()) - del index["steps"][0]["summary"] - result = ex.StepExecutor._build_step_context(index) - assert "setup" not in result - assert "core" in result - - def test_empty_when_no_completed(self): - index = {"steps": [{"step": 0, "name": "a", "status": "pending"}]} - result = ex.StepExecutor._build_step_context(index) - assert result == "" - - def test_has_header(self, phase_dir): - index = json.loads((phase_dir / "index.json").read_text()) - result = ex.StepExecutor._build_step_context(index) - assert result.startswith("## 이전 Step 산출물") - - -# --------------------------------------------------------------------------- -# _build_preamble -# --------------------------------------------------------------------------- - -class TestBuildPreamble: - def test_includes_project_name(self, executor): - result = executor._build_preamble("", "") - assert "TestProject" in result - - def test_includes_guardrails(self, executor): - result = executor._build_preamble("GUARD_CONTENT", "") - assert "GUARD_CONTENT" in result - - def test_includes_step_context(self, executor): - ctx = "## 이전 Step 산출물\n\n- Step 0: done" - result = executor._build_preamble("", ctx) - assert "이전 Step 산출물" in result - - def test_mentions_executor_commits(self, executor): - result = executor._build_preamble("", "") - assert "커밋은 execute.py가 정리한다" in result - - def test_includes_rules(self, executor): - result = executor._build_preamble("", "") - assert "작업 규칙" in result - assert "AC" in result - - def test_no_retry_section_by_default(self, executor): - result = executor._build_preamble("", "") - assert "이전 시도 실패" not in result - - def test_retry_section_with_prev_error(self, executor): - result = executor._build_preamble("", "", prev_error="타입 에러 발생") - assert "이전 시도 실패" in result - assert "타입 에러 발생" in result - - def test_includes_max_retries(self, executor): - result = executor._build_preamble("", "") - assert str(ex.StepExecutor.MAX_RETRIES) in result - - def test_includes_index_path(self, executor): - result = executor._build_preamble("", "") - assert "/phases/0-mvp/index.json" in result - - -# --------------------------------------------------------------------------- -# _update_top_index -# --------------------------------------------------------------------------- - -class TestUpdateTopIndex: - def test_completed(self, executor, top_index): - executor._top_index_file = top_index - executor._update_top_index("completed") - data = json.loads(top_index.read_text()) - mvp = next(p for p in data["phases"] if p["dir"] == "0-mvp") - assert mvp["status"] == "completed" - assert "completed_at" in mvp - - def test_error(self, executor, top_index): - executor._top_index_file = top_index - executor._update_top_index("error") - data = json.loads(top_index.read_text()) - mvp = next(p for p in data["phases"] if p["dir"] == "0-mvp") - assert mvp["status"] == "error" - assert "failed_at" in mvp - - def test_blocked(self, executor, top_index): - executor._top_index_file = top_index - executor._update_top_index("blocked") - data = json.loads(top_index.read_text()) - mvp = next(p for p in data["phases"] if p["dir"] == "0-mvp") - assert mvp["status"] == "blocked" - assert "blocked_at" in mvp - - def test_other_phases_unchanged(self, executor, top_index): - executor._top_index_file = top_index - executor._update_top_index("completed") - data = json.loads(top_index.read_text()) - polish = next(p for p in data["phases"] if p["dir"] == "1-polish") - assert polish["status"] == "pending" - - def test_nonexistent_dir_is_noop(self, executor, top_index): - executor._top_index_file = top_index - executor._phase_dir_name = "no-such-dir" - original = json.loads(top_index.read_text()) - executor._update_top_index("completed") - after = json.loads(top_index.read_text()) - for p_before, p_after in zip(original["phases"], after["phases"]): - assert p_before["status"] == p_after["status"] - - def test_no_top_index_file(self, executor, tmp_path): - executor._top_index_file = tmp_path / "nonexistent.json" - executor._update_top_index("completed") # should not raise - - -# --------------------------------------------------------------------------- -# _checkout_branch (mocked) -# --------------------------------------------------------------------------- - -class TestCheckoutBranch: - def _mock_git(self, executor, responses): - call_idx = {"i": 0} - def fake_git(*args): - idx = call_idx["i"] - call_idx["i"] += 1 - if idx < len(responses): - return responses[idx] - return MagicMock(returncode=0, stdout="", stderr="") - executor._run_git = fake_git - - def test_already_on_branch(self, executor): - self._mock_git(executor, [ - MagicMock(returncode=0, stdout="feat-mvp\n", stderr=""), - ]) - executor._checkout_branch() # should return without checkout - - def test_branch_exists_checkout(self, executor): - self._mock_git(executor, [ - MagicMock(returncode=0, stdout="main\n", stderr=""), - MagicMock(returncode=0, stdout="", stderr=""), - MagicMock(returncode=0, stdout="", stderr=""), - ]) - executor._checkout_branch() - - def test_branch_not_exists_create(self, executor): - self._mock_git(executor, [ - MagicMock(returncode=0, stdout="main\n", stderr=""), - MagicMock(returncode=1, stdout="", stderr="not found"), - MagicMock(returncode=0, stdout="", stderr=""), - ]) - executor._checkout_branch() - - def test_checkout_fails_exits(self, executor): - self._mock_git(executor, [ - MagicMock(returncode=0, stdout="main\n", stderr=""), - MagicMock(returncode=1, stdout="", stderr=""), - MagicMock(returncode=1, stdout="", stderr="dirty tree"), - ]) - with pytest.raises(SystemExit) as exc_info: - executor._checkout_branch() - assert exc_info.value.code == 1 - - def test_no_git_exits(self, executor): - self._mock_git(executor, [ - MagicMock(returncode=1, stdout="", stderr="not a git repo"), - ]) - with pytest.raises(SystemExit) as exc_info: - executor._checkout_branch() - assert exc_info.value.code == 1 - - -# --------------------------------------------------------------------------- -# _commit_step (mocked) -# --------------------------------------------------------------------------- - -class TestCommitStep: - def test_two_phase_commit(self, executor): - calls = [] - def fake_git(*args): - calls.append(args) - if args[:2] == ("diff", "--cached"): - return MagicMock(returncode=1) - return MagicMock(returncode=0, stdout="", stderr="") - executor._run_git = fake_git - - executor._commit_step(2, "ui") - - commit_calls = [c for c in calls if c[0] == "commit"] - assert len(commit_calls) == 2 - assert "feat(mvp):" in commit_calls[0][2] - assert "chore(mvp):" in commit_calls[1][2] - - def test_no_code_changes_skips_feat_commit(self, executor): - call_count = {"diff": 0} - calls = [] - def fake_git(*args): - calls.append(args) - if args[:2] == ("diff", "--cached"): - call_count["diff"] += 1 - if call_count["diff"] == 1: - return MagicMock(returncode=0) - return MagicMock(returncode=1) - return MagicMock(returncode=0, stdout="", stderr="") - executor._run_git = fake_git - - executor._commit_step(2, "ui") - - commit_msgs = [c[2] for c in calls if c[0] == "commit"] - assert len(commit_msgs) == 1 - assert "chore" in commit_msgs[0] - - -# --------------------------------------------------------------------------- -# _invoke_codex (mocked) -# --------------------------------------------------------------------------- - -class TestInvokeCodex: - def test_invokes_codex_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) - - cmd = mock_run.call_args[0][0] - kwargs = mock_run.call_args[1] - assert cmd[0] == "codex" - assert cmd[1] == "exec" - assert "--full-auto" in cmd - assert "--json" in cmd - assert "-o" in cmd - assert "PREAMBLE" in kwargs["input"] - assert "UI를 구현하세요" in kwargs["input"] - assert output["finalMessage"] is None - - def test_saves_output_json(self, executor): - def fake_run(*args, **kwargs): - cmd = args[0] - last_message_path = Path(cmd[cmd.index("-o") + 1]) - last_message_path.write_text("completed", encoding="utf-8") - return MagicMock(returncode=0, stdout='{"ok": true}', stderr="") - - step = {"step": 2, "name": "ui"} - - with patch("subprocess.run", side_effect=fake_run): - executor._invoke_codex(step, "preamble") - - output_file = executor._phase_dir / "step2-output.json" - assert output_file.exists() - data = json.loads(output_file.read_text()) - assert data["step"] == 2 - assert data["name"] == "ui" - assert data["exitCode"] == 0 - assert data["finalMessage"] == "completed" - - 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") - assert exc_info.value.code == 1 - - def test_timeout_is_1800(self, executor): - mock_result = MagicMock(returncode=0, stdout="{}", stderr="") - step = {"step": 2, "name": "ui"} - - with patch("subprocess.run", return_value=mock_result) as mock_run: - executor._invoke_codex(step, "preamble") - - assert mock_run.call_args[1]["timeout"] == 1800 - - -# --------------------------------------------------------------------------- -# progress_indicator (= 이전 Spinner) -# --------------------------------------------------------------------------- - -class TestProgressIndicator: - def test_context_manager(self): - import time - with ex.progress_indicator("test") as pi: - time.sleep(0.15) - assert pi.elapsed >= 0.1 - - def test_elapsed_increases(self): - import time - with ex.progress_indicator("test") as pi: - time.sleep(0.2) - assert pi.elapsed > 0 - - -# --------------------------------------------------------------------------- -# main() CLI 파싱 (mocked) -# --------------------------------------------------------------------------- - -class TestMainCli: - def test_no_args_exits(self): - with patch("sys.argv", ["execute.py"]): - with pytest.raises(SystemExit) as exc_info: - ex.main() - assert exc_info.value.code == 2 # argparse exits with 2 - - def test_invalid_phase_dir_exits(self): - with patch("sys.argv", ["execute.py", "nonexistent"]): - with patch.object(ex, "ROOT", Path("/tmp/fake_nonexistent")): - with pytest.raises(SystemExit) as exc_info: - ex.main() - assert exc_info.value.code == 1 - - def test_missing_index_exits(self, tmp_project): - (tmp_project / "phases" / "empty").mkdir() - with patch("sys.argv", ["execute.py", "empty"]): - with patch.object(ex, "ROOT", tmp_project): - with pytest.raises(SystemExit) as exc_info: - ex.main() - assert exc_info.value.code == 1 - - -# --------------------------------------------------------------------------- -# _check_blockers (= 이전 main() error/blocked 체크) -# --------------------------------------------------------------------------- - -class TestCheckBlockers: - def _make_executor_with_steps(self, tmp_project, steps): - d = tmp_project / "phases" / "test-phase" - d.mkdir(exist_ok=True) - index = {"project": "T", "phase": "test", "steps": steps} - (d / "index.json").write_text(json.dumps(index)) - - with patch.object(ex, "ROOT", tmp_project): - inst = ex.StepExecutor.__new__(ex.StepExecutor) - inst._root = str(tmp_project) - inst._phases_dir = tmp_project / "phases" - inst._phase_dir = d - inst._phase_dir_name = "test-phase" - inst._index_file = d / "index.json" - inst._top_index_file = tmp_project / "phases" / "index.json" - inst._phase_name = "test" - inst._total = len(steps) - return inst - - def test_error_step_exits_1(self, tmp_project): - steps = [ - {"step": 0, "name": "ok", "status": "completed"}, - {"step": 1, "name": "bad", "status": "error", "error_message": "fail"}, - ] - inst = self._make_executor_with_steps(tmp_project, steps) - with pytest.raises(SystemExit) as exc_info: - inst._check_blockers() - assert exc_info.value.code == 1 - - def test_blocked_step_exits_2(self, tmp_project): - steps = [ - {"step": 0, "name": "ok", "status": "completed"}, - {"step": 1, "name": "stuck", "status": "blocked", "blocked_reason": "API key"}, - ] - inst = self._make_executor_with_steps(tmp_project, steps) - with pytest.raises(SystemExit) as exc_info: - inst._check_blockers() - assert exc_info.value.code == 2 diff --git a/Coding/Gemini/scripts/validate_workspace.py b/Coding/Gemini/scripts/validate_workspace.py deleted file mode 100644 index f7c0526..0000000 --- a/Coding/Gemini/scripts/validate_workspace.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -"""Run repository validation commands for the Harness template.""" - -from __future__ import annotations - -import json -import os -import subprocess -import sys -from pathlib import Path - - -DEFAULT_NPM_ORDER = ("lint", "build", "test") - - -def load_env_commands() -> list[str]: - raw = os.environ.get("HARNESS_VALIDATION_COMMANDS", "") - return [line.strip() for line in raw.splitlines() if line.strip()] - - -def load_npm_commands(root: Path) -> list[str]: - package_json = root / "package.json" - if not package_json.exists(): - return [] - - try: - payload = json.loads(package_json.read_text(encoding="utf-8")) - except json.JSONDecodeError: - return [] - - scripts = payload.get("scripts", {}) - if not isinstance(scripts, dict): - return [] - - commands = [] - for name in DEFAULT_NPM_ORDER: - value = scripts.get(name) - if isinstance(value, str) and value.strip(): - commands.append(f"npm run {name}") - return commands - - -def discover_commands(root: Path) -> list[str]: - env_commands = load_env_commands() - if env_commands: - return env_commands - return load_npm_commands(root) - - -def run_command(command: str, root: Path) -> subprocess.CompletedProcess: - return subprocess.run( - command, - cwd=root, - shell=True, - capture_output=True, - text=True, - ) - - -def emit_stream(prefix: str, content: str, *, stream) -> None: - text = content.strip() - if not text: - return - print(prefix, file=stream) - print(text, file=stream) - - -def main() -> int: - root = Path(__file__).resolve().parent.parent - commands = discover_commands(root) - - if not commands: - print("No validation commands configured.") - print("Set HARNESS_VALIDATION_COMMANDS or add npm scripts for lint/build/test.") - return 0 - - for command in commands: - print(f"$ {command}") - result = run_command(command, root) - emit_stream("[stdout]", result.stdout, stream=sys.stdout) - emit_stream("[stderr]", result.stderr, stream=sys.stderr) - if result.returncode != 0: - print(f"Validation failed: {command}", file=sys.stderr) - return result.returncode - - print("Validation succeeded.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main())