hypomnema 1.8.1 → 1.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +133 -49
  4. package/README.ko.md +7 -7
  5. package/README.md +7 -7
  6. package/commands/crystallize.md +45 -8
  7. package/commands/graph.md +7 -4
  8. package/commands/lint.md +8 -1
  9. package/commands/query.md +6 -4
  10. package/commands/verify.md +12 -1
  11. package/docs/ARCHITECTURE.md +20 -12
  12. package/docs/CONTRIBUTING.md +23 -7
  13. package/hooks/close-journal.mjs +128 -0
  14. package/hooks/hooks.json +1 -9
  15. package/hooks/hypo-compact-guard.mjs +126 -23
  16. package/hooks/hypo-cwd-change.mjs +20 -19
  17. package/hooks/hypo-first-prompt.mjs +31 -16
  18. package/hooks/hypo-lookup.mjs +10 -5
  19. package/hooks/hypo-session-start.mjs +199 -13
  20. package/hooks/hypo-shared.mjs +521 -116
  21. package/hooks/hypo-web-fetch-ingest.mjs +9 -13
  22. package/hooks/proposal-store.mjs +35 -1
  23. package/hooks/shared.json +9 -0
  24. package/package.json +3 -1
  25. package/scripts/doctor.mjs +75 -94
  26. package/scripts/graph.mjs +22 -2
  27. package/scripts/init.mjs +54 -106
  28. package/scripts/lib/core-hooks.mjs +48 -22
  29. package/scripts/lib/crystallize-args.mjs +38 -2
  30. package/scripts/lib/crystallize-close-apply.mjs +634 -83
  31. package/scripts/lib/git-hooks-dir.mjs +427 -52
  32. package/scripts/lib/hook-inventory.mjs +150 -0
  33. package/scripts/lib/pkg-provenance.mjs +11 -0
  34. package/scripts/lib/plugin-detect.mjs +43 -9
  35. package/scripts/lib/template-schema-version.mjs +47 -0
  36. package/scripts/lint.mjs +166 -11
  37. package/scripts/query.mjs +22 -2
  38. package/scripts/uninstall.mjs +130 -66
  39. package/scripts/upgrade.mjs +245 -147
  40. package/scripts/verify.mjs +22 -2
  41. package/templates/SCHEMA.md +23 -1
  42. package/templates/hypo-automation.md +4 -2
  43. package/templates/hypo-config.md +1 -1
  44. package/templates/hypo-guide.md +1 -1
package/README.ko.md CHANGED
@@ -76,7 +76,7 @@ hypomnema
76
76
 
77
77
  > `init`은 `~/.zshrc`(또는 `~/.bashrc`)에 `claude()` 셸 함수도 추가합니다. `# hypo-managed:shell-setup:start`와 `:end` 마커 사이에 들어가며, `cd`만 한 세션에도 프로젝트 컨텍스트가 주입되게 합니다. `--no-shell`로 건너뛰거나 `--shell-config=<path>`로 대상 파일을 바꿀 수 있습니다. `uninstall --apply`는 이 블록을 지웁니다(기본으로 `~/.zshrc`와 `~/.bashrc` 둘 다 확인하고, `--shell-config`로 지정한 파일이 있으면 그것만 봅니다). 다만 마커를 손으로 편집해서 중복되거나 순서가 뒤바뀐 상태라면, 무엇을 지워야 할지 안전하게 판단할 수 없으므로 블록을 그대로 둡니다. `--keep-shell`을 주면 이 셸 rc 정리만 건너뛰고 나머지 uninstall 은 그대로 돕니다.
78
78
 
79
- > `init`은 위키 저장소에 pre-commit 훅도 설치합니다(`<위키>/.git/hooks/pre-commit`, `# hypo-managed:pre-commit:*` 마커). `.hypoignore`에 걸리는 파일이 스테이지되면 커밋을 막습니다. 해당 파일을 unstage 하거나 `git commit --no-verify`로 우회하세요. 이 훅에는 설치된 패키지의 절대 경로가 박히므로, Hypomnema를 옮기거나 재설치했다면 `hypomnema init`을 다시 돌려 경로를 갱신해야 합니다. 그러지 않으면 위키의 모든 커밋이 실패합니다. `uninstall --apply`는 이 훅을 지웁니다. 위키 위치는 `init`과 같은 방식으로 찾고, `--hypo-dir=<path>`로 직접 지정할 수도 있습니다. 다만 마커는 있지만 그 바깥에 다른 내용이 붙은 훅처럼 더 이상 온전히 Hypomnema 소유가 아닌 훅은 손대지 않고 그대로 둡니다. `--keep-wiki-hook`을 주면 이 위키 훅 정리만 건너뛰고 나머지 uninstall 은 그대로 돕니다.
79
+ > `init`은 위키 저장소에 pre-commit 훅도 설치합니다(`<위키>/.git/hooks/pre-commit`, `# hypo-managed:pre-commit:*` 마커). `.hypoignore`에 걸리는 파일이 스테이지되면 커밋을 막습니다. 해당 파일을 unstage 하거나 `git commit --no-verify`로 우회하세요. 이 훅은 설치 경로를 박아 두지 않습니다. 커밋 시점에 `~/.claude/hypo-pkg.json`의 `pkgRoot`를 먼저 확인하고, 그것으로 안 되면 플러그인 레지스트리(`~/.claude/plugins/installed_plugins.json`의 `hypo@hypomnema` 항목과 옛 `hypomnema@hypomnema` 항목을 user scope 우선으로)를 봅니다. 후보로 인정하는 조건은 절대 경로이고, 그 디렉터리의 `package.json` 이름이 `hypomnema`이며 버전이 있고, 실제로 부를 스크립트가 거기 있는 것입니다. 이보다 느슨하면 당신이 쓰지 않은 `pkgRoot`가 위키에서 실행될 수 있습니다. 아무것도 못 찾으면 `.hypoignore` 가드를 조용히 건너뛰는 대신, 무엇을 확인했고 어떻게 고치는지 출력하며 커밋을 거부합니다. 이렇게 해석하는 값으로 단계마다 Node 기동이 한 번 더 들어가 약 80ms가 붙고, `--lint-strict` 위키는 커밋당 대략 165ms를 냅니다. 예전 릴리스가 설치한 훅은 여전히 버전 고정 형태(`init` 시점에 절대 설치 경로가 박히고, 옮기거나 재설치할 때마다 낡는 형태)일 수 있는데, `hypomnema upgrade --apply`(또는 플러그인 설치라면 `/hypo:upgrade`에서 apply 단계 확인)가 스스로 경로를 찾는 새 형태로 옮겨 줍니다. 박힌 경로가 디스크에 이미 없어도 마찬가지로 옮겨 줍니다. 본문 자체를 알아볼 수 없는 훅(손으로 고쳤거나 손상된 경우)은 다릅니다. `doctor`와 `upgrade` 모두 손대지 않고 경고만 하며, 이때 고치는 명령은 `upgrade --apply`가 아니라 `hypomnema init --force-commands`(또는 플러그인의 `/hypo:init --force-commands`)로 가드를 다시 설치하는 것입니다. 패키지가 아니라 위키 자체를 옮겼다면 여전히 `hypomnema init`을 새로 돌려야 합니다. `upgrade`는 설치 루트 해석만 고치고, `--lint-strict` 훅에 이미 박힌 `--hypo-dir`은 건드리지 않기 때문입니다. `uninstall --apply`는 이 훅을 지웁니다. 위키 위치는 `init`과 같은 방식으로 찾고, `--hypo-dir=<path>`로 직접 지정할 수도 있습니다. 다만 마커는 있지만 그 바깥에 다른 내용이 붙은 훅처럼 더 이상 온전히 Hypomnema 소유가 아닌 훅은 손대지 않고 그대로 둡니다. `--keep-wiki-hook`을 주면 이 위키 훅 정리만 건너뛰고 나머지 uninstall 은 그대로 돕니다.
80
80
 
81
81
  > 두 번째 기기에서는 그냥 `hypomnema`를 돌리지 마세요. 원격과 무관한 새 위키가 만들어집니다. 기존 위키를 클론하는 쪽을 쓰세요. `hypomnema --from-remote=<git-url>`이 위키 루트로 클론한 뒤 실제 Hypomnema 위키인지 확인하고, 새 git 히스토리를 만들지 않은 채 훅과 슬래시 커맨드만 설치합니다. 대상 디렉터리가 이미 있으면 거부합니다.
82
82
 
