hypomnema 1.8.1 → 1.8.2

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.
@@ -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.8.1",
14
+ "version": "1.8.2",
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.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "LLM-native personal wiki system — session-aware knowledge base for Claude Code",
5
5
  "author": {
6
6
  "name": "sk-lim19f",
package/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ All notable changes to Hypomnema are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.8.2] - 2026-09-10
9
+
10
+ ### Bug Fixes
11
+
12
+ #### English
13
+
14
+ - Hook notices reach the model again. They were emitted with a top-level `additionalContext`, which Claude Code silently ignores, so the close gate, the resume contract, and lookup results never arrived. Measured after the fix: a canary planted in a project's `hot.md` came back verbatim from a session with every file tool blocked, and the same canary read as absent on the previous build. ([#287](https://github.com/sk-lim19f/Hypomnema/pull/287))
15
+ - The session-close gate reads a background-task notification the same way on every delivery path, so a task finishing after you asked to close no longer cancels the close. A refusal also reports `gateReason`, naming which check refused. ([#289](https://github.com/sk-lim19f/Hypomnema/pull/289))
16
+ - `lint`, `verify`, `graph`, `query` and `crystallize` exit 2 on an unrecognised flag or an empty `--hypo-dir=`, instead of silently dropping it and falling back to the default vault. Their command docs no longer print placeholder syntax as runnable arguments. ([#286](https://github.com/sk-lim19f/Hypomnema/pull/286))
17
+ - The `/compact` and `/clear` notice no longer orders a session close, no longer fires on another project's uncommitted files, and no longer goes silent when the vault is broken enough to be worth reporting (`decisions/0101`). ([#285](https://github.com/sk-lim19f/Hypomnema/pull/285))
18
+ - `/hypo:doctor` no longer accepts a session-closed marker as proof for a project the close gate never evaluated. The marker records the set the gate actually checked, and the audit requires an artifact's project to be in that set as well as in the marker's attribution. ([#284](https://github.com/sk-lim19f/Hypomnema/pull/284))
19
+
20
+ #### 한국어
21
+
22
+ - 훅 알림이 다시 모델에 닿습니다. 최상단 `additionalContext`로 나가고 있었는데 Claude Code가 그것을 조용히 무시해서 close 게이트, 세션 재개 계약, lookup 결과가 도착한 적이 없었습니다. 고친 뒤 실측했습니다. 프로젝트 `hot.md`에 심은 난수를 파일 도구를 전부 막은 세션이 그대로 답했고, 같은 난수를 이전 빌드는 못 읽었습니다. ([#287](https://github.com/sk-lim19f/Hypomnema/pull/287))
23
+ - 세션 마무리 게이트가 background task 알림을 어느 전달 경로로 오든 같게 읽습니다. 마무리를 요청한 뒤 작업이 끝나도 그 마무리가 취소되지 않습니다. 거절할 때는 `gateReason`으로 어느 검사가 거절했는지도 함께 알립니다. ([#289](https://github.com/sk-lim19f/Hypomnema/pull/289))
24
+ - `lint`·`verify`·`graph`·`query`·`crystallize`가 못 알아보는 플래그와 빈 `--hypo-dir=`를 조용히 버리고 기본 볼트로 가는 대신 exit 2를 냅니다. 명령 문서도 placeholder 문법을 실행 가능한 인자처럼 찍지 않습니다. ([#286](https://github.com/sk-lim19f/Hypomnema/pull/286))
25
+ - `/compact`·`/clear` 알림이 세션 마무리를 지시하지 않고, 다른 프로젝트의 미커밋 파일로 뜨지 않으며, 보고할 값어치가 있을 만큼 볼트가 망가졌을 때 조용해지지 않습니다 (`decisions/0101`). ([#285](https://github.com/sk-lim19f/Hypomnema/pull/285))
26
+ - `/hypo:doctor`가 close 게이트가 한 번도 평가하지 않은 프로젝트에 대해 session-closed 마커를 증거로 받지 않습니다. 마커가 게이트의 실제 검사 집합을 기록하고, 감사가 산출물의 프로젝트를 귀속뿐 아니라 그 집합에서도 요구합니다. ([#284](https://github.com/sk-lim19f/Hypomnema/pull/284))
27
+
28
+ ### Chores
29
+
30
+ #### English
31
+
32
+ - The shipped `SCHEMA.md` template defines `updated` as the day content changed, not the day the file was written, and states plainly that nothing enforces it. ([#288](https://github.com/sk-lim19f/Hypomnema/pull/288))
33
+
34
+ #### 한국어
35
+
36
+ - 출하되는 `SCHEMA.md` 템플릿이 `updated`를 파일을 쓴 날이 아니라 내용이 바뀐 날로 정의하고, 그것을 집행하는 장치가 없다는 사실도 함께 적습니다. ([#288](https://github.com/sk-lim19f/Hypomnema/pull/288))
37
+
8
38
  ## [1.8.1] - 2026-09-07
9
39
 
10
40
  ### Bug Fixes
package/README.ko.md CHANGED
@@ -253,11 +253,11 @@ Hypomnema는 청크가 아니라 페이지를 지식 단위로 봅니다. 새
253
253
  |---|---|---|
254
254
  | `hypo-close-guard.mjs` | `PreToolUse` | Write/Edit/MultiEdit가 세션 마무리 쓰기로 보이는데 트랜스크립트에 사용자의 마무리 신호가 없으면, 파일이 바뀌기 전에 확인을 받음 |
255
255
  | `hypo-session-start.mjs` | `SessionStart` | `hot.md` / `session-state.md` 주입 + `git pull --ff-only` |
256
- | `hypo-first-prompt.mjs` | `UserPromptSubmit` | 마커 기반 일회성 `hot.md` 주입 (10분 TTL) |
256
+ | `hypo-first-prompt.mjs` | `UserPromptSubmit` | 마커 기반으로 첫 프롬프트에 재개 한 줄을 요구한다(10분 TTL). `hot.md`를 다시 읽지는 않는다 |
257
257
  | `hypo-lookup.mjs` | `UserPromptSubmit` | BM25 top-3 HIT 주입 / MISS면 가까운 슬러그 신호 |
258
- | `hypo-compact-guard.mjs` | `UserPromptSubmit` | 채팅에 입력된 `/compact`나 `/clear`를 감지해 컨텍스트가 지워지기 전에 session-close 체크리스트를 강제 |
259
- | `hypo-cwd-change.mjs` | `CwdChanged` | cwd에 맞는 프로젝트 `hot.md` 주입 |
260
- | `hypo-file-watch.mjs` | `FileChanged` | 위키 파일 변경 알림 (`.hypoignore` 준수. 매칭 경로는 LLM 컨텍스트로 다시 주입하지 않음) |
258
+ | `hypo-compact-guard.mjs` | `UserPromptSubmit` | 채팅에 입력된 `/compact`나 `/clear`를 감지해 마무리가 덜 됐으면 알린다. compact 자체는 막지 않는다 |
259
+ | `hypo-cwd-change.mjs` | `CwdChanged` | cwd에 맞는 프로젝트 `hot.md`로 컨텍스트를 만든다. `CwdChanged`에는 Claude Code가 모델로 전달하는 출력 필드가 없어 지금은 닿지 않는다 |
260
+ | `hypo-file-watch.mjs` | `FileChanged` | 바뀐 위키 파일로 컨텍스트를 만든다(`.hypoignore` 준수). 이 패키지는 감시 경로를 등록하지 않아 이벤트가 발생할 계기가 없고, `FileChanged` 출력은 어차피 모델에 안 닿는다 |
261
261
  | `hypo-auto-stage.mjs` | `PostToolUse(Write/Edit/MultiEdit)` | 위키 파일 자동 stage |
262
262
  | `hypo-auto-commit.mjs` | `Stop` | 자동 commit + pull + push |
263
263
  | `hypo-hot-rebuild.mjs` | `Stop` | 루트 `hot.md` 포인터 테이블 재생성 (구조와 날짜) |
@@ -396,7 +396,7 @@ E. 멈춘 프로젝트 재개.
396
396
 
397
397
  `HYPO_SKIP_GATE=1`은 세션 중간에 사용자를 막을 수 있는 게이트가 존중합니다. `hypo-compact-guard`, `hypo-close-guard`, `hypo-auto-minimal-crystallize`, 그리고 `hypo-web-fetch-ingest`의 ingest 안내가 여기 해당합니다. `hypo-personal-check`(PreCompact)도 이 플래그를 읽지만, 이 훅은 이제 `/compact`를 막지 않으므로 플래그는 반복될 뻔한 미완성-마무리 `systemMessage`만 억제합니다. 기록할 필요 없는 가벼운 세션에 씁니다.
398
398
 
399
- > 모델 제공사에게 전송되는 범위: Hypomnema 훅은 위키 본문을 Claude Code의 추가 컨텍스트(`additionalContext`)에 실어 보내고, 이 내용은 프롬프트의 일부로 Claude 모델 제공사로 전송됩니다. `.hypoignore`에 등록된 경로는 모든 주입 훅(`hypo-file-watch`, `hypo-session-start`, `hypo-cwd-change`, `hypo-lookup`)과 `ingest`에서 제외되지만, 등록하지 않은 파일은 전송 대상입니다. (`hypo-auto-stage`/`hypo-auto-commit`은 git 스테이징용 훅이라 컨텍스트를 주입하지는 않지만, 스테이징 판단에도 `.hypoignore`를 참고합니다.) 비밀 정보는 위키에 두지 마시고, `HYPO_DIR` 아래에 민감한 내용을 저장하기 전에 `.hypoignore` 패턴을 먼저 점검하세요.
399
+ > 모델 제공사에게 전송되는 범위: Hypomnema 훅은 위키 본문을 Claude Code의 추가 컨텍스트(`additionalContext`)에 실어 보내고, 이 내용은 프롬프트의 일부로 Claude 모델 제공사로 전송됩니다. 이 경로가 실제로 모델까지 닿는 것은 `hypo-session-start`와 `hypo-lookup`입니다. `hypo-cwd-change`와 `hypo-file-watch`는 `CwdChanged`와 `FileChanged` 이벤트에서 동작하는데, Claude Code가 이 두 이벤트에는 모델에 닿는 출력 필드를 두지 않아서, 두 훅이 만드는 `additionalContext`는 지금은 전송 전에 버려집니다. `.hypoignore`는 출력이 모델까지 닿는지와 무관하게 이 네 훅 모두와 `ingest`에서 적용됩니다. 등록하지 않은 파일은 모델에 닿는 두 훅이 그대로 전송합니다. (`hypo-auto-stage`/`hypo-auto-commit`은 git 스테이징용 훅이라 컨텍스트를 주입하지는 않지만, 스테이징 판단에도 `.hypoignore`를 참고합니다.) 비밀 정보는 위키에 두지 마시고, `HYPO_DIR` 아래에 민감한 내용을 저장하기 전에 `.hypoignore` 패턴을 먼저 점검하세요.
400
400
 
401
401
  > git sync 범위: Hypomnema는 `~/hypomnema/` 위키 자체만 git sync합니다. 단 `init` / `upgrade`는 `~/.claude/` 안의 관리 대상 영역(Hypomnema 자체 hook `~/.claude/hooks/`, 슬래시 커맨드 `~/.claude/commands/hypo/`, `settings.json` 등록)을 설치·SHA 추적하고, extensions companion sync로 위키의 `~/hypomnema/extensions/`에 둔 `agents/`·`commands/`·`hooks/`·`skills/`도 미러링합니다(직접 `init`·`upgrade --apply`·`capture`를 돌릴 때. `--codex`면 `hooks`·`commands` 부분집합만 `~/.codex/`로). 이 관리 대상 바깥에 있는 `~/.claude/` 콘텐츠는 일부러 관리하지 않습니다. 위키를 거치지 않는 기타 agent/skill, 머신 고유 `settings.local.json` 같은 일반 Claude Code 설정의 기기 간 동기화는 [chezmoi](https://www.chezmoi.io/) 같은 별도 dotfiles 매니저를 권합니다.
402
402
 
package/README.md CHANGED
@@ -249,11 +249,11 @@ Nine commands cover the full capture → retrieval → consolidation cycle.
249
249
  |---|---|---|
250
250
  | `hypo-close-guard.mjs` | `PreToolUse` | When a Write/Edit/MultiEdit looks like a session-close write and the transcript carries no close signal from you, ask before it lands |
251
251
  | `hypo-session-start.mjs` | `SessionStart` | Inject `hot.md` / `session-state.md` + `git pull --ff-only` |
252
- | `hypo-first-prompt.mjs` | `UserPromptSubmit` | Marker-based one-shot `hot.md` injection (10-min TTL) |
252
+ | `hypo-first-prompt.mjs` | `UserPromptSubmit` | Marker-based one-shot request for a resume line on the first prompt (10-min TTL). It does not re-read `hot.md` |
253
253
  | `hypo-lookup.mjs` | `UserPromptSubmit` | BM25 top-3 HIT inject / MISS → closest-slug signal |
254
- | `hypo-compact-guard.mjs` | `UserPromptSubmit` | Detect a typed `/compact` or `/clear`, and enforce the session-close checklist before context is wiped |
255
- | `hypo-cwd-change.mjs` | `CwdChanged` | Inject the matching project's `hot.md` |
256
- | `hypo-file-watch.mjs` | `FileChanged` | Notify on wiki-file changes (honors `.hypoignore`; matched paths are never re-emitted into LLM context) |
254
+ | `hypo-compact-guard.mjs` | `UserPromptSubmit` | Detect a typed `/compact` or `/clear` and report an incomplete session close. It never blocks the compact |
255
+ | `hypo-cwd-change.mjs` | `CwdChanged` | Build context from the matching project's `hot.md`. `CwdChanged` has no output field Claude Code forwards to the model, so this does not currently reach Claude |
256
+ | `hypo-file-watch.mjs` | `FileChanged` | Build context for a changed wiki file (honors `.hypoignore`). Nothing in this package registers watch paths, so the event has no trigger here, and `FileChanged` output does not reach Claude either way |
257
257
  | `hypo-auto-stage.mjs` | `PostToolUse(Write/Edit/MultiEdit)` | Auto-stage wiki-file edits |
258
258
  | `hypo-auto-commit.mjs` | `Stop` | Auto commit + pull + push |
259
259
  | `hypo-hot-rebuild.mjs` | `Stop` | Rebuild the root `hot.md` pointer table (structure + dates) |
@@ -396,7 +396,7 @@ Place a `hypo-config.md` at the wiki root to make it portable across machines wi
396
396
 
397
397
  > If a session-close write finds its target changed since this session read it, Hypomnema does not overwrite it. It parks the bytes under `<wiki>/.cache/proposals/` and tells you. Review and apply them yourself: `hypomnema proposal list`, then `hypomnema proposal apply <id>` (at a terminal) or `hypomnema proposal discard <id>`. Inside a session, where there is no terminal to type at, the same approval is collected in the conversation: `hypomnema proposal challenge` prints the diffs, you type the line it gives you, and `hypomnema proposal resolve` writes exactly what you saw. There is no auto-apply.
398
398
 
399
- > Provider transmission disclaimer: Hypomnema hooks emit wiki content into Claude Code's `additionalContext`, which is transmitted to the Claude model provider as part of the prompt. `.hypoignore` is enforced at every content-injection hook (`hypo-file-watch`, `hypo-session-start`, `hypo-cwd-change`, `hypo-lookup`) and at `ingest`, but any file _not_ matched by `.hypoignore` is fair game for transmission. (`hypo-auto-stage` and `hypo-auto-commit` are git-staging hooks, not injection points, and also honor `.hypoignore` for their staging decisions.) Keep secrets out of the wiki, and review `.hypoignore` patterns before storing anything sensitive under `HYPO_DIR`.
399
+ > Provider transmission disclaimer: Hypomnema hooks emit wiki content into Claude Code's `additionalContext`, which is transmitted to the Claude model provider as part of the prompt. That path actually reaches the model for `hypo-session-start` and `hypo-lookup`. `hypo-cwd-change` and `hypo-file-watch` fire on `CwdChanged` and `FileChanged`, two events for which Claude Code's hook output has no field that reaches the model, so the `additionalContext` those two hooks emit is currently dropped before transmission. `.hypoignore` is enforced at all four of these hooks (whether or not their output reaches the model) and at `ingest`, but any file _not_ matched by `.hypoignore` is fair game for transmission through the two hooks that do reach the model. (`hypo-auto-stage` and `hypo-auto-commit` are git-staging hooks, not injection points, and also honor `.hypoignore` for their staging decisions.) Keep secrets out of the wiki, and review `.hypoignore` patterns before storing anything sensitive under `HYPO_DIR`.
400
400
 
401
401
  > Scope of git sync: Hypomnema git-syncs only the `~/hypomnema/` wiki itself. `init` / `upgrade` do install and SHA-track a defined surface inside `~/.claude/`: Hypomnema's own hooks at `~/.claude/hooks/`, slash commands at `~/.claude/commands/hypo/`, and `settings.json` registrations. The extensions companion sync covers one more surface: whatever you put in `~/hypomnema/extensions/` (`agents/`, `commands/`, `hooks/`, `skills/`) lands in `~/.claude/`. With `--codex`, hooks and commands also go to `~/.codex/`. Anything _outside_ that defined surface in `~/.claude/` is intentionally not managed by Hypomnema. For general cross-machine sync of Claude Code config (other agents/skills not staged via the wiki, machine-specific `settings.local.json`, etc.), use a separate dotfiles manager such as [chezmoi](https://www.chezmoi.io/).
402
402
 
@@ -85,10 +85,12 @@ node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs \
85
85
  --apply-session-close \
86
86
  --payload=/tmp/hypo-session-close-<session-id>.json \
87
87
  --session-id=<current-session-id> \
88
- --hypo-dir="<path>" \
89
88
  --json
90
89
  ```
91
90
 
91
+ Add `--hypo-dir="<path>"` only when the user specified a Hypomnema directory explicitly;
92
+ otherwise omit it and the script resolves the root itself.
93
+
92
94
  **`--session-id` is required for any close that carries a `--payload`.** It is not a
93
95
  switch that turns a check on; omitting it fails the check. Before a single byte is
94
96
  written or committed, the apply resolves that session's transcript and looks for
@@ -99,7 +101,7 @@ and nothing on disk, in three cases:
99
101
  |---|---|
100
102
  | `session-id-required` | No `--session-id` was passed. |
101
103
  | `transcript-unresolved` | The id resolves to no transcript under `~/.claude/projects/`. |
102
- | `no-user-close-signal` | The transcript exists, and the user never asked to close. |
104
+ | `no-user-close-signal` | The transcript exists, but no close authority is in force right now. This one string collapses three different gate outcomes, and only one of them means the user never asked. Read `gateReason` (below) before deciding what to do about it. |
103
105
 
104
106
  A refusal is not a failure to route around. It means the close should not happen: ask
105
107
  the user, and re-run only after they say so.
@@ -129,6 +131,15 @@ no session id.
129
131
  | `--apply-session-close --payload=<path> --session-id=<id>` | The only apply path. Verifies close authority against that session's transcript **first**; on a refusal nothing is written. On success: per-field idempotent writes (no-op when bytes match), strict verification, lint gate, commit, and the per-session closed marker. Safe to re-run. |
130
132
  | `--apply-session-close --force` | Skips the probe early-exit. It does **not** skip the authority check, and `--payload` plus `--session-id` are still required to apply anything. |
131
133
 
134
+ When a refusal carries `reason: 'no-user-close-signal'`, the JSON also carries
135
+ `gateReason`, naming which of the three gate checks refused: `no-open` (the
136
+ transcript holds no user close signal at all), `transcript-rewrite-detected`
137
+ (the transcript changed under the gate), or `no-new-open-since-resolution` (the
138
+ close signal predates a resolution already recorded). The collapsed `reason`
139
+ stays the same string in all three cases, so read `gateReason` rather than
140
+ parsing the `Gate detail:` fragment out of `error`. The field is absent for
141
+ every other refusal.
142
+
132
143
  **Two lint gates run automatically, scoped to the files this close writes:**
133
144
 
134
145
  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).
@@ -169,7 +180,12 @@ If `markerWritten: true`: ask: "Session closed. Would you like to also run knowl
169
180
 
170
181
  - `session-id-required`: you omitted `--session-id`. Pass the main conversation's id and re-run.
171
182
  - `transcript-unresolved`: the id resolved no transcript, so it is almost certainly not the main conversation's (a background-task or Agent-thread uuid, most often). Get the right one and re-run.
172
- - `no-user-close-signal`: the transcript is this session's, and the user never asked to close in wording the gate recognizes (e.g. "세션 마무리까지 진행해줘" falls outside the close-signal set). Re-running the same id changes nothing, because the transcript is unchanged. Confirm intent once with `AskUserQuestion`, header "세션", a single option labelled **세션 마무리** (설명: "이 세션을 마무리하고 close 마커를 기록"). If the user picks it, that answer lands in the transcript as a recognized close signal, so re-running the exact same command now applies **everything**: the writes, the commit, and the marker. If the user declines, the session stays open and nothing is written. Do NOT touch the close-signal matcher itself, and do not hand-write the files to work around the refusal.
183
+ - `no-user-close-signal`: the transcript is this session's, but no close authority is in force. **Branch on `gateReason` before doing anything.** The three cases need three different responses, and treating them alike is what made this refusal look like it had a new cause every time it appeared.
184
+ - `no-open`: the user genuinely never asked to close in wording the gate recognizes (e.g. "세션 마무리까지 진행해줘" falls outside the close-signal set). Re-running the same id changes nothing, because the transcript is unchanged. Confirm intent once with `AskUserQuestion`, header "세션", a single option labelled **세션 마무리** (설명: "이 세션을 마무리하고 close 마커를 기록"). If the user picks it, that answer lands in the transcript as a recognized close signal, so re-running the exact same command now applies **everything**: the writes, the commit, and the marker. If the user declines, the session stays open and nothing is written.
185
+ - `no-new-open-since-resolution`: the user DID ask, and that request was already resolved by an earlier close. Asking again makes them answer a question they have already answered. Report that this session is already closed and stop; do not re-prompt.
186
+ - `transcript-rewrite-detected`: the transcript changed underneath the gate, so the earlier signal can no longer be attested. This is not a statement about what the user wants. Say what happened rather than asking them to repeat themselves, and let a human decide.
187
+
188
+ In all three: do NOT touch the close-signal matcher itself, and do not hand-write the files to work around the refusal.
173
189
 
174
190
  If the apply succeeded but `markerWritten: false`, do NOT say "session closed." Branch on `markerSkipReason` (`compact-gate-not-ok`, `commit-failed: …`, `marker-did-not-land`): surface the reason verbatim and address it (resolve the compact blocker, fix the git / disk issue) before re-running.
175
191
 
@@ -182,9 +198,14 @@ If the user says stop, end here. Otherwise continue to Step 5.
182
198
  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
199
 
184
200
  ```bash
185
- node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs [--hypo-dir="<path>"] [--min-group=2]
201
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs --min-group=2
186
202
  ```
187
203
 
204
+ Add `--hypo-dir="<path>"` only when the user specified a Hypomnema directory explicitly;
205
+ otherwise omit it.
206
+
207
+ An unrecognized flag exits 2 instead of being ignored.
208
+
188
209
  Show the output to the user. If no candidates are found, tell them Hypomnema looks well-connected and no crystallization is needed.
189
210
 
190
211
  ---
@@ -255,10 +276,15 @@ Show what was created or modified, and offer to run `/hypo:lint` to verify all n
255
276
  `--check-session-close` (read-only strict gate, same check PreCompact runs) is still supported as a probe-only verification. Use it when you only want to verify that today's session-close is complete without applying anything:
256
277
 
257
278
  ```bash
258
- node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs --check-session-close [--hypo-dir="<path>"]
279
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs --check-session-close
259
280
  ```
260
281
 
261
- It reports any file as `missing` or `stale`. For an actual close, prefer `--apply-session-close --payload=<path>` (Step 3) — it bundles freshness + lint into one gate and is the documented dogfood path. (`parseArgs` only accepts the `--payload=<path>` spelling; a space-separated `--payload <path>` is silently ignored and triggers "payload is required".)
282
+ Add `--hypo-dir="<path>"` only when the user specified a Hypomnema directory explicitly;
283
+ otherwise omit it.
284
+
285
+ An unrecognized flag exits 2 instead of being ignored.
286
+
287
+ It reports any file as `missing` or `stale`. For an actual close, prefer `--apply-session-close --payload=<path>` (Step 3): it bundles freshness and lint into one gate and is the documented dogfood path. `parseArgs` only accepts the `--payload=<path|->` spelling (a path, or `-` for stdin); a space-separated `--payload <path>` is rejected outright with exit 2, not silently dropped.
262
288
 
263
289
  Add `--project=<slug>` to scope the check to one project (close status + lint scope) when recency picks the wrong one. This is a project-scoped diagnostic only: a green scoped result (JSON `scope: "project"`) attests that slug is close-complete, **not** that `/compact` is unblocked globally.
264
290
 
package/commands/graph.md CHANGED
@@ -23,12 +23,15 @@ If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. O
23
23
  ## Step 2 — Run the graph script
24
24
 
25
25
  ```bash
26
- node ${CLAUDE_PLUGIN_ROOT}/scripts/graph.mjs \
27
- [--hypo-dir="<path>"] \
28
- [--format=json|mermaid|dot] \
29
- [--min-edges=<n>]
26
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/graph.mjs
30
27
  ```
31
28
 
29
+ Add `--hypo-dir="<path>"` only when the user gave one (per Step 1). Add `--format=<fmt>`
30
+ and `--min-edges=<n>` only with one concrete value from the Options list below, for
31
+ example `--format=mermaid`; the defaults (`json`, `0`) apply when they are omitted.
32
+
33
+ An unrecognized flag exits 2 instead of being ignored.
34
+
32
35
  Options:
33
36
  - `--format=json` (default) — adjacency list with in/out degree counts
34
37
  - `--format=mermaid` — Mermaid `graph TD` diagram (paste into a Markdown code block)
package/commands/lint.md CHANGED
@@ -25,10 +25,17 @@ If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. O
25
25
  ## Step 2 — Run lint
26
26
 
27
27
  ```bash
28
- node ${CLAUDE_PLUGIN_ROOT}/scripts/lint.mjs [--hypo-dir="<path>"] [--json] [--fix]
28
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/lint.mjs --json
29
29
  ```
30
30
 
31
+ Add `--hypo-dir="<path>"` only when the user gave one (per Step 1); otherwise leave it
32
+ out and the script resolves the root itself.
33
+
34
+ An unrecognized flag exits 2 instead of being ignored. Do not add `--fix` on this first
35
+ pass; Step 4 covers when to offer it and re-run with it only after the user agrees.
36
+
31
37
  Options:
38
+ - `--hypo-dir=<path>` (optional): Hypomnema root, pass only when the user specified one explicitly
32
39
  - `--json` — output results as JSON (useful for tooling)
33
40
  - `--fix` — auto-add missing `updated` field (safe repairs only; no other fields are modified)
34
41
 
package/commands/query.md CHANGED
@@ -22,15 +22,17 @@ Ask the user what they want to know if it was not provided in the command invoca
22
22
 
23
23
  Bundled scripts here run via `${CLAUDE_PLUGIN_ROOT}/scripts/`. To resolve that package root: if `${CLAUDE_PLUGIN_ROOT}` is already an absolute path, use it; otherwise read `pkgRoot` from `~/.claude/hypo-pkg.json` (only when non-empty and the target script exists under it); otherwise use the `hypo@hypomnema` (or legacy `hypomnema@hypomnema`) installPath in `~/.claude/plugins/installed_plugins.json`; if none resolve, stop and tell the user to run `hypomnema upgrade --apply` (or `/hypo:upgrade` on a plugin install) or reinstall instead of guessing the cache layout.
24
24
 
25
+ If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise
26
+ omit the flag.
27
+
25
28
  Run full-text search:
26
29
 
27
30
  ```bash
28
- node ${CLAUDE_PLUGIN_ROOT}/scripts/query.mjs \
29
- --q="<query terms>" \
30
- [--hypo-dir="<path>"] \
31
- [--limit=10]
31
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/query.mjs --q="<query terms>" --limit=10
32
32
  ```
33
33
 
34
+ An unrecognized flag exits 2 instead of being ignored.
35
+
34
36
  ---
35
37
 
36
38
  ## Step 3 — Read relevant pages
@@ -15,11 +15,22 @@ You are running `/hypo:verify`. Audit wiki pages for overdue or missing `verify_
15
15
 
16
16
  Bundled scripts here run via `${CLAUDE_PLUGIN_ROOT}/scripts/`. To resolve that package root: if `${CLAUDE_PLUGIN_ROOT}` is already an absolute path, use it; otherwise read `pkgRoot` from `~/.claude/hypo-pkg.json` (only when non-empty and the target script exists under it); otherwise use the `hypo@hypomnema` (or legacy `hypomnema@hypomnema`) installPath in `~/.claude/plugins/installed_plugins.json`; if none resolve, stop and tell the user to run `hypomnema upgrade --apply` (or `/hypo:upgrade` on a plugin install) or reinstall instead of guessing the cache layout.
17
17
 
18
+ If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise
19
+ omit the flag.
20
+
18
21
  ```bash
19
- node ${CLAUDE_PLUGIN_ROOT}/scripts/verify.mjs [--hypo-dir="<path>"] [--file=<path>]
22
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/verify.mjs
20
23
  ```
21
24
 
25
+ Add `--file="<path>"` only to check a single page (for example, right after editing it).
26
+ Omitting it scans the whole vault (`pages/` and `projects/`); the script skips a `--file`
27
+ path it cannot read rather than erroring, so passing one that does not exist reports
28
+ "nothing to verify" instead of failing loudly.
29
+
30
+ An unrecognized flag exits 2 instead of being ignored.
31
+
22
32
  Options:
33
+ - `--hypo-dir=<path>` (optional): Hypomnema root, pass only when the user specified one explicitly
23
34
  - `--file=<path>` — check a single page only (useful after editing a page)
24
35
 
25
36
  ---
@@ -128,16 +128,16 @@ Hooks run automatically at Claude Code lifecycle events. They are deployed to `~
128
128
  | Hook | Responsibility |
129
129
  |---|---|
130
130
  | `hypo-session-start` | Inject `index.md`, root `hot.md`, project `hot.md`/`session-state.md`. Run `git pull --ff-only` (silent fail on missing remote) |
131
- | `hypo-first-prompt` | Marker-based one-shot `hot.md` injection on first user prompt (10-min TTL) — for sessions that bypass `SessionStart` |
131
+ | `hypo-first-prompt` | Reads the marker left by `hypo-session-start` or `hypo-cwd-change` and forces a one-line resume on the first user prompt (10-min TTL). It does not re-read or re-inject `hot.md` |
132
132
  | `hypo-lookup` | BM25 search over the wiki on every prompt. **HIT** → inject top-3 page snippets (≤2000 chars each; a page whose `verify_by_date` is overdue gets a `[STALE verify_by_date=…]` marker prepended). **MISS** → emit closest-slug signal that prompts Claude to research + `/hypo:ingest` |
133
- | `hypo-compact-guard` | Detect `/compact` invocations → enforce session-close checklist before allowing compact |
133
+ | `hypo-compact-guard` | Detect `/compact` or `/clear` typed in chat and, if session close is incomplete, tell Claude so. It never blocks `/compact` |
134
134
  | `hypo-personal-check` | PreCompact detection: lint blockers, uncommitted changes, missing session-log entries surface as a `systemMessage`; `/compact` is never blocked here |
135
135
  | `hypo-auto-stage` | After Write/Edit on a wiki path, run `git add` (skips paths matching `.hypoignore`) |
136
136
  | `hypo-hot-rebuild` | At session stop, regenerate root `hot.md` from recent activity; emit growth metrics + cache for next SessionStart |
137
137
  | `hypo-session-record` | At session stop, append `{session_id, transcript_path, recorded_at, cwd, device}` to `.cache/sessions/index.jsonl` (primary source for the observability audit) |
138
138
  | `hypo-auto-commit` | At session stop, filter changed paths through `.hypoignore`, commit non-ignored changes, `git pull --no-rebase` + `git push` (silent fail on missing remote) |
139
- | `hypo-cwd-change` | When working directory changes, re-resolve the active project and inject its `hot.md` |
140
- | `hypo-file-watch` | Notify on external wiki edits so the in-session view stays consistent |
139
+ | `hypo-cwd-change` | When working directory changes, re-resolve the active project and build `additionalContext` with its `hot.md`; `CwdChanged` has no field Claude Code forwards to the model, so this does not currently reach Claude |
140
+ | `hypo-file-watch` | Build `additionalContext` for a changed wiki file. Two things keep this inert: nothing in this package returns `watchPaths`, so the event has no registered trigger, and `FileChanged` output does not reach the model. The event also fires regardless of who changed the file, so it is not an external-edit signal |
141
141
 
142
142
  ### Deployment constraint
143
143
 
@@ -163,7 +163,7 @@ Scripts in `scripts/` are not deployed — they run from the package install pat
163
163
  | `hypoIsClean()` | Check git status + unpushed commits |
164
164
  | `hotMdIsClean()` | Validate `hot.md` structure |
165
165
  | `isCompactCommand(prompt)` | Detect `/compact` invocations |
166
- | `buildOutput(...)` | Format hook output for Claude Code's `additionalContext` channel |
166
+ | `buildOutput(hookEventName, context, extra)` | Build hook output that nests `additionalContext` under `hookSpecificOutput`, keeping control fields as top-level siblings. The event name is required: Claude Code drops a payload whose `hookEventName` does not match the firing event |
167
167
  | `SESSION_STATE_NEXT_HEADINGS` | Allowed headings for "next tasks" — `## 다음 이어받기` / `## 다음 작업`. Lint reuses this constant (DRY) |
168
168
 
169
169
  ---
@@ -298,7 +298,7 @@ SessionStart
298
298
  ├─► UserPromptSubmit (every prompt)
299
299
  │ ├─► hypo-first-prompt.mjs (one-shot, 10min TTL)
300
300
  │ ├─► hypo-lookup.mjs (BM25 inject)
301
- │ └─► hypo-compact-guard.mjs (block /compact when checklist incomplete)
301
+ │ └─► hypo-compact-guard.mjs (report incomplete checklist; never blocks /compact)
302
302
  │
303
303
  ├─► PostToolUse(Write/Edit)
304
304
  │ └─► hypo-auto-stage.mjs (git add)
@@ -307,7 +307,7 @@ SessionStart
307
307
  │ └─► hypo-personal-check.mjs (lint + session-close gate)
308
308
  │
309
309
  ├─► CwdChanged
310
- │ └─► hypo-cwd-change.mjs (re-inject project hot.md)
310
+ │ └─► hypo-cwd-change.mjs (builds hot.md context; does not currently reach Claude)
311
311
  │
312
312
  └─► Stop
313
313
  ├─► hypo-hot-rebuild.mjs (regenerate root hot.md + growth cache)
@@ -129,6 +129,8 @@ Everything else belongs in `commands/`, which loads the same way.
129
129
  npm test # tests/*.test.mjs, sharded across processes — unit + smoke + contract
130
130
  npm run lint # scripts/lint.mjs — frontmatter + wikilink validation + W8 (design-history stale
131
131
  # vs session-log) + W14 (design-history missing but session-log implies one)
132
+ # + W15 (synthesis older than the newest page in its sources_consulted)
133
+ # + W16 (sources_consulted names W15 could not resolve)
132
134
  npm run fix:verify # Phase 1 of learned_behavior #6 — verifies fix #N status claims in
133
135
  # a wiki spec against `// @fix #N: <test-name>` anchors, read as a
134
136
  # union across every tests/*.mjs. Maintainer dogfood; needs a wiki at
@@ -9,20 +9,47 @@
9
9
  * PreCompact event at all, so this hook is the only chat-side gate that can
10
10
  * prompt session-close before a context wipe.
11
11
  *
12
- * Behavior: if session close is incomplete → instruct Claude to run session close
13
- * immediately before /compact or /clear.
12
+ * Behavior: if session close is incomplete, tell Claude so (a description, not
13
+ * an instruction — session-close-scope-boundary spec §5) before /compact or
14
+ * /clear runs.
15
+ *
16
+ * This hook never calls precompactGateStatus: its own hooks.json timeout is
17
+ * 10s and the gate's lint spawn alone budgets 30s, three times over. Instead
18
+ * it re-checks only the cheap axes (session log, git, hot.md) directly, and
19
+ * narrows the git axis with the SAME project-vs-foreign path rule the gate
20
+ * uses (isForeignProjectFile / classifyForeignOnlyDirty in hypo-shared.mjs), so
21
+ * a different project's dangling close file does not fire a false alarm here
22
+ * either. Unlike the gate, this hook never passes transcriptTouched into that
23
+ * rule: parsing the transcript is exactly the evidence spec §5 excludes to
24
+ * stay inside the 10s budget, so only the cheap path-prefix axis runs here.
14
25
  */
15
26
 
16
27
  import {
17
28
  lastSubstantialOpIsSession,
18
29
  hypoIsClean,
30
+ gitDirtyFiles,
19
31
  hotMdIsClean,
20
32
  readChecklist,
21
33
  isClearCommand,
22
34
  isCompactOrClearCommand,
23
35
  isGateSkipped,
36
+ resolveGateProjectOverride,
37
+ classifyForeignOnlyDirty,
38
+ buildOutput,
39
+ HYPO_DIR,
24
40
  } from './hypo-shared.mjs';
25
41
 
42
+ // A fixed slice of this hook's own 10s hooks.json budget (hooks.json:60),
43
+ // shared as ONE deadline across every git spawn hypoIsClean and
44
+ // gitDirtyFiles make below (up to four): each call re-checks the time left
45
+ // and skips its own spawn once the shared budget is spent, so no NEW spawn
46
+ // starts once this budget is gone. That bounds the git-spawn total, not the
47
+ // hook's wall-clock time end to end: measured 145ms on a normal run and
48
+ // 6273ms with a slow fsmonitor in the mix, both inside the 10s hooks.json
49
+ // timeout, but a spawnSync child that outlives a SIGTERM has no coded upper
50
+ // bound here.
51
+ const GIT_DEADLINE_BUDGET_MS = 6000;
52
+
26
53
  let input = '';
27
54
  process.stdin.setEncoding('utf-8');
28
55
  process.stdin.on('data', (chunk) => {
@@ -40,24 +67,98 @@ process.stdin.on('end', () => {
40
67
 
41
68
  const detected = isClearCommand(prompt) ? '/clear' : '/compact';
42
69
 
43
- const hasSession = lastSubstantialOpIsSession();
44
- const gitStatus = hypoIsClean();
45
- const hotStatus = hotMdIsClean();
70
+ // These two run on EVERY /compact or /clear, unlike resolveGateProjectOverride
71
+ // below (which only runs when gitStatus.uncommitted is already true). A
72
+ // throw here would reach the outermost catch on every single prompt, not
73
+ // just the git-dirty ones, so the exposure is wider than the resolveGate
74
+ // case: fail closed locally instead ("reason present"), never fail open
75
+ // ("looks clean"). A read failure on either file is not the same fact as
76
+ // that file being genuinely absent or well-formed, so the fallback reason
77
+ // says "unreadable", not "missing" or "invalid", to keep the two causes
78
+ // tellable apart from the additionalContext text alone.
79
+ // lastSubstantialOpIsSession() now reads a MISSING log.md (ENOENT) as
80
+ // `false`, and reads only via a single readFileSync call (no separate
81
+ // existsSync precheck), so there is no check-then-read window where the
82
+ // file is deleted between the two and falls back to the old fail-open
83
+ // `true`. That keeps state-table row 1 (spec §5, "session log entry
84
+ // missing") surfaced: a brand-new vault with an all-foreign dirty tree
85
+ // and a clean hot.md still reports the missing log instead of going
86
+ // fully silent. Any OTHER read failure (EISDIR, EACCES, ...) is a real
87
+ // problem, so the function rethrows it, and the try/catch below turns
88
+ // that into the same fail-closed `hasSession = false` plus a stderr line.
89
+ let hasSession;
90
+ try {
91
+ hasSession = lastSubstantialOpIsSession();
92
+ } catch (err) {
93
+ process.stderr.write(
94
+ `[hypo-compact-guard] error: lastSubstantialOpIsSession failed, treating as session log entry missing: ${err?.message ?? String(err)}\n`,
95
+ );
96
+ hasSession = false;
97
+ }
98
+ const deadline = { end: performance.now() + GIT_DEADLINE_BUDGET_MS };
99
+ const gitStatus = hypoIsClean(undefined, { deadline });
100
+ let hotStatus;
101
+ try {
102
+ hotStatus = hotMdIsClean();
103
+ } catch (err) {
104
+ process.stderr.write(
105
+ `[hypo-compact-guard] error: hotMdIsClean failed, treating as hot.md unreadable: ${err?.message ?? String(err)}\n`,
106
+ );
107
+ hotStatus = { clean: false, reason: `hot.md unreadable: ${err?.message ?? String(err)}` };
108
+ }
46
109
 
47
- // Block on uncommitted (real unsaved work); unpushed commits (ahead)
48
- // are a soft, auto-synced state and must not block /compact or /clear — mirrors
49
- // the precompactGateStatus demote so the chat-side gate stays consistent.
50
- if (hasSession && !gitStatus.uncommitted && hotStatus.clean) {
51
- console.log(JSON.stringify({ continue: true, suppressOutput: true }));
52
- return;
110
+ // Uncommitted (real unsaved work) blocks; unpushed commits (ahead) are a
111
+ // soft, auto-synced state and never reach `gitStatus.reason` here, since
112
+ // `uncommitted` is what gates it — mirrors the precompactGateStatus
113
+ // demote so the chat-side gate stays consistent.
114
+ let gitReason = gitStatus.uncommitted ? gitStatus.reason : '';
115
+ if (gitStatus.uncommitted) {
116
+ // resolveGateProjectOverride (session-close-scope-boundary spec §2):
117
+ // the same cwd-to-project resolution PreCompact and Stop already use.
118
+ // null just means "no project this cwd unambiguously owns" — that
119
+ // keeps the git axis judged globally, exactly like today. Scoped
120
+ // inside `uncommitted` on purpose: it only narrows the git notice, so
121
+ // a clean /compact has no reason to pay for a projects/ scan.
122
+ //
123
+ // Unlike classifyForeignOnlyDirty, this call is NOT contract-bound to
124
+ // stay silent: it walks through collectProjectWorkingDirs' own
125
+ // readdirSync, which sits outside that function's try/catch. Left
126
+ // uncaught here, that throw would escape past this hook's git-axis
127
+ // logic into the outermost catch and come back as a FULLY suppressed
128
+ // {suppressOutput:true} — silently dropping the session-log and
129
+ // hot.md reasons too, not just this one. Catch it locally and demote
130
+ // to null (its own "no project" sentinel) so a broken vault still
131
+ // gets every reason it is due.
132
+ let attributionScope = null;
133
+ try {
134
+ attributionScope = resolveGateProjectOverride(HYPO_DIR, {
135
+ sessionCwd: data.cwd ?? null,
136
+ });
137
+ } catch (err) {
138
+ process.stderr.write(
139
+ `[hypo-compact-guard] error: resolveGateProjectOverride failed, treating as no override: ${err?.message ?? String(err)}\n`,
140
+ );
141
+ }
142
+ if (attributionScope) {
143
+ const dirty = gitDirtyFiles(HYPO_DIR, { deadline });
144
+ const classification = classifyForeignOnlyDirty(HYPO_DIR, dirty, {
145
+ effectiveOverride: attributionScope,
146
+ });
147
+ if (classification === 'foreign-only') gitReason = '';
148
+ }
53
149
  }
54
150
 
55
151
  const reasons = [
56
152
  !hasSession ? 'session log entry missing' : '',
57
- gitStatus.uncommitted ? gitStatus.reason : '',
153
+ gitReason,
58
154
  !hotStatus.clean ? hotStatus.reason : '',
59
155
  ].filter(Boolean);
60
156
 
157
+ if (reasons.length === 0) {
158
+ console.log(JSON.stringify({ continue: true, suppressOutput: true }));
159
+ return;
160
+ }
161
+
61
162
  const today = new Date().toISOString().slice(0, 10);
62
163
  const checklist = readChecklist(today);
63
164
  const body = checklist
@@ -65,17 +166,19 @@ process.stdin.on('end', () => {
65
166
  : 'See hypo-guide.md for the session-close checklist.';
66
167
 
67
168
  console.log(
68
- JSON.stringify({
69
- continue: true,
70
- additionalContext: [
71
- `[WIKI_AUTOCLOSE] ${detected} detected — session close incomplete (${reasons.join(', ')}).`,
72
- `Do NOT wait for user input. Run wiki session close NOW, then retry ${detected}.`,
73
- ``,
74
- body,
75
- ``,
76
- `To bypass: set HYPO_SKIP_GATE=1`,
77
- ].join('\n'),
78
- }),
169
+ JSON.stringify(
170
+ buildOutput(
171
+ 'UserPromptSubmit',
172
+ [
173
+ `[WIKI_AUTOCLOSE] ${detected} detected: session close incomplete (${reasons.join(', ')}).`,
174
+ ``,
175
+ body,
176
+ ``,
177
+ `To bypass: set HYPO_SKIP_GATE=1`,
178
+ ].join('\n'),
179
+ { continue: true },
180
+ ),
181
+ ),
79
182
  );
80
183
  } catch (err) {
81
184
  // Fail-open: any parse/runtime error must not block the user's prompt.
@@ -10,7 +10,6 @@ import { readFileSync, writeFileSync, existsSync, realpathSync } from 'fs';
10
10
  import { join } from 'path';
11
11
  import {
12
12
  HYPO_DIR,
13
- buildOutput,
14
13
  loadHypoIgnore,
15
14
  isIgnored,
16
15
  sessionMarkerPath,
@@ -153,16 +152,17 @@ process.stdin.on('end', () => {
153
152
  // working_dir distinct from the vault, surface where wiki files live.
154
153
  const vaultOrientation = buildVaultOrientation(newCwd);
155
154
  const orientPrefix = vaultOrientation ? `${vaultOrientation}\n\n` : '';
155
+ // Built inline rather than through buildOutput(): CwdChanged has no
156
+ // documented context-injection path, so the nested hookSpecificOutput
157
+ // shape buildOutput() now emits would be wrong for this event. The
158
+ // follow-up that moves this hook to systemMessage removes these three
159
+ // literals; until then they keep today's behaviour unchanged.
156
160
  console.log(
157
- JSON.stringify(
158
- buildOutput(
159
- `${orientPrefix}[WIKI: cwd changed → project=${sanitizeProjForPrompt(newHit.proj)}]\n\n${content}`,
160
- {
161
- continue: true,
162
- suppressOutput: true,
163
- },
164
- ),
165
- ),
161
+ JSON.stringify({
162
+ continue: true,
163
+ suppressOutput: true,
164
+ additionalContext: `${orientPrefix}[WIKI: cwd changed → project=${sanitizeProjForPrompt(newHit.proj)}]\n\n${content}`,
165
+ }),
166
166
  );
167
167
  return;
168
168
  }
@@ -199,9 +199,11 @@ process.stdin.on('end', () => {
199
199
  if (!globalContent) {
200
200
  if (suggestPrefix) {
201
201
  console.log(
202
- JSON.stringify(
203
- buildOutput(suggestPrefix.trimEnd(), { continue: true, suppressOutput: true }),
204
- ),
202
+ JSON.stringify({
203
+ continue: true,
204
+ suppressOutput: true,
205
+ additionalContext: suggestPrefix.trimEnd(),
206
+ }),
205
207
  );
206
208
  } else {
207
209
  console.log(JSON.stringify({ continue: true, suppressOutput: true }));
@@ -209,12 +211,11 @@ process.stdin.on('end', () => {
209
211
  return;
210
212
  }
211
213
  console.log(
212
- JSON.stringify(
213
- buildOutput(
214
- `${suggestPrefix}[WIKI: cwd changed → no project match, injecting global hot]\n\n${globalContent}`,
215
- { continue: true, suppressOutput: true },
216
- ),
217
- ),
214
+ JSON.stringify({
215
+ continue: true,
216
+ suppressOutput: true,
217
+ additionalContext: `${suggestPrefix}[WIKI: cwd changed → no project match, injecting global hot]\n\n${globalContent}`,
218
+ }),
218
219
  );
219
220
  } catch (err) {
220
221
  process.stderr.write(`[hypo-cwd-change] error: ${err?.message ?? String(err)}\n`);