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