hypomnema 1.7.0 → 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/hooks/hooks.json +11 -0
- package/hooks/hypo-auto-commit.mjs +92 -15
- package/hooks/hypo-auto-stage.mjs +28 -18
- package/hooks/hypo-close-guard.mjs +246 -0
- package/hooks/hypo-hot-rebuild.mjs +43 -5
- package/hooks/hypo-session-start.mjs +110 -13
- package/hooks/hypo-shared.mjs +1224 -96
- package/hooks/version-check.mjs +45 -0
- package/package.json +3 -1
- package/scripts/crystallize.mjs +112 -2
- package/scripts/doctor.mjs +483 -17
- package/scripts/graph.mjs +27 -12
- package/scripts/init.mjs +57 -7
- package/scripts/lib/git-hooks-dir.mjs +229 -0
- package/scripts/lib/project-create.mjs +5 -1
- package/scripts/lib/rename-marker.mjs +39 -0
- package/scripts/lint.mjs +84 -2
- package/scripts/rename.mjs +223 -18
- package/scripts/stats.mjs +14 -2
- package/templates/gitignore +4 -0
- package/templates/hypo-config.md +1 -1
|
@@ -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,
|
|
@@ -35,6 +37,7 @@ import {
|
|
|
35
37
|
currentDevice,
|
|
36
38
|
scopeVisible,
|
|
37
39
|
readVisibilityScope,
|
|
40
|
+
pkgRootDriftStatus,
|
|
38
41
|
} from './hypo-shared.mjs';
|
|
39
42
|
import {
|
|
40
43
|
defaultCachePath,
|
|
@@ -48,6 +51,9 @@ import {
|
|
|
48
51
|
computeSiblingNotice,
|
|
49
52
|
siblingAlreadyNotified,
|
|
50
53
|
markSiblingNotified,
|
|
54
|
+
pkgRootDriftAlreadyNotified,
|
|
55
|
+
markPkgRootDriftNotified,
|
|
56
|
+
clearPkgRootDriftNotified,
|
|
51
57
|
} from './version-check.mjs';
|
|
52
58
|
import { snapshotBase, overwriteTargets } from './base-store.mjs';
|
|
53
59
|
import { listProposals } from './proposal-store.mjs';
|
|
@@ -214,6 +220,52 @@ function buildSiblingNotice() {
|
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
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
|
+
|
|
217
269
|
const PROJECTS_DIR = join(HYPO_DIR, 'projects');
|
|
218
270
|
const GROWTH_CACHE = join(HYPO_DIR, '.cache', 'last-session-growth.json');
|
|
219
271
|
|
|
@@ -253,14 +305,22 @@ function buildClearRecoveryLine(source) {
|
|
|
253
305
|
);
|
|
254
306
|
}
|
|
255
307
|
|
|
256
|
-
/**
|
|
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
|
+
*/
|
|
257
315
|
function gitPull(dir) {
|
|
258
316
|
if (!existsSync(join(dir, '.git'))) return false;
|
|
259
317
|
const r = spawnSync('git', ['-C', dir, 'pull', '--ff-only', '--quiet'], {
|
|
260
318
|
stdio: 'pipe',
|
|
261
319
|
timeout: 10000,
|
|
262
320
|
});
|
|
263
|
-
|
|
321
|
+
const ok = r.status === 0;
|
|
322
|
+
if (ok) recordSyncSuccess(dir, 'pull');
|
|
323
|
+
return ok;
|
|
264
324
|
}
|
|
265
325
|
|
|
266
326
|
/**
|
|
@@ -274,7 +334,9 @@ function gitPull(dir) {
|
|
|
274
334
|
* fresh `hypo init` wiki does not git-ignore `.cache/`, so a broader cleanliness
|
|
275
335
|
* check would see the sync-state file itself and never clear.
|
|
276
336
|
*
|
|
277
|
-
* @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.
|
|
278
340
|
*/
|
|
279
341
|
function syncStateNotice(pullOk) {
|
|
280
342
|
const { entries, parseError } = readSyncState(HYPO_DIR);
|
|
@@ -295,13 +357,44 @@ function syncStateNotice(pullOk) {
|
|
|
295
357
|
return '';
|
|
296
358
|
}
|
|
297
359
|
const last = entries[entries.length - 1];
|
|
298
|
-
|
|
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') {
|
|
299
379
|
return (
|
|
300
380
|
`[WIKI: remote diverged — auto-merge was aborted to protect your edits ` +
|
|
301
381
|
`(your local work is committed and safe; the other machine's version is on the remote). ` +
|
|
302
382
|
`Resolve manually: \`git -C ${HYPO_DIR} pull --no-rebase\`, fix conflicts, then push.]`
|
|
303
383
|
);
|
|
304
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
|
+
}
|
|
305
398
|
return `[WIKI: last sync failed: ${last.op || '?'} — ${last.error || 'unknown'}]`;
|
|
306
399
|
}
|
|
307
400
|
/**
|
|
@@ -406,15 +499,17 @@ process.stdin.on('end', () => {
|
|
|
406
499
|
const clearRecoveryLine = buildClearRecoveryLine(data.source);
|
|
407
500
|
const updateLine = buildUpdateNotice();
|
|
408
501
|
const siblingLine = buildSiblingNotice();
|
|
409
|
-
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
|
|
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');
|
|
418
513
|
if (userMessage) outExtra = { ...outExtra, systemMessage: userMessage };
|
|
419
514
|
const notices = [
|
|
420
515
|
syncLine,
|
|
@@ -423,6 +518,7 @@ process.stdin.on('end', () => {
|
|
|
423
518
|
clearRecoveryLine,
|
|
424
519
|
updateLine,
|
|
425
520
|
siblingLine,
|
|
521
|
+
pkgDriftLine,
|
|
426
522
|
].filter(Boolean);
|
|
427
523
|
let noticePrefix = notices.length ? `${notices.join('\n\n')}\n\n` : '';
|
|
428
524
|
if (syncLine) process.stderr.write(`\n\x1b[33m${syncLine}\x1b[0m\n`);
|
|
@@ -432,6 +528,7 @@ process.stdin.on('end', () => {
|
|
|
432
528
|
process.stderr.write(`\n\x1b[33m${clearRecoveryLine.split('\n')[0]}\x1b[0m\n`);
|
|
433
529
|
if (updateLine) process.stderr.write(`\n\x1b[33m${updateLine}\x1b[0m\n`);
|
|
434
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`);
|
|
435
532
|
const cwd = data.cwd || data.directory || process.cwd();
|
|
436
533
|
const sessionId = data.session_id || 'default';
|
|
437
534
|
const MARKER_FILE = sessionMarkerPath(sessionId);
|