hypomnema 1.4.2 → 1.5.1

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 (70) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ko.md +35 -18
  4. package/README.md +25 -8
  5. package/commands/audit.md +2 -2
  6. package/commands/crystallize.md +22 -14
  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 +4 -4
  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-lookup.mjs +37 -2
  27. package/hooks/hypo-personal-check.mjs +37 -22
  28. package/hooks/hypo-session-end.mjs +1 -1
  29. package/hooks/hypo-session-record.mjs +2 -2
  30. package/hooks/hypo-session-start.mjs +62 -42
  31. package/hooks/hypo-shared.mjs +430 -85
  32. package/hooks/version-check.mjs +1 -1
  33. package/package.json +5 -1
  34. package/scripts/check-tracker-ids.mjs +69 -31
  35. package/scripts/crystallize.mjs +151 -53
  36. package/scripts/doctor.mjs +7 -7
  37. package/scripts/feedback-sync.mjs +5 -5
  38. package/scripts/feedback.mjs +9 -10
  39. package/scripts/init.mjs +7 -7
  40. package/scripts/lib/check-tracker-ids.mjs +90 -32
  41. package/scripts/lib/design-history-stale.mjs +1 -1
  42. package/scripts/lib/extensions.mjs +7 -7
  43. package/scripts/lib/failure-type.mjs +1 -1
  44. package/scripts/lib/feedback-scope.mjs +1 -1
  45. package/scripts/lib/page-usage.mjs +141 -0
  46. package/scripts/lib/project-create.mjs +2 -2
  47. package/scripts/lib/template-schema-version.mjs +1 -1
  48. package/scripts/lib/wd-match.mjs +181 -0
  49. package/scripts/lib/wikilink.mjs +1 -1
  50. package/scripts/lint.mjs +5 -5
  51. package/scripts/rename.mjs +1 -1
  52. package/scripts/resume.mjs +20 -22
  53. package/scripts/session-audit.mjs +1 -1
  54. package/scripts/stats.mjs +3 -3
  55. package/scripts/uninstall.mjs +3 -3
  56. package/scripts/upgrade.mjs +85 -18
  57. package/skills/crystallize/SKILL.md +17 -11
  58. package/skills/graph/SKILL.md +3 -3
  59. package/skills/ingest/SKILL.md +5 -5
  60. package/skills/lint/SKILL.md +3 -3
  61. package/skills/query/SKILL.md +3 -3
  62. package/skills/verify/SKILL.md +3 -3
  63. package/templates/SCHEMA.md +1 -1
  64. package/templates/hypo-config.md +1 -1
  65. package/templates/hypo-guide.md +21 -15
  66. package/templates/projects/_template/hot.md +1 -1
  67. package/scripts/fix-status-verify.mjs +0 -256
  68. package/scripts/lib/adr-corpus.mjs +0 -79
  69. package/scripts/lib/fix-manifest.mjs +0 -109
  70. package/scripts/lib/fix-status-verify.mjs +0 -439
@@ -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,10 @@ import {
28
28
  buildProjectSuggestionLine,
29
29
  recordSuggestionCooldown,
30
30
  sanitizeProjForPrompt,
31
+ pickProjectByCwd,
32
+ collectProjectWorkingDirs,
33
+ buildVaultOrientation,
34
+ staleMarkerFor,
31
35
  } from './hypo-shared.mjs';