@@ -253,11 +253,11 @@ Hypomnema는 청크가 아니라 페이지를 지식 단위로 봅니다. 새
253
253
  |---|---|---|
254
254
  | `hypo-close-guard.mjs` | `PreToolUse` | Write/Edit/MultiEdit가 세션 마무리 쓰기로 보이는데 트랜스크립트에 사용자의 마무리 신호가 없으면, 파일이 바뀌기 전에 확인을 받음 |
255
255
  | `hypo-session-start.mjs` | `SessionStart` | `hot.md` / `session-state.md` 주입 + `git pull --ff-only` |
256
- | `hypo-first-prompt.mjs` | `UserPromptSubmit` | 마커 기반 일회성 `hot.md` 주입 (10분 TTL) |
256
+ | `hypo-first-prompt.mjs` | `UserPromptSubmit` | 마커 기반으로 첫 프롬프트에 재개 한 줄을 요구한다(10분 TTL). `hot.md`를 다시 읽지는 않는다 |
257
257
  | `hypo-lookup.mjs` | `UserPromptSubmit` | BM25 top-3 HIT 주입 / MISS면 가까운 슬러그 신호 |
258
- | `hypo-compact-guard.mjs` | `UserPromptSubmit` | 채팅에 입력된 `/compact`나 `/clear`를 감지해 컨텍스트가 지워지기 전에 session-close 체크리스트를 강제 |
259
- | `hypo-cwd-change.mjs` | `CwdChanged` | cwd에 맞는 프로젝트 `hot.md` 주입 |
260
- | `hypo-file-watch.mjs` | `FileChanged` | 위키 파일 변경 알림 (`.hypoignore` 준수. 매칭 경로는 LLM 컨텍스트로 다시 주입하지 않음) |
258
+ | `hypo-compact-guard.mjs` | `UserPromptSubmit` | 채팅에 입력된 `/compact`나 `/clear`를 감지해 마무리가 덜 됐으면 알린다. compact 자체는 막지 않는다 |
259
+ | `hypo-cwd-change.mjs` | `CwdChanged` | cwd에 맞는 프로젝트 `hot.md`로 컨텍스트를 만든다. `CwdChanged`에는 Claude Code가 모델로 전달하는 출력 필드가 없어 지금은 닿지 않는다 |
260
+ | `hypo-file-watch.mjs` | `FileChanged` | 바뀐 위키 파일로 컨텍스트를 만든다(`.hypoignore` 준수). 이 패키지는 감시 경로를 등록하지 않아 이벤트가 발생할 계기가 없고, `FileChanged` 출력은 어차피 모델에 안 닿는다 |
261
261
  | `hypo-auto-stage.mjs` | `PostToolUse(Write/Edit/MultiEdit)` | 위키 파일 자동 stage |
262
262
  | `hypo-auto-commit.mjs` | `Stop` | 자동 commit + pull + push |
263
263
  | `hypo-hot-rebuild.mjs` | `Stop` | 루트 `hot.md` 포인터 테이블 재생성 (구조와 날짜) |
@@ -269,7 +269,7 @@ Hypomnema는 청크가 아니라 페이지를 지식 단위로 봅니다. 새
269
269
 
270
270
  PostToolUse 훅 둘은 matcher 없이 등록되고 각자 tool_name으로 거릅니다.
271
271
 
272
- 모든 훅은 위키 루트를 `HYPO_DIR` 환경변수 → `hypo-config.md` 스캔 → `~/hypomnema` 기본값 순으로 찾고, `hypo-shared.mjs`(`hooks.json`의 `shared` 필드로 선언)를 공유합니다.
272
+ 모든 훅은 위키 루트를 `HYPO_DIR` 환경변수 → `hypo-config.md` 스캔 → `~/hypomnema` 기본값 순으로 찾고, `hypo-shared.mjs`(`hooks/shared.json`에 선언된 `.mjs` 파일명 배열)를 공유합니다.
273
273
 
274
274
  ### 셋업 & 유지보수
275
275
 
@@ -396,7 +396,7 @@ E. 멈춘 프로젝트 재개.
396
396
 
397
397
  `HYPO_SKIP_GATE=1`은 세션 중간에 사용자를 막을 수 있는 게이트가 존중합니다. `hypo-compact-guard`, `hypo-close-guard`, `hypo-auto-minimal-crystallize`, 그리고 `hypo-web-fetch-ingest`의 ingest 안내가 여기 해당합니다. `hypo-personal-check`(PreCompact)도 이 플래그를 읽지만, 이 훅은 이제 `/compact`를 막지 않으므로 플래그는 반복될 뻔한 미완성-마무리 `systemMessage`만 억제합니다. 기록할 필요 없는 가벼운 세션에 씁니다.
398
398
 
399
- > 모델 제공사에게 전송되는 범위: Hypomnema 훅은 위키 본문을 Claude Code의 추가 컨텍스트(`additionalContext`)에 실어 보내고, 이 내용은 프롬프트의 일부로 Claude 모델 제공사로 전송됩니다. `.hypoignore`에 등록된 경로는 모든 주입 훅(`hypo-file-watch`, `hypo-session-start`, `hypo-cwd-change`, `hypo-lookup`)과 `ingest`에서 제외되지만, 등록하지 않은 파일은 전송 대상입니다. (`hypo-auto-stage`/`hypo-auto-commit`은 git 스테이징용 훅이라 컨텍스트를 주입하지는 않지만, 스테이징 판단에도 `.hypoignore`를 참고합니다.) 비밀 정보는 위키에 두지 마시고, `HYPO_DIR` 아래에 민감한 내용을 저장하기 전에 `.hypoignore` 패턴을 먼저 점검하세요.
399
+ > 모델 제공사에게 전송되는 범위: Hypomnema 훅은 위키 본문을 Claude Code의 추가 컨텍스트(`additionalContext`)에 실어 보내고, 이 내용은 프롬프트의 일부로 Claude 모델 제공사로 전송됩니다. 이 경로가 실제로 모델까지 닿는 것은 `hypo-session-start`와 `hypo-lookup`입니다. `hypo-cwd-change`와 `hypo-file-watch`는 `CwdChanged`와 `FileChanged` 이벤트에서 동작하는데, Claude Code가 이 두 이벤트에는 모델에 닿는 출력 필드를 두지 않아서, 두 훅이 만드는 `additionalContext`는 지금은 전송 전에 버려집니다. `.hypoignore`는 출력이 모델까지 닿는지와 무관하게 이 네 훅 모두와 `ingest`에서 적용됩니다. 등록하지 않은 파일은 모델에 닿는 두 훅이 그대로 전송합니다. (`hypo-auto-stage`/`hypo-auto-commit`은 git 스테이징용 훅이라 컨텍스트를 주입하지는 않지만, 스테이징 판단에도 `.hypoignore`를 참고합니다.) 비밀 정보는 위키에 두지 마시고, `HYPO_DIR` 아래에 민감한 내용을 저장하기 전에 `.hypoignore` 패턴을 먼저 점검하세요.
400
400
 
