From 2724ff7ea93c5228cab520b90ebd3ebd1d684107 Mon Sep 17 00:00:00 2001 From: hslee Date: Mon, 5 Oct 2026 17:54:01 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(decisions):=20=EA=B2=B0=EC=A0=95=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EB=A5=BC=20=ED=8C=80=20=EC=9A=B4=EC=98=81?= =?UTF-8?q?=C2=B7=ED=94=8C=EB=9F=AC=EA=B7=B8=EC=9D=B8=20=EA=B2=B0=EC=A0=95?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=82=98=EB=88=84=EA=B3=A0=20D11=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=ED=97=8C=EC=9E=A5=EC=9D=84=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - templates/docs/decisions.md에는 팀 운영 결정(D2·D4·D5·D6)만 남긴다. D7–D10과 새 D11은 저장소 docs/decisions.md에만. - doctor DECISION_HEADINGS를 D2·D4·D5·D6으로 줄인다(확인 대상 축소 — 소비자 새 경고 없음). - 바이트 동일성 테스트를 "템플릿은 팀 운영 결정만 + 공유 절은 레포와 글자 그대로 같음"으로 바꾸고, fence 계약 테스트의 감시자를 D8 → D6으로 옮긴다(closer 공백 허용/다른 글자 불허를 두 행으로 분리해 판별력 유지). - 소비자 쪽에서 끊길 D7·D8 참조 정리: AGENTS 템플릿·루트의 D7 줄 제거, migrate·review 명령은 "플러그인 저장소" 명시. - D11: 강제는 PR의 task 문서(spec·plan·handoff·artifact·다이어그램)뿐, 사이클은 제공. - docs/harness-cycle.md: 사이클 정의·결정 전문과 구현 순서. 검증: npm run test 1081 중 pass 1080 · fail 0, npm run docs:check 최신 Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 1 - CHANGELOG.md | 7 + commands/harness-migrate.md | 2 +- commands/harness-review.md | 2 +- docs/decisions.md | 56 +++- docs/harness-cycle.md | 242 ++++++++++++++++++ .../decision-log-split-artifact.md | 13 + .../decision-log-split-context.md | 22 ++ .../decision-log-split-handoff.md | 3 + .../decision-log-split-meta.json | 11 + .../decision-log-split-plan.md | 23 ++ .../decision-log-split-spec.md | 54 ++++ src/commands/doctor.mjs | 5 +- templates/AGENTS.md.hbs | 1 - templates/docs/decisions.md | 138 +--------- tests/agent-files.test.mjs | 23 +- tests/doctor.test.mjs | 47 ++-- 17 files changed, 481 insertions(+), 169 deletions(-) create mode 100644 docs/harness-cycle.md create mode 100644 docs/hslee/decision-log-split/decision-log-split-artifact.md create mode 100644 docs/hslee/decision-log-split/decision-log-split-context.md create mode 100644 docs/hslee/decision-log-split/decision-log-split-handoff.md create mode 100644 docs/hslee/decision-log-split/decision-log-split-meta.json create mode 100644 docs/hslee/decision-log-split/decision-log-split-plan.md create mode 100644 docs/hslee/decision-log-split/decision-log-split-spec.md diff --git a/AGENTS.md b/AGENTS.md index 5c9f7e00..3cd4a77e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,6 @@ > `harness-team summary --write`로만 갱신한다. > - **D6**: 작업 산출물에는 별도 컨텍스트의 **read-only 검증자**를 붙일 수 있다 — 검증자는 반박만 하고 > 고치지 않으며, 반영은 작성 세션이 재현·판별한 뒤 단일 스레드로 한다(절차는 harness-review 명령 문서). -> - **D7**: OpenCode·Gemini는 하네스 멤버에서 제외했다. ### 리뷰 프로토콜 중요한 변경(새 기능, 아키텍처, 복잡한 리팩토링, 보안, 스키마/API) 완료 후 diff --git a/CHANGELOG.md b/CHANGELOG.md index 61d3bff9..58510d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,13 @@ modified: 2026-09-13 ## [Unreleased] +### Changed +- **결정 로그 분리 + D11 범위 헌장** (task `decision-log-split`). 소비자에게 배포하는 `templates/docs/decisions.md`에는 + 팀 운영 결정(D2·D4·D5·D6)만 남기고, 플러그인 결정(D7–D10)과 새 **D11**(강제는 PR의 task 문서뿐, 사이클은 제공)은 + 이 저장소의 `docs/decisions.md`에만 둔다. `doctor`의 결정 로그 확인 헤딩도 D2·D4·D5·D6으로 줄였다 — 확인 대상이 줄어드는 + 방향이라 소비자에게 새 경고는 없다. 소비자 `AGENTS.md` 결정 규범에서 D7 줄을 뺐다(관리 절 변경이라 원할 때 `init --yes`로 받는다). + 사이클 정의·결정 전문은 `docs/harness-cycle.md`. + ## [0.45.0] - 2026-10-03 ### Added diff --git a/commands/harness-migrate.md b/commands/harness-migrate.md index 55136806..1afe9044 100644 --- a/commands/harness-migrate.md +++ b/commands/harness-migrate.md @@ -23,7 +23,7 @@ modified: 2026-09-10 refresh는 설치본의 바이트가 **하네스가 실제로 배포한 적 있는 버전**(sha256 테이블)과 일치할 때만 갱신한다. 사용자가 편집한 파일은 "customized"로 보고 **절대 덮지 않고** 경고만 남긴다. `docs/` seed(`README.md`·`decisions.md`)는 설치 후 팀 저작물이라 refresh 대상이 아니다. -낡은 설치본이 있으면 `harness-team doctor`가 경고로 알려 준다 — 근거는 `docs/decisions.md` D8. +낡은 설치본이 있으면 `harness-team doctor`가 경고로 알려 준다 — 근거는 플러그인 저장소 `docs/decisions.md` D8. **리뷰 증거 채택은 옵트인이다(`--adopt-reviews`).** 0.37.0 이후 `verify: required`의 정본은 meta의 `reviews[]`이고, 그 키가 없는 구 task는 종전대로 artifact 마커로 판정한다. 이 플래그는 열린 구 task를 diff --git a/commands/harness-review.md b/commands/harness-review.md index 867e505b..ac1198e8 100644 --- a/commands/harness-review.md +++ b/commands/harness-review.md @@ -36,7 +36,7 @@ Raw slash-command 인수: 탐지해 첫 가용 엔진을 쓴다. claude는 Claude Code 환경에서 항상 존재하므로 이 체인은 어느 머신에서든 리뷰어를 보장한다. vendor 분리 리뷰어(codex)를 우선하고, 컨텍스트 분리만 제공하는 claude가 마지막이다(엔진 표의 한계 참조). - Gemini는 하네스 멤버가 아니다(`docs/decisions.md` D7) — 필요하면 `custom`으로 등록한다. + Gemini는 하네스 멤버가 아니다(플러그인 저장소 `docs/decisions.md` D7) — 필요하면 `custom`으로 등록한다. `custom`은 체인에 포함되지 않는다 — 명시 호출 전용이다. 2. **Scope 결정** — `git status --short`가 dirty면 working tree 전체가 리뷰 대상이다. diff --git a/docs/decisions.md b/docs/decisions.md index c57bdc4e..87df7984 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -4,8 +4,8 @@ AI 팀 운영 규범의 근거와 이력. `AGENTS.md`의 역할·프로토콜 요약한 것이며, 전문(배경·근거·재평가 조건)은 이 파일이 정본이다. 새 결정은 번호를 이어 아래에 append 한다 — 규범이 바뀌면 `AGENTS.md`의 요약도 함께 갱신한다. -> 상류 출처: D2·D4·D5·D6·D7은 team-harness 플러그인 기본값의 결정 이력이다(bd-makers/team-harness). -> 프로젝트 고유 결정은 그 아래에 이어서 기록한다. +> 이 파일은 **플러그인 저장소의 결정 로그**다. 소비자에게 배포되는 `templates/docs/decisions.md`에는 +> 팀 운영 결정(D2·D4·D5·D6)만 같은 문장으로 들어 있고, D7 이후는 플러그인 결정이라 이 저장소에만 둔다(D11). ## D2 (2026-06-11) — drive/리뷰어 역할 분리 @@ -188,3 +188,55 @@ user 영역이 비었을 때만 제거한다. **AGENTS.md 규범 목록에 넣지 않은 이유**: D9와 같다 — 에이전트가 매 세션 지킬 규칙이 아니라 현 구조를 유지하는 **닫힌 결정**이다. `doctor`의 eager 계층 주석이 이 결정을 가리킨다. + +## D11 (2026-10-05) — 범위 헌장: 강제는 PR의 task 문서뿐, 사이클은 제공한다 + +README의 자기 정의는 "멀티에이전트 설정·상태 하네스"였지만, 2026-10-05 재검토에서 구성과 범위가 그 정의를 벗어나 있음을 +측정했다 — 하네스가 자기 자신을 migrate·진단·릴리스·문서화하는 코드가 약 25%, fix 커밋이 `task`·`doctor`·`migrate` +세 모듈에 집중, 품질 도구 실행까지 넓히는 변경(#119, 닫음). 메인테이너와의 대화로 정체성과 범위를 다시 정했다. +측정·대화·단계별 결정의 전문은 `docs/harness-cycle.md`가 정본이다. + +**정체성**: 한 프로젝트의 팀원들에게 같은 개발 사이클(S0 셋업 → S8 지식화, 검토 지점 R1–R3)을 **제공**하는 하네스. +사람은 루프 안에 선다 — spec과 plan은 개발자가 직접 만들고, 구현 방식은 개발자마다 자유다. + +**결정 — 강제하는 것(하나뿐)**: PR에 그 task의 문서를 담는다 — spec·plan·handoff·artifact, 그리고 다이어그램 +(로직·구조 변화가 없으면 `harness-team diagram record --skipped "<사유>"`로 대신). 확인은 결정론적 검사 하나로 모으고 +ship·git pre-push 훅·(원하는 팀의) CI가 같은 검사를 부른다. task 시작은 강제하지 않고(SessionStart nudge 유지), +종결 순서는 PR → 머지 → 기본 브랜치에서 `done`을 유지한다. + +**결정 — 제공하는 것**: 사이클 게이트(R1 원천 문서 검토 · R2 시나리오 ↔ 증거 대조 · R3 코드 리뷰, 커밋 프리셋), +선택형 기본 루프(오케스트레이터는 S3부터, Claude·Codex 기본 기능만), 가로축(인계·팀 가시성·안전 가드·루프 건강·갱신 경로). +쓰든 말든 개발자 자유다. + +**결정 — 소유하지 않는 것** +- **코드 안의 언어·도구별 로직** — 언어 지식은 프리셋 데이터로만 둔다. 훅·CLI에 언어 분기를 넣지 않는다. +- **구현 방식 강제**, **사이클 밖 편의 기능**(파일 배치 등), **서비스형 런타임 오케스트레이터**, OS·네트워크 격리. +- **개인 도구·서드파티 오케스트레이터 의존** — 메인테이너 개인 도구의 규칙·가이드·설정을 두지 않는다. 선택 동반 플러그인은 + 핀·라이선스·없을 때의 동작을 명시한 경우만 허용한다(현재 `diagram-design`). + +**2차 장치 규칙**: 하네스의 다른 장치를 고치거나, 지키거나, 옮기거나, 보여 주기 위한 새 장치를 추가하기 전에 원래 장치를 +빼거나 줄이는 안을 먼저 검토하고 기각 사유를 spec에 남긴다(D7 "고치는 것보다 빼는 것이 단순하다"의 일반화). + +**신규 기능 판정 질문**(spec의 Ambiguity 게이트보다 먼저): ① 사이클의 어느 단계(또는 가로축)를 위한 것인가 — 답이 없으면 비범위. +② 언어·도구별 지식이 필요한가 — 그렇다면 프리셋으로. ③ 다른 하네스 장치의 결함·부작용 때문인가 — 그렇다면 2차 장치 규칙. + +**의도적으로 유지 (재론하지 말 것)** +- 작은 수정도 full task다. 문서 대 코드 비율이 높은 것은 결함이 아니라 선택이다. task 폴더는 작업대이고, 머지 후 위키로 컴파일한다. +- `migrate`는 당겨 쓴다(pull) — 사용자가 필요할 때 해당 프로젝트에서 실행한다. 지원 창을 정책으로 두지 않고, 일회성 변환 단계가 + 유지비를 낼 때 지운다(그보다 오래된 설치본은 재 init). + +**결정 로그 분리 (이 결정과 함께)**: 소비자에게 배포하는 `templates/docs/decisions.md`에는 팀 운영 결정(D2·D4·D5·D6)만 둔다. +D7 이후는 플러그인 결정이라 이 저장소에만 둔다. `doctor`의 확인 헤딩도 D2·D4·D5·D6으로 줄인다 — 확인 대상이 줄어드는 방향이라 +소비자에게 새 경고는 생기지 않고, 이미 설치된 D7–D10 사본은 seed라 그대로 남는다(D8). + +**기각한 대안** +- **헌장 없이 사례별 판단** — 매 PR이 국소적으로는 타당했고, 그 합계가 위 측정이다. +- **정량 예산(LOC 상한 등)** — 지표가 목표가 된다. 측정은 재평가 신호로만 쓴다. +- **처음부터 다시 작성** — 남길 것이 약 80%라 옮기는 비용이 크고, 마커 병합·Codex 주입 형식·D10 같은 비싸게 얻은 지식을 다시 겪는다. +- **`done` → PR로 순서 변경** — 머지 후 종결 절차·`remote-task.mjs`·done-on-main 안내 등 손볼 장치가 많다. +- **task 시작 강제**(쓰기 차단·pre-commit) — 경계가 애매하다. PR 시점의 문서 확인으로 충분하다. + +**재평가 신호**: minor 릴리스마다 자기 관리 비중과 fix 집중 모듈을 다시 잰다. 나빠지면 이 헌장이 지켜지지 않고 있다는 뜻이다. + +**AGENTS.md 규범 목록에 넣지 않은 이유**: 에이전트가 매 세션 지킬 규칙이 아니라 메인테이너가 기능을 받을지 정하는 기준이다. +eager 계층 예산 여유도 거의 없다(D9). 진입점은 이 로그와 `docs/harness-cycle.md`다. diff --git a/docs/harness-cycle.md b/docs/harness-cycle.md new file mode 100644 index 00000000..156406e9 --- /dev/null +++ b/docs/harness-cycle.md @@ -0,0 +1,242 @@ +# 팀 개발 사이클 하네스 — 사이클 정의와 범위 헌장 + +> 2026-10-05 메인테이너와 합의한 방향이다. 범위 헌장은 `docs/decisions.md` **D11**로 기록했고, 이 문서는 그 배경·단계·결정 전문이다. +> 구현은 아래 §6의 순서로 task 단위 PR을 나눠 진행한다. 아직 구현되지 않은 내용이 많다 — "현재" 열과 §6을 함께 읽는다. +> 판단 렌즈는 하나다: **"이 구성요소는 사이클의 어느 단계를 위한 것인가."** 답이 없으면 두께, 단계는 있는데 장치가 없으면 공백이다. + +--- + +## 1. 정체성 + +**하나의 프로젝트에서 여러 팀원에게 같은 개발 사이클을 제공하는 하네스.** +**강제하는 것은 하나뿐이다**: PR을 올릴 때 그 task의 문서 — spec·plan·handoff·artifact·다이어그램 — 를 담는다. 다이어그램은 로직·구조 변화가 없으면 생략 사유를 남기고 뺄 수 있다. +R1부터 PR 승인까지의 사이클(단계·산출물·게이트)은 기본으로 **제공**하지만, 개발 방식은 개발자마다 완전히 자유다(2026-10-05 메인테이너 설명). + +- 사람은 루프 **안**에 있다(in the loop). spec과 plan 단계에 개발자가 직접 참여해 무엇을, 어떻게 만들지 먼저 인지한다. +- 티켓만 넣으면 끝까지 도는 완전 자동 루프(on the loop)는 목표가 아니다. 다만 그런 루프 하나를 **선택지로** 제공한다(주니어용 기본값). +- 원래 니즈 두 가지가 여전히 중심이다. + 1. 팀원이 무엇을, 어떻게, 어디를 바꿨는지가 산출물로 남는다(컨텍스트 스냅샷). + 2. 같은 지시 파일(`AGENTS.md`·`CLAUDE.md`)로 에이전트 결과물의 스타일이 비슷해진다. + +참고한 1차 자료(같은 계열): OpenAI "Harness engineering"(2026-02, 실행 계획을 저장소에 커밋, 품질은 linter·CI로 기계 강제), +Anthropic "Effective harnesses for long-running agents"(세션 간 진행 산출물 인계). + +--- + +## 2. 사이클 + +표기: ✓ 있음 · △ 약함 · ✗ 없음 (현재 0.45.0 기준) + +**R = 검토 지점(Review point)**: R1 원천 문서 검토(S1 직후) · R2 시나리오 ↔ 증거 대조(S4 구현 직후) · R3 코드 리뷰(S6, PR 전). S = 사이클 단계(Stage). + +| 단계 | 산출물 | 게이트 | 현재 | +|---|---|---|---| +| **S0 셋업** | AGENTS/CLAUDE, Codex 배선, **검증 프리셋** | — | ✓ init · △ 프리셋 없음(RN 하드코딩) | +| **S1 Spec** | `spec.md` (PRD·Figma·API 문서·기획서·정책서 소스) | **R1 소스 검토**(차단) | ✓ harness-spec · △ 소스 검토 없음(Ambiguity 게이트·contrarian은 spec 자체만 본다) | +| **S2 Plan** | `plan.md` + **plan 다이어그램**(이해용) | 개발자 확인 | ✓ plan · △ 다이어그램 옵트인 | +| **S3 구현** | 코드 · TCC(작업 카드) | — (자유) | ✓ 자유 · ✗ **기본 루프** 없음 | +| **S4 구현 검증** | spec 시나리오(Given/When/Then) ↔ 증거 | **R2 시나리오 대조**(기계 2 + 루브릭 2, §4-1b) | △ done 가드가 체크박스·evidence만 확인 | +| **S5 커밋** | — | **검증 프리셋 실행**(lint·tsc·test) | △ pre-commit-check (typecheck·test, node 전용) | +| **S6 코드 리뷰** | 리뷰 기록(`## Reviews`·meta.reviews) | **R3 로컬 리뷰** (PR 전 필수) | △ spec이 required 선언할 때만 강제 | +| **S7 PR** | **PR 다이어그램**(실제 구조 + plan 대비 변화), spec·plan·handoff 커밋 | — | ✓ 문서 커밋 · △ ship의 다이어그램 갱신은 옵트인 | +| **S8 지식화** | 최상위 **`wiki/`** 항목(기능·모듈 단위), task 폴더 삭제 | — | ✗ 없음 (task 단위로만 쌓임) | + +**가로축** (모든 단계에 걸침) +- 세션·머신 인계: handoff ✓ +- 팀 가시성: summary 집계(`task_summary.md`) ✓ — 원래 니즈 1의 직접 구현 +- 안전 가드: block-dangerous-git · protect-files ✓ +- 루프 건강: observe(트립와이어) ✓ +- 갱신 경로: migrate ✓ — 하네스 버전이 오를 때 설치된 프로젝트를 올리는 유일한 길 + +**PR 다이어그램이 필요한 이유** (메인테이너 설명): plan 다이어그램이 실제로 어떻게 달라졌는지 보여 주고, +코드만으로 변경 전체를 이해하는 데 드는 리뷰 시간을 줄인다. + +--- + +## 3. 구성요소 → 단계 매핑 + +| 분류 | 구성요소 | 판정 | +|---|---|---| +| 단계에 봉사 | init·AGENTS/CLAUDE 렌더·Codex 배선·스킬 래퍼, task 4문서·meta, harness-spec·interview·contrarian·simplifier, plan·diagram, TCC·boundary, done 가드, pre-commit-check, harness-review·adversarial-review, ship, retro·promote, unit·comp·inttest | **유지** (일부 강화) | +| 가로축 | handoff·session-context, summary, protect-files·block-dangerous-git, observe, migrate, doctor | **유지** | +| 단계 없음 | 백업 디렉터리 군(symlink·clone·delete·upgrade·backup, 참조 없는 `backup` CLI) | **제거** | +| 정리 (§4-5·4-6) | sha 테이블·legacy 변환(유지비가 나면 삭제), 버전 stamp(설치 버전 표시만), docs 버전 drift 검사, auto-format(→ 프리셋 `format`), sim·codex-sim·release(→ 저장소 전용), D7–D11(→ 저장소 전용). cursor 미러는 **유지** | 결정됨 | + +--- + +## 4. 결정 (2026-10-05) + +### 4-1. 세 검토 지점의 형태 (2026-10-05 피드백 반영) +세 지점은 묻는 것이 다르다. R1은 "원천 문서에 문제가 없나", R2는 "spec대로 만들어졌나", R3은 "코드가 좋은가"다. +- **R1 — 루브릭이 아니라 소스 검토 차단 게이트.** spec은 PRD·Figma·API 문서에서 나오고, 기획서·정책서가 더해질 수 있다. + R1은 spec에 점수를 매기는 대신 **이 원천 문서들 사이의 충돌·누락·모순**을 근본적으로 검토한다. + 문제가 발견되면 사용자에게 고지하고, **해결된 뒤에만** Plan으로 넘어간다(해결 후 재검토). +- **R2 — 시나리오 ↔ 증거 대조 (§4-1b).** "Done evidence로 흡수"라는 이전 권장은 철회한다(피드백: R2에도 평가 방법이 있어야 한다). +- **R3 — PR 전 코드 리뷰 루브릭, 필수.** D2의 작성자·리뷰어 분리 그 자체다. +- 현재와의 차이: Ambiguity 게이트와 contrarian(A1–A4)은 **spec 문서 자체**를 본다. 원천 문서끼리의 정합성 검토는 아직 없다. + +### 4-1b. R2의 평가 방법 — TDD·BDD·Gherkin +셋은 대안이 아니라 층이 다르다. +- **BDD**는 합의 방식이다. S1에서 "무엇이 되면 완료인가"를 기획·개발·QA가 같은 시나리오 문장으로 합의한다. +- **Gherkin**은 BDD의 문장 형식이다. spec의 수용 기준을 Given / When / Then으로 쓴다. 이것이 R2가 대조할 단위다. +- **TDD**는 구현 습관이다. S3 자유 영역이라 사이클이 강제하지 않고, 기본 루프(§4-3)에서 권장만 한다. +- **권장**: spec의 `Done evidence`를 **시나리오 표**로 확장한다. 시나리오마다 증거(테스트 이름·명령)를 연결하고, R2는 4행으로 판정한다. + 1. [기계] 모든 시나리오에 증거가 연결돼 있다. + 2. [기계] 증거 명령이 통과한다(exit 0). + 3. [루브릭] 증거가 Then을 실제로 검증한다 — "테스트가 있다"와 "테스트가 시나리오를 증명한다"의 차이를 잡는다. + 4. [루브릭] spec 밖의 동작이 들어오지 않았다. +- 시나리오를 실행 가능한 테스트로 묶는 러너(Cucumber 등)는 스택별이므로 **프리셋 옵션**(§4-2)으로 둔다. 하네스가 강제하는 것은 형식과 대조뿐이다. +- 실행 시점은 R3와 같은 리뷰 호출 안에서 별도 절로 돌려도 된다(호출 비용 1회). + +### 4-2. 언어별 템플릿을 하네스가 제공할 것인가 +- **권장**: 제공하되 **코드가 아니라 데이터(프리셋)로** 제공한다. + - `.harness/config.json`에 `gates.commit: ["npm run lint", "npx tsc --noEmit", ...]`처럼 **명령 목록**을 둔다. + - init이 스택 감지 결과로 프리셋(node-ts, react-native, swift, python …)을 **제안**하고, 개발자가 확정한다. + - 훅은 목록만 실행한다. 언어 분기가 훅 코드에 들어가지 않는다. 언어 추가는 프리셋 JSON 하나다. + - RN rules도 같은 방식의 **rules 프리셋**으로 옮긴다(RN 감지 시 제안, 고정 배포 아님). +- 근거: #119에서 본 비용(127 비대칭, PM 5종 실측, yarn berry 예외)은 하네스가 "무엇을 돌릴지"를 **추론**했기 때문에 생겼다. + 명령을 명시적으로 선언하면 127은 "설정 오류"로 단순히 차단하면 된다. +- #119와의 관계: #119의 "package.json 스크립트 감지"는 node 프리셋 하나와 같은 동작이다. 프리셋 구조가 정해지면 그 안으로 흡수한다. + +### 4-2b. 저장소 모양과 변경 감지 (2026-10-05 대화 반영) +현재 `src/detect-stack.mjs`는 루트 `package.json` 하나만 보고 스택 ID 하나를 고른다. workspace·turbo·nx 인식이 없다. + +**init이 저장소 모양을 먼저 판별한다** — 판별은 휴리스틱이므로 결과를 보여 주고 확인을 받는다. + +| 모양 | 판별 신호 | +|---|---| +| 단일 앱 | 루트 `package.json`만, workspace 없음 | +| 앱 + 내부 패키지 | workspace 있음, 앱은 하나 | +| 모노레포 | 앱 여러 개 + 패키지 여러 개 (`pnpm-workspace.yaml`, `workspaces`, `turbo.json`, `nx.json`, `apps/`·`packages/`) | + +- 앱과 패키지 구분: 앱은 dev·start 스크립트나 next·expo 같은 프레임워크 의존성, 패키지는 `main`·`exports`. +- Node 밖(Cargo workspace, Gradle 멀티 프로젝트, Swift 멀티 패키지)은 첫 버전에서 "단일"로 두고 수동 지정을 받는다. + +**package.json 등이 바뀌면 — 자동 갱신이 아니라 감지·제안** +- 프리셋은 개발자가 확정·수정한 값이라, 자동으로 덮으면 수정이 조용히 사라진다(D8 "커스터마이즈는 덮지 않는다"). +- init 때 감지 결과의 **지문**(스크립트 이름 목록, workspace 목록)을 config에 함께 저장한다. +- doctor 또는 커밋 훅이 현재 상태와 지문을 비교해 달라지면 알린다(예: "lint 스크립트가 추가됐습니다. gates를 갱신할까요?"). 갱신은 개발자가 확정한다. + +**모노레포의 커밋 게이트 — 두 갈래** +1. **turbo·nx가 있으면 그 도구에 맡긴다(권장).** 영향 범위 계산은 그 도구가 이미 한다. 하네스가 다시 구현하면 #119 같은 두께가 또 생긴다. + 예: `{ "commit": ["turbo run lint typecheck test --filter=...[HEAD]"] }` +2. **도구가 없으면 경로별 목록.** 커밋 훅은 스테이징된 파일이 속한 workspace의 목록만 실행한다. + 예: `{ "commit": { "apps/mobile": ["npm run lint -w apps/mobile", "npm test -w apps/mobile"], "packages/*": [...] } }` + +**따라오는 이점** +- RN rules 같은 rules 프리셋을 `.claude/rules`의 `paths:` 스코프로 해당 앱 경로(`apps/mobile/**`)에만 건다. 같은 저장소의 웹 앱에 섞이지 않는다. +- workspace 이름이 위키 area 키(`wiki/.md`, §4-4)가 된다. + +### 4-3. 기본 루프 — 오케스트레이터는 에이전트 자신 (2026-10-05 대화 반영) +- **원칙**: 기본 루프는 **Claude(또는 Codex)의 기본 기능만**으로 돈다. 서드파티 오케스트레이터나 개인 도구는 팀 하네스에 넣지 않는다. + 선택형이라 쓰지 않아도 사이클은 성립하고, 형태는 CLI 런타임이 아니라 **스킬(프롬프트) + 기존 CLI**(plan 체크·리뷰 엔진·커밋 게이트)다. +- **오케스트레이터의 개입 지점은 개발 단계(S3)부터다** (2026-10-05 피드백 반영). spec과 plan·다이어그램은 개발자가 직접 만든다 — + 에이전트의 도움은 받을 수 있지만 오케스트레이터 루프 밖이다. 이것이 "개발자가 무엇을, 어떻게 만들지 먼저 인지한다"는 원칙이다. +- **루프 모양**: + 1. [사람] R1 소스 검토 통과 + 2. [개발자] plan과 다이어그램을 직접 만들어 확정한다. **확정 직후 묻는다: 오케스트레이터로 시작할까, 개별로 진행할까?** + 오케스트레이터 사용은 개발자마다의 판단이다. 개별로 진행하면 사이클의 어느 게이트에도 합류할 의무가 없다 — + 강제되는 것은 PR에 담기는 task 문서뿐이다(§1). 이전 초안의 "개별 진행은 QA에서 합류" 권장은 철회한다. + 3. **오케스트레이터 = 메인 세션**이 여기서부터 맡는다. 자동 구간: **Dev**(쓰기 담당 1명)가 plan 한 단계를 구현 → + **QA**(읽기 전용, 별도 컨텍스트)가 커밋 프리셋 + R2 4행으로 판정 → 실패면 Dev로, 통과면 plan 체크 후 다음 단계 + 4. 모든 단계 통과 → R3 리뷰(Codex 엔진, 없으면 Claude 엔진) → [사람] PR 승인 +- **멈춤 조건**: 성공(모든 시나리오 + R3 통과, PR 생성은 사람) · spec 공백 발견(R1로 되돌려 사람에게 질문) · + 진전 없음(같은 시나리오가 변화 없이 연속 실패, 횟수 상한은 임의로 두지 않는다) · 승인 필요(push·PR·파괴적 변경). +- **실행 수단 — 세션이 가진 것으로** (`probe → degrade → record`, 다이어그램 단계와 같은 계약): + - **기본: 서브에이전트.** 모든 세션에 있다. Dev 1 + QA 1, 순차. D4(단일 쓰기)·D6(read-only 검증자)를 그대로 만족한다. + - **있으면: Workflows.** 단계 사이 검증을 코드로 강제할 수 있다. 단, 사용자의 명시적 opt-in이 필요하다. + - **있으면: Agent teams · cross-session messages.** 병렬 Dev가 필요할 때. 이때 각 Dev는 격리 worktree(D5)에서만 쓴다. + - 세션에 없는 수단은 건너뛰고, 어떤 수단으로 돌았는지 artifact에 한 줄 남긴다. +- **Codex의 자리**: v1 권장은 **Codex = QA·R3 검증자**(엔진이 달라 자기 승인 편향이 없다). Codex가 없으면 Claude 엔진으로 대신한다 + (`harness-review`의 기존 codex → claude 폴백과 같다). Codex를 오케스트레이터로 쓰려면 + D2(drive = Claude)와 D9(Codex 강제력은 SessionStart만)를 먼저 고쳐야 한다 — 별도 결정. +- **워크트리 (2026-10-05 결정)**: 워크트리로 할지는 **task를 정의할 때** 정한다. 처음부터 워크트리 안에서 task를 만들면 + 활성 task 포인터(`.harness/active.json`)와 문서가 그 워크트리에 생기므로 "워크트리로 넘어가면 task가 없다"는 문제가 생기지 않는다. + 작업 중간에 워크트리로 옮기는 세부 처리는 개발자가 Claude·Codex에게 직접 지시한다 — 하네스는 이를 위한 장치(포인터 복사 훅, + 브랜치 기반 자동 활성화)를 만들지 않는다. 하네스가 이미 하는 것은 워크트리 안에서 깨지지 않게 동작하는 것까지다(D5). +- **주니어용 장치**: 각 단계가 끝날 때 "무엇을, 왜 했는지" 한두 줄을 artifact에 남긴다. 머지 후 위키로 컴파일된다(§4-4). +- 조정할 기존 규범: README의 런타임 오케스트레이션 "의도적 비채택" 문구를 "서비스형 오케스트레이터는 비채택, 선택형 루프 스킬은 제공"으로 고친다. +- **미검증**: Codex 쪽 서브에이전트·병렬 기능의 현재 지원 범위는 확인하지 않았다. + +### 4-4. 프로젝트 지식 베이스 — task는 작업대, 위키가 기억 (2026-10-05 피드백 반영) +- 문제: task가 137개 쌓여도 전부 task 단위다. 이 형태로만 늘면 폴더가 끝없이 쌓이고, + "이 기능(모듈)은 지금 어떻게 생겼고 왜 그런가"를 답하려면 여러 task를 다 읽어야 한다. +- **방향**: task 폴더는 **작업 중에만** 존재한다. PR 머지(또는 그에 준하는 단계) 이후 **LLM 위키 컴파일**로 + task의 결정·바뀐 모듈·학습을 **기능·모듈 단위 위키 항목**에 모으고, **task 폴더는 삭제한다.** 원문은 git 이력과 PR에 남는다. +- **위치는 최상위 `wiki/` 폴더**다(`wiki/.md`). `docs/`는 사람이 쓰는 문서와 작업 중인 task가 사는 곳이라 + 수명(영구 대 임시)과 독자(LLM 조회·인덱싱)가 다른 위키를 섞지 않는다. RAG 인덱싱 범위도 폴더 하나로 정해진다. +- 위키 항목은 원 task 경로가 아니라 **PR 번호·커밋**을 출처로 가리킨다(폴더가 사라지므로). +- **지우기 전에 옮겨야 할 것** (2차 장치 규칙 대상): summary 집계(`task_summary.md`)·`done` 판정·handoff가 지금 task 폴더를 읽는다. + 이들의 입력을 위키와 PR 기준으로 옮기거나, 필요 없어지면 뺀다. +- 원래 니즈 1(팀원이 무엇을 바꿨나)은 위키 항목의 작성자·PR 기록으로 이어진다. +- 컴파일 실행 위치는 기본 브랜치가 자연스럽다(지금 `summary --write`가 있는 자리, D5). +- **메인테이너 입력 필요**: kc-platform·kc_vault 작업에서 본 형태(저장 위치, 키 체계, 인덱스·RAG 여부)를 기준으로 정한다. + +### 4-5. migrate의 범위 (2026-10-05 결정) +- **migrate는 당겨 쓴다(pull).** 사용자가 필요할 때 해당 프로젝트에서 직접 실행한다. 하네스도 에이전트도 먼저 밀지 않는다 + (doctor는 실행했을 때 처방만 한다 — 지금 그대로). +- migrate에는 두 종류가 섞여 있다. **① 갱신 경로**(`refreshClaudeHooks`·`refreshClaudeTemplates`, init 마커 병합)는 영구 유지한다. + **② 일회성 모양 변환** 13개(05–09월에 생김)는 상태 기준이라 실행할 때 해당 모양이 있으면 고치고 없으면 지나간다. +- **지원 창을 정책으로 두지 않는다.** pull이라 언제 누가 실행할지 모르므로, 날짜·버전 창보다 단순하게 간다: + 변환 단계가 유지비(테스트 깨짐·리팩터 방해)를 낼 때 그 단계를 지우고, 그보다 오래된 설치본은 재 init으로 안내한다(2차 장치 규칙). +- **정정**: 앞서 "버전 stamp(#118)로 sha 테이블을 대체 검토"라고 했으나 틀렸다. stamp는 설치 버전만 알려 주고 사용자가 파일을 + 고쳤는지는 알려 주지 못한다. 대신 D8이 기각한 "설치 시점 sha 매니페스트"는 기각 근거(기존 설치본에 기준값 없음)가 재 init으로 + 점차 사라지므로, 그때 D8 재검토 후보로 둔다. + +### 4-6. 남은 확인 (2026-10-05 전부 결정) +- **강제 범위 (2026-10-05 확정)**: 순서는 기존대로 PR → 머지 → (기본 브랜치에서) `done`. `done` → PR로 바꾸는 안은 손볼 장치 + (머지 후 종결 절차·`remote-task.mjs`·done-on-main 안내)가 많아 기각. task 시작도 강제하지 않는다(지금의 SessionStart nudge 유지). + lint·tsc·PR 전 로컬 리뷰는 '제공'이다. **PR에 spec·plan·handoff·artifact·다이어그램을 담는 것만 필수**이며 + (handoff는 커밋 훅이 자동 갱신하고, artifact는 머지 후 위키 컴파일(§4-4)의 재료다), 다이어그램은 기존 + `harness-team diagram record --skipped "<사유>"`로 생략을 기록할 수 있다. +- **PR 필수 문서 확인 (결정)**: 결정론적 CLI 검사 하나(`harness-team pr-check`, 가칭)를 두고 호출처를 셋으로 한다. + 검사: spec·plan·handoff·artifact가 있고 템플릿 그대로가 아님 + 다이어그램 파일 또는 생략 기록이 있음. + 호출처: ① ship 준비 보고 ② init이 설치하는 git pre-push 훅(post-commit 훅과 같은 방식) ③ 원하는 팀의 CI(같은 명령). + 하네스가 강제하는 유일한 것이라 확인 장치를 하나로 모은다. +- **다이어그램 (결정)**: PR에는 필수(로직·구조 변화가 없으면 생략 사유로 대신), plan 단계 다이어그램은 선택(이해용). +- **Cursor (결정)**: 쓰는 팀원이 있으므로 멤버와 cursor 미러를 **유지**한다. 앞선 기능 인터뷰의 "제외" 답은 무효. +- **auto-format (결정)**: 훅에 하드코딩된 `*.ts|*.tsx|*.js|*.jsx|*.json → npx prettier`는 4-2(언어 로직은 프리셋 데이터로)에 어긋난다. + 프리셋의 선택 항목 `format` 명령 목록으로 옮기고, 훅에서는 언어 분기를 뺀다. +- **sim·codex-sim·release (결정)**: 이 저장소 전용으로 돌린다. `.claude-plugin/plugin.json`의 commands 목록과 Codex 스킬에서 빼 + 소비자 슬래시 메뉴에서 없애고, 파일은 이 저장소의 `.claude/commands/`로 옮긴다. +- **D11 배포 (결정)**: 결정 로그를 둘로 나눈다. **팀 운영 결정**(D2·D4·D5·D6)은 소비자에게 배포, **플러그인 결정**(D7–D11)은 이 저장소에만. + 고칠 곳: `templates/docs/decisions.md`, doctor `DECISION_HEADINGS`(`src/commands/doctor.mjs:472`), 동일성 테스트(`tests/agent-files.test.mjs:103`). + 확인 대상 헤딩이 줄어드는 방향이라 소비자에게 새 경고는 뜨지 않는다. 기존 설치본의 D8–D10 사본은 seed라 그대로 남는다(D8). + +--- + +## 5. 범위 헌장 (요약 — 정본은 `docs/decisions.md` D11) + +**D11 — 사이클에 봉사하는 것만 소유한다** + +- **강제한다**: PR에 담기는 task 문서 — spec·plan·handoff·artifact, 다이어그램(또는 생략 사유). 이것만 막는다. +- **제공한다**: 사이클 S0–S8의 산출물·게이트, 그리고 가로축(인계·팀 가시성·안전 가드·루프 건강·갱신 경로). 쓰든 말든 자유다. +- **소유하지 않는다** + - **언어별 로직을 코드에**: 언어·도구 지식은 프리셋 데이터로만 둔다. 훅·CLI에 언어 분기를 넣지 않는다. + - **구현 방식 강제**: 기본 루프는 선택형이다. 구현 단계는 자유다. + - **사이클 밖의 편의 기능**: 파일 배치(백업 디렉터리 군) 같은 것. + - **서비스형 런타임 오케스트레이터**, OS·네트워크 격리. + - **개인 도구·서드파티 오케스트레이터에 대한 의존**: 메인테이너가 개인적으로 쓰는 도구의 규칙·가이드·설정을 팀 하네스에 두지 않는다. + 선택형 동반 플러그인은 핀·라이선스·없을 때의 동작을 명시한 경우만 허용한다(현재 `diagram-design`). +- **의도적으로 유지 (재론하지 말 것)**: 작은 수정도 full task다. 문서 대 코드 비율이 높은 것은 결함이 아니라 선택이다. + 다만 task는 작업대다. 머지 후 위키로 컴파일하고 폴더는 지운다(§4-4). +- **2차 장치 규칙**: 하네스의 다른 장치를 고치거나, 지키거나, 옮기거나, 보여 주기 위한 새 장치를 추가하기 전에, + 원래 장치를 빼거나 줄이는 안을 먼저 검토하고 기각 사유를 spec에 남긴다(D7의 일반화). +- **신규 기능 판정 질문** + 1. 사이클의 어느 단계(또는 가로축)를 위한 것인가? 답이 없으면 비범위다. + 2. 언어·도구별 지식이 필요한가? 그렇다면 코드가 아니라 프리셋으로 넣는다. + 3. 다른 하네스 장치의 결함·부작용 때문에 필요한가? 그렇다면 2차 장치 규칙을 적용한다. + +--- + +## 6. 구현 순서 + +결정은 끝났고 구현은 task 단위 PR로 나눈다. 앞의 것이 뒤의 기준이 된다. + +1. 결정 로그 분리 + D11 기록 + 이 문서 커밋 (task `decision-log-split`) +2. 제거: 백업 디렉터리 군, sim·codex-sim·release를 저장소 전용으로 이동 +3. 프리셋: 저장소 모양 판별(§4-2b), 지문 감지, `gates.commit`·`format` — 커밋 게이트와 auto-format 흡수(#119 대체) +4. `pr-check` + git pre-push 훅 + ship 연동(§4-6) +5. R1 원천 문서 검토 · R2 시나리오 ↔ 증거 대조 — spec 템플릿의 Gherkin 표와 루브릭(§4-1·4-1b) +6. 선택형 기본 루프 스킬 + README의 오케스트레이션 문구 정정(§4-3) +7. 위키 컴파일 + task 폴더 삭제(§4-4) — 위키 형태는 kc-platform·kc_vault 기준 입력이 필요하다 diff --git a/docs/hslee/decision-log-split/decision-log-split-artifact.md b/docs/hslee/decision-log-split/decision-log-split-artifact.md new file mode 100644 index 00000000..9ed6bc48 --- /dev/null +++ b/docs/hslee/decision-log-split/decision-log-split-artifact.md @@ -0,0 +1,13 @@ +# decision-log-split — Artifact + +*최종 결과물과 학습 내용을 기록한다.* + +## 결과 + + +## Reviews +*Codex 등 리뷰 실행 시 결과(요약·발견·조치)를 날짜와 함께 남긴다. 남기지 않은 리뷰는 "안 한 것"으로 간주.* +*기계 판독용 마커를 함께 남긴다: ``* + + +## Learnings diff --git a/docs/hslee/decision-log-split/decision-log-split-context.md b/docs/hslee/decision-log-split/decision-log-split-context.md new file mode 100644 index 00000000..86d1badd --- /dev/null +++ b/docs/hslee/decision-log-split/decision-log-split-context.md @@ -0,0 +1,22 @@ +# decision-log-split — Context Card + + +## Now +- Goal: 결정 로그 분리 + D11 + 사이클 문서 +- Current atomic step: 외부 리뷰(R3) → 커밋 · PR +- Stop / human-decision condition: 리뷰 BLOCKER, PR 머지는 사람 + +## Constraints and settled decisions +- + +## JIT retrieval map +- Identifiers / symbols: +- Narrow globs: +- Read next: +- Verification command: `npm run test && npm run docs:check` + +## Failure capsules (max 3 unresolved) +- (none) + +## Resume checklist +- diff --git a/docs/hslee/decision-log-split/decision-log-split-handoff.md b/docs/hslee/decision-log-split/decision-log-split-handoff.md new file mode 100644 index 00000000..0ae5a270 --- /dev/null +++ b/docs/hslee/decision-log-split/decision-log-split-handoff.md @@ -0,0 +1,3 @@ +# decision-log-split — Handoff + +(세션 종료 시 post-commit hook이 자동 갱신합니다) diff --git a/docs/hslee/decision-log-split/decision-log-split-meta.json b/docs/hslee/decision-log-split/decision-log-split-meta.json new file mode 100644 index 00000000..6b46e6b9 --- /dev/null +++ b/docs/hslee/decision-log-split/decision-log-split-meta.json @@ -0,0 +1,11 @@ +{ + "user": "hslee", + "task": "decision-log-split", + "created": "2026-10-05", + "firstActivatedAt": "2026-10-05T08:50:02.444Z", + "status": "open", + "closedAt": null, + "forcedAt": null, + "forcedIssues": null, + "reviews": [] +} diff --git a/docs/hslee/decision-log-split/decision-log-split-plan.md b/docs/hslee/decision-log-split/decision-log-split-plan.md new file mode 100644 index 00000000..f2aed71e --- /dev/null +++ b/docs/hslee/decision-log-split/decision-log-split-plan.md @@ -0,0 +1,23 @@ +# decision-log-split — Plan + +## 목표 +결정 로그를 팀 운영 결정(배포)과 플러그인 결정(저장소 전용)으로 나누고, D11과 사이클 문서를 기록한다. + +## 단계 +- [x] 템플릿 결정 로그에서 D7–D10 제거, 두 로그 머리말 정리 +- [x] D11 기록 +- [x] `doctor` `DECISION_HEADINGS` → D2·D4·D5·D6 +- [x] 테스트: 분리·동일성 계약, fence 계약 감시자 D6으로 이동 +- [x] 소비자 쪽 D7·D8 참조 정리(AGENTS 템플릿·루트, migrate·review 명령) +- [x] `docs/harness-cycle.md` 정식화 + 구현 순서 +- [x] 검증: `npm run test` · `npm run docs:check` +- [ ] 외부 리뷰(R3) 기록 +- [ ] 커밋 · PR + +## Ontology 변경 로그 +*개념이 새로 정의되거나 의미가 바뀌면 한 줄로 기록. spec.md의 Ontology 섹션을 갱신할 트리거가 된다.* + +- 2026-10-05 "팀 운영 결정"·"플러그인 결정" 구분 추가(spec Ontology, D11). + +## 참고 +- 머지 후 종결(`done`·`summary --write`)은 기본 브랜치 몫이다. diff --git a/docs/hslee/decision-log-split/decision-log-split-spec.md b/docs/hslee/decision-log-split/decision-log-split-spec.md new file mode 100644 index 00000000..81a51fad --- /dev/null +++ b/docs/hslee/decision-log-split/decision-log-split-spec.md @@ -0,0 +1,54 @@ +# decision-log-split — Spec + +## 목적 / 요구사항 +*문제(오늘 무엇이 안 되는가) → 영향받는 사용자·시스템 → 기대 결과 → 제약 순으로 쓴다. +답 없는 질문은 요구가 아니다 — `## 참고` 절에 `- (open) …`으로 남긴다.* + +- **문제**: 2026-10-05 재검토로 하네스의 정체성·범위를 다시 정했지만(사이클은 제공, 강제는 PR의 task 문서뿐) 기록이 없다. + 기록하려 해도 `docs/decisions.md`가 템플릿과 바이트 동일해야 하고, 템플릿에 절을 더하면 `doctor`가 모든 소비자에게 + "절 없음, 수동 병합" 경고를 낸다. 플러그인 결정(D7–D10)까지 소비자에게 배포되고 있다. +- **영향**: 소비자 프로젝트(결정 로그 seed, `AGENTS.md` 결정 규범, `doctor` 경고), 메인테이너(헌장이 없어 기능 범위 판단이 사례별). +- **기대 결과**: 소비자 템플릿에는 팀 운영 결정(D2·D4·D5·D6)만, 저장소 로그에는 D2–D11 전부. D11 기록. 사이클 정의 문서 커밋. +- **제약**: 소비자에게 새 경고를 만들지 않는다. 두 로그가 공유하는 절은 글자 그대로 같아야 한다. + +## 설계 / 접근 +1. `templates/docs/decisions.md`에서 D7–D10 절을 빼고 머리말을 "팀 운영 결정"으로 고친다. +2. `docs/decisions.md` 머리말을 "플러그인 저장소의 결정 로그"로 고치고 D11을 append한다. +3. `doctor` `DECISION_HEADINGS` → D2·D4·D5·D6. +4. 바이트 동일성 테스트를 "템플릿은 팀 운영 결정만 + 공유 절은 레포와 글자 그대로 같음 + 레포에 D7–D11 존재"로 바꾼다. + fence 계약 테스트의 감시자를 D8 → D6으로 옮기고, 판별력을 지키려 "closer 뒤 공백 허용 / 다른 글자 불허"를 두 행으로 나눈다. +5. 소비자 쪽에서 끊길 D7·D8 참조 정리: `templates/AGENTS.md.hbs`·루트 `AGENTS.md`의 D7 줄 제거(드리프트 테스트로 둘이 같아야 함), + `commands/harness-migrate.md`·`harness-review.md`는 "플러그인 저장소 `docs/decisions.md`"로 명시. +6. `docs/harness-cycle-draft.md`(미커밋 초안) → `docs/harness-cycle.md`로 정식화, 구현 순서(§6) 추가. + +## Ontology +*이 task가 다루는 핵심 개념의 정의. "X가 정확히 뭔가?"에 답한다.* + +- **팀 운영 결정**: 소비자 팀이 매 세션 지킬 규범의 근거(D2·D4·D5·D6). 템플릿으로 배포한다. +- **플러그인 결정**: 플러그인을 어떻게 만들지에 대한 결정(D7 이후). 플러그인 저장소에만 둔다. + +## Ambiguity 자가진단 +*각 항목이 명확하면 체크. 3개 이상 미체크면 구현 진입 금지 — 인터뷰/브레인스토밍으로 복귀해 +모호성을 제거한다. 게이트를 통과하면 그 근거를 위 Ontology 섹션에 한 줄로 남긴다.* + +- [x] **Goal 명확도** (40%) — 목표가 한 문장으로 구체화되었는가? +- [x] **Constraint 명확도** (30%) — 기술/시간/범위 제약이 명시되었는가? +- [x] **Success 기준** (30%) — 완료를 어떻게 측정하는가? +- [x] **Context 명확도** (brownfield 한정) — 영향 받는 기존 코드/파일을 식별했는가? +- [x] **Ambiguity ≤ 0.2** — 위 항목 가중합 ≥ 0.8 + +근거: 결정 내용은 2026-10-05 메인테이너 대화(4-1~4-6)로 확정됐고, 영향 파일은 grep으로 전수 확인했다. + +## Done evidence +```json +{ "version": 1, "review": "required", "tests": "required" } +``` +성공 기준: `npm run test` 통과(분리·동일성·fence 계약 테스트 포함), `npm run docs:check` 통과, 템플릿 헤딩 = D2·D4·D5·D6. + +## 참고 +*코드 기반 참조가 산문 설계보다 정밀하다 — 테스트 스위트·Boundary contract(JSON Schema)· +다이어그램·기존 코드 경로를 우선 링크하고, 산문은 코드로 표현 못 하는 의도만 담는다.* + +- 정본: `docs/decisions.md` D11, `docs/harness-cycle.md` +- 테스트: `tests/agent-files.test.mjs`(분리·동일성), `tests/doctor.test.mjs`(`checkDecisionLog`·fence 계약) +- CHANGELOG: PR #120과 같은 `[Unreleased]` 절에 항목을 더하므로 머지 순서에 따라 충돌이 날 수 있다(둘 다 남긴다). diff --git a/src/commands/doctor.mjs b/src/commands/doctor.mjs index ba6fcd8d..8e30358a 100644 --- a/src/commands/doctor.mjs +++ b/src/commands/doctor.mjs @@ -467,9 +467,10 @@ export async function checkCodexHookTrust(targetDir, env = process.env) { // upstream D-log sections (DECISION_HEADINGS) — and init cannot deliver them without // clobbering the team's own log. Warn-level: a missing file is fixable by init (scaffold // copies it); missing sections need a manual merge from the plugin's -// templates/docs/decisions.md. +// templates/docs/decisions.md. Only the team-operating decisions ship (D11): D7 onward are +// plugin decisions kept in the plugin repo's own log, so they are not checked here. export const DECISION_LOG_PATH = 'docs/decisions.md'; -export const DECISION_HEADINGS = ['## D2', '## D4', '## D5', '## D6', '## D7', '## D8', '## D9', '## D10']; +export const DECISION_HEADINGS = ['## D2', '## D4', '## D5', '## D6']; // Derived so the absence message cannot drift from the list it describes. const DECISION_IDS = DECISION_HEADINGS.map(h => h.replace(/^## /, '')).join('/'); diff --git a/templates/AGENTS.md.hbs b/templates/AGENTS.md.hbs index c398af1f..a5245e57 100644 --- a/templates/AGENTS.md.hbs +++ b/templates/AGENTS.md.hbs @@ -51,7 +51,6 @@ > `harness-team summary --write`로만 갱신한다. > - **D6**: 작업 산출물에는 별도 컨텍스트의 **read-only 검증자**를 붙일 수 있다 — 검증자는 반박만 하고 > 고치지 않으며, 반영은 작성 세션이 재현·판별한 뒤 단일 스레드로 한다(절차는 harness-review 명령 문서). -> - **D7**: OpenCode·Gemini는 하네스 멤버에서 제외했다. ### 리뷰 프로토콜 중요한 변경(새 기능, 아키텍처, 복잡한 리팩토링, 보안, 스키마/API) 완료 후 diff --git a/templates/docs/decisions.md b/templates/docs/decisions.md index c57bdc4e..d7dfdfd0 100644 --- a/templates/docs/decisions.md +++ b/templates/docs/decisions.md @@ -4,7 +4,8 @@ AI 팀 운영 규범의 근거와 이력. `AGENTS.md`의 역할·프로토콜 요약한 것이며, 전문(배경·근거·재평가 조건)은 이 파일이 정본이다. 새 결정은 번호를 이어 아래에 append 한다 — 규범이 바뀌면 `AGENTS.md`의 요약도 함께 갱신한다. -> 상류 출처: D2·D4·D5·D6·D7은 team-harness 플러그인 기본값의 결정 이력이다(bd-makers/team-harness). +> 상류 출처: D2·D4·D5·D6은 team-harness 플러그인이 제공하는 **팀 운영 결정**이다(bd-makers/team-harness). +> 플러그인 자체의 결정(D7 이후)은 플러그인 저장소의 `docs/decisions.md`에만 있다. > 프로젝트 고유 결정은 그 아래에 이어서 기록한다. ## D2 (2026-06-11) — drive/리뷰어 역할 분리 @@ -53,138 +54,3 @@ D4의 단일 스레드 쓰기 규칙이 금지하는 범위는 **같은 워킹 판정하지 못한다(가드 스스로 "이 체크가 막는 것은 망각이다"라고 명시). 품질 판정을 작업자 자신의 자가 채점에 두면 낙관적 체크가 게이트를 무력화하므로, 판정 주체를 분리하되 실행 메커니즘은 기존 리뷰 경로를 재사용해 표면 증가를 최소화한다. - -## D7 (2026-09-03) — OpenCode·Gemini 멤버 제외, 플러그인 소스 저장소는 자기 하네스를 dogfood하지 않음 - -OpenCode와 Gemini를 하네스 멤버에서 **우선 제외**한다. 스캐폴드 파일(`GEMINI.md`, `.opencode/opencode.json`), -역할표 행, `doctor`의 외부 도구·파일 검사, `.gitignore` 항목, 백업 스크립트 ITEMS, 리뷰 엔진 목록과 -probe 폴백 체인(codex → claude)에서 모두 뺀다. D2의 리뷰어 집합은 Codex(+ 같은 모델의 `claude` 엔진, -plan 모드)가 되고, D4의 "Claude·OpenCode 병렬 쓰기 금지" 조항은 멤버 일반("어떤 세션·에이전트도")으로 -읽는다. 근거: 2026-09-03 점검에서 두 표면이 규범과 어긋난 채 방치돼 있었다 — `opencode.json`이 -`.claude/rules/*.md`를 경로 스코프 없이 매 세션 로드하게 하면서 README는 OpenCode가 `.claude/`를 보지 -않는다고 적었고, `GEMINI.md`는 read-only 리뷰어에게 artifact 기록(쓰기)을 지시했다. 사용 흔적이 없는 -표면은 고치는 것보다 빼는 것이 단순하다(핵심 원칙 "단순함 우선"). 코드는 git 이력에 남고 재도입은 -별도 결정으로 한다. 같은 릴리스에서 `apply` 명령을 삭제했다 — `runInit`의 별칭이었고, `init`이 -마커 병합으로 멱등이라 재실행 동사를 둘 이유가 없었다. - -같은 날 **플러그인 소스 저장소(bd-makers/team-harness)는 자기 하네스를 dogfood하지 않는다**는 관행을 -결정으로 기록한다: 이 저장소에는 `.claude/hooks`·`.claude/rules`·`.cursor`가 없고 -`.claude/settings.json`에 SessionStart 훅이 없다. `doctor`는 plugin-dev 모드로 이를 n/a 처리한다. -AGENTS.md의 task-gate·rules 서술은 **소비자 프로젝트**에 대한 규범이다. 근거: 소비자 훅은 PATH의 -전역 `harness-team`을 호출하는데 소스 저장소는 `node bin/harness-team.mjs`로 개발 중 버전을 돌려야 -하고, 두 경로가 갈리면 훅이 낡은 전역 CLI를 타는 사고가 난다(`doctor`의 CLI drift 검사가 그 사고의 -산물이다). 소스 저장소가 자기 훅을 쓰려면 이 두 경로를 먼저 통일해야 하며, 그 결정은 열어 둔다. - -## D8 (2026-09-07) — 템플릿 갱신 정책: provenance 기반 refresh, 전면 덮어쓰기 금지 - -`templates/` 아래 정적 표면(`.claude/hooks`·`.claude/rules`·`.claude/skills`·`docs/` seed)은 -`copyStaticAssets`가 `skipExisting: true`로 복사하므로, **템플릿을 수정해도 기존 설치에는 도달하지 -않는다.** `copyTree`의 판정이 파일 단위라 *신규* 파일은 도달하지만 *수정된* 파일은 영영 도달하지 -않는다 — 이 비대칭이 문제의 본체다(샌드박스 소비자 프로젝트에서 실측). - -**결정**: 기존 설치에 대한 갱신 경로는 **`migrate`의 provenance 기반 refresh 하나로 통일한다.** -설치본의 바이트가 하네스가 실제로 배포한 적 있는 버전(sha256 테이블)과 일치할 때만 최신 템플릿으로 -갱신하고, 그 외에는 "커스터마이즈"로 보아 절대 덮지 않고 경고만 남긴다. `init`은 갱신 동사가 아니다. - -이 정책은 새로 만든 것이 아니라 `refreshClaudeHooks`(2026-08, PR #29 배달 경로)가 이미 쓰던 것이다. -이번 결정은 **적용 범위를 훅 6개에서 스킬 3 + 규칙 4까지 넓히고**(16개 정적 파일 중 6 → 13), -`doctor`에 stale 경고를 추가해 그 경로를 발견 가능하게 만든 것이다. 발견성이 없으면 정책이 있어도 -아무도 부르지 않는다 — 이 드리프트가 오래 눈에 띄지 않은 이유가 그것이다. - -**비목표**: `docs/` seed(`README.md`·`decisions.md`·`.gitkeep`)는 refresh 대상이 **아니다**. -설치 이후 팀이 저작하는 산출물이고(이 파일 자체가 그 예다), `copyStaticAssets`의 -`skip existing to preserve team work` 주석이 그 보존을 의도된 설계로 명시한다. - -**기각한 대안** -- **`skipExisting: false`로 전환** — 사용자가 편집한 훅·규칙을 말없이 덮는다. -- **마커 병합으로 승격**(`AGENTS.md` 방식) — `SKILL.md`는 에이전트가 읽는 산문이라 마커가 - 에이전트 가시 텍스트를 오염시킨다. 실비용이 있다. -- **설치 시점 매니페스트**(`.harness/installed.json`) — 확장성은 낫지만 **이미 드리프트한 기존 - 설치에는 baseline이 없어 아무 도움이 안 된다.** sha 테이블이 소급 동작하는 것이 정확히 그 이유다. - -**대가**: sha 테이블은 손으로 유지한다. 픽스처(`tests/fixtures/stock-templates`)와 개수를 대조하는 -드리프트 가드 테스트가 안전망이고, 표면이 13개라 감당 가능한 규모다. 표면이 크게 늘면 재검토한다. - -**열어 둔 예외**: 훅 refresh의 `legacyStock` 휴리스틱(`pre-commit-check.sh`에 `pnpm tsc --noEmit`이 -있고 `detect_pm`이 없으면 stock으로 간주)은 sha 테이블 이전부터 있던 net이라, 그 옛 훅에 사용자가 -줄을 덧붙인 경우에도 stock으로 판정해 덮어쓴다 — "커스터마이즈는 덮지 않는다"는 위 계약의 유일한 -구멍이다(2026-09-08 codex 리뷰 MAJOR). 좁히면 아주 오래된 바이트 드리프트본을 잡던 net이 사라지므로 -의도적으로 남겼다. `doctor`가 이제 모든 소비자를 `migrate`로 유도하니 이 경로는 전보다 자주 실행된다 — -훅 표면을 손볼 때 함께 재평가할 것. - -## D9 (2026-09-12) — Codex 강제력은 SessionStart 하나로 고정한다 (control 계층 배선 기각) - -`templates/.codex/hooks.json`은 SessionStart 1종이고 Claude는 5개 이벤트 + 스크립트 6종이다. -"Codex에도 control 계층(`PreToolUse` 차단·관측)을 배선할 것인가"를 2026-09-12에 결정했다. - -**전제는 다 확인됐다 — 기술적 제약이 아니다.** codex-cli 0.153.4 실측: -이벤트 12종(`PreToolUse`·`PostToolUse`·`PermissionRequest`·`SessionStart`·`Stop` 등)을 지원하고, -`Command blocked by PreToolUse hook:`과 `PreToolUseDecisionWire`(approve/block/allow/deny/ask)가 -바이너리에 있으며, wire protocol(`hookSpecificOutput`·`permissionDecision`·`decision`/`reason`)이 -Claude와 **같다** — 훅 스크립트를 거의 그대로 재사용할 수 있다. 실험에서 6개 이벤트가 전부 발화했다 -(`docs/chad/codex-project-hooks-probe/`). - -**결정**: 배선하지 않는다. Codex의 강제력은 **SessionStart(컨텍스트 주입) 하나**로 유지한다. - -**왜** — 기술이 아니라 **D2 때문이다.** D2는 Codex를 **리뷰어(read-only)** 로 규정했고, 그 가치는 -작성자와 리뷰어의 분리에서 나온다. 리뷰어 세션에 쓰기 차단 훅(`protect-files`·`block-dangerous-git`)을 -다는 것은 "리뷰어가 쓰지 않는다"는 전제를 **훅으로 다시 강제**하는 일이다 — 이미 `--sandbox read-only`로 -보장되는 것을 두 번째 계층으로 얹는 셈이고, 지키는 대상이 없다. 관측(`observe-tools`) 역시 리뷰어의 -도구 호출을 세는 것이라 하네스의 관측 목적(작성 세션의 루프 감지)과 맞지 않는다. - -**비목표**: 이 결정은 Codex 훅의 **품질**을 낮추지 않는다. 0.38.3에서 SessionStart는 주입 형식 -(`hookSpecificOutput.additionalContext`)을 고쳤고 신뢰 부재를 `doctor`가 경고한다 — 있는 것 하나를 -제대로 돌리는 것과 새 계층을 얹는 것은 다른 문제다. - -**기각한 대안** -- **PreToolUse만 선별 배선** — "차단은 안 하고 관측만"도 같은 문제를 남긴다. 리뷰어의 도구 호출을 - 작성 세션의 신호와 같은 로그에 섞으면 트립와이어 판정이 오염된다. -- **결정 보류** — 보류 상태가 오래가면 "설치는 했는데 왜 안 도나"를 다시 묻게 된다. 0.38.3에서 - 현재 상태를 정직하게 문서화했으므로 지금 닫는 편이 낫다. - -**뒤집는 조건**: Codex를 리뷰어가 아니라 **작성 세션**으로 쓰기 시작하면 이 결정은 무효다. -그때는 D2를 먼저 고쳐야 하고, 배선 비용은 위 전제대로 크지 않다. - -**AGENTS.md 규범 목록에 넣지 않은 이유**: 이 결정은 에이전트가 매 세션 지켜야 할 규칙이 아니라 -**닫힌 결정**이다(무엇을 더 만들지 않는다). eager 계층 예산(24 KiB, 현재 소계 17.5 KiB 상한)에 -여유가 27 B뿐이기도 하다 — MAINTAINING의 "절차 본문은 lazy 정본으로" 원칙 그대로 D-log에만 둔다. - -## D10 (2026-09-25) — CLAUDE.md 유지: AGENTS.md 네이티브 지원(2.1.277, fallback 기본)에도 불구하고 - -Claude Code 2.1.277부터 AGENTS.md를 네이티브로 읽는다. "그렇다면 `CLAUDE.md`(`@AGENTS.md` import)를 -없애고 AGENTS.md 하나로 갈 것인가"를 2026-09-25에 결정했다. - -**확인된 사실** — 2.1.281 바이너리에서 직접 확인: -- 기본 동작은 **fallback**이다 — 프로젝트에 CLAUDE.md가 **없을 때만** AGENTS.md를 대신 읽는다. -- 내장 plugin `agents-md`의 `instructionFiles` 옵션(사용자 `/config`): `claude-md` | - `claude-md-or-agents-md`(기본) | `claude-md-and-agents-md`(CLAUDE.md가 이미 import한 파일은 중복 - 로드하지 않음) | `managed-only`. -- 기능 전체가 원격 플래그 `tengu_agents_md_mod`(기본 true)에 묶여 있다. - -**미검증**(에이전트 조사만, 바이너리·실측 확인 없음): AGENTS.md 안의 `@import` 지원, 상위 디렉터리 -walk, `/memory` 미표시, 2.1.280에서 Bedrock/Vertex/Foundry로 확장. - -**결정**: CLAUDE.md(`@AGENTS.md` import) 구조를 **유지한다.** - -**왜** — 제거해서 얻는 것이 거의 없고 잃는 것은 조용히 잃는다. -- CLAUDE.md를 없애면 팀 계약의 로드 여부가 **버전(<2.1.277)·사용자 `/config`(`claude-md`·`managed-only`)· - 원격 플래그**에 따라 갈리고, 빠질 때 아무 경고도 없다. 지금 구조는 세 조건 모두에서 같은 결과를 낸다. -- Claude 전용 절(현재 CLAUDE.md 약 5.8 KB)은 어차피 어딘가로 옮겨야 한다. AGENTS.md로 옮기면 - Codex·Cursor에 Claude 전용 레버(플랜 모드·서브에이전트·advisor)가 노출되고, `.claude/rules`로 - 옮기면 cursor 미러와 `doctor`의 유래 마커 검사와 충돌한다. -- 영향 범위가 작지 않다 — CLAUDE.md를 전제하는 코드 경로가 약 13곳(`src/harness.mjs:16`, - `src/commands/doctor.mjs:685`, migrate·backup·clone·symlink·delete·upgrade·init, 셸 템플릿 3종)이고 - 테스트 참조가 약 100건(픽스처 제외 97행)이다. - -**기각한 대안** -- **CLAUDE.md 제거 + AGENTS.md 단독** — 위 세 조건 중 하나라도 어긋나면 계약이 조용히 빠진다. -- **`claude-md-and-agents-md` 모드를 팀 설정으로 요구** — 사용자 `/config` 값이라 하네스가 강제하거나 - 검사할 수단이 없고, 강제해도 CLAUDE.md가 import한 파일은 중복 로드되지 않으니 얻는 것이 없다. - -**재검토 조건**: ① 원격 플래그 없이 GA ② 팀 최소 버전 ≥ 2.1.277을 강제할 수 있음 ③ `/memory`에 표시됨. -셋이 갖춰지면 opt-in `init --agents-md-only`부터 시도한다 — Claude 전용 절은 -`.claude/rules/harness-workflow.md`로 옮기되 cursor 미러·유래 마커 검사에서 예외로 두고, CLAUDE.md는 -user 영역이 비었을 때만 제거한다. - -**AGENTS.md 규범 목록에 넣지 않은 이유**: D9와 같다 — 에이전트가 매 세션 지킬 규칙이 아니라 현 구조를 -유지하는 **닫힌 결정**이다. `doctor`의 eager 계층 주석이 이 결정을 가리킨다. diff --git a/tests/agent-files.test.mjs b/tests/agent-files.test.mjs index 83e97b15..b6aaab11 100644 --- a/tests/agent-files.test.mjs +++ b/tests/agent-files.test.mjs @@ -74,7 +74,7 @@ test('AGENTS.md(core) roles 표는 D2 반영 — Claude=drive, Codex=리뷰어, assert.ok(rows.some((l) => l.includes('**Claude Code**') && l.includes('drive')), 'Claude drive 행'); assert.ok(rows.some((l) => l.includes('**Codex**') && l.includes('리뷰어')), 'Codex 리뷰어 행'); assert.ok(!rows.some((l) => /OpenCode|Gemini/.test(l)), '역할표에 OpenCode·Gemini 행이 없어야 한다 (D7)'); - assert.match(out, /\*\*D7\*\*/, 'D7 요약이 코어에 있어야 한다'); + assert.doesNotMatch(out, /\*\*D7\*\*/, 'D7은 플러그인 결정이라 소비자 코어에 요약하지 않는다 (D11)'); }); // D4 (2026-07-28) — 쓰기는 단일 스레드. scaffold 되는 AGENTS.md/CLAUDE.md 쌍이 @@ -99,9 +99,24 @@ test('templates/docs/decisions.md는 D2/D4/D5 전문(날짜 포함)을 보존한 assert.match(log, /## D4 \(2026-07-28\)/, 'D4 전문 보존'); assert.match(log, /## D5 \(2026-08-20\)/, 'D5 전문 보존'); assert.match(log, /12-Factor Agents #8/, 'D4 근거(1차 소스) 보존'); - // 레포도 자기 하네스를 쓴다 — 스캐폴드본과 레포본이 어긋나면 드리프트다. - assert.equal(await readFile(join(ROOT, 'docs', 'decisions.md'), 'utf8'), log, - '레포 docs/decisions.md는 템플릿과 동일해야 함'); +}); + +// D11 (2026-10-05) — 결정 로그 분리. 소비자 템플릿에는 팀 운영 결정(D2·D4·D5·D6)만 가고, D7 이후는 +// 플러그인 결정이라 레포 로그에만 있다. 두 로그가 공유하는 절은 한 글자도 달라지면 안 된다 — 같은 결정의 +// 두 사본이 갈라지면 소비자와 메인테이너가 다른 규범을 읽는다. +const dlogSections = (log) => { + const out = new Map(); + const parts = log.split(/^(?=## D\d+ )/m).slice(1); + for (const p of parts) out.set(p.match(/^## (D\d+)/)[1], p.trimEnd()); + return out; +}; + +test('결정 로그 분리: 템플릿은 팀 운영 결정만, 공유 절은 레포 로그와 글자 그대로 같다', async () => { + const tplLog = dlogSections(await readFile(join(ROOT, 'templates', 'docs', 'decisions.md'), 'utf8')); + const repoLog = dlogSections(await readFile(join(ROOT, 'docs', 'decisions.md'), 'utf8')); + assert.deepEqual([...tplLog.keys()], ['D2', 'D4', 'D5', 'D6'], '템플릿에는 팀 운영 결정만'); + for (const [id, body] of tplLog) assert.equal(repoLog.get(id), body, `${id} 절이 레포 로그와 다르다`); + for (const id of ['D7', 'D8', 'D9', 'D10', 'D11']) assert.ok(repoLog.has(id), `레포 로그에 ${id}가 있어야 한다`); }); // D6 (2026-08-26) — 적대적 검증. 규범(decisions 전문 + AGENTS 요약)과 소비 표면(리뷰 마커 diff --git a/tests/doctor.test.mjs b/tests/doctor.test.mjs index 6b1fa702..17914e57 100644 --- a/tests/doctor.test.mjs +++ b/tests/doctor.test.mjs @@ -201,7 +201,8 @@ test('checkDecisionLog: docs/decisions.md 부재 → 경고 (init 스캐폴드 assert.ok(typeof w === 'string', 'returns a warning string'); assert.match(w, /없음/); assert.match(w, /harness-team init/, '부재는 init 스캐폴드가 해결하므로 init로 유도'); - assert.match(w, /D2\/D4\/D5\/D6\/D7\/D8\/D9\/D10/, '검사 대상 절 ID를 DECISION_HEADINGS에서 파생해 나열'); + assert.match(w, /D2\/D4\/D5\/D6 /, '검사 대상 절 ID를 DECISION_HEADINGS에서 파생해 나열'); + assert.doesNotMatch(w, /D7/, 'D7 이후는 플러그인 결정이라 검사 대상이 아니다 (D11)'); assert.match(w, /templates\/docs\/decisions\.md/, '가져올 원본 위치를 안내'); } finally { await rm(dir, { recursive: true, force: true }); } }); @@ -214,7 +215,7 @@ test('checkDecisionLog: 일부 절 누락 → 누락 절만 나열 + 템플릿 try { const w = await checkDecisionLog(dir); assert.ok(typeof w === 'string', 'returns a warning string'); - assert.match(w, /## D4, ## D5, ## D6, ## D7, ## D8, ## D9, ## D10 절 없음/, '누락된 절만 정확히 나열'); + assert.match(w, /## D4, ## D5, ## D6 절 없음/, '누락된 절만 정확히 나열'); assert.doesNotMatch(w, /## D2/, '존재하는 D2는 누락 목록에 없어야 한다'); // 원래 이 단언은 /init/ 부분일치였다. "init로 유도하지 말 것"이 의도인데, 문구가 // "init·migrate 어느 쪽도 덮어쓰지 않는다"고 *설명*하는 것까지 막고 있었다 — @@ -241,7 +242,7 @@ test("checkDecisionLog: fenced code block 안의 ## D은 절이 아니다 ( try { const w = await checkDecisionLog(dir); assert.ok(typeof w === "string", "returns a warning string"); - assert.match(w, /## D4, ## D5, ## D7, ## D8, ## D9, ## D10 절 없음/, "펜스 안 D4·D5는 누락으로, 펜스 뒤 D6은 존재로"); + assert.match(w, /## D4, ## D5 절 없음/, "펜스 안 D4·D5는 누락으로, 펜스 뒤 D6은 존재로"); assert.doesNotMatch(w, /## D[26]\b/, "존재하는 D2/D6은 누락 목록에 없어야 한다"); } finally { await rm(dir, { recursive: true, force: true }); } }); @@ -254,31 +255,35 @@ test("checkDecisionLog: 닫히지 않은 fenced code block은 문서 끝까지 try { const w = await checkDecisionLog(dir); assert.ok(typeof w === "string", "returns a warning string"); - assert.match(w, /## D4, ## D5, ## D6, ## D7, ## D8, ## D9, ## D10 절 없음/, "D2 외 전부 누락"); + assert.match(w, /## D4, ## D5, ## D6 절 없음/, "D2 외 전부 누락"); } finally { await rm(dir, { recursive: true, force: true }); } }); // 펜스 계약을 mutation이 못 뚫게 표로 고정한다(2026-09-09 codex P2·P3). 각 행: D2는 항상 진짜 절, -// 가운데 줄들이 케이스, 끝의 D8은 "제거가 문서 끝까지 먹지 않았다"의 감시자. expect = D4~D7 중 누락으로 -// 보고돼야 하는 것. 근거는 CommonMark §4.5: 여는 펜스는 백틱·물결 3개 이상(들여쓰기 0~3), 닫는 펜스는 -// 같은 문자로 여는 것 이상 길이(뒤에 공백만 허용), 백틱 펜스의 info string에는 백틱이 올 수 없고, -// 줄 끝은 LF·CRLF·단독 CR 모두다. +// 가운데 줄들이 케이스(D4·D5만 움직인다), 끝의 D6은 "제거가 문서 끝까지 먹지 않았다"의 감시자. +// expect = D4·D5 중 누락으로 보고돼야 하는 것(빈 배열이면 경고 없음 = null). 2026-10-05 D11로 검사 목록이 +// D2·D4·D5·D6으로 줄어 감시자를 D8에서 D6으로 옮겼다. 근거는 CommonMark §4.5: 여는 펜스는 백틱·물결 3개 이상 +// (들여쓰기 0~3), 닫는 펜스는 같은 문자로 여는 것 이상 길이(뒤에 공백만 허용), 백틱 펜스의 info string에는 +// 백틱이 올 수 없고, 줄 끝은 LF·CRLF·단독 CR 모두다. const FENCE_CASES = [ - ['4자 opener는 3자 closer로 안 닫힌다', ['````', '## D4', '```', '## D5', '````', '## D6'], ['D4', 'D5', 'D7', 'D9', 'D10']], - ['다른 문자 closer로는 안 닫힌다', ['```', '## D4', '~~~', '## D5', '```', '## D6'], ['D4', 'D5', 'D7', 'D9', 'D10']], - ['더 긴 closer로는 닫힌다', ['```', '## D4', '`````', '## D5'], ['D4', 'D6', 'D7', 'D9', 'D10']], - // `\`\`\` x`는 닫지 못하므로 D7까지 펜스 안이고, 감시자 D8을 살리려면 그 뒤에 진짜 closer가 필요하다. - ['closer 뒤 공백은 허용, 다른 글자는 불허', ['```', '## D4', '``` ', '## D5', '```', '## D6', '``` x', '## D7', '```'], ['D4', 'D6', 'D7', 'D9', 'D10']], - ['들여쓰기 3칸까지는 펜스다', [' ```', '## D4', ' ```', '## D5'], ['D4', 'D6', 'D7', 'D9', 'D10']], - ['들여쓰기 4칸은 펜스가 아니다(코드 블록 줄일 뿐)', [' ```', '## D4', ' ```', '## D5'], ['D6', 'D7', 'D9', 'D10']], - ['백틱 펜스 info string에 백틱이 있으면 펜스가 아니다', ['```md `x', '## D4', '## D5'], ['D6', 'D7', 'D9', 'D10']], - ['물결 펜스 info string에는 백틱이 와도 된다', ['~~~md `x', '## D4', '~~~', '## D5'], ['D4', 'D6', 'D7', 'D9', 'D10']], + ['4자 opener는 3자 closer로 안 닫힌다', ['````', '## D4', '```', '## D5', '````'], ['D4', 'D5']], + ['다른 문자 closer로는 안 닫힌다', ['```', '## D4', '~~~', '## D5', '```'], ['D4', 'D5']], + ['더 긴 closer로는 닫힌다', ['```', '## D4', '`````', '## D5'], ['D4']], + // 공백 closer가 안 닫히면 펜스가 끝까지 가 D5와 감시자 D6까지 누락으로 나온다. + ['closer 뒤 공백은 허용', ['```', '## D4', '``` ', '## D5'], ['D4']], + // `\`\`\` x`가 닫으면 D5가 절로 새어 나온다 — 진짜 closer로 감시자 D6을 살린다. + ['closer 뒤 다른 글자는 불허', ['```', '## D4', '``` x', '## D5', '```'], ['D4', 'D5']], + ['들여쓰기 3칸까지는 펜스다', [' ```', '## D4', ' ```', '## D5'], ['D4']], + ['들여쓰기 4칸은 펜스가 아니다(코드 블록 줄일 뿐)', [' ```', '## D4', ' ```', '## D5'], []], + ['백틱 펜스 info string에 백틱이 있으면 펜스가 아니다', ['```md `x', '## D4', '## D5'], []], + ['물결 펜스 info string에는 백틱이 와도 된다', ['~~~md `x', '## D4', '~~~', '## D5'], ['D4']], ]; for (const [name, mid, expect] of FENCE_CASES) { test(`checkDecisionLog fence 계약: ${name}`, async () => { - const dir = await makeDecisionLogFixture(['# Team Decision Log', '', '## D2 — a', ...mid, '## D8 — tail', ''].join('\n')); + const dir = await makeDecisionLogFixture(['# Team Decision Log', '', '## D2 — a', ...mid, '## D6 — tail', ''].join('\n')); try { const w = await checkDecisionLog(dir); + if (expect.length === 0) { assert.equal(w, null, name); return; } const want = expect.map(d => '## ' + d).join(', '); assert.ok(typeof w === 'string', 'returns a warning string'); assert.equal(/에 (.+?) 절 없음/.exec(w)?.[1], want, name); @@ -290,10 +295,10 @@ for (const [name, mid, expect] of FENCE_CASES) { // 써야 한다 — 어긋나면 CRLF·CR 문서에서만 펜스 안 헤딩이 절로 새어 나온다. for (const [name, eol] of [['CRLF', '\r\n'], ['단독 CR', '\r']]) { test(`checkDecisionLog fence 계약: ${name} 줄 끝에서도 펜스를 걷어낸다`, async () => { - const dir = await makeDecisionLogFixture(['# Team Decision Log', '', '## D2 — a', '```', '## D4', '```', '## D5', '## D8 — tail', ''].join(eol)); + const dir = await makeDecisionLogFixture(['# Team Decision Log', '', '## D2 — a', '```', '## D4', '```', '## D5', '## D6 — tail', ''].join(eol)); try { const w = await checkDecisionLog(dir); - assert.equal(/에 (.+?) 절 없음/.exec(w ?? '')?.[1], '## D4, ## D6, ## D7, ## D9, ## D10', name); + assert.equal(/에 (.+?) 절 없음/.exec(w ?? '')?.[1], '## D4', name); } finally { await rm(dir, { recursive: true, force: true }); } }); } @@ -308,7 +313,7 @@ test('checkDecisionLog: 옛 스캐폴드(D2/D4/D5만) → 이후 절 전부 누 try { const w = await checkDecisionLog(dir); assert.ok(typeof w === 'string', 'returns a warning string'); - assert.match(w, /## D6, ## D7, ## D8, ## D9, ## D10 절 없음/, '옛 스캐폴드 이후 절만 누락으로 나열'); + assert.match(w, /## D6 절 없음/, '옛 스캐폴드 이후 절만 누락으로 나열'); assert.doesNotMatch(w, /## D[245]\b/, '존재하는 D2/D4/D5는 누락 목록에 없어야 한다'); } finally { await rm(dir, { recursive: true, force: true }); } }); From 5481e4cedd2e8066100eec0bace21e83c1c93d52 Mon Sep 17 00:00:00 2001 From: hslee Date: Mon, 5 Oct 2026 18:05:51 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(doctor):=20=EA=B2=B0=EC=A0=95=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EA=B2=BD=EA=B3=A0=EC=9D=98=20D8=20?= =?UTF-8?q?=EC=B0=B8=EC=A1=B0=EB=A5=BC=20=ED=94=8C=EB=9F=AC=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=A0=80=EC=9E=A5=EC=86=8C=EB=A1=9C=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C=ED=95=9C=EB=8B=A4=20(codex=20P3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../decision-log-split-artifact.md | 23 +++++++++++++++++++ .../decision-log-split-handoff.md | 20 ++++++++++++++++ .../decision-log-split-meta.json | 12 +++++++++- .../decision-log-split-plan.md | 2 +- src/commands/doctor.mjs | 2 +- 5 files changed, 56 insertions(+), 3 deletions(-) diff --git a/docs/hslee/decision-log-split/decision-log-split-artifact.md b/docs/hslee/decision-log-split/decision-log-split-artifact.md index 9ed6bc48..f46f9a3d 100644 --- a/docs/hslee/decision-log-split/decision-log-split-artifact.md +++ b/docs/hslee/decision-log-split/decision-log-split-artifact.md @@ -3,11 +3,34 @@ *최종 결과물과 학습 내용을 기록한다.* ## 결과 +- 2026-10-05 결정 로그 분리(템플릿 D2·D4·D5·D6) · D11 기록 · `docs/harness-cycle.md` 정식화. 검증: `npm run test` 1081 중 pass 1080 · fail 0, `docs:check` 최신. +- 소비자 영향: 새 `doctor` 경고 없음. `AGENTS.md` 결정 규범의 D7 줄 제거는 관리 절 변경이라 소비자가 원할 때 `init --yes`로 받는다. +- 미검증: fence 계약 테스트의 판별력은 추론으로만 확인했다(소스를 변형해 실패를 확인하는 mutation 검증은 하지 않음). ## Reviews *Codex 등 리뷰 실행 시 결과(요약·발견·조치)를 날짜와 함께 남긴다. 남기지 않은 리뷰는 "안 한 것"으로 간주.* *기계 판독용 마커를 함께 남긴다: ``* +### 2026-10-05T08:55:21.574Z — codex (harness-team review) + +- engine: codex · scope: diff · tip: 2724ff7ea93c5228cab520b90ebd3ebd1d684107 · exit 0 · 876 B + +```text +전하, **P1·P2 수준의 유의미한 결함은 발견하지 못했습니다.** + +- **P3 nit — `src/commands/doctor.mjs:531`**: 소비자 경고에 출처 없는 `D8` 참조가 남아 있습니다. 새 seed에는 D8이 없으므로, 명령 문서처럼 “플러그인 저장소의 D8”으로 명시하면 혼동을 줄입니다. + +템플릿 D2·D4·D5·D6 분리, 공유 절 동일성, doctor 목록 축소와 fence 테스트 재설계는 정합적입니다. D11은 사이클 문서의 미구현 안내·구현 순서와 함께 확인했으며, 중요한 사실 오류는 찾지 못했습니다. + +검증: 관련 읽기 전용 테스트 **5개 통과**, `docs:check`·`git diff --check` 통과. 전체 테스트와 임시 파일을 생성하는 doctor 테스트는 실행하지 않았습니다. 파일 변경은 없습니다. + +**최종 판정: PASS — P3 정리 권장.** +``` + + + +판별(2026-10-05): P3 1건 — **진짜 결함**으로 판단해 반영. `doctor` 결정 로그 경고 문구의 "D8"이 새 소비자 seed에는 없는 절을 가리켰다 → +"플러그인 저장소 decisions D8"로 명시(`src/commands/doctor.mjs:531`). 재검증: `node --test tests/doctor.test.mjs tests/agent-files.test.mjs` 145 pass. ## Learnings diff --git a/docs/hslee/decision-log-split/decision-log-split-handoff.md b/docs/hslee/decision-log-split/decision-log-split-handoff.md index 0ae5a270..26250a82 100644 --- a/docs/hslee/decision-log-split/decision-log-split-handoff.md +++ b/docs/hslee/decision-log-split/decision-log-split-handoff.md @@ -1,3 +1,23 @@ # decision-log-split — Handoff (세션 종료 시 post-commit hook이 자동 갱신합니다) + +## 2026-10-05T08:54:02.319Z — 2724ff7 feat(decisions): 결정 로그를 팀 운영·플러그인 결정으로 나누고 D11 범위 헌장을 기록한다 +AGENTS.md | 1 - + CHANGELOG.md | 7 + + commands/harness-migrate.md | 2 +- + commands/harness-review.md | 2 +- + docs/decisions.md | 56 ++++- + docs/harness-cycle.md | 242 +++++++++++++++++++++ + .../decision-log-split-artifact.md | 13 ++ + .../decision-log-split-context.md | 22 ++ + .../decision-log-split-handoff.md | 3 + + .../decision-log-split-meta.json | 11 + + .../decision-log-split/decision-log-split-plan.md | 23 ++ + .../decision-log-split/decision-log-split-spec.md | 54 +++++ + src/commands/doctor.mjs | 5 +- + templates/AGENTS.md.hbs | 1 - + templates/docs/decisions.md | 138 +----------- + tests/agent-files.test.mjs | 23 +- + tests/doctor.test.mjs | 47 ++-- + 17 files changed, 481 insertions(+), 169 deletions(-) diff --git a/docs/hslee/decision-log-split/decision-log-split-meta.json b/docs/hslee/decision-log-split/decision-log-split-meta.json index 6b46e6b9..a316094e 100644 --- a/docs/hslee/decision-log-split/decision-log-split-meta.json +++ b/docs/hslee/decision-log-split/decision-log-split-meta.json @@ -7,5 +7,15 @@ "closedAt": null, "forcedAt": null, "forcedIssues": null, - "reviews": [] + "reviews": [ + { + "kind": "codex", + "engine": "codex", + "scope": "diff", + "tip": "2724ff7ea93c5228cab520b90ebd3ebd1d684107", + "at": "2026-10-05T08:55:21.574Z", + "exitCode": 0, + "outputBytes": 876 + } + ] } diff --git a/docs/hslee/decision-log-split/decision-log-split-plan.md b/docs/hslee/decision-log-split/decision-log-split-plan.md index f2aed71e..24215c9a 100644 --- a/docs/hslee/decision-log-split/decision-log-split-plan.md +++ b/docs/hslee/decision-log-split/decision-log-split-plan.md @@ -11,7 +11,7 @@ - [x] 소비자 쪽 D7·D8 참조 정리(AGENTS 템플릿·루트, migrate·review 명령) - [x] `docs/harness-cycle.md` 정식화 + 구현 순서 - [x] 검증: `npm run test` · `npm run docs:check` -- [ ] 외부 리뷰(R3) 기록 +- [x] 외부 리뷰(R3) 기록 — codex PASS, P3 1건 반영 - [ ] 커밋 · PR ## Ontology 변경 로그 diff --git a/src/commands/doctor.mjs b/src/commands/doctor.mjs index 8e30358a..bdaf18bb 100644 --- a/src/commands/doctor.mjs +++ b/src/commands/doctor.mjs @@ -528,7 +528,7 @@ export async function checkDecisionLog(targetDir, root) { const missing = DECISION_HEADINGS.filter(h => !new RegExp(`^${h}\\b`, 'm').test(prose)); if (missing.length === 0) return null; const source = root ? join(root, 'templates/docs/decisions.md') : '플러그인 templates/docs/decisions.md'; - return `${DECISION_LOG_PATH}에 ${missing.join(', ')} 절 없음 — 팀 결정 로그는 설치 후 팀이 저작하는 파일이라 init·migrate 어느 쪽도 덮어쓰지 않는다(D8: docs/ seed는 refresh 비목표). \`${source}\` 에서 해당 절을 복사해 ${DECISION_LOG_PATH} 끝에 덧붙여라 — 이미 있는 절은 건드리지 말 것`; + return `${DECISION_LOG_PATH}에 ${missing.join(', ')} 절 없음 — 팀 결정 로그는 설치 후 팀이 저작하는 파일이라 init·migrate 어느 쪽도 덮어쓰지 않는다(플러그인 저장소 decisions D8: docs/ seed는 refresh 비목표). \`${source}\` 에서 해당 절을 복사해 ${DECISION_LOG_PATH} 끝에 덧붙여라 — 이미 있는 절은 건드리지 말 것`; } // user handoff 는 추적하지 않는 워크트리 로컬 파일이다(USER_HANDOFF_IGNORE). 두 전환 누락을 잡는다 — From 63112cab415dcc8410059f63089d00de4dcc4406 Mon Sep 17 00:00:00 2001 From: hslee Date: Mon, 5 Oct 2026 18:06:19 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore(task):=20decision-log-split=20plan?= =?UTF-8?q?=C2=B7handoff=20=EA=B0=B1=EC=8B=A0=20(PR=20#121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../decision-log-split/decision-log-split-handoff.md | 8 ++++++++ docs/hslee/decision-log-split/decision-log-split-plan.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/hslee/decision-log-split/decision-log-split-handoff.md b/docs/hslee/decision-log-split/decision-log-split-handoff.md index 26250a82..05bc3ffb 100644 --- a/docs/hslee/decision-log-split/decision-log-split-handoff.md +++ b/docs/hslee/decision-log-split/decision-log-split-handoff.md @@ -21,3 +21,11 @@ AGENTS.md | 1 - tests/agent-files.test.mjs | 23 +- tests/doctor.test.mjs | 47 ++-- 17 files changed, 481 insertions(+), 169 deletions(-) + +## 2026-10-05T09:05:51.463Z — 5481e4c fix(doctor): 결정 로그 경고의 D8 참조를 플러그인 저장소로 명시한다 (codex P3) +.../decision-log-split-artifact.md | 23 ++++++++++++++++++++++ + .../decision-log-split-handoff.md | 20 +++++++++++++++++++ + .../decision-log-split-meta.json | 12 ++++++++++- + .../decision-log-split/decision-log-split-plan.md | 2 +- + src/commands/doctor.mjs | 2 +- + 5 files changed, 56 insertions(+), 3 deletions(-) diff --git a/docs/hslee/decision-log-split/decision-log-split-plan.md b/docs/hslee/decision-log-split/decision-log-split-plan.md index 24215c9a..d47b5bce 100644 --- a/docs/hslee/decision-log-split/decision-log-split-plan.md +++ b/docs/hslee/decision-log-split/decision-log-split-plan.md @@ -12,7 +12,7 @@ - [x] `docs/harness-cycle.md` 정식화 + 구현 순서 - [x] 검증: `npm run test` · `npm run docs:check` - [x] 외부 리뷰(R3) 기록 — codex PASS, P3 1건 반영 -- [ ] 커밋 · PR +- [x] 커밋 · PR (#121) ## Ontology 변경 로그 *개념이 새로 정의되거나 의미가 바뀌면 한 줄로 기록. spec.md의 Ontology 섹션을 갱신할 트리거가 된다.*