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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +79 -50
- package/README.md +63 -34
- package/commands/crystallize.md +5 -5
- package/hooks/hypo-hot-rebuild.mjs +22 -2
- package/hooks/hypo-session-start.mjs +60 -4
- package/hooks/hypo-shared.mjs +364 -118
- package/hooks/version-check.mjs +47 -0
- package/package.json +2 -1
- package/scripts/capture.mjs +15 -20
- package/scripts/crystallize.mjs +97 -12
- package/scripts/doctor.mjs +151 -7
- package/scripts/init.mjs +17 -7
- package/scripts/lib/extensions.mjs +43 -4
- package/scripts/lib/git-hooks-dir.mjs +10 -0
- package/scripts/lib/pkg-provenance.mjs +166 -0
- package/scripts/lint.mjs +45 -1
- package/scripts/uninstall.mjs +266 -2
- package/scripts/upgrade.mjs +8 -0
- package/templates/hypo-config.md +1 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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`, `
|
|
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
|
|
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
|
|
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
|
|
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` |
|
|
232
|
-
| `/hypo:crystallize` | Runs the session-close
|
|
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 (
|
|
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`
|
|
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
|
|
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` |
|
|
257
|
-
| `hypo-web-fetch-ingest.mjs` | `PostToolUse(WebFetch/WebSearch)` | Inject a `/hypo:ingest` nudge into `additionalContext` after a URL
|
|
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
|
-
|
|
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
|
|
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
|
-
│
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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`,
|
|
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
|
-
|
|
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
|
|
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
|
|
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/commands/crystallize.md
CHANGED
|
@@ -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
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
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]
|
|
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);
|