hypomnema 1.3.1 → 1.3.3

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 (65) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/README.ko.md +11 -10
  4. package/README.md +11 -10
  5. package/commands/audit.md +1 -1
  6. package/commands/crystallize.md +11 -11
  7. package/commands/doctor.md +1 -1
  8. package/commands/feedback.md +2 -2
  9. package/commands/graph.md +1 -1
  10. package/commands/ingest.md +1 -1
  11. package/commands/init.md +1 -1
  12. package/commands/lint.md +1 -1
  13. package/commands/query.md +1 -1
  14. package/commands/rename.md +79 -0
  15. package/commands/resume.md +2 -2
  16. package/commands/stats.md +1 -1
  17. package/commands/uninstall.md +1 -1
  18. package/commands/upgrade.md +1 -1
  19. package/commands/verify.md +1 -1
  20. package/docs/ARCHITECTURE.md +3 -3
  21. package/docs/CONTRIBUTING.md +2 -2
  22. package/hooks/hypo-auto-commit.mjs +8 -24
  23. package/hooks/hypo-auto-minimal-crystallize.mjs +57 -11
  24. package/hooks/hypo-compact-guard.mjs +6 -3
  25. package/hooks/hypo-cwd-change.mjs +1 -1
  26. package/hooks/hypo-first-prompt.mjs +2 -2
  27. package/hooks/hypo-hot-rebuild.mjs +14 -1
  28. package/hooks/hypo-personal-check.mjs +91 -179
  29. package/hooks/hypo-pre-commit.mjs +1 -1
  30. package/hooks/hypo-session-end.mjs +1 -1
  31. package/hooks/hypo-session-start.mjs +7 -7
  32. package/hooks/hypo-shared.mjs +1082 -89
  33. package/hooks/hypo-web-fetch-ingest.mjs +2 -2
  34. package/hooks/version-check-fetch.mjs +2 -8
  35. package/hooks/version-check.mjs +18 -0
  36. package/package.json +3 -2
  37. package/scripts/check-tracker-ids.mjs +329 -0
  38. package/scripts/crystallize.mjs +322 -110
  39. package/scripts/doctor.mjs +6 -9
  40. package/scripts/feedback-sync.mjs +1 -1
  41. package/scripts/init.mjs +1 -1
  42. package/scripts/install-git-hooks.mjs +75 -40
  43. package/scripts/lib/check-tracker-ids.mjs +140 -0
  44. package/scripts/lib/design-history-stale.mjs +59 -14
  45. package/scripts/lib/extensions.mjs +4 -4
  46. package/scripts/lib/fix-manifest.mjs +2 -2
  47. package/scripts/lib/fix-status-verify.mjs +5 -4
  48. package/scripts/lib/plugin-detect.mjs +15 -6
  49. package/scripts/lib/project-create.mjs +1 -1
  50. package/scripts/lint.mjs +63 -8
  51. package/scripts/rename.mjs +836 -0
  52. package/scripts/resume.mjs +75 -19
  53. package/scripts/uninstall.mjs +1 -1
  54. package/scripts/upgrade.mjs +26 -25
  55. package/skills/crystallize/SKILL.md +12 -9
  56. package/skills/graph/SKILL.md +1 -1
  57. package/skills/ingest/SKILL.md +1 -1
  58. package/skills/lint/SKILL.md +1 -1
  59. package/skills/query/SKILL.md +1 -1
  60. package/skills/verify/SKILL.md +1 -1
  61. package/templates/SCHEMA.md +2 -2
  62. package/templates/hypo-config.md +2 -2
  63. package/templates/hypo-guide.md +22 -1
  64. package/templates/hypo-help.md +1 -0
  65. package/templates/projects/_template/index.md +1 -1
@@ -18,7 +18,11 @@
18
18
  * ~/Documents/{hypomnema,wiki,notes}) that contains
19
19
  * a hypo-config.md marker.
20
20
  * 3. else the default ~/hypomnema.
21
- * --project=<name> Project name (default: most recently active from hot.md)
21
+ * --project=<name> Project name. When omitted, resolveActiveProject()
22
+ * prefers the project whose working_dir contains the
23
+ * current directory (cwd-first), and only falls back to
24
+ * the most recently active hot.md row when nothing under
25
+ * cwd matches.
22
26
  * --json Output as JSON
