hypomnema 1.4.2 → 1.5.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 (68) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ko.md +1 -1
  4. package/README.md +1 -1
  5. package/commands/audit.md +1 -1
  6. package/commands/crystallize.md +8 -8
  7. package/commands/doctor.md +1 -1
  8. package/commands/feedback.md +2 -2
  9. package/commands/graph.md +1 -1
  10. package/commands/ingest.md +1 -1
  11. package/commands/init.md +2 -2
  12. package/commands/lint.md +1 -1
  13. package/commands/query.md +1 -1
  14. package/commands/rename.md +1 -1
  15. package/commands/resume.md +1 -1
  16. package/commands/stats.md +1 -1
  17. package/commands/uninstall.md +4 -2
  18. package/commands/upgrade.md +1 -1
  19. package/commands/verify.md +1 -1
  20. package/docs/ARCHITECTURE.md +2 -2
  21. package/docs/CONTRIBUTING.md +8 -7
  22. package/hooks/hypo-auto-commit.mjs +2 -2
  23. package/hooks/hypo-auto-minimal-crystallize.mjs +7 -7
  24. package/hooks/hypo-compact-guard.mjs +2 -2
  25. package/hooks/hypo-cwd-change.mjs +27 -37
  26. package/hooks/hypo-personal-check.mjs +37 -22
  27. package/hooks/hypo-session-end.mjs +1 -1
  28. package/hooks/hypo-session-record.mjs +2 -2
  29. package/hooks/hypo-session-start.mjs +34 -40
  30. package/hooks/hypo-shared.mjs +271 -71
  31. package/hooks/version-check.mjs +1 -1
  32. package/package.json +5 -1
  33. package/scripts/check-tracker-ids.mjs +69 -31
  34. package/scripts/crystallize.mjs +82 -38
  35. package/scripts/doctor.mjs +7 -7
  36. package/scripts/feedback-sync.mjs +5 -5
  37. package/scripts/feedback.mjs +9 -10
  38. package/scripts/init.mjs +7 -7
  39. package/scripts/lib/check-tracker-ids.mjs +90 -32
  40. package/scripts/lib/design-history-stale.mjs +1 -1
  41. package/scripts/lib/extensions.mjs +7 -7
  42. package/scripts/lib/failure-type.mjs +1 -1
  43. package/scripts/lib/feedback-scope.mjs +1 -1
  44. package/scripts/lib/project-create.mjs +2 -2
  45. package/scripts/lib/template-schema-version.mjs +1 -1
  46. package/scripts/lib/wd-match.mjs +181 -0
  47. package/scripts/lib/wikilink.mjs +1 -1
  48. package/scripts/lint.mjs +5 -5
  49. package/scripts/rename.mjs +1 -1
  50. package/scripts/resume.mjs +20 -22
  51. package/scripts/session-audit.mjs +1 -1
  52. package/scripts/stats.mjs +3 -3
  53. package/scripts/uninstall.mjs +3 -3
  54. package/scripts/upgrade.mjs +17 -18
  55. package/skills/crystallize/SKILL.md +7 -7
  56. package/skills/graph/SKILL.md +2 -2
  57. package/skills/ingest/SKILL.md +4 -4
  58. package/skills/lint/SKILL.md +2 -2
  59. package/skills/query/SKILL.md +2 -2
  60. package/skills/verify/SKILL.md +2 -2
  61. package/templates/SCHEMA.md +1 -1
  62. package/templates/hypo-config.md +1 -1
  63. package/templates/hypo-guide.md +20 -14
  64. package/templates/projects/_template/hot.md +1 -1
  65. package/scripts/fix-status-verify.mjs +0 -256
  66. package/scripts/lib/adr-corpus.mjs +0 -79
  67. package/scripts/lib/fix-manifest.mjs +0 -109
  68. package/scripts/lib/fix-status-verify.mjs +0 -439
@@ -9,13 +9,13 @@
9
9
  * - hot.md has forbidden structure
10
10
  * - lint blockers exist
11
11
  *
12
- * Bypass options (checked in order, per ADR 0022 / spec §7.5):
12
+ * Bypass options (checked in order, per spec §7.5):
13
13
  * 1. HYPO_SKIP_GATE=1 env var
14
14
  * 2. HYPO_SKIP_GATE=1 in a recent *user-role* transcript message
