hypomnema 1.1.0 → 1.2.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 +4 -2
- package/README.md +4 -2
- package/commands/audit.md +2 -2
- package/commands/crystallize.md +113 -23
- package/commands/feedback.md +40 -26
- package/commands/ingest.md +31 -9
- package/commands/upgrade.md +2 -2
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONTRIBUTING.md +1 -1
- package/hooks/hooks.json +30 -1
- package/hooks/hypo-auto-commit.mjs +10 -4
- package/hooks/hypo-auto-minimal-crystallize.mjs +145 -0
- package/hooks/hypo-auto-stage.mjs +4 -3
- package/hooks/hypo-compact-guard.mjs +33 -24
- package/hooks/hypo-cwd-change.mjs +107 -24
- package/hooks/hypo-file-watch.mjs +23 -10
- package/hooks/hypo-first-prompt.mjs +37 -23
- package/hooks/hypo-hot-rebuild.mjs +22 -10
- package/hooks/hypo-lookup.mjs +171 -65
- package/hooks/hypo-personal-check.mjs +207 -112
- package/hooks/hypo-pre-commit.mjs +46 -0
- package/hooks/hypo-session-end.mjs +58 -0
- package/hooks/hypo-session-record.mjs +11 -5
- package/hooks/hypo-session-start.mjs +298 -52
- package/hooks/hypo-shared.mjs +793 -37
- package/hooks/hypo-web-fetch-ingest.mjs +121 -0
- package/hooks/version-check-fetch.mjs +74 -0
- package/hooks/version-check.mjs +184 -0
- package/package.json +17 -3
- package/scripts/crystallize.mjs +623 -18
- package/scripts/doctor.mjs +730 -47
- package/scripts/feedback-sync.mjs +974 -0
- package/scripts/feedback.mjs +253 -44
- package/scripts/graph.mjs +35 -22
- package/scripts/ingest.mjs +89 -16
- package/scripts/init.mjs +398 -113
- package/scripts/lib/design-history-stale.mjs +83 -0
- package/scripts/lib/extensions.mjs +749 -0
- package/scripts/lib/frontmatter.mjs +5 -1
- package/scripts/lib/hypo-ignore.mjs +12 -10
- package/scripts/lib/pkg-json.mjs +23 -5
- package/scripts/lib/project-create.mjs +225 -0
- package/scripts/lib/schema-vocab.mjs +96 -0
- package/scripts/lint.mjs +238 -31
- package/scripts/query.mjs +26 -10
- package/scripts/resume.mjs +11 -5
- package/scripts/session-audit.mjs +37 -27
- package/scripts/smoke-pack.mjs +224 -0
- package/scripts/stats.mjs +24 -10
- package/scripts/uninstall.mjs +363 -49
- package/scripts/upgrade.mjs +706 -202
- package/scripts/verify.mjs +24 -14
- package/scripts/weekly-report.mjs +59 -25
- package/skills/crystallize/SKILL.md +20 -7
- package/skills/ingest/SKILL.md +25 -5
- package/templates/.hypoignore +16 -2
- package/templates/Home.md +2 -0
- package/templates/SCHEMA.md +61 -6
- package/templates/extensions/agents/.gitkeep +0 -0
- package/templates/extensions/commands/.gitkeep +0 -0
- package/templates/extensions/hooks/.gitkeep +0 -0
- package/templates/extensions/skills/.gitkeep +0 -0
- package/templates/gitignore +5 -0
- package/templates/hot.md +2 -0
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +42 -2
- package/templates/hypo-help.md +1 -1
- package/templates/pages/observability/_index.md +77 -0
- package/templates/projects/_template/index.md +2 -2
- package/templates/projects/_template/prd.md +1 -1
|
@@ -8,13 +8,118 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
11
|
-
import { homedir
|
|
12
|
-
import { join } from 'path';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { spawnSync, spawn } from 'child_process';
|
|
15
|
+
import {
|
|
16
|
+
HYPO_DIR,
|
|
17
|
+
buildOutput,
|
|
18
|
+
SESSION_STATE_NEXT_HEADINGS,
|
|
19
|
+
formatGrowthMetrics,
|
|
20
|
+
readSyncState,
|
|
21
|
+
clearSyncState,
|
|
22
|
+
readClearMarker,
|
|
23
|
+
clearClearMarker,
|
|
24
|
+
loadHypoIgnore,
|
|
25
|
+
isIgnored,
|
|
26
|
+
sessionMarkerPath,
|
|
27
|
+
shouldSuggestProjectCreation,
|
|
28
|
+
buildProjectSuggestionLine,
|
|
29
|
+
recordSuggestionCooldown,
|
|
30
|
+
} from './hypo-shared.mjs';
|
|
31
|
+
import {
|
|
32
|
+
defaultCachePath,
|
|
33
|
+
detectChannel,
|
|
34
|
+
readCache,
|
|
35
|
+
cacheIsFresh,
|
|
36
|
+
computeNotice,
|
|
37
|
+
markNotified,
|
|
38
|
+
isOptedOut,
|
|
39
|
+
} from './version-check.mjs';
|
|
15
40
|
|
|
16
|
-
|
|
17
|
-
|
|
41
|
+
// Privacy guard: refuse to read+inject .hypoignore-matched
|
|
42
|
+
// wiki files into additionalContext. Without this, a user who lists
|
|
43
|
+
// `projects/private/hot.md` in .hypoignore would still see SECRET emit because
|
|
44
|
+
// session-start reads hot/state paths directly.
|
|
45
|
+
function readIfNotIgnored(path, maxChars, patterns) {
|
|
46
|
+
if (!path) return null;
|
|
47
|
+
if (patterns.length > 0 && isIgnored(path, HYPO_DIR, patterns)) return null;
|
|
48
|
+
return readFileSync(path, 'utf-8').slice(0, maxChars);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Directory of the running hook, and the install root one level up
|
|
52
|
+
// (<root>/hooks/...). The root is derived from the RUNNING hook path rather
|
|
53
|
+
// than ~/.claude/hypo-pkg.json so a dual install (npm + plugin) or a stale
|
|
54
|
+
// metadata file can't mislabel the channel (teams review (b), 2026-05-21).
|
|
55
|
+
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
56
|
+
const ACTIVE_ROOT = dirname(HOOK_DIR);
|
|
57
|
+
|
|
58
|
+
function readInstalledVersion(root) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')).version || null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Update-notifier (teams-reviewed 2026-05-21). Reads ONLY the cache — never a
|
|
68
|
+
* synchronous network call. When the cache is stale, fires a detached worker to
|
|
69
|
+
* refresh it (shown next session). Fully best-effort: any failure returns ''.
|
|
70
|
+
*/
|
|
71
|
+
function buildUpdateNotice() {
|
|
72
|
+
try {
|
|
73
|
+
if (isOptedOut()) return '';
|
|
74
|
+
const cachePath = defaultCachePath();
|
|
75
|
+
|
|
76
|
+
let root = ACTIVE_ROOT;
|
|
77
|
+
let version = readInstalledVersion(root);
|
|
78
|
+
if (!version) {
|
|
79
|
+
try {
|
|
80
|
+
const meta = JSON.parse(readFileSync(join(homedir(), '.claude', 'hypo-pkg.json'), 'utf-8'));
|
|
81
|
+
root = meta.pkgRoot || root;
|
|
82
|
+
version = meta.pkgVersion || readInstalledVersion(root);
|
|
83
|
+
} catch {
|
|
84
|
+
/* fallback unavailable */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!version) return '';
|
|
88
|
+
|
|
89
|
+
const channel = detectChannel(root);
|
|
90
|
+
const cache = readCache(cachePath);
|
|
91
|
+
|
|
92
|
+
if (!cacheIsFresh(cache)) {
|
|
93
|
+
try {
|
|
94
|
+
const worker = join(HOOK_DIR, 'version-check-fetch.mjs');
|
|
95
|
+
if (existsSync(worker)) {
|
|
96
|
+
const child = spawn(process.execPath, [worker, cachePath], {
|
|
97
|
+
detached: true,
|
|
98
|
+
stdio: 'ignore',
|
|
99
|
+
});
|
|
100
|
+
// spawn() failures (EAGAIN/EMFILE/ENOENT) surface ASYNChronously on
|
|
101
|
+
// the child's 'error' event — the try/catch above only catches the
|
|
102
|
+
// synchronous throw. Without this listener an unhandled 'error' would
|
|
103
|
+
// crash SessionStart, violating the best-effort contract.
|
|
104
|
+
child.on('error', () => {});
|
|
105
|
+
child.unref();
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
/* spawn is best-effort */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const notice = computeNotice(cache, channel, version);
|
|
113
|
+
if (!notice) return '';
|
|
114
|
+
markNotified(cachePath, channel, notice.latest);
|
|
115
|
+
return notice.line;
|
|
116
|
+
} catch {
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const PROJECTS_DIR = join(HYPO_DIR, 'projects');
|
|
122
|
+
const GROWTH_CACHE = join(HYPO_DIR, '.cache', 'last-session-growth.json');
|
|
18
123
|
|
|
19
124
|
function readLastGrowthLine() {
|
|
20
125
|
if (!existsSync(GROWTH_CACHE)) return '';
|
|
@@ -26,41 +131,110 @@ function readLastGrowthLine() {
|
|
|
26
131
|
}
|
|
27
132
|
}
|
|
28
133
|
|
|
134
|
+
/**
|
|
135
|
+
* fix #25 PR-A2 (ADR 0022 amendment 2026-05-14): if the prior session ended
|
|
136
|
+
* via `/clear`, hypo-session-end stashed its identity in `.cache/clear-marker.json`.
|
|
137
|
+
* Read it (with 7-day stale guard), unlink it (one-shot), and return a
|
|
138
|
+
* `[WIKI_AUTOCLOSE]` recovery line for additionalContext + stderr.
|
|
139
|
+
*
|
|
140
|
+
* @param {string|undefined} source SessionStart payload `source` field
|
|
141
|
+
* @returns {string} recovery line, or '' when no recovery is needed
|
|
142
|
+
*/
|
|
143
|
+
function buildClearRecoveryLine(source) {
|
|
144
|
+
if (source !== 'clear') return '';
|
|
145
|
+
const marker = readClearMarker(HYPO_DIR);
|
|
146
|
+
if (!marker) return '';
|
|
147
|
+
clearClearMarker(HYPO_DIR);
|
|
148
|
+
const prevId = marker.prev_session_id || 'unknown';
|
|
149
|
+
const prevTr = marker.prev_transcript_path || null;
|
|
150
|
+
const prevCwd = marker.prev_cwd || null;
|
|
151
|
+
const trLine = prevTr ? `\n prev_transcript: ${prevTr}` : '';
|
|
152
|
+
const cwdLine = prevCwd ? `\n prev_cwd: ${prevCwd}` : '';
|
|
153
|
+
return (
|
|
154
|
+
`[WIKI_AUTOCLOSE] 이전 세션(${prevId})이 /clear로 강제 종료됨.${trLine}${cwdLine}\n` +
|
|
155
|
+
` session-close가 미완료라면 지금 즉시 실행할 것 ` +
|
|
156
|
+
`(hot.md + session-state.md + log.md 최소 갱신).`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Pull the wiki repo. Returns true only when the pull actually succeeded. */
|
|
29
161
|
function gitPull(dir) {
|
|
30
|
-
if (!existsSync(join(dir, '.git'))) return;
|
|
31
|
-
spawnSync('git', ['-C', dir, 'pull', '--ff-only', '--quiet'], {
|
|
162
|
+
if (!existsSync(join(dir, '.git'))) return false;
|
|
163
|
+
const r = spawnSync('git', ['-C', dir, 'pull', '--ff-only', '--quiet'], {
|
|
164
|
+
stdio: 'pipe',
|
|
165
|
+
timeout: 10000,
|
|
166
|
+
});
|
|
167
|
+
return r.status === 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* fix #10: surface unresolved sync failures recorded by a prior session's
|
|
172
|
+
* Stop hook (#9). The entry is cleared only once this session's pull has
|
|
173
|
+
* succeeded AND there is no unpushed commit left behind by a failed push
|
|
174
|
+
* (`[ahead N]`).
|
|
175
|
+
*
|
|
176
|
+
* Resolution deliberately checks only the ahead-of-remote state, not the full
|
|
177
|
+
* working tree: uncommitted/untracked files are not a sync failure, and a
|
|
178
|
+
* fresh `hypo init` wiki does not git-ignore `.cache/`, so a broader cleanliness
|
|
179
|
+
* check would see the sync-state file itself and never clear.
|
|
180
|
+
*
|
|
181
|
+
* @returns {string} a `[WIKI: last sync failed: ...]` line, or '' when clear.
|
|
182
|
+
*/
|
|
183
|
+
function syncStateNotice(pullOk) {
|
|
184
|
+
const { entries, parseError } = readSyncState(HYPO_DIR);
|
|
185
|
+
// A corrupt JSONL file is still an "open" failure — surface it (doctor warns
|
|
186
|
+
// too) but never clear it, so the unreadable record survives for inspection.
|
|
187
|
+
if (parseError) return '[WIKI: last sync failed: sync-state.json unreadable — inspect manually]';
|
|
188
|
+
if (entries.length === 0) return '';
|
|
189
|
+
let resolved = false;
|
|
190
|
+
if (pullOk) {
|
|
191
|
+
const r = spawnSync('git', ['-C', HYPO_DIR, 'status', '--branch', '--porcelain'], {
|
|
192
|
+
encoding: 'utf-8',
|
|
193
|
+
timeout: 10000,
|
|
194
|
+
});
|
|
195
|
+
resolved = r.status === 0 && !/\[ahead \d+\]/.test(r.stdout || '');
|
|
196
|
+
}
|
|
197
|
+
if (resolved) {
|
|
198
|
+
clearSyncState(HYPO_DIR);
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
const last = entries[entries.length - 1];
|
|
202
|
+
return `[WIKI: last sync failed: ${last.op || '?'} — ${last.error || 'unknown'}]`;
|
|
32
203
|
}
|
|
33
|
-
const GLOBAL_HOT
|
|
34
|
-
const HOT_CHARS
|
|
35
|
-
const STATE_CHARS
|
|
204
|
+
const GLOBAL_HOT = join(HYPO_DIR, 'hot.md');
|
|
205
|
+
const HOT_CHARS = 2000;
|
|
206
|
+
const STATE_CHARS = 2000;
|
|
36
207
|
|
|
37
208
|
function parseFrontmatterField(content, key) {
|
|
38
209
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
39
210
|
if (!match) return null;
|
|
40
|
-
const line = match[1].split('\n').find(l => l.startsWith(`${key}:`));
|
|
211
|
+
const line = match[1].split('\n').find((l) => l.startsWith(`${key}:`));
|
|
41
212
|
if (!line) return null;
|
|
42
|
-
return line
|
|
213
|
+
return line
|
|
214
|
+
.slice(key.length + 1)
|
|
215
|
+
.trim()
|
|
216
|
+
.replace(/^['"]|['"]$/g, '');
|
|
43
217
|
}
|
|
44
218
|
|
|
45
219
|
function findProjectFiles(cwd) {
|
|
46
220
|
if (!existsSync(PROJECTS_DIR)) return null;
|
|
47
221
|
for (const proj of readdirSync(PROJECTS_DIR)) {
|
|
48
|
-
const projDir
|
|
222
|
+
const projDir = join(PROJECTS_DIR, proj);
|
|
49
223
|
if (!statSync(projDir).isDirectory()) continue;
|
|
50
224
|
const indexPath = join(projDir, 'index.md');
|
|
51
225
|
if (!existsSync(indexPath)) continue;
|
|
52
|
-
const content
|
|
226
|
+
const content = readFileSync(indexPath, 'utf-8');
|
|
53
227
|
const workingDir = parseFrontmatterField(content, 'working_dir');
|
|
54
228
|
if (!workingDir) continue;
|
|
55
229
|
const resolved = workingDir.startsWith('~/')
|
|
56
230
|
? join(homedir(), workingDir.slice(2))
|
|
57
231
|
: workingDir;
|
|
58
232
|
if (cwd === resolved || cwd.startsWith(resolved + '/')) {
|
|
59
|
-
const hotPath
|
|
233
|
+
const hotPath = join(projDir, 'hot.md');
|
|
60
234
|
const statePath = join(projDir, 'session-state.md');
|
|
61
235
|
return {
|
|
62
236
|
proj,
|
|
63
|
-
hotPath:
|
|
237
|
+
hotPath: existsSync(hotPath) ? hotPath : null,
|
|
64
238
|
statePath: existsSync(statePath) ? statePath : null,
|
|
65
239
|
};
|
|
66
240
|
}
|
|
@@ -82,18 +256,20 @@ function printTerminalSummary(proj, hotContent, stateContent) {
|
|
|
82
256
|
const nextFromState = stateContent
|
|
83
257
|
? extractSection(stateContent, SESSION_STATE_NEXT_HEADINGS)
|
|
84
258
|
: null;
|
|
85
|
-
const next = nextFromState
|
|
86
|
-
?? extractSection(hotContent ?? '', SESSION_STATE_NEXT_HEADINGS);
|
|
259
|
+
const next = nextFromState ?? extractSection(hotContent ?? '', SESSION_STATE_NEXT_HEADINGS);
|
|
87
260
|
const prev = hotContent
|
|
88
|
-
? (extractSection(hotContent, '직전 세션 \\([^)]+\\)')
|
|
89
|
-
|
|
90
|
-
|
|
261
|
+
? (extractSection(hotContent, '직전 세션 \\([^)]+\\)') ??
|
|
262
|
+
extractSection(hotContent, '직전 세션.*') ??
|
|
263
|
+
extractSection(hotContent, 'Last Session.*'))
|
|
91
264
|
: null;
|
|
92
265
|
const lines = ['', `\x1b[36m[Hypomnema]\x1b[0m project: \x1b[1m${proj}\x1b[0m`];
|
|
93
266
|
if (prev) lines.push(` prev: ${prev.split('\n')[0].replace(/^\*\*|\*\*$/g, '')}`);
|
|
94
267
|
if (next) {
|
|
95
268
|
lines.push(' next:');
|
|
96
|
-
next
|
|
269
|
+
next
|
|
270
|
+
.split('\n')
|
|
271
|
+
.slice(0, 20)
|
|
272
|
+
.forEach((l) => lines.push(` ${l}`));
|
|
97
273
|
}
|
|
98
274
|
lines.push('');
|
|
99
275
|
process.stderr.write(lines.join('\n'));
|
|
@@ -101,63 +277,133 @@ function printTerminalSummary(proj, hotContent, stateContent) {
|
|
|
101
277
|
|
|
102
278
|
let raw = '';
|
|
103
279
|
process.stdin.setEncoding('utf-8');
|
|
104
|
-
process.stdin.on('data', chunk => raw += chunk);
|
|
280
|
+
process.stdin.on('data', (chunk) => (raw += chunk));
|
|
105
281
|
process.stdin.on('end', () => {
|
|
106
282
|
try {
|
|
107
283
|
let data = {};
|
|
108
|
-
try {
|
|
284
|
+
try {
|
|
285
|
+
data = JSON.parse(raw);
|
|
286
|
+
} catch {}
|
|
109
287
|
|
|
110
|
-
gitPull(HYPO_DIR);
|
|
288
|
+
const pullOk = gitPull(HYPO_DIR);
|
|
289
|
+
const syncLine = syncStateNotice(pullOk);
|
|
111
290
|
const growthLine = readLastGrowthLine();
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
const
|
|
291
|
+
// fix #25 PR-A2 (ADR 0022 amendment): on source='clear', surface the dying
|
|
292
|
+
// session's identity that hypo-session-end stashed so Claude can recover
|
|
293
|
+
// session-close work that /clear skipped. One-shot: marker is unlinked
|
|
294
|
+
// immediately after read.
|
|
295
|
+
const clearRecoveryLine = buildClearRecoveryLine(data.source);
|
|
296
|
+
const updateLine = buildUpdateNotice();
|
|
297
|
+
// Intentional dual emit: stderr (yellow/cyan) is the human-visible nudge in
|
|
298
|
+
// the terminal; noticePrefix injects the same plain-text lines into the
|
|
299
|
+
// LLM's additionalContext so model and user start the session looking at
|
|
300
|
+
// the same state. ANSI escapes are kept out of additionalContext on purpose.
|
|
301
|
+
const notices = [syncLine, growthLine, clearRecoveryLine, updateLine].filter(Boolean);
|
|
302
|
+
let noticePrefix = notices.length ? `${notices.join('\n\n')}\n\n` : '';
|
|
303
|
+
if (syncLine) process.stderr.write(`\n\x1b[33m${syncLine}\x1b[0m\n`);
|
|
117
304
|
if (growthLine) process.stderr.write(`\n\x1b[36m${growthLine}\x1b[0m\n`);
|
|
305
|
+
if (clearRecoveryLine)
|
|
306
|
+
process.stderr.write(`\n\x1b[33m${clearRecoveryLine.split('\n')[0]}\x1b[0m\n`);
|
|
307
|
+
if (updateLine) process.stderr.write(`\n\x1b[33m${updateLine}\x1b[0m\n`);
|
|
118
308
|
const cwd = data.cwd || data.directory || process.cwd();
|
|
119
309
|
const sessionId = data.session_id || 'default';
|
|
120
|
-
const MARKER_FILE =
|
|
310
|
+
const MARKER_FILE = sessionMarkerPath(sessionId);
|
|
121
311
|
const hit = findProjectFiles(cwd);
|
|
122
312
|
|
|
313
|
+
const ignorePatterns = loadHypoIgnore(HYPO_DIR);
|
|
314
|
+
|
|
123
315
|
if (hit) {
|
|
124
|
-
const hotContent
|
|
125
|
-
const stateContent =
|
|
316
|
+
const hotContent = readIfNotIgnored(hit.hotPath, HOT_CHARS, ignorePatterns);
|
|
317
|
+
const stateContent = readIfNotIgnored(hit.statePath, STATE_CHARS, ignorePatterns);
|
|
126
318
|
|
|
127
319
|
if (hotContent || stateContent) {
|
|
128
320
|
printTerminalSummary(hit.proj, hotContent, stateContent);
|
|
129
|
-
writeFileSync(
|
|
321
|
+
writeFileSync(
|
|
322
|
+
MARKER_FILE,
|
|
323
|
+
JSON.stringify({
|
|
324
|
+
proj: hit.proj,
|
|
325
|
+
hotPath: hit.hotPath,
|
|
326
|
+
statePath: hit.statePath,
|
|
327
|
+
hasSnapshot: true,
|
|
328
|
+
ts: Date.now(),
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
130
331
|
const parts = [];
|
|
131
|
-
if (hotContent)
|
|
332
|
+
if (hotContent) parts.push(`[HOT]\n${hotContent}`);
|
|
132
333
|
if (stateContent) parts.push(`[SESSION STATE — 다음 작업]\n${stateContent}`);
|
|
133
|
-
console.log(
|
|
134
|
-
|
|
135
|
-
|
|
334
|
+
console.log(
|
|
335
|
+
JSON.stringify(
|
|
336
|
+
buildOutput(
|
|
337
|
+
`${noticePrefix}[WIKI HOT CACHE: project=${hit.proj}]\n\n${parts.join('\n\n')}`,
|
|
338
|
+
{ continue: true, suppressOutput: true },
|
|
339
|
+
),
|
|
340
|
+
),
|
|
341
|
+
);
|
|
136
342
|
} else {
|
|
137
|
-
process.stderr.write(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
343
|
+
process.stderr.write(
|
|
344
|
+
`\n\x1b[36m[Hypomnema]\x1b[0m project: \x1b[1m${hit.proj}\x1b[0m (no snapshot yet)\n\n`,
|
|
345
|
+
);
|
|
346
|
+
writeFileSync(
|
|
347
|
+
MARKER_FILE,
|
|
348
|
+
JSON.stringify({ proj: hit.proj, hotPath: null, ts: Date.now() }),
|
|
349
|
+
);
|
|
350
|
+
console.log(
|
|
351
|
+
JSON.stringify(
|
|
352
|
+
buildOutput(`${noticePrefix}[WIKI HOT CACHE: project=${hit.proj}, no snapshot yet]`, {
|
|
353
|
+
continue: true,
|
|
354
|
+
suppressOutput: true,
|
|
355
|
+
}),
|
|
356
|
+
),
|
|
357
|
+
);
|
|
142
358
|
}
|
|
143
359
|
return;
|
|
144
360
|
}
|
|
145
361
|
|
|
362
|
+
// MISS: cwd matches no project. fix #23 / ADR 0023 — offer to create one
|
|
363
|
+
// when the ADR trigger conditions hold (git repo + project marker + no
|
|
364
|
+
// cooldown + not previously declined). The actual scaffold is the LLM's
|
|
365
|
+
// job on a "Y" reply (scripts/lib/project-create.mjs); the hook only nudges.
|
|
366
|
+
if (shouldSuggestProjectCreation(cwd, HYPO_DIR)) {
|
|
367
|
+
const suggestLine = buildProjectSuggestionLine(cwd);
|
|
368
|
+
notices.push(suggestLine);
|
|
369
|
+
noticePrefix = `${notices.join('\n\n')}\n\n`;
|
|
370
|
+
recordSuggestionCooldown(HYPO_DIR, cwd);
|
|
371
|
+
process.stderr.write(`\n\x1b[33m${suggestLine}\x1b[0m\n`);
|
|
372
|
+
}
|
|
373
|
+
|
|
146
374
|
if (!existsSync(GLOBAL_HOT)) {
|
|
147
|
-
|
|
148
|
-
|
|
375
|
+
const notice = notices.join('\n\n');
|
|
376
|
+
if (notice) {
|
|
377
|
+
console.log(JSON.stringify(buildOutput(notice, { continue: true, suppressOutput: true })));
|
|
149
378
|
} else {
|
|
150
379
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
151
380
|
}
|
|
152
381
|
return;
|
|
153
382
|
}
|
|
154
383
|
|
|
155
|
-
const globalContent =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
384
|
+
const globalContent = readIfNotIgnored(GLOBAL_HOT, HOT_CHARS, ignorePatterns);
|
|
385
|
+
if (!globalContent) {
|
|
386
|
+
// GLOBAL_HOT exists but is empty or .hypoignore'd — still surface any
|
|
387
|
+
// pending notices (sync state, growth, AND the auto-project offer), which
|
|
388
|
+
// would otherwise be silently dropped here (codex review 2026-05-22).
|
|
389
|
+
const notice = notices.join('\n\n');
|
|
390
|
+
if (notice) {
|
|
391
|
+
console.log(JSON.stringify(buildOutput(notice, { continue: true, suppressOutput: true })));
|
|
392
|
+
} else {
|
|
393
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
394
|
+
}
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
console.log(
|
|
398
|
+
JSON.stringify(
|
|
399
|
+
buildOutput(
|
|
400
|
+
`${noticePrefix}[WIKI HOT CACHE: global — no project matched cwd=${cwd}]\n\n${globalContent}`,
|
|
401
|
+
{ continue: true, suppressOutput: true },
|
|
402
|
+
),
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
} catch (err) {
|
|
406
|
+
process.stderr.write(`[hypo-session-start] error: ${err?.message ?? String(err)}\n`);
|
|
161
407
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
162
408
|
}
|
|
163
409
|
});
|