hypomnema 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.ko.md +11 -10
- package/README.md +11 -10
- package/commands/audit.md +1 -1
- package/commands/crystallize.md +11 -11
- package/commands/doctor.md +1 -1
- package/commands/feedback.md +2 -2
- package/commands/graph.md +1 -1
- package/commands/ingest.md +1 -1
- package/commands/init.md +1 -1
- package/commands/lint.md +1 -1
- package/commands/query.md +1 -1
- package/commands/rename.md +79 -0
- package/commands/resume.md +2 -2
- package/commands/stats.md +1 -1
- package/commands/uninstall.md +1 -1
- package/commands/upgrade.md +1 -1
- package/commands/verify.md +1 -1
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/CONTRIBUTING.md +2 -2
- package/hooks/hypo-auto-commit.mjs +8 -24
- package/hooks/hypo-auto-minimal-crystallize.mjs +57 -11
- package/hooks/hypo-compact-guard.mjs +6 -3
- 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 +1082 -89
- 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 +322 -110
- 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 +836 -0
- package/scripts/resume.mjs +75 -19
- package/scripts/uninstall.mjs +1 -1
- package/scripts/upgrade.mjs +26 -25
- package/skills/crystallize/SKILL.md +12 -9
- package/skills/graph/SKILL.md +1 -1
- package/skills/ingest/SKILL.md +1 -1
- package/skills/lint/SKILL.md +1 -1
- package/skills/query/SKILL.md +1 -1
- package/skills/verify/SKILL.md +1 -1
- package/templates/SCHEMA.md +2 -2
- package/templates/hypo-config.md +2 -2
- package/templates/hypo-guide.md +22 -1
- package/templates/hypo-help.md +1 -0
- package/templates/projects/_template/index.md +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* hypo-auto-minimal-crystallize.mjs — Stop hook (
|
|
3
|
+
* hypo-auto-minimal-crystallize.mjs — Stop hook (ADR 0022 Layer 3)
|
|
4
4
|
*
|
|
5
5
|
* Last hook in the Stop chain: a final-line defense that blocks `Stop` when
|
|
6
|
-
* the current session
|
|
6
|
+
* the current session did substantial work (mutation, or a high-volume
|
|
7
|
+
* read-only investigation — see step 3) but never produced a verified
|
|
7
8
|
* session-close. Forces Claude to run minimal session-close before the
|
|
8
9
|
* conversation context evaporates.
|
|
9
10
|
*
|
|
@@ -11,8 +12,10 @@
|
|
|
11
12
|
*
|
|
12
13
|
* 1. stop_hook_active === true → continue (loop guard; PoC 2026-05-14)
|
|
13
14
|
* 2. wiki absent → continue (fail-open)
|
|
14
|
-
* 3.
|
|
15
|
-
*
|
|
15
|
+
* 3. not a substantial session → continue (substantial-session gate)
|
|
16
|
+
* substantial = ≥1 mutation tool_use, OR ≥5 read-only investigation
|
|
17
|
+
* calls (Read/Grep/Glob/Bash) — 6a, so read-only review/debug sessions
|
|
18
|
+
* are also nudged to close. Pure Q&A / incidental lookups still skip.
|
|
16
19
|
* 4. no recent user close-intent → continue (close-intent gate, see below)
|
|
17
20
|
* 5. readSessionClosedMarker(session_id) valid
|
|
18
21
|
* → continue (close already verified)
|
|
@@ -43,18 +46,19 @@ import { join } from 'path';
|
|
|
43
46
|
import {
|
|
44
47
|
HYPO_DIR,
|
|
45
48
|
PKG_ROOT,
|
|
46
|
-
|
|
49
|
+
isSubstantialSession,
|
|
47
50
|
readSessionClosedMarker,
|
|
48
51
|
extractUserMessages,
|
|
49
52
|
isClosePattern,
|
|
50
53
|
isGateSkipped,
|
|
54
|
+
precompactGateStatus,
|
|
51
55
|
} from './hypo-shared.mjs';
|
|
52
56
|
|
|
53
57
|
function emitContinue() {
|
|
54
58
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
function emitBlock(sessionId, transcriptPath) {
|
|
61
|
+
function emitBlock(sessionId, transcriptPath, gate = null) {
|
|
58
62
|
// One-line, skill-first. /hypo:crystallize is the documented session-close
|
|
59
63
|
// alias; passing --session-id there writes the per-session marker that clears
|
|
60
64
|
// this block. CLI fallback + bypass live in commands/crystallize.md, not here
|
|
@@ -64,12 +68,39 @@ function emitBlock(sessionId, transcriptPath) {
|
|
|
64
68
|
// coherence: a marker written without lint would only let Stop pass for
|
|
65
69
|
// /compact to immediately re-block on the same errors).
|
|
66
70
|
const transcriptHint = transcriptPath ? ` --transcript-path=${transcriptPath}` : '';
|
|
67
|
-
const
|
|
71
|
+
const markCmd = `crystallize --mark-session-closed --session-id=${sessionId}${transcriptHint}`;
|
|
72
|
+
// The log-only escape hatch for a non-project (wiki/tooling-only)
|
|
73
|
+
// session. Offered ONLY as an explicit alternative when a close blocker is
|
|
74
|
+
// present — never as the default recovery, so a real project session is not
|
|
75
|
+
// taught to bypass the ADR 0043 close invariant (codex design Finding 3).
|
|
76
|
+
const logOnlyCmd = `crystallize --mark-session-closed --log-only --session-id=${sessionId}${transcriptHint}`;
|
|
77
|
+
// ADR 0047: refine the message with the read-only /compact gate result.
|
|
78
|
+
// - gate green → the close is compact-ready and ONLY the marker is missing
|
|
79
|
+
// (the hand-edit close case: files Written + committed directly, bypassing
|
|
80
|
+
// the marker writer). Say so precisely + give the one command, instead of
|
|
81
|
+
// the generic "미완료" that reads as "you never closed".
|
|
82
|
+
// - gate has blockers → surface them so the user fixes the real issue first.
|
|
83
|
+
// - gate null (tooling error/unavailable) → generic message (fail-open).
|
|
84
|
+
let reason;
|
|
85
|
+
if (gate && gate.ok) {
|
|
86
|
+
reason = `[WIKI_AUTOCLOSE] close gate green — only the session-closed marker is missing. Run \`${markCmd}\` to finish (session_id=${sessionId}).`;
|
|
87
|
+
} else if (gate && gate.blockers && gate.blockers.length > 0) {
|
|
88
|
+
const blockers = gate.blockers.map((b) => b.reason).join('; ');
|
|
89
|
+
reason = `[WIKI_AUTOCLOSE] session-close incomplete — resolve: ${blockers}. Then run \`${markCmd}\` (session_id=${sessionId}).`;
|
|
90
|
+
// Only when a project-close blocker is what's holding the session: a
|
|
91
|
+
// non-project session has nothing to close, so offer log-only as the way out
|
|
92
|
+
// (Claude decides whether this session is project-scoped — no auto-attribution).
|
|
93
|
+
if (gate.blockers.some((b) => b.type === 'close')) {
|
|
94
|
+
reason += ` If this was a non-project (wiki/tooling-only) session with no project to close, run \`${logOnlyCmd}\` instead (log-only close, no project attribution).`;
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
reason = `[WIKI_AUTOCLOSE] session-close 미완료 — /hypo:crystallize 실행으로 마무리 (session_id=${sessionId}${transcriptHint}).`;
|
|
98
|
+
}
|
|
68
99
|
console.log(
|
|
69
100
|
JSON.stringify({
|
|
70
101
|
decision: 'block',
|
|
71
102
|
reason,
|
|
72
|
-
stopReason: 'session-close incomplete (
|
|
103
|
+
stopReason: 'session-close incomplete (ADR 0022 Layer 3)',
|
|
73
104
|
}),
|
|
74
105
|
);
|
|
75
106
|
}
|
|
@@ -112,8 +143,10 @@ process.stdin.on('end', () => {
|
|
|
112
143
|
const sessionId = payload.session_id || payload.sessionId || null;
|
|
113
144
|
const transcriptPath = payload.transcript_path || payload.transcriptPath || null;
|
|
114
145
|
|
|
115
|
-
// 3. substantial-session gate.
|
|
116
|
-
|
|
146
|
+
// 3. substantial-session gate. Pure Q&A / incidental-lookup sessions skip
|
|
147
|
+
// the block; mutating sessions AND high-volume read-only investigations
|
|
148
|
+
// (6a) pass through to the close-intent gate.
|
|
149
|
+
if (!isSubstantialSession(transcriptPath)) {
|
|
117
150
|
emitContinue();
|
|
118
151
|
return;
|
|
119
152
|
}
|
|
@@ -141,7 +174,20 @@ process.stdin.on('end', () => {
|
|
|
141
174
|
return;
|
|
142
175
|
}
|
|
143
176
|
|
|
144
|
-
|
|
177
|
+
// ADR 0047: read-only /compact gate (same precompactGateStatus the real
|
|
178
|
+
// PreCompact hook uses) sharpens the block message — distinguishes "close
|
|
179
|
+
// is compact-ready, only the marker is missing" from "there are real
|
|
180
|
+
// blockers". The hook NEVER writes the marker here (file-header invariant);
|
|
181
|
+
// this is read-only. Any error → null → emitBlock falls back to the generic
|
|
182
|
+
// message (fail-open).
|
|
183
|
+
let gate = null;
|
|
184
|
+
try {
|
|
185
|
+
gate = precompactGateStatus(HYPO_DIR, transcriptPath ? { transcriptPath } : {});
|
|
186
|
+
} catch {
|
|
187
|
+
gate = null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
emitBlock(sessionId, transcriptPath, gate);
|
|
145
191
|
} catch (err) {
|
|
146
192
|
// Fail-open on any unexpected error.
|
|
147
193
|
process.stderr.write(`[hypo-auto-minimal-crystallize] error: ${err?.message ?? String(err)}\n`);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* hypo-compact-guard.mjs — UserPromptSubmit hook
|
|
4
4
|
*
|
|
5
|
-
* Scope: detects "/compact" or "/clear" typed in chat only (ADR 0022 Layer 2
|
|
5
|
+
* Scope: detects "/compact" or "/clear" typed in chat only (ADR 0022 Layer 2).
|
|
6
6
|
* The CLI built-in /compact does NOT fire UserPromptSubmit — use personal-wiki-check.mjs
|
|
7
7
|
* (PreCompact hook) as the hard gate for that path. /clear has no PreCompact event, so
|
|
8
8
|
* this hook is the only chat-side gate that can prompt session-close before context wipe.
|
|
@@ -42,14 +42,17 @@ process.stdin.on('end', () => {
|
|
|
42
42
|
const gitStatus = hypoIsClean();
|
|
43
43
|
const hotStatus = hotMdIsClean();
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
// ADR 0056: block on uncommitted (real unsaved work); unpushed commits (ahead)
|
|
46
|
+
// are a soft, auto-synced state and must not block /compact or /clear — mirrors
|
|
47
|
+
// the precompactGateStatus demote so the chat-side gate stays consistent.
|
|
48
|
+
if (hasSession && !gitStatus.uncommitted && hotStatus.clean) {
|
|
46
49
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
47
50
|
return;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
const reasons = [
|
|
51
54
|
!hasSession ? 'session log entry missing' : '',
|
|
52
|
-
|
|
55
|
+
gitStatus.uncommitted ? gitStatus.reason : '',
|
|
53
56
|
!hotStatus.clean ? hotStatus.reason : '',
|
|
54
57
|
].filter(Boolean);
|
|
55
58
|
|
|
@@ -137,7 +137,7 @@ process.stdin.on('end', () => {
|
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
// MISS: cwd matches no project.
|
|
140
|
+
// MISS: cwd matches no project. ADR 0023 — offer to create one
|
|
141
141
|
// when the trigger conditions hold. Same nudge-only model as session-start.
|
|
142
142
|
let suggestPrefix = '';
|
|
143
143
|
if (shouldSuggestProjectCreation(newCwd, HYPO_DIR)) {
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* hypo-first-prompt.mjs — UserPromptSubmit hook
|
|
4
4
|
*
|
|
5
5
|
* Consumes the marker written by hypo-session-start.mjs (source omitted /
|
|
6
|
-
* 'session-start') or hypo-cwd-change.mjs (source 'cwd-change'
|
|
6
|
+
* 'session-start') or hypo-cwd-change.mjs (source 'cwd-change').
|
|
7
7
|
* On the FIRST user prompt after the marker is written, FORCES a one-line
|
|
8
|
-
* resume summary into the reply (
|
|
8
|
+
* resume summary into the reply (the old "answer only if related"
|
|
9
9
|
* conditional is removed; the line is injected unconditionally).
|
|
10
10
|
*
|
|
11
11
|
* hot.md / session-state.md content is NOT re-injected here — the upstream
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
14
14
|
import { join } from 'path';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
HYPO_DIR,
|
|
17
|
+
computeSessionGrowth,
|
|
18
|
+
formatGrowthMetrics,
|
|
19
|
+
deriveRootLogEntries,
|
|
20
|
+
} from './hypo-shared.mjs';
|
|
16
21
|
|
|
17
22
|
const HOT_PATH = join(HYPO_DIR, 'hot.md');
|
|
18
23
|
const GROWTH_CACHE = join(HYPO_DIR, '.cache', 'last-session-growth.json');
|
|
@@ -107,6 +112,14 @@ try {
|
|
|
107
112
|
} catch (err) {
|
|
108
113
|
process.stderr.write(`[hypo-hot-rebuild] error: ${err?.message ?? String(err)}\n`);
|
|
109
114
|
}
|
|
115
|
+
// Auto-derive the root log.md session entry from each project's session-log
|
|
116
|
+
// heading (runs AFTER rebuild() so root hot.md is already fresh and isn't itself
|
|
117
|
+
// counted as the project's open gate problem). Best-effort: own try/catch.
|
|
118
|
+
try {
|
|
119
|
+
deriveRootLogEntries(HYPO_DIR);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
process.stderr.write(`[hypo-hot-rebuild] log-derive error: ${err?.message ?? String(err)}\n`);
|
|
122
|
+
}
|
|
110
123
|
try {
|
|
111
124
|
emitGrowth();
|
|
112
125
|
} catch (err) {
|
|
@@ -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.
|