hypomnema 1.6.2 → 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.
Files changed (62) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ko.md +39 -14
  4. package/README.md +39 -14
  5. package/commands/capture.md +8 -6
  6. package/commands/crystallize.md +39 -20
  7. package/docs/ARCHITECTURE.md +49 -14
  8. package/docs/CONTRIBUTING.md +31 -29
  9. package/hooks/base-store.mjs +265 -0
  10. package/hooks/hooks.json +7 -1
  11. package/hooks/hypo-auto-minimal-crystallize.mjs +63 -20
  12. package/hooks/hypo-auto-stage.mjs +30 -0
  13. package/hooks/hypo-cwd-change.mjs +31 -2
  14. package/hooks/hypo-file-watch.mjs +21 -2
  15. package/hooks/hypo-first-prompt.mjs +19 -3
  16. package/hooks/hypo-lookup.mjs +86 -29
  17. package/hooks/hypo-personal-check.mjs +24 -3
  18. package/hooks/hypo-session-record.mjs +2 -3
  19. package/hooks/hypo-session-start.mjs +89 -7
  20. package/hooks/hypo-shared.mjs +904 -128
  21. package/hooks/proposal-store.mjs +513 -0
  22. package/package.json +40 -14
  23. package/scripts/capture.mjs +556 -37
  24. package/scripts/crystallize.mjs +639 -108
  25. package/scripts/doctor.mjs +304 -9
  26. package/scripts/feedback-sync.mjs +515 -44
  27. package/scripts/graph.mjs +9 -2
  28. package/scripts/init.mjs +230 -34
  29. package/scripts/lib/extensions.mjs +656 -1
  30. package/scripts/lib/hypo-ignore.mjs +54 -6
  31. package/scripts/lib/hypo-root.mjs +56 -6
  32. package/scripts/lib/page-usage.mjs +15 -2
  33. package/scripts/lib/pkg-json.mjs +40 -0
  34. package/scripts/lib/plugin-detect.mjs +96 -6
  35. package/scripts/lib/wd-match.mjs +23 -5
  36. package/scripts/lib/wikilink.mjs +32 -6
  37. package/scripts/lint.mjs +20 -4
  38. package/scripts/proposal.mjs +1032 -0
  39. package/scripts/query.mjs +25 -4
  40. package/scripts/resume.mjs +34 -12
  41. package/scripts/stats.mjs +28 -8
  42. package/scripts/uninstall.mjs +141 -6
  43. package/scripts/upgrade.mjs +197 -15
  44. package/skills/crystallize/SKILL.md +44 -7
  45. package/skills/debate/SKILL.md +88 -0
  46. package/skills/debate/references/orchestration-patterns.md +83 -0
  47. package/templates/.hyposcanignore +10 -0
  48. package/templates/SCHEMA.md +12 -0
  49. package/templates/gitignore +5 -0
  50. package/templates/hypo-config.md +1 -1
  51. package/templates/hypo-guide.md +6 -0
  52. package/scripts/.gitkeep +0 -0
  53. package/scripts/check-bilingual.mjs +0 -153
  54. package/scripts/check-readme-version.mjs +0 -126
  55. package/scripts/check-tracker-ids.mjs +0 -426
  56. package/scripts/check-versions.mjs +0 -171
  57. package/scripts/install-git-hooks.mjs +0 -293
  58. package/scripts/lib/changelog-classify.mjs +0 -216
  59. package/scripts/lib/check-bilingual.mjs +0 -244
  60. package/scripts/lib/check-tracker-ids.mjs +0 -217
  61. package/scripts/lib/pre-commit-format.mjs +0 -251
  62. package/scripts/pre-commit-format.mjs +0 -198
@@ -49,9 +49,11 @@ import {
49
49
  sha256 as sha256Buf,
50
50
  isRegularFile,
51
51
  readFileIfRegular,
52
+ readVersionAtRoot,
53
+ writeDualSkipProvenance,
52
54
  } from './lib/pkg-json.mjs';
53
55
  import { syncExtensions } from './lib/extensions.mjs';
54
- import { isHypomnemaPluginEnabled } from './lib/plugin-detect.mjs';
56
+ import { isHypomnemaPluginEnabled, resolveEnabledPluginRoot } from './lib/plugin-detect.mjs';
55
57
  import { classifyInstall, downgradeGuardMessage } from '../hooks/version-check.mjs';
56
58
 
57
59
  const HOME = homedir();
@@ -213,26 +215,58 @@ const SHARED_FILES = _hookConfig.shared ?? [];
213
215
 
214
216
  // ── checks ───────────────────────────────────────────────────────────────────
215
217
 
