hypomnema 1.7.4 → 1.8.0

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 (58) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +1202 -0
  4. package/README.ko.md +4 -4
  5. package/README.md +5 -5
  6. package/commands/audit.md +1 -1
  7. package/commands/capture.md +1 -1
  8. package/commands/crystallize.md +3 -3
  9. package/commands/doctor.md +15 -1
  10. package/commands/feedback.md +1 -1
  11. package/commands/graph.md +1 -1
  12. package/commands/ingest.md +1 -1
  13. package/commands/init.md +2 -2
  14. package/commands/lint.md +1 -1
  15. package/commands/query.md +1 -1
  16. package/commands/rename.md +1 -1
  17. package/commands/resume.md +1 -1
  18. package/commands/stats.md +1 -1
  19. package/commands/uninstall.md +1 -1
  20. package/commands/upgrade.md +2 -2
  21. package/commands/verify.md +1 -1
  22. package/docs/ARCHITECTURE.md +8 -3
  23. package/docs/CONTRIBUTING.md +2 -1
  24. package/hooks/base-store.mjs +198 -0
  25. package/hooks/close-gate-store.mjs +3 -2
  26. package/hooks/hypo-auto-minimal-crystallize.mjs +10 -0
  27. package/hooks/hypo-compact-guard.mjs +5 -3
  28. package/hooks/hypo-cwd-change.mjs +8 -0
  29. package/hooks/hypo-personal-check.mjs +127 -67
  30. package/hooks/hypo-session-start.mjs +91 -10
  31. package/hooks/hypo-shared.mjs +241 -19
  32. package/hooks/version-check.mjs +44 -6
  33. package/package.json +9 -3
  34. package/scripts/capture.mjs +49 -17
  35. package/scripts/crystallize.mjs +20 -2092
  36. package/scripts/doctor.mjs +334 -14
  37. package/scripts/init.mjs +106 -81
  38. package/scripts/lib/crystallize-args.mjs +50 -0
  39. package/scripts/lib/crystallize-close-apply.mjs +1830 -0
  40. package/scripts/lib/crystallize-close-check.mjs +238 -0
  41. package/scripts/lib/crystallize-close-gate.mjs +58 -0
  42. package/scripts/lib/crystallize-helpers.mjs +55 -0
  43. package/scripts/lib/extensions.mjs +175 -49
  44. package/scripts/lib/git-hooks-dir.mjs +88 -3
  45. package/scripts/lib/plugin-detect.mjs +176 -21
  46. package/scripts/lint.mjs +12 -6
  47. package/scripts/proposal.mjs +3 -3
  48. package/scripts/upgrade.mjs +243 -20
  49. package/skills/crystallize/SKILL.md +15 -13
  50. package/skills/graph/SKILL.md +3 -3
  51. package/skills/ingest/SKILL.md +2 -2
  52. package/skills/lint/SKILL.md +3 -3
  53. package/skills/query/SKILL.md +3 -3
  54. package/skills/verify/SKILL.md +3 -3
  55. package/templates/hypo-automation.md +59 -17
  56. package/templates/hypo-config.md +1 -1
  57. package/templates/hypo-guide.md +12 -9
  58. package/templates/hypo-help.md +1 -1
package/README.ko.md CHANGED
@@ -39,7 +39,7 @@ Andrej Karpathy의 "LLM 네이티브 위키" 스케치에서 출발했습니다.
39
39
 
40
40
  업그레이드 정책 하나만 미리 알아 두세요. `hypomnema upgrade --apply`는 사용자가 직접 편집한 `SCHEMA.md`를 덮어쓰지 않습니다. 스키마가 올라가면 위키 루트에 마이그레이션 보고서만 써 주고, 실제 반영은 사용자가 직접 합니다(코드에서 Option C라고 부르는 정책).
41
41
 
42
- 같은 보류 방식이 세션 마무리 파일을 쓸 때에도 적용됩니다. 세션 종료 쓰기의 대상 파일이 이 세션이 읽은 뒤에 바뀌었다면 Hypomnema는 덮어쓰지 않습니다. 그 내용을 `<위키>/.cache/proposals/` 아래에 보류하고 알려 줍니다. 검토와 적용은 직접 하세요. `hypomnema proposal list`로 목록을 보고 `hypomnema proposal apply <id>` 또는 `hypomnema proposal discard <id>`를 씁니다. 자동 적용은 없습니다.
42
+ 같은 보류 방식이 세션 마무리 파일을 쓸 때에도 적용됩니다. 세션 종료 쓰기의 대상 파일이 이 세션이 읽은 뒤에 바뀌었다면 Hypomnema는 덮어쓰지 않습니다. 그 내용을 `<위키>/.cache/proposals/` 아래에 보류하고 알려 줍니다. 검토와 적용은 직접 하세요. `hypomnema proposal list`로 목록을 보고 터미널에서 `hypomnema proposal apply <id>` 또는 `hypomnema proposal discard <id>`를 씁니다. 세션 안에서는 타이핑할 터미널이 없으므로 같은 승인을 대화에서 받습니다. `hypomnema proposal challenge`가 diff를 보여 주고, 거기 적힌 줄을 직접 입력하면, `hypomnema proposal resolve`가 본 그대로를 씁니다. 자동 적용은 없습니다.
43
43
 
