hypomnema 1.4.1 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/commands/audit.md +1 -1
- package/commands/crystallize.md +11 -9
- 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 +2 -2
- package/docs/CONTRIBUTING.md +20 -12
- package/hooks/hypo-auto-commit.mjs +2 -2
- package/hooks/hypo-auto-minimal-crystallize.mjs +30 -18
- package/hooks/hypo-compact-guard.mjs +2 -2
- package/hooks/hypo-cwd-change.mjs +27 -37
- 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 +34 -40
- package/hooks/hypo-shared.mjs +364 -82
- package/hooks/version-check.mjs +1 -1
- package/package.json +5 -1
- package/scripts/check-tracker-ids.mjs +69 -31
- package/scripts/crystallize.mjs +322 -70
- 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/project-create.mjs +25 -7
- package/scripts/lib/schema-vocab.mjs +105 -1
- 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 +14 -6
- 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 +17 -18
- package/skills/crystallize/SKILL.md +16 -8
- package/skills/graph/SKILL.md +2 -2
- package/skills/ingest/SKILL.md +4 -4
- package/skills/lint/SKILL.md +2 -2
- package/skills/query/SKILL.md +2 -2
- package/skills/verify/SKILL.md +2 -2
- package/templates/SCHEMA.md +12 -4
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +24 -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
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* project hot.md. Skips if still within the same project subtree.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { readFileSync, writeFileSync, existsSync,
|
|
10
|
-
import { homedir } from 'os';
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync, realpathSync } from 'fs';
|
|
11
10
|
import { join } from 'path';
|
|
12
11
|
import {
|
|
13
12
|
HYPO_DIR,
|
|
@@ -19,6 +18,9 @@ import {
|
|
|
19
18
|
buildProjectSuggestionLine,
|
|
20
19
|
recordSuggestionCooldown,
|
|
21
20
|
sanitizeProjForPrompt,
|
|
21
|
+
pickProjectByCwd,
|
|
22
|
+
collectProjectWorkingDirs,
|
|
23
|
+
buildVaultOrientation,
|
|
22
24
|
} from './hypo-shared.mjs';
|
|
23
25
|
|
|
24
26
|
const PROJECTS_DIR = join(HYPO_DIR, 'projects');
|
|
@@ -33,42 +35,26 @@ function readIfNotIgnored(path, patterns) {
|
|
|
33
35
|
return readFileSync(path, 'utf-8').slice(0, MAX_CHARS);
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
function parseFrontmatterField(content, key) {
|
|
37
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
38
|
-
if (!match) return null;
|
|
39
|
-
const line = match[1].split('\n').find((l) => l.startsWith(`${key}:`));
|
|
40
|
-
if (!line) return null;
|
|
41
|
-
return line
|
|
42
|
-
.slice(key.length + 1)
|
|
43
|
-
.trim()
|
|
44
|
-
.replace(/^['"]|['"]$/g, '');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
38
|
function findProjectHot(cwd) {
|
|
48
39
|
if (!existsSync(PROJECTS_DIR)) return null;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const content = readFileSync(indexPath, 'utf-8');
|
|
55
|
-
const workingDir = parseFrontmatterField(content, 'working_dir');
|
|
56
|
-
if (!workingDir) continue;
|
|
57
|
-
const resolved = workingDir.startsWith('~/')
|
|
58
|
-
? join(homedir(), workingDir.slice(2))
|
|
59
|
-
: workingDir;
|
|
60
|
-
if (cwd === resolved || cwd.startsWith(resolved + '/')) {
|
|
61
|
-
const hotPath = join(projDir, 'hot.md');
|
|
62
|
-
const statePath = join(projDir, 'session-state.md');
|
|
63
|
-
return {
|
|
64
|
-
proj,
|
|
65
|
-
hotPath: existsSync(hotPath) ? hotPath : null,
|
|
66
|
-
statePath: existsSync(statePath) ? statePath : null,
|
|
67
|
-
resolved,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
40
|
+
let realpathCwd = null;
|
|
41
|
+
try {
|
|
42
|
+
realpathCwd = realpathSync(cwd);
|
|
43
|
+
} catch {
|
|
44
|
+
realpathCwd = null;
|
|
70
45
|
}
|
|
71
|
-
|
|
46
|
+
// Two-tier match (absolute prefix, then cross-machine unique basename) so a
|
|
47
|
+
// vault synced from another machine still resolves the cwd to its project.
|
|
48
|
+
const proj = pickProjectByCwd(collectProjectWorkingDirs(HYPO_DIR), cwd, { realpathCwd });
|
|
49
|
+
if (!proj) return null;
|
|
50
|
+
const projDir = join(PROJECTS_DIR, proj);
|
|
51
|
+
const hotPath = join(projDir, 'hot.md');
|
|
52
|
+
const statePath = join(projDir, 'session-state.md');
|
|
53
|
+
return {
|
|
54
|
+
proj,
|
|
55
|
+
hotPath: existsSync(hotPath) ? hotPath : null,
|
|
56
|
+
statePath: existsSync(statePath) ? statePath : null,
|
|
57
|
+
};
|
|
72
58
|
}
|
|
73
59
|
|
|
74
60
|
let raw = '';
|
|
@@ -123,10 +109,14 @@ process.stdin.on('end', () => {
|
|
|
123
109
|
);
|
|
124
110
|
}
|
|
125
111
|
}
|
|
112
|
+
// Same vault orientation as session-start: when the new cwd is a project
|
|
113
|
+
// working_dir distinct from the vault, surface where wiki files live.
|
|
114
|
+
const vaultOrientation = buildVaultOrientation(newCwd);
|
|
115
|
+
const orientPrefix = vaultOrientation ? `${vaultOrientation}\n\n` : '';
|
|
126
116
|
console.log(
|
|
127
117
|
JSON.stringify(
|
|
128
118
|
buildOutput(
|
|
129
|
-
|
|
119
|
+
`${orientPrefix}[WIKI: cwd changed → project=${sanitizeProjForPrompt(newHit.proj)}]\n\n${content}`,
|
|
130
120
|
{
|
|
131
121
|
continue: true,
|
|
132
122
|
suppressOutput: true,
|
|
@@ -137,7 +127,7 @@ process.stdin.on('end', () => {
|
|
|
137
127
|
return;
|
|
138
128
|
}
|
|
139
129
|
|
|
140
|
-
// MISS: cwd matches no project.
|
|
130
|
+
// MISS: cwd matches no project. Offer to create one
|
|
141
131
|
// when the trigger conditions hold. Same nudge-only model as session-start.
|
|
142
132
|
let suggestPrefix = '';
|
|
143
133
|
if (shouldSuggestProjectCreation(newCwd, HYPO_DIR)) {
|
|
@@ -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
|
|
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
|
-
* (
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
156
|
-
//
|
|
157
|
-
//
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
//
|
|
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,
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
}
|
|
264
|
+
let realpathCwd = null;
|
|
265
|
+
try {
|
|
266
|
+
realpathCwd = realpathSync(cwd);
|
|
267
|
+
} catch {
|
|
268
|
+
realpathCwd = null;
|
|
292
269
|
}
|
|
293
|
-
|
|
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
|
-
//
|
|
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.
|
|
428
|
-
// when the
|
|
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)) {
|