hypomnema 1.9.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@
11
11
  "name": "hypo",
12
12
  "source": "./",
13
13
  "description": "LLM-native personal wiki — session-aware knowledge base for Claude Code",
14
- "version": "1.9.0",
14
+ "version": "1.9.1",
15
15
  "homepage": "https://github.com/sk-lim19f/Hypomnema"
16
16
  }
17
17
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hypo",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "LLM-native personal wiki system — session-aware knowledge base for Claude Code",
5
5
  "author": {
6
6
  "name": "sk-lim19f",
package/CHANGELOG.md CHANGED
@@ -5,6 +5,39 @@ All notable changes to Hypomnema are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.1] - 2026-10-12
9
+
10
+ ### Bug Fixes
11
+ #### English
12
+ - feedback-sync keeps drafts and copies of your text out of git in a vault that is not a git repo (judged by what git really ignores), appends the managed block to CLAUDE.md instead of rewriting it, and no longer loses a "kept the hand-written line" notice from a run nobody saw. ([#314](https://github.com/sk-lim19f/Hypomnema/pull/314))
13
+ - feedback-sync drafts of your own CLAUDE.md and MEMORY.md text are now written under `.cache/feedback-drafts/`, only when the vault's git ignores `.cache/`, never over another file, and old drafts git can see are reported ([#313](https://github.com/sk-lim19f/Hypomnema/pull/313))
14
+ - `feedback-sync` adds `--accept-wiki` for hand-edited managed blocks, removes exactly the hand-written index line a promoted bootstrap draft came from, keeps a copy before every write that can destroy your own text, never overwrites a side file you edited, leaves marker examples inside code alone, and PreCompact and doctor now show edited side files and write-time warnings. ([#312](https://github.com/sk-lim19f/Hypomnema/pull/312))
15
+ - Lint ignores session-log and design-history headings inside code fences, HTML comments and frontmatter, ends a fence one session left open at the next session entry, and session close lists W19 (a heading date not on the calendar) as a non-blocking notice instead of printing "lint clean". ([#311](https://github.com/sk-lim19f/Hypomnema/pull/311))
16
+ - `--check-session-close` and the close docs no longer say `/compact` is blocked. The verdict line says what it measures, the text output names `close_state` with `--session-id`, and a receipt is described as recording the close checkpoint with the Stop hook having the final say. ([#310](https://github.com/sk-lim19f/Hypomnema/pull/310))
17
+ - A session-log heading with a date that is not on the calendar (`2026-02-30`, month 13) no longer invents a date or a day gap. Lint names it and tells you to fix it; when it is the only reason for a finding, it is a new non-blocking warning, W19, so a typo heading alone no longer blocks close. ([#309](https://github.com/sk-lim19f/Hypomnema/pull/309))
18
+ - When the vault sits inside a larger git repository under a directory whose name starts with a space, close and commit attribution now see the vault's uncommitted files instead of none. Paths printed by git are no longer trimmed, only their line ending is removed. ([#308](https://github.com/sk-lim19f/Hypomnema/pull/308))
19
+ - feedback-sync writes its MEMORY.md index lines as `- [Title](file): summary` and its learned-behavior lines as `- [date] rule. 근거: [[slug]]`, with no dash separator. Blocks written in the old form are rewritten once on the next sync, and `--bootstrap` reads both forms. ([#307](https://github.com/sk-lim19f/Hypomnema/pull/307))
20
+ #### 한국어
21
+ - git 저장소가 아닌 볼트에서도 feedback-sync 의 초안과 사용자 글 사본이 git 에 들어가지 않고(git 이 실제로 무시하는지로 판정), CLAUDE.md 를 다시 쓰지 않고 관리 블록을 덧붙이며, 아무도 보지 못한 실행의 "손으로 쓴 줄을 남겼다" 알림을 더 이상 잃지 않습니다. ([#314](https://github.com/sk-lim19f/Hypomnema/pull/314))
22
+ - 직접 쓴 CLAUDE.md·MEMORY.md 를 복사한 feedback-sync 초안을 이제 `.cache/feedback-drafts/` 에, 볼트의 git 이 `.cache/` 를 무시할 때만, 다른 파일을 덮지 않고 쓰며, git 이 볼 수 있는 옛 초안은 알려 줍니다 ([#313](https://github.com/sk-lim19f/Hypomnema/pull/313))
23
+ - `feedback-sync` 에 손으로 고친 관리 블록을 위한 `--accept-wiki` 가 생기고, 승격된 bootstrap 초안의 근거가 된 손 색인 줄만 정확히 지우며, 사용자 글을 지울 수 있는 쓰기 전에 항상 사본을 남기고, 사용자가 고친 side 파일을 덮지 않고, 코드 안의 마커 예시를 건드리지 않습니다. PreCompact 와 doctor 는 고친 side 파일과 쓰기 시점 경고를 보입니다. ([#312](https://github.com/sk-lim19f/Hypomnema/pull/312))
24
+ - lint 가 코드펜스, HTML 주석, frontmatter 안의 session-log·design-history 헤딩을 읽지 않고, 한 세션이 열어 둔 펜스를 다음 세션 항목에서 끊으며, session close 는 "lint clean" 대신 W19(달력에 없는 헤딩 날짜)를 막지 않는 알림으로 보입니다. ([#311](https://github.com/sk-lim19f/Hypomnema/pull/311))
25
+ - `--check-session-close` 와 close 문서가 더 이상 `/compact` 가 막혀 있다고 말하지 않습니다. 판정 줄은 실제로 재는 것을 말하고, `--session-id` 를 주면 텍스트 출력이 `close_state` 를 이름째로 보이며, 영수증은 close 체크포인트를 기록하는 것이고 최종 판정은 Stop 훅이 한다고 설명합니다. ([#310](https://github.com/sk-lim19f/Hypomnema/pull/310))
26
+ - 달력에 없는 날짜(`2026-02-30`, 13월)를 단 session-log 헤딩이 더 이상 날짜나 일 차이를 지어내지 않습니다. lint 가 그 헤딩을 짚어 고치라고 안내하고, 그것만이 이유일 때는 막지 않는 새 경고 W19 로 냅니다. 오타 헤딩 하나만으로는 close 가 막히지 않습니다. ([#309](https://github.com/sk-lim19f/Hypomnema/pull/309))
27
+ - 볼트가 더 큰 git 저장소 안에서 이름이 공백으로 시작하는 디렉터리 아래에 있을 때, close 와 커밋 귀속이 볼트의 미커밋 파일을 하나도 못 보던 문제를 고쳤습니다. git 이 출력한 경로는 더 이상 trim 하지 않고 줄 끝만 지웁니다. ([#308](https://github.com/sk-lim19f/Hypomnema/pull/308))
28
+ - feedback-sync 가 MEMORY.md 색인 줄을 `- [제목](파일): 설명`, learned-behavior 줄을 `- [날짜] 규칙. 근거: [[slug]]` 형식으로 대시 구분자 없이 씁니다. 옛 형식으로 쓰인 블록은 다음 동기화 때 한 번 다시 쓰이고, `--bootstrap` 은 두 형식을 모두 읽습니다. ([#307](https://github.com/sk-lim19f/Hypomnema/pull/307))
29
+
30
+ ### Changelog
31
+ - [#314](https://github.com/sk-lim19f/Hypomnema/pull/314) fix(feedback-sync): keep user text out of git and stop losing notices an old hook never showed
32
+ - [#313](https://github.com/sk-lim19f/Hypomnema/pull/313) fix(feedback-sync): keep drafts of your own text out of git and never write over another file
33
+ - [#312](https://github.com/sk-lim19f/Hypomnema/pull/312) fix(feedback-sync): resolve hand-edited blocks and keep a copy before every destructive write
34
+ - [#311](https://github.com/sk-lim19f/Hypomnema/pull/311) fix(lint): ignore session-log headings in fences, comments and frontmatter, and show W19 at close
35
+ - [#310](https://github.com/sk-lim19f/Hypomnema/pull/310) fix(close): stop telling the model that /compact is blocked
36
+ - [#309](https://github.com/sk-lim19f/Hypomnema/pull/309) fix(lint): keep calendar-overflow session-log dates as written and report them as W19
37
+ - [#308](https://github.com/sk-lim19f/Hypomnema/pull/308) fix(hooks): keep spaces in git-printed paths, strip only the line ending
38
+ - [#307](https://github.com/sk-lim19f/Hypomnema/pull/307) fix(feedback-sync): write projected lines without a dash separator
39
+ Contributors: @sk-lim19f
40
+
8
41
  ## [1.9.0] - 2026-09-30
9
42
 
10
43
  ### New Features
@@ -95,6 +128,11 @@ Contributors: @sk-lim19f
95
128
 
96
129
  - 위키 결정 문서 포인터를 이 changelog 를 포함한 모든 공개 표면에서 없앴습니다. 레포가 그 문서를 담은 디렉터리를 출하하지 않아서, 관리자의 개인 기록 밖에 있는 독자에게는 아무것도 가리키지 않는 포인터였습니다. ([#294](https://github.com/sk-lim19f/Hypomnema/pull/294))
97
130
 
131
+ ### Notes
132
+
133
+ - This version shipped on the plugin channel only. It was never tagged and never published to npm, so `npm view` does not list it.
134
+ - 이 버전은 플러그인 채널로만 나갔습니다. 태그와 npm 발행이 없어서 `npm view` 목록에는 없습니다.
135
+
98
136
  ## [1.8.2] - 2026-09-10
99
137
 
100
138
  ### Bug Fixes
package/README.ko.md CHANGED
@@ -436,7 +436,7 @@ Claude(또는 다른 코딩 에이전트)로서 Hypomnema 위키를 운영한다
436
436
 
437
437
  위키가 단일 원천입니다. 사용자가 기억해 달라고 한 것은 전부 위키 루트 아래 마크다운으로 있고, 기억에 의존해 답하기 전에 그것부터 읽습니다. 한 프로젝트의 배경은 `projects/<name>/hot.md`에, 다음에 할 일은 `projects/<name>/session-state.md`에 있으며 `/hypo:resume`이 둘 다 불러옵니다. 위키 루트의 `hot.md`는 다른 파일입니다. 어떤 프로젝트가 활성인지 가리키는 포인터 테이블입니다.
438
438
 
439
- 그 상태를 위키 밖에 또 두지 마세요. `MEMORY.md`와 `~/.claude/CLAUDE.md`의 `<learned_behaviors>` 블록은 스스로의 메모리처럼 보이지만, Hypomnema가 `pages/feedback/`에서 뽑아내는 단방향 projection입니다. 교정은 `/hypo:feedback`으로 적고 동기화가 다시 만들게 두면 됩니다. projection을 손으로 고치면 다음 동기화가 그것을 병합하지 않고 conflict로 멈춥니다. 어느 쪽이 진실인지 판단할 근거가 없기 때문이고, 그때부터 feedback 페이지로 되돌려 정리하는 일이 남습니다. 위키에 있어야 할 내용을 혼자만 보는 곳에 따로 챙겨 두면, 사용자의 다른 기기에는 끝내 닿지 않습니다.
439
+ 그 상태를 위키 밖에 또 두지 마세요. `MEMORY.md`와 `~/.claude/CLAUDE.md`의 `<learned_behaviors>` 블록은 스스로의 메모리처럼 보이지만, Hypomnema가 `pages/feedback/`에서 뽑아내는 단방향 projection입니다. 교정은 `/hypo:feedback`으로 적고 동기화가 다시 만들게 두면 됩니다. projection을 손으로 고치면 다음 동기화가 그것을 병합하지 않고 conflict로 멈춥니다. 어느 쪽이 진실인지 판단할 근거가 없기 때문이고, 그때부터 feedback 페이지로 되돌려 정리하는 일이 남습니다. 손으로 고친 내용이 위키가 쓸 내용과 이미 같다면 예외로, 다음 `--write`가 그대로 받아들입니다. 충돌한 블록을 위키 버전으로 되돌리려면 먼저 `hypomnema feedback-sync --import-target-change --from=<target>`으로 고친 내용을 초안으로 남겨 두고, 이어서 `hypomnema feedback-sync --accept-wiki=<slug>`를 실행하세요. 위키에 있어야 할 내용을 혼자만 보는 곳에 따로 챙겨 두면, 사용자의 다른 기기에는 끝내 닿지 않습니다.
440
440
 
441
441
  `sources/`는 불변입니다. 그 아래는 무엇도 고치지 않습니다. 대신 `pages/`로 합성하고 출처를 링크로 되짚습니다.
442
442
 
package/README.md CHANGED
@@ -436,7 +436,7 @@ If you are Claude (or another coding agent) operating a Hypomnema wiki, these ar
436
436
 
437
437
  The wiki is the single source of truth. Everything the user asked you to remember about their work is markdown under the wiki root, and you read it before answering from recollection. `projects/<name>/hot.md` holds the background for one project, `projects/<name>/session-state.md` holds what to do next, and `/hypo:resume` loads both. The `hot.md` at the wiki root is a different file: a pointer table of which projects are active.
438
438
 
439
- Keep no second copy of that state. `MEMORY.md` and the `<learned_behaviors>` block inside `~/.claude/CLAUDE.md` look like your own memory, but they are one-way projections that Hypomnema derives from `pages/feedback/`. Record a correction with `/hypo:feedback` and let the sync regenerate them. Edit a projection by hand and the next sync stops on it as a conflict instead of merging it, because nothing can tell which side is now the truth; reconciling it back into the feedback page becomes your job. And a wiki fact you stash somewhere only you can see never reaches the user's other machines at all.
439
+ Keep no second copy of that state. `MEMORY.md` and the `<learned_behaviors>` block inside `~/.claude/CLAUDE.md` look like your own memory, but they are one-way projections that Hypomnema derives from `pages/feedback/`. Record a correction with `/hypo:feedback` and let the sync regenerate them. Edit a projection by hand and the next sync stops on it as a conflict instead of merging it, because nothing can tell which side is now the truth; reconciling it back into the feedback page becomes your job. The one exception is a hand edit that already matches what the wiki would write, which the next `--write` takes over. To keep the wiki version of a conflicted block instead, run `hypomnema feedback-sync --import-target-change --from=<target>` first to save a copy of the edit as a draft, then `hypomnema feedback-sync --accept-wiki=<slug>`. And a wiki fact you stash somewhere only you can see never reaches the user's other machines at all.
440
440
 
441
441
  Captured sources are immutable. Never edit anything under `sources/`. Synthesize into `pages/` instead, and link back to the source.
442
442
 
@@ -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).** 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.
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 the close check is green. 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.
@@ -114,14 +114,14 @@ the apply first files a commit-backed close checkpoint receipt at
114
114
  `HYPO_DIR/.cache/sessions/<id>/close-receipt.json`, then writes the per-session compat marker
115
115
  `HYPO_DIR/.cache/session-closed-<id>.marker` as a projection of it. If the marker does not land,
116
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
117
+ receipt behind. The checkpoint gate reads git more narrowly than the PreCompact gate and `--check-session-close` do. An uncommitted
118
118
  file inside the project folder being closed (`projects/<project>/`) blocks whether or not this
119
119
  session has a record of writing it. An uncommitted file at the vault root or in another project's
120
120
  folder blocks only when this session wrote it through Write or Edit since its last auto-commit;
121
121
  otherwise it is reported as a notice. If the record of what this session wrote cannot be read, the
122
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
123
+ not determine who owns those files. So the checkpoint can land while `--check-session-close` stays red on
124
+ someone else's dirty file. Neither one stops `/compact`: the PreCompact hook only shows a notice. The checkpoint receipt is the thing the
125
125
  Stop-chain Layer 3 hook (`hypo-auto-minimal-crystallize`) actually checks: it proves only that
126
126
  the file versions it names are in a specific commit, never that every change this session made
127
127
  is saved. (It has nothing to do with the `close-receipt-failed` result of `proposal resolve`
@@ -190,7 +190,7 @@ Both gates judge only the **payload files** (the 4 mandatory close files + `open
190
190
  >
191
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
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.
193
+ > The close checkpoint is recorded once the receipt is issued: `ok: true` from this command, and `close_state: closed` from `--check-session-close --session-id=<id>`. The Stop hook still checks this session's own project folder and has the final say. The final "Close check" line of `--check-session-close` covers the whole vault, so another session's uncommitted file can keep it red after your close has landed. A red line never stops `/compact`: PreCompact only shows a notice.
194
194
 
195
195
  ---
196
196
 
@@ -221,7 +221,8 @@ Once `ok: true`, report from the result JSON's `applied` and `skipped` arrays to
221
221
  - ✓ session-log entry appended (or already present)
222
222
  - ✓ open-questions applied (or skipped if unchanged)
223
223
  - ✓ log.md entry appended (or already present)
224
- - ✓ post-apply lint clean
224
+ - ✓ post-apply lint: no blockers (say "lint clean" only when `lintNotices` is empty and `notices[]` and `otherDebtCount` show no lint debt; otherwise say "no lint blockers")
225
+ - **`lintNotices[]`** (report any entries): non-blocking W19 findings, one `{id, file, message}` per project that has calendar-overflow session-log headings (a date like `2026-02-30` that is not on the calendar). The message lists every overflowing heading and its session-log file; the `file` field is that project's `design-history.md`. Report each entry's message verbatim. They never block the close or flip `ok`; the fix is to correct the date in that session-log heading.
225
226
  - **`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
227
  - **`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.
227
228
  - **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.
@@ -232,7 +233,7 @@ Once `ok: true`, report from the result JSON's `applied` and `skipped` arrays to
232
233
  - **`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
234
  - **`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
235
 
235
- If `markerWritten: true`: ask: "Session closed. Would you like to also run knowledge synthesis now, or stop here?"
236
+ If `markerWritten: true`: ask: "The close checkpoint is recorded. Would you like to also run knowledge synthesis now, or stop here?"
236
237
 
237
238
  **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.
238
239
 
@@ -247,7 +248,7 @@ If `markerWritten: true`: ask: "Session closed. Would you like to also run knowl
247
248
 
248
249
  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
 
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
+ - `compact-gate-not-ok`, `commit-failed: …`: surface the reason verbatim and address it (resolve the gate 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
252
  **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
253
  - `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
254
  - `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.
@@ -269,8 +270,6 @@ otherwise omit it.
269
270
 
270
271
  An unrecognized flag exits 2 instead of being ignored.
271
272
 
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
-
274
273
  Show the output to the user. If no candidates are found, tell them Hypomnema looks well-connected and no crystallization is needed.
275
274
 
276
275
  ---
@@ -341,7 +340,7 @@ Show what was created or modified, and offer to run `/hypo:lint` to verify all n
341
340
  `--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:
342
341
 
343
342
  ```bash
344
- node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs --check-session-close
343
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/crystallize.mjs --check-session-close --session-id=<current-session-id>
345
344
  ```
346
345
 
347
346
  Add `--hypo-dir="<path>"` only when the user specified a Hypomnema directory explicitly;
@@ -349,8 +348,10 @@ otherwise omit it.
349
348
 
350
349
  An unrecognized flag exits 2 instead of being ignored.
351
350
 
351
+ Its verdict covers the whole vault (with `--session-id` for a session whose close was log-only, the project-close checks are skipped), 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, or `legacy-closed` for an older marker written before receipts existed), not the vault-wide verdict. The Stop hook still checks this session's own project folder and has the final say.
352
+
352
353
  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.
353
354
 
354
- 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.
355
+ 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 the whole vault is clean.
355
356
 
356
357
  On the marker writer (`--mark-session-closed --project=<slug>`), `--project` names the project this session closed: it sets the marker's attribution slug and enters the close scope, so another session's incomplete close is reported as `close_debt` (a notice) instead of refusing your marker. Every other gate check stays global.
@@ -78,6 +78,16 @@ When `--targets` includes `claude-learned`, `--global-summary` and `--promote-to
78
78
 
79
79
  On a real (non-dry-run) write, the script automatically runs `feedback-sync --write` to refresh MEMORY.md / CLAUDE.md. If that post-step reports drift it prints a one-line warning — the page is still saved; reconcile with `hypomnema feedback-sync --check`.
80
80
 
81
+ A projection block you edited by hand is a conflict, except when the edit already matches what the wiki would write: the next `--write` takes it over. To keep the wiki version of a conflicted block, run `hypomnema feedback-sync --import-target-change --from=<target>` first to save a copy of the edit as a draft, then `hypomnema feedback-sync --accept-wiki=<slug>`. After the accept, the text you edited exists only under `.cache/`: in the kept copy of the whole file (`.cache/feedback-kept/`) and in that import draft (`.cache/feedback-drafts/`). Both are outside git and your other machines and are gone when `.cache/` is cleared, so the accept prints their paths as a warning (and in the `warnings` list of its `--json` report). Move what you want to keep to `pages/feedback/` or elsewhere before you clear `.cache/`. The accept itself does not move anything.
82
+
83
+ `--bootstrap` and `--import-target-change` write their drafts to `.cache/feedback-drafts/` in the wiki, not under `pages/feedback/`. A draft is a copy of your own hand-written lines or edited block, so it goes where the wiki's git does not look: in a wiki that is a git work tree nothing is written unless git ignores the whole `.cache/` directory (the default `.gitignore` does; a rule that names only the drafts is not enough, because the temp file a draft is written to first would still be visible), and otherwise the command exits 1 and says why. The same goes when git itself cannot answer (a broken `.git/config`, an ownership refusal): that is not read as "not a git work tree", so nothing is written. A wiki that git reports is not a git work tree has nothing that could stage them, so the drafts are written as they are. A run with nothing to draft skips this check and exits 0. If the hand line record cannot be saved, `--bootstrap` exits 1 before it publishes any draft (a draft without its record would be skipped as already there by every later run), so fix the cause and run it again. A draft has no `sensitivity` yet (`TODO`): set `public` or `sanitized` when you move it into `pages/feedback/`. The cost is that drafts are not synced to your other machines and are gone when `.cache/` is cleared. Drafts that an earlier version left in `pages/feedback/_drafts/` still count: `--bootstrap` does not draft the same slug again, and `--write` keeps the hand line record while one is there. Nothing moves or deletes them. Such a file is a copy of your own text that the earlier version wrote with `sensitivity: public`, and git can see it unless your `.gitignore` covers it: `--bootstrap`, `--import-target-change` and `--write` warn when it does, so move it to `.cache/feedback-drafts/` (`--bootstrap` and `--write` read it there just the same) or delete it. A slug skipped because of one of these files is reported as `draft-exists-legacy` with its path. A draft is created only if its name is free, and it appears under that name only once complete, so a second run or a crash never replaces or cuts one in half.
84
+
85
+ Before `--accept-wiki` replaces a file, before `--write` replaces `MEMORY.md` or `CLAUDE.md`, and before `--write` removes a hand line that `--bootstrap` drafted from, the old bytes are copied to `.cache/feedback-kept/` in the wiki. The copy holds your own text, so in a wiki that is a git work tree it is only written when git ignores `.cache/` (the default `.gitignore` does; a wiki that is not a git work tree has nothing that could stage it yet, so there the command adds a `.cache/` line to its `.gitignore` first, creating the file or appending one line (an append, so a symlinked `.gitignore` stays a link and keeps its mode) and saying so in the run's warnings, then asks git itself, in a throwaway repository over the wiki, whether `.cache/` is really ignored, and stops with exit 1 and writes nothing if it is not (a later `!.cache/` line overrides the rule: remove it or add a `.cache/` line after it) or if it cannot add the line; that way a later `git init` and `git add -A` does not stage the copy. Without git on the machine the line is added and the check is skipped, which the warning says); otherwise the command stops with exit 1 and writes nothing, and the same goes for the bootstrap line record. The one exception is the routine before-write copy of `MEMORY.md` or `CLAUDE.md`: in such a vault `--write` skips that copy, warns, and goes ahead. The same copy is made before `--write` rewrites or removes a generated `feedback_<slug>.md` that has no recorded hash (an older install, or a lost record); if that copy cannot be kept, that one file is left as it is and `--write` warns. Only the newest 10 copies of each kind are kept, and the older ones are deleted only after a run that ended with exit 0 or 3. The warnings a `--write` raises (a skipped copy, a side file it left alone, a record it could not save) are also in the `warnings` list of its `--json` report. The notice that a `--write` kept a hand line is printed once, and a `--write` whose output nobody may read (an older PreCompact hook discards it) also saves it on this machine, in `state/feedback-pending-notices-<id>.json` under the Claude home (outside the wiki, one file per wiki, so it needs no `.gitignore` rule and a wiki that does not ignore `.cache/` does not lose it; a file an earlier version kept in the wiki's `.cache/` is read once and deleted); the next run that shows its warnings to you (a `--check` or `--write` you run by hand, this command's post-step, the current PreCompact hook) reports it, and removes it when that run ends with exit 0 (a run that fails keeps it, so it is shown again). `--ensure-container` does not replace `CLAUDE.md` at all: it adds the container to the end with one append, so a save from your editor at the same moment is never overwritten and no copy is needed. If a run was cut off in the middle of that append, the next `--ensure-container` finishes it; text after the container tags that is not our own addition is still refused as corrupt. `--accept-wiki` reads each target again right before it copies and replaces it and stops with exit 1, writing nothing to that target, when it changed since the plan was made. `--accept-wiki` exits 3 when it accepted some target but another one that holds the block still needs a hand repair. That includes two blocks that lost an END and a START marker and now read as one: accept refuses to replace them.
86
+
87
+ A managed-block marker counts only when it starts its own line outside a code fence. An example of the markers inside a fenced block, inside inline code, or after other text on the same line is left alone. A fence that is never closed does not count as a fence, so marker lines below a stray ``` are still read as a real block; an example placed inside such an unclosed fence is rewritten like one, and the file's earlier bytes are in `.cache/feedback-kept/` when that copy could be kept.
88
+
89
+ A `feedback_<slug>.md` copy that you edited after `--write` generated it is neither rewritten nor removed. The run exits 3 and names the file: move your edits out and delete it, then `--write` generates it again. The hash of each generated copy is kept in `.cache/feedback-side-files.json`; a copy with no recorded hash is refreshed once and recorded. Every writing mode takes a lock on the wiki, and `--write` reads each file again right before replacing it, so a file that changed after the check is not overwritten (exit 1, run it again).
90
+
81
91
  ---
82
92
 
83
93
  ## Step 4 — Confirm
@@ -128,6 +128,7 @@ Everything else belongs in `commands/`, which loads the same way.
128
128
  npm test # tests/*.test.mjs, sharded across processes — unit + smoke + contract
129
129
  npm run lint # scripts/lint.mjs — frontmatter + wikilink validation + W8 (design-history stale
130
130
  # vs session-log) + W14 (design-history missing but session-log implies one)
131
+ # + W19 (session-log heading date not on the calendar, no real later date)
131
132
  # + W15 (synthesis older than the newest page in its sources_consulted)
132
133
  # + W16 (sources_consulted content problems: a source name W15 could not
133
134
  # resolve, a source or the synthesis's own updated that fails a strict
@@ -275,6 +275,11 @@ export function readReceiptStrict(hypoDir, sessionId) {
275
275
  if (toplevelRes.error || toplevelRes.status !== 0) {
276
276
  return { status: 'invalid', reason: 'not-a-git-repository' };
277
277
  }
278
+ // trim() on purpose, unlike the path outputs elsewhere: this value is only ever compared with
279
+ // the toplevel the receipt was issued with, which repoIdentity trims the same way, so both
280
+ // sides agree. Changing one side alone would void receipts issued by the other (an npm
281
+ // install can run a newer issuer against an older deployed copy of this file). If something
282
+ // ever uses this as a path, change both sides together.
278
283
  const actualToplevel = (toplevelRes.stdout || '').trim();
279
284
  if (actualToplevel !== receipt.repo.toplevel) {
280
285
  return { status: 'invalid', reason: 'repo-mismatch' };
@@ -50,11 +50,11 @@ process.stdin.on('end', () => {
50
50
  const input = JSON.parse(raw || '{}');
51
51
  transcriptPath = input.transcript_path ?? null;
52
52
  // A log-only marker for this session activates log-only gate
53
- // semantics (no project attribution) so /compact does not block a closed
53
+ // semantics (no project attribution) so the notice does not flag a closed
54
54
  // non-project session on the active/phantom project's files.
55
55
  sessionId = input.session_id ?? input.sessionId ?? null;
56
56
  // Authoritative session cwd for the session-cwd close check. This is the one
57
- // verified cwd source (mirrors hypo-session-record); it lets /compact block a
57
+ // verified cwd source (mirrors hypo-session-record); it lets the notice name a
58
58
  // session whose own project close was never started, which the recency-based
59
59
  // global status cannot see.
60
60
  sessionCwd = input.cwd ?? null;
@@ -101,7 +101,7 @@ process.stdin.on('end', () => {
101
101
 
102
102
  // The full PreCompact gate decision, single-sourced. The SAME
103
103
  // function backs `crystallize --check-session-close`, so a green self-check
104
- // there means this hook will not block. precompactGateStatus runs git-clean +
104
+ // there means this hook will raise no close notice. precompactGateStatus runs git-clean +
105
105
  // hot.md structure + session-close files (global invariant) + scoped
106
106
  // lint + W8 design-history + feedback projection. The transcript widens the
107
107
  // lint scope to this session's edited files; without one the scope
@@ -152,6 +152,11 @@ process.stdin.on('end', () => {
152
152
  // file can land before another fails), which the preflight narrows to genuine
153
153
  // fs errors and no further.
154
154
  let feedbackHealed = '';
155
+ // Notices the --write itself reports: a hand line it kept, and its write-time
156
+ // warnings (or, with no drift, the pending ones a --check shows). Its JSON report is the only place those exist (the record entry behind a
157
+ // kept line is dropped by that same write), so they are turned into notice lines
158
+ // below. A report that is missing or malformed yields none.
159
+ const healNotices = [];
155
160
  if (gate.ok && gate.driftTargets.length > 0) {
156
161
  const feedbackPath = PKG_ROOT ? join(PKG_ROOT, 'scripts', 'feedback-sync.mjs') : null;
157
162
  const w = feedbackPath
@@ -161,6 +166,10 @@ process.stdin.on('end', () => {
161
166
  feedbackPath,
162
167
  '--write',
163
168
  '--no-input',
169
+ '--json',
170
+ // this hook shows the report's warnings below, so notices an older hook run
171
+ // left pending can be reported (and cleared) here
172
+ '--ack-notices',
164
173
  `--hypo-dir=${HYPO_DIR}`,
165
174
  `--claude-home=${join(homedir(), '.claude')}`,
166
175
  ],
@@ -174,8 +183,63 @@ process.stdin.on('end', () => {
174
183
  reason: `feedback projection drift (${gate.driftTargets.join(', ')}) — auto-sync failed; run \`hypomnema feedback-sync --write\` manually`,
175
184
  });
176
185
  } else {
186
+ try {
187
+ const report = JSON.parse(w.stdout || '');
188
+ for (const [name, t] of Object.entries(report.targets || {})) {
189
+ for (const k of Array.isArray(t.handKept) ? t.handKept : []) {
190
+ healNotices.push(
191
+ `[WIKI CHECK] feedback-sync kept the hand-written line in ${k.file} that bootstrap drafted "${k.slug}" from (${name}); it was changed, moved or duplicated, or links another file, so it was not removed. Delete it by hand if it now duplicates the managed entry: ${k.line}`,
192
+ );
193
+ }
194
+ // sideWarnings are not repeated here: the gate's own side-file notice
195
+ // already carries them, and a second line would show the same warning twice.
196
+ }
197
+ // Warnings raised while writing (a before-write copy skipped, a side file left
198
+ // alone, a record that could not be saved) exist only in this report, so they
199
+ // are shown too, except a sentence the gate's own notices already carry.
200
+ const shown = new Set();
201
+ for (const m of Array.isArray(report.warnings) ? report.warnings : []) {
202
+ if (typeof m !== 'string' || shown.has(m)) continue;
203
+ if (gate.notices.some((n) => typeof n.reason === 'string' && n.reason.includes(m)))
204
+ continue;
205
+ shown.add(m);
206
+ healNotices.push(`[WIKI CHECK] feedback-sync warning while re-syncing: ${m}`);
207
+ }
208
+ } catch {
209
+ /* fail open: no report, no extra notices */
210
+ }
177
211
  feedbackHealed = `[WIKI CHECK] feedback projection re-synced (${gate.driftTargets.join(', ')}); MEMORY.md body may be unchanged — drift was in the managed block / side-files.`;
178
212
  }
213
+ } else if (PKG_ROOT) {
214
+ // No drift to repair, but an older hook's --write may have repaired it and left a kept-line
215
+ // notice pending (that run's output was thrown away). --check --ack-notices reports pending
216
+ // notices and removes them on exit 0, so they reach a reader here. Only those notices are
217
+ // shown: --check's other warnings (a skipped MEMORY projection) are not new information
218
+ // on every compact. Fail open: any fault means no extra notice.
219
+ try {
220
+ const c = spawnSync(
221
+ process.execPath,
222
+ [
223
+ join(PKG_ROOT, 'scripts', 'feedback-sync.mjs'),
224
+ '--check',
225
+ '--no-input',
226
+ '--json',
227
+ '--ack-notices',
228
+ `--hypo-dir=${HYPO_DIR}`,
229
+ `--claude-home=${join(homedir(), '.claude')}`,
230
+ ],
231
+ { encoding: 'utf-8', timeout: 30000 },
232
+ );
233
+ if (!c.error && c.status === 0) {
234
+ const report = JSON.parse(c.stdout || '');
235
+ for (const m of Array.isArray(report.warnings) ? report.warnings : []) {
236
+ if (typeof m === 'string' && /kept the hand-written line in /.test(m))
237
+ healNotices.push(`[WIKI CHECK] feedback-sync notice: ${m}`);
238
+ }
239
+ }
240
+ } catch {
241
+ /* fail open */
242
+ }
179
243
  }
180
244
 
181
245
  // Non-blocking heads-up, one line per notice TYPE (session-close-scope-boundary
@@ -271,6 +335,7 @@ process.stdin.on('end', () => {
271
335
  noticeLines.push(`[WIKI CHECK] ${n.reason}.`);
272
336
  }
273
337
 
338
+ noticeLines.push(...healNotices);
274
339
  let noticeText = noticeLines.join('\n');
275
340
  // Surface the self-heal so a re-synced projection is not a silent mutation of
276
341
  // the user's MEMORY.md / CLAUDE.md (transparency).
@@ -299,7 +364,11 @@ process.stdin.on('end', () => {
299
364
  console.log(
300
365
  JSON.stringify({
301
366
  continue: true,
302
- systemMessage: `[WIKI CHECK] gate bypassed via HYPO_SKIP_GATE=1 (incomplete: ${skipped}).`,
367
+ // noticeText carries the kept-line notices the no-drift call above already took off the
368
+ // pending file: dropping it here would clear them without anyone seeing them
369
+ systemMessage:
370
+ `[WIKI CHECK] gate bypassed via HYPO_SKIP_GATE=1 (incomplete: ${skipped}).` +
371
+ (noticeText ? `\n${noticeText}` : ''),
303
372
  }),
304
373
  );
305
374
  return;
@@ -335,8 +404,9 @@ process.stdin.on('end', () => {
335
404
  ` [ ] 12. lint — run /hypo:lint; fix errors in files YOU touched`,
336
405
  ` (other projects' / shared-page debt is reported as non-blocking notice)`,
337
406
  ` [ ] 13. git commit & push`,
338
- ` [ ] 14. verify — run /hypo:crystallize (--check-session-close mode); only declare`,
339
- ` the session closed once it prints "Compact-ready" (= this gate passes).`,
407
+ ` [ ] 14. verify: run /hypo:crystallize (--check-session-close --session-id=${sessionId ?? '<id>'});`,
408
+ ` the close is recorded once it reports close_state: closed (or legacy-closed).`,
409
+ ` The Stop hook still checks this session's own project folder and has the final say.`,
340
410
  ].join('\n');
341
411
 
342
412
  const closeIntentNote = hasCloseIntent