hypomnema 1.3.0 → 1.3.2

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 (48) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/README.ko.md +12 -10
  4. package/README.md +12 -10
  5. package/commands/crystallize.md +8 -8
  6. package/commands/feedback.md +1 -1
  7. package/commands/resume.md +1 -1
  8. package/commands/upgrade.md +2 -0
  9. package/docs/ARCHITECTURE.md +3 -3
  10. package/docs/CONTRIBUTING.md +2 -2
  11. package/hooks/hypo-auto-minimal-crystallize.mjs +57 -11
  12. package/hooks/hypo-compact-guard.mjs +1 -1
  13. package/hooks/hypo-cwd-change.mjs +1 -1
  14. package/hooks/hypo-first-prompt.mjs +2 -2
  15. package/hooks/hypo-hot-rebuild.mjs +14 -1
  16. package/hooks/hypo-personal-check.mjs +91 -179
  17. package/hooks/hypo-pre-commit.mjs +1 -1
  18. package/hooks/hypo-session-end.mjs +1 -1
  19. package/hooks/hypo-session-start.mjs +26 -19
  20. package/hooks/hypo-shared.mjs +839 -58
  21. package/hooks/hypo-web-fetch-ingest.mjs +2 -2
  22. package/hooks/version-check-fetch.mjs +2 -8
  23. package/hooks/version-check.mjs +18 -0
  24. package/package.json +3 -2
  25. package/scripts/check-tracker-ids.mjs +329 -0
  26. package/scripts/crystallize.mjs +249 -109
  27. package/scripts/doctor.mjs +6 -9
  28. package/scripts/feedback-sync.mjs +1 -1
  29. package/scripts/init.mjs +1 -1
  30. package/scripts/install-git-hooks.mjs +75 -40
  31. package/scripts/lib/check-tracker-ids.mjs +140 -0
  32. package/scripts/lib/design-history-stale.mjs +59 -14
  33. package/scripts/lib/extensions.mjs +4 -4
  34. package/scripts/lib/fix-manifest.mjs +2 -2
  35. package/scripts/lib/fix-status-verify.mjs +5 -4
  36. package/scripts/lib/plugin-detect.mjs +60 -0
  37. package/scripts/lib/project-create.mjs +1 -1
  38. package/scripts/lint.mjs +63 -8
  39. package/scripts/rename.mjs +373 -0
  40. package/scripts/resume.mjs +127 -13
  41. package/scripts/smoke-pack.mjs +23 -2
  42. package/scripts/uninstall.mjs +1 -1
  43. package/scripts/upgrade.mjs +266 -47
  44. package/skills/crystallize/SKILL.md +10 -7
  45. package/templates/SCHEMA.md +2 -2
  46. package/templates/hypo-config.md +2 -2
  47. package/templates/hypo-guide.md +20 -1
  48. package/templates/projects/_template/index.md +1 -1
@@ -16,11 +16,14 @@
16
16
  * --force-extensions Overwrite user-modified / conflicting extension copies (creates .bak)
17
17
  * --codex Mirror to ~/.codex/{hooks,commands,settings.json} — core
18
18
  * hook drift/apply, settings.json registration, the
