hypomnema 1.1.0 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +4 -2
- package/README.md +4 -2
- package/commands/audit.md +2 -2
- package/commands/crystallize.md +113 -23
- package/commands/feedback.md +40 -26
- package/commands/ingest.md +31 -9
- package/commands/upgrade.md +2 -2
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONTRIBUTING.md +1 -1
- package/hooks/hooks.json +30 -1
- package/hooks/hypo-auto-commit.mjs +10 -4
- package/hooks/hypo-auto-minimal-crystallize.mjs +145 -0
- package/hooks/hypo-auto-stage.mjs +4 -3
- package/hooks/hypo-compact-guard.mjs +33 -24
- package/hooks/hypo-cwd-change.mjs +107 -24
- package/hooks/hypo-file-watch.mjs +23 -10
- package/hooks/hypo-first-prompt.mjs +37 -23
- package/hooks/hypo-hot-rebuild.mjs +22 -10
- package/hooks/hypo-lookup.mjs +171 -65
- package/hooks/hypo-personal-check.mjs +207 -112
- package/hooks/hypo-pre-commit.mjs +46 -0
- package/hooks/hypo-session-end.mjs +58 -0
- package/hooks/hypo-session-record.mjs +11 -5
- package/hooks/hypo-session-start.mjs +298 -52
- package/hooks/hypo-shared.mjs +793 -37
- package/hooks/hypo-web-fetch-ingest.mjs +121 -0
- package/hooks/version-check-fetch.mjs +74 -0
- package/hooks/version-check.mjs +184 -0
- package/package.json +17 -3
- package/scripts/crystallize.mjs +623 -18
- package/scripts/doctor.mjs +730 -47
- package/scripts/feedback-sync.mjs +974 -0
- package/scripts/feedback.mjs +253 -44
- package/scripts/graph.mjs +35 -22
- package/scripts/ingest.mjs +89 -16
- package/scripts/init.mjs +398 -113
- package/scripts/lib/design-history-stale.mjs +83 -0
- package/scripts/lib/extensions.mjs +749 -0
- package/scripts/lib/frontmatter.mjs +5 -1
- package/scripts/lib/hypo-ignore.mjs +12 -10
- package/scripts/lib/pkg-json.mjs +23 -5
- package/scripts/lib/project-create.mjs +225 -0
- package/scripts/lib/schema-vocab.mjs +96 -0
- package/scripts/lint.mjs +238 -31
- package/scripts/query.mjs +26 -10
- package/scripts/resume.mjs +11 -5
- package/scripts/session-audit.mjs +37 -27
- package/scripts/smoke-pack.mjs +224 -0
- package/scripts/stats.mjs +24 -10
- package/scripts/uninstall.mjs +363 -49
- package/scripts/upgrade.mjs +706 -202
- package/scripts/verify.mjs +24 -14
- package/scripts/weekly-report.mjs +59 -25
- package/skills/crystallize/SKILL.md +20 -7
- package/skills/ingest/SKILL.md +25 -5
- package/templates/.hypoignore +16 -2
- package/templates/Home.md +2 -0
- package/templates/SCHEMA.md +61 -6
- package/templates/extensions/agents/.gitkeep +0 -0
- package/templates/extensions/commands/.gitkeep +0 -0
- package/templates/extensions/hooks/.gitkeep +0 -0
- package/templates/extensions/skills/.gitkeep +0 -0
- package/templates/gitignore +5 -0
- package/templates/hot.md +2 -0
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +42 -2
- package/templates/hypo-help.md +1 -1
- package/templates/pages/observability/_index.md +77 -0
- package/templates/projects/_template/index.md +2 -2
- 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
|
-
* -
|
|
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,
|
|
12
|
-
* 1.
|
|
13
|
-
* 2. HYPO_SKIP_GATE=1
|
|
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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
|
109
|
-
const
|
|
110
|
-
|
|
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 {
|
|
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
|
|
129
|
+
const lintOk = lintBlockers.length === 0;
|
|
132
130
|
const designHistoryOk = lintW8.length === 0;
|
|
133
131
|
|
|
134
|
-
|
|
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
|
-
!
|
|
143
|
-
!
|
|
144
|
-
!
|
|
220
|
+
!gitStatus.clean ? gitStatus.reason : '',
|
|
221
|
+
!hotStatus.clean ? hotStatus.reason : '',
|
|
222
|
+
!closeFiles.ok ? closeFilesReason : '',
|
|
145
223
|
!designHistoryOk ? `design-history stale (${lintW8.length})` : '',
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
!
|
|
158
|
-
!
|
|
159
|
-
!
|
|
160
|
-
!lintOk
|
|
161
|
-
!designHistoryOk
|
|
162
|
-
|
|
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
|
|
166
|
-
const checklistText =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
].
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
+
});
|
|
@@ -22,14 +22,16 @@ function emitContinue() {
|
|
|
22
22
|
|
|
23
23
|
let raw = '';
|
|
24
24
|
process.stdin.setEncoding('utf-8');
|
|
25
|
-
process.stdin.on('data', chunk => raw += chunk);
|
|
25
|
+
process.stdin.on('data', (chunk) => (raw += chunk));
|
|
26
26
|
process.stdin.on('end', () => {
|
|
27
27
|
try {
|
|
28
28
|
let payload = {};
|
|
29
|
-
try {
|
|
29
|
+
try {
|
|
30
|
+
payload = JSON.parse(raw) || {};
|
|
31
|
+
} catch {}
|
|
30
32
|
|
|
31
33
|
const transcriptPath = payload.transcript_path || payload.transcriptPath || null;
|
|
32
|
-
const sessionId
|
|
34
|
+
const sessionId = payload.session_id || payload.sessionId || null;
|
|
33
35
|
if (!transcriptPath || !sessionId) {
|
|
34
36
|
// Older Claude Code (no transcript_path) — fallback path in
|
|
35
37
|
// scripts/session-audit.mjs handles this case.
|
|
@@ -37,7 +39,10 @@ process.stdin.on('end', () => {
|
|
|
37
39
|
return;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
if (!existsSync(HYPO_DIR)) {
|
|
42
|
+
if (!existsSync(HYPO_DIR)) {
|
|
43
|
+
emitContinue();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
41
46
|
mkdirSync(dirname(INDEX_PATH), { recursive: true });
|
|
42
47
|
|
|
43
48
|
const entry = {
|
|
@@ -47,8 +52,9 @@ process.stdin.on('end', () => {
|
|
|
47
52
|
cwd: payload.cwd || process.cwd(),
|
|
48
53
|
};
|
|
49
54
|
appendFileSync(INDEX_PATH, JSON.stringify(entry) + '\n');
|
|
50
|
-
} catch {
|
|
55
|
+
} catch (err) {
|
|
51
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`);
|
|
52
58
|
}
|
|
53
59
|
emitContinue();
|
|
54
60
|
});
|