hypomnema 1.5.0 → 1.6.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 +36 -18
- package/README.md +26 -8
- package/commands/audit.md +1 -1
- package/commands/capture.md +40 -0
- package/commands/crystallize.md +15 -7
- package/docs/ARCHITECTURE.md +2 -2
- package/docs/CONTRIBUTING.md +18 -0
- package/hooks/hypo-lookup.mjs +37 -2
- package/hooks/hypo-session-start.mjs +28 -2
- package/hooks/hypo-shared.mjs +159 -14
- package/package.json +1 -1
- package/scripts/capture.mjs +695 -0
- package/scripts/crystallize.mjs +292 -121
- package/scripts/doctor.mjs +71 -4
- package/scripts/init.mjs +16 -5
- package/scripts/lib/core-hooks.mjs +156 -0
- package/scripts/lib/extensions.mjs +317 -9
- package/scripts/lib/page-usage.mjs +141 -0
- package/scripts/uninstall.mjs +84 -42
- package/scripts/upgrade.mjs +68 -0
- package/skills/crystallize/SKILL.md +10 -4
- package/skills/graph/SKILL.md +1 -1
- package/skills/ingest/SKILL.md +1 -1
- package/skills/lint/SKILL.md +1 -1
- package/skills/query/SKILL.md +1 -1
- package/skills/verify/SKILL.md +1 -1
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +1 -1
package/scripts/uninstall.mjs
CHANGED
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
EXT_TYPES,
|
|
39
39
|
CODEX_TYPES,
|
|
40
40
|
readExtensionPkgStateNoMutate,
|
|
41
|
+
parseExtKey,
|
|
42
|
+
buildHookCommand,
|
|
41
43
|
} from './lib/extensions.mjs';
|
|
42
44
|
|
|
43
45
|
const HOME = homedir();
|
|
@@ -138,13 +140,23 @@ function stripExtensionsFromPkg(pkgPath, target, removedKeys, apply) {
|
|
|
138
140
|
return changed;
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
// Remove
|
|
142
|
-
//
|
|
143
|
-
// hypo-pkg.json#extensions[target]
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
143
|
+
// Remove Hypomnema-installed extension hard-copies for one target (claude |
|
|
144
|
+
// codex). Ownership is decided by the per-target recorded SHA map in
|
|
145
|
+
// hypo-pkg.json#extensions[target]: we iterate the RECORDED KEYS (not a
|
|
146
|
+
// filesystem+prefix scan) because the reverse-capture design decoupled the install filename from
|
|
147
|
+
// the wiki `hypo-ext-*` storage name — a reverse-captured command installs as
|
|
148
|
+
// `commands/mycmd.md`, which a prefix scan would never reach. The SHA map key
|
|
149
|
+
// IS the install path relative to the target root, so it names every file we
|
|
150
|
+
// own regardless of its filename.
|
|
151
|
+
//
|
|
152
|
+
// Safety (capture design §8): a recorded key comes from an on-disk JSON we do not
|
|
153
|
+
// fully control, so each key is validated by `parseExtKey` to a single covered
|
|
154
|
+
// `<type>/<safe-basename>` segment (no separators / traversal) before any
|
|
155
|
+
// join/rm — a corrupt or malicious key can never delete outside the extension
|
|
156
|
+
// directory. "Never delete unowned" holds via SHA MATCHING, not the prefix: a
|
|
157
|
+
// file whose on-disk SHA differs from the recorded one is user-modified and
|
|
158
|
+
// preserved unless --force-extensions; a symlink/non-regular target is always
|
|
159
|
+
// preserved (force does not follow them, matching install/upgrade E3's guard).
|
|
148
160
|
function removeExtensions(target, apply, force) {
|
|
149
161
|
const targetRoot = target === 'codex' ? join(HOME, '.codex') : join(HOME, '.claude');
|
|
150
162
|
const types = target === 'codex' ? CODEX_TYPES : EXT_TYPES;
|
|
@@ -158,37 +170,27 @@ function removeExtensions(target, apply, force) {
|
|
|
158
170
|
const skippedUserModified = [];
|
|
159
171
|
const skippedNonRegular = [];
|
|
160
172
|
|
|
161
|
-
for (const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
for (const [key, recordedSHA] of Object.entries(recorded)) {
|
|
174
|
+
if (!recordedSHA) continue; // no ownership baseline → nothing to remove
|
|
175
|
+
const parsed = parseExtKey(key, types);
|
|
176
|
+
if (!parsed) continue; // untrusted / cross-target key → never join+rm
|
|
177
|
+
const fullPath = join(targetRoot, parsed.type, parsed.installFile);
|
|
178
|
+
if (!existsSync(fullPath)) continue; // already gone
|
|
179
|
+
|
|
180
|
+
if (!isRegularFile(fullPath)) {
|
|
181
|
+
// Refuse to follow symlinks/sockets even under --force-extensions.
|
|
182
|
+
skippedNonRegular.push(fullPath);
|
|
168
183
|
continue;
|
|
169
184
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
skippedNonRegular.push(fullPath);
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
const buf = readFileIfRegular(fullPath);
|
|
183
|
-
const sha = buf ? sha256(buf) : null;
|
|
184
|
-
|
|
185
|
-
if (sha === recordedSHA || force) {
|
|
186
|
-
if (apply) rmSync(fullPath);
|
|
187
|
-
removed.push(fullPath);
|
|
188
|
-
removedKeys.push(key);
|
|
189
|
-
} else {
|
|
190
|
-
skippedUserModified.push(fullPath);
|
|
191
|
-
}
|
|
185
|
+
const buf = readFileIfRegular(fullPath);
|
|
186
|
+
const sha = buf ? sha256(buf) : null;
|
|
187
|
+
|
|
188
|
+
if (sha === recordedSHA || force) {
|
|
189
|
+
if (apply) rmSync(fullPath);
|
|
190
|
+
removed.push(fullPath);
|
|
191
|
+
removedKeys.push(key);
|
|
192
|
+
} else {
|
|
193
|
+
skippedUserModified.push(fullPath);
|
|
192
194
|
}
|
|
193
195
|
}
|
|
194
196
|
return { target, removed, removedKeys, skippedUserModified, skippedNonRegular };
|
|
@@ -226,12 +228,39 @@ function unprocessedExtensionTargetRemains(pkgPath, processedTargets) {
|
|
|
226
228
|
return false;
|
|
227
229
|
}
|
|
228
230
|
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
+
// Build the set of settings.json commands owned by reverse-captured hooks that
|
|
232
|
+
// install under their ORIGINAL name (not the hypo-ext-* storage name). The prefix
|
|
233
|
+
// scan below only reaches hypo-ext-* commands; a captured hook registers as
|
|
234
|
+
// `node $HOME/.claude/hooks/foo.mjs`, which no prefix would ever match. The
|
|
235
|
+
// recorded per-target SHA map names every file we own, so its hooks `.mjs` keys
|
|
236
|
+
// reconstruct exactly the commands we registered. Read WITHOUT mutation and while
|
|
237
|
+
// the map is still whole. stripExtensionsFromPkg (which clears it) runs later.
|
|
238
|
+
// Only `.mjs` keys are commands; the paired `.manifest.json` sidecar keys (which
|
|
239
|
+
// parseExtKey also admits for hooks) are dropped by the extension check.
|
|
240
|
+
function ownedHookCommands(pkgPath, target, hooksDir) {
|
|
241
|
+
const types = target === 'codex' ? CODEX_TYPES : EXT_TYPES;
|
|
242
|
+
const recorded = readExtensionPkgStateNoMutate(pkgPath, target);
|
|
243
|
+
const commands = new Set();
|
|
244
|
+
for (const key of Object.keys(recorded)) {
|
|
245
|
+
const parsed = parseExtKey(key, types);
|
|
246
|
+
if (!parsed || parsed.type !== 'hooks') continue;
|
|
247
|
+
if (!parsed.installFile.endsWith('.mjs')) continue;
|
|
248
|
+
commands.add(buildHookCommand(hooksDir, parsed.installFile));
|
|
249
|
+
}
|
|
250
|
+
return commands;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// settings.json: strip groups whose command is one of ours under the target's
|
|
254
|
+
// hooks dir. Ownership is the UNION of two path-based signals (plan §0 D1, P2):
|
|
255
|
+
// the hypo-ext-* command prefix (catches wiki-authored hooks and hypo-ext-*
|
|
256
|
+
// leftovers whose recorded key was already cleared by a partial uninstall) and
|
|
257
|
+
// the recorded owned-command set (`ownedCommands`, catches reverse-captured hooks
|
|
258
|
+
// registered under their original name, which carry no hypo-ext-* marker). Both
|
|
259
|
+
// branches are needed, so this is a union, not a replacement. No hookMap needed
|
|
231
260
|
// because ext hooks are never enumerated there. Mixed groups (foreign hook +
|
|
232
261
|
// ours) keep the foreign hook; ours-only groups are dropped entirely. Mirrors
|
|
233
262
|
// stripSettingsJson's flatMap pattern so other-plugin invariants (§7.3) hold.
|
|
234
|
-
function stripExtensionSettings(settingsPath, hooksDir, apply) {
|
|
263
|
+
function stripExtensionSettings(settingsPath, hooksDir, apply, ownedCommands = new Set()) {
|
|
235
264
|
if (!existsSync(settingsPath)) return { stripped: [] };
|
|
236
265
|
let settings;
|
|
237
266
|
try {
|
|
@@ -243,7 +272,10 @@ function stripExtensionSettings(settingsPath, hooksDir, apply) {
|
|
|
243
272
|
|
|
244
273
|
const cmdPrefix = `node ${hooksDir.replace(HOME, '$HOME')}/${EXT_PREFIX}`;
|
|
245
274
|
const isExtHook = (h) =>
|
|
246
|
-
h &&
|
|
275
|
+
h &&
|
|
276
|
+
h.type === 'command' &&
|
|
277
|
+
typeof h.command === 'string' &&
|
|
278
|
+
(h.command.startsWith(cmdPrefix) || ownedCommands.has(h.command));
|
|
247
279
|
|
|
248
280
|
const stripped = [];
|
|
249
281
|
let changed = false;
|
|
@@ -432,7 +464,12 @@ const commandResult = removeCommands(args.apply, args.forceCommands);
|
|
|
432
464
|
// hook copy was deleted by hand (force-commands legacy state).
|
|
433
465
|
const pkgJsonPath = join(HOME, '.claude', 'hypo-pkg.json');
|
|
434
466
|
const claudeExtResult = removeExtensions('claude', args.apply, args.forceExtensions);
|
|
435
|
-
const claudeExtSettings = stripExtensionSettings(
|
|
467
|
+
const claudeExtSettings = stripExtensionSettings(
|
|
468
|
+
claudeSettings,
|
|
469
|
+
claudeHooksDir,
|
|
470
|
+
args.apply,
|
|
471
|
+
ownedHookCommands(pkgJsonPath, 'claude', claudeHooksDir),
|
|
472
|
+
);
|
|
436
473
|
|
|
437
474
|
let codexHookResult = { removed: [], missing: [] };
|
|
438
475
|
let codexSettingsResult = { stripped: [] };
|
|
@@ -452,7 +489,12 @@ if (args.codex) {
|
|
|
452
489
|
codexHookResult = removeHookFiles(codexHooksDir, hookFiles, args.apply);
|
|
453
490
|
codexSettingsResult = stripSettingsJson(codexSettings, codexHooksDir, hookMap, args.apply);
|
|
454
491
|
codexExtResult = removeExtensions('codex', args.apply, args.forceExtensions);
|
|
455
|
-
codexExtSettings = stripExtensionSettings(
|
|
492
|
+
codexExtSettings = stripExtensionSettings(
|
|
493
|
+
codexSettings,
|
|
494
|
+
codexHooksDir,
|
|
495
|
+
args.apply,
|
|
496
|
+
ownedHookCommands(pkgJsonPath, 'codex', codexHooksDir),
|
|
497
|
+
);
|
|
456
498
|
}
|
|
457
499
|
|
|
458
500
|
// Surgical per-target ext SHA strip — only for files we actually removed.
|
package/scripts/upgrade.mjs
CHANGED
|
@@ -465,6 +465,49 @@ function applyHypoignoreMigration(result) {
|
|
|
465
465
|
return appended;
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
+
// ── .gitignore migration: mirror .cache/ into .gitignore (page-usage privacy) ─
|
|
469
|
+
//
|
|
470
|
+
// Legacy vaults may have a .gitignore that predates .cache/ (init now scaffolds
|
|
471
|
+
// it, but old vaults were migrated only in .hypoignore). Without .cache/ in
|
|
472
|
+
// .gitignore, the page-usage log could be committed. This mirrors the hypoignore
|
|
473
|
+
// migration: idempotent append of the missing entry, no-op when already present
|
|
474
|
+
// or when there is no .gitignore (the runtime logging guard fails closed for the
|
|
475
|
+
// no-file case, so nothing leaks in the meantime).
|
|
476
|
+
|
|
477
|
+
const GITIGNORE_REQUIRED_ENTRIES = [
|
|
478
|
+
{
|
|
479
|
+
pattern: '.cache/',
|
|
480
|
+
comment: '# Hypomnema runtime cache (page-usage log, session growth metrics)',
|
|
481
|
+
},
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
function checkGitignore(hypoDir) {
|
|
485
|
+
const path = join(hypoDir, '.gitignore');
|
|
486
|
+
if (!existsSync(path)) return { status: 'no-file', missing: [], path };
|
|
487
|
+
const content = readFileSync(path, 'utf-8');
|
|
488
|
+
const entries = new Set(
|
|
489
|
+
content
|
|
490
|
+
.split('\n')
|
|
491
|
+
.map((l) => l.trim())
|
|
492
|
+
.filter((l) => l && !l.startsWith('#')),
|
|
493
|
+
);
|
|
494
|
+
const missing = GITIGNORE_REQUIRED_ENTRIES.filter((e) => !entries.has(e.pattern));
|
|
495
|
+
return { status: missing.length === 0 ? 'up-to-date' : 'needs-migration', missing, path };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function applyGitignoreMigration(result) {
|
|
499
|
+
if (result.status !== 'needs-migration') return [];
|
|
500
|
+
let content = readFileSync(result.path, 'utf-8');
|
|
501
|
+
if (!content.endsWith('\n')) content += '\n';
|
|
502
|
+
const appended = [];
|
|
503
|
+
for (const entry of result.missing) {
|
|
504
|
+
content += `\n${entry.comment}\n${entry.pattern}\n`;
|
|
505
|
+
appended.push(entry.pattern);
|
|
506
|
+
}
|
|
507
|
+
writeFileSync(result.path, content);
|
|
508
|
+
return appended;
|
|
509
|
+
}
|
|
510
|
+
|
|
468
511
|
function writeMigrationReport(hypoDir, fromVersion, toVersion, { pluginMode = false } = {}) {
|
|
469
512
|
const today = new Date().toISOString().slice(0, 10);
|
|
470
513
|
const filename = `MIGRATION-v${toVersion}.md`;
|
|
@@ -849,6 +892,7 @@ const pkgJson = checkPkgJson();
|
|
|
849
892
|
const commands = checkCommands();
|
|
850
893
|
const oldHookRefs = checkOldHookNames(claudeSettingsPath);
|
|
851
894
|
const hypoignore = checkHypoignore(args.hypoDir);
|
|
895
|
+
const gitignore = checkGitignore(args.hypoDir);
|
|
852
896
|
|
|
853
897
|
// when --codex is set, mirror the same core-hook checks against ~/.codex/
|
|
854
898
|
// so `hypomnema upgrade --codex` reports drift symmetrically and `--apply --codex`
|
|
@@ -930,6 +974,7 @@ let appliedSettingsCodex = [];
|
|
|
930
974
|
let appliedHookNameRenamesCodex = [];
|
|
931
975
|
let appliedCommands = [];
|
|
932
976
|
let appliedHypoignore = [];
|
|
977
|
+
let appliedGitignore = [];
|
|
933
978
|
let appliedExtensions = null;
|
|
934
979
|
let appliedExtensionsCodex = null;
|
|
935
980
|
|
|
@@ -1014,6 +1059,7 @@ if (args.apply) {
|
|
|
1014
1059
|
}
|
|
1015
1060
|
}
|
|
1016
1061
|
appliedHypoignore = applyHypoignoreMigration(hypoignore);
|
|
1062
|
+
appliedGitignore = applyGitignoreMigration(gitignore);
|
|
1017
1063
|
// codex core hooks + settings + wiki-*→hypo-* rename mirror. Same order
|
|
1018
1064
|
// as the claude side (rename first so subsequent hook copy can find renamed targets).
|
|
1019
1065
|
if (args.codex) {
|
|
@@ -1089,6 +1135,7 @@ const hasDrift =
|
|
|
1089
1135
|
pkgJsonDrift ||
|
|
1090
1136
|
schemaDrift ||
|
|
1091
1137
|
hypoignore.status === 'needs-migration' ||
|
|
1138
|
+
gitignore.status === 'needs-migration' ||
|
|
1092
1139
|
extDrift ||
|
|
1093
1140
|
codexCoreDrift;
|
|
1094
1141
|
|
|
@@ -1109,6 +1156,7 @@ if (args.json) {
|
|
|
1109
1156
|
commands,
|
|
1110
1157
|
oldHookRefs,
|
|
1111
1158
|
hypoignore,
|
|
1159
|
+
gitignore,
|
|
1112
1160
|
extensions: extCheck,
|
|
1113
1161
|
extensionsCodex: extCheckCodex,
|
|
1114
1162
|
// codex core mirror (null when --codex absent).
|
|
@@ -1122,6 +1170,7 @@ if (args.json) {
|
|
|
1122
1170
|
hookNameRenames: appliedHookNameRenames,
|
|
1123
1171
|
commands: appliedCommands,
|
|
1124
1172
|
hypoignore: appliedHypoignore,
|
|
1173
|
+
gitignore: appliedGitignore,
|
|
1125
1174
|
extensions: appliedExtensions,
|
|
1126
1175
|
extensionsCodex: appliedExtensionsCodex,
|
|
1127
1176
|
hooksCodex: appliedHooksCodex,
|
|
@@ -1369,6 +1418,18 @@ if (hypoignore.status === 'no-file') {
|
|
|
1369
1418
|
for (const e of hypoignore.missing) lines.push(` + ${e.pattern}`);
|
|
1370
1419
|
}
|
|
1371
1420
|
|
|
1421
|
+
// .gitignore migration (page-usage privacy: mirror .cache/ into .gitignore)
|
|
1422
|
+
if (gitignore.status === 'no-file') {
|
|
1423
|
+
lines.push(`⚠ .gitignore not found at ${gitignore.path} (init.mjs scaffolds this)`);
|
|
1424
|
+
} else if (gitignore.status === 'up-to-date') {
|
|
1425
|
+
lines.push(`✓ .gitignore required entries present`);
|
|
1426
|
+
} else {
|
|
1427
|
+
lines.push(
|
|
1428
|
+
`⚠ .gitignore ${gitignore.missing.length} missing entry(s), run --apply to append:`,
|
|
1429
|
+
);
|
|
1430
|
+
for (const e of gitignore.missing) lines.push(` + ${e.pattern}`);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1372
1433
|
// Extensions companion (conflict/drift gating E3, #31). Shared by the
|
|
1373
1434
|
// claude target and, under --codex, the codex target (E4, #32) — the label keeps
|
|
1374
1435
|
// the two blocks distinguishable in the report.
|
|
@@ -1424,6 +1485,7 @@ if (
|
|
|
1424
1485
|
appliedHookNameRenames.length > 0 ||
|
|
1425
1486
|
appliedCommands.length > 0 ||
|
|
1426
1487
|
appliedHypoignore.length > 0 ||
|
|
1488
|
+
appliedGitignore.length > 0 ||
|
|
1427
1489
|
appliedHooksCodex.length > 0 ||
|
|
1428
1490
|
appliedSettingsCodex.length > 0 ||
|
|
1429
1491
|
appliedHookNameRenamesCodex.length > 0
|
|
@@ -1452,6 +1514,10 @@ if (
|
|
|
1452
1514
|
lines.push(`✓ Appended .hypoignore entries (${appliedHypoignore.length}):`);
|
|
1453
1515
|
for (const e of appliedHypoignore) lines.push(` → ${e}`);
|
|
1454
1516
|
}
|
|
1517
|
+
if (appliedGitignore.length > 0) {
|
|
1518
|
+
lines.push(`✓ Appended .gitignore entries (${appliedGitignore.length}):`);
|
|
1519
|
+
for (const e of appliedGitignore) lines.push(` → ${e}`);
|
|
1520
|
+
}
|
|
1455
1521
|
// codex-target applied actions (mirrors claude blocks above).
|
|
1456
1522
|
if (appliedHookNameRenamesCodex.length > 0) {
|
|
1457
1523
|
lines.push(`✓ Renamed legacy hook references (codex) (${appliedHookNameRenamesCodex.length}):`);
|
|
@@ -1507,6 +1573,7 @@ const totalDrift =
|
|
|
1507
1573
|
(pkgJsonDrift ? 1 : 0) +
|
|
1508
1574
|
(schemaDrift ? 1 : 0) +
|
|
1509
1575
|
(hypoignore.status === 'needs-migration' ? hypoignore.missing.length : 0) +
|
|
1576
|
+
(gitignore.status === 'needs-migration' ? gitignore.missing.length : 0) +
|
|
1510
1577
|
extCheck.actions.filter(
|
|
1511
1578
|
(a) => a.action === 'create' || a.action === 'update' || a.action === 'force-update',
|
|
1512
1579
|
).length +
|
|
@@ -1544,6 +1611,7 @@ if (totalDrift === 0) {
|
|
|
1544
1611
|
(appliedPkgJson ? 1 : 0) +
|
|
1545
1612
|
appliedHookNameRenames.length +
|
|
1546
1613
|
appliedHypoignore.length +
|
|
1614
|
+
appliedGitignore.length +
|
|
1547
1615
|
appliedExtCount +
|
|
1548
1616
|
appliedHooksCodex.length +
|
|
1549
1617
|
appliedSettingsCodex.length +
|
|
@@ -92,7 +92,9 @@ main conversation id. Passing the wrong id causes `markerWritten: false` with
|
|
|
92
92
|
`markerSkipReason: "transcript-unresolved"` or `"no-user-close-signal"`: the
|
|
93
93
|
5 mandatory files are written (ok: true) but the Stop-chain marker is withheld,
|
|
94
94
|
so the session is not actually closed and the Stop hook re-prompts. The correct
|
|
95
|
-
id comes from the `[WIKI_AUTOCLOSE]` block reason or
|
|
95
|
+
id comes from the `[WIKI_AUTOCLOSE]` block reason or the injected
|
|
96
|
+
`$CLAUDE_CODE_SESSION_ID` (accept the legacy spelling via
|
|
97
|
+
`${CLAUDE_CODE_SESSION_ID:-$CLAUDE_SESSION_ID}`).
|
|
96
98
|
|
|
97
99
|
Once it passes, report each item with ✓:
|
|
98
100
|
- ✓ session-state.md
|
|
@@ -100,11 +102,15 @@ Once it passes, report each item with ✓:
|
|
|
100
102
|
- ✓ session-log entry
|
|
101
103
|
- ✓ open-questions (or skipped if unchanged)
|
|
102
104
|
- ✓ log.md entry
|
|
103
|
-
- **marker written?** (required, if `--apply-session-close --session-id` was used): check `markerWritten` in the JSON output. If `true`, report "session-close marker written." If `false`, do NOT declare the session closed or complete
|
|
105
|
+
- **marker written?** (required, if `--apply-session-close --session-id` was used): check `markerWritten` in the JSON output. If `true`, report "session-close marker written." If `false`, do NOT declare the session closed or complete; recover per the `markerSkipReason` branch below.
|
|
104
106
|
|
|
105
107
|
If `markerWritten: true` (or `--session-id` was not passed), ask: "Session closed. Would you like to also run knowledge synthesis now, or stop here?"
|
|
106
108
|
|
|
107
|
-
If `markerWritten: false`, do NOT say "session closed."
|
|
109
|
+
If `markerWritten: false`, do NOT say "session closed." Branch on `markerSkipReason`:
|
|
110
|
+
|
|
111
|
+
- `no-user-close-signal`: files applied cleanly and the transcript resolved, but it carries no close phrase the gate recognizes (the user's close wording fell outside the close-signal set, e.g. "세션 마무리까지 진행해줘" / "세션 마무리 진행"). Re-running the same id will not help. Confirm intent once with `AskUserQuestion` (header "세션", one option **세션 마무리**); if the user picks it, that answer is a recognized close signal, so re-run the same `--apply-session-close … --session-id` command (idempotent no-op writes; the marker lands). If the user declines, leave the session unmarked. Do NOT change the close-signal matcher.
|
|
112
|
+
- `transcript-unresolved` (wrong / background id): report "Files applied and verified (ok: true), but the marker was not written (reason: `<markerSkipReason>`). The Stop-chain is still active. Re-run with the correct main-conversation `--session-id`."
|
|
113
|
+
- any other reason (`compact-gate-not-ok`, `commit-failed: …`, `marker-did-not-land`): surface it verbatim and resolve the underlying blocker before re-running.
|
|
108
114
|
|
|
109
115
|
---
|
|
110
116
|
|
|
@@ -141,4 +147,4 @@ evidence_strength: inferred
|
|
|
141
147
|
|
|
142
148
|
---
|
|
143
149
|
|
|
144
|
-
> **Citation convention.** When you reference a wiki page in your response, link it as `[[page-slug]]
|
|
150
|
+
> **Citation convention.** When you reference a wiki page in your response, link it as `[[page-slug]]` so it stays connected in the graph. The observability audit scores sessions on search / ingest / feedback activity (recorded by `hypo-session-record`), not on these inline citations; run `/hypo:audit` to inspect and see [[pages/observability/_index]].
|
package/skills/graph/SKILL.md
CHANGED
|
@@ -55,4 +55,4 @@ If the graph has 0 edges, note that no `[[wikilinks]]` were found between pages.
|
|
|
55
55
|
|
|
56
56
|
---
|
|
57
57
|
|
|
58
|
-
> **Citation convention.** When you reference a wiki page in your response, link it as `[[page-slug]]
|
|
58
|
+
> **Citation convention.** When you reference a wiki page in your response, link it as `[[page-slug]]` so it stays connected in the graph. The observability audit scores sessions on search / ingest / feedback activity (recorded by `hypo-session-record`), not on these inline citations; run `/hypo:audit` to inspect and see [[pages/observability/_index]].
|
package/skills/ingest/SKILL.md
CHANGED
|
@@ -104,4 +104,4 @@ Append an ingest entry to `<wiki-root>/log.md`:
|
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
107
|
-
> **Citation convention.** When you reference a wiki page in your response, link it as `[[page-slug]]
|
|
107
|
+
> **Citation convention.** When you reference a wiki page in your response, link it as `[[page-slug]]` so it stays connected in the graph. The observability audit scores sessions on search / ingest / feedback activity (recorded by `hypo-session-record`), not on these inline citations; run `/hypo:audit` to inspect and see [[pages/observability/_index]].
|
package/skills/lint/SKILL.md
CHANGED
|
@@ -56,4 +56,4 @@ For **missing required fields** (`title`, `type`): open the affected files and h
|
|
|
56
56
|
|
|
57
57
|
---
|
|
58
58
|
|
|
59
|
-
> **Citation convention.** When you reference a wiki page in your response, link it as `[[page-slug]]
|
|
59
|
+
> **Citation convention.** When you reference a wiki page in your response, link it as `[[page-slug]]` so it stays connected in the graph. The observability audit scores sessions on search / ingest / feedback activity (recorded by `hypo-session-record`), not on these inline citations; run `/hypo:audit` to inspect and see [[pages/observability/_index]].
|
package/skills/query/SKILL.md
CHANGED
|
@@ -59,4 +59,4 @@ If zero results are returned, say so and offer to broaden the search or suggest
|
|
|
59
59
|
|
|
60
60
|
---
|
|
61
61
|
|
|
62
|
-
> **Citation convention.** When you reference a wiki page in your response, link it as `[[page-slug]]
|
|
62
|
+
> **Citation convention.** When you reference a wiki page in your response, link it as `[[page-slug]]` so it stays connected in the graph. The observability audit scores sessions on search / ingest / feedback activity (recorded by `hypo-session-record`), not on these inline citations; run `/hypo:audit` to inspect and see [[pages/observability/_index]].
|
package/skills/verify/SKILL.md
CHANGED
|
@@ -93,4 +93,4 @@ updated: <today YYYY-MM-DD>
|
|
|
93
93
|
|
|
94
94
|
---
|
|
95
95
|
|
|
96
|
-
> **Citation convention.** When you reference a wiki page in your response, link it as `[[page-slug]]
|
|
96
|
+
> **Citation convention.** When you reference a wiki page in your response, link it as `[[page-slug]]` so it stays connected in the graph. The observability audit scores sessions on search / ingest / feedback activity (recorded by `hypo-session-record`), not on these inline citations; run `/hypo:audit` to inspect and see [[pages/observability/_index]].
|
package/templates/hypo-config.md
CHANGED
package/templates/hypo-guide.md
CHANGED
|
@@ -225,7 +225,7 @@ ADRs are immutable once accepted. Mark deprecated/superseded but never edit cont
|
|
|
225
225
|
|
|
226
226
|
Hypomnema v1.1.0 ships an **autonomy score** so you can see whether the wiki is actually being used per session, rather than just trusting that it is.
|
|
227
227
|
|
|
228
|
-
- **Citation:** when a response uses a wiki page, link it inline as `[[page-slug]]
|
|
228
|
+
- **Citation:** when a response uses a wiki page, link it inline as `[[page-slug]]` to keep it connected in the graph. (The audit score is based on the tool and command usage below, not on these inline citations.)
|
|
229
229
|
- **Tools that count:** `Grep`, `WebSearch`, `WebFetch`, and any `/hypo:query` / `/hypo:ingest` / `/hypo:feedback` invocation in the transcript.
|
|
230
230
|
- **Classification (heuristic v0):**
|
|
231
231
|
- `normal` — at least one search, no missed URL ingests
|