hypomnema 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
 
package/hooks/hooks.json CHANGED
@@ -1,5 +1,16 @@
1
1
  {
2
2
  "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/hypo-close-guard.mjs",
9
+ "timeout": 10
10
+ }
11
+ ]
12
+ }
13
+ ],
3
14
  "SessionStart": [
4
15
  {
5
16
  "hooks": [
@@ -2,32 +2,109 @@
2
2
  /**
3
3
  * hypo-auto-commit.mjs — Stop hook
4
4
  *
5
- * At session end: stage all changes, commit if any, then pull+push to sync remote.
5
+ * At session end: stage this session's touched paths, commit if any, then
6
+ * pull+push to sync remote.
7
+ *
8
+ * Scoped, not whole-tree: this no longer sweeps the entire working tree. The
9
+ * scope is this session's accumulated touched-paths set (hypo-auto-stage.mjs
10
+ * writes, plus whatever the earlier Stop-chain generators, hot-rebuild and
11
+ * session-record, appended for the same session_id). No session_id means
12
+ * nothing was ever accumulated, so the scoped commit is skipped cleanly;
13
+ * never a whole-tree fallback.
14
+ *
15
+ * PEEK, don't drain, and hold ONE lock across peek+commit+clear
16
+ * (commitTouchedPaths, hypo-shared.mjs): a drain-then-requeue-on-failure
17
+ * design was tried and dropped — the requeue write is itself a fallible
18
+ * operation (lock-timeout, I/O), so a commit failure could still lose the
19
+ * scope in the narrow window between the drain and the requeue. A peek
20
+ * that released its lock before the commit, then a SEPARATE clear
21
+ * afterward, was also tried and dropped — a `recordTouchedPaths` for a
22
+ * path already in the just-peeked set could land in the window between the
23
+ * commit and the clear and be silently wiped out by it (the set only
24
+ * tracks path presence, not a version, so that write is indistinguishable
25
+ * from the one already peeked). commitTouchedPaths holds ONE per-session
26
+ * lock across the whole peek → commit → clear window, so neither loss mode
27
+ * is possible: nothing is deleted until the commit has actually succeeded,
28
+ * and no accumulate can land inside the window at all.
6
29
  */
7
30
 
8
31
  import { spawnSync } from 'child_process';
9
- import { HYPO_DIR, syncRemote, commitWikiChanges } from './hypo-shared.mjs';
32
+ import {
33
+ HYPO_DIR,
34
+ syncRemote,
35
+ commitWikiChanges,
36
+ commitTouchedPaths,
37
+ vaultCommitLockTarget,
38
+ withFileLock,
39
+ } from './hypo-shared.mjs';
10
40
 
11
41
  function hasRemote() {
12
42
  const r = spawnSync('git', ['-C', HYPO_DIR, 'remote'], { encoding: 'utf-8', timeout: 30000 });
13
43
  return (r.stdout || '').trim().length > 0;
14
44
  }
15
45
 
16
- // Stage + commit via the shared helper (same .hypoignore filter the apply path
17
- // uses). A real commit failure short-circuits before sync, exactly as
18
- // the inline logic did; "nothing to commit" is success and falls through to sync.
19
- const result = commitWikiChanges(HYPO_DIR);
20
- if (!result.committed) {
21
- console.log(JSON.stringify({ continue: true, suppressOutput: true }));
22
- process.exit(0);
46
+ // Overridable so a test can force a fast lock-timeout instead of waiting out
47
+ // the real default (mirrors crystallize.mjs's HYPO_APPEND_LOCK_TIMEOUT_MS).
48
+ const VAULT_LOCK_TIMEOUT_MS = Number(process.env.HYPO_VAULT_LOCK_TIMEOUT_MS) || 5000;
49
+
50
+ let input = {};
51
+ try {
52
+ const raw = await new Promise((r) => {
53
+ let d = '';
54
+ process.stdin.on('data', (c) => (d += c));
55
+ process.stdin.on('end', () => r(d));
56
+ });
57
+ input = JSON.parse(raw || '{}') || {};
58
+ } catch {
59
+ input = {};
23
60
  }
61
+ const sessionId = input.session_id || input.sessionId || null;
62
+
63
+ // Stage + commit + sync as one critical section, serialized against every
64
+ // other writer of this vault (the crystallize.mjs --apply-session-close path
65
+ // holds the SAME lock around its own stage+commit). Without this, two
66
+ // concurrent sessions on a shared vault could interleave `git add`/`git
67
+ // commit`/`git pull`/`git push`. This does NOT gate pushes on whole-tree
68
+ // cleanliness: a scoped commit may legitimately leave other sessions' dirty
69
+ // files behind, and a `git pull --no-rebase` failure from that residual is
70
+ // already logged via appendSyncFailure and surfaced by doctor/session-start.
71
+ // Full cross-session isolation is out of scope (it needs separate worktrees).
72
+ //
73
+ // The vault lock (shared with crystallize.mjs's apply commit) serializes
74
+ // git operations across concurrent sessions on this vault; the per-session
75
+ // touched-paths lock commitTouchedPaths takes internally is a DIFFERENT
76
+ // lock file, so the two nest without any ordering conflict (vault lock is
77
+ // always acquired first here; accumulation elsewhere only ever takes the
78
+ // per-session lock, never the vault lock).
79
+ try {
80
+ withFileLock(
81
+ vaultCommitLockTarget(HYPO_DIR),
82
+ () => {
83
+ // Peek this session's scope, run the scoped commit, and — only on
84
+ // success — clear exactly what committed, ALL under one hold of the
85
+ // per-session lock. See commitTouchedPaths's docstring for why a
86
+ // commit failure or a same-path race can't lose anything under this.
87
+ const result = commitTouchedPaths(HYPO_DIR, sessionId, (paths) =>
88
+ commitWikiChanges(HYPO_DIR, paths),
89
+ );
90
+ if (!result.committed) return;
24
91
 
25
- if (hasRemote()) {
26
- // pull/push failures must not stop the session, but they can no longer be
27
- // swallowed silently — syncRemote records each to .cache/sync-state.json and,
28
- // on a merge conflict, aborts the merge so the tree is never left half-merged
29
- // (part of the v1.4 sync hardening). session-start + doctor surface the result next session.
30
- syncRemote(HYPO_DIR);
92
+ if (hasRemote()) {
93
+ // pull/push failures must not stop the session, but they can no longer be
94
+ // swallowed silently — syncRemote records each to .cache/sync-state.json and,
95
+ // on a merge conflict, aborts the merge so the tree is never left half-merged
96
+ // (part of the v1.4 sync hardening). session-start + doctor surface the result next session.
97
+ syncRemote(HYPO_DIR);
98
+ }
99
+ },
100
+ { timeoutMs: VAULT_LOCK_TIMEOUT_MS },
101
+ );
102
+ } catch {
103
+ // Lock-timeout (or an unexpected lock error) on the OUTER vault lock: we
104
+ // never entered the critical section, so commitTouchedPaths never ran —
105
+ // the touched-paths file is untouched on disk, and the next Stop retries
106
+ // this session's commit from the same scope. Best-effort, like every
107
+ // other step in this hook.
31
108
  }
32
109
 
33
110
  console.log(JSON.stringify({ continue: true, suppressOutput: true }));
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { spawnSync } from 'child_process';
9
9
  import { relative } from 'path';
10
- import { HYPO_DIR, loadHypoIgnore, isIgnored } from './hypo-shared.mjs';
10
+ import { HYPO_DIR, loadHypoIgnore, isIgnored, recordTouchedPaths } from './hypo-shared.mjs';
11
11
  import { advanceBaseForWrite, hashContent } from './base-store.mjs';
12
12
 
13
13
  // Tools that REPLACE file bytes. The base advance below must fire only for these:
@@ -40,24 +40,34 @@ if (filePath.startsWith(HYPO_DIR + '/') || filePath === HYPO_DIR) {
40
40
  spawnSync('git', ['-C', HYPO_DIR, 'add', filePath], { stdio: 'ignore' });
41
41
  }
42
42
 
43
- // Write=proposal gate provenance: when this session's own write lands on one of
44
- // the overwrite targets it snapshotted at start, advance that target's base so
45
- // the close guard reads the change as "I wrote this", not "someone else did"
46
- // (which would fail safe into a false proposal against the session's own edit).
47
- // Self-scoping — a no-op unless the path is a tracked base key — so it runs
48
- // regardless of .hypoignore (provenance is independent of privacy). Best-effort.
49
- //
50
- // The Write tool carries its full `content`, so advance to the bytes THIS
51
- // session wrote (race-safe: a concurrent write landing between the tool and
52
- // this hook cannot be adopted as our base). Edit/MultiEdit have no full content
53
- // in the payload, so they fall back to a post-write disk read.
54
- if (WRITE_TOOLS.has(input.tool_name) && input.session_id) {
43
+ if (WRITE_TOOLS.has(input.tool_name)) {
55
44
  const rel = relative(HYPO_DIR, filePath);
56
- const known =
57
- input.tool_name === 'Write' && typeof input.tool_input?.content === 'string'
58
- ? hashContent(input.tool_input.content)
59
- : null;
60
- advanceBaseForWrite(HYPO_DIR, input.session_id, rel, filePath, known);
45
+
46
+ // Accumulate this write into the session's scoped auto-commit
47
+ // set, keyed by session_id (no-op without one; never a shared bucket).
48
+ // hypo-auto-commit.mjs drains this at Stop instead of sweeping the whole
49
+ // working tree, so another session's concurrent writes to this vault
50
+ // never land in THIS session's commit.
51
+ recordTouchedPaths(HYPO_DIR, input.session_id, rel);
52
+
53
+ // Write=proposal gate provenance: when this session's own write lands on one of
54
+ // the overwrite targets it snapshotted at start, advance that target's base so
55
+ // the close guard reads the change as "I wrote this", not "someone else did"
56
+ // (which would fail safe into a false proposal against the session's own edit).
57
+ // Self-scoping — a no-op unless the path is a tracked base key — so it runs
58
+ // regardless of .hypoignore (provenance is independent of privacy). Best-effort.
59
+ //
60
+ // The Write tool carries its full `content`, so advance to the bytes THIS
61
+ // session wrote (race-safe: a concurrent write landing between the tool and
62
+ // this hook cannot be adopted as our base). Edit/MultiEdit have no full content
63
+ // in the payload, so they fall back to a post-write disk read.
64
+ if (input.session_id) {
65
+ const known =
66
+ input.tool_name === 'Write' && typeof input.tool_input?.content === 'string'
67
+ ? hashContent(input.tool_input.content)
68
+ : null;
69
+ advanceBaseForWrite(HYPO_DIR, input.session_id, rel, filePath, known);
70
+ }
61
71
  }
62
72
  }
63
73