brainclaw 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +631 -499
  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/harvest.js +1 -1
  7. package/dist/commands/hooks.js +73 -73
  8. package/dist/commands/init.js +1 -1
  9. package/dist/commands/install-hooks.js +78 -78
  10. package/dist/commands/mcp-read-handlers.js +57 -14
  11. package/dist/commands/mcp.js +200 -13
  12. package/dist/commands/run-profile.js +3 -2
  13. package/dist/commands/switch.js +125 -93
  14. package/dist/commands/version.js +1 -1
  15. package/dist/core/agent-capability.js +19 -4
  16. package/dist/core/agent-files.js +131 -119
  17. package/dist/core/code-map/backend.js +123 -0
  18. package/dist/core/code-map/core.js +81 -0
  19. package/dist/core/code-map/drafts.js +2 -0
  20. package/dist/core/code-map/extractor.js +29 -0
  21. package/dist/core/code-map/finalizer.js +191 -0
  22. package/dist/core/code-map/freshness.js +108 -0
  23. package/dist/core/code-map/ids.js +0 -0
  24. package/dist/core/code-map/importable.js +35 -0
  25. package/dist/core/code-map/indexes.js +197 -0
  26. package/dist/core/code-map/lang/java/imports.scm +17 -0
  27. package/dist/core/code-map/lang/java/index.js +254 -0
  28. package/dist/core/code-map/lang/java/tags.scm +48 -0
  29. package/dist/core/code-map/lang/php/imports.scm +21 -0
  30. package/dist/core/code-map/lang/php/index.js +251 -0
  31. package/dist/core/code-map/lang/php/tags.scm +44 -0
  32. package/dist/core/code-map/lang/provider.js +9 -0
  33. package/dist/core/code-map/lang/providers.js +24 -0
  34. package/dist/core/code-map/lang/python/imports.scm +90 -0
  35. package/dist/core/code-map/lang/python/index.js +364 -0
  36. package/dist/core/code-map/lang/python/tags.scm +81 -0
  37. package/dist/core/code-map/lang/query-runtime.js +374 -0
  38. package/dist/core/code-map/lang/registry.js +125 -0
  39. package/dist/core/code-map/lang/typescript/imports.scm +90 -0
  40. package/dist/core/code-map/lang/typescript/index.js +306 -0
  41. package/dist/core/code-map/lang/typescript/tags.js.scm +106 -0
  42. package/dist/core/code-map/lang/typescript/tags.scm +151 -0
  43. package/dist/core/code-map/lock.js +210 -0
  44. package/dist/core/code-map/materialized.js +51 -0
  45. package/dist/core/code-map/memory-reader.js +59 -0
  46. package/dist/core/code-map/paths.js +53 -0
  47. package/dist/core/code-map/query.js +568 -0
  48. package/dist/core/code-map/refresh.js +0 -0
  49. package/dist/core/code-map/resolve.js +177 -0
  50. package/dist/core/code-map/store.js +206 -0
  51. package/dist/core/code-map/types.js +288 -0
  52. package/dist/core/code-map/vocabulary.js +57 -0
  53. package/dist/core/code-map/wasm-loader.js +294 -0
  54. package/dist/core/code-map/work-section.js +206 -0
  55. package/dist/core/codev-prompts.js +38 -38
  56. package/dist/core/codev-rounds.js +4 -0
  57. package/dist/core/default-profiles/doctor.yaml +11 -11
  58. package/dist/core/default-profiles/janitor.yaml +11 -11
  59. package/dist/core/default-profiles/onboarder.yaml +11 -11
  60. package/dist/core/default-profiles/reviewer.yaml +13 -13
  61. package/dist/core/dispatcher.js +1 -1
  62. package/dist/core/entity-operations.js +29 -3
  63. package/dist/core/execution-adapters.js +11 -10
  64. package/dist/core/execution-profile.js +58 -0
  65. package/dist/core/execution.js +1 -1
  66. package/dist/core/facade-schema.js +9 -0
  67. package/dist/core/instruction-templates.js +2 -0
  68. package/dist/core/loops/verbs.js +0 -1
  69. package/dist/core/mcp-command-resolution.js +3 -1
  70. package/dist/core/messaging.js +2 -2
  71. package/dist/core/protocol-skills.js +164 -164
  72. package/dist/core/runtime-signals.js +1 -1
  73. package/dist/core/search.js +19 -2
  74. package/dist/core/security-guard.js +207 -207
  75. package/dist/core/spawn-check.js +16 -2
  76. package/dist/core/staleness.js +1 -1
  77. package/dist/core/store-resolution.js +67 -11
  78. package/dist/core/worktree.js +18 -18
  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/PROTOCOL.md +1 -1
  91. package/docs/adapters/openclaw.md +43 -43
  92. package/docs/architecture/project-refs.md +328 -328
  93. package/docs/cli.md +2131 -2093
  94. package/docs/code-map.md +198 -0
  95. package/docs/concepts/coordination.md +52 -52
  96. package/docs/concepts/coordinator-runbook.md +129 -129
  97. package/docs/concepts/dispatch-lifecycle.md +245 -245
  98. package/docs/concepts/event-log-store.md +928 -928
  99. package/docs/concepts/ideation-loop.md +317 -317
  100. package/docs/concepts/loop-engine.md +520 -511
  101. package/docs/concepts/mcp-governance.md +268 -268
  102. package/docs/concepts/memory.md +84 -84
  103. package/docs/concepts/multi-agent-workflows.md +167 -167
  104. package/docs/concepts/observer-protocol.md +361 -361
  105. package/docs/concepts/plans-and-claims.md +217 -217
  106. package/docs/concepts/project-md-convention.md +35 -35
  107. package/docs/concepts/runtime-notes.md +38 -38
  108. package/docs/concepts/troubleshooting.md +254 -254
  109. package/docs/concepts/workspace-bootstrapping.md +142 -142
  110. package/docs/context-format-changelog.md +35 -35
  111. package/docs/context-format.md +48 -48
  112. package/docs/index.md +65 -65
  113. package/docs/integrations/agents.md +158 -158
  114. package/docs/integrations/claude-code.md +23 -23
  115. package/docs/integrations/cline.md +77 -77
  116. package/docs/integrations/continue.md +55 -55
  117. package/docs/integrations/copilot.md +68 -68
  118. package/docs/integrations/cursor.md +23 -23
  119. package/docs/integrations/kilocode.md +72 -72
  120. package/docs/integrations/mcp.md +385 -378
  121. package/docs/integrations/mistral-vibe.md +122 -122
  122. package/docs/integrations/openclaw.md +92 -92
  123. package/docs/integrations/opencode.md +84 -84
  124. package/docs/integrations/overview.md +115 -115
  125. package/docs/integrations/roo.md +71 -71
  126. package/docs/integrations/windsurf.md +77 -77
  127. package/docs/mcp-schema-changelog.md +364 -356
  128. package/docs/playbooks/integration/index.md +121 -121
  129. package/docs/playbooks/orchestration.md +37 -0
  130. package/docs/playbooks/productivity/index.md +99 -99
  131. package/docs/playbooks/team/index.md +117 -117
  132. package/docs/product/agent-first-model.md +184 -184
  133. package/docs/product/entity-model-audit.md +462 -462
  134. package/docs/product/positioning.md +86 -86
  135. package/docs/quickstart-existing-project.md +107 -107
  136. package/docs/quickstart.md +183 -183
  137. package/docs/release-maintenance.md +79 -79
  138. package/docs/reputation.md +52 -52
  139. package/docs/review.md +45 -45
  140. package/docs/security.md +212 -212
  141. package/docs/server-operations.md +118 -118
  142. package/docs/storage.md +106 -106
  143. package/package.json +86 -66
  144. package/docs/concepts/event-log-store-critique-A.md +0 -333
  145. package/docs/concepts/event-log-store-critique-B.md +0 -353
  146. package/docs/concepts/event-log-store-phase0-measurements.md +0 -58
  147. package/docs/concepts/event-log-store-proposal-A.md +0 -365
  148. package/docs/concepts/event-log-store-proposal-B.md +0 -404
  149. package/docs/concepts/identity-model-proposal.md +0 -371