44
44
  자동처럼 보이지만 실제로는 직접 실행해야 하는 기능이 두 가지 있습니다. extensions sync와 역방향 capture입니다. 위키 안 `~/hypomnema/extensions/{agents,commands,hooks,skills}/`에 둔 파일은 `~/.claude/`에 반영되는데(`--codex`를 붙이면 `hooks`·`commands`는 `~/.codex/`에도), 직접 `hypomnema init`, `hypomnema upgrade --apply`, 또는 dry-run이 아닌 `hypomnema capture`를 돌려야만 반영됩니다. 저절로 밀어 넣는 트리거는 없습니다. `hypomnema capture`(또는 `/hypo:capture`)는 반대 방향으로, 직접 만든 command·agent·hook·skill을 위키로 가져오는 경로인데 이것도 마찬가지로 명시적으로 불러야 동작합니다. 자세한 내용은 아래 명령어 표를 보세요.
45
45
 
@@ -203,7 +203,7 @@ Hypomnema는 청크가 아니라 페이지를 지식 단위로 봅니다. 새
203
203
  |---|---|
204
204
  | `SessionStart` | "어디까지 했더라?" `hot.md` / `session-state.md` 다시 읽기 |
205
205
  | `UserPromptSubmit` | "이거 이미 정리해 뒀나?" BM25 룩업, top-3 주입 |
206
- | `PreCompact` | "세션 정리는 했나?" 체크리스트 가드 |
206
+ | `PreCompact` | "세션 정리는 했나?" 체크리스트 알림 (권고일 뿐, `/compact`를 막지 않음) |
207
207
  | `PostToolUse` (Write/Edit/MultiEdit) | `git add` |
208
208
  | `Stop` | `git commit && git pull --no-rebase && git push` |
209
209
 
@@ -261,7 +261,7 @@ Hypomnema는 청크가 아니라 페이지를 지식 단위로 봅니다. 새
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` 포인터 테이블 재생성 (구조와 날짜) |
264
- | `hypo-personal-check.mjs` | `PreCompact` | session-close 미완, 위키 커밋/푸시 누락, hot.md 구조 위반, lint 블로커면 compact 차단 (우회: `HYPO_SKIP_GATE=1`) |
264
+ | `hypo-personal-check.mjs` | `PreCompact` | session-close 미완, 위키 커밋/푸시 누락, hot.md 구조 위반, lint 블로커를 `systemMessage`로 알림. `/compact`는 여기서 막지 않음. 세션 마무리 강제는 다른 자리에 남아 있음: Stop 훅(`hypo-auto-minimal-crystallize.mjs`)이 마무리 확인 없는 의미 있는 세션을 막고, `--mark-session-closed`는 여전히 red 게이트에서 마커를 거부함 |
265
265
  | `hypo-session-end.mjs` | `SessionEnd` | SessionEnd 마커 기록. 다음 SessionStart가 `source=clear` 복구를 감지하게 함 |
266
266
  | `hypo-session-record.mjs` | `Stop` | observability 점수 + auto-resume 신호용 세션 메타데이터 기록 |
267
267
  | `hypo-auto-minimal-crystallize.mjs` | `Stop` | 사용자가 마무리를 신호한 뒤, 의미 있는 작업을 했는데 세션 마무리가 확인되지 않으면 Stop을 막고 `crystallize.mjs --mark-session-closed` 명령을 돌려줌. 미커밋 변경이나 진행 중인 작업이 있으면 명령 대신 지금 닫을지 되물음 |
@@ -392,7 +392,7 @@ E. 멈춘 프로젝트 재개.
392
392
 
393
393
  세 번째 프라이버시 축은 경로가 아니라 페이지 단위로 걸립니다. 페이지 프런트매터에 `visibility_scope: machine:<device>`를 넣으면 그 기기에서만 노출됩니다. `<device>`의 기본값은 hostname이며, hostname이 고정적이지 않은 환경이라면 `HYPO_DEVICE`로 못박으세요. 그러지 않으면 페이지가 자기 기기와도 매치되지 않습니다. 필드를 생략하면 shared입니다.
394
394
 
395
- `HYPO_SKIP_GATE=1`은 세션 중간에 사용자를 막을 수 있는 게이트 전부가 존중합니다. `hypo-personal-check`(PreCompact), `hypo-compact-guard`, `hypo-close-guard`, `hypo-auto-minimal-crystallize`, 그리고 `hypo-web-fetch-ingest`의 ingest 안내까지입니다. 기록할 필요 없는 가벼운 세션에 씁니다.
395
+ `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`만 억제합니다. 기록할 필요 없는 가벼운 세션에 씁니다.
396
396
 
397
397
  > 모델 제공사에게 전송되는 범위: 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` 패턴을 먼저 점검하세요.
398
398
 
package/README.md CHANGED
@@ -201,7 +201,7 @@ Friction kills personal knowledge systems. If saving a thought takes three click
201
201
  |---|---|
202
202
  | `SessionStart` | "Where did I leave off?" reading `hot.md` / `session-state.md` |
203
203
  | `UserPromptSubmit` | "Do I already know this?" a BM25 lookup, top-3 inject |
204
- | `PreCompact` | "Did I close the session?" the checklist guard |
204
+ | `PreCompact` | "Did I close the session?" a checklist notice (advisory only; it does not block `/compact`) |
205
205
  | `PostToolUse` (Write/Edit/MultiEdit) | `git add` |
206
206
  | `Stop` | `git commit && git pull --no-rebase && git push` |
207
207
 