19
- * wiki-*.mjs → hypo-*.mjs rename migration (fix #48), and
20
- * the user-extensions companion sync (E4 / fix #32).
19
+ * wiki-*.mjs → hypo-*.mjs rename migration, and
20
+ * the user-extensions companion sync (E4).
21
21
  * --json Output results as JSON
22
22
  * --allow-downgrade Override the guard that refuses to overwrite a NEWER
23
23
  * active install with an older package (ADR 0038)
24
+ * --allow-dual-install Override the dual-install guard: register the Claude core
25
+ * surface even though the Hypomnema plugin is also enabled
26
+ * (knowingly accept the double-registration risk)
24
27
  */
25
28
 
26
29
  import {
@@ -47,6 +50,7 @@ import {
47
50
  readFileIfRegular,
48
51
  } from './lib/pkg-json.mjs';
49
52
  import { syncExtensions } from './lib/extensions.mjs';
53
+ import { isHypomnemaPluginEnabled } from './lib/plugin-detect.mjs';
50
54
  import { classifyInstall, downgradeGuardMessage } from '../hooks/version-check.mjs';
51
55
 
52
56
  const HOME = homedir();
@@ -80,6 +84,7 @@ function parseArgs(argv) {
80
84
  forceExtensions: false,
81
85
  codex: false,
82
86
  allowDowngrade: false,
87
+ allowDualInstall: false,
83
88
  };
84
89
  for (const arg of argv.slice(2)) {
85
90
  if (arg.startsWith('--hypo-dir=')) args.hypoDir = expandHome(arg.slice(11));
@@ -89,6 +94,7 @@ function parseArgs(argv) {
89
94
  else if (arg === '--codex') args.codex = true;
90
95
  else if (arg === '--json') args.json = true;
91
96
  else if (arg === '--allow-downgrade') args.allowDowngrade = true;
97
+ else if (arg === '--allow-dual-install') args.allowDualInstall = true;
92
98
  }
93
99
  if (!args.hypoDir) args.hypoDir = resolveHypoRoot();
94
100
  return args;
@@ -406,7 +412,7 @@ function applyHookNameMigration(oldRefs, settingsPath, hooksDir) {
406
412
  if (applied.length > 0) {
407
413
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
408
414
  // Copy renamed hook files to the target hooks dir (~/.claude/hooks or
409
- // ~/.codex/hooks per the caller — fix #48 mirror).
415
+ // ~/.codex/hooks per the caller — codex mirror).
410
416
  for (const [oldName, newName] of Object.entries(HOOK_RENAMES)) {
411
417
  const oldPath = join(hooksDir, oldName);
412
418
  const newPath = join(hooksDir, newName);
@@ -458,7 +464,7 @@ function applyHypoignoreMigration(result) {
458
464
  return appended;
459
465
  }
460
466
 
461
- function writeMigrationReport(hypoDir, fromVersion, toVersion) {
467
+ function writeMigrationReport(hypoDir, fromVersion, toVersion, { pluginMode = false } = {}) {
462
468
  const today = new Date().toISOString().slice(0, 10);
463
469
  const filename = `MIGRATION-v${toVersion}.md`;
464
470
  const dest = join(hypoDir, filename);
@@ -544,9 +550,15 @@ Review the SCHEMA diff and update your wiki pages accordingly.
544
550
 
545
551
  ## Action items
546
552
 
547
- This report was generated during \`/hypo:upgrade --apply\`. Hook files and settings.json
548
- entries were applied by that run (or skipped with a warning if the target was malformed —
549
- see the upgrade output). \`SCHEMA.md\` is intentionally **not** overwritten by upgrade the
553
+ This report was generated during \`/hypo:upgrade --apply\`. ${
554
+ pluginMode
555
+ ? 'You are on a **plugin install**, so the core hook files and settings.json hook ' +
556
+ 'registrations were NOT touched — the Claude Code plugin loader owns them (upgrade the ' +
557
+ 'plugin via `/plugin marketplace update hypomnema` then `/reload-plugins`). Vault ' +
558
+ 'extensions, if any, were still synced.'
559
+ : 'Hook files and settings.json entries were applied by that run (or skipped with a ' +
560
+ 'warning if the target was malformed — see the upgrade output).'
561
+ } \`SCHEMA.md\` is intentionally **not** overwritten by upgrade — the
550
562
  remaining steps are manual:
551
563
 
552
564
  - [ ] Compare your \`SCHEMA.md\` (v${fromVersion}) with the package template (v${toVersion}) and merge changes manually
@@ -749,6 +761,31 @@ function applyCommands(commandResults, force) {
749
761
  return applied;
750
762
  }
751
763
 
764
+ // In plugin mode `applyCommands` is skipped (no command copy), but the
765
+ // runtime still needs hypo-pkg.json to resolve PKG_ROOT for lint/feedback scripts
766
+ // (hooks/hypo-shared.mjs → hypo-personal-check). Write minimal metadata pointing
767
+ // at the plugin's package root, preserving any existing fields (e.g. `extensions`)
768
+ // but DROPPING any prior `commands` map (no commands were copied, so a stale map
769
+ // would falsely assert ownership of ~/.claude/commands/hypo).
770
+ function writePluginModeMetadata() {
771
+ const path = pkgJsonPath();
772
+ // Drop any prior top-level `commands` SHA map: no commands were copied in plugin
773
+ // mode, so keeping a manual install's map would falsely assert ownership of
774
+ // ~/.claude/commands/hypo. Preserve every other field (e.g. `extensions`).
775
+ const { commands: _droppedCommands, ...existing } = readPkgJsonSafe(path) || {};
776
+ let pkgVersion = null;
777
+ try {
778
+ pkgVersion = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf-8')).version;
779
+ } catch {}
780
+ writePkgJsonAtomic(path, {
781
+ ...existing,
782
+ pkgRoot: PKG_ROOT,
783
+ pkgVersion,
784
+ schemaVersion: '2.0',
785
+ });
786
+ return true;
787
+ }
788
+
752
789
  // ── main ─────────────────────────────────────────────────────────────────────
753
790
 
754
791
  const args = parseArgs(process.argv);
@@ -760,6 +797,44 @@ const claudeSettingsPath = join(HOME, '.claude', 'settings.json');
760
797
  const codexHooksDir = join(HOME, '.codex', 'hooks');
761
798
  const codexSettingsPath = join(HOME, '.codex', 'settings.json');
762
799
 
800
+ // When `/hypo:upgrade` runs as the Claude Code PLUGIN, the 15 core hooks
801
+ // and 14 slash commands are provided by the plugin's hooks.json + commands/
802
+ // (auto-wired by Claude Code), NOT copied into ~/.claude/. The manual/npm health
803
+ // check below would then report all of them "missing" and recommend `--apply`,
804
+ // which copies the hooks into ~/.claude/hooks/ and registers 14 settings.json
805
+ // events → Claude Code runs BOTH the plugin hooks.json AND user settings.json, so
806
+ // every hook fires TWICE. The decisive signal: the plugin command runs the
807
+ // PLUGIN's upgrade.mjs, so PKG_ROOT lives under ~/.claude/plugins/. (A manual/npm
808
+ // upgrade.mjs run while the plugin is ALSO enabled is a different failure mode —
809
+ // dual install — handled by the guard below.)
810
+ // Match the Claude plugin cache shape specifically (`~/.claude/plugins/…`), NOT a
811
+ // generic `/plugins/` substring — this flag now GATES install behavior, so a
812
+ // legitimate npm/dev checkout under some unrelated `…/plugins/…` path must not be
813
+ // misclassified and silently stop managing its hooks. (detectChannel's broad
814
+ // `/plugins/` test is fine for the notifier's display-only use, but too loose here.)
815
+ const pluginMode = PKG_ROOT.replace(/\\/g, '/').includes('/.claude/plugins/');
816
+ // Dual install: the OTHER way the same double-registration can happen.
817
+ // Here the MANUAL/npm upgrade.mjs is running (pluginMode=false), but the Hypomnema
818
+ // plugin is ALSO enabled in ~/.claude/settings.json — so the plugin loader already
819
+ // provides the core hooks/commands/settings. A manual/npm `--apply` would copy and
820
+ // register them on top, and every core hook fires twice. The detector is fail-open
821
+ // (see lib/plugin-detect.mjs): a false positive would wrongly alter a legitimate
822
+ // npm-only user's upgrade, so it only fires on an exact `hypo@<mp>: true` (or the
823
+ // legacy `hypomnema@<mp>: true`, matched across the plugin-rename migration window).
824
+ const hypomnemaPluginEnabled = !pluginMode && isHypomnemaPluginEnabled(claudeSettingsPath);
825
+ const dualInstallCoreConflict = hypomnemaPluginEnabled;
826
+ // Surface policy: the Claude core surface (hooks/settings/commands/hook-name
827
+ // migration) is skipped when EITHER the plugin runs this script (pluginMode) OR a
828
+ // manual/npm run detects the plugin is enabled (dualInstallCoreConflict) — unless
829
+ // the user knowingly overrides with --allow-dual-install. The codex core surface
830
+ // (--codex) and vault-defined extensions are NOT plugin-provided, so they stay
831
+ // managed in every case.
832
+ const managesClaudeCore = !pluginMode && (!dualInstallCoreConflict || args.allowDualInstall);
833
+ // dualSkip = core was skipped specifically because of the dual install (not a true
834
+ // plugin-mode run, and not overridden). Drives the warning banner and the metadata
835
+ // preservation below.
836
+ const dualSkip = dualInstallCoreConflict && !args.allowDualInstall;
837
+
763
838
  const schema = checkSchemaVersion(args.hypoDir);
764
839
  const hooks = checkHookFiles(claudeHooksDir);
765
840
  const settings = checkSettingsJson(claudeSettingsPath, claudeHooksDir);
@@ -790,7 +865,7 @@ const extCheck = syncExtensions({
790
865
  force: args.forceExtensions,
791
866
  });
792
867
 
793
- // E4 (fix #32): --codex mirrors the extensions sync into ~/.codex (hooks + commands
868
+ // E4: --codex mirrors the extensions sync into ~/.codex (hooks + commands
794
869
  // only; skills/agents skipped with a notice). The per-target SHA map lives in the
795
870
  // same ~/.claude/hypo-pkg.json under extensions.codex, so pkgPath is unchanged.
796
871
  const extCodexSettingsPath = codexSettingsPath;
@@ -823,7 +898,14 @@ const invalidSettingsCodex = settingsCodex
823
898
  ? settingsCodex.some((s) => s.status === 'invalid-json')
824
899
  : false;
825
900
  const schemaDrift = schema.bump !== 'none' && schema.bump !== 'unknown' && schema.bump !== 'ahead';
826
- const pkgJsonDrift = pkgJson.status !== 'up-to-date';
901
+ // Dual-install: when core is skipped, hypo-pkg.json is deliberately left
902
+ // pointing at the PLUGIN's package root (preserved identity), so checkPkgJson()
903
+ // reports it 'stale' relative to this npm/manual PKG_ROOT. That mismatch is
904
+ // INTENTIONAL — `--apply` will not (and must not) rewrite it — so it must not
905
+ // count as actionable drift, or the user would be nagged to run --apply forever.
906
+ // A genuinely missing/corrupt file (status 'missing') is still surfaced (warning
907
+ // below), because the runtime then cannot resolve its package root at all.
908
+ const pkgJsonDrift = pkgJson.status !== 'up-to-date' && !(dualSkip && pkgJson.status === 'stale');
827
909
  const staleCommands = commands.filter((c) => c.status === 'stale' || c.status === 'missing');
828
910
  const userModifiedCommands = commands.filter((c) => c.status === 'user-modified');
829
911
  const orphanedCommands = commands.filter((c) => c.status === 'orphaned');
@@ -871,21 +953,59 @@ if (args.apply) {
871
953
  process.exit(2);
872
954
  }
873
955
  }
874
- if (oldHookRefs.length > 0) {
875
- appliedHookNameRenames = applyHookNameMigration(
876
- oldHookRefs,
877
- claudeSettingsPath,
878
- claudeHooksDir,
879
- );
880
- }
956
+ // Migration report is vault-side (writes into the Hypomnema root) and applies
957
+ // in both install models.
881
958
  if (schema.bump === 'major' && schema.installed && schema.current && existsSync(args.hypoDir)) {
882
- migrationPath = writeMigrationReport(args.hypoDir, schema.installed, schema.current);
959
+ migrationPath = writeMigrationReport(args.hypoDir, schema.installed, schema.current, {
960
+ // Use the core-skipped predicate, not raw pluginMode: in a dual-install skip
961
+ // the core surface is plugin-owned too, so the report must not claim the
962
+ // core hooks/settings were applied.
963
+ pluginMode: !managesClaudeCore,
964
+ });
965
+ }
966
+ if (managesClaudeCore) {
967
+ if (oldHookRefs.length > 0) {
968
+ appliedHookNameRenames = applyHookNameMigration(
969
+ oldHookRefs,
970
+ claudeSettingsPath,
971
+ claudeHooksDir,
972
+ );
973
+ }
974
+ appliedHooks = applyHookFiles(hooks, claudeHooksDir);
975
+ appliedSettings = applySettingsJson(settings, claudeSettingsPath);
976
+ // applyCommands handles the single atomic hypo-pkg.json write (pkgRoot, version, schema, commands map)
977
+ appliedCommands = applyCommands(commands, args.forceCommands);
978
+ appliedPkgJson = true;
979
+ } else if (pluginMode) {
980
+ // Plugin mode: the plugin loader owns the core hooks/commands and
981
+ // settings.json wiring — copying them here would double-register. Skip those,
982
+ // but STILL write minimal package metadata so the runtime can resolve PKG_ROOT
983
+ // for lint/feedback scripts (hooks/hypo-shared.mjs → hypo-personal-check). The
984
+ // commands SHA map is intentionally omitted (no command copy happened). PKG_ROOT
985
+ // is the plugin's own path here, so this metadata is authoritative.
986
+ appliedPkgJson = writePluginModeMetadata();
987
+ } else {
988
+ // Dual-install skip: a manual/npm run while the plugin is enabled. We
989
+ // skip the core surface (the plugin owns it), but — unlike true plugin mode —
990
+ // PKG_ROOT here is the npm/manual path while the ACTIVE runtime hooks are the
991
+ // PLUGIN's. Rewriting a VALID hypo-pkg.json.pkgRoot to this npm path would
992
+ // mis-point the plugin runtime's lint/feedback resolution, so we PRESERVE an
993
+ // existing plugin-written identity (pkgJson.status 'stale'/'up-to-date' both
994
+ // mean a usable pkgRoot is already on disk) and do not touch it.
995
+ //
996
+ // If the metadata is MISSING or corrupt (status 'missing'; corrupt files are
997
+ // renamed to *.corrupt-*.json by readPkgJson and then read as absent), there is
998
+ // no plugin identity to preserve. Write minimal fallback metadata pointing at
999
+ // this (same-version) npm copy so the plugin runtime can resolve a package root
1000
+ // at all — strictly better than the pkgRoot-less file extension sync would
1001
+ // otherwise create, or no file at all. The dual-install banner still tells the
1002
+ // user to resolve the dual install.
1003
+ if (pkgJson.status === 'missing') {
1004
+ appliedPkgJson = writePluginModeMetadata();
1005
+ } else {
1006
+ appliedPkgJson = false;
1007
+ }
883
1008
  }
884
- appliedHooks = applyHookFiles(hooks, claudeHooksDir);
885
- appliedSettings = applySettingsJson(settings, claudeSettingsPath);
886
- // applyCommands handles the single atomic hypo-pkg.json write (pkgRoot, version, schema, commands map)
887
- appliedCommands = applyCommands(commands, args.forceCommands);
888
- appliedPkgJson = true;
889
1009
  appliedHypoignore = applyHypoignoreMigration(hypoignore);
890
1010
  // codex core hooks + settings + wiki-*→hypo-* rename mirror. Same order
891
1011
  // as the claude side (rename first so subsequent hook copy can find renamed targets).
@@ -910,7 +1030,7 @@ if (args.apply) {
910
1030
  apply: true,
911
1031
  force: args.forceExtensions,
912
1032
  });
913
- // E4 (fix #32): codex apply runs AFTER the claude apply so it reads the freshly
1033
+ // E4: codex apply runs AFTER the claude apply so it reads the freshly
914
1034
  // written hypo-pkg.json and merges extensions.codex alongside extensions.claude
915
1035
  // (the per-target spread in syncExtensions preserves the other target's map).
916
1036
  if (args.codex) {
@@ -939,17 +1059,28 @@ const codexCoreDrift =
939
1059
  invalidSettingsCodex ||
940
1060
  (oldHookRefsCodex?.length ?? 0) > 0);
941
1061
 
942
- const hasDrift =
1062
+ // Claude core-surface drift (hooks/settings/commands/rename/metadata). In plugin
1063
+ // mode these are plugin-managed, so they must NOT count as drift — otherwise the
1064
+ // report nags "N items need updating" and recommends a double-registering --apply.
1065
+ // Plugin-provided surface (hooks/settings/commands/rename) — excluded from drift
1066
+ // in plugin mode. pkgJsonDrift is intentionally NOT here: hypo-pkg.json is written
1067
+ // in BOTH install models (plugin mode writes minimal metadata so the runtime can
1068
+ // resolve PKG_ROOT for lint/feedback), so a missing/stale metadata file should
1069
+ // still prompt a (safe, metadata-only) --apply.
1070
+ const claudeCoreDrift =
943
1071
  staleHooks.length > 0 ||
944
1072
  missingSettings.length > 0 ||
945
- schemaDrift ||
946
1073
  invalidSettings ||
947
- pkgJsonDrift ||
948
1074
  oldHookRefs.length > 0 ||
949
1075
  staleCommands.length > 0 ||
950
1076
  userModifiedCommands.length > 0 ||
951
1077
  orphanedCommands.length > 0 ||
952
- nonRegularCommands.length > 0 ||
1078
+ nonRegularCommands.length > 0;
1079
+
1080
+ const hasDrift =
1081
+ (managesClaudeCore && claudeCoreDrift) ||
1082
+ pkgJsonDrift ||
1083
+ schemaDrift ||
953
1084
  hypoignore.status === 'needs-migration' ||
954
1085
  extDrift ||
955
1086
  codexCoreDrift;
@@ -958,6 +1089,12 @@ if (args.json) {
958
1089
  console.log(
959
1090
  JSON.stringify(
960
1091
  {
1092
+ pluginMode,
1093
+ // Dual-install signals.
1094
+ hypomnemaPluginEnabled,
1095
+ dualInstallCoreConflict,
1096
+ coreManagedBy: managesClaudeCore ? 'self' : pluginMode ? 'plugin' : 'plugin-enabled',
1097
+ dualInstallOverride: args.allowDualInstall,
961
1098
  schema,
962
1099
  hooks,
963
1100
  settings,
@@ -1002,6 +1139,49 @@ if (args.json) {
1002
1139
  // Human-readable report
1003
1140
  const lines = [];
1004
1141
 
1142
+ // Lead with the plugin-mode banner so the user understands why the core
1143
+ // hook/command/settings sections read "managed by plugin" and that `--apply` will
1144
+ // NOT touch them (only vault-side migrations + package metadata).
1145
+ if (pluginMode) {
1146
+ lines.push(
1147
+ 'ℹ Plugin install detected — Hypomnema is loaded via the Claude Code plugin.',
1148
+ ' Core hooks, slash commands, and settings.json wiring are provided by the',
1149
+ ' plugin loader, so `/hypo:upgrade` does NOT manage them (and `--apply` will',
1150
+ ' not copy/register them — that would double-register every hook).',
1151
+ ' → To upgrade the plugin: `/plugin marketplace update hypomnema` then `/reload-plugins`.',
1152
+ ' → `/hypo:upgrade --apply` here applies vault-side migrations (SCHEMA,',
1153
+ ' .hypoignore), refreshes package metadata, and still syncs any vault',
1154
+ ' extensions — but does NOT install the core hooks/commands/settings.',
1155
+ '',
1156
+ );
1157
+ }
1158
+
1159
+ // Dual install: a manual/npm upgrade.mjs is running while the Hypomnema plugin is ALSO
1160
+ // enabled — a dual install. Lead with a loud banner: the core surface is owned by
1161
+ // the plugin and is intentionally skipped, so `--apply` will not double-register.
1162
+ if (dualSkip) {
1163
+ lines.push(
1164
+ '⚠ Dual install detected — you are running the MANUAL/npm `upgrade.mjs`, but the',
1165
+ ' Hypomnema plugin is ALSO enabled in ~/.claude/settings.json. The plugin loader',
1166
+ ' already provides the core hooks, slash commands, and settings.json wiring, so',
1167
+ ' this run does NOT copy/register them (doing so would double-register every hook).',
1168
+ ' → Recommended: pick ONE install. To keep the plugin, remove the npm/manual copy',
1169
+ ' (`npm uninstall -g hypomnema`) and upgrade via `/plugin marketplace update',
1170
+ ' hypomnema` + `/reload-plugins`. Vault extensions + codex (if any) are still synced.',
1171
+ ' → To register the core surface here anyway (knowingly accept the double-register',
1172
+ ' risk), re-run with `--allow-dual-install`.',
1173
+ '',
1174
+ );
1175
+ } else if (dualInstallCoreConflict && args.allowDualInstall) {
1176
+ // Override path: the user forced core registration despite the enabled plugin.
1177
+ lines.push(
1178
+ '⚠ Dual install — `--allow-dual-install` set: registering the Claude core surface',
1179
+ ' even though the Hypomnema plugin is enabled. Every core hook may now fire TWICE',
1180
+ ' (plugin loader + ~/.claude registration) until one install is removed.',
1181
+ '',
1182
+ );
1183
+ }
1184
+
1005
1185
  // Schema version
1006
1186
  if (schema.bump === 'none') {
1007
1187
  lines.push(`✓ SCHEMA version ${schema.installed} (up to date)`);
@@ -1023,7 +1203,7 @@ if (schema.bump === 'none') {
1023
1203
  );
1024
1204
  }
1025
1205
 
1026
- // Hook files (target-aware so --codex can mirror the same block; fix #48).
1206
+ // Hook files (target-aware so --codex can mirror the same block).
1027
1207
  function pushHookSummary(hookList, label, targetPath) {
1028
1208
  const colHook = `Hook files${label}`.padEnd(20);
1029
1209
  const up = hookList.filter((h) => h.status === 'up-to-date').length;
@@ -1047,10 +1227,16 @@ function pushHookSummary(hookList, label, targetPath) {
1047
1227
  }
1048
1228
  }
1049
1229
  }
1050
- pushHookSummary(hooks, '', '~/.claude/hooks/');
1230
+ if (managesClaudeCore) {
1231
+ pushHookSummary(hooks, '', '~/.claude/hooks/');
1232
+ } else {
1233
+ lines.push(
1234
+ '✓ Hook files provided by the plugin loader (not managed in ~/.claude/hooks/)',
1235
+ );
1236
+ }
1051
1237
  if (hooksCodex) pushHookSummary(hooksCodex, ' (codex)', '~/.codex/hooks/');
1052
1238
 
1053
- // settings.json registrations (target-aware mirror; fix #48).
1239
+ // settings.json registrations (target-aware mirror).
1054
1240
  function pushSettingsSummary(sList, label, invalidFlag) {
1055
1241
  const colS = `settings.json${label}`.padEnd(20);
1056
1242
  const reg = sList.filter((s) => s.status === 'registered').length;
@@ -1066,11 +1252,33 @@ function pushSettingsSummary(sList, label, invalidFlag) {
1066
1252
  }
1067
1253
  }
1068
1254
  }
1069
- pushSettingsSummary(settings, '', invalidSettings);
1255
+ if (managesClaudeCore) {
1256
+ pushSettingsSummary(settings, '', invalidSettings);
1257
+ } else {
1258
+ lines.push(
1259
+ '✓ settings.json hook wiring provided by the plugin (no ~/.claude registration)',
1260
+ );
1261
+ }
1070
1262
  if (settingsCodex) pushSettingsSummary(settingsCodex, ' (codex)', invalidSettingsCodex);
1071
1263
 
1072
1264
  // Package metadata
1073
- if (pkgJson.status === 'up-to-date') {
1265
+ if (dualSkip && pkgJson.status === 'stale') {
1266
+ // Dual-install: the 'stale' here is the preserved plugin identity (pkgRoot points at
1267
+ // the plugin, not this npm/manual copy). That is intentional — not actionable.
1268
+ lines.push(
1269
+ `✓ Package metadata hypo-pkg.json plugin-owned (preserved — not rewritten in a dual install)`,
1270
+ );
1271
+ } else if (dualSkip && pkgJson.status === 'missing') {
1272
+ // Missing/corrupt in a dual install: there is no plugin identity to preserve, so
1273
+ // `--apply` writes minimal fallback metadata (pointing at this same-version npm
1274
+ // copy) — enough for the plugin runtime to resolve its scripts. The real fix is
1275
+ // still to resolve the dual install.
1276
+ lines.push(
1277
+ `⚠ Package metadata hypo-pkg.json missing/unreadable — \`--apply\` writes fallback metadata`,
1278
+ ` for this npm copy so the plugin runtime can resolve its scripts.`,
1279
+ ` Better: resolve the dual install (remove the npm/manual copy).`,
1280
+ );
1281
+ } else if (pkgJson.status === 'up-to-date') {
1074
1282
  lines.push(`✓ Package metadata hypo-pkg.json up to date`);
1075
1283
  } else if (pkgJson.status === 'stale') {
1076
1284
  lines.push(
@@ -1087,7 +1295,9 @@ const cmdMissCount = commands.filter((c) => c.status === 'missing').length;
1087
1295
  const cmdUserCount = userModifiedCommands.length;
1088
1296
  const cmdOrphanCount = orphanedCommands.length;
1089
1297
  const cmdNonRegCount = nonRegularCommands.length;
1090
- if (commands.length === 0) {
1298
+ if (!managesClaudeCore) {
1299
+ lines.push('✓ Slash commands provided by the plugin loader (not ~/.claude/commands/hypo/)');
1300
+ } else if (commands.length === 0) {
1091
1301
  lines.push(`⚠ Slash commands package commands/ is empty`);
1092
1302
  } else if (
1093
1303
  cmdStaleCount === 0 &&
@@ -1134,7 +1344,10 @@ function pushHookNameSummary(refs, label) {
1134
1344
  lines.push(`✓ ${colN}All hook references use current hypo-*.mjs names`);
1135
1345
  }
1136
1346
  }
1137
- pushHookNameSummary(oldHookRefs, '');
1347
+ // In plugin mode the Claude settings.json is plugin-owned and --apply skips the
1348
+ // rename migration, so do not print a "run --apply to rename" instruction it will
1349
+ // not honor. (codex hook-name migration is unaffected by pluginMode.)
1350
+ if (managesClaudeCore) pushHookNameSummary(oldHookRefs, '');
1138
1351
  if (oldHookRefsCodex) pushHookNameSummary(oldHookRefsCodex, ' (codex)');
1139
1352
 
1140
1353
  // .hypoignore migration (ensure required runtime patterns are present)
@@ -1174,7 +1387,7 @@ function pushExtSummary(check, label) {
1174
1387
  for (const c of check.conflicts) lines.push(` ✗ ${c.file} [${c.action} — left untouched]`);
1175
1388
  for (const d of check.drifts) lines.push(` ⚠ ${d.file} [drift — left untouched]`);
1176
1389
  }
1177
- // E3 (fix #31): a hard conflict blocks install (exit 1, even under --apply); drift is
1390
+ // E3: a hard conflict blocks install (exit 1, even under --apply); drift is
1178
1391
  // resolvable advisory. Emit the spec'd WIKI messages so the user knows the recovery.
1179
1392
  if (nConflicts > 0) {
1180
1393
  lines.push(' [WIKI: existing file conflicts. Backup and retry, or use --force-extensions]');
@@ -1269,26 +1482,32 @@ pushAppliedExt(appliedExtensionsCodex, ' (codex)');
1269
1482
 
1270
1483
  // Summary
1271
1484
  lines.push('');
1485
+ // Claude core-surface item count — zeroed in plugin mode (plugin-managed), so the
1486
+ // summary never reads "N items need updating" for hooks/settings/commands.
1487
+ const claudeCoreCount = managesClaudeCore
1488
+ ? staleHooks.length +
1489
+ missingSettings.length +
1490
+ (invalidSettings ? 1 : 0) +
1491
+ oldHookRefs.length +
1492
+ staleCommands.length +
1493
+ userModifiedCommands.length +
1494
+ orphanedCommands.length +
1495
+ nonRegularCommands.length
1496
+ : 0;
1497
+
1272
1498
  const totalDrift =
1273
- staleHooks.length +
1274
- missingSettings.length +
1275
- (schemaDrift ? 1 : 0) +
1276
- (invalidSettings ? 1 : 0) +
1499
+ claudeCoreCount +
1277
1500
  (pkgJsonDrift ? 1 : 0) +
1278
- oldHookRefs.length +
1279
- staleCommands.length +
1280
- userModifiedCommands.length +
1281
- orphanedCommands.length +
1282
- nonRegularCommands.length +
1501
+ (schemaDrift ? 1 : 0) +
1283
1502
  (hypoignore.status === 'needs-migration' ? hypoignore.missing.length : 0) +
1284
1503
  extCheck.actions.filter(
1285
1504
  (a) => a.action === 'create' || a.action === 'update' || a.action === 'force-update',
1286
1505
  ).length +
1287
- // E3 (fix #31): unresolved drift/conflict is pending work too — without these the
1506
+ // E3: unresolved drift/conflict is pending work too — without these the
1288
1507
  // summary printed "up to date" while the exit code was 1.
1289
1508
  extCheck.conflicts.length +
1290
1509
  extCheck.drifts.length +
1291
- // E4 (fix #32): codex-target pending work counts identically (same message/exit
1510
+ // E4: codex-target pending work counts identically (same message/exit
1292
1511
  // consistency the E3 review caught — a codex conflict must not read "up to date").
1293
1512
  (extCheckCodex
1294
1513
  ? extCheckCodex.actions.filter(
@@ -1329,7 +1548,7 @@ if (totalDrift === 0) {
1329
1548
 
1330
1549
  console.log(lines.join('\n'));
1331
1550
 
1332
- // E3 (fix #31): a hard extension conflict blocks even under --apply (unlike ordinary
1551
+ // E3: a hard extension conflict blocks even under --apply (unlike ordinary
1333
1552
  // drift, which only fails check mode). --force-extensions clears the resolvable
1334
1553
  // cases; an unfollowable symlink/non-regular dest still counts and stays exit 1.
1335
1554
  const extBlocked = extCheck.conflicts.length > 0 || (extCheckCodex?.conflicts.length ?? 0) > 0;
@@ -11,7 +11,7 @@ When invoked at the end of a session (or with phrases like "세션 종료", "wra
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 `crystallize.mjs --apply-session-close --payload=<path>` — which runs the lint gate automatically, **scoped to the files it writes** (debt elsewhere is a non-blocking notice). `--check-session-close` remains a read-only probe (freshness only). The PreCompact gate runs the same scoped lint, judging the session on the files it touched.
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 `crystallize.mjs --apply-session-close --payload=<path>` — 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 (ADR 0046) — 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
  ---
@@ -50,8 +50,8 @@ If `/hypo:crystallize` was invoked as a session-close action, run through this c
50
50
  Surface each of these four to the user first. Every one is **advisory** (ADR 0029 identity guard): the user confirms or declines, and none performs an automatic action, writes a file on its own, or bypasses the mandatory gate.
51
51
 
52
52
  - **Trivial-session check (#44)** — Was this session trivial (a single bug fix, a single-file edit, or Q&A with no durable artifact)? If so, recommend skipping session-close: *"이 세션은 trivial해 보입니다 — session-close를 건너뛸까요?"* A trivial skip is a recommendation, **not** a bypass: it must not mark the session closed, must not run `--mark-session-closed`, and must not claim `/compact` can pass. Any real close still requires all 5 mandatory files.
53
- - **ADR-candidate check (#41)** — Did this session make an architectural or design decision (a new pattern, a tradeoff, a convention)? If yes, ask whether it warrants an ADR and capture that intent in the session-log entry. If nothing rose to ADR level, record `ADR 없음 — <one-line reason>` in that same session-log entry. **Never auto-write an ADR file** — the session-log note is the only action here.
54
- - **design-history staleness check (#42)** — If `projects/<name>/design-history.md` exists and this session changed design decisions it does not yet reflect, recommend updating it (W8 lint flags this mechanically; an active-project W8 can also block at PreCompact). If the file does not exist, skip silently — do **not** create it just for this check. Never auto-update it.
53
+ - **ADR-candidate check (#41)** — Did this session make an architectural or design decision (a new pattern, a tradeoff, a convention)? If yes, ask whether it warrants an ADR and capture that intent in the session-log entry. If nothing rose to ADR level, you may record the literal marker `ADR 없음 — <one-line reason>` in that same session-log entry — but gate it on #42's bar, not this one: the marker is machine-read and W8 treats a session-log entry carrying `ADR 없음` (and no ADR reference) as a *no-design* session, excluding it from the design-history staleness check. So write `ADR 없음` only when the session had no design change at all. If it had a sub-ADR design shift (background / tradeoff / differentiation), append to design-history (#42a) instead — writing the marker there would suppress the W8 nudge that shift needs. **Never auto-write an ADR file** — the session-log note is the only action here.
54
+ - **design-history staleness check (#42)** — Two branches, so a stale W8 never blocks a clean close: (a) if this session changed design decisions that `projects/<name>/design-history.md` does not yet reflect including background / tradeoff / differentiation shifts that are below ADR level but still belong in the ledger — recommend appending to it now (W8 flags this mechanically; an active-project W8 hard-blocks at PreCompact — append before you commit, not after the gate fires). (b) only if this session made **no** design change at all does the `ADR 없음` marker from #41 exempt the entry from W8 — do **not** touch design-history. Caution: `ADR 없음` means "no design change," which is a stricter bar than "no ADR-level decision." A session with a sub-ADR design shift should take branch (a) and append; writing `ADR 없음` there would suppress the W8 nudge it actually needs. If the file does not exist, skip silently — do **not** create it just for this check. Never auto-update it.
55
55
  - **Ingest check (#43)** — Did this session consume trustworthy external knowledge (a fetched URL, official docs, or code you verified directly)? If so, recommend running `/hypo:ingest` to capture it under `sources/`. Proceed only on the user's confirmation.
56
56
 
57
57
  When uncertain, surface the question rather than skip it. None of the four blocks the close or writes on its own.
@@ -59,7 +59,7 @@ When uncertain, surface the question rather than skip it. None of the four block
59
59
  1. **session-state.md** — update `projects/<name>/session-state.md` with the next tasks list (what to tackle first next time).
60
60
  2. **hot.md (project)** — update `projects/<name>/hot.md` with a session snapshot: what changed and decisions made. Keep under 500 words. Do not put next-step tasks here; those belong in session-state.md.
61
61
  3. **hot.md (root)** — update `<wiki-root>/hot.md` active-projects pointer table: set the `Last Session` date for this project to today.
62
- 4. **session-log** — append a session entry to `projects/<name>/session-log/YYYY-MM.md` (create the file if it does not exist for this month).
62
+ 4. **session-log** — append a session entry to `projects/<name>/session-log/YYYY-MM-DD.md` (daily shard; the apply path creates today's file with seeded frontmatter if it does not exist yet).
63
63
  5. **open-questions** — only if `pages/open-questions.md` exists and questions were raised or resolved this session: move resolved ones out; add newly raised ones. Skip if unchanged.
64
64
  6. **log.md** — append a `session` entry to `<wiki-root>/log.md`.
65
65
 
@@ -69,9 +69,12 @@ After completing the checklist, verify it before reporting:
69
69
  node <package-root>/scripts/crystallize.mjs --check-session-close [--hypo-dir="<path>"]
70
70
  ```
71
71
 
72
- This runs the same strict check as the PreCompact hard gate (fix #17). Fix any
73
- file reported `missing` or `stale` and re-run until it passes — otherwise
74
- `/compact` will be blocked.
72
+ This runs the **full** PreCompact gate via the shared `precompactGateStatus`
73
+ (ADR 0046): close files (`missing` / `stale`) plus lint blockers, stale
74
+ design-history, and feedback projection over-cap/conflict. Fix every `✗` it
75
+ reports and re-run until it prints **"Compact-ready"** — that is the signal the
76
+ session is closed. A close-files-only pass is not enough; the real `/compact`
77
+ also blocks on those other checks.
75
78
 
76
79
  Once it passes, report each item with ✓ and ask: "Session closed. Would you like to also run knowledge synthesis now, or stop here?"
77
80
 
@@ -30,7 +30,7 @@ Read this before any wiki operation (ingest / query / lint).
30
30
  | `weekly-journal` | `journal/weekly/YYYY-Www.md` | Weekly session/ingest summary | append-only |
31
31
  | `prd` | `projects/*/prd.md` | Project purpose, success criteria, constraints | mutable |
32
32
  | `adr` | `projects/*/decisions/NNNN-*.md` | Architecture decision records | immutable |
33
- | `session-log` | `projects/*/session-log/YYYY-MM.md` | Chronological session narrative (monthly) | append-only |
33
+ | `session-log` | `projects/*/session-log/YYYY-MM-DD.md` | Chronological session narrative (daily shard) | append-only |
34
34
  | `session-state` | `projects/*/session-state.md` | Next-session handoff — overwritten each close | overwrite |
35
35
  | `project-index` | `projects/*/index.md` | Project overview + progress checklist | mutable |
36
36
  | `open-questions` | `pages/open-questions.md` | Unresolved question queue | append+resolve |
@@ -54,7 +54,7 @@ Files responsible for session continuity — separate from the type taxonomy abo
54
54
  | `hot.md` (root) | Active project pointer table + last session date | Pointers only. No session content. |
55
55
  | `projects/<name>/hot.md` | Last session snapshot — "what was done" | 500-word cap. No next-tasks. Overwrite each close. |
56
56
  | `projects/<name>/session-state.md` | Next-session handoff — "what to do next" | Overwrite each close. Read at session start. |
57
- | `projects/<name>/session-log/YYYY-MM.md` | Append-only narrative timeline (monthly) | No edits to existing entries. |
57
+ | `projects/<name>/session-log/YYYY-MM-DD.md` | Append-only narrative timeline (daily shard; legacy `YYYY-MM.md` still read) | No edits to existing entries. |
58
58
  | `pages/open-questions.md` | Cross-project unresolved question queue | Append + mark resolved. |
59
59
 
60
60
  **Operations**:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Hypomnema Config
3
3
  type: config
4
- version: "1.3.0"
4
+ version: "1.3.2"
5
5
  created: YYYY-MM-DD
6
6
  ---
7
7
 
@@ -36,6 +36,6 @@ HYPO_SKIP_GATE=1
36
36
  │ └── <name>/
37
37
  │ ├── index.md ← project overview (working_dir: field)
38
38
  │ ├── hot.md ← project-scoped session snapshot
39
- │ └── session-log/ ← monthly logs (YYYY-MM.md)
39
+ │ └── session-log/ ← daily shards (YYYY-MM-DD.md)
40
40
  └── sources/ ← raw ingested sources (read-only)
41
41
  ```
@@ -78,12 +78,31 @@ Ask: *"이 작업이 마무리되었나요? 세션을 정리(crystallize)할까
78
78
 
79
79
  1. Update `projects/<name>/session-state.md` (next tasks, overwrite)
80
80
  2. Update `projects/<name>/hot.md` (what was done, ≤500 words, overwrite)
81
- 3. Append to `projects/<name>/session-log/YYYY-MM.md` (narrative entry, append-only)
81
+ 3. Append to `projects/<name>/session-log/YYYY-MM-DD.md` (daily shard, narrative entry, append-only)
82
82
  4. Update root `hot.md` pointer table + date
83
83
  5. Run `scripts/lint.mjs` and fix errors in files **you** touched — debt in other
84
84
  projects / shared pages you did not author is reported as a non-blocking
85
85
  notice, not a gate. (The documented `crystallize.mjs --apply-session-close`
86
86
  path runs this lint automatically, scoped to the files it writes.)
87
+ 6. Verify with `scripts/crystallize.mjs --check-session-close`: a dry-run of the
88
+ **full** PreCompact gate (close files + lint + design-history + feedback
89
+ projection, ADR 0046), sharing one function with the gate. Only declare the
90
+ session closed once it prints **"Compact-ready"**. A "close files updated"
91
+ check alone is not enough — the real `/compact` gate also blocks on a lint
92
+ error in a close file or a feedback projection over-cap. (Not a hard
93
+ guarantee: the live gate can still differ on a context-≥70% prompt,
94
+ `HYPO_SKIP_GATE`, or a transcript-scoped lint error — pass `--transcript-path`
95
+ to include the last.) Pass `--session-id=<id>` to also see `marker_present`
96
+ (step 7).
97
+ 7. Record the session-closed marker (ADR 0047). The Stop hook blocks until this
98
+ session's per-session marker exists, and a hand-edit close (writing the files
99
+ directly + committing) never writes it; the marker is written only by the
100
+ crystallize writer, never by the hook (ADR 0022 bypass guard). Normal path:
101
+ close via `crystallize.mjs --apply-session-close --session-id=<id> --transcript-path=<path>`,
102
+ which writes the marker once the gate is green. Hand-edit recovery: after
103
+ committing the files, run `crystallize.mjs --mark-session-closed --session-id=<id> --transcript-path=<path>`.
104
+ Both writers gate the marker on the SAME `precompactGateStatus` as `/compact`,
105
+ so the marker only lands when step 6 would print **"Compact-ready"**.
87
106
 
88
107
  Skip session close for: single bug fix, single-file edit, Q&A only.
89
108
 
@@ -28,7 +28,7 @@ tags: [project]
28
28
  | `prd.md` | Purpose and success criteria |
29
29
  | `hot.md` | Session snapshot and background |
30
30
  | `session-state.md` | Next-session handoff |
31
- | `session-log/` | Monthly work narrative |
31
+ | `session-log/` | Daily-sharded work narrative (YYYY-MM-DD.md) |
32
32
  | `decisions/` | Architecture decision records |
33
33
 
34
34
  ---