hypomnema 1.0.1 → 1.2.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 (76) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ko.md +12 -5
  4. package/README.md +12 -5
  5. package/commands/audit.md +46 -0
  6. package/commands/crystallize.md +113 -23
  7. package/commands/feedback.md +40 -26
  8. package/commands/ingest.md +31 -9
  9. package/commands/upgrade.md +2 -2
  10. package/docs/ARCHITECTURE.md +83 -9
  11. package/docs/CONTRIBUTING.md +2 -2
  12. package/hooks/hooks.json +39 -1
  13. package/hooks/hypo-auto-commit.mjs +23 -4
  14. package/hooks/hypo-auto-minimal-crystallize.mjs +145 -0
  15. package/hooks/hypo-auto-stage.mjs +9 -5
  16. package/hooks/hypo-compact-guard.mjs +33 -24
  17. package/hooks/hypo-cwd-change.mjs +107 -24
  18. package/hooks/hypo-file-watch.mjs +23 -10
  19. package/hooks/hypo-first-prompt.mjs +37 -23
  20. package/hooks/hypo-hot-rebuild.mjs +31 -8
  21. package/hooks/hypo-lookup.mjs +171 -65
  22. package/hooks/hypo-personal-check.mjs +207 -112
  23. package/hooks/hypo-pre-commit.mjs +46 -0
  24. package/hooks/hypo-session-end.mjs +58 -0
  25. package/hooks/hypo-session-record.mjs +60 -0
  26. package/hooks/hypo-session-start.mjs +312 -44
  27. package/hooks/hypo-shared.mjs +880 -28
  28. package/hooks/hypo-web-fetch-ingest.mjs +121 -0
  29. package/hooks/version-check-fetch.mjs +74 -0
  30. package/hooks/version-check.mjs +184 -0
  31. package/package.json +17 -3
  32. package/scripts/crystallize.mjs +623 -18
  33. package/scripts/doctor.mjs +739 -46
  34. package/scripts/feedback-sync.mjs +974 -0
  35. package/scripts/feedback.mjs +253 -44
  36. package/scripts/graph.mjs +35 -22
  37. package/scripts/ingest.mjs +89 -16
  38. package/scripts/init.mjs +442 -114
  39. package/scripts/lib/design-history-stale.mjs +83 -0
  40. package/scripts/lib/extensions.mjs +749 -0
  41. package/scripts/lib/frontmatter.mjs +5 -1
  42. package/scripts/lib/hypo-ignore.mjs +12 -10
  43. package/scripts/lib/pkg-json.mjs +23 -5
  44. package/scripts/lib/project-create.mjs +225 -0
  45. package/scripts/lib/schema-vocab.mjs +96 -0
  46. package/scripts/lint.mjs +238 -31
  47. package/scripts/query.mjs +26 -10
  48. package/scripts/resume.mjs +11 -5
  49. package/scripts/session-audit.mjs +277 -0
  50. package/scripts/smoke-pack.mjs +224 -0
  51. package/scripts/stats.mjs +24 -10
  52. package/scripts/uninstall.mjs +369 -48
  53. package/scripts/upgrade.mjs +766 -195
  54. package/scripts/verify.mjs +24 -14
  55. package/scripts/weekly-report.mjs +211 -0
  56. package/skills/crystallize/SKILL.md +24 -7
  57. package/skills/graph/SKILL.md +4 -0
  58. package/skills/ingest/SKILL.md +29 -5
  59. package/skills/lint/SKILL.md +4 -0
  60. package/skills/query/SKILL.md +4 -0
  61. package/skills/verify/SKILL.md +4 -0
  62. package/templates/.hypoignore +19 -2
  63. package/templates/Home.md +2 -0
  64. package/templates/SCHEMA.md +61 -6
  65. package/templates/extensions/agents/.gitkeep +0 -0
  66. package/templates/extensions/commands/.gitkeep +0 -0
  67. package/templates/extensions/hooks/.gitkeep +0 -0
  68. package/templates/extensions/skills/.gitkeep +0 -0
  69. package/templates/gitignore +5 -0
  70. package/templates/hot.md +2 -0
  71. package/templates/hypo-config.md +1 -1
  72. package/templates/hypo-guide.md +63 -1
  73. package/templates/hypo-help.md +1 -1
  74. package/templates/pages/observability/_index.md +77 -0
  75. package/templates/projects/_template/index.md +2 -2
  76. package/templates/projects/_template/prd.md +1 -1