@@ -257,7 +257,7 @@ Nine commands cover the full capture → retrieval → consolidation cycle.
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) |
260
- | `hypo-personal-check.mjs` | `PreCompact` | Block compact on an unfinished session-close, an uncommitted or unpushed wiki, malformed `hot.md`, or lint blockers (bypass: `HYPO_SKIP_GATE=1`) |
260
+ | `hypo-personal-check.mjs` | `PreCompact` | Surfaces an unfinished session-close, an uncommitted or unpushed wiki, malformed `hot.md`, or lint blockers as a `systemMessage`; it never blocks `/compact`. Session close is still enforced elsewhere: the Stop hook (`hypo-auto-minimal-crystallize.mjs`) blocks on a substantial session with no verified close, and `--mark-session-closed` still refuses the marker on a red gate |
261
261
  | `hypo-session-end.mjs` | `SessionEnd` | Write a SessionEnd marker so SessionStart can detect `source=clear` recovery |
262
262
  | `hypo-session-record.mjs` | `Stop` | Record session metadata for the observability score and auto-resume signaling |
263
263
  | `hypo-auto-minimal-crystallize.mjs` | `Stop` | After the user signals wrap-up, blocks Stop on a substantial session with no verified close and hands back the `crystallize.mjs --mark-session-closed` command. With uncommitted changes or work still in flight it asks whether to close now instead |
@@ -384,15 +384,15 @@ Place a `hypo-config.md` at the wiki root to make it portable across machines wi
384
384
 
385
385
  `.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.
386
386
 
387
- > `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, re-run `hypomnema init` to repoint it or every wiki commit will fail. `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).
387
+ > `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).
388
388
 
389
389
  > 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.
390
390
 
391
391
  `.hyposcanignore` is a different file with a narrower job. `init` writes one at the root of every new vault, and it only excludes paths from catalog scans (`lint`, `stats`, `query`, `verify`, `doctor`). It is not a privacy boundary: a path matched by `.hyposcanignore` is still committed and still readable by hooks. To actually keep something out of injection and commits, use `.hypoignore`.
392
392
 
393
- > Gate bypass: `HYPO_SKIP_GATE=1` is honored by every gate that can stop you mid-session, not just one: `hypo-personal-check` (PreCompact), `hypo-compact-guard`, `hypo-close-guard`, `hypo-auto-minimal-crystallize`, and the ingest nudge in `hypo-web-fetch-ingest`. Set it for a trivial session you do not want recorded.
393
+ > Gate bypass: `HYPO_SKIP_GATE=1` is honored by every gate that can stop you mid-session: `hypo-compact-guard`, `hypo-close-guard`, `hypo-auto-minimal-crystallize`, and the ingest nudge in `hypo-web-fetch-ingest`. `hypo-personal-check` (PreCompact) also reads the flag, but since that hook never blocks `/compact` anymore, the flag only suppresses the incomplete-close `systemMessage` it would otherwise repeat. Set it for a trivial session you do not want recorded.
394
394
 
395
- > 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>` or `hypomnema proposal discard <id>`. There is no auto-apply.
395
+ > 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.
396
396
 
397
397
  > 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`.
398
398
 
package/commands/audit.md CHANGED
@@ -16,7 +16,7 @@ Definition: [[pages/observability/_index]].
16
16
 
17
17
  ## Step 1 — Run script
18
18
 
19
- 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 reinstall instead of guessing the cache layout.
19
+ 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.
20
20
 
21
21
  If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag.
22
22
 
