hypomnema 1.7.1 → 1.7.3

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/README.md CHANGED
@@ -19,21 +19,23 @@ Make Claude take notes, and measure whether it actually does.
19
19
 
20
20
  [Quick Start](#quick-start) • [How It Compares](#how-it-compares) • [Design Decisions](#design-decisions) • [Features](#features) • [Architecture](docs/ARCHITECTURE.md) • [Contributing](docs/CONTRIBUTING.md)
21
21
 
22
- Inspired by Andrej Karpathy's "LLM-native wiki" sketch, shaped by ten months of personal AI-workflow experiments and a month of dogfooding before the public release. Hypomnema ships the full lifecycle, from capturing a source to synthesizing, retrieving, and resuming a paused session, as Claude Code commands and lifecycle hooks.
22
+ It started from Andrej Karpathy's "LLM-native wiki" sketch, then took shape over ten months of personal AI-workflow experiments and a month of dogfooding before release. The whole cycle (capture a source, synthesize it, find it again, pick a paused session back up) ships as Claude Code commands and hooks.
23
23
 
24
24
  New to the terms below? Keep the [Term decoder](#term-decoder) open in another tab. It defines frontmatter, wikilink, projection, hook, `hot.md`, and `session-state.md`, one line each.
25
25
 
26
26
  ### Where automation stands today
27
27
 
28
- Wiki work (ingest, query, session-close) still starts from an explicit `/hypo:*` command or plain language. The v2 goal is full autonomy: Claude reading, writing, and synthesizing the wiki without being asked, which is the direction this is heading.
28
+ Wiki work (ingest, query, session-close) still starts from an explicit `/hypo:*` command or plain language. The v2 goal is full autonomy: Claude reading, writing, and synthesizing the wiki without being asked.
29
29
 
30
- The lanes that already run on their own: an observability score measures how often the wiki is used per session, and several areas run without a prompt.
30
+ An observability score measures how often the wiki is actually used per session. These already run without a prompt:
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
34
  - Project-aware resume on `cd`. Switching into a git-synced project directory injects that project's `hot.md` automatically, so a code-repo session already knows where the wiki lives and what happened last.
35
35
  - Auto-project creation. When you `cd` into a git repo with a project marker (`package.json`, `Cargo.toml`, etc.) and no matching wiki project exists, Hypomnema offers to scaffold one.
36
- - Session-close cleanup and `/clear` recovery. 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.
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
+
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.
37
39
 
38
40
  What changed per version lives in the [CHANGELOG](CHANGELOG.md).
39
41
 
@@ -45,7 +47,7 @@ Two things that look automatic but aren't: extensions sync and reverse capture.
45
47
 
46
48
  ## Quick Start
47
49
 
48
- There are two install paths. Either one ends up with the same wiki, hooks, and `/hypo:*` slash commands.
50
+ There are two install paths. Either one ends up with the same wiki, hooks, and `/hypo:*` slash commands. Pick one and stay on it: if both are installed, the plugin owns the core hooks and `hypomnema upgrade` deliberately refuses to register them again, printing a dual-install banner. Remove one rather than forcing it with `--allow-dual-install`, which makes every hook fire twice.
49
51
 
50
52
  ### Path A: Claude Code plugin (recommended)
51
53
 
@@ -57,7 +59,7 @@ Inside Claude Code:
57
59
  /hypo:init
58
60
  ```
59
61
 
60
- The plugin install registers `/hypo:*` commands from the package's `commands/` directory; `/hypo:init` then scaffolds the wiki and merges hooks into `~/.claude/settings.json`.
62
+ The plugin install registers `/hypo:*` commands from the package's `commands/` directory and auto-wires the lifecycle hooks from its `hooks/hooks.json`; `/hypo:init` then scaffolds the wiki. On this path, `init` deliberately skips the `~/.claude/hooks/` copy and the `settings.json` merge, because the plugin loader already provides them and installing both would fire every hook twice.
61
63
 
62
64
  ### Path B: npm CLI
63
65
 
@@ -70,8 +72,12 @@ hypomnema
70
72
 
71
73
  `hypomnema` (or `hypomnema --help` for flags) scaffolds the wiki and installs hooks. It also copies the slash command files to `~/.claude/commands/hypo/`, so `/hypo:*` works inside Claude Code afterwards. Later `hypomnema upgrade` runs use per-file SHA tracking, so anything you hand-edited stays put.
72
74
 
75
+ `init` also appends a `claude()` shell function to `~/.zshrc` or `~/.bashrc`, marked between `# hypo-managed:shell-setup:start` and `:end`, so a cd-only session still gets project context. Pass `--no-shell` to skip it, or `--shell-config=<path>` to target a different file. `uninstall` does not remove this block; delete the marked lines by hand.
76
+
73
77
  > Either path: restart Claude Code (or open a new session) after the first run so the new hooks and slash commands are picked up.
74
78
 
79
+ > On a second machine, do not run plain `hypomnema`; that scaffolds a fresh, unrelated wiki. Clone the existing one instead: `hypomnema --from-remote=<git-url>`. It clones into the resolved wiki root, verifies the repo really is a Hypomnema wiki, then installs hooks and slash commands without creating a new git history. The target directory must not already exist.
80
+
75
81
  ### Step 2: use it
76
82
 
77
83
  ```
@@ -100,7 +106,7 @@ Personal knowledge tools fall into five buckets. Each breaks at a different plac
100
106
  | AI-native notebooks (proprietary "second brain" apps) | Feels magical at first | Closed format, no git, opaque retrieval logic, vendor lock-in |
101
107
  | Code-only wikis (auto-generated from a repo) | Zero manual effort | Limited to code; can't capture decisions, research, AI behavior corrections |
102
108
 
103
- Hypomnema lives in the gap between them: structured synthesis on top of plain markdown, driven by Claude Code's lifecycle, version-controlled by git, local-first by default.
109
+ Hypomnema lives in the gap between them. It puts structured synthesis on top of plain markdown, runs off Claude Code's lifecycle, versions everything in git, and stays local by default.
104
110
 
105
111
  ```
106
112
  Note vaults ───► store everything, synthesize nothing
@@ -126,7 +132,7 @@ Hypomnema ───► synthesis · markdown · git · hooks · local
126
132
  | Workflow integration | Native to Claude Code | Separate app | Separate app / browser tab | Separate service | Separate app | Separate site |
127
133
  | Format | Plain markdown + frontmatter | Markdown | Proprietary | Vector store | Proprietary | HTML |
128
134
  | Behavior tuning | `/hypo:feedback` → permanent rules | None | None | None | Sometimes | None |
129
- | Auto-behavior | `/hypo:*` triggers + observability score + autonomous lanes; v2 target = fully autonomous | None | None | None | Black box | None |
135
+ | Auto-behavior | `/hypo:*` triggers + observability score + autonomous behaviors; v2 target = fully autonomous | None | None | None | Black box | None |
130
136
  | Setup cost | One command | One install | Sign-up | Pipeline build | Sign-up | Repo connect |
131
137
  | Lock-in | Zero (markdown + git) | Low | High | Medium | High | Medium |
132
138
 
@@ -135,13 +141,13 @@ Hypomnema ───► synthesis · markdown · git · hooks · local
135
141
  - Synthesis over storage. You don't end up with a graveyard of half-read articles. Each `/hypo:ingest` produces a structured page, and the next ingest on the same topic updates that page instead of adding a new one.
136
142
  - Compounding density. A wiki with 100 sources should not be 100 disconnected pages. The design intent: since ingest merges a new source into an existing page instead of always creating one, page count should grow sub-linearly while cross-links grow faster.
137
143
  - No context switch. You're already in Claude Code. The wiki is one slash command away, with no extra tab, app, or login.
138
- - Future-proof storage. Plain markdown + git will still be readable in 20 years, greps offline, moves to another tool anytime, and stays usable by AI assistants that don't exist yet, with no conversion needed.
144
+ - Future-proof storage. Plain markdown + git will still be readable in 20 years. You can grep it offline, move it to another tool any time, and hand it to AI assistants that don't exist yet, with no conversion.
139
145
 
140
146
  ---
141
147
 
142
148
  ## Term decoder
143
149
 
144
- These are the recurring terms used in the rest of the README. Keep this table open in another tab while you skim.
150
+ These are the recurring terms used in the rest of the README.
145
151
 
146
152
  | Term | Meaning in Hypomnema |
147
153
  |---|---|
@@ -157,12 +163,12 @@ These are the recurring terms used in the rest of the README. Keep this table op
157
163
  | `hot.md` | Per-project cache: "what just happened" (most recent session highlights) |
158
164
  | `session-state.md` | Per-project cache: "what's next" (the resume payload for the next session) |
159
165
  | `.hypoignore` | Glob patterns that exclude paths from every content-injection hook and from `ingest`; a privacy boundary |
160
- | `.hyposcanignore` | Glob patterns that exclude paths from catalog scans only (`lint`, `graph`, `stats`, `query`, `verify`, `doctor`); not a privacy boundary, a matched path still gets committed and read by hooks |
166
+ | `.hyposcanignore` | Glob patterns that exclude paths from catalog scans only (`lint`, `stats`, `query`, `verify`, `doctor`); not a privacy boundary, a matched path still gets committed and read by hooks |
161
167
  | observability score | A per-session metric (search / ingest / feedback activity) that measures whether the wiki is actually being used |
162
168
  | manifest | A small JSON the install scripts write to track exactly which files were installed and at what SHA |
163
169
  | `additionalContext` | The Claude Code hook field that injects extra context into the prompt: where content-injection hooks emit |
164
170
  | byte-equal | A file that comes out of `--apply` bit-for-bit identical to before: the strongest "we did not touch this" guarantee |
165
- | BM25 | A classic full-text ranking algorithm; powers the `/hypo:query` MISS-resistant lookup |
171
+ | BM25 | A classic full-text ranking algorithm; powers the hypo-lookup MISS-resistant lookup on every prompt |
166
172
  | Option C | The policy that `hypomnema upgrade --apply` never overwrites your `SCHEMA.md`: it only writes a migration report you apply by hand |
167
173
 
168
174
  If a term you hit later in the README is missing here, that is a documentation bug. Please open an issue.
@@ -196,14 +202,14 @@ Friction kills personal knowledge systems. If saving a thought takes three click
196
202
  | `SessionStart` | "Where did I leave off?" reading `hot.md` / `session-state.md` |
197
203
  | `UserPromptSubmit` | "Do I already know this?" a BM25 lookup, top-3 inject |
198
204
  | `PreCompact` | "Did I close the session?" the checklist guard |
199
- | `PostToolUse` (Write/Edit) | `git add` |
200
- | `Stop` | `git pull --rebase && git commit && git push` |
205
+ | `PostToolUse` (Write/Edit/MultiEdit) | `git add` |
206
+ | `Stop` | `git commit && git pull --no-rebase && git push` |
201
207
 
202
208
  Once installed, you stop _managing_ the wiki. It just accumulates.
203
209
 
204
210
  ### 4. Why a `hot.md` cache for resume
205
211
 
206
- 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, rebuild it on `Stop`, and inject it on `SessionStart`. Resume is O(1).
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 rebuilds on `Stop`, while each project's own `hot.md` is written by crystallize; both get injected on `SessionStart`. Resume is O(1).
207
213
 
208
214
  ### 5. Why a feedback → behavior pipeline
209
215
 
@@ -228,8 +234,8 @@ Nine commands cover the full capture → retrieval → consolidation cycle.
228
234
  | Command | What it does | When to reach for it |
229
235
  |---|---|---|
230
236
  | `/hypo:ingest` | Saves the raw source under `sources/`; Claude synthesizes a structured page under `pages/`. The shell helper (`scripts/ingest.mjs`) is read-only and only _lists_ pending sources | Anytime you read something worth keeping |
231
- | `/hypo:query` | BM25 retrieval + LLM synthesis with `[[wikilink]]` citations | When you need an answer grounded in your own notes |
232
- | `/hypo:crystallize` | Runs the session-close checklist (steps 1-6) and, on request, synthesizes drafts (steps 7-11) | End of a non-trivial session |
237
+ | `/hypo:query` | Full-text search + LLM synthesis with `[[wikilink]]` citations | When you need an answer grounded in your own notes |
238
+ | `/hypo:crystallize` | Runs the session-close path (Steps 1-4) and, on request, the synthesis scan (Steps 5-7) | End of a non-trivial session |
233
239
  | `/hypo:resume` | Loads the most recent session state for an active project | Coming back to a paused project |
234
240
  | `/hypo:feedback` | Records an AI behavior correction; eligible for promotion to permanent rules | When Claude gets something wrong, or gets it exactly right |
235
241
  | `/hypo:verify` | Audits pages with `verify_by` frontmatter | When time-bound knowledge might have aged out |
@@ -237,27 +243,32 @@ Nine commands cover the full capture → retrieval → consolidation cycle.
237
243
  | `/hypo:graph` | Generates a wikilink dependency graph | When you want to see how the structure grew |
238
244
  | `/hypo:rename` | Renames a page or directory and rewrites inbound `[[wikilinks]]` | When a page or project folder needs a new name |
239
245
 
240
- ### Lifecycle hooks (14)
246
+ ### Lifecycle hooks (15)
241
247
 
242
248
  | Hook | Event | Role |
243
249
  |---|---|---|
250
+ | `hypo-close-guard.mjs` | `PreToolUse` | When a Write/Edit/MultiEdit looks like a session-close write and the transcript carries no close signal from you, ask before it lands |
244
251
  | `hypo-session-start.mjs` | `SessionStart` | Inject `hot.md` / `session-state.md` + `git pull --ff-only` |
245
252
  | `hypo-first-prompt.mjs` | `UserPromptSubmit` | Marker-based one-shot `hot.md` injection (10-min TTL) |
246
253
  | `hypo-lookup.mjs` | `UserPromptSubmit` | BM25 top-3 HIT inject / MISS → closest-slug signal |
247
- | `hypo-compact-guard.mjs` | `UserPromptSubmit` | Detect `/compact` → enforce session-close checklist |
254
+ | `hypo-compact-guard.mjs` | `UserPromptSubmit` | Detect a typed `/compact` or `/clear`, and enforce the session-close checklist before context is wiped |
248
255
  | `hypo-cwd-change.mjs` | `CwdChanged` | Inject the matching project's `hot.md` |
249
256
  | `hypo-file-watch.mjs` | `FileChanged` | Notify on wiki-file changes (honors `.hypoignore`; matched paths are never re-emitted into LLM context) |
250
- | `hypo-auto-stage.mjs` | `PostToolUse(Write/Edit)` | Auto-stage wiki-file edits |
257
+ | `hypo-auto-stage.mjs` | `PostToolUse(Write/Edit/MultiEdit)` | Auto-stage wiki-file edits |
251
258
  | `hypo-auto-commit.mjs` | `Stop` | Auto commit + pull + push |
252
- | `hypo-hot-rebuild.mjs` | `Stop` | Rebuild `hot.md` |
253
- | `hypo-personal-check.mjs` | `PreCompact` | Block compact on lint failures or unfinished session-close |
259
+ | `hypo-hot-rebuild.mjs` | `Stop` | Rebuild the root `hot.md` pointer table (structure + dates) |
260
+ | `hypo-personal-check.mjs` | `PreCompact` | Block compact on an unfinished session-close, an uncommitted or unpushed wiki, malformed `hot.md`, or lint blockers (bypass: `HYPO_SKIP_GATE=1`) |
254
261
  | `hypo-session-end.mjs` | `SessionEnd` | Write a SessionEnd marker so SessionStart can detect `source=clear` recovery |
255
262
  | `hypo-session-record.mjs` | `Stop` | Record session metadata for the observability score and auto-resume signaling |
256
- | `hypo-auto-minimal-crystallize.mjs` | `Stop` | Offer (and on consent run) `/hypo:crystallize --apply-session-close --minimal` after non-trivial sessions |
257
- | `hypo-web-fetch-ingest.mjs` | `PostToolUse(WebFetch/WebSearch)` | Inject a `/hypo:ingest` nudge into `additionalContext` after a URL resolution (privacy-aware: redacts query/hash/userinfo) |
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
+ | `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
+ Both PostToolUse hooks are registered without a matcher and filter on `tool_name` themselves.
258
267
 
259
268
  All hooks resolve the wiki root via `HYPO_DIR` env → `hypo-config.md` scan → `~/hypomnema` default, and share `hypo-shared.mjs` (declared via `hooks.json`'s `shared` field).
260
269
 
270
+ When the Stop hook's pull hits a merge conflict it aborts the merge, skips the push, and records the failure. Your local commits are safe, but the machines stop converging until you act: `git -C <your wiki> pull --no-rebase`, fix the conflicts, commit, push. `/hypo:doctor` surfaces a stuck sync.
271
+
261
272
  ### Setup & maintenance
262
273
 
263
274
  | Command | Purpose |
@@ -270,11 +281,13 @@ All hooks resolve the wiki root via `HYPO_DIR` env → `hypo-config.md` scan →
270
281
  | `/hypo:audit` | Observability audit (per-session metrics, weekly report) |
271
282
  | `hypomnema capture` (or `/hypo:capture`) | Pull a command/agent you created directly under `~/.claude/{commands,agents}/`, a canonical hook registered in `settings.json`, or a skill under `~/.claude/skills/`, into the wiki, so it syncs to your other machines under its original name. Explicit only, never overwrites a differing wiki file; only captures what round-trips losslessly |
272
283
 
273
- > Update notice: the `SessionStart` hook runs a non-blocking background check against npm and the Claude Code plugin marketplace, and prints an "Update available!" banner the next time a newer version has been published. Opt out with `HYPO_NO_UPDATE_CHECK=1`, `NO_UPDATE_NOTIFIER=1`, or by running under `CI=true`.
284
+ Every maintenance command also runs from a plain shell, which is what you need when Claude Code itself is misbehaving: `hypomnema doctor`, `hypomnema uninstall --apply` (dry-run without `--apply`), `hypomnema upgrade --check|--apply`, `hypomnema feedback-sync --check|--write`, `hypomnema proposal list`.
285
+
286
+ > Update notice: the `SessionStart` hook runs a non-blocking background check against npm and the Claude Code plugin marketplace, and prints an "Update available!" banner at the next session start once a newer version is out. Opt out with `HYPO_NO_UPDATE_CHECK=1`, `NO_UPDATE_NOTIFIER=1`, or by running under `CI=true`.
274
287
 
275
288
  ### Claude Agent Skills
276
289
 
277
- The six synthesis-heavy commands (`ingest`, `query`, `crystallize`, `lint`, `verify`, `graph`) are also exposed as Claude Agent Skills in `skills/<name>/SKILL.md`, so they auto-trigger when the conversation matches their description, with no slash command required. A seventh skill, `debate`, has no slash-command counterpart: it runs a structured two-phase review to re-verify a wiki claim or harden a hard-to-reverse decision into an ADR. You don't need to know the exact command; just say what you want.
290
+ The six synthesis-heavy commands (`ingest`, `query`, `crystallize`, `lint`, `verify`, `graph`) are also exposed as Claude Agent Skills in `skills/<name>/SKILL.md`, so they auto-trigger when the conversation matches their description, with no slash command required. A seventh, `debate`, is a skill only: it has no file under `commands/`, but the plugin loads `skills/` too, so `/hypo:debate` works like the rest. It runs a structured three-phase review (interrogate, verify, synthesize) to re-verify a wiki claim or harden a hard-to-reverse decision into an ADR. It is absent from the command tables above because those count the files under `commands/`, not what you can type.
278
291
 
279
292
  | Say this | Skill it triggers |
280
293
  |---|---|
@@ -338,39 +351,54 @@ Do not store here:
338
351
  ├── hypo-guide.md ← operations guide
339
352
  ├── .hypoignore ← glob patterns excluded from hooks
340
353
  ├── .hyposcanignore ← glob patterns excluded from catalog scans only (not a privacy boundary)
354
+ ├── extensions/ ← your own agents/commands/hooks/skills, mirrored to ~/.claude on init, upgrade, and capture
355
+ │ └── {agents,commands,hooks,skills}/
341
356
  ├── pages/ ← permanent knowledge pages
342
- │ └── feedback/ ← AI behavior corrections
357
+ │ ├── feedback/ ← AI behavior corrections (created on the first /hypo:feedback)
358
+ │ └── observability/ ← per-session usage metrics and weekly reports (/hypo:audit)
343
359
  ├── projects/ ← project artifacts and session logs
360
+ │ ├── _template/ ← scaffold copied when a new project is created
344
361
  │ └── <name>/
345
362
  │ ├── hot.md
346
363
  │ ├── session-state.md
347
364
  │ └── session-log/
348
365
  ├── journal/ ← daily / weekly / monthly entries
349
- └── sources/ ← raw ingested sources (never edit)
366
+ ├── sources/ ← raw ingested sources (never edit)
367
+ └── .cache/ ← runtime state (gitignored): session scope, parked proposals, sync status
350
368
  ```
351
369
 
352
370
  ---
353
371
 
354
372
  ## Configuration
355
373
 
356
- The wiki path is resolved in this order (see `scripts/lib/hypo-root.mjs`):
374
+ The wiki path is resolved in this order (levels 2-4 in `scripts/lib/hypo-root.mjs`; level 1 is parsed by each script that accepts the flag):
357
375
 
358
376
  | Priority | Source |
359
377
  |---|---|
360
378
  | 1 | `--hypo-dir=<path>` CLI flag (per-script override; only honored by scripts that accept it) |
361
379
  | 2 | `HYPO_DIR` environment variable |
362
- | 3 | `hypo-config.md` marker discovered in a fixed list of home-relative candidates (`~/hypomnema`, `~/wiki`, `~/notes`, `~/knowledge`, `~/Documents/{hypomnema,wiki,notes}`) |
380
+ | 3 | `hypo-config.md` marker discovered in a fixed list of home-relative candidates (`~/hypomnema`, `~/wiki`, `~/notes`, `~/knowledge`, `~/Documents/{hypomnema,wiki}`). The CLI also scans `~/Documents/notes`; the hooks do not, so set `HYPO_DIR` if your vault lives there. |
363
381
  | 4 | Default: `~/hypomnema` |
364
382
 
365
383
  Place a `hypo-config.md` at the wiki root to make it portable across machines without setting environment variables.
366
384
 
367
- `.hypoignore` controls which paths the hooks ignore (default: `*.pdf`, `*.zip`, `*.pem`, `*.env`, …). Edit it directly; there is no privacy mode flag. One file, one source of truth.
385
+ `.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.
368
386
 
369
- `.hyposcanignore` is a different file with a narrower job. `init` writes one at the root of every new vault, and it only excludes paths from catalog scans (`lint`, `graph`, `stats`, `query`, `verify`, `doctor`). It is not a privacy boundary: a path matched by `.hyposcanignore` is still committed and still readable by hooks. To actually keep something out of injection and commits, use `.hypoignore`.
387
+ > `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 embeds the absolute path of the installed package, so if you move or reinstall Hypomnema, re-run `hypomnema init` to repoint it or every wiki commit will fail. `uninstall` leaves this file in place.
388
+
389
+ > 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.
390
+
391
+ `.hyposcanignore` is a different file with a narrower job. `init` writes one at the root of every new vault, and it only excludes paths from catalog scans (`lint`, `stats`, `query`, `verify`, `doctor`). It is not a privacy boundary: a path matched by `.hyposcanignore` is still committed and still readable by hooks. To actually keep something out of injection and commits, use `.hypoignore`.
392
+
393
+ > Gate bypass: `HYPO_SKIP_GATE=1` is honored by every gate that can stop you mid-session, not just one: `hypo-personal-check` (PreCompact), `hypo-compact-guard`, `hypo-close-guard`, `hypo-auto-minimal-crystallize`, and the ingest nudge in `hypo-web-fetch-ingest`. Set it for a trivial session you do not want recorded.
394
+
395
+ > 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>` or `hypomnema proposal discard <id>`. There is no auto-apply.
370
396
 
371
397
  > Provider transmission disclaimer: Hypomnema hooks emit wiki content into Claude Code's `additionalContext`, which is transmitted to the Claude model provider as part of the prompt. `.hypoignore` is enforced at every content-injection hook (`hypo-file-watch`, `hypo-session-start`, `hypo-cwd-change`, `hypo-lookup`) and at `ingest`, but any file _not_ matched by `.hypoignore` is fair game for transmission. (`hypo-auto-stage` and `hypo-auto-commit` are git-staging hooks, not injection points, and also honor `.hypoignore` for their staging decisions.) Keep secrets out of the wiki, and review `.hypoignore` patterns before storing anything sensitive under `HYPO_DIR`.
372
398
 
373
- > 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), plus, via the extensions companion sync, any `agents/` · `commands/` · `hooks/` · `skills/` you ship inside `~/hypomnema/extensions/` (and with `--codex`, the `hooks` + `commands` subset into `~/.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/).
399
+ > 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/).
400
+
401
+ > A third privacy axis works per page rather than per path: set `visibility_scope: machine:<device>` in a page's frontmatter and it only surfaces on that machine. `<device>` defaults to the hostname; pin it with `HYPO_DEVICE` if your hostname is not stable, or the page stops matching its own machine. Omitting the field means shared.
374
402
 
