modify harness framework

This commit is contained in:
김경종
2026-09-08 10:30:07 +09:00
parent 1239d5b9f6
commit 1a0f3c9aaf
68 changed files with 6729 additions and 957 deletions
@@ -0,0 +1,354 @@
# C# .NET 8 Harness 환경 구축 가이드
이 문서는 Windows에서 Harness를 설치하고 C#·.NET 8 제품의 빌드·테스트와 Codex
step 실행을 준비하는 절차를 설명한다. 명령은 별도 표시가 없으면 **대상 저장소
루트의 PowerShell**에서 실행한다.
이 저장소는 범용 템플릿이며 제품명·업무 기능·UI·배포 방식은 미정이다.
`tests/fixtures/dotnet/Smoke/`는 도구 검증용 프로젝트다. 아래 `Example` 경로는
제품 설정 예시이며 현재 제품이나 검증 성공을 뜻하지 않는다. 실제 검증 결과는
해당 [phase 기록](phase/README.md)에 남긴다.
## 1. 필요한 환경
| 구분 | 요구 사항 | 용도 및 확인 기준 |
|---|---|---|
| 운영체제 | Windows | Harness 실행·검증 환경 고정. 제품 배포 OS와 별개다. |
| 셸 | PowerShell | 아래 UTF-8 파일 작성 예제는 PowerShell 7 기준이다. |
| Git | Windows에서 실행 가능한 Git | 저장소 탐색, branch 전환, 자동 commit. `--push` 시 원격 인증이 필요하다. |
| Python | 3.10 이상, 실제 `python` 명령 실행 가능 | 실행기·훅은 표준 라이브러리를 사용한다. 자식 훅 환경의 PATH에서도 보여야 한다. |
| Codex CLI | 아래 옵션·훅 호환성 점검을 통과하는 버전 | `codex exec`, JSON 출력, stdin 프롬프트와 프로젝트 훅을 사용한다. |
| .NET | 루트 `global.json`을 만족하는 .NET 8 SDK | runtime만 설치해서는 restore/build/test를 실행할 수 없다. |
| 테스트 | 기본 xUnit v2 + VSTest, NuGet 접근 | 각 명시 test project의 실제 실행과 TRX 결과가 필요하다. |
| 선택 도구 | IDE, Node.js/npm, pytest/uv | IDE는 선택이다. npm은 해당 Codex 설치 방식에, pytest/uv는 Harness 테스트에 사용한다. |
제품 아키텍처나 CPU 대상을 일괄 지정하지 않는다. SDK와 제품 TFM의 역할은
[Microsoft global.json 문서](https://learn.microsoft.com/en-us/dotnet/core/tools/global-json)를 참고한다.
## 2. 기본 도구 설치
### Git과 Python
1. [Git for Windows 공식 설치 페이지](https://git-scm.com/install/windows)에서 설치하고
PowerShell과 외부 프로그램에서 `git`을 실행할 수 있도록 PATH를 설정한다.
2. [Python Windows 공식 배포 페이지](https://www.python.org/downloads/windows/)에서
Python 3.10 이상을 설치한다. 실행기와 자식 훅이 실제 설치 경로를 찾도록 설정한다.
3. 새 PowerShell 창에서 확인한다.
```powershell
git --version
python --version
python -c "import sys; print(sys.executable); assert sys.version_info >= (3, 10)"
Get-Command git, python | Select-Object Name, Source
```
`python`이 Microsoft Store를 열거나 WindowsApps 별칭으로 실행에 실패하면 앱 실행
별칭과 PATH 순서를 확인한다. `py`나 `uv run python`만 동작하는 상태는 충분하지
않다. 현재 Windows 훅은 `python`을 호출한다. 가상환경을 쓰면 활성화한 동일 터미널에서
Harness와 Codex를 실행한다. uv로 Python 테스트가 통과해도 훅 PATH 점검을 대신하지 않는다.
### .NET 8 SDK
[Microsoft .NET 8 다운로드](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)에서
환경에 맞는 **SDK**를 설치하고 새 터미널을 연다. 루트의 `global.json` 기준은 다음과 같다.
```json
{
"sdk": {
"version": "8.0.424",
"rollForward": "latestPatch",
"allowPrerelease": false
}
}
```
이 SDK 번호는 초기 검증 기준이며 최신 버전이라는 뜻이 아니다. `latestPatch`는 같은
feature band의 적합한 패치를 선택하며 설치된 SDK 9로 넘어가는 정책이 아니다.
설치 목록과 실제 선택 결과를 구분해 **저장소 루트에서** 확인한다.
```powershell
Get-Command dotnet | Select-Object Name, Source
dotnet --list-sdks
dotnet --version
```
선택 결과는 위 정책을 만족하는 `8.0.4xx`여야 한다. SDK가 없거나 조건에 맞지 않으면
설치를 복구하거나 검토 후 SDK 기준을 변경한다. Harness는 SDK를 자동 설치하지 않는다.
`global.json`은 도구 선택이고 `.csproj`의 `TargetFramework`는 제품 대상이다.
신규 프로젝트는 `net8.0`, `Nullable=enable`, `ImplicitUsings=enable`을 기본으로 하고
언어 버전은 SDK 기본값을 사용한다.
## 3. Codex CLI 설치와 호환성 확인
[공식 Codex CLI 설치 안내](https://learn.chatgpt.com/docs/codex/cli)를 따라 설치하고
새 터미널을 연다. npm 방식을 선택하면
[Node.js 공식 배포 페이지](https://nodejs.org/en/download)의 Node.js/npm이 필요하다.
Harness는 Codex 앱 설치 여부와 별개로 `codex` 명령을 직접 호출한다.
```powershell
codex --version
codex login
codex login status
codex exec --help
```
`codex login`의 안내에 따라 인증한다. Harness 설정에 계정이나 API 키를 넣지 않는다.
현재 실행기가 사용하는 명령 형태는 다음과 같다.
```text
codex exec --json --sandbox workspace-write --dangerously-bypass-hook-trust --cd <repository-root> -
```
저장소는 최소 Codex 버전을 선언하지 않으므로 `exec --help`로 해당 옵션을 확인한다.
2026-09-08 로컬 CLI `0.153.4`에서 옵션을 확인했지만 이것은 전체 모델 실행이나
자동 훅 차단 검증을 뜻하지 않는다.
실행기는 셸 alias나 PowerShell 함수 대신 Python 자식 프로세스로 CLI를 호출한다.
아래 점검에는 모델 호출이 없다.
```powershell
python -X utf8 -c "import subprocess; subprocess.run(['codex', '--version'], check=True)"
python -X utf8 -c "import subprocess; subprocess.run(['codex', 'exec', '--help'], check=True)"
```
터미널의 `codex`는 되는데 위 명령이 실패하면 `Get-Command codex -All`과
`where.exe codex`로 PATH를 확인한다. alias나 npm shim이 자식 프로세스에서 실행되지
않으면 공식 Windows 설치 방식과 실제 실행 파일 경로를 확인한 뒤 다시 점검한다.
## 4. 저장소 준비와 훅 확인
Git으로 clone하거나 기존 제품 저장소에 다음 구성요소를 가져온다. 기존 `AGENTS.md`와
훅 설정이 있으면 제품 규칙을 유지하면서 병합한다.
```text
AGENTS.md
global.json
.gitignore
.agents/skills/harness/SKILL.md
.agents/skills/review/SKILL.md
.codex/hooks.json
.harness/config.example.json
scripts/execute.py
scripts/hooks/
scripts/dotnet_harness/
docs/HARNESS.md
docs/HARNESS_WORKFLOW.md
docs/ENVIRONMENT_SETUP.md
docs/phase/README.md
```
실행기와 훅이 경로를 기준으로 저장소를 찾으므로 배치를 유지한다. 제품 권장 배치는
`src/<ProjectName>/`와 `tests/<ProjectName>.Tests/`이며 제품을 미리 만들어 놓았다는
뜻은 아니다. Harness 자체를 개발하려면 `tests/harness/`, `tests/fixtures/`와
`pytest.ini`도 유지한다.
```powershell
git rev-parse --show-toplevel
git status --short
git config user.name
git config user.email
python -X utf8 scripts/execute.py --help
```
Git 작성자 정보가 없으면 대상 저장소에서 아래 예시를 자신의 정보로 바꾸어 설정한다.
실제 실행 전 준비한 설정·phase 파일을 검토해 기준 commit을 만들고, 다른 작업의
변경이 없는 전용 checkout/worktree를 사용한다. 실행기는 `git add -A`와 자동 commit을
수행하므로 초기 HEAD가 없는 상태에서 실제 phase 실행을 시작하지 않는다.
```powershell
git config --local user.name 'YOUR_NAME'
git config --local user.email 'YOUR_EMAIL'
```
저장소 루트에서 `codex`를 실행하고 `/hooks`로 `.codex/hooks.json`의 `PreToolUse`와
`Stop`이 로드되는지 확인한다. 프로젝트와 변경된 훅의 신뢰, 사용자·관리자 설정을
확인한다. `commandWindows`는 Windows용 명령 재정의이며 현재 실행기의
`--dangerously-bypass-hook-trust`는 검토된 자동화에서 활성 훅의 지속 신뢰 요구를
건너뛰는 옵션이다. 비활성 훅을 켜거나 빌드 설정을 대신하지 않는다.
[공식 Codex 훅 문서](https://learn.chatgpt.com/docs/hooks)
Windows 훅은 `python -X utf8`과 Git 루트 탐색을 사용한다. 아래 직접 호출은 스크립트의
입력 처리만 확인하며 Codex가 해당 훅을 자동 호출하는지는 검증하지 않는다.
```powershell
'{"tool_name":"Edit","tool_input":{"file_path":"docs/ENVIRONMENT_SETUP.md"}}' | python -X utf8 scripts/hooks/pre_tool_use.py
$LASTEXITCODE
```
문서 편집 허용 기준은 종료 코드 `0`, 거부 JSON 없음, 입력 해석 오류 없음이다.
PreToolUse 거부는 stdout의 `hookSpecificOutput.permissionDecision: "deny"`와
`permissionDecisionReason`으로 판정한다. Windows PowerShell이 자식 프로세스의 종료
코드 `2`를 `1`로 바꿀 수 있어 훅은 JSON 응답과 종료 코드 `0`을 사용한다.
C# `Foo.cs` 편집에는 기본 루트 `tests/` 또는 편집 파일 옆 `tests/`, `test/`의
`FooTests.cs`나 `FooTest.cs`가 필요하다. 실제 실패 테스트 확인은 별도다. `Program.cs`는 자동 면제가
아니며 필요한 경우 정확한 `tdd.exclude` 경로와 이유를 기록한다.
실제 자동 훅 확인은 격리 저장소에서 테스트 없는 C# 편집 요청이 차단되는지,
대응 테스트 작성 후 허용되는지, 잘못된 빌드가 Stop에서 거부되는지를 관측한다.
등록 확인·직접 스크립트 실행·실제 CLI 이벤트 검증 결과를 구분해서 기록한다.
## 5. 제품 빌드 설정
`.harness/config.example.json`을 참고해 실제 제품에 맞는 version 2 설정을 작성한다.
JSON은 **UTF-8 BOM 없이**, 주석 없이 저장한다. PowerShell 7에서는
`Set-Content -Encoding utf8NoBOM`을 사용할 수 있다. 아래 `Example.sln`과 경로는
설명용이며 그대로 복사해 완성된 제품 설정으로 사용하지 않는다.
```json
{
"version": 2,
"projectType": "dotnet",
"dotnet": {
"target": "Example.sln",
"configuration": "Debug",
"testProjects": ["tests/Example.Tests/Example.Tests.csproj"]
},
"tdd": {
"testRoots": ["tests"],
"testPatterns": ["{stem}Tests.cs", "{stem}Test.cs"],
"exclude": []
}
}
```
target은 저장소 내부 `.sln` 또는 SDK 스타일 `.csproj`다. `projectType`는
`auto`(기본값) 또는 `dotnet`이다. target 생략 시 루트 단일 solution, 다음으로
제외 경로 밖 단일 project를 선택한다. 여러 후보면 target을 지정한다.
`.slnx`, 기존 .NET Framework project와 다른 언어 project는 지원하지 않는다.
제품이 있으면 `testProjects`에 **전체 제품 테스트**를 비어 있지 않은 목록으로 명시한다.
테스트가 solution에 포함되어 있어도 이 목록이 필요하다. 모든 설정 경로는 저장소
상대 경로이며 외부로 향하는 링크·junction, 없는 target과 알 수 없는 필드는 거부한다.
version 1은 새 설정으로 다시 작성한다. `exclude: []`는 유효하지만 명시된 test root,
test pattern과 제품의 test project 목록은 비워 둘 수 없다.
신규 테스트 기본값은 xUnit v2 + VSTest다. 테스트 project는 제품 project를 참조하고
`xunit`, `xunit.runner.visualstudio`, `Microsoft.NET.Test.Sdk`의 호환되는 안정 버전을
명시적으로 고정한다. fixture의 실제 버전은
[Smoke.Tests.csproj](../tests/fixtures/dotnet/Smoke/tests/Smoke.Tests/Smoke.Tests.csproj)를 참고한다.
fixture의 `NuGet.Config`는 도구 검증용 feed를 한정한다. 실제 제품에서는 제품의 feed와
인증 설정을 유지하고 restore 오류를 개별적으로 해결한다.
제품에서 테스트 project를 참조하지 않는다. 다른 기존 프레임워크도 VSTest/TRX 계약을
만족해야 한다. [Microsoft의 xUnit 구성 예제](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-csharp-with-xunit)
아래는 설정 확인 명령이다. JSON 구문 검사와 설정 load는 제품 빌드가 아니다.
```powershell
python -X utf8 -m json.tool .harness/config.json
python -X utf8 -c "import sys; from pathlib import Path; sys.path.insert(0, 'scripts'); from dotnet_harness.config import load_config; print(load_config(Path.cwd()))"
```
제품 경로를 실제 값으로 바꾸고 각 명령의 종료 코드가 0인지 확인한다. 실패한 단계가
있으면 다음 단계로 진행하지 않는다. SDK 확인 다음에는 target의 실제 각 프로젝트와
모든 test project와 SDK로 평가한 ProjectReference의 전체 참조 그래프에서 TFM·저장소 경계를 확인한다. 아래 `src/Example/Example.csproj`도 예시이며
solution에 포함된 실제 프로젝트마다 반복한다. 모든 test project에 restore/build/test를 반복한다.
```powershell
dotnet --version
dotnet msbuild src/Example/Example.csproj -getProperty:TargetFramework,TargetFrameworks -getItem:ProjectReference -p:Configuration=Debug
dotnet msbuild tests/Example.Tests/Example.Tests.csproj -getProperty:TargetFramework,TargetFrameworks -getItem:ProjectReference -p:Configuration=Debug
dotnet restore Example.sln -p:Configuration=Debug
dotnet restore tests/Example.Tests/Example.Tests.csproj -p:Configuration=Debug
dotnet build Example.sln -c Debug --no-restore -warnaserror
dotnet build tests/Example.Tests/Example.Tests.csproj -c Debug --no-restore -warnaserror
$manualResults = Join-Path '.harness/test-results' ([guid]::NewGuid().ToString('N'))
dotnet test tests/Example.Tests/Example.Tests.csproj -c Debug --no-build --no-restore --logger trx --results-directory $manualResults
```
TFM 평가와 restore의 `Configuration`, build/test의 `-c`는 설정의 같은 값을 사용한다.
평가 결과의 `TargetFrameworks` 또는 `TargetFramework`는 모두 .NET 8이어야 한다.
`net8.0`과 .NET 8의 플랫폼별 TFM을 허용하며 빈 값·비지원 TFM은 restore 전에 거부한다.
상속·조건부 참조도 따라가며 저장소 밖 프로젝트는 거부한다. 참조별 AdditionalProperties나
SetConfiguration 등 별도 속성 전달은 현재 지원하지 않는다. 상세 계약은 [운영 가이드](HARNESS.md)를 따른다.
이 검사는 상속된 `Directory.Build.props`와 속성 참조를 평가하므로 `.csproj`의 문자만
확인한 결과로 대체하지 않는다.
Harness도 target과 test project별 restore/build를 수행하므로 solution에서 빠진 테스트를
누락하지 않는다. 현재 호출의 고유 결과 경로에서 **각 project의 모든 TRX**를 확인하며
실행된 테스트가 있어야 한다. exit code 0만으로는 충분하지 않다. 실패·손상·누락,
0건과 skip-only는 실패이며 과거 TRX를 재사용하지 않는다.
[VSTest dotnet test 문서](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test-vstest)
`.gitignore`의 중첩 `bin/`, `obj/`, `.vs/`, `TestResults/`와 Harness 결과 제외를
확인한다. 공유할 `.harness/config.json`과 로컬 파일·산출물 정책을 자동 staging 전에
정한다. `tdd.exclude`는 테스트 요구 면제만 제어하고 제품 존재 탐색에는 적용되지 않는다.
`tests/fixtures/`는 도구 fixture로 예약하므로 실제 제품을 그 안에 두지 않는다.
## 6. 구축 완료 확인과 최초 실행
제품 metadata와 설정을 준비한 뒤 일반 터미널에서 Stop 훅을 직접 실행한다.
SDK 선택과 실제 TFM 평가에 이어 restore·build·test와 산출물 작성이 발생한다.
```powershell
python -X utf8 scripts/hooks/stop_validation.py
```
스크립트는 **실패해도 프로세스 종료 코드가 0일 수 있다**. 표준 출력의
`"continue": false`와 `stopReason`은 검증 실패다. 제품과 metadata가 없는 템플릿의
`제품 검증 대상 없음`이나 docs-only 생략을 제품 build 성공으로 기록하지 않는다.
`CODEX_STOP_VALIDATION_ACTIVE=1`인 재진입 환경에서도 생략될 수 있으므로 일반
터미널의 새 검증 결과와 실제 테스트 실행 건수를 확인한다.
실제 CLI에서도 Stop 실패가 종료 코드 `0`과 모델의 `completed` 상태를 남길 수 있다.
따라서 executor는 완료 보고 후 제품 검증을 직접 다시 실행하고, 성공해야만 step을
승인한다. 문서 전용 변경 판정도 이 단계에서 다시 확인하며 실패는 기존 재시도 예산을 사용한다.
[실행 흐름](HARNESS_WORKFLOW.md)과
[Harness 스킬](../.agents/skills/harness/SKILL.md)에 맞춰 실제 작업의
`phases/index.json`, `phases/<phase>/index.json`, `stepN.md`를 준비한다.
`context_files`에는 존재하는 파일만 넣는다. [phase 기록 형식](phase/README.md)에 따라
handoff는 약 60줄, step 기록에는 변경·결정·실제 검증·유효한 실패 접근을 남긴다.
실행 상태의 기준은 항상 phase index다.
`<phase>`를 실제 디렉터리 이름으로 바꾼다.
```powershell
python -X utf8 scripts/execute.py <phase> --dry-run
python -X utf8 scripts/execute.py <phase>
```
`--dry-run`은 첫 pending step의 문맥 경로와 프롬프트 크기만 확인하며 Codex, Git 변경,
인증과 제품 검증을 실행하지 않는다. 실제 실행은 branch 전환, 파일 수정과 자동 commit을
수행한다. 원격 전송이 필요할 때만 `--push`를 사용한다.
구축 완료 기준은 다음과 같다.
- Python 자식 프로세스가 Codex를 직접 실행하고 필요한 옵션·인증을 사용할 수 있다.
- 실제 CLI 이벤트에서 편집 훅과 Stop 검증이 동작한다.
- 실제 제품 또는 격리 fixture에서 .NET SDK 8 선택, 실제 TFM 평가, restore/build와 테스트가 성공하고
경고·차단 응답이 없으며 현재 TRX에서 실행 건수가 확인된다.
- 실제 phase의 dry-run이 파일·Git·상태를 변경하지 않고, 첫 step 실행 결과와 index가 일치한다.
Harness 자체 검증은 제품 검증과 별도로 실행한다.
```powershell
uv run --with pytest python -m pytest tests/harness -v -rs
uv run --with pytest python -m compileall -q scripts tests/harness
```
SDK/NuGet/CLI 문제로 실제 통합 검증이 skip되면 미검증으로 기록한다. subprocess 대역
테스트 통과와 실제 .NET·CLI 통과, 설치된 도구 확인과 새 환경의 설치 검증을 구분한다.
## 7. 자주 발생하는 문제
| 증상 | 확인 및 조치 |
|---|---|
| `python` 또는 `codex`를 찾지 못함 | 새 터미널의 PATH와 앱 실행 별칭을 확인한다. Python 자식 프로세스 점검까지 통과해야 한다. |
| `unexpected argument` / `unrecognized option` | `codex exec --help`로 실행기의 옵션 지원을 확인한다. |
| 인증 실패 | Harness를 실행하는 동일 Windows 계정과 Codex 환경에서 로그인 상태를 확인한다. |
| 훅이 로드되지만 차단되지 않음 | `/hooks`, 프로젝트·훅 신뢰, 사용자·관리자 설정, `commandWindows`, 자식 PATH와 실제 이벤트·matcher를 확인한다. |
| `dotnet` 누락 / SDK 없음 | runtime만 설치했는지 확인하고 SDK 설치와 PATH를 복구한다. |
| SDK 선택 오류 | 루트 `global.json`, `dotnet --list-sdks`와 루트 `dotnet --version`을 비교한다. 임의 디렉터리의 SDK 선택으로 대체하지 않는다. |
| 실제 TFM 평가 실패 | 같은 Configuration으로 각 `.csproj`의 `TargetFramework`/`TargetFrameworks`를 조회한다. 상속된 props·속성 참조·조건부 값을 확인하고 빈 값이나 .NET 8 이외의 대상을 수정한다. |
| NuGet restore 실패 | 오류 단계의 feed URL·네트워크·인증·패키지 버전을 확인한다. 성공한 테스트로 기록하지 않는다. |
| 여러 target 후보 | `dotnet.target`에 실제 `.sln` 또는 SDK 스타일 `.csproj`를 명시한다. |
| C# orphan / metadata 없음 | C# 소스에 맞는 지원 metadata와 target을 준비한다. exclude로 제품을 숨기지 않는다. |
| 테스트 0건 / skip-only / TRX 누락 | `testProjects`, 테스트 SDK·어댑터, 테스트 발견과 실행 조건을 확인한다. 현재 project별 결과를 확인한다. |
| build 경고로 실패 | nullable·분석기 등 실제 진단을 수정한다. 경고를 숨겨 통과시키지 않는다. |
| `TDD GUARD` 차단 | 대응 테스트를 먼저 추가하고 red 단계 확인 후 구현한다. `Program.cs`도 기본 검사 대상이다. |
| `dubious ownership` | 현재 계정 소유 checkout을 우선한다. 신뢰하는 공유 경로라면 해당 경로만 Git `safe.directory`에 등록한다. |
| 소스 탐색 중 `PermissionError` / 접근 거부 | Windows sandbox에서 생성한 폴더의 ACL과 훅 실행 계정을 확인한다. 일반 실행 계정이 소스·테스트 폴더를 읽을 수 있어야 한다. 필요하면 해당 계정으로 프로젝트 폴더를 먼저 준비한다. 읽기 실패는 대상 없는 템플릿으로 처리하지 않는다. |
| phase 또는 context 파일 누락 | 실제 phase/index/step과 존재하는 참조 파일을 준비한다. 인계 문서를 상태 파일로 사용하지 않는다. |
| build·Codex 시간 초과 | Stop 전체와 각 Codex 호출 제한은 1,800초다. 로그의 실패 단계·argv·cwd를 확인한다. |
재시도, `error`/`blocked` 복구는 [Harness 운영 가이드](HARNESS.md)와
[실행 흐름](HARNESS_WORKFLOW.md)을 따른다.