hypomnema 1.6.1 → 1.7.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 +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 +7 -1
- package/hooks/hypo-auto-minimal-crystallize.mjs +77 -31
- package/hooks/hypo-auto-stage.mjs +30 -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-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 +89 -7
- package/hooks/hypo-shared.mjs +944 -147
- package/hooks/proposal-store.mjs +513 -0
- package/package.json +40 -14
- package/scripts/capture.mjs +556 -37
- package/scripts/crystallize.mjs +639 -108
- package/scripts/doctor.mjs +304 -9
- package/scripts/feedback-sync.mjs +515 -44
- package/scripts/graph.mjs +9 -2
- package/scripts/init.mjs +230 -34
- package/scripts/lib/extensions.mjs +656 -1
- 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/wd-match.mjs +23 -5
- package/scripts/lib/wikilink.mjs +32 -6
- package/scripts/lint.mjs +20 -4
- package/scripts/proposal.mjs +1032 -0
- package/scripts/query.mjs +25 -4
- package/scripts/resume.mjs +34 -12
- package/scripts/stats.mjs +28 -8
- 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 +5 -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
|
@@ -32,6 +32,9 @@ import {
|
|
|
32
32
|
collectProjectWorkingDirs,
|
|
33
33
|
buildVaultOrientation,
|
|
34
34
|
staleMarkerFor,
|
|
35
|
+
currentDevice,
|
|
36
|
+
scopeVisible,
|
|
37
|
+
readVisibilityScope,
|
|
35
38
|
} from './hypo-shared.mjs';
|
|
36
39
|
import {
|
|
37
40
|
defaultCachePath,
|
|
@@ -46,15 +49,45 @@ import {
|
|
|
46
49
|
siblingAlreadyNotified,
|
|
47
50
|
markSiblingNotified,
|
|
48
51
|
} from './version-check.mjs';
|
|
52
|
+
import { snapshotBase, overwriteTargets } from './base-store.mjs';
|
|
53
|
+
import { listProposals } from './proposal-store.mjs';
|
|
49
54
|
|
|
50
55
|
// Privacy guard: refuse to read+inject .hypoignore-matched
|
|
51
56
|
// wiki files into additionalContext. Without this, a user who lists
|
|
52
57
|
// `projects/private/hot.md` in .hypoignore would still see SECRET emit because
|
|
53
58
|
// session-start reads hot/state paths directly.
|
|
59
|
+
//
|
|
60
|
+
// Visibility guard: a machine-scoped page (visibility_scope: machine:<owner>)
|
|
61
|
+
// must not be injected on a machine other than its owner. hypo-file-watch
|
|
62
|
+
// already filters these very files, so leaving session start unfiltered made the
|
|
63
|
+
// SAME file behave differently depending on which path opened it: the user sets
|
|
64
|
+
// the field, sees it honored on edit, and never learns that session start still
|
|
65
|
+
// ships the body. Read the scope from the RAW content before the maxChars slice:
|
|
66
|
+
// slicing first could cut the frontmatter off and silently fail open.
|
|
67
|
+
// The root hot.md is a frontmatter-less pointer table, so it reads as '' and
|
|
68
|
+
// passes (shared) unchanged.
|
|
54
69
|
function readIfNotIgnored(path, maxChars, patterns) {
|
|
55
70
|
if (!path) return null;
|
|
56
71
|
if (patterns.length > 0 && isIgnored(path, HYPO_DIR, patterns)) return null;
|
|
57
|
-
|
|
72
|
+
const raw = readFileSync(path, 'utf-8');
|
|
73
|
+
if (!scopeVisible(readVisibilityScope(raw), currentDevice())) return null;
|
|
74
|
+
return raw.slice(0, maxChars);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Scoped-out is not the same as absent. Both make readIfNotIgnored return null,
|
|
78
|
+
// but telling the model "no snapshot yet / first session" when the snapshot merely
|
|
79
|
+
// belongs to another machine is a lie it will act on. Returns false for an ignored
|
|
80
|
+
// or missing file so only a real machine-scope hide reports true.
|
|
81
|
+
// The caller may name the project and the fact, never the withheld body: a message
|
|
82
|
+
// explaining the hide must not re-leak what it hid.
|
|
83
|
+
function isScopedOut(path, patterns) {
|
|
84
|
+
try {
|
|
85
|
+
if (!path || !existsSync(path)) return false;
|
|
86
|
+
if (patterns.length > 0 && isIgnored(path, HYPO_DIR, patterns)) return false;
|
|
87
|
+
return !scopeVisible(readVisibilityScope(readFileSync(path, 'utf-8')), currentDevice());
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
58
91
|
}
|
|
59
92
|
|
|
60
93
|
// Compute the STALE marker for a hot/state file from its RAW content (readIfNotIgnored
|
|
@@ -271,6 +304,24 @@ function syncStateNotice(pullOk) {
|
|
|
271
304
|
}
|
|
272
305
|
return `[WIKI: last sync failed: ${last.op || '?'} — ${last.error || 'unknown'}]`;
|
|
273
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Surface the vault-wide count of parked write-proposals (T8). Routed
|
|
309
|
+
* exactly like syncStateNotice: the line joins the `notices` array (→
|
|
310
|
+
* additionalContext) and is also written to stderr, so both the model and the
|
|
311
|
+
* user's transcript see it. NOT a systemMessage banner (that channel is
|
|
312
|
+
* reserved for the update/sibling notices). Pure read (listProposals never
|
|
313
|
+
* mutates); best-effort so a store read failure never breaks SessionStart. '' when
|
|
314
|
+
* there are no pending proposals, so nothing surfaces on the empty path.
|
|
315
|
+
*/
|
|
316
|
+
function pendingProposalNotice() {
|
|
317
|
+
try {
|
|
318
|
+
const n = listProposals(HYPO_DIR).length;
|
|
319
|
+
if (n === 0) return '';
|
|
320
|
+
return `[WIKI: 대기 proposal ${n}건 (검토: hypomnema proposal list)]`;
|
|
321
|
+
} catch {
|
|
322
|
+
return '';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
274
325
|
const GLOBAL_HOT = join(HYPO_DIR, 'hot.md');
|
|
275
326
|
const HOT_CHARS = 2000;
|
|
276
327
|
const STATE_CHARS = 2000;
|
|
@@ -346,6 +397,7 @@ process.stdin.on('end', () => {
|
|
|
346
397
|
|
|
347
398
|
const pullOk = gitPull(HYPO_DIR);
|
|
348
399
|
const syncLine = syncStateNotice(pullOk);
|
|
400
|
+
const proposalLine = pendingProposalNotice();
|
|
349
401
|
const growthLine = readLastGrowthLine();
|
|
350
402
|
// On source='clear', surface the dying
|
|
351
403
|
// session's identity that hypo-session-end stashed so Claude can recover
|
|
@@ -364,11 +416,17 @@ process.stdin.on('end', () => {
|
|
|
364
416
|
// transcript/--verbose only and out of this banner's scope.)
|
|
365
417
|
const userMessage = [updateLine, siblingLine].filter(Boolean).join('\n\n');
|
|
366
418
|
if (userMessage) outExtra = { ...outExtra, systemMessage: userMessage };
|
|
367
|
-
const notices = [
|
|
368
|
-
|
|
369
|
-
|
|
419
|
+
const notices = [
|
|
420
|
+
syncLine,
|
|
421
|
+
proposalLine,
|
|
422
|
+
growthLine,
|
|
423
|
+
clearRecoveryLine,
|
|
424
|
+
updateLine,
|
|
425
|
+
siblingLine,
|
|
426
|
+
].filter(Boolean);
|
|
370
427
|
let noticePrefix = notices.length ? `${notices.join('\n\n')}\n\n` : '';
|
|
371
428
|
if (syncLine) process.stderr.write(`\n\x1b[33m${syncLine}\x1b[0m\n`);
|
|
429
|
+
if (proposalLine) process.stderr.write(`\n\x1b[33m${proposalLine}\x1b[0m\n`);
|
|
372
430
|
if (growthLine) process.stderr.write(`\n\x1b[36m${growthLine}\x1b[0m\n`);
|
|
373
431
|
if (clearRecoveryLine)
|
|
374
432
|
process.stderr.write(`\n\x1b[33m${clearRecoveryLine.split('\n')[0]}\x1b[0m\n`);
|
|
@@ -379,6 +437,18 @@ process.stdin.on('end', () => {
|
|
|
379
437
|
const MARKER_FILE = sessionMarkerPath(sessionId);
|
|
380
438
|
const hit = findProjectFiles(cwd);
|
|
381
439
|
|
|
440
|
+
// Observed-base snapshot for the write=proposal gate. Deliberately AFTER gitPull: the base must
|
|
441
|
+
// describe the tree this session actually starts from, remote merges
|
|
442
|
+
// included, or the first close would raise a proposal against content the
|
|
443
|
+
// session never had a chance to conflict with. Once per session
|
|
444
|
+
// (existence-check inside snapshotBase), so resume and compact leave it
|
|
445
|
+
// alone. `data.session_id` is used raw rather than the 'default' fallback
|
|
446
|
+
// above. A session with no id has no base and closes down the legacy
|
|
447
|
+
// direct-write path.
|
|
448
|
+
if (data.session_id) {
|
|
449
|
+
snapshotBase(HYPO_DIR, data.session_id, overwriteTargets(hit ? hit.proj : null));
|
|
450
|
+
}
|
|
451
|
+
|
|
382
452
|
const ignorePatterns = loadHypoIgnore(HYPO_DIR);
|
|
383
453
|
|
|
384
454
|
// When cwd is a project working_dir that is NOT the vault itself, tell the
|
|
@@ -425,17 +495,29 @@ process.stdin.on('end', () => {
|
|
|
425
495
|
),
|
|
426
496
|
);
|
|
427
497
|
} else {
|
|
498
|
+
// A snapshot that exists but is scoped to another machine must not be
|
|
499
|
+
// reported as "no snapshot yet": the model would treat a resumed project
|
|
500
|
+
// as a first session. Say which it is, and say nothing of the contents.
|
|
501
|
+
const scopedOut =
|
|
502
|
+
isScopedOut(hit.hotPath, ignorePatterns) || isScopedOut(hit.statePath, ignorePatterns);
|
|
503
|
+
const reason = scopedOut ? 'snapshot scoped to another machine' : 'no snapshot yet';
|
|
428
504
|
process.stderr.write(
|
|
429
|
-
`\n\x1b[36m[Hypomnema]\x1b[0m project: \x1b[1m${hit.proj}\x1b[0m (
|
|
505
|
+
`\n\x1b[36m[Hypomnema]\x1b[0m project: \x1b[1m${hit.proj}\x1b[0m (${reason})\n\n`,
|
|
430
506
|
);
|
|
507
|
+
// Carry the reason into the marker, not just this hook's output.
|
|
508
|
+
// hypo-first-prompt derives its resume line from the marker alone, so a
|
|
509
|
+
// marker that says only `hotPath: null` makes the NEXT prompt announce
|
|
510
|
+
// "first session" for a project that merely belongs to another machine.
|
|
511
|
+
// That lie is what invites the model to author a fresh hot.md over one
|
|
512
|
+
// that already exists elsewhere.
|
|
431
513
|
writeFileSync(
|
|
432
514
|
MARKER_FILE,
|
|
433
|
-
JSON.stringify({ proj: hit.proj, hotPath: null, ts: Date.now() }),
|
|
515
|
+
JSON.stringify({ proj: hit.proj, hotPath: null, scopedOut, ts: Date.now() }),
|
|
434
516
|
);
|
|
435
517
|
console.log(
|
|
436
518
|
JSON.stringify(
|
|
437
519
|
buildOutput(
|
|
438
|
-
`${noticePrefix}${hitPrefix}[WIKI HOT CACHE: project=${sanitizeProjForPrompt(hit.proj)},
|
|
520
|
+
`${noticePrefix}${hitPrefix}[WIKI HOT CACHE: project=${sanitizeProjForPrompt(hit.proj)}, ${reason}]`,
|
|
439
521
|
outExtra,
|
|
440
522
|
),
|
|
441
523
|
),
|