@sdsrs/code-graph 0.163.0 → 0.165.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/README.md +5 -6
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/agents/code-explorer.md +15 -12
- package/claude-plugin/hooks/hooks.json +91 -1
- package/claude-plugin/scripts/adopt.js +62 -37
- package/claude-plugin/scripts/cg-answer.js +7 -0
- package/claude-plugin/scripts/covering-tests.js +1 -2
- package/claude-plugin/scripts/doctor.js +40 -1
- package/claude-plugin/scripts/incremental-index.js +4 -1
- package/claude-plugin/scripts/lifecycle.js +49 -13
- package/claude-plugin/scripts/post-grep-inject.js +58 -3
- package/claude-plugin/scripts/pre-edit-guide.js +122 -83
- package/claude-plugin/scripts/pre-grep-guide.js +243 -11
- package/claude-plugin/scripts/pre-read-guide.js +42 -28
- package/claude-plugin/scripts/project-root.js +23 -1
- package/claude-plugin/scripts/recommendation-log.js +11 -9
- package/claude-plugin/scripts/session-edits.js +58 -3
- package/claude-plugin/scripts/session-init.js +142 -349
- package/claude-plugin/scripts/stop-impact.js +171 -10
- package/claude-plugin/scripts/user-prompt-context.js +48 -16
- package/claude-plugin/skills/explore/SKILL.md +10 -7
- package/claude-plugin/templates/code-graph-snapshot.yml +1 -1
- package/claude-plugin/templates/plugin_code_graph_mcp.md +58 -169
- package/package.json +6 -6
|
@@ -10,7 +10,8 @@ const {
|
|
|
10
10
|
} = require('./lifecycle');
|
|
11
11
|
const { UPDATE_STATE_FILE } = require('./cache-paths');
|
|
12
12
|
const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-utils');
|
|
13
|
-
const { maybeAutoAdopt, isAdopted, unadopt, unadoptCommand } = require('./adopt');
|
|
13
|
+
const { maybeAutoAdopt, isAdopted, unadopt, unadoptCommand, adoptCommand } = require('./adopt');
|
|
14
|
+
const { capContext } = require('./hook-emit');
|
|
14
15
|
const { isNonProjectCwd } = require('./project-detect');
|
|
15
16
|
const { hidden } = require('./proc-opts');
|
|
16
17
|
const { installHookFailOpen, remainingMs } = require('./hook-fail-open');
|
|
@@ -18,6 +19,7 @@ const { installHookFailOpen, remainingMs } = require('./hook-fail-open');
|
|
|
18
19
|
// treats any throw as "nothing to conclude", so a lazy require in there would
|
|
19
20
|
// turn a resolution failure into a silent disable (pre-tag review, JS-08).
|
|
20
21
|
const { resolveProjectRoot } = require('./project-root');
|
|
22
|
+
const { isOwnedPath, openOwned } = require('./recommendation-log');
|
|
21
23
|
|
|
22
24
|
// ── SessionStart budget (audit 2026-09-05 NEW-05) ─────────────────────────
|
|
23
25
|
//
|
|
@@ -48,6 +50,30 @@ const { resolveProjectRoot } = require('./project-root');
|
|
|
48
50
|
// being quieter than usual.
|
|
49
51
|
let budgetSkips = [];
|
|
50
52
|
|
|
53
|
+
// What this run has to say, delivered as ONE JSON envelope when the hook exits
|
|
54
|
+
// (sessionStartOutput): `notices` become the user-facing `systemMessage`,
|
|
55
|
+
// `contextParts` the model-facing `additionalContext`. Claude Code never shows
|
|
56
|
+
// a SessionStart hook's stderr when it exits 0, so the fifteen notices this
|
|
57
|
+
// hook used to write there — "CLAUDE.md was modified", "hooks look dark",
|
|
58
|
+
// "binary missing" — reached no one (decision D5, 2026-09-28 usage
|
|
59
|
+
// evaluation). Only notices a user must act on are kept. Module state, reset
|
|
60
|
+
// per run like budgetSkips.
|
|
61
|
+
let notices = [];
|
|
62
|
+
let contextParts = [];
|
|
63
|
+
|
|
64
|
+
function sessionStartOutput() {
|
|
65
|
+
if (notices.length === 0 && contextParts.length === 0) return '';
|
|
66
|
+
const out = {};
|
|
67
|
+
if (notices.length > 0) out.systemMessage = notices.join('\n');
|
|
68
|
+
if (contextParts.length > 0) {
|
|
69
|
+
out.hookSpecificOutput = {
|
|
70
|
+
hookEventName: 'SessionStart',
|
|
71
|
+
additionalContext: capContext(contextParts.join('\n')),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return JSON.stringify(out) + '\n';
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
function budgetFor(label, defaultMs) {
|
|
52
78
|
const ms = remainingMs(defaultMs);
|
|
53
79
|
if (ms === null) budgetSkips.push(label);
|
|
@@ -82,132 +108,6 @@ function shouldInjectMap({ available, quietHooks, adopted } = {}) {
|
|
|
82
108
|
return !!(available && !quietHooks && adopted);
|
|
83
109
|
}
|
|
84
110
|
|
|
85
|
-
// v0.63 — SessionStart "live context": the recent-change blast radius from the
|
|
86
|
-
// AST index. UNLIKE injectProjectMap (default-OFF because the static module map
|
|
87
|
-
// duplicates the CLAUDE.md block + the on-demand project_map tool), this is git-delta-
|
|
88
|
-
// derived — it changes every session and the static CLAUDE.md block cannot carry it, so it earns
|
|
89
|
-
// being ON by default for adopted projects. It is the graph-unique, actionable
|
|
90
|
-
// counterpart to mem's SessionStart dashboard (which pushes recent activity to
|
|
91
|
-
// create engagement). Selectivity is automatic: a session with no recently
|
|
92
|
-
// changed *source* files (a deps-only or release commit, a clean checkout off a
|
|
93
|
-
// non-code commit) injects nothing — zero standing-context cost when idle.
|
|
94
|
-
//
|
|
95
|
-
// Gating differs from the static map on purpose: it respects only the hard
|
|
96
|
-
// kill-switch (CODE_GRAPH_QUIET_HOOKS=1) and a dedicated opt-out, NOT the
|
|
97
|
-
// default-quiet flag that suppresses the duplicative map.
|
|
98
|
-
function shouldInjectRecentImpact({ available, adopted, env = {} } = {}) {
|
|
99
|
-
if (!available || !adopted) return false;
|
|
100
|
-
if (env.CODE_GRAPH_QUIET_HOOKS === '1') return false;
|
|
101
|
-
if (env.CODE_GRAPH_NO_RECENT_IMPACT === '1') return false;
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Pure: given whether there are uncommitted (WIP) source changes and the
|
|
106
|
-
// SessionStart `source`, decide if the recent-impact injection is worth its
|
|
107
|
-
// standing-context cost. Marginal-information argument, not measured:
|
|
108
|
-
// - WIP present → active work; the blast radius of what you're editing is the
|
|
109
|
-
// highest-value case → always show.
|
|
110
|
-
// - clean tree → we fall back to the LAST COMMIT. On a cold `startup` that's
|
|
111
|
-
// low value (you just made it, or you're starting on something unrelated),
|
|
112
|
-
// and the project's own evidence is that SessionStart dumps tend to go
|
|
113
|
-
// unreferenced ([[project_cross_project_interference]]) → suppress. On a
|
|
114
|
-
// resume (`clear`/`compact`/`resume`) the same last-commit reminder helps
|
|
115
|
-
// re-establish context → show.
|
|
116
|
-
// Unknown source (direct calls / tests) defaults to showing — only an explicit
|
|
117
|
-
// cold `startup` with no WIP is suppressed.
|
|
118
|
-
function recentImpactWorthShowing({ isWip, source } = {}) {
|
|
119
|
-
if (isWip) return true;
|
|
120
|
-
return source !== 'startup';
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Source-file extensions the indexer extracts symbols from (AST-bearing). Config
|
|
124
|
-
// /lockfile/doc changes have no graph blast radius, so they're filtered out
|
|
125
|
-
// before the `affected` call — keeps the "Changed:" line free of Cargo.lock noise
|
|
126
|
-
// and avoids spending the CLI call on a commit that only bumped versions.
|
|
127
|
-
const RECENT_SRC_EXT =
|
|
128
|
-
/\.(rs|ts|tsx|js|jsx|mjs|cjs|py|go|java|rb|php|swift|kt|kts|dart|c|h|cc|cpp|hpp|cs|sh|bash)$/i;
|
|
129
|
-
|
|
130
|
-
// Pure: filter file paths to indexed source files, capped. Accepts EITHER a raw
|
|
131
|
-
// `git diff --name-only` blob (string, split on newline) OR an already-parsed
|
|
132
|
-
// path array (from parseGitStatusPaths) — the array form is essential: passing
|
|
133
|
-
// the parser's array output to a string-only signature silently returned [] and
|
|
134
|
-
// broke WIP detection (caught by the composing test). Cap guards message length
|
|
135
|
-
// and the affected call's argv on a sweeping refactor.
|
|
136
|
-
function filterSourceFiles(input, cap = 25) {
|
|
137
|
-
const lines = Array.isArray(input)
|
|
138
|
-
? input
|
|
139
|
-
: (typeof input === 'string' ? input.split('\n') : []);
|
|
140
|
-
return lines
|
|
141
|
-
.map(s => s.trim())
|
|
142
|
-
.filter(Boolean)
|
|
143
|
-
.filter(f => RECENT_SRC_EXT.test(f))
|
|
144
|
-
.slice(0, cap);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Pure: extract file paths from `git status --porcelain` output. One call covers
|
|
148
|
-
// tracked changes (staged + unstaged) AND untracked files — the diff-only path
|
|
149
|
-
// MISSED untracked new source files you're actively editing (finding #3). Format
|
|
150
|
-
// is `XY␣PATH`, or `XY␣ORIG -> PATH` for renames (take the new path). Git quotes
|
|
151
|
-
// paths with special chars; best-effort unquote.
|
|
152
|
-
function parseGitStatusPaths(statusOutput) {
|
|
153
|
-
if (!statusOutput || typeof statusOutput !== 'string') return [];
|
|
154
|
-
const paths = [];
|
|
155
|
-
for (const line of statusOutput.split('\n')) {
|
|
156
|
-
if (line.length < 4) continue; // need 2 status chars + space + ≥1 path char
|
|
157
|
-
let rest = line.slice(3); // skip the XY status columns + separator space
|
|
158
|
-
const arrow = rest.indexOf(' -> '); // rename/copy → the path after the arrow is current
|
|
159
|
-
if (arrow >= 0) rest = rest.slice(arrow + 4);
|
|
160
|
-
rest = rest.trim();
|
|
161
|
-
if (rest.startsWith('"') && rest.endsWith('"')) rest = rest.slice(1, -1);
|
|
162
|
-
if (rest) paths.push(rest);
|
|
163
|
-
}
|
|
164
|
-
return paths;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Above ~15 direct dependents the per-name list is noise, not signal:
|
|
168
|
-
// information scales INVERSELY with blast size. A high-fanout node (a constants
|
|
169
|
-
// module, a shared util) "touches everything", so its actionable content
|
|
170
|
-
// collapses to "this is high-risk — run the full suite"; the arbitrary first-N
|
|
171
|
-
// dependent names add nothing. Below the threshold the specific dependents ARE
|
|
172
|
-
// the signal (edit one fn → these 3 callers). So scale detail to actionability.
|
|
173
|
-
const FANOUT_LIST_MAX = 15;
|
|
174
|
-
|
|
175
|
-
// Pure: render the injected text from the parsed `affected --json` payload.
|
|
176
|
-
// Returns null when there's nothing graph-relevant to say (no indexed dependents),
|
|
177
|
-
// so the caller injects nothing rather than an empty banner.
|
|
178
|
-
function formatRecentImpact(changed, affected, dependentCap = 6) {
|
|
179
|
-
if (!Array.isArray(changed) || changed.length === 0) return null;
|
|
180
|
-
const all = (affected && Array.isArray(affected.affected_files)) ? affected.affected_files : [];
|
|
181
|
-
if (all.length === 0) return null;
|
|
182
|
-
const direct = all.filter(a => a.depth === 1 && !a.is_test).map(a => a.path);
|
|
183
|
-
const testCount = Array.isArray(affected.tests) ? affected.tests.length : all.filter(a => a.is_test).length;
|
|
184
|
-
|
|
185
|
-
const changedShown = changed.slice(0, 8).join(', ') + (changed.length > 8 ? `, +${changed.length - 8}` : '');
|
|
186
|
-
const lines = [
|
|
187
|
-
'[code-graph] Recent changes — blast radius from the AST index (graph-only; not in MEMORY.md):',
|
|
188
|
-
` Changed: ${changedShown}`,
|
|
189
|
-
];
|
|
190
|
-
if (direct.length > FANOUT_LIST_MAX) {
|
|
191
|
-
// High fanout: the name list is noise; surface risk + test scope only.
|
|
192
|
-
lines.push(` High-fanout change — ${all.length} file(s) impacted (${direct.length} direct); run the full suite (${testCount} test file(s)).`);
|
|
193
|
-
} else {
|
|
194
|
-
lines.push(` Impacts ${all.length} file(s) (${direct.length} direct dependent(s)), ${testCount} test file(s) to re-run.`);
|
|
195
|
-
if (direct.length > 0) {
|
|
196
|
-
const shown = direct.slice(0, dependentCap).join(', ');
|
|
197
|
-
const more = direct.length > dependentCap ? `, +${direct.length - dependentCap} more` : '';
|
|
198
|
-
lines.push(` Direct dependents: ${shown}${more}`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
// Runnable verbatim when ≤4 changed; above that, show 4 + an explicit count
|
|
202
|
-
// rather than a bare "…" — a pasted "…" command yields a SMALLER blast than the
|
|
203
|
-
// numbers above (the computation used all changed files, up to the cap) — finding #4.
|
|
204
|
-
const runCmd = changed.length <= 4
|
|
205
|
-
? `code-graph-mcp affected ${changed.join(' ')}`
|
|
206
|
-
: `code-graph-mcp affected ${changed.slice(0, 4).join(' ')} (+${changed.length - 4} more changed file(s) — pass all for the full blast)`;
|
|
207
|
-
lines.push(` Re-run impacted tests: ${runCmd}`);
|
|
208
|
-
return lines.join('\n');
|
|
209
|
-
}
|
|
210
|
-
|
|
211
111
|
function launchBackgroundAutoUpdate(spawnFn = spawn, env = process.env, { force = false } = {}) {
|
|
212
112
|
try {
|
|
213
113
|
// Documented opt-out (issue #40). Checked HERE as well as inside
|
|
@@ -251,10 +151,10 @@ function isHighIntentSource(source) {
|
|
|
251
151
|
// Claude Code surfaces (same one injectProjectMap uses).
|
|
252
152
|
function reportRebuild(r) {
|
|
253
153
|
if (r && r.settingsRebuiltFrom) {
|
|
254
|
-
|
|
154
|
+
notices.push(
|
|
255
155
|
`[code-graph] ${settingsPath()} could not be parsed and has been REBUILT. ` +
|
|
256
156
|
`Your original is saved at ${r.settingsRebuiltFrom} — merge anything you ` +
|
|
257
|
-
`still need (model / env / permissions / your own hooks) back by hand
|
|
157
|
+
`still need (model / env / permissions / your own hooks) back by hand.`
|
|
258
158
|
);
|
|
259
159
|
}
|
|
260
160
|
// install()/update() have reported `manifestUnwritable` since they learned not
|
|
@@ -265,11 +165,11 @@ function reportRebuild(r) {
|
|
|
265
165
|
// re-run install() and re-report 'installed', forever, with nothing to show
|
|
266
166
|
// for it (audit 2026-08-16 review Minor tail).
|
|
267
167
|
if (r && r.manifestUnwritable) {
|
|
268
|
-
|
|
168
|
+
notices.push(
|
|
269
169
|
`[code-graph] The plugin manifest could not be written (${r.manifestUnwritable}). ` +
|
|
270
170
|
'Hooks are registered but the install will not be remembered, so this runs again ' +
|
|
271
171
|
'every session. Check permissions on ~/.claude/plugins/, then run ' +
|
|
272
|
-
'`code-graph-mcp doctor
|
|
172
|
+
'`code-graph-mcp doctor`.'
|
|
273
173
|
);
|
|
274
174
|
}
|
|
275
175
|
return r;
|
|
@@ -360,8 +260,19 @@ function syncLifecycleConfig() {
|
|
|
360
260
|
// exists) also heals. Previously only doctor checked staleness, so if the
|
|
361
261
|
// auto-update re-register step failed silently, users kept running old hook
|
|
362
262
|
// code indefinitely — the settings.json sibling of the binary-pin bug.
|
|
363
|
-
const { surveyHookCoverage } = require('./lifecycle');
|
|
263
|
+
const { surveyHookCoverage, hooksFromPluginManifest } = require('./lifecycle');
|
|
364
264
|
const cov = surveyHookCoverage(settings);
|
|
265
|
+
// Decision D2: a plugin session gets every hook from the plugin's hooks.json,
|
|
266
|
+
// so "missing from settings.json" is the healthy state there, and an entry
|
|
267
|
+
// still in settings.json (written by a pre-0.164 install, or re-added by
|
|
268
|
+
// hand) fires its hook a second time. install() removes ours on this path.
|
|
269
|
+
if (hooksFromPluginManifest(settings)) {
|
|
270
|
+
if (cov.present.length > 0) {
|
|
271
|
+
installReporting();
|
|
272
|
+
return 'removed-settings-hooks';
|
|
273
|
+
}
|
|
274
|
+
return 'noop';
|
|
275
|
+
}
|
|
365
276
|
if (cov.missing.length > 0) {
|
|
366
277
|
installReporting();
|
|
367
278
|
return 'self-healed-missing-settings-hook';
|
|
@@ -382,9 +293,10 @@ function syncLifecycleConfig() {
|
|
|
382
293
|
* health-check carries the verdict in `index_version_stale`. Best-effort: any
|
|
383
294
|
* failure → false (never force work off a bad probe).
|
|
384
295
|
*
|
|
385
|
-
* Returns true | false | null, where null means the SessionStart
|
|
386
|
-
* before the probe could run. `false` says "asked, not stale";
|
|
387
|
-
* two would let the caller report a freshness it never
|
|
296
|
+
* Returns true | false | null | 'corrupt', where null means the SessionStart
|
|
297
|
+
* budget ran out before the probe could run. `false` says "asked, not stale";
|
|
298
|
+
* conflating the two would let the caller report a freshness it never
|
|
299
|
+
* established. 'corrupt' is health-check's `reason:"corrupt"` verdict.
|
|
388
300
|
*/
|
|
389
301
|
function indexNeedsRevalidation(bin, cwd) {
|
|
390
302
|
const budget = budgetFor('health-check', 3000);
|
|
@@ -398,7 +310,13 @@ function indexNeedsRevalidation(bin, cwd) {
|
|
|
398
310
|
// health-check exits non-zero on an unhealthy index but still writes JSON.
|
|
399
311
|
out = ((e && e.stdout) || '').toString();
|
|
400
312
|
}
|
|
401
|
-
|
|
313
|
+
const report = JSON.parse(out);
|
|
314
|
+
// A corrupt index answers every hook with nothing, and a reader never
|
|
315
|
+
// rebuilds it (it reports and preserves). Only an indexer does, so the
|
|
316
|
+
// caller must start one — before this it read as "not stale" and every
|
|
317
|
+
// hook stayed dark with no word to the user (hook audit 2026-09-28 P1-7).
|
|
318
|
+
if (report.reason === 'corrupt') return 'corrupt';
|
|
319
|
+
return report.index_version_stale === true;
|
|
402
320
|
} catch {
|
|
403
321
|
return false;
|
|
404
322
|
}
|
|
@@ -434,6 +352,7 @@ function ensureIndexFresh() {
|
|
|
434
352
|
|
|
435
353
|
let needsRefresh = false;
|
|
436
354
|
let unprobed = false;
|
|
355
|
+
let corrupt = false;
|
|
437
356
|
// Trigger 1: git HEAD newer than index mtime.
|
|
438
357
|
const gitBudget = budgetFor('git-log', 2000);
|
|
439
358
|
if (gitBudget === null) {
|
|
@@ -451,7 +370,8 @@ function ensureIndexFresh() {
|
|
|
451
370
|
// Trigger 2: INDEX_VERSION mismatch (only probe when mtime looked fresh).
|
|
452
371
|
if (!needsRefresh) {
|
|
453
372
|
const stale = indexNeedsRevalidation(bin, cwd);
|
|
454
|
-
if (stale ===
|
|
373
|
+
if (stale === 'corrupt') corrupt = needsRefresh = true;
|
|
374
|
+
else if (stale === true) needsRefresh = true;
|
|
455
375
|
else if (stale === null) unprobed = true;
|
|
456
376
|
}
|
|
457
377
|
|
|
@@ -467,6 +387,14 @@ function ensureIndexFresh() {
|
|
|
467
387
|
stdio: 'ignore',
|
|
468
388
|
}));
|
|
469
389
|
if (child && typeof child.unref === 'function') child.unref();
|
|
390
|
+
if (corrupt) {
|
|
391
|
+
notices.push(
|
|
392
|
+
'[code-graph] The index at .code-graph/index.db was corrupt, so every hook had nothing to say.\n' +
|
|
393
|
+
' Rebuilding it in the background; hooks resume when it finishes.\n' +
|
|
394
|
+
' If this repeats: code-graph-mcp rebuild-index --confirm'
|
|
395
|
+
);
|
|
396
|
+
return 'rebuilding-corrupt';
|
|
397
|
+
}
|
|
470
398
|
return 'refreshing';
|
|
471
399
|
}
|
|
472
400
|
|
|
@@ -503,7 +431,7 @@ function verifyBinary() {
|
|
|
503
431
|
const { findBinary } = require('./find-binary');
|
|
504
432
|
const binary = findBinary();
|
|
505
433
|
if (!binary) {
|
|
506
|
-
|
|
434
|
+
notices.push(missingBinaryMessage().trimEnd());
|
|
507
435
|
return { available: false, binary: null };
|
|
508
436
|
}
|
|
509
437
|
|
|
@@ -511,13 +439,11 @@ function verifyBinary() {
|
|
|
511
439
|
try {
|
|
512
440
|
fs.accessSync(binary, fs.constants.X_OK);
|
|
513
441
|
} catch {
|
|
514
|
-
|
|
442
|
+
notices.push(
|
|
515
443
|
`[code-graph] Binary not executable: ${binary}\n` +
|
|
516
|
-
`Fix: chmod +x "${binary}"
|
|
444
|
+
`Fix: chmod +x "${binary}"` +
|
|
445
|
+
(process.platform === 'darwin' ? `\nAlso try: xattr -d com.apple.quarantine "${binary}"` : '')
|
|
517
446
|
);
|
|
518
|
-
if (process.platform === 'darwin') {
|
|
519
|
-
process.stderr.write(`Also try: xattr -d com.apple.quarantine "${binary}"\n`);
|
|
520
|
-
}
|
|
521
447
|
return { available: false, binary, issue: 'not-executable' };
|
|
522
448
|
}
|
|
523
449
|
|
|
@@ -538,17 +464,17 @@ function verifyBinary() {
|
|
|
538
464
|
const msg = (err.message || '') + (err.stderr ? err.stderr.toString() : '');
|
|
539
465
|
if (msg.includes('quarantine') || msg.includes('not permitted') ||
|
|
540
466
|
msg.includes('killed') || err.status === 137 || err.signal === 'SIGKILL') {
|
|
541
|
-
|
|
467
|
+
notices.push(
|
|
542
468
|
`[code-graph] macOS Gatekeeper is blocking the binary: ${binary}\n` +
|
|
543
469
|
`Fix: xattr -d com.apple.quarantine "${binary}"\n` +
|
|
544
|
-
|
|
470
|
+
'Then restart Claude Code to reconnect the MCP server.'
|
|
545
471
|
);
|
|
546
472
|
return { available: false, binary, issue: 'quarantine' };
|
|
547
473
|
}
|
|
548
474
|
// Other errors (e.g., missing libs) — still report
|
|
549
|
-
|
|
475
|
+
notices.push(
|
|
550
476
|
`[code-graph] Binary found but failed to run: ${binary}\n` +
|
|
551
|
-
`Error: ${msg.slice(0, 200)}
|
|
477
|
+
`Error: ${msg.slice(0, 200)}`
|
|
552
478
|
);
|
|
553
479
|
return { available: false, binary, issue: 'runtime-error' };
|
|
554
480
|
}
|
|
@@ -623,16 +549,8 @@ function consistencyCheck(binary) {
|
|
|
623
549
|
}
|
|
624
550
|
} catch { /* skip check on error */ }
|
|
625
551
|
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
const lines = [`[code-graph] ${issues.length} consistency issue(s):`];
|
|
629
|
-
issues.forEach((issue, i) => {
|
|
630
|
-
lines.push(` ${i + 1}. ${issue.msg}`);
|
|
631
|
-
lines.push(` → ${issue.fix}`);
|
|
632
|
-
});
|
|
633
|
-
process.stderr.write(lines.join('\n') + '\n');
|
|
634
|
-
}
|
|
635
|
-
|
|
552
|
+
// Returned, not printed: stderr never reaches the user, and a version skew
|
|
553
|
+
// self-heals through the background auto-update (decision D5).
|
|
636
554
|
return issues;
|
|
637
555
|
}
|
|
638
556
|
|
|
@@ -648,11 +566,51 @@ function samePath(a, b) {
|
|
|
648
566
|
return real(a) === real(b);
|
|
649
567
|
}
|
|
650
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Whether to show the out-of-date-block notice in this project, recording it
|
|
571
|
+
* as shown. Once per shipped-template fingerprint: 0.164.0 showed it at every
|
|
572
|
+
* session start. Recorded in `.code-graph/` only when that directory already
|
|
573
|
+
* exists — creating it would put an unexcluded directory in `git status`, the
|
|
574
|
+
* side effect D3/D4 removed. Anything unrecordable (no fingerprint, no
|
|
575
|
+
* directory, a failed write) shows the notice: a failure must never silence it.
|
|
576
|
+
*/
|
|
577
|
+
function staleNoticeDue(cwd, fingerprint) {
|
|
578
|
+
if (!fingerprint) return true;
|
|
579
|
+
const dir = path.join(cwd, '.code-graph');
|
|
580
|
+
const marker = path.join(dir, 'stale-block-notice');
|
|
581
|
+
// `.code-graph/` is repo content: a clone can put a symlink or a hard link
|
|
582
|
+
// where the record goes (tar a FIFO), and a plain read or write follows it
|
|
583
|
+
// out of the project. Only a single-link regular file in a real directory is
|
|
584
|
+
// read or written — recommendation-log.js's guards; anything else is
|
|
585
|
+
// unrecordable.
|
|
586
|
+
const what = 'the stale-block notice record';
|
|
587
|
+
let dirStat;
|
|
588
|
+
try { dirStat = fs.lstatSync(dir); } catch { return true; }
|
|
589
|
+
if (!isOwnedPath(dir, 'dir', dirStat, what) || !isOwnedPath(marker, 'file', undefined, what)) return true;
|
|
590
|
+
let fd;
|
|
591
|
+
try {
|
|
592
|
+
fd = openOwned(marker,
|
|
593
|
+
fs.constants.O_RDWR | fs.constants.O_CREAT | (fs.constants.O_NONBLOCK || 0), what);
|
|
594
|
+
} catch { return true; }
|
|
595
|
+
if (fd === null) return true;
|
|
596
|
+
try {
|
|
597
|
+
const buf = Buffer.alloc(64);
|
|
598
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
599
|
+
if (buf.toString('utf8', 0, n).trim() === fingerprint) return false;
|
|
600
|
+
fs.ftruncateSync(fd, 0);
|
|
601
|
+
fs.writeSync(fd, fingerprint + '\n', 0);
|
|
602
|
+
} catch { /* unrecorded: shown again next session */ }
|
|
603
|
+
finally { fs.closeSync(fd); }
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
|
|
651
607
|
function runSessionInit({ source } = {}) {
|
|
652
608
|
// Fresh per run: this is module state, and the test suite calls this function
|
|
653
609
|
// many times in one process. A carried-over array would report last run's
|
|
654
610
|
// skips as this one's.
|
|
655
611
|
budgetSkips = [];
|
|
612
|
+
notices = [];
|
|
613
|
+
contextParts = [];
|
|
656
614
|
// GC the shared tmp dir before anything else, so it happens even on the
|
|
657
615
|
// inactive / non-project early returns below — those sessions still wrote
|
|
658
616
|
// cooldown flags on the way in. Cheap (one readdir + a stat per entry) and
|
|
@@ -742,13 +700,9 @@ function runSessionInit({ source } = {}) {
|
|
|
742
700
|
return { inactive: false, nonProject: true, lifecycle, autoUpdateLaunched: false };
|
|
743
701
|
}
|
|
744
702
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
`[code-graph] Warning: conflicting install detected — ${conflict.existingId} (${conflict.scope || 'unknown'} scope). ` +
|
|
749
|
-
`Use /plugin to remove one to avoid config conflicts.\n`
|
|
750
|
-
);
|
|
751
|
-
}
|
|
703
|
+
// `doctor` reports a conflicting install; a SessionStart line about it went
|
|
704
|
+
// to stderr, which no one sees (decision D5).
|
|
705
|
+
checkScopeConflict();
|
|
752
706
|
|
|
753
707
|
// Verify binary availability — catch issues early with actionable diagnostics
|
|
754
708
|
const binaryCheck = verifyBinary();
|
|
@@ -756,81 +710,28 @@ function runSessionInit({ source } = {}) {
|
|
|
756
710
|
const autoUpdateLaunched = launchBackgroundAutoUpdate(spawn, process.env, { force: isHighIntentSource(source) });
|
|
757
711
|
const indexFreshness = binaryCheck.available ? ensureIndexFresh() : 'skipped';
|
|
758
712
|
|
|
759
|
-
//
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
|
|
765
|
-
// (map injection, recent impact, consistency check, both hook canaries) with a
|
|
766
|
-
// raw stack trace (audit 2026-08-16 P1-16). adopt() now returns reasons rather
|
|
767
|
-
// than throwing; this is the belt to that suspenders, so a future unguarded
|
|
768
|
-
// read inside it cannot take the session down again.
|
|
769
|
-
let autoAdopt = { attempted: false, result: null };
|
|
713
|
+
// SessionStart no longer writes CLAUDE.md (decision D4); maybeAutoAdopt only
|
|
714
|
+
// cleans this plugin's legacy memory-dir artifacts and reports the project's
|
|
715
|
+
// adoption state. A block that has drifted from the shipped template is the
|
|
716
|
+
// one case worth a notice: its guidance is out of date and only the user can
|
|
717
|
+
// decide to refresh or remove it. Never throws out of this hook (P1-16).
|
|
718
|
+
let autoAdopt = { attempted: false, reason: null };
|
|
770
719
|
if (!isRelic) {
|
|
771
720
|
try {
|
|
772
721
|
autoAdopt = maybeAutoAdopt({ scriptPath: __dirname });
|
|
773
722
|
} catch (e) {
|
|
774
|
-
autoAdopt = { attempted:
|
|
775
|
-
process.stderr.write(
|
|
776
|
-
`[code-graph] Skipped CLAUDE.md adoption for this project (${(e && e.code) || (e && e.message) || 'unknown error'}).\n` +
|
|
777
|
-
' Everything else in this session start continues normally.\n'
|
|
778
|
-
);
|
|
723
|
+
autoAdopt = { attempted: false, reason: 'threw', error: (e && e.message) || String(e) };
|
|
779
724
|
}
|
|
780
725
|
}
|
|
781
|
-
if (autoAdopt.
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
`${autoAdopt.result.error || 'unknown'}). Nothing was changed.\n` +
|
|
789
|
-
' Opt out permanently: CODE_GRAPH_NO_AUTO_ADOPT=1\n'
|
|
726
|
+
if (autoAdopt.reason === 'stale' && staleNoticeDue(process.cwd(), autoAdopt.fingerprint)) {
|
|
727
|
+
notices.push(
|
|
728
|
+
"[code-graph] This project's CLAUDE.md carries an out-of-date code-graph block.\n" +
|
|
729
|
+
` Refresh it: ${adoptCommand()}\n` +
|
|
730
|
+
` Remove it: ${unadoptCommand()}\n` +
|
|
731
|
+
' (Shown once per shipped template. SessionStart no longer edits CLAUDE.md itself;\n' +
|
|
732
|
+
' CODE_GRAPH_NO_TEMPLATE_REFRESH=1 silences this.)'
|
|
790
733
|
);
|
|
791
734
|
}
|
|
792
|
-
const migrated = autoAdopt.migrated || {};
|
|
793
|
-
if (migrated.memoryIndexPruned || migrated.legacyDetailRemoved) {
|
|
794
|
-
process.stderr.write(
|
|
795
|
-
'[code-graph] Migrated to CLAUDE.md steering — cleaned legacy memory-dir artifacts\n' +
|
|
796
|
-
' (MEMORY.md sentinel + detail file). Your other memories are untouched.\n'
|
|
797
|
-
);
|
|
798
|
-
}
|
|
799
|
-
if (autoAdopt.attempted && autoAdopt.result && autoAdopt.result.ok) {
|
|
800
|
-
if (autoAdopt.reason === 'refreshed') {
|
|
801
|
-
// Name BOTH refreshed surfaces: the drift-refresh fully overwrites the
|
|
802
|
-
// generated detail doc, so a user who hand-edited it must learn why
|
|
803
|
-
// their edits vanished and how to lock the file.
|
|
804
|
-
const detailNote = autoAdopt.result.detailWritten
|
|
805
|
-
? ' + .claude/plugin_code_graph_mcp.md (manual edits to that generated file are overwritten)'
|
|
806
|
-
: '';
|
|
807
|
-
process.stderr.write(
|
|
808
|
-
`[code-graph] Refreshed CLAUDE.md decision block to latest shipped version${detailNote}.\n` +
|
|
809
|
-
' Lock files: CODE_GRAPH_NO_TEMPLATE_REFRESH=1 in ~/.claude/settings.json env\n'
|
|
810
|
-
);
|
|
811
|
-
} else {
|
|
812
|
-
process.stderr.write(
|
|
813
|
-
'[code-graph] Installed code-graph block into project CLAUDE.md (plugin install → knowing consent).\n' +
|
|
814
|
-
' Detail table: .claude/plugin_code_graph_mcp.md (generated; safe to gitignore)\n' +
|
|
815
|
-
' Opt out: CODE_GRAPH_NO_AUTO_ADOPT=1 in ~/.claude/settings.json env\n' +
|
|
816
|
-
` Reverse: ${unadoptCommand()}\n`
|
|
817
|
-
);
|
|
818
|
-
}
|
|
819
|
-
// `adopt()` has returned `registryRecorded` since it stopped throwing on a
|
|
820
|
-
// broken registry, and nothing read it. The consequence is not cosmetic:
|
|
821
|
-
// `uninstall()` walks that registry to strip our managed block from every
|
|
822
|
-
// adopted project's CLAUDE.md, so an unrecorded project keeps the block
|
|
823
|
-
// FOREVER after uninstall, with no plugin code left to remove it — the
|
|
824
|
-
// teardown-asymmetry class this repo has already been bitten by (audit
|
|
825
|
-
// 2026-08-16 review Minor tail).
|
|
826
|
-
if (autoAdopt.result.registryRecorded === false) {
|
|
827
|
-
process.stderr.write(
|
|
828
|
-
'[code-graph] Note: this project could not be recorded in the adopted-projects registry,\n' +
|
|
829
|
-
' so `/plugin uninstall` will NOT strip the block from this CLAUDE.md.\n' +
|
|
830
|
-
` Remove it by hand with \`${unadoptCommand()}\` before uninstalling.\n`
|
|
831
|
-
);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
735
|
|
|
835
736
|
// quietHooks: default quiet (project_map injection duplicates MEMORY.md +
|
|
836
737
|
// on-demand tool). CODE_GRAPH_VERBOSE_HOOKS=1 to opt in to the dump;
|
|
@@ -842,12 +743,6 @@ function runSessionInit({ source } = {}) {
|
|
|
842
743
|
const mapInjected = shouldInjectMap({ available: binaryCheck.available, quietHooks, adopted })
|
|
843
744
|
? injectProjectMap()
|
|
844
745
|
: false;
|
|
845
|
-
// v0.63 — live context: recent-change blast radius. Default-ON for adopted
|
|
846
|
-
// projects (separate gate from the duplicative static map); self-selecting
|
|
847
|
-
// (nothing injected when no source files changed recently).
|
|
848
|
-
const recentImpactInjected = shouldInjectRecentImpact({ available: binaryCheck.available, adopted, env: process.env })
|
|
849
|
-
? injectRecentImpact({ source })
|
|
850
|
-
: false;
|
|
851
746
|
const consistencyIssues = binaryCheck.available
|
|
852
747
|
? consistencyCheck(binaryCheck.binary)
|
|
853
748
|
: [];
|
|
@@ -855,18 +750,19 @@ function runSessionInit({ source } = {}) {
|
|
|
855
750
|
// FAILED firing self-test result (and refreshes it in the background, off the
|
|
856
751
|
// 5s budget); Layer B is the runtime dispatch-dark canary. Both best-effort.
|
|
857
752
|
const hookFireWarn = checkHookFiring();
|
|
858
|
-
if (hookFireWarn)
|
|
753
|
+
if (hookFireWarn) notices.push(hookFireWarn.trimEnd());
|
|
859
754
|
const hookDarkWarn = detectHookDark();
|
|
860
|
-
if (hookDarkWarn)
|
|
755
|
+
if (hookDarkWarn) notices.push(hookDarkWarn.trimEnd());
|
|
861
756
|
return {
|
|
862
757
|
inactive: false, lifecycle,
|
|
863
|
-
autoUpdateLaunched, indexFreshness, mapInjected,
|
|
758
|
+
autoUpdateLaunched, indexFreshness, mapInjected, binaryCheck, consistencyIssues,
|
|
864
759
|
quietHooks, adopted, autoAdopted: autoAdopt.attempted,
|
|
865
760
|
hookFireWarn: !!hookFireWarn, hookDarkWarn: !!hookDarkWarn,
|
|
866
761
|
// Which children the 5s budget cut, in the order they were reached. Empty
|
|
867
762
|
// on every healthy run; non-empty is the only signal that this SessionStart
|
|
868
763
|
// did less than it looks like it did.
|
|
869
764
|
budgetSkipped: budgetSkips.slice(),
|
|
765
|
+
notices: notices.slice(),
|
|
870
766
|
};
|
|
871
767
|
}
|
|
872
768
|
|
|
@@ -883,8 +779,7 @@ function injectProjectMap() {
|
|
|
883
779
|
|
|
884
780
|
// findBinary, not bare 'code-graph-mcp' on PATH (finding #8): a stale/global
|
|
885
781
|
// PATH binary reads a different/older index and returns "(empty project)"
|
|
886
|
-
// even when the local index is populated
|
|
887
|
-
// already avoided via findBinary().
|
|
782
|
+
// even when the local index is populated.
|
|
888
783
|
const { findBinary } = require('./find-binary');
|
|
889
784
|
const bin = findBinary();
|
|
890
785
|
if (!bin) return false;
|
|
@@ -901,14 +796,12 @@ function injectProjectMap() {
|
|
|
901
796
|
encoding: 'utf8',
|
|
902
797
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
903
798
|
// Hook-internal delivery, not a model conversion — keep record_cli_use from
|
|
904
|
-
// logging this `map` run as a phantom `use
|
|
799
|
+
// logging this `map` run as a phantom `use`.
|
|
905
800
|
env: { ...process.env, CODE_GRAPH_INTERNAL: '1' },
|
|
906
801
|
}));
|
|
907
802
|
|
|
908
803
|
if (output && output.trim()) {
|
|
909
|
-
|
|
910
|
-
'[code-graph] Project map (indexed):\n' + output.trim() + '\n'
|
|
911
|
-
);
|
|
804
|
+
contextParts.push('[code-graph] Project map (indexed):\n' + output.trim());
|
|
912
805
|
return true;
|
|
913
806
|
}
|
|
914
807
|
} catch {
|
|
@@ -917,102 +810,6 @@ function injectProjectMap() {
|
|
|
917
810
|
return false;
|
|
918
811
|
}
|
|
919
812
|
|
|
920
|
-
/**
|
|
921
|
-
* v0.63 — inject the recent-change blast radius (graph-unique "live" context).
|
|
922
|
-
* Changed source files = working-tree changes vs HEAD (WIP), else the last commit.
|
|
923
|
-
* The last-commit fallback is suppressed on a cold `startup` (low marginal value)
|
|
924
|
-
* via recentImpactWorthShowing. One bounded `affected --json` call; degrades to
|
|
925
|
-
* silent no-op on any error. Records a `live_impact` event so `stats` can see the
|
|
926
|
-
* feature fire (else it's a dark metric). Returns true iff something was injected.
|
|
927
|
-
*/
|
|
928
|
-
function injectRecentImpact({ source } = {}) {
|
|
929
|
-
try {
|
|
930
|
-
// Index + telemetry live at the canonical root (worktree → main checkout,
|
|
931
|
-
// subdir → project root); git WIP detection stays in the SESSION dir — the
|
|
932
|
-
// worktree's branch state is what this session edits. Repo-relative git
|
|
933
|
-
// paths translate 1:1 to root-relative index paths (a worktree mirrors the
|
|
934
|
-
// checkout layout).
|
|
935
|
-
const { resolveProjectRoot } = require('./project-root');
|
|
936
|
-
const sessionDir = process.cwd();
|
|
937
|
-
const cwd = resolveProjectRoot(sessionDir) || sessionDir;
|
|
938
|
-
const dbPath = path.join(cwd, '.code-graph', 'index.db');
|
|
939
|
-
if (!fs.existsSync(dbPath)) return false;
|
|
940
|
-
|
|
941
|
-
// WIP = ALL working-tree changes (staged + unstaged + UNTRACKED) in one
|
|
942
|
-
// `git status` call — the old diff-only path missed untracked new source
|
|
943
|
-
// files you're actively editing (finding #3). Clean tree → fall back to the
|
|
944
|
-
// last commit. Timeouts tightened (finding #1): worst-case cap sum is now
|
|
945
|
-
// status(1s) + HEAD~1(1s) + affected(1.5s) = 3.5s, comfortably under the 5s
|
|
946
|
-
// SessionStart hook budget; the old 2+2+3=7s could get the whole hook killed.
|
|
947
|
-
// Each git call re-reads the budget and builds its own opts rather than
|
|
948
|
-
// sharing one object: the fallback `git diff` runs AFTER `git status` has
|
|
949
|
-
// already spent wall clock, so a timeout computed once would let the pair
|
|
950
|
-
// overshoot by up to its own value. `hidden(...)` stays inline at both call
|
|
951
|
-
// sites — windows-hide.test.js follows a `const x = hidden(...)` binding but
|
|
952
|
-
// not one returned from a helper, and that guard is right to be that strict.
|
|
953
|
-
let changed = [];
|
|
954
|
-
let isWip = false;
|
|
955
|
-
try {
|
|
956
|
-
const statusMs = budgetFor('git-status', 1000);
|
|
957
|
-
if (statusMs === null) return false;
|
|
958
|
-
changed = filterSourceFiles(parseGitStatusPaths(
|
|
959
|
-
execSync('git status --porcelain --untracked-files=all',
|
|
960
|
-
hidden({ cwd: sessionDir, timeout: statusMs, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }))));
|
|
961
|
-
isWip = changed.length > 0;
|
|
962
|
-
// Clean tree → fall back to what the last commit touched.
|
|
963
|
-
if (!isWip) {
|
|
964
|
-
const diffMs = budgetFor('git-diff', 1000);
|
|
965
|
-
if (diffMs === null) return false;
|
|
966
|
-
changed = filterSourceFiles(
|
|
967
|
-
execSync('git diff --name-only HEAD~1 HEAD',
|
|
968
|
-
hidden({ cwd: sessionDir, timeout: diffMs, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })));
|
|
969
|
-
}
|
|
970
|
-
} catch {
|
|
971
|
-
return false; // not a git repo / no commits — nothing to diff
|
|
972
|
-
}
|
|
973
|
-
if (changed.length === 0) return false;
|
|
974
|
-
|
|
975
|
-
// Marginal-value gate: cold startup + clean tree (last-commit fallback) → skip,
|
|
976
|
-
// before spending the affected CLI call.
|
|
977
|
-
if (!recentImpactWorthShowing({ isWip, source })) return false;
|
|
978
|
-
|
|
979
|
-
const { findBinary } = require('./find-binary');
|
|
980
|
-
const bin = findBinary();
|
|
981
|
-
if (!bin) return false;
|
|
982
|
-
|
|
983
|
-
const affectedBudget = budgetFor('affected', 1500);
|
|
984
|
-
if (affectedBudget === null) return false;
|
|
985
|
-
|
|
986
|
-
let affected;
|
|
987
|
-
try {
|
|
988
|
-
const raw = execFileSync(bin, ['affected', ...changed, '--json'], hidden({
|
|
989
|
-
cwd, timeout: affectedBudget, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
990
|
-
env: { ...process.env, CODE_GRAPH_INTERNAL: '1' },
|
|
991
|
-
}));
|
|
992
|
-
affected = JSON.parse(raw);
|
|
993
|
-
} catch {
|
|
994
|
-
return false;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
const text = formatRecentImpact(changed, affected);
|
|
998
|
-
if (!text) return false;
|
|
999
|
-
process.stdout.write(text + '\n');
|
|
1000
|
-
|
|
1001
|
-
// Instrumentation (step 3a): record that the injection fired so `stats`
|
|
1002
|
-
// surfaces it instead of the feature being dark. Carries the blast/direct
|
|
1003
|
-
// counts + WIP flag for later reference-rate / A-B analysis. Best-effort.
|
|
1004
|
-
try {
|
|
1005
|
-
const all = Array.isArray(affected.affected_files) ? affected.affected_files : [];
|
|
1006
|
-
const direct = all.filter(a => a.depth === 1 && !a.is_test).length;
|
|
1007
|
-
const { recordRecommendation } = require('./recommendation-log');
|
|
1008
|
-
recordRecommendation(cwd, { hook: 'session', action: 'live_impact', blast: all.length, direct, wip: isWip });
|
|
1009
|
-
} catch { /* telemetry must never break the injection */ }
|
|
1010
|
-
return true;
|
|
1011
|
-
} catch {
|
|
1012
|
-
return false;
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
813
|
// ── v0.67.0 hook-reliability surfaces ─────────────────────────────────
|
|
1017
814
|
//
|
|
1018
815
|
// Layer A (firing): surface the cached `verify-hooks-fire` result. PUSH — the
|
|
@@ -1096,17 +893,11 @@ module.exports = {
|
|
|
1096
893
|
ensureIndexFresh,
|
|
1097
894
|
indexNeedsRevalidation,
|
|
1098
895
|
injectProjectMap,
|
|
1099
|
-
injectRecentImpact,
|
|
1100
896
|
verifyBinary, missingBinaryMessage, unadoptCommand,
|
|
1101
897
|
consistencyCheck,
|
|
1102
|
-
runSessionInit,
|
|
898
|
+
runSessionInit, staleNoticeDue,
|
|
1103
899
|
computeQuietHooks,
|
|
1104
900
|
shouldInjectMap,
|
|
1105
|
-
shouldInjectRecentImpact,
|
|
1106
|
-
recentImpactWorthShowing,
|
|
1107
|
-
filterSourceFiles,
|
|
1108
|
-
parseGitStatusPaths,
|
|
1109
|
-
formatRecentImpact,
|
|
1110
901
|
hookFireWarning, checkHookFiring, analyzeHookDark, detectHookDark, // v0.67.0
|
|
1111
902
|
};
|
|
1112
903
|
|
|
@@ -1132,6 +923,8 @@ if (require.main === module) {
|
|
|
1132
923
|
// housekeeping (audit 2026-08-16 P1-16). One `[code-graph]` line, exit 0.
|
|
1133
924
|
try {
|
|
1134
925
|
runSessionInit({ source });
|
|
926
|
+
const out = sessionStartOutput();
|
|
927
|
+
if (out) process.stdout.write(out);
|
|
1135
928
|
} catch (e) {
|
|
1136
929
|
process.stderr.write(
|
|
1137
930
|
`[code-graph] SessionStart hook error (${(e && e.code) || (e && e.name) || 'Error'}): ` +
|