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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +30 -0
- package/README.ko.md +5 -5
- package/README.md +5 -5
- package/commands/crystallize.md +32 -6
- package/commands/graph.md +7 -4
- package/commands/lint.md +8 -1
- package/commands/query.md +6 -4
- package/commands/verify.md +12 -1
- package/docs/ARCHITECTURE.md +7 -7
- package/docs/CONTRIBUTING.md +2 -0
- package/hooks/hypo-compact-guard.mjs +126 -23
- package/hooks/hypo-cwd-change.mjs +20 -19
- package/hooks/hypo-first-prompt.mjs +31 -16
- package/hooks/hypo-lookup.mjs +10 -5
- package/hooks/hypo-session-start.mjs +5 -2
- package/hooks/hypo-shared.mjs +410 -83
- package/hooks/hypo-web-fetch-ingest.mjs +9 -13
- package/package.json +2 -1
- package/scripts/doctor.mjs +32 -3
- package/scripts/graph.mjs +22 -2
- package/scripts/lib/crystallize-args.mjs +38 -2
- package/scripts/lib/crystallize-close-apply.mjs +66 -5
- package/scripts/lint.mjs +166 -11
- package/scripts/query.mjs +22 -2
- package/scripts/upgrade.mjs +2 -2
- package/scripts/verify.mjs +22 -2
- package/templates/SCHEMA.md +23 -1
- package/templates/hypo-automation.md +4 -2
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +1 -1
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` | 마커
|
|
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`를 감지해
|
|
259
|
-
| `hypo-cwd-change.mjs` | `CwdChanged` | cwd에 맞는 프로젝트 `hot.md`
|
|
260
|
-
| `hypo-file-watch.mjs` | `FileChanged` | 위키
|
|
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 모델 제공사로 전송됩니다.
|
|
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
|
|
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
|
|
255
|
-
| `hypo-cwd-change.mjs` | `CwdChanged` |
|
|
256
|
-
| `hypo-file-watch.mjs` | `FileChanged` |
|
|
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.
|
|
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
|
|
package/commands/crystallize.md
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
|
279
|
+
node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs --check-session-close
|
|
259
280
|
```
|
|
260
281
|
|
|
261
|
-
|
|
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
|
|
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
|
package/commands/verify.md
CHANGED
|
@@ -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
|
|
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
|
---
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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` |
|
|
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`
|
|
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
|
|
140
|
-
| `hypo-file-watch` |
|
|
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(
|
|
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 (
|
|
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 (
|
|
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)
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -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
|
|
13
|
-
*
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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`);
|