hypomnema 1.6.2 → 1.7.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 +39 -14
- package/README.md +39 -14
- package/commands/capture.md +8 -6
- package/commands/crystallize.md +39 -20
- package/docs/ARCHITECTURE.md +49 -14
- package/docs/CONTRIBUTING.md +31 -29
- package/hooks/base-store.mjs +265 -0
- package/hooks/hooks.json +18 -1
- package/hooks/hypo-auto-commit.mjs +92 -15
- package/hooks/hypo-auto-minimal-crystallize.mjs +63 -20
- package/hooks/hypo-auto-stage.mjs +41 -1
- package/hooks/hypo-close-guard.mjs +246 -0
- package/hooks/hypo-cwd-change.mjs +31 -2
- package/hooks/hypo-file-watch.mjs +21 -2
- package/hooks/hypo-first-prompt.mjs +19 -3
- package/hooks/hypo-hot-rebuild.mjs +43 -5
- package/hooks/hypo-lookup.mjs +86 -29
- package/hooks/hypo-personal-check.mjs +24 -3
- package/hooks/hypo-session-record.mjs +2 -3
- package/hooks/hypo-session-start.mjs +199 -20
- package/hooks/hypo-shared.mjs +2080 -176
- package/hooks/proposal-store.mjs +513 -0
- package/hooks/version-check.mjs +45 -0
- package/package.json +42 -14
- package/scripts/capture.mjs +556 -37
- package/scripts/crystallize.mjs +751 -110
- package/scripts/doctor.mjs +787 -26
- package/scripts/feedback-sync.mjs +515 -44
- package/scripts/graph.mjs +35 -13
- package/scripts/init.mjs +287 -41
- package/scripts/lib/extensions.mjs +656 -1
- package/scripts/lib/git-hooks-dir.mjs +229 -0
- package/scripts/lib/hypo-ignore.mjs +54 -6
- package/scripts/lib/hypo-root.mjs +56 -6
- package/scripts/lib/page-usage.mjs +15 -2
- package/scripts/lib/pkg-json.mjs +40 -0
- package/scripts/lib/plugin-detect.mjs +96 -6
- package/scripts/lib/project-create.mjs +5 -1
- package/scripts/lib/rename-marker.mjs +39 -0
- package/scripts/lib/wd-match.mjs +23 -5
- package/scripts/lib/wikilink.mjs +32 -6
- package/scripts/lint.mjs +103 -5
- package/scripts/proposal.mjs +1032 -0
- package/scripts/query.mjs +25 -4
- package/scripts/rename.mjs +223 -18
- package/scripts/resume.mjs +34 -12
- package/scripts/stats.mjs +41 -9
- package/scripts/uninstall.mjs +141 -6
- package/scripts/upgrade.mjs +197 -15
- package/skills/crystallize/SKILL.md +44 -7
- package/skills/debate/SKILL.md +88 -0
- package/skills/debate/references/orchestration-patterns.md +83 -0
- package/templates/.hyposcanignore +10 -0
- package/templates/SCHEMA.md +12 -0
- package/templates/gitignore +9 -0
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +6 -0
- package/scripts/.gitkeep +0 -0
- package/scripts/check-bilingual.mjs +0 -153
- package/scripts/check-readme-version.mjs +0 -126
- package/scripts/check-tracker-ids.mjs +0 -426
- package/scripts/check-versions.mjs +0 -171
- package/scripts/install-git-hooks.mjs +0 -293
- package/scripts/lib/changelog-classify.mjs +0 -216
- package/scripts/lib/check-bilingual.mjs +0 -244
- package/scripts/lib/check-tracker-ids.mjs +0 -217
- package/scripts/lib/pre-commit-format.mjs +0 -251
- package/scripts/pre-commit-format.mjs +0 -198
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
formatGrowthMetrics,
|
|
20
20
|
readSyncState,
|
|
21
21
|
clearSyncState,
|
|
22
|
+
recordSyncSuccess,
|
|
23
|
+
classifySyncOp,
|
|
22
24
|
readClearMarker,
|
|
23
25
|
clearClearMarker,
|
|
24
26
|
loadHypoIgnore,
|
|
@@ -32,6 +34,10 @@ import {
|
|
|
32
34
|
collectProjectWorkingDirs,
|
|
33
35
|
buildVaultOrientation,
|
|
34
36
|
staleMarkerFor,
|
|
37
|
+
currentDevice,
|
|
38
|
+
scopeVisible,
|
|
39
|
+
readVisibilityScope,
|
|
40
|
+
pkgRootDriftStatus,
|
|
35
41
|
} from './hypo-shared.mjs';
|
|
36
42
|
import {
|
|
37
43
|
defaultCachePath,
|
|
@@ -45,16 +51,49 @@ import {
|
|
|
45
51
|
computeSiblingNotice,
|
|
46
52
|
siblingAlreadyNotified,
|
|
47
53
|
markSiblingNotified,
|
|
54
|
+
pkgRootDriftAlreadyNotified,
|
|
55
|
+
markPkgRootDriftNotified,
|
|
56
|
+
clearPkgRootDriftNotified,
|
|
48
57
|
} from './version-check.mjs';
|
|
58
|
+
import { snapshotBase, overwriteTargets } from './base-store.mjs';
|
|
59
|
+
import { listProposals } from './proposal-store.mjs';
|
|
49
60
|
|
|
50
61
|
// Privacy guard: refuse to read+inject .hypoignore-matched
|
|
51
62
|
// wiki files into additionalContext. Without this, a user who lists
|
|
52
63
|
// `projects/private/hot.md` in .hypoignore would still see SECRET emit because
|
|
53
64
|
// session-start reads hot/state paths directly.
|
|
65
|
+
//
|
|
66
|
+
// Visibility guard: a machine-scoped page (visibility_scope: machine:<owner>)
|
|
67
|
+
// must not be injected on a machine other than its owner. hypo-file-watch
|
|
68
|
+
// already filters these very files, so leaving session start unfiltered made the
|
|
69
|
+
// SAME file behave differently depending on which path opened it: the user sets
|
|
70
|
+
// the field, sees it honored on edit, and never learns that session start still
|
|
71
|
+
// ships the body. Read the scope from the RAW content before the maxChars slice:
|
|
72
|
+
// slicing first could cut the frontmatter off and silently fail open.
|
|
73
|
+
// The root hot.md is a frontmatter-less pointer table, so it reads as '' and
|
|
74
|
+
// passes (shared) unchanged.
|
|
54
75
|
function readIfNotIgnored(path, maxChars, patterns) {
|
|
55
76
|
if (!path) return null;
|
|
56
77
|
if (patterns.length > 0 && isIgnored(path, HYPO_DIR, patterns)) return null;
|
|
57
|
-
|
|
78
|
+
const raw = readFileSync(path, 'utf-8');
|
|
79
|
+
if (!scopeVisible(readVisibilityScope(raw), currentDevice())) return null;
|
|
80
|
+
return raw.slice(0, maxChars);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Scoped-out is not the same as absent. Both make readIfNotIgnored return null,
|
|
84
|
+
// but telling the model "no snapshot yet / first session" when the snapshot merely
|
|
85
|
+
// belongs to another machine is a lie it will act on. Returns false for an ignored
|
|
86
|
+
// or missing file so only a real machine-scope hide reports true.
|
|
87
|
+
// The caller may name the project and the fact, never the withheld body: a message
|
|
88
|
+
// explaining the hide must not re-leak what it hid.
|
|
89
|
+
function isScopedOut(path, patterns) {
|
|
90
|
+
try {
|
|
91
|
+
if (!path || !existsSync(path)) return false;
|
|
92
|
+
if (patterns.length > 0 && isIgnored(path, HYPO_DIR, patterns)) return false;
|
|
93
|
+
return !scopeVisible(readVisibilityScope(readFileSync(path, 'utf-8')), currentDevice());
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
58
97
|
}
|
|
59
98
|
|
|
60
99
|
// Compute the STALE marker for a hot/state file from its RAW content (readIfNotIgnored
|
|
@@ -181,6 +220,52 @@ function buildSiblingNotice() {
|
|
|
181
220
|
}
|
|
182
221
|
}
|
|
183
222
|
|
|
223
|
+
/**
|
|
224
|
+
* pkgRoot drift notice. hypo-shared.mjs's resolvePkgRoot() already
|
|
225
|
+
* self-corrects PKG_ROOT in memory whenever the code's own resolved location
|
|
226
|
+
* disagrees with the cached hypo-pkg.json — but silent self-correction is the
|
|
227
|
+
* exact failure this closes: the user's own `upgrade` habit stops mattering
|
|
228
|
+
* and nothing ever tells them hypo-pkg.json fell behind. Surfaced once per
|
|
229
|
+
* (cached → self-location) pair via the same notify-once cache the sibling
|
|
230
|
+
* notice above uses — a fresh drift (new self-location) re-notifies, but
|
|
231
|
+
* staying on the same drifted state doesn't nag every session.
|
|
232
|
+
*
|
|
233
|
+
* Tri-state (pkgRootDriftStatus): 'match' CLEARS any earlier mark (checked
|
|
234
|
+
* FIRST, unconditionally — even under opt-out, so a drift that resolves while
|
|
235
|
+
* opted out doesn't leave a stale mark that then suppresses a genuine
|
|
236
|
+
* recurrence once opt-out is lifted); 'unknown' touches nothing (self-location
|
|
237
|
+
* could not be resolved this session — the permanent steady state for the
|
|
238
|
+
* npm/manual channel, not evidence either way); only 'drift' can produce a
|
|
239
|
+
* banner, and opt-out is checked there so an opted-out session never marks a
|
|
240
|
+
* pair as notified it never actually showed.
|
|
241
|
+
*/
|
|
242
|
+
function buildPkgRootDriftNotice() {
|
|
243
|
+
try {
|
|
244
|
+
const status = pkgRootDriftStatus();
|
|
245
|
+
const cachePath = defaultCachePath();
|
|
246
|
+
if (status.status === 'match') {
|
|
247
|
+
clearPkgRootDriftNotified(cachePath);
|
|
248
|
+
return '';
|
|
249
|
+
}
|
|
250
|
+
if (status.status === 'unknown') return '';
|
|
251
|
+
if (isOptedOut()) return '';
|
|
252
|
+
const key = `${status.cached || '(none)'}->${status.self}`;
|
|
253
|
+
const cache = readCache(cachePath);
|
|
254
|
+
if (pkgRootDriftAlreadyNotified(cache, key)) return '';
|
|
255
|
+
markPkgRootDriftNotified(cachePath, key);
|
|
256
|
+
return (
|
|
257
|
+
`[Hypomnema] Package metadata drift: hypo-pkg.json still points at ` +
|
|
258
|
+
`\`${status.cached || '(none)'}\`, but the code actually running resolves to ` +
|
|
259
|
+
`\`${status.self}\`.\n` +
|
|
260
|
+
` Hooks already resolved the correct root for this session — this is a ` +
|
|
261
|
+
`heads-up, not a blocker.\n` +
|
|
262
|
+
` → run \`/hypo:upgrade --apply\` to bring hypo-pkg.json back in sync.`
|
|
263
|
+
);
|
|
264
|
+
} catch {
|
|
265
|
+
return '';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
184
269
|
const PROJECTS_DIR = join(HYPO_DIR, 'projects');
|
|
185
270
|
const GROWTH_CACHE = join(HYPO_DIR, '.cache', 'last-session-growth.json');
|
|
186
271
|
|
|
@@ -220,14 +305,22 @@ function buildClearRecoveryLine(source) {
|
|
|
220
305
|
);
|
|
221
306
|
}
|
|
222
307
|
|
|
223
|
-
/**
|
|
308
|
+
/**
|
|
309
|
+
* Pull the wiki repo. Returns true only when the pull actually succeeded. On
|
|
310
|
+
* success, also records the last-success timestamp (silently — no notice; the
|
|
311
|
+
* existing failure notice below is unchanged) so doctor never reports "never
|
|
312
|
+
* synced" right after a healthy startup pull, even when no auto-commit Stop
|
|
313
|
+
* hook has run yet this session.
|
|
314
|
+
*/
|
|
224
315
|
function gitPull(dir) {
|
|
225
316
|
if (!existsSync(join(dir, '.git'))) return false;
|
|
226
317
|
const r = spawnSync('git', ['-C', dir, 'pull', '--ff-only', '--quiet'], {
|
|
227
318
|
stdio: 'pipe',
|
|
228
319
|
timeout: 10000,
|
|
229
320
|
});
|
|
230
|
-
|
|
321
|
+
const ok = r.status === 0;
|
|
322
|
+
if (ok) recordSyncSuccess(dir, 'pull');
|
|
323
|
+
return ok;
|
|
231
324
|
}
|
|
232
325
|
|
|
233
326
|
/**
|
|
@@ -241,7 +334,9 @@ function gitPull(dir) {
|
|
|
241
334
|
* fresh `hypo init` wiki does not git-ignore `.cache/`, so a broader cleanliness
|
|
242
335
|
* check would see the sync-state file itself and never clear.
|
|
243
336
|
*
|
|
244
|
-
* @returns {string} a `[WIKI: last sync failed: ...]`
|
|
337
|
+
* @returns {string} a `[WIKI: last sync failed: ...]` (or, for a conflict/
|
|
338
|
+
* conflict-unresolved entry, dedicated manual-merge guidance) line, or ''
|
|
339
|
+
* when clear.
|
|
245
340
|
*/
|
|
246
341
|
function syncStateNotice(pullOk) {
|
|
247
342
|
const { entries, parseError } = readSyncState(HYPO_DIR);
|
|
@@ -262,15 +357,64 @@ function syncStateNotice(pullOk) {
|
|
|
262
357
|
return '';
|
|
263
358
|
}
|
|
264
359
|
const last = entries[entries.length - 1];
|
|
265
|
-
|
|
360
|
+
// classifySyncOp (hypo-shared.mjs) is the single judgment both this hook
|
|
361
|
+
// and doctor.mjs's checkSyncState branch on, so the two surfaces cannot
|
|
362
|
+
// silently diverge on WHICH op gets which treatment: this exact check used
|
|
363
|
+
// to be an exact `=== 'conflict'` comparison here that missed
|
|
364
|
+
// 'conflict-unresolved' — the MORE dangerous op, since the abort itself
|
|
365
|
+
// failed and the tree may still be half-merged — while doctor already
|
|
366
|
+
// caught it via startsWith('conflict').
|
|
367
|
+
const cls = classifySyncOp(last.op);
|
|
368
|
+
if (cls === 'conflict-unresolved') {
|
|
369
|
+
return (
|
|
370
|
+
`[WIKI: remote diverged AND the automatic merge-abort failed — the working ` +
|
|
371
|
+
`tree may still be half-merged (unmerged paths or an in-progress merge). ` +
|
|
372
|
+
`Do NOT commit or push yet. Inspect \`git -C ${HYPO_DIR} status\` first: if a ` +
|
|
373
|
+
`merge is in progress, resolve the conflicts, then \`git -C ${HYPO_DIR} add <resolved paths>\` ` +
|
|
374
|
+
`and \`git -C ${HYPO_DIR} commit\` (git refuses a commit while unmerged entries remain staged) ` +
|
|
375
|
+
`— or run \`git -C ${HYPO_DIR} merge --abort\` to discard it instead, before continuing.]`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (cls === 'conflict') {
|
|
266
379
|
return (
|
|
267
380
|
`[WIKI: remote diverged — auto-merge was aborted to protect your edits ` +
|
|
268
381
|
`(your local work is committed and safe; the other machine's version is on the remote). ` +
|
|
269
382
|
`Resolve manually: \`git -C ${HYPO_DIR} pull --no-rebase\`, fix conflicts, then push.]`
|
|
270
383
|
);
|
|
271
384
|
}
|
|
385
|
+
// An unrecognized `conflict*` op — some future syncRemote failure mode this
|
|
386
|
+
// hook has no dedicated branch for. Neither the clean-conflict claim above
|
|
387
|
+
// ("committed and safe") nor the conflict-unresolved claim ("the abort
|
|
388
|
+
// failed") is known to be true here, so assert neither: say plainly that
|
|
389
|
+
// the state is unknown and treat it as unresolved until a human checks.
|
|
390
|
+
if (cls === 'unknown-conflict') {
|
|
391
|
+
return (
|
|
392
|
+
`[WIKI: remote diverged — an unrecognized conflict-related sync failure was recorded ` +
|
|
393
|
+
`(op='${last.op}'). Its resolution state cannot be confirmed automatically, so treat it ` +
|
|
394
|
+
`as unresolved: do NOT commit or push yet. Inspect \`git -C ${HYPO_DIR} status\` first for ` +
|
|
395
|
+
`unmerged paths or an in-progress merge before continuing.]`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
272
398
|
return `[WIKI: last sync failed: ${last.op || '?'} — ${last.error || 'unknown'}]`;
|
|
273
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Surface the vault-wide count of parked write-proposals (T8). Routed
|
|
402
|
+
* exactly like syncStateNotice: the line joins the `notices` array (→
|
|
403
|
+
* additionalContext) and is also written to stderr, so both the model and the
|
|
404
|
+
* user's transcript see it. NOT a systemMessage banner (that channel is
|
|
405
|
+
* reserved for the update/sibling notices). Pure read (listProposals never
|
|
406
|
+
* mutates); best-effort so a store read failure never breaks SessionStart. '' when
|
|
407
|
+
* there are no pending proposals, so nothing surfaces on the empty path.
|
|
408
|
+
*/
|
|
409
|
+
function pendingProposalNotice() {
|
|
410
|
+
try {
|
|
411
|
+
const n = listProposals(HYPO_DIR).length;
|
|
412
|
+
if (n === 0) return '';
|
|
413
|
+
return `[WIKI: 대기 proposal ${n}건 (검토: hypomnema proposal list)]`;
|
|
414
|
+
} catch {
|
|
415
|
+
return '';
|
|
416
|
+
}
|
|
417
|
+
}
|
|
274
418
|
const GLOBAL_HOT = join(HYPO_DIR, 'hot.md');
|
|
275
419
|
const HOT_CHARS = 2000;
|
|
276
420
|
const STATE_CHARS = 2000;
|
|
@@ -346,6 +490,7 @@ process.stdin.on('end', () => {
|
|
|
346
490
|
|
|
347
491
|
const pullOk = gitPull(HYPO_DIR);
|
|
348
492
|
const syncLine = syncStateNotice(pullOk);
|
|
493
|
+
const proposalLine = pendingProposalNotice();
|
|
349
494
|
const growthLine = readLastGrowthLine();
|
|
350
495
|
// On source='clear', surface the dying
|
|
351
496
|
// session's identity that hypo-session-end stashed so Claude can recover
|
|
@@ -354,31 +499,53 @@ process.stdin.on('end', () => {
|
|
|
354
499
|
const clearRecoveryLine = buildClearRecoveryLine(data.source);
|
|
355
500
|
const updateLine = buildUpdateNotice();
|
|
356
501
|
const siblingLine = buildSiblingNotice();
|
|
357
|
-
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
|
|
502
|
+
const pkgDriftLine = buildPkgRootDriftNotice();
|
|
503
|
+
// The update + stale-sibling + pkgRoot-drift banners must reach the USER.
|
|
504
|
+
// On a SessionStart hook that exits 0, stderr is invisible in the normal
|
|
505
|
+
// TUI (only shown on exit 2 / --verbose) and additionalContext is
|
|
506
|
+
// model-only — `systemMessage` is the documented user-visible channel.
|
|
507
|
+
// Route those banners there. They ALSO stay in noticePrefix →
|
|
508
|
+
// additionalContext below, so the model and the user start the session
|
|
509
|
+
// looking at the same state. (The other stderr notices —
|
|
510
|
+
// sync/growth/clear/suggest — are intentionally transcript/--verbose only
|
|
511
|
+
// and out of this banner's scope.)
|
|
512
|
+
const userMessage = [updateLine, siblingLine, pkgDriftLine].filter(Boolean).join('\n\n');
|
|
366
513
|
if (userMessage) outExtra = { ...outExtra, systemMessage: userMessage };
|
|
367
|
-
const notices = [
|
|
368
|
-
|
|
369
|
-
|
|
514
|
+
const notices = [
|
|
515
|
+
syncLine,
|
|
516
|
+
proposalLine,
|
|
517
|
+
growthLine,
|
|
518
|
+
clearRecoveryLine,
|
|
519
|
+
updateLine,
|
|
520
|
+
siblingLine,
|
|
521
|
+
pkgDriftLine,
|
|
522
|
+
].filter(Boolean);
|
|
370
523
|
let noticePrefix = notices.length ? `${notices.join('\n\n')}\n\n` : '';
|
|
371
524
|
if (syncLine) process.stderr.write(`\n\x1b[33m${syncLine}\x1b[0m\n`);
|
|
525
|
+
if (proposalLine) process.stderr.write(`\n\x1b[33m${proposalLine}\x1b[0m\n`);
|
|
372
526
|
if (growthLine) process.stderr.write(`\n\x1b[36m${growthLine}\x1b[0m\n`);
|
|
373
527
|
if (clearRecoveryLine)
|
|
374
528
|
process.stderr.write(`\n\x1b[33m${clearRecoveryLine.split('\n')[0]}\x1b[0m\n`);
|
|
375
529
|
if (updateLine) process.stderr.write(`\n\x1b[33m${updateLine}\x1b[0m\n`);
|
|
376
530
|
if (siblingLine) process.stderr.write(`\n\x1b[33m${siblingLine}\x1b[0m\n`);
|
|
531
|
+
if (pkgDriftLine) process.stderr.write(`\n\x1b[33m${pkgDriftLine}\x1b[0m\n`);
|
|
377
532
|
const cwd = data.cwd || data.directory || process.cwd();
|
|
378
533
|
const sessionId = data.session_id || 'default';
|
|
379
534
|
const MARKER_FILE = sessionMarkerPath(sessionId);
|
|
380
535
|
const hit = findProjectFiles(cwd);
|
|
381
536
|
|
|
537
|
+
// Observed-base snapshot for the write=proposal gate. Deliberately AFTER gitPull: the base must
|
|
538
|
+
// describe the tree this session actually starts from, remote merges
|
|
539
|
+
// included, or the first close would raise a proposal against content the
|
|
540
|
+
// session never had a chance to conflict with. Once per session
|
|
541
|
+
// (existence-check inside snapshotBase), so resume and compact leave it
|
|
542
|
+
// alone. `data.session_id` is used raw rather than the 'default' fallback
|
|
543
|
+
// above. A session with no id has no base and closes down the legacy
|
|
544
|
+
// direct-write path.
|
|
545
|
+
if (data.session_id) {
|
|
546
|
+
snapshotBase(HYPO_DIR, data.session_id, overwriteTargets(hit ? hit.proj : null));
|
|
547
|
+
}
|
|
548
|
+
|
|
382
549
|
const ignorePatterns = loadHypoIgnore(HYPO_DIR);
|
|
383
550
|
|
|
384
551
|
// When cwd is a project working_dir that is NOT the vault itself, tell the
|
|
@@ -425,17 +592,29 @@ process.stdin.on('end', () => {
|
|
|
425
592
|
),
|
|
426
593
|
);
|
|
427
594
|
} else {
|
|
595
|
+
// A snapshot that exists but is scoped to another machine must not be
|
|
596
|
+
// reported as "no snapshot yet": the model would treat a resumed project
|
|
597
|
+
// as a first session. Say which it is, and say nothing of the contents.
|
|
598
|
+
const scopedOut =
|
|
599
|
+
isScopedOut(hit.hotPath, ignorePatterns) || isScopedOut(hit.statePath, ignorePatterns);
|
|
600
|
+
const reason = scopedOut ? 'snapshot scoped to another machine' : 'no snapshot yet';
|
|
428
601
|
process.stderr.write(
|
|
429
|
-
`\n\x1b[36m[Hypomnema]\x1b[0m project: \x1b[1m${hit.proj}\x1b[0m (
|
|
602
|
+
`\n\x1b[36m[Hypomnema]\x1b[0m project: \x1b[1m${hit.proj}\x1b[0m (${reason})\n\n`,
|
|
430
603
|
);
|
|
604
|
+
// Carry the reason into the marker, not just this hook's output.
|
|
605
|
+
// hypo-first-prompt derives its resume line from the marker alone, so a
|
|
606
|
+
// marker that says only `hotPath: null` makes the NEXT prompt announce
|
|
607
|
+
// "first session" for a project that merely belongs to another machine.
|
|
608
|
+
// That lie is what invites the model to author a fresh hot.md over one
|
|
609
|
+
// that already exists elsewhere.
|
|
431
610
|
writeFileSync(
|
|
432
611
|
MARKER_FILE,
|
|
433
|
-
JSON.stringify({ proj: hit.proj, hotPath: null, ts: Date.now() }),
|
|
612
|
+
JSON.stringify({ proj: hit.proj, hotPath: null, scopedOut, ts: Date.now() }),
|
|
434
613
|
);
|
|
435
614
|
console.log(
|
|
436
615
|
JSON.stringify(
|
|
437
616
|
buildOutput(
|
|
438
|
-
`${noticePrefix}${hitPrefix}[WIKI HOT CACHE: project=${sanitizeProjForPrompt(hit.proj)},
|
|
617
|
+
`${noticePrefix}${hitPrefix}[WIKI HOT CACHE: project=${sanitizeProjForPrompt(hit.proj)}, ${reason}]`,
|
|
439
618
|
outExtra,
|
|
440
619
|
),
|
|
441
620
|
),
|