hypomnema 1.8.4 → 1.9.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 (60) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +36 -0
  4. package/README.ko.md +16 -15
  5. package/README.md +14 -13
  6. package/commands/crystallize.md +84 -30
  7. package/commands/doctor.md +3 -1
  8. package/docs/ARCHITECTURE.md +32 -9
  9. package/docs/CONTRIBUTING.md +5 -1
  10. package/hooks/atomic-write.mjs +44 -0
  11. package/hooks/base-store.mjs +409 -90
  12. package/hooks/close-gate-store.mjs +131 -16
  13. package/hooks/close-journal.mjs +141 -19
  14. package/hooks/close-receipt.mjs +559 -0
  15. package/hooks/hooks.json +2 -29
  16. package/hooks/hypo-auto-commit.mjs +80 -10
  17. package/hooks/hypo-auto-minimal-crystallize.mjs +129 -25
  18. package/hooks/hypo-auto-stage.mjs +13 -1
  19. package/hooks/hypo-close-guard.mjs +2 -1
  20. package/hooks/hypo-cwd-change.mjs +45 -20
  21. package/hooks/hypo-file-watch.mjs +44 -6
  22. package/hooks/hypo-first-prompt.mjs +15 -9
  23. package/hooks/hypo-hot-rebuild.mjs +169 -112
  24. package/hooks/hypo-personal-check.mjs +9 -1
  25. package/hooks/hypo-session-record.mjs +1 -1
  26. package/hooks/hypo-session-start.mjs +381 -18
  27. package/hooks/hypo-shared.mjs +2151 -185
  28. package/hooks/hypo-stop.mjs +207 -0
  29. package/hooks/proposal-store.mjs +166 -34
  30. package/hooks/shared.json +7 -1
  31. package/hooks/version-check.mjs +9 -7
  32. package/package.json +2 -1
  33. package/scripts/crystallize.mjs +3 -4
  34. package/scripts/doctor.mjs +506 -32
  35. package/scripts/init.mjs +32 -3
  36. package/scripts/lib/crystallize-close-apply.mjs +1949 -230
  37. package/scripts/lib/crystallize-close-check.mjs +43 -23
  38. package/scripts/lib/design-history-stale.mjs +36 -9
  39. package/scripts/lib/frontmatter.mjs +39 -0
  40. package/scripts/lib/hook-inventory.mjs +31 -1
  41. package/scripts/lib/page-usage.mjs +1 -1
  42. package/scripts/lib/pkg-provenance.mjs +7 -1
  43. package/scripts/lib/project-create.mjs +40 -44
  44. package/scripts/lib/schema-vocab.mjs +22 -11
  45. package/scripts/lib/template-schema-version.mjs +1 -0
  46. package/scripts/lib/time.mjs +46 -0
  47. package/scripts/lint.mjs +66 -18
  48. package/scripts/proposal.mjs +423 -18
  49. package/scripts/resume.mjs +3 -2
  50. package/scripts/session-audit.mjs +2 -1
  51. package/scripts/uninstall.mjs +15 -1
  52. package/scripts/upgrade.mjs +281 -90
  53. package/scripts/verify.mjs +2 -1
  54. package/scripts/weekly-report.mjs +2 -1
  55. package/templates/SCHEMA.md +27 -7
  56. package/templates/gitignore +11 -0
  57. package/templates/hypo-automation.md +10 -13
  58. package/templates/hypo-config.md +1 -1
  59. package/templates/hypo-guide.md +53 -25
  60. package/templates/hot.md +0 -24
@@ -11,9 +11,12 @@
11
11
  * hot.md / session-state.md content is NOT re-injected here. On the SessionStart
12
12
  * path the upstream hook already put it in the model's context, and this hook
13
13
  * only forces the LLM to lead with a summary line drawn from it. On the
14
- * cwd-change path nothing was injected at all (CwdChanged has no documented
15
- * injection path), so that branch asks for a verbatim line instead of a
16
- * summary.
14
+ * cwd-change path this hook does not rely on any context having reached the
15
+ * model. The hook reference does not document whether CwdChanged's
16
+ * systemMessage reaches the model; as of Claude Code 2.1.276 (checked
17
+ * 2026-09-18), tracing the installed binary shows it does not (see the
18
+ * transmission note in README.md), so that branch asks for a verbatim line
19
+ * instead of a summary.
17
20
  * Marker expires after 10 minutes.
