hypomnema 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.ko.md +10 -10
- package/README.md +10 -10
- package/commands/crystallize.md +8 -8
- package/commands/feedback.md +1 -1
- package/commands/resume.md +1 -1
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/CONTRIBUTING.md +2 -2
- package/hooks/hypo-auto-minimal-crystallize.mjs +57 -11
- package/hooks/hypo-compact-guard.mjs +1 -1
- package/hooks/hypo-cwd-change.mjs +1 -1
- package/hooks/hypo-first-prompt.mjs +2 -2
- package/hooks/hypo-hot-rebuild.mjs +14 -1
- package/hooks/hypo-personal-check.mjs +91 -179
- package/hooks/hypo-pre-commit.mjs +1 -1
- package/hooks/hypo-session-end.mjs +1 -1
- package/hooks/hypo-session-start.mjs +7 -7
- package/hooks/hypo-shared.mjs +788 -72
- package/hooks/hypo-web-fetch-ingest.mjs +2 -2
- package/hooks/version-check-fetch.mjs +2 -8
- package/hooks/version-check.mjs +18 -0
- package/package.json +3 -2
- package/scripts/check-tracker-ids.mjs +329 -0
- package/scripts/crystallize.mjs +244 -109
- package/scripts/doctor.mjs +6 -9
- package/scripts/feedback-sync.mjs +1 -1
- package/scripts/init.mjs +1 -1
- package/scripts/install-git-hooks.mjs +75 -40
- package/scripts/lib/check-tracker-ids.mjs +140 -0
- package/scripts/lib/design-history-stale.mjs +59 -14
- package/scripts/lib/extensions.mjs +4 -4
- package/scripts/lib/fix-manifest.mjs +2 -2
- package/scripts/lib/fix-status-verify.mjs +5 -4
- package/scripts/lib/plugin-detect.mjs +15 -6
- package/scripts/lib/project-create.mjs +1 -1
- package/scripts/lint.mjs +63 -8
- package/scripts/rename.mjs +373 -0
- package/scripts/resume.mjs +75 -19
- package/scripts/uninstall.mjs +1 -1
- package/scripts/upgrade.mjs +26 -25
- package/skills/crystallize/SKILL.md +10 -7
- package/templates/SCHEMA.md +2 -2
- package/templates/hypo-config.md +2 -2
- package/templates/hypo-guide.md +20 -1
- package/templates/projects/_template/index.md +1 -1
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* hypo-personal-check.mjs — PreCompact hook
|
|
4
4
|
*
|
|
5
5
|
* Hard gate before /compact. Blocks if:
|
|
6
|
-
* - the session-close memory files were not updated this session
|
|
7
|
-
* session-state.md, project hot.md, root hot.md, session-log, log.md)
|
|
6
|
+
* - the session-close memory files were not updated this session
|
|
7
|
+
* (session-state.md, project hot.md, root hot.md, session-log, log.md)
|
|
8
8
|
* - wiki git repo has uncommitted/unpushed changes
|
|
9
9
|
* - hot.md has forbidden structure
|
|
10
10
|
* - lint blockers exist
|
|
@@ -14,7 +14,7 @@
|
|
|
14
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
16
|
*
|
|
17
|
-
* NOTE: capacity bypass (wiki-context-critical.json ≥90%) was REMOVED
|
|
17
|
+
* NOTE: capacity bypass (wiki-context-critical.json ≥90%) was REMOVED
|
|
18
18
|
* (ADR 0022 amendment 2026-05-13). Spec §7.5: even at full context, minimal
|
|
19
19
|
* session-close is mandatory — auto-bypass on capacity caused silent state loss.
|
|
20
20
|
*/
|
|
@@ -26,16 +26,11 @@ import { homedir } from 'os';
|
|
|
26
26
|
import {
|
|
27
27
|
HYPO_DIR,
|
|
28
28
|
PKG_ROOT,
|
|
29
|
-
|
|
30
|
-
hotMdIsClean,
|
|
31
|
-
sessionCloseFileStatus,
|
|
29
|
+
precompactGateStatus,
|
|
32
30
|
readChecklist,
|
|
33
31
|
isGateSkipped,
|
|
34
32
|
isClosePattern,
|
|
35
33
|
extractUserMessages,
|
|
36
|
-
extractTouchedWikiFiles,
|
|
37
|
-
closeFileTargets,
|
|
38
|
-
partitionLintScope,
|
|
39
34
|
} from './hypo-shared.mjs';
|
|
40
35
|
|
|
41
36
|
const WARNING_FILE = join(homedir(), '.claude', 'state', 'wiki-context-warning.json');
|
|
@@ -45,14 +40,19 @@ process.stdin.setEncoding('utf-8');
|
|
|
45
40
|
process.stdin.on('data', (chunk) => (raw += chunk));
|
|
46
41
|
process.stdin.on('end', () => {
|
|
47
42
|
let transcriptPath = null;
|
|
43
|
+
let sessionId = null;
|
|
48
44
|
try {
|
|
49
45
|
const input = JSON.parse(raw || '{}');
|
|
50
46
|
transcriptPath = input.transcript_path ?? null;
|
|
47
|
+
// A log-only marker for this session activates log-only gate
|
|
48
|
+
// semantics (no project attribution) so /compact does not block a closed
|
|
49
|
+
// non-project session on the active/phantom project's files.
|
|
50
|
+
sessionId = input.session_id ?? input.sessionId ?? null;
|
|
51
51
|
} catch {
|
|
52
52
|
/* fail-open */
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// ── Capacity bypass (≥90%) REMOVED —
|
|
55
|
+
// ── Capacity bypass (≥90%) REMOVED — ADR 0022 amendment 2026-05-13.
|
|
56
56
|
// Even at full context, minimal session-close is mandatory (spec §7.5).
|
|
57
57
|
// Bypass paths are now only: HYPO_SKIP_GATE env / HYPO_SKIP_GATE in transcript.
|
|
58
58
|
|
|
@@ -93,164 +93,82 @@ process.stdin.on('end', () => {
|
|
|
93
93
|
// ── Heavy checks ──
|
|
94
94
|
const today = new Date().toISOString().slice(0, 10);
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
96
|
+
// The full PreCompact gate decision, single-sourced (ADR 0046). The SAME
|
|
97
|
+
// function backs `crystallize --check-session-close`, so a green self-check
|
|
98
|
+
// there means this hook will not block. precompactGateStatus runs git-clean +
|
|
99
|
+
// hot.md structure + session-close files (ADR 0043 global invariant) + scoped
|
|
100
|
+
// lint + W8 design-history + feedback projection. The transcript widens the
|
|
101
|
+
// lint scope to this session's edited files (ADR 0041); without one the scope
|
|
102
|
+
// is the mandatory close files. Read-only: pure feedback drift comes back as
|
|
103
|
+
// gate.driftTargets, a self-heal effect requirement we run as --write below.
|
|
104
|
+
let gate;
|
|
105
|
+
try {
|
|
106
|
+
gate = precompactGateStatus(HYPO_DIR, {
|
|
107
|
+
transcriptPath,
|
|
108
|
+
...(sessionId ? { sessionId } : {}),
|
|
109
|
+
});
|
|
110
|
+
} catch (err) {
|
|
111
|
+
// Defense-in-depth: precompactGateStatus fails open per-check, but if it ever
|
|
112
|
+
// throws, never crash the PreCompact hook — fail open (continue) so a tooling
|
|
113
|
+
// fault can't wedge /compact.
|
|
114
|
+
process.stderr.write(`[hypo-personal-check] error: ${err?.message ?? String(err)}\n`);
|
|
115
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
// Self-heal pure feedback projection drift (ADR 0045): the one mutation the
|
|
120
|
+
// read-only gate leaves to the caller. Fails CLOSED — if the --write errors we
|
|
121
|
+
// turn the (otherwise non-blocking) drift into a blocker, since real drift is
|
|
122
|
+
// confirmed and silently passing it would defeat the gate. --write only applies
|
|
123
|
+
// when no target conflicts/over-caps (code===0 across ALL targets), so a late
|
|
124
|
+
// race exits non-zero and blocks here. It is semantic-preflight atomic but not
|
|
125
|
+
// filesystem-atomic (concurrent edit / mid-write I/O fault is best-effort) —
|
|
126
|
+
// pre-existing engine behavior, not introduced by the self-heal.
|
|
127
|
+
let feedbackHealed = '';
|
|
128
|
+
if (gate.ok && gate.driftTargets.length > 0) {
|
|
129
|
+
const feedbackPath = PKG_ROOT ? join(PKG_ROOT, 'scripts', 'feedback-sync.mjs') : null;
|
|
130
|
+
const w = feedbackPath
|
|
131
|
+
? spawnSync(
|
|
132
|
+
process.execPath,
|
|
133
|
+
[
|
|
134
|
+
feedbackPath,
|
|
135
|
+
'--write',
|
|
136
|
+
'--no-input',
|
|
137
|
+
`--hypo-dir=${HYPO_DIR}`,
|
|
138
|
+
`--claude-home=${join(homedir(), '.claude')}`,
|
|
139
|
+
],
|
|
140
|
+
{ encoding: 'utf-8', timeout: 30000 },
|
|
141
|
+
)
|
|
142
|
+
: { status: 1 };
|
|
143
|
+
if (w.error || w.status === null || w.status !== 0) {
|
|
144
|
+
gate.ok = false;
|
|
145
|
+
gate.blockers.push({
|
|
146
|
+
type: 'feedback',
|
|
147
|
+
reason: `feedback projection drift (${gate.driftTargets.join(', ')}) — auto-sync failed; run \`hypomnema feedback-sync --write\` manually`,
|
|
123
148
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const allW8 = (parsed.warns || []).filter((w) => w.id === 'W8');
|
|
127
|
-
// Bug B: judge this session on the files IT touched, not the whole vault.
|
|
128
|
-
// A readable transcript lets us scope (edited files ∪ mandatory close
|
|
129
|
-
// files); a missing/unreadable transcript falls back to the conservative
|
|
130
|
-
// global gate (never weaker than before).
|
|
131
|
-
const haveTranscript = !!(transcriptPath && existsSync(transcriptPath));
|
|
132
|
-
if (haveTranscript) {
|
|
133
|
-
const scope = new Set([
|
|
134
|
-
...extractTouchedWikiFiles(transcriptPath, HYPO_DIR),
|
|
135
|
-
...closeFileTargets(HYPO_DIR),
|
|
136
|
-
]);
|
|
137
|
-
const part = partitionLintScope(allErrors, scope);
|
|
138
|
-
lintBlockers = part.blocking;
|
|
139
|
-
lintNotices = part.notice;
|
|
140
|
-
// W8 (design-history stale) is the CURRENT project's close
|
|
141
|
-
// responsibility, not cross-project debt — block on the active
|
|
142
|
-
// project's, surface others' as notices.
|
|
143
|
-
if (closeFiles.project) {
|
|
144
|
-
const mine = `projects/${closeFiles.project}/design-history.md`;
|
|
145
|
-
lintW8 = allW8.filter((w) => w.file === mine);
|
|
146
|
-
lintNotices.push(...allW8.filter((w) => w.file !== mine));
|
|
147
|
-
} else {
|
|
148
|
-
lintW8 = allW8;
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
lintBlockers = allErrors;
|
|
152
|
-
lintW8 = allW8;
|
|
153
|
-
}
|
|
154
|
-
} catch (err) {
|
|
155
|
-
/* fail-open */
|
|
156
|
-
process.stderr.write(`[hypo-personal-check] error: ${err?.message ?? String(err)}\n`);
|
|
149
|
+
} else {
|
|
150
|
+
feedbackHealed = `[WIKI CHECK] feedback projection re-synced (${gate.driftTargets.join(', ')}); MEMORY.md body may be unchanged — drift was in the managed block / side-files.`;
|
|
157
151
|
}
|
|
158
152
|
}
|
|
159
153
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
154
|
+
// Non-blocking heads-up about pre-existing lint / out-of-scope design-history
|
|
155
|
+
// debt in untouched files (other projects / shared pages). Surfaced so it is
|
|
156
|
+
// visible but never blocks compact. (Mirrors the pre-ADR-0046 inline behavior,
|
|
157
|
+
// which lumped out-of-scope W8 into the same lint-notice list.)
|
|
158
|
+
const debtNotices = gate.notices.filter((n) => n.type === 'lint' || n.type === 'design-history');
|
|
159
|
+
let noticeText =
|
|
160
|
+
debtNotices.length > 0
|
|
161
|
+
? `[WIKI CHECK] ${debtNotices.length} pre-existing lint issue(s) in files this session did not touch (not blocking): ${[
|
|
162
|
+
...new Set(debtNotices.map((n) => n.reason.replace(/ \([^)]*\)$/, ''))),
|
|
168
163
|
]
|
|
169
164
|
.slice(0, 5)
|
|
170
|
-
.join(', ')}${
|
|
165
|
+
.join(', ')}${debtNotices.length > 5 ? ', …' : ''} — clean up when convenient.`
|
|
171
166
|
: '';
|
|
167
|
+
// Surface the self-heal so a re-synced projection is not a silent mutation of
|
|
168
|
+
// the user's MEMORY.md / CLAUDE.md (ADR 0045 transparency).
|
|
169
|
+
if (feedbackHealed) noticeText = noticeText ? `${noticeText}\n${feedbackHealed}` : feedbackHealed;
|
|
172
170
|
|
|
173
|
-
|
|
174
|
-
// Single blocking gate invariant (spec §7.5): integrate into THIS hook, never
|
|
175
|
-
// add a separate PreCompact hook. `feedback-sync --check --strict` reports
|
|
176
|
-
// projection drift (wiki feedback SoT vs MEMORY / CLAUDE.md learned-behaviors
|
|
177
|
-
// projection). `--no-input` keeps this non-TTY hook from ever blocking on a
|
|
178
|
-
// prompt, and the engine's skip-MEMORY warning is *soft* (never escalated by
|
|
179
|
-
// --strict) so a fresh / external user whose ~/.claude/projects/<id> dir does
|
|
180
|
-
// not exist yet is never gated (contract §5 step 4). Fail-open on any spawn
|
|
181
|
-
// error, exactly like the lint check above.
|
|
182
|
-
const feedbackPath = PKG_ROOT ? join(PKG_ROOT, 'scripts', 'feedback-sync.mjs') : null;
|
|
183
|
-
let feedbackOk = true;
|
|
184
|
-
let feedbackReason = '';
|
|
185
|
-
let feedbackSkipped = false;
|
|
186
|
-
if (!feedbackPath || !existsSync(feedbackPath)) {
|
|
187
|
-
feedbackSkipped = true;
|
|
188
|
-
} else {
|
|
189
|
-
try {
|
|
190
|
-
const r = spawnSync(
|
|
191
|
-
process.execPath,
|
|
192
|
-
[
|
|
193
|
-
feedbackPath,
|
|
194
|
-
'--check',
|
|
195
|
-
'--strict',
|
|
196
|
-
'--no-input',
|
|
197
|
-
'--json',
|
|
198
|
-
`--hypo-dir=${HYPO_DIR}`,
|
|
199
|
-
`--claude-home=${join(homedir(), '.claude')}`,
|
|
200
|
-
],
|
|
201
|
-
{ encoding: 'utf-8', timeout: 30000 },
|
|
202
|
-
);
|
|
203
|
-
if (r.error || r.status === null) {
|
|
204
|
-
feedbackSkipped = true; // spawn failure → fail-open (never block on tooling)
|
|
205
|
-
} else if (r.status !== 0) {
|
|
206
|
-
// exit≠0 alone is ambiguous. A *missing* target file (e.g. a system
|
|
207
|
-
// whose ~/.claude/CLAUDE.md was never created) reports buildError +
|
|
208
|
-
// exit 1, which is benign — there is nothing to gate. Decide from the
|
|
209
|
-
// JSON report's per-target state instead of the raw exit code: block
|
|
210
|
-
// ONLY when some target has a genuine, actionable issue (drift,
|
|
211
|
-
// conflict, over-cap, or a malformed managed region). buildError is
|
|
212
|
-
// never actionable here, so any mix that lacks a real issue fails open
|
|
213
|
-
// — including memory:clean + claude:buildError, where the prior
|
|
214
|
-
// `every(buildError)` predicate wrongly blocked that case. Mirrors
|
|
215
|
-
// doctor's buildError→warn (non-fatal) handling.
|
|
216
|
-
let report = null;
|
|
217
|
-
try {
|
|
218
|
-
report = JSON.parse(r.stdout || '');
|
|
219
|
-
} catch {
|
|
220
|
-
/* unparseable → fail-open below */
|
|
221
|
-
}
|
|
222
|
-
const targets = report ? Object.values(report.targets || {}) : [];
|
|
223
|
-
const conflicted = targets.some(
|
|
224
|
-
(t) =>
|
|
225
|
-
t.intruder || t.unpaired || t.outOfContainer || (t.conflicts && t.conflicts.length),
|
|
226
|
-
);
|
|
227
|
-
const overCap = targets.some((t) => t.overCap);
|
|
228
|
-
const drifted = targets.some((t) => t.dirty);
|
|
229
|
-
if (!report || !(conflicted || overCap || drifted)) {
|
|
230
|
-
feedbackSkipped = true; // missing target / pure warning / unparseable → fail-open
|
|
231
|
-
} else {
|
|
232
|
-
feedbackOk = false;
|
|
233
|
-
feedbackReason = conflicted
|
|
234
|
-
? 'feedback projection conflict (manual edit) — run `hypomnema feedback-sync --import-target-change --from=<memory|claude>`'
|
|
235
|
-
: overCap
|
|
236
|
-
? 'feedback projection over cap — demote/archive feedback pages'
|
|
237
|
-
: 'feedback projection drift — run `hypomnema feedback-sync --write`';
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
} catch (err) {
|
|
241
|
-
feedbackSkipped = true;
|
|
242
|
-
process.stderr.write(`[hypo-personal-check] error: ${err?.message ?? String(err)}\n`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
gitStatus.clean &&
|
|
248
|
-
hotStatus.clean &&
|
|
249
|
-
lintOk &&
|
|
250
|
-
designHistoryOk &&
|
|
251
|
-
closeFiles.ok &&
|
|
252
|
-
feedbackOk
|
|
253
|
-
) {
|
|
171
|
+
if (gate.ok) {
|
|
254
172
|
console.log(
|
|
255
173
|
JSON.stringify(
|
|
256
174
|
noticeText
|
|
@@ -264,13 +182,9 @@ process.stdin.on('end', () => {
|
|
|
264
182
|
// ── Bypass 3: HYPO_SKIP_GATE ──
|
|
265
183
|
if (isGateSkipped()) {
|
|
266
184
|
const skipped = [
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
!designHistoryOk ? `design-history stale (${lintW8.length})` : '',
|
|
271
|
-
!feedbackOk ? feedbackReason : '',
|
|
272
|
-
lintSkipped ? 'lint skipped (hypo-pkg.json missing)' : '',
|
|
273
|
-
feedbackSkipped ? 'feedback-sync skipped (hypo-pkg.json missing)' : '',
|
|
185
|
+
...gate.blockers.map((b) => b.reason),
|
|
186
|
+
gate.skipped.lint ? 'lint skipped (hypo-pkg.json missing)' : '',
|
|
187
|
+
gate.skipped.feedback ? 'feedback-sync skipped (hypo-pkg.json missing)' : '',
|
|
274
188
|
]
|
|
275
189
|
.filter(Boolean)
|
|
276
190
|
.join(', ');
|
|
@@ -284,18 +198,12 @@ process.stdin.on('end', () => {
|
|
|
284
198
|
}
|
|
285
199
|
|
|
286
200
|
// ── Block ──
|
|
201
|
+
// gate.blockers already carry per-type reasons in the canonical order
|
|
202
|
+
// (git, hot, close, lint, design-history, feedback) — same strings as before
|
|
203
|
+
// ADR 0046, now sourced from the shared gate instead of inline checks.
|
|
287
204
|
const reasons = [
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
!closeFiles.ok ? closeFilesReason : '',
|
|
291
|
-
!lintOk
|
|
292
|
-
? `lint blockers: ${[...new Set(lintBlockers.map((b) => b.id || b.file))].join(', ')}`
|
|
293
|
-
: '',
|
|
294
|
-
!designHistoryOk
|
|
295
|
-
? `design-history stale: ${lintW8.map((w) => w.file.split('/')[1]).join(', ')}`
|
|
296
|
-
: '',
|
|
297
|
-
!feedbackOk ? feedbackReason : '',
|
|
298
|
-
lintSkipped ? 'lint skipped (run `hypomnema init` to enable lint gate)' : '',
|
|
205
|
+
...gate.blockers.map((b) => b.reason),
|
|
206
|
+
gate.skipped.lint ? 'lint skipped (run `hypomnema init` to enable lint gate)' : '',
|
|
299
207
|
].filter(Boolean);
|
|
300
208
|
|
|
301
209
|
const checklist = readChecklist(today);
|
|
@@ -304,11 +212,13 @@ process.stdin.on('end', () => {
|
|
|
304
212
|
[
|
|
305
213
|
` [ ] 0. Read SCHEMA.md + hypo-guide.md (required before wiki work)`,
|
|
306
214
|
` [ ] 1. PRD — create projects/<name>/prd.md if missing`,
|
|
307
|
-
` [ ] 2. ADR — decide yes/no on 5 types
|
|
215
|
+
` [ ] 2. ADR — decide yes/no on 5 types. Design change → append to projects/<name>/design-history.md.`,
|
|
216
|
+
` If none, note the literal marker "ADR 없음 — reason: <why>" in the session-log entry`,
|
|
217
|
+
` (machine-readable; suppresses the W8 design-history gate for no-design sessions).`,
|
|
308
218
|
` [ ] 3. Ingest — if new external knowledge, save to sources/ and ingest`,
|
|
309
219
|
` [ ] 4. Pages — extract new concepts/patterns to pages/`,
|
|
310
220
|
` [ ] 5. Synthesis — if 3+ cross-page analysis results, save to pages/syntheses/`,
|
|
311
|
-
` [ ] 6. session-log — append to projects/<name>/session-log/YYYY-MM.md`,
|
|
221
|
+
` [ ] 6. session-log — append to projects/<name>/session-log/YYYY-MM-DD.md (daily shard)`,
|
|
312
222
|
` [ ] 7. index.md — update Projects section if needed`,
|
|
313
223
|
` [ ] 8. log.md — append ## [${today}] session | <project-name>`,
|
|
314
224
|
` [ ] 9. hot.md — update projects/<name>/hot.md (no exceptions)`,
|
|
@@ -317,6 +227,8 @@ process.stdin.on('end', () => {
|
|
|
317
227
|
` [ ] 12. lint — run scripts/lint.mjs; fix errors in files YOU touched`,
|
|
318
228
|
` (other projects' / shared-page debt is reported as non-blocking notice)`,
|
|
319
229
|
` [ ] 13. git commit & push`,
|
|
230
|
+
` [ ] 14. verify — run \`crystallize.mjs --check-session-close\`; only declare`,
|
|
231
|
+
` the session closed once it prints "Compact-ready" (= this gate passes).`,
|
|
320
232
|
].join('\n');
|
|
321
233
|
|
|
322
234
|
const closeIntentNote = hasCloseIntent
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* hypo-pre-commit.mjs — wiki git pre-commit hook worker (§6.8
|
|
3
|
+
* hypo-pre-commit.mjs — wiki git pre-commit hook worker (§6.8)
|
|
4
4
|
*
|
|
5
5
|
* Blocks staged files that match .hypoignore patterns.
|
|
6
6
|
* Installed by `hypo init` to <wiki>/.git/hooks/pre-commit.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* hypo-session-end.mjs — SessionEnd hook (
|
|
3
|
+
* hypo-session-end.mjs — SessionEnd hook (ADR 0022 Layer 2)
|
|
4
4
|
*
|
|
5
5
|
* `/clear` cannot be blocked: it never fires UserPromptSubmit (Stage 0 PoC,
|
|
6
6
|
* 2026-05-14). The only intervention point is the SessionEnd(reason='clear')
|
|
@@ -176,7 +176,7 @@ function readLastGrowthLine() {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
/**
|
|
179
|
-
*
|
|
179
|
+
* ADR 0022 amendment 2026-05-14: if the prior session ended
|
|
180
180
|
* via `/clear`, hypo-session-end stashed its identity in `.cache/clear-marker.json`.
|
|
181
181
|
* Read it (with 7-day stale guard), unlink it (one-shot), and return a
|
|
182
182
|
* `[WIKI_AUTOCLOSE]` recovery line for additionalContext + stderr.
|
|
@@ -212,8 +212,8 @@ function gitPull(dir) {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/**
|
|
215
|
-
*
|
|
216
|
-
* Stop hook
|
|
215
|
+
* Surface unresolved sync failures recorded by a prior session's
|
|
216
|
+
* Stop hook. The entry is cleared only once this session's pull has
|
|
217
217
|
* succeeded AND there is no unpushed commit left behind by a failed push
|
|
218
218
|
* (`[ahead N]`).
|
|
219
219
|
*
|
|
@@ -323,7 +323,7 @@ let raw = '';
|
|
|
323
323
|
process.stdin.setEncoding('utf-8');
|
|
324
324
|
process.stdin.on('data', (chunk) => (raw += chunk));
|
|
325
325
|
process.stdin.on('end', () => {
|
|
326
|
-
//
|
|
326
|
+
// Declared before the try so every emit branch — including the outer
|
|
327
327
|
// catch — carries the same `systemMessage` (the user-visible update/sibling
|
|
328
328
|
// banner). Reassigned once below after the notices are computed.
|
|
329
329
|
let outExtra = { continue: true, suppressOutput: true };
|
|
@@ -343,14 +343,14 @@ process.stdin.on('end', () => {
|
|
|
343
343
|
const clearRecoveryLine = buildClearRecoveryLine(data.source);
|
|
344
344
|
const updateLine = buildUpdateNotice();
|
|
345
345
|
const siblingLine = buildSiblingNotice();
|
|
346
|
-
//
|
|
346
|
+
// The update + stale-sibling banners must reach the USER. On a
|
|
347
347
|
// SessionStart hook that exits 0, stderr is invisible in the normal TUI
|
|
348
348
|
// (only shown on exit 2 / --verbose) and additionalContext is model-only —
|
|
349
349
|
// `systemMessage` is the documented user-visible channel. Route those two
|
|
350
350
|
// banners there. They ALSO stay in noticePrefix → additionalContext below,
|
|
351
351
|
// so the model and the user start the session looking at the same state.
|
|
352
352
|
// (The other stderr notices — sync/growth/clear/suggest — are intentionally
|
|
353
|
-
// transcript/--verbose only and out of
|
|
353
|
+
// transcript/--verbose only and out of this banner's scope.)
|
|
354
354
|
const userMessage = [updateLine, siblingLine].filter(Boolean).join('\n\n');
|
|
355
355
|
if (userMessage) outExtra = { ...outExtra, systemMessage: userMessage };
|
|
356
356
|
const notices = [syncLine, growthLine, clearRecoveryLine, updateLine, siblingLine].filter(
|
|
@@ -417,7 +417,7 @@ process.stdin.on('end', () => {
|
|
|
417
417
|
return;
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
-
// MISS: cwd matches no project.
|
|
420
|
+
// MISS: cwd matches no project. ADR 0023 — offer to create one
|
|
421
421
|
// when the ADR trigger conditions hold (git repo + project marker + no
|
|
422
422
|
// cooldown + not previously declined). The actual scaffold is the LLM's
|
|
423
423
|
// job on a "Y" reply (scripts/lib/project-create.mjs); the hook only nudges.
|