32
36
  import {
33
37
  defaultCachePath,
@@ -53,6 +57,21 @@ function readIfNotIgnored(path, maxChars, patterns) {
53
57
  return readFileSync(path, 'utf-8').slice(0, maxChars);
54
58
  }
55
59
 
60
+ // Compute the STALE marker for a hot/state file from its RAW content (readIfNotIgnored
61
+ // already slices, which could truncate frontmatter). Honors the same .hypoignore
62
+ // privacy guard, and returns '' for any miss (no path, ignored, absent, no
63
+ // verify_by_date, or error) so derived summaries pass through unchanged.
64
+ function staleMarkerForPath(path, patterns, today) {
65
+ try {
66
+ if (!path) return '';
67
+ if (patterns.length > 0 && isIgnored(path, HYPO_DIR, patterns)) return '';
68
+ if (!existsSync(path)) return '';
69
+ return staleMarkerFor(readFileSync(path, 'utf-8'), today);
70
+ } catch {
71
+ return '';
72
+ }
73
+ }
74
+
56
75
  // Directory of the running hook, and the install root one level up
57
76
  // (<root>/hooks/...). The root is derived from the RUNNING hook path rather
58
77
  // than ~/.claude/hypo-pkg.json so a dual install (npm + plugin) or a stale
@@ -124,7 +143,7 @@ function buildUpdateNotice() {
124
143
  }
125
144
 
126
145
  /**
127
- * Stale-sibling notice (ADR 0038, D3). The update-notifier above only knows
146
+ * Stale-sibling notice (D3). The update-notifier above only knows
128
147
  * whether the ACTIVE install is behind latest — it is blind to an OLDER sibling
129
148
  * that owns the `hypomnema` bin on PATH. That sibling is the live footgun:
130
149
  * running `hypomnema init`/`upgrade` through it downgrades the active hooks.
@@ -176,7 +195,7 @@ function readLastGrowthLine() {
176
195
  }
177
196
 
178
197
  /**
179
- * ADR 0022 amendment 2026-05-14: if the prior session ended
198
+ * Amendment 2026-05-14: if the prior session ended
180
199
  * via `/clear`, hypo-session-end stashed its identity in `.cache/clear-marker.json`.
181
200
  * Read it (with 7-day stale guard), unlink it (one-shot), and return a
182
201
  * `[WIKI_AUTOCLOSE]` recovery line for additionalContext + stderr.
@@ -256,41 +275,26 @@ const GLOBAL_HOT = join(HYPO_DIR, 'hot.md');
256
275
  const HOT_CHARS = 2000;
257
276
  const STATE_CHARS = 2000;
258
277
 
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
278
  function findProjectFiles(cwd) {
271
279
  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
- }
280
+ let realpathCwd = null;
281
+ try {
282
+ realpathCwd = realpathSync(cwd);
283
+ } catch {
284
+ realpathCwd = null;
292
285
  }
293
- return null;
286
+ // Two-tier match (absolute prefix, then cross-machine unique basename) so a
287
+ // vault synced from another machine still resolves the cwd to its project.
288
+ const proj = pickProjectByCwd(collectProjectWorkingDirs(HYPO_DIR), cwd, { realpathCwd });
289
+ if (!proj) return null;
290
+ const projDir = join(PROJECTS_DIR, proj);
291
+ const hotPath = join(projDir, 'hot.md');
292
+ const statePath = join(projDir, 'session-state.md');
293
+ return {
294
+ proj,
295
+ hotPath: existsSync(hotPath) ? hotPath : null,
296
+ statePath: existsSync(statePath) ? statePath : null,
297
+ };
294
298
  }
295
299
 
296
300
  function extractSection(content, heading) {
@@ -343,7 +347,7 @@ process.stdin.on('end', () => {
343
347
  const pullOk = gitPull(HYPO_DIR);
344
348
  const syncLine = syncStateNotice(pullOk);
345
349
  const growthLine = readLastGrowthLine();
346
- // ADR 0022 amendment: on source='clear', surface the dying
350
+ // On source='clear', surface the dying
347
351
  // session's identity that hypo-session-end stashed so Claude can recover
348
352
  // session-close work that /clear skipped. One-shot: marker is unlinked
349
353
  // immediately after read.
@@ -377,9 +381,25 @@ process.stdin.on('end', () => {
377
381
 
378
382
  const ignorePatterns = loadHypoIgnore(HYPO_DIR);
379
383
 
384
+ // When cwd is a project working_dir that is NOT the vault itself, tell the
385
+ // AI where the vault lives so it does not re-discover the path or look for
386
+ // wiki files in the code repo. '' when cwd === vault root.
387
+ const vaultOrientation = hit ? buildVaultOrientation(cwd) : '';
388
+ const hitPrefix = vaultOrientation ? `${vaultOrientation}\n\n` : '';
389
+
380
390
  if (hit) {
381
- const hotContent = readIfNotIgnored(hit.hotPath, HOT_CHARS, ignorePatterns);
382
- const stateContent = readIfNotIgnored(hit.statePath, STATE_CHARS, ignorePatterns);
391
+ // project hot/state only. root/global hot (below) is a derived pointer
392
+ // table with no per-page frontmatter, so it is never a STALE target and
393
+ // gets no marker logic. TODAY is UTC to match doctor.mjs (D1/D2). The
394
+ // marker is computed on raw content (staleMarkerForPath), then prepended
395
+ // onto the sliced display content; a no-op when there is no verify_by_date.
396
+ const TODAY = new Date().toISOString().slice(0, 10);
397
+ let hotContent = readIfNotIgnored(hit.hotPath, HOT_CHARS, ignorePatterns);
398
+ let stateContent = readIfNotIgnored(hit.statePath, STATE_CHARS, ignorePatterns);
399
+ const hotMarker = staleMarkerForPath(hit.hotPath, ignorePatterns, TODAY);
400
+ const stateMarker = staleMarkerForPath(hit.statePath, ignorePatterns, TODAY);
401
+ if (hotContent && hotMarker) hotContent = `${hotMarker}\n${hotContent}`;
402
+ if (stateContent && stateMarker) stateContent = `${stateMarker}\n${stateContent}`;
383
403
 
384
404
  if (hotContent || stateContent) {
385
405
  printTerminalSummary(hit.proj, hotContent, stateContent);
@@ -399,7 +419,7 @@ process.stdin.on('end', () => {
399
419
  console.log(
400
420
  JSON.stringify(
401
421
  buildOutput(
402
- `${noticePrefix}[WIKI HOT CACHE: project=${sanitizeProjForPrompt(hit.proj)}]\n\n${parts.join('\n\n')}`,
422
+ `${noticePrefix}${hitPrefix}[WIKI HOT CACHE: project=${sanitizeProjForPrompt(hit.proj)}]\n\n${parts.join('\n\n')}`,
403
423
  outExtra,
404
424
  ),
405
425
  ),
@@ -415,7 +435,7 @@ process.stdin.on('end', () => {
415
435
  console.log(
416
436
  JSON.stringify(
417
437
  buildOutput(
418
- `${noticePrefix}[WIKI HOT CACHE: project=${sanitizeProjForPrompt(hit.proj)}, no snapshot yet]`,
438
+ `${noticePrefix}${hitPrefix}[WIKI HOT CACHE: project=${sanitizeProjForPrompt(hit.proj)}, no snapshot yet]`,
419
439
  outExtra,
420
440
  ),
421
441
  ),
@@ -424,8 +444,8 @@ process.stdin.on('end', () => {
424
444
  return;
425
445
  }
426
446
 
427
- // MISS: cwd matches no project. ADR 0023 — offer to create one
428
- // when the ADR trigger conditions hold (git repo + project marker + no
447
+ // MISS: cwd matches no project. Offer to create one
448
+ // when the trigger conditions hold (git repo + project marker + no
429
449
  // cooldown + not previously declined). The actual scaffold is the LLM's
430
450
  // job on a "Y" reply (scripts/lib/project-create.mjs); the hook only nudges.
431
451
  if (shouldSuggestProjectCreation(cwd, HYPO_DIR)) {