@@ -6,7 +6,7 @@ import { buildContext } from '../core/context.js';
6
6
  import { buildExecutionContext, renderExecutionContextSummary } from '../core/execution-context.js';
7
7
  import { checkBrainclawInstallableUpdate, renderBrainclawInstallableUpdateNotice } from '../core/brainclaw-version.js';
8
8
  import { loadConfig } from '../core/config.js';
9
- import { loadAllSessions, loadCurrentSession, saveCurrentSession, gcStaleSessions } from '../core/identity.js';
9
+ import { loadAllSessions, loadCurrentSession, loadSessionById, saveCurrentSession, gcStaleSessions } from '../core/identity.js';
10
10
  import { loadState } from '../core/state.js';
11
11
  import { listArchivedCandidates, listCandidates, resolvedSource } from '../core/candidates.js';
12
12
  import { listClaims, assessClaimLiveness } from '../core/claims.js';
@@ -27,13 +27,13 @@ import { checkPolicy } from '../core/policy.js';
27
27
  import { buildGovernanceReport, renderGovernanceMarkdown } from '../core/governance.js';
28
28
  import { inferProjectFromTarget, loadInstructions, resolveInstructions } from '../core/instructions.js';
29
29
  import { buildReputationSnapshot, toPublicReputationSummary } from '../core/reputation.js';
30
- import { search } from '../core/search.js';
30
+ import { countLegacySearchMatches, search } from '../core/search.js';
31
31
  import { buildEstimationReport } from './estimation-report.js';
32
32
  import { runDoctor } from './doctor.js';
33
33
  import { buildProjectDiscovery, saveDiscoveryProfile, loadDiscoveryProfile, renderDiscoverySummary } from '../core/project-discovery.js';
34
34
  import { listCapabilities, listTools as listRegistryTools } from '../core/registries.js';
35
- import { listAvailableProjects, switchProject } from './switch.js';
36
- import { resolveEffectiveCwd } from '../core/store-resolution.js';
35
+ import { listAvailableProjectsForSession, switchProject } from './switch.js';
36
+ import { resolveEffectiveCwdInfo } from '../core/store-resolution.js';
37
37
  import { resolveProjectCwd } from '../core/cross-project.js';
38
38
  import { readUnseenEvents, buildNotificationSummary } from '../core/event-log.js';