216
- function checkSchemaVersion(hypoDir) {
217
- const pkgPath = join(TEMPLATES, 'SCHEMA.md');
218
- const hypoPath = join(hypoDir, 'SCHEMA.md');
218
+ // Shared by SCHEMA.md and hypo-guide.md: both templates carry a top-level
219
+ // `version:` frontmatter stamp, and both are compared the same way — installed
220
+ // copy vs. the package's own template — without ever writing to hypoPath. This
221
+ // function is read-only; nothing downstream may use its return value to
222
+ // overwrite the installed file (hand-authored wiki files are never
223
+ // silently clobbered).
224
+ function checkTemplateVersion(hypoDir, filename) {
225
+ const pkgPath = join(TEMPLATES, filename);
226
+ const hypoPath = join(hypoDir, filename);
219
227
 
220
228
  const pkgVersion = existsSync(pkgPath)
221
229
  ? parseVersion((parseFrontmatter(readFileSync(pkgPath, 'utf-8')) ?? {}).version)
222
230
  : null;
223
- const hypoVersion = existsSync(hypoPath)
231
+
232
+ const hypoExists = existsSync(hypoPath);
233
+ const hypoVersion = hypoExists
224
234
  ? parseVersion((parseFrontmatter(readFileSync(hypoPath, 'utf-8')) ?? {}).version)
225
235
  : null;
226
236
 
237
+ // File present but carries no `version:` frontmatter field at all — a
238
+ // pre-versioning install (the file predates this stamp existing). This is
239
+ // deliberately distinguished from "file missing entirely" (still routed to
240
+ // bumpType's 'unknown' below): a bare `bumpType(null, pkgVersion)` cannot
241
+ // tell the two apart, and folding this case into plain 'unknown' is exactly
242
+ // what let an already-installed, unstamped hypo-guide.md report "up to
243
+ // date" — the file this check exists to catch. The caller decides whether
244
+ // 'unstamped' counts as drift; this helper only classifies.
245
+ const unstamped = hypoExists && !hypoVersion && !!pkgVersion;
246
+
227
247
  return {
228
248
  installed: hypoVersion?.raw ?? null,
229
249
  current: pkgVersion?.raw ?? null,
230
- bump: bumpType(hypoVersion, pkgVersion),
250
+ bump: unstamped ? 'unstamped' : bumpType(hypoVersion, pkgVersion),
231
251
  hypoPath,
232
252
  pkgPath,
233
253
  };
234
254
  }
235
255
 
256
+ function checkSchemaVersion(hypoDir) {
257
+ return checkTemplateVersion(hypoDir, 'SCHEMA.md');
258
+ }
259
+
260
+ // hypo-guide.md had no update channel at all — upgrade.mjs never
261
+ // looked at it, and init.mjs only ever writes it once (skip if present), so an
262
+ // installed vault's copy went permanently stale the moment the shipped template
263
+ // changed, with no signal to the user. This check restores visibility (a warning
264
+ // on drift) without adding a write path: --apply still never touches an
265
+ // installed hypo-guide.md, exactly like SCHEMA.md's existing Option C contract.
266
+ function checkGuideVersion(hypoDir) {
267
+ return checkTemplateVersion(hypoDir, 'hypo-guide.md');
268
+ }
269
+
236
270
  // Target-aware: the same core-hook integrity check runs against ~/.claude/hooks/
237
271
  // for the default claude target, and against ~/.codex/hooks/ when --codex is set
238
272
  // The function reads only (apply happens in applyHookFiles).
@@ -479,6 +513,11 @@ const GITIGNORE_REQUIRED_ENTRIES = [
479
513
  pattern: '.cache/',
480
514
  comment: '# Hypomnema runtime cache (page-usage log, session growth metrics)',
481
515
  },
516
+ {
517
+ pattern: '*.lock',
518
+ comment:
519
+ '# Append-lock files (transient <target>.lock; a crashed holder can leave a stale one)',
520
+ },
482
521
  ];
483
522
 