375
403
  ### Where do `/hypo:*` commands live?
376
404
 
@@ -385,6 +413,7 @@ Place a `hypo-config.md` at the wiki root to make it portable across machines wi
385
413
 
386
414
  - Node.js ≥ 18 (tested on 18 / 20 / 22)
387
415
  - Claude Code CLI
416
+ - git (with `user.name` and `user.email` configured; the first commit fails without them)
388
417
 
389
418
  No external services. No API keys. No vector databases.
390
419
 
@@ -392,7 +421,7 @@ No external services. No API keys. No vector databases.
392
421
 
393
422
  ## Status
394
423
 
395
- - Tests: see `npm test`. Exact totals shift as lanes ship, so the runner is the source of truth
424
+ - Tests: see `npm test`. Exact totals shift as features ship, so the runner is the source of truth
396
425
  - CI: about a dozen independent jobs (test matrix, lint, tracker-ids, ship-surface, init/upgrade snapshots, replay, hypo-absent, uninstall-smoke, and more); see `.github/workflows/ci.yml` for the exact set
397
426
  - Release: `npm publish --provenance` on `v*` tag push
398
427
 
@@ -153,13 +153,13 @@ The result JSON includes a `stage` field when `ok: false`. Branch on it:
153
153
  | `post-apply-lint` | The payload introduced an error-level lint blocker in a payload file (malformed body / bad frontmatter), or lint crashed. | Fix the offending content in the payload, then re-run. (Broken wikilinks are W4 warnings — not gated.) |