401
401
  > git sync 범위: Hypomnema는 `~/hypomnema/` 위키 자체만 git sync합니다. 단 `init` / `upgrade`는 `~/.claude/` 안의 관리 대상 영역(Hypomnema 자체 hook `~/.claude/hooks/`, 슬래시 커맨드 `~/.claude/commands/hypo/`, `settings.json` 등록)을 설치·SHA 추적하고, extensions companion sync로 위키의 `~/hypomnema/extensions/`에 둔 `agents/`·`commands/`·`hooks/`·`skills/`도 미러링합니다(직접 `init`·`upgrade --apply`·`capture`를 돌릴 때. `--codex`면 `hooks`·`commands` 부분집합만 `~/.codex/`로). 이 관리 대상 바깥에 있는 `~/.claude/` 콘텐츠는 일부러 관리하지 않습니다. 위키를 거치지 않는 기타 agent/skill, 머신 고유 `settings.local.json` 같은 일반 Claude Code 설정의 기기 간 동기화는 [chezmoi](https://www.chezmoi.io/) 같은 별도 dotfiles 매니저를 권합니다.
402
402
 
package/README.md CHANGED
@@ -249,11 +249,11 @@ Nine commands cover the full capture → retrieval → consolidation cycle.
249
249
  |---|---|---|
250
250
  | `hypo-close-guard.mjs` | `PreToolUse` | When a Write/Edit/MultiEdit looks like a session-close write and the transcript carries no close signal from you, ask before it lands |
251
251
  | `hypo-session-start.mjs` | `SessionStart` | Inject `hot.md` / `session-state.md` + `git pull --ff-only` |
252
- | `hypo-first-prompt.mjs` | `UserPromptSubmit` | Marker-based one-shot `hot.md` injection (10-min TTL) |
252
+ | `hypo-first-prompt.mjs` | `UserPromptSubmit` | Marker-based one-shot request for a resume line on the first prompt (10-min TTL). It does not re-read `hot.md` |
253
253
  | `hypo-lookup.mjs` | `UserPromptSubmit` | BM25 top-3 HIT inject / MISS → closest-slug signal |
254
- | `hypo-compact-guard.mjs` | `UserPromptSubmit` | Detect a typed `/compact` or `/clear`, and enforce the session-close checklist before context is wiped |
255
- | `hypo-cwd-change.mjs` | `CwdChanged` | Inject the matching project's `hot.md` |
256
- | `hypo-file-watch.mjs` | `FileChanged` | Notify on wiki-file changes (honors `.hypoignore`; matched paths are never re-emitted into LLM context) |
254
+ | `hypo-compact-guard.mjs` | `UserPromptSubmit` | Detect a typed `/compact` or `/clear` and report an incomplete session close. It never blocks the compact |
255
+ | `hypo-cwd-change.mjs` | `CwdChanged` | Build context from the matching project's `hot.md`. `CwdChanged` has no output field Claude Code forwards to the model, so this does not currently reach Claude |
256
+ | `hypo-file-watch.mjs` | `FileChanged` | Build context for a changed wiki file (honors `.hypoignore`). Nothing in this package registers watch paths, so the event has no trigger here, and `FileChanged` output does not reach Claude either way |
257
257
  | `hypo-auto-stage.mjs` | `PostToolUse(Write/Edit/MultiEdit)` | Auto-stage wiki-file edits |
258
258
  | `hypo-auto-commit.mjs` | `Stop` | Auto commit + pull + push |
259
259
  | `hypo-hot-rebuild.mjs` | `Stop` | Rebuild the root `hot.md` pointer table (structure + dates) |
@@ -265,7 +265,7 @@ Nine commands cover the full capture → retrieval → consolidation cycle.
265
265
 
266
266
  Both PostToolUse hooks are registered without a matcher and filter on `tool_name` themselves.
267
267
 
268
- All hooks resolve the wiki root via `HYPO_DIR` env → `hypo-config.md` scan → `~/hypomnema` default, and share `hypo-shared.mjs` (declared via `hooks.json`'s `shared` field).
268
+ All hooks resolve the wiki root via `HYPO_DIR` env → `hypo-config.md` scan → `~/hypomnema` default, and share `hypo-shared.mjs` (declared in `hooks/shared.json`, a JSON array of `.mjs` basenames).
269
269
 
270
270
  When the Stop hook's pull hits a merge conflict it aborts the merge, skips the push, and records the failure. Your local commits are safe, but the machines stop converging until you act: `git -C <your wiki> pull --no-rebase`, fix the conflicts, commit, push. `/hypo:doctor` surfaces a stuck sync.
271
271
 
@@ -386,7 +386,7 @@ Place a `hypo-config.md` at the wiki root to make it portable across machines wi
386
386
 
387
387
  `.hypoignore` controls which paths the hooks ignore (default: `*.pdf`, `*.zip`, `*.pem`, `.env*`, `*credentials*`, `*secret*`, `*token*`, `*password*`, `*passwd*`, …). Edit it directly; there is no privacy mode flag. One file, one source of truth.
388
388
 
389
- > `init` also installs a pre-commit hook into the wiki repo at `<wiki>/.git/hooks/pre-commit`, marked `# hypo-managed:pre-commit:*`. It refuses any commit that stages a path matching `.hypoignore`; unstage the file, or override with `git commit --no-verify`. The hook embeds the absolute path of the installed package, so if you move or reinstall Hypomnema, this path goes stale and every wiki commit fails. `hypomnema upgrade --apply` (or `/hypo:upgrade`, confirming the apply step, on a plugin install) repoints it in most cases, since a plugin-channel version bump is exactly the case it is built to detect; re-run `hypomnema init` when `upgrade --apply` cannot, which happens for a manual/npm install running alongside an enabled plugin whose active root it cannot positively resolve, or when the vault itself (not just the package) has moved, since `upgrade` only ever corrects the embedded root, never the embedded `--hypo-dir`. `uninstall --apply` removes this hook, resolving the vault the same way `init` does (`--hypo-dir=<path>` to override); it only removes a hook that still carries the marker and no content outside it, so a hook you have since edited by hand is left in place. Pass `--keep-wiki-hook` to skip this wiki hook cleanup (the rest of the uninstall still runs).
389
+ > `init` also installs a pre-commit hook into the wiki repo at `<wiki>/.git/hooks/pre-commit`, marked `# hypo-managed:pre-commit:*`. It refuses any commit that stages a path matching `.hypoignore`; unstage the file, or override with `git commit --no-verify`. The hook does not bake in an install path: it resolves the active Hypomnema install itself, at commit time, from `~/.claude/hypo-pkg.json`'s `pkgRoot` and, failing that, the plugin registry (`~/.claude/plugins/installed_plugins.json`, both the `hypo@hypomnema` and the legacy `hypomnema@hypomnema` entries, user-scope first). A candidate only counts when it is an absolute path whose `package.json` names `hypomnema` and carries a version, and whose script is actually there — anything looser and a `pkgRoot` you did not write could run in your vault. When nothing resolves, it refuses the commit and prints what it checked and how to fix it, rather than silently skip the `.hypoignore` guard. Resolving this way costs a second Node start per step, about 80ms, so a `--lint-strict` vault pays roughly 165ms per commit. A hook an older release wrote may still carry the version-pinned form (an absolute install path baked in at `init` time, which goes stale on every move or reinstall); `hypomnema upgrade --apply` (or `/hypo:upgrade`, confirming the apply step, on a plugin install) migrates it onto the self-resolving form, even when the baked-in path no longer exists on disk. A hook whose body cannot be recognized at all (hand-edited, corrupted) is different: `doctor` and `upgrade` both warn about it instead of touching it, and the fix is `hypomnema init --force-commands` (or the plugin's `/hypo:init --force-commands`) to reinstall the guard, not `upgrade --apply`. Moving the vault itself, not just the package, still needs a fresh `hypomnema init`: `upgrade` only ever fixes the install-root resolution, never a `--hypo-dir` a `--lint-strict` hook already baked in. `uninstall --apply` removes this hook, resolving the vault the same way `init` does (`--hypo-dir=<path>` to override); it only removes a hook that still carries the marker and no content outside it, so a hook you have since edited by hand is left in place. Pass `--keep-wiki-hook` to skip this wiki hook cleanup (the rest of the uninstall still runs).
390
390
 
391
391
  > The credential-style patterns above are substring globs, so an ordinary page such as `pages/oauth-token-refresh.md` is matched too: it is never injected by any hook, and the wiki pre-commit hook refuses to commit it. If you write about these topics, narrow the patterns or rename the page.
392
392
 
@@ -396,7 +396,7 @@ Place a `hypo-config.md` at the wiki root to make it portable across machines wi
396
396
 
397
397
  > If a session-close write finds its target changed since this session read it, Hypomnema does not overwrite it. It parks the bytes under `<wiki>/.cache/proposals/` and tells you. Review and apply them yourself: `hypomnema proposal list`, then `hypomnema proposal apply <id>` (at a terminal) or `hypomnema proposal discard <id>`. Inside a session, where there is no terminal to type at, the same approval is collected in the conversation: `hypomnema proposal challenge` prints the diffs, you type the line it gives you, and `hypomnema proposal resolve` writes exactly what you saw. There is no auto-apply.
398
398
 
399
- > Provider transmission disclaimer: Hypomnema hooks emit wiki content into Claude Code's `additionalContext`, which is transmitted to the Claude model provider as part of the prompt. `.hypoignore` is enforced at every content-injection hook (`hypo-file-watch`, `hypo-session-start`, `hypo-cwd-change`, `hypo-lookup`) and at `ingest`, but any file _not_ matched by `.hypoignore` is fair game for transmission. (`hypo-auto-stage` and `hypo-auto-commit` are git-staging hooks, not injection points, and also honor `.hypoignore` for their staging decisions.) Keep secrets out of the wiki, and review `.hypoignore` patterns before storing anything sensitive under `HYPO_DIR`.
399
+ > Provider transmission disclaimer: Hypomnema hooks emit wiki content into Claude Code's `additionalContext`, which is transmitted to the Claude model provider as part of the prompt. That path actually reaches the model for `hypo-session-start` and `hypo-lookup`. `hypo-cwd-change` and `hypo-file-watch` fire on `CwdChanged` and `FileChanged`, two events for which Claude Code's hook output has no field that reaches the model, so the `additionalContext` those two hooks emit is currently dropped before transmission. `.hypoignore` is enforced at all four of these hooks (whether or not their output reaches the model) and at `ingest`, but any file _not_ matched by `.hypoignore` is fair game for transmission through the two hooks that do reach the model. (`hypo-auto-stage` and `hypo-auto-commit` are git-staging hooks, not injection points, and also honor `.hypoignore` for their staging decisions.) Keep secrets out of the wiki, and review `.hypoignore` patterns before storing anything sensitive under `HYPO_DIR`.
400
400
 
401
401
  > Scope of git sync: Hypomnema git-syncs only the `~/hypomnema/` wiki itself. `init` / `upgrade` do install and SHA-track a defined surface inside `~/.claude/`: Hypomnema's own hooks at `~/.claude/hooks/`, slash commands at `~/.claude/commands/hypo/`, and `settings.json` registrations. The extensions companion sync covers one more surface: whatever you put in `~/hypomnema/extensions/` (`agents/`, `commands/`, `hooks/`, `skills/`) lands in `~/.claude/`. With `--codex`, hooks and commands also go to `~/.codex/`. Anything _outside_ that defined surface in `~/.claude/` is intentionally not managed by Hypomnema. For general cross-machine sync of Claude Code config (other agents/skills not staged via the wiki, machine-specific `settings.local.json`, etc.), use a separate dotfiles manager such as [chezmoi](https://www.chezmoi.io/).
402
402
 
@@ -61,7 +61,9 @@ Field rules:
61
61
 
62
62
  Notes:
63
63
 
64
- - `sessionState` / `projectHot` / `rootHot` / `openQuestions` are **overwrite** (full-file content). `sessionLog` / `log` are **append** (entry-level idempotency — exact-entry dedup, safe to re-run).
64
+ - `sessionState` / `projectHot` / `rootHot` / `openQuestions` are **overwrite** (full-file content); `sessionLog` / `log` are **append** (entry-level idempotency, exact-entry dedup, safe to re-run).
65
+ - **Overwrite means overwrite: whatever you did not carry into the payload is gone.** A single session's close can legitimately touch only one thread of a file that tracks several (a project running two or three tracks in parallel is normal). Before composing an overwrite field, read the CURRENT on-disk file and carry forward every `##` section this session did not touch, verbatim; do not reconstruct a section from memory or drop it because this session had nothing new to add to it. This session's own edits go on top of that, not in place of it.
66
+ - Apply enforces the same rule mechanically: if an overwrite field's payload drops 2 or more of the file's existing `##` sections, apply withholds that field (parks it exactly like a base conflict, as a proposal artifact a human reviews and can force through) rather than writing it. If you are deliberately consolidating or retiring sections on purpose (the user asked for it, or the sections genuinely no longer apply), add `"restructure": true` next to that field's `"content"` to say so explicitly; it only waives the check for that one field, not the whole payload. A waiver that actually waived something, not just a `true` set on a field that never dropped anything, shows up in the result's `restructureWaivers[]` (see Step 4's close-result reporting).
65
67
  - Frontmatter `updated:` is NOT auto-fixed. If your payload's `updated:` is stale, the post-apply verification gate will fail with `stage='post-apply-verification'` and you must fix the payload and retry.
66
68
  - Write the payload to a **session-scoped** temp path, e.g. `/tmp/hypo-session-close-<session-id>.json`, using the same `<session-id>` you pass below. A date-based path (`<YYYY-MM-DD>`) collides when two sessions close on the same day: the second write silently overwrites the first, and the wrong payload gets applied. The `sessionId` field above is the second line of defense if a path is ever reused anyway.
67
69
 
@@ -85,10 +87,12 @@ node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs \
85
87
  --apply-session-close \
86
88
  --payload=/tmp/hypo-session-close-<session-id>.json \
87
89
  --session-id=<current-session-id> \
88
- --hypo-dir="<path>" \
89
90
  --json
90
91
  ```
91
92
 
93
+ Add `--hypo-dir="<path>"` only when the user specified a Hypomnema directory explicitly;
94
+ otherwise omit it and the script resolves the root itself.
95
+
92
96
  **`--session-id` is required for any close that carries a `--payload`.** It is not a
93
97
  switch that turns a check on; omitting it fails the check. Before a single byte is
94
98
  written or committed, the apply resolves that session's transcript and looks for
@@ -99,7 +103,7 @@ and nothing on disk, in three cases:
99
103
  |---|---|
100
104
  | `session-id-required` | No `--session-id` was passed. |
101
105
  | `transcript-unresolved` | The id resolves to no transcript under `~/.claude/projects/`. |
102
- | `no-user-close-signal` | The transcript exists, and the user never asked to close. |
106
+ | `no-user-close-signal` | The transcript exists, but no close authority is in force right now. This one string collapses three different gate outcomes, and only one of them means the user never asked. Read `gateReason` (below) before deciding what to do about it. |
103
107
 
104
108
  A refusal is not a failure to route around. It means the close should not happen: ask
105
109
  the user, and re-run only after they say so.
@@ -129,6 +133,15 @@ no session id.
129
133
  | `--apply-session-close --payload=<path> --session-id=<id>` | The only apply path. Verifies close authority against that session's transcript **first**; on a refusal nothing is written. On success: per-field idempotent writes (no-op when bytes match), strict verification, lint gate, commit, and the per-session closed marker. Safe to re-run. |
130
134
  | `--apply-session-close --force` | Skips the probe early-exit. It does **not** skip the authority check, and `--payload` plus `--session-id` are still required to apply anything. |
131
135
 
136
+ When a refusal carries `reason: 'no-user-close-signal'`, the JSON also carries
137
+ `gateReason`, naming which of the three gate checks refused: `no-open` (the
138
+ transcript holds no user close signal at all), `transcript-rewrite-detected`
139
+ (the transcript changed under the gate), or `no-new-open-since-resolution` (the
140
+ close signal predates a resolution already recorded). The collapsed `reason`
141
+ stays the same string in all three cases, so read `gateReason` rather than
142
+ parsing the `Gate detail:` fragment out of `error`. The field is absent for
143
+ every other refusal.
144
+
132
145
  **Two lint gates run automatically, scoped to the files this close writes:**
133
146
 
134
147
  Both gates judge only the **payload files** (the 5 mandatory close files + `open-questions.md`). Lint debt this close did not author is never gated — so an unrelated broken page elsewhere cannot block your close. It is reported as a non-blocking notice, scoped to the close-target project: debt under `projects/<project>/` is listed by file in `notices[]`; debt elsewhere (other projects, shared `pages/`, root files) folds into the `otherDebtCount` integer so the same untouched-file debt does not re-list its filenames on every close (run `/hypo:lint` for the full list).
@@ -152,6 +165,9 @@ The result JSON includes a `stage` field when `ok: false`. Branch on it:
152
165
  | `post-apply-verification` | A mandatory file's `updated:` frontmatter is stale (≠ today) after apply. | Edit the payload's stale `content` (or supply correct `date`), then re-run. Writes are idempotent — re-applying a corrected payload is safe. |
153
166
  | `post-apply-lint` | The payload introduced an error-level lint blocker in a payload file (malformed body / bad frontmatter), or lint crashed. | Fix the offending content in the payload, then re-run. (Broken wikilinks are W4 warnings — not gated.) |
154
167
  | `post-apply-verification+lint` | Both above. | Fix both; re-run. |
168
+ | `proposal-pending` (overwrite park) | At least one overwrite field drifted from this session's observed base, or its payload dropped 2 or more of an existing file's `##` sections without `restructure: true`. Each withheld field appears in `conflicts[]` with a `reason` code and a `why` (a ready-to-read sentence naming the cause, reuse it verbatim, don't reword it), and is parked as a `.cache/proposals/<id>.json` artifact; `proposals[]` lists the id, target, and path for every one. The target on disk is untouched. Other, non-conflicting fields in the same payload may already be on disk but uncommitted (`partialConflict: true`, `appliedUncommitted`). | Do not touch the parked file yourself, and do not tell the user to run `hypomnema proposal apply <id>`: that command reads from a TTY and is refused outright in this environment (no terminal attached). Use the transcript-approved path instead. Run `node ${CLAUDE_PLUGIN_ROOT}/scripts/proposal.mjs challenge --session-id=<current-session-id> --ids=<comma-separated ids from proposals[]>`. It prints a diff per target and mints a nonce; tell the user the exact line it prints (`apply-proposals <nonce>`, read the nonce off that output, never invent or reuse one), and once they have typed it back in the conversation, run `node ${CLAUDE_PLUGIN_ROOT}/scripts/proposal.mjs resolve --session-id=<current-session-id>` to apply the approved batch. `node ${CLAUDE_PLUGIN_ROOT}/scripts/proposal.mjs list` and `... discard <id>` (drop a proposal without applying it) both work with no TTY. Once resolved, re-run the same close; it is idempotent. |
169
+ | `proposal-pending` (append lock timeout) | An append field (`sessionLog` or `log`) could not take its file lock within the timeout (default 5s), usually a concurrent close holding the same target. Nothing is parked for it: it carries `kind: 'append'` in `conflicts[]` and never appears in `proposals[]`. | This is transient, not a review case. Do not run any `proposal` subcommand for it, there is nothing there to challenge or apply. Just re-run the same close; the next attempt normally clears once the other close releases the lock. |
170
+ | `proposal-store-failed` | A conflict was correctly withheld from disk, but writing its `.cache/proposals/` artifact also failed (see `proposalStoreFailures[]` and the loud stderr). The payload bytes for that field are on neither disk nor a proposal artifact right now. | Fix whatever is blocking `.cache/proposals/` (permissions, a file sitting where the directory should be, disk space), then re-run the exact same payload. Nothing was lost: the payload still holds the bytes, only the parking step failed. |
155
171
 
156
172
  Once `ok: true`, report from the result JSON's `applied` and `skipped` arrays together, not `applied` alone. `applied` lists only the fields this run actually wrote bytes for; `skipped` lists the fields that already matched what was on disk (a re-run of an already-applied payload). An idempotent re-run legitimately reports `applied: []`, and that is success, not a failure to report on: check `skipped` for the same 4-6 entries instead. Read `committed` the same way: `true` covers both a real commit and the case where nothing needed staging (a full no-op re-run); `false` means the commit itself ran and failed (see `markerSkipReason`); `null` means apply never reached the commit step at all, because `ok` was already false (an authority refusal before any write, or a verification/lint failure, or a withheld conflict, per `stage`). `null` does NOT mean nothing was written: `applied` can be non-empty (bytes landed on disk) while `committed` stays `null`, because those bytes were never staged into git.
157
173
 
@@ -161,6 +177,7 @@ Once `ok: true`, report from the result JSON's `applied` and `skipped` arrays to
161
177
  - ✓ open-questions applied (or skipped if unchanged)
162
178
  - ✓ log.md entry appended (or already present)
163
179
  - ✓ post-apply lint clean
180
+ - **`restructureWaivers[]`** (report it if non-empty): an overwrite field carried `"restructure": true`, and that flag actually waived a real section-loss trip (the payload dropped `##` sections the guard would otherwise have parked). Report each entry verbatim: the target file and the section names the waiver let through. The waiver is set by the same party (a model composing the payload) the section-loss guard exists to check, so it must never pass silently.
164
181
  - **marker written?** (required check): if `markerWritten: true`, report "session-close marker written"; if `markerWritten: false`, report "session-close marker NOT written (reason: `<markerSkipReason>`)" and do NOT declare the session "closed" or "complete". A missing marker means the Stop-chain is still open; recover per the `markerSkipReason` branch below.
165
182
 
166
183
  If `markerWritten: true`: ask: "Session closed. Would you like to also run knowledge synthesis now, or stop here?"
@@ -169,9 +186,19 @@ If `markerWritten: true`: ask: "Session closed. Would you like to also run knowl
169
186
 
170
187
  - `session-id-required`: you omitted `--session-id`. Pass the main conversation's id and re-run.
171
188
  - `transcript-unresolved`: the id resolved no transcript, so it is almost certainly not the main conversation's (a background-task or Agent-thread uuid, most often). Get the right one and re-run.
172
- - `no-user-close-signal`: the transcript is this session's, and the user never asked to close in wording the gate recognizes (e.g. "세션 마무리까지 진행해줘" falls outside the close-signal set). Re-running the same id changes nothing, because the transcript is unchanged. Confirm intent once with `AskUserQuestion`, header "세션", a single option labelled **세션 마무리** (설명: "이 세션을 마무리하고 close 마커를 기록"). If the user picks it, that answer lands in the transcript as a recognized close signal, so re-running the exact same command now applies **everything**: the writes, the commit, and the marker. If the user declines, the session stays open and nothing is written. Do NOT touch the close-signal matcher itself, and do not hand-write the files to work around the refusal.
189
+ - `no-user-close-signal`: the transcript is this session's, but no close authority is in force. **Branch on `gateReason` before doing anything.** The three cases need three different responses, and treating them alike is what made this refusal look like it had a new cause every time it appeared.
190
+ - `no-open`: the user genuinely never asked to close in wording the gate recognizes (e.g. "세션 마무리까지 진행해줘" falls outside the close-signal set). Re-running the same id changes nothing, because the transcript is unchanged. Confirm intent once with `AskUserQuestion`, header "세션", a single option labelled **세션 마무리** (설명: "이 세션을 마무리하고 close 마커를 기록"). If the user picks it, that answer lands in the transcript as a recognized close signal, so re-running the exact same command now applies **everything**: the writes, the commit, and the marker. If the user declines, the session stays open and nothing is written.
191
+ - `no-new-open-since-resolution`: the user DID ask, and that request was already resolved by an earlier close. Asking again makes them answer a question they have already answered. Report that this session is already closed and stop; do not re-prompt. On the apply path, a resolution is recorded only once the session-close marker itself lands, so this reason cannot appear for a session whose apply was never marked closed. `/clear` is the one exception: it ends the session as surely as a close apply does, so `hypo-session-end.mjs` records the same resolution on a `/clear` directly, with no marker involved at all. So a session that never ran an apply, but was ended with `/clear`, can still carry a recorded resolution.
192
+ - `transcript-rewrite-detected`: the transcript changed underneath the gate, so the earlier signal can no longer be attested. This is not a statement about what the user wants. Say what happened rather than asking them to repeat themselves, and let a human decide.
193
+
194
+ In all three: do NOT touch the close-signal matcher itself, and do not hand-write the files to work around the refusal.
195
+
196
+ If the apply succeeded but `markerWritten: false`, do NOT say "session closed." Branch on `markerSkipReason`, which carries one of five values:
173
197
 
174
- If the apply succeeded but `markerWritten: false`, do NOT say "session closed." Branch on `markerSkipReason` (`compact-gate-not-ok`, `commit-failed: …`, `marker-did-not-land`): surface the reason verbatim and address it (resolve the compact blocker, fix the git / disk issue) before re-running.
198
+ - `compact-gate-not-ok`, `commit-failed: …`, `marker-did-not-land`: surface the reason verbatim and address it (resolve the compact blocker, fix the git / disk issue) before re-running. **The re-run needs no fresh close phrase from the user for these three.** The resolution that spends a close signal is written only once the marker lands, so a run denied its marker at this stage leaves that signal unspent and the retry is authorized by the same one.
199
+ **A `commit-failed:` retry can need a hand first.** The payload writes land before the commit does, so after a failed commit those files already match what the retry would write. The retry skips them as already current, which leaves them out of the commit it makes, and they stay uncommitted and keep blocking the marker. If a retry reports `commit-failed:` or `compact-gate-not-ok` a second time on the same close, stop retrying and look at `git status` in the vault: stage and commit the close files yourself, or ask the user to, then run the close again.
200
+ - `transcript-unresolved`: the marker writer could not resolve any transcript for this `--session-id` at all, so it never got as far as checking for a close signal. Passing the exact same id again changes nothing; get the main conversation's real session id (not a background-task or Agent-thread uuid) and re-run.
201
+ - `no-user-close-signal`: a transcript was found, but it carries no close signal the gate recognizes. This is the one branch where the re-run DOES need a fresh signal: confirm intent once with `AskUserQuestion` (the same 세션 마무리 flow described under Step 3's `no-user-close-signal` table above), then re-run only after the user confirms.
175
202
 
176
203
  If the user says stop, end here. Otherwise continue to Step 5.
177
204
 
@@ -182,9 +209,14 @@ If the user says stop, end here. Otherwise continue to Step 5.
182
209
  Bundled scripts here run via `${CLAUDE_PLUGIN_ROOT}/scripts/`. To resolve that package root: if `${CLAUDE_PLUGIN_ROOT}` is already an absolute path, use it; otherwise read `pkgRoot` from `~/.claude/hypo-pkg.json` (only when non-empty and the target script exists under it); otherwise use the `hypo@hypomnema` (or legacy `hypomnema@hypomnema`) installPath in `~/.claude/plugins/installed_plugins.json`; if none resolve, stop and tell the user to run `hypomnema upgrade --apply` (or `/hypo:upgrade` on a plugin install) or reinstall instead of guessing the cache layout.
183
210
 
184
211
  ```bash
185
- node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs [--hypo-dir="<path>"] [--min-group=2]
212
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs --min-group=2
186
213
  ```
187
214
 
215
+ Add `--hypo-dir="<path>"` only when the user specified a Hypomnema directory explicitly;
216
+ otherwise omit it.
217
+
218
+ An unrecognized flag exits 2 instead of being ignored.
219
+
188
220
  Show the output to the user. If no candidates are found, tell them Hypomnema looks well-connected and no crystallization is needed.
189
221
 
190
222
  ---
@@ -255,10 +287,15 @@ Show what was created or modified, and offer to run `/hypo:lint` to verify all n
255
287
  `--check-session-close` (read-only strict gate, same check PreCompact runs) is still supported as a probe-only verification. Use it when you only want to verify that today's session-close is complete without applying anything:
256
288
 
257
289
  ```bash
258
- node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs --check-session-close [--hypo-dir="<path>"]
290
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs --check-session-close
259
291
  ```
260
292
 
261
- It reports any file as `missing` or `stale`. For an actual close, prefer `--apply-session-close --payload=<path>` (Step 3) — it bundles freshness + lint into one gate and is the documented dogfood path. (`parseArgs` only accepts the `--payload=<path>` spelling; a space-separated `--payload <path>` is silently ignored and triggers "payload is required".)
293
+ Add `--hypo-dir="<path>"` only when the user specified a Hypomnema directory explicitly;
294
+ otherwise omit it.
295
+
296
+ An unrecognized flag exits 2 instead of being ignored.
297
+
298
+ It reports any file as `missing` or `stale`. For an actual close, prefer `--apply-session-close --payload=<path>` (Step 3): it bundles freshness and lint into one gate and is the documented dogfood path. `parseArgs` only accepts the `--payload=<path|->` spelling (a path, or `-` for stdin); a space-separated `--payload <path>` is rejected outright with exit 2, not silently dropped.
262
299
 
263
300
  Add `--project=<slug>` to scope the check to one project (close status + lint scope) when recency picks the wrong one. This is a project-scoped diagnostic only: a green scoped result (JSON `scope: "project"`) attests that slug is close-complete, **not** that `/compact` is unblocked globally.
264
301
 
package/commands/graph.md CHANGED
@@ -23,12 +23,15 @@ If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. O
23
23
  ## Step 2 — Run the graph script
24
24
 
25
25
  ```bash
26
- node ${CLAUDE_PLUGIN_ROOT}/scripts/graph.mjs \
27
- [--hypo-dir="<path>"] \
28
- [--format=json|mermaid|dot] \
29
- [--min-edges=<n>]
26
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/graph.mjs
30
27
  ```
31
28
 
29
+ Add `--hypo-dir="<path>"` only when the user gave one (per Step 1). Add `--format=<fmt>`
30
+ and `--min-edges=<n>` only with one concrete value from the Options list below, for
31
+ example `--format=mermaid`; the defaults (`json`, `0`) apply when they are omitted.
32
+
33
+ An unrecognized flag exits 2 instead of being ignored.
34
+
32
35
  Options:
33
36
  - `--format=json` (default) — adjacency list with in/out degree counts
34
37
  - `--format=mermaid` — Mermaid `graph TD` diagram (paste into a Markdown code block)
package/commands/lint.md CHANGED
@@ -25,10 +25,17 @@ If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. O
25
25
  ## Step 2 — Run lint
26
26
 
27
27
  ```bash
28
- node ${CLAUDE_PLUGIN_ROOT}/scripts/lint.mjs [--hypo-dir="<path>"] [--json] [--fix]
28
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/lint.mjs --json
29
29
  ```
30
30
 
31
+ Add `--hypo-dir="<path>"` only when the user gave one (per Step 1); otherwise leave it
32
+ out and the script resolves the root itself.
33
+
34
+ An unrecognized flag exits 2 instead of being ignored. Do not add `--fix` on this first
35
+ pass; Step 4 covers when to offer it and re-run with it only after the user agrees.
36
+
31
37
  Options:
38
+ - `--hypo-dir=<path>` (optional): Hypomnema root, pass only when the user specified one explicitly
32
39
  - `--json` — output results as JSON (useful for tooling)
33
40
  - `--fix` — auto-add missing `updated` field (safe repairs only; no other fields are modified)
34
41
 
package/commands/query.md CHANGED
@@ -22,15 +22,17 @@ Ask the user what they want to know if it was not provided in the command invoca
22
22
 
23
23
  Bundled scripts here run via `${CLAUDE_PLUGIN_ROOT}/scripts/`. To resolve that package root: if `${CLAUDE_PLUGIN_ROOT}` is already an absolute path, use it; otherwise read `pkgRoot` from `~/.claude/hypo-pkg.json` (only when non-empty and the target script exists under it); otherwise use the `hypo@hypomnema` (or legacy `hypomnema@hypomnema`) installPath in `~/.claude/plugins/installed_plugins.json`; if none resolve, stop and tell the user to run `hypomnema upgrade --apply` (or `/hypo:upgrade` on a plugin install) or reinstall instead of guessing the cache layout.
24
24
 
25
+ If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise
26
+ omit the flag.
27
+
25
28
  Run full-text search:
26
29
 
27
30
  ```bash
28
- node ${CLAUDE_PLUGIN_ROOT}/scripts/query.mjs \
29
- --q="<query terms>" \
30
- [--hypo-dir="<path>"] \
31
- [--limit=10]
31
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/query.mjs --q="<query terms>" --limit=10
32
32
  ```
33
33
 
34
+ An unrecognized flag exits 2 instead of being ignored.
35
+
34
36
  ---
35
37
 
36
38
  ## Step 3 — Read relevant pages
@@ -15,11 +15,22 @@ You are running `/hypo:verify`. Audit wiki pages for overdue or missing `verify_
15
15
 
16
16
  Bundled scripts here run via `${CLAUDE_PLUGIN_ROOT}/scripts/`. To resolve that package root: if `${CLAUDE_PLUGIN_ROOT}` is already an absolute path, use it; otherwise read `pkgRoot` from `~/.claude/hypo-pkg.json` (only when non-empty and the target script exists under it); otherwise use the `hypo@hypomnema` (or legacy `hypomnema@hypomnema`) installPath in `~/.claude/plugins/installed_plugins.json`; if none resolve, stop and tell the user to run `hypomnema upgrade --apply` (or `/hypo:upgrade` on a plugin install) or reinstall instead of guessing the cache layout.
17
17
 
18
+ If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise
19
+ omit the flag.
20
+
18
21
  ```bash
19
- node ${CLAUDE_PLUGIN_ROOT}/scripts/verify.mjs [--hypo-dir="<path>"] [--file=<path>]
22
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/verify.mjs
20
23
  ```
21
24
 
25
+ Add `--file="<path>"` only to check a single page (for example, right after editing it).
26
+ Omitting it scans the whole vault (`pages/` and `projects/`); the script skips a `--file`
27
+ path it cannot read rather than erroring, so passing one that does not exist reports
28
+ "nothing to verify" instead of failing loudly.
29
+
30
+ An unrecognized flag exits 2 instead of being ignored.
31
+
22
32
  Options:
33
+ - `--hypo-dir=<path>` (optional): Hypomnema root, pass only when the user specified one explicitly
23
34
  - `--file=<path>` — check a single page only (useful after editing a page)
24
35
 
25
36
  ---
@@ -128,23 +128,22 @@ Hooks run automatically at Claude Code lifecycle events. They are deployed to `~
128
128
  | Hook | Responsibility |
129
129
  |---|---|
130
130
  | `hypo-session-start` | Inject `index.md`, root `hot.md`, project `hot.md`/`session-state.md`. Run `git pull --ff-only` (silent fail on missing remote) |
131
- | `hypo-first-prompt` | Marker-based one-shot `hot.md` injection on first user prompt (10-min TTL) — for sessions that bypass `SessionStart` |
131
+ | `hypo-first-prompt` | Reads the marker left by `hypo-session-start` or `hypo-cwd-change` and forces a one-line resume on the first user prompt (10-min TTL). It does not re-read or re-inject `hot.md` |
132
132
  | `hypo-lookup` | BM25 search over the wiki on every prompt. **HIT** → inject top-3 page snippets (≤2000 chars each; a page whose `verify_by_date` is overdue gets a `[STALE verify_by_date=…]` marker prepended). **MISS** → emit closest-slug signal that prompts Claude to research + `/hypo:ingest` |
133
- | `hypo-compact-guard` | Detect `/compact` invocations → enforce session-close checklist before allowing compact |
133
+ | `hypo-compact-guard` | Detect `/compact` or `/clear` typed in chat and, if session close is incomplete, tell Claude so. It never blocks `/compact` |
134
134
  | `hypo-personal-check` | PreCompact detection: lint blockers, uncommitted changes, missing session-log entries surface as a `systemMessage`; `/compact` is never blocked here |
135
135
  | `hypo-auto-stage` | After Write/Edit on a wiki path, run `git add` (skips paths matching `.hypoignore`) |
136
136
  | `hypo-hot-rebuild` | At session stop, regenerate root `hot.md` from recent activity; emit growth metrics + cache for next SessionStart |
137
137
  | `hypo-session-record` | At session stop, append `{session_id, transcript_path, recorded_at, cwd, device}` to `.cache/sessions/index.jsonl` (primary source for the observability audit) |
138
138
  | `hypo-auto-commit` | At session stop, filter changed paths through `.hypoignore`, commit non-ignored changes, `git pull --no-rebase` + `git push` (silent fail on missing remote) |
139
- | `hypo-cwd-change` | When working directory changes, re-resolve the active project and inject its `hot.md` |
140
- | `hypo-file-watch` | Notify on external wiki edits so the in-session view stays consistent |
139
+ | `hypo-cwd-change` | When working directory changes, re-resolve the active project and build `additionalContext` with its `hot.md`; `CwdChanged` has no field Claude Code forwards to the model, so this does not currently reach Claude |
140
+ | `hypo-file-watch` | Build `additionalContext` for a changed wiki file. Two things keep this inert: nothing in this package returns `watchPaths`, so the event has no registered trigger, and `FileChanged` output does not reach the model. The event also fires regardless of who changed the file, so it is not an external-edit signal |
141
141
 
142
142
  ### Deployment constraint
143
143
 
144
144
  Hooks run in an isolated environment at `~/.claude/hooks/`. They **cannot import from relative paths** outside their own directory. Therefore:
145
145
 
146
- - All shared hook logic lives in `hypo-shared.mjs`.
147
- - `hypo-shared.mjs` is declared via the `shared` field in `hooks.json` and copied alongside hooks at deploy time.
146
+ - Shared hook logic lives in the modules `hooks/shared.json` declares, not only in `hypo-shared.mjs`. That file (a plain JSON array of `.mjs` basenames) is the source of truth for the list and is copied alongside hooks at deploy time. It used to be a `shared` field inside `hooks.json` itself; it moved to this sibling file because the harness started warning on that unknown top-level key once it began validating `hooks.json` against its own hook-registration schema.
148
147
  - Hook utilities use **only Node.js built-ins** — no relative imports, no npm dependencies.
149
148
 
150
149
  Scripts in `scripts/` are not deployed — they run from the package install path — so they can import from `scripts/lib/`.
@@ -163,7 +162,7 @@ Scripts in `scripts/` are not deployed — they run from the package install pat
163
162
  | `hypoIsClean()` | Check git status + unpushed commits |
164
163
  | `hotMdIsClean()` | Validate `hot.md` structure |
165
164
  | `isCompactCommand(prompt)` | Detect `/compact` invocations |
166
- | `buildOutput(...)` | Format hook output for Claude Code's `additionalContext` channel |
165
+ | `buildOutput(hookEventName, context, extra)` | Build hook output that nests `additionalContext` under `hookSpecificOutput`, keeping control fields as top-level siblings. The event name is required: Claude Code drops a payload whose `hookEventName` does not match the firing event |
167
166
  | `SESSION_STATE_NEXT_HEADINGS` | Allowed headings for "next tasks" — `## 다음 이어받기` / `## 다음 작업`. Lint reuses this constant (DRY) |
168
167
 
169
168
  ---
@@ -246,6 +245,8 @@ Hooks inline this logic in `hypo-shared.mjs`. Scripts use `scripts/lib/hypo-root
246
245
  4. Detects drift (e.g., `pkgRoot` no longer exists) and refreshes the pointer.
247
246
  5. `--json` output emits `{ schema, hooks, settings, applied }`. `--dry-run` previews; `--apply` commits changes.
248
247
 
248
+ The SessionStart hook (`hooks/hypo-session-start.mjs`) is a third writer of this file, outside init/upgrade entirely. `hooks/hypo-shared.mjs`'s `pkgRootDriftStatus` detects drift by comparing the canonicalized `pkgRoot` alone, not `pkgVersion`, so a file whose `pkgRoot` already matches self-location reads as `match` even when its `pkgVersion` is stale. Only a `pkgRoot` mismatch triggers the rewrite (temp file, then rename), and `pkgVersion` is carried along in that same write rather than checked or fixed on its own. The rewrite is guarded by a lockfile, so two sessions self-locating to different install roots at once cannot race each other's write, and it writes nothing on a downgrade (self-location resolves to an older version than the file already records), an incoming version that fails to parse as semver, a lock it could not acquire, or an opted-out session.
249
+
249
250
  ### `/hypo:uninstall`
250
251
 
251
252
  Removes hypo-prefixed hooks from `~/.claude/hooks/` and matching entries from `~/.claude/settings.json`. **Non-hypo hooks are preserved**. `--apply` also reaches past `~/.claude/`: it strips the marked `claude()` block from the shell rc file(s) init wrote to, and removes the marked pre-commit hook from the wiki's own git repo. Both removals are marker-gated the same way the hooks-dir cleanup is, so a hand-edited block or hook is left in place rather than guessed at.
@@ -298,7 +299,7 @@ SessionStart
298
299
  ├─► UserPromptSubmit (every prompt)
299
300
  │ ├─► hypo-first-prompt.mjs (one-shot, 10min TTL)
300
301
  │ ├─► hypo-lookup.mjs (BM25 inject)
301
- │ └─► hypo-compact-guard.mjs (block /compact when checklist incomplete)
302
+ │ └─► hypo-compact-guard.mjs (report incomplete checklist; never blocks /compact)
302
303
  │
303
304
  ├─► PostToolUse(Write/Edit)
304
305
  │ └─► hypo-auto-stage.mjs (git add)
@@ -307,7 +308,7 @@ SessionStart
307
308
  │ └─► hypo-personal-check.mjs (lint + session-close gate)
308
309
  │
309
310
  ├─► CwdChanged
310
- │ └─► hypo-cwd-change.mjs (re-inject project hot.md)
311
+ │ └─► hypo-cwd-change.mjs (builds hot.md context; does not currently reach Claude)
311
312
  │
312
313
  └─► Stop
313
314
  ├─► hypo-hot-rebuild.mjs (regenerate root hot.md + growth cache)
@@ -501,7 +502,7 @@ Requires `NPM_TOKEN` secret.
501
502
 
502
503
  ## Plugin manifest
503
504
 
504
- `.claude-plugin/plugin.json` declares the plugin to Claude Code. `hooks/hooks.json` follows the standard plugin hooks schema:
505
+ `.claude-plugin/plugin.json` declares the plugin to Claude Code. `hooks/hooks.json` follows the standard plugin hooks schema, and carries only what that schema knows about: an event-to-hook map, nothing else.
505
506
 
506
507
  ```json
507
508
  {
@@ -517,11 +518,18 @@ Requires `NPM_TOKEN` secret.
517
518
  ]
518
519
  }
519
520
  ]
520
- },
521
- "shared": ["hypo-shared.mjs"]
521
+ }
522
522
  }
