hypomnema 1.8.4 → 1.9.0

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.
Files changed (60) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +36 -0
  4. package/README.ko.md +16 -15
  5. package/README.md +14 -13
  6. package/commands/crystallize.md +84 -30
  7. package/commands/doctor.md +3 -1
  8. package/docs/ARCHITECTURE.md +32 -9
  9. package/docs/CONTRIBUTING.md +5 -1
  10. package/hooks/atomic-write.mjs +44 -0
  11. package/hooks/base-store.mjs +409 -90
  12. package/hooks/close-gate-store.mjs +131 -16
  13. package/hooks/close-journal.mjs +141 -19
  14. package/hooks/close-receipt.mjs +559 -0
  15. package/hooks/hooks.json +2 -29
  16. package/hooks/hypo-auto-commit.mjs +80 -10
  17. package/hooks/hypo-auto-minimal-crystallize.mjs +129 -25
  18. package/hooks/hypo-auto-stage.mjs +13 -1
  19. package/hooks/hypo-close-guard.mjs +2 -1
  20. package/hooks/hypo-cwd-change.mjs +45 -20
  21. package/hooks/hypo-file-watch.mjs +44 -6
  22. package/hooks/hypo-first-prompt.mjs +15 -9
  23. package/hooks/hypo-hot-rebuild.mjs +169 -112
  24. package/hooks/hypo-personal-check.mjs +9 -1
  25. package/hooks/hypo-session-record.mjs +1 -1
  26. package/hooks/hypo-session-start.mjs +381 -18
  27. package/hooks/hypo-shared.mjs +2151 -185
  28. package/hooks/hypo-stop.mjs +207 -0
  29. package/hooks/proposal-store.mjs +166 -34
  30. package/hooks/shared.json +7 -1
  31. package/hooks/version-check.mjs +9 -7
  32. package/package.json +2 -1
  33. package/scripts/crystallize.mjs +3 -4
  34. package/scripts/doctor.mjs +506 -32
  35. package/scripts/init.mjs +32 -3
  36. package/scripts/lib/crystallize-close-apply.mjs +1949 -230
  37. package/scripts/lib/crystallize-close-check.mjs +43 -23
  38. package/scripts/lib/design-history-stale.mjs +36 -9
  39. package/scripts/lib/frontmatter.mjs +39 -0
  40. package/scripts/lib/hook-inventory.mjs +31 -1
  41. package/scripts/lib/page-usage.mjs +1 -1
  42. package/scripts/lib/pkg-provenance.mjs +7 -1
  43. package/scripts/lib/project-create.mjs +40 -44
  44. package/scripts/lib/schema-vocab.mjs +22 -11
  45. package/scripts/lib/template-schema-version.mjs +1 -0
  46. package/scripts/lib/time.mjs +46 -0
  47. package/scripts/lint.mjs +66 -18
  48. package/scripts/proposal.mjs +423 -18
  49. package/scripts/resume.mjs +3 -2
  50. package/scripts/session-audit.mjs +2 -1
  51. package/scripts/uninstall.mjs +15 -1
  52. package/scripts/upgrade.mjs +281 -90
  53. package/scripts/verify.mjs +2 -1
  54. package/scripts/weekly-report.mjs +2 -1
  55. package/templates/SCHEMA.md +27 -7
  56. package/templates/gitignore +11 -0
  57. package/templates/hypo-automation.md +10 -13
  58. package/templates/hypo-config.md +1 -1
  59. package/templates/hypo-guide.md +53 -25
  60. package/templates/hot.md +0 -24
