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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +35 -18
- package/README.md +25 -8
- package/commands/audit.md +2 -2
- package/commands/crystallize.md +22 -14
- package/commands/doctor.md +1 -1
- package/commands/feedback.md +2 -2
- package/commands/graph.md +1 -1
- package/commands/ingest.md +1 -1
- package/commands/init.md +2 -2
- package/commands/lint.md +1 -1
- package/commands/query.md +1 -1
- package/commands/rename.md +1 -1
- package/commands/resume.md +1 -1
- package/commands/stats.md +1 -1
- package/commands/uninstall.md +4 -2
- package/commands/upgrade.md +1 -1
- package/commands/verify.md +1 -1
- package/docs/ARCHITECTURE.md +4 -4
- package/docs/CONTRIBUTING.md +8 -7
- package/hooks/hypo-auto-commit.mjs +2 -2
- package/hooks/hypo-auto-minimal-crystallize.mjs +7 -7
- package/hooks/hypo-compact-guard.mjs +2 -2
- package/hooks/hypo-cwd-change.mjs +27 -37
- package/hooks/hypo-lookup.mjs +37 -2
- package/hooks/hypo-personal-check.mjs +37 -22
- package/hooks/hypo-session-end.mjs +1 -1
- package/hooks/hypo-session-record.mjs +2 -2
- package/hooks/hypo-session-start.mjs +62 -42
- package/hooks/hypo-shared.mjs +430 -85
- package/hooks/version-check.mjs +1 -1
- package/package.json +5 -1
- package/scripts/check-tracker-ids.mjs +69 -31
- package/scripts/crystallize.mjs +151 -53
- package/scripts/doctor.mjs +7 -7
- package/scripts/feedback-sync.mjs +5 -5
- package/scripts/feedback.mjs +9 -10
- package/scripts/init.mjs +7 -7
- package/scripts/lib/check-tracker-ids.mjs +90 -32
- package/scripts/lib/design-history-stale.mjs +1 -1
- package/scripts/lib/extensions.mjs +7 -7
- package/scripts/lib/failure-type.mjs +1 -1
- package/scripts/lib/feedback-scope.mjs +1 -1
- package/scripts/lib/page-usage.mjs +141 -0
- package/scripts/lib/project-create.mjs +2 -2
- package/scripts/lib/template-schema-version.mjs +1 -1
- package/scripts/lib/wd-match.mjs +181 -0
- package/scripts/lib/wikilink.mjs +1 -1
- package/scripts/lint.mjs +5 -5
- package/scripts/rename.mjs +1 -1
- package/scripts/resume.mjs +20 -22
- package/scripts/session-audit.mjs +1 -1
- package/scripts/stats.mjs +3 -3
- package/scripts/uninstall.mjs +3 -3
- package/scripts/upgrade.mjs +85 -18
- package/skills/crystallize/SKILL.md +17 -11
- package/skills/graph/SKILL.md +3 -3
- package/skills/ingest/SKILL.md +5 -5
- package/skills/lint/SKILL.md +3 -3
- package/skills/query/SKILL.md +3 -3
- package/skills/verify/SKILL.md +3 -3
- package/templates/SCHEMA.md +1 -1
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +21 -15
- package/templates/projects/_template/hot.md +1 -1
- package/scripts/fix-status-verify.mjs +0 -256
- package/scripts/lib/adr-corpus.mjs +0 -79
- package/scripts/lib/fix-manifest.mjs +0 -109
- 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,
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
382
|
-
|
|
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.
|
|
428
|
-
// when the
|
|
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)) {
|