@@ -39,4 +39,4 @@ A flat capture stores the file in the wiki as `extensions/<type>/hypo-ext-<name>
39
39
 
40
40
  ## Step 4: Commit the wiki, then sync elsewhere
41
41
 
42
- Show the script output verbatim. When something was captured, remind the user to commit and push the wiki, then run `hypomnema upgrade --apply` on their other machine to install the captured extension under its original name.
42
+ Show the script output verbatim. When something was captured, remind the user to commit and push the wiki, then run `hypomnema upgrade --apply` (or `/hypo:upgrade` on a plugin install) on their other machine to install the captured extension under its original name.
@@ -21,7 +21,7 @@ Before composing the payload (Step 2), run these four reflections and surface ea
21
21
 
22
22
  1. **Trivial-session check (#44)** — Was this session trivial (a single bug fix, a single-file edit, or Q&A with no durable artifact)? If so, recommend skipping session-close: *"이 세션은 trivial해 보입니다 — session-close를 건너뛸까요?"* and proceed only if the user wants a close. A trivial skip is a recommendation, **not** a bypass: it must not mark the session closed, must not run `--mark-session-closed`, and must not claim `/compact` can pass. Any real close still requires all 5 mandatory files.
23
23
  2. **ADR-candidate check (#41).** Did this session make an architectural or design decision (a new pattern, a tradeoff chosen, a convention established)? If yes, ask whether it warrants an ADR and, if so, capture that intent in the `sessionLog` entry you compose in Step 2. If nothing rose to ADR level, you may record `ADR 없음: <one-line reason>` in that same `sessionLog` entry, but gate it on #42's bar: the marker is machine-read and W8 excludes an entry carrying `ADR 없음` (with no ADR reference) from the design-history staleness check. Write it only when the session had **no design change at all**; a sub-ADR design shift takes #42a (append) instead, since the marker would suppress the W8 nudge it needs. **Never auto-write an ADR file.** Recording the decision (or its absence) in the session-log payload is the only action here. This check carries no `decisions/` directory precondition: run it whether or not that directory exists.
24
- 3. **design-history staleness check (#42).** Two branches, so a stale W8 never blocks a clean close: (a) if this session changed design decisions `projects/<name>/design-history.md` does not yet reflect (including sub-ADR background, tradeoff, or differentiation shifts), recommend appending now: the W8 lint warning flags this mechanically, and an active-project W8 hard-blocks at PreCompact, so append before you commit. **If the file does not exist yet and this session had a design change, recommend creating it now** with that change as the first entry; lint separately flags a missing-but-needed file as W14, a warning that never blocks. (b) only if the session made **no** design change does the `ADR 없음` marker (#41) exempt the entry from W8; do not touch design-history, and do not create the file just to satisfy this branch's check. `ADR 없음` means "no design change," a stricter bar than "no ADR-level decision." Never auto-write the file yourself in either branch: recommend it, and let the user decide.
24
+ 3. **design-history staleness check (#42).** Two branches, so a stale W8 never blocks a clean close: (a) if this session changed design decisions `projects/<name>/design-history.md` does not yet reflect (including sub-ADR background, tradeoff, or differentiation shifts), recommend appending now: the W8 lint warning flags this mechanically, and an active-project W8 still counts as a `--check-session-close` blocker even though the PreCompact hook no longer stops `/compact` on it, so append before you commit. **If the file does not exist yet and this session had a design change, recommend creating it now** with that change as the first entry; lint separately flags a missing-but-needed file as W14, a warning that never blocks. (b) only if the session made **no** design change does the `ADR 없음` marker (#41) exempt the entry from W8; do not touch design-history, and do not create the file just to satisfy this branch's check. `ADR 없음` means "no design change," a stricter bar than "no ADR-level decision." Never auto-write the file yourself in either branch: recommend it, and let the user decide.
25
25
  4. **Ingest check (#43)** — Did this session consume trustworthy external knowledge (a fetched URL, official docs, or code you verified directly)? If so, recommend running `/hypo:ingest` to capture it under `sources/`. Proceed only on the user's confirmation.
26
26
 
27
27
  These are judgment calls; when uncertain, surface the question rather than skip it. None of the four blocks the close or writes on its own.
@@ -78,7 +78,7 @@ Content guidance for each slot:
78
78
 
79
79
  ## Step 3 — Apply the payload
80
80
 
81
- 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 reinstall instead of guessing the cache layout.
81
+ 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.
82
82
 
83
83
  ```bash
84
84
  node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs \
@@ -179,7 +179,7 @@ If the user says stop, end here. Otherwise continue to Step 5.
179
179
 
180
180
  ## Step 5 — Surface synthesis candidates
181
181
 
182
- 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 reinstall instead of guessing the cache layout.
182
+ 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
183
 
184
184
  ```bash
185
185
  node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs [--hypo-dir="<path>"] [--min-group=2]
@@ -18,7 +18,7 @@ You are running `/hypo:doctor`. Verify the health of the current Hypomnema wiki
18
18
 
19
19
  ## Step 1 — Resolve Hypomnema directory
20
20
 
21
- 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 reinstall instead of guessing the cache layout.
21
+ 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.
22
22
 
23
23
  If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag and the script resolves the Hypomnema root automatically: `HYPO_DIR` env → `hypo-config.md` scan → `~/hypomnema` default.
24
24
 
@@ -52,6 +52,15 @@ For any `✗` failures:
52
52
  - Missing Hypomnema root or required directories/files → run `/hypo:init`.
53
53
  - 0 hook files installed → run `/hypo:init`.
54
54
  - 0 hook registrations in settings.json → run `/hypo:init`.
55
+ - `Hook execution smoke test` failed → show the stderr excerpt in the detail
56
+ verbatim; it names the cause. The repair depends on the channel, and
57
+ `/hypo:init` is only right on one of them. If `Hook files installed` says
58
+ "provided by the plugin loader", the install is plugin-managed and
59
+ `/hypo:init` deliberately leaves core hooks alone: tell the user to
60
+ re-install the plugin. Otherwise (npm or manual install) `/hypo:init`
61
+ repairs an incomplete copy. When the excerpt is not a missing module, have
62
+ the user run the hook directly in their shell
63
+ (`echo '{}' | node <the path in the detail>`) and read the error there.
55
64
 
56
65
  For `⚠` warnings:
57
66
  - Missing `hypo-config.md` → run `/hypo:init` — it creates the config marker.
@@ -59,6 +68,11 @@ For `⚠` warnings:
59
68
  - Partial hook files (some missing) → run `/hypo:init` to install missing hooks.
60
69
  - Partial settings.json registrations → run `/hypo:init` to merge missing entries.
61
70
  - Missing git remote → `git -C <hypo-dir> remote add origin <url>`.
71
+ - `Installed cache vs marketplace HEAD` says the install is behind → the plugin
72
+ version string did not move, so the update left the old cache in place. Wait
73
+ for the next version bump, or swap the cache directory by hand knowing the
74
+ next real update overwrites it. If it says *ahead* instead, that is a local
75
+ checkout running in front of the marketplace and there is nothing to do.
62
76
  - Broken `[[links]]` → list the affected files and ask if the user wants to fix them now.
63
77
  - Overdue `verify_by_date` → offer to open the affected pages for review.
64
78
  - Missing `verify_by` question → suggest adding a `verify_by` field to the listed pages.
@@ -36,7 +36,7 @@ If **claude-learned** is among the targets, the page must be `scope: global` + `
36
36
 
37
37
  ## Step 2 — List existing feedback (optional)
38
38
 
39
- 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 reinstall instead of guessing the cache layout.
39
+ 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.
40
40
 