23
27
  */
24
28
 
@@ -58,7 +62,8 @@ function parseFrontmatterField(content, key) {
58
62
 
59
63
  // Among `slugs`, return the one whose projects/<slug>/index.md `working_dir`
60
64
  // is the LONGEST prefix of cwd (so /repo/sub wins over /repo). Returns null
61
- // when cwd is falsy or matches none. Used only as a same-date tie-breaker.
65
+ // when cwd is falsy or matches none. resume gives this authority OVER recency
66
+ // (ADR 0044): a cwd↔working_dir match wins regardless of which row is newest.
62
67
  function pickByCwd(hypoDir, slugs, cwd) {
63
68
  if (!cwd) return null;
64
69
  let best = null;
@@ -78,6 +83,33 @@ function pickByCwd(hypoDir, slugs, cwd) {
78
83
  return best;
79
84
  }
80
85
 
86
+ // When cwd is set but no project's working_dir matches it, resume falls back to
87
+ // recency silently — the user lands in an unrelated project with no clue why.
88
+ // Emit a one-line stderr diagnostic (stdout `Project:`/`--json` contract is untouched)
89
+ // naming why each candidate failed: missing index.md, missing working_dir, or a
90
+ // working_dir that simply doesn't contain cwd. Logic mirrors pickByCwd's lookup.
91
+ function warnCwdFallback(hypoDir, slugs, cwd) {
92
+ // No cwd, or no candidate rows at all (fresh-init / no real project): there is
93
+ // nothing to "fall back to most-recent" toward, so stay silent — the caller
94
+ // surfaces the real "no active project found" error instead.
95
+ if (!cwd || slugs.length === 0) return;
96
+ const reasons = [];
97
+ for (const slug of slugs) {
98
+ const indexPath = join(hypoDir, 'projects', slug, 'index.md');
99
+ if (!existsSync(indexPath)) {
100
+ reasons.push(`${slug} (no index.md)`);
101
+ continue;
102
+ }
103
+ const wd = parseFrontmatterField(readFileSync(indexPath, 'utf-8'), 'working_dir');
104
+ if (!wd) reasons.push(`${slug} (no working_dir)`);
105
+ // else: has working_dir but didn't contain cwd — expected, not flagged.
106
+ }
107
+ const detail = reasons.length ? ` Candidates missing cwd metadata: ${reasons.join(', ')}.` : '';
108
+ process.stderr.write(
109
+ `note: cwd "${cwd}" matched no project working_dir; falling back to most-recent.${detail}\n`,
110
+ );
111
+ }
112
+
81
113
  function resolveActiveProject(hypoDir, cwd = null) {
82
114
  const hotPath = join(hypoDir, 'hot.md');
83
115
  if (!existsSync(hotPath)) return null;
@@ -93,38 +125,62 @@ function resolveActiveProject(hypoDir, cwd = null) {
93
125
  ),
94
126
  ].map((m) => ({ name: m[1].trim(), date: m[2] || '', slug: m[3] }));
95
127
  if (wikiRows.length > 0) {
96
- wikiRows.sort((a, b) => b.date.localeCompare(a.date));
97
- // Same-date tie-break (ISSUE-1): when the top date is shared by >1 row,
98
- // prefer the project whose working_dir contains cwd. No cwd / no match
99
- // keep the stable-sort winner (the legacy "first table row" behavior).
100
- const topDate = wikiRows[0].date;
101
- const tied = wikiRows.filter((r) => r.date === topDate);
102
- if (cwd && tied.length > 1) {
128
+ // cwd-first (ADR 0044): a cwd↔working_dir match wins over recency, across
129
+ // ALL rows (not just a same-date tie). The user is physically in that
130
+ // project, so cwd is a stronger intent signal than "some other project was
131
+ // touched more recently". This reverses the earlier tie-breaker-only
132
+ // semantics now that resume=cwd-positive (ADR 0043). Tradeoff: a stale cwd
133
+ // match can mask a genuinely newer project; `--project` overrides. close
134
+ // callers pass null → recency path below (resume=cwd-positive / close=no-pick).
135
+ if (cwd) {
103
136
  const picked = pickByCwd(
104
137
  hypoDir,
105
- tied.map((r) => r.slug),
138
+ wikiRows.map((r) => r.slug),
106
139
  cwd,
107
140
  );
108
141
  if (picked) return picked;
109
142
  }
143
+ // No cwd match → most recent by date (stable-sort keeps the first table row
144
+ // on a tie, the legacy behavior).
145
+ if (cwd)
146
+ warnCwdFallback(
147
+ hypoDir,
148
+ wikiRows.map((r) => r.slug),
149
+ cwd,
150
+ );
151
+ wikiRows.sort((a, b) => b.date.localeCompare(a.date));
110
152
  return wikiRows[0].slug;
111
153
  }
112
154
  // Legacy markdown-link rows: | [name](projects/name/...) | ...
113
- const mdRow = content.match(/\|\s*\[([^\]]+)\]\(projects\/([^/)]+)/);
114
- if (mdRow) return mdRow[2];
155
+ const mdSlugs = [...content.matchAll(/\|\s*\[([^\]]+)\]\(projects\/([^/)]+)/g)].map((m) => m[2]);
156
+ if (mdSlugs.length > 0) {
157
+ if (cwd) {
158
+ const picked = pickByCwd(hypoDir, mdSlugs, cwd);
159
+ if (picked) return picked;
160
+ warnCwdFallback(hypoDir, mdSlugs, cwd);
161
+ }
162
+ return mdSlugs[0]; // legacy: first table row
163
+ }
115
164
 
116
- // fallback: most recently modified project with a session-state.md
165
+ // fallback: a cwd-matched project, else the most recently modified one with a
166
+ // session-state.md. (mtime is only a heuristic once hot.md can't name a
167
+ // project — an explicit working_dir match is safer, so cwd-first here too.)
117
168
  const projectsDir = join(hypoDir, 'projects');
118
169
  if (!existsSync(projectsDir)) return null;
119
170
 
171
+ // Skip the scaffold project init.mjs writes — it isn't a real active project.
172
+ const candidates = readdirSync(projectsDir).filter(
173
+ (p) => p !== '_template' && existsSync(join(projectsDir, p, 'session-state.md')),
174
+ );
175
+ if (cwd) {
176
+ const picked = pickByCwd(hypoDir, candidates, cwd);
177
+ if (picked) return picked;
178
+ warnCwdFallback(hypoDir, candidates, cwd);
179
+ }
120
180
  let latest = null;
121
181
  let latestMtime = 0;
122
- for (const p of readdirSync(projectsDir)) {
123
- // Skip the scaffold project init.mjs writes — it isn't a real active project.
124
- if (p === '_template') continue;
125
- const ssPath = join(projectsDir, p, 'session-state.md');
126
- if (!existsSync(ssPath)) continue;
127
- const mtime = statSync(ssPath).mtimeMs;
182
+ for (const p of candidates) {
183
+ const mtime = statSync(join(projectsDir, p, 'session-state.md')).mtimeMs;
128
184
  if (mtime > latestMtime) {
129
185
  latestMtime = mtime;
130
186
  latest = p;
@@ -15,7 +15,7 @@
15
15
  * --force-extensions Remove user-modified extension files (hypo-ext-*) instead of preserving them
16
16
  * --hooks-dir=<path> Override Claude hooks directory (default: ~/.claude/hooks)
17
17
  *
18
- * Extensions (ADR 0024 fix #34): hypo-ext-* hard-copies under
18
+ * Extensions (ADR 0024): hypo-ext-* hard-copies under
19
19
  * ~/.claude/{hooks,commands,skills,agents}/ and ~/.codex/{hooks,commands}/ (with
20
20
  * --codex) are removed when their on-disk SHA matches the recorded one in
21
21
  * ~/.claude/hypo-pkg.json#extensions.<target>. User-modified copies are preserved
@@ -16,12 +16,12 @@
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 ISSUE-8 guard: register the Claude core
24
+ * --allow-dual-install Override the dual-install guard: register the Claude core
25
25
  * surface even though the Hypomnema plugin is also enabled
26
26
  * (knowingly accept the double-registration risk)
27
27
  */
@@ -412,7 +412,7 @@ function applyHookNameMigration(oldRefs, settingsPath, hooksDir) {
412
412
  if (applied.length > 0) {
413
413
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
414
414
  // Copy renamed hook files to the target hooks dir (~/.claude/hooks or
415
- // ~/.codex/hooks per the caller — fix #48 mirror).
415
+ // ~/.codex/hooks per the caller — codex mirror).
416
416
  for (const [oldName, newName] of Object.entries(HOOK_RENAMES)) {
417
417
  const oldPath = join(hooksDir, oldName);
418
418
  const newPath = join(hooksDir, newName);
@@ -761,7 +761,7 @@ function applyCommands(commandResults, force) {
761
761
  return applied;
762
762
  }
763
763
 
764
- // ISSUE-6: in plugin mode `applyCommands` is skipped (no command copy), but the
764
+ // In plugin mode `applyCommands` is skipped (no command copy), but the
765
765
  // runtime still needs hypo-pkg.json to resolve PKG_ROOT for lint/feedback scripts
766
766
  // (hooks/hypo-shared.mjs → hypo-personal-check). Write minimal metadata pointing
767
767
  // at the plugin's package root, preserving any existing fields (e.g. `extensions`)
@@ -797,7 +797,7 @@ const claudeSettingsPath = join(HOME, '.claude', 'settings.json');
797
797
  const codexHooksDir = join(HOME, '.codex', 'hooks');
798
798
  const codexSettingsPath = join(HOME, '.codex', 'settings.json');
799
799
 
800
- // ISSUE-6: when `/hypo:upgrade` runs as the Claude Code PLUGIN, the 15 core hooks
800
+ // When `/hypo:upgrade` runs as the Claude Code PLUGIN, the 15 core hooks
801
801
  // and 14 slash commands are provided by the plugin's hooks.json + commands/
802
802
  // (auto-wired by Claude Code), NOT copied into ~/.claude/. The manual/npm health
803
803
  // check below would then report all of them "missing" and recommend `--apply`,
@@ -806,20 +806,21 @@ const codexSettingsPath = join(HOME, '.codex', 'settings.json');
806
806
  // every hook fires TWICE. The decisive signal: the plugin command runs the
807
807
  // PLUGIN's upgrade.mjs, so PKG_ROOT lives under ~/.claude/plugins/. (A manual/npm
808
808
  // upgrade.mjs run while the plugin is ALSO enabled is a different failure mode —
809
- // dual install — tracked as ISSUE-8.)
809
+ // dual install — handled by the guard below.)
810
810
  // Match the Claude plugin cache shape specifically (`~/.claude/plugins/…`), NOT a
811
811
  // generic `/plugins/` substring — this flag now GATES install behavior, so a
812
812
  // legitimate npm/dev checkout under some unrelated `…/plugins/…` path must not be
813
813
  // misclassified and silently stop managing its hooks. (detectChannel's broad
814
814
  // `/plugins/` test is fine for the notifier's display-only use, but too loose here.)
815
815
  const pluginMode = PKG_ROOT.replace(/\\/g, '/').includes('/.claude/plugins/');
816
- // ISSUE-8 (dual install): the OTHER way the same double-registration can happen.
816
+ // Dual install: the OTHER way the same double-registration can happen.
817
817
  // Here the MANUAL/npm upgrade.mjs is running (pluginMode=false), but the Hypomnema
818
818
  // plugin is ALSO enabled in ~/.claude/settings.json — so the plugin loader already
819
819
  // provides the core hooks/commands/settings. A manual/npm `--apply` would copy and
820
820
  // register them on top, and every core hook fires twice. The detector is fail-open
821
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 `hypomnema@<mp>: true`.
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).
823
824
  const hypomnemaPluginEnabled = !pluginMode && isHypomnemaPluginEnabled(claudeSettingsPath);
824
825
  const dualInstallCoreConflict = hypomnemaPluginEnabled;
825
826
  // Surface policy: the Claude core surface (hooks/settings/commands/hook-name
@@ -864,7 +865,7 @@ const extCheck = syncExtensions({
864
865
  force: args.forceExtensions,
865
866
  });
866
867
 
867
- // E4 (fix #32): --codex mirrors the extensions sync into ~/.codex (hooks + commands
868
+ // E4: --codex mirrors the extensions sync into ~/.codex (hooks + commands
868
869
  // only; skills/agents skipped with a notice). The per-target SHA map lives in the
869
870
  // same ~/.claude/hypo-pkg.json under extensions.codex, so pkgPath is unchanged.
870
871
  const extCodexSettingsPath = codexSettingsPath;
@@ -897,7 +898,7 @@ const invalidSettingsCodex = settingsCodex
897
898
  ? settingsCodex.some((s) => s.status === 'invalid-json')
898
899
  : false;
899
900
  const schemaDrift = schema.bump !== 'none' && schema.bump !== 'unknown' && schema.bump !== 'ahead';
900
- // ISSUE-8 dual-install: when core is skipped, hypo-pkg.json is deliberately left
901
+ // Dual-install: when core is skipped, hypo-pkg.json is deliberately left
901
902
  // pointing at the PLUGIN's package root (preserved identity), so checkPkgJson()
902
903
  // reports it 'stale' relative to this npm/manual PKG_ROOT. That mismatch is
903
904
  // INTENTIONAL — `--apply` will not (and must not) rewrite it — so it must not
@@ -958,7 +959,7 @@ if (args.apply) {
958
959
  migrationPath = writeMigrationReport(args.hypoDir, schema.installed, schema.current, {
959
960
  // Use the core-skipped predicate, not raw pluginMode: in a dual-install skip
960
961
  // the core surface is plugin-owned too, so the report must not claim the
961
- // core hooks/settings were applied (ISSUE-8, W2 review note).
962
+ // core hooks/settings were applied.
962
963
  pluginMode: !managesClaudeCore,
963
964
  });
964
965
  }
@@ -976,7 +977,7 @@ if (args.apply) {
976
977
  appliedCommands = applyCommands(commands, args.forceCommands);
977
978
  appliedPkgJson = true;
978
979
  } else if (pluginMode) {
979
- // ISSUE-6 plugin mode: the plugin loader owns the core hooks/commands and
980
+ // Plugin mode: the plugin loader owns the core hooks/commands and
980
981
  // settings.json wiring — copying them here would double-register. Skip those,
981
982
  // but STILL write minimal package metadata so the runtime can resolve PKG_ROOT
982
983
  // for lint/feedback scripts (hooks/hypo-shared.mjs → hypo-personal-check). The
@@ -984,7 +985,7 @@ if (args.apply) {
984
985
  // is the plugin's own path here, so this metadata is authoritative.
985
986
  appliedPkgJson = writePluginModeMetadata();
986
987
  } else {
987
- // ISSUE-8 dual-install skip: a manual/npm run while the plugin is enabled. We
988
+ // Dual-install skip: a manual/npm run while the plugin is enabled. We
988
989
  // skip the core surface (the plugin owns it), but — unlike true plugin mode —
989
990
  // PKG_ROOT here is the npm/manual path while the ACTIVE runtime hooks are the
990
991
  // PLUGIN's. Rewriting a VALID hypo-pkg.json.pkgRoot to this npm path would
@@ -1029,7 +1030,7 @@ if (args.apply) {
1029
1030
  apply: true,
1030
1031
  force: args.forceExtensions,
1031
1032
  });
1032
- // 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
1033
1034
  // written hypo-pkg.json and merges extensions.codex alongside extensions.claude
1034
1035
  // (the per-target spread in syncExtensions preserves the other target's map).
1035
1036
  if (args.codex) {
@@ -1089,7 +1090,7 @@ if (args.json) {
1089
1090
  JSON.stringify(
1090
1091
  {
1091
1092
  pluginMode,
1092
- // ISSUE-8 dual-install signals.
1093
+ // Dual-install signals.
1093
1094
  hypomnemaPluginEnabled,
1094
1095
  dualInstallCoreConflict,
1095
1096
  coreManagedBy: managesClaudeCore ? 'self' : pluginMode ? 'plugin' : 'plugin-enabled',
@@ -1138,7 +1139,7 @@ if (args.json) {
1138
1139
  // Human-readable report
1139
1140
  const lines = [];
1140
1141
 
1141
- // ISSUE-6: lead with the plugin-mode banner so the user understands why the core
1142
+ // Lead with the plugin-mode banner so the user understands why the core
1142
1143
  // hook/command/settings sections read "managed by plugin" and that `--apply` will
1143
1144
  // NOT touch them (only vault-side migrations + package metadata).
1144
1145
  if (pluginMode) {
@@ -1155,7 +1156,7 @@ if (pluginMode) {
1155
1156
  );
1156
1157
  }
1157
1158
 
1158
- // ISSUE-8: a manual/npm upgrade.mjs is running while the Hypomnema plugin is ALSO
1159
+ // Dual install: a manual/npm upgrade.mjs is running while the Hypomnema plugin is ALSO
1159
1160
  // enabled — a dual install. Lead with a loud banner: the core surface is owned by
1160
1161
  // the plugin and is intentionally skipped, so `--apply` will not double-register.
1161
1162
  if (dualSkip) {
@@ -1202,7 +1203,7 @@ if (schema.bump === 'none') {
1202
1203
  );
1203
1204
  }
1204
1205
 
1205
- // 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).
1206
1207
  function pushHookSummary(hookList, label, targetPath) {
1207
1208
  const colHook = `Hook files${label}`.padEnd(20);
1208
1209
  const up = hookList.filter((h) => h.status === 'up-to-date').length;
@@ -1235,7 +1236,7 @@ if (managesClaudeCore) {
1235
1236
  }
1236
1237
  if (hooksCodex) pushHookSummary(hooksCodex, ' (codex)', '~/.codex/hooks/');
1237
1238
 
1238
- // settings.json registrations (target-aware mirror; fix #48).
1239
+ // settings.json registrations (target-aware mirror).
1239
1240
  function pushSettingsSummary(sList, label, invalidFlag) {
1240
1241
  const colS = `settings.json${label}`.padEnd(20);
1241
1242
  const reg = sList.filter((s) => s.status === 'registered').length;
@@ -1262,7 +1263,7 @@ if (settingsCodex) pushSettingsSummary(settingsCodex, ' (codex)', invalidSetting
1262
1263
 
1263
1264
  // Package metadata
1264
1265
  if (dualSkip && pkgJson.status === 'stale') {
1265
- // ISSUE-8: the 'stale' here is the preserved plugin identity (pkgRoot points at
1266
+ // Dual-install: the 'stale' here is the preserved plugin identity (pkgRoot points at
1266
1267
  // the plugin, not this npm/manual copy). That is intentional — not actionable.
1267
1268
  lines.push(
1268
1269
  `✓ Package metadata hypo-pkg.json plugin-owned (preserved — not rewritten in a dual install)`,
@@ -1386,7 +1387,7 @@ function pushExtSummary(check, label) {
1386
1387
  for (const c of check.conflicts) lines.push(` ✗ ${c.file} [${c.action} — left untouched]`);
1387
1388
  for (const d of check.drifts) lines.push(` ⚠ ${d.file} [drift — left untouched]`);
1388
1389
  }
1389
- // 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
1390
1391
  // resolvable advisory. Emit the spec'd WIKI messages so the user knows the recovery.
1391
1392
  if (nConflicts > 0) {
1392
1393
  lines.push(' [WIKI: existing file conflicts. Backup and retry, or use --force-extensions]');
@@ -1502,11 +1503,11 @@ const totalDrift =
1502
1503
  extCheck.actions.filter(
1503
1504
  (a) => a.action === 'create' || a.action === 'update' || a.action === 'force-update',
1504
1505
  ).length +
1505
- // 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
1506
1507
  // summary printed "up to date" while the exit code was 1.
1507
1508
  extCheck.conflicts.length +
1508
1509
  extCheck.drifts.length +
1509
- // E4 (fix #32): codex-target pending work counts identically (same message/exit
1510
+ // E4: codex-target pending work counts identically (same message/exit
1510
1511
  // consistency the E3 review caught — a codex conflict must not read "up to date").
1511
1512
  (extCheckCodex
1512
1513
  ? extCheckCodex.actions.filter(
@@ -1547,7 +1548,7 @@ if (totalDrift === 0) {
1547
1548
 
1548
1549
  console.log(lines.join('\n'));
1549
1550
 
1550
- // 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
1551
1552
  // drift, which only fails check mode). --force-extensions clears the resolvable
1552
1553
  // cases; an unfollowable symlink/non-regular dest still counts and stays exit 1.
1553
1554
  const extBlocked = extCheck.conflicts.length > 0 || (extCheckCodex?.conflicts.length ?? 0) > 0;
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Close a session (steps 1~6) and, on request, consolidate scattered wiki knowledge into stable pages (steps 7~11)
2
+ description: Close a session by capturing what happened into the wiki (steps 1-6), and on request consolidate scattered notes into stable pages (steps 7-11). Use when the user explicitly signals session end, asks to save or crystallize the session, or before a /compact. Task completion alone is not a close signal.
3
3
  ---
4
4
 
5
5
  You are running `/hypo:crystallize`. The command serves two modes (spec §5.2.7 / §8.3):
@@ -7,11 +7,11 @@ You are running `/hypo:crystallize`. The command serves two modes (spec §5.2.7
7
7
  1. **Session close (steps 1~6)** — gate the 5 mandatory memory files plus open-questions (conditional) so `/compact` can pass.
8
8
  2. **Synthesis (steps 7~11)** — surface tag clusters, orphan pages, and drafts that are ready to consolidate.
9
9
 
10
- When invoked at the end of a session (or with phrases like "세션 종료", "wrap up"), run the session-close checklist first. The synthesis scan only runs after close is confirmed and the user agrees.
10
+ When invoked to close a session via an explicit close signal ("세션 종료", "wrap up"), an accepted proactive-offer [세션 마무리], or `/compact` — run the session-close checklist first. Task completion alone does not put you in close mode. The synthesis scan only runs after close is confirmed and the user agrees.
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
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Generate a wikilink dependency graph from wiki pages
2
+ description: Generate a wikilink dependency graph from wiki pages (json, mermaid, or dot). Use when the user asks to visualize wiki structure, find orphan or hub pages, or map how notes link together.
3
3
  ---
4
4
 
5
5
  You are running `/hypo:graph`. Build a wikilink dependency graph from all pages under `pages/` and `projects/` and output it in the requested format.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Add a source document to the wiki and synthesize a source-summary page
2
+ description: Add an external source (URL, doc, article, command output) to the wiki and synthesize a citable summary page. Use when the user shares a source to capture, asks to ingest something, or wants reliable external knowledge saved for reuse.
3
3
  ---
4
4
 
5
5
  You are running `/hypo:ingest`. Add a new source document to `sources/` and create (or update) its corresponding `source-summary` page under `pages/`.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Lint wiki pages for frontmatter and broken wikilinks
2
+ description: Lint wiki pages for frontmatter errors and broken wikilinks. Use when the user asks to check or validate the wiki health, before a commit, or after bulk edits or renames.
3
3
  ---
4
4
 
5
5
  You are running `/hypo:lint`. Validate all wiki pages for frontmatter correctness and broken `[[wikilink]]` references.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Search wiki pages by keyword and retrieve relevant knowledge
2
+ description: Search the wiki by keyword and synthesize an answer from the relevant pages. Use when the user asks what the wiki knows about a topic, wants to recall a past decision, or needs prior context before starting work.
3
3
  ---
4
4
 
5
5
  You are running `/hypo:query`. Full-text search across all wiki pages and projects, then synthesize an answer from the matching pages.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Check wiki pages for stale or unverified knowledge and prompt review
2
+ description: Surface wiki knowledge that is stale or past its verify-by date and prompt review. Use when the user asks what needs re-checking, wants to audit knowledge freshness, or is reviewing the wiki for rot.
3
3
  ---
4
4
 
5
5
  You are running `/hypo:verify`. Check all wiki pages for `verify_by` and `verify_by_date` fields, surface overdue pages, and guide a review pass.
@@ -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.1"
4
+ version: "1.3.3"
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
  ```
@@ -74,16 +74,37 @@ Trigger: explicit close mention, `/compact` request, or context limit approachin
74
74
 
75
75
  Do **not** treat these as completion: plan approvals, clarification answers, permission grants, progress updates, partial findings, or mid-task checkpoints. A "좋아요" / "감사합니다" / "ok" following any of those is *not* a close signal — only offer after a genuine final completion/verification report.
76
76
 
77
+ **Proactive offer means offer, not close.** In this path (task done, no close signal) do not run the close checklist, write the `session-closed` marker, or declare the session ended on your own. Fire `AskUserQuestion` and proceed only after the user picks [세션 마무리]. Task completion is not a close trigger; closing without asking violates this procedure. This scopes the proactive path only: a real close signal, `/compact`, or context limit still closes via the triggers above.
78
+
77
79
  Ask: *"이 작업이 마무리되었나요? 세션을 정리(crystallize)할까요?"* with options **[세션 마무리 / 계속 작업]**. On **세션 마무리** → run the session close checklist (or invoke `/hypo:crystallize`). On **계속 작업** → continue and do not re-ask until the next task completes. Ask at most once per completed task; never loop. (Decline simply ends the turn — Layer 3's Stop-chain only blocks on an explicit close signal, so no repeated prompts occur.)
78
80
 
79
81
  1. Update `projects/<name>/session-state.md` (next tasks, overwrite)
80
82
  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)
83
+ 3. Append to `projects/<name>/session-log/YYYY-MM-DD.md` (daily shard, narrative entry, append-only)
82
84
  4. Update root `hot.md` pointer table + date
83
85
  5. Run `scripts/lint.mjs` and fix errors in files **you** touched — debt in other
84
86
  projects / shared pages you did not author is reported as a non-blocking
85
87
  notice, not a gate. (The documented `crystallize.mjs --apply-session-close`
86
88
  path runs this lint automatically, scoped to the files it writes.)
89
+ 6. Verify with `scripts/crystallize.mjs --check-session-close`: a dry-run of the
90
+ **full** PreCompact gate (close files + lint + design-history + feedback
91
+ projection, ADR 0046), sharing one function with the gate. Only declare the
92
+ session closed once it prints **"Compact-ready"**. A "close files updated"
93
+ check alone is not enough — the real `/compact` gate also blocks on a lint
94
+ error in a close file or a feedback projection over-cap. (Not a hard
95
+ guarantee: the live gate can still differ on a context-≥70% prompt,
96
+ `HYPO_SKIP_GATE`, or a transcript-scoped lint error — pass `--transcript-path`
97
+ to include the last.) Pass `--session-id=<id>` to also see `marker_present`
98
+ (step 7).
99
+ 7. Record the session-closed marker (ADR 0047). The Stop hook blocks until this
100
+ session's per-session marker exists, and a hand-edit close (writing the files
101
+ directly + committing) never writes it; the marker is written only by the
102
+ crystallize writer, never by the hook (ADR 0022 bypass guard). Normal path:
103
+ close via `crystallize.mjs --apply-session-close --session-id=<id> --transcript-path=<path>`,
104
+ which writes the marker once the gate is green. Hand-edit recovery: after
105
+ committing the files, run `crystallize.mjs --mark-session-closed --session-id=<id> --transcript-path=<path>`.
106
+ Both writers gate the marker on the SAME `precompactGateStatus` as `/compact`,
107
+ so the marker only lands when step 6 would print **"Compact-ready"**.
87
108
 
88
109
  Skip session close for: single bug fix, single-file edit, Q&A only.
89
110
 
@@ -42,6 +42,7 @@ Quick reference for all `/hypo:*` commands.
42
42
  | `/hypo:verify` | Review overdue verify_by deadlines |
43
43
  | `/hypo:lint` | Validate frontmatter and `[[wikilinks]]` |
44
44
  | `/hypo:graph` | Generate link graph (json / mermaid / dot) |
45
+ | `/hypo:rename` | Rename a page or directory, rewriting inbound `[[wikilinks]]` |
45
46
 
46
47
  ---
47
48
 
@@ -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
  ---