modify harness framework

This commit is contained in:
KOKO\Mimi
2026-08-05 01:42:21 +09:00
parent 6646344113
commit 41020d78d8
74 changed files with 2663 additions and 3145 deletions
+172
View File
@@ -0,0 +1,172 @@
---
name: harness
description: Use when planning agentic implementation phases, creating phases/index.json and self-contained step files, or running the Harness step executor.
---
# Harness Workflow
이 프로젝트는 Harness 프레임워크를 사용한다. 아래 워크플로에 따라 작업한다.
## A. 탐색
`AGENTS.md``docs/` 하위 문서(PRD, ARCHITECTURE, ADR 등)를 읽고 프로젝트의 기획,
아키텍처, 설계 의도를 파악한다. 병렬 탐색이 실제로 유용하고 현재 세션에서 허용될
때만 Codex subagent를 선택적으로 사용한다.
## B. 논의
구현을 위해 구체화하거나 기술적으로 결정해야 할 사항이 있으면 사용자에게 한 번에
하나씩 제시하고 논의한다.
## C. Step 설계
사용자가 구현 계획 작성을 지시하면 여러 step으로 나뉜 초안을 작성해 피드백을
요청한다.
설계 원칙:
1. **Scope 최소화** — 하나의 step에서 하나의 레이어 또는 모듈만 다룬다. 여러
모듈을 동시에 수정해야 하면 step을 쪼갠다.
2. **자기완결성** — 각 step 파일은 독립된 Codex 실행에서 사용된다. 외부 대화
참조를 금지하고 필요한 정보를 모두 파일 안에 적는다.
3. **사전 준비 강제** — 관련 문서와 이전 step에서 생성하거나 수정한 파일 경로를
명시한다.
4. **시그니처 수준 지시** — 함수와 클래스의 인터페이스를 제시하고 내부 구현은
Codex 재량에 맡긴다. 멱등성, 보안, 데이터 무결성 같은 핵심 규칙은 명시한다.
5. **AC는 실행 가능한 command** — 추상적 조건 대신 실제 빌드와 테스트 command를
포함한다.
6. **주의사항은 구체적으로** — "X를 하지 마라. 이유: Y" 형식으로 적는다.
7. **네이밍** — step name은 핵심 작업을 표현하는 kebab-case slug로 정한다.
## D. 파일 생성
사용자가 초안을 승인한 후에만 다음 파일을 생성한다.
### D-1. `phases/index.json`
여러 task를 관리하는 top-level 인덱스다. 이미 존재하면 `phases` 배열에 새 항목을
추가한다.
```json
{
"phases": [
{
"dir": "0-mvp",
"status": "pending"
}
]
}
```
- `dir`: task 디렉터리명
- `status`: `pending` | `completed` | `error` | `blocked`
- timestamp는 executor가 상태를 바꿀 때 기록하므로 생성 시 넣지 않는다.
### D-2. `phases/{task-name}/index.json`
```json
{
"project": "<프로젝트명>",
"phase": "<task-name>",
"steps": [
{ "step": 0, "name": "project-setup", "status": "pending" },
{ "step": 1, "name": "core-types", "status": "pending" },
{ "step": 2, "name": "api-layer", "status": "pending" }
]
}
```
필드 규칙:
- `project`: `AGENTS.md`에 정의된 프로젝트명
- `phase`: task 이름이며 디렉터리명과 일치
- `steps[].step`: 0부터 시작하는 순번
- `steps[].name`: kebab-case slug
- `steps[].status`: 초기값 `pending`
상태와 기록 주체:
| 전이 | 기록 필드 | 기록 주체 |
|------|-----------|-----------|
| `completed` | `summary`, `completed_at` | Codex가 summary, executor가 timestamp |
| `error` | `error_message`, `failed_at` | Codex가 message, executor가 timestamp |
| `blocked` | `blocked_reason`, `blocked_at` | Codex가 reason, executor가 timestamp |
`summary`에는 다음 step에 유용한 생성 파일과 핵심 결정을 한 줄로 적는다.
task `created_at`과 step `started_at`은 executor가 기록하므로 생성 시 넣지 않는다.
### D-3. `phases/{task-name}/step{N}.md`
````markdown
# Step {N}: {이름}
## 읽어야 할 파일
먼저 아래 파일을 읽고 프로젝트의 아키텍처와 설계 의도를 파악하라:
- `/AGENTS.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
- 이전 step에서 생성하거나 수정한 파일 경로
이전 step의 코드를 꼼꼼히 읽고 설계 의도를 이해한 뒤 작업하라.
## 작업
구체적인 구현 지시를 파일 경로, 클래스와 함수 시그니처, 로직 설명과 함께 적는다.
구현체는 Codex에 맡기되 설계 의도에서 벗어나면 안 되는 핵심 규칙은 명시한다.
## Acceptance Criteria
프로젝트 형식에 맞는 명령을 사용한다. `.harness/config.json`이 있으면 해당 preset,
solution, configuration, platform, test command를 우선한다.
```powershell
# CMake
cmake --build .harness/build --config Debug
ctest --test-dir .harness/build -C Debug --output-on-failure
# 직접 MSBuild
MSBuild.exe MyProject.sln /m /p:Configuration=Debug /p:Platform=x64
.\build\tests\Debug\MyProjectTests.exe
```
## 검증 절차
1. Acceptance Criteria command를 실행한다.
2. ARCHITECTURE 디렉터리 구조를 따르는지 확인한다.
3. ADR 기술 스택과 `AGENTS.md` CRITICAL 규칙을 확인한다.
4. 결과에 따라 task index의 해당 step을 갱신한다.
- 성공: `status`를 `completed`로 바꾸고 한 줄 `summary` 기록
- 수정 3회 후 실패: `status`를 `error`로 바꾸고 `error_message` 기록
- 사용자 개입 필요: `status`를 `blocked`로 바꾸고 `blocked_reason` 기록 후 중단
## 금지사항
- 이 step의 범위 밖 기능을 추가하지 마라. 이유: step의 독립성을 깨뜨린다.
- 기존 테스트를 깨뜨리지 마라. 이유: 이전 동작을 회귀시킨다.
````
## E. 실행
```bash
python scripts/execute.py {task-name}
python scripts/execute.py {task-name} --push
```
환경에서 Python 3 실행 명령이 `python3`이면 그 명령을 대신 사용한다.
executor가 처리하는 작업:
- `feat-{task-name}` 브랜치 생성 또는 checkout
- `AGENTS.md`와 `docs/*.md` guardrail 주입
- 완료 step의 summary를 다음 prompt에 누적
- 실패 시 최대 3회 재시도하며 이전 오류를 prompt에 전달
- 코드 변경과 metadata를 분리해 commit
- `started_at`, `completed_at`, `failed_at`, `blocked_at` 기록
에러 복구:
- `error`: 해당 status를 `pending`으로 바꾸고 `error_message`를 삭제한 뒤 재실행
- `blocked`: 원인을 해결하고 status를 `pending`으로 바꾸고 `blocked_reason`을 삭제한
뒤 재실행
+41
View File
@@ -0,0 +1,41 @@
---
name: review
description: Use when reviewing repository changes against AGENTS.md, architecture decisions, tests, and build requirements.
---
# Repository Review
먼저 다음 문서를 읽는다.
- `/AGENTS.md`
- `/docs/ARCHITECTURE.md`
- `/docs/ADR.md`
사용자가 범위를 지정하지 않으면 현재 작업 트리의 변경을 리뷰한다. 관련 diff를
확인하고 가능한 빌드와 테스트 command를 실제로 실행한다.
## 체크리스트
1. MSVC toolset와 C++ 표준이 AGENTS.md/ADR과 일치하는가?
2. CMake 또는 MSBuild Debug/x64 빌드가 통과하는가?
3. CTest 또는 `.harness/config.json`의 명시적 test command가 통과하는가?
4. 새 C/C++ 소스와 헤더에 대응 테스트가 있는가?
5. CRITICAL 아키텍처 규칙과 public header 경계를 지키는가?
## 출력 형식
실제 결함을 심각도순으로 먼저 제시한다. 각 finding에 파일과 줄 번호, 영향,
재현 또는 근거, 구체적인 수정 방안을 포함한다.
그 뒤 다음 표를 제공한다.
| 항목 | 결과 | 비고 |
|------|------|------|
| 아키텍처 준수 | ✅/❌/미검증 | 상세 |
| 기술 스택 준수 | ✅/❌/미검증 | 상세 |
| 테스트 존재 | ✅/❌/미검증 | 상세 |
| CRITICAL 규칙 | ✅/❌/미검증 | 상세 |
| 빌드 가능 | ✅/❌/미검증 | 상세 |
실행할 수 없는 검사는 성공으로 추정하지 말고 `미검증`과 이유를 적는다. finding이
없으면 발견된 문제가 없다고 명시하고 남아 있는 검증 공백을 설명한다.
+18 -15
View File
@@ -10,7 +10,7 @@ Mission:
- Run build and test validation only after Implementation Agent work.
- Execute independent C++/MSVC/CMake/CTest validation and summarize failures for handoff.
- Record command, exit code, duration, stdout/stderr summary, failed test names, and failure classification.
- Keep the output aligned with AGENTS.md, docs/SOLVER_AGENT_DESIGN.md, scripts/validate_workspace.py, and the implementation plan/report.
- Keep the output aligned with AGENTS.md, docs/HARNESS_WORKFLOW.md, docs/SOLVER_AGENT_DESIGN.md, `.harness/config.json` when present, and the implementation plan/report.
Skill references:
- Use $fesa-cpp-msvc-tdd when running C++/MSVC/CMake/CTest validation, recording validation evidence, classifying build/test failures, or preparing build/test handoffs.
@@ -32,23 +32,26 @@ Input priorities:
2. Implementation Agent report.
3. docs/implementation-plans/<feature-id>-implementation-plan.md.
4. AGENTS.md and docs/SOLVER_AGENT_DESIGN.md.
5. scripts/validate_workspace.py.
6. CMakePresets.json, CMakeLists.txt, CMake files, and CTest metadata when present.
5. `.harness/config.json` when present.
6. CMakePresets.json, CMakeLists.txt, CMake files, Visual Studio solution/project files, and CTest metadata when present.
7. Related docs/reference-models/<feature-id>-reference-models.md when present.
8. Stored reference artifacts when present, read-only.
Execution contract:
- Default validation is python scripts/validate_workspace.py.
- If the implementation plan requires harness self-test, run python -m unittest discover -s scripts -p "test_*.py" first.
- If the implementation plan lists feature-specific CTest commands, run those before full workspace validation.
- Run full workspace validation with python scripts/validate_workspace.py last.
- scripts/validate_workspace.py resolves HARNESS_VALIDATION_COMMANDS, CMakePresets.json msvc-debug, or CMake/MSVC x64 Debug commands.
- The default CMake/MSVC x64 Debug commands are:
1. cmake -S . -B build/msvc-debug -G "Visual Studio 17 2022" -A x64
2. cmake --build build/msvc-debug --config Debug
3. ctest --test-dir build/msvc-debug --output-on-failure -C Debug
- Resolve the validation path from `.harness/config.json` first, then Harness project auto detection.
- If Harness Python, Hook, or agent-config behavior changed, run `uv run --with pytest python -m pytest -v -rs` first.
- Configure and build before running feature-specific and full tests.
- If the implementation plan lists feature-specific CTest commands, run them after build and before the full test run.
- For a non-preset CMake project, run:
1. cmake -S . -B .harness/build -A x64
2. cmake --build .harness/build --config Debug
3. ctest --test-dir .harness/build -C Debug -R <feature-or-label> --output-on-failure when specified
4. ctest --test-dir .harness/build -C Debug --show-only=json-v1
5. ctest --test-dir .harness/build -C Debug --output-on-failure
- If `.harness/config.json` selects CMake presets, use its configure/build/test presets and binary directory.
- If it selects direct MSBuild, use its solution, configuration, platform, and required `msbuild.testCommand`.
- Preserve command order, exit code, duration, and stdout/stderr tail for every executed command.
- For no-CMake workspaces, record the scripts/validate_workspace.py informational success path instead of treating it as a failure.
- Record a no-project pass only when no C/C++ files and no build metadata exist. C/C++ files without build metadata are an error.
- Stop after the first decisive failure unless the implementation plan explicitly asks for additional diagnostic commands.
Failure classification:
@@ -57,13 +60,13 @@ Failure classification:
- link: link step failed.
- test: CTest or unit/integration tests failed.
- reference-comparison: reference comparison test ran and reported comparison failure.
- harness: Python harness self-test or validation script failed.
- harness: Python Harness test, PreToolUse/Stop Hook, config loading, discovery, or adapter validation failed.
- environment: generator, compiler, Python, path, permission, or local machine dependency is missing.
- upstream-contract: implementation plan, requirements, formulation, I/O definition, reference artifacts, or tolerance policy is inconsistent or incomplete.
Required Build/Test Report sections:
1. Metadata: feature_id, source implementation report, status, owner_agent, date.
2. Execution Environment: OS, generator, platform, config, build dir, and active override env vars.
2. Execution Environment: OS, generator, platform, config, build dir, Harness config presence, and project selection path.
3. Command Log Summary: command, exit code, duration, stdout/stderr tail.
4. Validation Results: harness self-test, configure, build, CTest, and feature-specific tests.
5. Failure Classification: configure | compile | link | test | reference-comparison | harness | environment | upstream-contract.
+5 -4
View File
@@ -48,8 +48,9 @@ Execution contract:
- MINIMAL FIX: modify only implementation-owned source, header, test, or CMake files needed to fix the classified failure.
- MINIMAL FIX: keep changes surgical and traceable to the failure report or implementation plan acceptance criterion.
- VERIFY: rerun the targeted command that reproduced the failure first.
- VERIFY: run python scripts/validate_workspace.py after the targeted command.
- VERIFY: run python -m unittest discover -s scripts -p "test_*.py" when harness, hook, or agent config behavior is involved.
- VERIFY: run the full MSVC build/test commands resolved from `.harness/config.json` or Harness auto detection after the targeted command.
- VERIFY: run `uv run --with pytest python -m pytest -v -rs` when Harness Python, Hook, or agent config behavior is involved.
- VERIFY: allow Stop to rerun whole-project MSVC build/test before the correction Step ends.
- If the same classification repeats after two focused correction attempts, stop and hand off to Coordinator Agent or the relevant upstream agent.
- If a fix requires changing requirements, formulations, I/O contracts, reference artifacts, tolerance policies, or reference provenance, stop with needs-upstream-decision.
- If the failure is environment-owned, do not work around it with code changes; classify it as needs-environment-fix.
@@ -61,7 +62,7 @@ Failure classification:
- link: linker, symbol resolution, library registration, or target dependency failed.
- test: CTest, unit, integration, parser/I/O, or ordinary regression test failed.
- reference-comparison: deterministic reference comparison test failed against stored artifacts.
- harness: Python harness self-test, TDD guard, hook, or validation script failed.
- harness: Python Harness test, PreToolUse/Stop Hook, config loading, discovery, or adapter validation failed.
- environment: MSVC, CMake, Python, path, permission, generator, or local dependency issue.
- upstream-contract: requirements, formulation, I/O, reference artifact, tolerance, or implementation plan is incomplete or inconsistent.
@@ -70,7 +71,7 @@ Required Correction Report sections:
2. Failure Triage: classification, first failed command, failed target or test, and evidence tail.
3. Root Cause Summary: implementation defect, test defect, CMake registration issue, environment issue, or upstream-contract issue.
4. Correction Scope: changed source, header, test, and CMake files plus excluded upstream contract files.
5. Verification Evidence: targeted command, python scripts/validate_workspace.py, and Python harness self-test when relevant.
5. Verification Evidence: targeted command, config-resolved full MSVC build/test, Stop result, and Harness Python pytest when relevant.
6. Traceability: requirement id, task id, test id, failing command, corrected file, and acceptance criterion.
7. Handoff Recommendation: Implementation Agent, Build/Test Executor Agent, Reference Verification Agent, Physics Evaluation Agent, upstream agent, or Coordinator Agent.
8. Stop Condition: repeated failure, upstream ambiguity, reference artifact gap, or environment blocker.
+11 -5
View File
@@ -43,7 +43,9 @@ Execution contract:
- RED: write the planned C++ unit, integration, parser/I/O, or reference-comparison test first.
- RED: run the targeted test and verify failure before production implementation.
- GREEN: implement the minimum code needed for the planned task and acceptance criterion.
- VERIFY: run the targeted CTest command, then the workspace validation commands.
- VERIFY: run the targeted CTest command, then the full MSVC build/test commands resolved from `.harness/config.json` or the Harness defaults.
- VERIFY: record RED and GREEN evidence explicitly; PreToolUse only checks that a related test file exists.
- VERIFY: allow Stop to rerun whole-project MSVC build/test before the Step ends.
- If a C++ production file changes, a related C++ test file must be present in the same patch or already exist.
- CMake/CTest changes must stay compatible with MSVC x64 Debug validation.
- Abaqus reference CSV files are read-only verification inputs.
@@ -69,15 +71,19 @@ Required Implementation Report sections:
2. Implemented Scope: completed task ids, skipped task ids, and reason.
3. Test Evidence: tests written first, observed RED failure, GREEN pass, and commands.
4. Code Changes: source, header, test, and CMake/CTest change summary.
5. Validation Evidence: ctest -C Debug, python scripts/validate_workspace.py, and python -m unittest discover -s scripts -p "test_*.py" when relevant.
5. Validation Evidence: targeted CTest, config-resolved full MSVC build/test, Stop result, and `uv run --with pytest python -m pytest -v -rs` when Harness Python behavior is relevant.
6. Traceability: requirement id, task id, test id, and acceptance criterion.
7. Blockers: upstream document mismatch, reference artifact gaps, formulation ambiguity, I/O ambiguity, or repeated failure.
8. Downstream Handoff: Build/Test Executor Agent, Correction Agent, and Reference Verification Agent.
Validation commands:
- python -m unittest discover -s scripts -p "test_*.py"
- python scripts/validate_workspace.py
- ctest -C Debug -R <feature-or-label>
- cmake -S . -B .harness/build -A x64
- cmake --build .harness/build --config Debug
- ctest --test-dir .harness/build -C Debug -R <feature-or-label> --output-on-failure
- ctest --test-dir .harness/build -C Debug --show-only=json-v1
- ctest --test-dir .harness/build -C Debug --output-on-failure
- Use configured CMake presets or direct MSBuild commands instead when `.harness/config.json` selects them.
- Run `uv run --with pytest python -m pytest -v -rs` when Harness Python, Hook, or agent-config behavior changes.
Status rules:
- in-progress: implementation is underway.
@@ -55,11 +55,11 @@ Required Implementation Plan sections:
3. Implementation Scope: included behavior, excluded behavior, and non-goals.
4. Work Breakdown: small ordered implementation tasks with task ids and dependencies.
5. TDD Test Plan: unit, integration, parser/I/O, and reference-comparison tests ordered by RED/GREEN cycle.
6. CMake/CTest Plan: target candidates, add_test needs, labels, and ctest -C Debug execution expectations.
6. CMake/CTest Plan: target candidates, add_test needs, labels, and `.harness/config.json` or default `.harness/build` execution expectations.
7. Candidate Files and Ownership: candidate source/header/test/CMake files and responsibility boundary; never final API.
8. Data Flow Contract: Abaqus .inp input, internal model, solver results.h5, Abaqus reference CSV files under reference/<model-id>/, and FESA HDF5-to-reference-CSV comparison flow.
9. Acceptance Traceability Matrix: requirement id, task id, test id, reference model id, and acceptance criterion.
10. Validation Commands: python -m unittest discover -s scripts -p \"test_*.py\", python scripts/validate_workspace.py, and feature-specific CTest commands.
10. Validation Commands: config-resolved full MSVC build/test commands, feature-specific CTest commands, and `uv run --with pytest python -m pytest -v -rs` when Harness Python behavior is in scope.
11. Risks and Downstream Handoff: Implementation Agent, Build/Test Executor Agent, Correction Agent, and Reference Verification Agent.
12. Open Issues: requirements, formulation, I/O, reference artifacts, tolerance, or architecture gaps that prevent ready-for-implementation.
+1 -1
View File
@@ -62,7 +62,7 @@ Required Release Report sections:
2. Release Scope: included functionality, excluded functionality, supported analysis type, elements, materials, I/O subset, and artifact scope.
3. Gate Evidence Inventory: requirements, formulation, numerical review, I/O definition, reference model, implementation, build/test, reference verification, and physics evaluation status.
4. Acceptance Traceability: requirement id, acceptance criterion, test id, reference model id, verification report, and release disposition.
5. Validation Evidence: python scripts/validate_workspace.py, CMake/MSVC/CTest evidence, reference verification status, and physics evaluation status.
5. Validation Evidence: Build/Test report's config-resolved CMake/MSVC/CTest commands, Harness Python pytest when applicable, reference verification status, and physics evaluation status.
6. Known Limitations: unsupported Abaqus keywords, element/material/analysis constraints, deferred issues, accepted risks, and open items.
7. Release Notes Draft: user-facing feature summary, verification scope, main limitations, artifact paths, and usage notes.
8. Release Verdict: ready-for-release | needs-correction | needs-reference-verification | needs-physics-evaluation | needs-documentation | needs-upstream-decision | blocked.
+1 -1
View File
@@ -1,4 +1,4 @@
#:schema https://developers.openai.com/codex/config-schema.json
[features]
codex_hooks = true
hooks = true
+13 -9
View File
@@ -1,25 +1,29 @@
{
"description": "Harness TDD, command safety, and MSVC C/C++ validation hooks.",
"hooks": {
"PreToolUse": [
{
"matcher": "^Bash$",
"matcher": "Bash|shell_command|PowerShell|apply_patch|Edit|MultiEdit|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' / 'pre_commit_checks.py'), run_name='__main__')\"",
"timeout": 600,
"statusMessage": "Running pre-commit checks"
"command": "python3 -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/pre_tool_use.py\"",
"commandWindows": "python -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/pre_tool_use.py\"",
"timeout": 30,
"statusMessage": "Checking Harness policies"
}
]
},
}
],
"Stop": [
{
"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"
"command": "python3 -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/stop_validation.py\"",
"commandWindows": "python -X utf8 \"$(git rev-parse --show-toplevel)/scripts/hooks/stop_validation.py\"",
"timeout": 1800,
"statusMessage": "Running MSVC build and tests"
}
]
}
-89
View File
@@ -1,89 +0,0 @@
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())
-205
View File
@@ -1,205 +0,0 @@
import json
import subprocess
import sys
from pathlib import Path
SOURCE_SUFFIXES = {".h", ".hpp", ".hh", ".hxx", ".c", ".cc", ".cpp", ".cxx", ".ixx"}
TEST_SUFFIXES = {".h", ".hpp", ".hh", ".hxx", ".c", ".cc", ".cpp", ".cxx", ".ixx"}
CONFIG_SUFFIXES = {".json", ".md", ".yml", ".yaml", ".txt", ".cmake"}
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]
path = Path(path_text)
return (
"/tests/" in f"/{normalized}"
or "/test/" in f"/{normalized}"
or name.endswith("_test.cpp")
or name.startswith("test_")
or ".test." in name
or ".spec." in name
) and path.suffix.lower() in TEST_SUFFIXES
def _token(text: str) -> str:
return "".join(ch for ch in text.lower() if ch.isalnum())
def _module_token(path: Path) -> str:
parts = [part.lower() for part in path.parts]
for marker in ("include", "src"):
if marker not in parts:
continue
idx = parts.index(marker)
if marker == "include" and idx + 2 < len(parts) and parts[idx + 1] == "fesa":
return _token(parts[idx + 2])
if marker == "src" and idx + 1 < len(parts):
return _token(parts[idx + 1])
return ""
def _related_tokens(path: Path) -> set[str]:
tokens = {_token(_base_name(path))}
module = _module_token(path)
if module:
tokens.add(module)
return {token for token in tokens if token}
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)):
candidates.append(resolved)
for test_root_name in ("tests", "test"):
test_root = root / test_root_name
if not test_root.is_dir():
continue
for suffix in TEST_SUFFIXES:
candidates.extend(test_root.rglob(f"*{suffix}"))
return candidates
def _has_related_test(path: Path, candidate_tests: list[Path]) -> bool:
tokens = _related_tokens(path)
for test_path in candidate_tests:
test_token = _token(test_path.stem)
if any(token and token in test_token for token in tokens):
return True
return False
def _is_exempt(path_text: str) -> bool:
normalized = _normalize(path_text)
path = Path(path_text)
name = path.name.lower()
if name == "cmakelists.txt":
return True
if _is_test_path(path_text):
return True
if path.suffix.lower() in CONFIG_SUFFIXES:
return True
if "/cmake/" in normalized:
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 sorted(SOURCE_SUFFIXES, key=len, reverse=True):
if path.name.lower().endswith(suffix):
return path.name[: -len(suffix)]
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 test file for "
f"{names}. Write or add the test first."
),
}
}
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+15 -10
View File
@@ -26,10 +26,12 @@ Read these first:
3. RED: write the planned unit, integration, parser/I/O, or reference-comparison test first.
4. RED: run the targeted test and verify the expected failure before production code.
5. GREEN: implement the minimum C++17/MSVC-compatible code needed for the task.
6. VERIFY: run the targeted command, then `python scripts/validate_workspace.py`.
6. VERIFY: run the targeted command, then the full MSVC build/test commands resolved from `.harness/config.json` or the Harness defaults.
7. For C++ production changes, require a related C++ test file in the same patch or already present.
8. For failure triage, classify as `configure | compile | link | test | reference-comparison | harness | environment | upstream-contract`.
9. Fix implementation-owned failures only and keep changes traceable to the implementation plan.
8. Treat PreToolUse as a test-file-existence guardrail, not proof that RED was observed. Record the RED and GREEN commands and results in the implementation report.
9. Let Stop perform the final whole-project MSVC build/test before the Step ends.
10. For failure triage, classify as `configure | compile | link | test | reference-comparison | harness | environment | upstream-contract`.
11. Fix implementation-owned failures only and keep changes traceable to the implementation plan.
## Output Contract
@@ -43,17 +45,19 @@ Produce one of these, depending on role:
Required validation commands:
```powershell
python -m unittest discover -s scripts -p "test_*.py"
python scripts/validate_workspace.py
ctest -C Debug -R <feature-or-label>
cmake -S . -B .harness/build -A x64
cmake --build .harness/build --config Debug
ctest --test-dir .harness/build -C Debug -R <feature-or-label> --output-on-failure
ctest --test-dir .harness/build -C Debug --show-only=json-v1
ctest --test-dir .harness/build -C Debug --output-on-failure
```
Default MSVC path:
Use configured CMake presets or direct MSBuild commands instead when
`.harness/config.json` selects them. For Harness Python, Hook, or agent-config
changes, also run:
```powershell
cmake -S . -B build/msvc-debug -G "Visual Studio 17 2022" -A x64
cmake --build build/msvc-debug --config Debug
ctest --test-dir build/msvc-debug --output-on-failure -C Debug
uv run --with pytest python -m pytest -v -rs
```
## Boundaries
@@ -73,6 +77,7 @@ ctest --test-dir build/msvc-debug --output-on-failure -C Debug
- Every `must` requirement maps to at least one task and one test.
- Each test has a clear RED condition, GREEN condition, linked task, and command.
- CMake/CTest plans remain compatible with MSVC x64 Debug validation.
- Stop validation is green for the whole discovered C/C++ project; a no-project pass is valid only when no C/C++ files and no build metadata exist.
- Build/test reports record command, exit code, duration, stdout/stderr tail, and failure classification.
- Correction attempts stop when repeated failure indicates upstream contract ambiguity.
-44
View File
@@ -1,44 +0,0 @@
---
name: harness-review
description: Use when reviewing this C++/MSVC Harness repository: local changes, generated phase files, step outputs, implementation diffs, missing tests, MSVC build 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++/MSVC 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, stack choices, C++ test coverage, critical rules, and MSVC/CMake 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
| Item | Question |
| --- | --- |
| Architecture | Does the change follow `docs/ARCHITECTURE.md` ownership boundaries? |
| Stack | Does the change stay within C++/MSVC/CMake decisions documented in `docs/ADR.md`? |
| Tests | Are new or changed behaviors covered by Python Harness tests or C++ tests? |
| TDD Guard | Would C++ production edits be blocked without related tests? |
| Critical Rules | Does the change violate any `AGENTS.md` CRITICAL rule? |
| Build | Do `python -m unittest discover -s scripts -p "test_*.py"` and `python scripts/validate_workspace.py` pass or provide an expected no-CMake message? |
## 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.
@@ -1,4 +0,0 @@
interface:
display_name: "Harness Review"
short_description: "Review Harness changes safely"
default_prompt: "Use $harness-review to review Harness repository changes."
-130
View File
@@ -1,130 +0,0 @@
---
name: harness-workflow
description: Use when planning or running this C++/MSVC 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. Keep every step grounded in repository docs, C++/MSVC 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 C++ behavior changes, require tests first and name the expected test file or test executable.
- 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": "<task-name>",
"steps": [
{ "step": 0, "name": "project-setup", "status": "pending", "allowed_paths": ["CMakeLists.txt", "tests/"] },
{ "step": 1, "name": "core-types", "status": "pending", "allowed_paths": ["src/fesa/core/", "tests/unit/"] },
{ "step": 2, "name": "validation-path", "status": "pending", "allowed_paths": ["scripts/", "docs/"] }
]
}
```
Rules:
- `project` comes from `AGENTS.md`.
- `phase` matches the task directory name.
- `steps[].step` starts at `0`.
- Initial status is always `pending`.
- Each step must declare non-empty `allowed_paths` using repository-relative paths, directory prefixes, or glob patterns.
- 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
- {Exact C++ or Python test file and behavior to add before implementation.}
## 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 규칙을 위반하지 않았는가?
- C++ 변경에는 관련 테스트가 존재하는가?
3. 결과에 따라 `phases/{task-name}/index.json`의 해당 step을 업데이트한다:
- 성공: `"status": "completed"`, `"summary": "산출물 한 줄 요약"`
- 3회 수정 시도 후 실패: `"status": "error"`, `"error_message": "구체적 에러 내용"`
- 사용자 개입 필요: `"status": "blocked"`, `"blocked_reason": "구체적 사유"` 후 중단
## 금지사항
- JavaScript/TypeScript/npm fallback을 추가하지 마라. Reason: 이 Harness는 C++/MSVC 전용이다.
- 기존 테스트를 깨뜨리지 마라.
```
## 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 `codex/{task-name}`, refuses dirty worktrees, requires per-step `allowed_paths`, stages only explicit allowed paths and runner housekeeping files, validates before every runner-created commit, injects `AGENTS.md` and `docs/*.md` into each prompt, carries completed step summaries forward, retries failed steps up to three times, 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.
@@ -1,4 +0,0 @@
interface:
display_name: "Harness Workflow"
short_description: "Plan staged Harness workflow steps"
default_prompt: "Use $harness-workflow to plan Harness phases and step files."
+4
View File
@@ -18,6 +18,10 @@ Testing/
__pycache__/
*.pyc
# local Harness configuration and build outputs
.harness/config.json
.harness/build/
# phase execution outputs
phases/**/phase*-output.json
phases/**/step*-output.json
+16
View File
@@ -0,0 +1,16 @@
{
"version": 1,
"projectType": "auto",
"tdd": {
"testRoots": [
"tests",
"test"
],
"testPatterns": [
"{stem}_test.cpp",
"{stem}_tests.cpp",
"test_{stem}.cpp",
"{stem}.test.cpp"
]
}
}
+39 -29
View File
@@ -11,7 +11,7 @@
## 프로젝트 정체성
- FESA는 유한요소법 기반 구조해석 솔버 개발 프로젝트이다.
- Harness는 솔버 자체가 아니라 요구조건, TDD, phase 실행, 검증을 통제하는 개발 운영 인프라이다.
- Harness는 솔버 자체가 아니라 요구조건, TDD, phase 실행, 검증을 통제하는 개발 운영 인프라이다. 전체 실행 흐름은 `docs/HARNESS_WORKFLOW.md`, 설치와 설정은 `docs/HARNESS.md`를 따른다.
- 문서와 구현은 full Abaqus compatibility를 주장하지 않는다. 기능별로 승인된 Abaqus keyword subset만 지원한다.
- 공식 solver output은 HDF5 `results.h5`이다.
- reference 결과는 FESA와 같은 Abaqus `.inp` 모델을 Abaqus로 해석해 생성한 CSV 파일이다.
@@ -19,12 +19,12 @@
- CSV는 FESA 공식 output이 아니며, FESA HDF5에서 추출한 deterministic CSV view는 비교 디버깅/검토용 보조 artifact로만 둔다.
## 아키텍처 규칙
- CRITICAL: 기본 검증 경로는 `python scripts/validate_workspace.py`이다.
- CRITICAL: C++ 빌드는 CMake/MSVC/x64/Debug 기준으로 검증한다.
- CRITICAL: 새 기능 또는 동작 변경은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현한다.
- CRITICAL: C++ production file을 바꿀 때는 관련 C++ test file이 있어야 한다.
- CRITICAL: Abaqus reference artifact 생성, 수정, 복원은 명시적으로 요청된 phase에서만 수행한다.
- CRITICAL: `harness-workflow` 스킬은 사용자가 명시적으로 허용하기 전까지 사용하지 않는다.
- CRITICAL: public header와 implementation 의존성 방향을 역전하지 말 것
- CRITICAL: 사람 개발자가 검토하기 쉽도록 코드를 작성할것. 그리고 주석도 필수로 작성할 것.
- Domain은 입력 파일에서 생성된 전체 모델 정의를 소유하고, 파싱 이후 가능한 한 불변으로 취급한다.
- AnalysisModel은 현재 step에서 활성화된 elements, loads, boundary conditions, properties/materials의 view를 제공하며 Domain을 복사하지 않는다.
- DofManager는 node별 자유도 정의, constrained/free mapping, equation numbering, sparse pattern ownership을 전담한다. Node 또는 Element 내부에 equation id를 분산 저장하지 않는다.
@@ -32,16 +32,16 @@
- MKL, TBB, HDF5 API는 solver core에 직접 노출하지 않는다. `LinearSolver`, `ParallelFor`, `ResultsWriter`, `Vector`, `Matrix`, `SparseMatrix` adapter 경계 뒤에 둔다.
- Codex custom agent의 `model_reasoning_effort` 기본값은 `extra high`로 둔다.
- Harness runner는 `scripts/execute.py`에 둔다.
- `scripts/execute.py``codex/<phase-name>` branch prefix 사용한다.
- `scripts/execute.py` 실행 전 worktree는 clean 상태여야 한다.
- 각 phase step은 non-empty `allowed_paths`를 선언해야 한다.
- runner는 explicit allowed path와 runner housekeeping file만 stage하며 broad staging을 사용하지 않는다.
- runner가 만드는 모든 commit 전에는 Harness Python self-test와 `python scripts/validate_workspace.py`가 통과해야 한다.
- Codex hook 정책은 `.codex/hooks/`에 둔다.
- `scripts/execute.py``feat-<phase-name>` branch prefix 사용한다.
- runner는 `git add -A`로 변경사항을 stage하므로 실행 전 clean worktree 또는 별도 Git worktree를 사용한다.
- Hook 연결은 `.codex/hooks.json`, 구현은 `scripts/hooks/``scripts/msvc_harness/`에 둔다.
- PreToolUse는 위험 명령과 C++ production file의 대응 테스트 존재 여부를 검사하는 guardrail이며 RED 실행을 증명하지 않는다.
- Stop은 `.harness/config.json` 또는 자동 감지 결과에 따라 MSVC build와 test를 모두 검증한다.
- Generated phase execution outputs remain ignored under `phases/**/step*-output.json`.
## 개발 프로세스
- TDD를 기본으로 한다. 구현은 `RED -> GREEN -> VERIFY` 순서를 따른다.
- CRITICAL: 빌드 경고를 새로 추가하지 말 것.
- 기능 개발은 다음 gate를 순서대로 통과해야 한다.
1. 요구조건 분석
2. 연구자료 조사
@@ -54,10 +54,7 @@
9. reference comparison
10. physics sanity
11. release readiness
- 커밋 전 hook은 Harness Python self-test와 workspace validation을 실행해야 한다.
- 커밋 메시지는 conventional commits 형식을 따른다: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`.
- Codex는 작업 완료 후 검증을 마치면 즉시 변경사항을 commit하고 push한다.
- 계획이 필요한 장기 작업은 Harness phase로 나누고, 각 step은 독립 실행 가능해야 한다.
- 커밋 메시지는 conventional commits 형식을 따른다: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`.
## Agent/Skill Workflow
| 개발 과정 | Agent | Skill | 산출물 |
@@ -76,22 +73,35 @@
| 배포 준비 | `release-agent` | `fesa-release-readiness` | `docs/releases/<feature-id>-release.md` |
## 명령어
```bash
python -m unittest discover -s scripts -p "test_*.py"
python scripts/validate_workspace.py
python scripts/execute.py <phase-dir>
python scripts/execute.py <phase-dir> --push
### Harness Python 검증
```powershell
uv run --with pytest python -m pytest -v -rs
```
## MSVC 검증 기본값
- Generator: `Visual Studio 17 2022`
- Platform: `x64`
- Config: `Debug`
- Build directory: `build/msvc-debug`
### Phase 실행
Override variables:
- `HARNESS_VALIDATION_COMMANDS`
- `HARNESS_CMAKE_GENERATOR`
- `HARNESS_CMAKE_PLATFORM`
- `HARNESS_CMAKE_CONFIG`
- `HARNESS_BUILD_DIR`
```powershell
python scripts/execute.py <phase-name>
python scripts/execute.py <phase-name> --push
```
### CMake/CTest 프로젝트
```powershell
cmake -S . -B .harness/build -A x64
cmake --build .harness/build --config Debug
ctest --test-dir .harness/build -C Debug --show-only=json-v1
ctest --test-dir .harness/build -C Debug --output-on-failure
```
### Visual Studio solution 프로젝트
```powershell
MSBuild.exe MyProject.sln /m /p:Configuration=Debug /p:Platform=x64
.\build\tests\Debug\MyProjectTests.exe
```
실제 solution, preset, 테스트 명령은 `.harness/config.json`과 프로젝트 문서를
우선한다.
+14 -7
View File
@@ -6,18 +6,18 @@ FESA의 architecture decision은 solver correctness, verification traceability,
---
### ADR-001: FESA는 구조해석 솔버 프로젝트이고 Harness는 운영 인프라로 둔다
**결정**: 저장소의 주 목적은 유한요소법 기반 구조해석 솔버 개발이다. Harness scaffold는 phase execution, TDD guard, commit validation, workspace validation을 제공하는 보조 계층으로 유지한다.
**결정**: 저장소의 주 목적은 유한요소법 기반 구조해석 솔버 개발이다. Harness는 승인된 Step 계획, 독립 세션 실행, PreToolUse guardrail, Stop MSVC build/test 검증을 제공하는 보조 계층으로 유지한다.
**이유**: 기존 문서가 Harness 중심이면 agent가 solver architecture, FEM verification, Abaqus/HDF5 계약보다 운영 스크립트에 과도하게 맞춰 행동한다.
**트레이드오프**: Harness 문서의 비중은 낮아지지만, 검증 명령과 hook 정책은 계속 필수 운영 규칙으로 유지한다.
**트레이드오프**: Harness 문서의 비중은 낮아지지만, `docs/HARNESS_WORKFLOW.md`의 실행 계약과 `.codex/hooks.json`검증 정책은 계속 필수 운영 규칙으로 유지한다.
### ADR-002: C++17/MSVC/CMake/CTest를 기본 구현 환경으로 둔다
**결정**: 기본 solver 구현과 validation은 C++17 이상, Visual Studio 17 2022 generator, x64 platform, Debug config, CMake, CTest로 수행한다.
**이유**: FESA의 목표 환경은 Windows/MSVC 기반 C++이다. CMake/CTest는 solver source tree가 추가되거나 확장될 때 가장 일관된 build/test entry point다.
**트레이드오프**: Visual Studio solution-only workflow는 기본 지원하지 않는다. 필요하면 `HARNESS_VALIDATION_COMMANDS`로 override한다.
**트레이드오프**: FESA solver source는 CMake/CTest를 기본으로 유지한다. Harness 자체는 `.harness/config.json`에 solution과 test command를 명시한 직접 MSBuild 프로젝트도 검증할 수 있지만, 이는 FESA 제품이 solution-only workflow를 지원한다는 의미가 아니다.
### ADR-003: Abaqus `.inp` full compatibility가 아니라 기능별 keyword subset을 지원한다
**결정**: FESA parser는 Abaqus keyword/data/comment line 규칙을 따르되, 기능별로 승인된 keyword subset만 지원한다. 미지원 keyword는 명확한 diagnostic을 남긴다.
@@ -75,9 +75,16 @@ FESA의 architecture decision은 solver correctness, verification traceability,
**트레이드오프**: reference 준비가 느려질 수 있다. 대신 검증 기준의 신뢰도와 감사 가능성이 높아진다.
### ADR-011: C++ production 변경은 TDD guard와 workspace validation을 통과해야 한다
**결정**: C++ production file 변경은 관련 C++ test file이 없으면 차단한다. 기본 검증은 `python -m unittest discover -s scripts -p "test_*.py"``python scripts/validate_workspace.py`를 사용한다.
### ADR-011: 구형 단일 검증 진입점 계약을 폐기한다
**이유**: FEM solver 결함은 작은 부호, DOF ordering, integration rule 오류에서도 발생한다. 테스트 없는 변경을 막아야 reference validation 이전 단계에서 회귀를 줄일 수 있다.
**상태**: ADR-012로 대체됨.
**트레이드오프**: 초기 scaffolding 작업에서 guard가 엄격하게 느껴질 수 있다. 문서, CMake 설정, Harness metadata는 guard 대상에서 제외한다.
**결정**: 삭제된 legacy validation entry point, legacy Python test discovery, 환경 변수 기반 command override를 Harness의 기본 검증 계약으로 사용하지 않는다.
### ADR-012: Harness는 계획, 실행, Hook 검증의 세 계층으로 운영한다
**결정**: 계획은 `.agents/skills/harness`, Step 실행과 Git 상태 관리는 `scripts/execute.py`, 도구 호출 전 정책과 종료 전 검증은 `.codex/hooks.json`에 연결된 `scripts/hooks/`가 담당한다. C++ production 변경은 관련 테스트 파일이 있어야 하며, 실제 RED 실패와 GREEN 성공은 같은 Step 안에서 확인한다. Stop은 `.harness/config.json` 또는 자동 감지 결과로 전체 MSVC build/test를 검증한다. Harness Python 변경은 `uv run --with pytest python -m pytest -v -rs`로 검증한다.
**이유**: 테스트 파일 존재 검사, TDD 실행 증거, 전체 build/test는 서로 다른 책임이다. 이를 분리하면 Hook이 보장하는 범위를 과장하지 않으면서 Step 종료 시 green 상태를 강제할 수 있다.
**트레이드오프**: PreToolUse만으로 구현 전 RED 실행을 증명할 수 없으므로 Implementation report에 RED/GREEN 명령과 결과를 기록해야 한다. C/C++가 없는 저장소는 Stop이 통과하므로 Harness Python 검증은 별도 acceptance command로 유지한다.
+22 -49
View File
@@ -9,7 +9,7 @@ FESA의 아키텍처 목표는 Abaqus `.inp` subset을 내부 semantic model로
- sparse linear algebra backend isolation
- deterministic verification
- incremental feature addition
- Harness 기반 TDD와 workspace validation
- Harness 기반 TDD
## 디렉토리 구조
```text
@@ -31,6 +31,9 @@ src/
analysis/ # static, modal, dynamic, nonlinear procedure drivers
results/ # recovery, field/history output, diagnostics
validation/ # comparison metrics and tolerance helpers
math/
Vector/ # managing double array including vector operation, vector operation must use BLAS from MKL
Matrix/ # managing double array including matrix operation, matrix operation must use BLAS from MKL
tests/
unit/
integration/
@@ -43,24 +46,29 @@ reference/
<model-id>_reactions.csv
<model-id>_internalforces.csv
<model-id>_stresses.csv
.agents/
skills/ # Harness and review skills
.codex/
hooks/ # Codex hook scripts
skills/ # FESA solver and Harness instructions
hooks.json # PreToolUse/Stop hook registration
agents/ # FESA workflow custom agents
skills/ # FESA solver workflow skills
docs/ # Product, architecture, ADR, workflow artifacts
scripts/
execute.py # Phase step executor
validate_workspace.py # Default validation entry point
test_*.py # Harness self-tests
hooks/ # PreToolUse/Stop hook implementations
msvc_harness/ # MSVC project discovery and validation adapters
phases/ # Optional generated phase plans
```
## Harness Execution Layer
`scripts/execute.py`:
- creates or checks out `codex/<phase-name>`
- refuses to run on a dirty worktree
- requires per-step `allowed_paths`
- stages only explicit allowed paths and runner housekeeping files
- runs Python Harness self-tests and workspace validation before every runner-created commit
Harness는 solver core와 분리된 세 계층의 개발 운영 인프라다.
- 계획 계층: `.agents/skills/harness`가 사용자 승인 전 Step 초안을 만들고, 승인 후 `phases/` 파일을 생성한다.
- 실행 계층: `scripts/execute.py``feat-<phase-name>` 브랜치에서 Step마다 독립 Codex 세션을 실행하고 상태와 커밋을 관리한다.
- 검증 계층: `.codex/hooks.json``scripts/hooks/pre_tool_use.py``scripts/hooks/stop_validation.py`를 연결한다. Stop 검증은 `scripts/msvc_harness/`를 통해 MSVC build와 test를 실행한다.
Runner는 `git add -A`를 사용하므로 clean worktree 또는 별도 Git worktree가 실행 전제다. 전체 동작 계약은 `docs/HARNESS_WORKFLOW.md`, 설치와 `.harness/config.json` 설정은 `docs/HARNESS.md`를 source of truth로 삼는다.
## 모듈 경계
- `core`는 외부 라이브러리에 의존하지 않는다.
@@ -148,6 +156,9 @@ Results
├── ResultFrame
├── FieldOutput
└── HistoryOutput
Vector
Matrix
```
## 상태 관리
@@ -231,41 +242,3 @@ Schema requirements:
- Abaqus reference results는 `reference/<model-id>/` 아래 CSV 파일이다.
- Verification은 documented IDs, components, units, coordinate system, step/frame identity, tolerance 기준으로 FESA HDF5 rows와 Abaqus reference CSV rows를 비교한다.
- FESA HDF5에서 추출한 deterministic CSV view는 optional debugging/review artifact이며 공식 solver output 또는 reference artifact가 아니다.
## Test Architecture
- unit: parser, DOF map, shape functions, material law, sparse assembly, HDF5 schema
- integration: small `.inp` to HDF5 end-to-end
- reference: FESA `results.h5` rows and Abaqus reference CSV rows comparison
- physics: equilibrium, sign, symmetry, rigid body mode, stress sanity
- harness: hooks, phase executor, workspace validation
## Hook 흐름
```text
apply_patch/Edit/Write
-> .codex/hooks/tdd-guard.py
-> C++ production changes require related tests
git commit command
-> .codex/hooks/pre_commit_checks.py
-> Python Harness self-tests
-> scripts/validate_workspace.py
```
## Validation 흐름
```text
HARNESS_VALIDATION_COMMANDS set
-> run exact commands
CMakePresets.json has msvc-debug configure preset
-> cmake --preset msvc-debug
-> cmake --build preset binary dir --config Debug
-> ctest --test-dir preset binary dir -C Debug
CMakeLists.txt exists
-> cmake -S . -B build/msvc-debug -G "Visual Studio 17 2022" -A x64
-> cmake --build build/msvc-debug --config Debug
-> ctest --test-dir build/msvc-debug --output-on-failure -C Debug
No CMake project
-> print guidance and exit successfully
```
+124
View File
@@ -0,0 +1,124 @@
# Harness 운영 가이드
## Requirements
Windows, Python 3.10 이상, Codex CLI가 필요하다. CMake 프로젝트에는 Visual Studio의
Desktop development with C++ 워크로드와 MSBuild, CMake/CTest를 설치한다.
## 프로젝트 자동 감지
프로젝트 형식은 다음 순서로 결정한다: `.harness/config.json`의 명시적 type, 루트의
CMake metadata, 하나의 `.sln`, 하나의 `.vcxproj` 순서다. C/C++가 아닌 저장소는
건너뛰며, C/C++ 파일은 있지만 CMake/solution metadata가 없는 orphan-C++ 저장소는
오류로 처리한다.
설정 파일은 선택 사항이다. 기본 자동 감지와 `.harness/build` 경로를 그대로 사용할
때는 만들지 않아도 된다. 프로젝트별 override가 필요하면 다음처럼 예시를 복사한다.
```powershell
Copy-Item .harness/config.example.json .harness/config.json
```
계획을 승인해 phase 파일을 만든 뒤 Executor를 실행한다.
```powershell
python scripts/execute.py <phase-name>
python scripts/execute.py <phase-name> --push
```
## Harness Python 검증
이 저장소의 테스트와 최종 acceptance 검증은 pytest를 시스템 Python에 설치하지 않고
다음 명령으로 실행한다.
```powershell
uv run --with pytest python -m pytest -v -rs
```
## CMake preset 설정
`projectType``cmake`로 지정하거나 자동 감지를 사용한다. `cmake.sourceDir`,
`binaryDir`, `configurePreset`, `buildPreset`, `testPreset`은 preset을 사용할 때 함께
지정해야 한다. 빌드 산출물은 저장소의 `.harness/build/`처럼 격리된 경로에 둔다.
```json
{
"version": 1,
"projectType": "cmake",
"cmake": {
"sourceDir": ".",
"binaryDir": "out/build/windows-debug",
"configurePreset": "windows-debug",
"buildPreset": "windows-debug",
"testPreset": "windows-debug"
}
}
```
```powershell
cmake --preset windows-debug
cmake --build --preset windows-debug
ctest --preset windows-debug --output-on-failure
```
Preset을 쓰지 않는 경우에는 같은 격리된 build directory를 명시한다.
```powershell
cmake -S . -B .harness/build -A x64
cmake --build .harness/build --config Debug
ctest --test-dir .harness/build -C Debug --show-only=json-v1
ctest --test-dir .harness/build -C Debug --output-on-failure
```
## 직접 MSBuild 설정
`projectType``msbuild`로 설정하면 `msbuild.solution`, `configuration`, `platform`
지정한다. 직접 MSBuild 프로젝트에서는 `msbuild.testCommand`가 필수이며, 테스트 실행
파일과 인수를 JSON 배열로 적는다.
```json
{
"version": 1,
"projectType": "msbuild",
"msbuild": {
"solution": "MyProject.sln",
"configuration": "Debug",
"platform": "x64",
"testCommand": ["build/tests/Debug/MyProjectTests.exe"]
}
}
```
```powershell
MSBuild.exe MyProject.sln /m /p:Configuration=Debug /p:Platform=x64
.\build\tests\Debug\MyProjectTests.exe
```
## TDD 확장
`tdd.testRoots``tdd.testPatterns`로 테스트 위치와 이름을 확장한다. 패턴마다
`{stem}`이 필요하다. `main`, 테스트, 외부 의존성, 생성 파일, build directory 같은
기본 제외 항목은 Harness가 관리하며, `tdd.exclude`의 사용자 제외 항목은 이를
대체하지 않고 추가한다.
```json
{
"version": 1,
"tdd": {
"testRoots": ["tests", "integration-tests"],
"testPatterns": ["{stem}_test.cpp", "test_{stem}.cpp"],
"exclude": ["legacy/generated/**"]
}
}
```
## 실패 복구
- Visual Studio C++ workload가 없으면 Installer에서 Desktop development with C++를 설치한 뒤 다시 실행한다.
- solution 또는 project가 여러 개라서 모호하면 `projectType``msbuild.solution`을 명시한다.
- MSVC가 아닌 컴파일러가 감지되면 MSVC Developer Command Prompt에서 실행하거나 toolchain을 MSVC로 전환한다.
- CTest가 0개 테스트를 보고하면 `enable_testing()`과 테스트 등록을 확인한다.
- 직접 MSBuild 구성에 test command가 없으면 `msbuild.testCommand` 배열을 추가한다.
- timeout 또는 명령 실패 시 Stop 응답의 stage, 안전한 argv 배열, 작업 디렉터리,
종료 코드와 출력 tail을 확인하고 해당 명령을 단독으로 다시 실행한다. Harness는
별도의 로그 파일을 만들지 않는다.
+607
View File
@@ -0,0 +1,607 @@
# Harness Framework 동작 과정
이 문서는 자연어 요구사항을 받은 뒤 Harness Framework가 계획을 만들고, 독립된
Codex 세션에서 Step을 실행하고, MSVC로 C++ 프로젝트를 검증하는 전체 과정을
설명한다. 설치 및 설정 예시는 [Harness 운영 가이드](HARNESS.md)를 참고한다.
## 1. 핵심 구조
Harness Framework는 다음 세 계층으로 구성된다.
1. **계획 계층**: 요구사항을 분석하고 사용자가 승인할 실행 가능한 Step으로 변환한다.
2. **실행 계층**: Step Executor가 Step마다 독립 Codex 세션을 실행하고 상태와 Git
커밋을 관리한다.
3. **검증 계층**: PreToolUse 훅이 편집 전 정책을 검사하고, Stop 훅이 종료 전 MSVC
빌드와 테스트를 실행한다.
전체 흐름은 다음과 같다.
```text
사용자 요구사항
프로젝트 탐색 및 요구사항 논의
Step 초안 작성
사용자 승인
phases/index.json, task index, stepN.md 생성
Step Executor 시작
각 Step을 독립 Codex 세션에서 실행
├─ 도구 호출 전: PreToolUse 정책 검사
└─ 응답 종료 전: Stop MSVC 빌드·테스트
성공: 커밋 후 다음 Step
실패: 수정 또는 최대 3회 재시도
차단: 사용자 개입을 기다리며 중단
```
자연어 요구사항만으로 Executor가 자동 시작되지는 않는다. 계획을 사용자가 승인하고
phase 파일을 생성한 다음 `scripts/execute.py`를 실행해야 구현 루프가 시작된다.
## 2. 요구사항 탐색과 구체화
예를 들어 사용자가 다음 요구사항을 전달했다고 가정한다.
> CMake 기반 C++20 라이브러리에 `divide()` 함수를 추가하고, 0으로 나누면 예외를
> 발생시키며 GoogleTest 테스트를 작성한다.
계획을 작성하기 전에 다음 자료를 확인한다.
- `AGENTS.md`
- `docs/PRD.md`
- `docs/ARCHITECTURE.md`
- `docs/ADR.md`
- 관련 제품 코드와 테스트
- `.harness/config.json`
이 탐색을 통해 다음 조건을 구체화한다.
- 사용하는 MSVC toolset과 C++ 표준
- CMake 프로젝트인지 Visual Studio solution/project인지
- 테스트 프레임워크와 테스트 실행 방법
- public header와 implementation의 의존성 방향
- 수정할 모듈과 범위 밖 항목
- 실행 가능한 Acceptance Criteria 명령
요구사항에 결정되지 않은 부분이 있으면 구현 전에 사용자와 논의한다. 위 예에서는
예외 타입, 정수 또는 부동소수점 연산 여부, public API와 ABI 변경 허용 여부가 이에
해당한다.
## 3. 요구사항을 Step으로 분해
사용자가 구현 계획 작성을 요청하면 요구사항을 작은 Step으로 나눈다. Step 설계
규칙은 [Harness Workflow](../.agents/skills/harness/SKILL.md)에 정의되어 있다.
각 Step은 다음 조건을 만족해야 한다.
- 하나의 모듈 또는 명확한 한 가지 책임만 다룬다.
- 다른 대화 내용을 참조하지 않아도 실행할 수 있도록 자기완결적으로 작성한다.
- 먼저 읽을 문서와 이전 Step의 관련 파일을 명시한다.
- 클래스와 함수 시그니처 수준으로 작업 범위를 설명한다.
- 실제 실행 가능한 빌드·테스트 명령을 Acceptance Criteria로 사용한다.
- 성공, 오류, 사용자 개입 필요 상태의 판정 기준을 적는다.
- 범위 밖 기능과 기존 테스트 회귀를 명시적으로 금지한다.
예시 Step은 다음과 같은 내용을 포함할 수 있다.
```text
Step 0: division-api
읽어야 할 파일
- AGENTS.md
- include/calculator.hpp
- src/calculator.cpp
- tests/calculator_test.cpp
작업
- divide(double lhs, double rhs)의 실패 테스트를 먼저 추가한다.
- rhs가 0이면 std::invalid_argument가 발생하도록 최소 구현한다.
Acceptance Criteria
- CMake/MSBuild 빌드가 성공한다.
- 전체 테스트가 성공한다.
- 새로운 컴파일러 경고가 없다.
```
### TDD와 Step 경계
프로젝트 규칙은 실패하는 테스트를 먼저 요구하지만 Stop 훅은 Codex가 Step을 종료할
때 전체 테스트 성공을 요구한다. 따라서 다음처럼 실패 상태를 Step 사이에 남겨둘 수
없다.
```text
Step 0: 실패하는 테스트만 추가하고 종료
Step 1: 제품 코드를 구현해 테스트 통과
```
실제 red-green 순서는 하나의 Codex 실행 안에서 완료되어야 한다.
```text
테스트 작성
→ 테스트 실패 확인
→ 최소 제품 코드 구현
→ 테스트 성공 확인
→ Step 종료
```
즉, 테스트가 구현보다 먼저 작성되는 순서는 지키되 각 Step은 최종적으로 green
상태여야 한다.
## 4. 사용자 승인 후 생성되는 파일
Step 초안을 사용자가 승인한 뒤에만 다음 파일을 생성한다.
```text
phases/
├── index.json
└── add-division/
├── index.json
├── step0.md
├── step1.md
└── ...
```
### 4.1 Top-level index
`phases/index.json`은 여러 task의 상태를 관리한다.
```json
{
"phases": [
{
"dir": "add-division",
"status": "pending"
}
]
}
```
### 4.2 Task index
`phases/add-division/index.json`은 task 내부 Step의 상태를 관리한다.
```json
{
"project": "Calculator",
"phase": "add-division",
"steps": [
{
"step": 0,
"name": "division-api",
"status": "pending"
}
]
}
```
상태별 기록은 다음과 같이 나뉜다.
| 상태 | Codex가 기록 | Executor가 기록 |
|---|---|---|
| `completed` | `summary` | `completed_at` |
| `error` | `error_message` | `failed_at` |
| `blocked` | `blocked_reason` | `blocked_at` |
Executor는 task의 `created_at`과 Step의 `started_at`도 기록한다. `summary`는 다음
독립 Codex 세션이 이전 Step의 핵심 산출물과 결정을 이해할 수 있도록 한 줄로
작성한다.
### 4.3 Step 파일
`stepN.md`에는 다음 내용이 들어간다.
- 읽어야 할 파일
- 작업 범위와 인터페이스
- 핵심 동작 및 불변 조건
- Acceptance Criteria 명령
- 아키텍처·ADR·CRITICAL 규칙 확인 절차
- 성공, 오류, 차단 상태 기록 방법
- 범위 밖 변경 금지사항
Step은 독립 Codex 실행의 전체 작업 지시서이므로 이전 대화만 참조하는 표현을 넣지
않는다.
## 5. Step Executor 시작
계획 파일을 생성한 뒤 다음 명령으로 실행한다.
```powershell
python scripts/execute.py add-division
```
완료된 브랜치를 원격 저장소에 자동 push하려면 `--push`를 추가한다.
```powershell
python scripts/execute.py add-division --push
```
[Step Executor](../scripts/execute.py)는 시작할 때 다음 작업을 수행한다.
1. phase 디렉터리와 task index가 존재하는지 검사한다.
2. 이전 실행에서 `error` 또는 `blocked`로 끝난 Step이 있는지 검사한다.
3. `feat-{phase-name}` 브랜치를 생성하거나 checkout한다.
4. `AGENTS.md``docs/*.md`를 guardrail로 읽는다.
5. task의 `created_at`이 없으면 기록한다.
6. 첫 번째 `pending` Step부터 순차 실행한다.
`AGENTS.md`와 모든 `docs/*.md` 내용은 각 Codex 프롬프트에 직접 삽입된다. 따라서
이 문서들은 참고 자료가 아니라 실제 실행 입력이다. 서로 충돌하거나 placeholder가
남아 있으면 Codex도 그 모순을 입력으로 받는다.
## 6. Step마다 독립 Codex 세션 실행
Executor는 각 Step을 다음 형태의 독립 프로세스로 실행한다.
```text
codex exec
--json
--sandbox workspace-write
--dangerously-bypass-hook-trust
--cd <repository-root>
-
```
Codex에 전달하는 프롬프트는 다음 내용의 조합이다.
```text
AGENTS.md와 docs 문서
+ 이전에 완료된 Step의 summary
+ 이전 시도의 오류(재시도인 경우)
+ Executor 공통 작업 규칙
+ 현재 stepN.md
```
이전 Step의 전체 대화나 Codex 세션은 전달하지 않는다. task index에 기록한
`summary`만 다음 Step에 누적한다.
Codex 실행 결과의 exit code, stdout, stderr는 다음 파일에 저장한다.
```text
phases/{task-name}/step{N}-output.json
```
## 7. 도구 호출 전 PreToolUse 검사
[`.codex/hooks.json`](../.codex/hooks.json)은 shell 및 파일 편집 도구에
[PreToolUse 훅](../scripts/hooks/pre_tool_use.py)을 등록한다. Codex가 실제 명령이나
편집을 수행하기 전에 이 훅이 요청을 검사한다.
### 7.1 위험 명령 차단
다음 유형의 명령은 요구사항과 관계없이 차단한다.
- `git reset --hard`
- `git push --force` 또는 `--force-with-lease`
- `rm -rf`
- `Remove-Item -Recurse -Force`
- `rmdir /s /q`
- `DROP TABLE`
위험 패턴이 발견되면 훅은 차단 이유를 stderr로 출력하고 종료 코드 2를 반환한다.
그러면 해당 도구 호출은 실행되지 않는다.
### 7.2 C++ TDD 검사
`apply_patch`, `Edit`, `MultiEdit`, `Write`로 다음 C/C++ 확장자의 파일을 편집하려
하면 [TDD 정책](../scripts/msvc_harness/tdd_policy.py)을 검사한다.
```text
.c .cc .cpp .cxx .h .hpp .hxx
```
일반 제품 코드를 수정하려면 대응되는 테스트 파일이 먼저 존재해야 한다. 예를 들어
`src/calculator.cpp`의 기본 대응 테스트 이름은 다음과 같다.
```text
calculator_test.cpp
calculator_tests.cpp
test_calculator.cpp
calculator.test.cpp
```
테스트는 다음 위치에서 검색한다.
- `.harness/config.json``tdd.testRoots`
- 제품 파일과 같은 디렉터리 아래 `tests/`
- 제품 파일과 같은 디렉터리 아래 `test/`
다음 파일과 디렉터리는 대응 테스트 존재 검사가 면제된다.
- 테스트 파일 자체
- `main.cpp`
- `.harness/build/**`, `build/**`, `out/**`
- `cmake-build-*/**`
- `third_party/**`, `external/**`, `vendor/**`
- `generated/**`
- `tdd.exclude`에 추가한 경로
`tdd.exclude`는 기본 제외 항목을 대체하지 않고 추가한다.
### 7.3 TDD 검사가 보장하는 범위
현재 TDD 훅이 직접 보장하는 것은 대응되는 이름의 테스트 파일이 존재한다는
사실이다. 다음 항목까지 증명하지는 않는다.
- 테스트가 이번 요구사항을 실제로 검증하는가
- 구현 전에 테스트가 실제로 실패했는가
- 테스트의 assertion과 경계 조건이 충분한가
- 기존 테스트 파일을 이번 변경과 함께 수정했는가
또한 shell 명령의 리다이렉션 등으로 C++ 파일을 쓰는 경우 shell 위험 패턴 검사는
적용되지만 경로 기반 TDD 검사는 적용되지 않는다. 따라서 이 훅은 완전한 TDD
증명기가 아니라 테스트 우선 편집을 유도하는 guardrail이다.
## 8. Codex 종료 전 Stop 검증
Codex가 Step 작업을 끝내고 응답을 종료하려 하면
[Stop 훅](../scripts/hooks/stop_validation.py)이 실행된다. Stop 훅은 변경 파일만이
아니라 발견된 C/C++ 프로젝트 전체를 빌드하고 테스트한다.
### 8.1 저장소 루트와 재진입 방지
Stop 훅은 `git rev-parse --show-toplevel`로 프로젝트 루트를 결정한다. Git 저장소를
찾을 수 없으면 현재 디렉터리를 사용한다.
빌드나 테스트의 자식 프로세스에는 `CODEX_STOP_VALIDATION_ACTIVE=1`을 전달한다.
같은 훅이 자식 프로세스에서 다시 진입하면 즉시 성공 처리하여 검증 재귀를 막는다.
### 8.2 설정 로드
[설정 로더](../scripts/msvc_harness/config.py)는 `.harness/config.json`을 읽는다.
파일이 없으면 다음 기본값을 사용한다.
- `version`: 1
- `projectType`: `auto`
- CMake source: 저장소 루트
- preset 미사용 시 binary directory: `.harness/build`
- configuration: `Debug`
- platform: `x64`
- 테스트 루트: `tests`, `test`
- 기본 테스트 이름 패턴 네 개
설정은 다음 조건을 엄격하게 검사한다.
- 알 수 없는 필드를 거부한다.
- `version`은 숫자 1만 허용한다.
- `projectType``auto`, `cmake`, `msbuild`만 허용한다.
- 저장소 상대 경로만 허용한다.
- 저장소 밖으로 해석되는 경로를 거부한다.
- CMake preset을 사용하면 `configurePreset`, `buildPreset`, `testPreset`,
`binaryDir`를 모두 요구한다.
- 모든 `tdd.testPatterns``{stem}`을 요구한다.
### 8.3 프로젝트 자동 감지
[프로젝트 탐색기](../scripts/msvc_harness/discovery.py)는 다음 순서로 프로젝트를
선택한다.
1. `projectType: cmake` 또는 `projectType: msbuild` 명시 설정
2. 루트의 `CMakePresets.json`
3. 루트의 `CMakeUserPresets.json`
4. 루트의 `CMakeLists.txt`
5. 루트의 단일 `.sln`
6. 루트의 단일 `.vcxproj`
자동 감지 결과는 다음처럼 처리한다.
| 저장소 상태 | 결과 |
|---|---|
| CMake metadata가 있음 | CMake 프로젝트 선택 |
| 하나의 `.sln` 또는 `.vcxproj`가 있음 | MSBuild 프로젝트 선택 |
| 여러 solution/project가 있음 | 설정으로 하나를 지정하라는 오류 |
| C/C++ 파일과 build metadata가 모두 없음 | 검증할 프로젝트가 없으므로 통과 |
| C/C++ 파일은 있지만 build metadata가 없음 | orphan C++ 프로젝트 오류 |
### 8.4 MSVC 도구 탐색
[도구 탐색기](../scripts/msvc_harness/toolchain.py)는 `vswhere.exe`로 다음을
확인한다.
- Visual Studio 설치 경로
- Desktop development with C++ workload
- `MSBuild.exe`
CMake 프로젝트에서는 다음 우선순위로 CMake와 CTest를 선택한다.
1. PATH에서 발견한 독립 `cmake.exe``ctest.exe`
2. Visual Studio에 번들된 CMake와 CTest
따라서 새로 설치한 CMake의 `bin` 디렉터리가 PATH에 반영되어 있으면 독립 CMake를
우선 사용한다.
## 9. 빌드 시스템별 검증 계획
### 9.1 CMake preset 미사용
[CMake adapter](../scripts/msvc_harness/adapters/cmake.py)는 다음 검증 계획을 만든다.
```powershell
cmake -S <source> -B .harness/build -A x64
cmake --build .harness/build --config Debug
ctest --test-dir .harness/build -C Debug --show-only=json-v1
ctest --test-dir .harness/build -C Debug --output-on-failure
```
명령 성공 외에 다음 결과도 검사한다.
- 생성된 CMake compiler metadata의 `CMAKE_CXX_COMPILER_ID``MSVC`인가
- CTest JSON에 한 개 이상의 테스트가 있는가
따라서 빌드가 성공해도 MinGW 등 다른 컴파일러를 사용했거나 CTest가 테스트를 한
개도 발견하지 못하면 실패한다.
### 9.2 CMake preset 사용
`.harness/config.json`에 preset을 완전히 지정하면 다음 형태로 실행한다.
```powershell
cmake --preset <configurePreset>
cmake --build --preset <buildPreset>
ctest --preset <testPreset> --show-only=json-v1
ctest --preset <testPreset> --output-on-failure
```
이 경우 모든 명령은 `cmake.sourceDir`에서 실행하고 compiler metadata 검사는 설정한
`binaryDir`에서 수행한다.
### 9.3 직접 MSBuild
[MSBuild adapter](../scripts/msvc_harness/adapters/msbuild.py)는 다음 순서로 실행한다.
```powershell
MSBuild.exe <solution-or-vcxproj> /m /nologo `
/p:Configuration=<configuration> `
/p:Platform=<platform>
<msbuild.testCommand>
```
직접 MSBuild 프로젝트는 표준 테스트 탐색 명령이 없으므로
`.harness/config.json``msbuild.testCommand`가 반드시 필요하다. 이 값이 없으면
Stop 검증이 실패한다.
## 10. 명령 실행 안전성과 제한시간
[검증 실행기](../scripts/msvc_harness/process.py)는 다음 안전 규칙을 적용한다.
- 명령을 shell 문자열이 아닌 argv 배열로 실행한다.
- `shell=False`를 사용한다.
- 각 명령의 working directory가 저장소 내부인지 검사한다.
- 명령별 제한시간과 Stop 전체 제한시간 중 더 짧은 값을 적용한다.
- 종료 코드가 0이 아니면 즉시 해당 stage를 실패 처리한다.
Stop 훅의 전체 제한시간은 저장소 탐색, toolchain 탐색, configure, build, test discovery,
test를 모두 포함해 1,800초다. `.codex/hooks.json`의 Stop command timeout도 1,800초다.
실패 메시지에는 다음 진단 정보를 포함한다.
- 실패 stage
- 안전하게 표현한 argv 배열
- working directory
- 종료 코드
- stdout과 stderr의 마지막 8,000자
Harness는 별도 빌드 로그 파일을 생성하지 않는다.
## 11. 성공, 실패, 차단 처리
### 11.1 Stop 검증 성공
빌드와 테스트가 모두 성공하면 Stop 훅은 출력 없이 종료한다. Codex가 정상 종료하면
Executor가 task index를 다시 읽는다.
Codex가 Step을 다음처럼 기록한 경우:
```json
{
"step": 0,
"name": "division-api",
"status": "completed",
"summary": "divide API와 0 나누기 테스트를 추가함"
}
```
Executor는 `completed_at`을 기록하고 변경사항을 커밋한 뒤 다음 `pending` Step을
실행한다.
### 11.2 Stop 검증 실패
Stop 훅은 Codex hook protocol에 따라 다음 형태의 응답을 출력한다.
```json
{
"continue": false,
"stopReason": "build failed ...",
"systemMessage": "build failed ..."
}
```
Codex 프로세스에 대한 훅 자체의 종료 코드는 0이지만 `continue: false`가 Codex의
응답 종료를 막는다. Codex는 같은 세션에서 오류를 확인하고 수정을 계속한다.
### 11.3 Executor 재시도
Codex 프로세스가 끝났는데 Step 상태가 `completed` 또는 `blocked`가 아니면 Executor가
새 Codex 세션으로 재시도한다.
```text
첫 번째 시도 실패
→ 오류를 다음 프롬프트에 삽입
→ 두 번째 독립 Codex 실행
→ 다시 실패하면 세 번째 독립 Codex 실행
→ 세 번째도 실패하면 error 기록 후 종료
```
즉, 실패 복구에는 두 층이 있다.
1. Stop 훅이 같은 Codex 세션에서 수정하도록 요구한다.
2. 세션 자체가 성공하지 못하면 Executor가 새 세션으로 최대 3회 재시도한다.
### 11.4 사용자 개입 필요
인증, API 키, 수동 설치처럼 Codex가 자동으로 해결할 수 없는 문제가 있으면 Step을
`blocked`로 기록한다. Executor는 `blocked_at`과 top-level 상태를 갱신하고 종료 코드
2로 중단한다.
재개하려면 원인을 해결하고 해당 Step을 `pending`으로 되돌린 뒤
`blocked_reason`을 제거하고 다시 실행한다. `error`도 같은 방식으로 `pending`으로
되돌리고 `error_message`를 제거한 뒤 재실행한다.
## 12. Git 커밋과 phase 완료
Step이 성공하면 제품 변경과 Harness metadata를 분리해 다음 형식으로 커밋한다.
```text
feat(add-division): step 0 — division-api
chore(add-division): step 0 output
```
두 번째 커밋의 `output`은 task index의 Step 상태와 summary 같은 Harness metadata를
뜻한다. 원시 Codex 실행 기록인 `stepN-output.json``.gitignore` 대상이며 커밋에
포함되지 않는다.
모든 Step이 완료되면 Executor는 다음 작업을 수행한다.
- task의 `completed_at` 기록
- `phases/index.json`의 task 상태를 `completed`로 변경
- 최종 metadata 커밋
- `--push` 사용 시 `origin/feat-{phase-name}`으로 push
커밋 과정은 `git add -A`를 사용한다. 실행 전에 작업 트리에 관련 없는 사용자
변경사항이 남아 있으면 그 변경도 Step 커밋에 포함될 수 있다. 따라서 깨끗한
worktree 또는 별도 Git worktree에서 실행하는 것이 안전하다.
## 13. 요구사항 종류별 동작
| 받은 요구사항 또는 변경 | PreToolUse 동작 | Stop 동작 |
|---|---|---|
| 새 C++ 제품 파일 추가 | 대응 테스트가 먼저 없으면 차단 | 전체 빌드·테스트 |
| 기존 C++ 구현 또는 header 수정 | 대응 테스트 파일 존재 여부 검사 | 전체 빌드·테스트 |
| 테스트 파일 추가 | TDD 차단 없이 허용 | 모든 테스트가 성공해야 종료 |
| `main.cpp` 수정 | TDD 대응 테스트 검사 면제 | 전체 빌드·테스트 |
| 문서, JSON, Python 수정 | C++ TDD 검사 없음 | C++ 프로젝트가 있으면 전체 검증 |
| 위험한 Git 또는 삭제 명령 | 즉시 차단 | 도달하지 않음 |
| C++ 파일은 있지만 build metadata 없음 | 편집은 허용될 수 있음 | orphan 프로젝트 오류 |
| 직접 MSBuild인데 `testCommand` 없음 | 편집은 허용될 수 있음 | 설정 오류로 종료 차단 |
| C/C++가 전혀 없는 저장소 | 관련 편집 검사 없음 | 검증할 프로젝트가 없어 통과 |
## 14. 적용 전 준비사항
이 저장소는 대상 C++ 프로젝트에 맞게 채워 사용하는 템플릿이다. 실행 전 다음을
확인한다.
1. `AGENTS.md`의 프로젝트명, toolset, C++ 표준, 테스트 프레임워크, CRITICAL 규칙을
실제 값으로 교체한다.
2. `docs/PRD.md`, `docs/ARCHITECTURE.md`, `docs/ADR.md`의 placeholder와 예시를 실제
프로젝트 정보로 교체한다.
3. 기본 자동 감지로 충분하지 않을 때만 `.harness/config.example.json`을 참고해
`.harness/config.json`을 만든다.
4. CMake 또는 MSBuild metadata와 테스트 실행 방법을 확인한다.
5. `phases/`가 없다면 요구사항 논의와 계획 승인을 거쳐 task 파일을 먼저 만든다.
6. Executor 실행 전에 Git working tree가 깨끗한지 확인한다.
특히 `AGENTS.md``docs/*.md`는 각 Codex 실행에 그대로 주입된다. C++ 프로젝트에서
TypeScript 예시나 미완성 placeholder가 남아 있으면 실제 작업 지시와 충돌할 수 있다.
+8 -16
View File
@@ -20,11 +20,11 @@ FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 유한요소법 기
6. `LinearSolver` adapter를 통한 MKL PARDISO backend와 향후 iterative solver 확장
7. HDF5 기반 `ResultStep` -> `ResultFrame` -> `FieldOutput`/`HistoryOutput` 저장
8. FESA HDF5 rows와 `reference/<model-id>/` 아래 Abaqus reference CSV rows의 직접 비교
9. CMake/MSVC/x64/Debug, CTest, Harness validation, TDD guard 기반 개발 검증
9. CMake/MSVC/x64/Debug, CTest 기반 개발 검증
## V0 범위
- 선형 정적 해석 골격
- 첫 end-to-end 기능 후보: 1D truss/bar element
- 선형 정적 해석 파이프라인 구현
- 첫 end-to-end 기능 후보: Isoparametric 3D Euler beam element
- 최소 Abaqus keyword subset:
- `*HEADING`
- `*NODE`
@@ -33,24 +33,17 @@ FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 유한요소법 기
- `*ELSET`
- `*MATERIAL`
- `*ELASTIC`
- section keyword
- `*Beam General Section`
- `*BOUNDARY`
- `*CLOAD`
- `*STEP`
- `*STATIC`
- output request subset
- displacement 중심의 최소 `AnalysisState`
- MKL PARDISO 기반 sparse direct solver
- displacements, reactions, elemental forces 중심의 verification
- HDF5 result schema v0
- FESA HDF5 to Abaqus reference CSV comparison 계약
## V1 범위
- 2D plane stress/plane strain element
- 3D solid element
- MKL PARDISO 기반 sparse direct solve
- TBB element-local computation 병렬화
- reference model portfolio 확장
- nonlinear static, dynamic, frequency, heat transfer 해석을 위한 interface 확장점
## 기능 요구조건
| ID | 요구조건 | Acceptance Criteria | Verification Method |
| --- | --- | --- | --- |
@@ -61,7 +54,7 @@ FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 유한요소법 기
| FESA-PRD-005 | FESA는 해석 중 변하는 물리량을 `AnalysisState`에 저장해야 한다. | displacement, force, residual, increment/iteration 상태가 step/frame 출력과 연결된다. | state unit test, integration test |
| FESA-PRD-006 | FESA는 solver 결과를 HDF5 authoritative output `results.h5`로 저장해야 한다. | step/frame, field/history, metadata, diagnostics가 schema version과 함께 저장된다. | HDF5 schema test |
| FESA-PRD-007 | FESA는 Abaqus reference CSV rows와 비교 가능한 deterministic row mapping을 제공해야 한다. | displacement, reaction, internal force, stress 등 검증 물리량의 row identity와 tolerance source가 명확하다. | reference comparison report |
| FESA-PRD-008 | FESA의 production C++ 변경은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현해야 한다. | 관련 C++ test file이 있고 Harness TDD guard를 통과한다. | hook test, CTest |
| FESA-PRD-008 | FESA의 production C++ 변경은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현해야 한다. | 관련 C++ test file, RED 실패와 후속 GREEN 성공 증거가 있고 Stop의 전체 MSVC build/test가 통과한다. | implementation report, Hook guardrail, CTest |
| FESA-PRD-009 | FESA는 외부 라이브러리 API를 solver core에 직접 노출하지 않아야 한다. | MKL, TBB, HDF5 의존은 adapter module에 제한된다. | architecture review, dependency review |
| FESA-PRD-010 | FESA 기능 완료는 reference comparison과 physics sanity 통과를 요구해야 한다. | 수치 tolerance와 물리 검토가 모두 pass이고 known limitation이 기록된다. | verification report, physics evaluation report |
@@ -79,7 +72,7 @@ FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 유한요소법 기
2. Research evidence complete: 정식화와 benchmark 근거가 신뢰도와 한계와 함께 정리되어 있다.
3. Formulation reviewed: 약형, shape function, B matrix, constitutive contract, 수치적분, output recovery가 검토되어 있다.
4. I/O contract approved: Abaqus keyword subset, internal model mapping, HDF5 result contract, reference CSV comparison row contract가 승인되어 있다.
5. Tests fail before implementation: 구현 전 실패해야 하는 C++/integration/reference test가 준비되어 있다.
5. Tests fail before implementation: C++/integration/reference test를 제품 코드보다 먼저 작성하고 같은 Step 안에서 RED 실패와 후속 GREEN 성공을 확인한다.
6. CMake/CTest pass: MSVC/x64/Debug 기준 configure, build, test가 통과한다.
7. Reference comparison pass: FESA `results.h5` rows와 Abaqus reference CSV rows가 documented IDs, components, units, coordinate system, step/frame identity, tolerance 기준 안에 있다.
8. Physics sanity pass: equilibrium, reaction consistency, displacement direction, symmetry, stress sanity가 검토되어 있다.
@@ -92,4 +85,3 @@ FESA는 Abaqus `.inp` keyword subset을 입력으로 받아 유한요소법 기
- GUI 또는 postprocessor
- Visual Studio `.sln`/`.vcxproj` 전용 MSBuild workflow
- Explicit dynamics, contact, plasticity, shell end-to-end 구현
- JavaScript/TypeScript fallback 유지
+17 -9
View File
@@ -12,6 +12,7 @@
- Abaqus나 Nastran을 Agent가 직접 실행하지 않는다. `reference/<model-id>/`에 저장된 `model.inp`, `metadata.json`, Abaqus reference CSV files를 검증 기준으로 사용한다.
- 기본 개발 환경은 C++17 이상, MSVC, CMake, CTest이다.
- 모든 기능은 tolerance 기준을 명시하고, 기준을 만족할 때만 배포 후보가 된다.
- Harness 운영은 `docs/HARNESS_WORKFLOW.md`의 계획, 독립 Step 실행, PreToolUse/Stop 검증 계층을 따른다.
## 전체 Agent 구성
@@ -168,15 +169,20 @@ C++ 코드를 구현하는 Agent이다.
빌드와 테스트를 실행하는 Agent이다.
책임:
- Harness validation을 실행한다.
- `.harness/config.json` 또는 자동 감지 결과에 맞는 MSVC build/test 명령을 실행한다.
- MSVC x64 Debug CMake configure/build/CTest 결과를 수집한다.
- 실패 로그를 요약하고 Correction Agent에 전달한다.
기본 검증 명령:
기본 CMake 검증 명령:
```powershell
python scripts/validate_workspace.py
cmake -S . -B .harness/build -A x64
cmake --build .harness/build --config Debug
ctest --test-dir .harness/build -C Debug --show-only=json-v1
ctest --test-dir .harness/build -C Debug --output-on-failure
```
Preset 또는 직접 MSBuild 프로젝트는 `.harness/config.json`에 선언된 명령을 따른다.
검증 대상:
- CMake configure
- MSVC Debug build
@@ -301,7 +307,8 @@ flowchart TD
통과 조건:
- CMake/MSVC/CTest validation이 통과한다.
- 단위 테스트와 통합 테스트가 통과한다.
- Harness TDD guard를 만족한다.
- 관련 C++ test file이 있고 같은 구현 Step 안에 RED 실패와 후속 GREEN 성공 증거가 있다.
- Stop의 전체 MSVC build/test 검증이 통과한다.
### Gate 5: 레퍼런스 검증
통과 조건:
@@ -352,11 +359,12 @@ Coordinator Agent는 분류 결과에 따라 Requirement, Formulation, I/O Defin
## 초기 적용 우선순위
1. 선형 정적 해석의 최소 골격
2. 1D truss 또는 bar element
3. 2D plane stress/plane strain element
4. 3D solid element
5. material model 확장
6. nonlinear 또는 dynamic analysis 확장
2. Isoparametric 3D Euler beam element
3. 1D truss 또는 bar element
4. 2D plane stress/plane strain element
5. 3D solid element
6. material model 확장
7. nonlinear 또는 dynamic analysis 확장
각 단계는 요구조건, 정식화, 테스트모델, 구현, 레퍼런스 비교, 배포 Gate를 독립적으로 통과해야 한다.
+20 -14
View File
@@ -15,7 +15,8 @@ Agent는 역할과 책임 단위이고, skill은 여러 Agent가 반복적으로
- Abaqus, Nastran 또는 reference solver 실행은 skill 범위에 포함하지 않는다.
- Abaqus reference CSV 파일 생성/수정은 skill 범위에 포함하지 않는다.
- C++ 구현 관련 skill은 C++17 이상, MSVC, CMake, CTest, TDD 원칙을 따른다.
- 기본 workspace validation 명령은 `python scripts/validate_workspace.py`다.
- C++ 검증 명령은 `.harness/config.json` 또는 `docs/HARNESS.md`의 자동 감지 기본값을 따른다.
- Harness Python 변경은 `uv run --with pytest python -m pytest -v -rs`로 검증한다.
## Skill 구성
@@ -34,10 +35,10 @@ Agent는 역할과 책임 단위이고, skill은 여러 Agent가 반복적으로
## 개발 과정별 사용 예
예시 기능: `linear-truss-1d`
예시 기능: `isoparametric-3d-euler-beam`
1. Requirement Agent는 `fesa-requirements-baseline`을 사용해 기능 범위, 제외 범위, 입력, 출력, 검증 물리량, tolerance, `Requirement Verification Matrix`를 작성한다.
2. Research Agent는 `fesa-research-evidence`를 사용해 truss/bar element 이론, benchmark 후보, source reliability, applicability limits를 정리한다.
2. Research Agent는 `fesa-research-evidence`를 사용해 3D Euler beam element 이론, benchmark 후보, source reliability, applicability limits를 정리한다.
3. Formulation Agent는 `fesa-formulation-spec`을 사용해 strong form, weak form, shape functions, B matrix, element stiffness, output recovery를 정리한다.
4. Numerical Review Agent는 `fesa-numerical-review`를 사용해 rigid body modes, patch test, stiffness symmetry, Jacobian, locking 위험을 검토하고 `pass-for-implementation-planning` 여부를 판단한다.
5. I/O Definition Agent는 `fesa-io-contract`를 사용해 지원할 Abaqus `.inp` keyword subset, `results.h5` schema, reference CSV comparison row schema를 정의한다.
@@ -98,9 +99,17 @@ Agent는 역할과 책임 단위이고, skill은 여러 Agent가 반복적으로
- 기본 검증 명령:
```powershell
python -m unittest discover -s scripts -p "test_*.py"
python scripts/validate_workspace.py
ctest -C Debug -R <feature-or-label>
cmake -S . -B .harness/build -A x64
cmake --build .harness/build --config Debug
ctest --test-dir .harness/build -C Debug -R <feature-or-label> --output-on-failure
ctest --test-dir .harness/build -C Debug --output-on-failure
```
Preset 또는 직접 MSBuild 프로젝트는 `.harness/config.json`의 설정을 사용한다. Harness
Python, Hook, agent config를 변경한 경우에는 다음 명령도 실행한다.
```powershell
uv run --with pytest python -m pytest -v -rs
```
- 실패는 `configure | compile | link | test | reference-comparison | harness | environment | upstream-contract`로 분류한다.
@@ -148,7 +157,8 @@ ctest -C Debug -R <feature-or-label>
## 검증 기준
Skill 구성 검증은 `scripts/test_fesa_solver_skills.py`가 담당한다.
Skill 구성은 실제 `.codex/skills/` 파일을 source of truth로 삼아 정적 계약과 repository
pytest suite로 검증한다.
검증 항목:
@@ -163,15 +173,11 @@ Skill 구성 검증은 `scripts/test_fesa_solver_skills.py`가 담당한다.
검증 명령:
```powershell
python -m unittest discover -s scripts -p "test_*.py"
python scripts/validate_workspace.py
uv run --with pytest python -m pytest -v -rs
```
Skill 구조 검증:
```powershell
python C:\Users\user\.codex\skills\.system\skill-creator\scripts\quick_validate.py .codex\skills\<skill-name>
```
개별 skill schema를 점검할 때는 현재 Codex 설치에 포함된 `skill-creator` validator를
사용하되 사용자 홈을 하드코딩한 경로를 프로젝트 계약으로 두지 않는다.
## v1 범위
@@ -1,5 +1,10 @@
# FESA 초기 문서 완성 계획 노트
> **Historical / Superseded:** 이 문서는 2026-06-10 시점의 초기 조사와 실행 기록이다.
> 현재 제품 범위는 `docs/PRD.md`, Harness 운영 계약은 `docs/HARNESS_WORKFLOW.md`
> `docs/HARNESS.md`를 따른다. 아래의 구형 스킬명, 검증 명령, 기능 우선순위는 현재
> 지침으로 사용하지 않는다.
## 메타데이터
- 작성일: 2026-06-10
- 목적: `AGENTS.md`, `docs/PRD.md`, `docs/ARCHITECTURE.md`를 유한요소법 기반 구조해석 솔버 개발 프로젝트 문서로 완성하기 위한 조사 내용과 실행 계획 정리
+40 -23
View File
@@ -2,16 +2,17 @@
이 디렉터리는 Build/Test Executor Agent가 작성하거나 제안하는 기능별 build/test 실행 리포트를 보관하는 위치다.
Build/Test Executor Agent는 Implementation Agent 이후 독립적으로 C++/MSVC/CMake/CTest 검증을 실행하고, 실패를 분류해 다음 agent로 handoff한다. 이 agent는 source code, tests, CMake files, requirements, formulations, I/O contracts, reference artifacts, tolerance policies를 수정하지 않는다. build artifacts와 test outputs`build/` 아래 생성될 수 있다.
Build/Test Executor Agent는 Implementation Agent 이후 독립적으로 C++/MSVC/CMake/CTest 검증을 실행하고, 실패를 분류해 다음 agent로 handoff한다. 이 agent는 source code, tests, CMake files, requirements, formulations, I/O contracts, reference artifacts, tolerance policies를 수정하지 않는다. 기본 build artifact는 `.harness/build/` 아래 생성다.
기본 문서명은 `docs/build-test-reports/<feature-id>-build-test.md` 형식을 사용한다.
## Build/Test Executor Agent 역할
수행한다:
- `python scripts/validate_workspace.py`를 기본 검증 명령으로 실행한다.
- implementation plan/report에 명시된 경우 harness self-test와 feature-specific CTest를 실행한다.
- `HARNESS_VALIDATION_COMMANDS`, `CMakePresets.json``msvc-debug`, 기본 CMake/MSVC x64 Debug 경로 중 어떤 검증 경로가 사용되었는지 기록한다.
- `.harness/config.json`과 프로젝트 자동 감지 결과를 확인하고 같은 build/test 경로를 독립 실행한다.
- implementation plan/report에 명시된 feature-specific CTest를 전체 검증 전에 실행한다.
- Harness Python, Hook, agent config 변경이 포함되면 `uv run --with pytest python -m pytest -v -rs`를 실행한다.
- CMake preset, 직접 MSBuild, 기본 CMake/MSVC x64 Debug 중 어떤 검증 경로가 사용되었는지 기록한다.
- configure, compile, link, test, reference-comparison, harness, environment, upstream-contract 실패를 구분한다.
- command, exit code, duration, stdout/stderr tail, failed test name을 요약한다.
- 실패 원인에 따라 Implementation Agent, Correction Agent, Reference Verification Agent, Implementation Planning Agent 중 handoff 대상을 제안한다.
@@ -28,29 +29,43 @@ Build/Test Executor Agent는 Implementation Agent 이후 독립적으로 C++/MSV
## 실행 순서
기본 순서는 implementation plan/report에 따라 다음 중 필요한 항목 실행한다.
기본 순서는 implementation plan/report에 따라 다음 중 필요한 항목 실행한다.
```powershell
python -m unittest discover -s scripts -p "test_*.py"
ctest -C Debug -R <feature-or-label>
python scripts/validate_workspace.py
cmake -S . -B .harness/build -A x64
cmake --build .harness/build --config Debug
ctest --test-dir .harness/build -C Debug -R <feature-or-label> --output-on-failure
ctest --test-dir .harness/build -C Debug --show-only=json-v1
ctest --test-dir .harness/build -C Debug --output-on-failure
```
`scripts/validate_workspace.py`의 command discovery 우선순위는 다음과 같다.
Harness Python, Hook, agent config 변경이 검증 범위에 포함되면 다음 명령을 먼저 실행한다.
1. `HARNESS_VALIDATION_COMMANDS`
2. `CMakePresets.json``msvc-debug`
3. 기본 CMake/MSVC x64 Debug 명령
4. `CMakeLists.txt`가 없고 override도 없으면 안내 메시지와 함께 성공 종료
```powershell
uv run --with pytest python -m pytest -v -rs
```
프로젝트 선택 우선순위는 다음과 같다.
1. `.harness/config.json`의 명시적 `projectType`
2. 루트의 CMake metadata
3. 루트의 단일 `.sln`
4. 루트의 단일 `.vcxproj`
기본 CMake/MSVC x64 Debug 명령은 다음과 같다.
```powershell
cmake -S . -B build/msvc-debug -G "Visual Studio 17 2022" -A x64
cmake --build build/msvc-debug --config Debug
ctest --test-dir build/msvc-debug --output-on-failure -C Debug
cmake -S . -B .harness/build -A x64
cmake --build .harness/build --config Debug
ctest --test-dir .harness/build -C Debug --show-only=json-v1
ctest --test-dir .harness/build -C Debug --output-on-failure
```
CMake preset을 사용하면 configure/build/test preset과 `binaryDir`를 모두
`.harness/config.json`에 지정한다. 직접 MSBuild는 solution/project와
`msbuild.testCommand`를 지정한다. C/C++와 build metadata가 모두 없으면 검증 대상이
없으므로 통과하지만, C/C++ 파일만 있고 build metadata가 없으면 오류다.
## 문서 템플릿
```markdown
@@ -69,17 +84,19 @@ ctest --test-dir build/msvc-debug --output-on-failure -C Debug
- generator: Visual Studio 17 2022 | <observed generator>
- platform: x64 | <observed platform>
- config: Debug | <observed config>
- build_dir: build/msvc-debug | <observed build dir>
- active_override_env_vars: HARNESS_VALIDATION_COMMANDS | HARNESS_CMAKE_GENERATOR | HARNESS_CMAKE_PLATFORM | HARNESS_CMAKE_CONFIG | HARNESS_BUILD_DIR | none
- command_discovery_path: HARNESS_VALIDATION_COMMANDS | CMakePresets.json msvc-debug | default CMake/MSVC x64 Debug | no-CMake informational success
- build_dir: .harness/build | <configured/observed build dir>
- harness_config: .harness/config.json | absent-defaults
- project_selection: configured cmake | configured msbuild | auto CMake | auto MSBuild | no C/C++ project
- command_discovery_path: CMake preset | direct MSBuild | default CMake/MSVC x64 Debug | no C/C++ project
## Command Log Summary
| order | command | exit_code | duration | stdout_stderr_tail |
| --- | --- | --- | --- | --- |
| 1 | python -m unittest discover -s scripts -p "test_*.py" | <code> | <duration> | <tail summary> |
| 2 | ctest -C Debug -R <feature-or-label> | <code> | <duration> | <tail summary> |
| 3 | python scripts/validate_workspace.py | <code> | <duration> | <tail summary> |
| 1 | uv run --with pytest python -m pytest -v -rs (when applicable) | <code or skipped> | <duration> | <tail summary> |
| 2 | <config-resolved configure/build commands> | <code> | <duration> | <tail summary> |
| 3 | <feature-specific test command when applicable> | <code or skipped> | <duration> | <tail summary> |
| 4 | <config-resolved test discovery/full test commands> | <code> | <duration> | <tail summary> |
## Validation Results
@@ -137,7 +154,7 @@ ctest --test-dir build/msvc-debug --output-on-failure -C Debug
- 모든 실행 명령과 exit code를 기록해야 한다.
- 실패 로그는 전체 원문을 복제하지 않고 마지막 핵심 구간과 실패 원인을 요약한다.
- configure, compile, link, test, reference-comparison, harness, environment, upstream-contract 실패를 구분한다.
- no-CMake 상황 `scripts/validate_workspace.py` 정책대로 안내 메시지와 성공 종료로 기록한다.
- C/C++와 build metadata가 모두 없는 상황 `no C/C++ project` 성공으로 기록한다. C/C++ 파일이 있는데 build metadata가 없으면 `environment` 또는 `configure` 실패로 기록한다.
- 성공 판정은 build/test 통과까지만 의미한다.
- reference tolerance, physics validation, release readiness는 판정하지 않는다.
- upstream 계약 문제는 Implementation Agent에 임의 수정으로 넘기지 않고 적절한 upstream agent로 handoff한다.
+4 -4
View File
@@ -53,7 +53,7 @@ INTAKE -> STATE AUDIT -> GATE DECISION -> HANDOFF PACKAGE -> STATUS REPORT
- Reference Verification Agent report
- Physics Evaluation Agent report
- Release Agent report
- validation command evidence: `python scripts/validate_workspace.py`
- Build/Test report의 `.harness/config.json` 또는 자동 감지 기반 MSVC build/test command evidence
## 문서 템플릿
@@ -184,6 +184,6 @@ INTAKE -> STATE AUDIT -> GATE DECISION -> HANDOFF PACKAGE -> STATUS REPORT
## 검증 기준
- Coordinator Agent config와 문서 템플릿 검증은 Python unittest행한다.
- workspace 검증은 `python scripts/validate_workspace.py`를 사용한다.
- 현재 repository에 CMake 프로젝트가 없으면 harness 정책에 따라 no-CMake validation 경로가 성공으로 기록될 수 있다.
- Coordinator Agent config와 문서 템플릿에 자동화된 Python 검증이 있으면 `uv run --with pytest python -m pytest -v -rs`행한다.
- C++ build/test evidence는 `.harness/config.json` 또는 Harness 자동 감지 기본값에 따른 Build/Test report에서 확인한다.
- C/C++ 파일과 build metadata가 모두 없을 때만 `no C/C++ project` 성공을 허용한다. C/C++ 파일만 있고 build metadata가 없으면 통과 evidence로 사용하지 않는다.
+13 -8
View File
@@ -12,8 +12,8 @@ Correction Agent는 Build/Test Executor Agent, Reference Verification Agent, Phy
- 실패 로그와 implementation report를 읽고 failure classification을 먼저 확정한다.
- configure, compile, link, test, reference-comparison, harness, environment, upstream-contract 실패를 구분한다.
- implementation-owned failure에 한해 source/header/test/CMake를 최소 수정한다.
- 수정 후 targeted command를 먼저 실행하고 `python scripts/validate_workspace.py`를 실행한다.
- harness, hook, agent config 관련 수정에서는 `python -m unittest discover -s scripts -p "test_*.py"`도 실행한다.
- 수정 후 targeted command를 먼저 실행하고 `.harness/config.json` 또는 자동 감지 기본값에 따른 전체 MSVC build/test를 실행한다.
- Harness Python, Hook, agent config 관련 수정에서는 `uv run --with pytest python -m pytest -v -rs`도 실행한다.
- 반복 실패 또는 upstream 계약 문제를 Coordinator Agent나 관련 upstream agent로 handoff한다.
수행하지 않는다:
@@ -40,11 +40,16 @@ TRIAGE -> MINIMAL FIX -> VERIFY -> REPORT
```powershell
<targeted command that reproduced the failure>
python scripts/validate_workspace.py
python -m unittest discover -s scripts -p "test_*.py"
cmake -S . -B .harness/build -A x64
cmake --build .harness/build --config Debug
ctest --test-dir .harness/build -C Debug --show-only=json-v1
ctest --test-dir .harness/build -C Debug --output-on-failure
```
`python -m unittest discover -s scripts -p "test_*.py"`는 harness, hook, agent config, Python validation behavior가 correction 범위에 포함될 때 실행한다.
Preset 또는 직접 MSBuild 프로젝트는 `.harness/config.json`의 설정을 따른다. Harness
Python, Hook, agent config, Python validation behavior가 correction 범위에 포함될 때는
`uv run --with pytest python -m pytest -v -rs`도 실행한다. Stop 검증은 응답 종료 전에
같은 전체 프로젝트 검증을 다시 수행한다.
## Failure Classification
@@ -53,7 +58,7 @@ python -m unittest discover -s scripts -p "test_*.py"
- `link`: linker, symbol resolution, target dependency 실패
- `test`: CTest, unit, integration, parser/I/O, ordinary regression test 실패
- `reference-comparison`: 저장된 reference artifact와 deterministic comparison 실패
- `harness`: Python harness self-test, TDD guard, hook, validation script 실패
- `harness`: Python Harness test, PreToolUse/Stop Hook, config loading, discovery, adapter validation 실패
- `environment`: MSVC, CMake, Python, path, permission, generator, local dependency 문제
- `upstream-contract`: requirements, formulation, I/O, reference artifact, tolerance, implementation plan 불일치 또는 누락
@@ -104,8 +109,8 @@ Excluded files:
| order | command | exit_code | result | evidence |
| --- | --- | --- | --- | --- |
| 1 | <targeted command> | <code> | pass | fail | <summary> |
| 2 | python scripts/validate_workspace.py | <code> | pass | fail | <summary> |
| 3 | python -m unittest discover -s scripts -p "test_*.py" | <code or skipped> | pass | fail | skipped | <summary> |
| 2 | <config-resolved full MSVC build/test commands> | <code> | pass | fail | <summary> |
| 3 | uv run --with pytest python -m pytest -v -rs | <code or skipped> | pass | fail | skipped | <summary> |
## Traceability
+12 -6
View File
@@ -15,7 +15,7 @@ Implementation Planning Agent는 승인된 요구조건, 연구 브리프, 정
- CMake/CTest target, `add_test`, label, `ctest -C Debug` 검증 계획을 정의한다.
- candidate source/header/test/CMake 파일과 ownership boundary를 제안한다.
- requirement, task, test, reference model, acceptance criterion을 Acceptance Traceability Matrix로 연결한다.
- `python scripts/validate_workspace.py`를 포함한 validation command를 명시한다.
- `.harness/config.json` 또는 자동 감지 기본값에서 해석되는 MSVC build/test 명령과 feature-specific command를 명시한다.
수행하지 않는다:
- C++ 코드를 구현하지 않는다.
@@ -80,8 +80,8 @@ Implementation Planning Agent는 승인된 요구조건, 연구 브리프, 정
- add_test_needs: <CTest registration needs>
- labels: unit | integration | reference | parser | io
- msvc_config: Debug
- expected_feature_command: ctest -C Debug -R <feature-or-label>
- workspace_validation: python scripts/validate_workspace.py
- expected_feature_command: ctest --test-dir .harness/build -C Debug -R <feature-or-label> --output-on-failure
- full_validation_source: .harness/config.json | Harness auto detection
## Candidate Files and Ownership
@@ -107,11 +107,17 @@ Implementation Planning Agent는 승인된 요구조건, 연구 브리프, 정
## Validation Commands
```powershell
python -m unittest discover -s scripts -p "test_*.py"
python scripts/validate_workspace.py
ctest -C Debug -R <feature-or-label>
cmake -S . -B .harness/build -A x64
cmake --build .harness/build --config Debug
ctest --test-dir .harness/build -C Debug -R <feature-or-label> --output-on-failure
ctest --test-dir .harness/build -C Debug --output-on-failure
```
Preset 또는 직접 MSBuild 프로젝트는 `.harness/config.json`에 해석 가능한 명령을 적는다.
Harness Python, Hook, agent config 변경이 계획 범위에 포함되면
`uv run --with pytest python -m pytest -v -rs`도 추가한다. Stop 검증은 Step 종료 전 전체
MSVC build/test를 다시 확인하며, 구현 보고서의 RED 실패 증거를 대체하지 않는다.
## Risks and Downstream Handoff
### Implementation Agent
+3 -3
View File
@@ -43,7 +43,7 @@ GATE AUDIT -> TRACEABILITY CHECK -> RELEASE DOCUMENTATION -> RELEASE VERDICT
- Build/Test report status: `pass-for-reference-verification`
- Implementation report와 implementation plan의 feature scope 일치
- requirements, formulations, numerical reviews, I/O definitions, reference models 문서의 feature scope 일치
- validation command evidence: `python scripts/validate_workspace.py`
- Build/Test report의 `.harness/config.json` 또는 자동 감지 기반 MSVC build/test command evidence
## 문서 템플릿
@@ -99,8 +99,8 @@ GATE AUDIT -> TRACEABILITY CHECK -> RELEASE DOCUMENTATION -> RELEASE VERDICT
| command_or_report | expected | observed | notes |
| --- | --- | --- | --- |
| python scripts/validate_workspace.py | pass | <result> | <summary> |
| CMake/MSVC/CTest | pass | <result> | <summary> |
| Build/Test report의 config-resolved CMake/MSVC/CTest | pass | <result> | <summary> |
| Harness Python pytest (when applicable) | pass or N/A | <result> | <summary> |
| reference verification | pass-for-physics-evaluation | <status> | <summary> |
| physics evaluation | pass-for-release-agent | <status> | <summary> |
@@ -1,5 +1,9 @@
# Harden Execute Runner Implementation Plan
> **Historical / Superseded:** 이 계획의 `codex/` 브랜치, `allowed_paths`, explicit
> staging, 구형 workspace validation 계약은 현재 Runner 동작이 아니다. 현재 계약은
> `docs/HARNESS_WORKFLOW.md`와 `docs/HARNESS.md`를 따른다.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make `scripts/execute.py` safe enough to use by enforcing `codex/` branch names, clean starting state, explicit staging, per-step file allowlists, and validation before every runner-created commit.
@@ -1,5 +1,10 @@
# Reference CSV Comparison Contract Implementation Plan
> **Historical / Partially Superseded:** Reference CSV/HDF5 제품 계약의 배경 기록으로만
> 보존한다. 이 문서의 Python `unittest`, 삭제된 workspace validation script, Harness
> 환경 변수 관련 검증 절차는 폐기되었으며 현재 운영 계약은
> `docs/HARNESS_WORKFLOW.md``docs/HARNESS.md`를 따른다.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Re-align FESA reference comparison contracts so FESA solver output remains authoritative HDF5, while Abaqus-derived reference results are CSV files compared directly against the FESA `results.h5` datasets.
+2
View File
@@ -0,0 +1,2 @@
[pytest]
testpaths = tests
@@ -0,0 +1,12 @@
Part Instance Name, Node Label, U-U1, U-U2, U-U3, UR-UR1, UR-UR2, UR-UR3
PART-1_1-1,1,0.000000E+00,0.000000E+00,-1.000000E-30,0.000000E+00,1.000000E-29,0.000000E+00
PART-1_1-1,2,0.000000E+00,0.000000E+00,-2.900000E-04,0.000000E+00,5.428570E-04,0.000000E+00
PART-1_1-1,3,0.000000E+00,0.000000E+00,-1.094290E-03,0.000000E+00,1.028570E-03,0.000000E+00
PART-1_1-1,4,0.000000E+00,0.000000E+00,-2.355720E-03,0.000000E+00,1.457140E-03,0.000000E+00
PART-1_1-1,5,0.000000E+00,0.000000E+00,-4.017140E-03,0.000000E+00,1.828570E-03,0.000000E+00
PART-1_1-1,6,0.000000E+00,0.000000E+00,-6.021430E-03,0.000000E+00,2.142860E-03,0.000000E+00
PART-1_1-1,7,0.000000E+00,0.000000E+00,-8.311430E-03,0.000000E+00,2.400000E-03,0.000000E+00
PART-1_1-1,8,0.000000E+00,0.000000E+00,-1.083000E-02,0.000000E+00,2.600000E-03,0.000000E+00
PART-1_1-1,9,0.000000E+00,0.000000E+00,-1.352000E-02,0.000000E+00,2.742860E-03,0.000000E+00
PART-1_1-1,10,0.000000E+00,0.000000E+00,-1.632430E-02,0.000000E+00,2.828570E-03,0.000000E+00
PART-1_1-1,11,0.000000E+00,0.000000E+00,-1.918570E-02,0.000000E+00,2.857140E-03,0.000000E+00
1 Part Instance Name Node Label U-U1 U-U2 U-U3 UR-UR1 UR-UR2 UR-UR3
2 PART-1_1-1 1 0.000000E+00 0.000000E+00 -1.000000E-30 0.000000E+00 1.000000E-29 0.000000E+00
3 PART-1_1-1 2 0.000000E+00 0.000000E+00 -2.900000E-04 0.000000E+00 5.428570E-04 0.000000E+00
4 PART-1_1-1 3 0.000000E+00 0.000000E+00 -1.094290E-03 0.000000E+00 1.028570E-03 0.000000E+00
5 PART-1_1-1 4 0.000000E+00 0.000000E+00 -2.355720E-03 0.000000E+00 1.457140E-03 0.000000E+00
6 PART-1_1-1 5 0.000000E+00 0.000000E+00 -4.017140E-03 0.000000E+00 1.828570E-03 0.000000E+00
7 PART-1_1-1 6 0.000000E+00 0.000000E+00 -6.021430E-03 0.000000E+00 2.142860E-03 0.000000E+00
8 PART-1_1-1 7 0.000000E+00 0.000000E+00 -8.311430E-03 0.000000E+00 2.400000E-03 0.000000E+00
9 PART-1_1-1 8 0.000000E+00 0.000000E+00 -1.083000E-02 0.000000E+00 2.600000E-03 0.000000E+00
10 PART-1_1-1 9 0.000000E+00 0.000000E+00 -1.352000E-02 0.000000E+00 2.742860E-03 0.000000E+00
11 PART-1_1-1 10 0.000000E+00 0.000000E+00 -1.632430E-02 0.000000E+00 2.828570E-03 0.000000E+00
12 PART-1_1-1 11 0.000000E+00 0.000000E+00 -1.918570E-02 0.000000E+00 2.857140E-03 0.000000E+00
@@ -0,0 +1,21 @@
Part Instance Name, Element Label, Node Label, SF-SF1, SF-SF2, SF-SF3, SM-SM1, SM-SM2, SM-SM3,,,,,
PART-1_1-1,1,1,0.000000E+00,-1.000000E+06,0.000000E+00,9.500000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,1,2,0.000000E+00,-1.000000E+06,0.000000E+00,9.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,2,2,0.000000E+00,-1.000000E+06,0.000000E+00,9.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,2,3,0.000000E+00,-1.000000E+06,0.000000E+00,8.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,3,3,0.000000E+00,-1.000000E+06,0.000000E+00,8.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,3,4,0.000000E+00,-1.000000E+06,0.000000E+00,7.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,4,4,0.000000E+00,-1.000000E+06,0.000000E+00,7.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,4,5,0.000000E+00,-1.000000E+06,0.000000E+00,6.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,5,5,0.000000E+00,-1.000000E+06,0.000000E+00,6.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,5,6,0.000000E+00,-1.000000E+06,0.000000E+00,5.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,6,6,0.000000E+00,-1.000000E+06,0.000000E+00,5.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,6,7,0.000000E+00,-1.000000E+06,0.000000E+00,4.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,7,7,0.000000E+00,-1.000000E+06,0.000000E+00,4.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,7,8,0.000000E+00,-1.000000E+06,0.000000E+00,3.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,8,8,0.000000E+00,-1.000000E+06,0.000000E+00,3.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,8,9,0.000000E+00,-1.000000E+06,0.000000E+00,2.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,9,9,0.000000E+00,-1.000000E+06,0.000000E+00,2.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,9,10,0.000000E+00,-1.000000E+06,0.000000E+00,1.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,10,10,0.000000E+00,-1.000000E+06,0.000000E+00,1.000000E+06,0.000000E+00,0.000000E+00,,,,,
PART-1_1-1,10,11,0.000000E+00,-1.000000E+06,0.000000E+00,5.000000E+05,0.000000E+00,0.000000E+00,,,,,
1 Part Instance Name Element Label Node Label SF-SF1 SF-SF2 SF-SF3 SM-SM1 SM-SM2 SM-SM3
2 PART-1_1-1 1 1 0.000000E+00 -1.000000E+06 0.000000E+00 9.500000E+06 0.000000E+00 0.000000E+00
3 PART-1_1-1 1 2 0.000000E+00 -1.000000E+06 0.000000E+00 9.000000E+06 0.000000E+00 0.000000E+00
4 PART-1_1-1 2 2 0.000000E+00 -1.000000E+06 0.000000E+00 9.000000E+06 0.000000E+00 0.000000E+00
5 PART-1_1-1 2 3 0.000000E+00 -1.000000E+06 0.000000E+00 8.000000E+06 0.000000E+00 0.000000E+00
6 PART-1_1-1 3 3 0.000000E+00 -1.000000E+06 0.000000E+00 8.000000E+06 0.000000E+00 0.000000E+00
7 PART-1_1-1 3 4 0.000000E+00 -1.000000E+06 0.000000E+00 7.000000E+06 0.000000E+00 0.000000E+00
8 PART-1_1-1 4 4 0.000000E+00 -1.000000E+06 0.000000E+00 7.000000E+06 0.000000E+00 0.000000E+00
9 PART-1_1-1 4 5 0.000000E+00 -1.000000E+06 0.000000E+00 6.000000E+06 0.000000E+00 0.000000E+00
10 PART-1_1-1 5 5 0.000000E+00 -1.000000E+06 0.000000E+00 6.000000E+06 0.000000E+00 0.000000E+00
11 PART-1_1-1 5 6 0.000000E+00 -1.000000E+06 0.000000E+00 5.000000E+06 0.000000E+00 0.000000E+00
12 PART-1_1-1 6 6 0.000000E+00 -1.000000E+06 0.000000E+00 5.000000E+06 0.000000E+00 0.000000E+00
13 PART-1_1-1 6 7 0.000000E+00 -1.000000E+06 0.000000E+00 4.000000E+06 0.000000E+00 0.000000E+00
14 PART-1_1-1 7 7 0.000000E+00 -1.000000E+06 0.000000E+00 4.000000E+06 0.000000E+00 0.000000E+00
15 PART-1_1-1 7 8 0.000000E+00 -1.000000E+06 0.000000E+00 3.000000E+06 0.000000E+00 0.000000E+00
16 PART-1_1-1 8 8 0.000000E+00 -1.000000E+06 0.000000E+00 3.000000E+06 0.000000E+00 0.000000E+00
17 PART-1_1-1 8 9 0.000000E+00 -1.000000E+06 0.000000E+00 2.000000E+06 0.000000E+00 0.000000E+00
18 PART-1_1-1 9 9 0.000000E+00 -1.000000E+06 0.000000E+00 2.000000E+06 0.000000E+00 0.000000E+00
19 PART-1_1-1 9 10 0.000000E+00 -1.000000E+06 0.000000E+00 1.000000E+06 0.000000E+00 0.000000E+00
20 PART-1_1-1 10 10 0.000000E+00 -1.000000E+06 0.000000E+00 1.000000E+06 0.000000E+00 0.000000E+00
21 PART-1_1-1 10 11 0.000000E+00 -1.000000E+06 0.000000E+00 5.000000E+05 0.000000E+00 0.000000E+00
@@ -0,0 +1,12 @@
Part Instance Name, Node Label, RF-RF1, RF-RF2, RF-RF3, RM-RM1, RM-RM2, RM-RM3
PART-1_1-1,1,0.000000E+00,0.000000E+00,1.000000E+06,0.000000E+00,-1.000000E+07,0.000000E+00
PART-1_1-1,2,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
PART-1_1-1,3,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
PART-1_1-1,4,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
PART-1_1-1,5,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
PART-1_1-1,6,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
PART-1_1-1,7,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
PART-1_1-1,8,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
PART-1_1-1,9,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
PART-1_1-1,10,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
PART-1_1-1,11,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00,0.000000E+00
1 Part Instance Name Node Label RF-RF1 RF-RF2 RF-RF3 RM-RM1 RM-RM2 RM-RM3
2 PART-1_1-1 1 0.000000E+00 0.000000E+00 1.000000E+06 0.000000E+00 -1.000000E+07 0.000000E+00
3 PART-1_1-1 2 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00
4 PART-1_1-1 3 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00
5 PART-1_1-1 4 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00
6 PART-1_1-1 5 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00
7 PART-1_1-1 6 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00
8 PART-1_1-1 7 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00
9 PART-1_1-1 8 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00
10 PART-1_1-1 9 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00
11 PART-1_1-1 10 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00
12 PART-1_1-1 11 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00
@@ -0,0 +1,104 @@
*Heading
** Job name: Job-1 Model name: Model-1
** Generated by: Abaqus/CAE Learning Edition 2024
*Preprint, echo=NO, model=NO, history=NO, contact=NO
**
** PARTS
**
*Part, name=PART-1_1
*Node
1, 0., 0., 0.
2, 1., 0., 0.
3, 2., 0., 0.
4, 3., 0., 0.
5, 4., 0., 0.
6, 5., 0., 0.
7, 6., 0., 0.
8, 7., 0., 0.
9, 8., 0., 0.
10, 9., 0., 0.
11, 10., 0., 0.
*Element, type=B31
1, 1, 2
2, 2, 3
3, 3, 4
4, 4, 5
5, 5, 6
6, 6, 7
7, 7, 8
8, 8, 9
9, 9, 10
10, 10, 11
*Elset, elset=Set-1, generate
1, 10, 1
*Elset, elset=Set-2, generate
1, 10, 1
** Section: Section-1 Profile: Profile-1
*Beam General Section, elset=Set-1, material=Material-1, section=GENERAL
1., 0.0833333, 0., 0.0833333, 0.140833
0.,1.,0.
*Transverse Shear Stiffness
6.73077e+10, 6.73077e+10, 0.25
*End Part
**
**
** ASSEMBLY
**
*Assembly, name=Assembly
**
*Instance, name=PART-1_1-1, part=PART-1_1
*End Instance
**
*Nset, nset=Set-3, instance=PART-1_1-1
1,
*Nset, nset=Set-4, instance=PART-1_1-1
11,
*End Assembly
**
** MATERIALS
**
*Material, name=Material-1
*Elastic
2.1e+11, 0.3
**
** BOUNDARY CONDITIONS
**
** Name: BC-1 Type: Displacement/Rotation
*Boundary
Set-3, 1, 1
Set-3, 2, 2
Set-3, 3, 3
Set-3, 4, 4
Set-3, 5, 5
Set-3, 6, 6
** ----------------------------------------------------------------
**
** STEP: Step-1
**
*Step, name=Step-1, nlgeom=NO
*Static
1., 1., 1e-05, 1.
**
** LOADS
**
** Name: Load-1 Type: Concentrated force
*Cload
Set-4, 3, -1e+06
**
** OUTPUT REQUESTS
**
*Restart, write, frequency=0
**
** FIELD OUTPUT: F-Output-1
**
*Output, field
*Node Output
CF, RF, TF, U
*Element Output, directions=YES
LE, NFORC, NFORCSO, PE, PEEQ, PEMAG, S, SF
*Contact Output, variable=PRESELECT
**
** HISTORY OUTPUT: H-Output-1
**
*Output, history, variable=PRESELECT
*End Step
+104 -185
View File
@@ -8,10 +8,8 @@ Usage:
import argparse
import contextlib
import fnmatch
import json
import os
import re
import subprocess
import sys
import threading
@@ -24,6 +22,10 @@ from typing import Optional
ROOT = Path(__file__).resolve().parent.parent
class CodexEnvironmentError(RuntimeError):
"""재시도로 해결할 수 없는 Codex CLI 환경 오류."""
@contextlib.contextmanager
def progress_indicator(label: str):
"""터미널 진행 표시기. with 문으로 사용하며 .elapsed 로 경과 시간을 읽는다."""
@@ -56,10 +58,6 @@ class StepExecutor:
"""Phase 디렉토리 안의 step들을 순차 실행하는 하네스."""
MAX_RETRIES = 3
VALIDATION_COMMANDS = (
[sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py"],
[sys.executable, "scripts/validate_workspace.py"],
)
FEAT_MSG = "feat({phase}): step {num}{name}"
CHORE_MSG = "chore({phase}): step {num} output"
TZ = timezone(timedelta(hours=9))
@@ -89,7 +87,6 @@ class StepExecutor:
def run(self):
self._print_header()
self._check_blockers()
self._assert_clean_worktree("before branch checkout")
self._checkout_branch()
guardrails = self._load_guardrails()
self._ensure_created_at()
@@ -117,117 +114,8 @@ class StepExecutor:
cmd = ["git"] + list(args)
return subprocess.run(cmd, cwd=self._root, capture_output=True, text=True)
def _validate_before_commit(self, commit_message: str):
print(f" Validation before commit: {commit_message}")
for cmd in self.VALIDATION_COMMANDS:
r = subprocess.run(cmd, cwd=self._root, capture_output=True, text=True)
if r.returncode != 0:
print(f" ERROR: validation failed before commit: {' '.join(cmd)}")
if r.stdout:
print(r.stdout[-2000:])
if r.stderr:
print(r.stderr[-2000:])
sys.exit(1)
def _branch_name(self) -> str:
slug = re.sub(r"[^A-Za-z0-9._-]+", "-", self._phase_name.strip())
slug = slug.strip("/.-")
if not slug:
slug = self._phase_dir_name
return f"codex/{slug}"
def _assert_clean_worktree(self, context: str):
r = self._run_git("status", "--porcelain")
if r.returncode != 0:
print(" ERROR: git status failed.")
print(f" {r.stderr.strip()}")
sys.exit(1)
dirty = r.stdout.strip()
if dirty:
print(f" ERROR: dirty worktree detected {context}.")
print(" Commit, stash, or remove these changes before running scripts/execute.py:")
for line in dirty.splitlines():
print(f" {line}")
sys.exit(1)
@staticmethod
def _normalize_rel_path(path: str) -> str:
return path.replace("\\", "/").lstrip("./")
def _path_allowed(self, path: str, patterns: list[str]) -> bool:
rel = self._normalize_rel_path(path)
for raw in patterns:
pattern = self._normalize_rel_path(str(raw))
if not pattern:
continue
if pattern.endswith("/") and rel.startswith(pattern):
return True
if any(ch in pattern for ch in "*?[") and fnmatch.fnmatchcase(rel, pattern):
return True
if rel == pattern:
return True
return False
def _validate_step_allowlist(self, step: dict):
allowed = step.get("allowed_paths")
if (
not isinstance(allowed, list)
or not allowed
or not all(isinstance(p, str) and p.strip() for p in allowed)
):
print(f" ERROR: Step {step.get('step')} must define non-empty allowed_paths.")
sys.exit(1)
def _changed_paths(self) -> list[str]:
paths: list[str] = []
tracked = self._run_git("diff", "--name-only")
if tracked.returncode != 0:
print(" ERROR: git diff --name-only failed.")
print(f" {tracked.stderr.strip()}")
sys.exit(1)
paths.extend(tracked.stdout.splitlines())
staged = self._run_git("diff", "--cached", "--name-only")
if staged.returncode != 0:
print(" ERROR: git diff --cached --name-only failed.")
print(f" {staged.stderr.strip()}")
sys.exit(1)
paths.extend(staged.stdout.splitlines())
untracked = self._run_git("ls-files", "--others", "--exclude-standard")
if untracked.returncode != 0:
print(" ERROR: git ls-files --others failed.")
print(f" {untracked.stderr.strip()}")
sys.exit(1)
paths.extend(untracked.stdout.splitlines())
return sorted({self._normalize_rel_path(p) for p in paths if p.strip()})
def _housekeeping_paths(self, step_num: int) -> set[str]:
return {
f"phases/{self._phase_dir_name}/index.json",
f"phases/{self._phase_dir_name}/step{step_num}-output.json",
"phases/index.json",
}
def _classify_step_changes(self, step_num: int, step: dict, changed_paths: list[str]) -> tuple[list[str], list[str], list[str]]:
allowed_patterns = step.get("allowed_paths", [])
housekeeping_set = self._housekeeping_paths(step_num)
allowed: list[str] = []
housekeeping: list[str] = []
disallowed: list[str] = []
for path in changed_paths:
rel = self._normalize_rel_path(path)
if rel in housekeeping_set:
housekeeping.append(rel)
elif self._path_allowed(rel, allowed_patterns):
allowed.append(rel)
else:
disallowed.append(rel)
return allowed, housekeeping, disallowed
def _checkout_branch(self):
branch = self._branch_name()
branch = f"feat-{self._phase_name}"
r = self._run_git("rev-parse", "--abbrev-ref", "HEAD")
if r.returncode != 0:
@@ -249,45 +137,28 @@ class StepExecutor:
print(f" Branch: {branch}")
def _stage_paths(self, paths: list[str]):
if not paths:
return
r = self._run_git("add", "--", *paths)
if r.returncode != 0:
print(" ERROR: git add failed.")
print(f" {r.stderr.strip()}")
sys.exit(1)
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"
def _commit_step(self, step: dict, step_name: str):
step_num = step["step"]
changed = self._changed_paths()
allowed, housekeeping, disallowed = self._classify_step_changes(step_num, step, changed)
if disallowed:
print(f" ERROR: Step {step_num} modified files outside allowed_paths:")
for path in disallowed:
print(f" {path}")
sys.exit(1)
self._run_git("add", "-A")
self._run_git("reset", "HEAD", "--", output_rel)
self._run_git("reset", "HEAD", "--", index_rel)
if allowed:
if self._run_git("diff", "--cached", "--quiet").returncode != 0:
msg = self.FEAT_MSG.format(phase=self._phase_name, num=step_num, name=step_name)
self._validate_before_commit(msg)
self._stage_paths(allowed)
if self._run_git("diff", "--cached", "--quiet").returncode != 0:
r = self._run_git("commit", "-m", msg)
if r.returncode != 0:
print(f" ERROR: code commit failed: {r.stderr.strip()}")
sys.exit(1)
r = self._run_git("commit", "-m", msg)
if r.returncode == 0:
print(f" Commit: {msg}")
else:
print(f" WARN: 코드 커밋 실패: {r.stderr.strip()}")
if housekeeping:
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)
self._validate_before_commit(msg)
self._stage_paths(housekeeping)
if self._run_git("diff", "--cached", "--quiet").returncode != 0:
r = self._run_git("commit", "-m", msg)
if r.returncode != 0:
print(f" ERROR: housekeeping commit failed: {r.stderr.strip()}")
sys.exit(1)
r = self._run_git("commit", "-m", msg)
if r.returncode != 0:
print(f" WARN: housekeeping 커밋 실패: {r.stderr.strip()}")
# --- top-level index ---
@@ -311,11 +182,16 @@ 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(
"## 프로젝트 규칙 (AGENTS.md)\n\n"
f"{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')}")
sections.append(
f"## {doc.stem}\n\n{doc.read_text(encoding='utf-8')}"
)
return "\n\n---\n\n".join(sections) if sections else ""
@staticmethod
@@ -330,11 +206,7 @@ class StepExecutor:
return "## 이전 Step 산출물\n\n" + "\n".join(lines) + "\n\n"
def _build_preamble(self, guardrails: str, step_context: str,
allowed_paths: list[str],
prev_error: Optional[str] = None) -> str:
commit_example = self.FEAT_MSG.format(
phase=self._phase_name, num="N", name="<step-name>"
)
retry_section = ""
if prev_error:
retry_section = (
@@ -345,9 +217,6 @@ class StepExecutor:
f"당신은 {self._project} 프로젝트의 개발자입니다. 아래 step을 수행하세요.\n\n"
f"{guardrails}\n\n---\n\n"
f"{step_context}{retry_section}"
f"## Step file allowlist\n\n"
f"This step may modify only these repository-relative paths:\n"
f"{chr(10).join(f'- {p}' for p in allowed_paths)}\n\n"
f"## 작업 규칙\n\n"
f"1. 이전 step에서 작성된 코드를 확인하고 일관성을 유지하라.\n"
f"2. 이 step에 명시된 작업만 수행하라. 추가 기능이나 파일을 만들지 마라.\n"
@@ -357,8 +226,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. 모든 변경사항을 커밋하라:\n"
f" {commit_example}\n\n---\n\n"
f"6. 변경사항을 직접 커밋하지 마라. Git 커밋과 timestamp는 실행기가 처리한다.\n\n"
f"---\n\n"
)
# --- Codex 호출 ---
@@ -372,10 +241,30 @@ class StepExecutor:
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,
)
command = [
"codex",
"exec",
"--json",
"--sandbox",
"workspace-write",
"--dangerously-bypass-hook-trust",
"--cd",
self._root,
"-",
]
try:
result = subprocess.run(
command,
cwd=self._root,
input=prompt,
capture_output=True,
text=True,
timeout=1800,
)
except FileNotFoundError as exc:
raise CodexEnvironmentError(
"Codex CLI를 찾을 수 없습니다. codex를 설치하고 PATH를 확인하세요."
) from exc
if result.returncode != 0:
print(f"\n WARN: Codex가 비정상 종료됨 (code {result.returncode})")
@@ -388,11 +277,28 @@ class StepExecutor:
"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)
self._write_json(out_path, output)
return output
@staticmethod
def _codex_environment_failure(output: dict) -> Optional[str]:
if output.get("exitCode", 0) == 0:
return None
diagnostic = (
f"{output.get('stderr', '')}\n{output.get('stdout', '')}"
).lower()
markers = {
"not logged in": "Codex 인증이 필요합니다.",
"authentication": "Codex 인증에 실패했습니다.",
"unexpected argument": "현재 Codex CLI가 필요한 옵션을 지원하지 않습니다.",
"unrecognized option": "현재 Codex CLI가 필요한 옵션을 지원하지 않습니다.",
}
for marker, message in markers.items():
if marker in diagnostic:
return message
return None
# --- 헤더 & 검증 ---
def _print_header(self):
@@ -436,16 +342,20 @@ class StepExecutor:
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, step.get("allowed_paths", []), prev_error)
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)
output = self._invoke_codex(step, preamble)
elapsed = int(pi.elapsed)
environment_failure = self._codex_environment_failure(output)
if environment_failure:
raise CodexEnvironmentError(environment_failure)
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()
@@ -455,7 +365,7 @@ class StepExecutor:
if s["step"] == step_num:
s["completed_at"] = ts
self._write_json(self._index_file, index)
self._commit_step(step, step_name)
self._commit_step(step_num, step_name)
print(f" ✓ Step {step_num}: {step_name} [{elapsed}s]")
return True
@@ -470,9 +380,22 @@ class StepExecutor:
self._update_top_index("blocked")
sys.exit(2)
runtime_error = ""
if output["exitCode"] != 0:
runtime_error = (
output.get("stderr", "").strip()
or output.get("stdout", "").strip()
)
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",
(
s.get(
"error_message",
runtime_error or "Step did not update status",
)
for s in index["steps"]
if s["step"] == step_num
),
runtime_error or "Step did not update status",
)
if attempt < self.MAX_RETRIES:
@@ -490,7 +413,7 @@ class StepExecutor:
s["error_message"] = f"[{self.MAX_RETRIES}회 시도 후 실패] {err_msg}"
s["failed_at"] = ts
self._write_json(self._index_file, index)
self._commit_step(step, step_name)
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")
@@ -506,7 +429,6 @@ class StepExecutor:
print("\n All steps completed!")
return
self._validate_step_allowlist(pending)
step_num = pending["step"]
for s in index["steps"]:
if s["step"] == step_num and "started_at" not in s:
@@ -522,22 +444,15 @@ class StepExecutor:
self._write_json(self._index_file, index)
self._update_top_index("completed")
final_paths = [f"phases/{self._phase_dir_name}/index.json"]
if self._top_index_file.exists():
final_paths.append("phases/index.json")
self._validate_before_commit(f"chore({self._phase_name}): mark phase completed")
self._stage_paths(final_paths)
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" ERROR: phase completion commit failed: {r.stderr.strip()}")
sys.exit(1)
else:
if r.returncode == 0:
print(f"{msg}")
if self._auto_push:
branch = self._branch_name()
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()}")
@@ -555,7 +470,11 @@ def main():
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()
try:
StepExecutor(args.phase_dir, auto_push=args.push).run()
except CodexEnvironmentError as exc:
print(f"ERROR: {exc}")
sys.exit(1)
if __name__ == "__main__":
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""Codex PreToolUse 정책: 위험 명령과 테스트 없는 구현 파일 수정을 차단한다."""
import json
import re
import sys
from pathlib import Path
from typing import Any
SCRIPTS_DIR = Path(__file__).resolve().parents[1]
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
from msvc_harness.config import ConfigError, load_config
from msvc_harness.tdd_policy import CPP_SUFFIXES, evaluate_paths
DANGEROUS_PATTERNS = (
re.compile(r"\bgit\s+reset\s+--hard\b", re.IGNORECASE),
re.compile(r"\bgit\s+push\b[^\n]*--force(?:-with-lease)?\b", re.IGNORECASE),
re.compile(r"\brm\s+-rf\b", re.IGNORECASE),
re.compile(
r"\bRemove-Item\b(?=[^\n]*-Recurse\b)(?=[^\n]*-Force\b)",
re.IGNORECASE,
),
re.compile(
r"\b(?:rmdir|rd)\b(?=[^\n]*/s\b)(?=[^\n]*/q\b)",
re.IGNORECASE,
),
re.compile(r"\bDROP\s+TABLE\b", re.IGNORECASE),
)
PATCH_PATH = re.compile(
r"^\*\*\* (?:(?:Add|Update|Delete) File:|Move to:) (?P<path>.+?)\s*$",
re.MULTILINE,
)
def _tool_input(payload: dict[str, Any]) -> dict[str, Any]:
value = payload.get("tool_input", {})
return value if isinstance(value, dict) else {}
def _candidate_paths(payload: dict[str, Any], root: Path) -> list[Path]:
tool_input = _tool_input(payload)
raw_paths: list[str] = []
for key in ("path", "file_path"):
value = tool_input.get(key)
if isinstance(value, str) and value.strip():
raw_paths.append(value.strip())
edits = tool_input.get("edits")
if isinstance(edits, list):
for edit in edits:
if not isinstance(edit, dict):
continue
value = edit.get("path")
if isinstance(value, str) and value.strip():
raw_paths.append(value.strip())
for key in ("patch", "input"):
value = tool_input.get(key)
if isinstance(value, str):
raw_paths.extend(match.group("path") for match in PATCH_PATH.finditer(value))
paths: list[Path] = []
for raw_path in raw_paths:
path = Path(raw_path)
paths.append(path.resolve() if path.is_absolute() else (root / path).resolve())
return list(dict.fromkeys(paths))
def evaluate(payload: dict[str, Any], root: Path) -> str | None:
"""Return a blocking reason, or None when the tool call is allowed."""
tool_name = str(payload.get("tool_name", ""))
tool_input = _tool_input(payload)
if tool_name in {"Bash", "shell_command", "PowerShell"}:
command = tool_input.get("command", "")
if isinstance(command, str) and any(
pattern.search(command) for pattern in DANGEROUS_PATTERNS
):
return "위험한 명령어가 감지되어 실행을 차단했습니다."
if tool_name not in {"apply_patch", "Edit", "MultiEdit", "Write"}:
return None
paths = _candidate_paths(payload, root)
try:
config = load_config(root)
except ConfigError as exc:
if any(path.suffix.lower() in CPP_SUFFIXES for path in paths):
return f"TDD GUARD: .harness/config.json must be repaired: {exc}"
return None
return evaluate_paths(paths, root, config.tdd)
def main() -> int:
try:
payload = json.load(sys.stdin)
if not isinstance(payload, dict):
raise TypeError("hook input must be a JSON object")
cwd = payload.get("cwd")
if cwd is not None and not isinstance(cwd, str):
raise TypeError("hook cwd must be a string")
except (json.JSONDecodeError, TypeError) as exc:
print(f"TDD GUARD: hook 입력을 해석하지 못해 검사를 건너뜁니다: {exc}", file=sys.stderr)
return 0
root = Path(payload.get("cwd") or Path.cwd()).resolve()
reason = evaluate(payload, root)
if reason:
print(reason, file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""Codex Stop hook: validate discovered C/C++ projects with MSVC."""
import json
import os
import subprocess
import sys
import time
from pathlib import Path
SCRIPTS = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(SCRIPTS))
from msvc_harness.adapters.base import AdapterError
from msvc_harness.adapters.cmake import CMakeAdapter
from msvc_harness.adapters.msbuild import MSBuildAdapter
from msvc_harness.config import ConfigError, load_config
from msvc_harness.discovery import DiscoveryError, discover_project
from msvc_harness.models import ProjectKind
from msvc_harness.process import ValidationFailure, execute_plan
from msvc_harness.toolchain import ToolchainError, discover_toolchain
REENTRY_ENV = "CODEX_STOP_VALIDATION_ACTIVE"
TOTAL_TIMEOUT_SECONDS = 1800
def _remaining(deadline, clock, stage):
remaining = deadline - clock()
if remaining <= 0:
raise ValidationFailure(f"{stage} timed out before it could start")
return remaining
def _project_root(
cwd: Path,
*,
deadline: float,
run=subprocess.run,
clock=time.monotonic,
) -> Path:
argv = ["git", "rev-parse", "--show-toplevel"]
resolved_cwd = cwd.resolve()
try:
result = run(
argv,
cwd=resolved_cwd,
capture_output=True,
text=True,
shell=False,
timeout=_remaining(deadline, clock, "repository discovery"),
)
except subprocess.TimeoutExpired as exc:
raise ValidationFailure(
f"repository discovery timed out; argv={argv!r}; "
f"cwd={str(resolved_cwd)!r}"
) from exc
if result.returncode == 0 and result.stdout.strip():
return Path(result.stdout.strip()).resolve()
return resolved_cwd
def run_validations(
root: Path,
*,
deadline: float | None = None,
clock=time.monotonic,
) -> tuple[bool, str]:
"""Build and test a discovered C/C++ project, when one exists."""
if deadline is None:
deadline = clock() + TOTAL_TIMEOUT_SECONDS
try:
config = load_config(root)
discovery = discover_project(root, config)
if discovery.selection is None:
return True, ""
selection = discovery.selection
tools = discover_toolchain(
selection.kind,
deadline=deadline,
clock=clock,
)
adapter = (
CMakeAdapter()
if selection.kind is ProjectKind.CMAKE
else MSBuildAdapter()
)
plan = adapter.create_plan(root, selection, config, tools)
child_env = os.environ.copy()
child_env[REENTRY_ENV] = "1"
execute_plan(
plan,
root,
env=child_env,
deadline=deadline,
clock=clock,
)
return True, ""
except (
ConfigError,
DiscoveryError,
ToolchainError,
AdapterError,
ValidationFailure,
OSError,
) as exc:
return False, str(exc)
def _emit_stop_response(message: str) -> None:
print(
json.dumps(
{
"continue": False,
"stopReason": message,
"systemMessage": message,
},
ensure_ascii=False,
)
)
def main(*, run=subprocess.run, clock=time.monotonic) -> int:
if os.environ.get(REENTRY_ENV) == "1":
return 0
deadline = clock() + TOTAL_TIMEOUT_SECONDS
try:
root = _project_root(
Path.cwd(),
deadline=deadline,
run=run,
clock=clock,
)
ok, message = run_validations(root, deadline=deadline, clock=clock)
except (OSError, ValidationFailure) as exc:
_emit_stop_response(f"validation hook failed: {exc}")
return 0
if ok:
return 0
_emit_stop_response(message)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+3
View File
@@ -0,0 +1,3 @@
from .config import ConfigError, load_config
__all__ = ["ConfigError", "load_config"]
@@ -0,0 +1,5 @@
from .base import AdapterError, ValidationAdapter
from .cmake import CMakeAdapter
from .msbuild import MSBuildAdapter
__all__ = ["AdapterError", "CMakeAdapter", "MSBuildAdapter", "ValidationAdapter"]
+19
View File
@@ -0,0 +1,19 @@
from pathlib import Path
from typing import Protocol
from ..models import HarnessConfig, ProjectSelection, Toolchain, ValidationPlan
class AdapterError(RuntimeError):
pass
class ValidationAdapter(Protocol):
def create_plan(
self,
root: Path,
selection: ProjectSelection,
config: HarnessConfig,
tools: Toolchain,
) -> ValidationPlan:
raise NotImplementedError
+99
View File
@@ -0,0 +1,99 @@
from pathlib import Path
from ..models import (
CommandSpec,
HarnessConfig,
ProjectKind,
ProjectSelection,
ResultCheck,
ResultCheckKind,
Toolchain,
ValidationPlan,
ValidationStep,
)
from .base import AdapterError
class CMakeAdapter:
def create_plan(
self,
root: Path,
selection: ProjectSelection,
config: HarnessConfig,
tools: Toolchain,
) -> ValidationPlan:
if selection.kind is not ProjectKind.CMAKE:
raise AdapterError("CMakeAdapter requires a CMake project selection")
if tools.cmake is None:
raise AdapterError("CMake tool is required")
if tools.ctest is None:
raise AdapterError("CTest tool is required")
if config.cmake.configure_preset is None:
command_cwd = root
binary = root / ".harness/build"
configure = (
str(tools.cmake),
"-S",
str(config.cmake.source_dir),
"-B",
str(binary),
"-A",
"x64",
)
build = (str(tools.cmake), "--build", str(binary), "--config", "Debug")
discover = (
str(tools.ctest),
"--test-dir",
str(binary),
"-C",
"Debug",
"--show-only=json-v1",
)
test = (
str(tools.ctest),
"--test-dir",
str(binary),
"-C",
"Debug",
"--output-on-failure",
)
else:
command_cwd = config.cmake.source_dir
binary = config.cmake.binary_dir
if (
binary is None
or config.cmake.build_preset is None
or config.cmake.test_preset is None
):
raise AdapterError("CMake presets require binary, build, and test settings")
configure = (str(tools.cmake), "--preset", config.cmake.configure_preset)
build = (str(tools.cmake), "--build", "--preset", config.cmake.build_preset)
discover = (
str(tools.ctest),
"--preset",
config.cmake.test_preset,
"--show-only=json-v1",
)
test = (
str(tools.ctest),
"--preset",
config.cmake.test_preset,
"--output-on-failure",
)
return ValidationPlan(
ProjectKind.CMAKE,
(
ValidationStep(
CommandSpec(configure, command_cwd, "configure"),
(ResultCheck(ResultCheckKind.CMAKE_COMPILER_IS_MSVC, binary),),
),
ValidationStep(CommandSpec(build, command_cwd, "build")),
ValidationStep(
CommandSpec(discover, command_cwd, "test-discovery"),
(ResultCheck(ResultCheckKind.CTEST_HAS_TESTS),),
),
ValidationStep(CommandSpec(test, command_cwd, "test")),
),
)
+60
View File
@@ -0,0 +1,60 @@
from pathlib import Path
from ..models import (
CommandSpec,
HarnessConfig,
ProjectKind,
ProjectSelection,
Toolchain,
ValidationPlan,
ValidationStep,
)
from .base import AdapterError
def _test_argv(root: Path, command: tuple[str, ...]) -> tuple[str, ...]:
if "/" not in command[0] and "\\" not in command[0]:
return command
first = Path(command[0].replace("\\", "/"))
executable = first if first.is_absolute() else (root / first).resolve()
try:
executable.relative_to(root)
except ValueError as exc:
raise AdapterError(
"msbuild.testCommand resolves outside the repository"
) from exc
return (str(executable), *command[1:])
class MSBuildAdapter:
def create_plan(
self,
root: Path,
selection: ProjectSelection,
config: HarnessConfig,
tools: Toolchain,
) -> ValidationPlan:
if selection.kind is not ProjectKind.MSBUILD:
raise AdapterError("MSBuildAdapter received a non-MSBuild project")
if not config.msbuild.test_command:
raise AdapterError(
"msbuild.testCommand is required for .sln/.vcxproj validation"
)
build = ValidationStep(
CommandSpec(
(
str(tools.msbuild),
str(selection.project_file),
"/m",
"/nologo",
f"/p:Configuration={config.msbuild.configuration}",
f"/p:Platform={config.msbuild.platform}",
),
root,
"build",
)
)
test = ValidationStep(
CommandSpec(_test_argv(root, config.msbuild.test_command), root, "test")
)
return ValidationPlan(ProjectKind.MSBUILD, (build, test))
+146
View File
@@ -0,0 +1,146 @@
import json
from pathlib import Path
from typing import Any
from .models import CMakeConfig, HarnessConfig, MSBuildConfig, TDDConfig
DEFAULT_TEST_PATTERNS = (
"{stem}_test.cpp",
"{stem}_tests.cpp",
"test_{stem}.cpp",
"{stem}.test.cpp",
)
class ConfigError(ValueError):
pass
def _reject_unknown(data: dict[str, Any], allowed: set[str], label: str) -> None:
unknown = sorted(set(data) - allowed)
if unknown:
raise ConfigError(f"{label} contains unexpected field: {unknown[0]}")
def _repo_path(root: Path, raw: str, label: str) -> Path:
candidate = Path(raw)
if candidate.is_absolute():
raise ConfigError(f"{label} must be repository-relative")
resolved = (root / candidate).resolve()
try:
resolved.relative_to(root)
except ValueError as exc:
raise ConfigError(f"{label} resolves outside the repository") from exc
return resolved
def _mapping(value: Any, label: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise ConfigError(f"{label} must be an object")
return value
def _nonempty_string(value: Any, label: str) -> str:
if not isinstance(value, str) or not value:
raise ConfigError(f"{label} must be a non-empty string")
return value
def _string_array(value: Any, label: str) -> tuple[str, ...]:
if not isinstance(value, list):
raise ConfigError(f"{label} must be an array")
if not value or any(not isinstance(item, str) or not item for item in value):
raise ConfigError(f"{label} must be a non-empty string array")
return tuple(value)
def load_config(root: Path) -> HarnessConfig:
root = root.resolve()
config_path = root / ".harness" / "config.json"
if config_path.is_file():
try:
data = json.loads(config_path.read_text(encoding="utf-8"))
except UnicodeDecodeError as exc:
raise ConfigError(
f"{config_path}: configuration must be valid UTF-8: {exc}"
) from exc
except json.JSONDecodeError as exc:
raise ConfigError(f"{config_path}: {exc}") from exc
else:
data = {"version": 1}
data = _mapping(data, "config")
_reject_unknown(data, {"version", "projectType", "cmake", "msbuild", "tdd"}, "config")
if data.get("version") != 1 or isinstance(data.get("version"), bool):
raise ConfigError("version must be 1")
project_type = data.get("projectType", "auto")
if project_type not in {"auto", "cmake", "msbuild"}:
raise ConfigError("projectType must be auto, cmake, or msbuild")
cmake_data = _mapping(data.get("cmake", {}), "cmake")
_reject_unknown(
cmake_data,
{"sourceDir", "binaryDir", "configurePreset", "buildPreset", "testPreset"},
"cmake",
)
source_dir = _repo_path(
root, _nonempty_string(cmake_data.get("sourceDir", "."), "cmake.sourceDir"), "cmake.sourceDir"
)
binary_dir = None
if "binaryDir" in cmake_data:
binary_dir = _repo_path(
root, _nonempty_string(cmake_data["binaryDir"], "cmake.binaryDir"), "cmake.binaryDir"
)
preset_keys = ("configurePreset", "buildPreset", "testPreset", "binaryDir")
if any(key in cmake_data for key in preset_keys) and not all(key in cmake_data for key in preset_keys):
raise ConfigError("cmake configurePreset, buildPreset, testPreset, and binaryDir must be specified together")
configure_preset = (
_nonempty_string(cmake_data["configurePreset"], "cmake.configurePreset")
if "configurePreset" in cmake_data
else None
)
build_preset = (
_nonempty_string(cmake_data["buildPreset"], "cmake.buildPreset")
if "buildPreset" in cmake_data
else None
)
test_preset = (
_nonempty_string(cmake_data["testPreset"], "cmake.testPreset")
if "testPreset" in cmake_data
else None
)
msbuild_data = _mapping(data.get("msbuild", {}), "msbuild")
_reject_unknown(msbuild_data, {"solution", "configuration", "platform", "testCommand"}, "msbuild")
solution = None
if "solution" in msbuild_data:
solution = _repo_path(
root, _nonempty_string(msbuild_data["solution"], "msbuild.solution"), "msbuild.solution"
)
configuration = _nonempty_string(msbuild_data.get("configuration", "Debug"), "msbuild.configuration")
platform = _nonempty_string(msbuild_data.get("platform", "x64"), "msbuild.platform")
test_command = (
_string_array(msbuild_data["testCommand"], "msbuild.testCommand")
if "testCommand" in msbuild_data
else None
)
tdd_data = _mapping(data.get("tdd", {}), "tdd")
_reject_unknown(tdd_data, {"testRoots", "testPatterns", "exclude"}, "tdd")
raw_test_roots = _string_array(tdd_data["testRoots"], "tdd.testRoots") if "testRoots" in tdd_data else ("tests", "test")
test_roots = tuple(_repo_path(root, item, "tdd.testRoots") for item in raw_test_roots)
test_patterns = _string_array(tdd_data["testPatterns"], "tdd.testPatterns") if "testPatterns" in tdd_data else DEFAULT_TEST_PATTERNS
if any("{stem}" not in pattern for pattern in test_patterns):
raise ConfigError("every tdd.testPatterns entry must contain {stem}")
exclude = _string_array(tdd_data["exclude"], "tdd.exclude") if "exclude" in tdd_data else ()
for item in exclude:
_repo_path(root, item, "tdd.exclude")
return HarnessConfig(
version=1,
project_type=project_type,
cmake=CMakeConfig(source_dir, binary_dir, configure_preset, build_preset, test_preset),
msbuild=MSBuildConfig(solution, configuration, platform, test_command),
tdd=TDDConfig(test_roots, test_patterns, exclude),
)
+77
View File
@@ -0,0 +1,77 @@
from pathlib import Path
from .models import DiscoveryResult, HarnessConfig, ProjectKind, ProjectSelection
from .tdd_policy import find_cpp_files
class DiscoveryError(RuntimeError):
pass
def _cmake_entry(source: Path) -> Path | None:
for name in ("CMakePresets.json", "CMakeUserPresets.json", "CMakeLists.txt"):
candidate = source / name
if candidate.is_file():
return candidate
return None
def _single_msbuild(root: Path, explicit: Path | None) -> Path:
if explicit is not None:
if explicit.is_file() and explicit.suffix.lower() in {".sln", ".vcxproj"}:
return explicit
raise DiscoveryError(f"configured MSBuild project is invalid: {explicit}")
solutions = sorted(root.glob("*.sln"))
if len(solutions) > 1:
raise DiscoveryError("multiple .sln files found; set msbuild.solution")
if solutions:
return solutions[0]
projects = sorted(root.glob("*.vcxproj"))
if len(projects) > 1:
raise DiscoveryError("multiple .vcxproj files found; set msbuild.solution")
if projects:
return projects[0]
raise DiscoveryError("no root .sln or .vcxproj was found")
def discover_project(root: Path, config: HarnessConfig) -> DiscoveryResult:
root = root.resolve()
if config.project_type == "cmake":
entry = _cmake_entry(config.cmake.source_dir)
if entry is None:
raise DiscoveryError(
f"no CMake project found in {config.cmake.source_dir}"
)
return DiscoveryResult(ProjectSelection(ProjectKind.CMAKE, entry))
if config.project_type == "msbuild":
project = _single_msbuild(root, config.msbuild.solution)
return DiscoveryResult(ProjectSelection(ProjectKind.MSBUILD, project))
entry = _cmake_entry(root)
if entry is not None:
return DiscoveryResult(ProjectSelection(ProjectKind.CMAKE, entry))
solutions = sorted(root.glob("*.sln"))
if len(solutions) > 1:
raise DiscoveryError("multiple .sln files found; set msbuild.solution")
if solutions:
return DiscoveryResult(
ProjectSelection(ProjectKind.MSBUILD, solutions[0])
)
projects = sorted(root.glob("*.vcxproj"))
if len(projects) > 1:
raise DiscoveryError("multiple .vcxproj files found; set msbuild.solution")
if projects:
return DiscoveryResult(
ProjectSelection(ProjectKind.MSBUILD, projects[0])
)
cpp_files = find_cpp_files(root)
if cpp_files:
raise DiscoveryError(
"C/C++ files exist but no CMakeLists.txt, preset, .sln, or .vcxproj "
"was found; configure projectType and project path"
)
return DiscoveryResult(None, ())
+100
View File
@@ -0,0 +1,100 @@
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
class ProjectKind(Enum):
CMAKE = "cmake"
MSBUILD = "msbuild"
class ResultCheckKind(Enum):
CMAKE_COMPILER_IS_MSVC = "cmake_compiler_is_msvc"
CTEST_HAS_TESTS = "ctest_has_tests"
@dataclass(frozen=True)
class CMakeConfig:
source_dir: Path
binary_dir: Path | None = None
configure_preset: str | None = None
build_preset: str | None = None
test_preset: str | None = None
@dataclass(frozen=True)
class MSBuildConfig:
solution: Path | None = None
configuration: str = "Debug"
platform: str = "x64"
test_command: tuple[str, ...] | None = None
@dataclass(frozen=True)
class TDDConfig:
test_roots: tuple[Path, ...]
test_patterns: tuple[str, ...]
exclude: tuple[str, ...] = ()
@dataclass(frozen=True)
class HarnessConfig:
version: int
project_type: str
cmake: CMakeConfig
msbuild: MSBuildConfig
tdd: TDDConfig
@dataclass(frozen=True)
class ProjectSelection:
kind: ProjectKind
project_file: Path
@dataclass(frozen=True)
class DiscoveryResult:
selection: ProjectSelection | None
cpp_files: tuple[Path, ...] = ()
@dataclass(frozen=True)
class Toolchain:
installation: Path
msbuild: Path
cmake: Path | None = None
ctest: Path | None = None
@dataclass(frozen=True)
class CommandSpec:
argv: tuple[str, ...]
cwd: Path
stage: str
timeout_seconds: int = 1800
@dataclass(frozen=True)
class ResultCheck:
kind: ResultCheckKind
path: Path | None = None
@dataclass(frozen=True)
class ValidationStep:
command: CommandSpec
checks: tuple[ResultCheck, ...] = ()
@dataclass(frozen=True)
class ValidationPlan:
project_kind: ProjectKind
steps: tuple[ValidationStep, ...]
@dataclass(frozen=True)
class CommandResult:
command: CommandSpec
returncode: int
stdout: str
stderr: str
+140
View File
@@ -0,0 +1,140 @@
import json
import locale
import re
import subprocess
import time
from .models import CommandResult, ResultCheckKind
DIAGNOSTIC_LIMIT = 8000
class ValidationFailure(RuntimeError):
pass
def _decode(value):
if value is None:
return ""
if isinstance(value, str):
return value
for encoding in ("utf-8", locale.getpreferredencoding(False)):
try:
return value.decode(encoding)
except (UnicodeDecodeError, LookupError):
continue
return value.decode("utf-8", errors="replace")
def _check_result(check, result):
if check.kind is ResultCheckKind.CMAKE_COMPILER_IS_MSVC:
if check.path is None:
raise ValidationFailure("MSVC check is missing binaryDir")
files = sorted(check.path.glob("CMakeFiles/*/CMakeCXXCompiler.cmake"))
if not files:
raise ValidationFailure("CMake did not generate compiler metadata")
text = files[-1].read_text(encoding="utf-8", errors="replace")
if not re.search(
r'set\s*\(\s*CMAKE_CXX_COMPILER_ID\s+"MSVC"\s*\)', text
):
raise ValidationFailure("CMake selected a compiler other than MSVC")
elif check.kind is ResultCheckKind.CTEST_HAS_TESTS:
try:
payload = json.loads(result.stdout)
except json.JSONDecodeError as exc:
raise ValidationFailure("CTest discovery did not return JSON") from exc
if not isinstance(payload, dict):
raise ValidationFailure("CTest discovery JSON must be an object")
if not isinstance(payload.get("tests"), list) or not payload["tests"]:
raise ValidationFailure("CTest discovered no tests")
def _diagnostic(stdout, stderr):
diagnostic = (stdout + "\n" + stderr).rstrip()[-DIAGNOSTIC_LIMIT:]
return diagnostic.replace("\r\n", " | ").replace("\n", " | ")
def _command_context(argv, cwd):
return f"argv={list(argv)!r}; cwd={str(cwd)!r}"
def execute_plan(
plan,
root,
*,
env=None,
total_timeout_seconds=1800,
deadline=None,
run=subprocess.run,
clock=time.monotonic,
):
root = root.resolve()
if deadline is None:
deadline = clock() + total_timeout_seconds
results = []
for step in plan.steps:
cwd = step.command.cwd.resolve()
context = _command_context(step.command.argv, cwd)
try:
cwd.relative_to(root)
except ValueError as exc:
raise ValidationFailure(
f"{step.command.stage} working directory is outside the repository; "
f"{context}"
) from exc
remaining = deadline - clock()
if remaining <= 0:
raise ValidationFailure(
f"{step.command.stage} timed out before it could start; {context}"
)
timeout = min(step.command.timeout_seconds, remaining)
try:
completed = run(
list(step.command.argv),
cwd=cwd,
env=env,
capture_output=True,
shell=False,
timeout=timeout,
)
except subprocess.TimeoutExpired as exc:
stdout = _decode(exc.output)
stderr = _decode(exc.stderr)
diagnostic = _diagnostic(stdout, stderr)
message = (
f"{step.command.stage} timed out after {timeout} seconds; {context}"
)
if diagnostic:
message = f"{message}; output tail: {diagnostic}"
raise ValidationFailure(message) from exc
stdout = _decode(completed.stdout)
stderr = _decode(completed.stderr)
result = CommandResult(step.command, completed.returncode, stdout, stderr)
if completed.returncode != 0:
diagnostic = _diagnostic(stdout, stderr)
message = (
f"{step.command.stage} failed with exit code {completed.returncode}; "
f"{context}"
)
if diagnostic:
message = f"{message}; output tail: {diagnostic}"
raise ValidationFailure(message)
try:
for check in step.checks:
_check_result(check, result)
except ValidationFailure as exc:
diagnostic = _diagnostic(stdout, stderr)
message = (
f"{step.command.stage} result check failed: {exc}; {context}"
)
if diagnostic:
message = f"{message}; output tail: {diagnostic}"
raise ValidationFailure(message) from exc
results.append(result)
return tuple(results)
+88
View File
@@ -0,0 +1,88 @@
from fnmatch import fnmatch
from pathlib import Path
CPP_SUFFIXES = frozenset({".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".hxx"})
DEFAULT_EXCLUDES = (
".harness/build/**",
"build/**",
"out/**",
"cmake-build-*/**",
"third_party/**",
"external/**",
"vendor/**",
"generated/**",
)
def is_excluded_path(path: Path, root: Path, extra: tuple[str, ...] = ()) -> bool:
try:
relative = path.resolve().relative_to(root.resolve()).as_posix()
except ValueError:
return False
return any(fnmatch(relative, pattern) for pattern in DEFAULT_EXCLUDES + extra)
def find_cpp_files(
root: Path,
extra_excludes: tuple[str, ...] = (),
) -> tuple[Path, ...]:
found = (
path
for path in root.resolve().rglob("*")
if path.is_file()
and path.suffix.lower() in CPP_SUFFIXES
and not is_excluded_path(path, root, extra_excludes)
)
return tuple(sorted(found))
def is_test_file(path: Path, root: Path, config) -> bool:
resolved = path.resolve()
if any(root == resolved or root in resolved.parents for root in config.test_roots):
return True
relative = resolved.relative_to(root.resolve())
if {"test", "tests"} & {part.lower() for part in relative.parts}:
return True
stem = resolved.stem.lower()
return stem.startswith("test_") or stem.endswith(("_test", "_tests", ".test"))
def matching_test_exists(path: Path, config) -> bool:
names = tuple(pattern.format(stem=path.stem) for pattern in config.test_patterns)
roots = (
*config.test_roots,
path.parent / "tests",
path.parent / "test",
)
for root in roots:
if not root.is_dir():
continue
for name in names:
if any(candidate.is_file() for candidate in root.rglob(name)):
return True
return False
def evaluate_paths(paths, root: Path, config) -> str | None:
root = root.resolve()
for raw in paths:
path = raw.resolve()
try:
path.relative_to(root)
except ValueError:
return f"TDD GUARD: '{path}' is outside the repository"
if path.suffix.lower() not in CPP_SUFFIXES:
continue
if (
path.name.lower() == "main.cpp"
or is_excluded_path(path, root, config.exclude)
or is_test_file(path, root, config)
):
continue
if not matching_test_exists(path, config):
expected = config.test_patterns[0].format(stem=path.stem)
return (
f"TDD GUARD: '{path.name}' requires an existing test such as "
f"'{expected}'. Add the test in a configured test root first."
)
return None
+169
View File
@@ -0,0 +1,169 @@
import os
import shutil
import subprocess
import time
from pathlib import Path
from .models import ProjectKind, Toolchain
VC_WORKLOAD = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
VS_CMAKE_RELATIVE = Path(
"Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin"
)
class ToolchainError(RuntimeError):
pass
def _decode(value):
if isinstance(value, str):
return value
return (value or b"").decode("utf-8", errors="replace")
def _first_path(result, label, argv, cwd):
text = _decode(result.stdout).strip()
if result.returncode != 0:
diagnostic = (
_decode(result.stdout) + "\n" + _decode(result.stderr)
).rstrip()[-8000:]
diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ")
message = (
f"{label} probe failed with exit code {result.returncode}; "
f"argv={argv!r}; cwd={str(cwd)!r}"
)
if diagnostic:
message = f"{message}; output tail: {diagnostic}"
raise ToolchainError(
f"{message}; install Visual Studio Desktop development with C++"
)
if not text:
raise ToolchainError(
f"{label} probe returned no path; argv={argv!r}; cwd={str(cwd)!r}; "
"install Visual Studio Desktop development with C++"
)
path = Path(text.splitlines()[0]).resolve()
if not path.exists():
diagnostic = (
_decode(result.stdout) + "\n" + _decode(result.stderr)
).rstrip()[-8000:]
diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ")
message = (
f"{label} probe returned nonexistent path with exit code 0; "
f"argv={argv!r}; cwd={str(cwd)!r}"
)
if diagnostic:
message = f"{message}; output tail: {diagnostic}"
raise ToolchainError(
f"{message}; install or repair Visual Studio Desktop development with C++"
)
return path
def _require_path(path, label):
if path is None or not path.exists():
raise ToolchainError(f"{label} not found")
return path.resolve()
def _remaining(deadline, clock, label):
if deadline is None:
return None
remaining = deadline - clock()
if remaining <= 0:
raise ToolchainError(f"{label} probe timed out before it could start")
return remaining
def _probe(run, argv, label, *, deadline, clock):
cwd = Path.cwd().resolve()
kwargs = {
"cwd": cwd,
"capture_output": True,
"check": False,
"shell": False,
}
remaining = _remaining(deadline, clock, label)
if remaining is not None:
kwargs["timeout"] = remaining
try:
return run(argv, **kwargs)
except subprocess.TimeoutExpired as exc:
raise ToolchainError(
f"{label} probe timed out; argv={argv!r}; cwd={str(cwd)!r}; "
"install or repair Visual Studio Desktop development with C++"
) from exc
def discover_toolchain(
kind: ProjectKind,
*,
env=None,
which=shutil.which,
run=subprocess.run,
deadline=None,
clock=time.monotonic,
) -> Toolchain:
environment = os.environ if env is None else env
vswhere_path = which("vswhere.exe")
if vswhere_path is None:
program_files = environment.get("ProgramFiles(x86)")
if program_files:
candidate = Path(program_files) / "Microsoft Visual Studio/Installer/vswhere.exe"
vswhere_path = str(candidate) if candidate.exists() else None
vswhere = _require_path(
Path(vswhere_path) if vswhere_path else None,
"vswhere.exe; install Visual Studio Desktop development with C++",
)
install_argv = [
str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD,
"-property", "installationPath",
]
install_result = _probe(
run,
install_argv,
"Visual Studio installation",
deadline=deadline,
clock=clock,
)
probe_cwd = Path.cwd().resolve()
installation = _require_path(
_first_path(
install_result,
"Visual Studio installation",
install_argv,
probe_cwd,
),
"Visual Studio installation",
)
msbuild_argv = [
str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD,
"-find", r"MSBuild\**\Bin\MSBuild.exe",
]
msbuild_result = _probe(
run,
msbuild_argv,
"MSBuild",
deadline=deadline,
clock=clock,
)
msbuild = _require_path(
_first_path(msbuild_result, "MSBuild", msbuild_argv, probe_cwd),
"MSBuild.exe",
)
if kind is not ProjectKind.CMAKE:
return Toolchain(installation=installation, msbuild=msbuild)
bundled_cmake_dir = installation / VS_CMAKE_RELATIVE
cmake = _require_path(
Path(which("cmake.exe")) if which("cmake.exe") else bundled_cmake_dir / "cmake.exe",
"cmake.exe",
)
ctest = _require_path(
Path(which("ctest.exe")) if which("ctest.exe") else bundled_cmake_dir / "ctest.exe",
"ctest.exe",
)
return Toolchain(installation=installation, msbuild=msbuild, cmake=cmake, ctest=ctest)
-75
View File
@@ -1,75 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENTS_ROOT = ROOT / ".codex" / "agents"
AGENT_SKILL_REFERENCES = {
"coordinator-agent.toml": (
"fesa-requirements-baseline",
"fesa-reference-models",
"fesa-release-readiness",
),
"requirement-agent.toml": ("fesa-requirements-baseline",),
"research-agent.toml": (
"fesa-research-evidence",
"fem-theory-query",
),
"formulation-agent.toml": (
"fesa-formulation-spec",
"fem-theory-query",
),
"numerical-review-agent.toml": (
"fesa-numerical-review",
"fem-theory-query",
),
"io-definition-agent.toml": (
"fesa-io-contract",
"fem-theory-query",
),
"reference-model-agent.toml": (
"fesa-reference-models",
"fem-theory-query",
),
"implementation-planning-agent.toml": (
"fesa-formulation-spec",
"fesa-reference-models",
"fesa-cpp-msvc-tdd",
"fem-theory-query",
),
"implementation-agent.toml": ("fesa-cpp-msvc-tdd",),
"build-test-executor-agent.toml": ("fesa-cpp-msvc-tdd",),
"correction-agent.toml": ("fesa-cpp-msvc-tdd",),
"reference-verification-agent.toml": (
"fesa-reference-comparison",
"fesa-io-contract",
),
"physics-evaluation-agent.toml": (
"fesa-physics-sanity",
"fem-theory-query",
),
"release-agent.toml": ("fesa-release-readiness",),
}
class AgentSkillReferenceTests(unittest.TestCase):
def test_agents_reference_their_solver_skills(self):
for agent_file, skill_names in AGENT_SKILL_REFERENCES.items():
with self.subTest(agent=agent_file):
text = (AGENTS_ROOT / agent_file).read_text(encoding="utf-8")
data = tomllib.loads(text)
instructions = data["developer_instructions"]
self.assertIn("Skill references:", instructions)
for skill_name in skill_names:
self.assertIn(f"${skill_name}", instructions)
if __name__ == "__main__":
unittest.main()
@@ -1,86 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "build-test-executor-agent.toml"
BUILD_TEST_REPORTS_README = ROOT / "docs" / "build-test-reports" / "README.md"
class BuildTestExecutorAgentConfigTests(unittest.TestCase):
def test_build_test_executor_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "build-test-executor-agent")
self.assertIn("C++/MSVC/CMake/CTest validation", data["description"])
self.assertEqual(data["sandbox_mode"], "workspace-write")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_build_test_executor_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not edit source code.",
"Do not edit tests.",
"Do not edit CMake.",
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"Do not approve release readiness.",
):
self.assertIn(required_text, instructions)
def test_build_test_executor_agent_instructions_define_validation_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"python scripts/validate_workspace.py",
"HARNESS_VALIDATION_COMMANDS",
"msvc-debug",
"CMake/MSVC x64 Debug",
"ctest --test-dir",
"--output-on-failure",
"-C Debug",
):
self.assertIn(required_text, instructions)
def test_build_test_executor_agent_instructions_define_output_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Execution Environment",
"Command Log Summary",
"Validation Results",
"Failure Classification",
"Failed Test Inventory",
"Handoff Recommendation",
"No-Change Assertion",
):
self.assertIn(required_text, instructions)
def test_build_test_report_guide_defines_template_and_status_values(self):
guide = BUILD_TEST_REPORTS_README.read_text(encoding="utf-8")
for required_text in (
"docs/build-test-reports/<feature-id>-build-test.md",
"Execution Environment",
"Command Log Summary",
"Validation Results",
"Failure Classification",
"Failed Test Inventory",
"Handoff Recommendation",
"No-Change Assertion",
"pass-for-reference-verification",
"needs-correction",
"needs-environment-fix",
):
self.assertIn(required_text, guide)
if __name__ == "__main__":
unittest.main()
-102
View File
@@ -1,102 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "coordinator-agent.toml"
COORDINATION_README = ROOT / "docs" / "coordination" / "README.md"
class CoordinatorAgentConfigTests(unittest.TestCase):
def test_coordinator_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "coordinator-agent")
self.assertIn("workflow state", data["description"])
self.assertEqual(data["sandbox_mode"], "workspace-write")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_coordinator_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not implement code.",
"Do not edit source code.",
"Do not edit tests.",
"Do not edit CMake.",
"Do not run build/test validation.",
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"Do not automatically spawn subagents.",
"Do not approve release readiness independently.",
):
self.assertIn(required_text, instructions)
def test_coordinator_agent_instructions_define_workflow_and_status_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"INTAKE -> STATE AUDIT -> GATE DECISION -> HANDOFF PACKAGE -> STATUS REPORT",
"Requirement Agent",
"Research Agent",
"Formulation Agent",
"Numerical Review Agent",
"I/O Definition Agent",
"Reference Model Agent",
"Implementation Planning Agent",
"Implementation Agent",
"Build/Test Executor Agent",
"Correction Agent",
"Reference Verification Agent",
"Physics Evaluation Agent",
"Release Agent",
"ready-for-implementation",
"pass-for-reference-verification",
"pass-for-physics-evaluation",
"pass-for-release-agent",
"ready-for-release",
"completed",
):
self.assertIn(required_text, instructions)
def test_coordinator_agent_instructions_define_output_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Gate Evidence Inventory",
"Decision Log",
"Next Agent Handoff",
"Traceability Snapshot",
"Risk and Blocker Register",
"Rework Loop Control",
"No-Change Assertion",
):
self.assertIn(required_text, instructions)
def test_coordination_guide_defines_template_and_status_values(self):
guide = COORDINATION_README.read_text(encoding="utf-8")
for required_text in (
"docs/coordination/<feature-id>-coordination.md",
"Gate Evidence Inventory",
"Decision Log",
"Next Agent Handoff",
"Traceability Snapshot",
"Risk and Blocker Register",
"Rework Loop Control",
"No-Change Assertion",
"needs-user-decision",
"blocked",
"python scripts/validate_workspace.py",
):
self.assertIn(required_text, guide)
if __name__ == "__main__":
unittest.main()
-90
View File
@@ -1,90 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "correction-agent.toml"
CORRECTIONS_README = ROOT / "docs" / "corrections" / "README.md"
class CorrectionAgentConfigTests(unittest.TestCase):
def test_correction_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "correction-agent")
self.assertIn("C++/MSVC/CMake/CTest fixes", data["description"])
self.assertEqual(data["sandbox_mode"], "workspace-write")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_correction_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not change requirements",
"Do not change formulations",
"Do not change I/O contracts",
"Do not change reference artifacts",
"Do not change tolerance policies",
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"Do not approve release readiness.",
):
self.assertIn(required_text, instructions)
def test_correction_agent_instructions_define_triage_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"TRIAGE -> MINIMAL FIX -> VERIFY -> REPORT",
"configure",
"compile",
"link",
"test",
"reference-comparison",
"harness",
"environment",
"upstream-contract",
):
self.assertIn(required_text, instructions)
def test_correction_agent_instructions_define_output_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Failure Triage",
"Root Cause Summary",
"Correction Scope",
"Verification Evidence",
"Traceability",
"Handoff Recommendation",
"Stop Condition",
):
self.assertIn(required_text, instructions)
def test_correction_report_guide_defines_template_and_status_values(self):
guide = CORRECTIONS_README.read_text(encoding="utf-8")
for required_text in (
"docs/corrections/<feature-id>-correction.md",
"Failure Triage",
"Root Cause Summary",
"Correction Scope",
"Verification Evidence",
"Traceability",
"Handoff Recommendation",
"Stop Condition",
"corrected-for-build-test",
"needs-upstream-decision",
"python scripts/validate_workspace.py",
):
self.assertIn(required_text, guide)
if __name__ == "__main__":
unittest.main()
-286
View File
@@ -1,286 +0,0 @@
import importlib.util
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
def load_execute():
module_path = Path(__file__).resolve().parent / "execute.py"
spec = importlib.util.spec_from_file_location("execute", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def write_phase(root: Path, phase_dir: str = "0-mvp", phase_name: str = "0-mvp", steps=None):
phase_path = root / "phases" / phase_dir
phase_path.mkdir(parents=True)
if steps is None:
steps = [
{
"step": 1,
"name": "Docs",
"status": "pending",
"summary": "",
"allowed_paths": ["docs/*.md"],
}
]
(phase_path / "index.json").write_text(
json.dumps({"project": "FESA", "phase": phase_name, "steps": steps}, indent=2),
encoding="utf-8",
)
(phase_path / "step1.md").write_text("# Step 1\n", encoding="utf-8")
return phase_path
def make_executor(execute, root: Path, phase_dir: str = "0-mvp"):
with patch.object(execute, "ROOT", root):
return execute.StepExecutor(phase_dir)
class ExecuteRunnerSafetyTests(unittest.TestCase):
def test_scaffold_loads_execute_module(self):
execute = load_execute()
self.assertTrue(hasattr(execute, "StepExecutor"))
def test_branch_name_uses_codex_prefix_and_sanitized_phase(self):
execute = load_execute()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
write_phase(root, phase_name="linear truss/1d")
executor = make_executor(execute, root)
self.assertEqual(executor._branch_name(), "codex/linear-truss-1d")
def test_finalize_push_uses_codex_branch_name(self):
execute = load_execute()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
write_phase(root, phase_name="0-mvp")
executor = make_executor(execute, root)
executor._auto_push = True
calls = []
def fake_git(*args):
calls.append(args)
if args == ("diff", "--cached", "--quiet"):
return subprocess.CompletedProcess(args, 0, "", "")
return subprocess.CompletedProcess(args, 0, "", "")
with patch.object(executor, "_run_git", side_effect=fake_git):
with patch.object(executor, "_validate_before_commit", create=True):
with patch("builtins.print"):
executor._finalize()
self.assertIn(("push", "-u", "origin", "codex/0-mvp"), calls)
def test_finalize_stages_only_phase_indexes(self):
execute = load_execute()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
write_phase(root)
(root / "phases" / "index.json").write_text('{"phases":[]}', encoding="utf-8")
executor = make_executor(execute, root)
calls = []
def fake_git(*args):
calls.append(args)
if args == ("diff", "--cached", "--quiet"):
return subprocess.CompletedProcess(args, 1, "", "")
return subprocess.CompletedProcess(args, 0, "", "")
with patch.object(executor, "_run_git", side_effect=fake_git):
with patch.object(executor, "_validate_before_commit"):
with patch("builtins.print"):
executor._finalize()
self.assertNotIn(("add", "-A"), calls)
self.assertIn(("add", "--", "phases/0-mvp/index.json", "phases/index.json"), calls)
def test_assert_clean_worktree_exits_when_git_status_has_changes(self):
execute = load_execute()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
write_phase(root)
executor = make_executor(execute, root)
with patch.object(
executor,
"_run_git",
return_value=subprocess.CompletedProcess([], 0, " M AGENTS.md\n?? scratch.txt\n", ""),
):
with patch("builtins.print"):
with self.assertRaises(SystemExit) as cm:
executor._assert_clean_worktree("before checkout")
self.assertEqual(cm.exception.code, 1)
def test_run_checks_clean_worktree_before_checkout(self):
execute = load_execute()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
write_phase(root)
executor = make_executor(execute, root)
calls = []
def record(name):
def inner(*args, **kwargs):
calls.append(name)
return inner
with patch.object(executor, "_assert_clean_worktree", side_effect=record("clean")):
with patch.object(executor, "_checkout_branch", side_effect=record("checkout")):
with patch.object(executor, "_print_header"):
with patch.object(executor, "_check_blockers"):
with patch.object(executor, "_load_guardrails", return_value=""):
with patch.object(executor, "_ensure_created_at"):
with patch.object(executor, "_execute_all_steps"):
with patch.object(executor, "_finalize"):
executor.run()
self.assertLess(calls.index("clean"), calls.index("checkout"))
def test_step_allowlist_accepts_exact_prefix_and_glob_paths(self):
execute = load_execute()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
write_phase(root)
executor = make_executor(execute, root)
patterns = ["AGENTS.md", "docs/", "scripts/*.py"]
self.assertTrue(executor._path_allowed("AGENTS.md", patterns))
self.assertTrue(executor._path_allowed("docs/PRD.md", patterns))
self.assertTrue(executor._path_allowed("scripts/execute.py", patterns))
self.assertFalse(executor._path_allowed(".codex/hooks.json", patterns))
def test_step_without_allowed_paths_is_rejected_before_codex_invocation(self):
execute = load_execute()
steps = [{"step": 1, "name": "Unsafe", "status": "pending", "summary": ""}]
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
write_phase(root, steps=steps)
executor = make_executor(execute, root)
with patch("builtins.print"):
with self.assertRaises(SystemExit) as cm:
executor._validate_step_allowlist(steps[0])
self.assertEqual(cm.exception.code, 1)
def test_classify_step_changes_splits_allowed_housekeeping_and_disallowed_paths(self):
execute = load_execute()
step = {
"step": 1,
"name": "Docs",
"status": "completed",
"summary": "",
"allowed_paths": ["docs/*.md"],
}
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
write_phase(root)
executor = make_executor(execute, root)
changed = [
"docs/PRD.md",
"phases/0-mvp/index.json",
"phases/0-mvp/step1-output.json",
"scripts/execute.py",
]
allowed, housekeeping, disallowed = executor._classify_step_changes(1, step, changed)
self.assertEqual(allowed, ["docs/PRD.md"])
self.assertEqual(housekeeping, ["phases/0-mvp/index.json", "phases/0-mvp/step1-output.json"])
self.assertEqual(disallowed, ["scripts/execute.py"])
def test_commit_step_stages_only_explicit_allowed_and_housekeeping_paths(self):
execute = load_execute()
step = {
"step": 1,
"name": "Docs",
"status": "completed",
"summary": "",
"allowed_paths": ["docs/*.md"],
}
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
write_phase(root)
executor = make_executor(execute, root)
calls = []
def fake_git(*args):
calls.append(args)
if args in {
("diff", "--quiet", "--cached", "--"),
("diff", "--cached", "--quiet"),
}:
return subprocess.CompletedProcess(args, 1, "", "")
return subprocess.CompletedProcess(args, 0, "", "")
with patch.object(
executor,
"_changed_paths",
return_value=[
"docs/PRD.md",
"phases/0-mvp/index.json",
"phases/0-mvp/step1-output.json",
],
):
with patch.object(executor, "_run_git", side_effect=fake_git):
with patch.object(executor, "_validate_before_commit", create=True):
with patch("builtins.print"):
executor._commit_step(step, "Docs")
self.assertNotIn(("add", "-A"), calls)
self.assertIn(("add", "--", "docs/PRD.md"), calls)
self.assertIn(("add", "--", "phases/0-mvp/index.json", "phases/0-mvp/step1-output.json"), calls)
def test_validate_before_commit_runs_python_selftest_then_workspace_validation(self):
execute = load_execute()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
write_phase(root)
executor = make_executor(execute, root)
commands = []
def fake_run(cmd, **kwargs):
commands.append(cmd)
return subprocess.CompletedProcess(cmd, 0, "ok", "")
with patch.object(execute.subprocess, "run", side_effect=fake_run):
with patch("builtins.print"):
executor._validate_before_commit("feat(0-mvp): step 1")
self.assertEqual(
commands,
[
[sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py"],
[sys.executable, "scripts/validate_workspace.py"],
],
)
def test_validate_before_commit_exits_before_commit_when_validation_fails(self):
execute = load_execute()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
write_phase(root)
executor = make_executor(execute, root)
def fake_run(cmd, **kwargs):
return subprocess.CompletedProcess(cmd, 1, "bad", "failed")
with patch.object(execute.subprocess, "run", side_effect=fake_run):
with patch("builtins.print"):
with self.assertRaises(SystemExit) as cm:
executor._validate_before_commit("feat(0-mvp): step 1")
self.assertEqual(cm.exception.code, 1)
if __name__ == "__main__":
unittest.main()
-344
View File
@@ -1,344 +0,0 @@
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SKILLS_ROOT = ROOT / ".codex" / "skills"
COMMON_SECTIONS = (
"## Inputs",
"## Workflow",
"## Output Contract",
"## Boundaries",
"## Quality Gate",
"## Handoff",
)
ACTIVE_CONTRACT_FILES = (
ROOT / "AGENTS.md",
ROOT / "docs" / "ProjectInitialPlanNote.md",
ROOT / "docs" / "PRD.md",
ROOT / "docs" / "ARCHITECTURE.md",
ROOT / "docs" / "ADR.md",
ROOT / "docs" / "SOLVER_AGENT_DESIGN.md",
ROOT / "docs" / "SOLVER_SKILL_DESIGN.md",
ROOT / "docs" / "reference-models" / "README.md",
ROOT / "docs" / "reference-verifications" / "README.md",
ROOT / "docs" / "io-definitions" / "README.md",
ROOT / "docs" / "implementation-plans" / "README.md",
ROOT / "docs" / "physics-evaluations" / "README.md",
ROOT / "docs" / "requirements" / "README.md",
ROOT / ".codex" / "agents" / "reference-model-agent.toml",
ROOT / ".codex" / "agents" / "reference-verification-agent.toml",
ROOT / ".codex" / "agents" / "io-definition-agent.toml",
ROOT / ".codex" / "agents" / "implementation-planning-agent.toml",
ROOT / ".codex" / "agents" / "implementation-agent.toml",
ROOT / ".codex" / "agents" / "physics-evaluation-agent.toml",
ROOT / ".codex" / "agents" / "release-agent.toml",
ROOT / ".codex" / "agents" / "requirement-agent.toml",
ROOT / ".codex" / "agents" / "coordinator-agent.toml",
ROOT / ".codex" / "skills" / "fesa-reference-models" / "SKILL.md",
ROOT / ".codex" / "skills" / "fesa-reference-comparison" / "SKILL.md",
ROOT / ".codex" / "skills" / "fesa-io-contract" / "SKILL.md",
ROOT / ".codex" / "skills" / "fesa-physics-sanity" / "SKILL.md",
ROOT / ".codex" / "skills" / "fesa-cpp-msvc-tdd" / "SKILL.md",
ROOT / ".codex" / "skills" / "fesa-release-readiness" / "SKILL.md",
ROOT / ".codex" / "skills" / "fesa-requirements-baseline" / "SKILL.md",
)
STALE_REFERENCE_CONTRACT_PHRASES = (
"reference" ".h5",
"stored reference " "HDF5",
"reference " "HDF5 artifact",
"results.h5 and " "reference" ".h5",
"results.h5` and `reference" ".h5",
"derived from reference" ".h5",
"references/" "<feature-id>/<model-id>/",
)
SKILLS = {
"fesa-requirements-baseline": {
"description_terms": (
"Use when",
"FESA solver",
"requirements",
"acceptance criteria",
"verification matrix",
),
"body_terms": (
"docs/requirements/<feature-id>.md",
"Requirement Verification Matrix",
"shall",
"FESA-REQ-<FEATURE>-###",
"Verification Quantities",
"Tolerance Policy",
"Reference Artifact Requirements",
"Do not implement C++ code.",
),
},
"fesa-research-evidence": {
"description_terms": (
"Use when",
"FESA solver",
"research",
"FEM theory",
"benchmarks",
),
"body_terms": (
"docs/research/<feature-id>-research.md",
"Source Inventory",
"Source Reliability Tier",
"Candidate Benchmarks",
"Verification Relevance",
"Applicability Limits",
"Separate verified facts from inference.",
),
},
"fesa-formulation-spec": {
"description_terms": (
"Use when",
"FESA FEM",
"formulation",
"element equations",
"output recovery",
),
"body_terms": (
"docs/formulations/<feature-id>-formulation.md",
"Strong Form",
"Weak or Variational Form",
"Discretization",
"Kinematics",
"Element Equations",
"Jacobian",
"Output Recovery",
"Do not design C++ APIs.",
),
},
"fesa-numerical-review": {
"description_terms": (
"Use when",
"FESA FEM",
"numerical review",
"stability",
"implementation planning",
),
"body_terms": (
"docs/numerical-reviews/<feature-id>-review.md",
"pass-for-implementation-planning",
"rigid body modes",
"patch test",
"hourglass",
"locking",
"Jacobian",
"Do not edit formulations directly.",
),
},
"fesa-io-contract": {
"description_terms": (
"Use when",
"FESA solver",
"Abaqus .inp",
"HDF5",
"CSV",
"I/O",
),
"body_terms": (
"docs/io-definitions/<feature-id>-io.md",
"Abaqus Input Scope",
"Internal Model Contract",
"Output HDF5 Schema",
"FESA HDF5 to Reference CSV Comparison Schema",
"results.h5",
"reference/<model-id>/",
"*NODE",
"*ELEMENT",
"*MATERIAL",
"*BOUNDARY",
"*STEP",
"Do not implement parsers.",
),
},
"fesa-reference-models": {
"description_terms": (
"Use when",
"FESA",
"reference model",
"Abaqus input",
"CSV",
),
"body_terms": (
"docs/reference-models/<feature-id>-reference-models.md",
"reference/<model-id>/",
"model.inp",
"metadata.json",
"<model-id>_displacements.csv",
"<model-id>_reactions.csv",
"<model-id>_internalforces.csv",
"<model-id>_stresses.csv",
"Coverage Matrix",
"Do not generate or modify Abaqus reference CSV files.",
),
},
"fesa-cpp-msvc-tdd": {
"description_terms": (
"Use when",
"FESA solver",
"C++",
"MSVC",
"TDD",
),
"body_terms": (
"docs/implementation-plans/<feature-id>-implementation-plan.md",
"RED -> GREEN -> VERIFY",
"python -m unittest discover -s scripts -p \"test_*.py\"",
"python scripts/validate_workspace.py",
"ctest",
"configure | compile | link | test | reference-comparison",
"Do not change requirements.",
),
},
"fesa-reference-comparison": {
"description_terms": (
"Use when",
"FESA solver",
"HDF5",
"reference CSV",
"tolerance",
"comparison",
),
"body_terms": (
"docs/reference-verifications/<feature-id>-reference-verification.md",
"ARTIFACT CHECK -> COMPARE -> CLASSIFY -> REPORT",
"results.h5",
"Abaqus reference CSV",
"reference/<model-id>/",
"<model-id>_displacements.csv",
"<model-id>_reactions.csv",
"<model-id>_internalforces.csv",
"<model-id>_stresses.csv",
"max absolute error",
"max relative error",
"RMS error",
"missing rows",
"extra rows",
"pass-for-physics-evaluation",
"Do not change tolerance policies.",
),
},
"fesa-physics-sanity": {
"description_terms": (
"Use when",
"FESA solver",
"physical plausibility",
"equilibrium",
"physics",
),
"body_terms": (
"docs/physics-evaluations/<feature-id>-physics-evaluation.md",
"global equilibrium",
"reaction consistency",
"displacement direction",
"symmetry",
"element force balance",
"model coverage",
"pass-for-release-agent",
"Do not approve release readiness.",
),
},
"fesa-release-readiness": {
"description_terms": (
"Use when",
"FESA solver",
"release readiness",
"release notes",
"known limitations",
),
"body_terms": (
"docs/releases/<feature-id>-release.md",
"GATE AUDIT -> TRACEABILITY CHECK -> RELEASE DOCUMENTATION -> RELEASE VERDICT",
"ready-for-release",
"Known Limitations",
"Release Notes Draft",
"pass-for-reference-verification",
"pass-for-physics-evaluation",
"pass-for-release-agent",
"Do not publish, deploy, package, tag, commit, or externally release anything unless the user explicitly asks.",
),
},
}
def read_skill(skill_name):
return (SKILLS_ROOT / skill_name / "SKILL.md").read_text(encoding="utf-8")
def parse_frontmatter(text):
lines = text.splitlines()
if not lines or lines[0] != "---":
raise AssertionError("SKILL.md must start with YAML frontmatter")
fields = {}
for line in lines[1:]:
if line == "---":
return fields
key, sep, value = line.partition(":")
if not sep:
raise AssertionError(f"Invalid frontmatter line: {line}")
fields[key.strip()] = value.strip()
raise AssertionError("SKILL.md frontmatter must be closed")
class FesaSolverSkillTests(unittest.TestCase):
def test_all_solver_skill_files_exist_with_required_frontmatter(self):
for skill_name, spec in SKILLS.items():
with self.subTest(skill=skill_name):
skill_path = SKILLS_ROOT / skill_name / "SKILL.md"
self.assertTrue(skill_path.exists(), f"{skill_name} SKILL.md is missing")
fields = parse_frontmatter(read_skill(skill_name))
self.assertEqual(set(fields), {"name", "description"})
self.assertEqual(fields["name"], skill_name)
for term in spec["description_terms"]:
self.assertIn(term, fields["description"])
def test_all_solver_skills_define_common_contract_sections(self):
for skill_name in SKILLS:
with self.subTest(skill=skill_name):
body = read_skill(skill_name)
for section in COMMON_SECTIONS:
self.assertIn(section, body)
self.assertIn("AGENTS.md", body)
self.assertIn("docs/SOLVER_AGENT_DESIGN.md", body)
self.assertNotIn("docs/SOLVER_SKILL_DESIGN.md", body)
def test_solver_skills_define_skill_specific_contracts(self):
for skill_name, spec in SKILLS.items():
with self.subTest(skill=skill_name):
body = read_skill(skill_name)
for term in spec["body_terms"]:
self.assertIn(term, body)
def test_solver_skills_have_openai_ui_metadata(self):
for skill_name in SKILLS:
with self.subTest(skill=skill_name):
metadata = SKILLS_ROOT / skill_name / "agents" / "openai.yaml"
self.assertTrue(metadata.exists(), f"{skill_name} openai.yaml is missing")
text = metadata.read_text(encoding="utf-8")
self.assertIn("interface:", text)
self.assertIn("display_name:", text)
self.assertIn("short_description:", text)
self.assertIn("default_prompt:", text)
self.assertIn(f"${skill_name}", text)
def test_active_contracts_do_not_require_reference_hdf5(self):
for path in ACTIVE_CONTRACT_FILES:
with self.subTest(path=str(path.relative_to(ROOT))):
text = path.read_text(encoding="utf-8")
for stale_phrase in STALE_REFERENCE_CONTRACT_PHRASES:
self.assertNotIn(stale_phrase, text)
if __name__ == "__main__":
unittest.main()
-88
View File
@@ -1,88 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "formulation-agent.toml"
FORMULATIONS_README = ROOT / "docs" / "formulations" / "README.md"
class FormulationAgentConfigTests(unittest.TestCase):
def test_formulation_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "formulation-agent")
self.assertIn("FEM formulation", data["description"])
self.assertEqual(data["sandbox_mode"], "read-only")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_formulation_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not implement code.",
"Do not design C++ APIs",
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"Do not approve release readiness.",
"docs/SOLVER_AGENT_DESIGN.md",
"docs/requirements/<feature-id>.md",
"docs/research/<feature-id>-research.md",
):
self.assertIn(required_text, instructions)
def test_formulation_agent_instructions_define_output_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Strong Form and Boundary Conditions",
"Weak or Variational Form",
"Discretization",
"Kinematics",
"Constitutive Contract",
"Element Equations",
"Mapping and Numerical Integration",
"Output Recovery",
"Numerical Risks",
"Downstream Handoff",
):
self.assertIn(required_text, instructions)
def test_formulation_agent_instructions_define_numerical_risk_policy(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"rigid body modes",
"patch test",
"hourglass",
"locking",
"Jacobian",
):
self.assertIn(required_text, instructions)
def test_formulation_document_guide_defines_output_contract(self):
guide = FORMULATIONS_README.read_text(encoding="utf-8")
for required_text in (
"Strong Form and Boundary Conditions",
"Weak or Variational Form",
"Discretization",
"Kinematics",
"Constitutive Contract",
"Element Equations",
"Mapping and Numerical Integration",
"Output Recovery",
"Numerical Risks",
"Downstream Handoff",
):
self.assertIn(required_text, guide)
if __name__ == "__main__":
unittest.main()
@@ -1,81 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "implementation-agent.toml"
class ImplementationAgentConfigTests(unittest.TestCase):
def test_implementation_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "implementation-agent")
self.assertIn("C++17/MSVC", data["description"])
self.assertEqual(data["sandbox_mode"], "workspace-write")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_implementation_agent_instructions_define_tdd_execution_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Write tests first",
"verify failure",
"minimum code",
"C++17",
"MSVC",
"CMake",
"CTest",
"RED -> GREEN -> VERIFY",
):
self.assertIn(required_text, instructions)
def test_implementation_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"Do not approve release readiness.",
"Do not change requirements",
"Do not change formulations",
"Do not change I/O contracts",
"Do not change reference artifacts",
"Do not produce the final reference verification report.",
):
self.assertIn(required_text, instructions)
def test_implementation_agent_instructions_define_output_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Test Evidence",
"Code Changes",
"Validation Evidence",
"Traceability",
"Downstream Handoff",
"Build/Test Executor Agent",
"Correction Agent",
"Reference Verification Agent",
):
self.assertIn(required_text, instructions)
def test_implementation_agent_instructions_define_validation_commands(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"python -m unittest discover -s scripts -p \"test_*.py\"",
"python scripts/validate_workspace.py",
"ctest -C Debug",
):
self.assertIn(required_text, instructions)
if __name__ == "__main__":
unittest.main()
@@ -1,87 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "implementation-planning-agent.toml"
IMPLEMENTATION_PLANS_README = ROOT / "docs" / "implementation-plans" / "README.md"
class ImplementationPlanningAgentConfigTests(unittest.TestCase):
def test_implementation_planning_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "implementation-planning-agent")
self.assertIn("TDD-first C++/MSVC implementation plans", data["description"])
self.assertEqual(data["sandbox_mode"], "read-only")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_implementation_planning_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not implement code.",
"Do not write tests.",
"Do not edit CMake.",
"Do not run CMake/CTest.",
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"Do not compare solver results.",
"Do not approve release readiness.",
):
self.assertIn(required_text, instructions)
def test_implementation_planning_agent_instructions_define_tdd_msvc_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"C++17",
"MSVC",
"CMake",
"CTest",
"TDD",
"failing unit tests first",
"reference comparison tests",
):
self.assertIn(required_text, instructions)
def test_implementation_planning_agent_instructions_define_output_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Readiness Check",
"Work Breakdown",
"TDD Test Plan",
"CMake/CTest Plan",
"Acceptance Traceability Matrix",
"Validation Commands",
"Downstream Handoff",
):
self.assertIn(required_text, instructions)
def test_implementation_planning_document_guide_defines_output_contract(self):
guide = IMPLEMENTATION_PLANS_README.read_text(encoding="utf-8")
for required_text in (
"docs/implementation-plans/<feature-id>-implementation-plan.md",
"Readiness Check",
"Work Breakdown",
"TDD Test Plan",
"CMake/CTest Plan",
"Acceptance Traceability Matrix",
"Validation Commands",
"Downstream Handoff",
"python scripts/validate_workspace.py",
"ctest -C Debug",
):
self.assertIn(required_text, guide)
if __name__ == "__main__":
unittest.main()
-101
View File
@@ -1,101 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "io-definition-agent.toml"
IO_DEFINITIONS_README = ROOT / "docs" / "io-definitions" / "README.md"
class IoDefinitionAgentConfigTests(unittest.TestCase):
def test_io_definition_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "io-definition-agent")
self.assertIn("Abaqus input-file subsets", data["description"])
self.assertEqual(data["sandbox_mode"], "read-only")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_io_definition_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not implement parsers.",
"Do not design C++ APIs",
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"Do not approve release readiness.",
"Do not claim full Abaqus compatibility",
):
self.assertIn(required_text, instructions)
def test_io_definition_agent_instructions_define_abaqus_input_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"FESA solver input files are Abaqus input files.",
"Abaqus input files use keyword lines, data lines, and comment lines.",
"Model data and history data",
"supported Abaqus keyword subset",
"HDF5 result schema",
"reference CSV comparison row schema",
):
self.assertIn(required_text, instructions)
def test_io_definition_agent_instructions_define_output_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Abaqus Input Scope",
"Syntax Policy",
"Model Data Mapping",
"History Data Mapping",
"Internal Model Contract",
"Output HDF5 Schema",
"FESA HDF5 to Reference CSV Comparison Schema",
"Validation Rules",
"Downstream Handoff",
):
self.assertIn(required_text, instructions)
def test_io_definition_agent_instructions_define_keyword_policy(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"*NODE",
"*ELEMENT",
"*MATERIAL",
"*BOUNDARY",
"*STEP",
"*OUTPUT",
"*NODE OUTPUT",
"*ELEMENT OUTPUT",
):
self.assertIn(required_text, instructions)
def test_io_definition_document_guide_defines_output_contract(self):
guide = IO_DEFINITIONS_README.read_text(encoding="utf-8")
for required_text in (
"Abaqus Input Scope",
"Syntax Policy",
"Model Data Mapping",
"History Data Mapping",
"Internal Model Contract",
"Output HDF5 Schema",
"FESA HDF5 to Reference CSV Comparison Schema",
"Validation Rules",
"Downstream Handoff",
"FESA 솔버의 입력 파일은 Abaqus input file이다.",
):
self.assertIn(required_text, guide)
if __name__ == "__main__":
unittest.main()
@@ -1,86 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "numerical-review-agent.toml"
NUMERICAL_REVIEWS_README = ROOT / "docs" / "numerical-reviews" / "README.md"
class NumericalReviewAgentConfigTests(unittest.TestCase):
def test_numerical_review_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "numerical-review-agent")
self.assertIn("numerical correctness", data["description"])
self.assertEqual(data["sandbox_mode"], "read-only")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_numerical_review_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not implement code.",
"Do not edit formulations directly.",
"Do not design C++ APIs",
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"Do not approve release readiness.",
"docs/SOLVER_AGENT_DESIGN.md",
"docs/formulations/<feature-id>-formulation.md",
):
self.assertIn(required_text, instructions)
def test_numerical_review_agent_instructions_define_output_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Review Verdict",
"Critical Findings",
"Numerical Risk Assessment",
"Consistency Checks",
"Verification Readiness",
"Required Revisions",
"Downstream Handoff",
):
self.assertIn(required_text, instructions)
def test_numerical_review_agent_instructions_define_risk_policy(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"rigid body modes",
"patch test",
"symmetry",
"positive definiteness",
"hourglass",
"locking",
"singular Jacobian",
"conditioning",
):
self.assertIn(required_text, instructions)
def test_numerical_review_document_guide_defines_output_contract(self):
guide = NUMERICAL_REVIEWS_README.read_text(encoding="utf-8")
for required_text in (
"Review Verdict",
"Critical Findings",
"Numerical Risk Assessment",
"Consistency Checks",
"Verification Readiness",
"Required Revisions",
"Downstream Handoff",
"pass-for-implementation-planning",
):
self.assertIn(required_text, guide)
if __name__ == "__main__":
unittest.main()
@@ -1,89 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "physics-evaluation-agent.toml"
PHYSICS_EVALUATIONS_README = ROOT / "docs" / "physics-evaluations" / "README.md"
class PhysicsEvaluationAgentConfigTests(unittest.TestCase):
def test_physics_evaluation_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "physics-evaluation-agent")
self.assertIn("physical plausibility", data["description"])
self.assertEqual(data["sandbox_mode"], "workspace-write")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_physics_evaluation_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not edit source code.",
"Do not edit tests.",
"Do not edit CMake.",
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"Do not change tolerances.",
"Do not approve release readiness.",
):
self.assertIn(required_text, instructions)
def test_physics_evaluation_agent_instructions_define_physics_checks(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"global equilibrium",
"reaction consistency",
"displacement direction",
"symmetry",
"element force balance",
"stress/strain",
"rigid body mode",
"energy/residual",
):
self.assertIn(required_text, instructions)
def test_physics_evaluation_agent_instructions_define_output_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"HDF5",
"Abaqus reference CSV files",
"Input Evidence",
"Physics Checks",
"Failure Classification",
"Evaluation Verdict",
"Handoff Recommendation",
"No-Change Assertion",
):
self.assertIn(required_text, instructions)
def test_physics_evaluation_report_guide_defines_template_and_status_values(self):
guide = PHYSICS_EVALUATIONS_README.read_text(encoding="utf-8")
for required_text in (
"docs/physics-evaluations/<feature-id>-physics-evaluation.md",
"Input Evidence",
"Physics Checks",
"Failure Classification",
"Evaluation Verdict",
"Handoff Recommendation",
"No-Change Assertion",
"pass-for-release-agent",
"needs-correction",
"needs-reference-model",
"needs-upstream-decision",
):
self.assertIn(required_text, guide)
if __name__ == "__main__":
unittest.main()
-41
View File
@@ -1,41 +0,0 @@
import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path
def load_pre_commit_checks():
module_path = Path(__file__).resolve().parent.parent / ".codex" / "hooks" / "pre_commit_checks.py"
spec = importlib.util.spec_from_file_location("pre_commit_checks", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class PreCommitChecksTests(unittest.TestCase):
def test_git_commit_runs_python_self_tests_and_workspace_validation(self):
pre_commit_checks = load_pre_commit_checks()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
commands = pre_commit_checks._build_pre_commit_commands(root)
self.assertEqual(
commands,
[
[sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py"],
[sys.executable, "scripts/validate_workspace.py"],
],
)
self.assertFalse(any("npm" in part.lower() for command in commands for part in command))
def test_only_git_commit_commands_trigger_checks(self):
pre_commit_checks = load_pre_commit_checks()
self.assertTrue(pre_commit_checks._is_git_commit('git commit -m "change"'))
self.assertTrue(pre_commit_checks._is_git_commit('git -c core.editor=true commit -m "change"'))
self.assertFalse(pre_commit_checks._is_git_commit("git status --short"))
self.assertFalse(pre_commit_checks._is_git_commit("echo git commit"))
if __name__ == "__main__":
unittest.main()
@@ -1,90 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "reference-model-agent.toml"
REFERENCE_MODELS_README = ROOT / "docs" / "reference-models" / "README.md"
class ReferenceModelAgentConfigTests(unittest.TestCase):
def test_reference_model_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "reference-model-agent")
self.assertIn("reference model packages", data["description"])
self.assertEqual(data["sandbox_mode"], "read-only")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_reference_model_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not implement code.",
"Do not implement parsers.",
"Do not design C++ APIs",
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"Do not compare solver results.",
"Do not approve release readiness.",
):
self.assertIn(required_text, instructions)
def test_reference_model_agent_instructions_define_reference_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"FESA reference models use Abaqus input files.",
"reference/<model-id>/",
"model.inp",
"metadata.json",
"<model-id>_displacements.csv",
"<model-id>_reactions.csv",
"<model-id>_internalforces.csv",
"<model-id>_stresses.csv",
):
self.assertIn(required_text, instructions)
self.assertNotIn("reference" ".h5", instructions)
self.assertNotIn("references/" "<feature-id>/<model-id>/", instructions)
def test_reference_model_agent_instructions_define_output_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Reference Strategy",
"Model Inventory",
"Abaqus Input Requirements",
"Artifact Bundle Contract",
"Metadata JSON Contract",
"Abaqus Reference CSV Requirements",
"Coverage Matrix",
"Downstream Handoff",
):
self.assertIn(required_text, instructions)
def test_reference_model_document_guide_defines_output_contract(self):
guide = REFERENCE_MODELS_README.read_text(encoding="utf-8")
for required_text in (
"Reference Strategy",
"Model Inventory",
"Abaqus Input Requirements",
"Artifact Bundle Contract",
"Metadata JSON Contract",
"Abaqus Reference CSV Requirements",
"Coverage Matrix",
"Downstream Handoff",
"reference/<model-id>/",
):
self.assertIn(required_text, guide)
if __name__ == "__main__":
unittest.main()
@@ -1,91 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "reference-verification-agent.toml"
REFERENCE_VERIFICATIONS_README = ROOT / "docs" / "reference-verifications" / "README.md"
class ReferenceVerificationAgentConfigTests(unittest.TestCase):
def test_reference_verification_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "reference-verification-agent")
self.assertIn("HDF5", data["description"])
self.assertIn("Abaqus reference CSV", data["description"])
self.assertEqual(data["sandbox_mode"], "workspace-write")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_reference_verification_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not edit source code.",
"Do not edit tests.",
"Do not edit CMake.",
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"Do not approve release readiness.",
"Do not change tolerance policies.",
):
self.assertIn(required_text, instructions)
def test_reference_verification_agent_instructions_define_artifact_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"results.h5",
"Abaqus reference CSV",
"reference/<model-id>/",
"<model-id>_displacements.csv",
"<model-id>_reactions.csv",
"<model-id>_internalforces.csv",
"<model-id>_stresses.csv",
"metadata.json",
):
self.assertIn(required_text, instructions)
self.assertNotIn("reference" ".h5", instructions)
self.assertNotIn("stored reference " "HDF5", instructions)
def test_reference_verification_agent_instructions_define_output_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Artifact Inventory",
"Comparison Contract",
"Quantity Results",
"Failure Classification",
"Handoff Recommendation",
"No-Change Assertion",
):
self.assertIn(required_text, instructions)
def test_reference_verification_report_guide_defines_template_and_status_values(self):
guide = REFERENCE_VERIFICATIONS_README.read_text(encoding="utf-8")
for required_text in (
"docs/reference-verifications/<feature-id>-reference-verification.md",
"Artifact Inventory",
"Comparison Contract",
"Quantity Results",
"Failure Classification",
"Handoff Recommendation",
"No-Change Assertion",
"pass-for-physics-evaluation",
"needs-correction",
"needs-reference-artifacts",
"needs-upstream-decision",
):
self.assertIn(required_text, guide)
if __name__ == "__main__":
unittest.main()
-92
View File
@@ -1,92 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "release-agent.toml"
RELEASES_README = ROOT / "docs" / "releases" / "README.md"
class ReleaseAgentConfigTests(unittest.TestCase):
def test_release_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "release-agent")
self.assertIn("release readiness", data["description"])
self.assertEqual(data["sandbox_mode"], "workspace-write")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_release_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not implement code.",
"Do not edit source code.",
"Do not edit tests.",
"Do not edit CMake.",
"Do not change requirements",
"Do not change formulations",
"Do not change I/O contracts",
"Do not change reference artifacts",
"Do not change tolerance policies",
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"Do not override failed or missing upstream gates.",
):
self.assertIn(required_text, instructions)
def test_release_agent_instructions_define_gate_and_status_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"GATE AUDIT -> TRACEABILITY CHECK -> RELEASE DOCUMENTATION -> RELEASE VERDICT",
"pass-for-release-agent",
"pass-for-physics-evaluation",
"pass-for-reference-verification",
"ready-for-release",
"known limitations",
):
self.assertIn(required_text, instructions)
def test_release_agent_instructions_define_output_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Gate Evidence Inventory",
"Acceptance Traceability",
"Validation Evidence",
"Known Limitations",
"Release Notes Draft",
"Release Verdict",
"No-Change Assertion",
):
self.assertIn(required_text, instructions)
def test_release_report_guide_defines_template_and_status_values(self):
guide = RELEASES_README.read_text(encoding="utf-8")
for required_text in (
"docs/releases/<feature-id>-release.md",
"Gate Evidence Inventory",
"Acceptance Traceability",
"Validation Evidence",
"Known Limitations",
"Release Notes Draft",
"Release Verdict",
"No-Change Assertion",
"ready-for-release",
"needs-documentation",
"needs-upstream-decision",
"python scripts/validate_workspace.py",
):
self.assertIn(required_text, guide)
if __name__ == "__main__":
unittest.main()
-55
View File
@@ -1,55 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "requirement-agent.toml"
REQUIREMENTS_README = ROOT / "docs" / "requirements" / "README.md"
class RequirementAgentConfigTests(unittest.TestCase):
def test_requirement_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "requirement-agent")
self.assertIn("verifiable requirements", data["description"])
self.assertEqual(data["sandbox_mode"], "read-only")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_requirement_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not implement code.",
"Do not write finite element formulations.",
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"Requirement Verification Matrix",
"docs/SOLVER_AGENT_DESIGN.md",
"reference/<model-id>/",
):
self.assertIn(required_text, instructions)
def test_requirement_document_guide_defines_output_contract(self):
guide = REQUIREMENTS_README.read_text(encoding="utf-8")
for required_text in (
"feature_id",
"Verification Quantities",
"Tolerance Policy",
"Reference Artifact Requirements",
"Requirement Verification Matrix",
"Downstream Handoff",
"FESA-REQ-<FEATURE>-001",
):
self.assertIn(required_text, guide)
if __name__ == "__main__":
unittest.main()
-79
View File
@@ -1,79 +0,0 @@
import unittest
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib
ROOT = Path(__file__).resolve().parents[1]
AGENT_PATH = ROOT / ".codex" / "agents" / "research-agent.toml"
RESEARCH_README = ROOT / "docs" / "research" / "README.md"
class ResearchAgentConfigTests(unittest.TestCase):
def test_research_agent_toml_has_required_codex_fields(self):
data = tomllib.loads(AGENT_PATH.read_text(encoding="utf-8"))
self.assertEqual(data["name"], "research-agent")
self.assertIn("FEM theory", data["description"])
self.assertEqual(data["sandbox_mode"], "read-only")
self.assertEqual(data["model_reasoning_effort"], "extra high")
self.assertIn("developer_instructions", data)
def test_research_agent_instructions_enforce_boundaries(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Do not implement code.",
"Do not finalize FEM formulations.",
"Do not run Abaqus, Nastran, or any reference solver.",
"Do not generate or modify Abaqus reference CSV files.",
"docs/SOLVER_AGENT_DESIGN.md",
"docs/requirements/<feature-id>.md",
"Separate verified facts from inference.",
):
self.assertIn(required_text, instructions)
def test_research_agent_instructions_define_output_contract(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"Source Inventory",
"Candidate Benchmarks",
"Verification Relevance",
"Applicability Limits",
"Downstream Handoff",
):
self.assertIn(required_text, instructions)
def test_research_agent_instructions_define_source_policy(self):
instructions = AGENT_PATH.read_text(encoding="utf-8")
for required_text in (
"ASME V&V 10",
"Abaqus Verification Guide",
"Abaqus Benchmarks Guide",
"NAFEMS benchmarks",
"NASA FEMCI",
"MMS and MES papers",
):
self.assertIn(required_text, instructions)
def test_research_document_guide_defines_output_contract(self):
guide = RESEARCH_README.read_text(encoding="utf-8")
for required_text in (
"Source Reliability Tier",
"Source Inventory",
"Candidate Benchmarks",
"Verification Relevance",
"Applicability Limits",
"Downstream Handoff",
):
self.assertIn(required_text, guide)
if __name__ == "__main__":
unittest.main()
-61
View File
@@ -1,61 +0,0 @@
import importlib.util
import tempfile
import unittest
from pathlib import Path
def load_tdd_guard():
module_path = Path(__file__).resolve().parent.parent / ".codex" / "hooks" / "tdd-guard.py"
spec = importlib.util.spec_from_file_location("tdd_guard", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class CppTddGuardTests(unittest.TestCase):
def test_cpp_production_file_without_related_test_is_blocked(self):
tdd_guard = load_tdd_guard()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
source = root / "include" / "fesa" / "Core" / "DofManager.hpp"
source.parent.mkdir(parents=True)
source.write_text("#pragma once\n", encoding="utf-8")
self.assertEqual(tdd_guard._guarded_paths([str(source)], root, root), ["DofManager"])
def test_cpp_production_file_with_module_test_is_allowed(self):
tdd_guard = load_tdd_guard()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
source = root / "include" / "fesa" / "Core" / "DofManager.hpp"
source.parent.mkdir(parents=True)
source.write_text("#pragma once\n", encoding="utf-8")
tests_dir = root / "tests"
tests_dir.mkdir()
(tests_dir / "test_core_module_includes.cpp").write_text("int main() { return 0; }\n", encoding="utf-8")
self.assertEqual(tdd_guard._guarded_paths([str(source)], root, root), [])
def test_cpp_production_file_with_basename_test_in_same_patch_is_allowed(self):
tdd_guard = load_tdd_guard()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
source = root / "src" / "Math" / "DenseMatrix.cpp"
source.parent.mkdir(parents=True)
source.write_text("void f() {}\n", encoding="utf-8")
test_path = root / "tests" / "test_dense_matrix.cpp"
self.assertEqual(tdd_guard._guarded_paths([str(source), str(test_path)], root, root), [])
def test_cmake_and_docs_are_exempt(self):
tdd_guard = load_tdd_guard()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
self.assertEqual(
tdd_guard._guarded_paths(["CMakeLists.txt", "docs/ARCHITECTURE.md", "cmake/toolchain.cmake"], root, root),
[],
)
if __name__ == "__main__":
unittest.main()
-94
View File
@@ -1,94 +0,0 @@
import importlib.util
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
def load_validate_workspace():
module_path = Path(__file__).resolve().parent / "validate_workspace.py"
spec = importlib.util.spec_from_file_location("validate_workspace", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class ValidateWorkspaceTests(unittest.TestCase):
def test_env_commands_override_cmake_detection(self):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8")
with patch.dict(os.environ, {"HARNESS_VALIDATION_COMMANDS": "echo first\n echo second \n"}, clear=True):
self.assertEqual(validate_workspace.discover_commands(root), ["echo first", "echo second"])
def test_msvc_debug_cmake_commands_are_default_for_cmake_project(self):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8")
build_dir = root / "build" / "msvc-debug"
with patch.dict(os.environ, {}, clear=True):
self.assertEqual(
validate_workspace.discover_commands(root),
[
f'cmake -S "{root}" -B "{build_dir}" -G "Visual Studio 17 2022" -A x64',
f'cmake --build "{build_dir}" --config Debug',
f'ctest --test-dir "{build_dir}" --output-on-failure -C Debug',
],
)
def test_msvc_debug_configure_preset_is_preferred_when_present(self):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8")
(root / "CMakePresets.json").write_text(
"""
{
"version": 3,
"configurePresets": [
{
"name": "msvc-debug",
"generator": "Visual Studio 17 2022",
"binaryDir": "${sourceDir}/out/msvc-debug"
}
]
}
""",
encoding="utf-8",
)
with patch.dict(os.environ, {}, clear=True):
self.assertEqual(
validate_workspace.discover_commands(root),
[
"cmake --preset msvc-debug",
f'cmake --build "{root / "out" / "msvc-debug"}" --config Debug',
f'ctest --test-dir "{root / "out" / "msvc-debug"}" --output-on-failure -C Debug',
],
)
def test_no_cmake_project_has_no_validation_commands(self):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
with patch.dict(os.environ, {}, clear=True):
self.assertEqual(validate_workspace.discover_commands(root), [])
def test_common_cmake_install_path_is_prepended_when_cmake_is_not_on_path(self):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
common_bin = Path(tmp) / "CMake" / "bin"
common_bin.mkdir(parents=True)
(common_bin / "cmake.exe").write_text("", encoding="utf-8")
(common_bin / "ctest.exe").write_text("", encoding="utf-8")
with patch.object(validate_workspace, "COMMON_CMAKE_BIN", common_bin):
with patch.object(validate_workspace.shutil, "which", return_value=None):
env = validate_workspace.validation_environment({"PATH": "C:\\Windows\\System32"})
self.assertTrue(env["PATH"].startswith(str(common_bin)))
if __name__ == "__main__":
unittest.main()
-151
View File
@@ -1,151 +0,0 @@
#!/usr/bin/env python3
"""Run C++/MSVC Harness validation commands."""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
DEFAULT_GENERATOR = "Visual Studio 17 2022"
DEFAULT_PLATFORM = "x64"
DEFAULT_CONFIG = "Debug"
DEFAULT_BUILD_DIR = "build/msvc-debug"
PRESET_NAME = "msvc-debug"
COMMON_CMAKE_BIN = Path(r"C:\Program Files\CMake\bin")
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 _cmake_config() -> tuple[str, str, str, Path]:
generator = os.environ.get("HARNESS_CMAKE_GENERATOR", DEFAULT_GENERATOR)
platform = os.environ.get("HARNESS_CMAKE_PLATFORM", DEFAULT_PLATFORM)
config = os.environ.get("HARNESS_CMAKE_CONFIG", DEFAULT_CONFIG)
build_dir = Path(os.environ.get("HARNESS_BUILD_DIR", DEFAULT_BUILD_DIR))
return generator, platform, config, build_dir
def _read_presets(root: Path) -> dict:
presets_file = root / "CMakePresets.json"
if not presets_file.exists():
return {}
try:
return json.loads(presets_file.read_text(encoding="utf-8"))
except json.JSONDecodeError:
return {}
def _preset_binary_dir(root: Path, preset: dict) -> Path:
binary_dir = str(preset.get("binaryDir") or DEFAULT_BUILD_DIR)
binary_dir = binary_dir.replace("${sourceDir}", str(root))
binary_dir = binary_dir.replace("$sourceDir", str(root))
path = Path(binary_dir)
return path if path.is_absolute() else root / path
def load_preset_commands(root: Path) -> list[str]:
payload = _read_presets(root)
config = os.environ.get("HARNESS_CMAKE_CONFIG", DEFAULT_CONFIG)
for preset in payload.get("configurePresets", []):
if isinstance(preset, dict) and preset.get("name") == PRESET_NAME:
build_dir = _preset_binary_dir(root, preset)
return [
f"cmake --preset {PRESET_NAME}",
f'cmake --build "{build_dir}" --config {config}',
f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}',
]
return []
def load_cmake_commands(root: Path) -> list[str]:
if not (root / "CMakeLists.txt").exists():
return []
generator, platform, config, build_dir = _cmake_config()
if not build_dir.is_absolute():
build_dir = root / build_dir
return [
f'cmake -S "{root}" -B "{build_dir}" -G "{generator}" -A {platform}',
f'cmake --build "{build_dir}" --config {config}',
f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}',
]
def discover_commands(root: Path) -> list[str]:
env_commands = load_env_commands()
if env_commands:
return env_commands
preset_commands = load_preset_commands(root)
if preset_commands:
return preset_commands
return load_cmake_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",
env=validation_environment(os.environ),
)
def validation_environment(base_env: os._Environ | dict[str, str]) -> dict[str, str]:
env = dict(base_env)
if shutil.which("cmake") is not None:
return env
cmake_exe = COMMON_CMAKE_BIN / "cmake.exe"
if not cmake_exe.exists():
return env
current_path = env.get("PATH", "")
paths = [part for part in current_path.split(os.pathsep) if part]
common_bin_text = str(COMMON_CMAKE_BIN)
if not any(part.lower() == common_bin_text.lower() for part in paths):
env["PATH"] = common_bin_text + (os.pathsep + current_path if current_path else "")
return env
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
commands = discover_commands(root)
if not commands:
print("No C++ validation commands configured.")
print("Add CMakeLists.txt 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())