39
39
  import { boundListResult, DEFAULT_FIND_CHAR_BUDGET } from '../core/entity-operations.js';
@@ -100,9 +100,12 @@ function getReviewAssignee(tags) {
100
100
  }
101
101
  export function handleMcpReadToolCall(name, args = {}, context = {}) {
102
102
  const baseCwd = context.cwd ?? process.cwd();
103
- let cwd = name === 'bclaw_switch'
104
- ? baseCwd
105
- : resolveEffectiveCwd({ baseCwd });
103
+ const effective = name === 'bclaw_switch'
104
+ ? { cwd: baseCwd, active_source: 'cwd', resolved_project: undefined }
105
+ : context.effectiveScope ?? resolveEffectiveCwdInfo({ baseCwd, sessionId: context.connectionSessionId });
106
+ let cwd = effective.cwd;
107
+ let activeSource = effective.active_source;
108
+ let resolvedProject = effective.resolved_project;
106
109
  // If a project param is provided, resolve it to an actual cwd override.
107
110
  // resolveProjectCwd unifies cross_project_links (siblings/peers) AND
108
111
  // workspace store-chain children. Throws on unknown project — surfaces
@@ -115,6 +118,14 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
115
118
  let projectRoutingApplied = false;
116
119
  if (targetProjectArg) {
117
120
  cwd = resolveProjectCwd(targetProjectArg, cwd);
121
+ activeSource = 'explicit';
122
+ try {
123
+ const config = loadConfig(cwd);
124
+ resolvedProject = { path: cwd, name: config.project_name };
125
+ }
126
+ catch {
127
+ resolvedProject = { path: cwd };
128
+ }
118
129
  projectRoutingApplied = true;
119
130
  }
120
131
  if (name === 'bclaw_get_context') {
@@ -631,13 +642,25 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
631
642
  }
632
643
  const offset = Math.max(0, Number(args.offset) || 0);
633
644
  const limit = typeof args.limit === 'number' ? args.limit : 10;
645
+ const includeLegacy = args.includeLegacy === true || (typeof args.filter === 'object' && args.filter?.includeLegacy === true);
634
646
  const allResults = search({
635
647
  query,
636
648
  section: (args.section ?? args.type),
637
649
  since: args.since,
638
- maxResults: offset + limit,
650
+ maxResults: Number.MAX_SAFE_INTEGER,
651
+ includeLegacy,
639
652
  cwd,
640
653
  });
654
+ const excludedLegacy = includeLegacy
655
+ ? 0
656
+ : countLegacySearchMatches({
657
+ query,
658
+ section: (args.section ?? args.type),
659
+ since: args.since,
660
+ maxResults: offset + limit,
661
+ includeLegacy: true,
662
+ cwd,
663
+ });
641
664
  const total = allResults.length;
642
665
  const page = allResults.slice(offset, offset + limit);
643
666
  // trp#449 class — bound the page by size (pln#542). budget_tokens tightens
@@ -647,16 +670,34 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
647
670
  const bounded = boundListResult({ entity: 'search_result', total, items: page }, offset, charBudget);
648
671
  const lines = bounded.items.map((result) => `[${result.id}] (${result.section}) score=${result.score.toFixed(2)}: ${result.text.slice(0, 120)}`);
649
672
  const nextActions = bounded.has_more
650
- ? [{ tool: 'bclaw_search', args: { query, offset: bounded.next_offset, limit }, when: 'to fetch the next page' }]
673
+ ? [{
674
+ tool: 'bclaw_search',
675
+ args: {
676
+ query,
677
+ offset: bounded.next_offset,
678
+ limit,
679
+ ...(args.project ? { project: args.project } : {}),
680
+ ...(args.section ? { section: args.section } : {}),
681
+ ...(args.type ? { type: args.type } : {}),
682
+ ...(args.since ? { since: args.since } : {}),
683
+ ...(args.budget_tokens ? { budget_tokens: args.budget_tokens } : {}),
684
+ ...(includeLegacy ? { includeLegacy: true } : {}),
685
+ },
686
+ when: 'to fetch the next page',
687
+ }]
651
688
  : [];
689
+ const legacyNote = excludedLegacy > 0 ? ` (${excludedLegacy} legacy result(s) excluded; pass includeLegacy=true to include them)` : '';
652
690
  return {
653
- content: [{ type: 'text', text: bounded.items.length > 0 ? lines.join('\n') : 'No results found.' }],
691
+ content: [{ type: 'text', text: bounded.items.length > 0 ? `${lines.join('\n')}${legacyNote}` : `No results found.${legacyNote}` }],
654
692
  structuredContent: {
655
693
  total,
656
694
  offset,
657
695
  limit,
658
696
  results: bounded.items,
659
697
  returned: bounded.returned,
698
+ excluded_legacy: excludedLegacy,
699
+ resolved_project: resolvedProject ?? { path: cwd },
700
+ active_source: activeSource,
660
701
  has_more: bounded.has_more,
661
702
  ...(bounded.next_offset !== undefined ? { next_offset: bounded.next_offset } : {}),
662
703
  ...(bounded.omitted_for_size ? { omitted_for_size: bounded.omitted_for_size } : {}),
@@ -1034,7 +1075,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1034
1075
  }
1035
1076
  catch { /* defensive: never block events query on reconcile failure */ }
1036
1077
  }
1037
- let events = queryRuntimeEvents({
1078
+ const events = queryRuntimeEvents({
1038
1079
  ...(id ? { id } : {}),
1039
1080
  ...(assignmentId ? { assignment_id: assignmentId } : {}),
1040
1081
  ...(runId ? { run_id: runId } : {}),
@@ -1422,7 +1463,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1422
1463
  if (name === 'bclaw_switch') {
1423
1464
  if (args.list === true) {
1424
1465
  try {
1425
- const result = listAvailableProjects(cwd);
1466
+ const result = listAvailableProjectsForSession(cwd, context.connectionSessionId);
1426
1467
  const lines = result.projects.map(p => {
1427
1468
  const marker = p.active ? '→' : ' ';
1428
1469
  const label = p.name ? `${p.name} (${p.relative_path})` : p.relative_path;
@@ -1439,7 +1480,9 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1439
1480
  }
1440
1481
  if (args.clear === true) {
1441
1482
  try {
1442
- const session = loadCurrentSession(cwd);
1483
+ const session = context.connectionSessionId
1484
+ ? loadSessionById(context.connectionSessionId, cwd)
1485
+ : loadCurrentSession(cwd);
1443
1486
  if (session?.active_project) {
1444
1487
  const { active_project: _removed, ...rest } = session;
1445
1488
  saveCurrentSession(rest, cwd);
@@ -1458,7 +1501,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1458
1501
  return createToolErrorResponse('validation_error', 'Missing required argument: project (or use list=true / clear=true)');
1459
1502
  }
1460
1503
  try {
1461
- const result = switchProject(projectRef, { cwd, sessionOnly: true });
1504
+ const result = switchProject(projectRef, { cwd, sessionOnly: true, sessionId: context.connectionSessionId });
1462
1505
  const text = `✔ Switched to ${result.name ? `"${result.name}"` : result.path} (${result.scope}-scoped)`;
1463
1506
  return {
1464
1507
  content: [{ type: 'text', text }],
@@ -39,7 +39,7 @@ import { createCapability, createTool as createRegistryTool } from '../core/regi
39
39
  import { detectAiAgent } from '../core/ai-agent-detection.js';
40
40
  import { checkGitPresence, scanGitRepos, parseRoots, parseRepoSelection, parseAgentSelection, getDetectedSetupAgentNames, getInstalledAgentNames, runGlobalInstall, initReposAndConfigureAgents, readSetupState, ALL_KNOWN_AGENTS, } from './setup.js';
41
41
  import { buildAgentInventory } from '../core/agent-inventory.js';
42
- import { resolveEffectiveCwd, resolveProjectRef, resolveTargetStore } from '../core/store-resolution.js';
42
+ import { resolveEffectiveCwd, resolveEffectiveCwdInfo, resolveProjectRef, resolveTargetStore } from '../core/store-resolution.js';
43
43
  import { assessBootstrapNeed, probeForQuickSetup, buildQuickSetupProbeResponse, buildOnboardingPreview, resolveEmptyMemoryRecommendation } from '../core/setup-flow.js';
44
44
  import { ensureUserStore, resolveHomeDir } from '../core/setup-state.js';
45
45
  import { createPlan, addStep as addStepOp, completeStep as completeStepOp, updateStep as updateStepOp, deleteStep as deleteStepOp, deletePlan as deletePlanOp } from '../core/operations/plan.js';
@@ -48,6 +48,7 @@ import { dispatch, dispatchReview, generateDispatchBrief } from '../core/dispatc
48
48
  import { deleteMemoryItem, updateMemoryItem } from '../core/operations/memory-mutation.js';
49
49
  import { assessMemoryPressure, buildCompactionTemplate, applyCompaction } from '../core/gc-semantic.js';
50
50
  import { WorkRequestSchema, CoordinateRequestSchema } from '../core/facade-schema.js';
51
+ import { codeMapWorkSection, codeMapRefreshNextActions } from '../core/code-map/work-section.js';
51
52
  import { getSpawnableAgents, getCapabilityProfile, buildInvokeCommand, validateAgentForDispatch } from '../core/agent-capability.js';
52
53
  import { attemptExecution } from '../core/execution.js';
53
54
  import { createAgentRun, transitionAgentRun } from '../core/agentruns.js';
@@ -167,6 +168,8 @@ export const MCP_READ_TOOLS = [
167
168
  type: { type: 'string', description: 'Filter by section: decisions, constraints, traps, handoffs, candidates, plans, sequences.' },
168
169
  section: { type: 'string', description: 'Filter by section (state, candidates, runtime).' },
169
170
  since: { type: 'string', description: 'Filter items created after this ISO date.' },
171
+ project: { type: 'string', description: 'Optional project name/path to search. Defaults to the active project.' },
172
+ includeLegacy: { type: 'boolean', description: 'Include records with provenance.kind="legacy" (default false). Response reports excluded_legacy when false.' },
170
173
  limit: { type: 'number', description: 'Maximum number of results to return (default 10).' },
171
174
  offset: { type: 'number', description: 'Number of results to skip (for pagination).' },
172
175
  budget_tokens: { type: 'number', description: 'Optional token budget for the result page (~4 chars/token). The page is size-bounded; has_more/next_offset advertise the rest.' },
@@ -447,8 +450,54 @@ export const MCP_READ_TOOLS = [
447
450
  required: ['target_id'],
448
451
  },
449
452
  },
453
+ {
454
+ name: 'bclaw_code_status',
455
+ description: 'Code Map status for this project: store presence, freshness badge (fresh / stale_changed_files / stale_extractor / stale_grammar / partial / missing_index), and index stats (files, nodes, edges). Read-only; never refreshes. Pair with bclaw_code_refresh when freshness is missing_index or stale.',
456
+ annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'auto' },
457
+ inputSchema: {
458
+ type: 'object',
459
+ properties: {},
460
+ },
461
+ },
462
+ {
463
+ name: 'bclaw_code_find',
464
+ description: 'Search the Code Map symbol index for a query (function/class/component/hook/type names). Returns ranked matches with path + score, plus a freshness_badge from the lazy read-path check. Read-only; never refreshes — a missing_index badge means run bclaw_code_refresh first.',
465
+ annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'auto' },
466
+ inputSchema: {
467
+ type: 'object',
468
+ properties: {
469
+ query: { type: 'string', description: 'Symbol or token to search for (e.g. "App", "useAuth", "dispatch").' },
470
+ limit: { type: 'number', description: 'Max matches to return.' },
471
+ },
472
+ required: ['query'],
473
+ },
474
+ },
475
+ {
476
+ name: 'bclaw_code_brief',
477
+ description: 'Before editing, ask Code Map what to read: returns a ranked suggested_files_to_read list (cap 12) for a symbol or path, related brainclaw memory (cap 5), and a freshness_badge. Read-only; never refreshes.',
478
+ annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'auto' },
479
+ inputSchema: {
480
+ type: 'object',
481
+ properties: {
482
+ target: { type: 'string', description: 'Symbol name or file path to build a reading brief for.' },
483
+ limit: { type: 'number', description: 'Max suggested files (hard-capped at 12 by the spec).' },
484
+ },
485
+ required: ['target'],
486
+ },
487
+ },
450
488
  ];
451
489
  const MCP_WRITE_TOOLS = [
490
+ {
491
+ name: 'bclaw_code_refresh',
492
+ description: 'Rebuild the Code Map index for this project (Tree-sitter parse + shards + indexes, behind the per-project lock). scope="changed" (default) reparses changed files; scope="all" does a full refresh + compaction. A live competing lock fails fast with a clear status — refresh never blocks. Returns the resulting freshness_badge.',
493
+ annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'prompt' },
494
+ inputSchema: {
495
+ type: 'object',
496
+ properties: {
497
+ scope: { type: 'string', enum: ['changed', 'all'], description: 'changed (default) reparses changed files only; all does a full refresh with orphan compaction.' },
498
+ },
499
+ },
500
+ },
452
501
  {
453
502
  name: 'bclaw_dispatch',
454
503
  description: 'Unified dispatch entry for sequence-lane parallelization (parallelize plans across lanes). To open a NEW review of a commit/branch, use `bclaw_coordinate(intent="review", open_loop=true, targetAgents=[…])` instead — bclaw_dispatch is for sequence-driven execution, NOT for opening new reviews. `intent` discriminator: analysis (sequence lane status, read-only), execute (default — analyze + generate briefs + send to agents), review (routes an EXISTING already-reflected handoff to a reviewer — only for handoffs produced by `session-end --reflect-handoff` or similar). Consolidates the legacy bclaw_dispatch_analysis / bclaw_dispatch / bclaw_dispatch_review. Returns FacadeResponse; for verification semantics see the same response-validation guidance documented on `bclaw_coordinate`.',
@@ -1782,6 +1831,32 @@ function explicitSessionIdFromEnv() {
1782
1831
  || process.env.CLAUDE_SESSION_ID?.trim()
1783
1832
  || process.env.COPILOT_SESSION_ID?.trim();
1784
1833
  }
1834
+ function projectInfoForCwd(cwd) {
1835
+ try {
1836
+ const config = loadConfig(cwd);
1837
+ return { path: cwd, name: config.project_name };
1838
+ }
1839
+ catch {
1840
+ return { path: cwd };
1841
+ }
1842
+ }
1843
+ function scopeMetadataForTarget(args, targetCwd, effectiveScope) {
1844
+ const hasExplicitProject = typeof args.project === 'string' && args.project.trim().length > 0;
1845
+ return {
1846
+ resolved_project: projectInfoForCwd(targetCwd),
1847
+ active_source: hasExplicitProject ? 'explicit' : effectiveScope.active_source,
1848
+ };
1849
+ }
1850
+ function renderProvenanceFilterNote(result) {
1851
+ const parts = [];
1852
+ if ((result.excluded_legacy ?? 0) > 0) {
1853
+ parts.push(`${result.excluded_legacy} legacy item(s) excluded (pass filter.includeLegacy=true to include them)`);
1854
+ }
1855
+ if ((result.excluded_low_confidence_auto_reflect ?? 0) > 0) {
1856
+ parts.push(`${result.excluded_low_confidence_auto_reflect} low-confidence auto_reflect item(s) excluded (lower filter.minAutoReflectConfidence to include them)`);
1857
+ }
1858
+ return parts.length > 0 ? parts.join('; ') : undefined;
1859
+ }
1785
1860
  export function parseMcpLine(line) {
1786
1861
  let parsed;
1787
1862
  try {
@@ -2557,6 +2632,11 @@ import { handleMcpReadToolCall } from './mcp-read-handlers.js';
2557
2632
  export { handleMcpReadToolCall };
2558
2633
  async function _executeMcpToolCallInner(payload) {
2559
2634
  const { name, args, cwd, connectionSessionId } = payload;
2635
+ const scopeInfo = payload.effectiveScope ?? {
2636
+ cwd,
2637
+ active_source: 'cwd',
2638
+ resolved_project: projectInfoForCwd(cwd),
2639
+ };
2560
2640
  try {
2561
2641
  if (isLegacyMcpToolFacadeDisabled(name)) {
2562
2642
  return { response: createLegacyMcpToolDisabledResponse() };
@@ -2566,9 +2646,62 @@ async function _executeMcpToolCallInner(payload) {
2566
2646
  const { handleCheckSecurity } = await import('./check-security-mcp.js');
2567
2647
  return { response: toolResponse(await handleCheckSecurity(args, cwd)) };
2568
2648
  }
2649
+ // Code Map tools (spec §9). These delegate to the async JsonlBackend, so
2650
+ // they are handled here rather than via the synchronous read-tool path.
2651
+ // status/find/brief are reads; refresh is a write (prompt approval).
2652
+ if (name === 'bclaw_code_status' || name === 'bclaw_code_find' || name === 'bclaw_code_brief' || name === 'bclaw_code_refresh') {
2653
+ const { JsonlBackend } = await import('../core/code-map/backend.js');
2654
+ const be = new JsonlBackend();
2655
+ if (name === 'bclaw_code_status') {
2656
+ const status = await be.status({ cwd });
2657
+ return {
2658
+ response: toolResponse({
2659
+ content: [{ type: 'text', text: `Code Map: ${status.store_exists ? 'store present' : 'no store'} — freshness=${status.freshness_badge.status}` }],
2660
+ structuredContent: { ...status, freshness_badge: status.freshness_badge },
2661
+ }),
2662
+ };
2663
+ }
2664
+ if (name === 'bclaw_code_refresh') {
2665
+ const scope = args.scope === 'all' ? 'all' : 'changed';
2666
+ const result = await be.refresh({ scope, cwd });
2667
+ return {
2668
+ response: toolResponse({
2669
+ content: [{ type: 'text', text: `Code Map refresh [${result.scope}]: ran=${result.ran} freshness=${result.freshness_badge.status}${result.lock_status ? ` (${result.lock_status})` : ''}` }],
2670
+ structuredContent: { ...result, freshness_badge: result.freshness_badge },
2671
+ }),
2672
+ };
2673
+ }
2674
+ if (name === 'bclaw_code_find') {
2675
+ const query = typeof args.query === 'string' ? args.query : '';
2676
+ if (!query.trim()) {
2677
+ return { response: createToolErrorResponse('validation_error', 'bclaw_code_find requires a non-empty query.') };
2678
+ }
2679
+ const limit = typeof args.limit === 'number' ? args.limit : undefined;
2680
+ const result = await be.find({ query, limit, cwd });
2681
+ return {
2682
+ response: toolResponse({
2683
+ content: [{ type: 'text', text: `Code Map find "${result.query}": ${result.matches.length} match(es), freshness=${result.freshness_badge.status}` }],
2684
+ structuredContent: { ...result, freshness_badge: result.freshness_badge },
2685
+ }),
2686
+ };
2687
+ }
2688
+ // bclaw_code_brief
2689
+ const target = typeof args.target === 'string' ? args.target : '';
2690
+ if (!target.trim()) {
2691
+ return { response: createToolErrorResponse('validation_error', 'bclaw_code_brief requires a non-empty target.') };
2692
+ }
2693
+ const limit = typeof args.limit === 'number' ? args.limit : undefined;
2694
+ const result = await be.brief({ target, limit, cwd });
2695
+ return {
2696
+ response: toolResponse({
2697
+ content: [{ type: 'text', text: `Code Map brief "${result.target}": ${result.suggested_files_to_read.length} file(s) to read, freshness=${result.freshness_badge.status}` }],
2698
+ structuredContent: { ...result, freshness_badge: result.freshness_badge },
2699
+ }),
2700
+ };
2701
+ }
2569
2702
  if (MCP_READ_TOOLS.some((tool) => tool.name === name) || LEGACY_READ_TOOL_HANDLERS.has(name)) {
2570
2703
  return {
2571
- response: appendLegacyMcpToolWarning(toolResponse(handleMcpReadToolCall(name, args, { cwd })), name),
2704
+ response: appendLegacyMcpToolWarning(toolResponse(handleMcpReadToolCall(name, args, { cwd, connectionSessionId, effectiveScope: scopeInfo })), name),
2572
2705
  };
2573
2706
  }
2574
2707
  // Resolve model once for all write operations
@@ -4821,6 +4954,26 @@ async function _executeMcpToolCallInner(payload) {
4821
4954
  nextActions.push({ tool: 'bclaw_context', args: { kind: 'memory' }, when: 'to read the full changed items behind context_diff' });
4822
4955
  }
4823
4956
  nextActions.push({ tool: 'bclaw_quick_capture', args: { text: '<finding>', type: '<decision|trap|constraint|note>' }, when: 'capture decisions/traps as you work' });
4957
+ // Code Map opt-in section (spec §10). Single live seam: returns null
4958
+ // (and does no work) unless the project's Code Map manifest carries
4959
+ // code_map_enabled:true. Never refreshes; bounded ≤2500ms on an active
4960
+ // lock; surfaces a missing_index hint or stale results with the badge.
4961
+ let codeMapSection = null;
4962
+ try {
4963
+ codeMapSection = await codeMapWorkSection(targetCwd, {
4964
+ query: workReq.scope ?? workReq.contextTarget,
4965
+ });
4966
+ }
4967
+ catch {
4968
+ // Best-effort: Code Map must never break or block bclaw_work.
4969
+ }
4970
+ // Code Map onboarding/freshness nudge (pln#588): promote the actionable
4971
+ // refresh to a first-class next_action so a fresh or stale project's first
4972
+ // bclaw_work TELLS the agent to build/update the index — the passive
4973
+ // missing_index hint alone was easy to skip. Still never refreshes here.
4974
+ for (const action of codeMapRefreshNextActions(codeMapSection)) {
4975
+ nextActions.push(action);
4976
+ }
4824
4977
  const facadeResponse = {
4825
4978
  status: 'ok',
4826
4979
  intent: workReq.intent,
@@ -4835,6 +4988,7 @@ async function _executeMcpToolCallInner(payload) {
4835
4988
  bootstrap_verdict: bootstrapVerdict,
4836
4989
  next_action: nextAction,
4837
4990
  next_actions: nextActions,
4991
+ ...(codeMapSection ? { code_map: codeMapSection } : {}),
4838
4992
  };
4839
4993
  const summaryParts = [`✔ bclaw_work [${workReq.intent}] session=${sessionResult.session_id}`];
4840
4994
  if (claimId)
@@ -6214,6 +6368,7 @@ async function _executeMcpToolCallInner(payload) {
6214
6368
  try {
6215
6369
  const entity = String(args.entity ?? '');
6216
6370
  const targetCwd = resolveProjectCwd(args.project, cwd);
6371
+ const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
6217
6372
  // pln#460 follow-up — some MCP clients (notably Claude Code with a
6218
6373
  // tool schema that declares `filter: { type: 'object' }` without a
6219
6374
  // sub-property schema) stringify the filter object before shipping
@@ -6269,10 +6424,10 @@ async function _executeMcpToolCallInner(payload) {
6269
6424
  const bounded = boundListResult(result, offset, charBudget);
6270
6425
  const warnings = collectLoadValidationWarnings(entity, targetCwd);
6271
6426
  const nextActions = [
6272
- { tool: 'bclaw_get', args: { entity, id: '<id from items>' }, when: 'to read one item in full' },
6427
+ { tool: 'bclaw_get', args: { entity, id: '<id from items>', ...(args.project ? { project: args.project } : {}), ...(args.budget_tokens ? { budget_tokens: args.budget_tokens } : {}) }, when: 'to read one item in full' },
6273
6428
  ];
6274
6429
  if (bounded.has_more) {
6275
- nextActions.push({ tool: 'bclaw_find', args: { entity, filter: { ...filter, offset: bounded.next_offset } }, when: 'to fetch the next page' });
6430
+ nextActions.push({ tool: 'bclaw_find', args: { entity, filter: { ...filter, offset: bounded.next_offset }, ...(args.project ? { project: args.project } : {}), ...(args.budget_tokens ? { budget_tokens: args.budget_tokens } : {}) }, when: 'to fetch the next page' });
6276
6431
  }
6277
6432
  // structuredContent is the canonical MCP return channel that clients
6278
6433
  // (VS Code extension, Codex, etc.) read for machine-parseable data.
@@ -6281,10 +6436,20 @@ async function _executeMcpToolCallInner(payload) {
6281
6436
  // `result.items` arrived as undefined on the client — the root cause
6282
6437
  // of the VS Code Backlog section rendering empty.
6283
6438
  const moreNote = bounded.has_more ? ` (returned ${bounded.returned}; ${result.total - bounded.returned} more — offset ${bounded.next_offset})` : '';
6439
+ const provenanceFilterNote = renderProvenanceFilterNote(result);
6440
+ const legacyNote = provenanceFilterNote
6441
+ ? `; ${provenanceFilterNote}`
6442
+ : '';
6284
6443
  return {
6285
6444
  response: toolResponse({
6286
- content: [{ type: 'text', text: `✔ ${result.total} ${entity} item(s)${moreNote}` }],
6287
- structuredContent: { ...bounded, warnings, next_actions: nextActions },
6445
+ content: [{ type: 'text', text: `✔ ${result.total} ${entity} item(s)${moreNote}${legacyNote}` }],
6446
+ structuredContent: {
6447
+ ...bounded,
6448
+ warnings,
6449
+ resolved_project: targetScope.resolved_project,
6450
+ active_source: targetScope.active_source,
6451
+ next_actions: nextActions,
6452
+ },
6288
6453
  }),
6289
6454
  };
6290
6455
  }
@@ -6361,6 +6526,7 @@ async function _executeMcpToolCallInner(payload) {
6361
6526
  }
6362
6527
  const rawData = (args.data ?? {});
6363
6528
  const targetCwd = resolveProjectCwd(args.project, cwd);
6529
+ const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
6364
6530
  // Auto-fill identity fields. Without this, a caller who omits author/agent
6365
6531
  // creates a schema-invalid record that is silently dropped on read and
6366
6532
  // GC'd from disk on the next mutation.
@@ -6392,7 +6558,11 @@ async function _executeMcpToolCallInner(payload) {
6392
6558
  return {
6393
6559
  response: toolResponse({
6394
6560
  content: [{ type: 'text', text: `✔ created ${entity} ${result.id}` }],
6395
- structuredContent: { ...result },
6561
+ structuredContent: {
6562
+ ...result,
6563
+ resolved_project: targetScope.resolved_project,
6564
+ active_source: targetScope.active_source,
6565
+ },
6396
6566
  }),
6397
6567
  };
6398
6568
  }
@@ -6406,13 +6576,18 @@ async function _executeMcpToolCallInner(payload) {
6406
6576
  const id = String(args.id ?? '');
6407
6577
  const patch = (args.patch ?? {});
6408
6578
  const targetCwd = resolveProjectCwd(args.project, cwd);
6579
+ const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
6409
6580
  const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
6410
6581
  const result = updateEntity(entity, id, patch, targetCwd);
6411
6582
  appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'update', item_id: id, item_type: entity }, targetCwd);
6412
6583
  return {
6413
6584
  response: toolResponse({
6414
6585
  content: [{ type: 'text', text: `✔ updated ${entity} ${id}` }],
6415
- structuredContent: { ...result },
6586
+ structuredContent: {
6587
+ ...result,
6588
+ resolved_project: targetScope.resolved_project,
6589
+ active_source: targetScope.active_source,
6590
+ },
6416
6591
  }),
6417
6592
  };
6418
6593
  }
@@ -6426,13 +6601,18 @@ async function _executeMcpToolCallInner(payload) {
6426
6601
  const id = String(args.id ?? '');
6427
6602
  const purge = args.purge === true;
6428
6603
  const targetCwd = resolveProjectCwd(args.project, cwd);
6604
+ const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
6429
6605
  const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
6430
6606
  const result = removeEntity(entity, id, targetCwd, purge);
6431
6607
  appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'delete', item_id: id, item_type: entity, reason: purge ? 'purged' : 'archived' }, targetCwd);
6432
6608
  return {
6433
6609
  response: toolResponse({
6434
6610
  content: [{ type: 'text', text: `✔ removed ${entity} ${id}` }],
6435
- structuredContent: { ...result },
6611
+ structuredContent: {
6612
+ ...result,
6613
+ resolved_project: targetScope.resolved_project,
6614
+ active_source: targetScope.active_source,
6615
+ },
6436
6616
  }),
6437
6617
  };
6438
6618
  }
@@ -6454,13 +6634,18 @@ async function _executeMcpToolCallInner(payload) {
6454
6634
  const to = String(args.to ?? '');
6455
6635
  const reason = args.reason;
6456
6636
  const targetCwd = resolveProjectCwd(args.project, cwd);
6637
+ const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
6457
6638
  const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
6458
6639
  const result = transitionEntity(entity, id, to, targetCwd, reason);
6459
6640
  appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'update', item_id: id, item_type: entity, reason: `transition ${result.from} → ${to}${reason ? ` (${reason})` : ''}` }, targetCwd);
6460
6641
  return {
6461
6642
  response: toolResponse({
6462
6643
  content: [{ type: 'text', text: `✔ ${entity} ${id}: ${result.from} → ${to}` }],
6463
- structuredContent: { ...result },
6644
+ structuredContent: {
6645
+ ...result,
6646
+ resolved_project: targetScope.resolved_project,
6647
+ active_source: targetScope.active_source,
6648
+ },
6464
6649
  }),
6465
6650
  };
6466
6651
  }
@@ -6504,9 +6689,10 @@ async function _executeMcpToolCallInner(payload) {
6504
6689
  */
6505
6690
  export async function executeMcpToolCall(payload) {
6506
6691
  const baseCwd = payload.cwd;
6507
- const cwd = payload.name === 'bclaw_switch'
6508
- ? baseCwd
6509
- : resolveEffectiveCwd({ baseCwd });
6692
+ const effective = payload.name === 'bclaw_switch'
6693
+ ? { cwd: baseCwd, active_source: 'cwd', resolved_project: undefined }
6694
+ : resolveEffectiveCwdInfo({ baseCwd, sessionId: payload.connectionSessionId });
6695
+ const cwd = effective.cwd;
6510
6696
  const envClaimId = process.env.BRAINCLAW_CLAIM_ID?.trim() || undefined;
6511
6697
  // ── Auto-session ────────────────────────────────────────────────────────────
6512
6698
  let autoSessionId;
@@ -6564,6 +6750,7 @@ export async function executeMcpToolCall(payload) {
6564
6750
  ...payload,
6565
6751
  cwd,
6566
6752
  connectionSessionId: effectiveConnectionSessionId,
6753
+ effectiveScope: effective,
6567
6754
  });
6568
6755
  // Apply legacy deprecation warning uniformly (Phase 3 slice 3g). Read tools
6569
6756
  // already get it at line 2560; write tools historically did not. This
@@ -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}`);