brainclaw 1.9.1 → 1.10.1

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.
Files changed (95) hide show
  1. package/README.md +78 -25
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +18 -1
  4. package/dist/commands/code-map.js +129 -0
  5. package/dist/commands/codev.js +7 -0
  6. package/dist/commands/dispatch-watch.js +1 -1
  7. package/dist/commands/doctor.js +3 -5
  8. package/dist/commands/loops-handlers.js +4 -1
  9. package/dist/commands/mcp-read-handlers.js +8 -0
  10. package/dist/commands/mcp.js +121 -1
  11. package/dist/commands/metrics.js +0 -1
  12. package/dist/commands/release-claims.js +1 -1
  13. package/dist/commands/run-profile.js +3 -2
  14. package/dist/commands/sequence.js +1 -1
  15. package/dist/commands/switch.js +100 -89
  16. package/dist/commands/sync.js +1 -1
  17. package/dist/commands/upgrade.js +0 -7
  18. package/dist/core/agent-context.js +1 -1
  19. package/dist/core/agent-files.js +13 -2
  20. package/dist/core/agent-integrations.js +3 -3
  21. package/dist/core/agent-registry.js +2 -2
  22. package/dist/core/assignments.js +12 -0
  23. package/dist/core/brainclaw-version.js +2 -2
  24. package/dist/core/code-map/backend.js +176 -0
  25. package/dist/core/code-map/core.js +81 -0
  26. package/dist/core/code-map/drafts.js +2 -0
  27. package/dist/core/code-map/extractor.js +29 -0
  28. package/dist/core/code-map/finalizer.js +191 -0
  29. package/dist/core/code-map/freshness.js +144 -0
  30. package/dist/core/code-map/ids.js +0 -0
  31. package/dist/core/code-map/importable.js +35 -0
  32. package/dist/core/code-map/indexes.js +197 -0
  33. package/dist/core/code-map/lang/java/imports.scm +17 -0
  34. package/dist/core/code-map/lang/java/index.js +254 -0
  35. package/dist/core/code-map/lang/java/tags.scm +48 -0
  36. package/dist/core/code-map/lang/php/imports.scm +21 -0
  37. package/dist/core/code-map/lang/php/index.js +251 -0
  38. package/dist/core/code-map/lang/php/tags.scm +44 -0
  39. package/dist/core/code-map/lang/provider.js +9 -0
  40. package/dist/core/code-map/lang/providers.js +24 -0
  41. package/dist/core/code-map/lang/python/imports.scm +90 -0
  42. package/dist/core/code-map/lang/python/index.js +364 -0
  43. package/dist/core/code-map/lang/python/tags.scm +81 -0
  44. package/dist/core/code-map/lang/query-runtime.js +374 -0
  45. package/dist/core/code-map/lang/registry.js +125 -0
  46. package/dist/core/code-map/lang/typescript/imports.scm +90 -0
  47. package/dist/core/code-map/lang/typescript/index.js +306 -0
  48. package/dist/core/code-map/lang/typescript/tags.js.scm +106 -0
  49. package/dist/core/code-map/lang/typescript/tags.scm +151 -0
  50. package/dist/core/code-map/lock.js +210 -0
  51. package/dist/core/code-map/materialized.js +51 -0
  52. package/dist/core/code-map/memory-reader.js +59 -0
  53. package/dist/core/code-map/paths.js +53 -0
  54. package/dist/core/code-map/query.js +599 -0
  55. package/dist/core/code-map/refresh.js +0 -0
  56. package/dist/core/code-map/resolve.js +177 -0
  57. package/dist/core/code-map/store.js +206 -0
  58. package/dist/core/code-map/types.js +293 -0
  59. package/dist/core/code-map/vocabulary.js +57 -0
  60. package/dist/core/code-map/wasm-loader.js +294 -0
  61. package/dist/core/code-map/work-section.js +206 -0
  62. package/dist/core/codev-rounds.js +4 -0
  63. package/dist/core/context.js +1 -1
  64. package/dist/core/cross-project.js +1 -1
  65. package/dist/core/dispatcher.js +0 -2
  66. package/dist/core/entity-operations.js +0 -3
  67. package/dist/core/execution-adapters.js +11 -10
  68. package/dist/core/execution-profile.js +58 -0
  69. package/dist/core/facade-schema.js +9 -0
  70. package/dist/core/ids.js +1 -1
  71. package/dist/core/instruction-templates.js +2 -0
  72. package/dist/core/instructions.js +0 -1
  73. package/dist/core/loops/lock.js +0 -3
  74. package/dist/core/mcp-command-resolution.js +3 -1
  75. package/dist/core/protocol-skills.js +5 -3
  76. package/dist/core/security-detectors.js +2 -2
  77. package/dist/core/security-extract.js +2 -2
  78. package/dist/core/store-resolution.js +41 -4
  79. package/dist/facts.js +9 -5
  80. package/dist/facts.json +8 -4
  81. package/dist/vendor/web-tree-sitter/tree-sitter.js +3980 -0
  82. package/dist/vendor/web-tree-sitter/tree-sitter.wasm +0 -0
  83. package/dist/wasm/tree-sitter-java.wasm +0 -0
  84. package/dist/wasm/tree-sitter-javascript.wasm +0 -0
  85. package/dist/wasm/tree-sitter-php.wasm +0 -0
  86. package/dist/wasm/tree-sitter-python.wasm +0 -0
  87. package/dist/wasm/tree-sitter-tsx.wasm +0 -0
  88. package/dist/wasm/tree-sitter-typescript.wasm +0 -0
  89. package/dist/wasm/tree-sitter.wasm +0 -0
  90. package/docs/cli.md +46 -8
  91. package/docs/code-map.md +209 -0
  92. package/docs/integrations/mcp.md +13 -6
  93. package/docs/mcp-schema-changelog.md +7 -3
  94. package/docs/quickstart.md +1 -1
  95. package/package.json +11 -6