523
523
  ```
524
524
 
525
+ The shared-file list (helpers a registered hook imports, such as `hypo-shared.mjs`) is Hypomnema's own convention, not something the harness's schema recognizes, so it lives in a sibling file instead: `hooks/shared.json`, a plain JSON array of `.mjs` basenames.
526
+
527
+ ```json
528
+ ["hypo-shared.mjs", "version-check.mjs", "..."]
529
+ ```
530
+
531
+ `hooks/hooks.json` is still the source of truth for the event-to-hook map; `hooks/shared.json` is the source of truth for the shared-file list. A hook registered in the former, or a helper listed in the latter, is what init/upgrade deploy. Anything left off either file never reaches an installed copy.
532
+
525
533
  `claude plugin validate .` is run as part of CI's plugin-snapshot checks.
526
534
 
527
535
  ---
@@ -62,8 +62,7 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for the full breakdown. Short version:
62
62
 
63
63
  **Hooks run in `~/.claude/hooks/` in isolation. They cannot import from relative paths.**
64
64
 
65
- - All shared logic must live in `hooks/hypo-shared.mjs`.
66
- - `hypo-shared.mjs` is declared via the `shared` field in `hooks/hooks.json` and copied alongside each hook at deploy time.
65
+ - Shared logic must live in one of the modules `hooks/shared.json` declares, not only in `hypo-shared.mjs`. That file (a plain JSON array of `.mjs` basenames) is the source of truth for the list and is copied alongside each hook at deploy time. It used to be a `shared` field inside `hooks.json` itself; that moved to this sibling file once the harness started warning on that unknown top-level key.
67
66
  - Hook utilities may use **only** Node.js built-ins (`fs`, `path`, `os`, `child_process`, `crypto`).
68
67
  - No relative imports, no npm dependencies.
69
68
 
@@ -85,7 +84,7 @@ Scripts under `scripts/` are not deployed and may freely import from `scripts/li
85
84
 