@@ -119,7 +119,7 @@ Hooks run automatically at Claude Code lifecycle events. They are deployed to `~
119
119
  | `UserPromptSubmit` | `hypo-first-prompt.mjs` → `hypo-lookup.mjs` → `hypo-compact-guard.mjs` |
120
120
  | `PreCompact` | `hypo-personal-check.mjs` |
121
121
  | `PostToolUse` (Write/Edit) | `hypo-auto-stage.mjs` |
122
- | `Stop` | `hypo-hot-rebuild.mjs` → `hypo-session-record.mjs` → `hypo-auto-commit.mjs` |
122
+ | `Stop` | `hypo-stop.mjs` (runs `hypo-hot-rebuild.mjs` → `hypo-session-record.mjs` → `hypo-auto-commit.mjs` → `hypo-auto-minimal-crystallize.mjs` as ordered stages, not separate registrations) |
123
123
  | `CwdChanged` | `hypo-cwd-change.mjs` |
124
124
  | `FileChanged` | `hypo-file-watch.mjs` |
125
125
 
@@ -127,17 +127,17 @@ Hooks run automatically at Claude Code lifecycle events. They are deployed to `~
127
127
 
128
128
  | Hook | Responsibility |
129
129
  |---|---|
130
- | `hypo-session-start` | Inject `index.md`, root `hot.md`, project `hot.md`/`session-state.md`. Run `git pull --ff-only` (silent fail on missing remote) |
130
+ | `hypo-session-start` | Run `git pull --ff-only` (silent fail on missing remote), rebuild the root `hot.md` projection, inject `index.md`, root `hot.md`, project `hot.md`/`session-state.md` |
131
131
  | `hypo-first-prompt` | Reads the marker left by `hypo-session-start` or `hypo-cwd-change` and forces a one-line resume on the first user prompt (10-min TTL). It does not re-read or re-inject `hot.md` |
132
132
  | `hypo-lookup` | BM25 search over the wiki on every prompt. **HIT** → inject top-3 page snippets (≤2000 chars each; a page whose `verify_by_date` is overdue gets a `[STALE verify_by_date=…]` marker prepended). **MISS** → emit closest-slug signal that prompts Claude to research + `/hypo:ingest` |
133
133
  | `hypo-compact-guard` | Detect `/compact` or `/clear` typed in chat and, if session close is incomplete, tell Claude so. It never blocks `/compact` |
134
134
  | `hypo-personal-check` | PreCompact detection: lint blockers, uncommitted changes, missing session-log entries surface as a `systemMessage`; `/compact` is never blocked here |
135
135
  | `hypo-auto-stage` | After Write/Edit on a wiki path, run `git add` (skips paths matching `.hypoignore`) |
136
- | `hypo-hot-rebuild` | At session stop, regenerate root `hot.md` from recent activity; emit growth metrics + cache for next SessionStart |
136
+ | `hypo-hot-rebuild` | At session stop, regenerate the root `hot.md` projection (the same generator `hypo-session-start` also calls); emit growth metrics + cache for next SessionStart. A scan failure (not just an empty `projects/`) skips the write and leaves a health notice for the next `SessionStart` to surface |
137
137
  | `hypo-session-record` | At session stop, append `{session_id, transcript_path, recorded_at, cwd, device}` to `.cache/sessions/index.jsonl` (primary source for the observability audit) |
138
138
  | `hypo-auto-commit` | At session stop, filter changed paths through `.hypoignore`, commit non-ignored changes, `git pull --no-rebase` + `git push` (silent fail on missing remote) |
