From fd37fa34b6ee510a077be456e2b3a297d02c1531 Mon Sep 17 00:00:00 2001 From: hslee Date: Sun, 27 Sep 2026 22:46:13 +0900 Subject: [PATCH 1/6] =?UTF-8?q?docs(task):=20user-handoff-parallel-conflic?= =?UTF-8?q?t=20spec=C2=B7plan=20=E2=80=94=20=EB=B3=91=EB=A0=AC=20PR?= =?UTF-8?q?=EC=9D=98=20user=20handoff=20=EC=B6=A9=EB=8F=8C=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=EC=A7=80=EC=99=80=20=EA=B6=8C=EC=9E=A5=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- ...user-handoff-parallel-conflict-artifact.md | 13 ++ .../user-handoff-parallel-conflict-context.md | 24 +++ .../user-handoff-parallel-conflict-handoff.md | 3 + .../user-handoff-parallel-conflict-meta.json | 11 ++ .../user-handoff-parallel-conflict-plan.md | 29 +++ .../user-handoff-parallel-conflict-spec.md | 167 ++++++++++++++++++ 6 files changed, 247 insertions(+) create mode 100644 docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-artifact.md create mode 100644 docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-context.md create mode 100644 docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md create mode 100644 docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-meta.json create mode 100644 docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-plan.md create mode 100644 docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-spec.md diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-artifact.md b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-artifact.md new file mode 100644 index 00000000..601fb3a3 --- /dev/null +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-artifact.md @@ -0,0 +1,13 @@ +# user-handoff-parallel-conflict — Artifact + +*최종 결과물과 학습 내용을 기록한다.* + +## 결과 + + +## Reviews +*Codex 등 리뷰 실행 시 결과(요약·발견·조치)를 날짜와 함께 남긴다. 남기지 않은 리뷰는 "안 한 것"으로 간주.* +*기계 판독용 마커를 함께 남긴다: ``* + + +## Learnings diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-context.md b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-context.md new file mode 100644 index 00000000..c2662f96 --- /dev/null +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-context.md @@ -0,0 +1,24 @@ +# user-handoff-parallel-conflict — Context Card + + +## Now +- Goal: 병렬 PR 사이 `docs//-handoff.md` 충돌을 구조적으로 제거(진입점 기능 유지) +- Current atomic step: 오케스트레이터 결정 대기(선택지 A~D, 권장 A / migrate 수행 vs 안내) +- Stop / human-decision condition: 결정 수령 전 구현 코드 작성 금지. push·PR 금지. + +## Constraints and settled decisions +- 코드 읽기 소비자 0 — session-context는 active.json을 직접 읽는다(확인됨) +- `handoffRelPaths`는 변경하지 않는다(전환기 churn 방지) +- gitignore 패턴 `docs/*/*-handoff.md`는 깊이 2만 매치(임시 repo로 확인) + +## JIT retrieval map +- Identifiers / symbols: renderUserHandoff, runHandoffAuto, runDone, handoffRelPaths, appendGitignore, userHandoffRel +- Narrow globs: src/commands/task.mjs, src/harness.mjs, src/task-paths.mjs, tests/user-handoff.test.mjs, tests/handoff-hook-churn.test.mjs +- Read next: spec `## 설계 / 접근` A의 세부 설계 +- Verification command: npm test && npm run docs:check + +## Failure capsules (max 3 unresolved) +- (none) + +## Resume checklist +- 결정 내용을 spec Ambiguity 게이트에 반영 → plan 3단계 체크 → 실패 테스트부터 diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md new file mode 100644 index 00000000..a4badfc4 --- /dev/null +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md @@ -0,0 +1,3 @@ +# user-handoff-parallel-conflict — Handoff + +(세션 종료 시 post-commit hook이 자동 갱신합니다) diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-meta.json b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-meta.json new file mode 100644 index 00000000..6ca5286b --- /dev/null +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-meta.json @@ -0,0 +1,11 @@ +{ + "user": "hslee", + "task": "user-handoff-parallel-conflict", + "created": "2026-09-27", + "firstActivatedAt": "2026-09-27T13:43:11.726Z", + "status": "open", + "closedAt": null, + "forcedAt": null, + "forcedIssues": null, + "reviews": [] +} diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-plan.md b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-plan.md new file mode 100644 index 00000000..3ef29b40 --- /dev/null +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-plan.md @@ -0,0 +1,29 @@ +# user-handoff-parallel-conflict — Plan + +## 목표 +병렬 PR(D5)끼리 `docs//-handoff.md` 때문에 충돌하는 일을 구조적으로 없앤다. +권장안 A(같은 경로 유지·추적 해제, 워크트리 로컬 상태로 강등) 기준 초안 — **선택지 결정 전 구현 금지**. + +## 단계 +- [x] 소비자 전수 조사·충돌 실측 확인(`3323c6f`·`fdcffbc`·`a7c8354`) → spec 기록 +- [x] 선택지 A~D 정리·권장안(A) 선정 → spec 기록 +- [ ] 오케스트레이터 결정 수령(선택지 + migrate 수행/안내) — spec Ambiguity 게이트 통과 처리 +- [ ] 실패 테스트 먼저: 같은 user 두 브랜치가 각자 커밋 후 머지해도 user handoff 충돌이 없음(e2e) · gitignore 목록에 `docs/*/*-handoff.md` 포함 · `task ` 활성화가 user handoff를 쓴다 +- [ ] `src/harness.mjs` `appendGitignore` `harnessNeeded`에 `DOCS_DIR` 기반 패턴 추가 +- [ ] `src/commands/task.mjs` task 활성화 경로에서 `renderUserHandoff` 활성 형태 1회 기록 +- [ ] `migrate`: 추적 중인 user handoff 감지 → 결정된 방식(안내 또는 `git rm --cached`)으로 처리 +- [ ] `doctor`: 추적 중인 user handoff 경고 1줄 +- [ ] 규범 문구 갱신: AGENTS.md·`templates/AGENTS.md.hbs`·`templates/docs/README.md`·`commands/harness-task.md`·`commands/harness-ship.md`·`docs/ao-worker-rules.md` §2·§7·`docs/index.html` 링크 +- [ ] golden fixture(`tests/fixtures/task-paths-golden/expected.txt`) 갱신·`npm test`·`npm run docs:check` 통과 +- [ ] CHANGELOG `## [Unreleased]` 항목 추가 +- [ ] 리뷰(read-only 외부 검증) → artifact `## Reviews` 기록 +- [ ] 커밋·PR(지시 시) — 머지 후 기본 브랜치에서 이 레포 사본 추적 해제 커밋(`git rm --cached docs/chad/chad-handoff.md docs/hslee/hslee-handoff.md`)은 종결 커밋과 별도로 사람 승인 후 + +## Ontology 변경 로그 +*개념이 새로 정의되거나 의미가 바뀌면 한 줄로 기록. spec.md의 Ontology 섹션을 갱신할 트리거가 된다.* + +- 2026-09-27: user handoff = 추적되는 세션 진입점 → (A 채택 시) 워크트리 로컬 렌더링. 정본은 `.harness/active.json`. + +## 참고 +- 설계·선택지·소비자 조사: `user-handoff-parallel-conflict-spec.md` +- 다이어그램 단계 없음 — AO 워커는 사용자에게 옵트인을 물을 수 없어(ao-worker-rules §5) 추가하지 않았다. diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-spec.md b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-spec.md new file mode 100644 index 00000000..f4a6f638 --- /dev/null +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-spec.md @@ -0,0 +1,167 @@ +# user-handoff-parallel-conflict — Spec + +## 목적 / 요구사항 + +**문제(사실).** post-commit 훅(`runHandoffAuto`, `src/commands/task.mjs`)은 커밋마다 task 디렉터리 **밖**의 +`docs//-handoff.md`를 `renderUserHandoff`로 **통째로 재작성**한다(Active Task·Last Commit 줄에 커밋 +sha·메시지가 들어가 커밋마다 바이트가 바뀐다). #105 이후 이 변경은 "다음 작업 커밋에 함께 stage"된다 +(`commands/harness-task.md` post-commit handoff 절). 따라서 같은 user로 D5 병렬 PR 두 개가 열리면 두 브랜치 모두 +같은 파일의 같은 줄을 반드시 바꾸고, 먼저 머지된 쪽 뒤로 나머지는 **항상** 충돌한다. + +**실측(08-26 이후 3회, 병합 커밋 메시지로 확인).** +- `3323c6f` (08-26) `Conflicts: docs/chad/chad-handoff.md` — AO 워커 브랜치에 main 병합 +- `fdcffbc` (08-30) `Conflicts: docs/hslee/hslee-handoff.md` — 기능 브랜치에 main 병합 +- `a7c8354` (09-08) `chad-handoff.md` 충돌을 main 쪽으로 해소 — 메시지가 이미 근본 원인을 짚는다: + "이 파일은 post-commit hook이 생성하는 렌더링 … 활성 task 판정의 정본은 gitignore된 `.harness/active.json`" + +**현재 수습.** `docs/ao-worker-rules.md` §2(#112)가 "충돌하면 기본 브랜치 쪽을 취한다"로 운용만 한다. +충돌 자체는 매 병렬 PR마다 남고, AO처럼 워커가 여럿이면 선형으로 늘어난다. + +**근본 원인(추론, 아래 근거로 확정).** 이 파일의 내용은 전부 **워크트리 로컬 상태의 파생**이다. +- `Active Task` ← `.harness/active.json`(gitignore, 워크트리마다 다름) +- `Last Commit` ← 그 워크트리의 `git log -1` +- `Full Context` ← task 경로(결정론적) + +즉 "브랜치마다 달라야 정상인 값"을 "브랜치 사이에 공유되는 추적 파일"에 쓰고 있다. 머지하면 어느 쪽을 취하든 +**다른 워크트리의 사실**이 남는다 — 충돌 해소를 아무리 잘해도 커밋된 값은 머지 직후 이미 틀리다 +(현재 main의 `hslee-handoff.md`도 이 워크트리의 활성 task와 무관한 값을 가리킨다). + +**영향받는 사용자·시스템.** 같은 user로 병렬 브랜치·워크트리를 쓰는 모든 경로(AO 워커, `claude/*` 브랜치, 소비자 +프로젝트의 팀원). 소비자 프로젝트도 이 파일을 커밋하므로 플러그인 사용자 전체가 대상이다. + +**기대 결과.** 병렬 PR끼리 user handoff 때문에 충돌하는 일이 **구조적으로 0**이 된다. 세션 진입점 기능 +("지금 활성 task가 무엇이고 어디서 이어 읽는가")은 유지된다. + +**제약.** +- 런타임 의존성 0, Node ≥24(레포 규칙). 새 의존성 없음. +- 기존 소비자 프로젝트의 커밋된 파일을 **깨뜨리지 않는** 마이그레이션 경로가 있어야 한다(D8 — 전면 덮어쓰기 금지 정신). +- `handoffRelPaths` 단일 정의(훅 제외 집합 = `done` 가드 무시 집합)는 유지한다. +- 버전 범프·릴리스는 범위 밖(ao-worker-rules §5). + +### 소비자 전수 조사 (2026-09-27, 이 워크트리 `c87b50d` 기준) + +**쓰기 경로 (코드)** +| 위치 | 동작 | +|---|---| +| `src/commands/task.mjs` `runHandoffAuto` | 활성 task가 있으면 커밋마다 활성 형태로 재작성(sweep 커밋은 침묵) | +| `src/commands/task.mjs` `runDone` | 종결 시 1회 종결 형태로 재작성 | +| `src/commands/task.mjs` `renderUserHandoff` | 유일 렌더러(두 형태) | +| `src/task-paths.mjs` `userHandoffRel`·`userHandoffPath` | 경로 정의 | +| `src/commands/task.mjs` `handoffRelPaths` | 훅의 "handoff만 바꾼 커밋" 판정 + `done` 가드 무시 집합 | + +**읽기 경로 (코드)** — **없다.** `userHandoffPath`/`userHandoffRel`을 읽는 코드는 테스트뿐이다. +- `session-context`(SessionStart task-gate, Claude·Codex 공통 — D9)는 이 파일이 아니라 `.harness/active.json` + (`readActive`)과 TCC를 직접 읽는다. 즉 **기계 진입점은 이미 이 파일에 의존하지 않는다.** +- `summary`·`doctor`·`remote-task`·`list`는 task handoff(`-handoff.md`)나 meta만 본다. + +**읽기 경로 (규범·문서 — 사람/에이전트가 읽음)** +- `AGENTS.md`·`templates/AGENTS.md.hbs` "세션 시작 시 1. `docs//-handoff.md` 읽기", 컨텍스트 파일 표, + "commit 시: post-commit hook이 handoff 2파일 갱신 — 다음 커밋에 담는다" +- `templates/docs/README.md` 디렉터리 트리 설명 +- `commands/harness-task.md` post-commit handoff 절, `commands/harness-ship.md` §55(handoff 2파일 동봉) +- `docs/ao-worker-rules.md` §2(충돌 수습)·§7(리뷰 덱 커밋에 handoff 2파일 동봉) +- `docs/index.html` 링크(`chad/chad-handoff.md`, `hslee/hslee-handoff.md`) +- 과거 HTML 스냅샷(`docs/harness-*-0.x.html`)·`CHANGELOG.md` — 이력이므로 갱신 대상 아님 +- 외부: `~/.claude` 개인 스킬(session-handoff 등)은 이 파일을 읽지 않는다(별도 경로 `.claude/handoffs/`). + +**테스트** +- `tests/user-handoff.test.mjs`(12케이스, 렌더러·종결 형태·done 연동) +- `tests/handoff-hook-churn.test.mjs`(`handoffRelPaths`가 두 경로를 돌려준다·sweep 침묵) +- `tests/done-guard.test.mjs`(rename 제외 판정에 user handoff 경로 사용) +- `tests/fixtures/task-paths-golden/expected.txt`(`handoff updated: docs/tester/tester-handoff.md` 출력·파일 본문) + +## 설계 / 접근 + +### 선택지 + +**A. 로컬 상태로 강등 — 같은 경로를 유지하되 추적 해제(gitignore) (권장)** +훅·`done`·렌더러는 그대로 두고, `docs/*/*-handoff.md`(깊이 2 한정 — task handoff `docs/u/t/t-handoff.md`는 +매치하지 않는다)를 하네스 관리 gitignore 목록(`appendGitignore`의 `harnessNeeded`)에 추가하고, 이미 추적 중인 +사본은 1회 `git rm --cached`로 인덱스에서 뺀다. 파일은 정본(`active.json`)과 같은 수명·범위(워크트리 로컬)를 갖게 된다. +- 호환성: 경로·형식 불변 → "세션 시작 시 이 파일을 읽어라" 규범이 그대로 동작. 달라지는 것은 "커밋에 담는다"뿐. +- 소비자 영향: 코드 읽기 소비자 0. 새 워크트리·새 clone에선 첫 커밋 전까지 파일이 없다 — 대신 `task ` + 활성화 시점에 활성 형태를 1회 쓰면 공백이 사라진다(오늘도 새 워크트리의 커밋된 사본은 **다른 브랜치의 값**이라 틀리다 — + 없는 편이 틀린 것보다 낫다). +- 잃는 것: 다른 머신·팀원이 git으로 보던 "내 마지막 종결 task" — 이는 생성물 `-task.md`(summary)와 + task meta가 이미 정본으로 담는다. +- 마이그레이션: ① gitignore 줄 추가(init·migrate가 이미 쓰는 경로) ② 추적 해제 1회 — 이 레포는 기본 브랜치에서 + 커밋 1개, 소비자는 `migrate`가 안내(또는 수행 — 결정 필요, 아래 open). 전환기 1회 부작용: 아직 파일을 수정한 + 구 브랜치를 머지하면 modify/delete 충돌 1회, 새 main을 pull한 워크트리에선 로컬 파일이 한 번 지워졌다가 다음 커밋에 재생성. +- 테스트 부담: 작음 — gitignore 목록 테스트 1, `task` 활성화 시 기록 1, golden fixture 갱신, 기존 12케이스는 불변. + +**B. 기본 브랜치 전용 생성물 — summary처럼 기본 브랜치에서만 갱신** +훅이 기본 브랜치가 아니면 user handoff를 쓰지 않는다(`default-branch-primitive` 재사용). 추적은 유지. +- 호환성: 파일은 계속 커밋된다. 하지만 기능 브랜치에서 작업 중인 세션의 진입점이 **main의 낡은 값**을 가리킨다 — + 진입점의 존재 이유(현재 활성 task)가 병렬 작업 중에 정확히 깨진다. +- 기본 브랜치 판정이 모호한 환경(origin 없음·detached·AO `ao/*/root`)에서 판정 불가 처리 규칙이 추가로 필요. +- 병렬 세션이 기본 브랜치에서 동시에 커밋하면 여전히 충돌(드묾). +- 테스트 부담: 중간(브랜치 판정 분기·판정 불가 경로). +- 기각 근거: 충돌은 줄이지만 **내용이 틀린 상태**를 공식화한다. + +**C. task 디렉터리로 분할 — user 레벨 파일 폐지, 포인터를 task별 파일로** +`docs///`에 이미 `-handoff.md`가 있으므로 user 레벨 포인터를 없애고 "활성 task의 handoff를 읽어라"로 +규범을 바꾼다. 병렬 PR은 서로 다른 task 디렉터리를 쓰므로 충돌이 없다. +- 호환성: 규범 변경(AGENTS.md·템플릿·README), 파일 삭제 → 기존 소비자 저장소의 커밋된 사본이 고아가 된다. +- "지금 활성 task가 무엇인가"는 active.json·session-context로만 알 수 있다 — 사실상 D와 같아지고, 분할 자체는 + 얻는 것이 없다(task handoff는 이미 분할되어 있다). +- 테스트 부담: 큼(user-handoff 12케이스 재작성·golden). + +**D. 파생 뷰로 대체 — 파일 폐지, `harness-team session-context`(또는 `handoff --show`)가 진입점** +기계 진입점은 이미 session-context다(Claude·Codex SessionStart). 파일을 없애고 규범 1단계를 "session-context 출력(자동 주입)"으로 바꾼다. +- 가장 깔끔한 개념(정본 1개, 파생물 0개). 그러나 파일을 직접 열어 보는 사람·Cursor(훅 없음, 규범 의존) 경로가 끊기고, + AGENTS.md 코어 규범·템플릿·README·렌더러·`done`·테스트를 한꺼번에 바꾸는 큰 변경이다(5-A 게이트 해당). +- 테스트 부담: 큼. + +### 권장: A + +한 줄 근거: 충돌의 원인은 "워크트리 로컬 파생값을 추적한다"는 것 하나이고, A는 **그것만** 고친다 — 경로·형식·렌더러· +읽는 규범이 모두 그대로라 변경 면적이 가장 작고, 정본(`active.json`)과 수명이 같아져 머지 후 값이 틀리는 문제도 함께 사라진다. +D는 개념적으로 더 깨끗하지만 규범·Cursor 경로를 깨므로, A 적용 후 필요가 드러나면 후속으로 검토한다. + +### A의 세부 설계 (결정 후 구현) +1. `appendGitignore`의 `harnessNeeded`에 `docs/*/*-handoff.md` 추가. `DOCS_DIR` 상수에서 조립한다(하드코딩 복제 금지). +2. `task ` 활성화(생성·재활성) 시 활성 형태를 1회 쓴다(`commitMsg` 빈 값 허용 — 렌더러 기존 인자). 새 워크트리 공백 해소. +3. 추적 해제: 이 레포는 기본 브랜치에서 `git rm --cached docs/chad/chad-handoff.md docs/hslee/hslee-handoff.md` 커밋 1개. + 소비자는 `migrate`가 추적 중인 user handoff를 감지해 처리(open 참고). +4. `doctor`: 추적 중인 user handoff가 남아 있으면 경고 1줄(전환 누락 탐지). +5. 규범 문구: AGENTS.md(·템플릿) "commit 시 handoff 2파일" → task handoff 1파일 + user handoff는 로컬, harness-task.md + post-commit 절, harness-ship.md·ao-worker-rules §2·§7의 "2파일" 문구, templates/docs/README.md 주석, docs/index.html 링크 제거. +6. `handoffRelPaths`는 **변경하지 않는다** — 무시 목록에 ignored 경로가 있어도 무해하고, 전환기(아직 추적 중인 저장소)에서 + churn 방지가 계속 필요하다. + +## Ontology +*이 task가 다루는 핵심 개념의 정의. "X가 정확히 뭔가?"에 답한다.* + +- **user handoff**: `docs//-handoff.md`. 활성 task 포인터·마지막 커밋·task handoff 경로를 담은 **렌더링**. + 정본이 아니다 — 정본은 `.harness/active.json`(활성), task meta(종결), task handoff(커밋 이력). +- **task handoff**: `docs///-handoff.md`. task별 커밋 로그(append). task 디렉터리에 격리되어 병렬 충돌 없음. +- **워크트리 로컬 상태**: 워크트리·clone마다 달라야 정상인 값. gitignore 대상(`active.json`, `config.json`, 그리고 A 채택 시 user handoff). +- **생성물(기본 브랜치 전용)**: 공유되지만 기본 브랜치에서만 갱신하는 추적 파일(`task_summary.md`, `-task.md`). 선택지 B가 user handoff를 여기 넣는 안. +- **게이트 근거**: 문제·원인·소비자·선택지가 코드·커밋으로 확정됐고, 남은 모호성은 선택지 결정 1건과 migrate 수행 방식 1건뿐이다. + +## Ambiguity 자가진단 +*각 항목이 명확하면 체크. 3개 이상 미체크면 구현 진입 금지 — 인터뷰/브레인스토밍으로 복귀해 +모호성을 제거한다. 게이트를 통과하면 그 근거를 위 Ontology 섹션에 한 줄로 남긴다.* + +- [x] **Goal 명확도** (40%) — 병렬 PR 사이 user handoff 충돌을 구조적으로 0으로, 진입점 기능은 유지. +- [ ] **Constraint 명확도** (30%) — 선택지(A/B/C/D)와 소비자 마이그레이션 방식이 사용자 결정 대기. +- [x] **Success 기준** (30%) — 같은 user의 두 브랜치가 각자 커밋 후 머지해도 user handoff 충돌이 없다(e2e 재현 테스트), `npm test`·`docs:check` 통과. +- [x] **Context 명확도** (brownfield 한정) — 쓰기 5곳·읽기 코드 0곳·규범 문서·테스트 4파일 식별(위 전수 조사). +- [ ] **Ambiguity ≤ 0.2** — Constraint 미결로 가중합 0.7. **결정 전 구현 진입 금지.** + + +## Done evidence + + +## 참고 +- 정본: `commands/harness-task.md` post-commit handoff 절 · `src/commands/task.mjs` `renderUserHandoff`·`runHandoffAuto`·`runDone`·`handoffRelPaths` · `src/harness.mjs` `appendGitignore` · `AGENTS.md` D4·D5 · `docs/decisions.md` D5·D8 +- 충돌 실측: `3323c6f`·`fdcffbc`·`a7c8354` (병합 메시지의 `Conflicts:`) +- 선행 task: `docs/chad/done-user-handoff-freeze`(종결 형태 도입), `docs/chad/handoff-hook-churn`(sweep 침묵), `docs/hslee/handoff-sweep-fold`(#105 다음 커밋 동봉), `docs/chad/d5-parallel-pr-scope` +- (open) 소비자 저장소의 추적 해제를 `migrate`가 **직접 수행**(`git rm --cached`, 인덱스 변경)할지 **안내만** 할지. 권장: 안내 + doctor 경고(인덱스 변경은 사용자 커밋 경계와 겹친다). +- (open) 선택지 결정 — A 권장. From 97fd1858d789ad34bd4267d8fac9d7e07d6b60ff Mon Sep 17 00:00:00 2001 From: hslee Date: Sun, 27 Sep 2026 23:35:30 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix(handoff):=20user=20handoff=EB=A5=BC=20?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=ED=8A=B8=EB=A6=AC=20=EB=A1=9C=EC=BB=AC=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=E2=80=94=20=EB=B3=91=EB=A0=AC=20?= =?UTF-8?q?PR=20=EC=B6=A9=EB=8F=8C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs//-handoff.md 는 gitignore된 active.json과 그 워크트리의 git log -1 을 렌더링한 값인데 추적 파일이라, 같은 user의 병렬 PR이 머지마다 이 파일에서 충돌했다 (3323c6f·fdcffbc·a7c8354). 추적을 멈춘다. - 하네스 관리 .gitignore에 docs/*/*-handoff.md (깊이 2 — task handoff는 계속 추적) - task 활성화가 user handoff를 곧바로 써 새 워크트리 진입점 공백 해소 - doctor: 추적 중·무시 줄 누락 경고 / migrate: 무시 줄 보장 + git rm --cached 안내만 - "handoff 2파일" 규범을 task handoff 1파일로 정정, 이 레포 사본 추적 해제 - codex 리뷰 P2 2건 조치 Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + AGENTS.md | 5 +- CHANGELOG.md | 8 + commands/harness-ship.md | 2 +- commands/harness-task.md | 11 +- docs/ao-worker-rules.md | 8 +- docs/chad/chad-handoff.md | 11 - docs/harness-overview.html | 5 + docs/hslee/hslee-handoff.md | 11 - ...user-handoff-parallel-conflict-artifact.md | 34 +++ .../user-handoff-parallel-conflict-context.md | 5 +- .../user-handoff-parallel-conflict-handoff.md | 9 + .../user-handoff-parallel-conflict-meta.json | 12 +- .../user-handoff-parallel-conflict-plan.md | 24 +- .../user-handoff-parallel-conflict-spec.md | 12 +- docs/index.html | 2 - src/commands/doctor.mjs | 23 +- src/commands/migrate.mjs | 22 +- src/commands/task.mjs | 44 +++- src/harness.mjs | 11 +- src/task-paths.mjs | 7 + templates/AGENTS.md.hbs | 5 +- templates/docs/README.md | 2 +- tests/gitignore-entries.test.mjs | 2 +- tests/summary.test.mjs | 7 +- tests/user-handoff-untracked.test.mjs | 209 ++++++++++++++++++ 26 files changed, 420 insertions(+), 72 deletions(-) delete mode 100644 docs/chad/chad-handoff.md delete mode 100644 docs/hslee/hslee-handoff.md create mode 100644 tests/user-handoff-untracked.test.mjs diff --git a/.gitignore b/.gitignore index d63aefa5..32c01563 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ node_modules/ .claude/handoffs/ .superpowers/ tests/fixtures/**/actual/ +docs/*/*-handoff.md diff --git a/AGENTS.md b/AGENTS.md index d2c858f3..5c9f7e00 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -127,7 +127,8 @@ SSOT는 여전히 네 파일이며, TCC는 거기서 파생된 현재 working se - **경계 계약**: spec의 `## Boundary contracts` JSON 선언이 있으면 plan checkbox 완료 직전에 `harness-team boundary check`가 생산자·소비자 JSON Schema의 필수 필드와 기본 type을 대조한다. 선언이 없으면 `boundary: not-configured`으로 통과한다. -- **commit 시**: post-commit hook이 handoff 2파일 갱신 — 다음 커밋에 담는다(예외: harness-task) +- **commit 시**: post-commit hook이 task handoff를 갱신 — 다음 커밋에 담는다(예외: harness-task). + `-handoff.md`도 함께 갱신되지만 워크트리 로컬(gitignore)이라 커밋하지 않는다 - **PR/MR 직전(ship)**: spec·plan·artifact를 최종 갱신하고 준비 완료를 보고한다 — 다이어그램 갱신은 옵트인이며, 절차·산출물 계약은 ship 명령 문서가 정본이다(PR/MR 생성은 별도 지시). - **완료**: plan 전체 완료 감지 또는 사용자 신호 → AskUserQuestion → `harness-team done` @@ -158,7 +159,7 @@ SSOT는 여전히 네 파일이며, TCC는 거기서 파생된 현재 working se | `CLAUDE.md` | `@AGENTS.md` import + Claude 전용 (워크플로우·서브에이전트·advisor) | | `docs///` | task별 작업 문서 4종 (spec·plan·handoff·artifact) | | `.harness/active.json` | 현재 활성 task 포인터 | -| `docs//-handoff.md` | 세션 시작 진입점 | +| `docs//-handoff.md` | 세션 시작 진입점 (워크트리 로컬·gitignore — task 활성화·커밋 때 갱신) | | `docs/decisions.md` | 팀 결정 로그 — 역할·프로토콜 규범의 근거·이력 정본 | | `.claude/rules/*.md` | 영역별 코딩 규칙 | diff --git a/CHANGELOG.md b/CHANGELOG.md index 67cd3215..ce15a097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,14 @@ modified: 2026-09-13 ## [Unreleased] ### Fixed +- **같은 user의 병렬 PR이 머지마다 `docs//-handoff.md`에서 충돌하던 결함** (task `user-handoff-parallel-conflict`). + 이 파일의 내용은 전부 gitignore된 `.harness/active.json`과 그 워크트리의 `git log -1`에서 나오는 워크트리 로컬 렌더링인데 + 추적 파일이라, post-commit 훅이 커밋마다 재작성한 값이 브랜치끼리 반드시 부딪혔다(실측 3회). 이제 추적하지 않는다. + - 하네스 관리 `.gitignore`에 `docs/*/*-handoff.md`(깊이 2만 — task handoff는 계속 추적) 추가. 소비자 도달은 `init`·`migrate`. + - `task ` 활성화(생성·재활성)가 user handoff를 곧바로 쓴다 — 새 워크트리·clone에서 첫 커밋 전 진입점 공백 해소. + - 아직 추적 중인 저장소: `doctor`가 경고하고 `migrate`는 `.gitignore` 줄을 보장한 뒤 `git rm --cached` 명령을 **안내만** 한다(인덱스 무변경). + - "handoff 2파일을 다음 커밋에 담는다" 규범을 task handoff 1파일로 정정(AGENTS.md 관리 절은 `init --yes`로 도달). + - 전환기 1회: 아직 이 파일을 수정한 구 브랜치를 머지하면 modify/delete 충돌이 한 번 난다 — 삭제 쪽을 취한다. - **`init --stack X`로 강제한 스택이 저장되지 않아 `doctor`가 `stack` 관리 절을 stale로 잘못 경고하던 결함.** `doctor`의 관리 절 stale 검사·`migrate`의 관리 절 백업 diff·플래그 없는 `init`이 모두 감지 스택으로 다시 렌더해, `doctor`의 처방(`harness-team init`)을 따르면 강제한 스택이 감지 스택으로 조용히 되돌아갔다. diff --git a/commands/harness-ship.md b/commands/harness-ship.md index 024769aa..9029eba7 100644 --- a/commands/harness-ship.md +++ b/commands/harness-ship.md @@ -52,7 +52,7 @@ Raw slash-command 인수: 5. **artifact 갱신** — `-artifact.md`에 결과·검증 증거(실제 테스트 출력)·남은 리스크· 후속 작업을 기록한다. 리뷰를 돌렸다면 `## Reviews`에 날짜와 함께 남긴다 — 기록 없는 리뷰는 - 안 한 것이다. 이 문서 갱신을 커밋할 때 트리에 남은 handoff 2파일 변경도 **같은 커밋에** 담는다 — + 안 한 것이다. 이 문서 갱신을 커밋할 때 트리에 남은 task handoff 변경도 **같은 커밋에** 담는다 — handoff만의 커밋을 따로 만들지 않는다(`commands/harness-task.md` post-commit handoff 절). 6. **다이어그램 (옵트인)** — `--no-diagram`이 없으면 `AskUserQuestion`으로 **한 번만** 묻는다: diff --git a/commands/harness-task.md b/commands/harness-task.md index 547eb762..f21c70ad 100644 --- a/commands/harness-task.md +++ b/commands/harness-task.md @@ -139,14 +139,21 @@ created: docs/// ## post-commit handoff — sweep 커밋에서는 침묵한다 `handoff`(post-commit 훅)는 커밋 **뒤에** `-handoff.md`(항목 append)와 `-handoff.md`(재작성)를 -쓴다. 그래서 커밋 직후 트리는 dirty다. +쓴다. 커밋 대상은 **task handoff 하나**다 — `-handoff.md`는 gitignore된 워크트리 로컬 파일이다 +(내용이 전부 `.harness/active.json`과 그 워크트리의 `git log -1`에서 나온다. 추적하던 시절엔 같은 user의 병렬 PR이 +머지마다 이 파일에서 충돌했다). 그래도 커밋 직후 트리는 task handoff 때문에 dirty다. **그 변경만 담는 커밋을 따로 만들지 않는다.** 다음 작업 커밋에 함께 stage한다(`git add` 대상에 -두 handoff 파일을 포함). 커밋마다 sweep을 붙이면 이력의 약 4분의 1이 "handoff 반영" 커밋이 된다 +task handoff를 포함). 커밋마다 sweep을 붙이면 이력의 약 4분의 1이 "handoff 반영" 커밋이 된다 (2026-09-20~25 실측: 98커밋 중 23개). dirty handoff는 `done` 가드가 무시하므로 남겨 둬도 종결을 막지 않는다. **예외** — 푸시·브랜치 전환·워크트리 정리 직전에 남은 변경이 handoff뿐이면 그때 단독 sweep 커밋 하나를 만든다. 보통 PR/MR당 한 번이지만 상한은 아니다 — 푸시 뒤 리뷰 대응 커밋이 생기면 다음 푸시 직전에 다시 한 번 쓴다. +`-handoff.md`는 `task ` 활성화 때도 한 번 쓴다 — 추적하지 않으므로 새 워크트리·clone에는 없고, +첫 커밋까지 세션 진입점이 비지 않게 하기 위해서다. 아직 이 파일을 **추적 중인** 저장소(gitignore 줄 도입 전)는 +무시 줄만으로는 충돌이 남는다 — `doctor`가 경고하고 `migrate`가 `.gitignore` 줄을 보장한 뒤 `git rm --cached` 명령을 +**안내만** 한다(인덱스는 건드리지 않는다). 기본 브랜치에서 한 번 실행해 커밋한다. + 단독 sweep 커밋이 **핸드오프 파일만** 바꿨다면 훅은 아무것도 쓰지 않는다 — 기록할 작업이 없고, 쓰면 churn이 자기 자신을 먹여 트리가 깨끗해지는 지점이 사라지기 때문이다(0.38.1). 판정은 보수적이다: diff --git a/docs/ao-worker-rules.md b/docs/ao-worker-rules.md index b1ec8607..fbd9fa1e 100644 --- a/docs/ao-worker-rules.md +++ b/docs/ao-worker-rules.md @@ -24,8 +24,8 @@ gh api "repos/bd-makers/team-harness/check-runs//annotations" --jq '.[] | "\ `docs/task_summary.md` 와 `docs//-task.md` 는 **생성물**이고 기본 브랜치에서 `harness-team summary --write` 로만 갱신된다 — PR 브랜치에서 절대 건드리지 않는다. AO 는 워커마다 별도 워크트리를 띄우므로, PR 브랜치에서 수정하면 **여러 워커가 같은 줄을 동시에 고쳐 충돌한다.** task SSOT 4파일(`spec`·`plan`·`handoff`·`artifact`)은 task 디렉터리에 격리되어 있으니 자유롭게 수정해도 된다. -단, `docs//-handoff.md` 는 task 디렉터리 밖에 있고 post-commit 훅이 커밋마다 통째로 다시 쓴다 — 병렬 PR 끼리 반드시 -충돌한다. 충돌하면 손으로 병합하지 말고 기본 브랜치 쪽을 취한다(다음 커밋에서 훅이 다시 쓴다). +`docs//-handoff.md` 는 워크트리 로컬 파일이라 **추적하지 않는다**(gitignore) — 훅이 다시 써도 커밋·충돌 대상이 아니다. +`git add -f` 로 억지로 담지 마라. ## 3. 설치 단계는 없다 @@ -71,8 +71,8 @@ task 다이어그램은 **옵트인 단계**이지 상시 의무가 아니다. PR 단위 슬라이드 덱이며, 오케스트레이터가 지정한 상시 단계다. - **호출은 PR 이 생긴 뒤, 번호를 명시해서:** `/mr-change-diagram `. 스킬은 번호가 없으면 멈추고 되묻는다 — 현재 브랜치로 추측하게 두지 않는다. - **저장 경로: `docs/diagrams/pr/pr-<번호>-<슬러그>.html`.** 스킬은 폴더를 스스로 정하지 않으므로, 경로를 주지 않으면 되묻고 멈춘다. -- **이 산출물에 한해 커밋이 선승인되어 있다.** opt-in 을 다시 묻지 말고 덱 HTML 과, 트리에 남아 있는 handoff 2파일 - (`-handoff.md`·`-handoff.md`) 변경만 함께 스테이징한다 — 그 밖의 변경은 담지 않는다(`commands/harness-task.md` +- **이 산출물에 한해 커밋이 선승인되어 있다.** opt-in 을 다시 묻지 말고 덱 HTML 과, 트리에 남아 있는 task handoff + (`-handoff.md`) 변경만 함께 스테이징한다 — 그 밖의 변경은 담지 않는다(`commands/harness-task.md` post-commit handoff 절). PR 소스 브랜치에 푸시한다(`docs(diagram): PR <번호> 변경 슬라이드 추가`). `--amend`·`--force` 는 쓰지 않는다. - **건너뛰어도 되는 경우**: 문서·설정만 바뀌어 제품 동작 변화가 없는 PR. 근거를 한 줄 남긴다. - **스킬이 없는 머신**이면 실패로 처리하지 말고 건너뛰되, 보고에 `리뷰 덱 미실행(스킬 없음)` 이라고 남긴다. diff --git a/docs/chad/chad-handoff.md b/docs/chad/chad-handoff.md deleted file mode 100644 index 98c2aaf3..00000000 --- a/docs/chad/chad-handoff.md +++ /dev/null @@ -1,11 +0,0 @@ -# Session Handoff - -## Active Task -없음 — `.harness/active.json` 은 `null` 이다. -새 작업은 `harness-team task ` 으로 시작한다. - -## Last Completed Task (2026-09-24) -`task-area-flag` — done - -## Full Context -→ docs/chad/task-area-flag/task-area-flag-handoff.md diff --git a/docs/harness-overview.html b/docs/harness-overview.html index 89b2be13..75dc5315 100644 --- a/docs/harness-overview.html +++ b/docs/harness-overview.html @@ -2878,6 +2878,11 @@

