add harness framework
This commit is contained in:
@@ -0,0 +1,172 @@
|
|||||||
|
---
|
||||||
|
name: harness
|
||||||
|
description: Use when planning agentic implementation phases, creating phases/index.json and self-contained step files, or running the Harness step executor.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Harness Workflow
|
||||||
|
|
||||||
|
이 프로젝트는 Harness 프레임워크를 사용한다. 아래 워크플로에 따라 작업한다.
|
||||||
|
|
||||||
|
## A. 탐색
|
||||||
|
|
||||||
|
`AGENTS.md`와 `docs/` 하위 문서(PRD, ARCHITECTURE, ADR 등)를 읽고 프로젝트의 기획,
|
||||||
|
아키텍처, 설계 의도를 파악한다. 병렬 탐색이 실제로 유용하고 현재 세션에서 허용될
|
||||||
|
때만 Codex subagent를 선택적으로 사용한다.
|
||||||
|
|
||||||
|
## B. 논의
|
||||||
|
|
||||||
|
구현을 위해 구체화하거나 기술적으로 결정해야 할 사항이 있으면 사용자에게 한 번에
|
||||||
|
하나씩 제시하고 논의한다.
|
||||||
|
|
||||||
|
## C. Step 설계
|
||||||
|
|
||||||
|
사용자가 구현 계획 작성을 지시하면 여러 step으로 나뉜 초안을 작성해 피드백을
|
||||||
|
요청한다.
|
||||||
|
|
||||||
|
설계 원칙:
|
||||||
|
|
||||||
|
1. **Scope 최소화** — 하나의 step에서 하나의 레이어 또는 모듈만 다룬다. 여러
|
||||||
|
모듈을 동시에 수정해야 하면 step을 쪼갠다.
|
||||||
|
2. **자기완결성** — 각 step 파일은 독립된 Codex 실행에서 사용된다. 외부 대화
|
||||||
|
참조를 금지하고 필요한 정보를 모두 파일 안에 적는다.
|
||||||
|
3. **사전 준비 강제** — 관련 문서와 이전 step에서 생성하거나 수정한 파일 경로를
|
||||||
|
명시한다.
|
||||||
|
4. **시그니처 수준 지시** — 함수와 클래스의 인터페이스를 제시하고 내부 구현은
|
||||||
|
Codex 재량에 맡긴다. 멱등성, 보안, 데이터 무결성 같은 핵심 규칙은 명시한다.
|
||||||
|
5. **AC는 실행 가능한 command** — 추상적 조건 대신 실제 빌드와 테스트 command를
|
||||||
|
포함한다.
|
||||||
|
6. **주의사항은 구체적으로** — "X를 하지 마라. 이유: Y" 형식으로 적는다.
|
||||||
|
7. **네이밍** — step name은 핵심 작업을 표현하는 kebab-case slug로 정한다.
|
||||||
|
|
||||||
|
## D. 파일 생성
|
||||||
|
|
||||||
|
사용자가 초안을 승인한 후에만 다음 파일을 생성한다.
|
||||||
|
|
||||||
|
### D-1. `phases/index.json`
|
||||||
|
|
||||||
|
여러 task를 관리하는 top-level 인덱스다. 이미 존재하면 `phases` 배열에 새 항목을
|
||||||
|
추가한다.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"phases": [
|
||||||
|
{
|
||||||
|
"dir": "0-mvp",
|
||||||
|
"status": "pending"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `dir`: task 디렉터리명
|
||||||
|
- `status`: `pending` | `completed` | `error` | `blocked`
|
||||||
|
- timestamp는 executor가 상태를 바꿀 때 기록하므로 생성 시 넣지 않는다.
|
||||||
|
|
||||||
|
### D-2. `phases/{task-name}/index.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"project": "<프로젝트명>",
|
||||||
|
"phase": "<task-name>",
|
||||||
|
"steps": [
|
||||||
|
{ "step": 0, "name": "project-setup", "status": "pending" },
|
||||||
|
{ "step": 1, "name": "core-types", "status": "pending" },
|
||||||
|
{ "step": 2, "name": "api-layer", "status": "pending" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
필드 규칙:
|
||||||
|
|
||||||
|
- `project`: `AGENTS.md`에 정의된 프로젝트명
|
||||||
|
- `phase`: task 이름이며 디렉터리명과 일치
|
||||||
|
- `steps[].step`: 0부터 시작하는 순번
|
||||||
|
- `steps[].name`: kebab-case slug
|
||||||
|
- `steps[].status`: 초기값 `pending`
|
||||||
|
|
||||||
|
상태와 기록 주체:
|
||||||
|
|
||||||
|
| 전이 | 기록 필드 | 기록 주체 |
|
||||||
|
|------|-----------|-----------|
|
||||||
|
| `completed` | `summary`, `completed_at` | Codex가 summary, executor가 timestamp |
|
||||||
|
| `error` | `error_message`, `failed_at` | Codex가 message, executor가 timestamp |
|
||||||
|
| `blocked` | `blocked_reason`, `blocked_at` | Codex가 reason, executor가 timestamp |
|
||||||
|
|
||||||
|
`summary`에는 다음 step에 유용한 생성 파일과 핵심 결정을 한 줄로 적는다.
|
||||||
|
task `created_at`과 step `started_at`은 executor가 기록하므로 생성 시 넣지 않는다.
|
||||||
|
|
||||||
|
### D-3. `phases/{task-name}/step{N}.md`
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
# Step {N}: {이름}
|
||||||
|
|
||||||
|
## 읽어야 할 파일
|
||||||
|
|
||||||
|
먼저 아래 파일을 읽고 프로젝트의 아키텍처와 설계 의도를 파악하라:
|
||||||
|
|
||||||
|
- `/AGENTS.md`
|
||||||
|
- `/docs/ARCHITECTURE.md`
|
||||||
|
- `/docs/ADR.md`
|
||||||
|
- 이전 step에서 생성하거나 수정한 파일 경로
|
||||||
|
|
||||||
|
이전 step의 코드를 꼼꼼히 읽고 설계 의도를 이해한 뒤 작업하라.
|
||||||
|
|
||||||
|
## 작업
|
||||||
|
|
||||||
|
구체적인 구현 지시를 파일 경로, 클래스와 함수 시그니처, 로직 설명과 함께 적는다.
|
||||||
|
구현체는 Codex에 맡기되 설계 의도에서 벗어나면 안 되는 핵심 규칙은 명시한다.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
프로젝트 형식에 맞는 명령을 사용한다. `.harness/config.json`이 있으면 해당 preset,
|
||||||
|
solution, configuration, platform, test command를 우선한다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# CMake
|
||||||
|
cmake --build .harness/build --config Debug
|
||||||
|
ctest --test-dir .harness/build -C Debug --output-on-failure
|
||||||
|
|
||||||
|
# 직접 MSBuild
|
||||||
|
MSBuild.exe MyProject.sln /m /p:Configuration=Debug /p:Platform=x64
|
||||||
|
.\build\tests\Debug\MyProjectTests.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 검증 절차
|
||||||
|
|
||||||
|
1. Acceptance Criteria command를 실행한다.
|
||||||
|
2. ARCHITECTURE 디렉터리 구조를 따르는지 확인한다.
|
||||||
|
3. ADR 기술 스택과 `AGENTS.md` CRITICAL 규칙을 확인한다.
|
||||||
|
4. 결과에 따라 task index의 해당 step을 갱신한다.
|
||||||
|
- 성공: `status`를 `completed`로 바꾸고 한 줄 `summary` 기록
|
||||||
|
- 수정 3회 후 실패: `status`를 `error`로 바꾸고 `error_message` 기록
|
||||||
|
- 사용자 개입 필요: `status`를 `blocked`로 바꾸고 `blocked_reason` 기록 후 중단
|
||||||
|
|
||||||
|
## 금지사항
|
||||||
|
|
||||||
|
- 이 step의 범위 밖 기능을 추가하지 마라. 이유: step의 독립성을 깨뜨린다.
|
||||||
|
- 기존 테스트를 깨뜨리지 마라. 이유: 이전 동작을 회귀시킨다.
|
||||||
|
````
|
||||||
|
|
||||||
|
## E. 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/execute.py {task-name}
|
||||||
|
python scripts/execute.py {task-name} --push
|
||||||
|
```
|
||||||
|
|
||||||
|
환경에서 Python 3 실행 명령이 `python3`이면 그 명령을 대신 사용한다.
|
||||||
|
|
||||||
|
executor가 처리하는 작업:
|
||||||
|
|
||||||
|
- `feat-{task-name}` 브랜치 생성 또는 checkout
|
||||||
|
- `AGENTS.md`와 `docs/*.md` guardrail 주입
|
||||||
|
- 완료 step의 summary를 다음 prompt에 누적
|
||||||
|
- 실패 시 최대 3회 재시도하며 이전 오류를 prompt에 전달
|
||||||
|
- 코드 변경과 metadata를 분리해 commit
|
||||||
|
- `started_at`, `completed_at`, `failed_at`, `blocked_at` 기록
|
||||||
|
|
||||||
|
에러 복구:
|
||||||
|
|
||||||
|
- `error`: 해당 status를 `pending`으로 바꾸고 `error_message`를 삭제한 뒤 재실행
|
||||||
|
- `blocked`: 원인을 해결하고 status를 `pending`으로 바꾸고 `blocked_reason`을 삭제한
|
||||||
|
뒤 재실행
|
||||||
@@ -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이
|
||||||
|
없으면 발견된 문제가 없다고 명시하고 남아 있는 검증 공백을 설명한다.
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
|
*.py[cod]
|
||||||
|
.harness/build/
|
||||||
@@ -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/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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`과 프로젝트 문서를
|
||||||
|
우선한다.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Architecture Decision Records
|
||||||
|
|
||||||
|
## 철학
|
||||||
|
{프로젝트의 핵심 가치관 (예: MVP 속도 최우선. 외부 의존성 최소화. 작동하는 최소 구현을 선택.)}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-001: {결정 사항 (예: Next.js App Router 선택)}
|
||||||
|
**결정**: {뭘 선택했는지}
|
||||||
|
**이유**: {왜 선택했는지}
|
||||||
|
**트레이드오프**: {뭘 포기했는지}
|
||||||
|
|
||||||
|
### ADR-002: {결정 사항}
|
||||||
|
**결정**: {뭘 선택했는지}
|
||||||
|
**이유**: {왜 선택했는지}
|
||||||
|
**트레이드오프**: {뭘 포기했는지}
|
||||||
|
|
||||||
|
### ADR-003: {결정 사항}
|
||||||
|
**결정**: {뭘 선택했는지}
|
||||||
|
**이유**: {왜 선택했는지}
|
||||||
|
**트레이드오프**: {뭘 포기했는지}
|
||||||
@@ -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)}
|
||||||
@@ -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 <phase-name>
|
||||||
|
python scripts/execute.py <phase-name> --push
|
||||||
|
```
|
||||||
|
|
||||||
|
## Harness Python 검증
|
||||||
|
|
||||||
|
이 저장소의 테스트와 최종 acceptance 검증은 pytest를 시스템 Python에 설치하지 않고
|
||||||
|
다음 명령으로 실행한다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run --with pytest python -m pytest -v -rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## CMake preset 설정
|
||||||
|
|
||||||
|
`projectType`을 `cmake`로 지정하거나 자동 감지를 사용한다. `cmake.sourceDir`,
|
||||||
|
`binaryDir`, `configurePreset`, `buildPreset`, `testPreset`은 preset을 사용할 때 함께
|
||||||
|
지정해야 한다. 빌드 산출물은 저장소의 `.harness/build/`처럼 격리된 경로에 둔다.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"projectType": "cmake",
|
||||||
|
"cmake": {
|
||||||
|
"sourceDir": ".",
|
||||||
|
"binaryDir": "out/build/windows-debug",
|
||||||
|
"configurePreset": "windows-debug",
|
||||||
|
"buildPreset": "windows-debug",
|
||||||
|
"testPreset": "windows-debug"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake --preset windows-debug
|
||||||
|
cmake --build --preset windows-debug
|
||||||
|
ctest --preset windows-debug --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
Preset을 쓰지 않는 경우에는 같은 격리된 build directory를 명시한다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake -S . -B .harness/build -A x64
|
||||||
|
cmake --build .harness/build --config Debug
|
||||||
|
ctest --test-dir .harness/build -C Debug --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는
|
||||||
|
별도의 로그 파일을 만들지 않는다.
|
||||||
@@ -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 <repository-root>
|
||||||
|
-
|
||||||
|
```
|
||||||
|
|
||||||
|
Codex에 전달하는 프롬프트는 다음 내용의 조합이다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
AGENTS.md와 docs 문서
|
||||||
|
+ 이전에 완료된 Step의 summary
|
||||||
|
+ 이전 시도의 오류(재시도인 경우)
|
||||||
|
+ Executor 공통 작업 규칙
|
||||||
|
+ 현재 stepN.md
|
||||||
|
```
|
||||||
|
|
||||||
|
이전 Step의 전체 대화나 Codex 세션은 전달하지 않는다. task index에 기록한
|
||||||
|
`summary`만 다음 Step에 누적한다.
|
||||||
|
|
||||||
|
Codex 실행 결과의 exit code, stdout, stderr는 다음 파일에 저장한다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
phases/{task-name}/step{N}-output.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 도구 호출 전 PreToolUse 검사
|
||||||
|
|
||||||
|
[`.codex/hooks.json`](../.codex/hooks.json)은 shell 및 파일 편집 도구에
|
||||||
|
[PreToolUse 훅](../scripts/hooks/pre_tool_use.py)을 등록한다. Codex가 실제 명령이나
|
||||||
|
편집을 수행하기 전에 이 훅이 요청을 검사한다.
|
||||||
|
|
||||||
|
### 7.1 위험 명령 차단
|
||||||
|
|
||||||
|
다음 유형의 명령은 요구사항과 관계없이 차단한다.
|
||||||
|
|
||||||
|
- `git reset --hard`
|
||||||
|
- `git push --force` 또는 `--force-with-lease`
|
||||||
|
- `rm -rf`
|
||||||
|
- `Remove-Item -Recurse -Force`
|
||||||
|
- `rmdir /s /q`
|
||||||
|
- `DROP TABLE`
|
||||||
|
|
||||||
|
위험 패턴이 발견되면 훅은 차단 이유를 stderr로 출력하고 종료 코드 2를 반환한다.
|
||||||
|
그러면 해당 도구 호출은 실행되지 않는다.
|
||||||
|
|
||||||
|
### 7.2 C++ TDD 검사
|
||||||
|
|
||||||
|
`apply_patch`, `Edit`, `MultiEdit`, `Write`로 다음 C/C++ 확장자의 파일을 편집하려
|
||||||
|
하면 [TDD 정책](../scripts/msvc_harness/tdd_policy.py)을 검사한다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
.c .cc .cpp .cxx .h .hpp .hxx
|
||||||
|
```
|
||||||
|
|
||||||
|
일반 제품 코드를 수정하려면 대응되는 테스트 파일이 먼저 존재해야 한다. 예를 들어
|
||||||
|
`src/calculator.cpp`의 기본 대응 테스트 이름은 다음과 같다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
calculator_test.cpp
|
||||||
|
calculator_tests.cpp
|
||||||
|
test_calculator.cpp
|
||||||
|
calculator.test.cpp
|
||||||
|
```
|
||||||
|
|
||||||
|
테스트는 다음 위치에서 검색한다.
|
||||||
|
|
||||||
|
- `.harness/config.json`의 `tdd.testRoots`
|
||||||
|
- 제품 파일과 같은 디렉터리 아래 `tests/`
|
||||||
|
- 제품 파일과 같은 디렉터리 아래 `test/`
|
||||||
|
|
||||||
|
다음 파일과 디렉터리는 대응 테스트 존재 검사가 면제된다.
|
||||||
|
|
||||||
|
- 테스트 파일 자체
|
||||||
|
- `main.cpp`
|
||||||
|
- `.harness/build/**`, `build/**`, `out/**`
|
||||||
|
- `cmake-build-*/**`
|
||||||
|
- `third_party/**`, `external/**`, `vendor/**`
|
||||||
|
- `generated/**`
|
||||||
|
- `tdd.exclude`에 추가한 경로
|
||||||
|
|
||||||
|
`tdd.exclude`는 기본 제외 항목을 대체하지 않고 추가한다.
|
||||||
|
|
||||||
|
### 7.3 TDD 검사가 보장하는 범위
|
||||||
|
|
||||||
|
현재 TDD 훅이 직접 보장하는 것은 대응되는 이름의 테스트 파일이 존재한다는
|
||||||
|
사실이다. 다음 항목까지 증명하지는 않는다.
|
||||||
|
|
||||||
|
- 테스트가 이번 요구사항을 실제로 검증하는가
|
||||||
|
- 구현 전에 테스트가 실제로 실패했는가
|
||||||
|
- 테스트의 assertion과 경계 조건이 충분한가
|
||||||
|
- 기존 테스트 파일을 이번 변경과 함께 수정했는가
|
||||||
|
|
||||||
|
또한 shell 명령의 리다이렉션 등으로 C++ 파일을 쓰는 경우 shell 위험 패턴 검사는
|
||||||
|
적용되지만 경로 기반 TDD 검사는 적용되지 않는다. 따라서 이 훅은 완전한 TDD
|
||||||
|
증명기가 아니라 테스트 우선 편집을 유도하는 guardrail이다.
|
||||||
|
|
||||||
|
## 8. Codex 종료 전 Stop 검증
|
||||||
|
|
||||||
|
Codex가 Step 작업을 끝내고 응답을 종료하려 하면
|
||||||
|
[Stop 훅](../scripts/hooks/stop_validation.py)이 실행된다. Stop 훅은 변경 파일만이
|
||||||
|
아니라 발견된 C/C++ 프로젝트 전체를 빌드하고 테스트한다.
|
||||||
|
|
||||||
|
### 8.1 저장소 루트와 재진입 방지
|
||||||
|
|
||||||
|
Stop 훅은 `git rev-parse --show-toplevel`로 프로젝트 루트를 결정한다. Git 저장소를
|
||||||
|
찾을 수 없으면 현재 디렉터리를 사용한다.
|
||||||
|
|
||||||
|
빌드나 테스트의 자식 프로세스에는 `CODEX_STOP_VALIDATION_ACTIVE=1`을 전달한다.
|
||||||
|
같은 훅이 자식 프로세스에서 다시 진입하면 즉시 성공 처리하여 검증 재귀를 막는다.
|
||||||
|
|
||||||
|
### 8.2 설정 로드
|
||||||
|
|
||||||
|
[설정 로더](../scripts/msvc_harness/config.py)는 `.harness/config.json`을 읽는다.
|
||||||
|
파일이 없으면 다음 기본값을 사용한다.
|
||||||
|
|
||||||
|
- `version`: 1
|
||||||
|
- `projectType`: `auto`
|
||||||
|
- CMake source: 저장소 루트
|
||||||
|
- preset 미사용 시 binary directory: `.harness/build`
|
||||||
|
- configuration: `Debug`
|
||||||
|
- platform: `x64`
|
||||||
|
- 테스트 루트: `tests`, `test`
|
||||||
|
- 기본 테스트 이름 패턴 네 개
|
||||||
|
|
||||||
|
설정은 다음 조건을 엄격하게 검사한다.
|
||||||
|
|
||||||
|
- 알 수 없는 필드를 거부한다.
|
||||||
|
- `version`은 숫자 1만 허용한다.
|
||||||
|
- `projectType`은 `auto`, `cmake`, `msbuild`만 허용한다.
|
||||||
|
- 저장소 상대 경로만 허용한다.
|
||||||
|
- 저장소 밖으로 해석되는 경로를 거부한다.
|
||||||
|
- CMake preset을 사용하면 `configurePreset`, `buildPreset`, `testPreset`,
|
||||||
|
`binaryDir`를 모두 요구한다.
|
||||||
|
- 모든 `tdd.testPatterns`에 `{stem}`을 요구한다.
|
||||||
|
|
||||||
|
### 8.3 프로젝트 자동 감지
|
||||||
|
|
||||||
|
[프로젝트 탐색기](../scripts/msvc_harness/discovery.py)는 다음 순서로 프로젝트를
|
||||||
|
선택한다.
|
||||||
|
|
||||||
|
1. `projectType: cmake` 또는 `projectType: msbuild` 명시 설정
|
||||||
|
2. 루트의 `CMakePresets.json`
|
||||||
|
3. 루트의 `CMakeUserPresets.json`
|
||||||
|
4. 루트의 `CMakeLists.txt`
|
||||||
|
5. 루트의 단일 `.sln`
|
||||||
|
6. 루트의 단일 `.vcxproj`
|
||||||
|
|
||||||
|
자동 감지 결과는 다음처럼 처리한다.
|
||||||
|
|
||||||
|
| 저장소 상태 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| CMake metadata가 있음 | CMake 프로젝트 선택 |
|
||||||
|
| 하나의 `.sln` 또는 `.vcxproj`가 있음 | MSBuild 프로젝트 선택 |
|
||||||
|
| 여러 solution/project가 있음 | 설정으로 하나를 지정하라는 오류 |
|
||||||
|
| C/C++ 파일과 build metadata가 모두 없음 | 검증할 프로젝트가 없으므로 통과 |
|
||||||
|
| C/C++ 파일은 있지만 build metadata가 없음 | orphan C++ 프로젝트 오류 |
|
||||||
|
|
||||||
|
### 8.4 MSVC 도구 탐색
|
||||||
|
|
||||||
|
[도구 탐색기](../scripts/msvc_harness/toolchain.py)는 `vswhere.exe`로 다음을
|
||||||
|
확인한다.
|
||||||
|
|
||||||
|
- Visual Studio 설치 경로
|
||||||
|
- Desktop development with C++ workload
|
||||||
|
- `MSBuild.exe`
|
||||||
|
|
||||||
|
CMake 프로젝트에서는 다음 우선순위로 CMake와 CTest를 선택한다.
|
||||||
|
|
||||||
|
1. PATH에서 발견한 독립 `cmake.exe`와 `ctest.exe`
|
||||||
|
2. Visual Studio에 번들된 CMake와 CTest
|
||||||
|
|
||||||
|
따라서 새로 설치한 CMake의 `bin` 디렉터리가 PATH에 반영되어 있으면 독립 CMake를
|
||||||
|
우선 사용한다.
|
||||||
|
|
||||||
|
## 9. 빌드 시스템별 검증 계획
|
||||||
|
|
||||||
|
### 9.1 CMake preset 미사용
|
||||||
|
|
||||||
|
[CMake adapter](../scripts/msvc_harness/adapters/cmake.py)는 다음 검증 계획을 만든다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake -S <source> -B .harness/build -A x64
|
||||||
|
cmake --build .harness/build --config Debug
|
||||||
|
ctest --test-dir .harness/build -C Debug --show-only=json-v1
|
||||||
|
ctest --test-dir .harness/build -C Debug --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
명령 성공 외에 다음 결과도 검사한다.
|
||||||
|
|
||||||
|
- 생성된 CMake compiler metadata의 `CMAKE_CXX_COMPILER_ID`가 `MSVC`인가
|
||||||
|
- CTest JSON에 한 개 이상의 테스트가 있는가
|
||||||
|
|
||||||
|
따라서 빌드가 성공해도 MinGW 등 다른 컴파일러를 사용했거나 CTest가 테스트를 한
|
||||||
|
개도 발견하지 못하면 실패한다.
|
||||||
|
|
||||||
|
### 9.2 CMake preset 사용
|
||||||
|
|
||||||
|
`.harness/config.json`에 preset을 완전히 지정하면 다음 형태로 실행한다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake --preset <configurePreset>
|
||||||
|
cmake --build --preset <buildPreset>
|
||||||
|
ctest --preset <testPreset> --show-only=json-v1
|
||||||
|
ctest --preset <testPreset> --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
이 경우 모든 명령은 `cmake.sourceDir`에서 실행하고 compiler metadata 검사는 설정한
|
||||||
|
`binaryDir`에서 수행한다.
|
||||||
|
|
||||||
|
### 9.3 직접 MSBuild
|
||||||
|
|
||||||
|
[MSBuild adapter](../scripts/msvc_harness/adapters/msbuild.py)는 다음 순서로 실행한다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
MSBuild.exe <solution-or-vcxproj> /m /nologo `
|
||||||
|
/p:Configuration=<configuration> `
|
||||||
|
/p:Platform=<platform>
|
||||||
|
|
||||||
|
<msbuild.testCommand>
|
||||||
|
```
|
||||||
|
|
||||||
|
직접 MSBuild 프로젝트는 표준 테스트 탐색 명령이 없으므로
|
||||||
|
`.harness/config.json`의 `msbuild.testCommand`가 반드시 필요하다. 이 값이 없으면
|
||||||
|
Stop 검증이 실패한다.
|
||||||
|
|
||||||
|
## 10. 명령 실행 안전성과 제한시간
|
||||||
|
|
||||||
|
[검증 실행기](../scripts/msvc_harness/process.py)는 다음 안전 규칙을 적용한다.
|
||||||
|
|
||||||
|
- 명령을 shell 문자열이 아닌 argv 배열로 실행한다.
|
||||||
|
- `shell=False`를 사용한다.
|
||||||
|
- 각 명령의 working directory가 저장소 내부인지 검사한다.
|
||||||
|
- 명령별 제한시간과 Stop 전체 제한시간 중 더 짧은 값을 적용한다.
|
||||||
|
- 종료 코드가 0이 아니면 즉시 해당 stage를 실패 처리한다.
|
||||||
|
|
||||||
|
Stop 훅의 전체 제한시간은 저장소 탐색, toolchain 탐색, configure, build, test discovery,
|
||||||
|
test를 모두 포함해 1,800초다. `.codex/hooks.json`의 Stop command timeout도 1,800초다.
|
||||||
|
|
||||||
|
실패 메시지에는 다음 진단 정보를 포함한다.
|
||||||
|
|
||||||
|
- 실패 stage
|
||||||
|
- 안전하게 표현한 argv 배열
|
||||||
|
- working directory
|
||||||
|
- 종료 코드
|
||||||
|
- stdout과 stderr의 마지막 8,000자
|
||||||
|
|
||||||
|
Harness는 별도 빌드 로그 파일을 생성하지 않는다.
|
||||||
|
|
||||||
|
## 11. 성공, 실패, 차단 처리
|
||||||
|
|
||||||
|
### 11.1 Stop 검증 성공
|
||||||
|
|
||||||
|
빌드와 테스트가 모두 성공하면 Stop 훅은 출력 없이 종료한다. Codex가 정상 종료하면
|
||||||
|
Executor가 task index를 다시 읽는다.
|
||||||
|
|
||||||
|
Codex가 Step을 다음처럼 기록한 경우:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"step": 0,
|
||||||
|
"name": "division-api",
|
||||||
|
"status": "completed",
|
||||||
|
"summary": "divide API와 0 나누기 테스트를 추가함"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Executor는 `completed_at`을 기록하고 변경사항을 커밋한 뒤 다음 `pending` Step을
|
||||||
|
실행한다.
|
||||||
|
|
||||||
|
### 11.2 Stop 검증 실패
|
||||||
|
|
||||||
|
Stop 훅은 Codex hook protocol에 따라 다음 형태의 응답을 출력한다.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"continue": false,
|
||||||
|
"stopReason": "build failed ...",
|
||||||
|
"systemMessage": "build failed ..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Codex 프로세스에 대한 훅 자체의 종료 코드는 0이지만 `continue: false`가 Codex의
|
||||||
|
응답 종료를 막는다. Codex는 같은 세션에서 오류를 확인하고 수정을 계속한다.
|
||||||
|
|
||||||
|
### 11.3 Executor 재시도
|
||||||
|
|
||||||
|
Codex 프로세스가 끝났는데 Step 상태가 `completed` 또는 `blocked`가 아니면 Executor가
|
||||||
|
새 Codex 세션으로 재시도한다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
첫 번째 시도 실패
|
||||||
|
→ 오류를 다음 프롬프트에 삽입
|
||||||
|
→ 두 번째 독립 Codex 실행
|
||||||
|
→ 다시 실패하면 세 번째 독립 Codex 실행
|
||||||
|
→ 세 번째도 실패하면 error 기록 후 종료
|
||||||
|
```
|
||||||
|
|
||||||
|
즉, 실패 복구에는 두 층이 있다.
|
||||||
|
|
||||||
|
1. Stop 훅이 같은 Codex 세션에서 수정하도록 요구한다.
|
||||||
|
2. 세션 자체가 성공하지 못하면 Executor가 새 세션으로 최대 3회 재시도한다.
|
||||||
|
|
||||||
|
### 11.4 사용자 개입 필요
|
||||||
|
|
||||||
|
인증, API 키, 수동 설치처럼 Codex가 자동으로 해결할 수 없는 문제가 있으면 Step을
|
||||||
|
`blocked`로 기록한다. Executor는 `blocked_at`과 top-level 상태를 갱신하고 종료 코드
|
||||||
|
2로 중단한다.
|
||||||
|
|
||||||
|
재개하려면 원인을 해결하고 해당 Step을 `pending`으로 되돌린 뒤
|
||||||
|
`blocked_reason`을 제거하고 다시 실행한다. `error`도 같은 방식으로 `pending`으로
|
||||||
|
되돌리고 `error_message`를 제거한 뒤 재실행한다.
|
||||||
|
|
||||||
|
## 12. Git 커밋과 phase 완료
|
||||||
|
|
||||||
|
Step이 성공하면 제품 변경과 Harness metadata를 분리해 다음 형식으로 커밋한다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
feat(add-division): step 0 — division-api
|
||||||
|
chore(add-division): step 0 output
|
||||||
|
```
|
||||||
|
|
||||||
|
모든 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가 남아 있으면 실제 작업 지시와 충돌할 수 있다.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# PRD: {프로젝트명}
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
{이 프로젝트가 해결하려는 문제를 한 줄로 요약}
|
||||||
|
|
||||||
|
## 사용자
|
||||||
|
{누가 이 제품을 쓰는지}
|
||||||
|
|
||||||
|
## 핵심 기능
|
||||||
|
1. {기능 1}
|
||||||
|
2. {기능 2}
|
||||||
|
3. {기능 3}
|
||||||
|
|
||||||
|
## MVP 제외 사항
|
||||||
|
- {안 만들 것 1}
|
||||||
|
- {안 만들 것 2}
|
||||||
|
- {안 만들 것 3}
|
||||||
|
|
||||||
|
## 디자인
|
||||||
|
- {디자인 방향 (예: 다크모드 고정, 미니멀)}
|
||||||
|
- {색상 (예: 무채색 + 포인트 1가지)}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[pytest]
|
||||||
|
testpaths = tests
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Harness Step Executor — phase 내 step을 순차 실행하고 자가 교정한다.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/execute.py <phase-dir> [--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()
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Codex PreToolUse 정책: 위험 명령과 테스트 없는 구현 파일 수정을 차단한다."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
SCRIPTS_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
if str(SCRIPTS_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||||
|
|
||||||
|
from msvc_harness.config import ConfigError, load_config
|
||||||
|
from msvc_harness.tdd_policy import CPP_SUFFIXES, evaluate_paths
|
||||||
|
|
||||||
|
|
||||||
|
DANGEROUS_PATTERNS = (
|
||||||
|
re.compile(r"\bgit\s+reset\s+--hard\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\bgit\s+push\b[^\n]*--force(?:-with-lease)?\b", re.IGNORECASE),
|
||||||
|
re.compile(r"\brm\s+-rf\b", re.IGNORECASE),
|
||||||
|
re.compile(
|
||||||
|
r"\bRemove-Item\b(?=[^\n]*-Recurse\b)(?=[^\n]*-Force\b)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
),
|
||||||
|
re.compile(
|
||||||
|
r"\b(?:rmdir|rd)\b(?=[^\n]*/s\b)(?=[^\n]*/q\b)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
),
|
||||||
|
re.compile(r"\bDROP\s+TABLE\b", re.IGNORECASE),
|
||||||
|
)
|
||||||
|
PATCH_PATH = re.compile(
|
||||||
|
r"^\*\*\* (?:(?:Add|Update|Delete) File:|Move to:) (?P<path>.+?)\s*$",
|
||||||
|
re.MULTILINE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _tool_input(payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
value = payload.get("tool_input", {})
|
||||||
|
return value if isinstance(value, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _candidate_paths(payload: dict[str, Any], root: Path) -> list[Path]:
|
||||||
|
tool_input = _tool_input(payload)
|
||||||
|
raw_paths: list[str] = []
|
||||||
|
for key in ("path", "file_path"):
|
||||||
|
value = tool_input.get(key)
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
raw_paths.append(value.strip())
|
||||||
|
|
||||||
|
edits = tool_input.get("edits")
|
||||||
|
if isinstance(edits, list):
|
||||||
|
for edit in edits:
|
||||||
|
if not isinstance(edit, dict):
|
||||||
|
continue
|
||||||
|
value = edit.get("path")
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
raw_paths.append(value.strip())
|
||||||
|
|
||||||
|
for key in ("patch", "input"):
|
||||||
|
value = tool_input.get(key)
|
||||||
|
if isinstance(value, str):
|
||||||
|
raw_paths.extend(match.group("path") for match in PATCH_PATH.finditer(value))
|
||||||
|
|
||||||
|
paths: list[Path] = []
|
||||||
|
for raw_path in raw_paths:
|
||||||
|
path = Path(raw_path)
|
||||||
|
paths.append(path.resolve() if path.is_absolute() else (root / path).resolve())
|
||||||
|
return list(dict.fromkeys(paths))
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate(payload: dict[str, Any], root: Path) -> str | None:
|
||||||
|
"""Return a blocking reason, or None when the tool call is allowed."""
|
||||||
|
tool_name = str(payload.get("tool_name", ""))
|
||||||
|
tool_input = _tool_input(payload)
|
||||||
|
|
||||||
|
if tool_name in {"Bash", "shell_command", "PowerShell"}:
|
||||||
|
command = tool_input.get("command", "")
|
||||||
|
if isinstance(command, str) and any(
|
||||||
|
pattern.search(command) for pattern in DANGEROUS_PATTERNS
|
||||||
|
):
|
||||||
|
return "위험한 명령어가 감지되어 실행을 차단했습니다."
|
||||||
|
|
||||||
|
if tool_name not in {"apply_patch", "Edit", "MultiEdit", "Write"}:
|
||||||
|
return None
|
||||||
|
|
||||||
|
paths = _candidate_paths(payload, root)
|
||||||
|
try:
|
||||||
|
config = load_config(root)
|
||||||
|
except ConfigError as exc:
|
||||||
|
if any(path.suffix.lower() in CPP_SUFFIXES for path in paths):
|
||||||
|
return f"TDD GUARD: .harness/config.json must be repaired: {exc}"
|
||||||
|
return None
|
||||||
|
return evaluate_paths(paths, root, config.tdd)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
try:
|
||||||
|
payload = json.load(sys.stdin)
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise TypeError("hook input must be a JSON object")
|
||||||
|
cwd = payload.get("cwd")
|
||||||
|
if cwd is not None and not isinstance(cwd, str):
|
||||||
|
raise TypeError("hook cwd must be a string")
|
||||||
|
except (json.JSONDecodeError, TypeError) as exc:
|
||||||
|
print(f"TDD GUARD: hook 입력을 해석하지 못해 검사를 건너뜁니다: {exc}", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
root = Path(payload.get("cwd") or Path.cwd()).resolve()
|
||||||
|
reason = evaluate(payload, root)
|
||||||
|
if reason:
|
||||||
|
print(reason, file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -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())
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .config import ConfigError, load_config
|
||||||
|
|
||||||
|
__all__ = ["ConfigError", "load_config"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .base import AdapterError, ValidationAdapter
|
||||||
|
from .cmake import CMakeAdapter
|
||||||
|
from .msbuild import MSBuildAdapter
|
||||||
|
|
||||||
|
__all__ = ["AdapterError", "CMakeAdapter", "MSBuildAdapter", "ValidationAdapter"]
|
||||||
@@ -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
|
||||||
@@ -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")),
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -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))
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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, ())
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -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`.
|
||||||
|
|
||||||
@@ -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: <path>` 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.
|
||||||
+4
@@ -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."
|
||||||
+87
@@ -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.
|
||||||
+69
@@ -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.
|
||||||
|
|
||||||
+62
@@ -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.
|
||||||
|
|
||||||
@@ -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.
|
||||||
|
|
||||||
+6
@@ -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
|
||||||
Reference in New Issue
Block a user