@@ -3,111 +3,106 @@
3
3
  * hypo-personal-check.mjs — PreCompact hook
4
4
  *
5
5
  * Hard gate before /compact. Blocks if:
6
- * - last substantial wiki op is not a session close
6
+ * - the session-close memory files were not updated this session (fix #17:
7
+ * session-state.md, project hot.md, root hot.md, session-log, log.md)
7
8
  * - wiki git repo has uncommitted/unpushed changes
8
9
  * - hot.md has forbidden structure
9
10
  * - lint blockers exist
10
11
  *
11
- * Bypass options (checked in order, short-circuits before heavy checks):
12
- * 1. wiki-context-critical.json exists (context ≥ 90% — hard limit imminent)
13
- * 2. HYPO_SKIP_GATE=1 env var
14
- * 3. HYPO_SKIP_GATE=1 in a recent *user-role* transcript message
12
+ * Bypass options (checked in order, per ADR 0022 / spec §7.5):
13
+ * 1. HYPO_SKIP_GATE=1 env var
14
+ * 2. HYPO_SKIP_GATE=1 in a recent *user-role* transcript message
15
15
  * (assistant/tool output is excluded to prevent self-triggering from block reason text)
16
+ *
17
+ * NOTE: capacity bypass (wiki-context-critical.json ≥90%) was REMOVED by fix #26
18
+ * (ADR 0022 amendment 2026-05-13). Spec §7.5: even at full context, minimal
19
+ * session-close is mandatory — auto-bypass on capacity caused silent state loss.
16
20
  */
17
21
 
18
22
  import { spawnSync } from 'child_process';
19
23
  import { join } from 'path';
20
- import { readFileSync, existsSync, unlinkSync } from 'fs';
24
+ import { existsSync, unlinkSync } from 'fs';
21
25
  import { homedir } from 'os';
22
26
  import {
23
27
  HYPO_DIR,
24
28
  PKG_ROOT,
25
- lastSubstantialOpIsSession,
26
29
  hypoIsClean,
27
30
  hotMdIsClean,
31
+ sessionCloseFileStatus,
28
32
  readChecklist,
29
33
  isGateSkipped,
34
+ isClosePattern,
35
+ extractUserMessages,
30
36
  } from './hypo-shared.mjs';
31
37
 
32
- const CRITICAL_FILE = join(homedir(), '.claude', 'state', 'wiki-context-critical.json');
33
- const WARNING_FILE = join(homedir(), '.claude', 'state', 'wiki-context-warning.json');
34
-
35
- /** Parse JSONL transcript and return concatenated text of user-role messages only.
36
- *
37
- * Claude Code transcript format: each line is `{ type: "user", message: { role: "user", content: ... } }`.
38
- * Older shape (`{ role, content }` at top level) is also accepted for forward compatibility.
39
- */
40
- function extractUserMessages(transcriptPath) {
41
- try {
42
- const lines = readFileSync(transcriptPath, 'utf-8').split('\n');
43
- const tail = lines.slice(-30); // last 30 lines is enough
44
- return tail.map(line => {
45
- try {
46
- const obj = JSON.parse(line);
47
- const msg = obj.message ?? obj;
48
- const role = msg.role ?? obj.role ?? obj.type;
49
- if (role !== 'user') return '';
50
- const content = msg.content ?? obj.content;
51
- return typeof content === 'string' ? content : JSON.stringify(content);
52
- } catch { return ''; }
53
- }).join('\n');
54
- } catch { return ''; }
55
- }
38
+ const WARNING_FILE = join(homedir(), '.claude', 'state', 'wiki-context-warning.json');
56
39
 
57
40
  let raw = '';
58
41
  process.stdin.setEncoding('utf-8');
