From c9b7cc62a42763304c4138a8b0e61990969fa384 Mon Sep 17 00:00:00 2001 From: hslee Date: Sat, 3 Oct 2026 13:46:20 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(doctor):=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20=EC=A0=81=EC=9A=A9=EB=90=9C=20=ED=95=98?= =?UTF-8?q?=EB=84=A4=EC=8A=A4=20=EB=B2=84=EC=A0=84=EC=9D=84=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=ED=95=98=EA=B3=A0=20doctor=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B3=B4=EC=97=AC=20=EC=A4=80=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit init이 render-state에 harnessVersion(실행 중인 하네스 package.json version)을 남기고, doctor가 project applied · CLI · plugin 한 줄과 --json versions를 낸다. 적용 버전 < CLI면 init --yes 처방 경고, > CLI면 관리 절 퇴행 위험 경고. 기록 없는 기존 설치본은 unknown으로 보고 경고하지 않는다. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 ++ README.md | 3 +- .../harness-version-stamp-artifact.md | 14 ++++ .../harness-version-stamp-context.md | 27 +++++++ .../harness-version-stamp-handoff.md | 3 + .../harness-version-stamp-meta.json | 11 +++ .../harness-version-stamp-plan.md | 24 ++++++ .../harness-version-stamp-spec.md | 74 +++++++++++++++++++ src/commands/doctor.mjs | 62 ++++++++++++++-- src/harness.mjs | 11 ++- src/render-state.mjs | 18 ++++- tests/doctor.test.mjs | 57 +++++++++++++- tests/render-state.test.mjs | 19 ++++- 13 files changed, 317 insertions(+), 13 deletions(-) create mode 100644 docs/hslee/harness-version-stamp/harness-version-stamp-artifact.md create mode 100644 docs/hslee/harness-version-stamp/harness-version-stamp-context.md create mode 100644 docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md create mode 100644 docs/hslee/harness-version-stamp/harness-version-stamp-meta.json create mode 100644 docs/hslee/harness-version-stamp/harness-version-stamp-plan.md create mode 100644 docs/hslee/harness-version-stamp/harness-version-stamp-spec.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7d72d3..7ec26a08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,13 @@ modified: 2026-09-13 ## [Unreleased] +### Added +- **프로젝트에 적용된 하네스 버전 기록·표시** (task `harness-version-stamp`). `init`이 `.harness/render-state.json`에 + 실행 중인 하네스의 `package.json` version을 `harnessVersion`으로 남기고(타임스탬프 없음 — 같은 버전 재실행은 diff 없음), + `doctor`가 `harness version: project applied X · CLI Y · plugin Z` 한 줄과 `--json` `versions` 필드로 보여 준다. + 적용 버전 < 실행 중 CLI면 `harness-team init --yes`를 처방하는 경고, 적용 버전 > CLI면 관리 절 퇴행 위험 경고를 낸다(차단 없음). + 필드가 없거나 형식이 틀린 기존 설치본은 `unknown (기록 이전 설치)`로 보고 경고하지 않는다. + ## [0.44.5] - 2026-09-28 ### Fixed diff --git a/README.md b/README.md index f951fe85..963a2cdd 100644 --- a/README.md +++ b/README.md @@ -342,6 +342,7 @@ All checks passed. 심볼: `✓` 정상, `✗` 실패(exit 1), `-` 선택 항목 없음(정상). 소비자 프로젝트에서는 PATH의 `harness-team`이 `session-context`·`handoff`·`boundary`를 지원하는지도 경고로 점검합니다. 플러그인 소스 저장소는 소비자 훅을 설치하지 않으므로 이 항목이 n/a로 건너뛰어집니다. +마지막 줄 근처의 `harness version: project applied … · CLI … · plugin …`은 이 프로젝트에 마지막으로 `init`을 적용한 하네스 버전(`.harness/render-state.json`의 `harnessVersion`), doctor를 실행 중인 CLI 버전, 설치된 플러그인 버전을 나란히 보여 줍니다(`--json`은 `versions` 필드). 기록이 없는 설치본은 `unknown (기록 이전 설치)`로 표시되고 경고하지 않습니다 — 다음 `init`부터 기록됩니다. 적용 버전이 실행 중인 CLI보다 낮으면 `harness-team init --yes`를 처방하는 경고를, 높으면 구버전 CLI로 `init`하면 관리 절이 퇴행할 수 있다는 경고를 냅니다(경고만 — `init`을 막지는 않습니다). 관측 로그의 트립와이어가 발화한 상태면 `observe trip wires` 경고 1건(wire id·수치·`harness-team observe` 안내·루프백 nudge)을 냅니다 — warn 수준이라 exit code는 그대로이고, 로그가 없거나 발화가 없으면 항목 자체가 없습니다. ### `/harness-observe` — 관측 로그 스코어카드 · 트립와이어 @@ -755,7 +756,7 @@ cd ~/work/project-a ## 설치 결과물 -설치되는 파일과 task 계약은 scaffold 되는 `AGENTS.md`의 **작업 프로토콜** 및 `templates/`를 확인합니다. 개인 상태 파일은 `.harness/active.json`에 보관됩니다. 반면 백업 클론 폴더 경로를 기억하는 `.harness/backup.json`은 팀이 공유하는 설정이므로 commit을 권장합니다. 관리 절의 마지막 렌더 해시를 담는 `.harness/render-state.json`도 **팀 상태이므로 반드시 commit 합니다** — 커밋하지 않으면 팀원이 clone한 뒤 첫 `init`에서 판정 근거가 없어 관리 절의 사용자 편집을 한 번 덮어씁니다. +설치되는 파일과 task 계약은 scaffold 되는 `AGENTS.md`의 **작업 프로토콜** 및 `templates/`를 확인합니다. 개인 상태 파일은 `.harness/active.json`에 보관됩니다. 반면 백업 클론 폴더 경로를 기억하는 `.harness/backup.json`은 팀이 공유하는 설정이므로 commit을 권장합니다. 관리 절의 마지막 렌더 해시를 담는 `.harness/render-state.json`도 **팀 상태이므로 반드시 commit 합니다** — 커밋하지 않으면 팀원이 clone한 뒤 첫 `init`에서 판정 근거가 없어 관리 절의 사용자 편집을 한 번 덮어씁니다. 이 파일의 `harnessVersion`은 마지막으로 `init`을 적용한 하네스 버전이고(`version`은 파일 스키마 버전), `doctor`가 실행 중인 CLI와 비교합니다. 타임스탬프는 남기지 않으므로 같은 버전으로 `init`을 다시 돌려도 diff가 생기지 않습니다. 자동으로 `.gitignore`에 추가되는 항목(`.harness/`를 통째로 무시하지 않습니다 — `backup.json`·`cursor-mirror.json`·`render-state.json`은 팀 상태): - `.claude/settings.local.json` (개인 권한 오버라이드) diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-artifact.md b/docs/hslee/harness-version-stamp/harness-version-stamp-artifact.md new file mode 100644 index 00000000..140be41f --- /dev/null +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-artifact.md @@ -0,0 +1,14 @@ +# harness-version-stamp — Artifact + +*최종 결과물과 학습 내용을 기록한다.* + +## 결과 + +- 다이어그램: 미실행 — orchestrator 위임 — 소규모 변경 (2026-10-03) + +## Reviews +*Codex 등 리뷰 실행 시 결과(요약·발견·조치)를 날짜와 함께 남긴다. 남기지 않은 리뷰는 "안 한 것"으로 간주.* +*기계 판독용 마커를 함께 남긴다: ``* + + +## Learnings diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-context.md b/docs/hslee/harness-version-stamp/harness-version-stamp-context.md new file mode 100644 index 00000000..566fd217 --- /dev/null +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-context.md @@ -0,0 +1,27 @@ +# harness-version-stamp — Context Card + + +## Now +- Goal: +- Current atomic step: +- Stop / human-decision condition: + +## Constraints and settled decisions +- + +## JIT retrieval map +- Identifiers / symbols: +- Narrow globs: +- Read next: +- Verification command: + +## Failure capsules (max 3 unresolved) +### F-001 +- Signal: +- Tried: +- Compact finding / current hypothesis: +- Next discriminator: +- Source (safe path or command): + +## Resume checklist +- diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md b/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md new file mode 100644 index 00000000..2061612d --- /dev/null +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md @@ -0,0 +1,3 @@ +# harness-version-stamp — Handoff + +(세션 종료 시 post-commit hook이 자동 갱신합니다) diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-meta.json b/docs/hslee/harness-version-stamp/harness-version-stamp-meta.json new file mode 100644 index 00000000..05dd5179 --- /dev/null +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-meta.json @@ -0,0 +1,11 @@ +{ + "user": "hslee", + "task": "harness-version-stamp", + "created": "2026-10-03", + "firstActivatedAt": "2026-10-03T04:42:01.968Z", + "status": "open", + "closedAt": null, + "forcedAt": null, + "forcedIssues": null, + "reviews": [] +} diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-plan.md b/docs/hslee/harness-version-stamp/harness-version-stamp-plan.md new file mode 100644 index 00000000..6240a9af --- /dev/null +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-plan.md @@ -0,0 +1,24 @@ +# harness-version-stamp — Plan + +## 목표 +init이 render-state에 적용 하네스 버전(`harnessVersion`)을 기록하고, doctor가 project·CLI·plugin 버전을 +한 줄로 보여 주며 project ≠ CLI면 방향별 경고를 낸다. + +## 단계 +- [x] spec/plan 다이어그램 — 미실행(orchestrator 위임 — 소규모 변경) +- [x] render-state: `readHarnessVersion` + `loadRenderState`의 `harnessVersion` semver 필터 +- [x] planChanges: renderState에 `harnessVersion` 기록 (init 저장 경로 재사용) +- [x] doctor: `compareVersions`·`harnessVersionReport`·`readInstalledHarnessVersion` + 출력/JSON/next_actions +- [x] 테스트: init 후 기록 / 필드 없음 unknown / 경고 작음·같음·큼 / 잘못된 형식 폐기 +- [x] README: render-state 설명·doctor 절 갱신, `npm run docs:check` +- [x] `npm run test` 통과 +- [ ] codex read-only 리뷰 → artifact ## Reviews 기록·반영 +- [ ] ship: spec·plan·artifact 최종 갱신, push, PR 생성 + +## Ontology 변경 로그 +*개념이 새로 정의되거나 의미가 바뀌면 한 줄로 기록. spec.md의 Ontology 섹션을 갱신할 트리거가 된다.* + +- 2026-10-03: applied version(`harnessVersion`) 신설 — render-state 스키마 `version`과 구분. + +## 참고 +- 후속 후보: SessionStart 훅 nudge(기록 < CLI일 때 한 줄) — 이번 범위 밖. diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-spec.md b/docs/hslee/harness-version-stamp/harness-version-stamp-spec.md new file mode 100644 index 00000000..e3a73668 --- /dev/null +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-spec.md @@ -0,0 +1,74 @@ +# harness-version-stamp — Spec + +## 목적 / 요구사항 +*문제(오늘 무엇이 안 되는가) → 영향받는 사용자·시스템 → 기대 결과 → 제약 순으로 쓴다. +답 없는 질문은 요구가 아니다 — `## 참고` 절에 `- (open) …`으로 남긴다.* + +- **문제**: 소비자 프로젝트에 "마지막으로 init을 돌린 하네스 버전"이 기록되지 않는다. 확인 수단은 + `harness-team --version`(PATH CLI)과 doctor의 `cliDriftWarning`(PATH CLI vs 설치 플러그인, 불일치일 + 때만 경고)뿐이고, 둘 다 머신 상태지 프로젝트 상태가 아니다. `.harness/render-state.json`의 + `"version": 1`은 스키마 버전이다. +- **영향**: 소비자 프로젝트 팀원 — 프로젝트가 낡은 관리 절로 남았는지, 반대로 더 새 하네스로 적용된 + 프로젝트에 구버전 CLI로 init 하려는지 알 수 없다. +- **기대 결과**: + 1. `init`이 render-state를 저장할 때 실행 중인 하네스의 `package.json` version을 `harnessVersion`으로 쓴다. + 2. `doctor`(사람용 + `--json`)가 `project applied X · CLI Y · plugin Z` 한 줄을 보여 준다. + 기록 없음은 `unknown (기록 이전 설치)`. + 3. 기록 < 실행 중 CLI → warning + next_actions `harness-team init --yes`. + 기록 > 실행 중 CLI → warning(구버전 CLI init은 관리 절 퇴행 위험). +- **제약**: 새 의존성 금지. 타임스탬프(`appliedAt`)를 넣지 않는다(매 init diff 유발). init 차단은 범위 밖 — + 경고만. SessionStart 훅 nudge는 범위 밖(후속 후보). src/ 수정 파일 5개 이하. + +## 설계 / 접근 + +- `src/render-state.mjs` + - `readHarnessVersion(root)` — `/package.json`의 version(semver 형식일 때만, 아니면 null). + - `loadRenderState`는 `harnessVersion`을 #113 `stack` 선례처럼 **semver 형식일 때만** 통과시킨다. + 형식이 틀리거나 없으면 필드를 빼서 "기록 이전 설치"와 같게 본다. +- `src/harness.mjs` `planChanges` — renderState 구성에 `harnessVersion`(= `readHarnessVersion(ctx.root)`)을 넣는다. + init은 이 renderState를 Apply 뒤 저장하므로 `src/commands/init.mjs`는 수정이 필요 없다(저장 경로 그대로). +- `src/commands/doctor.mjs` + - `compareVersions(a, b)` — major.minor.patch 3-way(-1/0/1), 형식이 아니면 null. `isAtLeast`와 같은 파일. + - `harnessVersionReport({ applied, cli, plugin, pluginDev })` — 순수 함수. `{ line, warning, action }`. + - `checkCliDrift`의 installed_plugins.json 읽기를 `readInstalledHarnessVersion(env)`로 떼어 재사용한다. + - 출력: check `harness version`(일치·unknown은 pass, 불일치는 warning) + JSON `extra.versions` + `{ project, cli, plugin }`(모르면 null). + - plugin-dev 저장소는 init 대상이 아니므로 project를 `n/a (plugin-dev repo)`로 표시하고 경고하지 않는다. + - 기록 > CLI 경고의 next_action은 기존 CLI 갱신 명령(`cliDriftAction`)을 재사용한다. + +## Ontology +*이 task가 다루는 핵심 개념의 정의. "X가 정확히 뭔가?"에 답한다.* + +- **applied version (`harnessVersion`)**: 이 프로젝트에 마지막으로 init(Apply까지 완료)을 실행한 하네스의 + `package.json` version. render-state.json에 저장. 스키마 버전 `version`과 별개. +- **CLI version**: doctor를 실행 중인 하네스 코드(`ctx.root/package.json`)의 version. PATH CLI와 다를 수 있다 + (그 불일치는 기존 `global CLI version drift`가 담당). +- **plugin version**: `installed_plugins.json`의 harness 레코드 version(`installedHarnessVersion`). +- **기록 이전 설치**: render-state에 `harnessVersion`이 없거나 형식이 틀린 설치본 — unknown, 경고 없음. +- 게이트 근거: 목표·제약·성공기준·영향 파일이 사용자 승인 계획으로 확정됨(2026-10-03 orchestrator 위임). + +## Ambiguity 자가진단 +*각 항목이 명확하면 체크. 3개 이상 미체크면 구현 진입 금지 — 인터뷰/브레인스토밍으로 복귀해 +모호성을 제거한다. 게이트를 통과하면 그 근거를 위 Ontology 섹션에 한 줄로 남긴다.* + +- [x] **Goal 명확도** (40%) — 목표가 한 문장으로 구체화되었는가? +- [x] **Constraint 명확도** (30%) — 기술/시간/범위 제약이 명시되었는가? +- [x] **Success 기준** (30%) — 완료를 어떻게 측정하는가? +- [x] **Context 명확도** (brownfield 한정) — 영향 받는 기존 코드/파일을 식별했는가? +- [x] **Ambiguity ≤ 0.2** — 위 항목 가중합 ≥ 0.8 + + +## Done evidence +```json +{ "version": 1, "review": "required", "tests": "required" } +``` + +## 참고 +*코드 기반 참조가 산문 설계보다 정밀하다 — 테스트 스위트·Boundary contract(JSON Schema)· +다이어그램·기존 코드 경로를 우선 링크하고, 산문은 코드로 표현 못 하는 의도만 담는다.* + +- `src/render-state.mjs` `loadRenderState` — `stack` 필드 선례(#113) +- `src/commands/doctor.mjs` `isAtLeast`·`cliDriftWarning`·`installedHarnessVersion` +- 테스트: `tests/render-state.test.mjs`, `tests/doctor.test.mjs`, `tests/cli-drift.test.mjs` diff --git a/src/commands/doctor.mjs b/src/commands/doctor.mjs index 9853307c..ba6fcd8d 100644 --- a/src/commands/doctor.mjs +++ b/src/commands/doctor.mjs @@ -7,7 +7,7 @@ import { exists, readTextSafe } from '../fsx.mjs'; import { loadBackupDir, settingsHasBoundaryCheckpoint, codexHooksHaveSessionContext, isHarnessCodexSessionCommand, AGENT_FILE_TEMPLATES } from '../harness.mjs'; import { render } from '../render.mjs'; import { resolveStack } from '../detect-stack.mjs'; -import { loadRenderState } from '../render-state.mjs'; +import { loadRenderState, readHarnessVersion } from '../render-state.mjs'; import { mergeMarkdown, extractSections } from '../merge.mjs'; import { buildEnvelope, buildErrorPacket, emitObservation } from '../observation.mjs'; import { settingsHasSessionGate } from './session-context.mjs'; @@ -137,6 +137,38 @@ function isAtLeast(version, floor) { return true; } +// 3-way major.minor.patch comparison (-1 / 0 / 1), null when either side is not +// semver-shaped. Prerelease is ignored for the same reason as isAtLeast: the +// question is which release line applied the project, not rc ordering. +export function compareVersions(a, b) { + const pa = String(a ?? '').match(/^(\d+)\.(\d+)\.(\d+)/); + const pb = String(b ?? '').match(/^(\d+)\.(\d+)\.(\d+)/); + if (!pa || !pb) return null; + for (let i = 1; i <= 3; i++) { + const d = Number(pa[i]) - Number(pb[i]); + if (d) return d > 0 ? 1 : -1; + } + return 0; +} + +// The project's applied harness (render-state `harnessVersion`, stamped by init) +// against the CLI running this doctor. The plugin record is shown for context +// only — PATH-vs-plugin disagreement is cliDriftWarning's job. Unknown (an install +// that predates the stamp) is reported but not warned: nothing to compare. +// plugin-dev is the source, not an install, so it has no applied version. +export function harnessVersionReport({ applied, cli, plugin, pluginDev = false }) { + const project = pluginDev ? 'n/a (plugin-dev repo)' : (applied ?? 'unknown (기록 이전 설치)'); + const line = `project applied ${project} · CLI ${cli ?? 'unknown'} · plugin ${plugin ?? 'unknown'}`; + const order = pluginDev ? null : compareVersions(applied, cli); + if (order === -1) { + return { line, kind: 'behind', warning: `프로젝트에 적용된 하네스 ${applied}가 실행 중인 CLI ${cli}보다 낡음 — \`harness-team init --yes\`로 관리 절·스캐폴드를 갱신` }; + } + if (order === 1) { + return { line, kind: 'ahead', warning: `프로젝트에 적용된 하네스 ${applied}가 실행 중인 CLI ${cli}보다 새것 — 이 CLI로 init 하면 관리 절이 구버전으로 퇴행할 수 있다; init 전에 CLI를 갱신` }; + } + return { line, kind: null, warning: null }; +} + // Four outcomes. `missing` and `legacy` need different fixes — install the CLI // vs refresh the source it points at — and `unknown` exists so a CLI that never // got to answer is not dated by its silence. A timeout, a missing interpreter @@ -210,14 +242,16 @@ export function cliDriftAction(env = process.env) { return `git -C "${join(pluginsRoot, 'marketplaces', HOOK_CLI_MARKETPLACE_DIR)}" pull`; } -export async function checkCliDrift(env = process.env) { +export async function readInstalledHarnessVersion(env = process.env) { const pluginsRoot = env.CLAUDE_PLUGINS_ROOT ?? join(homedir(), '.claude/plugins'); const installedPath = join(pluginsRoot, 'installed_plugins.json'); const raw = await readFile(installedPath, 'utf8').catch(() => null); if (!raw) return null; - let installed; - try { installed = JSON.parse(raw); } catch { return null; } - const installedVersion = installedHarnessVersion(installed); + try { return installedHarnessVersion(JSON.parse(raw)); } catch { return null; } +} + +export async function checkCliDrift(env = process.env) { + const installedVersion = await readInstalledHarnessVersion(env); if (!installedVersion) return null; return cliDriftWarning({ pathCli: await readPathCliVersion(env), @@ -1016,6 +1050,19 @@ export async function runDoctor(ctx) { add('global CLI version drift', 'warning', driftWarning, `\n⚠️ ${driftWarning}`); } + const versions = { + project: pluginDev ? null : ((await loadRenderState(ctx.targetDir)).harnessVersion ?? null), + cli: await readHarnessVersion(ctx.root), + plugin: await readInstalledHarnessVersion(), + }; + const versionReport = harnessVersionReport({ applied: versions.project, cli: versions.cli, plugin: versions.plugin, pluginDev }); + if (versionReport.warning) { + add('harness version', 'warning', `${versionReport.line}; ${versionReport.warning}`, + `\nharness version: ${versionReport.line}\n⚠️ ${versionReport.warning}`); + } else { + add('harness version', 'pass', versionReport.line, `\nharness version: ${versionReport.line}`); + } + if (json) { const warnCount = warnings; const skipCount = checks.filter(c => c.status === 'skip').length; @@ -1044,7 +1091,8 @@ export async function runDoctor(ctx) { if (staleTemplates.length) warnActions.push('harness-team migrate'); if (jqMissing) warnActions.push(jqInstallAction()); if (!pluginDev && !hookCliOk) warnActions.push(hookCliInstall); - if (driftWarning) warnActions.push(cliDriftAction()); + if (versionReport.kind === 'behind') warnActions.push('harness-team init --yes'); + if (driftWarning || versionReport.kind === 'ahead') warnActions.push(cliDriftAction()); emitObservation(buildEnvelope({ command: 'doctor', status, @@ -1060,7 +1108,7 @@ export async function runDoctor(ctx) { safeDefault: 'doctor는 읽기 전용이다 — 아무것도 고치지 않고 실패 목록만 남긴다', stop: '필수 파일/스크립트 누락이면 harness-team init 또는 migrate로 복구', }) : null, - extra: { checks, mode: pluginDev ? 'plugin-dev' : 'project' }, + extra: { checks, mode: pluginDev ? 'plugin-dev' : 'project', versions }, })); } else { // "All checks passed" after a printed ⚠️ contradicts the lines above it — diff --git a/src/harness.mjs b/src/harness.mjs index dee5d39e..a4213869 100644 --- a/src/harness.mjs +++ b/src/harness.mjs @@ -6,7 +6,7 @@ import { render } from './render.mjs'; import { backupDirFromConfig } from './backup-dir.mjs'; import { mergeMarkdown, deepMergeJson, simpleDiff } from './merge.mjs'; import { stackPermissions, RN_STACK_IDS } from './settings-permissions.mjs'; -import { loadRenderState, sectionHashes } from './render-state.mjs'; +import { loadRenderState, sectionHashes, readHarnessVersion } from './render-state.mjs'; import { USER_HANDOFF_IGNORE } from './task-paths.mjs'; export const DEFAULT_BACKUP_PARENT = 'harness-backup'; @@ -180,7 +180,14 @@ export async function planChanges(ctx, { stack, stackPin }) { // 이전 상태를 기본값으로 깔고 시작한다. 루프가 어떤 이유로든 파일을 건너뛰어도 // (symlink 레거시 · 마커 깨짐 · 템플릿 없음 → continue) 그 파일의 해시가 통째로 // 지워지지 않는다 — 지우면 다음 실행이 부트스트랩으로 판정해 덮는다. - const renderState = { version: 1, ...(stackPin ? { stack: stackPin } : {}), sections: { ...priorState.sections } }; + // harnessVersion은 이번 실행의 버전으로 갈아끼운다 — "마지막으로 Apply한 하네스"가 그 뜻이다. + const harnessVersion = await readHarnessVersion(root); + const renderState = { + version: 1, + ...(stackPin ? { stack: stackPin } : {}), + ...(harnessVersion ? { harnessVersion } : {}), + sections: { ...priorState.sections }, + }; // Each agent file is marker-merged independently: managed sections updated, // user text preserved. diff --git a/src/render-state.mjs b/src/render-state.mjs index c806d1b4..796078fb 100644 --- a/src/render-state.mjs +++ b/src/render-state.mjs @@ -18,6 +18,9 @@ export const RENDER_STATE_REL = '.harness/render-state.json'; // `stack`(선택): `init --stack X`로 감지와 다른 스택을 강제했을 때의 X. 관리 절 렌더 입력 중 // 디스크에서 다시 얻을 수 없는 유일한 값이라 여기 남긴다 — 없으면 플래그 없는 init·doctor·migrate가 // 감지 스택으로 렌더해 강제 선택을 되돌린다. 필드가 없으면(기존 설치본) 감지 스택을 쓴다. +// `harnessVersion`(선택): 마지막으로 init을 Apply한 하네스의 package.json version. `version`은 이 파일의 +// 스키마 버전이라 별개다. doctor가 실행 중인 CLI와 비교한다. 필드가 없으면 기록 이전 설치본이다. +// 타임스탬프는 두지 않는다 — 같은 버전으로 init을 다시 돌려도 diff가 생기지 않아야 한다. const EMPTY = () => ({ version: 1, sections: {} }); // 마커를 포함한 블록 전체를 해싱한다 — mergeMarkdown이 교체하는 단위가 바로 그 블록이다. @@ -37,10 +40,23 @@ export async function loadRenderState(targetDir) { if (!data || typeof data !== 'object' || typeof data.sections !== 'object' || data.sections === null) return EMPTY(); // 모르는 id(손 편집·오타)는 버린다 — resolveStack은 모르는 id를 generic으로 렌더한다. const stack = KNOWN_STACK_IDS.includes(data.stack) ? { stack: data.stack } : {}; - return { version: data.version ?? 1, ...stack, sections: data.sections }; + // 형식이 아닌 값(손 편집)도 버린다 — 비교할 수 없는 값은 기록 없음과 같다. + const harnessVersion = isSemver(data.harnessVersion) ? { harnessVersion: data.harnessVersion } : {}; + return { version: data.version ?? 1, ...stack, ...harnessVersion, sections: data.sections }; } catch { return EMPTY(); } } +const SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/; +const isSemver = (v) => typeof v === 'string' && SEMVER.test(v); + +// 실행 중인 하네스(root = 플러그인 루트)의 package.json version. 읽을 수 없거나 형식이 아니면 null. +export async function readHarnessVersion(root) { + try { + const { version } = JSON.parse(await readTextSafe(join(root, 'package.json'))); + return isSemver(version) ? version : null; + } catch { return null; } +} + // 원자적으로 쓴다. 중간에 끊기면 깨진 JSON이 남고, loadRenderState는 그것을 빈 부트스트랩 // 상태로 읽어 다음 init이 관리 절을 덮는다 — 실패 모드가 "보호 해제"라 조용하고 위험하다 // (codex 리뷰 P2). 임시 파일에 쓴 뒤 rename으로 갈아끼운다(같은 디렉터리라 원자적이다). diff --git a/tests/doctor.test.mjs b/tests/doctor.test.mjs index 3416dad5..6b1fa702 100644 --- a/tests/doctor.test.mjs +++ b/tests/doctor.test.mjs @@ -6,7 +6,7 @@ import { mkdtemp, mkdir, writeFile, readFile, rm, symlink, chmod } from 'node:fs import { tmpdir, homedir } from 'node:os'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; -import { classifyHookCommand, collectHookCommands, redactCommand, checkCommand, checkSelfCli, checkHookCli, hookCliInstallCommand, HOOK_CLI_MARKETPLACE_DIR, checkActiveSpecGate, checkActiveDoneOnMain, detectLegacyStructure, checkSessionStartHook, checkBoundaryCheckpointHook, checkDecisionLog, DECISION_HEADINGS, checkObserveTripWires, checkEagerTierSize, globalClaudeMdPath, EAGER_TIER_MAX_BYTES, isPluginDevRepo, jqFallbackGaps, jqInstallAction, JQ_FALLBACK_MARKER, findStaleManagedSections } from '../src/commands/doctor.mjs'; +import { classifyHookCommand, collectHookCommands, redactCommand, checkCommand, checkSelfCli, checkHookCli, hookCliInstallCommand, HOOK_CLI_MARKETPLACE_DIR, checkActiveSpecGate, checkActiveDoneOnMain, detectLegacyStructure, checkSessionStartHook, checkBoundaryCheckpointHook, checkDecisionLog, DECISION_HEADINGS, checkObserveTripWires, checkEagerTierSize, globalClaudeMdPath, EAGER_TIER_MAX_BYTES, isPluginDevRepo, jqFallbackGaps, jqInstallAction, JQ_FALLBACK_MARKER, findStaleManagedSections, compareVersions, harnessVersionReport } from '../src/commands/doctor.mjs'; import { render } from '../src/render.mjs'; import { detectStack } from '../src/detect-stack.mjs'; import { sectionHashes } from '../src/render-state.mjs'; @@ -1208,3 +1208,58 @@ test('findStaleManagedSections: init --stack 으로 감지와 다른 스택을 assert.deepEqual(await findStaleManagedSections(dir, ROOT), []); } finally { await rm(dir, { recursive: true, force: true }); } }); + +test('compareVersions: major.minor.patch 3-way, 형식이 아니면 null', () => { + assert.equal(compareVersions('0.44.4', '0.44.5'), -1); + assert.equal(compareVersions('0.44.5', '0.44.5'), 0); + assert.equal(compareVersions('0.45.0', '0.44.10'), 1); + assert.equal(compareVersions('0.44.5-rc.1', '0.44.5'), 0, 'prerelease 는 무시한다'); + assert.equal(compareVersions(undefined, '0.44.5'), null); + assert.equal(compareVersions('latest', '0.44.5'), null); +}); + +test('harnessVersionReport: 기록 < CLI 는 init 경고, 같으면 침묵, 기록 > CLI 는 퇴행 경고', () => { + const behind = harnessVersionReport({ applied: '0.44.0', cli: '0.44.5', plugin: '0.44.5' }); + assert.equal(behind.line, 'project applied 0.44.0 · CLI 0.44.5 · plugin 0.44.5'); + assert.equal(behind.kind, 'behind'); + assert.match(behind.warning, /harness-team init --yes/); + const same = harnessVersionReport({ applied: '0.44.5', cli: '0.44.5', plugin: null }); + assert.equal(same.warning, null); + assert.match(same.line, /plugin unknown$/); + const ahead = harnessVersionReport({ applied: '0.45.0', cli: '0.44.5', plugin: '0.45.0' }); + assert.equal(ahead.kind, 'ahead'); + assert.match(ahead.warning, /퇴행/); +}); + +test('harnessVersionReport: 기록 없는 설치본은 unknown 으로 보이고 경고하지 않는다 (plugin-dev 는 n/a)', () => { + const legacy = harnessVersionReport({ applied: null, cli: '0.44.5', plugin: '0.44.5' }); + assert.match(legacy.line, /^project applied unknown \(기록 이전 설치\) · /); + assert.equal(legacy.warning, null); + const dev = harnessVersionReport({ applied: '0.1.0', cli: '0.44.5', plugin: null, pluginDev: true }); + assert.match(dev.line, /^project applied n\/a \(plugin-dev repo\)/); + assert.equal(dev.warning, null); +}); + +test('init 이 harnessVersion 을 기록하고 doctor 가 CLI 보다 낡은 기록을 init --yes 로 처방한다', async () => { + const base = await mkdtemp(join(tmpdir(), 'harness-doctor-hv-')); + const dir = join(base, 'p'); + try { + await mkdir(dir); + await pexec('node', [join(ROOT, 'bin/harness-team.mjs'), 'init', '--yes', '--backup-dir', join(base, 'bk')], { cwd: dir, timeout: 20000 }); + const { version } = JSON.parse(await readFile(join(ROOT, 'package.json'), 'utf8')); + const statePath = join(dir, '.harness/render-state.json'); + const state = JSON.parse(await readFile(statePath, 'utf8')); + assert.equal(state.harnessVersion, version); + + // 플러그인 레코드는 머신 상태라 빈 루트로 고정한다. + const env = { ...process.env, CLAUDE_PLUGINS_ROOT: base }; + const same = await doctorJson(dir, env); + assert.deepEqual(same.versions, { project: version, cli: version, plugin: null }); + assert.equal(checkOf(same, 'harness version').status, 'pass'); + + await writeFile(statePath, JSON.stringify({ ...state, harnessVersion: '0.0.1' })); + const behind = await doctorJson(dir, env); + assert.equal(checkOf(behind, 'harness version').status, 'warning'); + assert.ok(behind.next_actions.includes('harness-team init --yes'), JSON.stringify(behind.next_actions)); + } finally { await rm(base, { recursive: true, force: true }); } +}); diff --git a/tests/render-state.test.mjs b/tests/render-state.test.mjs index 253785c7..55fa0055 100644 --- a/tests/render-state.test.mjs +++ b/tests/render-state.test.mjs @@ -4,7 +4,7 @@ import { mkdtemp, mkdir, writeFile, readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { createHash } from 'node:crypto'; -import { sectionHashes, loadRenderState, saveRenderState, RENDER_STATE_REL } from '../src/render-state.mjs'; +import { sectionHashes, loadRenderState, saveRenderState, readHarnessVersion, RENDER_STATE_REL } from '../src/render-state.mjs'; const BLOCK = '\n- npm\n'; const DOC = `# T\n\n${BLOCK}\n\ntail\n`; @@ -59,3 +59,20 @@ test('loadRenderState: stack 은 알려진 id 만 남긴다 (모르는 id 는 ge await saveRenderState(dir, { version: 1, stack: 'reakt', sections: {} }); assert.deepEqual(await loadRenderState(dir), { version: 1, sections: {} }); }); + +test('loadRenderState: harnessVersion 은 semver 형식만 남긴다 (없거나 틀리면 기록 이전 설치본)', async () => { + const dir = await mkdtemp(join(tmpdir(), 'harness-rs-hv-')); + await saveRenderState(dir, { version: 1, harnessVersion: '0.44.5', sections: {} }); + assert.equal((await loadRenderState(dir)).harnessVersion, '0.44.5'); + await saveRenderState(dir, { version: 1, harnessVersion: 'latest', sections: {} }); + assert.deepEqual(await loadRenderState(dir), { version: 1, sections: {} }); + await saveRenderState(dir, { version: 1, harnessVersion: 44, sections: {} }); + assert.deepEqual(await loadRenderState(dir), { version: 1, sections: {} }); +}); + +test('readHarnessVersion: 플러그인 루트 package.json 의 version, 없으면 null', async () => { + const root = join(import.meta.dirname, '..'); + const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')); + assert.equal(await readHarnessVersion(root), pkg.version); + assert.equal(await readHarnessVersion(await mkdtemp(join(tmpdir(), 'harness-rs-nopkg-'))), null); +}); From 72dab4b2b58da647ccad5e7828a6994639d90bf5 Mon Sep 17 00:00:00 2001 From: hslee Date: Sat, 3 Oct 2026 13:58:30 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix(render-state):=20prerelease=EC=99=80=20?= =?UTF-8?q?build=20metadata=EB=A5=BC=20=ED=95=A8=EA=BB=98=20=EC=93=B4=20se?= =?UTF-8?q?mver=EB=8F=84=20harnessVersion=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit codex 리뷰 P2 반영 — 문자 클래스에 +가 없어 0.44.5-rc.1+build.7을 거부했다. artifact에 결과·리뷰·학습을 기록하고 context card를 ship 단계로 갱신한다. Co-Authored-By: Claude Opus 5.5 --- .../harness-version-stamp-artifact.md | 17 ++++++++++++ .../harness-version-stamp-context.md | 26 ++++++++----------- .../harness-version-stamp-handoff.md | 16 ++++++++++++ .../harness-version-stamp-plan.md | 2 +- src/render-state.mjs | 2 +- tests/render-state.test.mjs | 2 ++ 6 files changed, 48 insertions(+), 17 deletions(-) diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-artifact.md b/docs/hslee/harness-version-stamp/harness-version-stamp-artifact.md index 140be41f..5f532e71 100644 --- a/docs/hslee/harness-version-stamp/harness-version-stamp-artifact.md +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-artifact.md @@ -5,10 +5,27 @@ ## 결과 - 다이어그램: 미실행 — orchestrator 위임 — 소규모 변경 (2026-10-03) +- 2026-10-03: `init`이 `.harness/render-state.json`에 `harnessVersion`(실행 중 하네스 package.json version)을 기록한다 + (`src/harness.mjs` planChanges — init 저장 경로 재사용이라 `src/commands/init.mjs`는 무수정). + `loadRenderState`는 semver 형식만 통과(#113 `stack` 선례). doctor는 `harness version: project applied X · CLI Y · plugin Z` + check와 `--json` `versions`를 낸다. 적용 < CLI → warning + `harness-team init --yes`, 적용 > CLI → warning + CLI 갱신 명령 + (`cliDriftAction` 재사용). 기록 없음은 `unknown (기록 이전 설치)` · 경고 없음, plugin-dev는 `n/a`. +- src 수정 파일 3개(render-state·harness·doctor). 테스트: render-state 2·doctor 4 추가. README·CHANGELOG [Unreleased] 갱신. +- 검증: `npm run test` 1078 pass / 0 fail / 1 skip (+perf 1 pass), `npm run docs:check` 최신, + 임시 소비자 프로젝트에서 init→doctor 실측(일치 pass, 0.40.0으로 낮추면 warning + `init --yes`). +- 후속 후보: SessionStart 훅 nudge(적용 < CLI일 때 한 줄) — 이번 범위 밖. ## Reviews *Codex 등 리뷰 실행 시 결과(요약·발견·조치)를 날짜와 함께 남긴다. 남기지 않은 리뷰는 "안 한 것"으로 간주.* *기계 판독용 마커를 함께 남긴다: ``* +### 2026-10-03 — codex read-only (`codex exec --sandbox read-only`, origin/main...c9b7cc6) + +- 요약: 결함 1건(P2). +- **P2** `src/render-state.mjs` SEMVER 정규식이 prerelease와 build metadata를 함께 쓴 유효 semver(`0.44.5-rc.1+build.7`)를 + 거부 → 그런 CLI는 `harnessVersion`을 기록 못 하고 doctor가 unknown으로 보며 비교 경고를 놓친다. + - 조치: 재현 확인 후 `(?:-…)?(?:\+…)?`로 분리, render-state 테스트에 동시 사용 케이스 추가. + ## Learnings +- `codex exec`는 stdin이 열려 있으면 "Reading additional input from stdin..."에서 무한 대기한다 — 비대화형 호출은 `< /dev/null`. diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-context.md b/docs/hslee/harness-version-stamp/harness-version-stamp-context.md index 566fd217..c48b421b 100644 --- a/docs/hslee/harness-version-stamp/harness-version-stamp-context.md +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-context.md @@ -2,26 +2,22 @@ ## Now -- Goal: -- Current atomic step: -- Stop / human-decision condition: +- Goal: init이 render-state에 harnessVersion을 기록하고 doctor가 project·CLI·plugin 버전을 보여 주며 방향별 경고. +- Current atomic step: ship — push 후 PR 생성(base main). 머지·release·done·summary --write는 하지 않는다. +- Stop / human-decision condition: src 수정 파일 5개 초과 또는 설계 변경 필요 시. ## Constraints and settled decisions -- +- 타임스탬프 없음(재실행 diff 방지). init 차단 없음 — 경고만. 새 의존성 금지. +- plugin-dev 저장소는 project `n/a`·경고 없음. 적용 > CLI 경고 next_action은 `cliDriftAction` 재사용. ## JIT retrieval map -- Identifiers / symbols: -- Narrow globs: -- Read next: -- Verification command: +- Identifiers / symbols: readHarnessVersion, harnessVersion, compareVersions, harnessVersionReport, readInstalledHarnessVersion +- Narrow globs: src/render-state.mjs, src/harness.mjs, src/commands/doctor.mjs +- Read next: tests/render-state.test.mjs, tests/doctor.test.mjs (말미) +- Verification command: npm run test && npm run docs:check ## Failure capsules (max 3 unresolved) -### F-001 -- Signal: -- Tried: -- Compact finding / current hypothesis: -- Next discriminator: -- Source (safe path or command): +- (none) ## Resume checklist -- +- codex 리뷰 P2 반영 완료 → artifact ## Reviews 참조. diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md b/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md index 2061612d..6d9ff3e9 100644 --- a/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md @@ -1,3 +1,19 @@ # harness-version-stamp — Handoff (세션 종료 시 post-commit hook이 자동 갱신합니다) + +## 2026-10-03T04:46:20.649Z — c9b7cc6 feat(doctor): 프로젝트에 적용된 하네스 버전을 기록하고 doctor에서 보여 준다 +CHANGELOG.md | 7 ++ + README.md | 3 +- + .../harness-version-stamp-artifact.md | 14 ++++ + .../harness-version-stamp-context.md | 27 ++++++++ + .../harness-version-stamp-handoff.md | 3 + + .../harness-version-stamp-meta.json | 11 ++++ + .../harness-version-stamp-plan.md | 24 +++++++ + .../harness-version-stamp-spec.md | 74 ++++++++++++++++++++++ + src/commands/doctor.mjs | 62 ++++++++++++++++-- + src/harness.mjs | 11 +++- + src/render-state.mjs | 18 +++++- + tests/doctor.test.mjs | 57 ++++++++++++++++- + tests/render-state.test.mjs | 19 +++++- + 13 files changed, 317 insertions(+), 13 deletions(-) diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-plan.md b/docs/hslee/harness-version-stamp/harness-version-stamp-plan.md index 6240a9af..9cdefc06 100644 --- a/docs/hslee/harness-version-stamp/harness-version-stamp-plan.md +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-plan.md @@ -12,7 +12,7 @@ init이 render-state에 적용 하네스 버전(`harnessVersion`)을 기록하 - [x] 테스트: init 후 기록 / 필드 없음 unknown / 경고 작음·같음·큼 / 잘못된 형식 폐기 - [x] README: render-state 설명·doctor 절 갱신, `npm run docs:check` - [x] `npm run test` 통과 -- [ ] codex read-only 리뷰 → artifact ## Reviews 기록·반영 +- [x] codex read-only 리뷰 → artifact ## Reviews 기록·반영 - [ ] ship: spec·plan·artifact 최종 갱신, push, PR 생성 ## Ontology 변경 로그 diff --git a/src/render-state.mjs b/src/render-state.mjs index 796078fb..a96d7639 100644 --- a/src/render-state.mjs +++ b/src/render-state.mjs @@ -46,7 +46,7 @@ export async function loadRenderState(targetDir) { } catch { return EMPTY(); } } -const SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/; +const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; const isSemver = (v) => typeof v === 'string' && SEMVER.test(v); // 실행 중인 하네스(root = 플러그인 루트)의 package.json version. 읽을 수 없거나 형식이 아니면 null. diff --git a/tests/render-state.test.mjs b/tests/render-state.test.mjs index 55fa0055..eb8eabb0 100644 --- a/tests/render-state.test.mjs +++ b/tests/render-state.test.mjs @@ -64,6 +64,8 @@ test('loadRenderState: harnessVersion 은 semver 형식만 남긴다 (없거나 const dir = await mkdtemp(join(tmpdir(), 'harness-rs-hv-')); await saveRenderState(dir, { version: 1, harnessVersion: '0.44.5', sections: {} }); assert.equal((await loadRenderState(dir)).harnessVersion, '0.44.5'); + await saveRenderState(dir, { version: 1, harnessVersion: '0.44.5-rc.1+build.7', sections: {} }); + assert.equal((await loadRenderState(dir)).harnessVersion, '0.44.5-rc.1+build.7', 'prerelease+build 동시 사용도 유효한 semver'); await saveRenderState(dir, { version: 1, harnessVersion: 'latest', sections: {} }); assert.deepEqual(await loadRenderState(dir), { version: 1, sections: {} }); await saveRenderState(dir, { version: 1, harnessVersion: 44, sections: {} }); From 26595f5cd7e78f97d94b1e40d0b97ee4a130dba9 Mon Sep 17 00:00:00 2001 From: hslee Date: Sat, 3 Oct 2026 13:58:59 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore(task):=20harness-version-stamp=20ship?= =?UTF-8?q?=20=E2=80=94=20plan=C2=B7artifact=C2=B7handoff=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0=20(PR=20#118)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../harness-version-stamp-artifact.md | 1 + .../harness-version-stamp-context.md | 2 +- .../harness-version-stamp-handoff.md | 9 +++++++++ .../harness-version-stamp/harness-version-stamp-plan.md | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-artifact.md b/docs/hslee/harness-version-stamp/harness-version-stamp-artifact.md index 5f532e71..8544c7fd 100644 --- a/docs/hslee/harness-version-stamp/harness-version-stamp-artifact.md +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-artifact.md @@ -14,6 +14,7 @@ - 검증: `npm run test` 1078 pass / 0 fail / 1 skip (+perf 1 pass), `npm run docs:check` 최신, 임시 소비자 프로젝트에서 init→doctor 실측(일치 pass, 0.40.0으로 낮추면 warning + `init --yes`). - 후속 후보: SessionStart 훅 nudge(적용 < CLI일 때 한 줄) — 이번 범위 밖. +- PR: https://github.com/bd-makers/team-harness/pull/118 (2026-10-03, 머지 전) ## Reviews *Codex 등 리뷰 실행 시 결과(요약·발견·조치)를 날짜와 함께 남긴다. 남기지 않은 리뷰는 "안 한 것"으로 간주.* diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-context.md b/docs/hslee/harness-version-stamp/harness-version-stamp-context.md index c48b421b..e174d7e3 100644 --- a/docs/hslee/harness-version-stamp/harness-version-stamp-context.md +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-context.md @@ -3,7 +3,7 @@ ## Now - Goal: init이 render-state에 harnessVersion을 기록하고 doctor가 project·CLI·plugin 버전을 보여 주며 방향별 경고. -- Current atomic step: ship — push 후 PR 생성(base main). 머지·release·done·summary --write는 하지 않는다. +- Current atomic step: PR #118 생성 완료 — 리뷰·CI 대기. 머지·release·done·summary --write는 하지 않는다. - Stop / human-decision condition: src 수정 파일 5개 초과 또는 설계 변경 필요 시. ## Constraints and settled decisions diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md b/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md index 6d9ff3e9..79635510 100644 --- a/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md @@ -17,3 +17,12 @@ CHANGELOG.md | 7 ++ tests/doctor.test.mjs | 57 ++++++++++++++++- tests/render-state.test.mjs | 19 +++++- 13 files changed, 317 insertions(+), 13 deletions(-) + +## 2026-10-03T04:58:30.448Z — 72dab4b fix(render-state): prerelease와 build metadata를 함께 쓴 semver도 harnessVersion으로 인정한다 +.../harness-version-stamp-artifact.md | 17 ++++++++++++++ + .../harness-version-stamp-context.md | 26 +++++++++------------- + .../harness-version-stamp-handoff.md | 16 +++++++++++++ + .../harness-version-stamp-plan.md | 2 +- + src/render-state.mjs | 2 +- + tests/render-state.test.mjs | 2 ++ + 6 files changed, 48 insertions(+), 17 deletions(-) diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-plan.md b/docs/hslee/harness-version-stamp/harness-version-stamp-plan.md index 9cdefc06..b7c387d4 100644 --- a/docs/hslee/harness-version-stamp/harness-version-stamp-plan.md +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-plan.md @@ -13,7 +13,7 @@ init이 render-state에 적용 하네스 버전(`harnessVersion`)을 기록하 - [x] README: render-state 설명·doctor 절 갱신, `npm run docs:check` - [x] `npm run test` 통과 - [x] codex read-only 리뷰 → artifact ## Reviews 기록·반영 -- [ ] ship: spec·plan·artifact 최종 갱신, push, PR 생성 +- [x] ship: spec·plan·artifact 최종 갱신, push, PR 생성 ## Ontology 변경 로그 *개념이 새로 정의되거나 의미가 바뀌면 한 줄로 기록. spec.md의 Ontology 섹션을 갱신할 트리거가 된다.* From 848f0603c759531672cf1ebe91126503d7b24a78 Mon Sep 17 00:00:00 2001 From: hslee Date: Sat, 3 Oct 2026 14:21:08 +0900 Subject: [PATCH 4/4] chore(handoff): harness-version-stamp handoff sweep Co-Authored-By: Claude Opus 5.5 --- .../harness-version-stamp/harness-version-stamp-handoff.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md b/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md index 79635510..c066b1fd 100644 --- a/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md +++ b/docs/hslee/harness-version-stamp/harness-version-stamp-handoff.md @@ -26,3 +26,10 @@ CHANGELOG.md | 7 ++ src/render-state.mjs | 2 +- tests/render-state.test.mjs | 2 ++ 6 files changed, 48 insertions(+), 17 deletions(-) + +## 2026-10-03T04:58:59.467Z — 26595f5 chore(task): harness-version-stamp ship — plan·artifact·handoff 갱신 (PR #118) +.../harness-version-stamp/harness-version-stamp-artifact.md | 1 + + .../hslee/harness-version-stamp/harness-version-stamp-context.md | 2 +- + .../hslee/harness-version-stamp/harness-version-stamp-handoff.md | 9 +++++++++ + docs/hslee/harness-version-stamp/harness-version-stamp-plan.md | 2 +- + 4 files changed, 12 insertions(+), 2 deletions(-)