41
41
  To check for an existing topic, run:
42
42
 
package/commands/graph.md CHANGED
@@ -14,7 +14,7 @@ You are running `/hypo:graph`. Generate a link dependency graph from wiki pages.
14
14
 
15
15
  ## Step 1 — Run script
16
16
 
17
- 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 reinstall instead of guessing the cache layout.
17
+ 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.
18
18
 
19
19
  If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag.
20
20
 
@@ -27,7 +27,7 @@ Do **not** fetch the URL or read the file yet — the privacy guard in Step 2 mu
27
27
 
28
28
  ## Step 2 — Privacy guard (`.hypoignore`)
29
29
 
30
- 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 reinstall instead of guessing the cache layout.
30
+ 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.
31
31
 
32
32
  Refuse to ingest secrets (`.env`, SSH keys, credentials) before they ever reach `sources/`. Run the guard for **both** the input path and the destination path:
33
33
 
package/commands/init.md CHANGED
@@ -43,7 +43,7 @@ Ask the following questions **one at a time**. Use the default if the user press
43
43
 
44
44
  ## Step 2a — From Remote (skip wizard) {#step-2a}
45
45
 
46
- 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 reinstall instead of guessing the cache layout.
46
+ 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.
47
47
 
48
48
  If the user provided `--from-remote <url>`, run:
49
49
 
@@ -64,7 +64,7 @@ node ${CLAUDE_PLUGIN_ROOT}/scripts/init.mjs \
64
64
 
65
65
  ## Step 2 — Run the init script (new wiki)
66
66
 
67
- 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 reinstall instead of guessing the cache layout.
67
+ 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.
68
68
  Then run:
69
69
 
70
70
  ```bash
package/commands/lint.md CHANGED
@@ -16,7 +16,7 @@ You are running `/hypo:lint`. Validate all wiki pages for frontmatter correctnes
16
16
 
17
17
  ## Step 1 — Run script
18
18
 
19
- 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 reinstall instead of guessing the cache layout.
19
+ 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.
20
20
 
21
21
  If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag and the script resolves the Hypomnema root automatically via `HYPO_DIR` → `hypo-config.md` scan → `~/hypomnema`.
22
22
 
package/commands/query.md CHANGED
@@ -20,7 +20,7 @@ Ask the user what they want to know if it was not provided in the command invoca
20
20
 
21
21
  ## Step 2 — Search
22
22
 
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 reinstall instead of guessing the cache layout.
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
25
  Run full-text search:
26
26
 
@@ -16,7 +16,7 @@ You are running `/hypo:rename`. Move a page or directory and content-aware rewri
16
16
 
17
17
  ## Step 1 — Run script
18
18
 
19
- 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 reinstall instead of guessing the cache layout.
19
+ 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.
20
20
 
21
21
  If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag.
22
22
 
@@ -20,7 +20,7 @@ If the user named a project in the command invocation, use that. Otherwise, run:
20
20
  node ${CLAUDE_PLUGIN_ROOT}/scripts/resume.mjs [--hypo-dir="<path>"] [--project=<name>]
21
21
  ```
22
22
 
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 reinstall instead of guessing the cache layout.
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
25
  When `--project` is omitted, the script prefers the project whose `working_dir` contains the current directory (cwd-first); if nothing under the current directory matches, it falls back to the most recently active project from `hot.md`.
26
26
 
package/commands/stats.md CHANGED
@@ -15,7 +15,7 @@ You are running `/hypo:stats`. Display a summary of wiki health and activity.
15
15
 
16
16
  ## Step 1 — Run script
17
17
 
18
- 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 reinstall instead of guessing the cache layout.
18
+ 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.
19
19
 
20
20
  If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag.
21
21
 
@@ -25,7 +25,7 @@ Say:
25
25
 
26
26
  Default: yes (dry-run first)
27
27
 
28
- 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 reinstall instead of guessing the cache layout.
28
+ 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.
29
29
 
30
30
  ---
31
31
 
@@ -14,7 +14,7 @@ You are running `/hypo:upgrade`. Check if the installed Hypomnema wiki is out of
14
14
 
15
15
  ## Step 1 — Run script
16
16
 
17
- 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 reinstall instead of guessing the cache layout.
17
+ 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.
18
18
 
19
19
  If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag.
20
20
 
@@ -28,7 +28,7 @@ node ${CLAUDE_PLUGIN_ROOT}/scripts/upgrade.mjs [--hypo-dir="<path>"]
28
28
 
29
29
  Show the output verbatim.
30
30
 
31
- > **Plugin installs**: if the output begins with `ℹ Plugin install detected`, the core hooks, slash commands, and `settings.json` wiring are managed by the Claude Code plugin loader — **not** by `/hypo:upgrade`. Do **not** run `--apply` expecting it to update them (it intentionally skips those to avoid double-registering every hook). To upgrade the plugin itself: `/plugin marketplace update hypomnema` then `/reload-plugins`. `--apply` in plugin mode applies vault-side migrations (SCHEMA, `.hypoignore`), refreshes package metadata, and still syncs any vault extensions — but does **not** install the core hooks/commands/settings (the plugin provides those).
31
+ > **Plugin installs**: if the output begins with `ℹ Plugin install detected`, the core hooks, slash commands, and `settings.json` wiring are managed by the Claude Code plugin loader — **not** by `/hypo:upgrade`. Do **not** run `--apply` expecting it to update them (it intentionally skips those to avoid double-registering every hook). To upgrade the plugin itself: `/plugin marketplace update hypomnema` then `/reload-plugins`. **Then run `--apply` anyway**: the vault's git pre-commit hook holds an absolute path baked in at init time, and reloading the plugin does not move it, so without that step every wiki commit keeps running the install you had when you first ran init. `--apply` in plugin mode applies vault-side migrations (SCHEMA, `.hypoignore`), refreshes package metadata, and still syncs any vault extensions — but does **not** install the core hooks/commands/settings (the plugin provides those).
32
32
 