15
15
  * (assistant/tool output is excluded to prevent self-triggering from block reason text)
16
16
  *
17
17
  * NOTE: capacity bypass (wiki-context-critical.json ≥90%) was REMOVED
18
- * (ADR 0022 amendment 2026-05-13). Spec §7.5: even at full context, minimal
18
+ * (amendment 2026-05-13). Spec §7.5: even at full context, minimal
19
19
  * session-close is mandatory — auto-bypass on capacity caused silent state loss.
20
20
  */
21
21
 
@@ -31,6 +31,7 @@ import {
31
31
  isGateSkipped,
32
32
  isClosePattern,
33
33
  extractUserMessages,
34
+ isUnderProjectDirs,
34
35
  } from './hypo-shared.mjs';
35
36
 
36
37
  const WARNING_FILE = join(homedir(), '.claude', 'state', 'wiki-context-warning.json');
@@ -52,7 +53,7 @@ process.stdin.on('end', () => {
52
53
  /* fail-open */
53
54
  }
54
55
 
55
- // ── Capacity bypass (≥90%) REMOVED — ADR 0022 amendment 2026-05-13.
56
+ // ── Capacity bypass (≥90%) REMOVED: amendment 2026-05-13.
56
57
  // Even at full context, minimal session-close is mandatory (spec §7.5).
57
58
  // Bypass paths are now only: HYPO_SKIP_GATE env / HYPO_SKIP_GATE in transcript.
58
59
 
@@ -93,12 +94,12 @@ process.stdin.on('end', () => {
93
94
  // ── Heavy checks ──
94
95
  const today = new Date().toISOString().slice(0, 10);
95
96
 
96
- // The full PreCompact gate decision, single-sourced (ADR 0046). The SAME
97
+ // The full PreCompact gate decision, single-sourced. The SAME
97
98
  // function backs `crystallize --check-session-close`, so a green self-check
98
99
  // there means this hook will not block. precompactGateStatus runs git-clean +
99
- // hot.md structure + session-close files (ADR 0043 global invariant) + scoped
100
+ // hot.md structure + session-close files (global invariant) + scoped
100
101
  // lint + W8 design-history + feedback projection. The transcript widens the
101
- // lint scope to this session's edited files (ADR 0041); without one the scope
102
+ // lint scope to this session's edited files; without one the scope
102
103
  // is the mandatory close files. Read-only: pure feedback drift comes back as
103
104
  // gate.driftTargets, a self-heal effect requirement we run as --write below.
104
105
  let gate;
@@ -116,7 +117,7 @@ process.stdin.on('end', () => {
116
117
  return;
117
118
  }
118
119
 
119
- // Self-heal pure feedback projection drift (ADR 0045): the one mutation the
120
+ // Self-heal pure feedback projection drift: the one mutation the
120
121
  // read-only gate leaves to the caller. Fails CLOSED — if the --write errors we
121
122
  // turn the (otherwise non-blocking) drift into a blocker, since real drift is
122
123
  // confirmed and silently passing it would defeat the gate. --write only applies
@@ -152,20 +153,34 @@ process.stdin.on('end', () => {
152
153
  }
153
154
 
154
155
  // Non-blocking heads-up about pre-existing lint / out-of-scope design-history
155
- // debt in untouched files (other projects / shared pages). Surfaced so it is
156
- // visible but never blocks compact. (Mirrors the pre-ADR-0046 inline behavior,
157
- // which lumped out-of-scope W8 into the same lint-notice list.)
156
+ // debt in untouched files. Surfaced so it is visible but never blocks compact.
157
+ // Scoped to the close-target (today-active) projects: debt under one of their
158
+ // dirs stays listed by filename; debt elsewhere (other projects, shared pages,
159
+ // root files) folds into a count so the same untouched-file debt does not
160
+ // re-list its filenames on every compact. Non-file diagnostics (a fail-open
161
+ // "lint skipped" notice carries no path) are preserved verbatim, never folded.
158
162
  const debtNotices = gate.notices.filter((n) => n.type === 'lint' || n.type === 'design-history');
159
- let noticeText =
160
- debtNotices.length > 0
161
- ? `[WIKI CHECK] ${debtNotices.length} pre-existing lint issue(s) in files this session did not touch (not blocking): ${[
162
- ...new Set(debtNotices.map((n) => n.reason.replace(/ \([^)]*\)$/, ''))),
163
- ]
164
- .slice(0, 5)
165
- .join(', ')}${debtNotices.length > 5 ? ', …' : ''} — clean up when convenient.`
166
- : '';
163
+ const activeSlugs = (gate.close?.projects || []).map((p) => p.project).filter(Boolean);
164
+ const nonFileNotices = debtNotices.filter((n) => !n.file);
165
+ const fileNotices = debtNotices.filter((n) => n.file);
166
+ const inScopeNotices = fileNotices.filter((n) => isUnderProjectDirs(n.file, activeSlugs));
167
+ const otherDebtCount = fileNotices.length - inScopeNotices.length;
168
+ const listed = [
169
+ ...nonFileNotices.map((n) => n.reason),
170
+ ...new Set(inScopeNotices.map((n) => n.reason.replace(/ \([^)]*\)$/, ''))),
171
+ ];
172
+ let noticeText = '';
173
+ if (listed.length > 0) {
174
+ noticeText = `[WIKI CHECK] ${listed.length} pre-existing lint issue(s) in files this session did not touch (not blocking): ${listed
175
+ .slice(0, 5)
176
+ .join(', ')}${listed.length > 5 ? ', …' : ''} — clean up when convenient.`;
177
+ }
178
+ if (otherDebtCount > 0) {
179
+ const fold = `+${otherDebtCount} pre-existing lint issue(s) elsewhere in the vault (other projects / shared pages, not blocking) — run \`/hypo:lint\` for the full list.`;
180
+ noticeText = noticeText ? `${noticeText}\n${fold}` : `[WIKI CHECK] ${fold}`;
181
+ }
167
182
  // Surface the self-heal so a re-synced projection is not a silent mutation of
168
- // the user's MEMORY.md / CLAUDE.md (ADR 0045 transparency).
183
+ // the user's MEMORY.md / CLAUDE.md (transparency).
169
184
  if (feedbackHealed) noticeText = noticeText ? `${noticeText}\n${feedbackHealed}` : feedbackHealed;
170
185
 
171
186
  if (gate.ok) {
@@ -200,7 +215,7 @@ process.stdin.on('end', () => {
200
215
  // ── Block ──
201
216
  // gate.blockers already carry per-type reasons in the canonical order
202
217
  // (git, hot, close, lint, design-history, feedback) — same strings as before
203
- // ADR 0046, now sourced from the shared gate instead of inline checks.
218
+ // Now sourced from the shared gate instead of inline checks.
204
219
  const reasons = [
205
220
  ...gate.blockers.map((b) => b.reason),
206
221
  gate.skipped.lint ? 'lint skipped (run `hypomnema init` to enable lint gate)' : '',
@@ -224,10 +239,10 @@ process.stdin.on('end', () => {
224
239
  ` [ ] 9. hot.md — update projects/<name>/hot.md (no exceptions)`,
225
240
  ` [ ] 10. root hot.md — update ~/hypomnema/hot.md active project table`,
226
241
  ` [ ] 11. updated: field — verify today's date on all touched .md files`,
227
- ` [ ] 12. lint — run scripts/lint.mjs; fix errors in files YOU touched`,
242
+ ` [ ] 12. lint — run /hypo:lint; fix errors in files YOU touched`,
228
243
  ` (other projects' / shared-page debt is reported as non-blocking notice)`,
229
244
  ` [ ] 13. git commit & push`,
230
- ` [ ] 14. verify — run \`crystallize.mjs --check-session-close\`; only declare`,
245
+ ` [ ] 14. verify — run /hypo:crystallize (--check-session-close mode); only declare`,
231
246
  ` the session closed once it prints "Compact-ready" (= this gate passes).`,
232
247
  ].join('\n');
233
248
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * hypo-session-end.mjs — SessionEnd hook (ADR 0022 Layer 2)
3
+ * hypo-session-end.mjs — SessionEnd hook (Layer 2)
4
4
  *
5
5
  * `/clear` cannot be blocked: it never fires UserPromptSubmit (Stage 0 PoC,
6
6
  * 2026-05-14). The only intervention point is the SessionEnd(reason='clear')
@@ -5,7 +5,7 @@
5
5
  * Appends an entry to ~/hypomnema/.cache/sessions/index.jsonl for each
6
6
  * completed session. The index.jsonl is the **primary** source for
7
7
  * scripts/session-audit.mjs (which falls back to ~/.claude/projects/<encoded>/
8
- * if the index is empty or missing — see ADR 0019).
8
+ * if the index is empty or missing).
9
9
  *
10
10
  * Silent: never blocks, never emits user-visible output.
11
11
  */
@@ -51,7 +51,7 @@ process.stdin.on('end', () => {
51
51
  transcript_path: transcriptPath,
52
52
  recorded_at: new Date().toISOString(),
53
53
  cwd: payload.cwd || process.cwd(),
54
- // PRAC-17: machine identity for multi-machine audit. index.jsonl lives
54
+ // machine identity for multi-machine audit. index.jsonl lives
55
55
  // under .cache/ (gitignored in a normal vault), so this is a LOCAL-only
56
56
  // per-session record — accurate for every session, no sync/privacy cost.
57
57
  device: hostname() || 'unknown',
@@ -7,7 +7,7 @@
7
7
  * MISS → inject global hot.md pointer only (no fan-out to all projects)
8
8
  */
9
9
 
10
- import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs';
10
+ import { readFileSync, writeFileSync, existsSync, realpathSync } from 'fs';
11
11
  import { homedir } from 'os';
12
12
  import { join, dirname } from 'path';
13
13
  import { fileURLToPath } from 'url';
@@ -28,6 +28,9 @@ import {
28
28
  buildProjectSuggestionLine,
29
29
  recordSuggestionCooldown,
30
30
  sanitizeProjForPrompt,
31
+ pickProjectByCwd,
32
+ collectProjectWorkingDirs,
33
+ buildVaultOrientation,
31
34
  } from './hypo-shared.mjs';
32
35
  import {
33
36
  defaultCachePath,
@@ -124,7 +127,7 @@ function buildUpdateNotice() {
124
127
  }
125
128
 
126
129
  /**
127
- * Stale-sibling notice (ADR 0038, D3). The update-notifier above only knows
130
+ * Stale-sibling notice (D3). The update-notifier above only knows
128
131
  * whether the ACTIVE install is behind latest — it is blind to an OLDER sibling
129
132
  * that owns the `hypomnema` bin on PATH. That sibling is the live footgun:
130
133
  * running `hypomnema init`/`upgrade` through it downgrades the active hooks.
@@ -176,7 +179,7 @@ function readLastGrowthLine() {
176
179
  }
177
180
 
178
181
  /**
179
- * ADR 0022 amendment 2026-05-14: if the prior session ended
182
+ * Amendment 2026-05-14: if the prior session ended
180
183
  * via `/clear`, hypo-session-end stashed its identity in `.cache/clear-marker.json`.
181
184
  * Read it (with 7-day stale guard), unlink it (one-shot), and return a
182
185
  * `[WIKI_AUTOCLOSE]` recovery line for additionalContext + stderr.
@@ -256,41 +259,26 @@ const GLOBAL_HOT = join(HYPO_DIR, 'hot.md');
256
259
  const HOT_CHARS = 2000;
257
260
  const STATE_CHARS = 2000;
258
261
 
259
- function parseFrontmatterField(content, key) {
260
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
261
- if (!match) return null;
262
- const line = match[1].split('\n').find((l) => l.startsWith(`${key}:`));
263
- if (!line) return null;
264
- return line
265
- .slice(key.length + 1)
266
- .trim()
267
- .replace(/^['"]|['"]$/g, '');
268
- }
269
-
270
262
  function findProjectFiles(cwd) {
271
263
  if (!existsSync(PROJECTS_DIR)) return null;
272
- for (const proj of readdirSync(PROJECTS_DIR)) {
273
- const projDir = join(PROJECTS_DIR, proj);
274
- if (!statSync(projDir).isDirectory()) continue;
275
- const indexPath = join(projDir, 'index.md');
276
- if (!existsSync(indexPath)) continue;
277
- const content = readFileSync(indexPath, 'utf-8');
278
- const workingDir = parseFrontmatterField(content, 'working_dir');
279
- if (!workingDir) continue;
280
- const resolved = workingDir.startsWith('~/')
281
- ? join(homedir(), workingDir.slice(2))
282
- : workingDir;
283
- if (cwd === resolved || cwd.startsWith(resolved + '/')) {
284
- const hotPath = join(projDir, 'hot.md');
285
- const statePath = join(projDir, 'session-state.md');
286
- return {
287
- proj,
288
- hotPath: existsSync(hotPath) ? hotPath : null,
289
- statePath: existsSync(statePath) ? statePath : null,
290
- };
291
- }
264
+ let realpathCwd = null;
265
+ try {
266
+ realpathCwd = realpathSync(cwd);
267
+ } catch {
268
+ realpathCwd = null;
292
269
  }
293
- return null;
270
+ // Two-tier match (absolute prefix, then cross-machine unique basename) so a
271
+ // vault synced from another machine still resolves the cwd to its project.
272
+ const proj = pickProjectByCwd(collectProjectWorkingDirs(HYPO_DIR), cwd, { realpathCwd });
273
+ if (!proj) return null;
274
+ const projDir = join(PROJECTS_DIR, proj);
275
+ const hotPath = join(projDir, 'hot.md');
276
+ const statePath = join(projDir, 'session-state.md');
277
+ return {
278
+ proj,
279
+ hotPath: existsSync(hotPath) ? hotPath : null,
280
+ statePath: existsSync(statePath) ? statePath : null,
281
+ };
294
282
  }
295
283
 
296
284
  function extractSection(content, heading) {
@@ -343,7 +331,7 @@ process.stdin.on('end', () => {
343
331
  const pullOk = gitPull(HYPO_DIR);
344
332
  const syncLine = syncStateNotice(pullOk);
345
333
  const growthLine = readLastGrowthLine();
346
- // ADR 0022 amendment: on source='clear', surface the dying
334
+ // On source='clear', surface the dying
347
335
  // session's identity that hypo-session-end stashed so Claude can recover
348
336
  // session-close work that /clear skipped. One-shot: marker is unlinked
349
337
  // immediately after read.
@@ -377,6 +365,12 @@ process.stdin.on('end', () => {
377
365
 
378
366
  const ignorePatterns = loadHypoIgnore(HYPO_DIR);
379
367
 
368
+ // When cwd is a project working_dir that is NOT the vault itself, tell the
369
+ // AI where the vault lives so it does not re-discover the path or look for
370
+ // wiki files in the code repo. '' when cwd === vault root.
371
+ const vaultOrientation = hit ? buildVaultOrientation(cwd) : '';
372
+ const hitPrefix = vaultOrientation ? `${vaultOrientation}\n\n` : '';
373
+
380
374
  if (hit) {
381
375
  const hotContent = readIfNotIgnored(hit.hotPath, HOT_CHARS, ignorePatterns);
382
376
  const stateContent = readIfNotIgnored(hit.statePath, STATE_CHARS, ignorePatterns);
@@ -399,7 +393,7 @@ process.stdin.on('end', () => {
399
393
  console.log(
400
394
  JSON.stringify(
401
395
  buildOutput(
402
- `${noticePrefix}[WIKI HOT CACHE: project=${sanitizeProjForPrompt(hit.proj)}]\n\n${parts.join('\n\n')}`,
396
+ `${noticePrefix}${hitPrefix}[WIKI HOT CACHE: project=${sanitizeProjForPrompt(hit.proj)}]\n\n${parts.join('\n\n')}`,
403
397
  outExtra,
404
398
  ),
405
399
  ),
@@ -415,7 +409,7 @@ process.stdin.on('end', () => {
415
409
  console.log(
416
410
  JSON.stringify(
417
411
  buildOutput(
418
- `${noticePrefix}[WIKI HOT CACHE: project=${sanitizeProjForPrompt(hit.proj)}, no snapshot yet]`,
412
+ `${noticePrefix}${hitPrefix}[WIKI HOT CACHE: project=${sanitizeProjForPrompt(hit.proj)}, no snapshot yet]`,
419
413
  outExtra,
420
414
  ),
421
415
  ),
@@ -424,8 +418,8 @@ process.stdin.on('end', () => {
424
418
  return;
425
419
  }
426
420
 
427
- // MISS: cwd matches no project. ADR 0023 — offer to create one
428
- // when the ADR trigger conditions hold (git repo + project marker + no
421
+ // MISS: cwd matches no project. Offer to create one
422
+ // when the trigger conditions hold (git repo + project marker + no
429
423
  // cooldown + not previously declined). The actual scaffold is the LLM's
430
424
  // job on a "Y" reply (scripts/lib/project-create.mjs); the hook only nudges.
431
425
  if (shouldSuggestProjectCreation(cwd, HYPO_DIR)) {