brainclaw 1.28.4 → 1.28.6
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 +1 -0
- package/dist/commands/code-map.js +5 -1
- package/dist/commands/mcp-catalog.js +9 -2
- package/dist/commands/mcp.js +44 -9
- package/dist/commands/switch.js +8 -1
- package/dist/core/code-map/backend.js +19 -5
- package/dist/core/code-map/refresh-jobs.js +158 -0
- package/dist/core/code-map/refresh-worker.js +12 -0
- package/dist/core/loops/impl-bind.js +20 -1
- package/dist/core/loops/store.js +19 -1
- package/dist/core/loops/types.js +1 -1
- package/dist/facts.js +9 -9
- package/dist/facts.json +8 -8
- package/docs/cli.md +4 -4
- package/docs/code-map.md +30 -18
- package/docs/integrations/mcp.md +4 -4
- package/docs/loops/implementation.md +17 -6
- package/docs/mcp-schema-changelog.md +23 -2
- package/package.json +1 -1
|
Binary file
|
|
@@ -7,6 +7,7 @@ export function registerCodeMapCommands(program) {
|
|
|
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
|
+
.option('--scope <scope>', 'For refresh: changed or all (same selector as MCP)')
|
|
10
11
|
.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
12
|
.option('--limit <n>', 'Max results for find/brief/impact/outline', (v) => parseInt(v, 10))
|
|
12
13
|
.option('--depth <n>', 'For impact/export: maximum graph depth (export is hard-capped at 4)', (v) => parseInt(v, 10))
|
|
@@ -27,7 +27,10 @@ export async function runCodeMap(subcommand, args, options = {}) {
|
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
if (normalized === 'refresh') {
|
|
30
|
-
|
|
30
|
+
if (options.scope && options.scope !== 'all' && options.scope !== 'changed') {
|
|
31
|
+
throw new Error('code-map refresh --scope must be "changed" or "all".');
|
|
32
|
+
}
|
|
33
|
+
const scope = options.scope === 'all' || options.all ? 'all' : 'changed';
|
|
31
34
|
const result = await be.refresh({ scope, cwd, cascade: options.cascade });
|
|
32
35
|
printRefresh(result, options);
|
|
33
36
|
return;
|
|
@@ -108,6 +111,7 @@ function printStatus(status, options) {
|
|
|
108
111
|
}
|
|
109
112
|
console.log('Code Map status');
|
|
110
113
|
console.log(` Store: ${status.store_exists ? 'present' : 'absent'}`);
|
|
114
|
+
console.log(` Index: ${status.index_exists ? 'ready' : status.index_manifest_exists ? 'invalid manifest' : 'not built'}`);
|
|
111
115
|
console.log(` Root: ${status.resolution.project_root}`);
|
|
112
116
|
console.log(` Path: ${status.resolution.store_path}`);
|
|
113
117
|
console.log(` ${badgeLine(status.freshness_badge)}`);
|
|
@@ -374,11 +374,12 @@ export const MCP_READ_TOOLS = [
|
|
|
374
374
|
},
|
|
375
375
|
{
|
|
376
376
|
name: 'bclaw_code_status',
|
|
377
|
-
description: 'Code Map status for the active session project: store presence, freshness
|
|
377
|
+
description: 'Code Map status for the active session or explicit project: physical store-path presence, readable-index presence, exact resolution, freshness, stats, and latest durable refresh job. Read-only; never refreshes. In a multi-project workspace, cascade=true adds per-child coverage plus progress/terminal diagnostics for the latest durable cascade job.',
|
|
378
378
|
annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'auto' },
|
|
379
379
|
inputSchema: {
|
|
380
380
|
type: 'object',
|
|
381
381
|
properties: {
|
|
382
|
+
project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
|
|
382
383
|
cascade: { type: 'boolean', description: 'Multi-project workspace recap: also report per-child store presence + freshness for every nested project. No-op outside a multi-project workspace.' },
|
|
383
384
|
},
|
|
384
385
|
},
|
|
@@ -390,6 +391,7 @@ export const MCP_READ_TOOLS = [
|
|
|
390
391
|
inputSchema: {
|
|
391
392
|
type: 'object',
|
|
392
393
|
properties: {
|
|
394
|
+
project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
|
|
393
395
|
query: { type: 'string', description: 'Symbol or token to search for (e.g. "App", "useAuth", "dispatch").' },
|
|
394
396
|
limit: { type: 'number', description: 'Max matches to return.' },
|
|
395
397
|
},
|
|
@@ -403,6 +405,7 @@ export const MCP_READ_TOOLS = [
|
|
|
403
405
|
inputSchema: {
|
|
404
406
|
type: 'object',
|
|
405
407
|
properties: {
|
|
408
|
+
project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
|
|
406
409
|
target: { type: 'string', description: 'Symbol name or file path to build a reading brief for.' },
|
|
407
410
|
limit: { type: 'number', description: 'Max suggested files (hard-capped at 12 by the spec).' },
|
|
408
411
|
},
|
|
@@ -416,6 +419,7 @@ export const MCP_READ_TOOLS = [
|
|
|
416
419
|
inputSchema: {
|
|
417
420
|
type: 'object',
|
|
418
421
|
properties: {
|
|
422
|
+
project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
|
|
419
423
|
target: { type: 'string', description: 'Symbol name or source-file path whose impact to inspect.' },
|
|
420
424
|
depth: { type: 'number', description: 'Maximum graph depth. 1 (default) returns direct dependents only; 2+ opts into transitives. Clamped to 4.' },
|
|
421
425
|
limit: { type: 'number', description: 'Maximum rows in each dependent section and in naming suggestions. Clamped to 100.' },
|
|
@@ -430,6 +434,7 @@ export const MCP_READ_TOOLS = [
|
|
|
430
434
|
inputSchema: {
|
|
431
435
|
type: 'object',
|
|
432
436
|
properties: {
|
|
437
|
+
project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
|
|
433
438
|
target: { type: 'string', description: 'Symbol name or source-file path around which to export a local subgraph.' },
|
|
434
439
|
targetKind: { type: 'string', enum: ['symbol', 'file'], description: 'Optional explicit target kind; otherwise a source path is detected safely.' },
|
|
435
440
|
direction: { type: 'string', enum: ['outgoing', 'incoming', 'both'], description: 'Which edge direction(s) to follow. Default: both.' },
|
|
@@ -449,6 +454,7 @@ export const MCP_READ_TOOLS = [
|
|
|
449
454
|
inputSchema: {
|
|
450
455
|
type: 'object',
|
|
451
456
|
properties: {
|
|
457
|
+
project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
|
|
452
458
|
path: { type: 'string', description: 'Workspace-relative source file path (for example `src/app/App.tsx`).' },
|
|
453
459
|
limit: { type: 'number', description: 'Maximum symbols to return; clamped to the hard cap of 200.' },
|
|
454
460
|
},
|
|
@@ -459,11 +465,12 @@ export const MCP_READ_TOOLS = [
|
|
|
459
465
|
const MCP_WRITE_TOOLS = [
|
|
460
466
|
{
|
|
461
467
|
name: 'bclaw_code_refresh',
|
|
462
|
-
description: '
|
|
468
|
+
description: 'Accept a durable background rebuild of the Code Map index for the active or explicit project and return immediately. scope="changed" (default) reparses changed files; scope="all" does a full refresh + compaction. Follow with bclaw_code_status, which reports progress and terminal diagnostics without an MCP timeout. In a multi-project workspace, cascade=true applies the same contract across nested projects.',
|
|
463
469
|
annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'prompt' },
|
|
464
470
|
inputSchema: {
|
|
465
471
|
type: 'object',
|
|
466
472
|
properties: {
|
|
473
|
+
project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
|
|
467
474
|
scope: { type: 'string', enum: ['changed', 'all'], description: 'changed (default) reparses changed files only; all does a full refresh with orphan compaction.' },
|
|
468
475
|
cascade: { type: 'boolean', description: 'Multi-project cascade: refresh every nested brainclaw project + a child-scoped root store. No-op outside a multi-project workspace.' },
|
|
469
476
|
},
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1097,19 +1097,32 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1097
1097
|
const { JsonlBackend } = await import('../core/code-map/backend.js');
|
|
1098
1098
|
const be = new JsonlBackend();
|
|
1099
1099
|
// Session-scoped project selection is authoritative for Code Map too.
|
|
1100
|
-
|
|
1100
|
+
// An explicit project selector wins, matching the canonical read grammar.
|
|
1101
|
+
let codeCwd = scopeInfo.cwd;
|
|
1102
|
+
let codeScope = scopeInfo;
|
|
1103
|
+
if (typeof args.project === 'string' && args.project.trim()) {
|
|
1104
|
+
codeCwd = resolveProjectCwd(args.project.trim(), codeCwd);
|
|
1105
|
+
codeScope = {
|
|
1106
|
+
cwd: codeCwd,
|
|
1107
|
+
active_source: 'explicit',
|
|
1108
|
+
resolved_project: projectInfoForCwd(codeCwd),
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1101
1111
|
if (name === 'bclaw_code_status') {
|
|
1102
1112
|
const status = await be.status({ cwd: codeCwd, cascade: args.cascade === true });
|
|
1103
1113
|
const diskVersion = readDiskBrainclawVersion();
|
|
1104
1114
|
return {
|
|
1105
1115
|
response: toolResponse({
|
|
1106
|
-
content: [{
|
|
1116
|
+
content: [{
|
|
1117
|
+
type: 'text',
|
|
1118
|
+
text: `Code Map: path=${status.store_exists ? 'present' : 'absent'}, index=${status.index_exists ? 'ready' : status.index_manifest_exists ? 'invalid' : 'missing'} — freshness=${status.freshness_badge.freshness}`,
|
|
1119
|
+
}],
|
|
1107
1120
|
structuredContent: {
|
|
1108
1121
|
...status,
|
|
1109
1122
|
freshness_badge: status.freshness_badge,
|
|
1110
1123
|
mcp_resolution: {
|
|
1111
|
-
active_source:
|
|
1112
|
-
resolved_project:
|
|
1124
|
+
active_source: codeScope.active_source,
|
|
1125
|
+
resolved_project: codeScope.resolved_project,
|
|
1113
1126
|
server_version: getInstalledBrainclawVersion(),
|
|
1114
1127
|
disk_version: diskVersion,
|
|
1115
1128
|
restart_required: diskVersion !== '0.0.0' && diskVersion !== getInstalledBrainclawVersion(),
|
|
@@ -1136,12 +1149,27 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1136
1149
|
};
|
|
1137
1150
|
}
|
|
1138
1151
|
}
|
|
1139
|
-
const
|
|
1140
|
-
const
|
|
1152
|
+
const { startCodeRefreshJob, summarizeCodeRefreshJob } = await import('../core/code-map/refresh-jobs.js');
|
|
1153
|
+
const job = startCodeRefreshJob(codeCwd, scope);
|
|
1154
|
+
const accepted = job.status !== 'failed' && job.scope === scope;
|
|
1155
|
+
const acknowledgement = accepted
|
|
1156
|
+
? `Code Map refresh accepted: job=${job.job_id}, scope=${job.scope}, project=${codeCwd}.`
|
|
1157
|
+
: job.status === 'failed'
|
|
1158
|
+
? `Code Map refresh failed to start: job=${job.job_id}, project=${codeCwd}, error=${job.error ?? 'unknown'}.`
|
|
1159
|
+
: `Code Map refresh not queued: active job=${job.job_id} has scope=${job.scope}; requested scope=${scope}.`;
|
|
1141
1160
|
return {
|
|
1142
1161
|
response: toolResponse({
|
|
1143
|
-
content: [{ type: 'text', text:
|
|
1144
|
-
structuredContent: {
|
|
1162
|
+
content: [{ type: 'text', text: `${acknowledgement} Follow with bclaw_code_status${typeof args.project === 'string' ? `(project=${JSON.stringify(args.project)})` : ''}.` }],
|
|
1163
|
+
structuredContent: {
|
|
1164
|
+
accepted,
|
|
1165
|
+
requested_scope: scope,
|
|
1166
|
+
...summarizeCodeRefreshJob(job),
|
|
1167
|
+
next_actions: [{
|
|
1168
|
+
tool: 'bclaw_code_status',
|
|
1169
|
+
args: typeof args.project === 'string' ? { project: args.project } : {},
|
|
1170
|
+
when: 'follow refresh progress and terminal outcome',
|
|
1171
|
+
}],
|
|
1172
|
+
},
|
|
1145
1173
|
}),
|
|
1146
1174
|
};
|
|
1147
1175
|
}
|
|
@@ -1226,8 +1254,15 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1226
1254
|
};
|
|
1227
1255
|
}
|
|
1228
1256
|
if (MCP_READ_TOOLS.some((tool) => tool.name === name) || LEGACY_READ_TOOL_HANDLERS.has(name)) {
|
|
1257
|
+
const response = appendLegacyMcpToolWarning(toolResponse(handleMcpReadToolCall(name, args, { cwd, connectionSessionId, effectiveScope: scopeInfo })), name);
|
|
1258
|
+
const switchedSessionId = name === 'bclaw_switch'
|
|
1259
|
+
? response.structuredContent?.session_id
|
|
1260
|
+
: undefined;
|
|
1229
1261
|
return {
|
|
1230
|
-
response
|
|
1262
|
+
response,
|
|
1263
|
+
...(typeof switchedSessionId === 'string' && switchedSessionId
|
|
1264
|
+
? { nextConnectionSessionId: switchedSessionId }
|
|
1265
|
+
: {}),
|
|
1231
1266
|
};
|
|
1232
1267
|
}
|
|
1233
1268
|
// Resolve model once for all write operations
|
package/dist/commands/switch.js
CHANGED
|
@@ -72,7 +72,14 @@ export function switchProject(projectRef, options = {}) {
|
|
|
72
72
|
...session,
|
|
73
73
|
active_project: { path: resolved, name: projectName, switched_at: now },
|
|
74
74
|
}, cwd);
|
|
75
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
switched: true,
|
|
77
|
+
path: resolved,
|
|
78
|
+
name: projectName,
|
|
79
|
+
scope: 'session',
|
|
80
|
+
workspace_root: wsRoot,
|
|
81
|
+
session_id: session.session_id,
|
|
82
|
+
};
|
|
76
83
|
}
|
|
77
84
|
if (sessionOnly) {
|
|
78
85
|
throw new Error('Cannot switch project without an active agent session. Start with bclaw_work or bclaw_session_start first.');
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* `freshness_badge`, locking the response shape for later sprints.
|
|
10
10
|
*/
|
|
11
11
|
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import fs from 'node:fs';
|
|
12
13
|
import path from 'node:path';
|
|
13
|
-
import { readManifest, readShard
|
|
14
|
+
import { readManifest, readShard } from './store.js';
|
|
14
15
|
import { refresh as runRefresh } from './refresh.js';
|
|
15
16
|
import { applyGitHeadDrift, withFreshness } from './freshness.js';
|
|
16
17
|
import { brief as runBrief, find as runFind } from './query.js';
|
|
@@ -21,8 +22,9 @@ import { resolveTraversal, aggregateFind, aggregateBrief } from './aggregate.js'
|
|
|
21
22
|
import { defaultMemoryReader } from './memory-reader.js';
|
|
22
23
|
import { inspectNestedProjects, refreshWorkspaceCascade } from './cascade.js';
|
|
23
24
|
import { latestCascadeRefreshJob, summarizeCascadeRefreshJob } from './cascade-jobs.js';
|
|
25
|
+
import { latestCodeRefreshJob, summarizeCodeRefreshJob } from './refresh-jobs.js';
|
|
24
26
|
import { loadConfig } from '../config.js';
|
|
25
|
-
import { codeMapDir } from './paths.js';
|
|
27
|
+
import { codeMapDir, manifestPath } from './paths.js';
|
|
26
28
|
/** spec §9 caps the brief reading list at 12 files. */
|
|
27
29
|
export const BRIEF_FILE_CAP = 12;
|
|
28
30
|
/**
|
|
@@ -116,9 +118,12 @@ function buildCascadeStatus(rootCwd) {
|
|
|
116
118
|
const discovery = inspectNestedProjects(root);
|
|
117
119
|
const children = discovery.projects.map((abs) => {
|
|
118
120
|
const m = readManifest(abs);
|
|
121
|
+
const storePath = codeMapDir(abs);
|
|
119
122
|
return {
|
|
120
123
|
path: path.relative(root, abs).replace(/\\/g, '/') || '.',
|
|
121
|
-
store_exists:
|
|
124
|
+
store_exists: fs.existsSync(storePath),
|
|
125
|
+
index_exists: m !== null,
|
|
126
|
+
index_manifest_exists: fs.existsSync(manifestPath(abs)),
|
|
122
127
|
freshness: m ? m.freshness.status : 'missing_index',
|
|
123
128
|
files_indexed: m ? m.stats.files_indexed : null,
|
|
124
129
|
...(m && m.stats.files_indexed === 0 ? { reason: 'no_eligible_files' } : {}),
|
|
@@ -157,9 +162,13 @@ export class JsonlBackend {
|
|
|
157
162
|
store_path: codeMapDir(projectRoot, input.preferredDirName),
|
|
158
163
|
};
|
|
159
164
|
const manifest = readManifest(input.cwd, input.preferredDirName);
|
|
165
|
+
const storePathExists = fs.existsSync(resolution.store_path);
|
|
166
|
+
const manifestFileExists = fs.existsSync(manifestPath(input.cwd, input.preferredDirName));
|
|
160
167
|
const result = manifest
|
|
161
168
|
? {
|
|
162
|
-
store_exists:
|
|
169
|
+
store_exists: storePathExists,
|
|
170
|
+
index_exists: true,
|
|
171
|
+
index_manifest_exists: true,
|
|
163
172
|
resolution,
|
|
164
173
|
freshness_badge: this.withHeadDrift(badge(manifest.freshness.status, {
|
|
165
174
|
stale_file_count: manifest.freshness.stale_file_count,
|
|
@@ -172,7 +181,9 @@ export class JsonlBackend {
|
|
|
172
181
|
},
|
|
173
182
|
}
|
|
174
183
|
: {
|
|
175
|
-
store_exists:
|
|
184
|
+
store_exists: storePathExists,
|
|
185
|
+
index_exists: false,
|
|
186
|
+
index_manifest_exists: manifestFileExists,
|
|
176
187
|
resolution,
|
|
177
188
|
freshness_badge: badge('missing_index'),
|
|
178
189
|
stats: null,
|
|
@@ -183,6 +194,9 @@ export class JsonlBackend {
|
|
|
183
194
|
if (input.cascade && isMultiProjectWorkspace(input.cwd)) {
|
|
184
195
|
result.cascade = buildCascadeStatus(input.cwd);
|
|
185
196
|
}
|
|
197
|
+
const latestRefresh = latestCodeRefreshJob(projectRoot);
|
|
198
|
+
if (latestRefresh)
|
|
199
|
+
result.refresh_job = summarizeCodeRefreshJob(latestRefresh);
|
|
186
200
|
return result;
|
|
187
201
|
}
|
|
188
202
|
/**
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { writeFileAtomic } from '../io.js';
|
|
7
|
+
import { codeMapDir } from './paths.js';
|
|
8
|
+
function jobsDir(root) {
|
|
9
|
+
return path.join(codeMapDir(root), 'refresh-jobs');
|
|
10
|
+
}
|
|
11
|
+
function jobPath(root, jobId) {
|
|
12
|
+
return path.join(jobsDir(root), `${jobId}.json`);
|
|
13
|
+
}
|
|
14
|
+
function writeJob(job) {
|
|
15
|
+
fs.mkdirSync(jobsDir(job.root), { recursive: true });
|
|
16
|
+
writeFileAtomic(jobPath(job.root, job.job_id), `${JSON.stringify(job, null, 2)}\n`);
|
|
17
|
+
}
|
|
18
|
+
export function readCodeRefreshJob(root, jobId) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(fs.readFileSync(jobPath(path.resolve(root), jobId), 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function latestCodeRefreshJob(root) {
|
|
27
|
+
try {
|
|
28
|
+
return fs.readdirSync(jobsDir(path.resolve(root)))
|
|
29
|
+
.filter((name) => name.endsWith('.json'))
|
|
30
|
+
.map((name) => {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(path.join(jobsDir(path.resolve(root)), name), 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.filter((job) => job !== null)
|
|
39
|
+
.sort((a, b) => b.updated_at.localeCompare(a.updated_at))[0] ?? null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function summarizeCodeRefreshJob(job) {
|
|
46
|
+
return {
|
|
47
|
+
...job,
|
|
48
|
+
...(job.result
|
|
49
|
+
? {
|
|
50
|
+
result: {
|
|
51
|
+
ran: job.result.ran,
|
|
52
|
+
scope: job.result.scope,
|
|
53
|
+
lock_acquired: job.result.lock_acquired,
|
|
54
|
+
...(job.result.lock_status ? { lock_status: job.result.lock_status } : {}),
|
|
55
|
+
freshness_badge: job.result.freshness_badge,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
: {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function processAlive(pid) {
|
|
62
|
+
if (!pid)
|
|
63
|
+
return false;
|
|
64
|
+
try {
|
|
65
|
+
process.kill(pid, 0);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Start a detached, durable refresh and return before parsing begins. */
|
|
73
|
+
export function startCodeRefreshJob(root, scope) {
|
|
74
|
+
const resolvedRoot = path.resolve(root);
|
|
75
|
+
const previous = latestCodeRefreshJob(resolvedRoot);
|
|
76
|
+
if (previous && (previous.status === 'queued' || previous.status === 'running') && processAlive(previous.pid)) {
|
|
77
|
+
return previous;
|
|
78
|
+
}
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
const job = {
|
|
81
|
+
job_id: `cmj_${crypto.randomBytes(8).toString('hex')}`,
|
|
82
|
+
root: resolvedRoot,
|
|
83
|
+
scope,
|
|
84
|
+
status: 'queued',
|
|
85
|
+
created_at: now,
|
|
86
|
+
updated_at: now,
|
|
87
|
+
};
|
|
88
|
+
writeJob(job);
|
|
89
|
+
const worker = fileURLToPath(new URL('./refresh-worker.js', import.meta.url));
|
|
90
|
+
if (!fs.existsSync(worker)) {
|
|
91
|
+
const failed = {
|
|
92
|
+
...job,
|
|
93
|
+
status: 'failed',
|
|
94
|
+
error: `code refresh worker is missing: ${worker}`,
|
|
95
|
+
completed_at: new Date().toISOString(),
|
|
96
|
+
updated_at: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
writeJob(failed);
|
|
99
|
+
return failed;
|
|
100
|
+
}
|
|
101
|
+
const child = spawn(process.execPath, [worker, resolvedRoot, job.job_id, scope], {
|
|
102
|
+
cwd: resolvedRoot,
|
|
103
|
+
detached: true,
|
|
104
|
+
stdio: 'ignore',
|
|
105
|
+
windowsHide: true,
|
|
106
|
+
});
|
|
107
|
+
child.once('error', (error) => {
|
|
108
|
+
const current = readCodeRefreshJob(resolvedRoot, job.job_id) ?? job;
|
|
109
|
+
if (current.status === 'completed' || current.status === 'failed')
|
|
110
|
+
return;
|
|
111
|
+
const completed = new Date().toISOString();
|
|
112
|
+
writeJob({
|
|
113
|
+
...current,
|
|
114
|
+
status: 'failed',
|
|
115
|
+
error: `code refresh worker failed to start: ${error.message}`,
|
|
116
|
+
completed_at: completed,
|
|
117
|
+
updated_at: completed,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
child.unref();
|
|
121
|
+
const queued = readCodeRefreshJob(resolvedRoot, job.job_id) ?? job;
|
|
122
|
+
queued.pid = child.pid;
|
|
123
|
+
queued.updated_at = new Date().toISOString();
|
|
124
|
+
writeJob(queued);
|
|
125
|
+
return queued;
|
|
126
|
+
}
|
|
127
|
+
/** Worker seam exported for deterministic tests. */
|
|
128
|
+
export async function runCodeRefreshJob(root, jobId, scope) {
|
|
129
|
+
const job = readCodeRefreshJob(root, jobId);
|
|
130
|
+
if (!job)
|
|
131
|
+
throw new Error(`code refresh job not found: ${jobId}`);
|
|
132
|
+
const started = new Date().toISOString();
|
|
133
|
+
writeJob({ ...job, status: 'running', pid: process.pid, started_at: started, updated_at: started });
|
|
134
|
+
try {
|
|
135
|
+
// Dynamic import avoids coupling the query backend's status path to worker startup.
|
|
136
|
+
const { JsonlBackend } = await import('./backend.js');
|
|
137
|
+
const result = await new JsonlBackend().refresh({ cwd: root, scope });
|
|
138
|
+
const completed = new Date().toISOString();
|
|
139
|
+
writeJob({
|
|
140
|
+
...(readCodeRefreshJob(root, jobId) ?? job),
|
|
141
|
+
status: 'completed',
|
|
142
|
+
result,
|
|
143
|
+
completed_at: completed,
|
|
144
|
+
updated_at: completed,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const completed = new Date().toISOString();
|
|
149
|
+
writeJob({
|
|
150
|
+
...(readCodeRefreshJob(root, jobId) ?? job),
|
|
151
|
+
status: 'failed',
|
|
152
|
+
error: error instanceof Error ? error.message : String(error),
|
|
153
|
+
completed_at: completed,
|
|
154
|
+
updated_at: completed,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=refresh-jobs.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { runCodeRefreshJob } from './refresh-jobs.js';
|
|
2
|
+
const [root, jobId, scope] = process.argv.slice(2);
|
|
3
|
+
if (!root || !jobId || (scope !== 'changed' && scope !== 'all')) {
|
|
4
|
+
process.exitCode = 2;
|
|
5
|
+
}
|
|
6
|
+
else {
|
|
7
|
+
await runCodeRefreshJob(root, jobId, scope);
|
|
8
|
+
// Dedicated detached process: no library-owned handle may keep the caller's
|
|
9
|
+
// project directory busy after the durable terminal state has been written.
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=refresh-worker.js.map
|
|
@@ -34,9 +34,28 @@ function deriveBindings(loop, sequenceId, cwd) {
|
|
|
34
34
|
if (loop.slots.length !== lanes.length) {
|
|
35
35
|
throw new Error(`impl-bind lane/slot mismatch: sequence ${sequenceId} has ${lanes.length} lane(s) (${lanes.join(', ')}) but loop has ${loop.slots.length} slot(s); open one worker slot per lane`);
|
|
36
36
|
}
|
|
37
|
+
// A positional fallback is unsafe here: callers commonly attach a
|
|
38
|
+
// perspective to a slot, and silently pairing that perspective with a
|
|
39
|
+
// different alphabetically-sorted lane can invert the lane's scope policy.
|
|
40
|
+
// Single-lane sequences retain the historical default-slot convenience;
|
|
41
|
+
// multi-lane binds must carry an explicit, unique lane on every slot.
|
|
42
|
+
if (lanes.length > 1) {
|
|
43
|
+
const slotLanes = loop.slots.map((slot) => slot.lane?.trim() || '');
|
|
44
|
+
if (slotLanes.some((lane) => !lane)) {
|
|
45
|
+
throw new Error(`impl-bind requires explicit slot.lane for every lane when binding multiple lanes (${lanes.join(', ')}); positional slot order is rejected to prevent lane permutation`);
|
|
46
|
+
}
|
|
47
|
+
const uniqueSlotLanes = new Set(slotLanes);
|
|
48
|
+
if (uniqueSlotLanes.size !== slotLanes.length || uniqueSlotLanes.size !== lanes.length) {
|
|
49
|
+
throw new Error(`impl-bind requires one unique slot.lane per sequence lane; slots=${slotLanes.join(', ')}, lanes=${lanes.join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
const unknown = slotLanes.filter((lane) => !grouped.has(lane));
|
|
52
|
+
if (unknown.length > 0) {
|
|
53
|
+
throw new Error(`impl-bind slot lane(s) not present in sequence ${sequenceId}: ${unknown.join(', ')}; expected ${lanes.join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
37
56
|
const bindings = {};
|
|
38
57
|
loop.slots.forEach((slot, index) => {
|
|
39
|
-
const lane = lanes[index];
|
|
58
|
+
const lane = lanes.length > 1 ? slot.lane.trim() : (slot.lane?.trim() || lanes[index]);
|
|
40
59
|
const items = grouped.get(lane);
|
|
41
60
|
const scopes = [...new Set(items.map((item) => item.scope_hint?.trim()).filter((value) => Boolean(value)))];
|
|
42
61
|
bindings[slot.slot_id] = {
|
package/dist/core/loops/store.js
CHANGED
|
@@ -62,6 +62,18 @@ function resolveProtocol(kind, mode) {
|
|
|
62
62
|
}
|
|
63
63
|
return iteration ? { iteration } : undefined;
|
|
64
64
|
}
|
|
65
|
+
function collectStopConditionPhases(condition) {
|
|
66
|
+
switch (condition.kind) {
|
|
67
|
+
case 'phase_reached':
|
|
68
|
+
case 'artifact_produced':
|
|
69
|
+
return [condition.phase];
|
|
70
|
+
case 'any':
|
|
71
|
+
case 'all':
|
|
72
|
+
return condition.conditions.flatMap(collectStopConditionPhases);
|
|
73
|
+
default:
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
65
77
|
function buildSlot(partial) {
|
|
66
78
|
return {
|
|
67
79
|
slot_id: partial.slot_id ?? generateSlotId(),
|
|
@@ -141,6 +153,12 @@ export function openLoop(input, cwd) {
|
|
|
141
153
|
if (phaseNames.size !== phases.length) {
|
|
142
154
|
throw new Error('openLoop: phase names must be unique');
|
|
143
155
|
}
|
|
156
|
+
const stopCondition = input.stop_condition ?? protocolDefaults.stop_condition;
|
|
157
|
+
const referencedPhases = collectStopConditionPhases(stopCondition);
|
|
158
|
+
const invalidPhase = referencedPhases.find((phase) => !phaseNames.has(phase));
|
|
159
|
+
if (invalidPhase) {
|
|
160
|
+
throw new Error(`openLoop: stop_condition references unknown phase "${invalidPhase}"; expected one of ${[...phaseNames].join(', ')}`);
|
|
161
|
+
}
|
|
144
162
|
const now = nowISO();
|
|
145
163
|
const id = generateLoopId();
|
|
146
164
|
const mutation_id = generateMutationId();
|
|
@@ -174,7 +192,7 @@ export function openLoop(input, cwd) {
|
|
|
174
192
|
// questions; the request_input handler (step 2) appends/removes ids.
|
|
175
193
|
open_questions: [],
|
|
176
194
|
linked: input.linked,
|
|
177
|
-
stop_condition:
|
|
195
|
+
stop_condition: stopCondition,
|
|
178
196
|
evidence_policy: evidencePolicyForNewLoop(),
|
|
179
197
|
created_at: now,
|
|
180
198
|
updated_at: now,
|
package/dist/core/loops/types.js
CHANGED
|
@@ -33,7 +33,7 @@ export const LoopLinksSchema = z.object({
|
|
|
33
33
|
source_artifact_digest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
34
34
|
/** Durable, deterministic identity of the policy decision that created this loop. */
|
|
35
35
|
continuation_key: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
36
|
-
});
|
|
36
|
+
}).strict();
|
|
37
37
|
/**
|
|
38
38
|
* Memory categories a loop phase can request via `context_filter` (pln#492).
|
|
39
39
|
*
|
package/dist/facts.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
|
|
2
|
-
// Source: brainclaw v1.28.
|
|
2
|
+
// Source: brainclaw v1.28.6 on 2026-08-28T22:03:41.224Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.28.
|
|
5
|
-
"generated_at": "2026-08-
|
|
4
|
+
"version": "1.28.6",
|
|
5
|
+
"generated_at": "2026-08-28T22:03:41.224Z",
|
|
6
6
|
"tools": {
|
|
7
7
|
"count": 71,
|
|
8
8
|
"published_count": 69,
|
|
@@ -479,7 +479,7 @@ export const FACTS = {
|
|
|
479
479
|
},
|
|
480
480
|
"bench": {
|
|
481
481
|
"schema": "brainclaw.bench.v1",
|
|
482
|
-
"generated_at": "2026-08-
|
|
482
|
+
"generated_at": "2026-08-28T22:03:39.012Z",
|
|
483
483
|
"node_version": "v24.19.0",
|
|
484
484
|
"platform": "linux-x64",
|
|
485
485
|
"repeats": 3,
|
|
@@ -488,7 +488,7 @@ export const FACTS = {
|
|
|
488
488
|
"name": "cold_onboard",
|
|
489
489
|
"volume": "empty",
|
|
490
490
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
491
|
-
"duration_ms_median":
|
|
491
|
+
"duration_ms_median": 77,
|
|
492
492
|
"payload_chars_median": 1640,
|
|
493
493
|
"payload_tokens_est_median": 410
|
|
494
494
|
},
|
|
@@ -496,7 +496,7 @@ export const FACTS = {
|
|
|
496
496
|
"name": "warm_work",
|
|
497
497
|
"volume": "medium",
|
|
498
498
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
499
|
-
"duration_ms_median":
|
|
499
|
+
"duration_ms_median": 129,
|
|
500
500
|
"payload_chars_median": 2626,
|
|
501
501
|
"payload_tokens_est_median": 657
|
|
502
502
|
},
|
|
@@ -504,9 +504,9 @@ export const FACTS = {
|
|
|
504
504
|
"name": "first_edit",
|
|
505
505
|
"volume": "medium",
|
|
506
506
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
507
|
-
"duration_ms_median":
|
|
508
|
-
"payload_chars_median":
|
|
509
|
-
"payload_tokens_est_median":
|
|
507
|
+
"duration_ms_median": 12,
|
|
508
|
+
"payload_chars_median": 1680,
|
|
509
|
+
"payload_tokens_est_median": 420
|
|
510
510
|
}
|
|
511
511
|
]
|
|
512
512
|
}
|
package/dist/facts.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.28.
|
|
3
|
-
"generated_at": "2026-08-
|
|
2
|
+
"version": "1.28.6",
|
|
3
|
+
"generated_at": "2026-08-28T22:03:41.224Z",
|
|
4
4
|
"tools": {
|
|
5
5
|
"count": 71,
|
|
6
6
|
"published_count": 69,
|
|
@@ -477,7 +477,7 @@
|
|
|
477
477
|
},
|
|
478
478
|
"bench": {
|
|
479
479
|
"schema": "brainclaw.bench.v1",
|
|
480
|
-
"generated_at": "2026-08-
|
|
480
|
+
"generated_at": "2026-08-28T22:03:39.012Z",
|
|
481
481
|
"node_version": "v24.19.0",
|
|
482
482
|
"platform": "linux-x64",
|
|
483
483
|
"repeats": 3,
|
|
@@ -486,7 +486,7 @@
|
|
|
486
486
|
"name": "cold_onboard",
|
|
487
487
|
"volume": "empty",
|
|
488
488
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
489
|
-
"duration_ms_median":
|
|
489
|
+
"duration_ms_median": 77,
|
|
490
490
|
"payload_chars_median": 1640,
|
|
491
491
|
"payload_tokens_est_median": 410
|
|
492
492
|
},
|
|
@@ -494,7 +494,7 @@
|
|
|
494
494
|
"name": "warm_work",
|
|
495
495
|
"volume": "medium",
|
|
496
496
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
497
|
-
"duration_ms_median":
|
|
497
|
+
"duration_ms_median": 129,
|
|
498
498
|
"payload_chars_median": 2626,
|
|
499
499
|
"payload_tokens_est_median": 657
|
|
500
500
|
},
|
|
@@ -502,9 +502,9 @@
|
|
|
502
502
|
"name": "first_edit",
|
|
503
503
|
"volume": "medium",
|
|
504
504
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
505
|
-
"duration_ms_median":
|
|
506
|
-
"payload_chars_median":
|
|
507
|
-
"payload_tokens_est_median":
|
|
505
|
+
"duration_ms_median": 12,
|
|
506
|
+
"payload_chars_median": 1680,
|
|
507
|
+
"payload_tokens_est_median": 420
|
|
508
508
|
}
|
|
509
509
|
]
|
|
510
510
|
}
|
package/docs/cli.md
CHANGED
|
@@ -632,18 +632,18 @@ brainclaw note create "Need follow-up on launcher script" --plan pln_abc123
|
|
|
632
632
|
|
|
633
633
|
## Code Map
|
|
634
634
|
|
|
635
|
-
A per-project Tree-sitter symbol + import index
|
|
635
|
+
A per-project Tree-sitter symbol + import index across 11 languages so agents
|
|
636
636
|
can ask "where is X / what should I read first" before editing. The MCP equivalents
|
|
637
637
|
are `bclaw_code_status` / `bclaw_code_find` / `bclaw_code_brief` / `bclaw_code_refresh`.
|
|
638
638
|
Full reference (freshness model, supported languages, WASM bundling): [docs/code-map.md](code-map.md).
|
|
639
639
|
|
|
640
640
|
### `brainclaw code-map status [--cascade]`
|
|
641
641
|
|
|
642
|
-
|
|
642
|
+
Physical store-path presence, readable-index presence, freshness badge (`fresh` / `stale_changed_files` / `stale_extractor` / `stale_grammar` / `partial` / `missing_index`), and index stats (files, nodes, edges). Read-only. In a multi-project workspace, `--cascade` adds compact coverage counts and names only non-fresh projects. On MCP, status also follows the latest durable refresh job and accepts `project` for explicit targeting.
|
|
643
643
|
|
|
644
|
-
### `brainclaw code-map refresh [--all|--changed] [--cascade]`
|
|
644
|
+
### `brainclaw code-map refresh [--all|--changed|--scope changed|all] [--cascade]`
|
|
645
645
|
|
|
646
|
-
Build or update the index. `--changed` (default) re-parses only touched files; `--all` does a full re-index. Run this when status shows `missing_index` or a stale badge. Fails fast
|
|
646
|
+
Build or update the index. `--changed` (default) re-parses only touched files; `--all` does a full re-index; `--scope changed|all` is the MCP-compatible spelling. Run this when status shows `missing_index` or a stale badge. Fails fast if another writer holds the project lock. In a multi-project workspace, `--cascade` synchronously refreshes each discovered project into its own store plus a root store scoped to files no child owns. MCP refreshes return durable jobs immediately; follow them with `bclaw_code_status`, preserving the same optional `project`, or use `cascade=true` for a workspace job. See [docs/code-map.md](code-map.md#cascading-a-multi-project-workspace---cascade).
|
|
647
647
|
|
|
648
648
|
### `brainclaw code-map find <query> [--limit <n>]`
|
|
649
649
|
|
package/docs/code-map.md
CHANGED
|
@@ -13,7 +13,7 @@ code, never blocks `bclaw_work`, and degrades gracefully: if the index is
|
|
|
13
13
|
missing or stale, every command says so via a freshness badge instead of
|
|
14
14
|
returning silently wrong answers.
|
|
15
15
|
|
|
16
|
-
The index lives under `.brainclaw/code
|
|
16
|
+
The index lives under `.brainclaw/code/` (one JSONL shard per file, plus
|
|
17
17
|
named symbol/import indexes and a manifest). It is safe to delete; a refresh
|
|
18
18
|
rebuilds it.
|
|
19
19
|
|
|
@@ -37,8 +37,10 @@ accepts `--json` for machine-readable output, and prints a `Freshness:` line.
|
|
|
37
37
|
|
|
38
38
|
### `brainclaw code-map status`
|
|
39
39
|
|
|
40
|
-
Read-only. Reports whether the store exists,
|
|
41
|
-
stats (files indexed, nodes,
|
|
40
|
+
Read-only. Reports whether the physical store path exists, whether a valid
|
|
41
|
+
index is readable, the freshness badge, and index stats (files indexed, nodes,
|
|
42
|
+
edges). `store_exists`, `index_exists`, and `index_manifest_exists` deliberately
|
|
43
|
+
separate a directory containing job records from a usable index. Never refreshes.
|
|
42
44
|
|
|
43
45
|
```bash
|
|
44
46
|
brainclaw code-map status
|
|
@@ -47,6 +49,7 @@ brainclaw code-map status
|
|
|
47
49
|
```
|
|
48
50
|
Code Map status
|
|
49
51
|
Store: present
|
|
52
|
+
Index: ready
|
|
50
53
|
Root: /workspace/apps/api
|
|
51
54
|
Path: /workspace/apps/api/.brainclaw/code
|
|
52
55
|
Freshness: fresh
|
|
@@ -55,7 +58,7 @@ Code Map status
|
|
|
55
58
|
Edges: 2410
|
|
56
59
|
```
|
|
57
60
|
|
|
58
|
-
### `brainclaw code-map refresh [--changed | --all]`
|
|
61
|
+
### `brainclaw code-map refresh [--changed | --all | --scope changed|all]`
|
|
59
62
|
|
|
60
63
|
Rebuilds the index behind a per-project lock. Defaults to `--changed`.
|
|
61
64
|
|
|
@@ -63,6 +66,7 @@ Rebuilds the index behind a per-project lock. Defaults to `--changed`.
|
|
|
63
66
|
|---|---|
|
|
64
67
|
| `--changed` (default) | Re-parses files whose **content** changed (git status + file-hash diff) **and** any shard whose stored extractor-config / grammar / engine hashes no longer match the current ones (i.e. `stale_extractor` / `stale_grammar`). A config or grammar bump is therefore healed by this cheap path — not only by `--all`. Compaction is limited to git-proven deletes. |
|
|
65
68
|
| `--all` | Enumerates every supported file, re-parses, and performs full orphan compaction (drops shards whose file is gone or now ignored). |
|
|
69
|
+
| `--scope changed\|all` | Uses the same scope spelling as MCP. Existing `--changed` / `--all` flags remain supported. |
|
|
66
70
|
|
|
67
71
|
If a live writer already holds the project lock, `refresh` **fails fast** with a
|
|
68
72
|
clear status rather than blocking — it never stalls `bclaw_work`.
|
|
@@ -70,6 +74,7 @@ clear status rather than blocking — it never stalls `bclaw_work`.
|
|
|
70
74
|
```bash
|
|
71
75
|
brainclaw code-map refresh # changed (cheap, default)
|
|
72
76
|
brainclaw code-map refresh --all # full rebuild + compaction
|
|
77
|
+
brainclaw code-map refresh --scope changed
|
|
73
78
|
```
|
|
74
79
|
|
|
75
80
|
### `brainclaw code-map find <query>`
|
|
@@ -126,11 +131,15 @@ all return a `freshness_badge`:
|
|
|
126
131
|
|
|
127
132
|
| Tool | Kind | Purpose |
|
|
128
133
|
|---|---|---|
|
|
129
|
-
| `bclaw_code_status` | read | Active-session project store, freshness,
|
|
130
|
-
| `bclaw_code_find` | read | Ranked symbol-index search (`query`, optional `limit`). Never refreshes. |
|
|
131
|
-
| `bclaw_code_brief` | read | Reading brief for a symbol/path (`target`, optional `limit`, files capped at 12). Never refreshes. |
|
|
134
|
+
| `bclaw_code_status` | read | Active-session or explicit `project` store/path/index diagnostics, freshness, stats, and latest refresh job; `cascade=true` also follows the latest cascade job. Never refreshes. |
|
|
135
|
+
| `bclaw_code_find` | read | Ranked symbol-index search (`query`, optional `limit`/`project`). Never refreshes. |
|
|
136
|
+
| `bclaw_code_brief` | read | Reading brief for a symbol/path (`target`, optional `limit`/`project`, files capped at 12). Never refreshes. |
|
|
132
137
|
| `bclaw_code_export` | read | Bounded local subgraph around required `target`; direction/depth/node/edge caps, confidence filtering, and optional Mermaid projection. Never refreshes. |
|
|
133
|
-
| `bclaw_code_refresh` | write |
|
|
138
|
+
| `bclaw_code_refresh` | write | Accept a durable background rebuild and return immediately. `scope` = `"changed"` (default) or `"all"`; optional `project` targets a named/id/path project, and `cascade=true` spans a workspace. |
|
|
139
|
+
|
|
140
|
+
Every MCP Code Map tool accepts the same optional `project` selector (project
|
|
141
|
+
name, id, or workspace-relative path). It overrides the active session for that
|
|
142
|
+
call without mutating the session.
|
|
134
143
|
|
|
135
144
|
The read tools never trigger a parse — if `bclaw_code_status` /
|
|
136
145
|
`bclaw_code_find` / `bclaw_code_brief` report `missing_index` or a stale badge,
|
|
@@ -186,9 +195,9 @@ reconciliation at the read path:
|
|
|
186
195
|
file-hash diff vs the stored shards), so a stale index is always *visible*,
|
|
187
196
|
never silently wrong.
|
|
188
197
|
3. `refresh --changed` re-parses only the changed files (incremental); `--all` does
|
|
189
|
-
a full rebuild + orphan compaction.
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
a full rebuild + orphan compaction. MCP refreshes are explicit durable jobs;
|
|
199
|
+
their progress is read through `bclaw_code_status` (or
|
|
200
|
+
`bclaw_code_status(cascade=true)` for a workspace cascade).
|
|
192
201
|
4. `bclaw_work` nudges a refresh when the badge is `missing_index` or stale, so an
|
|
193
202
|
agent knows to reconcile before trusting the map.
|
|
194
203
|
|
|
@@ -219,10 +228,12 @@ like any other directory.
|
|
|
219
228
|
Both CLI and MCP status responses disclose the exact resolved project root and
|
|
220
229
|
Code Map store path. The MCP response additionally includes `active_source`, the
|
|
221
230
|
resolved project identity, the running server version, and the package version
|
|
222
|
-
visible on disk.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
231
|
+
visible on disk. `store_exists` describes the physical directory;
|
|
232
|
+
`index_exists` describes a valid readable manifest. In a monorepo, compare
|
|
233
|
+
these fields before concluding that an index is missing: a root store and a
|
|
234
|
+
child store are intentionally distinct. If the versions differ, restart the
|
|
235
|
+
MCP server; if the roots differ, use `project="<name-or-path>"`, select the
|
|
236
|
+
intended session project, or pass `cascade=true` at the workspace root.
|
|
226
237
|
|
|
227
238
|
### Cascading a multi-project workspace (`--cascade`)
|
|
228
239
|
|
|
@@ -242,9 +253,10 @@ double-indexing**, even when projects nest inside one another. `--cascade` is
|
|
|
242
253
|
opt-in; without it, the root refresh keeps its single-tree behaviour (above), and
|
|
243
254
|
single-project repos ignore the flag entirely.
|
|
244
255
|
|
|
245
|
-
The CLI cascade stays synchronous. MCP `
|
|
246
|
-
|
|
247
|
-
|
|
256
|
+
The CLI cascade stays synchronous. Every MCP refresh returns a durable `job_id`
|
|
257
|
+
immediately, avoiding client timeouts; follow a normal refresh with
|
|
258
|
+
`bclaw_code_status(project=...)`, or a cascade with
|
|
259
|
+
`bclaw_code_status(cascade=true)`. Status reports completed
|
|
248
260
|
and total project counts, the project currently being indexed, and terminal
|
|
249
261
|
outcomes. Successful rows are aggregated; only exceptions are named. A project
|
|
250
262
|
with a valid empty index is labeled `no_eligible_files`, while lock contention and
|
package/docs/integrations/mcp.md
CHANGED
|
@@ -109,13 +109,13 @@ Each tool also has an `annotations.category` field: `session`, `context`, `memor
|
|
|
109
109
|
| `bclaw_remove` | memory | Archive or purge a canonical entity |
|
|
110
110
|
| `bclaw_transition` | memory | Move an entity through its validated state machine |
|
|
111
111
|
| `bclaw_move` | memory | Relocate an item to another project, id-preserving (multi-project) |
|
|
112
|
-
| `bclaw_code_status` | discovery | Active-session Code Map
|
|
113
|
-
| `bclaw_code_find` | discovery | Search the Code Map symbol index by name
|
|
114
|
-
| `bclaw_code_brief` | discovery | Ranked reading list + related decisions/traps before editing a symbol or path |
|
|
112
|
+
| `bclaw_code_status` | discovery | Active-session or explicit `project` Code Map path/index diagnostics + freshness/stats; follows durable refresh jobs, with `cascade:true` for workspace progress |
|
|
113
|
+
| `bclaw_code_find` | discovery | Search the active or explicit `project` Code Map symbol index by name |
|
|
114
|
+
| `bclaw_code_brief` | discovery | Ranked reading list + related decisions/traps before editing a symbol or path, optionally targeted by `project` |
|
|
115
115
|
| `bclaw_code_impact` | discovery | Explainable local blast radius from resolved imports: definition, direct dependents, opt-in bounded transitives, tests, and count-based risk |
|
|
116
116
|
| `bclaw_code_export` | discovery | Compact bounded local nodes/edges around one symbol or file; preserves edge kind/source/confidence, with optional Mermaid projection |
|
|
117
117
|
| `bclaw_code_outline` | discovery | Source-ordered symbols of one indexed file (span, exported, confidence) — no reparse |
|
|
118
|
-
| `bclaw_code_refresh` | discovery |
|
|
118
|
+
| `bclaw_code_refresh` | discovery | Accept a durable Code Map rebuild (`scope: changed \| all`, optional `project`) and return immediately; `cascade:true` spans a workspace |
|
|
119
119
|
|
|
120
120
|
See [code map](../code-map.md) for the full Code Map reference (CLI, freshness model, supported languages).
|
|
121
121
|
|
|
@@ -20,9 +20,11 @@ a worker. `execute ↔ verify` iterates until the verify command is green or
|
|
|
20
20
|
the cycle cap is hit.
|
|
21
21
|
|
|
22
22
|
Binding validates the complete graph: every sequence item must reference a
|
|
23
|
-
linked, existing plan (and an existing step when `stepId` is present).
|
|
24
|
-
sequence
|
|
25
|
-
|
|
23
|
+
linked, existing plan (and an existing step when `stepId` is present). A
|
|
24
|
+
multi-lane sequence requires exactly one explicitly named worker slot per lane;
|
|
25
|
+
positional slots are rejected because sorting lanes could otherwise attach a
|
|
26
|
+
perspective or scope policy to the wrong lane. Each slot then carries its lane,
|
|
27
|
+
plan/step ids, and `scope_hint`.
|
|
26
28
|
|
|
27
29
|
## Default protocol
|
|
28
30
|
|
|
@@ -46,9 +48,13 @@ participating slot must produce its expected artifact before advance fires.
|
|
|
46
48
|
## Entry points
|
|
47
49
|
|
|
48
50
|
- **Direct open (typical).**
|
|
49
|
-
`bclaw_loop(intent='open', kind='implementation', slots=[…], linked={plan_ids:[…], sequence_ids:[…]}, allow_orphan=true)`
|
|
50
|
-
followed by `bind`. `allow_orphan=true`
|
|
51
|
-
drive worker turns.
|
|
51
|
+
`bclaw_loop(intent='open', kind='implementation', slots=[{role:'implementer', lane:'api'}, …], linked={plan_ids:[…], sequence_ids:[…]}, allow_orphan=true)`
|
|
52
|
+
followed by a dry-run `bind`, then the real `bind`. `allow_orphan=true`
|
|
53
|
+
acknowledges that the caller will drive worker turns. For multiple lanes,
|
|
54
|
+
every slot must name its lane; the bind response must still be checked for
|
|
55
|
+
lane/perspective alignment. The `linked` key is exactly
|
|
56
|
+
`{sequence_ids:[…], plan_ids:[…]}`; unknown keys are rejected by the open
|
|
57
|
+
envelope, and a dry-run is the safe way to validate the complete contract.
|
|
52
58
|
- **Via bind.** `bclaw_loop(intent='bind', loop_id=…)` validates the linked
|
|
53
59
|
sequence and advances `bind → execute`. Historical launch options
|
|
54
60
|
(`lanes`, `auto_execute`, `model`, `max_assignments`) remain accepted
|
|
@@ -59,6 +65,11 @@ participating slot must produce its expected artifact before advance fires.
|
|
|
59
65
|
slots may be dispatched concurrently. Without `dispatch=true`, `turn` is
|
|
60
66
|
state-only and never starts a process.
|
|
61
67
|
|
|
68
|
+
An implementation loop has no `update` intent. If opening or binding fails,
|
|
69
|
+
cancel the invalid open loop and reopen it with corrected links/slots. The
|
|
70
|
+
default phases are `bind`, `execute`, `verify`, and `handoff_ready`; a custom
|
|
71
|
+
`stop_condition` must reference only phases present in the loop.
|
|
72
|
+
|
|
62
73
|
## Advance gates
|
|
63
74
|
|
|
64
75
|
`verify` carries an `advance_gate`:
|
|
@@ -8,6 +8,25 @@ guarantees this changelog follows.
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## [1.28.5] — 2026-08-28
|
|
12
|
+
|
|
13
|
+
- MCP public surface fingerprint: `sha256:f410416669e7fc9a`
|
|
14
|
+
|
|
15
|
+
**Changed — Code Map project targeting and durable refresh**
|
|
16
|
+
|
|
17
|
+
- Every `bclaw_code_*` input schema gains the same optional `project` selector
|
|
18
|
+
(name, id, or workspace-relative path). It overrides the active session for
|
|
19
|
+
one call without mutating it.
|
|
20
|
+
- `bclaw_code_refresh` now acknowledges every refresh as a durable job, not
|
|
21
|
+
only multi-project cascades. Its `scope: "changed" | "all"` contract is
|
|
22
|
+
unchanged and no new field is required.
|
|
23
|
+
- `bclaw_code_status` keeps its input compatibility while its result separates
|
|
24
|
+
physical path presence (`store_exists`) from a readable index
|
|
25
|
+
(`index_exists`, `index_manifest_exists`) and exposes the latest single-project
|
|
26
|
+
refresh job.
|
|
27
|
+
|
|
28
|
+
All input changes are additive; no tool was added, removed, or renamed.
|
|
29
|
+
|
|
11
30
|
## [1.28.4] — 2026-08-28
|
|
12
31
|
|
|
13
32
|
- MCP public surface fingerprint: `sha256:42cd662667260792`
|
|
@@ -475,8 +494,10 @@ will still succeed. A follow-up PR will strip the dead handler code.
|
|
|
475
494
|
changelog records the published MCP surface fingerprint. When a tool
|
|
476
495
|
name, tier, category, or input schema changes, the test fails until
|
|
477
496
|
this section is updated.
|
|
478
|
-
- MCP public surface fingerprint: `sha256:
|
|
479
|
-
(updated 2026-08-28 for
|
|
497
|
+
- MCP public surface fingerprint: `sha256:f410416669e7fc9a`
|
|
498
|
+
(updated 2026-08-28 for optional `project` targeting on every Code Map MCP
|
|
499
|
+
tool. Previous: `sha256:42cd662667260792`, updated for the additive
|
|
500
|
+
`bclaw_harvest` surface, sequential
|
|
480
501
|
ideation controls, compact read projections, and loop-slot perspective and
|
|
481
502
|
completion fields.)
|
|
482
503
|
Previous: `sha256:be86e5571fcd0226`
|