86
85
  1. Edit the hook file in `hooks/`.
87
86
  2. If it's new, register it in `hooks/hooks.json` under the correct event key.
88
- 3. Shared utilities go in `hooks/hypo-shared.mjs`.
87
+ 3. Shared utilities go in a module listed in `hooks/shared.json`, usually `hypo-shared.mjs`.
89
88
  4. Add a contract test in the hook's area file, e.g. `tests/session-hooks.test.mjs` (input → expected `additionalContext` shape).
90
89
  5. After your change, run `/hypo:upgrade` in a real Claude Code session and verify the hook fires.
91
90
 
@@ -129,6 +128,8 @@ Everything else belongs in `commands/`, which loads the same way.
129
128
  npm test # tests/*.test.mjs, sharded across processes — unit + smoke + contract
130
129
  npm run lint # scripts/lint.mjs — frontmatter + wikilink validation + W8 (design-history stale
131
130
  # vs session-log) + W14 (design-history missing but session-log implies one)
131
+ # + W15 (synthesis older than the newest page in its sources_consulted)
132
+ # + W16 (sources_consulted names W15 could not resolve)
132
133
  npm run fix:verify # Phase 1 of learned_behavior #6 — verifies fix #N status claims in
133
134
  # a wiki spec against `// @fix #N: <test-name>` anchors, read as a