18
21
  */
19
22
 
@@ -74,12 +77,15 @@ process.stdin.on('end', () => {
74
77
  // for the model to fill the placeholders with. Provide a concrete fallback
75
78
  // line so the model doesn't leak literal `[one-line summary]` text on a
76
79
  // first-ever session (codex v2 review 2026-05-26).
77
- // A cwd-change marker arms this hook, but CwdChanged has no documented
78
- // context-injection path, so no [HOT] / [SESSION STATE] ever reached the
79
- // model for that move. Asking for a summary would make the model invent
80
- // one, or emit the literal brackets. This is not a temporary branch: the
81
- // follow-up that moves the hook to systemMessage sends that text to the
82
- // user, not the model, so the model still gets nothing for a cwd move.
80
+ // A cwd-change marker arms this hook. The hook reference does not
81
+ // document whether CwdChanged's systemMessage reaches the model; as of
82
+ // Claude Code 2.1.276 (checked 2026-09-18), tracing the installed binary
83
+ // shows it does not (see the transmission note in README.md), so this
84
+ // hook does not rely on [HOT] / [SESSION STATE] having reached the model
85
+ // for that move. Asking for a summary on context that never arrived
86
+ // would risk the model inventing one, or emitting the literal brackets.
87
+ // This is not a temporary branch: it holds until the documentation says
88
+ // otherwise.
83
89
  const cwdMove = marker.source === 'cwd-change';
84
90
  const exampleLine = cwdMove
85
91
  ? `${verb} ${projSafe}. What would you like to work on here?`
@@ -1,16 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * hypo-hot-rebuild.mjs — Stop hook
3
+ * hypo-hot-rebuild.mjs: Stop chain stage 1 (spawned by hypo-stop.mjs)
4
4
  *
5
- * Rebuilds root hot.md in canonical format on every session end.
6
- * Preserves the project pointer table rows while refreshing dates
7
- * from each project's hot.md frontmatter `updated:` field.
8
- *
9
- * Claude manages: adding/removing project rows in the pointer table.
10
- * This script manages: frontmatter, H2 structure, date fields.
5
+ * Rebuilds root hot.md as a projection on every session end: the row set and
6
+ * every field come from a fresh directory scan of `projects/*\/hot.md`
7
+ * (renderRootHotProjection in hypo-shared.mjs), never from parsing the
8
+ * previous root file. Claude no longer manages this file by hand; a project
9
+ * row exists exactly when that project has a hot.md, nothing more.
11
10
  */
12
11
 
13
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
12
+ import { writeFileSync, existsSync, mkdirSync } from 'fs';
14
13
  import { join } from 'path';
15
14
  import {
16
15
  HYPO_DIR,
@@ -18,18 +17,33 @@ import {
18
17
  formatGrowthMetrics,
19
18
  deriveRootLogEntries,
20
19
  recordTouchedPaths,
20
+ writeRootHotProjection,
21
+ writeRootHotHealthNotice,
22
+ rootHotBackupRecoveryNotice,
21
23
  } from './hypo-shared.mjs';
22
- import { advanceBase, hashContent } from './base-store.mjs';
23
24
 
24
- const HOT_PATH = join(HYPO_DIR, 'hot.md');
25
25
  const GROWTH_CACHE = join(HYPO_DIR, '.cache', 'last-session-growth.json');
26
26
 
27
- // This Stop hook runs BEFORE hypo-auto-commit and can write hot.md
28
- // (rebuild) and log.md (deriveRootLogEntries), both hook-generated, not user
29
- // Write/Edit, so hypo-auto-stage never sees them. Read session_id off stdin so
30
- // whatever this hook writes still lands in the scoped commit's set; without
31
- // this, a scope built from Write/Edit alone would silently drop these files
32
- // from every session's auto-commit.
27
+ // This stage runs BEFORE hypo-auto-commit, and what guarantees that is
28
+ // hypo-stop.mjs: Stop carries ONE registration, and its STAGES list spawns the
29
+ // four stages in order, each one waited on before the next starts. The four
30
+ // used to be four separate Stop registrations, which Claude Code runs in
31
+ // parallel, so this sentence was false for as long as it stood: auto-commit
32
+ // could take the vault lock and commit between this file's write and the
33
+ // touched-paths claim at the bottom, leaving fresh hot.md/log.md bytes
34
+ // uncommitted with no failure anywhere to show for it.
35
+ //
36
+ // Put either of these four back into hooks.json as its own Stop registration
37
+ // and the guarantee is gone again, silently: this file still writes, still
38
+ // claims, and still reports success, and nothing about the order is checked at
39
+ // runtime. tests/session-hooks.test.mjs pins it from the outside instead, by
40
+ // asserting that this stage's hot.md reaches the commit auto-commit makes.
41
+ //
42
+ // This stage can write hot.md (rebuild) and log.md (deriveRootLogEntries), both
43
+ // hook-generated, not user Write/Edit, so hypo-auto-stage never sees them. Read
44
+ // session_id off stdin so whatever this stage writes still lands in the scoped
45
+ // commit's set; without this, a scope built from Write/Edit alone would
46
+ // silently drop these files from every session's auto-commit.
33
47
  let sessionId = null;
34
48
  try {
35
49
  const raw = await new Promise((r) => {
@@ -43,102 +57,111 @@ try {
43
57
  sessionId = null;
44
58
  }
45
59
 
46
- function parseFrontmatter(content) {
47
- const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
48
- if (!m) return {};
49
- const result = {};
50
- for (const line of m[1].split('\n')) {
51
- const kv = line.match(/^(\w[\w_-]*):\s*(.*)$/);
52
- if (kv) result[kv[1]] = kv[2].trim();
53
- }
54
- return result;
55
- }
56
-
57
- function getProjectDate(slug) {
58
- const hotPath = join(HYPO_DIR, 'projects', slug, 'hot.md');
59
- if (!existsSync(hotPath)) return null;
60
- try {
61
- return parseFrontmatter(readFileSync(hotPath, 'utf-8')).updated || null;
62
- } catch {
63
- return null;
64
- }
65
- }
66
-
67
- function parsePointerRows(content) {
68
- const rows = [];
69
- for (const line of content.split('\n')) {
70
- const m = line.match(/^\|\s*(.+?)\s*\|\s*.+?\s*\|\s*\[\[projects\/(.+?)\/hot\]\]\s*\|/);
71
- if (m) rows.push({ name: m[1].trim(), slug: m[2].trim() });
72
- }
73
- return rows;
74
- }
75
-
76
- /** @returns {boolean} true when hot.md was actually rewritten. */
60
+ /**
61
+ * Rebuild root hot.md as a directory-scan projection. No
62
+ * longer parses the previous root file's rows at all: the projection's row
63
+ * set is decided entirely by `writeRootHotProjection` from
64
+ * `projects/<slug>/hot.md` on disk, which is what lets a manually deleted row
65
+ * come back. See hypo-shared.mjs's projection section for the generation
66
+ * rules (blank date, not `|| today`; frontmatter `updated:` is the row-date
67
+ * max, not today) and for the scanError / backup semantics this function
68
+ * passes straight through.
69
+ *
70
+ * A scanError leaves a durable health notice for the next SessionStart to
71
+ * surface, since this hook's own stdout is suppressed at the bottom of this
72
+ * file and a Stop-time failure would otherwise be invisible until someone
73
+ * went looking at stderr.
74
+ * @param {string|null} sessionId
75
+ * @returns {{written: boolean, scanError: boolean, lockTimeout: boolean, backedUp: boolean, backupPath: string|null, gitignoreUpdated: boolean, warnings: string[], content: string|null}}
76
+ */
77
77
  function rebuild(sessionId) {
78
- if (!existsSync(HOT_PATH)) return false;
79
-
80
- const current = readFileSync(HOT_PATH, 'utf-8');
81
- const rows = parsePointerRows(current);
82
- if (rows.length === 0) return false;
83
-
84
- const today = new Date().toISOString().slice(0, 10);
85
-
86
- const tableRows = rows
87
- .map(({ name, slug }) => {
88
- const date = getProjectDate(slug) || today;
89
- return `| ${name} | ${date} | [[projects/${slug}/hot]] |`;
90
- })
91
- .join('\n');
92
-
93
- const canonical = `---
94
- title: Hot Cache — Pointer
95
- type: reference
96
- updated: ${today}
97
- tags: [wiki, operations]
98
- ---
99
-
100
- # Hot Cache
101
-
102
- > Read at session start → navigate to the relevant project session-state.md and hot.md.
103
- > Update at session close: project session-state.md, project hot.md, and this file's "Active Projects" table.
104
-
105
- ## Active Projects
106
-
107
- | Project | Last Session | Hot Cache |
108
- |---|---|---|
109
- ${tableRows}
110
-
111
- ## Session Start Checklist
112
-
113
- 1. Check this file for the relevant project link
114
- 2. Read \`projects/<name>/session-state.md\` for next tasks if it exists
115
- 3. Read \`projects/<name>/hot.md\` for project background
116
- `;
117
-
118
- if (canonical !== current) {
119
- writeFileSync(HOT_PATH, canonical);
120
- // This write bypasses the Write/Edit tool, so hypo-auto-stage's
121
- // PostToolUse-based advanceBaseForWrite never sees it (see the file-header
122
- // comment above). Without advancing the base here, this session's own
123
- // rewrite of hot.md looks -- at close time -- exactly like a DIFFERENT
124
- // session having edited it, and the observed-base guard in crystallize.mjs
125
- // parks a false conflict against this session's own work. Mirrors
126
- // crystallize.mjs's overwrite(): advanceBase right after the write that
127
- // made it true.
128
- //
129
- // No session_id on stdin (empty/malformed payload): advanceBase is a
130
- // no-op without a snapshot anyway, so this stays silent-safe like before
131
- // this fix -- just observable on stderr instead of a guess.
132
- if (sessionId) {
133
- advanceBase(HYPO_DIR, sessionId, 'hot.md', hashContent(canonical));
134
- } else {
135
- process.stderr.write(
136
- '[hypo-hot-rebuild] no session_id on stdin; base not advanced for hot.md\n',
78
+ // Mirrors hypo-session-start.mjs's own existsSync(HYPO_DIR) guard around its
79
+ // projection call. Without this, an install with no wiki at all (HYPO_DIR
80
+ // unresolved or absent) hits ENOENT on every Stop, forever, since this hook
81
+ // runs on every session end regardless of whether a wiki exists. The old
82
+ // rebuild() had the same guard in spirit (`if (!existsSync(HOT_PATH)) return
83
+ // false`), just against the file instead of the directory.
84
+ if (!existsSync(HYPO_DIR)) {
85
+ return {
86
+ written: false,
87
+ scanError: false,
88
+ lockTimeout: false,
89
+ backedUp: false,
90
+ backupPath: null,
91
+ gitignoreUpdated: false,
92
+ warnings: [],
93
+ content: null,
94
+ };
95
+ }
96
+ // A read-error on the existing hot.md (BLOCKER fix) throws out of
97
+ // writeRootHotProjection instead of silently overwriting it; the outer
98
+ // try/catch around this whole rebuild() call already leaves a health
99
+ // notice for that case, same as any other unexpected error here.
100
+ // n1 fix: pass sessionId so this session's own digest lands in ITS OWN
101
+ // receipt, not just the shared global ownership record (see
102
+ // writeRootHotProjection's doc comment).
103
+ const result = writeRootHotProjection(HYPO_DIR, undefined, sessionId);
104
+
105
+ if (result.lockTimeout) {
106
+ // BLOCKER fix: the projection writer holds the vault lock now, so a Stop
107
+ // that overlaps another session's commit can find it taken. Nothing was
108
+ // written and nothing is claimed below (that is gated on
109
+ // `result.written`), so the only thing left to do is say so.
110
+ writeRootHotHealthNotice(
111
+ HYPO_DIR,
112
+ '이전 세션 종료 시 루트 hot.md 를 갱신하지 못했습니다: 다른 세션이 이 저장소를 쓰는 중이라 잠금을 얻지 못했습니다. 이전 내용을 그대로 두었고, 다음 세션 시작이나 종료 때 다시 시도합니다.',
113
+ );
114
+ } else if (result.scanError) {
115
+ writeRootHotHealthNotice(
116
+ HYPO_DIR,
117
+ `이전 세션 종료 시 루트 hot.md 갱신에 실패했습니다: projects/ 디렉터리를 읽을 수 없어 이전 파일을 그대로 두었습니다. 권한과 경로를 확인하고(\`ls -ld ${join(HYPO_DIR, 'projects')}\`), 고치면 다음 세션 시작이나 종료 때 자동으로 다시 시도합니다.`,
118
+ );
119
+ } else {
120
+ // scanError and a written result never coexist (writeRootHotProjection
121
+ // skips the write outright on a scanError), but a per-project warning and
122
+ // a migration backup CAN both be true on the same write, so both must
123
+ // land in the SAME notice call. writeRootHotHealthNotice holds exactly
124
+ // one pending message: a second call before the first is ever consumed
125
+ // silently replaces it (atomicWrite overwrites, it does not append), so
126
+ // issuing it twice here would drop whichever ran first instead of
127
+ // surfacing both.
128
+ const lines = [];
129
+ if (result.warnings.length > 0) {
130
+ // major-1/3: a per-project read failure kept its row (blank date), but
131
+ // Stop's own stdout is suppressed, so the reason still needs a durable
132
+ // notice or it never reaches anyone.
133
+ lines.push(
134
+ `이전 세션 종료 시 루트 hot.md 갱신 중 일부 프로젝트를 읽지 못했습니다:\n${result.warnings.join('\n')}\n` +
135
+ '해당 파일의 권한과 경로를 확인하면 다음 세션 시작이나 종료 때 자동으로 다시 읽습니다.',
137
136
  );
138
137
  }
139
- return true;
138
+ if (result.backedUp && result.backupPath) {
139
+ // MAJOR fix (codex 3rd-tier finding 6): this used to name only the
140
+ // backup filename, with no recovery guidance at all. The reader had
141
+ // to already know NOT to copy it back onto root hot.md, and where the
142
+ // content actually goes. Stop's own stdout is suppressed, so a backup
143
+ // made here has no channel of its own besides this health notice,
144
+ // consumed at the next SessionStart (same delivery path already used
145
+ // for a scanError or a per-project warning above); before this fix
146
+ // that was the ONE surface where the guidance never arrived.
147
+ // rootHotBackupRecoveryNotice is the same sentence SessionStart's own
148
+ // write site uses, verbatim, so the two writers cannot drift apart.
149
+ lines.push(rootHotBackupRecoveryNotice(result.backupPath));
150
+ }
151
+ if (lines.length > 0) writeRootHotHealthNotice(HYPO_DIR, lines.join('\n\n'));
140
152
  }
141
- return false;
153
+
154
+ // This write used to advance the session's observed base for hot.md right
155
+ // here, because the write bypasses the Write/Edit tool and hypo-auto-stage's
156
+ // PostToolUse advanceBaseForWrite never sees it. That call is gone with root
157
+ // hot.md itself: the file is no longer one of base-store's
158
+ // `overwriteTargets`, so no session snapshots it, no close payload writes it,
159
+ // and there is no base for this regeneration to keep honest. Advancing one
160
+ // from here would MINT a guarded key that nothing ever compares, which is the
161
+ // exact shape that produced a `base-mismatch` park on a file no human had
162
+ // touched. If root hot.md ever needs a base again, it comes back through
163
+ // `overwriteTargets`, not through a hook's own write.
164
+ return result;
142
165
  }
143
166
 
144
167
  function emitGrowth() {
@@ -152,11 +175,24 @@ function emitGrowth() {
152
175
  } catch {}
153
176
  }
154
177
 
155
- let hotWritten = false;
178
+ let hotResult = {
179
+ written: false,
180
+ scanError: false,
181
+ lockTimeout: false,
182
+ backedUp: false,
183
+ backupPath: null,
184
+ gitignoreUpdated: false,
185
+ warnings: [],
186
+ content: null,
187
+ };
156
188
  try {
157
- hotWritten = rebuild(sessionId);
189
+ hotResult = rebuild(sessionId);
158
190
  } catch (err) {
159
191
  process.stderr.write(`[hypo-hot-rebuild] error: ${err?.message ?? String(err)}\n`);
192
+ writeRootHotHealthNotice(
193
+ HYPO_DIR,
194
+ `이전 세션 종료 시 루트 hot.md 갱신 중 오류가 발생했습니다: ${err?.message ?? String(err)}`,
195
+ );
160
196
  }
161
197
  // Auto-derive the root log.md session entry from each project's session-log
162
198
  // heading (runs AFTER rebuild() so root hot.md is already fresh and isn't itself
@@ -177,9 +213,30 @@ try {
177
213
  // set (see the sessionId comment above). No-op without a session_id.
178
214
  try {
179
215
  const touched = [];
180
- if (hotWritten) touched.push('hot.md');
216
+ if (hotResult.written) touched.push('hot.md');
217
+ // The backup file itself is never claimed here: it is gitignored the
218
+ // moment writeRootHotProjection creates it (ensureVaultGitignorePattern),
219
+ // so nothing would ever stage or commit it anyway, and claiming a path git
220
+ // will never see as changed just leaves a permanently-dropped stale entry.
221
+ if (hotResult.gitignoreUpdated) touched.push('.gitignore');
181
222
  if (logEntriesAdded > 0) touched.push('log.md');
182
- if (touched.length > 0) recordTouchedPaths(HYPO_DIR, sessionId, touched);
223
+ if (touched.length > 0) {
224
+ const claimed = recordTouchedPaths(HYPO_DIR, sessionId, touched);
225
+ // A failed claim here means this hook's own writes just landed on disk
226
+ // with nothing in this session's touched-paths set to bring them into
227
+ // the auto-commit that runs right after this hook, the same orphan
228
+ // hypo-session-start.mjs's own claim guards against, just on the Stop
229
+ // side. There is no earlier point to claim it from at Stop (unlike
230
+ // SessionStart, the touched paths are only known after rebuild() has
231
+ // already run), so this leaves a health notice instead of a pre-write
232
+ // guard.
233
+ if (!claimed && hotResult.written) {
234
+ writeRootHotHealthNotice(
235
+ HYPO_DIR,
236
+ `루트 hot.md 갱신 후 이번 세션의 커밋 범위 기록에 실패했습니다: ${touched.join(', ')} 가 미커밋 파일로 남을 수 있습니다.`,
237
+ );
238
+ }
239
+ }
183
240
  } catch (err) {
184
241
  process.stderr.write(`[hypo-hot-rebuild] touched-paths error: ${err?.message ?? String(err)}\n`);
185
242
  }
@@ -35,6 +35,7 @@ import {
35
35
  isUnderProjectDirs,
36
36
  resolveGateProjectOverride,
37
37
  } from './hypo-shared.mjs';
38
+ import { closeCheckpointState, isCloseComplete } from './close-receipt.mjs';
38
39
 
39
40
  const WARNING_FILE = join(homedir(), '.claude', 'state', 'wiki-context-warning.json');
40
41
 
@@ -118,9 +119,16 @@ process.stdin.on('end', () => {
118
119
 
119
120
  let gate;
120
121
  try {
122
+ // The marker only scopes this gate (log-only mode, attribution) when
123
+ // closeCheckpointState calls this session's close finished, the verdict
124
+ // Stop uses too. A marker whose receipt is missing or invalid is ignored.
125
+ const checkpoint = sessionId ? closeCheckpointState(HYPO_DIR, sessionId) : null;
121
126
  gate = precompactGateStatus(HYPO_DIR, {
122
127
  transcriptPath,
123
128
  ...(sessionId ? { sessionId } : {}),
129
+ ...(checkpoint
130
+ ? { closeMarker: isCloseComplete(checkpoint) ? checkpoint.marker : null }
131
+ : {}),
124
132
  ...(sessionCwd ? { sessionCwd } : {}),
125
133
  ...(attributionScope ? { attributionScope } : {}),
126
134
  });
@@ -322,7 +330,7 @@ process.stdin.on('end', () => {
322
330
  ` [ ] 7. index.md — update Projects section if needed`,
323
331
  ` [ ] 8. log.md — append ## [${today}] session | <project-name>`,
324
332
  ` [ ] 9. hot.md — update projects/<name>/hot.md (no exceptions)`,
325
- ` [ ] 10. root hot.md — update ~/hypomnema/hot.md active project table`,
333
+ ` [ ] 10. root hot.md: generated automatically from projects/*/hot.md; no manual edit needed`,
326
334
  ` [ ] 11. updated: field — verify today's date on all touched .md files`,
327
335
  ` [ ] 12. lint — run /hypo:lint; fix errors in files YOU touched`,
328
336
  ` (other projects' / shared-page debt is reported as non-blocking notice)`,
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * hypo-session-record.mjs — Stop hook
3
+ * hypo-session-record.mjs: Stop chain stage 2 (spawned by hypo-stop.mjs)
4
4
  *
5
5
  * Appends an entry to ~/hypomnema/.cache/sessions/index.jsonl for each
6
6
  * completed session. The index.jsonl is the **primary** source for