diff --git a/Codex/Coding/Codex-cpp/.codex/config.toml b/Codex/Coding/Codex-cpp/.codex/config.toml index b75aa36..151b8b6 100644 --- a/Codex/Coding/Codex-cpp/.codex/config.toml +++ b/Codex/Coding/Codex-cpp/.codex/config.toml @@ -1,4 +1,4 @@ #:schema https://developers.openai.com/codex/config-schema.json [features] -codex_hooks = true +hooks = true diff --git a/Codex/Coding/Codex-cs/.codex/config.toml b/Codex/Coding/Codex-cs/.codex/config.toml new file mode 100644 index 0000000..151b8b6 --- /dev/null +++ b/Codex/Coding/Codex-cs/.codex/config.toml @@ -0,0 +1,4 @@ +#:schema https://developers.openai.com/codex/config-schema.json + +[features] +hooks = true diff --git a/Codex/Coding/Codex-cs/.codex/hooks.json b/Codex/Coding/Codex-cs/.codex/hooks.json new file mode 100644 index 0000000..0229d62 --- /dev/null +++ b/Codex/Coding/Codex-cs/.codex/hooks.json @@ -0,0 +1,28 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "^Bash$", + "hooks": [ + { + "type": "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" + } + ] + }, + { + "matcher": "^(apply_patch|Edit|Write)$", + "hooks": [ + { + "type": "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' / 'tdd-guard.py'), run_name='__main__')\"", + "timeout": 30, + "statusMessage": "Checking TDD guard" + } + ] + } + ] + } +} diff --git a/Codex/Coding/Codex-cs/.codex/hooks/pre_commit_checks.py b/Codex/Coding/Codex-cs/.codex/hooks/pre_commit_checks.py new file mode 100644 index 0000000..92dd51d --- /dev/null +++ b/Codex/Coding/Codex-cs/.codex/hooks/pre_commit_checks.py @@ -0,0 +1,89 @@ +import json +import re +import subprocess +import sys +from pathlib import Path + + +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 _is_git_commit(command: str) -> bool: + return re.search( + r"^\s*git(?:\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 _build_pre_commit_commands(root: Path) -> list[list[str]]: + return [ + [sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py"], + [sys.executable, "scripts/validate_workspace.py"], + ] + + +def _run_checks(root: Path) -> str | None: + for command in _build_pre_commit_commands(root): + result = subprocess.run(command, cwd=root, capture_output=True, text=True) + if result.returncode != 0: + details = _tail(result.stdout + "\n" + result.stderr) + label = " ".join(command) + if details: + return f"{label} failed:\n{details}" + return f"{label} 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) + if failure: + _deny(f"PRE-COMMIT CHECKS: {failure}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Codex/Coding/Codex-cs/.codex/hooks/tdd-guard.py b/Codex/Coding/Codex-cs/.codex/hooks/tdd-guard.py new file mode 100644 index 0000000..04531a1 --- /dev/null +++ b/Codex/Coding/Codex-cs/.codex/hooks/tdd-guard.py @@ -0,0 +1,183 @@ +import json +import subprocess +import sys +from pathlib import Path + + +SOURCE_SUFFIXES = {".cs"} +TEST_SUFFIXES = {".cs"} + + +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 _path_segments(path_text: str) -> list[str]: + return [segment for segment in _normalize(path_text).split("/") if segment] + + +def _is_ignored_path(path_text: str) -> bool: + return any(segment in {"bin", "obj"} for segment in _path_segments(path_text)) + + +def _is_test_path(path_text: str) -> bool: + path = Path(path_text) + if path.suffix.lower() not in TEST_SUFFIXES: + return False + + segments = _path_segments(path_text) + name = segments[-1] if segments else "" + directories = segments[:-1] + return ( + any( + segment in {"test", "tests"} + or segment.endswith(".test") + or segment.endswith(".tests") + for segment in directories + ) + or name.endswith("test.cs") + or name.endswith("tests.cs") + ) + + +def _token(text: str) -> str: + return "".join(ch for ch in text.lower() if ch.isalnum()) + + +def _candidate_test_paths(paths: list[str], cwd: Path, root: Path) -> list[Path]: + candidates: list[Path] = [] + for path_text in paths: + resolved = _resolve_path(path_text, cwd) + if _is_test_path(str(resolved)) and not _is_ignored_path(str(resolved)): + candidates.append(resolved) + + for candidate in root.rglob("*"): + if ( + candidate.is_file() + and candidate.suffix.lower() in TEST_SUFFIXES + and _is_test_path(str(candidate)) + and not _is_ignored_path(str(candidate)) + ): + candidates.append(candidate) + + return candidates + + +def _has_related_test(path: Path, candidate_tests: list[Path]) -> bool: + source_token = _token(path.stem) + for test_path in candidate_tests: + test_token = _token(test_path.stem) + if source_token and source_token in test_token: + return True + return False + + +def _is_exempt(path_text: str) -> bool: + return _is_test_path(path_text) + + +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: + return path.stem + + +def _guarded_paths(paths: list[str], cwd: Path, root: Path) -> list[str]: + missing_tests: list[str] = [] + candidate_tests = _candidate_test_paths(paths, cwd, root) + 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_related_test(path, candidate_tests): + 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 C# test file for " + f"{names}. Write or add the test first." + ), + } + } + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Codex/Coding/Codex-cs/.codex/skills/harness-review/SKILL.md b/Codex/Coding/Codex-cs/.codex/skills/harness-review/SKILL.md new file mode 100644 index 0000000..6de8994 --- /dev/null +++ b/Codex/Coding/Codex-cs/.codex/skills/harness-review/SKILL.md @@ -0,0 +1,46 @@ +--- +name: harness-review +description: "Use when reviewing this C#/.NET Harness repository: local changes, generated phase files, step outputs, implementation diffs, missing tests, dotnet validation readiness, or compliance with AGENTS.md, docs/ARCHITECTURE.md, docs/ADR.md, and Harness acceptance criteria." +--- + +# Harness Review + +## Overview + +Use this skill to review Harness work against the repository's persistent rules, architecture docs, C#/.NET SDK constraints, TDD guard policy, and executable verification requirements. Prioritize bugs, regressions, missing tests, and rule violations. + +## Review Process + +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, C#/.NET stack choices, related C# test coverage, critical rules, and `dotnet` validation readiness. +4. Run Python Harness self-tests when relevant and use `python scripts/validate_workspace.py` for the default build/test review. If a command cannot be run, report that as residual risk. +5. Lead with actionable findings. Keep summaries secondary. + +## Checklist + +| Item | Question | +| --- | --- | +| Architecture | Does the change follow `docs/ARCHITECTURE.md` ownership boundaries? | +| Stack | Does the change stay within the C#/.NET SDK and `dotnet` CLI decisions documented in `docs/ADR.md`, using the installed `dotnet` without SDK installation or `global.json` pinning? | +| Tests | Are new or changed behaviors covered by Python Harness tests or related C# tests? | +| TDD Guard | Would C# production `.cs` edits be blocked without a related C# test file? | +| Critical Rules | Does the change violate any `AGENTS.md` CRITICAL rule? | +| Build | Does `python scripts/validate_workspace.py` honor `HARNESS_VALIDATION_COMMANDS`, otherwise prefer one root `.sln` and fall back to one root `.csproj`, then run the documented `dotnet` validation sequence? Do the Python self-tests and workspace validation pass? | + +When no root `.sln` or `.csproj` exists, treat the C#/.NET guidance messages `No C#/.NET validation target found.` and `Add a root .sln or .csproj, or set HARNESS_VALIDATION_COMMANDS.` with a successful exit as the expected no-project path, not a build-readiness failure. + +## Output Format + +If there are findings, list them first in severity order with file and line references when possible. Then include this table: + +| 항목 | 결과 | 비고 | +| --- | --- | --- | +| 아키텍처 준수 | PASS/FAIL | {상세} | +| 기술 스택 준수 | PASS/FAIL | {상세} | +| 테스트 존재 | PASS/FAIL | {상세} | +| TDD Guard | PASS/FAIL | {상세} | +| CRITICAL 규칙 | PASS/FAIL | {상세} | +| 빌드/검증 가능 | PASS/FAIL | {상세} | + +When there are no findings, say that clearly, then mention any commands not run or remaining risk. diff --git a/Codex/Coding/Codex-cs/.codex/skills/harness-review/agents/openai.yaml b/Codex/Coding/Codex-cs/.codex/skills/harness-review/agents/openai.yaml new file mode 100644 index 0000000..9af296a --- /dev/null +++ b/Codex/Coding/Codex-cs/.codex/skills/harness-review/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Harness Review" + short_description: "Review Harness changes safely" + default_prompt: "Use $harness-review to review Harness repository changes." diff --git a/Codex/Coding/Codex-cs/.codex/skills/harness-workflow/SKILL.md b/Codex/Coding/Codex-cs/.codex/skills/harness-workflow/SKILL.md new file mode 100644 index 0000000..ded7ff2 --- /dev/null +++ b/Codex/Coding/Codex-cs/.codex/skills/harness-workflow/SKILL.md @@ -0,0 +1,132 @@ +--- +name: harness-workflow +description: "Use when planning or running this C#/.NET 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 + +## Overview + +Use this skill to turn a user-approved task into small, self-contained Harness steps that another Codex session can execute reliably. This Harness is dedicated to C#/.NET SDK projects and Python Harness scripts. Keep every step grounded in repository docs, C#/.NET constraints, TDD, and executable acceptance criteria. + +## Workflow + +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. + +## Step Design Rules + +- 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, numerical conventions, data integrity, API contracts, or other non-negotiables. +- Use executable acceptance criteria such as `python scripts/validate_workspace.py`, not abstract statements. +- For every C# behavior change, require Codex to write the related C# test file first, run it, and confirm that it fails for the expected reason before changing production code. Name the expected test file or test project. +- Name steps with kebab-case slugs such as `project-setup`, `core-types`, or `solver-validation`. + +## Phase Files + +Create or update `phases/index.json`: + +```json +{ + "phases": [ + { + "dir": "0-mvp", + "status": "pending" + } + ] +} +``` + +Create `phases/{task-name}/index.json`: + +```json +{ + "project": "FESA Harness", + "phase": "", + "steps": [ + { "step": 0, "name": "project-setup", "status": "pending" }, + { "step": 1, "name": "core-types", "status": "pending" }, + { "step": 2, "name": "validation-path", "status": "pending" } + ] +} +``` + +Rules: + +- `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`. + +## Step Template + +```markdown +# Step {N}: {name} + +## 읽어야 할 파일 + +먼저 아래 파일들을 읽고 프로젝트의 아키텍처와 설계 의도를 파악하라: + +- `/AGENTS.md` +- `/docs/ARCHITECTURE.md` +- `/docs/ADR.md` +- {previously created or modified files} + +이전 step에서 만들어진 코드를 꼼꼼히 읽고, 설계 의도를 이해한 뒤 작업하라. + +## 작업 + +{Concrete instructions with file paths, interfaces, signatures, and rules.} + +## Tests To Write First + +- {For a C# behavior change, name the exact C# test file and behavior to add before implementation. Require running it first and confirming the expected failure. For a Harness script change, name the exact Python test.} + +## Acceptance Criteria + +```bash +python -m unittest discover -s scripts -p "test_*.py" +python scripts/validate_workspace.py +``` + +## 검증 절차 + +1. 위 AC 커맨드를 실행한다. +2. 아키텍처 체크리스트를 확인한다: + - ARCHITECTURE.md 디렉토리 구조를 따르는가? + - ADR 기술 스택을 벗어나지 않았는가? + - AGENTS.md CRITICAL 규칙을 위반하지 않았는가? + - PATH에 설치된 `dotnet`을 사용하며 SDK 설치나 `global.json` 고정을 추가하지 않았는가? + - `HARNESS_VALIDATION_COMMANDS`가 있으면 우선하고, 그렇지 않으면 루트의 단일 `.sln`을 우선하며 solution이 없을 때만 루트의 단일 `.csproj`를 선택하는가? + - 동일 우선순위의 루트 target이 여러 개일 때 임의 선택하지 않는가? + - C# production `.cs` 변경에는 먼저 실패를 확인한 관련 C# test가 존재하는가? +3. 결과에 따라 `phases/{task-name}/index.json`의 해당 step을 업데이트한다: + - 성공: `"status": "completed"`, `"summary": "산출물 한 줄 요약"` + - 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"` + - 사용자 개입 필요: `"status": "blocked"`, `"blocked_reason": "구체적 사유"` 후 중단 + +## 금지사항 + +- 다른 언어 또는 npm fallback을 추가하지 마라. Reason: 이 Harness는 C#/.NET SDK 전용이며 언어별 fallback은 validation과 TDD 계약을 흐린다. +- 기존 테스트를 깨뜨리지 마라. +``` + +## 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/Codex/Coding/Codex-cs/.codex/skills/harness-workflow/agents/openai.yaml b/Codex/Coding/Codex-cs/.codex/skills/harness-workflow/agents/openai.yaml new file mode 100644 index 0000000..3a671bf --- /dev/null +++ b/Codex/Coding/Codex-cs/.codex/skills/harness-workflow/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Harness Workflow" + short_description: "Plan staged Harness workflow steps" + default_prompt: "Use $harness-workflow to plan Harness phases and step files." diff --git a/Codex/Coding/Codex-cs/.gitignore b/Codex/Coding/Codex-cs/.gitignore new file mode 100644 index 0000000..1910f40 --- /dev/null +++ b/Codex/Coding/Codex-cs/.gitignore @@ -0,0 +1,16 @@ +.vs/ +[Bb]in/ +[Oo]bj/ +TestResults/ +*.user +*.suo +*.nupkg +*.snupkg +coverage/ +coverage.* +__pycache__/ +*.pyc + +# phase execution outputs +phases/**/phase*-output.json +phases/**/step*-output.json diff --git a/Codex/Coding/Codex-cs/AGENTS.md b/Codex/Coding/Codex-cs/AGENTS.md new file mode 100644 index 0000000..66fb040 --- /dev/null +++ b/Codex/Coding/Codex-cs/AGENTS.md @@ -0,0 +1,48 @@ +# Project: FESA Harness + +## 기술 스택 +- C#/.NET SDK +- `dotnet` CLI +- Harness scripts in Python 3 + +## 아키텍처 규칙 +- CRITICAL: 기본 검증 경로는 `python scripts/validate_workspace.py`이다. +- CRITICAL: `HARNESS_VALIDATION_COMMANDS`가 설정되어 있으면 자동 대상 탐색보다 우선한다. +- CRITICAL: 자동 탐색은 루트의 단일 `.sln`을 우선하고, 없으면 루트의 단일 SDK-style `.csproj`를 사용한다. 동일 우선순위 대상이 여러 개면 임의 선택하지 않고 오류로 중단한다. +- CRITICAL: 기본 검증은 선택한 target에 대해 `dotnet restore`, `dotnet build --configuration Debug --no-restore`, `dotnet test --configuration Debug --no-build --no-restore` 순서로 실행한다. +- CRITICAL: PATH에 설치된 `dotnet`을 사용하며 Harness는 .NET SDK 버전이나 `global.json`을 생성하거나 고정하지 않는다. +- CRITICAL: 새 기능 또는 동작 변경은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현한다. +- CRITICAL: Abaqus reference artifact나 solver 코드 복원은 명시적으로 요청된 phase에서만 수행한다. +- 다중 project 저장소는 루트 solution에 모든 build/test 대상을 포함해야 한다. 단일 `.csproj` fallback은 단일-project 저장소에서만 사용한다. +- solution/project가 아직 없으면 안내 메시지를 출력하고 성공 종료하여 Harness 자체 테스트를 허용한다. +- Harness runner는 `scripts/execute.py`에 둔다. +- Codex hook 정책은 `.codex/hooks/`에 둔다. +- Harness planning/review instructions are stored in `.codex/skills/`. +- Generated phase execution outputs remain ignored under `phases/**/step*-output.json`. + +## 개발 프로세스 +- TDD를 기본으로 한다. C# production `.cs` file을 바꿀 때는 관련 C# test file이 있어야 한다. +- 커밋 전 hook은 Harness Python self-test와 workspace validation을 실행해야 한다. +- 커밋 메시지는 conventional commits 형식을 따른다: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`. +- 계획이 필요한 장기 작업은 Harness phase로 나누고, 각 step은 독립 실행 가능해야 한다. + +## 명령어 +```bash +python -m unittest discover -s scripts -p "test_*.py" +python scripts/validate_workspace.py +python scripts/execute.py +python scripts/execute.py --push +``` + +## .NET 검증 기본값 +- SDK resolution: PATH의 `dotnet` 사용; SDK 버전 및 `global.json` 생성·고정 없음 +- Target selection: 루트의 단일 `.sln` 우선, 없으면 루트의 단일 SDK-style `.csproj` +- Ambiguity: 동일 우선순위 대상이 여러 개면 오류 +- Configuration: `Debug` +- Commands: + 1. `dotnet restore ` + 2. `dotnet build --configuration Debug --no-restore` + 3. `dotnet test --configuration Debug --no-build --no-restore` + +Override variable: +- `HARNESS_VALIDATION_COMMANDS` diff --git a/Codex/Coding/Codex-cs/docs/ADR.md b/Codex/Coding/Codex-cs/docs/ADR.md new file mode 100644 index 0000000..68a45c2 --- /dev/null +++ b/Codex/Coding/Codex-cs/docs/ADR.md @@ -0,0 +1,52 @@ +# Architecture Decision Records + +## 철학 +Harness는 현재 프로젝트의 실제 기술 스택을 반영해야 한다. 이 저장소는 C#/.NET SDK 전용 정책을 사용하며 다른 언어의 fallback이나 언어별 profile/registry를 두지 않는다. + +--- + +### ADR-001: C#/.NET 전용 Harness +**결정**: Harness scaffold는 C#/.NET SDK와 `dotnet` CLI 전용으로 운영하고, Harness script에는 Python 3을 사용한다. 언어별 profile이나 registry 및 다른 언어 fallback은 두지 않는다. + +**이유**: 단일 기술 스택을 명시해야 validation, TDD guard, acceptance criteria가 실제 FESA 개발 환경과 일치한다. + +**트레이드오프**: 다른 언어 프로젝트에는 이 Harness를 그대로 재사용할 수 없다. + +### ADR-002: 호스트의 .NET SDK 사용 +**결정**: PATH에 설치된 `dotnet`을 사용한다. Harness는 .NET SDK 버전을 선택·설치하지 않으며 `global.json`을 생성하거나 고정하지 않는다. + +**이유**: SDK 수명주기와 설치 정책은 개발 환경이 소유하고, Harness는 저장소 검증만 담당한다. + +**트레이드오프**: 실행 결과는 호스트에서 해석되는 SDK에 의존하므로 필요한 SDK 제공은 환경 구성의 책임이다. + +### ADR-003: 결정적인 루트 target 선택 +**결정**: 자동 탐색은 루트의 단일 `.sln`을 우선한다. solution이 없을 때만 루트의 단일 SDK-style `.csproj`를 단일-project fallback으로 사용한다. 동일 우선순위 대상이 여러 개면 임의 선택하지 않고 오류로 중단한다. solution/project가 없으면 안내 후 성공 종료한다. + +**이유**: solution은 다중 project의 build/test 경계를 명시한다. 모호한 대상을 자동 선택하면 일부 project나 test가 검증에서 누락될 수 있고, 아직 application이 없는 Harness scaffold는 자체 테스트가 가능해야 한다. + +**트레이드오프**: 다중 project 저장소는 루트 solution에 모든 build/test 대상을 포함해야 한다. 루트 solution이 없고 `.csproj`가 여러 개인 저장소는 명시적으로 구조를 정리해야 한다. + +### ADR-004: 표준 workspace validation 순서 +**결정**: 기본 진입점은 `python scripts/validate_workspace.py`이다. `HARNESS_VALIDATION_COMMANDS`가 설정되어 있으면 자동 target 탐색보다 우선하여 지정된 명령을 실행한다. 그렇지 않으면 선택한 target에 대해 다음 순서로 실행한다. + +1. `dotnet restore ` +2. `dotnet build --configuration Debug --no-restore` +3. `dotnet test --configuration Debug --no-build --no-restore` + +**이유**: restore, build, test 단계를 분리하고 후속 단계의 암묵적 restore/build를 차단하면 실패 지점과 검증 범위가 명확해진다. + +**트레이드오프**: 특수한 저장소는 `HARNESS_VALIDATION_COMMANDS`로 전체 검증 절차를 명시해야 한다. + +### ADR-005: 엄격한 C# TDD Guard +**결정**: C# production `.cs` file 변경은 관련 C# test file이 없으면 차단한다. + +**이유**: Harness의 핵심 목적은 agent가 검증 없는 production 변경을 만들지 않도록 하는 것이다. + +**트레이드오프**: 초기 scaffolding 작업에서 guard가 엄격하게 느껴질 수 있다. 문서, project 설정, Harness metadata는 guard 대상에서 제외한다. + +### ADR-006: Harness 자체 테스트 우선 +**결정**: commit hook은 먼저 Python Harness self-test를 실행한 뒤 `python scripts/validate_workspace.py`를 실행한다. + +**이유**: solution/project가 아직 없더라도 Harness 자체는 검증 가능해야 이후 source 및 project 구성을 안전하게 진행할 수 있다. + +**트레이드오프**: commit 시간이 조금 늘어나지만 hook/validation regression을 빠르게 발견한다. diff --git a/Codex/Coding/Codex-cs/docs/ARCHITECTURE.md b/Codex/Coding/Codex-cs/docs/ARCHITECTURE.md new file mode 100644 index 0000000..cb25be7 --- /dev/null +++ b/Codex/Coding/Codex-cs/docs/ARCHITECTURE.md @@ -0,0 +1,68 @@ +# 아키텍처 + +## 목표 +이 저장소의 현재 책임은 C#/.NET SDK 프로젝트를 위한 Codex Harness scaffold를 제공하는 것이다. Harness는 phase execution, edit guard, commit validation, workspace validation을 분리해서 관리한다. + +Application과 test target은 C#/.NET SDK를 사용하고 Harness script는 Python 3으로 실행한다. Harness는 PATH에 설치된 `dotnet`을 그대로 사용한다. .NET SDK 버전을 선택하거나 설치하지 않으며 `global.json`을 생성하거나 고정하지 않는다. + +## 디렉토리 구조 +```text +.codex/ +├── hooks/ # Codex hook scripts +└── skills/ # Harness planning/review instructions +docs/ # Project and Harness guidance +scripts/ +├── execute.py # Phase step executor +├── validate_workspace.py +└── test_*.py # Harness self-tests +phases/ # Optional generated phase plans +``` + +## 데이터 흐름 +```text +User-approved task +-> Harness phase files under phases/ +-> scripts/execute.py injects AGENTS.md and docs/*.md +-> Codex executes one step at a time +-> step updates phases/{phase}/index.json +-> validation runs through scripts/validate_workspace.py +``` + +## Hook 흐름 +```text +apply_patch/Edit/Write +-> .codex/hooks/tdd-guard.py +-> C# production .cs changes require related C# test files + +git commit command +-> .codex/hooks/pre_commit_checks.py +-> Python Harness self-tests +-> scripts/validate_workspace.py +``` + +## Validation 흐름 +```text +python scripts/validate_workspace.py +-> HARNESS_VALIDATION_COMMANDS set? + -> yes: run the exact override commands; skip automatic target discovery + -> no: inspect repository-root targets + +Exactly one root .sln exists +-> select that solution, even when root .csproj files also exist + +No root .sln exists and exactly one root SDK-style .csproj exists +-> select that project as the single-project fallback + +Multiple targets exist at the same active priority +-> report ambiguity and exit with an error; never choose arbitrarily + +No root solution or SDK-style project exists +-> print guidance and exit successfully + +Selected target +-> dotnet restore +-> dotnet build --configuration Debug --no-restore +-> dotnet test --configuration Debug --no-build --no-restore +``` + +다중 project 저장소는 루트 solution에 모든 build/test 대상을 포함해야 한다. 루트의 단일 SDK-style `.csproj` fallback은 단일-project 저장소만을 위한 경로다. diff --git a/Codex/Coding/Codex-cs/docs/PRD.md b/Codex/Coding/Codex-cs/docs/PRD.md new file mode 100644 index 0000000..1caa4ed --- /dev/null +++ b/Codex/Coding/Codex-cs/docs/PRD.md @@ -0,0 +1,32 @@ +# PRD: C#/.NET Harness + +## 목표 +Codex Harness가 C#/.NET SDK 프로젝트에서 phase planning, TDD guard, commit validation, workspace validation을 일관되게 수행하게 한다. + +## 기술 스택 +- C#/.NET SDK +- `dotnet` CLI +- Harness scripts in Python 3 + +## 사용자 +- `dotnet` CLI 기반 C#/.NET 개발자 +- Harness phase를 작성하고 실행하는 Codex agent +- Harness 결과를 검토하는 reviewer + +## 핵심 기능 +1. 기본 진입점은 `python scripts/validate_workspace.py`이며, `HARNESS_VALIDATION_COMMANDS`가 설정되어 있으면 자동 탐색보다 우선한다. +2. 자동 탐색은 루트의 단일 `.sln`을 우선하고, solution이 없으면 루트의 단일 SDK-style `.csproj`를 선택한다. 동일 우선순위 대상이 여러 개면 임의 선택하지 않고 오류로 중단한다. +3. 선택한 target에 대해 `dotnet restore `, `dotnet build --configuration Debug --no-restore`, `dotnet test --configuration Debug --no-build --no-restore` 순서로 검증한다. +4. PATH에 설치된 `dotnet`을 사용하며 Harness는 .NET SDK 버전이나 `global.json`을 생성하거나 고정하지 않는다. +5. C# production `.cs` 변경에 관련 C# test file을 요구하는 엄격한 TDD guard를 제공한다. +6. 다중 project 저장소는 루트 solution에 모든 build/test 대상을 포함해야 하며, 단일 `.csproj` fallback은 단일-project 저장소에서만 사용한다. +7. solution/project가 아직 없으면 안내 후 성공 종료하여 Harness 자체 테스트를 허용한다. +8. commit hook은 Python Harness self-test를 먼저 실행한 뒤 workspace validation을 실행한다. + +## 제외 사항 +- 언어별 profile 또는 registry +- 다른 언어 validation fallback +- .NET SDK 버전 선택, 설치 또는 `global.json` 생성 +- C# sample application 또는 project 생성 +- 이전 FESA solver source tree 복원 +- Abaqus reference artifact 생성 또는 solver reference 비교 구현 diff --git a/Codex/Coding/Codex-cs/docs/UI_GUIDE.md b/Codex/Coding/Codex-cs/docs/UI_GUIDE.md new file mode 100644 index 0000000..c3d280f --- /dev/null +++ b/Codex/Coding/Codex-cs/docs/UI_GUIDE.md @@ -0,0 +1,76 @@ +# 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/Codex/Coding/Codex-cs/scripts/execute.py b/Codex/Coding/Codex-cs/scripts/execute.py new file mode 100644 index 0000000..8016252 --- /dev/null +++ b/Codex/Coding/Codex-cs/scripts/execute.py @@ -0,0 +1,417 @@ +#!/usr/bin/env python3 +""" +Harness Step Executor — phase 내 step을 순차 실행하고 자가 교정한다. + +Usage: + python scripts/execute.py [--push] +""" + +import argparse +import contextlib +import json +import os +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: + commit_example = self.FEAT_MSG.format( + phase=self._phase_name, num="N", name="" + ) + 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. 모든 변경사항을 커밋하라:\n" + f" {commit_example}\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") + result = subprocess.run( + ["codex", "exec", "--dangerously-bypass-approvals-and-sandbox", "--json", prompt], + cwd=self._root, 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]}") + + output = { + "step": step_num, "name": step_name, + "exitCode": result.returncode, + "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/Codex/Coding/Codex-cs/scripts/validate_workspace.py b/Codex/Coding/Codex-cs/scripts/validate_workspace.py new file mode 100644 index 0000000..9984d5b --- /dev/null +++ b/Codex/Coding/Codex-cs/scripts/validate_workspace.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Run C#/.NET Harness validation commands.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + + +class ValidationConfigurationError(Exception): + """Raised when automatic target discovery is ambiguous.""" + + +def load_env_commands() -> list[str]: + raw = os.environ.get("HARNESS_VALIDATION_COMMANDS", "") + return [line for line in raw.splitlines() if line.strip()] + + +def _root_files_with_suffix(root: Path, suffix: str) -> list[Path]: + return sorted( + ( + path.resolve() + for path in root.iterdir() + if path.is_file() and path.suffix.lower() == suffix + ), + key=lambda path: path.name.casefold(), + ) + + +def _select_single_target(candidates: list[Path], candidate_kind: str) -> Path | None: + if len(candidates) > 1: + names = ", ".join(path.name for path in candidates) + raise ValidationConfigurationError( + f"Multiple {candidate_kind} targets found at repository root: {names}. " + "Keep exactly one target or set HARNESS_VALIDATION_COMMANDS " + "to define validation explicitly." + ) + return candidates[0] if candidates else None + + +def discover_target(root: Path) -> Path | None: + solutions = _root_files_with_suffix(root, ".sln") + if solutions: + return _select_single_target(solutions, "solution (.sln)") + + projects = _root_files_with_suffix(root, ".csproj") + return _select_single_target(projects, "project (.csproj)") + + +def load_dotnet_commands(root: Path) -> list[str]: + target = discover_target(root) + if target is None: + return [] + + quoted_target = f'"{target}"' + return [ + f"dotnet restore {quoted_target}", + f"dotnet build {quoted_target} --configuration Debug --no-restore", + f"dotnet test {quoted_target} --configuration Debug --no-build --no-restore", + ] + + +def discover_commands(root: Path) -> list[str]: + env_commands = load_env_commands() + if env_commands: + return env_commands + return load_dotnet_commands(root) + + +def run_command(command: str, root: Path) -> subprocess.CompletedProcess: + return subprocess.run( + command, + cwd=root, + shell=True, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + + +def emit_stream(prefix: str, content: str, *, stream) -> None: + text = (content or "").strip() + if not text: + return + print(prefix, file=stream) + print(text, file=stream) + + +def main() -> int: + root = Path(__file__).resolve().parent.parent + try: + commands = discover_commands(root) + except ValidationConfigurationError as error: + print(f"Validation configuration error: {error}", file=sys.stderr) + return 2 + + if not commands: + print("No C#/.NET validation target found.") + print("Add a root .sln or .csproj, or set HARNESS_VALIDATION_COMMANDS.") + 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/Codex/Coding/Codex-tsjs/.codex/config.toml b/Codex/Coding/Codex-tsjs/.codex/config.toml index b75aa36..151b8b6 100644 --- a/Codex/Coding/Codex-tsjs/.codex/config.toml +++ b/Codex/Coding/Codex-tsjs/.codex/config.toml @@ -1,4 +1,4 @@ #:schema https://developers.openai.com/codex/config-schema.json [features] -codex_hooks = true +hooks = true