134
135
  # union across every tests/*.mjs. Maintainer dogfood; needs a wiki at
@@ -254,7 +255,7 @@ After both blocks, language-neutral:
254
255
 
255
256
  - **PR title**: Conventional Commits plus a scope, e.g. `feat(feedback): add failure_type enum`. The type drives the CHANGELOG section (see the classification table below).
256
257
  - **Merge commit**: the squash-merge subject carries the PR number (`#123`). That is where `#N` comes from, not the PR title. The two conventions stay separate.
257
- - Internal tracker ids (`FEAT-`, `IMPR-`, `ISSUE-`, `PRAC-`, `fix #N`) may appear in your local notes and in `tests/` (where they aid test-to-issue traceability and never reach an installed user), but not in shipped code or workflow comments, and never on the published changelog and release surface: not in the CHANGELOG body, not in the PR `## Changelog` block, not in a tag annotation, not in a GitHub Release. The only tracker identifier that ships in those is the PR number `#N`; the lone exception is the ADR carve-out noted below. `check-tracker-ids` gates the file, message, and tag surfaces (`--all`/`--staged` for files, `--commit-msg` for messages, `--tag` for the tag body), and `check-pr-surface` gates the PR title and body; the migration keeps the CHANGELOG body clean. `ADR NNNN` / `decisions/NNNN` are exempt on the changelog surfaces (the CHANGELOG body, the tag body, and the PR `## Changelog` block), where a release line legitimately cites the decision behind it.
258
+ - Internal tracker ids (`FEAT-`, `IMPR-`, `ISSUE-`, `PRAC-`, `fix #N`, `ADR NNNN`, `decisions/NNNN`) may appear in your local notes and in `tests/` (where they aid test-to-issue traceability and never reach an installed user), but not in shipped code or workflow comments, and never on the published changelog and release surface: not in the CHANGELOG body, not in the PR `## Changelog` block, not in a tag annotation, not in a GitHub Release. The only identifier that ships in those is the PR number `#N`. `ADR NNNN` / `decisions/NNNN` used to be exempt on the changelog surfaces, on the theory that a release line legitimately cites the decision behind it; that exemption is gone, because this repo ships no `decisions/` directory for anyone outside the maintainer's private wiki to open — an external reader hits the same dead end on `decisions/NNNN` as on `ISSUE-N`, so the exemption was blocking nothing but its own contradiction. `check-tracker-ids` gates the file, message, and tag surfaces (`--all`/`--staged` for files, `--commit-msg` for messages, `--tag` for the tag body), and `check-pr-surface` gates the PR title and body; the migration keeps the CHANGELOG body clean.
258
259
 