484
523
  function checkGitignore(hypoDir) {
@@ -836,6 +875,15 @@ function writePluginModeMetadata() {
836
875
  return true;
837
876
  }
838
877
 
878
+ // readVersionAtRoot and writeDualSkipProvenance now live in ./lib/pkg-json.mjs
879
+ // (imported above) — writeDualSkipProvenance is deliberately not
880
+ // writePluginModeMetadata(): that function hardcodes THIS npm run's
881
+ // PKG_ROOT/PKG_VERSION and always drops `commands` — both correct for true
882
+ // plugin mode, both wrong for a dualSkip correction, where the identity being
883
+ // written is the OTHER install's. Moving both into lib/pkg-json.mjs (alongside
884
+ // the other pkgJsonPath-taking helpers already there) also makes the writer's
885
+ // TOCTOU refusal directly unit-testable without spawning the whole script.
886
+
839
887
  // ── main ─────────────────────────────────────────────────────────────────────
840
888
 
841
889
  const args = parseArgs(process.argv);
@@ -886,9 +934,41 @@ const managesClaudeCore = !pluginMode && (!dualInstallCoreConflict || args.allow
886
934
  const dualSkip = dualInstallCoreConflict && !args.allowDualInstall;
887
935
 
888
936
  const schema = checkSchemaVersion(args.hypoDir);
937
+ const guide = checkGuideVersion(args.hypoDir);
889
938
  const hooks = checkHookFiles(claudeHooksDir);
890
939
  const settings = checkSettingsJson(claudeSettingsPath, claudeHooksDir);
891
940
  const pkgJson = checkPkgJson();
941
+ // dualSkip provenance self-heal: the registry root the enabled plugin key
942
+ // POSITIVELY resolves to (same lookup init.mjs's resolveDurableRoot uses), or
943
+ // null when the registry is absent/unreadable/corrupt or names no usable entry
944
+ // for the enabled key. Computed unconditionally (fail-open on any uncertainty —
945
+ // see lib/plugin-detect.mjs) so both the --apply write below and the read-only
946
+ // report can agree on the same positively-resolved identity without a second
947
+ // registry read racing the first.
948
+ const dualSkipRegistryRoot = dualSkip
949
+ ? resolveEnabledPluginRoot(
950
+ claudeSettingsPath,
951
+ join(HOME, '.claude', 'plugins', 'installed_plugins.json'),
952
+ )
953
+ : null;
954
+ // The recorded pkgRoot currently on disk, read non-mutatingly (readPkgJsonSafe
955
+ // would rename aside a corrupt file — a write — before this script has decided
956
+ // whether to touch anything).
957
+ const dualSkipRecordedRoot = (() => {
958
+ try {
959
+ const v = JSON.parse(readFileSync(pkgJsonPath(), 'utf-8')).pkgRoot;
960
+ return typeof v === 'string' ? v : null;
961
+ } catch {
962
+ return null;
963
+ }
964
+ })();
965
+ // A CONDITIONAL self-heal, never an unconditional rewrite: correct only when the
966
+ // registry positively resolves a usable root that DIFFERS from what's already
967
+ // recorded. An unresolvable registry (null) must PRESERVE the existing pointer,
968
+ // not blank it or fall back to this npm PKG_ROOT — a null here is "cannot prove
969
+ // a correction is needed", not "there is nothing to preserve".
970
+ const dualSkipWouldCorrect =
971
+ dualSkip && dualSkipRegistryRoot !== null && dualSkipRegistryRoot !== dualSkipRecordedRoot;
892
972
  const commands = checkCommands();
893
973
  const oldHookRefs = checkOldHookNames(claudeSettingsPath);
894
974
  const hypoignore = checkHypoignore(args.hypoDir);
@@ -948,15 +1028,41 @@ const missingSettingsCodex = settingsCodex
948
1028
  const invalidSettingsCodex = settingsCodex
949
1029
  ? settingsCodex.some((s) => s.status === 'invalid-json')
950
1030
  : false;
951
- const schemaDrift = schema.bump !== 'none' && schema.bump !== 'unknown' && schema.bump !== 'ahead';
952
- // Dual-install: when core is skipped, hypo-pkg.json is deliberately left
953
- // pointing at the PLUGIN's package root (preserved identity), so checkPkgJson()
954
- // reports it 'stale' relative to this npm/manual PKG_ROOT. That mismatch is
955
- // INTENTIONAL — `--apply` will not (and must not) rewrite it — so it must not
956
- // count as actionable drift, or the user would be nagged to run --apply forever.
957
- // A genuinely missing/corrupt file (status 'missing') is still surfaced (warning
1031
+ // SCHEMA.md keeps its pre-existing behavior: 'unstamped' is deliberately NOT
1032
+ // counted as drift here (SCHEMA.md is user-owned vocabulary, Option C an
1033
+ // unstamped SCHEMA.md was already the "cannot compare" case before this
1034
+ // classification existed, and no SCHEMA test exercises it as actionable).
1035
+ const schemaDrift =
1036
+ schema.bump !== 'none' &&
1037
+ schema.bump !== 'unknown' &&
1038
+ schema.bump !== 'ahead' &&
1039
+ schema.bump !== 'unstamped';
1040
+ // hypo-guide.md: 'unstamped' DOES count as drift — this is the exact shape of
1041
+ // an already-installed pre-versioning vault, the population this change targets.
1042
+ // Excluding it (as the old unknown-only check did) is the bug this fixes.
1043
+ const guideDrift = guide.bump !== 'none' && guide.bump !== 'unknown' && guide.bump !== 'ahead';
1044
+ // Dual-install: when core is skipped, hypo-pkg.json is normally left pointing
1045
+ // at the PLUGIN's package root (preserved identity), so checkPkgJson() reports
1046
+ // it 'stale' relative to this npm/manual PKG_ROOT. That mismatch is USUALLY
1047
+ // intentional — `--apply` will not rewrite an already-correct plugin pointer
1048
+ // — so it must not count as actionable drift by default, or the user would be
1049
+ // nagged to run --apply forever. The one exception is dualSkipWouldCorrect: an
1050
+ // npm-first divergence where the recorded pointer differs from what the plugin
1051
+ // registry POSITIVELY resolves (e.g. `npm init`, enable the plugin, `npm init`
1052
+ // again — the npm root stays recorded forever otherwise). THAT case IS
1053
+ // actionable — `--apply` corrects it via writeDualSkipProvenance — so it must
1054
+ // count as drift, or the user has no signal to ever run --apply. dualSkipWouldCorrect
1055
+ // is checked FIRST and unconditionally (not gated behind `pkgJson.status !==
1056
+ // 'up-to-date'`): checkPkgJson() compares the recorded pointer only against
1057
+ // THIS run's own PKG_ROOT, so an npm-first divergence where the recorded
1058
+ // pointer happens to equal PKG_ROOT (but the registry resolves a DIFFERENT
1059
+ // plugin root) would read 'up-to-date' and slip past that status check
1060
+ // entirely — leaving a correctable divergence permanently unreported. A
1061
+ // genuinely missing/corrupt file (status 'missing') is still surfaced (warning
958
1062
  // below), because the runtime then cannot resolve its package root at all.
959
- const pkgJsonDrift = pkgJson.status !== 'up-to-date' && !(dualSkip && pkgJson.status === 'stale');
1063
+ const pkgJsonDrift =
1064
+ dualSkipWouldCorrect ||
1065
+ (pkgJson.status !== 'up-to-date' && !(dualSkip && pkgJson.status === 'stale'));
960
1066
  const staleCommands = commands.filter((c) => c.status === 'stale' || c.status === 'missing');
961
1067
  const userModifiedCommands = commands.filter((c) => c.status === 'user-modified');
962
1068
  const orphanedCommands = commands.filter((c) => c.status === 'orphaned');
@@ -968,6 +1074,11 @@ let migrationPath = null;
968
1074
  let appliedHooks = [];
969
1075
  let appliedSettings = [];
970
1076
  let appliedPkgJson = false;
1077
+ // True only once the dualSkip provenance self-heal actually WROTE a correction
1078
+ // this run (a positively-resolved registry root that differed from the
1079
+ // recorded one). Distinct from dualSkipWouldCorrect (a pre-apply preview
1080
+ // computed above), which stays true even in a dry run.
1081
+ let dualSkipCorrected = false;
971
1082
  let appliedHookNameRenames = [];
972
1083
  let appliedHooksCodex = [];
973
1084
  let appliedSettingsCodex = [];
@@ -1054,6 +1165,20 @@ if (args.apply) {
1054
1165
  // user to resolve the dual install.
1055
1166
  if (pkgJson.status === 'missing') {
1056
1167
  appliedPkgJson = writePluginModeMetadata();
1168
+ } else if (dualSkipWouldCorrect) {
1169
+ // npm-first correction: a usable pkgRoot IS already recorded (status
1170
+ // 'stale' or 'up-to-date'), but the registry positively resolves a
1171
+ // DIFFERENT usable root — e.g. `npm init`, then enable the plugin, then
1172
+ // `npm init` again, leaving the npm root recorded forever. Correct the
1173
+ // provenance to the registry's own root/version. When the registry
1174
+ // cannot positively resolve one (dualSkipWouldCorrect is false — null
1175
+ // registry lookup, or it already matches what's recorded), fall through
1176
+ // to preserving the existing pointer untouched; never blank it.
1177
+ // writeDualSkipProvenance can still refuse here (TOCTOU: the registry
1178
+ // root's own package.json became unreadable between resolution and this
1179
+ // write) — dualSkipCorrected only follows an ACTUAL write, never assume it.
1180
+ appliedPkgJson = writeDualSkipProvenance(pkgJsonPath(), dualSkipRegistryRoot);
1181
+ dualSkipCorrected = appliedPkgJson;
1057
1182
  } else {
1058
1183
  appliedPkgJson = false;
1059
1184
  }
@@ -1134,6 +1259,7 @@ const hasDrift =
1134
1259
  (managesClaudeCore && claudeCoreDrift) ||
1135
1260
  pkgJsonDrift ||
1136
1261
  schemaDrift ||
1262
+ guideDrift ||
1137
1263
  hypoignore.status === 'needs-migration' ||
1138
1264
  gitignore.status === 'needs-migration' ||
1139
1265
  extDrift ||
@@ -1150,6 +1276,7 @@ if (args.json) {
1150
1276
  coreManagedBy: managesClaudeCore ? 'self' : pluginMode ? 'plugin' : 'plugin-enabled',
1151
1277
  dualInstallOverride: args.allowDualInstall,
1152
1278
  schema,
1279
+ guide,
1153
1280
  hooks,
1154
1281
  settings,
1155
1282
  pkgJson,
@@ -1249,6 +1376,12 @@ if (schema.bump === 'none') {
1249
1376
  lines.push(
1250
1377
  `⚠ SCHEMA version ${schema.installed} (installed is ahead of package ${schema.current})`,
1251
1378
  );
1379
+ } else if (schema.bump === 'unstamped') {
1380
+ // Not counted as drift for SCHEMA.md (Option C, unchanged) — still surfaced
1381
+ // as advisory-only visibility, same spirit as the pre-existing 'unknown' text.
1382
+ lines.push(
1383
+ `⚠ SCHEMA version installed has no version stamp (pre-versioning copy), package=${schema.current} (cannot compare automatically)`,
1384
+ );
1252
1385
  } else if (schema.bump === 'major') {
1253
1386
  lines.push(
1254
1387
  `✗ SCHEMA version ${schema.installed} → ${schema.current} [MAJOR — --apply writes MIGRATION report; SCHEMA must be merged manually]`,
@@ -1259,6 +1392,33 @@ if (schema.bump === 'none') {
1259
1392
  );
1260
1393
  }
1261
1394
 
1395
+ // hypo-guide.md version stamp. Visibility only — a drift warning,
1396
+ // never a write: --apply must not overwrite an installed hypo-guide.md, since a
1397
+ // user may have customized it (same Option C contract as SCHEMA.md above).
1398
+ if (guide.bump === 'none') {
1399
+ lines.push(`✓ hypo-guide.md v${guide.installed} (up to date)`);
1400
+ } else if (guide.bump === 'unknown') {
1401
+ lines.push(
1402
+ `⚠ hypo-guide.md installed=${guide.installed ?? 'not found (no version stamp)'}, package=${guide.current ?? 'not found'} (cannot compare)`,
1403
+ );
1404
+ } else if (guide.bump === 'ahead') {
1405
+ lines.push(
1406
+ `⚠ hypo-guide.md v${guide.installed} (installed is ahead of package v${guide.current})`,
1407
+ );
1408
+ } else if (guide.bump === 'unstamped') {
1409
+ // The target case: the file exists (this is not a fresh/missing
1410
+ // install) but predates the version stamp entirely. Counted as drift above
1411
+ // (guideDrift) — the message must be actionable, not "cannot compare",
1412
+ // since a pre-versioning copy silently reporting "up to date" was the bug.
1413
+ lines.push(
1414
+ `⚠ hypo-guide.md installed copy has no version stamp (pre-versioning stale copy; package=v${guide.current}) — review templates/hypo-guide.md and update your copy manually; --apply does not overwrite it`,
1415
+ );
1416
+ } else {
1417
+ lines.push(
1418
+ `⚠ hypo-guide.md v${guide.installed} → v${guide.current} [package template changed — review templates/hypo-guide.md and update your copy manually; --apply does not overwrite it]`,
1419
+ );
1420
+ }
1421
+
1262
1422
  // Hook files (target-aware so --codex can mirror the same block).
1263
1423
  function pushHookSummary(hookList, label, targetPath) {
1264
1424
  const colHook = `Hook files${label}`.padEnd(20);
@@ -1318,7 +1478,28 @@ if (managesClaudeCore) {
1318
1478
  if (settingsCodex) pushSettingsSummary(settingsCodex, ' (codex)', invalidSettingsCodex);
1319
1479
 
1320
1480
  // Package metadata
1321
- if (dualSkip && pkgJson.status === 'stale') {
1481
+ if (dualSkip && dualSkipCorrected) {
1482
+ // npm-first correction actually WROTE this run: the pre-apply pkgJson
1483
+ // classification above was computed against the running npm PKG_ROOT, so
1484
+ // without this branch a corrected pointer would still be reported as
1485
+ // "preserved — not rewritten" (or "up to date"/"stale") below, contradicting
1486
+ // what --apply just did. Report the explicit outcome instead, with the
1487
+ // registry package's own version — the identity now on disk.
1488
+ const correctedVersion = readVersionAtRoot(dualSkipRegistryRoot);
1489
+ lines.push(
1490
+ `✓ Package metadata hypo-pkg.json corrected to enabled plugin registry root` +
1491
+ (correctedVersion ? ` (v${correctedVersion})` : ''),
1492
+ );
1493
+ } else if (dualSkip && dualSkipWouldCorrect) {
1494
+ // Same divergence, but this was a dry run (no --apply): preview what
1495
+ // --apply would do rather than claiming the identity is already preserved.
1496
+ const previewVersion = readVersionAtRoot(dualSkipRegistryRoot);
1497
+ lines.push(
1498
+ `⚠ Package metadata hypo-pkg.json points at a stale npm/manual root — \`--apply\` will` +
1499
+ ` correct it to the enabled plugin registry root` +
1500
+ (previewVersion ? ` (v${previewVersion})` : ''),
1501
+ );
1502
+ } else if (dualSkip && pkgJson.status === 'stale') {
1322
1503
  // Dual-install: the 'stale' here is the preserved plugin identity (pkgRoot points at
1323
1504
  // the plugin, not this npm/manual copy). That is intentional — not actionable.
1324
1505
  lines.push(
@@ -1572,6 +1753,7 @@ const totalDrift =
1572
1753
  claudeCoreCount +
1573
1754
  (pkgJsonDrift ? 1 : 0) +
1574
1755
  (schemaDrift ? 1 : 0) +
1756
+ (guideDrift ? 1 : 0) +
1575
1757
  (hypoignore.status === 'needs-migration' ? hypoignore.missing.length : 0) +
1576
1758
  (gitignore.status === 'needs-migration' ? gitignore.missing.length : 0) +
1577
1759
  extCheck.actions.filter(
@@ -11,7 +11,7 @@ When invoked to close a session — via an explicit close signal ("세션 종료
11
11
 
12
12
  ## What this does
13
13
 
14
- - **Close mode**: walks the checklist (session-state, project hot.md, root hot.md, session-log, open-questions(변경 시), log.md) plus a lint step, then writes via `/hypo:crystallize` in `--apply-session-close --payload=<path>` mode which runs the lint gate automatically, **scoped to the files it writes** (debt elsewhere is a non-blocking notice). `--check-session-close` is a read-only dry-run of the **full** PreCompact gate: close files + scoped lint + design-history + feedback projection, sharing one function (`precompactGateStatus`) with the gate. A green check means no gate blocker needs a human fix, so it is the signal to declare the session closed (pass `--transcript-path` to widen the lint scope to this session's edited files exactly as the interactive hook does). It is not a hard guarantee: the live `/compact` can still differ on a context-≥70% prompt, `HYPO_SKIP_GATE`, or a transcript-scoped lint error the check did not see.
14
+ - **Close mode**: walks the checklist (session-state, project hot.md, root hot.md, session-log, open-questions(변경 시), log.md) plus a lint step, then writes via `/hypo:crystallize` in `--apply-session-close --payload=<path> --session-id=<id>` mode, which runs the lint gate automatically, **scoped to the files it writes** (debt elsewhere is a non-blocking notice). **`--session-id` is required.** Before writing a byte, the apply resolves that session's transcript and refuses the whole close, exit 1 and nothing on disk, unless the **user** asked for it (`session-id-required` / `transcript-unresolved` / `no-user-close-signal`). A refusal is the answer, not an obstacle: ask the user, and re-run only if they say yes. `--check-session-close` is a read-only dry-run of the **full** PreCompact gate: close files + scoped lint + design-history + feedback projection, sharing one function (`precompactGateStatus`) with the gate. A green check means no gate blocker needs a human fix, so it is the signal to declare the session closed (pass `--transcript-path` to widen the lint scope to this session's edited files exactly as the interactive hook does). It is not a hard guarantee: the live `/compact` can still differ on a context-≥70% prompt, `HYPO_SKIP_GATE`, or a transcript-scoped lint error the check did not see.
15
15
  - **Synthesis mode**: finds tag clusters (≥ N pages), orphan pages (no outbound `[[wikilinks]]`), and draft / stub pages, then guides consolidation into `pages/syntheses/<topic>.md` with back-links and `index.md` updates.
16
16
 
17
17
  ---
@@ -102,15 +102,52 @@ Once it passes, report each item with ✓:
102
102
  - ✓ session-log entry
103
103
  - ✓ open-questions (or skipped if unchanged)
104
104
  - ✓ log.md entry
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.
105
+ - **marker written?** (required): 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 branches below.
106
106
 
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?"
107
+ If `markerWritten: true`, ask: "Session closed. Would you like to also run knowledge synthesis now, or stop here?"
108
108
 
109
- If `markerWritten: false`, do NOT say "session closed." Branch on `markerSkipReason`:
109
+ **If `ok: false` with an authority `reason`, the close did not happen at all.** No file was written, nothing was committed. Do not report a partial close, and do not write the files by hand to get around it.
110
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.
111
+ - `session-id-required`: `--session-id` was omitted. Pass the main conversation's id and re-run.
112
+ - `transcript-unresolved`: the id resolved no transcript, so it is most likely a background-task or Agent-thread uuid rather than the main conversation's. Get the right one and re-run.
113
+ - `no-user-close-signal`: the transcript is this session's, and the user never asked to close in wording the gate recognizes (e.g. "세션 마무리까지 진행해줘" falls outside the close-signal set). Re-running the same id changes nothing. Confirm intent once with `AskUserQuestion` (header "세션", one option **세션 마무리**); if the user picks it, that answer becomes a recognized close signal, so the same command now applies everything: writes, commit, and marker. If the user declines, the session stays open and nothing is written. Do NOT change the close-signal matcher.
114
+
115
+ **If `ok: false` with `stage: "session-id-mismatch"`,** the payload carries a `sessionId` that is not the `--session-id` you passed, so this file was authored for a different session (a reused or overwritten temp path). Nothing was written. Do not force it: rebuild the payload for THIS session (write it to a session-scoped path, e.g. `/tmp/hypo-session-close-<session-id>.json`) and re-run.
116
+
117
+ If the apply succeeded but `markerWritten: false`, branch on `markerSkipReason` (`compact-gate-not-ok`, `commit-failed: …`, `marker-did-not-land`): surface it verbatim and resolve the underlying blocker before re-running.
118
+
119
+ ### If the close came back `ok: false, stage: "proposal-pending"`
120
+
121
+ A concurrent session wrote one of the pages this close wants to replace, so the close
122
+ withheld the bytes rather than clobber that session's work, and parked them. Nothing is
123
+ lost and nothing is broken. The close is waiting on a decision only the user makes:
124
+ whether to replace the other session's bytes.
125
+
126
+ Do NOT write the pages with the Write tool and mark the session closed. That path skips
127
+ the store entirely and silently destroys the other session's edits. Drive it out properly:
128
+
129
+ 1. **Look at `proposals`.** If the array is EMPTY, no page was parked. The only conflicts
130
+ were append-lock timeouts, which the next close re-reads and re-appends by itself. Just
131
+ re-run the same close command.
132
+ 2. **`hypomnema proposal challenge --session-id <id> --ids <id,...>`** with the ids from
133
+ `proposals`. It re-reads each page from disk and prints the diff between what is there
134
+ now and what this close wants to write.
135
+ 3. **Show the user those diffs** and tell them plainly what would be overwritten. Then give
136
+ them the approval line the command printed, and ask them to type it:
137
+
138
+ apply-proposals <nonce>
139
+
140
+ They have to TYPE it. A click on `AskUserQuestion` does not approve an overwrite (you
141
+ author the option labels, so a click cannot prove they approved this specific write),
142
+ and the command refuses it. If they would rather keep the other session's page, run
143
+ `hypomnema proposal discard <id>` instead and re-run the close.
144
+ 4. **`hypomnema proposal resolve --session-id <id>`** once they have typed it. It verifies
145
+ the approval in the transcript, checks the pages have not moved since they saw the diff,
146
+ and writes them.
147
+ 5. **Re-run the close.** `resolve` writes the pages; it does NOT close the session. The
148
+ re-run skips what already landed and finishes the close.
149
+ 6. **Check `markerWritten` again** before you say the session is closed. It is never
150
+ automatic.
114
151
 
115
152
  ---
116
153
 
@@ -0,0 +1,88 @@
1
+ ---
2
+ description: Structured debate to re-verify a wiki knowledge claim before you trust it, or to harden a hard-to-reverse decision into a durable wiki record. Runs a fixed order (interrogate, then verify against code and docs, then synthesize). Use when hypo:verify flags a page as overdue and you need to actually re-check its claim, when a decision is hard to reverse (architecture, refactor, external API integration) and should leave an ADR, or when the user asks to "debate", "stress-test", or "verify this claim". Skip it for reversible or already-obvious calls.
3
+ ---
4
+
5
+ You are running `/hypo:debate`. It formalizes an informal practice (a two-worker adversarial
6
+ review) into a reproducible fixed order, so the same rigor applies every time.
7
+
8
+ The premise: separate the mind that **generates and interrogates** from the mind that
9
+ **verifies**. Interrogation without verification lets a plausible wrong answer survive;
10
+ verification without interrogation confirms a wrong premise with precision. The order is
11
+ fixed for that reason.
12
+
13
+ The debate always ends in a durable wiki artifact: a refreshed page, a correction, or an
14
+ ADR. A debate that leaves nothing on disk was just a conversation. This is the producer
15
+ reviewer pattern applied to a single claim; for the orchestration vocabulary it belongs to,
16
+ see [references/orchestration-patterns.md](references/orchestration-patterns.md).
17
+
18
+ ## Two uses
19
+
20
+ 1. **Re-verify a wiki claim (primary).** `/hypo:verify` surfaces pages whose `verify_by_date`
21
+ is overdue (or that carry no `verify_by` question at all). This skill is the deep re-check
22
+ behind that prompt: does the page's `verify_by` question still hold against the current
23
+ code and docs? The outcome updates the page exactly as `/hypo:verify` Step 4 does (see
24
+ below), with the debate supplying the evidence.
25
+ 2. **Harden a hard-to-reverse decision.** Architecture choices, refactor direction, external
26
+ API or dependency integration, where the first-pass answer is often wrong (concurrency,
27
+ migrations, security boundaries, distributed state). The decision must land as an ADR or a
28
+ wiki page; if it would not, it is not big enough for a debate.
29
+
30
+ ## When not to use
31
+
32
+ - A change whose diff you can state in one sentence. Just do it.
33
+ - A reversible call, an already-obvious choice, or a one-off lookup. The debate's fixed cost
34
+ exceeds its value.
35
+
36
+ ## Fixed three phases
37
+
38
+ ### Phase 1. Interrogate (do not answer yet)
39
+
40
+ Attack the claim or decision. Build a list of questions, not conclusions.
41
+
42
+ - Surface hidden assumptions: "this rests on X being true. What if X is false?"
43
+ - Stand up at least two alternatives (three including the incumbent). Best and worst case of
44
+ each.
45
+ - List failure modes: how does this decision bite six months from now?
46
+ - Output: a list of **falsifiable claims**. Each must be resolvable to true / false by code,
47
+ docs, or a command ("p95 under 50ms", not "it will be fast"). For a wiki claim, restate the
48
+ page's `verify_by` question in a form you can actually check against the current code or
49
+ docs.
50
+
51
+ ### Phase 2. Verify (against code, docs, and the wiki)
52
+
53
+ Check each Phase 1 claim, one at a time. Do not invent new claims here.
54
+
55
+ - Read the code, find the docs, run the command. Judge each claim true / false / unknown.
56
+ - Unknown is unknown. A zero-result grep is not proof of absence.
57
+ - For a wiki claim, check it against the page's own cited sources and the code or docs it
58
+ describes, not against memory.
59
+ - For genuine independence, hand this phase to a reviewer who did not run the interrogation
60
+ (a fresh-context subagent, or a second person). Self-review of your own interrogation
61
+ breeds confirmation bias.
62
+ - Output: a **per-claim verdict table** (claim, verdict, evidence line or command).
63
+
64
+ ### Phase 3. Synthesize
65
+
66
+ Reconcile Phases 1 and 2 into one outcome, and write it down.
67
+
68
+ - Build the conclusion only from claims that survived verification. For each rejected
69
+ alternative, leave one line on why it fell.
70
+ - Name the residual risk (remaining unknowns, fail-open points) alongside the outcome. Do not
71
+ hide it.
72
+ - Land the durable artifact:
73
+ - Wiki claim: answer the page's `verify_by` question **yes / no / partially**. On yes,
74
+ refresh `last_reviewed` to today and push `verify_by_date` forward. On no or partially,
75
+ edit the page to correct it, then refresh both fields. This is exactly `/hypo:verify`
76
+ Step 4; the debate is what earns the answer.
77
+ - Decision: record it as an ADR (or a wiki page) holding the **decision, rationale, and
78
+ residual risk**. That record, not the discussion, is the deliverable.
79
+
80
+ ## Execution notes
81
+
82
+ - Run the three phases inline in order, or split only Phase 2 out to an independent reviewer.
83
+ That independence contributes the most to decision quality (the verifier's fresh context).
84
+ - Keep the boundary between interrogation (1) and verification (2). If a new alternative
85
+ occurs during verification, note it for Phase 1 and finish verifying the current claims
86
+ before opening a new round. Mixing the two blurs both.
87
+ - Do not accept a reviewer's verdict uncritically. The final call at synthesis stays with the
88
+ person running the debate.
@@ -0,0 +1,83 @@
1
+ # Multi-agent orchestration patterns
2
+
3
+ A shared vocabulary for designing multi-agent work. `/hypo:debate` is one instance of it
4
+ (the producer-reviewer pattern). When you orchestrate several agents around your wiki work,
5
+ name the pattern first, then build.
6
+
7
+ The one selection rule: **does the next stage need a barrier?** If not, pipeline. If it must
8
+ see everything at once, fan-out / fan-in. If several angles look at the same target, expert
9
+ pool. If one agent must grade another's output, producer-reviewer. If one place must hold the
10
+ decision, supervisor. If delegation spawns more delegation, hierarchical.
11
+
12
+ ## 1. Pipeline
13
+
14
+ Each item flows through the stages independently. No barrier between stages, so item A can be
15
+ in stage 3 while item B is still in stage 1. With enough parallel capacity, wall-clock trends
16
+ toward the slowest single-item chain rather than the sum of per-stage worsts (setting aside
17
+ pipeline fill and drain). With limited capacity, items queue and it degrades toward the
18
+ barrier case.
19
+
20
+ - Use when: every item runs the same chain and items do not wait on each other (per-file
21
+ transforms, a find then fix then verify done per item).
22
+ - Mistake: adding a barrier because you think a stage needs all prior results. That is
23
+ fan-out / fan-in, not a pipeline. Do any flatten / map / filter inside a stage.
24
+
25
+ ## 2. Fan-out / fan-in
26
+
27
+ Spread work in parallel (fan-out), then gather all of it at a barrier (fan-in) to merge,
28
+ dedup, or aggregate. Justified only when stage N needs every result of stage N-1 together.
29
+
30
+ - Use when: dedup across the full set before an expensive downstream step, early-exit when
31
+ the total is zero, or a result that must be compared against all the others.
32
+ - Mistake: forgetting the barrier's wait cost. Fast workers idle waiting on slow ones. Use it
33
+ only when you genuinely need all results.
34
+
35
+ ## 3. Expert pool
36
+
37
+ Several agents look at the same target through **different lenses** at once. Each holds a
38
+ different expertise (correctness, security, performance, reproducibility) or a different
39
+ viewpoint, blind to what the others see.
40
+
41
+ - Use when: one search angle cannot find everything, or a defect can break in more than one
42
+ way. The goal is **perspective diversity**, not redundant voting.
43
+ - Mistake: giving N agents the same prompt and calling it an expert pool. That is just
44
+ redundancy. The lenses must actually differ.
45
+
46
+ ## 4. Producer-reviewer
47
+
48
+ A producer makes something and an **independent** reviewer verifies it adversarially. Use it
49
+ when the maker should not grade their own work. Prompt the reviewer to find why it is wrong
50
+ first. `/hypo:debate` is this pattern applied to one claim.
51
+
52
+ - Use when: catching defects in a change, a design, or an artifact from a fresh context.
53
+ - Mistake: handing the reviewer the producer's rationale. The reviewer should see the target,
54
+ not the conclusion, to stay independent. Do not accept the review uncritically; the final
55
+ call stays with the orchestrator.
56
+
57
+ ## 5. Supervisor
58
+
59
+ One orchestrator delegates to workers and integrates the results. The **decision and the
60
+ human gate stay with the orchestrator**; only the heavy execution goes down.
61
+
62
+ - Use when: keeping the main context for decisions while isolating large raw material or
63
+ diffs in subagents. When you delegate, hand over everything the main context knows: the
64
+ goal, the read / write scope, the constraints already confirmed, the verify command, the
65
+ return schema.
66
+ - Mistake: treating a worker's output as evidence. It is a candidate. Trust it blindly and
67
+ you get a phantom backlog.
68
+
69
+ ## 6. Hierarchical delegation
70
+
71
+ A tree of delegation: a higher worker re-delegates to lower ones. Splits a large problem into
72
+ sub-problems handled by depth.
73
+
74
+ - Use when: a breadth or depth one orchestrator cannot hold (a large migration, a broad
75
+ audit).
76
+ - Mistake: allowing unbounded nesting depth. Keep the hierarchy to a controlled one or two
77
+ levels, and do not let a subagent open its own sub-delegation without a reason.
78
+
79
+ ## When not to reach for multi-agent at all
80
+
81
+ If the main context already holds the material and the task finishes faster than the delegation
82
+ overhead, do it inline. Before picking a pattern, ask whether the work is worth splitting. Most
83
+ single lookups are not.
@@ -0,0 +1,10 @@
1
+ # .hyposcanignore — patterns excluded from scan (lint/graph/stats/query/verify/doctor) only
2
+ # One glob per line. Lines starting with # are comments.
3
+ # Matched files are skipped by catalog scans but are still committed and still
4
+ # readable by hooks. For secrets and anything that must never be committed or
5
+ # surfaced by a hook, use .hypoignore instead — this file does NOT block commits.
6
+ # Syntax follows .gitignore glob rules.
7
+
8
+ # Draft / scratch (uncomment to hide from scans without blocking their commit)
9
+ # drafts/
10
+ # scratch/
@@ -91,11 +91,23 @@ Optional fields (add as needed):
91
91
  confidence: high | medium | low | speculative
92
92
  evidence_strength: direct | inferred | hearsay
93
93
  scope: always | project | session
94
+ visibility_scope: shared | machine:<device>
94
95
  source: <slug or URL>
95
96
  verify_by: <question to re-check at next review>
96
97
  verify_by_date: YYYY-MM-DD
97
98
  ```
98
99
 
100
+ `visibility_scope` is a different axis from `scope`: `scope` is memory lifetime,
101
+ `visibility_scope` is where a page may surface. Omitting it means `shared` (the
102
+ implicit default of every pre-existing page), so lookup/query/injection stay
103
+ unchanged. `machine:<device>` hides the page from every machine except the one
104
+ whose `currentDevice()` output equals `<device>`. Copy that exact string from the
105
+ newest `device` value in `.cache/sessions/index.jsonl`, or from a recent
106
+ session-log shard's `device:` frontmatter (both are `currentDevice()` outputs); a
107
+ hand-typed value that does not match will hide the page on its own machine too.
108
+ An empty owner (`machine:`) hides the page everywhere. `agent:<id>` is a reserved
109
+ forward-compat value that is not filtered yet.
110
+
99
111
  ### 3.1. `feedback` type — projection fields
100
112
 
101
113
  Feedback pages are the single source of truth for behavior corrections;
@@ -3,3 +3,8 @@
3
3
 
4
4
  # Runtime cache: sync-state.json contains hostname() and timestamps
5
5
  .cache/
6
+
7
+ # Append-lock files: transient <target>.lock guards around session-log / log.md
8
+ # writes (crystallize apply, deriveRootLogEntries). Normally deleted immediately;
9
+ # a crashed holder can leave a stale one, which must never be committed.
10
+ *.lock
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Hypomnema Config
3
3
  type: config
4
- version: "1.6.2"
4
+ version: "1.7.0"
5
5
  created: YYYY-MM-DD
6
6
  ---
7
7