hypomnema 1.4.2 → 1.5.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 (68) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ko.md +1 -1
  4. package/README.md +1 -1
  5. package/commands/audit.md +1 -1
  6. package/commands/crystallize.md +8 -8
  7. package/commands/doctor.md +1 -1
  8. package/commands/feedback.md +2 -2
  9. package/commands/graph.md +1 -1
  10. package/commands/ingest.md +1 -1
  11. package/commands/init.md +2 -2
  12. package/commands/lint.md +1 -1
  13. package/commands/query.md +1 -1
  14. package/commands/rename.md +1 -1
  15. package/commands/resume.md +1 -1
  16. package/commands/stats.md +1 -1
  17. package/commands/uninstall.md +4 -2
  18. package/commands/upgrade.md +1 -1
  19. package/commands/verify.md +1 -1
  20. package/docs/ARCHITECTURE.md +2 -2
  21. package/docs/CONTRIBUTING.md +8 -7
  22. package/hooks/hypo-auto-commit.mjs +2 -2
  23. package/hooks/hypo-auto-minimal-crystallize.mjs +7 -7
  24. package/hooks/hypo-compact-guard.mjs +2 -2
  25. package/hooks/hypo-cwd-change.mjs +27 -37
  26. package/hooks/hypo-personal-check.mjs +37 -22
  27. package/hooks/hypo-session-end.mjs +1 -1
  28. package/hooks/hypo-session-record.mjs +2 -2
  29. package/hooks/hypo-session-start.mjs +34 -40
  30. package/hooks/hypo-shared.mjs +271 -71
  31. package/hooks/version-check.mjs +1 -1
  32. package/package.json +5 -1
  33. package/scripts/check-tracker-ids.mjs +69 -31
  34. package/scripts/crystallize.mjs +82 -38
  35. package/scripts/doctor.mjs +7 -7
  36. package/scripts/feedback-sync.mjs +5 -5
  37. package/scripts/feedback.mjs +9 -10
  38. package/scripts/init.mjs +7 -7
  39. package/scripts/lib/check-tracker-ids.mjs +90 -32
  40. package/scripts/lib/design-history-stale.mjs +1 -1
  41. package/scripts/lib/extensions.mjs +7 -7
  42. package/scripts/lib/failure-type.mjs +1 -1
  43. package/scripts/lib/feedback-scope.mjs +1 -1
  44. package/scripts/lib/project-create.mjs +2 -2
  45. package/scripts/lib/template-schema-version.mjs +1 -1
  46. package/scripts/lib/wd-match.mjs +181 -0
  47. package/scripts/lib/wikilink.mjs +1 -1
  48. package/scripts/lint.mjs +5 -5
  49. package/scripts/rename.mjs +1 -1
  50. package/scripts/resume.mjs +20 -22
  51. package/scripts/session-audit.mjs +1 -1
  52. package/scripts/stats.mjs +3 -3
  53. package/scripts/uninstall.mjs +3 -3
  54. package/scripts/upgrade.mjs +17 -18
  55. package/skills/crystallize/SKILL.md +7 -7
  56. package/skills/graph/SKILL.md +2 -2
  57. package/skills/ingest/SKILL.md +4 -4
  58. package/skills/lint/SKILL.md +2 -2
  59. package/skills/query/SKILL.md +2 -2
  60. package/skills/verify/SKILL.md +2 -2
  61. package/templates/SCHEMA.md +1 -1
  62. package/templates/hypo-config.md +1 -1
  63. package/templates/hypo-guide.md +20 -14
  64. package/templates/projects/_template/hot.md +1 -1
  65. package/scripts/fix-status-verify.mjs +0 -256
  66. package/scripts/lib/adr-corpus.mjs +0 -79
  67. package/scripts/lib/fix-manifest.mjs +0 -109
  68. package/scripts/lib/fix-status-verify.mjs +0 -439
@@ -11,7 +11,7 @@
11
11
  "name": "hypo",
12
12
  "source": "./",
13
13
  "description": "LLM-native personal wiki — session-aware knowledge base for Claude Code",
14
- "version": "1.4.2",
14
+ "version": "1.5.0",
15
15
  "homepage": "https://github.com/sk-lim19f/Hypomnema"
16
16
  }