139
- | `hypo-cwd-change` | When working directory changes, re-resolve the active project and build `additionalContext` with its `hot.md`; `CwdChanged` has no field Claude Code forwards to the model, so this does not currently reach Claude |
140
- | `hypo-file-watch` | Build `additionalContext` for a changed wiki file. Two things keep this inert: nothing in this package returns `watchPaths`, so the event has no registered trigger, and `FileChanged` output does not reach the model. The event also fires regardless of who changed the file, so it is not an external-edit signal |
139
+ | `hypo-cwd-change` | When working directory changes, re-resolve the active project and build a notice carrying its `hot.md`. The hook reference documents `watchPaths` and `systemMessage` as `CwdChanged`'s output fields and says only that "it shows the `systemMessage` as a brief terminal notification" and "the message doesn't reach the SDK message stream"; it does not say whether `systemMessage` reaches the model on this event either way. As of Claude Code 2.1.276 (checked 2026-09-18) and reconfirmed on 2.1.283, tracing the installed binary shows the only consumer of that `systemMessage` is the terminal notification queue, a low-priority toast that a same-keyed message replaces and that disappears after five seconds, with no branch that forwards it to the model; outside the interactive REPL the injecting function is null. No live-session observation of this path exists either way. Until the documentation says otherwise, this event is treated as not reaching the model |
140
+ | `hypo-file-watch` | Build a `systemMessage` notice for a changed wiki file, over the same field as `hypo-cwd-change` whose reach to the model the reference leaves undocumented; as of Claude Code 2.1.276 (checked 2026-09-18) and reconfirmed on 2.1.283 it is likewise treated as not reaching the model, for the same shared-consumer reason (see the `hypo-cwd-change` row). On a stock install the event still has no registered trigger: its matcher key is omitted (not blank), which the reference says matches every watched file while adding nothing to the watch list, and nothing in this package returns `watchPaths` to seed that list. The watch list is session-global, not scoped to this plugin, so another hook or plugin that does seed it would make this matcher-less group fire on every watched file; the hook still returns immediately for any path that does not start with `HYPO_DIR` (a string-prefix check, not a symlink-resolving one). That check only screens the path string: a symlink living inside the wiki still starts with `HYPO_DIR` and passes it, and the hook's `readFileSync` follows the link, so such a symlink can still surface content from wherever it points. The event also fires regardless of who changed the file, so it is not an external-edit signal |
141
141
 
142
142
  ### Deployment constraint
143
143
 
@@ -293,6 +293,7 @@ SessionStart
293
293
  │
294
294
  ├─► hypo-session-start.mjs
295
295
  │ ├─► git pull --ff-only (if remote)
