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/.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/hooks/hooks.json +11 -0
- package/hooks/hypo-auto-commit.mjs +92 -15
- package/hooks/hypo-auto-stage.mjs +28 -18
- package/hooks/hypo-close-guard.mjs +246 -0
- package/hooks/hypo-hot-rebuild.mjs +43 -5
- package/hooks/hypo-session-start.mjs +166 -13
- package/hooks/hypo-shared.mjs +1358 -130
- package/hooks/version-check.mjs +92 -0
- package/package.json +4 -1
- package/scripts/crystallize.mjs +112 -2
- package/scripts/doctor.mjs +627 -17
- package/scripts/graph.mjs +27 -12
- package/scripts/init.mjs +66 -7
- package/scripts/lib/git-hooks-dir.mjs +229 -0
- package/scripts/lib/pkg-provenance.mjs +166 -0
- package/scripts/lib/project-create.mjs +5 -1
- package/scripts/lib/rename-marker.mjs +39 -0
- package/scripts/lint.mjs +84 -2
- package/scripts/rename.mjs +223 -18
- package/scripts/stats.mjs +14 -2
- package/scripts/uninstall.mjs +12 -0
- package/scripts/upgrade.mjs +8 -0
- package/templates/gitignore +4 -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/hooks/hooks.json
CHANGED
|
@@ -2,32 +2,109 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* hypo-auto-commit.mjs — Stop hook
|
|
4
4
|
*
|
|
5
|
-
* At session end: stage
|
|
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 {
|
|
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
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|