diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index cea7058..348fee3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,7 +7,7 @@ "name": "haku", "source": "./", "description": "Git-issue driven planning, orchestrator/worker development and verified merge-to-close for Claude Code (GitHub first)", - "version": "0.16.31", + "version": "0.16.32", "category": "workflow" } ] diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a2b7bf0..ee1d462 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "haku", - "version": "0.16.31", + "version": "0.16.32", "description": "HAKU - Harness Assistant Keeping Users in the loop. Git-issue driven story planning, orchestrator/worker development and verified merge-to-close for Claude Code (GitHub first, GitLab next).", "author": { "name": "dongqdev" }, "license": "MIT", diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 1b3d04f..22fc275 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,35 +1,34 @@ -# 인수인계 (2026-11-01) +# 인수인계 (2026-11-02) -다른 에이전트가 바로 이어받을 수 있도록 현재 상태를 적어 둡니다. 이어받은 뒤 이 파일은 지워도 됩니다. +다른 세션이 이 파일만 보고 이어받을 수 있게 적었습니다. 이어받은 뒤 이 파일은 지워도 됩니다(지우는 것이 기본). -## 프로젝트 원칙 -- haku는 최신 트렌드 기반의 **가벼운 하네스**입니다. 핵심 목표는 **에이전트가 쉬지 않는 것**(감시 체계나 루프로 작업을 계속 이어감)입니다. -- 사용자 지시와 프로그램 방향에 항상 의문을 가지세요. 최신 트렌드에 맞지 않거나 더 좋은 길이 있으면 먼저 제안합니다. -- 답변은 한글, 사용자 결정·승인은 선택지로 받습니다. +## 원칙 +- haku는 최신 트렌드 기반의 **가벼운 하네스**, 핵심 목표는 **에이전트가 쉬지 않는 것**입니다. +- 사용자 지시와 방향에 항상 의문을 갖고, 더 나은 길이 있으면 먼저 제안합니다. +- 답변은 한글, 사용자 결정·승인은 선택지로 받습니다. 병합은 사용자 승인 후에만 합니다. +- 이슈 하나당 작은 PR 하나. 공개 파일이 바뀌면 `node scripts/bump-version.js origin/main`(커밋 후 실행)으로 patch +1. +- 검증: `node --test tests/*.test.js`, `node scripts/check-eol.js --worktree`. -## 작업 규칙 -- 이슈 하나당 작은 PR 하나. 버전은 `node scripts/bump-version.js origin/main`으로 올립니다(공개 파일이 바뀐 PR마다 patch +1). -- 병합 전: Claude 코드리뷰(`/code-review`), CI(3개 OS·version·paths) 통과, 사용자 승인 후 squash 병합. 본인 PR이라 GitHub 승인은 불가합니다. -- 전체 테스트: `node --test tests/*.test.js`, 줄바꿈: `node scripts/check-eol.js --worktree`. +## 현재 상태 +- main은 0.16.31. PR #134(브랜치 `claude/project-thread-mawt75`, 0.16.32)가 열려 있고 CI는 통과했으며 사용자 승인 후 squash 병합 대기입니다. + - 내용: `hooks/worker-stop.js`의 `claimBlock`에서 `session_id` 폴백 제거(같은 세션의 다른 워커가 검사 없이 통과하던 문제), 테스트 추가, 이전 HANDOFF 정리. +- 열린 이슈 없음. 실세션 배너/ask 훅 검증은 사용자가 보류했습니다. -## 이번 세션 결과 -| 이슈 | 상태 | PR | -|---|---|---| -| #117 워커 워크트리 의존성 준비 | 병합 (0.16.26, `linkFromMain`) | #128 | -| #91 retro-done --skip 문구 | 병합 (0.16.27) | #129 | -| #113 deferred 재개 제안 | 닫음(범위가 커서 가벼운 하네스 원칙에 안 맞음, 필요하면 status에 deferred 건수 표시 정도로 재제안) | - | -| #111 reconcile --apply --only/--except | PR 열림, 병합 대기 (0.16.28) | 이 PR | +## 결정 대기 (사용자 선택 필요) +"쉬지 않는 에이전트" 갭을 어떻게 메울지: +1. **문서+종료조건만 (권장)**: `skills/build/SKILL.md` 종료 조건을 `/goal` 형식(측정 가능한 종료 상태, 확인 방법, 턴 상한 문구)으로 쓰고, `docs/resume.md`를 공식 비교표로 갱신. 새 코드 없음. +2. 위 + 진전 없는 반복 감지 Stop 훅(같은 상태 반복 시 멈춤). +3. 자체 자동 재시작 루프 구현(가장 무겁고 공식 기능과 겹침). +4. 보류. +사용자가 아직 고르지 않았습니다. 답이 없으면 먼저 사용자에게 다시 묻습니다. -## 남은 이슈 (우선순위 순) -1. **#111** 이 PR을 코드리뷰·CI 확인 후 사용자 승인 받아 병합. -2. **#119** plan 검증: Scope의 구체 파일 경로가 기본 브랜치에 있는지 경고. 힌트: `scripts/lib/publish.js`(검증 모드)와 `tests/publish-scope-warning.test.js`. 와일드카드는 건너뛰고, 확장자만 다른 파일을 제안하며, 새 파일 표시(`"new": true` 등)는 경고에서 뺍니다. plan 스킬 승인 화면에 한 줄 추가. -3. **#112** build 스킬(`skills/build/SKILL.md`)에 차단된 사이트의 사용자 제공 링크 처리 절차를 문서로 추가(코드 변경 없음, 인수 기준은 이슈 본문). -4. **#92** 미해결 표식 검사 빈틈(`scripts/lib/publish.js`, `tests/unresolved-marker.test.js`): 변형 표기, id 검사, CLI 종료 코드 시험, 우회 안내. +## 트렌드 조사 요약 (2026-11-02, 웹 조사; 일부는 2차 자료) +- 방향 적합: 얇은 하네스, 상태를 이슈/PR/라벨/워크트리에 두기(Ralph loop의 git 호스팅 변형), 서로 대화하지 않는 격리 워커, 훅 게이트는 업계 수렴 방향과 일치. agent teams는 아직 실험 단계라 도입 불필요. +- 공식 기능: `/goal`(Stop 훅 기반, 평가자는 대화 증거만 봄, 세션 재개 시 복원), `/loop`(세션 범위, 7일 만료, 재개 시 self-paced 미복원), 클라우드 scheduled tasks(구 routines: 스케줄/API/GitHub 이벤트 트리거, 최소 1시간, 이벤트마다 새 세션). +- 갭: (1) 쉬지 않기가 기본 흐름에 없고 `docs/resume.md` 수동 설정뿐, (2) build 종료 조건이 `/goal` 형식이 아님, (3) resume.md가 낡았을 수 있음, (4) 무인 실행 안전장치(턴/예산 상한, 진전 없음 감지) 부족. +- 출처: code.claude.com/docs/en/goal, /scheduled-tasks, /routines, /agent-teams, /hooks-guide; anthropic.com/engineering/effective-harnesses-for-long-running-agents; addyosmani.com/blog/long-running-agents; arxiv.org/pdf/2609.00006 -## 이 세션에서 알게 된 점 -- `linkFromMain`은 심볼릭 링크 이름을 저장소 로컬 `info/exclude`에 추가합니다(제거하지 않음). -- 지정 브랜치 `claude/project-thread-g9b6id`를 병합 후 재사용할 때는 `git checkout -B origin/main` 뒤 `git merge -s ours origin/`로 강제 푸시 없이 올렸습니다. - -## 추가 개선 후보 (사용자 확인 전 제안) -- 실제 세션에서 배너/ask 훅 동작 검증(자동 테스트는 있으나 실세션 확인 기록 없음). -- "쉬지 않는 에이전트" 목표 갭: 워커가 멈췄을 때 감시·재시작하는 루프(예: `/loop` 또는 `Stop` 훅 기반 자동 이어가기)가 문서에만 있고 기본 흐름에 묶여 있지 않음. +## 다음 순서 +1. 사용자에게 PR #134 병합 승인과 위 결정을 받는다. +2. 선택에 따라 새 이슈/PR 하나로 진행(브랜치는 병합 후 `git checkout -B origin/main`). +3. 인계 완료 후 이 파일 삭제. diff --git a/hooks/worker-stop.js b/hooks/worker-stop.js index 41a5af1..b6dfad4 100644 --- a/hooks/worker-stop.js +++ b/hooks/worker-stop.js @@ -37,7 +37,7 @@ function decide(input) { // Marker for "this agent was already sent back once". Returns true when it was created now (first block), false when it // already existed or cannot be told (then the worker passes). No key in the input means no block. function claimBlock(input, dir) { - const id = input.agent_id || input.agent_transcript_path || input.session_id; + const id = input.agent_id || input.agent_transcript_path; if (typeof id !== 'string' || !id) return false; try { const name = 'haku-worker-stop-' + crypto.createHash('sha256').update(id).digest('hex').slice(0, 32); diff --git a/tests/hook-worker-stop.test.js b/tests/hook-worker-stop.test.js index de2d6d0..bf2c449 100644 --- a/tests/hook-worker-stop.test.js +++ b/tests/hook-worker-stop.test.js @@ -43,3 +43,10 @@ test('without stop_hook_active in the input it blocks an agent once, then passes assert.strictEqual(run(one).status, 0); assert.strictEqual(run(JSON.stringify({ agent_type: 'worker', last_assistant_message: 'done.' })).status, 0); }); + +test('the session id alone is never a key: other workers of the same session are not skipped', () => { + const s = o => JSON.stringify({ session_id: 'sess', agent_type: 'worker', last_assistant_message: 'done.', ...o }); + assert.strictEqual(run(s({})).status, 0); + assert.strictEqual(run(s({ agent_id: 'w-one' })).status, 2); + assert.strictEqual(run(s({ agent_id: 'w-two' })).status, 2); +});