296
+ │ ├─► rebuild root hot.md (projection of projects/*/hot.md)
296
297
  │ ├─► inject index.md / hot.md / project session-state.md
297
298
  │ └─► (Claude resumes work)
298
299
  │
@@ -308,14 +309,36 @@ SessionStart
308
309
  │ └─► hypo-personal-check.mjs (lint + session-close gate)
309
310
  │
310
311
  ├─► CwdChanged
311
- │ └─► hypo-cwd-change.mjs (builds hot.md context; does not currently reach Claude)
312
+ │ └─► hypo-cwd-change.mjs (builds a hot.md notice on systemMessage; the reference documents the terminal notification, tracing the installed binary as of Claude Code 2.1.276, checked 2026-09-18 and reconfirmed on 2.1.283, shows it does not reach the model, and no live-session observation exists either way; see the hook table above)
312
313
  │
313
314
  └─► Stop
314
- ├─► hypo-hot-rebuild.mjs (regenerate root hot.md + growth cache)
315
- ├─► hypo-session-record.mjs (append .cache/sessions/index.jsonl)
316
- └─► hypo-auto-commit.mjs (.hypoignore-filtered stage + commit + pull + push)
315
+ └─► hypo-stop.mjs, running four stages in order:
316
+ ├─► hypo-hot-rebuild.mjs (regenerate root hot.md projection + growth cache; SessionStart also calls this generator)
317
+ ├─► hypo-session-record.mjs (append .cache/sessions/index.jsonl)
318
+ ├─► hypo-auto-commit.mjs (.hypoignore-filtered stage + commit + pull + push)
319
+ └─► hypo-auto-minimal-crystallize.mjs (blocks Stop if session close never ran)
317
320
  ```
318
321
 
322
+ Root hot.md's frontmatter `updated:` is the max of every `projects/*/hot.md` row date, not the
323
+ date the projection itself was last rewritten. A vault whose newest project row is a week old
324
+ keeps that same week-old date across every SessionStart and Stop until a project's own `hot.md`
325
+ moves forward, even though the root file's bytes get recomputed and checked far more often.
326
+
327
+ Before overwriting content it does not recognize as its own, the projection backs it up next to
328
+ `hot.md` as `hot.md.pre-projection-backup.md` (or a numbered variant, `-2.md`, `-3.md`, ...). These
329
+ backups are never cleaned up automatically. Delete the ones you no longer need by hand.
330
+
331
+ The write itself goes through `writeRootHotProjection` in `hooks/hypo-shared.mjs`, which holds
332
+ the vault commit lock (`vaultCommitLockTarget`, the same lock `hypo-auto-commit.mjs` and
333
+ `crystallize.mjs`'s apply take around stage and commit) across the whole projection write: the
334
+ first read of `hot.md`, the ownership check, the backup, and the replacing rename. `hypo-session-start.mjs`'s
335
+ own `git pull` takes and releases the same lock before calling into the projection, so the two
336
+ never overlap; they still serialize against each other and against a concurrent `auto-commit` or
337
+ `crystallize` apply. The lock is not reentrant: nothing inside the projection's write path may
338
+ take it a second time, and a caller that already holds it has to call the unlocked form instead.
339
+ When the lock cannot be acquired within its timeout, the write is skipped rather than blocked on
340
+ indefinitely, and the caller is told the pointer table was left as-is.
341
+
319
342
  ---
320
343
 
321
344
  ## Feedback → Behavior pipeline
@@ -129,7 +129,11 @@ npm test # tests/*.test.mjs, sharded across processes — unit + smoke
129
129
  npm run lint # scripts/lint.mjs — frontmatter + wikilink validation + W8 (design-history stale
130
130
  # vs session-log) + W14 (design-history missing but session-log implies one)
131
131
  # + W15 (synthesis older than the newest page in its sources_consulted)
132
- # + W16 (sources_consulted names W15 could not resolve)
132
+ # + W16 (sources_consulted content problems: a source name W15 could not
133
+ # resolve, a source or the synthesis's own updated that fails a strict
134
+ # YYYY-MM-DD parse, or sources_consulted written as a YAML block list
135
+ # instead of the required flow list; a single page can carry more than one
136
+ # W16 at once)
133
137
  npm run fix:verify # Phase 1 of learned_behavior #6 — verifies fix #N status claims in
134
138
  # a wiki spec against `// @fix #N: <test-name>` anchors, read as a
135
139
  # union across every tests/*.mjs. Maintainer dogfood; needs a wiki at
@@ -0,0 +1,44 @@
1
+ // The one temp+rename writer. Six byte-identical copies of this function used to
2
+ // live in hooks/ and scripts/lib/, and a fix landed in two of them (#296) while
3
+ // the other four kept the bug. That is the whole reason this file exists: a
4
+ // defense that lives in six places is a defense you can only half-apply.
5
+ //
6
+ // Hooks are copied standalone into ~/.claude/hooks/, so this has to live in
7
+ // hooks/ and be listed in hooks/shared.json. scripts/ may import it; the reverse
8
+ // direction is what breaks for every installed copy.
9
+ //
10
+ // Node built-ins only, per the hooks rule.
11
+ import { mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
12
+ import { dirname } from 'node:path';
13
+
14
+ /**
15
+ * Commit `content` to `path` via a temp file and a rename, so a partial or
16
+ * failed write lands on a throwaway name and the target is never torn.
17
+ *
18
+ * The temp name carries this pid and a fresh random rather than a shared
19
+ * `<path>.tmp` slot, so concurrent writers never fight over it. That is also
20
+ * why a leaked temp is permanent: nothing ever picks the same name again, so
21
+ * the file sits in the vault forever, close after close. Both failure points
22
+ * leak one: the rename (the target stays untouched, which is the point) and
23
+ * the write itself (ENOSPC or EDQUOT partway through). Clean up on either, and
24
+ * never let the cleanup hide the error that got us here.
25
+ *
26
+ * Rename atomicity swaps the directory entry. It is NOT power-loss durable.
27
+ * there is no fsync, same as everything else in the vault.
28
+ *
29
+ * @param {string} path absolute path to write
30
+ * @param {string|Buffer} content bytes to commit
31
+ */
32
+ export function atomicWrite(path, content) {
33
+ mkdirSync(dirname(path), { recursive: true });
34
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
35
+ try {
36
+ writeFileSync(tmp, content);
37
+ renameSync(tmp, path);
38
+ } catch (err) {
39
+ try {
40
+ rmSync(tmp, { force: true });
41
+ } catch {}
42
+ throw err;
43
+ }
44
+ }