From 0adb9c26a9a504ac49311c81d4779bbf5a1ba73b Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Fri, 28 Aug 2026 00:34:51 +0900 Subject: [PATCH] add harness framework --- .../.agents/skills/harness/SKILL.md | 172 +++++ .../.agents/skills/review/SKILL.md | 41 ++ .../harness framework cpp/.codex/hooks.json | 32 + Codex/Coding/harness framework cpp/.gitignore | 4 + .../.harness/config.example.json | 31 + Codex/Coding/harness framework cpp/AGENTS.md | 40 ++ .../Coding/harness framework cpp/docs/ADR.md | 21 + .../docs/ARCHITECTURE.md | 24 + .../harness framework cpp/docs/HARNESS.md | 120 ++++ .../docs/HARNESS_WORKFLOW.md | 602 ++++++++++++++++++ .../Coding/harness framework cpp/docs/PRD.md | 21 + Codex/Coding/harness framework cpp/pytest.ini | 2 + .../harness framework cpp/scripts/execute.py | 481 ++++++++++++++ .../scripts/hooks/pre_tool_use.py | 118 ++++ .../scripts/hooks/stop_validation.py | 148 +++++ .../scripts/msvc_harness/__init__.py | 3 + .../scripts/msvc_harness/adapters/__init__.py | 5 + .../scripts/msvc_harness/adapters/base.py | 19 + .../scripts/msvc_harness/adapters/cmake.py | 99 +++ .../scripts/msvc_harness/adapters/msbuild.py | 60 ++ .../scripts/msvc_harness/config.py | 146 +++++ .../scripts/msvc_harness/discovery.py | 77 +++ .../scripts/msvc_harness/models.py | 100 +++ .../scripts/msvc_harness/process.py | 140 ++++ .../scripts/msvc_harness/tdd_policy.py | 88 +++ .../scripts/msvc_harness/toolchain.py | 169 +++++ .../1.0.0/.codex-plugin/plugin.json | 35 + .../agents-md-management v2/1.0.0/LICENSE | 202 ++++++ .../agents-md-management v2/1.0.0/README.md | 58 ++ .../1.0.0/skills/auditing-agents-md/SKILL.md | 106 +++ .../auditing-agents-md/agents/openai.yaml | 4 + .../references/quality-criteria.md | 87 +++ .../references/templates.md | 69 ++ .../references/update-guidelines.md | 62 ++ .../1.0.0/skills/revising-agents-md/SKILL.md | 108 ++++ .../revising-agents-md/agents/openai.yaml | 6 + 36 files changed, 3500 insertions(+) create mode 100644 Codex/Coding/harness framework cpp/.agents/skills/harness/SKILL.md create mode 100644 Codex/Coding/harness framework cpp/.agents/skills/review/SKILL.md create mode 100644 Codex/Coding/harness framework cpp/.codex/hooks.json create mode 100644 Codex/Coding/harness framework cpp/.gitignore create mode 100644 Codex/Coding/harness framework cpp/.harness/config.example.json create mode 100644 Codex/Coding/harness framework cpp/AGENTS.md create mode 100644 Codex/Coding/harness framework cpp/docs/ADR.md create mode 100644 Codex/Coding/harness framework cpp/docs/ARCHITECTURE.md create mode 100644 Codex/Coding/harness framework cpp/docs/HARNESS.md create mode 100644 Codex/Coding/harness framework cpp/docs/HARNESS_WORKFLOW.md create mode 100644 Codex/Coding/harness framework cpp/docs/PRD.md create mode 100644 Codex/Coding/harness framework cpp/pytest.ini create mode 100644 Codex/Coding/harness framework cpp/scripts/execute.py create mode 100644 Codex/Coding/harness framework cpp/scripts/hooks/pre_tool_use.py create mode 100644 Codex/Coding/harness framework cpp/scripts/hooks/stop_validation.py create mode 100644 Codex/Coding/harness framework cpp/scripts/msvc_harness/__init__.py create mode 100644 Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/__init__.py create mode 100644 Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/base.py create mode 100644 Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/cmake.py create mode 100644 Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/msbuild.py create mode 100644 Codex/Coding/harness framework cpp/scripts/msvc_harness/config.py create mode 100644 Codex/Coding/harness framework cpp/scripts/msvc_harness/discovery.py create mode 100644 Codex/Coding/harness framework cpp/scripts/msvc_harness/models.py create mode 100644 Codex/Coding/harness framework cpp/scripts/msvc_harness/process.py create mode 100644 Codex/Coding/harness framework cpp/scripts/msvc_harness/tdd_policy.py create mode 100644 Codex/Coding/harness framework cpp/scripts/msvc_harness/toolchain.py create mode 100644 Codex/plugins/agents-md-management v2/1.0.0/.codex-plugin/plugin.json create mode 100644 Codex/plugins/agents-md-management v2/1.0.0/LICENSE create mode 100644 Codex/plugins/agents-md-management v2/1.0.0/README.md create mode 100644 Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/SKILL.md create mode 100644 Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/agents/openai.yaml create mode 100644 Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/references/quality-criteria.md create mode 100644 Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/references/templates.md create mode 100644 Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/references/update-guidelines.md create mode 100644 Codex/plugins/agents-md-management v2/1.0.0/skills/revising-agents-md/SKILL.md create mode 100644 Codex/plugins/agents-md-management v2/1.0.0/skills/revising-agents-md/agents/openai.yaml diff --git a/Codex/Coding/harness framework cpp/.agents/skills/harness/SKILL.md b/Codex/Coding/harness framework cpp/.agents/skills/harness/SKILL.md new file mode 100644 index 0000000..49519ee --- /dev/null +++ b/Codex/Coding/harness framework cpp/.agents/skills/harness/SKILL.md @@ -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": "", + "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`을 삭제한 + 뒤 재실행 diff --git a/Codex/Coding/harness framework cpp/.agents/skills/review/SKILL.md b/Codex/Coding/harness framework cpp/.agents/skills/review/SKILL.md new file mode 100644 index 0000000..9f695da --- /dev/null +++ b/Codex/Coding/harness framework cpp/.agents/skills/review/SKILL.md @@ -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이 +없으면 발견된 문제가 없다고 명시하고 남아 있는 검증 공백을 설명한다. diff --git a/Codex/Coding/harness framework cpp/.codex/hooks.json b/Codex/Coding/harness framework cpp/.codex/hooks.json new file mode 100644 index 0000000..598631c --- /dev/null +++ b/Codex/Coding/harness framework cpp/.codex/hooks.json @@ -0,0 +1,32 @@ +{ + "description": "Harness TDD, command safety, and MSVC C/C++ validation hooks.", + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash|shell_command|PowerShell|apply_patch|Edit|MultiEdit|Write", + "hooks": [ + { + "type": "command", + "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": [ + { + "hooks": [ + { + "type": "command", + "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" + } + ] + } + ] + } +} diff --git a/Codex/Coding/harness framework cpp/.gitignore b/Codex/Coding/harness framework cpp/.gitignore new file mode 100644 index 0000000..024d626 --- /dev/null +++ b/Codex/Coding/harness framework cpp/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +.pytest_cache/ +*.py[cod] +.harness/build/ diff --git a/Codex/Coding/harness framework cpp/.harness/config.example.json b/Codex/Coding/harness framework cpp/.harness/config.example.json new file mode 100644 index 0000000..70463a7 --- /dev/null +++ b/Codex/Coding/harness framework cpp/.harness/config.example.json @@ -0,0 +1,31 @@ +{ + "version": 1, + "projectType": "auto", + "cmake": { + "sourceDir": ".", + "binaryDir": "out/build/windows-debug", + "configurePreset": "windows-debug", + "buildPreset": "windows-debug", + "testPreset": "windows-debug" + }, + "msbuild": { + "solution": "MyProject.sln", + "configuration": "Debug", + "platform": "x64", + "testCommand": [ + "build/tests/Debug/MyProjectTests.exe" + ] + }, + "tdd": { + "testRoots": [ + "tests" + ], + "testPatterns": [ + "{stem}_test.cpp", + "test_{stem}.cpp" + ], + "exclude": [ + "legacy/generated/**" + ] + } +} diff --git a/Codex/Coding/harness framework cpp/AGENTS.md b/Codex/Coding/harness framework cpp/AGENTS.md new file mode 100644 index 0000000..ff87069 --- /dev/null +++ b/Codex/Coding/harness framework cpp/AGENTS.md @@ -0,0 +1,40 @@ +# 프로젝트: {프로젝트명} + +## 기술 스택 + +- MSVC toolset: {예: v143} +- C++ 표준: {예: C++20} +- 빌드 시스템: {CMake 또는 Visual Studio solution} +- 테스트 프레임워크: {예: GoogleTest, Catch2, Microsoft C++ Unit Test} + +## 아키텍처 규칙 + +- CRITICAL: {절대 지켜야 할 모듈 또는 계층 경계} +- CRITICAL: public header와 implementation 의존성 방향을 역전하지 말 것 +- 제품 코드는 `src/`, public header는 `include/`, 테스트는 `tests/`에 둔다. + +## 개발 프로세스 + +- CRITICAL: 새 기능과 버그 수정은 실패하는 테스트를 먼저 작성한 뒤 구현할 것. +- CRITICAL: 빌드 경고를 새로 추가하지 말 것. +- 커밋 메시지는 conventional commits 형식을 따른다. + +## 명령어 + +### CMake/CTest 프로젝트 + +```powershell +cmake -S . -B .harness/build -A x64 +cmake --build .harness/build --config Debug +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`과 프로젝트 문서를 +우선한다. diff --git a/Codex/Coding/harness framework cpp/docs/ADR.md b/Codex/Coding/harness framework cpp/docs/ADR.md new file mode 100644 index 0000000..7b51333 --- /dev/null +++ b/Codex/Coding/harness framework cpp/docs/ADR.md @@ -0,0 +1,21 @@ +# Architecture Decision Records + +## 철학 +{프로젝트의 핵심 가치관 (예: MVP 속도 최우선. 외부 의존성 최소화. 작동하는 최소 구현을 선택.)} + +--- + +### ADR-001: {결정 사항 (예: Next.js App Router 선택)} +**결정**: {뭘 선택했는지} +**이유**: {왜 선택했는지} +**트레이드오프**: {뭘 포기했는지} + +### ADR-002: {결정 사항} +**결정**: {뭘 선택했는지} +**이유**: {왜 선택했는지} +**트레이드오프**: {뭘 포기했는지} + +### ADR-003: {결정 사항} +**결정**: {뭘 선택했는지} +**이유**: {왜 선택했는지} +**트레이드오프**: {뭘 포기했는지} \ No newline at end of file diff --git a/Codex/Coding/harness framework cpp/docs/ARCHITECTURE.md b/Codex/Coding/harness framework cpp/docs/ARCHITECTURE.md new file mode 100644 index 0000000..1c84a7d --- /dev/null +++ b/Codex/Coding/harness framework cpp/docs/ARCHITECTURE.md @@ -0,0 +1,24 @@ +# 아키텍처 + +## 디렉토리 구조 +``` +src/ +├── app/ # 페이지 + API 라우트 +├── components/ # UI 컴포넌트 +├── types/ # TypeScript 타입 정의 +├── lib/ # 유틸리티 + 헬퍼 +└── services/ # 외부 API 래퍼 +``` + +## 패턴 +{사용하는 디자인 패턴 (예: Server Components 기본, 인터랙션이 필요한 곳만 Client Component)} + +## 데이터 흐름 +``` +{데이터가 어떻게 흐르는지 (예: +사용자 입력 → Client Component → API Route → 외부 API → 응답 → UI 업데이트 +)} +``` + +## 상태 관리 +{상태 관리 방식 (예: 서버 상태는 Server Components, 클라이언트 상태는 useState/useReducer)} \ No newline at end of file diff --git a/Codex/Coding/harness framework cpp/docs/HARNESS.md b/Codex/Coding/harness framework cpp/docs/HARNESS.md new file mode 100644 index 0000000..248134f --- /dev/null +++ b/Codex/Coding/harness framework cpp/docs/HARNESS.md @@ -0,0 +1,120 @@ +# Harness 운영 가이드 + +요구사항 접수부터 Step 실행, TDD 정책, MSVC 빌드·테스트, 재시도와 커밋까지의 +전체 흐름은 [Harness Framework 동작 과정](HARNESS_WORKFLOW.md)을 참고한다. + +## 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++ 저장소는 +오류로 처리한다. + +설정을 시작하려면 다음을 실행한다. + +```powershell +Copy-Item .harness/config.example.json .harness/config.json +python scripts/execute.py +python scripts/execute.py --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 --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는 + 별도의 로그 파일을 만들지 않는다. diff --git a/Codex/Coding/harness framework cpp/docs/HARNESS_WORKFLOW.md b/Codex/Coding/harness framework cpp/docs/HARNESS_WORKFLOW.md new file mode 100644 index 0000000..efa507a --- /dev/null +++ b/Codex/Coding/harness framework cpp/docs/HARNESS_WORKFLOW.md @@ -0,0 +1,602 @@ +# 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 + - +``` + +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 -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 +cmake --build --preset +ctest --preset --show-only=json-v1 +ctest --preset --output-on-failure +``` + +이 경우 모든 명령은 `cmake.sourceDir`에서 실행하고 compiler metadata 검사는 설정한 +`binaryDir`에서 수행한다. + +### 9.3 직접 MSBuild + +[MSBuild adapter](../scripts/msvc_harness/adapters/msbuild.py)는 다음 순서로 실행한다. + +```powershell +MSBuild.exe /m /nologo ` + /p:Configuration= ` + /p:Platform= + + +``` + +직접 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 +``` + +모든 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가 남아 있으면 실제 작업 지시와 충돌할 수 있다. diff --git a/Codex/Coding/harness framework cpp/docs/PRD.md b/Codex/Coding/harness framework cpp/docs/PRD.md new file mode 100644 index 0000000..9006215 --- /dev/null +++ b/Codex/Coding/harness framework cpp/docs/PRD.md @@ -0,0 +1,21 @@ +# PRD: {프로젝트명} + +## 목표 +{이 프로젝트가 해결하려는 문제를 한 줄로 요약} + +## 사용자 +{누가 이 제품을 쓰는지} + +## 핵심 기능 +1. {기능 1} +2. {기능 2} +3. {기능 3} + +## MVP 제외 사항 +- {안 만들 것 1} +- {안 만들 것 2} +- {안 만들 것 3} + +## 디자인 +- {디자인 방향 (예: 다크모드 고정, 미니멀)} +- {색상 (예: 무채색 + 포인트 1가지)} \ No newline at end of file diff --git a/Codex/Coding/harness framework cpp/pytest.ini b/Codex/Coding/harness framework cpp/pytest.ini new file mode 100644 index 0000000..5ee6477 --- /dev/null +++ b/Codex/Coding/harness framework cpp/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = tests diff --git a/Codex/Coding/harness framework cpp/scripts/execute.py b/Codex/Coding/harness framework cpp/scripts/execute.py new file mode 100644 index 0000000..3ca984b --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/execute.py @@ -0,0 +1,481 @@ +#!/usr/bin/env python3 +""" +Harness Step Executor — phase 내 step을 순차 실행하고 자가 교정한다. + +Usage: + python scripts/execute.py [--push] +""" + +import argparse +import contextlib +import json +import os +import subprocess +import sys +import threading +import time +import types +from datetime import datetime, timezone, timedelta +from pathlib import Path +from typing import Optional + +ROOT = Path(__file__).resolve().parent.parent + + +class CodexEnvironmentError(RuntimeError): + """재시도로 해결할 수 없는 Codex CLI 환경 오류.""" + + +@contextlib.contextmanager +def progress_indicator(label: str): + """터미널 진행 표시기. with 문으로 사용하며 .elapsed 로 경과 시간을 읽는다.""" + frames = "◐◓◑◒" + stop = threading.Event() + t0 = time.monotonic() + + def _animate(): + idx = 0 + while not stop.wait(0.12): + sec = int(time.monotonic() - t0) + sys.stderr.write(f"\r{frames[idx % len(frames)]} {label} [{sec}s]") + sys.stderr.flush() + idx += 1 + sys.stderr.write("\r" + " " * (len(label) + 20) + "\r") + sys.stderr.flush() + + th = threading.Thread(target=_animate, daemon=True) + th.start() + info = types.SimpleNamespace(elapsed=0.0) + try: + yield info + finally: + stop.set() + th.join() + info.elapsed = time.monotonic() - t0 + + +class StepExecutor: + """Phase 디렉토리 안의 step들을 순차 실행하는 하네스.""" + + MAX_RETRIES = 3 + FEAT_MSG = "feat({phase}): step {num} — {name}" + CHORE_MSG = "chore({phase}): step {num} output" + TZ = timezone(timedelta(hours=9)) + + def __init__(self, phase_dir_name: str, *, auto_push: bool = False): + self._root = str(ROOT) + self._phases_dir = ROOT / "phases" + self._phase_dir = self._phases_dir / phase_dir_name + self._phase_dir_name = phase_dir_name + self._top_index_file = self._phases_dir / "index.json" + self._auto_push = auto_push + + if not self._phase_dir.is_dir(): + print(f"ERROR: {self._phase_dir} not found") + sys.exit(1) + + self._index_file = self._phase_dir / "index.json" + if not self._index_file.exists(): + print(f"ERROR: {self._index_file} not found") + sys.exit(1) + + idx = self._read_json(self._index_file) + self._project = idx.get("project", "project") + self._phase_name = idx.get("phase", phase_dir_name) + self._total = len(idx["steps"]) + + def run(self): + self._print_header() + self._check_blockers() + self._checkout_branch() + guardrails = self._load_guardrails() + self._ensure_created_at() + self._execute_all_steps(guardrails) + self._finalize() + + # --- timestamps --- + + def _stamp(self) -> str: + return datetime.now(self.TZ).strftime("%Y-%m-%dT%H:%M:%S%z") + + # --- JSON I/O --- + + @staticmethod + def _read_json(p: Path) -> dict: + return json.loads(p.read_text(encoding="utf-8")) + + @staticmethod + def _write_json(p: Path, data: dict): + p.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8") + + # --- git --- + + def _run_git(self, *args) -> subprocess.CompletedProcess: + cmd = ["git"] + list(args) + return subprocess.run(cmd, cwd=self._root, capture_output=True, text=True) + + def _checkout_branch(self): + branch = f"feat-{self._phase_name}" + + r = self._run_git("rev-parse", "--abbrev-ref", "HEAD") + if r.returncode != 0: + print(f" ERROR: git을 사용할 수 없거나 git repo가 아닙니다.") + print(f" {r.stderr.strip()}") + sys.exit(1) + + if r.stdout.strip() == branch: + return + + r = self._run_git("rev-parse", "--verify", branch) + r = self._run_git("checkout", branch) if r.returncode == 0 else self._run_git("checkout", "-b", branch) + + if r.returncode != 0: + print(f" ERROR: 브랜치 '{branch}' checkout 실패.") + print(f" {r.stderr.strip()}") + print(f" Hint: 변경사항을 stash하거나 commit한 후 다시 시도하세요.") + sys.exit(1) + + print(f" Branch: {branch}") + + def _commit_step(self, step_num: int, step_name: str): + output_rel = f"phases/{self._phase_dir_name}/step{step_num}-output.json" + index_rel = f"phases/{self._phase_dir_name}/index.json" + + self._run_git("add", "-A") + self._run_git("reset", "HEAD", "--", output_rel) + self._run_git("reset", "HEAD", "--", index_rel) + + if self._run_git("diff", "--cached", "--quiet").returncode != 0: + msg = self.FEAT_MSG.format(phase=self._phase_name, num=step_num, name=step_name) + r = self._run_git("commit", "-m", msg) + if r.returncode == 0: + print(f" Commit: {msg}") + else: + print(f" WARN: 코드 커밋 실패: {r.stderr.strip()}") + + self._run_git("add", "-A") + if self._run_git("diff", "--cached", "--quiet").returncode != 0: + msg = self.CHORE_MSG.format(phase=self._phase_name, num=step_num) + r = self._run_git("commit", "-m", msg) + if r.returncode != 0: + print(f" WARN: housekeeping 커밋 실패: {r.stderr.strip()}") + + # --- top-level index --- + + def _update_top_index(self, status: str): + if not self._top_index_file.exists(): + return + top = self._read_json(self._top_index_file) + ts = self._stamp() + for phase in top.get("phases", []): + if phase.get("dir") == self._phase_dir_name: + phase["status"] = status + ts_key = {"completed": "completed_at", "error": "failed_at", "blocked": "blocked_at"}.get(status) + if ts_key: + phase[ts_key] = ts + break + self._write_json(self._top_index_file, top) + + # --- guardrails & context --- + + def _load_guardrails(self) -> str: + sections = [] + agents_md = ROOT / "AGENTS.md" + if agents_md.exists(): + sections.append( + "## 프로젝트 규칙 (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')}" + ) + return "\n\n---\n\n".join(sections) if sections else "" + + @staticmethod + def _build_step_context(index: dict) -> str: + lines = [ + f"- Step {s['step']} ({s['name']}): {s['summary']}" + for s in index["steps"] + if s["status"] == "completed" and s.get("summary") + ] + if not lines: + return "" + return "## 이전 Step 산출물\n\n" + "\n".join(lines) + "\n\n" + + def _build_preamble(self, guardrails: str, step_context: str, + prev_error: Optional[str] = None) -> str: + retry_section = "" + if prev_error: + retry_section = ( + f"\n## ⚠ 이전 시도 실패 — 아래 에러를 반드시 참고하여 수정하라\n\n" + f"{prev_error}\n\n---\n\n" + ) + return ( + f"당신은 {self._project} 프로젝트의 개발자입니다. 아래 step을 수행하세요.\n\n" + f"{guardrails}\n\n---\n\n" + f"{step_context}{retry_section}" + f"## 작업 규칙\n\n" + f"1. 이전 step에서 작성된 코드를 확인하고 일관성을 유지하라.\n" + f"2. 이 step에 명시된 작업만 수행하라. 추가 기능이나 파일을 만들지 마라.\n" + f"3. 기존 테스트를 깨뜨리지 마라.\n" + f"4. AC(Acceptance Criteria) 검증을 직접 실행하라.\n" + f"5. /phases/{self._phase_dir_name}/index.json의 해당 step status를 업데이트하라:\n" + f" - AC 통과 → \"completed\" + \"summary\" 필드에 이 step의 산출물을 한 줄로 요약\n" + f" - {self.MAX_RETRIES}회 수정 시도 후에도 실패 → \"error\" + \"error_message\" 기록\n" + f" - 사용자 개입이 필요한 경우 (API 키, 인증, 수동 설정 등) → \"blocked\" + \"blocked_reason\" 기록 후 즉시 중단\n" + f"6. 변경사항을 직접 커밋하지 마라. Git 커밋과 timestamp는 실행기가 처리한다.\n\n" + f"---\n\n" + ) + + # --- Codex 호출 --- + + def _invoke_codex(self, step: dict, preamble: str) -> dict: + step_num, step_name = step["step"], step["name"] + step_file = self._phase_dir / f"step{step_num}.md" + + if not step_file.exists(): + print(f" ERROR: {step_file} not found") + sys.exit(1) + + prompt = preamble + step_file.read_text(encoding="utf-8") + 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})") + if result.stderr: + print(f" stderr: {result.stderr[:500]}") + + output = { + "step": step_num, "name": step_name, + "exitCode": result.returncode, + "stdout": result.stdout, "stderr": result.stderr, + } + out_path = self._phase_dir / f"step{step_num}-output.json" + 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): + print(f"\n{'='*60}") + print(f" Harness Step Executor") + print(f" Phase: {self._phase_name} | Steps: {self._total}") + if self._auto_push: + print(f" Auto-push: enabled") + print(f"{'='*60}") + + def _check_blockers(self): + index = self._read_json(self._index_file) + for s in reversed(index["steps"]): + if s["status"] == "error": + print(f"\n ✗ Step {s['step']} ({s['name']}) failed.") + print(f" Error: {s.get('error_message', 'unknown')}") + print(f" Fix and reset status to 'pending' to retry.") + sys.exit(1) + if s["status"] == "blocked": + print(f"\n ⏸ Step {s['step']} ({s['name']}) blocked.") + print(f" Reason: {s.get('blocked_reason', 'unknown')}") + print(f" Resolve and reset status to 'pending' to retry.") + sys.exit(2) + if s["status"] != "pending": + break + + def _ensure_created_at(self): + index = self._read_json(self._index_file) + if "created_at" not in index: + index["created_at"] = self._stamp() + self._write_json(self._index_file, index) + + # --- 실행 루프 --- + + def _execute_single_step(self, step: dict, guardrails: str) -> bool: + """단일 step 실행 (재시도 포함). 완료되면 True, 실패/차단이면 False.""" + step_num, step_name = step["step"], step["name"] + done = sum(1 for s in self._read_json(self._index_file)["steps"] if s["status"] == "completed") + prev_error = None + + for attempt in range(1, self.MAX_RETRIES + 1): + index = self._read_json(self._index_file) + step_context = self._build_step_context(index) + preamble = self._build_preamble(guardrails, step_context, prev_error) + + tag = f"Step {step_num}/{self._total - 1} ({done} done): {step_name}" + if attempt > 1: + tag += f" [retry {attempt}/{self.MAX_RETRIES}]" + + with progress_indicator(tag) as pi: + 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() + + if status == "completed": + for s in index["steps"]: + if s["step"] == step_num: + s["completed_at"] = ts + self._write_json(self._index_file, index) + self._commit_step(step_num, step_name) + print(f" ✓ Step {step_num}: {step_name} [{elapsed}s]") + return True + + if status == "blocked": + for s in index["steps"]: + if s["step"] == step_num: + s["blocked_at"] = ts + self._write_json(self._index_file, index) + reason = next((s.get("blocked_reason", "") for s in index["steps"] if s["step"] == step_num), "") + print(f" ⏸ Step {step_num}: {step_name} blocked [{elapsed}s]") + print(f" Reason: {reason}") + self._update_top_index("blocked") + sys.exit(2) + + runtime_error = "" + if output["exitCode"] != 0: + runtime_error = ( + output.get("stderr", "").strip() + or output.get("stdout", "").strip() + ) + err_msg = next( + ( + 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: + for s in index["steps"]: + if s["step"] == step_num: + s["status"] = "pending" + s.pop("error_message", None) + self._write_json(self._index_file, index) + prev_error = err_msg + print(f" ↻ Step {step_num}: retry {attempt}/{self.MAX_RETRIES} — {err_msg}") + else: + for s in index["steps"]: + if s["step"] == step_num: + s["status"] = "error" + s["error_message"] = f"[{self.MAX_RETRIES}회 시도 후 실패] {err_msg}" + s["failed_at"] = ts + self._write_json(self._index_file, index) + self._commit_step(step_num, step_name) + print(f" ✗ Step {step_num}: {step_name} failed after {self.MAX_RETRIES} attempts [{elapsed}s]") + print(f" Error: {err_msg}") + self._update_top_index("error") + sys.exit(1) + + return False # unreachable + + def _execute_all_steps(self, guardrails: str): + while True: + index = self._read_json(self._index_file) + pending = next((s for s in index["steps"] if s["status"] == "pending"), None) + if pending is None: + print("\n All steps completed!") + return + + step_num = pending["step"] + for s in index["steps"]: + if s["step"] == step_num and "started_at" not in s: + s["started_at"] = self._stamp() + self._write_json(self._index_file, index) + break + + self._execute_single_step(pending, guardrails) + + def _finalize(self): + index = self._read_json(self._index_file) + index["completed_at"] = self._stamp() + self._write_json(self._index_file, index) + self._update_top_index("completed") + + self._run_git("add", "-A") + if self._run_git("diff", "--cached", "--quiet").returncode != 0: + msg = f"chore({self._phase_name}): mark phase completed" + r = self._run_git("commit", "-m", msg) + if r.returncode == 0: + print(f" ✓ {msg}") + + if self._auto_push: + branch = f"feat-{self._phase_name}" + r = self._run_git("push", "-u", "origin", branch) + if r.returncode != 0: + print(f"\n ERROR: git push 실패: {r.stderr.strip()}") + sys.exit(1) + print(f" ✓ Pushed to origin/{branch}") + + print(f"\n{'='*60}") + print(f" Phase '{self._phase_name}' completed!") + print(f"{'='*60}") + + +def main(): + parser = argparse.ArgumentParser(description="Harness Step Executor") + parser.add_argument("phase_dir", help="Phase directory name (e.g. 0-mvp)") + parser.add_argument("--push", action="store_true", help="Push branch after completion") + args = parser.parse_args() + + try: + StepExecutor(args.phase_dir, auto_push=args.push).run() + except CodexEnvironmentError as exc: + print(f"ERROR: {exc}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/Codex/Coding/harness framework cpp/scripts/hooks/pre_tool_use.py b/Codex/Coding/harness framework cpp/scripts/hooks/pre_tool_use.py new file mode 100644 index 0000000..bca43ba --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/hooks/pre_tool_use.py @@ -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.+?)\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()) diff --git a/Codex/Coding/harness framework cpp/scripts/hooks/stop_validation.py b/Codex/Coding/harness framework cpp/scripts/hooks/stop_validation.py new file mode 100644 index 0000000..5c58c8c --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/hooks/stop_validation.py @@ -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()) diff --git a/Codex/Coding/harness framework cpp/scripts/msvc_harness/__init__.py b/Codex/Coding/harness framework cpp/scripts/msvc_harness/__init__.py new file mode 100644 index 0000000..c59cfff --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/msvc_harness/__init__.py @@ -0,0 +1,3 @@ +from .config import ConfigError, load_config + +__all__ = ["ConfigError", "load_config"] diff --git a/Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/__init__.py b/Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/__init__.py new file mode 100644 index 0000000..c343612 --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/__init__.py @@ -0,0 +1,5 @@ +from .base import AdapterError, ValidationAdapter +from .cmake import CMakeAdapter +from .msbuild import MSBuildAdapter + +__all__ = ["AdapterError", "CMakeAdapter", "MSBuildAdapter", "ValidationAdapter"] diff --git a/Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/base.py b/Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/base.py new file mode 100644 index 0000000..0c97c4e --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/base.py @@ -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 diff --git a/Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/cmake.py b/Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/cmake.py new file mode 100644 index 0000000..f7d449d --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/cmake.py @@ -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")), + ), + ) diff --git a/Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/msbuild.py b/Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/msbuild.py new file mode 100644 index 0000000..00c95e0 --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/msvc_harness/adapters/msbuild.py @@ -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)) diff --git a/Codex/Coding/harness framework cpp/scripts/msvc_harness/config.py b/Codex/Coding/harness framework cpp/scripts/msvc_harness/config.py new file mode 100644 index 0000000..dd9ef43 --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/msvc_harness/config.py @@ -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), + ) diff --git a/Codex/Coding/harness framework cpp/scripts/msvc_harness/discovery.py b/Codex/Coding/harness framework cpp/scripts/msvc_harness/discovery.py new file mode 100644 index 0000000..282c69d --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/msvc_harness/discovery.py @@ -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, ()) diff --git a/Codex/Coding/harness framework cpp/scripts/msvc_harness/models.py b/Codex/Coding/harness framework cpp/scripts/msvc_harness/models.py new file mode 100644 index 0000000..1c03085 --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/msvc_harness/models.py @@ -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 diff --git a/Codex/Coding/harness framework cpp/scripts/msvc_harness/process.py b/Codex/Coding/harness framework cpp/scripts/msvc_harness/process.py new file mode 100644 index 0000000..0911b13 --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/msvc_harness/process.py @@ -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) diff --git a/Codex/Coding/harness framework cpp/scripts/msvc_harness/tdd_policy.py b/Codex/Coding/harness framework cpp/scripts/msvc_harness/tdd_policy.py new file mode 100644 index 0000000..b5f5b8c --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/msvc_harness/tdd_policy.py @@ -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 diff --git a/Codex/Coding/harness framework cpp/scripts/msvc_harness/toolchain.py b/Codex/Coding/harness framework cpp/scripts/msvc_harness/toolchain.py new file mode 100644 index 0000000..b1d63d0 --- /dev/null +++ b/Codex/Coding/harness framework cpp/scripts/msvc_harness/toolchain.py @@ -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) diff --git a/Codex/plugins/agents-md-management v2/1.0.0/.codex-plugin/plugin.json b/Codex/plugins/agents-md-management v2/1.0.0/.codex-plugin/plugin.json new file mode 100644 index 0000000..f9ab12b --- /dev/null +++ b/Codex/plugins/agents-md-management v2/1.0.0/.codex-plugin/plugin.json @@ -0,0 +1,35 @@ +{ + "name": "agents-md-management", + "version": "1.0.0", + "description": "Audit Codex AGENTS.md guidance and capture durable session learnings.", + "author": { + "name": "Anthropic (original plugin); local Codex adaptation" + }, + "homepage": "https://github.com/anthropics/claude-plugins-official/tree/main/plugins/claude-md-management", + "repository": "https://github.com/anthropics/claude-plugins-official", + "license": "Apache-2.0", + "keywords": [ + "agents-md", + "codex", + "project-guidance", + "instructions" + ], + "skills": "./skills/", + "interface": { + "displayName": "AGENTS.md Management", + "shortDescription": "Audit guidance and capture durable learnings", + "longDescription": "A local Codex adaptation of Anthropic's CLAUDE.md management workflows for auditing AGENTS.md files and proposing durable session learnings.", + "developerName": "Anthropic (original); local Codex adaptation", + "category": "Productivity", + "capabilities": [ + "Read", + "Write" + ], + "websiteURL": "https://github.com/anthropics/claude-plugins-official/tree/main/plugins/claude-md-management", + "defaultPrompt": [ + "Audit the AGENTS.md guidance in this repository.", + "Propose durable AGENTS.md updates from this session." + ] + } +} + diff --git a/Codex/plugins/agents-md-management v2/1.0.0/LICENSE b/Codex/plugins/agents-md-management v2/1.0.0/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/Codex/plugins/agents-md-management v2/1.0.0/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Codex/plugins/agents-md-management v2/1.0.0/README.md b/Codex/plugins/agents-md-management v2/1.0.0/README.md new file mode 100644 index 0000000..4f69521 --- /dev/null +++ b/Codex/plugins/agents-md-management v2/1.0.0/README.md @@ -0,0 +1,58 @@ +# AGENTS.md Management + +A local Codex adaptation of Anthropic's +[`claude-md-management`](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/claude-md-management) +plugin. + +This derivative is not an Anthropic-published Codex plugin. It preserves the +original Apache-2.0 license and adapts the two workflows exclusively for +Codex's `AGENTS.md` and `AGENTS.override.md` instruction system. + +## Skills + +### `auditing-agents-md` + +Audits repository guidance against current code, commands, paths, scope, +override precedence, duplication, and the instruction byte budget. It produces +a 100-point quality report and proposed diffs before asking for edit approval. + +Example: + +```text +Audit the AGENTS.md guidance in this repository. +``` + +### `revising-agents-md` + +Reviews the current session for verified, durable learnings and proposes the +narrowest appropriate `AGENTS.md` destination. It excludes sensitive, +unverified, generic, transient, and duplicate content. + +This skill is explicit-only. Invoke it from the skill picker or as +`$revising-agents-md`. + +Example: + +```text +Use $revising-agents-md to propose durable AGENTS.md updates from this session. +``` + +## Safety + +Both skills: + +- show evidence and exact diffs before editing; +- require approval for the proposed destinations and content; +- edit only approved instruction files; +- verify the resulting instruction chain after a change. + +They do not operate on Claude instruction files and include no hooks, MCP +servers, apps, network integrations, or credential handling. + +## Attribution + +Original plugin copyright and authorship belong to Anthropic. The original +plugin was authored by Isabella He and published in Anthropic's official Claude +plugin repository. This Codex-specific adaptation was generated locally from +upstream commit `a28630018d1f8ed486d5a88169ac1f2fb8df89ba`. + diff --git a/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/SKILL.md b/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/SKILL.md new file mode 100644 index 0000000..21c91cd --- /dev/null +++ b/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/SKILL.md @@ -0,0 +1,106 @@ +--- +name: auditing-agents-md +description: Use when a user asks to audit, check, review, improve, update, or fix AGENTS.md or AGENTS.override.md files, or asks whether Codex repository guidance is accurate and current. +--- + +# Audit AGENTS.md + +## Overview + +Audit Codex guidance against the repository that it governs. Treat instruction +precedence, factual accuracy, scope, and conciseness as observable properties: +report evidence before proposing a change, and propose before editing. + +## Workflow + +1. **Resolve scope.** Identify the project root, current working directory, and + requested audit scope. Resolve Codex home from `CODEX_HOME`, falling back to + `~/.codex`; inspect global guidance only when it is in scope. +2. **Discover guidance.** Find `AGENTS.override.md` and `AGENTS.md` while + excluding VCS metadata, dependencies, caches, and generated output. At each + directory, an override is selected before the standard file. Map the active + chain from project root to the working directory and label files that govern + only other subtrees. +3. **Inspect evidence.** Read each selected file completely. Check commands, + paths, architecture claims, and conventions against manifests, source, + tests, config, and version control. Run only safe, relevant verification; + do not deploy, mutate external state, or expose credentials to score a file. +4. **Score every file.** Read + [quality-criteria.md](references/quality-criteria.md). Use its six weighted + criteria, 100-point total, and grade bands. Check conflicts, duplication, + misplaced scope, override behavior, and the combined 32 KiB default + instruction budget. +5. **Report before changing.** Emit the output contract below even when the + guidance is already strong. +6. **Propose targeted diffs.** Read + [update-guidelines.md](references/update-guidelines.md). Include only claims + supported by repository evidence. Use + [templates.md](references/templates.md) only when a missing structure is + genuinely useful. +7. **Request approval.** Stop after the report and proposed diffs. Ask which + changes the user wants applied. +8. **Apply and verify.** Edit only approved files, preserve their organization, + and re-check commands, paths, conflicts, active-chain order, and byte budget. + +## Required Report + +```markdown +## AGENTS.md Quality Report + +### Summary +- Files found: N +- Active chain: path -> path +- Average score: N/100 +- Files needing update: N +- Combined active-chain size: N bytes (default limit: 32 KiB) + +### File-by-File Assessment +#### 1. path (scope; active/inactive) +**Score: N/100 (Grade: A-F)** + +| Criterion | Score | Evidence | +| --- | ---: | --- | +| Commands and workflows | N/20 | ... | +| Repository map and instruction scope | N/20 | ... | +| Non-obvious constraints and gotchas | N/15 | ... | +| Conciseness and duplication | N/15 | ... | +| Currency against the codebase | N/15 | ... | +| Actionability and verifiability | N/15 | ... | + +**Issues** +- ... + +**Recommended changes** +- ... +``` + +After all assessments, show each proposed diff under `### Update: ` with +a one-line `Why:`. End with an explicit approval question. Do not edit in the +same turn unless the user already approved the exact diff. + +## Quick Reference + +| Question | Evidence | +| --- | --- | +| Which file applies? | Global selection, then one selected file per directory from root to working directory | +| Is a command current? | Manifest plus a safe command run when practical | +| Is a path accurate? | Filesystem and repository search | +| Is guidance misplaced? | Narrowest subtree where the rule is true | +| Is the chain too large? | Combined selected-file bytes versus configured/default limit | +| Should content be added? | Project-specific, durable, non-obvious, and actionable | + +## Example + +If `AGENTS.md` requires `npm test` but `package.json` contains only +`test:unit`, score currency and actionability down, cite `package.json`, and +propose the verified command. Do not invent an aggregate script or silently +edit the file. + +## Common Mistakes + +- Scoring the repository once instead of scoring every guidance file. +- Treating a nested override as deleting unrelated root guidance. +- Claiming a file is an entry point because its name looks plausible. +- Recommending generic best practices or facts obvious from code. +- Running destructive, deployment, or credential-sensitive commands to audit. +- Applying recommendations before the user approves the displayed diff. diff --git a/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/agents/openai.yaml b/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/agents/openai.yaml new file mode 100644 index 0000000..88562c8 --- /dev/null +++ b/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Audit AGENTS.md" + short_description: "Audit Codex repository guidance for accuracy" + default_prompt: "Use $auditing-agents-md to audit this repository guidance." diff --git a/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/references/quality-criteria.md b/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/references/quality-criteria.md new file mode 100644 index 0000000..81a6a0d --- /dev/null +++ b/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/references/quality-criteria.md @@ -0,0 +1,87 @@ +# AGENTS.md Quality Criteria + +## Assessment Order + +1. Read each selected instruction file completely. +2. Map its scope and place in the active chain. +3. Cross-reference every factual claim with repository evidence. +4. Score all six criteria independently. +5. Add the scores, assign a grade, and list evidence-backed issues. + +## Scoring Rubric + +### Commands and Workflows - 20 points + +- **20:** Essential setup, build, test, lint, and handoff commands are current, + scoped, and copy-paste ready. +- **15:** Most essential commands are accurate; minor context is missing. +- **10:** Only basic commands are present or important workflow context is + absent. +- **5:** Several commands are missing or stale. +- **0:** No usable commands are documented. + +### Repository Map and Instruction Scope - 20 points + +- **20:** Important directories and relationships are accurate; root, nested, + and override rules are placed at the narrowest correct scope. +- **15:** The map and scope are useful with minor omissions. +- **10:** A directory list exists but relationships or scope are unclear. +- **5:** Paths or precedence are substantially misleading. +- **0:** No usable map exists or the file governs the wrong content. + +### Non-Obvious Constraints and Gotchas - 15 points + +- **15:** Recurring quirks, prerequisites, ordering constraints, and reliable + workarounds are concise and evidenced. +- **10:** Some important non-obvious constraints are documented. +- **5:** Only minimal or weakly supported constraints are present. +- **0:** Important recurring gotchas are absent. + +### Conciseness and Duplication - 15 points + +- **15:** Every line earns prompt space; no filler, obvious facts, or duplicate + rules exist across the active chain. +- **10:** Mostly concise with limited repetition. +- **5:** Noticeable verbosity, duplication, or misplaced detail. +- **0:** Mostly filler or repeated content. + +Also report the combined selected-file byte count. The default +`project_doc_max_bytes` is 32 KiB; use the configured value when repository or +user config proves a different limit. + +### Currency Against the Codebase - 15 points + +- **15:** Commands, paths, versions, architecture, and tool names match current + repository evidence. +- **10:** Mostly current with minor staleness. +- **5:** Several claims are outdated or unverified. +- **0:** Guidance is severely stale or misleading. + +### Actionability and Verifiability - 15 points + +- **15:** Instructions are concrete, safely verifiable, and include clear + completion expectations. +- **10:** Mostly actionable with small ambiguities. +- **5:** Several vague or untestable rules remain. +- **0:** Guidance is theoretical or unusable. + +## Grades + +| Score | Grade | Meaning | +| ---: | :---: | --- | +| 90-100 | A | Comprehensive, current, concise, and actionable | +| 70-89 | B | Good guidance with minor gaps | +| 50-69 | C | Useful basics with important omissions | +| 30-49 | D | Sparse, stale, conflicting, or poorly scoped | +| 0-29 | F | Missing or severely misleading | + +## Red Flags + +- A command missing from the relevant manifest or task runner. +- A reference to a deleted or moved path. +- Root and nested rules that conflict without an intentional override. +- The same rule repeated across the active chain. +- A broad file containing rules true only for one subtree. +- An override used when additive nested guidance was intended. +- Combined guidance near or beyond the configured byte limit. +- Templates copied without real paths, commands, or repository details. diff --git a/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/references/templates.md b/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/references/templates.md new file mode 100644 index 0000000..94732fe --- /dev/null +++ b/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/references/templates.md @@ -0,0 +1,69 @@ +# AGENTS.md Patterns + +Use only sections supported by repository evidence. Prefer a short accurate +file over a comprehensive-looking template. + +## Repository Root + +```markdown +# Repository guidance + +## Commands + +- `npm ci` - Install the locked dependency set. +- `npm run test:unit` - Run unit tests. +- `npm run lint` - Check formatting and static rules. + +## Architecture + +- `src/server.ts` - HTTP entry point. +- `packages/payments/` - Payment domain package. + +## Verification + +- Run the smallest relevant test during iteration. +- Before handoff, run `npm run lint` and `npm run test:unit`. +``` + +## Nested Additive Guidance + +Use nested `AGENTS.md` when local rules add to broader guidance. + +```markdown +# Payments package guidance + +## Commands + +- `npm run test:payments` - Run payment-domain tests from the repository root. + +## Gotchas + +- Payment fixtures use UTC timestamps; do not compare local-time strings. +``` + +## Nested Override + +Use `AGENTS.override.md` only when same-directory standard guidance must be +replaced. Broader parent guidance still remains earlier in the chain. + +```markdown +# Generated client override + +## Editing + +- Do not hand-edit files under `src/generated/`. +- Regenerate them with `npm run generate:client`. + +## Verification + +- Run `npm run check:generated`. +``` + +## Scope Rules + +- Put team-wide repository rules at the root. +- Put subtree-only commands and constraints in the closest nested directory. +- Put personal cross-repository preferences in Codex-home guidance only when + the user explicitly requests global scope. +- Avoid overrides for ordinary additive instructions. + diff --git a/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/references/update-guidelines.md b/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/references/update-guidelines.md new file mode 100644 index 0000000..fddd49c --- /dev/null +++ b/Codex/plugins/agents-md-management v2/1.0.0/skills/auditing-agents-md/references/update-guidelines.md @@ -0,0 +1,62 @@ +# AGENTS.md Update Guidelines + +## Evidence Contract + +Every proposed line must be: + +- specific to the repository or explicitly requested global scope; +- supported by a file, manifest, safe command result, or user-confirmed fact; +- durable enough to help future sessions; +- placed at the narrowest scope where it is true; +- expressed in the shortest actionable form that preserves meaning. + +Never add secrets, credential values, personal data, unverified claims, +session narrative, generic advice, facts obvious from names, or one-off fixes. + +## Safe Verification + +Prefer read-only inspection. Run a command only when it is relevant, safe, and +unlikely to change external state. Do not deploy, publish, migrate data, write +credentials, or invoke destructive cleanup merely to validate guidance. + +If a command cannot safely be run, label it unverified rather than treating it +as current. + +## Precedence and Size + +- Select `AGENTS.override.md` before `AGENTS.md` at each directory. +- Build the active project chain from root to current working directory. +- Later, closer guidance wins on conflict. +- Parent guidance remains active unless a later instruction contradicts it. +- Flag duplicate or conflicting rules across selected files. +- Compare combined selected-file bytes with configured + `project_doc_max_bytes`, using 32 KiB when no override is evidenced. + +## Diff Contract + +For every proposed change: + +```markdown +### Update: path/to/AGENTS.md + +**Why:** `package.json` defines `test:unit`, while the documented `npm test` +command does not exist. + +```diff +- Run `npm test` before handoff. ++ Run `npm run test:unit` before handoff. +``` +``` + +Group diffs by destination. After all diffs, ask which changes the user wants +applied. Do not edit before that approval. + +## Post-Edit Validation + +- Re-read every changed file. +- Rebuild the active chain for the affected subtree. +- Check for conflicts and duplication. +- Verify changed commands and paths. +- Recalculate the combined byte count. +- Report exactly what was changed and what could not be verified. + diff --git a/Codex/plugins/agents-md-management v2/1.0.0/skills/revising-agents-md/SKILL.md b/Codex/plugins/agents-md-management v2/1.0.0/skills/revising-agents-md/SKILL.md new file mode 100644 index 0000000..ed99f3b --- /dev/null +++ b/Codex/plugins/agents-md-management v2/1.0.0/skills/revising-agents-md/SKILL.md @@ -0,0 +1,108 @@ +--- +name: revising-agents-md +description: Use when a user explicitly asks to capture durable learnings from the current session in AGENTS.md after discovering reusable commands, conventions, validation steps, architecture facts, environment constraints, or recurring gotchas. +--- + +# Revise AGENTS.md + +## Overview + +Turn verified session discoveries into concise Codex guidance for future work. +Keep only durable information, place it at the narrowest scope where it +governs work, show the proposed diff, and obtain approval before editing. + +## Workflow + +1. **Reflect.** Extract candidate commands, conventions, validation steps, + architecture relationships, environment constraints, and recurring + gotchas from the current session. +2. **Inspect evidence.** Read the relevant repository files and existing + `AGENTS.md` or `AGENTS.override.md` chain. Check Git tracking and ignore + state when deciding whether guidance is shared or personal. Treat a claim + as verified only when supported by a safe command result, repository file, + or explicit user confirmation. +3. **Classify every candidate.** Record `candidate`, `keep/exclude`, + `evidence`, `destination`, and `reason`. Exclude content already documented + accurately. +4. **Filter aggressively.** Exclude secrets, credentials, personal data, + unverified claims, generic advice, facts obvious from code, one-off fixes, + temporary state, and session narrative. Never reproduce a sensitive value, + even in an exclusion explanation. +5. **Choose the narrowest durable scope.** + - Codex-home `AGENTS.md`: cross-repository personal preferences, only when + the user explicitly requests global scope. + - Repository-root `AGENTS.md`: guidance true for work across the repository. + - Nested `AGENTS.md`: guidance true only for one package, service, feature, + or subtree. + - `AGENTS.override.md`: only when the user explicitly requests replacement + semantics at that directory. + + Scope follows the code or work governed by the instruction, not the + directory from which its command must run. A payment-only test command + belongs in payments guidance even when it says to run from repository root. +6. **Draft grouped diffs.** For each destination, show a one-line reason and + the exact minimal diff. Prefer one line per concept and preserve the + existing file's structure. +7. **Request approval.** End with an explicit question asking which proposed + changes the user wants applied. Do not create or edit a file before the + user approves its exact destination and content. +8. **Apply and verify.** Edit only approved destinations. Re-read the resulting + instruction chain, check conflicts and duplication, and verify changed + commands and paths. + +## Required Proposal + +```markdown +## Session Learning Review + +| Candidate | Decision | Evidence | Destination | Reason | +| --- | --- | --- | --- | --- | +| ... | Keep/Exclude | ... | path or none | ... | + +### Update: path/to/AGENTS.md +**Why:** one durable, evidence-backed reason + +```diff ++ concise instruction +``` + +No files were edited. Which proposed changes should I apply? +``` + +When no candidate qualifies, say so and do not propose an empty file. + +## Quick Reference + +| Candidate | Decision | +| --- | --- | +| Verified recurring command | Keep at the scope whose work uses it | +| Package-only gotcha | Keep in that package's nested `AGENTS.md` | +| Personal preference | Global only after explicit global request | +| Existing accurate instruction | Exclude as duplicate | +| One-off fix or session history | Exclude | +| Generic best practice | Exclude | +| Unverified claim | Exclude or verify safely first | +| Secret or personal data | Exclude without reproducing the value | + +## Example + +A session verifies `npm run test:payments` and proves it must run from the +repository root. If only payment work needs that command, propose this in +`services/payments/AGENTS.md`: + +```diff ++- Run `npm run test:payments` from the repository root. +``` + +Do not place it at repository root merely because that is the execution +directory. + +## Common Mistakes + +- Saving everything because the session was difficult or time-consuming. +- Confusing command execution location with instruction scope. +- Creating an override for ordinary additive guidance. +- Duplicating an instruction that is already accurate. +- Persisting unverified deployment or environment claims. +- Showing a diff without explicitly asking for edit approval. + diff --git a/Codex/plugins/agents-md-management v2/1.0.0/skills/revising-agents-md/agents/openai.yaml b/Codex/plugins/agents-md-management v2/1.0.0/skills/revising-agents-md/agents/openai.yaml new file mode 100644 index 0000000..5ad4e6c --- /dev/null +++ b/Codex/plugins/agents-md-management v2/1.0.0/skills/revising-agents-md/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "Revise AGENTS.md" + short_description: "Capture durable Codex session learnings safely" + default_prompt: "Use $revising-agents-md to propose durable AGENTS.md updates from this session." +policy: + allow_implicit_invocation: false