259
260
  ### The `## Changelog` block
260
261
 
@@ -370,6 +371,14 @@ node scripts/bump-version.mjs <new-semver> # e.g. 1.2.2 or 1.3.0-rc.1
370
371
  # version-consistency gate stay green (the lock carries the version twice).
371
372
  npm install --package-lock-only
372
373
 
374
+ # 1c. If this release also bumps templates/SCHEMA.md's `version:` frontmatter,
375
+ # add a matching line to SCHEMA_VERSION_DELTAS in
376
+ # scripts/lib/template-schema-version.mjs, naming what that version added.
377
+ # Skipping this is silent: upgrade.mjs's notice just falls back to "review
378
+ # manually" for that version, with no test failing at bump time except
379
+ # the one in tests/upgrade.test.mjs that pins the SHIPPED version has an
380
+ # entry.
381
+
373
382
  # 2. Draft the new CHANGELOG.md section from the merged PRs' `## Changelog`
374
383
  # blocks (see CHANGELOG conventions above). The collector prints a draft for
375
384
  # the range since the last tag; paste it, then finalize the wording by hand,
@@ -404,8 +413,10 @@ git commit -m "chore: release v<version>"
404
413
  # then a Korean summary block. The GitHub Release republishes this body
405
414
  # verbatim, so keep it on the same public surface as the CHANGELOG: PR