154
154
  | `post-apply-verification+lint` | Both above. | Fix both; re-run. |
155
155
 
156
- Once `ok: true`, report:
156
+ Once `ok: true`, report from the result JSON's `applied` and `skipped` arrays together, not `applied` alone. `applied` lists only the fields this run actually wrote bytes for; `skipped` lists the fields that already matched what was on disk (a re-run of an already-applied payload). An idempotent re-run legitimately reports `applied: []`, and that is success, not a failure to report on: check `skipped` for the same 4-6 entries instead. Read `committed` the same way: `true` covers both a real commit and the case where nothing needed staging (a full no-op re-run); `false` means the commit itself ran and failed (see `markerSkipReason`); `null` means apply never reached the commit step at all, because `ok` was already false (an authority refusal before any write, or a verification/lint failure, or a withheld conflict, per `stage`). `null` does NOT mean nothing was written: `applied` can be non-empty (bytes landed on disk) while `committed` stays `null`, because those bytes were never staged into git.
157
157
 
158
- - ✓ session-state.md applied
159
- - ✓ hot.md (project + root) applied
160
- - ✓ session-log entry appended
158
+ - ✓ session-state.md applied (or already current, per `skipped`)
159
+ - ✓ hot.md (project + root) applied (or already current)
160
+ - ✓ session-log entry appended (or already present)
161
161
  - ✓ open-questions applied (or skipped if unchanged)