병합 마커 포맷

Test 자동 회귀 검증 + + tests/user-handoff-untracked.test.mjs + Test + 자동 회귀 검증 + tests/user-handoff.test.mjs Test diff --git a/docs/hslee/hslee-handoff.md b/docs/hslee/hslee-handoff.md deleted file mode 100644 index f0f393f0..00000000 --- a/docs/hslee/hslee-handoff.md +++ /dev/null @@ -1,11 +0,0 @@ -# Session Handoff - -## Active Task -없음 — `.harness/active.json` 은 `null` 이다. -새 작업은 `harness-team task ` 으로 시작한다. - -## Last Completed Task (2026-09-27) -`init-stack-stale-false-positive` — done - -## Full Context -→ docs/hslee/init-stack-stale-false-positive/init-stack-stale-false-positive-handoff.md diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-artifact.md b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-artifact.md index 601fb3a3..0eb6b6b0 100644 --- a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-artifact.md +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-artifact.md @@ -3,11 +3,45 @@ *최종 결과물과 학습 내용을 기록한다.* ## 결과 +**2026-09-27** — 선택지 A 구현(사용자 결정: A · 소비자 migrate는 안내만). +- `src/task-paths.mjs` `USER_HANDOFF_IGNORE` = `docs/*/*-handoff.md` → `src/harness.mjs` `appendGitignore`(export) 관리 목록에 추가. +- `src/commands/task.mjs`: `writeActivationHandoff`(생성·재활성 시 활성 형태 1회), `trackedUserHandoffs`(`git ls-files :(glob)…`). +- `src/commands/doctor.mjs` `checkTrackedUserHandoffs`(warn-only), `src/commands/migrate.mjs` `migrateUserHandoffUntrack`(gitignore 보장 + `git rm --cached` 안내, 인덱스 무변경). +- 이 레포 사본 `docs/chad/chad-handoff.md`·`docs/hslee/hslee-handoff.md` 추적 해제 + `.gitignore` 줄. +- 규범: AGENTS.md·템플릿·harness-task.md·harness-ship.md·ao-worker-rules §2·§7·templates/docs/README.md·docs/index.html. +- 검증: `tests/user-handoff-untracked.test.mjs` 7케이스(무시 줄을 빼면 4개 실패 — 병렬 머지 충돌 재현 확인), + `npm test` 1058 tests / 0 fail, `npm run docs:check` 최신. 기존 `tests/summary.test.mjs` 병렬 원장 테스트가 + 활성화 기록 때문에 실패 → fixture에 설치본과 같은 무시 줄 추가(전환 전 저장소는 종전과 같은 충돌 프로파일). +- 남은 리스크: 전환기 구 브랜치 머지 시 modify/delete 충돌 1회. 새 main을 pull한 워크트리에서 로컬 user handoff가 + 한 번 지워졌다가 다음 활성화·커밋에 재생성. ## Reviews *Codex 등 리뷰 실행 시 결과(요약·발견·조치)를 날짜와 함께 남긴다. 남기지 않은 리뷰는 "안 한 것"으로 간주.* *기계 판독용 마커를 함께 남긴다: ``* +### 2026-09-27T14:33:44.091Z — codex (harness-team review) + +- engine: codex · scope: worktree · tip: fd37fa34b6ee510a077be456e2b3a297d02c1531 · exit 0 · 1288 B + +```text +전하, **P2 두 건이 있어 수정 후 진행을 권합니다.** 작업 트리와 staged 삭제를 모두 확인했으며 파일은 변경하지 않았습니다. + +- **P2 — [src/commands/migrate.mjs:1087](/Users/hsonpro/.ao/data/worktrees/harness-aijient-team-plugin/harness-aijient-team-plugin-6/src/commands/migrate.mjs:1087):** 추적 중인 handoff가 없으면 `.gitignore` 갱신도 건너뛰므로, 기존 설치에서 이후 생성한 user handoff가 다시 커밋될 수 있습니다. `doctor`도 이 경우 경고하지 않습니다. +- **P2 — [src/task-paths.mjs:28](/Users/hsonpro/.ao/data/worktrees/harness-aijient-team-plugin/harness-aijient-team-plugin-6/src/task-paths.mjs:28), [src/commands/task.mjs:712](/Users/hsonpro/.ao/data/worktrees/harness-aijient-team-plugin/harness-aijient-team-plugin-6/src/commands/task.mjs:712):** glob은 깊이 2만 제한하고 파일명이 실제 `-handoff.md`인지 확인하지 않습니다. 같은 위치의 다른 `*-handoff.md`도 무시되며 `git rm --cached` 안내 대상에 들어갑니다. + +**판정:** should-fix. 활성화 시점 기록과 task handoff를 제외하는 깊이 제한은 확인했습니다. `git diff --check`는 통과했으며, 읽기 전용 리뷰라 테스트는 실행하지 않았습니다. +``` + + + +**조치 (작성 세션 판별):** +- P2-1 **수용** — 추적 해제만 되고 무시 줄이 없으면 다음 `git add -A`가 재추적한다(재현 테스트로 확인). `migrate`가 추적 + 사본 유무와 무관하게 `.gitignore` 줄을 보장(`appendGitignore`가 변경 여부 반환), `doctor`가 `userHandoffIgnored`(probe + 경로 `git check-ignore`)로 무시 줄 누락도 경고. +- P2-2 **부분 수용** — gitignore 문법은 "파일명 = 디렉터리명-handoff.md"를 표현할 수 없어 같은 깊이의 다른 `*-handoff.md` + 신규 파일도 무시되는 것은 설계 한계로 남기고 `USER_HANDOFF_IGNORE` 주석에 명시(이미 추적 중인 파일은 영향 없음). + 추적 해제 **안내** 대상은 `/-handoff.md`만 남기도록 `trackedUserHandoffs`에 필터 추가. +- 회귀 테스트 2개 추가(9케이스), `npm test` 1060 tests / 0 fail, `docs:check` 최신. ## Learnings diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-context.md b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-context.md index c2662f96..04139a93 100644 --- a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-context.md +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-context.md @@ -3,10 +3,11 @@ ## Now - Goal: 병렬 PR 사이 `docs//-handoff.md` 충돌을 구조적으로 제거(진입점 기능 유지) -- Current atomic step: 오케스트레이터 결정 대기(선택지 A~D, 권장 A / migrate 수행 vs 안내) -- Stop / human-decision condition: 결정 수령 전 구현 코드 작성 금지. push·PR 금지. +- Current atomic step: 구현·테스트 완료 → codex 리뷰 기록 → 커밋·push·PR·CI +- Stop / human-decision condition: 머지 금지(사람 지시로만). 0.44.3이 main에 먼저 들어가면 갱신(CHANGELOG 양쪽 유지). ## Constraints and settled decisions +- 결정(09-27): 선택지 A, 소비자 migrate는 안내만(인덱스 무변경) - 코드 읽기 소비자 0 — session-context는 active.json을 직접 읽는다(확인됨) - `handoffRelPaths`는 변경하지 않는다(전환기 churn 방지) - gitignore 패턴 `docs/*/*-handoff.md`는 깊이 2만 매치(임시 repo로 확인) diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md index a4badfc4..b9607e9f 100644 --- a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md @@ -1,3 +1,12 @@ # user-handoff-parallel-conflict — Handoff (세션 종료 시 post-commit hook이 자동 갱신합니다) + +## 2026-09-27T13:46:14.031Z — fd37fa3 docs(task): user-handoff-parallel-conflict spec·plan — 병렬 PR의 user handoff 충돌 선택지와 권장안 +.../user-handoff-parallel-conflict-artifact.md | 13 ++ + .../user-handoff-parallel-conflict-context.md | 24 +++ + .../user-handoff-parallel-conflict-handoff.md | 3 + + .../user-handoff-parallel-conflict-meta.json | 11 ++ + .../user-handoff-parallel-conflict-plan.md | 29 ++++ + .../user-handoff-parallel-conflict-spec.md | 167 +++++++++++++++++++++ + 6 files changed, 247 insertions(+) diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-meta.json b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-meta.json index 6ca5286b..2b9e9314 100644 --- a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-meta.json +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-meta.json @@ -7,5 +7,15 @@ "closedAt": null, "forcedAt": null, "forcedIssues": null, - "reviews": [] + "reviews": [ + { + "kind": "codex", + "engine": "codex", + "scope": "worktree", + "tip": "fd37fa34b6ee510a077be456e2b3a297d02c1531", + "at": "2026-09-27T14:33:44.091Z", + "exitCode": 0, + "outputBytes": 1288 + } + ] } diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-plan.md b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-plan.md index 3ef29b40..cd6cd35b 100644 --- a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-plan.md +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-plan.md @@ -2,22 +2,22 @@ ## 목표 병렬 PR(D5)끼리 `docs//-handoff.md` 때문에 충돌하는 일을 구조적으로 없앤다. -권장안 A(같은 경로 유지·추적 해제, 워크트리 로컬 상태로 강등) 기준 초안 — **선택지 결정 전 구현 금지**. +선택지 A(같은 경로 유지·추적 해제, 워크트리 로컬 상태로 강등) — 2026-09-27 사용자 결정. ## 단계 - [x] 소비자 전수 조사·충돌 실측 확인(`3323c6f`·`fdcffbc`·`a7c8354`) → spec 기록 - [x] 선택지 A~D 정리·권장안(A) 선정 → spec 기록 -- [ ] 오케스트레이터 결정 수령(선택지 + migrate 수행/안내) — spec Ambiguity 게이트 통과 처리 -- [ ] 실패 테스트 먼저: 같은 user 두 브랜치가 각자 커밋 후 머지해도 user handoff 충돌이 없음(e2e) · gitignore 목록에 `docs/*/*-handoff.md` 포함 · `task ` 활성화가 user handoff를 쓴다 -- [ ] `src/harness.mjs` `appendGitignore` `harnessNeeded`에 `DOCS_DIR` 기반 패턴 추가 -- [ ] `src/commands/task.mjs` task 활성화 경로에서 `renderUserHandoff` 활성 형태 1회 기록 -- [ ] `migrate`: 추적 중인 user handoff 감지 → 결정된 방식(안내 또는 `git rm --cached`)으로 처리 -- [ ] `doctor`: 추적 중인 user handoff 경고 1줄 -- [ ] 규범 문구 갱신: AGENTS.md·`templates/AGENTS.md.hbs`·`templates/docs/README.md`·`commands/harness-task.md`·`commands/harness-ship.md`·`docs/ao-worker-rules.md` §2·§7·`docs/index.html` 링크 -- [ ] golden fixture(`tests/fixtures/task-paths-golden/expected.txt`) 갱신·`npm test`·`npm run docs:check` 통과 -- [ ] CHANGELOG `## [Unreleased]` 항목 추가 -- [ ] 리뷰(read-only 외부 검증) → artifact `## Reviews` 기록 -- [ ] 커밋·PR(지시 시) — 머지 후 기본 브랜치에서 이 레포 사본 추적 해제 커밋(`git rm --cached docs/chad/chad-handoff.md docs/hslee/hslee-handoff.md`)은 종결 커밋과 별도로 사람 승인 후 +- [x] 오케스트레이터 결정 수령(A · migrate 안내만) — spec Ambiguity 게이트 통과 처리 +- [x] 실패 테스트 먼저(`tests/user-handoff-untracked.test.mjs` 7케이스 — done 가드·sweep 무시 파일 포함): 같은 user 두 브랜치가 각자 커밋 후 머지해도 user handoff 충돌이 없음(e2e) · gitignore 목록에 `docs/*/*-handoff.md` 포함 · `task ` 활성화가 user handoff를 쓴다 +- [x] `src/harness.mjs` `appendGitignore` `harnessNeeded`에 `DOCS_DIR` 기반 패턴 추가 +- [x] `src/commands/task.mjs` task 활성화 경로에서 `renderUserHandoff` 활성 형태 1회 기록 +- [x] `migrate`: 추적 중인 user handoff 감지 → `.gitignore` 줄 보장 + `git rm --cached` 안내(인덱스 무변경) +- [x] `doctor`: 추적 중인 user handoff 경고 1줄 +- [x] 규범 문구 갱신: AGENTS.md·`templates/AGENTS.md.hbs`·`templates/docs/README.md`·`commands/harness-task.md`·`commands/harness-ship.md`·`docs/ao-worker-rules.md` §2·§7·`docs/index.html` 링크 +- [x] `npm test`·`npm run docs:check` 통과(golden fixture는 변경 불필요, `tests/summary.test.mjs` fixture에 무시 줄 추가) +- [x] CHANGELOG `## [Unreleased]` 항목 추가 +- [x] 리뷰(read-only 외부 검증) → artifact `## Reviews` 기록 — codex P2 2건 조치 +- [ ] 커밋·PR — 이 레포 사본 추적 해제(`git rm --cached docs/chad/chad-handoff.md docs/hslee/hslee-handoff.md`)는 결정 [1]대로 이 PR에 포함 ## Ontology 변경 로그 *개념이 새로 정의되거나 의미가 바뀌면 한 줄로 기록. spec.md의 Ontology 섹션을 갱신할 트리거가 된다.* diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-spec.md b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-spec.md index f4a6f638..33d5267f 100644 --- a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-spec.md +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-spec.md @@ -122,7 +122,7 @@ D는 개념적으로 더 깨끗하지만 규범·Cursor 경로를 깨므로, A 1. `appendGitignore`의 `harnessNeeded`에 `docs/*/*-handoff.md` 추가. `DOCS_DIR` 상수에서 조립한다(하드코딩 복제 금지). 2. `task ` 활성화(생성·재활성) 시 활성 형태를 1회 쓴다(`commitMsg` 빈 값 허용 — 렌더러 기존 인자). 새 워크트리 공백 해소. 3. 추적 해제: 이 레포는 기본 브랜치에서 `git rm --cached docs/chad/chad-handoff.md docs/hslee/hslee-handoff.md` 커밋 1개. - 소비자는 `migrate`가 추적 중인 user handoff를 감지해 처리(open 참고). + 소비자는 `migrate`가 gitignore 줄을 보장하고(파일 편집만), 추적 중인 user handoff가 있으면 `git rm --cached` 명령을 **안내만** 한다. 4. `doctor`: 추적 중인 user handoff가 남아 있으면 경고 1줄(전환 누락 탐지). 5. 규범 문구: AGENTS.md(·템플릿) "commit 시 handoff 2파일" → task handoff 1파일 + user handoff는 로컬, harness-task.md post-commit 절, harness-ship.md·ao-worker-rules §2·§7의 "2파일" 문구, templates/docs/README.md 주석, docs/index.html 링크 제거. @@ -137,17 +137,17 @@ D는 개념적으로 더 깨끗하지만 규범·Cursor 경로를 깨므로, A - **task handoff**: `docs///-handoff.md`. task별 커밋 로그(append). task 디렉터리에 격리되어 병렬 충돌 없음. - **워크트리 로컬 상태**: 워크트리·clone마다 달라야 정상인 값. gitignore 대상(`active.json`, `config.json`, 그리고 A 채택 시 user handoff). - **생성물(기본 브랜치 전용)**: 공유되지만 기본 브랜치에서만 갱신하는 추적 파일(`task_summary.md`, `-task.md`). 선택지 B가 user handoff를 여기 넣는 안. -- **게이트 근거**: 문제·원인·소비자·선택지가 코드·커밋으로 확정됐고, 남은 모호성은 선택지 결정 1건과 migrate 수행 방식 1건뿐이다. +- **게이트 근거**: 문제·원인·소비자·선택지가 코드·커밋으로 확정됐고, 남은 두 결정(선택지 A, migrate 안내만)을 2026-09-27 사용자가 내렸다. ## Ambiguity 자가진단 *각 항목이 명확하면 체크. 3개 이상 미체크면 구현 진입 금지 — 인터뷰/브레인스토밍으로 복귀해 모호성을 제거한다. 게이트를 통과하면 그 근거를 위 Ontology 섹션에 한 줄로 남긴다.* - [x] **Goal 명확도** (40%) — 병렬 PR 사이 user handoff 충돌을 구조적으로 0으로, 진입점 기능은 유지. -- [ ] **Constraint 명확도** (30%) — 선택지(A/B/C/D)와 소비자 마이그레이션 방식이 사용자 결정 대기. +- [x] **Constraint 명확도** (30%) — 2026-09-27 사용자 결정: 선택지 A, 소비자 저장소는 migrate가 인덱스를 건드리지 않고 안내 + doctor 경고. - [x] **Success 기준** (30%) — 같은 user의 두 브랜치가 각자 커밋 후 머지해도 user handoff 충돌이 없다(e2e 재현 테스트), `npm test`·`docs:check` 통과. - [x] **Context 명확도** (brownfield 한정) — 쓰기 5곳·읽기 코드 0곳·규범 문서·테스트 4파일 식별(위 전수 조사). -- [ ] **Ambiguity ≤ 0.2** — Constraint 미결로 가중합 0.7. **결정 전 구현 진입 금지.** +- [x] **Ambiguity ≤ 0.2** — 가중합 1.0. 게이트 통과(2026-09-27). diff --git a/templates/docs/README.md b/templates/docs/README.md index 56d55009..439a7c99 100644 --- a/templates/docs/README.md +++ b/templates/docs/README.md @@ -17,7 +17,7 @@ task별 디렉토리 하나에 SSOT 4개 파일과 비-SSOT Context Card 1개가 docs/ ├── task_summary.md # 전체 task 요약 표 — 생성물 (harness-team summary) └── / # git config user.name 또는 $USER - ├── -handoff.md # 세션 시작 진입점 (현재 active task) + ├── -handoff.md # 세션 시작 진입점 (현재 active task) — 워크트리 로컬, gitignore ├── -task.md # 이 사용자의 task 인덱스 — 생성물 (harness-team summary) └── / # task 디렉토리 ├── -spec.md # 요구사항 / 설계 (사람이 먼저 작성) diff --git a/tests/gitignore-entries.test.mjs b/tests/gitignore-entries.test.mjs index 6cc977c1..e05f24a7 100644 --- a/tests/gitignore-entries.test.mjs +++ b/tests/gitignore-entries.test.mjs @@ -17,7 +17,7 @@ test('.gitignore: .harness/를 통째로 무시하지 않고 개인 상태 파 try { await copyStaticAssets({ root: ROOT, targetDir: dir, flags: {}, stackId: 'node', addAiGitignore: true }); const lines = (await readFile(join(dir, '.gitignore'), 'utf8')).split('\n').map(l => l.trim()); - for (const wanted of ['.claude/settings.local.json', '.harness/active.json', '.harness/config.json', '.harness/observability/']) { + for (const wanted of ['.claude/settings.local.json', '.harness/active.json', '.harness/config.json', '.harness/observability/', 'docs/*/*-handoff.md']) { assert.ok(lines.includes(wanted), `${wanted} 는 무시돼야 한다`); } assert.ok(!lines.includes('.harness/'), '.harness/ 전체를 무시하면 backup.json을 커밋할 수 없다'); diff --git a/tests/summary.test.mjs b/tests/summary.test.mjs index 60fa7c24..db98e382 100644 --- a/tests/summary.test.mjs +++ b/tests/summary.test.mjs @@ -6,6 +6,7 @@ import { tmpdir } from 'node:os'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { runTask, runDone } from '../src/commands/task.mjs'; +import { USER_HANDOFF_IGNORE } from '../src/task-paths.mjs'; import { collectTasks, renderTaskSummary, renderUserIndex, runSummary, readTaskMeta, defaultBranchCandidates, readLedger, taskMetaTemplate, writeTaskMeta as writeTaskMetaForTest, @@ -21,9 +22,9 @@ async function initRepo(dir) { await git(dir, 'init', '-q', '-b', 'main'); await git(dir, 'config', 'user.email', 'test@example.com'); await git(dir, 'config', 'user.name', 'test'); - // Real installs gitignore .harness/; without it the active-task pointer would be - // committed and would itself collide across branches. - await writeFile(join(dir, '.gitignore'), '.harness/\n'); + // Real installs gitignore .harness/ and the worktree-local user handoff; without them the + // active-task pointer (and its rendering) would be committed and collide across branches. + await writeFile(join(dir, '.gitignore'), `.harness/\n${USER_HANDOFF_IGNORE}\n`); await writeFile(join(dir, 'README.md'), '# seed\n'); await git(dir, 'add', '-A'); await git(dir, 'commit', '-qm', 'seed'); diff --git a/tests/user-handoff-untracked.test.mjs b/tests/user-handoff-untracked.test.mjs new file mode 100644 index 00000000..eebbcc05 --- /dev/null +++ b/tests/user-handoff-untracked.test.mjs @@ -0,0 +1,209 @@ +// `docs//-handoff.md` 는 **워크트리 로컬 렌더링**이다 — 내용이 전부 gitignore 된 `.harness/active.json` +// 과 그 워크트리의 `git log -1` 에서 나온다. 추적하던 시절에는 같은 user 의 병렬 PR 이 머지될 때마다 이 파일에서 +// 반드시 충돌했다(3323c6f·fdcffbc·a7c8354). 여기서는 추적 해제 이후의 계약을 고정한다: +// - init 이 쓰는 .gitignore 가 깊이 2 의 user handoff 만 무시하고 task handoff 는 추적한다 +// - 병렬 브랜치가 각자 커밋·sweep 한 뒤 머지해도 충돌하지 않는다 +// - sweep(#105)·done 가드(#109)가 무시 파일을 "훅 출력" 으로 올바로 다룬다 +// - task 활성화가 user handoff 를 곧바로 써서 새 워크트리에 공백이 없다 +// - 아직 추적 중인 저장소는 doctor 가 경고하고 migrate 는 인덱스를 건드리지 않고 안내만 한다 +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { + runTask, runHandoffAuto, runDone, taskArtifactTemplate, trackedUserHandoffs, +} from '../src/commands/task.mjs'; +import { USER_HANDOFF_IGNORE } from '../src/task-paths.mjs'; +import { appendGitignore } from '../src/harness.mjs'; +import { checkTrackedUserHandoffs } from '../src/commands/doctor.mjs'; +import { migrateUserHandoffUntrack } from '../src/commands/migrate.mjs'; + +const pexec = promisify(execFile); +const git = (dir, ...args) => pexec('git', ['-C', dir, ...args]); +const USER = 'tester'; +const noRemote = async () => null; + +function quiet() { + const logs = []; + const orig = console.log; + console.log = (...a) => logs.push(a.join(' ')); + return { logs, restore: () => { console.log = orig; } }; +} + +async function makeRepo() { + const dir = await mkdtemp(join(tmpdir(), 'harness-uh-')); + await git(dir, 'init', '-q', '-b', 'main'); + await git(dir, 'config', 'user.email', 't@e.com'); + await git(dir, 'config', 'user.name', 't'); + await mkdir(join(dir, '.harness'), { recursive: true }); + await appendGitignore(dir); + await writeFile(join(dir, 'src.txt'), 'v1\n'); + await git(dir, 'add', '-A'); + await git(dir, 'commit', '-qm', 'base'); + return dir; +} + +async function startTask(dir, name) { + const q = quiet(); + try { await runTask({ targetDir: dir, flags: { member: USER }, taskArgs: [name] }, { doneOnMain: noRemote }); } + finally { q.restore(); } +} + +// 실제 작업 커밋 → post-commit 훅 → 다음 커밋에 남은 변경을 담는 흐름(#105)을 그대로 밟는다. +async function commitWithHook(dir, msg) { + await git(dir, 'add', '-A'); + await git(dir, 'commit', '-qm', msg); + await runHandoffAuto({ targetDir: dir }); +} + +const porcelain = async (dir) => (await git(dir, 'status', '--porcelain')).stdout.trim(); + +test('.gitignore: user handoff(깊이 2)는 무시하고 task handoff 는 추적한다', async () => { + const dir = await makeRepo(); + try { + const lines = (await readFile(join(dir, '.gitignore'), 'utf8')).split('\n'); + assert.ok(lines.includes(USER_HANDOFF_IGNORE), `${USER_HANDOFF_IGNORE} 줄이 있어야 한다`); + const ignored = (p) => git(dir, 'check-ignore', '-q', '--no-index', p).then(() => true, () => false); + assert.equal(await ignored(`docs/${USER}/${USER}-handoff.md`), true); + assert.equal(await ignored(`docs/${USER}/demo/demo-handoff.md`), false, 'task handoff 는 SSOT — 추적한다'); + } finally { await rm(dir, { recursive: true, force: true }); } +}); + +test('task 생성·재활성화가 곧바로 user handoff 를 쓴다 — 새 워크트리 공백 없음', async () => { + const dir = await makeRepo(); + const userHandoff = join(dir, 'docs', USER, `${USER}-handoff.md`); + try { + await startTask(dir, 'alpha'); + assert.match(await readFile(userHandoff, 'utf8'), /## Active Task\nalpha\n/, '생성 직후 활성 형태'); + await startTask(dir, 'beta'); + await startTask(dir, 'alpha'); + const after = await readFile(userHandoff, 'utf8'); + assert.match(after, /## Active Task\nalpha\n/, '재활성화도 포인터를 옮긴다'); + assert.match(after, /→ docs\/tester\/alpha\/alpha-handoff\.md/); + } finally { await rm(dir, { recursive: true, force: true }); } +}); + +test('같은 user 의 병렬 브랜치가 각자 커밋·sweep 후 머지해도 충돌하지 않는다', async () => { + const dir = await makeRepo(); + try { + await git(dir, 'switch', '-qc', 'feat-a'); + await startTask(dir, 'alpha'); + await writeFile(join(dir, 'a.txt'), 'a\n'); + await commitWithHook(dir, 'feat: a'); + await commitWithHook(dir, 'chore(handoff): sweep'); + + await git(dir, 'switch', '-q', 'main'); + await git(dir, 'switch', '-qc', 'feat-b'); + await startTask(dir, 'beta'); + await writeFile(join(dir, 'b.txt'), 'b\n'); + await commitWithHook(dir, 'feat: b'); + await commitWithHook(dir, 'chore(handoff): sweep'); + + await git(dir, 'switch', '-q', 'main'); + await git(dir, 'merge', '-q', '--no-ff', '-m', 'merge a', 'feat-a'); + await git(dir, 'merge', '-q', '--no-ff', '-m', 'merge b', 'feat-b'); // 충돌하면 throw + const { stdout } = await git(dir, 'log', '--all', '--name-only', '--format='); + assert.ok(!stdout.split('\n').includes(`docs/${USER}/${USER}-handoff.md`), 'user handoff 는 어느 커밋에도 없다'); + } finally { await rm(dir, { recursive: true, force: true }); } +}); + +test('sweep(#105): 작업 커밋 뒤 남는 변경은 task handoff 하나이고, 그것만 담은 커밋에서 훅은 침묵한다', async () => { + const dir = await makeRepo(); + try { + await startTask(dir, 'alpha'); + await commitWithHook(dir, 'feat: work'); + assert.equal(await porcelain(dir), 'M docs/tester/alpha/alpha-handoff.md', '무시 파일은 dirty 로 보이지 않는다'); + await commitWithHook(dir, 'chore(handoff): sweep'); + assert.equal(await porcelain(dir), '', 'sweep 뒤 트리가 깨끗하다 — 루프가 끝난다'); + } finally { await rm(dir, { recursive: true, force: true }); } +}); + +test('done 가드(#109): 무시된 user handoff 와 dirty task handoff 만 남으면 종결을 막지 않는다', async () => { + const dir = await makeRepo(); + try { + await startTask(dir, 'alpha'); + const taskDir = join(dir, 'docs', USER, 'alpha'); + await writeFile(join(taskDir, 'alpha-plan.md'), '# alpha — Plan\n\n## 단계\n- [x] done\n'); + await writeFile(join(taskDir, 'alpha-artifact.md'), taskArtifactTemplate('alpha') + '\n- 실제 결과\n'); + await writeFile(join(taskDir, 'alpha-spec.md'), '# alpha — Spec\n\n## Done evidence\n```json\n{ "version": 1, "tests": "skip" }\n```\n'); + await commitWithHook(dir, 'feat: work'); + const q = quiet(); + try { await runDone({ targetDir: dir, flags: {} }); } finally { q.restore(); } + assert.ok(q.logs.some(l => l.startsWith('done:')), q.logs.join('\n')); + assert.match(await readFile(join(dir, 'docs', USER, `${USER}-handoff.md`), 'utf8'), /Last Completed Task/, '종결 형태는 로컬에 쓴다'); + } finally { process.exitCode = undefined; await rm(dir, { recursive: true, force: true }); } +}); + +test('전환 전 저장소: 추적 중인 user handoff 를 doctor 가 경고하고 migrate 는 인덱스를 건드리지 않고 안내한다', async () => { + const dir = await mkdtemp(join(tmpdir(), 'harness-uh-legacy-')); + try { + await git(dir, 'init', '-q', '-b', 'main'); + await git(dir, 'config', 'user.email', 't@e.com'); + await git(dir, 'config', 'user.name', 't'); + await mkdir(join(dir, 'docs', USER, 'alpha'), { recursive: true }); + await writeFile(join(dir, 'docs', USER, `${USER}-handoff.md`), '# Session Handoff\n'); + await writeFile(join(dir, 'docs', USER, 'alpha', 'alpha-handoff.md'), '# alpha — Handoff\n'); + await git(dir, 'add', '-A'); + await git(dir, 'commit', '-qm', 'legacy'); + + assert.deepEqual(await trackedUserHandoffs(dir), [`docs/${USER}/${USER}-handoff.md`], 'task handoff 는 대상이 아니다'); + assert.match(await checkTrackedUserHandoffs(dir), /git rm --cached docs\/tester\/tester-handoff\.md/); + + const indexBefore = (await git(dir, 'ls-files', '-s')).stdout; + const q = quiet(); + let changed; + try { changed = await migrateUserHandoffUntrack({ targetDir: dir }); } finally { q.restore(); } + assert.equal(changed, true); + assert.equal((await git(dir, 'ls-files', '-s')).stdout, indexBefore, '인덱스 무변경'); + assert.ok(q.logs.some(l => l.includes('git rm --cached docs/tester/tester-handoff.md')), q.logs.join('\n')); + assert.ok((await readFile(join(dir, '.gitignore'), 'utf8')).split('\n').includes(USER_HANDOFF_IGNORE)); + + await git(dir, 'rm', '-q', '--cached', `docs/${USER}/${USER}-handoff.md`); + await git(dir, 'commit', '-qm', 'untrack'); + assert.deepEqual(await trackedUserHandoffs(dir), []); + assert.equal(await checkTrackedUserHandoffs(dir), null, '전환 뒤에는 경고하지 않는다'); + const q2 = quiet(); + try { assert.equal(await migrateUserHandoffUntrack({ targetDir: dir }), false, '재실행은 no-op'); } finally { q2.restore(); } + } finally { await rm(dir, { recursive: true, force: true }); } +}); + +test('추적 해제만 되고 무시 줄이 없는 저장소: doctor 가 경고하고 migrate 가 줄을 넣는다 (codex P2)', async () => { + const dir = await mkdtemp(join(tmpdir(), 'harness-uh-noignore-')); + try { + await git(dir, 'init', '-q', '-b', 'main'); + assert.deepEqual(await trackedUserHandoffs(dir), []); + assert.match(await checkTrackedUserHandoffs(dir), /\.gitignore 에 `docs\/\*\/\*-handoff\.md` 줄이 없어/); + const q = quiet(); + let changed; + try { changed = await migrateUserHandoffUntrack({ targetDir: dir }); } finally { q.restore(); } + assert.equal(changed, true, '추적 사본이 없어도 무시 줄은 보장한다'); + assert.equal(await checkTrackedUserHandoffs(dir), null); + } finally { await rm(dir, { recursive: true, force: true }); } +}); + +test('같은 깊이의 다른 *-handoff.md 는 추적 해제 안내 대상이 아니다 (codex P2)', async () => { + const dir = await mkdtemp(join(tmpdir(), 'harness-uh-other-')); + try { + await git(dir, 'init', '-q', '-b', 'main'); + await git(dir, 'config', 'user.email', 't@e.com'); + await git(dir, 'config', 'user.name', 't'); + await mkdir(join(dir, 'docs', 'diagrams'), { recursive: true }); + await mkdir(join(dir, 'docs', USER), { recursive: true }); + await writeFile(join(dir, 'docs', 'diagrams', 'release-handoff.md'), 'x\n'); + await writeFile(join(dir, 'docs', USER, `${USER}-handoff.md`), 'x\n'); + await git(dir, 'add', '-A'); + await git(dir, 'commit', '-qm', 'seed'); + assert.deepEqual(await trackedUserHandoffs(dir), [`docs/${USER}/${USER}-handoff.md`]); + } finally { await rm(dir, { recursive: true, force: true }); } +}); + +test('git 저장소가 아니면 추적 판정은 빈 목록 — doctor 는 조용하다', async () => { + const dir = await mkdtemp(join(tmpdir(), 'harness-uh-nogit-')); + try { + assert.deepEqual(await trackedUserHandoffs(dir), []); + assert.equal(await checkTrackedUserHandoffs(dir), null); + } finally { await rm(dir, { recursive: true, force: true }); } +}); From f90245b7a0ef780b1253a100497dcc7fd21746a0 Mon Sep 17 00:00:00 2001 From: hslee Date: Sun, 27 Sep 2026 23:35:42 +0900 Subject: [PATCH 3/6] chore(handoff): user-handoff-parallel-conflict handoff sweep Co-Authored-By: Claude Opus 5.5 --- .../user-handoff-parallel-conflict-handoff.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md index b9607e9f..981d24a8 100644 --- a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md @@ -10,3 +10,32 @@ .../user-handoff-parallel-conflict-plan.md | 29 ++++ .../user-handoff-parallel-conflict-spec.md | 167 +++++++++++++++++++++ 6 files changed, 247 insertions(+) + +## 2026-09-27T14:35:30.400Z — 97fd185 fix(handoff): user handoff를 워크트리 로컬 파일로 — 병렬 PR 충돌 제거 +.gitignore | 1 + + AGENTS.md | 5 +- + CHANGELOG.md | 8 + + commands/harness-ship.md | 2 +- + commands/harness-task.md | 11 +- + docs/ao-worker-rules.md | 8 +- + docs/chad/chad-handoff.md | 11 -- + docs/harness-overview.html | 5 + + docs/hslee/hslee-handoff.md | 11 -- + .../user-handoff-parallel-conflict-artifact.md | 34 ++++ + .../user-handoff-parallel-conflict-context.md | 5 +- + .../user-handoff-parallel-conflict-handoff.md | 9 + + .../user-handoff-parallel-conflict-meta.json | 12 +- + .../user-handoff-parallel-conflict-plan.md | 24 +-- + .../user-handoff-parallel-conflict-spec.md | 12 +- + docs/index.html | 2 - + src/commands/doctor.mjs | 23 ++- + src/commands/migrate.mjs | 22 ++- + src/commands/task.mjs | 44 ++++- + src/harness.mjs | 11 +- + src/task-paths.mjs | 7 + + templates/AGENTS.md.hbs | 5 +- + templates/docs/README.md | 2 +- + tests/gitignore-entries.test.mjs | 2 +- + tests/summary.test.mjs | 7 +- + tests/user-handoff-untracked.test.mjs | 209 +++++++++++++++++++++ + 26 files changed, 420 insertions(+), 72 deletions(-) From 395648e857cbfd1540f52dedfbb73e37947c2c02 Mon Sep 17 00:00:00 2001 From: hslee Date: Sun, 27 Sep 2026 23:36:29 +0900 Subject: [PATCH 4/6] chore(handoff): user-handoff-parallel-conflict handoff sweep Co-Authored-By: Claude Opus 5.5 --- .../user-handoff-parallel-conflict-handoff.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md index 981d24a8..7a6c684a 100644 --- a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md @@ -39,3 +39,39 @@ tests/summary.test.mjs | 7 +- tests/user-handoff-untracked.test.mjs | 209 +++++++++++++++++++++ 26 files changed, 420 insertions(+), 72 deletions(-) + +## 2026-09-27T14:36:23.763Z — 593d31b Merge refs/remotes/origin/main (0.44.3) into user-handoff-parallel-conflict +.claude-plugin/marketplace.json | 2 +- + .claude-plugin/plugin.json | 2 +- + .codex-plugin/plugin.json | 2 +- + CHANGELOG.md | 16 + + MAINTAINING.md | 13 +- + commands/harness-init.md | 1 + + docs/ao-worker-rules.md | 2 +- + docs/followups.md | 13 +- + docs/harness-overview.html | 18 +- + docs/harness-overview.template.html | 8 +- + .../docs-version-drift-check-artifact.md | 33 +++ + .../docs-version-drift-check-context.md | 27 ++ + .../docs-version-drift-check-handoff.md | 32 ++ + .../docs-version-drift-check-meta.json | 21 ++ + .../docs-version-drift-check-plan.md | 20 ++ + .../docs-version-drift-check-spec.md | 42 +++ + docs/hslee/hslee-task.md | 2 + + .../stack-pin-display-artifact.md | 30 ++ + .../stack-pin-display/stack-pin-display-context.md | 27 ++ + .../stack-pin-display/stack-pin-display-handoff.md | 30 ++ + .../stack-pin-display/stack-pin-display-meta.json | 21 ++ + .../stack-pin-display/stack-pin-display-plan.md | 21 ++ + .../stack-pin-display/stack-pin-display-spec.md | 52 ++++ + docs/index.html | 1 + + docs/task_summary.md | 2 + + docs/what-changes-0.44.3.html | 322 +++++++++++++++++++++ + docs/what-changes-latest-version.html | 76 +++-- + package.json | 2 +- + scripts/docs-version-drift.mjs | 102 +++++++ + scripts/generate-harness-overview.mjs | 10 + + src/commands/stack.mjs | 22 +- + tests/detect-testing.test.mjs | 23 ++ + tests/docs-version-drift.test.mjs | 93 ++++++ + 33 files changed, 1040 insertions(+), 48 deletions(-) From e0f0810a74bb985d12208a862f1166dc1dde81ad Mon Sep 17 00:00:00 2001 From: hslee Date: Sun, 27 Sep 2026 23:43:09 +0900 Subject: [PATCH 5/6] =?UTF-8?q?docs(readme):=20init=20--stack=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20=EC=97=AC=EB=B6=80=EB=A5=BC=20harness-team=20stack?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=99=95=EC=9D=B8=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 012e5efb..2b5d91bd 100644 --- a/README.md +++ b/README.md @@ -300,6 +300,7 @@ harness-team --help 스택 옵션: `react-native` | `react` | `next` | `node` | `python` | `go` | `generic` — 목록 밖의 값은 exit 2로 거부합니다. 감지와 다른 스택은 `.harness/render-state.json`에 고정되어 이후 플래그 없는 `init`·`doctor`도 그 스택을 쓰며, 감지된 id를 다시 주면 고정이 풀립니다. +고정 여부는 `harness-team stack`으로 확인합니다 — text는 `pin:` 줄, `--json`은 `stackPin`(고정 없으면 `null`) 필드에 감지·고정·유효 스택과 해제 명령이 나옵니다. React Native/Expo 전용 rules 4종은 유효 stack이 RN 계열일 때만 설치됩니다. ### `/harness-sync` — 내부 정합성 동기화 From 2f3177895a7b6f4c08d3721dadfe33da126a6524 Mon Sep 17 00:00:00 2001 From: hslee Date: Sun, 27 Sep 2026 23:43:14 +0900 Subject: [PATCH 6/6] chore(handoff): user-handoff-parallel-conflict handoff sweep Co-Authored-By: Claude Opus 5.5 --- .../user-handoff-parallel-conflict-handoff.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md index 7a6c684a..061faa44 100644 --- a/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md +++ b/docs/hslee/user-handoff-parallel-conflict/user-handoff-parallel-conflict-handoff.md @@ -75,3 +75,7 @@ tests/detect-testing.test.mjs | 23 ++ tests/docs-version-drift.test.mjs | 93 ++++++ 33 files changed, 1040 insertions(+), 48 deletions(-) + +## 2026-09-27T14:43:10.068Z — e0f0810 docs(readme): init --stack 고정 여부를 harness-team stack으로 확인한다 +README.md | 1 + + 1 file changed, 1 insertion(+)