@@ -37,8 +37,9 @@ export function runRunProfile(profileName, options = {}) {
37
37
  console.error(`Unknown agent: ${options.agent}. Using profile invoke template.`);
38
38
  }
39
39
  }
40
- // Replace {prompt} placeholder with the profile prompt
41
- const command = invoke.replace(/\{prompt\}/g, profile.prompt.replace(/"/g, '\\"'));
40
+ // Replace {prompt} placeholder with the profile prompt. Escape backslashes
41
+ // before quotes so a backslash in the prompt can't break out of the quoting.
42
+ const command = invoke.replace(/\{prompt\}/g, profile.prompt.replace(/\\/g, '\\\\').replace(/"/g, '\\"'));
42
43
  if (options.dry) {
43
44
  console.log(`[dry-run] Profile: ${profile.name}`);
44
45
  console.log(`[dry-run] Command: ${command}`);
@@ -11,7 +11,7 @@ function parseItems(raw) {
11
11
  parsed = JSON.parse(raw);
12
12
  }
13
13
  catch (error) {
14
- throw new Error(`Invalid --items JSON: ${error instanceof Error ? error.message : String(error)}`);
14
+ throw new Error(`Invalid --items JSON: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
15
15
  }
16
16
  if (!Array.isArray(parsed)) {
17
17
  throw new Error('Invalid --items JSON: expected an array');
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { loadActiveProject, saveActiveProject, clearActiveProject } from '../core/active-project.js';
3
- import { buildOperationalIdentity, loadCurrentSession, loadSessionById, saveCurrentSession } from '../core/identity.js';
3
+ import { buildOperationalIdentity, loadCurrentSession, loadSessionById, resolveCurrentSessionId, saveCurrentSession } from '../core/identity.js';
4
4
  import { memoryExists } from '../core/io.js';
5
5
  import { resolveProjectRef } from '../core/store-resolution.js';
6
6
  import { resolveCrossProjectLinks, resolveProjectCwd } from '../core/cross-project.js';
@@ -160,59 +160,70 @@ export function runSwitch(projectRef, options = {}) {
160
160
  }
161
161
  // --list: show available projects
162
162
  if (options.list) {
163
- listProjects(wsRoot, options.json ?? false);
163
+ listProjects(wsRoot, cwd, options.json ?? false);
164
164
  return;
165
165
  }
166
- // --clear: remove active project
166
+ // --clear: remove active project. Session-scoped by default (F3) — clearing
167
+ // the SHARED global pointer is an opt-in (--global) so one agent's clear no
168
+ // longer wipes every other agent's resolution.
167
169
  if (options.clear) {
168
- const session = loadCurrentSession(cwd);
169
- if (session?.active_project) {
170
- const { active_project: _removed, ...rest } = session;
171
- saveCurrentSession(rest, cwd);
170
+ let scope;
171
+ if (options.global) {
172
+ clearActiveProject(wsRoot);
173
+ scope = 'global';
174
+ }
175
+ else {
176
+ const session = loadCurrentSession(cwd);
177
+ if (session?.active_project) {
178
+ const { active_project: _removed, ...rest } = session;
179
+ saveCurrentSession(rest, cwd);
180
+ }
181
+ scope = 'session';
172
182
  }
173
- clearActiveProject(wsRoot);
174
183
  if (options.json) {
175
- console.log(JSON.stringify({ cleared: true }));
184
+ console.log(JSON.stringify({ cleared: true, scope }));
176
185
  }
177
186
  else {
178
- console.log('✔ Active project cleared. Commands will use current directory.');
187
+ const hint = scope === 'session' ? ' (session-scoped)' : ' (global)';
188
+ console.log(`✔ Active project cleared${hint}. Commands will use current directory.`);
179
189
  }
180
190
  return;
181
191
  }
182
192
  // No argument: show current active project
183
193
  if (!projectRef) {
184
- showCurrent(wsRoot, options.json ?? false);
194
+ showCurrent(wsRoot, cwd, options.json ?? false);
185
195
  return;
186
196
  }
187
197
  // Switch to project
188
- const resolved = resolveProjectRef(projectRef, cwd);
189
- if (!resolved) {
190
- console.error(`Error: cannot resolve project "${projectRef}".`);
191
- console.error('Use `brainclaw switch --list` to see available projects.');
192
- process.exit(1);
193
- }
194
- let projectName;
195
- try {
196
- const config = loadConfig(resolved);
197
- projectName = config.project_name;
198
- }
199
- catch {
200
- // name is optional
201
- }
202
198
  const now = new Date().toISOString();
203
- const session = loadCurrentSession(cwd);
204
- const scopedToSession = options.session ?? !!session;
205
199
  let scope;
206
- if (scopedToSession && session) {
207
- // Write to session state — only this agent sees this switch
208
- saveCurrentSession({
209
- ...session,
210
- active_project: { path: resolved, name: projectName, switched_at: now },
211
- }, cwd);
212
- scope = 'session';
213
- }
214
- else {
215
- // Fall back to global active-project.json
200
+ let switchedPath;
201
+ let switchedName;
202
+ if (options.global) {
203
+ // Opt-in, audited: set the SHARED workspace default for every agent on the
204
+ // host. Bypasses the session entirely (an operator setting a default).
205
+ // Resolve store-chain children AND cross-project links (mirror switchProject)
206
+ // so `switch <linked> --global` matches what --list shows and what the
207
+ // session path can target (Codex final review F3-F5 finding).
208
+ let resolved = resolveProjectRef(projectRef, cwd);
209
+ if (!resolved) {
210
+ try {
211
+ const linkResolved = resolveProjectCwd(projectRef, cwd);
212
+ if (linkResolved !== cwd)
213
+ resolved = linkResolved;
214
+ }
215
+ catch { /* falls through to the error below */ }
216
+ }
217
+ if (!resolved) {
218
+ console.error(`Error: cannot resolve project "${projectRef}".`);
219
+ console.error('Use `brainclaw switch --list` to see available projects.');
220
+ process.exit(1);
221
+ }
222
+ let projectName;
223
+ try {
224
+ projectName = loadConfig(resolved).project_name;
225
+ }
226
+ catch { /* name is optional */ }
216
227
  saveActiveProject(wsRoot, {
217
228
  path: resolved,
218
229
  name: projectName,
@@ -220,21 +231,47 @@ export function runSwitch(projectRef, options = {}) {
220
231
  switched_by: process.env.BRAINCLAW_AGENT_NAME ?? process.env.USER ?? 'unknown',
221
232
  });
222
233
  scope = 'global';
234
+ switchedPath = resolved;
235
+ switchedName = projectName;
236
+ }
237
+ else {
238
+ // F3 default: session-scoped + isolated. Delegate to switchProject — the
239
+ // safe model that auto-creates the session, honours an explicit
240
+ // BRAINCLAW_SESSION_ID (resolveCurrentSessionId returns it WITHOUT
241
+ // persisting, so the session file must be created), resolves cross-project
242
+ // links, and never touches the shared global pointer.
243
+ try {
244
+ const explicitSessionId = resolveCurrentSessionId(process.env, cwd) || undefined;
245
+ const result = switchProject(projectRef, { cwd, sessionOnly: true, sessionId: explicitSessionId });
246
+ scope = 'session';
247
+ switchedPath = result.path;
248
+ switchedName = result.name;
249
+ }
250
+ catch (err) {
251
+ console.error(`Error: ${err.message}`);
252
+ console.error('Use `brainclaw switch --list` to see available projects.');
253
+ process.exit(1);
254
+ }
223
255
  }
224
256
  if (options.json) {
225
- console.log(JSON.stringify({ switched: true, path: resolved, name: projectName, scope }));
257
+ console.log(JSON.stringify({ switched: true, path: switchedPath, name: switchedName, scope }));
226
258
  }
227
259
  else {
228
- const rel = path.relative(wsRoot, resolved) || '.';
229
- const scopeHint = scope === 'session' ? ' (session-scoped)' : '';
230
- console.log(`✔ Switched to ${projectName ? `"${projectName}" (${rel})` : rel}${scopeHint}`);
260
+ const rel = path.relative(wsRoot, switchedPath) || '.';
261
+ const scopeHint = scope === 'session' ? ' (session-scoped)' : ' (global — all agents)';
262
+ console.log(`✔ Switched to ${switchedName ? `"${switchedName}" (${rel})` : rel}${scopeHint}`);
231
263
  }
232
264
  }
233
- function showCurrent(wsRoot, json) {
234
- const active = loadActiveProject(wsRoot);
265
+ function showCurrent(wsRoot, cwd, json) {
266
+ // F5: prefer the session's own active project so an agent sees its own
267
+ // session-scoped switch, not just the shared global pointer.
268
+ const sessionActive = loadCurrentSession(cwd)?.active_project;
269
+ const globalActive = loadActiveProject(wsRoot);
270
+ const active = sessionActive ?? globalActive;
271
+ const source = sessionActive ? 'session' : globalActive ? 'global' : 'none';
235
272
  if (!active) {
236
273
  if (json) {
237
- console.log(JSON.stringify({ active: false }));
274
+ console.log(JSON.stringify({ active: false, scope: 'none' }));
238
275
  }
239
276
  else {
240
277
  console.log('No active project. Commands use current directory.');
@@ -243,67 +280,41 @@ function showCurrent(wsRoot, json) {
243
280
  return;
244
281
  }
245
282
  const rel = path.relative(wsRoot, active.path) || '.';
283
+ const switchedBy = 'switched_by' in active ? active.switched_by : undefined;
246
284
  if (json) {
247
- console.log(JSON.stringify({ active: true, ...active, relative_path: rel }));
285
+ console.log(JSON.stringify({ active: true, ...active, relative_path: rel, scope: source }));
248
286
  }
249
287
  else {
250
- console.log(`Active project: ${active.name ? `"${active.name}" (${rel})` : rel}`);
288
+ const scopeHint = source === 'session' ? ' (session-scoped)' : ' (global — all agents)';
289
+ console.log(`Active project: ${active.name ? `"${active.name}" (${rel})` : rel}${scopeHint}`);
251
290
  console.log(` switched at: ${active.switched_at}`);
252
- if (active.switched_by)
253
- console.log(` switched by: ${active.switched_by}`);
291
+ if (switchedBy)
292
+ console.log(` switched by: ${switchedBy}`);
254
293
  }
255
294
  }
256
- function listProjects(wsRoot, json) {
257
- const active = loadActiveProject(wsRoot);
258
- const projects = [];
259
- // Add workspace root itself
260
- if (memoryExists(wsRoot)) {
261
- try {
262
- const config = loadConfig(wsRoot);
263
- projects.push({
264
- name: config.project_name,
265
- path: wsRoot,
266
- relative_path: '.',
267
- active: active?.path === wsRoot,
268
- });
269
- }
270
- catch {
271
- projects.push({
272
- path: wsRoot,
273
- relative_path: '.',
274
- active: active?.path === wsRoot,
275
- });
276
- }
277
- }
278
- // Discover child projects (depth 7 covers deep workspace layouts like /srv/dev/repos/global/applications/*/...)
279
- const children = scanNestedBrainclawProjects(wsRoot, 7);
280
- for (const child of children) {
281
- const childPath = path.resolve(child.path);
282
- if (childPath === wsRoot)
283
- continue;
284
- const rel = path.relative(wsRoot, childPath) || '.';
285
- projects.push({
286
- name: child.project_name,
287
- path: childPath,
288
- relative_path: rel,
289
- active: active?.path === childPath,
290
- });
291
- }
295
+ function listProjects(wsRoot, cwd, json) {
296
+ // F5: delegate to the session-aware lister so the active marker reflects the
297
+ // agent's own session active project, falling back to the global pointer.
298
+ const result = listAvailableProjectsForSession(cwd);
292
299
  if (json) {
293
- console.log(JSON.stringify({ workspace: wsRoot, projects }, null, 2));
300
+ console.log(JSON.stringify({
301
+ workspace: result.workspace_root,
302
+ active_source: result.active_source,
303
+ projects: result.projects,
304
+ }, null, 2));
294
305
  return;
295
306
  }
296
- if (projects.length === 0) {
307
+ if (result.projects.length === 0) {
297
308
  console.log('No brainclaw projects found in this workspace.');
298
309
  return;
299
310
  }
300
- console.log(`Projects in ${wsRoot}:\n`);
301
- for (const p of projects) {
311
+ console.log(`Projects in ${result.workspace_root}:\n`);
312
+ for (const p of result.projects) {
302
313
  const marker = p.active ? '→ ' : ' ';
303
314
  const name = p.name ? `${p.name} (${p.relative_path})` : p.relative_path;
304
315
  console.log(`${marker}${name}`);
305
316
  }
306
- if (!active) {
317
+ if (result.active_source === 'none') {
307
318
  console.log('\nNo active project. Use `brainclaw switch <project>` to set one.');
308
319
  }
309
320
  }
@@ -61,7 +61,7 @@ export function runSync(options = {}) {
61
61
  return;
62
62
  }
63
63
  // Check git status of .brainclaw/
64
- let gitStatus = '';
64
+ let gitStatus;
65
65
  try {
66
66
  // Security: execFileSync (no shell) + scopePaths spread as separate args so
67
67
  // path specs cannot inject (Socket 2026-06-08 class).
@@ -463,13 +463,6 @@ function upsertSection(existingContent, section) {
463
463
  const trimmed = existingContent.trimEnd();
464
464
  return trimmed.length > 0 ? `${trimmed}\n\n${section}\n` : `${section}\n`;
465
465
  }
466
- function listJsonFiles(dir) {
467
- if (!fs.existsSync(dir))
468
- return [];
469
- return fs.readdirSync(dir)
470
- .filter(f => f.endsWith('.json'))
471
- .map(f => path.join(dir, f));
472
- }
473
466
  function listJsonFilesRecursive(dir) {
474
467
  if (!fs.existsSync(dir))
475
468
  return [];
@@ -63,7 +63,7 @@ function readAgentsMarkdown(cwd) {
63
63
  // Only extract rules from actionable sections, not from descriptive sections
64
64
  // like "why this matters" which contain explanatory bullets, not instructions.
65
65
  const SKIP_SECTIONS = /why this matters|what it provides|what brainclaw/i;
66
- let currentSection = '';
66
+ let currentSection;
67
67
  let skipSection = false;
68
68
  const rules = [];
69
69
  for (const line of lines) {
@@ -24,6 +24,18 @@ This project uses brainclaw for shared coordination between humans and agents.
24
24
  2. Check **Your open work** for active claims and in-progress plans assigned to you
25
25
  3. Respect active claims from other agents — check \`brainclaw claim list\` before editing a claimed scope
26
26
 
27
+ ### Before editing unfamiliar code (Code Map)
28
+
29
+ Don't grep the repo blind. Orient with the Code Map first:
30
+
31
+ \`\`\`bash
32
+ brainclaw code-map brief <symbol-or-path> # ranked reading list + related decisions/traps (MCP: bclaw_code_brief)
33
+ brainclaw code-map find <name> # locate a symbol/class/component (MCP: bclaw_code_find)
34
+ brainclaw code-map status # freshness
35
+ brainclaw code-map refresh --all # when status is missing_index
36
+ brainclaw code-map refresh --changed # when status is stale_*
37
+ \`\`\`
38
+
27
39
  ### Before finishing (required)
28
40
 
29
41
  1. Release claims you opened: \`brainclaw claim release <id>\` — or \`brainclaw session-end --auto-release\`
@@ -1146,7 +1158,7 @@ export function ensureClaudeCodeCommand(cwd) {
1146
1158
  relativePath: CLAUDE_CODE_COMMAND_RELATIVE_PATH,
1147
1159
  };
1148
1160
  }
1149
- export function ensureClaudeCodeUserSettings(homeDir, env = process.env) {
1161
+ export function ensureClaudeCodeUserSettings(homeDir, _env = process.env) {
1150
1162
  if (!homeDir)
1151
1163
  return undefined;
1152
1164
  const filePath = path.join(homeDir, '.claude', 'settings.json');
@@ -1618,7 +1630,6 @@ export function ensureCodexMcpConfig(homeDir, env = process.env) {
1618
1630
  const autoApprovedSet = new Set(getHeadlessAutoApprovedToolNames());
1619
1631
  const toolSectionRe = /^\[mcp_servers\.brainclaw\.tools\.([^\]]+)\]/gm;
1620
1632
  const approvalModeRe = /^\s*approval_mode\s*=\s*"([^"]+)"/m;
1621
- let m;
1622
1633
  const warnings = [];
1623
1634
  // Split into sections to check each tool block
1624
1635
  const lines = existing.split('\n');
@@ -160,8 +160,8 @@ export function extractMcpCommandVal(agentName, expectedPath) {
160
160
  return { is_valid: false };
161
161
  }
162
162
  if (expectedPath.endsWith('.toml')) {
163
- const cmdMatch = content.match(/\[mcp_servers\.brainclaw\](?:[^\[]*)command\s*=\s*(["'])(.+?)\1/is);
164
- const argsMatch = content.match(/\[mcp_servers\.brainclaw\](?:[^\[]*)args\s*=\s*\[(.+?)\]/is);
163
+ const cmdMatch = content.match(/\[mcp_servers\.brainclaw\](?:[^[]*)command\s*=\s*(["'])(.+?)\1/is);
164
+ const argsMatch = content.match(/\[mcp_servers\.brainclaw\](?:[^[]*)args\s*=\s*\[(.+?)\]/is);
165
165
  let args;
166
166
  if (argsMatch) {
167
167
  args = argsMatch[1]
@@ -274,7 +274,7 @@ export function assessAgentIntegrationReadiness(config, cwd, env = process.env)
274
274
  const surfaces = declaration.surfaces.map((surface) => surfaceExists(surface, cwd, env, declaration.agent_name));
275
275
  const missingSurfaces = surfaces.filter((surface) => !surface.exists);
276
276
  const driftingSurfaces = surfaces.filter((surface) => surface.drift_message != null);
277
- let effectiveTier = 'tier-b';
277
+ let effectiveTier;
278
278
  const selfHealingGuidance = [];
279
279
  const hasMissingMcpOrHook = missingSurfaces.some((s) => s.kind === 'mcp' || s.kind === 'hook');
280
280
  const hasDriftingMcp = driftingSurfaces.some((s) => s.kind === 'mcp');
@@ -275,7 +275,7 @@ export function registerAgentIdentity(input) {
275
275
  saveAgentIdentity(created, input.cwd, input.preferredDirName);
276
276
  return created;
277
277
  }
278
- export function resolveCurrentAgentIdentity(cwd, preferredDirName, homeDir) {
278
+ export function resolveCurrentAgentIdentity(cwd, preferredDirName, _homeDir) {
279
279
  // env var takes priority over config — allows AI agent to self-identify
280
280
  const envAgentId = (process.env.BRAINCLAW_AGENT_ID ?? '').trim();
281
281
  const envAgentName = (process.env.BRAINCLAW_AGENT_NAME ?? process.env.BRAINCLAW_AGENT ?? '').trim();
@@ -537,7 +537,7 @@ export function resolveCurrentModel(cwd) {
537
537
  * Note: config.current_agent is intentionally NOT used here — it's a singleton
538
538
  * global that causes cross-agent confusion in multi-agent setups.
539
539
  */
540
- export function resolveCurrentAgentName(cwd, _homeDir) {
540
+ export function resolveCurrentAgentName(_cwd, _homeDir) {
541
541
  const fromEnv = (process.env.BRAINCLAW_AGENT_NAME ?? process.env.BRAINCLAW_AGENT)?.trim();
542
542
  if (fromEnv)
543
543
  return fromEnv;
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Assignment lifecycle — Agent SDK runtime protocol.
3
+ *
4
+ * An Assignment is the canonical coordination entity that tracks the full
5
+ * lifecycle of dispatched work: from creation through offer, acceptance,
6
+ * execution, and completion (or failure/timeout/reroute).
7
+ *
8
+ * Assignments reference Claims (scope lock) and InboxMessages (brief delivery)
9
+ * but don't replace them. They own the status FSM and heartbeat tracking.
10
+ *
11
+ * @module
12
+ */
1
13
  import fs from 'node:fs';
2
14
  import { AssignmentSchema } from './schema.js';
3
15
  import { resolveEntityDir } from './io.js';
@@ -555,7 +555,7 @@ function readWorkspaceBrainclawPackage(cwd) {
555
555
  }
556
556
  catch (error) {
557
557
  const message = error instanceof Error ? error.message : String(error);
558
- throw new Error(`Failed to read package.json: ${message}`);
558
+ throw new Error(`Failed to read package.json: ${message}`, { cause: error });
559
559
  }
560
560
  const name = typeof parsed.name === 'string' ? parsed.name.trim() : '';
561
561
  const version = typeof parsed.version === 'string' ? parsed.version.trim() : '';
@@ -617,7 +617,7 @@ function parseNpmDistTags(stdout) {
617
617
  }
618
618
  catch (error) {
619
619
  const message = error instanceof Error ? error.message : String(error);
620
- throw new Error(`npm view returned invalid JSON: ${message}`);
620
+ throw new Error(`npm view returned invalid JSON: ${message}`, { cause: error });
621
621
  }
622
622
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
623
623
  throw new Error('npm view did not return a dist-tag object.');
@@ -0,0 +1,176 @@
1
+ /**
2
+ * CodeQueryBackend — the agent-facing query contract (spec §8).
3
+ *
4
+ * Introduced in P0 so a future Memgraph (or other) backend can be added without
5
+ * changing the agent-facing APIs. P0 ships exactly one implementation:
6
+ * `JsonlBackend`. In this sprint, `status()` and `refresh()` are minimally real
7
+ * (they read/init the durable store and report freshness); `find()`/`brief()`
8
+ * return not-yet-implemented placeholders that still carry a real
9
+ * `freshness_badge`, locking the response shape for later sprints.
10
+ */
11
+ import { execFileSync } from 'node:child_process';
12
+ import path from 'node:path';
13
+ import { readManifest, storeExists } from './store.js';
14
+ import { refresh as runRefresh } from './refresh.js';
15
+ import { applyGitHeadDrift } from './freshness.js';
16
+ import { brief as runBrief, find as runFind } from './query.js';
17
+ import { defaultMemoryReader } from './memory-reader.js';
18
+ /** spec §9 caps the brief reading list at 12 files. */
19
+ export const BRIEF_FILE_CAP = 12;
20
+ function badge(status, details = {}) {
21
+ return { status, details };
22
+ }
23
+ /**
24
+ * Read the working tree's current commit at `root` (read-path git-HEAD drift,
25
+ * trp_42688015). Returns null on any failure or a non-git project (also detached
26
+ * HEAD resolves to the commit sha, which is the correct comparison key) — a null
27
+ * makes the comparison a no-op, preserving existing behaviour.
28
+ *
29
+ * COST (review finding, LOW): one synchronous `git rev-parse HEAD` per status/
30
+ * find/brief call. These are interactive, human-/agent-paced reads (not a tight
31
+ * loop), so a single ~5–15ms spawn is acceptable and keeps branch-switch detection
32
+ * immediate. If this ever shows up on a profile, memoize per `root` behind a short
33
+ * TTL (a few seconds) — short enough that a checkout is still caught promptly.
34
+ */
35
+ function readCurrentGitHead(root) {
36
+ try {
37
+ const out = execFileSync('git', ['rev-parse', 'HEAD'], {
38
+ cwd: root,
39
+ encoding: 'utf-8',
40
+ stdio: ['ignore', 'pipe', 'ignore'],
41
+ }).trim();
42
+ return out || null;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ /**
49
+ * P0 JSONL-backed query backend. Reads the durable file store (manifest +
50
+ * shards + indexes); no graph DB. find()/brief() are stubbed for Sprint 1.
51
+ */
52
+ export class JsonlBackend {
53
+ /**
54
+ * Related-memory read seam (spec §11). Defaults to the canonical entity read
55
+ * path; tests inject an in-memory reader to assert attachment without a store.
56
+ */
57
+ memoryReader;
58
+ /** Read-path git-HEAD reader (injectable for tests). trp_42688015. */
59
+ gitHeadReader;
60
+ constructor(opts = {}) {
61
+ this.memoryReader = opts.memoryReader ?? defaultMemoryReader;
62
+ this.gitHeadReader = opts.gitHeadReader ?? readCurrentGitHead;
63
+ }
64
+ async status(input) {
65
+ const manifest = readManifest(input.cwd, input.preferredDirName);
66
+ if (!manifest) {
67
+ return {
68
+ store_exists: storeExists(input.cwd, input.preferredDirName),
69
+ freshness_badge: badge('missing_index'),
70
+ stats: null,
71
+ };
72
+ }
73
+ const base = badge(manifest.freshness.status, {
74
+ stale_file_count: manifest.freshness.stale_file_count,
75
+ partial_reason: manifest.freshness.partial_reason,
76
+ });
77
+ return {
78
+ store_exists: true,
79
+ freshness_badge: this.withHeadDrift(base, manifest, input.cwd),
80
+ stats: {
81
+ files_indexed: manifest.stats.files_indexed,
82
+ nodes: manifest.stats.nodes,
83
+ edges: manifest.stats.edges,
84
+ },
85
+ };
86
+ }
87
+ /**
88
+ * Real refresh (spec §7): resolves project identity (input -> manifest ->
89
+ * cwd-derived default), then runs the Tree-sitter parse + index + materialize
90
+ * pipeline behind the project lock. A live competing lock fails fast with a
91
+ * clear status — refresh never blocks bclaw_work (rule 8).
92
+ */
93
+ async refresh(input) {
94
+ const scope = input.scope ?? 'changed';
95
+ const manifest = readManifest(input.cwd, input.preferredDirName);
96
+ const projectRoot = input.projectRoot ?? manifest?.project_root ?? input.cwd ?? process.cwd();
97
+ const projectId = input.projectId ?? manifest?.project_id ?? `prj_${path.basename(path.resolve(projectRoot))}`;
98
+ const result = await runRefresh({
99
+ projectId,
100
+ projectRoot,
101
+ scope,
102
+ cwd: input.cwd,
103
+ preferredDirName: input.preferredDirName,
104
+ ownerAgent: input.ownerAgent ?? null,
105
+ ownerAgentId: input.ownerAgentId ?? null,
106
+ });
107
+ return {
108
+ ran: result.ran,
109
+ scope,
110
+ lock_acquired: result.lock_acquired,
111
+ freshness_badge: badge(result.freshness.status, {
112
+ stale_file_count: result.freshness.stale_file_count,
113
+ partial_reason: result.freshness.partial_reason,
114
+ files_parsed: result.files_parsed,
115
+ files_compacted: result.files_compacted,
116
+ duration_ms: result.duration_ms,
117
+ }),
118
+ ...(result.lock_status ? { lock_status: result.lock_status } : {}),
119
+ };
120
+ }
121
+ /**
122
+ * Agent-facing symbol search (spec §12.1). Ranks symbols-index matches and
123
+ * lazily validates each backing shard against the live file before serving it
124
+ * as confident (§6.1); the response badge reflects any detected drift.
125
+ */
126
+ async find(input) {
127
+ const ctx = this.queryContext(input);
128
+ const out = runFind(input.query, input.limit, ctx);
129
+ const manifest = readManifest(input.cwd, input.preferredDirName);
130
+ const base = {
131
+ status: out.freshness_badge.status,
132
+ details: out.freshness_badge.details,
133
+ };
134
+ return {
135
+ query: out.query,
136
+ matches: out.matches,
137
+ freshness_badge: this.withHeadDrift(base, manifest, input.cwd),
138
+ };
139
+ }
140
+ /**
141
+ * Agent-facing reading list (spec §9, §11). Produces a ranked
142
+ * suggested_files_to_read (cap 12), attaches related brainclaw memory (cap 5),
143
+ * and carries a §6.1 lazy-validated freshness badge.
144
+ */
145
+ async brief(input) {
146
+ const ctx = this.queryContext(input);
147
+ const out = runBrief(input.target, input.limit, ctx, this.memoryReader);
148
+ const manifest = readManifest(input.cwd, input.preferredDirName);
149
+ const base = {
150
+ status: out.freshness_badge.status,
151
+ details: out.freshness_badge.details,
152
+ };
153
+ return {
154
+ target: out.target,
155
+ suggested_files_to_read: out.suggested_files_to_read,
156
+ related_memory: out.related_memory,
157
+ freshness_badge: this.withHeadDrift(base, manifest, input.cwd),
158
+ };
159
+ }
160
+ /**
161
+ * Annotate a read badge with git-HEAD drift vs the commit the index was built
162
+ * against (`manifest.git.head`). trp_42688015 — a branch switch (whole-tree move)
163
+ * is otherwise unflagged because `status` reads only write-side freshness and the
164
+ * per-file lazy check is query-scoped + budgeted. No-op for non-git projects.
165
+ */
166
+ withHeadDrift(base, manifest, cwd) {
167
+ if (!manifest)
168
+ return base;
169
+ const root = manifest.project_root || cwd || process.cwd();
170
+ return applyGitHeadDrift(base, manifest.git.head, this.gitHeadReader(root));
171
+ }
172
+ queryContext(input) {
173
+ return { cwd: input.cwd, preferredDirName: input.preferredDirName };
174
+ }
175
+ }
176
+ //# sourceMappingURL=backend.js.map