162
- - ✓ log.md entry appended
162
+ - ✓ log.md entry appended (or already present)
163
163
  - ✓ post-apply lint clean
164
164
  - **marker written?** (required check): if `markerWritten: true`, report "session-close marker written"; if `markerWritten: false`, report "session-close marker NOT written (reason: `<markerSkipReason>`)" and do NOT declare the session "closed" or "complete". A missing marker means the Stop-chain is still open; recover per the `markerSkipReason` branch below.
165
165
 
@@ -19,6 +19,7 @@ import {
19
19
  deriveRootLogEntries,
20
20
  recordTouchedPaths,
21
21
  } from './hypo-shared.mjs';
22
+ import { advanceBase, hashContent } from './base-store.mjs';
22
23
 
23
24
  const HOT_PATH = join(HYPO_DIR, 'hot.md');
24
25
  const GROWTH_CACHE = join(HYPO_DIR, '.cache', 'last-session-growth.json');
@@ -73,7 +74,7 @@ function parsePointerRows(content) {
73
74
  }
74
75
 
75
76
  /** @returns {boolean} true when hot.md was actually rewritten. */
76
- function rebuild() {
77
+ function rebuild(sessionId) {
77
78
  if (!existsSync(HOT_PATH)) return false;
78
79
 
79
80
  const current = readFileSync(HOT_PATH, 'utf-8');
@@ -116,6 +117,25 @@ ${tableRows}
116
117
 
117
118
  if (canonical !== current) {
118
119
  writeFileSync(HOT_PATH, canonical);
120
+ // This write bypasses the Write/Edit tool, so hypo-auto-stage's
121
+ // PostToolUse-based advanceBaseForWrite never sees it (see the file-header
122
+ // comment above). Without advancing the base here, this session's own
123
+ // rewrite of hot.md looks -- at close time -- exactly like a DIFFERENT
124
+ // session having edited it, and the observed-base guard in crystallize.mjs
125
+ // parks a false conflict against this session's own work. Mirrors
126
+ // crystallize.mjs's overwrite(): advanceBase right after the write that
127
+ // made it true.
128
+ //
129
+ // No session_id on stdin (empty/malformed payload): advanceBase is a
130
+ // no-op without a snapshot anyway, so this stays silent-safe like before
131
+ // this fix -- just observable on stderr instead of a guess.
132
+ if (sessionId) {
133
+ advanceBase(HYPO_DIR, sessionId, 'hot.md', hashContent(canonical));
134
+ } else {
135
+ process.stderr.write(
136
+ '[hypo-hot-rebuild] no session_id on stdin; base not advanced for hot.md\n',
137
+ );
138
+ }
119
139
  return true;
120
140
  }
121
141
  return false;
@@ -134,7 +154,7 @@ function emitGrowth() {
134
154
 
135
155
  let hotWritten = false;
136
156
  try {
137
- hotWritten = rebuild();
157
+ hotWritten = rebuild(sessionId);
138
158
  } catch (err) {
139
159
  process.stderr.write(`[hypo-hot-rebuild] error: ${err?.message ?? String(err)}\n`);
140
160
  }
@@ -38,6 +38,7 @@ import {
38
38
  scopeVisible,
39
39
  readVisibilityScope,
40
40
  pkgRootDriftStatus,
41
+ PKG_ROOT,
41
42
  } from './hypo-shared.mjs';
42
43
  import {
43
44
  defaultCachePath,
@@ -54,6 +55,9 @@ import {
54
55
  pkgRootDriftAlreadyNotified,
55
56
  markPkgRootDriftNotified,
56
57
  clearPkgRootDriftNotified,
58
+ pkgRootNullAlreadyNotified,
59
+ markPkgRootNullNotified,
60
+ clearPkgRootNullNotified,
57
61
  } from './version-check.mjs';
58
62
  import { snapshotBase, overwriteTargets } from './base-store.mjs';
59
63
  import { listProposals } from './proposal-store.mjs';
@@ -266,6 +270,48 @@ function buildPkgRootDriftNotice() {
266
270
  }
267
271
  }
268
272
 
273
+ /**
274
+ * PKG_ROOT-null notice. A different failure than the drift banner above:
275
+ * drift only fires when self-location DID resolve (PKG_ROOT is non-null,
276
+ * just disagreeing with the cache). This fires when hooks/hypo-shared.mjs's
277
+ * resolvePkgRoot() came up with nothing at all — self-location failed AND no
278
+ * verified provenance sidecar covered it — which is exactly the state where
279
+ * PreCompact's lint/feedback calls silently no-op (they have no root to
280
+ * shell scripts through). The two conditions cannot both hold in the same
281
+ * session (drift requires a non-null self-location), so there is no overlap
282
+ * to arbitrate — they use separate notify-once cache fields regardless, so
283
+ * neither one depends on that being true forever.
284
+ *
285
+ * Same notify-once shape as the drift banner: shown once, cleared as soon as
286
+ * PKG_ROOT resolves again so a later recurrence re-notifies instead of
287
+ * staying suppressed by a mark from a different install state.
288
+ */
289
+ function buildPkgRootNullNotice() {
290
+ try {
291
+ const cachePath = defaultCachePath();
292
+ if (PKG_ROOT) {
293
+ clearPkgRootNullNotified(cachePath);
294
+ return '';
295
+ }
296
+ if (isOptedOut()) return '';
297
+ const cache = readCache(cachePath);
298
+ if (pkgRootNullAlreadyNotified(cache)) return '';
299
+ markPkgRootNullNotified(cachePath);
300
+ return (
301
+ `[Hypomnema] Package root unresolved: this install's hooks cannot locate ` +
302
+ `their own package, so PreCompact's lint/feedback checks are silently ` +
303
+ `skipped this session.\n` +
304
+ ` → run \`hypomnema upgrade --apply\` to sync this install's hook copies ` +
305
+ `with the current package (the \`/hypo:upgrade --apply\` slash command does ` +
306
+ `the same thing, where slash commands were installed). \`/hypo:init\` will ` +
307
+ `NOT fix this — it skips every hook file that already exists.\n` +
308
+ ` → or run \`hypomnema doctor\` to see what's missing.`
309
+ );
310
+ } catch {
311
+ return '';
312
+ }
313
+ }
314
+
269
315
  const PROJECTS_DIR = join(HYPO_DIR, 'projects');
270
316
  const GROWTH_CACHE = join(HYPO_DIR, '.cache', 'last-session-growth.json');
271
317
 
@@ -500,16 +546,24 @@ process.stdin.on('end', () => {
500
546
  const updateLine = buildUpdateNotice();
501
547
  const siblingLine = buildSiblingNotice();
502
548
  const pkgDriftLine = buildPkgRootDriftNotice();
503
- // The update + stale-sibling + pkgRoot-drift banners must reach the USER.
504
- // On a SessionStart hook that exits 0, stderr is invisible in the normal
505
- // TUI (only shown on exit 2 / --verbose) and additionalContext is
549
+ // pkgDriftLine and pkgNullLine can never both be non-empty in the same
550
+ // session: drift requires PKG_ROOT to have resolved (non-null) via
551
+ // self-location, while the null notice fires exactly when it did not.
552
+ // Listed together below on that basis, not because one is chosen over
553
+ // the other.
554
+ const pkgNullLine = buildPkgRootNullNotice();
555
+ // The update + stale-sibling + pkgRoot-drift/null banners must reach the
556
+ // USER. On a SessionStart hook that exits 0, stderr is invisible in the
557
+ // normal TUI (only shown on exit 2 / --verbose) and additionalContext is
506
558
  // model-only — `systemMessage` is the documented user-visible channel.
507
559
  // Route those banners there. They ALSO stay in noticePrefix →
508
560
  // additionalContext below, so the model and the user start the session
509
561
  // looking at the same state. (The other stderr notices —
510
562
  // sync/growth/clear/suggest — are intentionally transcript/--verbose only
511
563
  // and out of this banner's scope.)
512
- const userMessage = [updateLine, siblingLine, pkgDriftLine].filter(Boolean).join('\n\n');
564
+ const userMessage = [updateLine, siblingLine, pkgDriftLine, pkgNullLine]
565
+ .filter(Boolean)
566
+ .join('\n\n');
513
567
  if (userMessage) outExtra = { ...outExtra, systemMessage: userMessage };
514
568
  const notices = [
515
569
  syncLine,
@@ -519,6 +573,7 @@ process.stdin.on('end', () => {
519
573
  updateLine,
520
574
  siblingLine,
521
575
  pkgDriftLine,
576
+ pkgNullLine,
522
577
  ].filter(Boolean);
523
578
  let noticePrefix = notices.length ? `${notices.join('\n\n')}\n\n` : '';
524
579
  if (syncLine) process.stderr.write(`\n\x1b[33m${syncLine}\x1b[0m\n`);
@@ -529,6 +584,7 @@ process.stdin.on('end', () => {
529
584
  if (updateLine) process.stderr.write(`\n\x1b[33m${updateLine}\x1b[0m\n`);
530
585
  if (siblingLine) process.stderr.write(`\n\x1b[33m${siblingLine}\x1b[0m\n`);
531
586
  if (pkgDriftLine) process.stderr.write(`\n\x1b[33m${pkgDriftLine}\x1b[0m\n`);
587
+ if (pkgNullLine) process.stderr.write(`\n\x1b[33m${pkgNullLine}\x1b[0m\n`);
532
588
  const cwd = data.cwd || data.directory || process.cwd();
533
589
  const sessionId = data.session_id || 'default';
534
590
  const MARKER_FILE = sessionMarkerPath(sessionId);