brainclaw 1.24.0 → 1.26.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/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-code-map.js +9 -2
- package/dist/commands/code-map.js +120 -6
- package/dist/commands/mcp-catalog.js +46 -0
- package/dist/commands/mcp.js +58 -6
- package/dist/commands/session-start.js +84 -13
- package/dist/core/bootstrap.js +28 -4
- package/dist/core/code-map/aggregate.js +36 -31
- package/dist/core/code-map/backend.js +162 -5
- package/dist/core/code-map/core.js +1 -0
- package/dist/core/code-map/export.js +212 -0
- package/dist/core/code-map/finalizer.js +57 -2
- package/dist/core/code-map/freshness.js +81 -15
- package/dist/core/code-map/impact.js +409 -0
- package/dist/core/code-map/indexes.js +64 -3
- package/dist/core/code-map/lang/python/index.js +4 -2
- package/dist/core/code-map/lang/query-runtime.js +2 -0
- package/dist/core/code-map/lang/typescript/config.js +271 -0
- package/dist/core/code-map/lang/typescript/index.js +24 -6
- package/dist/core/code-map/lang/usages.js +333 -0
- package/dist/core/code-map/memory-reader.js +15 -0
- package/dist/core/code-map/query.js +285 -71
- package/dist/core/code-map/refresh.js +0 -0
- package/dist/core/code-map/resolve.js +28 -2
- package/dist/core/code-map/store.js +1 -0
- package/dist/core/code-map/types.js +70 -9
- package/dist/core/code-map/vocabulary.js +6 -0
- package/dist/core/code-map/work-section.js +12 -14
- package/dist/core/context-diff.js +17 -3
- package/dist/core/entity-operations.js +14 -2
- package/dist/core/federation-pull.js +151 -3
- package/dist/core/federation-push.js +16 -3
- package/dist/core/hint-aging.js +4 -1
- package/dist/core/identity.js +69 -17
- package/dist/core/io.js +27 -0
- package/dist/core/project-discovery.js +7 -1
- package/dist/core/protocol-tool-policy.js +3 -0
- package/dist/core/runtime.js +23 -0
- package/dist/core/worktree.js +89 -2
- package/dist/facts.js +15 -12
- package/dist/facts.json +14 -11
- package/docs/cli.md +8 -0
- package/docs/code-map.md +60 -28
- package/docs/integrations/mcp.md +5 -2
- package/docs/mcp-schema-changelog.md +11 -1
- package/package.json +1 -1
|
Binary file
|
|
@@ -3,12 +3,19 @@ export function registerCodeMapCommands(program) {
|
|
|
3
3
|
// --- code-map ---
|
|
4
4
|
program
|
|
5
5
|
.command('code-map <subcommand> [args...]')
|
|
6
|
-
.description('Query the per-project Code Map (status, refresh, find, brief)')
|
|
6
|
+
.description('Query the per-project Code Map (status, refresh, find, brief, impact, export, outline)')
|
|
7
7
|
.option('--json', 'Output as JSON')
|
|
8
8
|
.option('--all', 'For refresh: enumerate all supported files (full refresh)')
|
|
9
9
|
.option('--changed', 'For refresh: only changed files (default)')
|
|
10
10
|
.option('--cascade', 'For refresh/status in a multi-project workspace: cascade across every nested project (each gets its own store; the root store is scoped to files no child owns)')
|
|
11
|
-
.option('--limit <n>', 'Max results for find/brief', (v) => parseInt(v, 10))
|
|
11
|
+
.option('--limit <n>', 'Max results for find/brief/impact/outline', (v) => parseInt(v, 10))
|
|
12
|
+
.option('--depth <n>', 'For impact/export: maximum graph depth (export is hard-capped at 4)', (v) => parseInt(v, 10))
|
|
13
|
+
.option('--direction <direction>', 'For export: outgoing, incoming, or both')
|
|
14
|
+
.option('--format <format>', 'For export: json (default) or mermaid')
|
|
15
|
+
.option('--max-nodes <n>', 'For export: maximum nodes (hard-capped at 100)', (v) => parseInt(v, 10))
|
|
16
|
+
.option('--max-edges <n>', 'For export: maximum edges (hard-capped at 200)', (v) => parseInt(v, 10))
|
|
17
|
+
.option('--min-confidence <n>', 'For export: persisted confidence floor (minimum 0.5)', (v) => parseFloat(v))
|
|
18
|
+
.option('--target-kind <kind>', 'For export: symbol or file (auto-detected by default)')
|
|
12
19
|
.action((subcommand, args, options) => {
|
|
13
20
|
void runCodeMap(subcommand, args, options).catch((err) => {
|
|
14
21
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `brainclaw code-map <subcommand>` — CLI surface over the Code Map backend
|
|
3
3
|
* (spec §9). Mirrors plan-resource.ts: a switch over the subcommand delegating
|
|
4
|
-
* to a JsonlBackend (status | refresh | find | brief). The backend owns
|
|
4
|
+
* to a JsonlBackend (status | refresh | find | brief | impact | export | outline). The backend owns
|
|
5
5
|
* query logic; this file only adapts it to argv + stdout (text or --json), and
|
|
6
6
|
* every output carries the freshness_badge.
|
|
7
7
|
*/
|
|
8
8
|
import { JsonlBackend } from '../core/code-map/backend.js';
|
|
9
|
-
const KNOWN_SUBCOMMANDS = new Set(['status', 'refresh', 'find', 'brief']);
|
|
9
|
+
const KNOWN_SUBCOMMANDS = new Set(['status', 'refresh', 'find', 'brief', 'impact', 'export', 'outline']);
|
|
10
10
|
function backend() {
|
|
11
11
|
return new JsonlBackend();
|
|
12
12
|
}
|
|
@@ -15,10 +15,7 @@ function badgeLine(badge) {
|
|
|
15
15
|
const detail = detailKeys.length
|
|
16
16
|
? ` (${detailKeys.map((k) => `${k}=${JSON.stringify(badge.details[k])}`).join(', ')})`
|
|
17
17
|
: '';
|
|
18
|
-
|
|
19
|
-
// the precise status + details. `coarse` may be absent on legacy/hand-built badges.
|
|
20
|
-
const coarse = badge.coarse ? `${badge.coarse} · ` : '';
|
|
21
|
-
return `Freshness: ${coarse}${badge.status}${detail}`;
|
|
18
|
+
return `Freshness: ${badge.freshness}${detail}`;
|
|
22
19
|
}
|
|
23
20
|
export async function runCodeMap(subcommand, args, options = {}) {
|
|
24
21
|
const normalized = (subcommand ?? '').trim().toLowerCase();
|
|
@@ -57,6 +54,49 @@ export async function runCodeMap(subcommand, args, options = {}) {
|
|
|
57
54
|
printBrief(result, options);
|
|
58
55
|
return;
|
|
59
56
|
}
|
|
57
|
+
if (normalized === 'impact') {
|
|
58
|
+
const target = args.join(' ').trim();
|
|
59
|
+
if (!target) {
|
|
60
|
+
console.error('Error: code-map impact requires <symbol-or-path>.');
|
|
61
|
+
console.error(' Usage: brainclaw code-map impact <symbol-or-path> [--depth 2]');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const result = await be.impact({ target, depth: options.depth, limit: options.limit, cwd });
|
|
65
|
+
printImpact(result, options);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (normalized === 'export') {
|
|
69
|
+
const target = args.join(' ').trim();
|
|
70
|
+
if (!target) {
|
|
71
|
+
console.error('Error: code-map export requires <symbol-or-path>.');
|
|
72
|
+
console.error(' Usage: brainclaw code-map export <symbol-or-path> [--direction both] [--depth 1] [--max-nodes 100] [--max-edges 200]');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const result = await be.exportGraph({
|
|
76
|
+
target,
|
|
77
|
+
targetKind: options.targetKind,
|
|
78
|
+
direction: options.direction,
|
|
79
|
+
depth: options.depth,
|
|
80
|
+
maxNodes: options.maxNodes,
|
|
81
|
+
maxEdges: options.maxEdges,
|
|
82
|
+
minConfidence: options.minConfidence,
|
|
83
|
+
format: options.format,
|
|
84
|
+
cwd,
|
|
85
|
+
});
|
|
86
|
+
printExport(result, options);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (normalized === 'outline') {
|
|
90
|
+
const target = args.join(' ').trim();
|
|
91
|
+
if (!target) {
|
|
92
|
+
console.error('Error: code-map outline requires <file>.');
|
|
93
|
+
console.error(' Usage: brainclaw code-map outline <file>');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
const result = await be.outline({ path: target, limit: options.limit, cwd });
|
|
97
|
+
printOutline(result, options);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
60
100
|
console.error(`Error: unknown code-map subcommand "${subcommand}".`);
|
|
61
101
|
console.error(` Available: ${[...KNOWN_SUBCOMMANDS].join(', ')}`);
|
|
62
102
|
process.exit(1);
|
|
@@ -144,4 +184,78 @@ function printBrief(result, options) {
|
|
|
144
184
|
}
|
|
145
185
|
}
|
|
146
186
|
}
|
|
187
|
+
function printImpact(result, options) {
|
|
188
|
+
if (options.json) {
|
|
189
|
+
console.log(JSON.stringify(result, null, 2));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const cause = (row) => row.causes.map((item) => `${item.kind}${item.module ? ` ${item.module}` : ''}${item.source_line ? `:${item.source_line}` : ''}`).join(', ');
|
|
193
|
+
console.log(`Code Map impact: "${result.target}"`);
|
|
194
|
+
console.log(` ${badgeLine(result.freshness_badge)}`);
|
|
195
|
+
console.log(` Definition: ${result.definition.entries.length} (${result.definition.match_kind})`);
|
|
196
|
+
for (const entry of result.definition.entries)
|
|
197
|
+
console.log(` ${entry.name} — ${entry.path}`);
|
|
198
|
+
console.log(` Direct dependents: ${result.direct_dependents.length}${result.limits.direct_truncated ? '+' : ''}`);
|
|
199
|
+
for (const dependent of result.direct_dependents)
|
|
200
|
+
console.log(` ${dependent.path} — ${cause(dependent)}`);
|
|
201
|
+
if (result.limits.max_depth > 1) {
|
|
202
|
+
console.log(` Transitive dependents: ${result.transitive_dependents.length}${result.limits.transitive_truncated ? '+' : ''}`);
|
|
203
|
+
for (const dependent of result.transitive_dependents)
|
|
204
|
+
console.log(` [depth ${dependent.depth}] ${dependent.path} — ${cause(dependent)}`);
|
|
205
|
+
}
|
|
206
|
+
console.log(` Tests: ${result.risk.counters.resolved_test_files} resolved, ${result.risk.counters.suggested_test_files} naming suggestion(s)`);
|
|
207
|
+
for (const test of result.tests_for)
|
|
208
|
+
console.log(` [${test.relation}, confidence=${test.confidence}] ${test.path} — ${test.reason}`);
|
|
209
|
+
console.log(` Risk: ${result.risk.score} (${result.risk.formula}; direct=${result.risk.counters.direct_dependents}, transitive=${result.risk.counters.transitive_dependents})`);
|
|
210
|
+
}
|
|
211
|
+
function printExport(result, options) {
|
|
212
|
+
if (options.json) {
|
|
213
|
+
console.log(JSON.stringify(result, null, 2));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
console.log(`Code Map export: "${result.target}" (${result.target_kind})`);
|
|
217
|
+
console.log(` ${badgeLine(result.freshness_badge)}`);
|
|
218
|
+
console.log(` Roots: ${result.root_node_ids.length}; nodes=${result.nodes.length}/${result.limits.max_nodes}; edges=${result.edges.length}/${result.limits.max_edges}; depth=${result.limits.max_depth}; direction=${result.limits.direction}; min-confidence=${result.limits.min_confidence}`);
|
|
219
|
+
const truncation = Object.entries(result.truncated).filter(([, value]) => value).map(([key]) => key);
|
|
220
|
+
if (truncation.length > 0)
|
|
221
|
+
console.log(` Truncated: ${truncation.join(', ')}`);
|
|
222
|
+
if (result.format === 'mermaid' && result.mermaid) {
|
|
223
|
+
console.log(result.mermaid);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
for (const edge of result.edges) {
|
|
227
|
+
const source = edge.source ? ` @ ${edge.source.path}${edge.source.line === null || edge.source.line === undefined ? '' : `:${edge.source.line}`}` : '';
|
|
228
|
+
console.log(` ${edge.from} -[${edge.kind}, confidence=${edge.confidence}${source}]-> ${edge.to}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function printOutline(result, options) {
|
|
232
|
+
if (options.json) {
|
|
233
|
+
console.log(JSON.stringify(result, null, 2));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
console.log(`Code Map outline: "${result.path}"`);
|
|
237
|
+
console.log(` Index: ${result.index_status}`);
|
|
238
|
+
console.log(` ${badgeLine(result.freshness_badge)}`);
|
|
239
|
+
if (!result.file_indexed) {
|
|
240
|
+
console.log(' Symbols: (file not indexed)');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
console.log(` Parse: ${result.parse_status}`);
|
|
244
|
+
if (result.symbols.length === 0) {
|
|
245
|
+
console.log(' Symbols: (none)');
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
for (const symbol of result.symbols) {
|
|
249
|
+
const subtype = symbol.subtype ? ` ${symbol.subtype}` : '';
|
|
250
|
+
const span = symbol.span ? `${symbol.span.start_line}:${symbol.span.start_col}` : 'unknown';
|
|
251
|
+
const exported = symbol.exported ? ' export' : '';
|
|
252
|
+
console.log(` [${span}] ${symbol.kind}${subtype} ${symbol.name}${exported} confidence=${symbol.confidence}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (result.truncated)
|
|
256
|
+
console.log(` … ${result.symbol_count - result.symbols.length} more indexed symbol(s)`);
|
|
257
|
+
if (result.diagnostics.length > 0) {
|
|
258
|
+
console.log(` Diagnostics: ${result.diagnostics.length}${result.diagnostics_truncated ? '+' : ''}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
147
261
|
//# sourceMappingURL=code-map.js.map
|
|
@@ -409,6 +409,52 @@ export const MCP_READ_TOOLS = [
|
|
|
409
409
|
required: ['target'],
|
|
410
410
|
},
|
|
411
411
|
},
|
|
412
|
+
{
|
|
413
|
+
name: 'bclaw_code_impact',
|
|
414
|
+
description: 'Explain a symbol or source file\'s local blast radius from existing resolved imports: definition, direct dependents with edge causes, optional bounded transitives, affected test files, low-confidence naming suggestions kept separate, and a count-based risk score. Read-only; never refreshes.',
|
|
415
|
+
annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'auto' },
|
|
416
|
+
inputSchema: {
|
|
417
|
+
type: 'object',
|
|
418
|
+
properties: {
|
|
419
|
+
target: { type: 'string', description: 'Symbol name or source-file path whose impact to inspect.' },
|
|
420
|
+
depth: { type: 'number', description: 'Maximum graph depth. 1 (default) returns direct dependents only; 2+ opts into transitives. Clamped to 4.' },
|
|
421
|
+
limit: { type: 'number', description: 'Maximum rows in each dependent section and in naming suggestions. Clamped to 100.' },
|
|
422
|
+
},
|
|
423
|
+
required: ['target'],
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: 'bclaw_code_export',
|
|
428
|
+
description: 'Export a compact, deterministic, confidence-filtered local Code Map subgraph around one symbol or file. Requires a target; defaults to one hop and hard caps nodes (100), edges (200), and depth (4), so it never exports a whole graph. JSON retains every edge kind, source, and confidence. format="mermaid" adds a projection of the same JSON nodes/edges. Read-only; never refreshes or calls an external service.',
|
|
429
|
+
annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'auto' },
|
|
430
|
+
inputSchema: {
|
|
431
|
+
type: 'object',
|
|
432
|
+
properties: {
|
|
433
|
+
target: { type: 'string', description: 'Symbol name or source-file path around which to export a local subgraph.' },
|
|
434
|
+
targetKind: { type: 'string', enum: ['symbol', 'file'], description: 'Optional explicit target kind; otherwise a source path is detected safely.' },
|
|
435
|
+
direction: { type: 'string', enum: ['outgoing', 'incoming', 'both'], description: 'Which edge direction(s) to follow. Default: both.' },
|
|
436
|
+
depth: { type: 'number', description: 'Maximum graph distance from the root(s). Default 1; clamped to 0..4.' },
|
|
437
|
+
maxNodes: { type: 'number', description: 'Maximum nodes returned. Default 100; hard-capped at 100.' },
|
|
438
|
+
maxEdges: { type: 'number', description: 'Maximum edges returned. Default 200; hard-capped at 200.' },
|
|
439
|
+
minConfidence: { type: 'number', description: 'Persisted confidence threshold for nodes and relationships. Default/minimum 0.5.' },
|
|
440
|
+
format: { type: 'string', enum: ['json', 'mermaid'], description: 'Default json; mermaid adds a projection derived from the same bounded graph.' },
|
|
441
|
+
},
|
|
442
|
+
required: ['target'],
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: 'bclaw_code_outline',
|
|
447
|
+
description: 'Return a bounded source-ordered outline for one indexed file: symbol name, kind, subtype, span, exported flag, and persisted confidence. Read-only: it reads the existing file shard only, never parses or refreshes. `index_status` distinguishes a missing index, a file not indexed, and an indexed file with zero symbols.',
|
|
448
|
+
annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'auto' },
|
|
449
|
+
inputSchema: {
|
|
450
|
+
type: 'object',
|
|
451
|
+
properties: {
|
|
452
|
+
path: { type: 'string', description: 'Workspace-relative source file path (for example `src/app/App.tsx`).' },
|
|
453
|
+
limit: { type: 'number', description: 'Maximum symbols to return; clamped to the hard cap of 200.' },
|
|
454
|
+
},
|
|
455
|
+
required: ['path'],
|
|
456
|
+
},
|
|
457
|
+
},
|
|
412
458
|
];
|
|
413
459
|
const MCP_WRITE_TOOLS = [
|
|
414
460
|
{
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1089,15 +1089,15 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1089
1089
|
}
|
|
1090
1090
|
// Code Map tools (spec §9). These delegate to the async JsonlBackend, so
|
|
1091
1091
|
// they are handled here rather than via the synchronous read-tool path.
|
|
1092
|
-
// status/find/brief are reads; refresh is a write (prompt approval).
|
|
1093
|
-
if (name === 'bclaw_code_status' || name === 'bclaw_code_find' || name === 'bclaw_code_brief' || name === 'bclaw_code_refresh') {
|
|
1092
|
+
// status/find/brief/impact/export/outline are reads; refresh is a write (prompt approval).
|
|
1093
|
+
if (name === 'bclaw_code_status' || name === 'bclaw_code_find' || name === 'bclaw_code_brief' || name === 'bclaw_code_impact' || name === 'bclaw_code_export' || name === 'bclaw_code_outline' || name === 'bclaw_code_refresh') {
|
|
1094
1094
|
const { JsonlBackend } = await import('../core/code-map/backend.js');
|
|
1095
1095
|
const be = new JsonlBackend();
|
|
1096
1096
|
if (name === 'bclaw_code_status') {
|
|
1097
1097
|
const status = await be.status({ cwd, cascade: args.cascade === true });
|
|
1098
1098
|
return {
|
|
1099
1099
|
response: toolResponse({
|
|
1100
|
-
content: [{ type: 'text', text: `Code Map: ${status.store_exists ? 'store present' : 'no store'} — freshness=${status.freshness_badge.
|
|
1100
|
+
content: [{ type: 'text', text: `Code Map: ${status.store_exists ? 'store present' : 'no store'} — freshness=${status.freshness_badge.freshness}` }],
|
|
1101
1101
|
structuredContent: { ...status, freshness_badge: status.freshness_badge },
|
|
1102
1102
|
}),
|
|
1103
1103
|
};
|
|
@@ -1108,7 +1108,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1108
1108
|
const cascadeNote = result.cascade ? ` cascade=${result.cascade.children_refreshed} child(ren)+root` : '';
|
|
1109
1109
|
return {
|
|
1110
1110
|
response: toolResponse({
|
|
1111
|
-
content: [{ type: 'text', text: `Code Map refresh [${result.scope}]: ran=${result.ran} freshness=${result.freshness_badge.
|
|
1111
|
+
content: [{ type: 'text', text: `Code Map refresh [${result.scope}]: ran=${result.ran} freshness=${result.freshness_badge.freshness}${cascadeNote}${result.lock_status ? ` (${result.lock_status})` : ''}` }],
|
|
1112
1112
|
structuredContent: { ...result, freshness_badge: result.freshness_badge },
|
|
1113
1113
|
}),
|
|
1114
1114
|
};
|
|
@@ -1122,7 +1122,59 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1122
1122
|
const result = await be.find({ query, limit, cwd });
|
|
1123
1123
|
return {
|
|
1124
1124
|
response: toolResponse({
|
|
1125
|
-
content: [{ type: 'text', text: `Code Map find "${result.query}": ${result.matches.length} match(es), freshness=${result.freshness_badge.
|
|
1125
|
+
content: [{ type: 'text', text: `Code Map find "${result.query}": ${result.matches.length} match(es), freshness=${result.freshness_badge.freshness}` }],
|
|
1126
|
+
structuredContent: { ...result, freshness_badge: result.freshness_badge },
|
|
1127
|
+
}),
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
// bclaw_code_export
|
|
1131
|
+
if (name === 'bclaw_code_export') {
|
|
1132
|
+
const target = typeof args.target === 'string' ? args.target : '';
|
|
1133
|
+
if (!target.trim()) {
|
|
1134
|
+
return { response: createToolErrorResponse('validation_error', 'bclaw_code_export requires a non-empty target.') };
|
|
1135
|
+
}
|
|
1136
|
+
const targetKind = args.targetKind === 'symbol' || args.targetKind === 'file' ? args.targetKind : undefined;
|
|
1137
|
+
const direction = args.direction === 'outgoing' || args.direction === 'incoming' || args.direction === 'both' ? args.direction : undefined;
|
|
1138
|
+
const depth = typeof args.depth === 'number' ? args.depth : undefined;
|
|
1139
|
+
const maxNodes = typeof args.maxNodes === 'number' ? args.maxNodes : undefined;
|
|
1140
|
+
const maxEdges = typeof args.maxEdges === 'number' ? args.maxEdges : undefined;
|
|
1141
|
+
const minConfidence = typeof args.minConfidence === 'number' ? args.minConfidence : undefined;
|
|
1142
|
+
const format = args.format === 'mermaid' ? 'mermaid' : args.format === 'json' ? 'json' : undefined;
|
|
1143
|
+
const result = await be.exportGraph({ target, targetKind, direction, depth, maxNodes, maxEdges, minConfidence, format, cwd });
|
|
1144
|
+
return {
|
|
1145
|
+
response: toolResponse({
|
|
1146
|
+
content: [{ type: 'text', text: `Code Map export "${result.target}": ${result.nodes.length} node(s), ${result.edges.length} edge(s), depth=${result.limits.max_depth}, freshness=${result.freshness_badge.freshness}` }],
|
|
1147
|
+
structuredContent: { ...result, freshness_badge: result.freshness_badge },
|
|
1148
|
+
}),
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
// bclaw_code_impact
|
|
1152
|
+
if (name === 'bclaw_code_impact') {
|
|
1153
|
+
const target = typeof args.target === 'string' ? args.target : '';
|
|
1154
|
+
if (!target.trim()) {
|
|
1155
|
+
return { response: createToolErrorResponse('validation_error', 'bclaw_code_impact requires a non-empty target.') };
|
|
1156
|
+
}
|
|
1157
|
+
const depth = typeof args.depth === 'number' ? args.depth : undefined;
|
|
1158
|
+
const limit = typeof args.limit === 'number' ? args.limit : undefined;
|
|
1159
|
+
const result = await be.impact({ target, depth, limit, cwd });
|
|
1160
|
+
return {
|
|
1161
|
+
response: toolResponse({
|
|
1162
|
+
content: [{ type: 'text', text: `Code Map impact "${result.target}": ${result.risk.counters.direct_dependents} direct, ${result.risk.counters.transitive_dependents} transitive dependent(s), risk=${result.risk.score}, freshness=${result.freshness_badge.freshness}` }],
|
|
1163
|
+
structuredContent: { ...result, freshness_badge: result.freshness_badge },
|
|
1164
|
+
}),
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
// bclaw_code_outline
|
|
1168
|
+
if (name === 'bclaw_code_outline') {
|
|
1169
|
+
const outlinePath = typeof args.path === 'string' ? args.path : '';
|
|
1170
|
+
if (!outlinePath.trim()) {
|
|
1171
|
+
return { response: createToolErrorResponse('validation_error', 'bclaw_code_outline requires a non-empty path.') };
|
|
1172
|
+
}
|
|
1173
|
+
const limit = typeof args.limit === 'number' ? args.limit : undefined;
|
|
1174
|
+
const result = await be.outline({ path: outlinePath, limit, cwd });
|
|
1175
|
+
return {
|
|
1176
|
+
response: toolResponse({
|
|
1177
|
+
content: [{ type: 'text', text: `Code Map outline "${result.path}": ${result.symbols.length}/${result.symbol_count} symbol(s), index=${result.index_status}, freshness=${result.freshness_badge.freshness}` }],
|
|
1126
1178
|
structuredContent: { ...result, freshness_badge: result.freshness_badge },
|
|
1127
1179
|
}),
|
|
1128
1180
|
};
|
|
@@ -1136,7 +1188,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1136
1188
|
const result = await be.brief({ target, limit, cwd });
|
|
1137
1189
|
return {
|
|
1138
1190
|
response: toolResponse({
|
|
1139
|
-
content: [{ type: 'text', text: `Code Map brief "${result.target}": ${result.suggested_files_to_read.length} file(s) to read, freshness=${result.freshness_badge.
|
|
1191
|
+
content: [{ type: 'text', text: `Code Map brief "${result.target}": ${result.suggested_files_to_read.length} file(s) to read, freshness=${result.freshness_badge.freshness}` }],
|
|
1140
1192
|
structuredContent: { ...result, freshness_badge: result.freshness_badge },
|
|
1141
1193
|
}),
|
|
1142
1194
|
};
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { execSync } from 'node:child_process';
|
|
5
|
-
import { memoryExists, resolveEntityDir } from '../core/io.js';
|
|
5
|
+
import { isSessionSnapshotRecordFilename, memoryExists, resolveEntityDir, sessionSnapshotRecordPaths } from '../core/io.js';
|
|
6
6
|
import { loadVersionedJsonFile, saveVersionedJsonFile } from '../core/migration.js';
|
|
7
7
|
import { buildOperationalIdentity, loadAllSessions, saveCurrentSession } from '../core/identity.js';
|
|
8
8
|
import { requireMinimumTrustLevel, resolveCurrentModel, resolveOrAutoRegisterAgentIdentity } from '../core/agent-registry.js';
|
|
@@ -25,11 +25,55 @@ import { loadHygienePolicy } from '../core/hygiene-policy.js';
|
|
|
25
25
|
import { maybeCreateCheckpoint } from '../core/events/checkpoint.js';
|
|
26
26
|
import { pullSignalsFromLinkedProjects, markSignalProcessed } from '../core/federation-transport.js';
|
|
27
27
|
import { materializeFederationSignal } from '../core/federation-materialize.js';
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* pln#670 — snapshot writes always target the canonical directory ('write' mode).
|
|
30
|
+
* The previous 'read'-mode resolution meant a fresh store (no coordination/sessions
|
|
31
|
+
* yet) landed the snapshot in the legacy dir — the current_session home — where
|
|
32
|
+
* saveCurrentSession then clobbered it (same `<session_id>.json` name, same id).
|
|
33
|
+
*/
|
|
34
|
+
function sessionSnapshotWriteDir(cwd) {
|
|
35
|
+
return resolveEntityDir('sessions', cwd ?? process.cwd(), 'write');
|
|
30
36
|
}
|
|
31
37
|
function sessionSnapshotPath(sessionId, cwd) {
|
|
32
|
-
return path.join(
|
|
38
|
+
return path.join(sessionSnapshotWriteDir(cwd), `${sessionId}.snapshot.json`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* pln#670 — lazy migration of pre-split snapshot records: rename `<id>.json` to
|
|
42
|
+
* `<id>.snapshot.json` in the CANONICAL sessions directory only. The legacy
|
|
43
|
+
* directory is never scanned — it is the current_session home. Only files that
|
|
44
|
+
* validate as session_snapshot are touched; anything else is left in place.
|
|
45
|
+
*/
|
|
46
|
+
export function migrateLegacySnapshotNames(cwd) {
|
|
47
|
+
const dir = sessionSnapshotWriteDir(cwd);
|
|
48
|
+
if (!fs.existsSync(dir))
|
|
49
|
+
return 0;
|
|
50
|
+
let renamed = 0;
|
|
51
|
+
for (const file of fs.readdirSync(dir)) {
|
|
52
|
+
// Case-insensitive suffix checks (codex review P1): Windows filesystems
|
|
53
|
+
// match names case-insensitively — an upper-cased `.SNAPSHOT.json` is the
|
|
54
|
+
// same record a lower-case probe reads, and must not be re-suffixed.
|
|
55
|
+
if (!file.toLowerCase().endsWith('.json') || isSessionSnapshotRecordFilename(file))
|
|
56
|
+
continue;
|
|
57
|
+
const from = path.join(dir, file);
|
|
58
|
+
try {
|
|
59
|
+
// Discriminate on the RAW file — the migration loader zod-strips unknown
|
|
60
|
+
// keys, so a current_session record parses as a clean snapshot after it.
|
|
61
|
+
// Key PRESENCE, not value type (codex review P1): the invariant is
|
|
62
|
+
// "never touch a record CARRYING last_seen_at", and `last_seen_at: null`
|
|
63
|
+
// must be rejected too.
|
|
64
|
+
const raw = JSON.parse(fs.readFileSync(from, 'utf-8'));
|
|
65
|
+
if (Object.hasOwn(raw, 'last_seen_at'))
|
|
66
|
+
continue;
|
|
67
|
+
SessionSnapshotSchema.parse(loadVersionedJsonFile('session_snapshot', from).document);
|
|
68
|
+
const to = path.join(dir, `${file.slice(0, -'.json'.length)}.snapshot.json`);
|
|
69
|
+
if (!fs.existsSync(to)) {
|
|
70
|
+
fs.renameSync(from, to);
|
|
71
|
+
renamed++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch { /* not a session_snapshot — leave it alone */ }
|
|
75
|
+
}
|
|
76
|
+
return renamed;
|
|
33
77
|
}
|
|
34
78
|
export async function runSessionStart(options = {}) {
|
|
35
79
|
try {
|
|
@@ -155,7 +199,7 @@ export async function startSession(options = {}) {
|
|
|
155
199
|
...(model ? { model } : {}),
|
|
156
200
|
};
|
|
157
201
|
// Persist snapshot
|
|
158
|
-
const dir =
|
|
202
|
+
const dir = sessionSnapshotWriteDir(options.cwd);
|
|
159
203
|
if (!fs.existsSync(dir))
|
|
160
204
|
fs.mkdirSync(dir, { recursive: true });
|
|
161
205
|
saveVersionedJsonFile('session_snapshot', sessionSnapshotPath(snapshot.session_id, options.cwd), SessionSnapshotSchema.parse(snapshot));
|
|
@@ -218,6 +262,14 @@ export async function startSession(options = {}) {
|
|
|
218
262
|
inventoryAdvisory = lines;
|
|
219
263
|
}
|
|
220
264
|
catch { /* non-fatal — inventory scan failure should not block session start */ }
|
|
265
|
+
// pln#670 — lazy rename of pre-split snapshot records to the type-suffixed
|
|
266
|
+
// name. Session-start full maintenance is the natural sweep point (no daemon,
|
|
267
|
+
// feedback_lazy_reconcile_pattern); dual-read keeps unrenamed records readable
|
|
268
|
+
// in the meantime.
|
|
269
|
+
try {
|
|
270
|
+
migrateLegacySnapshotNames(options.cwd);
|
|
271
|
+
}
|
|
272
|
+
catch { /* non-fatal — name migration must never block session start */ }
|
|
221
273
|
// pln#564 step B — cap the runtime-note tree on session start (no LLM gate,
|
|
222
274
|
// unlike the compaction-phase archiveSessionNotes). Keeps the newest N
|
|
223
275
|
// session/lifecycle notes per agent + all genuine observations, parks the
|
|
@@ -367,14 +419,33 @@ function isPidAlive(pid) {
|
|
|
367
419
|
}
|
|
368
420
|
}
|
|
369
421
|
export function loadSessionSnapshot(sessionId, cwd) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
422
|
+
// pln#670 — probe the type-suffixed name first, then the pre-split `<id>.json`
|
|
423
|
+
// layouts. SessionSnapshotSchema is non-strict, so a current_session record for
|
|
424
|
+
// the same id would parse too (zod strips unknown keys) — the negative
|
|
425
|
+
// discriminant is `last_seen_at`, which only current_session carries.
|
|
426
|
+
for (const p of sessionSnapshotRecordPaths(sessionId, cwd)) {
|
|
427
|
+
if (!fs.existsSync(p))
|
|
428
|
+
continue;
|
|
429
|
+
try {
|
|
430
|
+
// Discriminate on the RAW file: the migration loader zod-strips unknown
|
|
431
|
+
// keys, so a current_session record would come back looking like a clean
|
|
432
|
+
// snapshot. Key PRESENCE, not value type (codex review P1) — a record
|
|
433
|
+
// carrying `last_seen_at: null` must be rejected too.
|
|
434
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
435
|
+
if (Object.hasOwn(raw, 'last_seen_at'))
|
|
436
|
+
continue;
|
|
437
|
+
const snapshot = SessionSnapshotSchema.parse(loadVersionedJsonFile('session_snapshot', p).document);
|
|
438
|
+
// The filename is not a type-safe identity boundary by itself (codex
|
|
439
|
+
// review): querying `sess_x.snapshot` also constructs `sess_x.snapshot.json`
|
|
440
|
+
// — the snapshot of sess_x. Only return a payload that names the caller's id.
|
|
441
|
+
if (snapshot.session_id !== sessionId)
|
|
442
|
+
continue;
|
|
443
|
+
return snapshot;
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
378
448
|
}
|
|
449
|
+
return undefined;
|
|
379
450
|
}
|
|
380
451
|
//# sourceMappingURL=session-start.js.map
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -9,6 +9,7 @@ import { mutate } from './mutation-pipeline.js';
|
|
|
9
9
|
import { BootstrapApplicationReceiptSchema, BootstrapInterviewAnswerSchema, BootstrapInterviewPlanSchema, BootstrapInterviewQuestionSchema, BootstrapImportPlanDocumentSchema, BootstrapProfileDocumentSchema, BootstrapSuggestionDocumentSchema, MemorySeedDocumentSchema, } from './schema.js';
|
|
10
10
|
import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
|
|
11
11
|
import { analyzeRepository, findNestedAgentsFiles } from './repo-analysis.js';
|
|
12
|
+
import { isManagedByBrainclaw } from './project-discovery.js';
|
|
12
13
|
import { buildExecutionContext, compactExecutionContext } from './execution-context.js';
|
|
13
14
|
import { buildAgentToolingContext } from './agent-context.js';
|
|
14
15
|
import { createInstruction, loadInstructions, saveInstruction } from './instructions.js';
|
|
@@ -256,15 +257,38 @@ function buildBootstrapArtifacts(input) {
|
|
|
256
257
|
sourcesScanned.push('README');
|
|
257
258
|
seeds.push(...extractReadmeSeeds(readmePath, input.target));
|
|
258
259
|
}
|
|
260
|
+
// pln#671 — instruction files generated by `brainclaw export` derive FROM
|
|
261
|
+
// brainclaw memory: deriving seeds from them feeds the store its own output
|
|
262
|
+
// back as "new" knowledge. Seed extraction skips managed files; DETECTION
|
|
263
|
+
// (agentsPresent, native_instruction_files, source fingerprint) stays
|
|
264
|
+
// complete — that is environment inventory, not knowledge to import. The
|
|
265
|
+
// skip is traced in sources_scanned so the exclusion is never silent.
|
|
259
266
|
const agentsPath = path.join(scanRoot, 'AGENTS.md');
|
|
260
267
|
const agentsPresent = fs.existsSync(agentsPath);
|
|
261
268
|
if (agentsPresent) {
|
|
262
|
-
|
|
263
|
-
|
|
269
|
+
if (isManagedByBrainclaw(agentsPath)) {
|
|
270
|
+
sourcesScanned.push('AGENTS.md (brainclaw-managed — skipped)');
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
sourcesScanned.push('AGENTS.md');
|
|
274
|
+
seeds.push(...extractAgentsSeeds(agentsPath, input.target));
|
|
275
|
+
}
|
|
264
276
|
}
|
|
265
277
|
if (nativeInstructionFiles.length > 0) {
|
|
266
|
-
|
|
267
|
-
|
|
278
|
+
// AGENTS.md is handled by its own extractor above — keep it out of the
|
|
279
|
+
// native accounting so a managed AGENTS.md is not counted as skipped twice.
|
|
280
|
+
const nativeCandidates = nativeInstructionFiles
|
|
281
|
+
.filter((relativePath) => path.basename(relativePath) !== 'AGENTS.md');
|
|
282
|
+
const humanAuthored = nativeCandidates
|
|
283
|
+
.filter((relativePath) => !isManagedByBrainclaw(path.join(scanRoot, relativePath)));
|
|
284
|
+
const skippedManaged = nativeCandidates.length - humanAuthored.length;
|
|
285
|
+
if (humanAuthored.length > 0) {
|
|
286
|
+
sourcesScanned.push('native_instructions');
|
|
287
|
+
seeds.push(...extractNativeInstructionSeeds(humanAuthored.map((relativePath) => path.join(scanRoot, relativePath)), scanRoot, input.target));
|
|
288
|
+
}
|
|
289
|
+
if (skippedManaged > 0) {
|
|
290
|
+
sourcesScanned.push(`native_instructions (${skippedManaged} brainclaw-managed — skipped)`);
|
|
291
|
+
}
|
|
268
292
|
}
|
|
269
293
|
const manifestResult = extractManifestSeeds(scanRoot, input.target);
|
|
270
294
|
if (manifestResult.seeds.length > 0) {
|
|
@@ -26,7 +26,7 @@ import path from 'node:path';
|
|
|
26
26
|
import { loadConfig } from '../config.js';
|
|
27
27
|
import { listNestedProjects } from './cascade.js';
|
|
28
28
|
import { readManifest, readImportsIndex } from './store.js';
|
|
29
|
-
import {
|
|
29
|
+
import { makeFreshnessBadge, applyGitHeadDrift } from './freshness.js';
|
|
30
30
|
import { findInStore, briefInStore, makeLazyChecker, newAccumulator, deriveBadge, reserveSourceSlots, attachRelatedMemory, attachMemoryIds, validateStoreEntry, BRIEF_FILE_CAP, LAZY_BUDGET, } from './query.js';
|
|
31
31
|
/** Same default cap as the single-store find (query.ts DEFAULT_FIND_LIMIT). */
|
|
32
32
|
const DEFAULT_FIND_LIMIT = 20;
|
|
@@ -191,22 +191,11 @@ function statusRank(s) {
|
|
|
191
191
|
function mergeBadges(perStore) {
|
|
192
192
|
const total = perStore.length;
|
|
193
193
|
const indexed = perStore.filter((p) => p.hasIndex);
|
|
194
|
-
const unindexed = perStore
|
|
195
|
-
.filter((p) => !p.hasIndex)
|
|
196
|
-
.map((p) => p.ref.relPath || '.')
|
|
197
|
-
.sort();
|
|
194
|
+
const unindexed = perStore.filter((p) => !p.hasIndex).map((p) => p.ref.relPath || '.').sort();
|
|
198
195
|
if (indexed.length === 0) {
|
|
199
|
-
return {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
details: {
|
|
203
|
-
traversal: 'workspace',
|
|
204
|
-
projects_indexed: 0,
|
|
205
|
-
projects_total: total,
|
|
206
|
-
unindexed_projects: unindexed,
|
|
207
|
-
hint: 'run refresh --cascade',
|
|
208
|
-
},
|
|
209
|
-
};
|
|
196
|
+
return makeFreshnessBadge('missing_index', {
|
|
197
|
+
extra: { traversal: 'workspace', projects_indexed: 0, projects_total: total, unindexed_projects: unindexed, hint: 'run refresh --cascade' },
|
|
198
|
+
});
|
|
210
199
|
}
|
|
211
200
|
let worst = indexed[0].badge.status;
|
|
212
201
|
for (const p of indexed) {
|
|
@@ -221,12 +210,11 @@ function mergeBadges(perStore) {
|
|
|
221
210
|
};
|
|
222
211
|
if (unindexed.length)
|
|
223
212
|
details.unindexed_projects = unindexed;
|
|
224
|
-
// Merge the per-store detail path-sets, prefixing each with its store's
|
|
225
|
-
// workspace-relative dir so a bare `src/index.ts` isn't ambiguous across packages.
|
|
226
213
|
const prefixMerge = (key) => {
|
|
227
214
|
const out = [];
|
|
228
215
|
for (const p of indexed) {
|
|
229
|
-
const
|
|
216
|
+
const spot = p.badge.details.spot_check;
|
|
217
|
+
const arr = spot?.[key];
|
|
230
218
|
if (Array.isArray(arr)) {
|
|
231
219
|
for (const f of arr)
|
|
232
220
|
out.push(p.ref.relPath ? `${p.ref.relPath}/${String(f)}` : String(f));
|
|
@@ -234,16 +222,25 @@ function mergeBadges(perStore) {
|
|
|
234
222
|
}
|
|
235
223
|
return out.sort();
|
|
236
224
|
};
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
225
|
+
const spotChecks = indexed.map((p) => p.badge.details.spot_check);
|
|
226
|
+
const spotStatus = spotChecks.some((spot) => spot?.status === 'partial')
|
|
227
|
+
? 'partial'
|
|
228
|
+
: spotChecks.some((spot) => spot?.status === 'stale')
|
|
229
|
+
? 'stale'
|
|
230
|
+
: spotChecks.some((spot) => spot?.status === 'fresh') ? 'fresh' : 'not_run';
|
|
231
|
+
const partialSpot = spotChecks.find((spot) => spot?.status === 'partial');
|
|
232
|
+
return makeFreshnessBadge(worst, {
|
|
233
|
+
spotCheck: {
|
|
234
|
+
status: spotStatus,
|
|
235
|
+
checked_files: spotChecks.reduce((sum, spot) => sum + (spot?.checked_files ?? 0), 0),
|
|
236
|
+
stale_changed_files: prefixMerge('stale_changed_files'),
|
|
237
|
+
deleted_files: prefixMerge('deleted_files'),
|
|
238
|
+
unchecked_files: prefixMerge('unchecked_files'),
|
|
239
|
+
budget_exhausted: spotChecks.some((spot) => spot?.budget_exhausted === true),
|
|
240
|
+
partial_reason: partialSpot?.partial_reason ?? null,
|
|
241
|
+
},
|
|
242
|
+
extra: details,
|
|
243
|
+
});
|
|
247
244
|
}
|
|
248
245
|
/**
|
|
249
246
|
* Aggregated find across a resolved multi-project workspace. Shares ONE lazy budget
|
|
@@ -426,9 +423,15 @@ export function aggregateBrief(target, limit, resolved, currentHead, memoryReade
|
|
|
426
423
|
const seen = new Set();
|
|
427
424
|
const mergedDefiningPaths = new Set();
|
|
428
425
|
const symbolNames = new Set();
|
|
426
|
+
const memorySymbolNames = new Set();
|
|
427
|
+
const memoryImportNames = new Set();
|
|
429
428
|
for (const p of contributing) {
|
|
430
429
|
for (const e of p.r.defining)
|
|
431
430
|
symbolNames.add(e.name);
|
|
431
|
+
for (const name of p.r.memorySymbolNames)
|
|
432
|
+
memorySymbolNames.add(name);
|
|
433
|
+
for (const name of p.r.memoryImportNames)
|
|
434
|
+
memoryImportNames.add(name);
|
|
432
435
|
for (const dp of p.r.definingPaths)
|
|
433
436
|
mergedDefiningPaths.add(p.ref.relPath ? `${p.ref.relPath}/${dp}` : dp);
|
|
434
437
|
for (const rf of p.r.confident) {
|
|
@@ -460,8 +463,10 @@ export function aggregateBrief(target, limit, resolved, currentHead, memoryReade
|
|
|
460
463
|
const capped = reserveSourceSlots(merged, cap, mergedDefiningPaths);
|
|
461
464
|
if (symbolNames.size === 0)
|
|
462
465
|
symbolNames.add(target);
|
|
463
|
-
|
|
464
|
-
|
|
466
|
+
if (memorySymbolNames.size === 0)
|
|
467
|
+
memorySymbolNames.add(target);
|
|
468
|
+
const related = attachRelatedMemory(memoryReader({ cwd: resolved.root }), capped.map((f) => f.path), [...memorySymbolNames], [...memoryImportNames]);
|
|
469
|
+
const baseEntries = attachMemoryIds(capped, related, mergedDefiningPaths);
|
|
465
470
|
const suggested = baseEntries.map((s, i) => ({
|
|
466
471
|
...s,
|
|
467
472
|
project: capped[i].project,
|