33
33
  > **Note**: A major SCHEMA bump is only **detected** in this step. The informational `MIGRATION-vX.Y.md` file is written later by `--apply` (Step 4) and only on a major bump. `SCHEMA.md` is never auto-overwritten.
34
34
 
@@ -13,7 +13,7 @@ You are running `/hypo:verify`. Audit wiki pages for overdue or missing `verify_
13
13
 
14
14
  ## Step 1 — Run
15
15
 
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 reinstall instead of guessing the cache layout.
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
18
  ```bash
19
19
  node ${CLAUDE_PLUGIN_ROOT}/scripts/verify.mjs [--hypo-dir="<path>"] [--file=<path>]
@@ -117,7 +117,7 @@ Hooks run automatically at Claude Code lifecycle events. They are deployed to `~
117
117
  | `hypo-first-prompt` | Marker-based one-shot `hot.md` injection on first user prompt (10-min TTL) — for sessions that bypass `SessionStart` |
118
118
  | `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` |
119
119
  | `hypo-compact-guard` | Detect `/compact` invocations → enforce session-close checklist before allowing compact |
120
- | `hypo-personal-check` | PreCompact validation: lint blockers, uncommitted changes, missing session-log entries → block compact |
120
+ | `hypo-personal-check` | PreCompact detection: lint blockers, uncommitted changes, missing session-log entries surface as a `systemMessage`; `/compact` is never blocked here |
121
121
  | `hypo-auto-stage` | After Write/Edit on a wiki path, run `git add` (skips paths matching `.hypoignore`) |
122
122
  | `hypo-hot-rebuild` | At session stop, regenerate root `hot.md` from recent activity; emit growth metrics + cache for next SessionStart |
123
123
  | `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) |
@@ -218,7 +218,8 @@ Hooks inline this logic in `hypo-shared.mjs`. Scripts use `scripts/lib/hypo-root
218
218
  2. Creates the vault directory structure: `pages/`, `projects/`, `sources/`, `journal/{daily,weekly,monthly}/`.
219
219
  3. Copies `templates/` files: 6 root files (`hypo-config.md`, `index.md`, `hot.md`, `log.md`, `SCHEMA.md`, `hypo-guide.md`) + 4 helpers (`Home.md`, `Overview.md`, `hypo-automation.md`, `hypo-help.md`) + `pages/_index.md` + `projects/_template/`.
220
220
  4. Deploys 10 hooks to `~/.claude/hooks/` and merges entries into `~/.claude/settings.json` (idempotent; preserves non-hypo hooks).
221
- 5. Writes `~/.claude/hypo-pkg.json` with `pkgRoot` for upgrade tracking.
221
+ 5. Writes `~/.claude/hypo-pkg.json` with `pkgRoot` for upgrade tracking (skipped when the
222
+ plugin channel judgment fails; see `init.mjs`'s `resolveDurableRoot`).
222
223
  6. `git init` + first commit `init: hypomnema wiki`. Pushes to remote when one is provided.
223
224
 
224
225
  `--dry-run` previews; `--no-hooks` and `--no-git-init` are also supported. Re-running `init` is idempotent — existing files are skipped, never overwritten.
@@ -448,7 +449,11 @@ does not matter.
448
449
 
449
450
  ## CI / Release
450
451
 
451
- ### `ci.yml` — 7 independent jobs
452
+ ### `ci.yml` — independent jobs
453
+
454
+ The table below is not exhaustive and the count is deliberately left out: it went
455
+ stale once already. `.github/workflows/ci.yml` is the source of truth for which
456
+ jobs exist.
452
457
 
453
458
  | Job | Purpose |
454
459
  |---|---|
@@ -247,7 +247,7 @@ After both blocks, language-neutral:
247
247
 
248
248
  - **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).
249
249
  - **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.
250
- - Internal tracker ids (`FEAT-`, `IMPR-`, `ISSUE-`, `PRAC-`, `fix #N`) may appear in your local notes and in `tests/` / `qa-runs/` (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.
250
+ - 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.
251
251
 
252
252
  ### The `## Changelog` block
253
253
 
@@ -384,6 +384,7 @@ npm run check:bilingual # each gated CHANGELOG section has #### English + ###
384
384
  npm run smoke:plugin # plugin manifest + hooks/commands/skills load-valid
385
385
  npm run smoke-pack # packed tarball installs and resolves
386
386
  npm run check:tracker-ids # no private-tracker pointers leaked into shipped files
387
+ npm run format:check # every checked file matches Prettier's output
387
388
 
388
389
  # 4. Commit every file the bump touched, plus the lockfile.
389
390
  git add package.json package-lock.json .claude-plugin/ templates/hypo-config.md \
@@ -250,6 +250,204 @@ export function advanceBaseForWrite(hypoDir, sessionId, relPath, absPath, knownH
250
250
  }
251
251
  }
252
252
 