17
17
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hypo",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "LLM-native personal wiki system — session-aware knowledge base for Claude Code",
5
5
  "author": {
6
6
  "name": "sk-lim19f",
package/README.ko.md CHANGED
@@ -25,7 +25,7 @@ Andrej Karpathy의 "LLM 네이티브 위키" 스케치에서 출발했습니다.
25
25
 
26
26
  ### 지금 어디까지 자동인가
27
27
 
28
- 현재 릴리스는 v1.4.2이고, 세션 마무리 경로를 견고하게 다듬었습니다: 마무리가 올바른 프로젝트에 기록되고, 미등록 태그나 수기 `log.md` 엔트리가 끝난 마무리를 더는 잘못 막지 않으며, 안내하는 복구 명령이 플러그인 설치에서도 그대로 실행됩니다. 위키 작업(자료 정리·검색·세션 마무리)은 아직 `/hypo:*` 명령어를 직접 입력해 시작합니다. v2의 목표는 Claude가 시키지 않아도 위키를 읽고 쓰고 합성하는 완전 자율 동작이고, 지금은 그쪽으로 가는 중입니다. v1.1.0이 위키가 한 세션에서 얼마나 쓰였는지 재는 관측성 점수를 넣었습니다. v1.2.0은 자동으로 동작하는 영역 네 가지를 추가했습니다.
28
+ 현재 릴리스는 v1.5.0이고, 머신 안정성과 일상의 마찰로 초점을 옮겼습니다: git 동기화로 여러 머신이 공유하는 볼트에서 `cwd`-first resume가 올바른 프로젝트를 찾고, 코드 레포에서 작업하는 세션이 매번 위키를 다시 찾는 대신 위키 위치를 안내받으며, 세션 마무리가 무관한 lint 부채를 다시 나열하거나 수백 개 경고를 컨텍스트에 쏟지 않습니다. 그 앞 v1.4.x는 세션 마무리 경로를 견고하게 다듬었습니다. 위키 작업(자료 정리·검색·세션 마무리)은 아직 `/hypo:*` 명령어를 직접 입력해 시작합니다. v2의 목표는 Claude가 시키지 않아도 위키를 읽고 쓰고 합성하는 완전 자율 동작이고, 지금은 그쪽으로 가는 중입니다. v1.1.0이 위키가 한 세션에서 얼마나 쓰였는지 재는 관측성 점수를 넣었습니다. v1.2.0은 자동으로 동작하는 영역 네 가지를 추가했습니다.
29
29
 
30
30
  - 피드백 한 곳만 고치면 나머지는 따라옵니다. `pages/feedback/`에 한 번 적으면 위키가 `MEMORY.md`와 `~/.claude/CLAUDE.md`의 `<learned_behaviors>` 블록을 자동으로 갱신합니다(단방향 projection).
31
31
  - 확장 파일도 함께 동기화. 위키 안 `~/hypomnema/extensions/{agents,commands,hooks,skills}/`에 둔 파일을 `~/.claude/`에 자동 반영합니다. `--codex`를 붙이면 `hooks`·`commands`는 `~/.codex/`에도 반영하지만, `agents`와 `skills`는 Claude 전용이라 건너뜁니다.
package/README.md CHANGED
@@ -25,7 +25,7 @@ New to the terms below? Keep the [Term decoder](#term-decoder) open in another t
25
25
 
26
26
  ### Where automation stands today
27
27
 
28
- The current release is v1.4.2, which hardened the session-close path: a close is attributed to the right project, an unregistered tag or a hand-written `log.md` entry no longer false-blocks it, and the recovery command it prints is runnable in a plugin install. Wiki work (ingest, query, session-close) still runs on explicit `/hypo:*` commands. The v2 goal is full autonomy: Claude reading, writing, and synthesizing the wiki without being asked, which is the direction this is heading. v1.1.0 shipped the observability score that measures how often the wiki is actually used per session. v1.2.0 added four autonomous lanes on top:
28
+ The current release is v1.5.0, which turns to cross-machine reliability and everyday friction: `cwd`-first resume now finds the right project on a git-synced vault shared across machines, a session working in a code repo is told where the wiki lives instead of re-discovering it each time, and session close stops re-listing unrelated lint debt or dumping hundreds of warnings into context. The v1.4.x line hardened the session-close path before it. Wiki work (ingest, query, session-close) still runs on explicit `/hypo:*` commands. The v2 goal is full autonomy: Claude reading, writing, and synthesizing the wiki without being asked, which is the direction this is heading. v1.1.0 shipped the observability score that measures how often the wiki is actually used per session. v1.2.0 added four autonomous lanes on top:
29
29
 
30
30
  - Edit feedback in one place and the rest follows. `pages/feedback/` is the single source of truth for behavior corrections, and Hypomnema derives `MEMORY.md` and the `<learned_behaviors>` block inside `~/.claude/CLAUDE.md` from it automatically.
31
31
  - Extensions sync alongside. Anything under `~/hypomnema/extensions/{agents,commands,hooks,skills}/` is mirrored into `~/.claude/` automatically. The `--codex` flag also mirrors `hooks` and `commands` into `~/.codex/`; `agents` and `skills` are Claude-only and skipped on purpose.
package/commands/audit.md CHANGED
@@ -16,7 +16,7 @@ Definition: [[pages/observability/_index]].
16
16
 
17
17
  ## Step 1 — Run script
18
18
 
19
- The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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
 
@@ -17,7 +17,7 @@ If `/hypo:crystallize` was invoked to close a session (via an explicit close sig
17
17
 
18
18
  ## Step 1a — Session-close advisory reflections
19
19
 
20
- Before composing the payload (Step 2), run these four reflections and surface each to the user. Every one is **advisory** (ADR 0029 identity guard) the user confirms or declines, and none performs an automatic action, writes a file on its own, or bypasses the mandatory gate.
20
+ Before composing the payload (Step 2), run these four reflections and surface each to the user. Every one is **advisory** (identity guard): the user confirms or declines, and none performs an automatic action, writes a file on its own, or bypasses the mandatory gate.
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.
@@ -30,9 +30,9 @@ These are judgment calls; when uncertain, surface the question rather than skip
30
30
 
31
31
  ## Step 2 — Compose the session-close payload
32
32
 
33
- The session-close path is **payload-driven**. Instead of writing the 5 mandatory files one-by-one, you compose a single JSON payload that describes the full session-close state, then hand it to `crystallize.mjs --apply-session-close`, which performs idempotent atomic writes and gates the result with lint.
33
+ The session-close path is **payload-driven**. Instead of writing the 5 mandatory files one-by-one, you compose a single JSON payload that describes the full session-close state, then hand it to the `--apply-session-close` apply path (the Step 3 command), which performs idempotent atomic writes and gates the result with lint.
34
34
 
35
- Payload shape (`project` + 5 content fields required, 1 conditional, per Spec §5.2.7 / §8.3 + ADR 0029):
35
+ Payload shape (`project` + 5 content fields required, 1 conditional, per Spec §5.2.7 / §8.3):
36
36
 
37
37
  ```json
38
38
  {
@@ -75,7 +75,7 @@ Content guidance for each slot:
75
75
 
76
76
  ## Step 3 — Apply the payload
77
77
 
78
- The script paths in this command resolve via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run them as written. If one appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from the cache layout.
78
+ 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.
79
79
 
80
80
  ```bash
81
81
  node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs \
@@ -114,12 +114,12 @@ synthesis (no session-close intent) — the marker is then simply not written.
114
114
 
115
115
  **Two lint gates run automatically, scoped to the files this close writes:**
116
116
 
117
- Both gates judge only the **payload files** (the 5 mandatory close files + `open-questions.md`). Lint debt in other projects or shared `pages/` this close did not author is reported as a non-blocking `notices[]` entry, never gated so an unrelated broken page elsewhere cannot block your close.
117
+ 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).
118
118
 
119
- 1. **Preflight** — `lint.mjs --json` runs **before** any payload bytes are written. Errors in overwrite targets (sessionState / projectHot / rootHot / openQuestions) are filtered (about to be replaced). Errors in an **append target** (session-log / log.md) still block (appending can't repair existing corruption) → exit 1 with `stage='preflight-lint'`. Errors outside the payload files → `notices[]`, apply proceeds.
119
+ 1. **Preflight** — an internal `lint --json` preflight runs **before** any payload bytes are written. Errors in overwrite targets (sessionState / projectHot / rootHot / openQuestions) are filtered (about to be replaced). Errors in an **append target** (session-log / log.md) still block (appending can't repair existing corruption) → exit 1 with `stage='preflight-lint'`. Errors outside the payload files → `notices[]`, apply proceeds.
120
120
  2. **Post-apply** — lint re-runs after the writes. Blocks only on **errors** in payload files (a payload-introduced malformed body / bad frontmatter); pre-existing errors elsewhere → `notices[]`. A lint crash (unparseable output) always blocks. Broken wikilinks are lint **warnings** (W4 — forward references to planned pages are normal) and are not gated here. Surfaces as `stage='post-apply-lint'` (or `'post-apply-verification+lint'` if freshness also fails).
121
121
 
122
- > **Manual close (direct Write tool calls)** clears the Stop-chain block via `--mark-session-closed --session-id=<id>`. Both marker writers apply a **user-close hard gate** (ADR 0055): the marker is written only when the session's transcript carries a genuine user close signal — an NL close phrase, a `/compact`, or an accepted AskUserQuestion [세션 마무리] answer. The transcript is resolved **strictly from `--session-id`** (a globally-unique id, globbed under `~/.claude/projects/`), never from a CLI arg, so a model that runs the writer on its own — without the user ever signalling close — is refused, and a forged path cannot point the gate at someone else's close-intent. The lint scope is widened from that same resolved transcript. `--transcript-path` is **not** consulted by the marker gate; it survives only to scope `--check-session-close`'s lint (which writes no marker).
122
+ > **Manual close (direct Write tool calls)** clears the Stop-chain block via `--mark-session-closed --session-id=<id>`. Both marker writers apply a **user-close hard gate**: the marker is written only when the session's transcript carries a genuine user close signal — an NL close phrase, a `/compact`, or an accepted AskUserQuestion [세션 마무리] answer. The transcript is resolved **strictly from `--session-id`** (a globally-unique id, globbed under `~/.claude/projects/`), never from a CLI arg, so a model that runs the writer on its own — without the user ever signalling close — is refused, and a forged path cannot point the gate at someone else's close-intent. The lint scope is widened from that same resolved transcript. `--transcript-path` is **not** consulted by the marker gate; it survives only to scope `--check-session-close`'s lint (which writes no marker).
123
123
 
124
124
  ---
125
125
 
@@ -154,7 +154,7 @@ If the user says stop, end here. Otherwise continue to Step 5.
154
154
 
155
155
  ## Step 5 — Surface synthesis candidates
156
156
 
157
- The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from the cache layout.
157
+ 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.
158
158
 
159
159
  ```bash
160
160
  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
- The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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
 
@@ -2,7 +2,7 @@
2
2
  description: Record an AI behavior correction or preference into the wiki. Use when the user corrects how you work or states a lasting preference to remember.
3
3
  ---
4
4
 
5
- You are running `/hypo:feedback`. Capture a behavior correction or preference into `pages/feedback/` — the **single source of truth** for learned behaviors (ADR 0031).
5
+ You are running `/hypo:feedback`. Capture a behavior correction or preference into `pages/feedback/` — the **single source of truth** for learned behaviors.
6
6
 
7
7
  ## What this does
8
8
 
@@ -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
- The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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
- The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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
- The script paths below resolve via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run them as written. If one appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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
- The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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
- The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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
- The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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
- The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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
- The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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
- The plugin harness expands `${CLAUDE_PLUGIN_ROOT}` to this package's absolute path before you see it, so run the command as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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
- The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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
 
@@ -20,13 +20,15 @@ Say:
20
20
 
21
21
  Default: yes (dry-run first)
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.
24
+
23
25
  ---
24
26
 
25
27
  ## Step 2 — Dry run
26
28
 
27
29
  Run:
28
30
  ```
29
- node scripts/uninstall.mjs
31
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/uninstall.mjs
30
32
  ```
31
33
 
32
34
  Show the output to the user. Ask:
@@ -39,7 +41,7 @@ If no → abort and confirm nothing was changed.
39
41
  ## Step 3 — Apply (if confirmed)
40
42
 
41
43
  ```
42
- node scripts/uninstall.mjs --apply
44
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/uninstall.mjs --apply
43
45
  ```
44
46
 
45
47
  If the user also wants Codex hooks removed, append `--codex`.
@@ -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
- The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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
 
@@ -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
- The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from 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 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>]
@@ -335,7 +335,7 @@ journal/weekly/<YYYY-Www>.md ← committed report (heuristic v0, s
335
335
 
336
336
  `session-audit.mjs` reads transcripts from two locations, in priority order:
337
337
 
338
- 1. **Primary:** `<hypo-root>/.cache/sessions/index.jsonl` — written by the Stop hook `hypo-session-record.mjs`. Each line: `{ session_id, transcript_path, recorded_at, cwd, device }`. `device` (PRAC-17) is `os.hostname()` for multi-machine audit; the index lives under `.cache/` (gitignored in a normal vault) so it is a local-only per-session record.
338
+ 1. **Primary:** `<hypo-root>/.cache/sessions/index.jsonl` — written by the Stop hook `hypo-session-record.mjs`. Each line: `{ session_id, transcript_path, recorded_at, cwd, device }`. `device` is `os.hostname()` for multi-machine audit; the index lives under `.cache/` (gitignored in a normal vault) so it is a local-only per-session record.
339
339
  2. **Fallback:** `~/.claude/projects/<encoded>/*.jsonl` — scanned when the index is missing or empty (legacy / freshly-installed wikis).
340
340
 
341
341
  ### Classification
@@ -366,7 +366,7 @@ The score is a **proxy, not ground truth**. The four-week baseline plan (capture
366
366
 
367
367
  The observability pipeline reads but never republishes raw transcripts. Weekly reports only emit `session_id` plus aggregate counts — no transcript content, no URLs, no tool inputs. Transcripts themselves live under `~/.claude/projects/` or `.cache/sessions/` which `.hypoignore` already excludes from any sync.
368
368
 
369
- **PRAC-17 audit fields (`device`, `session_id`).** `device` (`os.hostname()`) is recorded in two places with different sync boundaries, by design. In `.cache/sessions/index.jsonl` it is per-session-accurate but **local-only** (`.cache/` is gitignored in a normal vault). In the seeded session-log daily-shard frontmatter it is an **intentional synced field** — a creator-only stamp (the session/machine that first created the day's shard), carried across machines so the wiki itself records which machine opened each day. `session_id` (the Claude session UUID) is seeded into that same synced frontmatter, but only on the Stop-chain close path that passes `--session-id`; on manual/skill closes it is omitted. This deliberately puts a hostname (and, on the automated path, the session UUID) into git-tracked, synced content; it is acceptable for a single-user private vault where multi-machine identification is the goal. Operators who do not want a hostname synced can override `os.hostname()` at the OS level or treat the field as a pseudonymous label.
369
+ **Audit fields (`device`, `session_id`).** `device` (`os.hostname()`) is recorded in two places with different sync boundaries, by design. In `.cache/sessions/index.jsonl` it is per-session-accurate but **local-only** (`.cache/` is gitignored in a normal vault). In the seeded session-log daily-shard frontmatter it is an **intentional synced field** — a creator-only stamp (the session/machine that first created the day's shard), carried across machines so the wiki itself records which machine opened each day. `session_id` (the Claude session UUID) is seeded into that same synced frontmatter, but only on the Stop-chain close path that passes `--session-id`; on manual/skill closes it is omitted. This deliberately puts a hostname (and, on the automated path, the session UUID) into git-tracked, synced content; it is acceptable for a single-user private vault where multi-machine identification is the goal. Operators who do not want a hostname synced can override `os.hostname()` at the OS level or treat the field as a pseudonymous label.
370
370
 
371
371
  ### Growth metrics (Lane B)
372
372
 
@@ -124,7 +124,8 @@ npm run lint # scripts/lint.mjs — frontmatter + wikilink validation + W8
124
124
  npm run fix:verify # Phase 1 of learned_behavior #6 — verifies fix #N status claims in
125
125
  # a wiki spec against `// @fix #N: <test-name>` anchors in
126
126
  # tests/runner.mjs. Maintainer dogfood; needs a local wiki at
127
- # $HYPO_DIR or ~/hypomnema. Does NOT grep ADR core decision lines.
127
+ # $HYPO_DIR or ~/hypomnema (source checkout only; not shipped in
128
+ # the npm package). Does NOT grep ADR core decision lines.
128
129
  ```
129
130
 
130
131
  > **`fix:verify` needs an explicit `--spec`.** The default path
@@ -242,7 +243,7 @@ After both blocks, language-neutral:
242
243
 
243
244
  - **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).
244
245
  - **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.
245
- - Internal tracker ids (`FEAT-`, `IMPR-`, `ISSUE-`, `PRAC-`) may appear in your local notes and in source or workflow comments (an internal reference, like an ADR anchor), but 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`. `check-tracker-ids --tag` gates the tag body; the migration keeps the CHANGELOG body clean.
246
+ - Internal tracker ids (`FEAT-`, `IMPR-`, `ISSUE-`, `PRAC-`) 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 identifier that ships in those is the PR number `#N`. `check-tracker-ids` gates every surface (`--all`/`--staged` for files, `--commit-msg` for messages, `--tag` for the tag body); the migration keeps the CHANGELOG body clean.
246
247
 
247
248
  ### The `## Changelog` block
248
249
 
@@ -272,9 +273,9 @@ At and after the v1.2.0 cutoff, each non-empty gated section (`New Features`, `B
272
273
  ```
273
274
  ### New Features
274
275
  #### English
275
- - Feedback pages accept an optional `failure_type`. (#141)
276
+ - Feedback pages accept an optional `failure_type`. ([#141](https://github.com/sk-lim19f/Hypomnema/pull/141))
276
277
  #### 한국어
277
- - 피드백 페이지가 선택적 `failure_type`를 받습니다. (#141)
278
+ - 피드백 페이지가 선택적 `failure_type`를 받습니다. ([#141](https://github.com/sk-lim19f/Hypomnema/pull/141))
278
279
  ```
279
280
 
280
281
  Versions below the v1.2.0 cutoff (1.0.0, 1.0.1, 1.1.0) are English-only: the English body sits directly under the section header with no `####` sub-blocks. Korean is not back-filled for those releases.
@@ -283,12 +284,12 @@ The `### Changelog` index is language-neutral, one merged PR per line:
283
284
 
284
285
  ```
285
286
  ### Changelog
286
- - #141 feedback failure_type classification
287
- - #140 invalid-YAML lint guard
287
+ - [#141](https://github.com/sk-lim19f/Hypomnema/pull/141) feedback failure_type classification
288
+ - [#140](https://github.com/sk-lim19f/Hypomnema/pull/140) invalid-YAML lint guard
288
289
  Contributors: @handle
289
290
  ```
290
291
 
291
- Each line is `- #N <short title>`, no em dash. The `Contributors:` line lists that version's PR authors, de-duplicated; the release collector fills the handles from the GitHub API.
292
+ Each line is `- [#N](<pr-url>) <short title>`, no em dash: the collector links each `#N` to its GitHub PR (a repo markdown file does not autolink a bare `#N`), deriving the URL from the `repository` field in `package.json`. The `Contributors:` line lists that version's PR authors, de-duplicated; the release collector fills the handles from the GitHub API.
292
293
 
293
294
  A `### Known Issues` or `### Notes` block, when a version has one, is a trailing note (a caveat, not a change). It is not one of the gated sections, so it carries no `####` sub-blocks even at or after the cutoff; bilingual text there is recommended, not enforced.
294
295
 
@@ -14,7 +14,7 @@ function hasRemote() {
14
14
  }
15
15
 
16
16
  // Stage + commit via the shared helper (same .hypoignore filter the apply path
17
- // uses — ADR 0056). A real commit failure short-circuits before sync, exactly as
17
+ // uses). A real commit failure short-circuits before sync, exactly as
18
18
  // the inline logic did; "nothing to commit" is success and falls through to sync.
19
19
  const result = commitWikiChanges(HYPO_DIR);
20
20
  if (!result.committed) {
@@ -26,7 +26,7 @@ if (hasRemote()) {
26
26
  // pull/push failures must not stop the session, but they can no longer be
27
27
  // swallowed silently — syncRemote records each to .cache/sync-state.json and,
28
28
  // on a merge conflict, aborts the merge so the tree is never left half-merged
29
- // (FEAT-17 hardening). session-start + doctor surface the result next session.
29
+ // (part of the v1.4 sync hardening). session-start + doctor surface the result next session.
30
30
  syncRemote(HYPO_DIR);
31
31
  }
32
32
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * hypo-auto-minimal-crystallize.mjs — Stop hook (ADR 0022 Layer 3)
3
+ * hypo-auto-minimal-crystallize.mjs — Stop hook (Layer 3)
4
4
  *
5
5
  * Last hook in the Stop chain: a final-line defense that blocks `Stop` when
6
6
  * the current session did substantial work (mutation, or a high-volume
@@ -8,7 +8,7 @@
8
8
  * session-close. Forces Claude to run minimal session-close before the
9
9
  * conversation context evaporates.
10
10
  *
11
- * Decision flow (see ADR 0022 amendment 2026-05-19 Q1+Q2 + 2nd amendment Q-close-gate):
11
+ * Decision flow (see amendment 2026-05-19 Q1+Q2 + 2nd amendment Q-close-gate):
12
12
  *
13
13
  * 1. stop_hook_active === true → continue (loop guard; PoC 2026-05-14)
14
14
  * 2. wiki absent → continue (fail-open)
@@ -24,7 +24,7 @@
24
24
  * Close-intent gate (added after PR-C dogfooding revealed every-turn block —
25
25
  * codex 2-worker debate 2026-05-19, both REQUEST_CHANGES). Stop fires after
26
26
  * EVERY assistant turn, not at session end; blocking on "mutation + no marker"
27
- * alone nags the user on every turn of a long mutating session. ADR 0022's
27
+ * alone nags the user on every turn of a long mutating session. The hook's
28
28
  * real intent is "block when the session is ENDING and close is incomplete".
29
29
  * We approximate the end signal by reusing isClosePattern() over recent
30
30
  * user-message text (the same low-false-positive signal PreCompact uses):
@@ -82,11 +82,11 @@ function emitBlock(sessionId, transcriptPath, gate = null) {
82
82
  // The log-only escape hatch for a non-project (wiki/tooling-only)
83
83
  // session. Offered ONLY as an explicit alternative when a close blocker is
84
84
  // present — never as the default recovery, so a real project session is not
85
- // taught to bypass the ADR 0043 close invariant (codex design Finding 3).
85
+ // taught to bypass the close invariant (codex design Finding 3).
86
86
  const logOnlyCmd = cliBase
87
87
  ? `${cliBase} --mark-session-closed --log-only --session-id=${sessionId}${transcriptHint}`
88
88
  : `/hypo:crystallize --log-only (session_id=${sessionId}${transcriptHint})`;
89
- // ADR 0047: refine the message with the read-only /compact gate result.
89
+ // Refine the message with the read-only /compact gate result.
90
90
  // - gate green → the close is compact-ready and ONLY the marker is missing
91
91
  // (the hand-edit close case: files Written + committed directly, bypassing
92
92
  // the marker writer). Say so precisely + give the one command, instead of
@@ -112,7 +112,7 @@ function emitBlock(sessionId, transcriptPath, gate = null) {
112
112
  JSON.stringify({
113
113
  decision: 'block',
114
114
  reason,
115
- stopReason: 'session-close incomplete (ADR 0022 Layer 3)',
115
+ stopReason: 'session-close incomplete (Layer 3)',
116
116
  }),
117
117
  );
118
118
  }
@@ -186,7 +186,7 @@ process.stdin.on('end', () => {
186
186
  return;
187
187
  }
188
188
 
189
- // ADR 0047: read-only /compact gate (same precompactGateStatus the real
189
+ // Read-only /compact gate (same precompactGateStatus the real
190
190
  // PreCompact hook uses) sharpens the block message — distinguishes "close
191
191
  // is compact-ready, only the marker is missing" from "there are real
192
192
  // blockers". The hook NEVER writes the marker here (file-header invariant);
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * hypo-compact-guard.mjs — UserPromptSubmit hook
4
4
  *
5
- * Scope: detects "/compact" or "/clear" typed in chat only (ADR 0022 Layer 2).
5
+ * Scope: detects "/compact" or "/clear" typed in chat only (Layer 2).
6
6
  * The CLI built-in /compact does NOT fire UserPromptSubmit — use personal-wiki-check.mjs
7
7
  * (PreCompact hook) as the hard gate for that path. /clear has no PreCompact event, so
8
8
  * this hook is the only chat-side gate that can prompt session-close before context wipe.
@@ -42,7 +42,7 @@ process.stdin.on('end', () => {
42
42
  const gitStatus = hypoIsClean();
43
43
  const hotStatus = hotMdIsClean();
44
44
 
45
- // ADR 0056: block on uncommitted (real unsaved work); unpushed commits (ahead)
45
+ // Block on uncommitted (real unsaved work); unpushed commits (ahead)
46
46
  // are a soft, auto-synced state and must not block /compact or /clear — mirrors
47
47
  // the precompactGateStatus demote so the chat-side gate stays consistent.
48
48
  if (hasSession && !gitStatus.uncommitted && hotStatus.clean) {
@@ -6,8 +6,7 @@
6
6
  * project hot.md. Skips if still within the same project subtree.
7
7
  */
8
8
 
9
- import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs';
10
- import { homedir } from 'os';
9
+ import { readFileSync, writeFileSync, existsSync, realpathSync } from 'fs';
11
10
  import { join } from 'path';
12
11
  import {
13
12
  HYPO_DIR,
@@ -19,6 +18,9 @@ import {
19
18
  buildProjectSuggestionLine,
20
19
  recordSuggestionCooldown,
21
20
  sanitizeProjForPrompt,
21
+ pickProjectByCwd,
22
+ collectProjectWorkingDirs,
23
+ buildVaultOrientation,
22
24
  } from './hypo-shared.mjs';
23
25
 
24
26
  const PROJECTS_DIR = join(HYPO_DIR, 'projects');
@@ -33,42 +35,26 @@ function readIfNotIgnored(path, patterns) {
33
35
  return readFileSync(path, 'utf-8').slice(0, MAX_CHARS);
34
36
  }
35
37
 
36
- function parseFrontmatterField(content, key) {
37
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
38
- if (!match) return null;
39
- const line = match[1].split('\n').find((l) => l.startsWith(`${key}:`));
40
- if (!line) return null;
41
- return line
42
- .slice(key.length + 1)
43
- .trim()
44
- .replace(/^['"]|['"]$/g, '');
45
- }
46
-
47
38
  function findProjectHot(cwd) {
48
39
  if (!existsSync(PROJECTS_DIR)) return null;
49
- for (const proj of readdirSync(PROJECTS_DIR)) {
50
- const projDir = join(PROJECTS_DIR, proj);
51
- if (!statSync(projDir).isDirectory()) continue;
52
- const indexPath = join(projDir, 'index.md');
53
- if (!existsSync(indexPath)) continue;
54
- const content = readFileSync(indexPath, 'utf-8');
55
- const workingDir = parseFrontmatterField(content, 'working_dir');
56
- if (!workingDir) continue;
57
- const resolved = workingDir.startsWith('~/')
58
- ? join(homedir(), workingDir.slice(2))
59
- : workingDir;
60
- if (cwd === resolved || cwd.startsWith(resolved + '/')) {
61
- const hotPath = join(projDir, 'hot.md');
62
- const statePath = join(projDir, 'session-state.md');
63
- return {
64
- proj,
65
- hotPath: existsSync(hotPath) ? hotPath : null,
66
- statePath: existsSync(statePath) ? statePath : null,
67
- resolved,
68
- };
69
- }
40
+ let realpathCwd = null;
41
+ try {
42
+ realpathCwd = realpathSync(cwd);
43
+ } catch {
44
+ realpathCwd = null;
70
45
  }
71
- return null;
46
+ // Two-tier match (absolute prefix, then cross-machine unique basename) so a
47
+ // vault synced from another machine still resolves the cwd to its project.
48
+ const proj = pickProjectByCwd(collectProjectWorkingDirs(HYPO_DIR), cwd, { realpathCwd });
49
+ if (!proj) return null;
50
+ const projDir = join(PROJECTS_DIR, proj);
51
+ const hotPath = join(projDir, 'hot.md');
52
+ const statePath = join(projDir, 'session-state.md');
53
+ return {
54
+ proj,
55
+ hotPath: existsSync(hotPath) ? hotPath : null,
56
+ statePath: existsSync(statePath) ? statePath : null,
57
+ };
72
58
  }
73
59
 
74
60
  let raw = '';
@@ -123,10 +109,14 @@ process.stdin.on('end', () => {
123
109
  );
124
110
  }
125
111
  }
112
+ // Same vault orientation as session-start: when the new cwd is a project
113
+ // working_dir distinct from the vault, surface where wiki files live.
114
+ const vaultOrientation = buildVaultOrientation(newCwd);
115
+ const orientPrefix = vaultOrientation ? `${vaultOrientation}\n\n` : '';
126
116
  console.log(
127
117
  JSON.stringify(
128
118
  buildOutput(
129
- `[WIKI: cwd changed → project=${sanitizeProjForPrompt(newHit.proj)}]\n\n${content}`,
119
+ `${orientPrefix}[WIKI: cwd changed → project=${sanitizeProjForPrompt(newHit.proj)}]\n\n${content}`,
130
120
  {
131
121
  continue: true,
132
122
  suppressOutput: true,
@@ -137,7 +127,7 @@ process.stdin.on('end', () => {
137
127
  return;
138
128
  }
139
129
 
140
- // MISS: cwd matches no project. ADR 0023 — offer to create one
130
+ // MISS: cwd matches no project. Offer to create one
141
131
  // when the trigger conditions hold. Same nudge-only model as session-start.
142
132
  let suggestPrefix = '';
143
133
  if (shouldSuggestProjectCreation(newCwd, HYPO_DIR)) {