brainclaw 1.28.4 → 1.28.5

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.
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
- const scope = options.all ? 'all' : 'changed';
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 badge, and index stats. 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.',
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: 'Rebuild the Code Map index for the active session project. scope="changed" (default) reparses changed files; scope="all" does a full refresh + compaction. In a multi-project workspace, cascade=true starts a durable background job immediately; follow it with bclaw_code_status(cascade=true), which reports progress and terminal per-project diagnostics without an MCP timeout.',
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
  },
@@ -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
- const codeCwd = scopeInfo.cwd;
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: [{ type: 'text', text: `Code Map: ${status.store_exists ? 'store present' : 'no store'} — freshness=${status.freshness_badge.freshness}` }],
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: scopeInfo.active_source,
1112
- resolved_project: scopeInfo.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 result = await be.refresh({ scope, cwd: codeCwd, cascade: args.cascade === true });
1140
- const cascadeNote = result.cascade ? ` cascade=${result.cascade.children_refreshed} child(ren)+root` : '';
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: `Code Map refresh [${result.scope}]: ran=${result.ran} freshness=${result.freshness_badge.freshness}${cascadeNote}${result.lock_status ? ` (${result.lock_status})` : ''}` }],
1144
- structuredContent: { ...result, freshness_badge: result.freshness_badge },
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: appendLegacyMcpToolWarning(toolResponse(handleMcpReadToolCall(name, args, { cwd, connectionSessionId, effectiveScope: scopeInfo })), name),
1262
+ response,
1263
+ ...(typeof switchedSessionId === 'string' && switchedSessionId
1264
+ ? { nextConnectionSessionId: switchedSessionId }
1265
+ : {}),
1231
1266
  };
1232
1267
  }
1233
1268
  // Resolve model once for all write operations
@@ -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 { switched: true, path: resolved, name: projectName, scope: 'session', workspace_root: wsRoot };
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, storeExists } from './store.js';
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: m ? true : storeExists(abs),
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: true,
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: storeExists(input.cwd, input.preferredDirName),
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
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.4 on 2026-08-28T07:25:30.486Z
2
+ // Source: brainclaw v1.28.5 on 2026-08-28T18:04:39.470Z
3
3
  export const FACTS = {
4
- "version": "1.28.4",
5
- "generated_at": "2026-08-28T07:25:30.486Z",
4
+ "version": "1.28.5",
5
+ "generated_at": "2026-08-28T18:04:39.470Z",
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-28T07:25:28.367Z",
482
+ "generated_at": "2026-08-28T18:04:37.892Z",
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": 80,
491
+ "duration_ms_median": 51,
492
492
  "payload_chars_median": 1640,
493
493
  "payload_tokens_est_median": 410
494
494
  },
@@ -496,17 +496,17 @@ 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": 124,
500
- "payload_chars_median": 2626,
501
- "payload_tokens_est_median": 657
499
+ "duration_ms_median": 72,
500
+ "payload_chars_median": 2625,
501
+ "payload_tokens_est_median": 656
502
502
  },
503
503
  {
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": 11,
508
- "payload_chars_median": 1629,
509
- "payload_tokens_est_median": 407
507
+ "duration_ms_median": 8,
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.4",
3
- "generated_at": "2026-08-28T07:25:30.486Z",
2
+ "version": "1.28.5",
3
+ "generated_at": "2026-08-28T18:04:39.470Z",
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-28T07:25:28.367Z",
480
+ "generated_at": "2026-08-28T18:04:37.892Z",
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": 80,
489
+ "duration_ms_median": 51,
490
490
  "payload_chars_median": 1640,
491
491
  "payload_tokens_est_median": 410
492
492
  },
@@ -494,17 +494,17 @@
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": 124,
498
- "payload_chars_median": 2626,
499
- "payload_tokens_est_median": 657
497
+ "duration_ms_median": 72,
498
+ "payload_chars_median": 2625,
499
+ "payload_tokens_est_median": 656
500
500
  },
501
501
  {
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": 11,
506
- "payload_chars_median": 1629,
507
- "payload_tokens_est_median": 407
505
+ "duration_ms_median": 8,
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 (JS/TS, Python, PHP, Java) so agents
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
- Store 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 the MCP surface, the equivalent `bclaw_code_status(cascade=true)` also follows the latest durable cascade job.
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 (never blocks) 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 (zero double-indexing). The MCP equivalent starts a durable background job instead; follow it with `bclaw_code_status(cascade=true)`. See [docs/code-map.md](code-map.md#cascading-a-multi-project-workspace---cascade).
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-map/` (one JSONL shard per file, plus
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, the freshness badge, and index
41
- stats (files indexed, nodes, edges). Never refreshes.
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, index stats; `cascade=true` also follows the latest cascade job. Never refreshes. |
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 | Rebuild the index. `scope` = `"changed"` (default) or `"all"`; MCP `cascade=true` starts a durable background job and returns immediately. |
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. The one bounded background path is an
190
- explicitly requested MCP monorepo cascade, whose durable progress is read
191
- through `bclaw_code_status(cascade=true)`.
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. In a monorepo, compare these fields before concluding that an
223
- index is missing: a root store and a child store are intentionally distinct. If
224
- the versions differ, restart the MCP server; if the roots differ, select the
225
- intended project/session (or pass `cascade=true` at the workspace root).
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 `bclaw_code_refresh(cascade=true)` returns
246
- a durable `job_id` immediately, avoiding the client timeout that a large workspace
247
- can hit; follow it with `bclaw_code_status(cascade=true)`. Status reports completed
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
@@ -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 freshness + stats; `cascade:true` follows durable monorepo refresh progress and exceptions |
113
- | `bclaw_code_find` | discovery | Search the Code Map symbol index by name (function/class/component/hook/type) |
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 | Rebuild the Code Map index (`scope: changed \| all`); `cascade:true` starts a durable background job |
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
 
@@ -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:42cd662667260792`
479
- (updated 2026-08-28 for the additive `bclaw_harvest` surface, sequential
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`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainclaw",
3
- "version": "1.28.4",
3
+ "version": "1.28.5",
4
4
  "description": "Shared project memory for humans and coding agents.",
5
5
  "type": "module",
6
6
  "repository": {