59
- process.stdin.on('data', chunk => raw += chunk);
42
+ process.stdin.on('data', (chunk) => (raw += chunk));
60
43
  process.stdin.on('end', () => {
61
44
  let transcriptPath = null;
62
45
  try {
63
46
  const input = JSON.parse(raw || '{}');
64
47
  transcriptPath = input.transcript_path ?? null;
65
- } catch { /* fail-open */ }
66
-
67
- // ── Bypass 1: context critical (≥90%) — short-circuit BEFORE all checks ──
68
- if (existsSync(CRITICAL_FILE)) {
69
- try { unlinkSync(CRITICAL_FILE); } catch {}
70
- console.log(JSON.stringify({
71
- continue: true,
72
- systemMessage: '[WIKI CHECK] gate auto-bypassed (context ≥90% critical). Session close pending next session.',
73
- }));
74
- return;
48
+ } catch {
49
+ /* fail-open */
75
50
  }
76
51
 
52
+ // ── Capacity bypass (≥90%) REMOVED — fix #26, ADR 0022 amendment 2026-05-13.
53
+ // Even at full context, minimal session-close is mandatory (spec §7.5).
54
+ // Bypass paths are now only: HYPO_SKIP_GATE env / HYPO_SKIP_GATE in transcript.
55
+
77
56
  // ── Block 1.5: context warning (≥70%) — request session-compact before compact ──
78
57
  if (existsSync(WARNING_FILE)) {
79
- try { unlinkSync(WARNING_FILE); } catch {}
80
- console.log(JSON.stringify({
81
- decision: 'block',
82
- reason: [
83
- `[WIKI CHECK — BLOCKING] Context ≥70%: run /session-compact before compacting.`,
84
- `STOP. Do NOT compact yet.`,
85
- `1. If Skill tool is available: call it with skill="session-compact" immediately.`,
86
- `2. If Skill tool is unavailable: perform the full session-close checklist from hypo-guide.md.`,
87
- `After session close completes, compact will proceed normally.`,
88
- ``,
89
- `To skip: set HYPO_SKIP_GATE=1`,
90
- ].join('\n'),
91
- }));
58
+ try {
59
+ unlinkSync(WARNING_FILE);
60
+ } catch {}
61
+ console.log(
62
+ JSON.stringify({
63
+ decision: 'block',
64
+ reason: [
65
+ `[WIKI CHECK BLOCKING] Context ≥70%: run /session-compact before compacting.`,
66
+ `STOP. Do NOT compact yet.`,
67
+ `1. If Skill tool is available: call it with skill="session-compact" immediately.`,
68
+ `2. If Skill tool is unavailable: perform the full session-close checklist from hypo-guide.md.`,
69
+ `After session close completes, compact will proceed normally.`,
70
+ ``,
71
+ `To skip: set HYPO_SKIP_GATE=1`,
72
+ ].join('\n'),
73
+ }),
74
+ );
92
75
  return;
93
76
  }
94
77
 
95
- // ── Bypass 2: env var ──
96
- if (!isGateSkipped() && transcriptPath && existsSync(transcriptPath)) {
97
- // Only scan user-role messages to avoid matching the block reason text
98
- // which itself contains "Bypass with HYPO_SKIP_GATE=1"
78
+ // ── Transcript scan (Bypass 2 + #20 close-intent detection) ──
79
+ let hasCloseIntent = false;
80
+ if (transcriptPath && existsSync(transcriptPath)) {
99
81
  const userText = extractUserMessages(transcriptPath);
100
- if (/HYPO_SKIP_GATE=1/.test(userText)) {
82
+ // Bypass 2: user-role "HYPO_SKIP_GATE=1" (scan before gate so bypass takes effect)
83
+ if (!isGateSkipped() && /HYPO_SKIP_GATE=1/.test(userText)) {
101
84
  process.env.HYPO_SKIP_GATE = '1';
102
85
  }
86
+ // #20: natural-language close-intent detection (informational — enriches block message)
87
+ hasCloseIntent = isClosePattern(userText);
103
88
  }
104
89
 
105
90
  // ── Heavy checks ──
106
91
  const today = new Date().toISOString().slice(0, 10);
107
92
 
108
- const hasSession = lastSubstantialOpIsSession();
109
- const gitStatus = hypoIsClean();
110
- const hotStatus = hotMdIsClean();
93
+ const gitStatus = hypoIsClean();
94
+ const hotStatus = hotMdIsClean();
95
+ // fix #17: strict session-close (steps 1~6 of the 11-step crystallize
96
+ // checklist). closeFiles gates the 5 mandatory files (steps 1-4 + log.md);
97
+ // open-questions.md (step 5) is conditional ("변경 시") and intentionally
98
+ // ungated — see hypo-shared.mjs sessionCloseFileStatus and spec §5.2.7.
99
+ const closeFiles = sessionCloseFileStatus(HYPO_DIR);
100
+ const closeFilesReason = closeFiles.ok
101
+ ? ''
102
+ : `memory files not updated this session: ${[
103
+ ...closeFiles.missing.map((f) => `${f} (missing)`),
104
+ ...closeFiles.stale.map((f) => `${f} (stale)`),
105
+ ].join(', ')}`;
111
106
 
112
107
  const lintPath = PKG_ROOT ? join(PKG_ROOT, 'scripts', 'lint.mjs') : null;
113
108
  let lintBlockers = [];
@@ -124,14 +119,97 @@ process.stdin.on('end', () => {
124
119
  });
125
120
  const parsed = JSON.parse(r.stdout || '{}');
126
121
  lintBlockers = parsed.errors || [];
127
- lintW8 = (parsed.warns || []).filter(w => w.id === 'W8');
128
- } catch { /* fail-open */ }
122
+ lintW8 = (parsed.warns || []).filter((w) => w.id === 'W8');
123
+ } catch (err) {
124
+ /* fail-open */
125
+ process.stderr.write(`[hypo-personal-check] error: ${err?.message ?? String(err)}\n`);
126
+ }
129
127
  }
130
128
 
131
- const lintOk = lintBlockers.length === 0;
129
+ const lintOk = lintBlockers.length === 0;
132
130
  const designHistoryOk = lintW8.length === 0;
133
131
 
134
- if (hasSession && gitStatus.clean && hotStatus.clean && lintOk && designHistoryOk) {
132
+ // ── fix #37 Phase C: feedback projection drift (ADR 0031) ──
133
+ // Single blocking gate invariant (spec §7.5): integrate into THIS hook, never
134
+ // add a separate PreCompact hook. `feedback-sync --check --strict` reports
135
+ // projection drift (wiki feedback SoT vs MEMORY / CLAUDE.md learned-behaviors
136
+ // projection). `--no-input` keeps this non-TTY hook from ever blocking on a
137
+ // prompt, and the engine's skip-MEMORY warning is *soft* (never escalated by
138
+ // --strict) so a fresh / external user whose ~/.claude/projects/<id> dir does
139
+ // not exist yet is never gated (contract §5 step 4). Fail-open on any spawn
140
+ // error, exactly like the lint check above.
141
+ const feedbackPath = PKG_ROOT ? join(PKG_ROOT, 'scripts', 'feedback-sync.mjs') : null;
142
+ let feedbackOk = true;
143
+ let feedbackReason = '';
144
+ let feedbackSkipped = false;
145
+ if (!feedbackPath || !existsSync(feedbackPath)) {
146
+ feedbackSkipped = true;
147
+ } else {
148
+ try {
149
+ const r = spawnSync(
150
+ process.execPath,
151
+ [
152
+ feedbackPath,
153
+ '--check',
154
+ '--strict',
155
+ '--no-input',
156
+ '--json',
157
+ `--hypo-dir=${HYPO_DIR}`,
158
+ `--claude-home=${join(homedir(), '.claude')}`,
159
+ ],
160
+ { encoding: 'utf-8', timeout: 30000 },
161
+ );
162
+ if (r.error || r.status === null) {
163
+ feedbackSkipped = true; // spawn failure → fail-open (never block on tooling)
164
+ } else if (r.status !== 0) {
165
+ // exit≠0 alone is ambiguous. A *missing* target file (e.g. a system
166
+ // whose ~/.claude/CLAUDE.md was never created) reports buildError +
167
+ // exit 1, which is benign — there is nothing to gate. Decide from the
168
+ // JSON report's per-target state instead of the raw exit code: block
169
+ // ONLY when some target has a genuine, actionable issue (drift,
170
+ // conflict, over-cap, or a malformed managed region). buildError is
171
+ // never actionable here, so any mix that lacks a real issue fails open
172
+ // — including memory:clean + claude:buildError (codex review: the prior
173
+ // `every(buildError)` predicate wrongly blocked that case). Mirrors
174
+ // doctor's buildError→warn (non-fatal) handling.
175
+ let report = null;
176
+ try {
177
+ report = JSON.parse(r.stdout || '');
178
+ } catch {
179
+ /* unparseable → fail-open below */
180
+ }
181
+ const targets = report ? Object.values(report.targets || {}) : [];
182
+ const conflicted = targets.some(
183
+ (t) =>
184
+ t.intruder || t.unpaired || t.outOfContainer || (t.conflicts && t.conflicts.length),
185
+ );
186
+ const overCap = targets.some((t) => t.overCap);
187
+ const drifted = targets.some((t) => t.dirty);
188
+ if (!report || !(conflicted || overCap || drifted)) {
189
+ feedbackSkipped = true; // missing target / pure warning / unparseable → fail-open
190
+ } else {
191
+ feedbackOk = false;
192
+ feedbackReason = conflicted
193
+ ? 'feedback projection conflict (manual edit) — run `hypomnema feedback-sync --import-target-change --from=<memory|claude>`'
194
+ : overCap
195
+ ? 'feedback projection over cap — demote/archive feedback pages'
196
+ : 'feedback projection drift — run `hypomnema feedback-sync --write`';
197
+ }
198
+ }
199
+ } catch (err) {
200
+ feedbackSkipped = true;
201
+ process.stderr.write(`[hypo-personal-check] error: ${err?.message ?? String(err)}\n`);
202
+ }
203
+ }
204
+
205
+ if (
206
+ gitStatus.clean &&
207
+ hotStatus.clean &&
208
+ lintOk &&
209
+ designHistoryOk &&
210
+ closeFiles.ok &&
211
+ feedbackOk
212
+ ) {
135
213
  console.log(JSON.stringify({ continue: true, suppressOutput: true }));
136
214
  return;
137
215
  }
@@ -139,57 +217,74 @@ process.stdin.on('end', () => {
139
217
  // ── Bypass 3: HYPO_SKIP_GATE ──
140
218
  if (isGateSkipped()) {
141
219
  const skipped = [
142
- !hasSession ? 'session log missing' : '',
143
- !gitStatus.clean ? gitStatus.reason : '',
144
- !hotStatus.clean ? hotStatus.reason : '',
220
+ !gitStatus.clean ? gitStatus.reason : '',
221
+ !hotStatus.clean ? hotStatus.reason : '',
222
+ !closeFiles.ok ? closeFilesReason : '',
145
223
  !designHistoryOk ? `design-history stale (${lintW8.length})` : '',
146
- lintSkipped ? 'lint skipped (hypo-pkg.json missing)' : '',
147
- ].filter(Boolean).join(', ');
148
- console.log(JSON.stringify({
149
- continue: true,
150
- systemMessage: `[WIKI CHECK] gate bypassed via HYPO_SKIP_GATE=1 (incomplete: ${skipped}).`,
151
- }));
224
+ !feedbackOk ? feedbackReason : '',
225
+ lintSkipped ? 'lint skipped (hypo-pkg.json missing)' : '',
226
+ feedbackSkipped ? 'feedback-sync skipped (hypo-pkg.json missing)' : '',
227
+ ]
228
+ .filter(Boolean)
229
+ .join(', ');
230
+ console.log(
231
+ JSON.stringify({
232
+ continue: true,
233
+ systemMessage: `[WIKI CHECK] gate bypassed via HYPO_SKIP_GATE=1 (incomplete: ${skipped}).`,
234
+ }),
235
+ );
152
236
  return;
153
237
  }
154
238
 
155
239
  // ── Block ──
156
240
  const reasons = [
157
- !hasSession ? 'session log entry missing' : '',
158
- !gitStatus.clean ? gitStatus.reason : '',
159
- !hotStatus.clean ? hotStatus.reason : '',
160
- !lintOk ? `lint blockers: ${lintBlockers.map(b => b.id).join(', ')}` : '',
161
- !designHistoryOk ? `design-history stale: ${lintW8.map(w => w.file.split('/')[1]).join(', ')}` : '',
162
- lintSkipped ? 'lint skipped (run `hypomnema init` to enable lint gate)' : '',
241
+ !gitStatus.clean ? gitStatus.reason : '',
242
+ !hotStatus.clean ? hotStatus.reason : '',
243
+ !closeFiles.ok ? closeFilesReason : '',
244
+ !lintOk ? `lint blockers: ${lintBlockers.map((b) => b.id).join(', ')}` : '',
245
+ !designHistoryOk
246
+ ? `design-history stale: ${lintW8.map((w) => w.file.split('/')[1]).join(', ')}`
247
+ : '',
248
+ !feedbackOk ? feedbackReason : '',
249
+ lintSkipped ? 'lint skipped (run `hypomnema init` to enable lint gate)' : '',
163
250
  ].filter(Boolean);
164
251
 
165
- const checklist = readChecklist(today);
166
- const checklistText = checklist ?? [
167
- ` [ ] 0. Read SCHEMA.md + hypo-guide.md (required before wiki work)`,
168
- ` [ ] 1. PRD — create projects/<name>/prd.md if missing`,
169
- ` [ ] 2. ADR — decide yes/no on 5 types; if all N, note "no ADR — reason: <why>"`,
170
- ` [ ] 3. Ingest if new external knowledge, save to sources/ and ingest`,
171
- ` [ ] 4. Pages extract new concepts/patterns to pages/`,
172
- ` [ ] 5. Synthesis — if 3+ cross-page analysis results, save to pages/syntheses/`,
173
- ` [ ] 6. session-log append to projects/<name>/session-log/YYYY-MM.md`,
174
- ` [ ] 7. index.md update Projects section if needed`,
175
- ` [ ] 8. log.md — append ## [${today}] session | <project-name>`,
176
- ` [ ] 9. hot.md — update projects/<name>/hot.md (no exceptions)`,
177
- ` [ ] 10. root hot.md update ~/hypomnema/hot.md active project table`,
178
- ` [ ] 11. updated: field verify today's date on all touched .md files`,
179
- ` [ ] 12. git commit & push`,
180
- ].join('\n');
181
-
182
- console.log(JSON.stringify({
183
- decision: 'block',
184
- reason: [
185
- `[WIKI CHECKBLOCKING] Session close incomplete. (${reasons.join(', ')})`,
186
- `Run the checklist below in order, then retry /compact:`,
187
- ``,
188
- checklistText,
189
- ``,
190
- `Trivial session? Bypass with HYPO_SKIP_GATE=1`,
191
- ].join('\n'),
192
- continue: false,
193
- stopReason: `Session close incomplete: ${reasons.join(', ')}`,
194
- }));
252
+ const checklist = readChecklist(today);
253
+ const checklistText =
254
+ checklist ??
255
+ [
256
+ ` [ ] 0. Read SCHEMA.md + hypo-guide.md (required before wiki work)`,
257
+ ` [ ] 1. PRD create projects/<name>/prd.md if missing`,
258
+ ` [ ] 2. ADR decide yes/no on 5 types; if all N, note "no ADR — reason: <why>"`,
259
+ ` [ ] 3. Ingest — if new external knowledge, save to sources/ and ingest`,
260
+ ` [ ] 4. Pages extract new concepts/patterns to pages/`,
261
+ ` [ ] 5. Synthesis if 3+ cross-page analysis results, save to pages/syntheses/`,
262
+ ` [ ] 6. session-log — append to projects/<name>/session-log/YYYY-MM.md`,
263
+ ` [ ] 7. index.md — update Projects section if needed`,
264
+ ` [ ] 8. log.md append ## [${today}] session | <project-name>`,
265
+ ` [ ] 9. hot.md update projects/<name>/hot.md (no exceptions)`,
266
+ ` [ ] 10. root hot.md update ~/hypomnema/hot.md active project table`,
267
+ ` [ ] 11. updated: field — verify today's date on all touched .md files`,
268
+ ` [ ] 12. git commit & push`,
269
+ ].join('\n');
270
+
271
+ const closeIntentNote = hasCloseIntent
272
+ ? `[Close intent detected in recent messages completing session close first.]\n`
273
+ : '';
274
+
275
+ console.log(
276
+ JSON.stringify({
277
+ decision: 'block',
278
+ reason: [
279
+ `${closeIntentNote}[WIKI CHECK — BLOCKING] Session close incomplete. (${reasons.join(', ')})`,
280
+ `Run the checklist below in order, then retry /compact:`,
281
+ ``,
282
+ checklistText,
283
+ ``,
284
+ `Trivial session? Bypass with HYPO_SKIP_GATE=1`,
285
+ ].join('\n'),
286
+ continue: false,
287
+ stopReason: `Session close incomplete: ${reasons.join(', ')}`,
288
+ }),
289
+ );
195
290
  });
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * hypo-pre-commit.mjs — wiki git pre-commit hook worker (§6.8 fix #24)
4
+ *
5
+ * Blocks staged files that match .hypoignore patterns.
6
+ * Installed by `hypo init` to <wiki>/.git/hooks/pre-commit.
7
+ * The shell wrapper in that file calls: node <pkgRoot>/hooks/hypo-pre-commit.mjs
8
+ */
9
+
10
+ import { spawnSync } from 'child_process';
11
+ import { join } from 'path';
12
+ import { loadHypoIgnore, isIgnored } from '../scripts/lib/hypo-ignore.mjs';
13
+
14
+ // Detect wiki root = git top-level of the wiki repo
15
+ const gitRoot = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf-8' });
16
+ if (gitRoot.status !== 0) process.exit(0);
17
+
18
+ const hypoDir = gitRoot.stdout.replace(/\r?\n$/, '');
19
+
20
+ // Get staged files (NUL-separated for safe handling of special chars)
21
+ const staged = spawnSync('git', ['diff', '--cached', '--name-only', '-z'], {
22
+ encoding: 'utf-8',
23
+ cwd: hypoDir,
24
+ });
25
+ if (staged.status !== 0 || !staged.stdout) process.exit(0);
26
+
27
+ const files = staged.stdout
28
+ .split('\0')
29
+ .filter(Boolean)
30
+ .map((f) => join(hypoDir, f));
31
+ const patterns = loadHypoIgnore(hypoDir);
32
+
33
+ if (patterns.length === 0) process.exit(0);
34
+
35
+ const blocked = files.filter((f) => isIgnored(f, hypoDir, patterns));
36
+ if (blocked.length === 0) process.exit(0);
37
+
38
+ const rel = blocked.map((f) => f.slice(hypoDir.length + 1));
39
+ process.stderr.write(
40
+ `[hypo] Commit blocked — staged files match .hypoignore patterns:\n` +
41
+ rel.map((f) => ` ${f}`).join('\n') +
42
+ '\n' +
43
+ `\nUnstage with: git restore --staged <file>\n` +
44
+ `Override (at your own risk): git commit --no-verify\n`,
45
+ );
46
+ process.exit(1);
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * hypo-session-end.mjs — SessionEnd hook (fix #25 PR-A2, ADR 0022 Layer 2)
4
+ *
5
+ * `/clear` cannot be blocked: it never fires UserPromptSubmit (Stage 0 PoC,
6
+ * 2026-05-14). The only intervention point is the SessionEnd(reason='clear')
7
+ * → SessionStart(source='clear') pair. This hook captures the dying session's
8
+ * identity into `.cache/clear-marker.json` so hypo-session-start can issue a
9
+ * recovery nudge on the next session.
10
+ *
11
+ * Scope: writing the marker on reason='clear' only. Other reasons
12
+ * ('prompt_input_exit', 'logout', etc.) are deliberate user exits and need no
13
+ * recovery — touching them would pollute the unrelated next session.
14
+ *
15
+ * Silent: never blocks, never emits user-visible output. Failures are stderr
16
+ * debug lines only.
17
+ */
18
+
19
+ import { HYPO_DIR, writeClearMarker } from './hypo-shared.mjs';
20
+ import { existsSync } from 'fs';
21
+
22
+ function emitContinue() {
23
+ console.log(JSON.stringify({ continue: true, suppressOutput: true }));
24
+ }
25
+
26
+ let raw = '';
27
+ process.stdin.setEncoding('utf-8');
28
+ process.stdin.on('data', (chunk) => (raw += chunk));
29
+ process.stdin.on('end', () => {
30
+ try {
31
+ let payload = {};
32
+ try {
33
+ payload = JSON.parse(raw) || {};
34
+ } catch {}
35
+
36
+ const reason = payload.reason || '';
37
+ if (reason !== 'clear') {
38
+ emitContinue();
39
+ return;
40
+ }
41
+
42
+ // Wiki absent → nothing to recover into; skip silently.
43
+ if (!existsSync(HYPO_DIR)) {
44
+ emitContinue();
45
+ return;
46
+ }
47
+
48
+ writeClearMarker(HYPO_DIR, {
49
+ prev_session_id: payload.session_id || payload.sessionId || null,
50
+ prev_transcript_path: payload.transcript_path || payload.transcriptPath || null,
51
+ prev_cwd: payload.cwd || null,
52
+ });
53
+ } catch (err) {
54
+ // Best-effort: a marker failure must not break /clear itself.
55
+ process.stderr.write(`[hypo-session-end] error: ${err?.message ?? String(err)}\n`);
56
+ }
57
+ emitContinue();
58
+ });
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * hypo-session-record.mjs — Stop hook
4
+ *
5
+ * Appends an entry to ~/hypomnema/.cache/sessions/index.jsonl for each
6
+ * completed session. The index.jsonl is the **primary** source for
7
+ * scripts/session-audit.mjs (which falls back to ~/.claude/projects/<encoded>/
8
+ * if the index is empty or missing — see ADR 0019).
9
+ *
10
+ * Silent: never blocks, never emits user-visible output.
11
+ */
12
+
13
+ import { existsSync, mkdirSync, appendFileSync } from 'fs';
14
+ import { dirname, join } from 'path';
15
+ import { HYPO_DIR } from './hypo-shared.mjs';
16
+
17
+ const INDEX_PATH = join(HYPO_DIR, '.cache', 'sessions', 'index.jsonl');
18
+
19
+ function emitContinue() {
20
+ console.log(JSON.stringify({ continue: true, suppressOutput: true }));
21
+ }
22
+
23
+ let raw = '';
24
+ process.stdin.setEncoding('utf-8');
25
+ process.stdin.on('data', (chunk) => (raw += chunk));
26
+ process.stdin.on('end', () => {
27
+ try {
28
+ let payload = {};
29
+ try {
30
+ payload = JSON.parse(raw) || {};
31
+ } catch {}
32
+
33
+ const transcriptPath = payload.transcript_path || payload.transcriptPath || null;
34
+ const sessionId = payload.session_id || payload.sessionId || null;
35
+ if (!transcriptPath || !sessionId) {
36
+ // Older Claude Code (no transcript_path) — fallback path in
37
+ // scripts/session-audit.mjs handles this case.
38
+ emitContinue();
39
+ return;
40
+ }
41
+
42
+ if (!existsSync(HYPO_DIR)) {
43
+ emitContinue();
44
+ return;
45
+ }
46
+ mkdirSync(dirname(INDEX_PATH), { recursive: true });
47
+
48
+ const entry = {
49
+ session_id: sessionId,
50
+ transcript_path: transcriptPath,
51
+ recorded_at: new Date().toISOString(),
52
+ cwd: payload.cwd || process.cwd(),
53
+ };
54
+ appendFileSync(INDEX_PATH, JSON.stringify(entry) + '\n');
55
+ } catch (err) {
56
+ // Audit is best-effort observability — never let it block session close.
57
+ process.stderr.write(`[hypo-session-record] error: ${err?.message ?? String(err)}\n`);
58
+ }
59
+ emitContinue();
60
+ });