hypomnema 1.8.2 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +141 -51
- package/README.ko.md +18 -17
- package/README.md +16 -15
- package/commands/crystallize.md +89 -24
- package/commands/doctor.md +3 -1
- package/docs/ARCHITECTURE.md +45 -14
- package/docs/CONTRIBUTING.md +26 -8
- package/hooks/atomic-write.mjs +44 -0
- package/hooks/base-store.mjs +409 -90
- package/hooks/close-gate-store.mjs +131 -16
- package/hooks/close-journal.mjs +250 -0
- package/hooks/close-receipt.mjs +559 -0
- package/hooks/hooks.json +3 -38
- package/hooks/hypo-auto-commit.mjs +80 -10
- package/hooks/hypo-auto-minimal-crystallize.mjs +129 -25
- package/hooks/hypo-auto-stage.mjs +13 -1
- package/hooks/hypo-close-guard.mjs +2 -1
- package/hooks/hypo-cwd-change.mjs +45 -20
- package/hooks/hypo-file-watch.mjs +44 -6
- package/hooks/hypo-first-prompt.mjs +15 -9
- package/hooks/hypo-hot-rebuild.mjs +169 -112
- package/hooks/hypo-personal-check.mjs +9 -1
- package/hooks/hypo-session-record.mjs +1 -1
- package/hooks/hypo-session-start.mjs +566 -20
- package/hooks/hypo-shared.mjs +2250 -206
- package/hooks/hypo-stop.mjs +207 -0
- package/hooks/proposal-store.mjs +201 -35
- package/hooks/shared.json +15 -0
- package/hooks/version-check.mjs +9 -7
- package/package.json +3 -1
- package/scripts/crystallize.mjs +3 -4
- package/scripts/doctor.mjs +549 -123
- package/scripts/init.mjs +86 -109
- package/scripts/lib/core-hooks.mjs +48 -22
- package/scripts/lib/crystallize-close-apply.mjs +2397 -188
- package/scripts/lib/crystallize-close-check.mjs +43 -23
- package/scripts/lib/design-history-stale.mjs +36 -9
- package/scripts/lib/frontmatter.mjs +39 -0
- package/scripts/lib/git-hooks-dir.mjs +427 -52
- package/scripts/lib/hook-inventory.mjs +180 -0
- package/scripts/lib/page-usage.mjs +1 -1
- package/scripts/lib/pkg-provenance.mjs +18 -1
- package/scripts/lib/plugin-detect.mjs +43 -9
- package/scripts/lib/project-create.mjs +40 -44
- package/scripts/lib/schema-vocab.mjs +22 -11
- package/scripts/lib/template-schema-version.mjs +48 -0
- package/scripts/lib/time.mjs +46 -0
- package/scripts/lint.mjs +66 -18
- package/scripts/proposal.mjs +423 -18
- package/scripts/resume.mjs +3 -2
- package/scripts/session-audit.mjs +2 -1
- package/scripts/uninstall.mjs +144 -66
- package/scripts/upgrade.mjs +523 -234
- package/scripts/verify.mjs +2 -1
- package/scripts/weekly-report.mjs +2 -1
- package/templates/SCHEMA.md +27 -7
- package/templates/gitignore +11 -0
- package/templates/hypo-automation.md +10 -13
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +53 -25
- package/templates/hot.md +0 -24
package/README.ko.md
CHANGED
|
@@ -31,15 +31,15 @@ Andrej Karpathy의 "LLM 네이티브 위키" 스케치에서 출발했습니다.
|
|
|
31
31
|
|
|
32
32
|
- 신선도 신호. `verify_by_date`가 지난 페이지는 세션 시작 주입이나 lookup 주입 시 `[STALE ...]`로 표시되고, lookup 사용을 로컬에서 추적해 최근 90일간 lookup으로 주입된 적 없는 페이지를 crystallize가 cold 후보로 끌어올려 보여 줍니다.
|
|
33
33
|
- 피드백 한 곳만 고치면 나머지는 따라옵니다. `pages/feedback/`에 한 번 적으면 위키가 `MEMORY.md`와 `~/.claude/CLAUDE.md`의 `<learned_behaviors>` 블록을 자동으로 갱신합니다(단방향 projection).
|
|
34
|
-
-
|
|
35
|
-
- 프로젝트 자동 생성. 프로젝트 표식(`package.json`·`Cargo.toml` 등)이 있는 git
|
|
34
|
+
- Claude가 세션 안에서 셸 명령으로 디렉터리를 옮기면 재개 신호가 걸립니다. 옮긴 곳이 위키 프로젝트의 `working_dir`와 맞고 그 프로젝트 `hot.md`에 내용이 있으며 이 기기에서 보이는 경우, 이동 후 10분 안에 다음 프롬프트를 보내면 Claude의 응답이 "Resuming `<project>`" 한 줄로 시작합니다. (터미널에서 먼저 `cd`한 뒤 Claude를 여는 경우는 다른 경로입니다. `SessionStart`가 그 `hot.md`를 바로 컨텍스트에 넣어 주므로 별도 신호가 필요 없습니다.) 터미널 알림도 함께 뜨는데, 그 프로젝트 `hot.md`를 최대 3천 자까지 담습니다. 훅 문서는 이를 짧은 터미널 알림이라 하고 SDK 메시지 스트림에는 닿지 않는다고만 밝히며, 모델에 닿는지는 말하지 않습니다. Claude Code 2.1.276과 2.1.283의 동작을 분석한 결과로는 모델에 닿는 경로가 없었습니다(아래 전송 안내 참조). 모델에게 `hot.md`를 직접 보여 주려면 `/hypo:resume`을 부르세요.
|
|
35
|
+
- 프로젝트 자동 생성. 프로젝트 표식(`package.json`·`Cargo.toml` 등)이 있는 git 저장소에서 세션을 시작하면, 대응하는 위키 프로젝트가 없을 때 생성 제안이 Claude 컨텍스트에 바로 들어가서 그 자리에서 반응할 수 있습니다. 단 같은 디렉터리에 대한 같은 제안이 최근 5분 안에 이미 기록됐으면 뜨지 않습니다. 이 쿨다운은 세션 중 `cd` 경로, 그리고 `claude()` 셸 함수(아래 설치 안내 참조)가 세션 전에 한 번 돌리는 사전 실행과 함께 씁니다. 그래서 그 함수로 세션을 시작하면 Claude가 첫 프롬프트를 받기도 전에 제안이 이미 억제돼 있을 수 있습니다. 세션 도중 같은 조건의 디렉터리로 `cd`하면 같은 제안이 뜨지만, 위 재개 항목에서 설명한 터미널 토스트로만 나타나 Claude는 보지 못합니다. 한 번 거절한 디렉터리에는 제안이 다시 뜨지 않습니다. 제안이 보이지 않으면 기다리지 말고 Claude에게 직접 프로젝트를 만들어 달라고 요청하세요. 기본 `claude()` 함수로 시작하는 세션은 사전 실행이 쿨다운을 다시 기록하므로, 5분을 기다려도 자동 제안이 다시 뜬다는 보장은 없습니다.
|
|
36
36
|
- 세션 종료 자동 정리와 `/clear` 복구. 의미 있는 세션을 마치겠다고 신호하면 "마무리 메모를 짧게 남길까요?"가 자동으로 뜹니다. 단 진행 중인 작업(백그라운드 셸 작업이나 예약된 wake 포함)이 남아 있으면 강제로 닫거나 매 턴 다시 묻지 않고, 지금 닫을지만 되묻습니다. 마무리하지 않은 채 `/clear`를 입력해도 다음 세션 시작 때 이어서 정리할 수 있습니다.
|
|
37
37
|
|
|
38
38
|
버전별로 무엇이 바뀌었는지는 [CHANGELOG](CHANGELOG.md)에 있습니다.
|
|
39
39
|
|
|
40
40
|
업그레이드 정책 하나만 미리 알아 두세요. `hypomnema upgrade --apply`는 사용자가 직접 편집한 `SCHEMA.md`를 덮어쓰지 않습니다. 스키마가 올라가면 위키 루트에 마이그레이션 보고서만 써 주고, 실제 반영은 사용자가 직접 합니다(코드에서 Option C라고 부르는 정책).
|
|
41
41
|
|
|
42
|
-
같은 보류 방식이 세션 마무리 파일을 쓸 때에도 적용됩니다. 세션 종료 쓰기의 대상 파일이 이 세션이 읽은 뒤에 바뀌었다면 Hypomnema는 덮어쓰지 않습니다. 그 내용을 `<위키>/.cache/proposals/` 아래에 보류하고 알려 줍니다. 검토와 적용은 직접 하세요. `hypomnema proposal list`로 목록을 보고 터미널에서 `hypomnema proposal apply <id>` 또는 `hypomnema proposal discard <id>`를 씁니다. 세션 안에서는 타이핑할 터미널이 없으므로 같은 승인을 대화에서 받습니다. `hypomnema proposal challenge`가 diff를 보여 주고, 거기 적힌 줄을 직접 입력하면, `hypomnema proposal resolve`가 본 그대로를 씁니다. 자동 적용은 없습니다.
|
|
42
|
+
같은 보류 방식이 세션 마무리 파일을 쓸 때에도 적용됩니다. 세션 종료 쓰기의 대상 파일이 이 세션이 읽은 뒤에 바뀌었다면 Hypomnema는 덮어쓰지 않습니다. 그 내용을 `<위키>/.cache/proposals/` 아래에 보류하고 알려 줍니다. 검토와 적용은 직접 하세요. `hypomnema proposal list`로 목록을 보고 터미널에서 `hypomnema proposal apply <id>` 또는 `hypomnema proposal discard <id>`를 씁니다. 세션 안에서는 타이핑할 터미널이 없으므로 같은 승인을 대화에서 받습니다. `hypomnema proposal challenge`가 diff를 보여 주고, 거기 적힌 줄을 직접 입력하면, `hypomnema proposal resolve`가 본 그대로를 씁니다. 자동 적용은 없습니다. `list`는 보류마다 나이와 대상이 그 뒤에 움직였는지를 함께 보여 주어, diff를 일일이 열지 않고도 버릴 것을 고를 수 있습니다. 스스로 빠지는 것은 없습니다. 모든 제거는 사용자가 돌리는 `discard` 입니다.
|
|
43
43
|
|
|
44
44
|
자동처럼 보이지만 실제로는 직접 실행해야 하는 기능이 두 가지 있습니다. extensions sync와 역방향 capture입니다. 위키 안 `~/hypomnema/extensions/{agents,commands,hooks,skills}/`에 둔 파일은 `~/.claude/`에 반영되는데(`--codex`를 붙이면 `hooks`·`commands`는 `~/.codex/`에도), 직접 `hypomnema init`, `hypomnema upgrade --apply`, 또는 dry-run이 아닌 `hypomnema capture`를 돌려야만 반영됩니다. 저절로 밀어 넣는 트리거는 없습니다. `hypomnema capture`(또는 `/hypo:capture`)는 반대 방향으로, 직접 만든 command·agent·hook·skill을 위키로 가져오는 경로인데 이것도 마찬가지로 명시적으로 불러야 동작합니다. 자세한 내용은 아래 명령어 표를 보세요.
|
|
45
45
|
|
|
@@ -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
|
|
|
@@ -211,7 +211,11 @@ Hypomnema는 청크가 아니라 페이지를 지식 단위로 봅니다. 새
|
|
|
211
211
|
|
|
212
212
|
### 4. 왜 재개에 `hot.md` 캐시를 쓰는가
|
|
213
213
|
|
|
214
|
-
멈춘 프로젝트에서 가장 비싼 건 일을 다시 하는 게 아니라 컨텍스트를 다시 쌓는 일입니다. `session-log/`를 처음부터 다시 읽으면 분 단위 시간과 토큰이 들지만, 한 페이지짜리 `hot.md`를 읽는 건 둘 다 들지 않습니다. 그래서 최근 상태를
|
|
214
|
+
멈춘 프로젝트에서 가장 비싼 건 일을 다시 하는 게 아니라 컨텍스트를 다시 쌓는 일입니다. `session-log/`를 처음부터 다시 읽으면 분 단위 시간과 토큰이 들지만, 한 페이지짜리 `hot.md`를 읽는 건 둘 다 들지 않습니다. 그래서 최근 상태를 명시적으로 캐싱합니다. `hot.md`는 두 층입니다. 프로젝트별 `hot.md`는 세션 마무리(`crystallize`)가 쓰고, 볼트 루트의 `hot.md`는 그 파일들을 훑어 만드는 포인터 표입니다. 루트 쪽은 `SessionStart`와 `Stop`에서 훅이 다시 만드니 손으로 고치지 마세요. 둘 다 `SessionStart`에 주입됩니다. 재개는 O(1)입니다.
|
|
215
|
+
|
|
216
|
+
루트 `hot.md`는 훅이 만들어 내는 결과물이지, 손으로 편집하는 자리가 아닙니다. 거기 직접 써 넣은 내용은 다음번 재생성(다음 `SessionStart`나 `Stop`) 때 사라집니다. 훅이 만든 내용인지 사람이 쓴 내용인지 구분해서 판단할 방법이 없기 때문입니다. 파일 rename에는 범용 compare and swap이 없고, 편집기도 훅이 잡은 advisory lock을 따르지 않습니다. 그래서 훅은 자기가 쓴 내용인지 먼저 확인하고, 아니라고 판단되면 그 내용을 버리는 대신 `hot.md.pre-projection-backup.md`(둘 이상 쌓이면 `-2.md`, `-3.md`, ...)로 같은 자리에 백업해 둡니다. 다만 이 백업은 보장이 아니라 최선을 다한 구조일 뿐입니다. 재작성 도중에 저장이 겹치거나, 편집기가 제자리 저장 방식을 쓰면 백업이 일부만 담기거나 아예 안 남을 수도 있습니다. 안전망이지 쓰는 자리가 아니라는 뜻입니다.
|
|
217
|
+
|
|
218
|
+
백업을 다시 루트 `hot.md`로 되돌리지 마세요. 다음 훅 실행이 곧바로 또 덮어씁니다. 대신 백업 파일을 열어 읽어 보고, 남길 내용은 해당 프로젝트의 `projects/<slug>/hot.md`나 별도 페이지로 옮긴 뒤(어느 프로젝트 것인지는 훅이 판단 못 하니 사람이 정합니다) 백업을 지우세요. 볼트 전체에 얼마나 쌓였는지 보려면 볼트 루트에서 `ls hot.md.pre-projection-backup*.md`를 실행하면 됩니다.
|
|
215
219
|
|
|
216
220
|
### 5. 왜 feedback → behavior 파이프라인인가
|
|
217
221
|
|
|
@@ -252,24 +256,21 @@ Hypomnema는 청크가 아니라 페이지를 지식 단위로 봅니다. 새
|
|
|
252
256
|
| 훅 | 이벤트 | 역할 |
|
|
253
257
|
|---|---|---|
|
|
254
258
|
| `hypo-close-guard.mjs` | `PreToolUse` | Write/Edit/MultiEdit가 세션 마무리 쓰기로 보이는데 트랜스크립트에 사용자의 마무리 신호가 없으면, 파일이 바뀌기 전에 확인을 받음 |
|
|
255
|
-
| `hypo-session-start.mjs` | `SessionStart` | `hot.md` / `session-state.md` 주입
|
|
259
|
+
| `hypo-session-start.mjs` | `SessionStart` | `git pull --ff-only`, 루트 `hot.md` 투영 재생성, `hot.md` / `session-state.md` 주입 |
|
|
256
260
|
| `hypo-first-prompt.mjs` | `UserPromptSubmit` | 마커 기반으로 첫 프롬프트에 재개 한 줄을 요구한다(10분 TTL). `hot.md`를 다시 읽지는 않는다 |
|
|
257
261
|
| `hypo-lookup.mjs` | `UserPromptSubmit` | BM25 top-3 HIT 주입 / MISS면 가까운 슬러그 신호 |
|
|
258
262
|
| `hypo-compact-guard.mjs` | `UserPromptSubmit` | 채팅에 입력된 `/compact`나 `/clear`를 감지해 마무리가 덜 됐으면 알린다. compact 자체는 막지 않는다 |
|
|
259
|
-
| `hypo-cwd-change.mjs` | `CwdChanged` | cwd에 맞는 프로젝트 `hot.md`로
|
|
260
|
-
| `hypo-file-watch.mjs` | `FileChanged` | 바뀐 위키 파일로
|
|
263
|
+
| `hypo-cwd-change.mjs` | `CwdChanged` | cwd에 맞는 프로젝트 `hot.md`로 알림을 만든다(아래 전송 안내 참조) |
|
|
264
|
+
| `hypo-file-watch.mjs` | `FileChanged` | 바뀐 위키 파일로 알림을 만든다(`.hypoignore` 준수). 이 패키지는 감시 경로를 등록하지 않아 기본 설치에서는 이벤트가 발생할 계기 자체가 없다(아래 전송 안내 참조) |
|
|
261
265
|
| `hypo-auto-stage.mjs` | `PostToolUse(Write/Edit/MultiEdit)` | 위키 파일 자동 stage |
|
|
262
|
-
| `hypo-
|
|
263
|
-
| `hypo-
|
|
264
|
-
| `hypo-
|
|
265
|
-
| `hypo-
|
|
266
|
-
| `hypo-session-record.mjs` | `Stop` | observability 점수 + auto-resume 신호용 세션 메타데이터 기록 |
|
|
267
|
-
| `hypo-auto-minimal-crystallize.mjs` | `Stop` | 사용자가 마무리를 신호한 뒤, 의미 있는 작업을 했는데 세션 마무리가 확인되지 않으면 Stop을 막고 `crystallize.mjs --mark-session-closed` 명령을 돌려줌. 미커밋 변경이나 진행 중인 작업이 있으면 명령 대신 지금 닫을지 되물음 |
|
|
268
|
-
| `hypo-web-fetch-ingest.mjs` | `PostToolUse(WebFetch/WebSearch)` | WebFetch/WebSearch 뒤 `additionalContext`에 `/hypo:ingest` 권유 주입 (URL의 query/hash/userinfo 제거) |
|
|
266
|
+
| `hypo-stop.mjs` | `Stop` | 등록된 유일한 `Stop` 훅이다. 네 단계를 병렬이 아니라 순서대로 돌린다: 루트 `hot.md` 투영 재생성(`SessionStart`도 같은 생성기를 부른다), observability 점수와 auto-resume 신호용 세션 메타데이터 기록, commit + pull + push, 그리고 사용자가 마무리를 신호한 뒤 의미 있는 작업을 했는데 마무리가 확인되지 않으면 Stop을 막고 `crystallize.mjs --mark-session-closed` 명령을 돌려준다(미커밋 변경이나 진행 중인 작업이 있으면 명령 대신 지금 닫을지 되묻는다) |
|
|
267
|
+
| `hypo-personal-check.mjs` | `PreCompact` | session-close 미완, 위키 커밋/푸시 누락, hot.md 구조 위반, lint 블로커를 `systemMessage`로 알린다. `/compact`는 여기서 막지 않는다. 세션 마무리 강제는 다른 자리에 남아 있다: `hypo-stop.mjs`의 crystallize 단계가 마무리 확인 없는 의미 있는 세션을 막고, `--mark-session-closed`는 여전히 red 게이트에서 마커를 거부한다 |
|
|
268
|
+
| `hypo-session-end.mjs` | `SessionEnd` | SessionEnd 마커를 기록해 다음 SessionStart가 `source=clear` 복구를 감지하게 한다 |
|
|
269
|
+
| `hypo-web-fetch-ingest.mjs` | `PostToolUse(WebFetch/WebSearch)` | WebFetch/WebSearch 뒤 `additionalContext`에 `/hypo:ingest` 권유를 주입한다(URL의 query/hash/userinfo는 제거) |
|
|
269
270
|
|
|
270
271
|
PostToolUse 훅 둘은 matcher 없이 등록되고 각자 tool_name으로 거릅니다.
|
|
271
272
|
|
|
272
|
-
모든 훅은 위키 루트를 `HYPO_DIR` 환경변수 → `hypo-config.md` 스캔 → `~/hypomnema` 기본값 순으로 찾고, `hypo-shared.mjs`(`hooks.json
|
|
273
|
+
모든 훅은 위키 루트를 `HYPO_DIR` 환경변수 → `hypo-config.md` 스캔 → `~/hypomnema` 기본값 순으로 찾고, `hypo-shared.mjs`(`hooks/shared.json`에 선언된 `.mjs` 파일명 배열)를 공유합니다.
|
|
273
274
|
|
|
274
275
|
### 셋업 & 유지보수
|
|
275
276
|
|
|
@@ -396,7 +397,7 @@ E. 멈춘 프로젝트 재개.
|
|
|
396
397
|
|
|
397
398
|
`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
399
|
|
|
399
|
-
> 모델 제공사에게 전송되는 범위: Hypomnema 훅은 위키 본문을 Claude Code의 추가 컨텍스트(`additionalContext`)에 실어 보내고, 이 내용은 프롬프트의 일부로 Claude 모델 제공사로 전송됩니다. 이 경로가 실제로 모델까지 닿는 것은 `hypo-session-start`와 `hypo-lookup`입니다. `hypo-cwd-change`와 `hypo-file-watch`는 `CwdChanged`와 `FileChanged` 이벤트에서 동작하는데, Claude Code가 이
|
|
400
|
+
> 모델 제공사에게 전송되는 범위: Hypomnema 훅은 위키 본문을 Claude Code의 추가 컨텍스트(`additionalContext`)에 실어 보내고, 이 내용은 프롬프트의 일부로 Claude 모델 제공사로 전송됩니다. 이 경로가 실제로 모델까지 닿는 것은 `hypo-session-start`와 `hypo-lookup`입니다. `hypo-cwd-change`와 `hypo-file-watch`는 대신 `CwdChanged`와 `FileChanged` 이벤트에서 동작하는데, 두 이벤트 모두 `additionalContext` 필드를 문서화하지 않습니다. 훅 문서는 이 두 이벤트 출력에서 Claude Code가 읽는 필드로 `watchPaths`와 `systemMessage`만 적고, "대화형 세션에서는 `systemMessage`를 짧은 터미널 알림으로 보여줍니다"와 "이 메시지는 SDK 메시지 스트림에 닿지 않습니다"라고만 말할 뿐, 모델에 닿는지는 어느 쪽으로도 밝히지 않습니다. 문서 자체가 이 물음을 열어 두고 있는 셈입니다. 2026-09-18 기준 Claude Code 2.1.276을 직접 뜯어 추적하고 2.1.283에서 다시 확인해 봐도, 그 터미널 알림에서 모델로 가는 경로는 찾지 못했습니다. 실세션에서 이 경로를 직접 관찰한 기록도 없습니다. 문서가 뒤집기 전까지는 `CwdChanged`와 `FileChanged`를 모델에 닿지 않는 것으로 다룹니다. `hypo-file-watch`는 기본 설치 기준으로 트리거 자체가 없습니다. 이 그룹은 matcher 키 자체가 없는데(빈 문자열이 아닙니다), 훅 문서는 생략된 matcher가 감시 중인 파일 전부에 매치되면서 감시 목록에는 아무것도 더하지 않는다고 적습니다. 이 패키지 안 어디서도 `watchPaths`를 내서 그 목록에 씨를 뿌리지 않으니, 기본 설치라면 이 패키지 때문에 위키 파일이 감시 대상에 오르는 일은 없습니다. 다만 감시 목록은 이 플러그인 전용이 아니라 세션 전역이라서, 사용자의 `settings.json`이나 함께 설치된 다른 플러그인이 목록에 씨를 뿌리면 matcher가 없는 이 그룹은 위키 파일뿐 아니라 감시 중인 모든 파일에서 발화합니다. 그런 경우에도 이 훅 자체가 `HYPO_DIR`로 시작하지 않는 경로에는 즉시 되돌아가므로(`hooks/hypo-file-watch.mjs` 참고, 문자열 접두사 검사라 symlink는 풀지 않습니다), 다른 데서 씨 뿌려진 감시가 위키 밖 경로를 직접 가리키게 만들지는 못합니다. 다만 이 검사는 경로 문자열만 보므로, 위키 안에 있으면서 밖을 가리키는 symlink는 여전히 `HYPO_DIR`로 시작해 통과하고, 이 훅은 그 링크를 따라 읽으므로 그런 symlink가 있으면 가리키는 곳의 내용이 그대로 실릴 수 있습니다. `.hypoignore`는 출력이 모델까지 닿는지와 무관하게 이 네 훅 모두와 `ingest`에서 적용됩니다. 등록하지 않은 파일은 모델에 닿는 두 훅이 그대로 전송합니다. (`hypo-auto-stage`/`hypo-auto-commit`은 git 스테이징용 훅이라 컨텍스트를 주입하지는 않지만, 스테이징 판단에도 `.hypoignore`를 참고합니다.) 비밀 정보는 위키에 두지 마시고, `HYPO_DIR` 아래에 민감한 내용을 저장하기 전에 `.hypoignore` 패턴을 먼저 점검하세요.
|
|
400
401
|
|
|
401
402
|
> 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
403
|
|
package/README.md
CHANGED
|
@@ -31,8 +31,8 @@ An observability score measures how often the wiki is actually used per session.
|
|
|
31
31
|
|
|
32
32
|
- Freshness signaling. A page whose `verify_by_date` has passed is flagged `[STALE ...]` when it's injected at session start or via lookup, and lookup usage is tracked locally so crystallize can surface a page not injected via lookup in the last 90 days as a cold candidate.
|
|
33
33
|
- Edit feedback in one place and the rest follows. `pages/feedback/` is the single source of truth for behavior corrections, and Hypomnema derives `MEMORY.md` and the `<learned_behaviors>` block inside `~/.claude/CLAUDE.md` from it automatically.
|
|
34
|
-
- Project-aware resume
|
|
35
|
-
- Auto-project creation.
|
|
34
|
+
- Project-aware resume when Claude changes directories in-session. When Claude runs a shell command that moves the working directory to one matching a wiki project's `working_dir`, and that project's `hot.md` has content and is visible on this machine, it arms a one-line "Resuming `<project>`" cue: Claude's reply leads with that line, as long as your next prompt comes within 10 minutes of the move. (`cd`ing in the terminal before you start Claude takes a different path: `SessionStart` puts that `hot.md` directly in context, no cue needed.) A terminal notification also fires, carrying up to 3,000 characters of that project's `hot.md`. Per the hook reference it is a brief terminal notification that doesn't reach the SDK message stream; the reference doesn't say whether it reaches the model, and tracing Claude Code 2.1.276 and 2.1.283 found no path that does (see the transmission note below). To put the project's `hot.md` in front of the model, run `/hypo:resume`.
|
|
35
|
+
- Auto-project creation. Starting a session inside a git repo with a project marker (`package.json`, `Cargo.toml`, etc.) and no matching wiki project puts the create-a-project offer in Claude's own context, so it can act on it right away, provided the same offer for that exact directory has not already fired in the last 5 minutes. That cooldown is shared with the mid-session `cd` path and with the `claude()` shell function's throwaway pre-run (see the shell-setup note under installation), so a session launched through that function can find the offer already suppressed before Claude ever sees a prompt. `cd`ing into the same kind of directory mid-session raises the identical offer, but only as the terminal toast described in the resume bullet above, which Claude never sees. A directory you declined once is never offered again. If the offer doesn't show, ask Claude to create the project directly rather than waiting: a session started through the default `claude()` function records the cooldown again in its pre-run, so waiting 5 minutes does not guarantee the offer comes back.
|
|
36
36
|
- Session-close cleanup and `/clear` recovery. When you signal wrap-up after a non-trivial session, a "save a minimal session-close note?" prompt appears automatically, but not if work is still in flight (a running background shell task or a scheduled wake); in that case Hypomnema asks whether to close now instead of forcing it or nagging every turn. A `/clear` after a forgotten close is detected and recovered at the next session start.
|
|
37
37
|
|
|
38
38
|
> The `<learned_behaviors>` projection has one precondition, and no install step sets it up for you: `~/.claude/CLAUDE.md` must contain an empty `<learned_behaviors></learned_behaviors>` pair. Without it every sync is a silent no-op and no global rule loads. Run `hypomnema feedback-sync --ensure-container` once after install; it appends the pair to an existing `CLAUDE.md`, is idempotent, and refuses rather than guessing if the container is malformed. If you have no `CLAUDE.md` at all it is a no-op, so create the file first. Confirm with `hypomnema feedback-sync --check`; `/hypo:doctor` reports the broken state as a Feedback projection failure.
|
|
@@ -209,7 +209,11 @@ Once installed, you stop _managing_ the wiki. It just accumulates.
|
|
|
209
209
|
|
|
210
210
|
### 4. Why a `hot.md` cache for resume
|
|
211
211
|
|
|
212
|
-
The most expensive part of a paused project isn't redoing the work, it's rebuilding context. Reading `session-log/` from scratch costs minutes and tokens; reading a one-page `hot.md` costs neither. So we cache the most recent state explicitly: the root `hot.md` pointer table
|
|
212
|
+
The most expensive part of a paused project isn't redoing the work, it's rebuilding context. Reading `session-log/` from scratch costs minutes and tokens; reading a one-page `hot.md` costs neither. So we cache the most recent state explicitly: the root `hot.md` pointer table is a projection rebuilt from every project's own `hot.md` on both `SessionStart` and `Stop`, never hand-edited, while each project's own `hot.md` is written by crystallize; both get injected on `SessionStart`. Resume is O(1).
|
|
213
|
+
|
|
214
|
+
The root `hot.md` is a generated file, not a place to edit by hand. Whatever you type into it will be replaced the next time a hook rebuilds the projection (the next `SessionStart` or `Stop`), because the projection has no way to tell "content worth keeping" from "content in the way": there's no general-purpose compare-and-swap for a file rename, and your editor doesn't honor the advisory lock the hook takes. The first time the projection overwrites content it doesn't recognize as its own, it backs that content up next to the file as `hot.md.pre-projection-backup.md` (or `-2.md`, `-3.md`, ... if more than one has piled up), so nothing is silently discarded. But that backup is a best-effort save, not a safety guarantee: a save that lands mid-rewrite, or an editor that saves in place, can still leave a partial or missing copy. Treat it as a rescue net, not a place to write.
|
|
215
|
+
|
|
216
|
+
Don't copy a backup back into the root `hot.md`. The next hook run will just replace it again. Instead: open the backup, read it, move whatever's worth keeping into the right project's `projects/<slug>/hot.md` or a standalone page (you decide which project it belongs to, the hook can't), then delete the backup. To see how many have piled up across the vault, list them from the vault root: `ls hot.md.pre-projection-backup*.md`.
|
|
213
217
|
|
|
214
218
|
### 5. Why a feedback → behavior pipeline
|
|
215
219
|
|
|
@@ -248,24 +252,21 @@ Nine commands cover the full capture → retrieval → consolidation cycle.
|
|
|
248
252
|
| Hook | Event | Role |
|
|
249
253
|
|---|---|---|
|
|
250
254
|
| `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
|
-
| `hypo-session-start.mjs` | `SessionStart` |
|
|
255
|
+
| `hypo-session-start.mjs` | `SessionStart` | `git pull --ff-only`, rebuild the root `hot.md` projection, inject `hot.md` / `session-state.md` |
|
|
252
256
|
| `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
257
|
| `hypo-lookup.mjs` | `UserPromptSubmit` | BM25 top-3 HIT inject / MISS → closest-slug signal |
|
|
254
258
|
| `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
|
|
256
|
-
| `hypo-file-watch.mjs` | `FileChanged` | Build
|
|
259
|
+
| `hypo-cwd-change.mjs` | `CwdChanged` | Build a notice from the matching project's `hot.md` (see the transmission note below) |
|
|
260
|
+
| `hypo-file-watch.mjs` | `FileChanged` | Build a notice for a changed wiki file (honors `.hypoignore`); nothing in this package registers watch paths, so on a stock install the event has no trigger here at all (see the transmission note below) |
|
|
257
261
|
| `hypo-auto-stage.mjs` | `PostToolUse(Write/Edit/MultiEdit)` | Auto-stage wiki-file edits |
|
|
258
|
-
| `hypo-
|
|
259
|
-
| `hypo-
|
|
260
|
-
| `hypo-personal-check.mjs` | `PreCompact` | Surfaces an unfinished session-close, an uncommitted or unpushed wiki, malformed `hot.md`, or lint blockers as a `systemMessage`; it never blocks `/compact`. Session close is still enforced elsewhere: the Stop hook (`hypo-auto-minimal-crystallize.mjs`) blocks on a substantial session with no verified close, and `--mark-session-closed` still refuses the marker on a red gate |
|
|
262
|
+
| `hypo-stop.mjs` | `Stop` | The only registered `Stop` hook. Runs four stages in order, not in parallel: rebuild the root `hot.md` projection (`SessionStart` calls the same generator), record session metadata for the observability score and auto-resume signaling, commit + pull + push, then, after the user signals wrap-up, block `Stop` on a substantial session with no verified close and hand back the `crystallize.mjs --mark-session-closed` command (with uncommitted changes or work still in flight it asks whether to close now instead) |
|
|
263
|
+
| `hypo-personal-check.mjs` | `PreCompact` | Surfaces an unfinished session-close, an uncommitted or unpushed wiki, malformed `hot.md`, or lint blockers as a `systemMessage`; it never blocks `/compact`. Session close is still enforced elsewhere: `hypo-stop.mjs`'s crystallize stage blocks on a substantial session with no verified close, and `--mark-session-closed` still refuses the marker on a red gate |
|
|
261
264
|
| `hypo-session-end.mjs` | `SessionEnd` | Write a SessionEnd marker so SessionStart can detect `source=clear` recovery |
|
|
262
|
-
| `hypo-session-record.mjs` | `Stop` | Record session metadata for the observability score and auto-resume signaling |
|
|
263
|
-
| `hypo-auto-minimal-crystallize.mjs` | `Stop` | After the user signals wrap-up, blocks Stop on a substantial session with no verified close and hands back the `crystallize.mjs --mark-session-closed` command. With uncommitted changes or work still in flight it asks whether to close now instead |
|
|
264
265
|
| `hypo-web-fetch-ingest.mjs` | `PostToolUse(WebFetch/WebSearch)` | Inject a `/hypo:ingest` nudge into `additionalContext` after a WebFetch (URL redacted of query/hash/userinfo) or a WebSearch |
|
|
265
266
|
|
|
266
267
|
Both PostToolUse hooks are registered without a matcher and filter on `tool_name` themselves.
|
|
267
268
|
|
|
268
|
-
All hooks resolve the wiki root via `HYPO_DIR` env → `hypo-config.md` scan → `~/hypomnema` default, and share `hypo-shared.mjs` (declared
|
|
269
|
+
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
270
|
|
|
270
271
|
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
272
|
|
|
@@ -386,7 +387,7 @@ Place a `hypo-config.md` at the wiki root to make it portable across machines wi
|
|
|
386
387
|
|
|
387
388
|
`.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
389
|
|
|
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
|
|
390
|
+
> `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
391
|
|
|
391
392
|
> 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
393
|
|
|
@@ -394,9 +395,9 @@ Place a `hypo-config.md` at the wiki root to make it portable across machines wi
|
|
|
394
395
|
|
|
395
396
|
> Gate bypass: `HYPO_SKIP_GATE=1` is honored by every gate that can stop you mid-session: `hypo-compact-guard`, `hypo-close-guard`, `hypo-auto-minimal-crystallize`, and the ingest nudge in `hypo-web-fetch-ingest`. `hypo-personal-check` (PreCompact) also reads the flag, but since that hook never blocks `/compact` anymore, the flag only suppresses the incomplete-close `systemMessage` it would otherwise repeat. Set it for a trivial session you do not want recorded.
|
|
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
|
+
> 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. `list` shows each park's age and whether its target moved since, so you can pick what to drop without opening every diff. Nothing leaves on its own: every removal is a `discard` you run.
|
|
398
399
|
|
|
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
|
|
400
|
+
> 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` instead, and neither event documents an `additionalContext` field: the hook reference documents `watchPaths` and `systemMessage` as the fields Claude Code reads from these two events' output, and says only, "it shows the `systemMessage` as a brief terminal notification" and "the message doesn't reach the SDK message stream," without saying either way whether it reaches the model. So the reference itself leaves that open. Tracing the installed Claude Code binary (2.1.276, checked 2026-09-18, and reconfirmed on 2.1.283) found no branch that forwards that terminal notification to the model. No live-session observation of this path exists either way. Until the documentation says otherwise, we treat `CwdChanged` and `FileChanged` as not reaching the model. `hypo-file-watch` additionally has no registered trigger on a stock install: the group's matcher key is omitted (not an empty string), which the reference says matches every watched file while adding nothing to the watch list, and nothing in this package returns `watchPaths` to seed that list, so on a stock install nothing here ever puts a wiki file under watch. The watch list is session-global, though, not scoped to this plugin: your own `settings.json` or another installed plugin can seed it, and if anything else does, this matcher-less group fires for every watched file, not only wiki ones. Even then, the hook itself returns immediately for any path that does not start with `HYPO_DIR` (see `hooks/hypo-file-watch.mjs`; a string-prefix check, not one that resolves symlinks), so a watch seeded elsewhere cannot point it at a path outside the wiki. That check only looks at the path string, though: a symlink that lives inside the wiki and points outside it still starts with `HYPO_DIR`, and the hook reads through it, so such a symlink can still surface content from wherever it points. `.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
401
|
|
|
401
402
|
> 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
403
|
|
package/commands/crystallize.md
CHANGED
|
@@ -19,7 +19,7 @@ If `/hypo:crystallize` was invoked to close a session (via an explicit close sig
|
|
|
19
19
|
|
|
20
20
|
Before composing the payload (Step 2), run these four reflections and surface each to the user. Every one is **advisory** (identity guard): the user confirms or declines, and none performs an automatic action, writes a file on its own, or bypasses the mandatory gate.
|
|
21
21
|
|
|
22
|
-
1. **Trivial-session check (#44)
|
|
22
|
+
1. **Trivial-session check (#44).** Was this session trivial (a single bug fix, a single-file edit, or Q&A with no durable artifact)? If so, recommend skipping session-close: *"이 세션은 trivial해 보입니다. session-close를 건너뛸까요?"* and proceed only if the user wants a close. A trivial skip is a recommendation, **not** a bypass: it must not mark the session closed, must not run `--mark-session-closed`, and must not claim `/compact` can pass. Any real close still requires all 4 mandatory files.
|
|
23
23
|
2. **ADR-candidate check (#41).** Did this session make an architectural or design decision (a new pattern, a tradeoff chosen, a convention established)? If yes, ask whether it warrants an ADR and, if so, capture that intent in the `sessionLog` entry you compose in Step 2. If nothing rose to ADR level, you may record `ADR 없음: <one-line reason>` in that same `sessionLog` entry, but gate it on #42's bar: the marker is machine-read and W8 excludes an entry carrying `ADR 없음` (with no ADR reference) from the design-history staleness check. Write it only when the session had **no design change at all**; a sub-ADR design shift takes #42a (append) instead, since the marker would suppress the W8 nudge it needs. **Never auto-write an ADR file.** Recording the decision (or its absence) in the session-log payload is the only action here. This check carries no `decisions/` directory precondition: run it whether or not that directory exists.
|
|
24
24
|
3. **design-history staleness check (#42).** Two branches, so a stale W8 never blocks a clean close: (a) if this session changed design decisions `projects/<name>/design-history.md` does not yet reflect (including sub-ADR background, tradeoff, or differentiation shifts), recommend appending now: the W8 lint warning flags this mechanically, and an active-project W8 still counts as a `--check-session-close` blocker even though the PreCompact hook no longer stops `/compact` on it, so append before you commit. **If the file does not exist yet and this session had a design change, recommend creating it now** with that change as the first entry; lint separately flags a missing-but-needed file as W14, a warning that never blocks. (b) only if the session made **no** design change does the `ADR 없음` marker (#41) exempt the entry from W8; do not touch design-history, and do not create the file just to satisfy this branch's check. `ADR 없음` means "no design change," a stricter bar than "no ADR-level decision." Never auto-write the file yourself in either branch: recommend it, and let the user decide.
|
|
25
25
|
4. **Ingest check (#43)** — Did this session consume trustworthy external knowledge (a fetched URL, official docs, or code you verified directly)? If so, recommend running `/hypo:ingest` to capture it under `sources/`. Proceed only on the user's confirmation.
|
|
@@ -30,9 +30,9 @@ These are judgment calls; when uncertain, surface the question rather than skip
|
|
|
30
30
|
|
|
31
31
|
## Step 2 — Compose the session-close payload
|
|
32
32
|
|
|
33
|
-
The session-close path is **payload-driven**. Instead of writing the
|
|
33
|
+
The session-close path is **payload-driven**. Instead of writing the 4 mandatory files one-by-one, you compose a single JSON payload that describes the full session-close state, then hand it to the `--apply-session-close` apply path (the Step 3 command), which performs idempotent atomic writes and gates the result with lint.
|
|
34
34
|
|
|
35
|
-
Payload shape (`project` +
|
|
35
|
+
Payload shape (`project` + 3 content fields required; `log` optional/derived; `openQuestions` conditional, per Spec §5.2.7 / §8.3):
|
|
36
36
|
|
|
37
37
|
```json
|
|
38
38
|
{
|
|
@@ -41,7 +41,6 @@ Payload shape (`project` + 4 content fields required; `log` optional/derived; `o
|
|
|
41
41
|
"date": "YYYY-MM-DD",
|
|
42
42
|
"sessionState": { "content": "<full body of projects/<name>/session-state.md>" },
|
|
43
43
|
"projectHot": { "content": "<full body of projects/<name>/hot.md>" },
|
|
44
|
-
"rootHot": { "content": "<full body of <hypo-root>/hot.md>" },
|
|
45
44
|
"sessionLog": { "entry": "<entry to append to projects/<name>/session-log/YYYY-MM-DD.md (daily shard)>" },
|
|
46
45
|
"log": { "entry": "<entry to append to <hypo-root>/log.md>" },
|
|
47
46
|
"openQuestions": { "content": "<full body of pages/open-questions.md>" }
|
|
@@ -54,14 +53,17 @@ Field rules:
|
|
|
54
53
|
|
|
55
54
|
- `project`: **required**. Slug of the project being closed (matches a `projects/<slug>/` directory). Must be a single path segment, charset `A-Za-z0-9._-`, with at least one alphanumeric and not a dot-only name (`.`, `..`, `...`). Apply never infers the target from recency; a same-date pointer-table tie could otherwise write the close into the wrong project (B-3). A missing, malformed, or non-existent value fails the apply before any write.
|
|
56
55
|
- `sessionId`: recommended. The current session id, the same value you pass to `--session-id`. When present, apply refuses (`stage='session-id-mismatch'`, exit 1, nothing written) if it does not equal `--session-id`, so a payload file another session overwrote cannot be applied under this session's marker. It is a string self-check, not the transcript resolver; authority still comes from `--session-id`. Optional for backward-compat: omitting it or setting it to `null` skips the check (fail-open), but an empty string `""` counts as a mismatch and is refused. Omit only if you genuinely cannot determine the id.
|
|
57
|
-
- `date
|
|
56
|
+
- `date`: optional. Defaults to today (local). If supplied, it must be a real calendar date in `YYYY-MM-DD` form; a day that does not exist, such as `2026-09-31`, fails the apply before any write.
|
|
58
57
|
- `openQuestions` — optional. Include only when `pages/open-questions.md` exists and changed this session.
|
|
59
58
|
- `log`: optional. Omit it by default (apply derives the canonical `## [date] session | <project>` line from your `sessionLog` heading). Supply it only for a custom log.md line, which must still be a canonical `session | <project>` heading, or the apply fails at `stage='pre-apply-verification'`.
|
|
60
|
-
- The remaining fields (`sessionState`, `projectHot`, `
|
|
59
|
+
- The remaining fields (`sessionState`, `projectHot`, `sessionLog`) are required.
|
|
60
|
+
- `rootHot` is **gone from this payload.** The root `hot.md` pointer table is regenerated by the SessionStart and Stop hooks from each `projects/*/hot.md`, so a table composed here is overwritten by this same turn's Stop hook before it ever reaches git. It is not an omission: do not add it back. An older installed copy of this command still sends it, so apply does not fail on it: it reports one line in `notices[]` saying the field was ignored, and writes nothing for it.
|
|
61
61
|
|
|
62
62
|
Notes:
|
|
63
63
|
|
|
64
|
-
- `sessionState` / `projectHot` / `
|
|
64
|
+
- `sessionState` / `projectHot` / `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. **This changed: the flag used to write the bytes straight through, and it no longer does.** It records your claim for whoever reviews the park, and it changes the park reason so they can see the drop was deliberate rather than an omission. The bytes still land only once a human approves the proposal. Nothing you can put in a payload approves your own destructive overwrite. A flag that waived a real trip, as opposed to 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
|
|
|
@@ -69,10 +71,11 @@ Content guidance for each slot:
|
|
|
69
71
|
|
|
70
72
|
1. **sessionState** — next tasks list for the upcoming session (what to tackle first next time).
|
|
71
73
|
2. **projectHot** — session snapshot under 500 words: what changed and decisions made. Do **not** put next-step tasks here; those belong in `sessionState`.
|
|
72
|
-
3. **
|
|
73
|
-
4. **
|
|
74
|
-
5. **
|
|
75
|
-
|
|
74
|
+
3. **sessionLog**: one session entry to append to `projects/<name>/session-log/YYYY-MM-DD.md` (daily shard).
|
|
75
|
+
4. **log** (optional): a custom `session` entry for `<hypo-root>/log.md`. Omit it to let apply derive the canonical line from `sessionLog`.
|
|
76
|
+
5. **openQuestions** (conditional): only if `pages/open-questions.md` exists and questions were raised or resolved this session.
|
|
77
|
+
|
|
78
|
+
There is no slot for the root `hot.md` here. Its `Last Session` dates come from the project `hot.md` files themselves, and the hooks rebuild the table from those on every session start and every session end. Writing it into `projectHot` is what makes it appear in the root table; writing it by hand is what gets thrown away.
|
|
76
79
|
|
|
77
80
|
---
|
|
78
81
|
|
|
@@ -106,11 +109,30 @@ and nothing on disk, in three cases:
|
|
|
106
109
|
A refusal is not a failure to route around. It means the close should not happen: ask
|
|
107
110
|
the user, and re-run only after they say so.
|
|
108
111
|
|
|
109
|
-
On a verified close (`ok: true
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
On a verified close (`ok: true`, no uncommitted file in the project folder being closed, and no uncommitted file elsewhere that this session wrote through Write or Edit since its last auto-commit)
|
|
113
|
+
the apply first files a commit-backed close checkpoint receipt at
|
|
114
|
+
`HYPO_DIR/.cache/sessions/<id>/close-receipt.json`, then writes the per-session compat marker
|
|
115
|
+
`HYPO_DIR/.cache/session-closed-<id>.marker` as a projection of it. If the marker does not land,
|
|
116
|
+
the receipt is withdrawn again, so a run that reports the marker as failed never leaves a valid
|
|
117
|
+
receipt behind. The checkpoint gate reads git more narrowly than `/compact` does. An uncommitted
|
|
118
|
+
file inside the project folder being closed (`projects/<project>/`) blocks whether or not this
|
|
119
|
+
session has a record of writing it. An uncommitted file at the vault root or in another project's
|
|
120
|
+
folder blocks only when this session wrote it through Write or Edit since its last auto-commit;
|
|
121
|
+
otherwise it is reported as a notice. If the record of what this session wrote cannot be read, the
|
|
122
|
+
gate does not block on it: it treats the session as having no record and adds a notice that it could
|
|
123
|
+
not determine who owns those files. So the checkpoint can land while `/compact` still waits on
|
|
124
|
+
someone else's dirty file. The checkpoint receipt is the thing the
|
|
125
|
+
Stop-chain Layer 3 hook (`hypo-auto-minimal-crystallize`) actually checks: it proves only that
|
|
126
|
+
the file versions it names are in a specific commit, never that every change this session made
|
|
127
|
+
is saved. (It has nothing to do with the `close-receipt-failed` result of `proposal resolve`
|
|
128
|
+
below, which concerns the close-journal's handoff receipt.) Stop stops re-prompting for the close
|
|
129
|
+
procedure itself, and still surfaces any other uncommitted vault change separately, once per
|
|
130
|
+
receipt, as a notice rather than a repeat of "session-close incomplete". A shared append
|
|
131
|
+
target (`log.md`, a session-log shard) is checked by containment, not byte-for-byte: the
|
|
132
|
+
receipt only confirms this close's own entry is in the file, and the commit that lands it
|
|
133
|
+
carries the whole file as it stood at commit time, including any other session's
|
|
134
|
+
already-dirty bytes in the same file. Running crystallize purely for **synthesis** carries
|
|
135
|
+
no payload and needs no session id.
|
|
114
136
|
|
|
115
137
|
> **Source rule for `--session-id`:** use only the main conversation's session id
|
|
116
138
|
> (the id shown in the `[WIKI_AUTOCLOSE]` block reason, or the injected
|
|
@@ -126,7 +148,7 @@ no session id.
|
|
|
126
148
|
|
|
127
149
|
| Invocation | Behavior |
|
|
128
150
|
|---|---|
|
|
129
|
-
| `--apply-session-close` (no `--payload`) | **Probe mode
|
|
151
|
+
| `--apply-session-close` (no `--payload`) | **Probe mode**: exits 0 with "오늘 이미 close 완료로 보임" if all 4 files are fresh today; exits 1 with "payload is required" otherwise. Writes nothing, so it needs no session id. |
|
|
130
152
|
| `--apply-session-close --payload=<path>` | **Refused**, exit 1, `reason: 'session-id-required'`. Nothing is written and nothing is committed. |
|
|
131
153
|
| `--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. |
|
|
132
154
|
| `--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. |
|
|
@@ -138,22 +160,43 @@ transcript holds no user close signal at all), `transcript-rewrite-detected`
|
|
|
138
160
|
close signal predates a resolution already recorded). The collapsed `reason`
|
|
139
161
|
stays the same string in all three cases, so read `gateReason` rather than
|
|
140
162
|
parsing the `Gate detail:` fragment out of `error`. The field is absent for
|
|
141
|
-
every other refusal.
|
|
163
|
+
every other refusal. `markerGateReason` on a successful apply is the same
|
|
164
|
+
diagnostic one stage later: the apply itself went through, and only the marker
|
|
165
|
+
was withheld for lack of a signal, so it carries the same three values and is
|
|
166
|
+
read the same way (see the `markerSkipReason` branches below).
|
|
142
167
|
|
|
143
168
|
**Two lint gates run automatically, scoped to the files this close writes:**
|
|
144
169
|
|
|
145
|
-
Both gates judge only the **payload files** (the
|
|
170
|
+
Both gates judge only the **payload files** (the 4 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).
|
|
146
171
|
|
|
147
|
-
1. **Preflight
|
|
172
|
+
1. **Preflight**: an internal `lint --json` preflight runs **before** any payload bytes are written. Errors in overwrite targets (sessionState / projectHot / openQuestions) are filtered (about to be replaced). Errors in an **append target** (session-log / log.md) still block (appending can't repair existing corruption) → exit 1 with `stage='preflight-lint'`. Errors outside the payload files → `notices[]`, apply proceeds.
|
|
148
173
|
2. **Post-apply** — lint re-runs after the writes. Blocks only on **errors** in payload files (a payload-introduced malformed body / bad frontmatter); pre-existing errors elsewhere → `notices[]`. A lint crash (unparseable output) always blocks. Broken wikilinks are lint **warnings** (W4 — forward references to planned pages are normal) and are not gated here. Surfaces as `stage='post-apply-lint'` (or `'post-apply-verification+lint'` if freshness also fails).
|
|
149
174
|
|
|
150
175
|
> **Manual close (direct Write tool calls)** clears the Stop-chain block via `--mark-session-closed --session-id=<id>`. Both marker writers apply a **user-close hard gate**: the marker is written only when the session's transcript carries a genuine user close signal — an NL close phrase, a `/compact`, or an accepted AskUserQuestion [세션 마무리] answer. The transcript is resolved **strictly from `--session-id`** (a globally-unique id, globbed under `~/.claude/projects/`), never from a CLI arg, so a model that runs the writer on its own — without the user ever signalling close — is refused, and a forged path cannot point the gate at someone else's close-intent. The lint scope is widened from that same resolved transcript. `--transcript-path` is **not** consulted by the marker gate; it survives only to scope `--check-session-close`'s lint (which writes no marker).
|
|
176
|
+
>
|
|
177
|
+
> `--mark-session-closed` files the checkpoint receipt only when session-state.md, hot.md, the session-log entry and log.md of every project it attributes are already **committed**, so commit them first. On any failure it prints `ok: false` and exits 1 without a marker.
|
|
178
|
+
>
|
|
179
|
+
> The `stage` table in Step 4 belongs to `--apply-session-close`. `--mark-session-closed` has no `stage` field. A refusal after the receipt proof carries a `reason` instead (and `error` in prose), and so does `prior-checkpoint-rewritten`, which comes before the gate runs. Branch on that:
|
|
180
|
+
>
|
|
181
|
+
> | `reason` | What broke | How to recover |
|
|
182
|
+
> |---|---|---|
|
|
183
|
+
> | `incomplete` | `incompleteProjects[]` names each project whose session-state.md, hot.md, session-log evidence file or log.md is missing, not fresh, or not committed. | Commit the close files of the projects named, or finish writing them, then re-run the same command. |
|
|
184
|
+
> | `mismatch` | The version of a proof file committed at HEAD differs from what is in the working tree. `mismatches[]` lists each `{path, reason}` (for example a file with an uncommitted edit, or one a `.hypoignore` rule keeps out of the commit). | Run `git status` in the vault, commit the reviewed paths, then re-run. |
|
|
185
|
+
> | `no-commit-identity` | The vault has no readable commit to certify against (not a git repository, or no commit exists yet). | Commit the close files so the vault has a HEAD, then re-run. |
|
|
186
|
+
> | `write-failed` | Writing the checkpoint receipt under `.cache/sessions/<session-id>/` failed; `writeReason` names the error. If `retractFailed` is also present, taking back what the write may have left failed too and a stale receipt may remain there. | Fix what blocks that directory (a file where the directory should be, permissions, disk space), remove any stale `close-receipt.json` it names, then re-run. |
|
|
187
|
+
> | `marker-did-not-land` | The receipt was filed but the marker file itself did not land. The receipt is withdrawn again; `retractFailed` is present only when that failed too. | Fix what blocks the marker path under `.cache/` (permissions, a directory sitting where the marker goes, disk space), then re-run. |
|
|
188
|
+
> | `prior-checkpoint-rewritten` | This session had filed a close receipt earlier, and the commit it proved is no longer in the branch history (a hard reset or a rebase dropped it). The command refuses before it touches anything: the old receipt and marker stay where they are, and the result carries `prior_commit`. Issuing a new receipt over today's other close files would report "closed" while the original close record stays lost. A receipt that is missing or unparsable does not trigger this, and neither does one whose commit is still in the history but whose marker is gone (that one recovers with this command as usual). | Restore the dropped commit (for example from `git reflog`), or ask the user to request the close again and run `--apply-session-close` with a new payload. Do not retry `--mark-session-closed`: it refuses the same way until the history holds that commit again. |
|
|
189
|
+
> | `vault-commit-lock-timeout` | Another close or commit held the vault-commit lock past the timeout. | Re-run the same command a moment later. |
|
|
190
|
+
>
|
|
191
|
+
> Three refusals come before the receipt proof and carry no `reason`. A gate refusal (`blockers[]` plus `missing` and `stale`, with `error: 'session-close gate not satisfied'`) means the same close-file, lint or feedback checks as `--check-session-close` found something this session owns: fix it and re-run. A `skipReason` of `no-user-close-signal` (with `gateReason` when the gate has one) or `no-attribution-evidence` means the transcript shows no close request, or nothing ties this session to a project (pass `--project=<slug>` or `--log-only`). A bare `error` with none of these fields means the prior receipt or marker could not be set aside; fix the permission or disk problem under `.cache/` it names.
|
|
192
|
+
>
|
|
193
|
+
> A close is complete once the receipt is issued: `ok: true` from this command, and `close_state: closed` from `--check-session-close --session-id=<id>`. The "Compact-ready" line of `--check-session-close` covers the whole vault, so another session's uncommitted file can keep it red after your close has landed.
|
|
151
194
|
|
|
152
195
|
---
|
|
153
196
|
|
|
154
197
|
## Step 4 — Stage-based recovery
|
|
155
198
|
|
|
156
|
-
The result JSON includes a `stage` field when `ok: false`. Branch on it
|
|
199
|
+
The result JSON of `--apply-session-close` includes a `stage` field when `ok: false`. Branch on it. (`--mark-session-closed` reports `reason` instead; see the table in Step 3.)
|
|
157
200
|
|
|
158
201
|
| `stage` | What broke | How to recover |
|
|
159
202
|
|---|---|---|
|
|
@@ -163,17 +206,32 @@ The result JSON includes a `stage` field when `ok: false`. Branch on it:
|
|
|
163
206
|
| `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. |
|
|
164
207
|
| `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.) |
|
|
165
208
|
| `post-apply-verification+lint` | Both above. | Fix both; re-run. |
|
|
209
|
+
| `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. `restructure: true` does not exempt a field from this; it only changes which park reason is recorded (`section-loss-guard-restructure-pending` instead of `section-loss-guard`). One reason, `will-overwrite-local-change`, fires whenever disk has moved away from what this session's own close last applied to that target (almost always a hand edit through Write/Edit after that apply), regardless of whether the new payload is the exact old bytes or something else entirely: there is no way from here to tell a payload that folds the hand edit back in from one that ignores it, so both park the same way and both need a human's `proposal challenge`/`proposal resolve` to land. 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[]>`. If the session resolving this approval is NOT the same session that ran this close (the original session ended and this is a fresh one finishing its park), also pass `--close-session-id=<the original close's session id>`, so the approved bytes get credited back to THAT close's own retry instead of this one's. 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 **original** close (the one named by `--close-session-id`, or this same session if you omitted it); it is idempotent. If `resolve` itself reports `close-receipt-failed` (the close-journal's handoff receipt, unrelated to the close checkpoint receipt described above), do NOT re-run the close: the approved bytes landed on the page, but the close whose retry needs to see them was not told. Fix whatever is blocking `.cache/close-journal/` and run `node ${CLAUDE_PLUGIN_ROOT}/scripts/proposal.mjs reconcile` instead, which recovers the missing receipt from the audit log without writing any page, then re-run the original close. |
|
|
210
|
+
| `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. |
|
|
211
|
+
| `invalidate-failed` | A new close was authorized, and before writing anything the apply tried to set aside this session's prior close checkpoint receipt and marker, because they must not keep certifying a close that is being redone. That rename failed, usually a permission or disk problem under `.cache/`. Nothing was written and nothing was committed (`applied: []`, `committed: null`). The JSON carries `error` but no `mismatches[]`. | Fix the permission or disk problem named in `error`, then re-run the same close. No fresh close phrase is needed: nothing was recorded as resolved. |
|
|
212
|
+
| `receipt-proof-mismatch` | The writes and the commit went through, but the committed bytes do not match what this close meant to certify, so the checkpoint receipt was withheld and no marker was written. `ok: false`, exit 1, `committed: true`, and `mismatches[]` lists each `{path, reason}` that failed (for example a target that a `.hypoignore` rule keeps out of the commit, or a file whose committed version differs from the payload). A `path` of `(commit)` with `no-commit-identity` means the vault has no readable commit to certify against. | Run `git status` in the vault and look at each path in `mismatches[]` to see why that file is not committed as written. Once you have reviewed those paths, commit them, then re-run the same close. The retry needs **no fresh close phrase**, because a close signal is spent only once the receipt and marker land. |
|
|
213
|
+
| `receipt-write-failed` | Same point as above (committed, proof verified), but writing the checkpoint receipt file itself failed. `ok: false`, exit 1, and `mismatches[]` holds one entry `{path: '(receipt)', reason}` naming the write error. When the file was written but could not be read back, the receipt is withdrawn again if it is this close's own; if that fails too, a second entry `{path: '(receipt)', reason: 'retract-failed: …'}` follows and a stale receipt may remain. | Fix what is blocking `.cache/sessions/<session-id>/` (a file sitting where the directory should be, permissions, disk space), then re-run the same close. No fresh close phrase is needed. |
|
|
214
|
+
| `marker-did-not-land` | Every close precondition cleared (the gate was satisfied, the writes landed, the commit succeeded, the checkpoint receipt was filed) and only the session-close marker's own write to disk failed. This is the one marker failure that is a disk problem rather than a policy withhold, so it comes back `ok: false` and exit 1 instead of a `markerSkipReason` on a successful result. The receipt just filed is withdrawn again, so no reader sees this session as closed. If withdrawing it failed too, `mismatches[]` carries `{path: '(receipt)', reason: 'retract-failed: …'}` and a stale receipt may remain under `.cache/sessions/<session-id>/`. | Fix what is blocking the marker path under `.cache/` (permissions, a directory sitting where the marker file goes, disk space), then re-run the same close. **No fresh close phrase is needed**: a close signal is spent only once the marker lands, so this run left it unspent. Everything else is already idempotent, so the retry re-applies cleanly. |
|
|
215
|
+
| `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. |
|
|
166
216
|
|
|
167
217
|
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.
|
|
168
218
|
|
|
169
219
|
- ✓ session-state.md applied (or already current, per `skipped`)
|
|
170
|
-
- ✓ hot.md (project
|
|
220
|
+
- ✓ hot.md (project) applied (or already current). The root `hot.md` pointer table is not part of this apply: it's a separate projection a hook rebuilds from every project's own `hot.md`. A `rootHot` field in an older payload is not applied here either; it's ignored, and the result JSON reports that under `notices[]` (see below).
|
|
171
221
|
- ✓ session-log entry appended (or already present)
|
|
172
222
|
- ✓ open-questions applied (or skipped if unchanged)
|
|
173
223
|
- ✓ log.md entry appended (or already present)
|
|
174
224
|
- ✓ post-apply lint clean
|
|
225
|
+
- **`notices[]`** (report any entries): non-blocking messages, including the obsolete-field notice above when a payload sent `rootHot`. Report them alongside the checklist so whoever sent an older payload knows their root table field was ignored, not silently dropped.
|
|
226
|
+
- **`hostTagWarning`** (report it verbatim whenever present): this close was granted while a queued item shaped like a known host tag stayed neutral instead of retracting it. The host mints that shape, but a person can type or paste it too, so the close may not reflect a decision the user actually made. Print the string as it comes, including the undo instructions it carries, and print it once per run of the command. A close that keeps failing and gets retried is expected to print it again each time, because each run writes its own bytes; what must not happen is the same run repeating it, or a later, unrelated run inheriting it. It is **not** limited to `ok: true`: a run that committed the payload and then failed its marker write (`stage: 'marker-did-not-land'`) is exactly the case where it matters most, because those bytes are already in the vault's history. The undo instructions differ per run and name only what that run actually did, so never reword them and never swap in instructions you remember from another run. `--mark-session-closed` carries the same field, on the same terms, for a manual close.
|
|
175
227
|
- **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.
|
|
176
228
|
|
|
229
|
+
**Check these three regardless of `ok`.** The result JSON carries them on a failed apply exactly as it does on a successful one: the apply script reports them outside its success branch on purpose. A park most often lands on exactly the run where `ok: false`, `stage: 'proposal-pending'`, since that is the run where the backlog just grew and a damaged artifact is most likely sitting next to the new one. A payload that never reaches `ok: true` still needs these three reported.
|
|
230
|
+
|
|
231
|
+
- **`restructureWaivers[]`** (report it if non-empty): an overwrite field carried `"restructure": true` on a field that really did drop `##` sections. Report each entry verbatim: the target file and the section names. **Nothing was let through.** The field is parked like any other section-loss trip and appears in `conflicts[]` too. This list is the claim, not the outcome, and it is set by the same party (a model composing the payload) the guard exists to check, so it must never pass silently.
|
|
232
|
+
- **`parkedTotal`** (report it if not `null` and > 0): the vault-wide count of parked write-proposal artifacts, not just what this close just parked (`proposals[]` above is only this run's own). It mixes pending, already-approved-but-unreconciled, and evidence-broken artifacts; the breakdown by state lives in doctor, not here. Tell the user, in one line, that N parked write-proposal artifact(s) exist vault-wide and that doctor shows what state each one is in: `/hypo:doctor` in Claude Code, or `hypomnema doctor` from a shell with the npm CLI (a plugin-only install has no `hypomnema` command). Do not repeat the per-id detail already covered by `proposals[]`. `null` means `.cache/proposals` itself could not be listed: report that as a measurement failure, never as "no parked proposals."
|
|
233
|
+
- **`parkedUnreadable`** (report it whenever non-empty, regardless of `parkedTotal`): filenames under `.cache/proposals` that exist as `.json` candidates but could not be parsed into an artifact (corrupt, permission-denied, or hand-edited into an unrecognizable shape). These are **not** counted in `parkedTotal` above; do not add the two numbers together. Tell the user, by name, which file(s) are unreadable and that doctor is where to inspect them (`/hypo:doctor` in Claude Code, or `hypomnema doctor` with the npm CLI); neither `proposal list` nor `proposal reconcile` can see these. This can be non-empty even when `parkedTotal` is `0`: a vault whose only proposal file is broken must never read as "nothing parked."
|
|
234
|
+
|
|
177
235
|
If `markerWritten: true`: ask: "Session closed. Would you like to also run knowledge synthesis now, or stop here?"
|
|
178
236
|
|
|
179
237
|
**If `ok: false` with an authority `reason`, the close did not happen at all.** Nothing was written, nothing was committed. Do not report a partial close, and do not go looking for another way in.
|
|
@@ -182,12 +240,17 @@ If `markerWritten: true`: ask: "Session closed. Would you like to also run knowl
|
|
|
182
240
|
- `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.
|
|
183
241
|
- `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
242
|
- `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.
|
|
243
|
+
- `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. Before you say anything, read `close_state` from `--check-session-close --session-id=<id>`. If it is `closed` (or `legacy-closed`), report that this session is already closed and stop; do not re-prompt. If it is `broken`, do **not** say the session is already closed: the earlier close's proof no longer holds (a commit it named was dropped by a reset or rebase, or the receipt and marker disagree). First run `--mark-session-closed --session-id=<id>`: when only the marker is missing, that recovers the close without asking the user anything. Only when it refuses with `prior-checkpoint-rewritten` (the commit its receipt named left the history) tell the user so and ask them to request the close again, then run `--apply-session-close` with a new payload. 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.
|
|
186
244
|
- `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
245
|
|
|
188
246
|
In all three: do NOT touch the close-signal matcher itself, and do not hand-write the files to work around the refusal.
|
|
189
247
|
|
|
190
|
-
If the apply succeeded but `markerWritten: false`, do NOT say "session closed." Branch on `markerSkipReason
|
|
248
|
+
If the apply succeeded but `markerWritten: false`, do NOT say "session closed." Branch on `markerSkipReason`, which carries one of four values here. Three more (`marker-did-not-land`, `receipt-proof-mismatch`, `receipt-write-failed`) no longer reach this branch: when every other precondition cleared and the close still could not certify itself, that is a failure and not a policy withhold, so the result comes back `ok: false` with that value as `stage` and exit 1. Each has its own row in the stage table above.
|
|
249
|
+
|
|
250
|
+
- `compact-gate-not-ok`, `commit-failed: …`: surface the reason verbatim and address it (resolve the compact blocker, fix the git issue) before re-running. The JSON's `gateBlockers[]` names what the checkpoint gate refused on; read the cause there, not from `--check-session-close`, which judges the whole vault on a wider git axis and also lists other sessions' files that did not block this close. One `compact-gate-not-ok` blocker is an uncommitted file in the project folder being closed, which blocks even when this session has no record of writing it: commit it or revert it. **The re-run needs no fresh close phrase from the user for these two.** 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.
|
|
251
|
+
**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, or comes back with `stage: 'receipt-proof-mismatch'` naming those close files, 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.
|
|
252
|
+
- `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.
|
|
253
|
+
- `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. **Read `markerGateReason` first.** It carries the same three values as the authority refusal's `gateReason` (`no-open`, `transcript-rewrite-detected`, `no-new-open-since-resolution`), and only `no-open` means the user never asked. It also names a queued item shaped like an unregistered host tag when one retracted the close, which is a stale allowlist rather than a change of mind: report that name to the user instead of asking them to close again.
|
|
191
254
|
|
|
192
255
|
If the user says stop, end here. Otherwise continue to Step 5.
|
|
193
256
|
|
|
@@ -206,6 +269,8 @@ otherwise omit it.
|
|
|
206
269
|
|
|
207
270
|
An unrecognized flag exits 2 instead of being ignored.
|
|
208
271
|
|
|
272
|
+
Its verdict covers the whole vault, so it can stay red on another session's uncommitted file after your own close has landed. Whether this session is closed is decided by the close checkpoint receipt: pass `--session-id=<id>` and read `close_state` (`closed` once the receipt is valid), not the vault-wide verdict.
|
|
273
|
+
|
|
209
274
|
Show the output to the user. If no candidates are found, tell them Hypomnema looks well-connected and no crystallization is needed.
|
|
210
275
|
|
|
211
276
|
---
|
package/commands/doctor.md
CHANGED
|
@@ -51,6 +51,7 @@ Then show the summary line: `Result: N passed, N warnings, N failed`
|
|
|
51
51
|
For any `✗` failures:
|
|
52
52
|
- Missing Hypomnema root or required directories/files → run `/hypo:init`.
|
|
53
53
|
- 0 hook files installed → run `/hypo:init`.
|
|
54
|
+
- Partial hook files on an npm or manual install (some missing) → run `/hypo:init`; it copies the missing ones. The detail names up to three of them, so report those names rather than only the count.
|
|
54
55
|
- 0 hook registrations in settings.json → run `/hypo:init`.
|
|
55
56
|
- `Hook execution smoke test` failed → show the stderr excerpt in the detail
|
|
56
57
|
verbatim; it names the cause. The repair depends on the channel, and
|
|
@@ -65,7 +66,8 @@ For any `✗` failures:
|
|
|
65
66
|
For `⚠` warnings:
|
|
66
67
|
- Missing `hypo-config.md` → run `/hypo:init` — it creates the config marker.
|
|
67
68
|
- Missing baseline files (`index.md`, `hot.md`, etc.) → run `/hypo:init`.
|
|
68
|
-
- Partial hook files (some missing) →
|
|
69
|
+
- Partial hook files on a plugin install (some missing, and `Hook files installed` says the plugin loader supplies them) → leftovers from an earlier non-plugin install. **Read `settings.json hook registrations` before calling it harmless.** Stale files alone do nothing, because the loader is what runs the hooks; stale files plus surviving settings.json entries mean every hook named there fires twice. That check reports the duplicate case on its own, so go by what it says. This one stays a `⚠` on purpose: nothing the user can run copies hooks on that channel, so failing over it would be a red they cannot clear.
|
|
70
|
+
- Hypomnema hooks registered in settings.json while the plugin loader is active → a double registration, not a partial one. Each named hook runs twice per event. Report it and give the user the removal step the detail names; do NOT tell them to run `/hypo:init`, which skips settings.json entirely on this channel.
|
|
69
71
|
- Partial settings.json registrations → run `/hypo:init` to merge missing entries.
|
|
70
72
|
- Missing git remote → `git -C <hypo-dir> remote add origin <url>`.
|
|
71
73
|
- `Installed cache vs marketplace HEAD` says the install is behind → the plugin
|