253
+ // ── observed set ───────────────────────────────────────────────────────────
254
+ //
255
+ // `targets` never moves except through the two invariants above, so a session
256
+ // that outlives its first snapshot by days sees every intervening legitimate
257
+ // write from OTHER sessions as drift and parks all four overwrite targets.
258
+ // The observed set is a second, additive record: what this session was
259
+ // actually SHOWN by a later SessionStart (resume/compact), kept separate from
260
+ // `targets` so it can only ever widen what a close may write, never narrow or
261
+ // replace the original base. `readBaseEntry`'s shape and `targets`' meaning
262
+ // are unchanged; a consumer that never calls the functions below sees
263
+ // identical behavior to before this section existed.
264
+ //
265
+ // Two guards keep the widening bounded to "what this session was just shown":
266
+ //
267
+ // - Generation. `observedGeneration` is a per-session counter, bumped once
268
+ // per SessionStart by `beginObservedGeneration` (BEFORE the first read of
269
+ // that SessionStart, so it covers everything that SessionStart injects).
270
+ // `recordObserved` stamps each entry with the CURRENT generation but never
271
+ // advances it — advancing on every record would put the two files a HIT
272
+ // SessionStart injects (hot.md, session-state.md) into different
273
+ // generations, since they are recorded one call apart, and the second call
274
+ // would expire the first. `readObservedHash` only returns a hash whose
275
+ // `generation` equals the CURRENT `observedGeneration`; a SessionStart
276
+ // that bumps the generation and then injects nothing (ignored, scoped out,
277
+ // absent, no session_id) leaves every existing entry one generation stale,
278
+ // so it reads back as null everywhere. This is what makes "only the most
279
+ // recent SessionStart's injection licenses a write" true without an
280
+ // explicit expiry pass: staleness falls out of the generation compare.
281
+ // - Tracked-key scoping. Exactly like `advanceBaseForWrite`, `recordObserved`
282
+ // is a no-op for a key that is not already in `targets` — it cannot mint a
283
+ // new guarded target, only add provenance to one that was already
284
+ // snapshotted for this session.
285
+ //
286
+ // One entry per path, not an array: a later observation of the SAME path
287
+ // simply overwrites the old `{generation, hash}` pair, so there is no
288
+ // unbounded growth to cap or dedup.
289
+
290
+ /**
291
+ * Bump this session's observed generation. Call once per SessionStart
292
+ * invocation, before the first `recordObserved` of that invocation — this is
293
+ * what makes an injection-free SessionStart (resume that hit no target, a
294
+ * scoped-out file, .hypoignore) expire every prior observation instead of
295
+ * leaving it licensed forever.
296
+ *
297
+ * No-op when the session has no snapshot yet: there is nothing to bump.
298
+ *
299
+ * @returns {boolean} true when base.json was updated
300
+ */
301
+ export function beginObservedGeneration(hypoDir, sessionId) {
302
+ if (!sessionId) return false;
303
+ const parsed = readBaseFile(hypoDir, sessionId);
304
+ if (!parsed) return false;
305
+ const current = typeof parsed.observedGeneration === 'number' ? parsed.observedGeneration : 0;
306
+ parsed.observedGeneration = current + 1;
307
+ try {
308
+ atomicWrite(basePath(hypoDir, sessionId), JSON.stringify(parsed, null, 2));
309
+ return true;
310
+ } catch {
311
+ return false;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Record that this session was just SHOWN `hash` for `relPath` (the exact
317
+ * bytes a SessionStart injection read, not a fresh disk re-read — the caller
318
+ * must pass the hash of the bytes it actually displayed).
319
+ *
320
+ * `truncated`: true when the injection sliced the file (2000-char HOT_CHARS /
321
+ * STATE_CHARS) before showing it, i.e. the caller only passed `hash` of a
322
+ * prefix's worth of trust even though `hash` itself is the FULL file's hash.
323
+ * A truncated observation is stored, not dropped, so a parked close can name
324
+ * the reason (`base-mismatch-truncated-observation`) instead of the plain
325
+ * `base-mismatch` a bare no-op would produce — but `readObservedHash` below
326
+ * refuses to hand it out as a licence: seeing 5% of a file is not seeing it.
327
+ *
328
+ * No-op, in order: no snapshot for this session; `relPath` is not one of the
329
+ * four tracked overwrite targets (mirrors `advanceBaseForWrite`'s scoping —
330
+ * this must not be able to mint a new guarded key). Stamped with the CURRENT
331
+ * `observedGeneration`, never advancing it: the caller advances once via
332
+ * `beginObservedGeneration`, not once per recorded target.
333
+ *
334
+ * @returns {boolean} true when base.json was updated
335
+ */
336
+ /**
337
+ * A safe integer, or null. base.json is on disk and another writer (an older
338
+ * release, a half-finished write, a hand edit) can leave any shape in it, so a
339
+ * generation counter is only trusted when it is exactly that: `NaN`, `Infinity`,
340
+ * `1.5` and `"1"` all read as absent rather than as a value to compare against.
341
+ */
342
+ function safeGeneration(v) {
343
+ return Number.isSafeInteger(v) ? v : null;
344
+ }
345
+
346
+ export function recordObserved(hypoDir, sessionId, relPath, hash, truncated = false) {
347
+ if (!sessionId) return false;
348
+ const parsed = readBaseFile(hypoDir, sessionId);
349
+ if (!parsed) return false;
350
+ if (!Object.prototype.hasOwnProperty.call(parsed.targets, relPath)) return false;
351
+ if (typeof hash !== 'string') return false;
352
+ const generation = typeof parsed.observedGeneration === 'number' ? parsed.observedGeneration : 0;
353
+ if (!parsed.observed || typeof parsed.observed !== 'object' || Array.isArray(parsed.observed)) {
354
+ parsed.observed = {};
355
+ }
356
+ parsed.observed[relPath] = { generation, hash, truncated: !!truncated };
357
+ try {
358
+ atomicWrite(basePath(hypoDir, sessionId), JSON.stringify(parsed, null, 2));
359
+ return true;
360
+ } catch {
361
+ return false;
362
+ }
363
+ }
364
+
365
+ /**
366
+ * The hash this session was shown for `relPath`, but ONLY when it was shown
367
+ * IN FULL during the CURRENT observed generation — the actual enforcement
368
+ * point of "only the most recent SessionStart's injection licenses a write".
369
+ * Returns null for: no snapshot, a session whose observed-generation counter
370
+ * was never created (see below), no observed entry, a malformed entry, a
371
+ * generation that does not match (stale — superseded by a later SessionStart,
372
+ * or never refreshed by one that injected nothing), or an entry the injection
373
+ * itself marked `truncated`.
374
+ *
375
+ * The `observedGeneration` check is deliberately ASYMMETRIC with how
376
+ * `recordObserved` reads the same field: that function normalizes a missing
377
+ * counter to `0` only to pick a generation to STAMP an entry with. Doing the
378
+ * same here — treating "no counter" as "generation 0" — would make a session
379
+ * whose `beginObservedGeneration` call never ran (never bumped past 0) match
380
+ * an entry `recordObserved` also stamped at 0, and the observed set would
381
+ * license writes despite the expiry mechanism that is supposed to gate it
382
+ * never having run at all. So here, "no counter" reads as "no current
383
+ * generation for anything to match" — null, not 0.
384
+ *
385
+ * @returns {string|null}
386
+ */
387
+ export function readObservedHash(hypoDir, sessionId, relPath) {
388
+ if (!sessionId) return null;
389
+ const parsed = readBaseFile(hypoDir, sessionId);
390
+ if (!parsed) return null;
391
+ const current = safeGeneration(parsed.observedGeneration);
392
+ if (current === null) return null;
393
+ const observed = parsed.observed;
394
+ if (!observed || typeof observed !== 'object' || Array.isArray(observed)) return null;
395
+ const entry = observed[relPath];
396
+ if (!entry || typeof entry !== 'object' || typeof entry.hash !== 'string' || !entry.hash) {
397
+ return null;
398
+ }
399
+ if (safeGeneration(entry.generation) !== current) return null;
400
+ // `truncated` is checked for a STRICT boolean, and any other shape refuses the
401
+ // licence rather than falling through to `=== true` being false. A corrupt
402
+ // `"true"` string used to pass that comparison and hand out a licence for a
403
+ // sliced observation — the one thing this field exists to deny. Corruption
404
+ // parks; it never widens.
405
+ if (entry.truncated !== false) return null;
406
+ return entry.hash;
407
+ }
408
+
409
+ /**
410
+ * Whether this session has a CURRENT-generation observed entry for `relPath`
411
+ * that exists but was marked `truncated` by `recordObserved` — the one bit
412
+ * `readObservedHash`'s null collapses away. Consulted only to pick a park
413
+ * reason (`base-mismatch-truncated-observation` vs plain `base-mismatch`),
414
+ * never to license a write; a caller must keep treating `readObservedHash`'s
415
+ * null as "no licence" regardless of what this returns.
416
+ *
417
+ * @returns {boolean}
418
+ */
419
+ export function wasObservedTruncated(hypoDir, sessionId, relPath) {
420
+ if (!sessionId) return false;
421
+ const parsed = readBaseFile(hypoDir, sessionId);
422
+ if (!parsed) return false;
423
+ const current = safeGeneration(parsed.observedGeneration);
424
+ if (current === null) return false;
425
+ const observed = parsed.observed;
426
+ if (!observed || typeof observed !== 'object' || Array.isArray(observed)) return false;
427
+ const entry = observed[relPath];
428
+ if (!entry || typeof entry !== 'object') return false;
429
+ if (safeGeneration(entry.generation) !== current) return false;
430
+ // Mirrors readObservedHash's strict check: anything that is not exactly `false`
431
+ // counts as truncated here. This only picks the park REASON (readObservedHash
432
+ // has already refused the licence), so erring toward "truncated" names a
433
+ // narrower cause than the generic mismatch and never unblocks a write.
434
+ return entry.truncated !== false;
435
+ }
436
+
437
+ // The four whole-file overwrite targets are prose-and-table markdown documents, and
438
+ // a base-mismatch on one of them always parks. Five predicates lived here that tried
439
+ // to skip the park when a payload "provably" lost nothing, and four rounds of review
440
+ // broke all five against the real vault. The last one accepted an insertion between a
441
+ // table's header and its separator, which keeps every byte and stops the table from
442
+ // being a table. They all failed the same way: a markdown document's meaning comes
443
+ // from block context that begins far above the line under judgement, and a hook that
444
+ // may use Node built-ins only is not the place to own a block parser.
445
+ //
446
+ // The pointer table this was built for should stop being a shared whole-file
447
+ // overwrite target and become a locally generated projection of the per-project files
448
+ // that already own those facts. Then two machines never contend over it, and nothing
449
+ // here needs to prove anything.
450
+
253
451
  /**
254
452
  * The four overwrite targets crystallize replaces wholesale. `project` may be
255
453
  * null when cwd resolves to no project; the two project-scoped paths are then