406
415
  # numbers (#N) only, no `FEAT-`/`IMPR-`/`ISSUE-`/`PRAC-`/`fix #N` tracker
407
- # ids (check-tracker-ids --tag gates it). Like the CHANGELOG, the tag body MAY cite
408
- # `ADR NNNN` / `decisions/NNNN` for the decision behind a release.
416
+ # ids (check-tracker-ids --tag gates it), and no `ADR NNNN` / `decisions/NNNN`
417
+ # either: this repo ships no `decisions/` directory, so a reader outside the
418
+ # maintainer's private wiki cannot open either one. Say what changed in
419
+ # plain prose, or cite the PR number.
409
420
  git tag -a v<version> -m "$(cat <<'EOF'
410
421
  Hypomnema v<version>: <one-line English summary>
411
422
 
@@ -419,7 +430,12 @@ Hypomnema v<version>: <한 줄 한글 요약>
419
430
  EOF
420
431
  )"
421
432
 
422
- # 6. Rehearse the release locally (same checks CI runs against the tag)
433
+ # 6. Rehearse the release locally (same checks CI runs against the tag).
434
+ # This is the ONLY gate that runs before the tag object is public. The
435
+ # release workflow re-runs these, but it starts when the push in step 7
436
+ # lands, so by then the annotated tag body is already on GitHub: CI can
437
+ # stop the npm publish and the Release, not the tag. Skip these three and
438
+ # nothing catches a leaked id until a reader finds it.
423
439
  node scripts/check-bilingual.mjs --tag v<version>
424
440
  node scripts/check-versions.mjs --tag v<version>
425
441
  node scripts/check-tracker-ids.mjs --tag v<version> # no tracker ids leak into the tag body