brainclaw 1.8.0 → 1.9.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 (178) hide show
  1. package/README.md +592 -505
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +138 -13
  4. package/dist/commands/add-step.js +1 -1
  5. package/dist/commands/bootstrap.js +2 -26
  6. package/dist/commands/check-security-mcp.js +50 -33
  7. package/dist/commands/check-security.js +86 -43
  8. package/dist/commands/claim.js +22 -21
  9. package/dist/commands/confirm.js +26 -0
  10. package/dist/commands/context-diff.js +1 -1
  11. package/dist/commands/dispatch-watch.js +142 -0
  12. package/dist/commands/doctor.js +113 -2
  13. package/dist/commands/estimation-report.js +115 -16
  14. package/dist/commands/harvest.js +286 -23
  15. package/dist/commands/hooks.js +73 -73
  16. package/dist/commands/init.js +124 -22
  17. package/dist/commands/install-hooks.js +78 -78
  18. package/dist/commands/loops-handlers.js +4 -0
  19. package/dist/commands/mcp-read-handlers.js +253 -41
  20. package/dist/commands/mcp.js +664 -102
  21. package/dist/commands/memory.js +21 -17
  22. package/dist/commands/migrate.js +81 -17
  23. package/dist/commands/prune.js +78 -4
  24. package/dist/commands/reflect.js +26 -20
  25. package/dist/commands/register-agent.js +57 -1
  26. package/dist/commands/repair.js +20 -0
  27. package/dist/commands/session-end.js +15 -6
  28. package/dist/commands/session-start.js +18 -1
  29. package/dist/commands/setup-security.js +39 -18
  30. package/dist/commands/setup.js +26 -27
  31. package/dist/commands/stale.js +16 -2
  32. package/dist/commands/switch.js +26 -5
  33. package/dist/commands/uninstall.js +126 -34
  34. package/dist/commands/update-step.js +6 -0
  35. package/dist/commands/version.js +1 -1
  36. package/dist/commands/worktree.js +60 -0
  37. package/dist/core/actions.js +12 -3
  38. package/dist/core/agent-capability.js +30 -17
  39. package/dist/core/agent-files.js +963 -666
  40. package/dist/core/agent-integrations.js +0 -3
  41. package/dist/core/agent-inventory.js +67 -0
  42. package/dist/core/agent-registry.js +163 -29
  43. package/dist/core/agentrun-reconciler.js +33 -2
  44. package/dist/core/agentruns.js +7 -1
  45. package/dist/core/ai-agent-detection.js +31 -44
  46. package/dist/core/archival.js +15 -9
  47. package/dist/core/assignment-reconciler.js +56 -0
  48. package/dist/core/assignment-sweeper.js +127 -4
  49. package/dist/core/assignments.js +69 -11
  50. package/dist/core/bootstrap.js +233 -67
  51. package/dist/core/brainclaw-version.js +22 -0
  52. package/dist/core/candidates.js +21 -1
  53. package/dist/core/claims.js +313 -150
  54. package/dist/core/codev-prompts.js +38 -38
  55. package/dist/core/config.js +6 -1
  56. package/dist/core/context-diff.js +148 -20
  57. package/dist/core/context.js +129 -8
  58. package/dist/core/coordination.js +22 -3
  59. package/dist/core/default-profiles/doctor.yaml +11 -11
  60. package/dist/core/default-profiles/janitor.yaml +11 -11
  61. package/dist/core/default-profiles/onboarder.yaml +11 -11
  62. package/dist/core/default-profiles/reviewer.yaml +13 -13
  63. package/dist/core/dispatch-status.js +79 -5
  64. package/dist/core/dispatcher.js +65 -12
  65. package/dist/core/entity-operations.js +74 -27
  66. package/dist/core/entity-registry.js +31 -5
  67. package/dist/core/event-log.js +138 -21
  68. package/dist/core/events/checkpoint.js +258 -0
  69. package/dist/core/events/genesis.js +220 -0
  70. package/dist/core/events/journal.js +507 -0
  71. package/dist/core/events/materialize.js +126 -0
  72. package/dist/core/events/registry-post-image.js +110 -0
  73. package/dist/core/events/verify.js +109 -0
  74. package/dist/core/execution-adapters.js +23 -0
  75. package/dist/core/execution.js +1 -1
  76. package/dist/core/facade-schema.js +38 -0
  77. package/dist/core/gc-semantic.js +130 -5
  78. package/dist/core/handoff-snapshot.js +68 -0
  79. package/dist/core/ids.js +19 -8
  80. package/dist/core/instruction-templates.js +34 -115
  81. package/dist/core/io.js +39 -3
  82. package/dist/core/json-store.js +10 -1
  83. package/dist/core/lock.js +153 -28
  84. package/dist/core/loops/bootstrap-acquire.js +25 -1
  85. package/dist/core/loops/facade-schema.js +2 -0
  86. package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
  87. package/dist/core/loops/index.js +1 -0
  88. package/dist/core/loops/presets/bootstrap.js +7 -0
  89. package/dist/core/loops/store.js +17 -0
  90. package/dist/core/loops/verbs.js +24 -2
  91. package/dist/core/markdown.js +8 -76
  92. package/dist/core/mcp-command-resolution.js +245 -0
  93. package/dist/core/memory-compactor.js +5 -3
  94. package/dist/core/memory-lifecycle.js +282 -0
  95. package/dist/core/merge-risk.js +150 -0
  96. package/dist/core/messaging.js +10 -3
  97. package/dist/core/migration.js +11 -1
  98. package/dist/core/observer-mode.js +26 -0
  99. package/dist/core/operations/memory-mutation.js +90 -65
  100. package/dist/core/operations/plan.js +27 -1
  101. package/dist/core/protocol-skills.js +210 -0
  102. package/dist/core/reflection-safety.js +6 -7
  103. package/dist/core/reputation.js +84 -2
  104. package/dist/core/runtime-signals.js +72 -10
  105. package/dist/core/runtime.js +84 -1
  106. package/dist/core/schema.js +114 -0
  107. package/dist/core/search.js +19 -2
  108. package/dist/core/security-detectors.js +125 -0
  109. package/dist/core/security-extract.js +189 -0
  110. package/dist/core/security-guard.js +217 -139
  111. package/dist/core/security-packages.js +121 -0
  112. package/dist/core/security-scoring.js +76 -9
  113. package/dist/core/security.js +34 -2
  114. package/dist/core/sequence.js +11 -2
  115. package/dist/core/setup-flow.js +141 -13
  116. package/dist/core/spawn-check.js +16 -2
  117. package/dist/core/staleness.js +73 -2
  118. package/dist/core/state.js +250 -54
  119. package/dist/core/store-resolution.js +45 -12
  120. package/dist/core/worktree.js +90 -26
  121. package/dist/facts.js +8 -8
  122. package/dist/facts.json +7 -7
  123. package/docs/PROTOCOL.md +223 -0
  124. package/docs/adapters/openclaw.md +43 -43
  125. package/docs/architecture/project-refs.md +328 -328
  126. package/docs/cli.md +2097 -2096
  127. package/docs/concepts/coordination.md +52 -52
  128. package/docs/concepts/coordinator-runbook.md +129 -0
  129. package/docs/concepts/dispatch-lifecycle.md +245 -245
  130. package/docs/concepts/event-log-store.md +928 -0
  131. package/docs/concepts/ideation-loop.md +317 -317
  132. package/docs/concepts/loop-engine.md +520 -511
  133. package/docs/concepts/mcp-governance.md +268 -268
  134. package/docs/concepts/memory.md +89 -88
  135. package/docs/concepts/multi-agent-workflows.md +167 -167
  136. package/docs/concepts/observer-protocol.md +361 -0
  137. package/docs/concepts/parallel-merge-protocol.md +71 -0
  138. package/docs/concepts/plans-and-claims.md +217 -174
  139. package/docs/concepts/project-md-convention.md +35 -35
  140. package/docs/concepts/runtime-notes.md +38 -38
  141. package/docs/concepts/skills.md +78 -0
  142. package/docs/concepts/troubleshooting.md +254 -254
  143. package/docs/concepts/workspace-bootstrapping.md +142 -81
  144. package/docs/context-format-changelog.md +35 -35
  145. package/docs/context-format.md +48 -48
  146. package/docs/index.md +65 -65
  147. package/docs/integrations/agents.md +162 -162
  148. package/docs/integrations/claude-code.md +23 -23
  149. package/docs/integrations/cline.md +87 -88
  150. package/docs/integrations/codex.md +2 -2
  151. package/docs/integrations/continue.md +60 -60
  152. package/docs/integrations/copilot.md +82 -80
  153. package/docs/integrations/cursor.md +23 -23
  154. package/docs/integrations/kilocode.md +72 -72
  155. package/docs/integrations/mcp.md +377 -377
  156. package/docs/integrations/mistral-vibe.md +122 -122
  157. package/docs/integrations/openclaw.md +99 -98
  158. package/docs/integrations/opencode.md +84 -84
  159. package/docs/integrations/overview.md +122 -122
  160. package/docs/integrations/roo.md +74 -74
  161. package/docs/integrations/windsurf.md +83 -83
  162. package/docs/mcp-schema-changelog.md +360 -329
  163. package/docs/playbooks/integration/index.md +121 -121
  164. package/docs/playbooks/orchestration.md +37 -0
  165. package/docs/playbooks/productivity/index.md +99 -99
  166. package/docs/playbooks/team/index.md +117 -117
  167. package/docs/product/agent-first-model.md +184 -184
  168. package/docs/product/entity-model-audit.md +462 -462
  169. package/docs/product/positioning.md +86 -86
  170. package/docs/quickstart-existing-project.md +107 -107
  171. package/docs/quickstart.md +148 -147
  172. package/docs/release-maintenance.md +79 -79
  173. package/docs/reputation.md +52 -52
  174. package/docs/review.md +45 -45
  175. package/docs/security.md +212 -53
  176. package/docs/server-operations.md +118 -118
  177. package/docs/storage.md +110 -108
  178. package/package.json +86 -69
@@ -1,18 +1,19 @@
1
1
  import { applyBootstrapImport, renderBootstrapInterview, renderBootstrapSummary, runBootstrapProfile, uninstallBootstrapImport } from '../core/bootstrap.js';
2
2
  import { buildAgentToolingContext, renderAgentToolingSummary } from '../core/agent-context.js';
3
- import { buildCoordinationSnapshot } from '../core/coordination.js';
3
+ import { buildCoordinationSnapshot, buildCrossProjectSnapshot } from '../core/coordination.js';
4
4
  import { scanDescendantPlans } from './list-plans.js';
5
5
  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';
13
13
  import { listAssignments } from '../core/assignments.js';
14
14
  import { listAgentRuns } from '../core/agentruns.js';
15
15
  import { reconcileAgentRun } from '../core/agentrun-reconciler.js';
16
+ import { isObserverMode } from '../core/observer-mode.js';
16
17
  import { getDispatchStatus } from '../core/dispatch-status.js';
17
18
  import { listActionRequired } from '../core/actions.js';
18
19
  import { queryRuntimeEvents } from '../core/events.js';
@@ -26,15 +27,17 @@ import { checkPolicy } from '../core/policy.js';
26
27
  import { buildGovernanceReport, renderGovernanceMarkdown } from '../core/governance.js';
27
28
  import { inferProjectFromTarget, loadInstructions, resolveInstructions } from '../core/instructions.js';
28
29
  import { buildReputationSnapshot, toPublicReputationSummary } from '../core/reputation.js';
29
- import { search } from '../core/search.js';
30
+ import { countLegacySearchMatches, search } from '../core/search.js';
30
31
  import { buildEstimationReport } from './estimation-report.js';
31
32
  import { runDoctor } from './doctor.js';
32
33
  import { buildProjectDiscovery, saveDiscoveryProfile, loadDiscoveryProfile, renderDiscoverySummary } from '../core/project-discovery.js';
33
34
  import { listCapabilities, listTools as listRegistryTools } from '../core/registries.js';
34
- import { listAvailableProjects, switchProject } from './switch.js';
35
- import { resolveEffectiveCwd } from '../core/store-resolution.js';
35
+ import { listAvailableProjectsForSession, switchProject } from './switch.js';
36
+ import { resolveEffectiveCwdInfo } from '../core/store-resolution.js';
36
37
  import { resolveProjectCwd } from '../core/cross-project.js';
37
38
  import { readUnseenEvents, buildNotificationSummary } from '../core/event-log.js';
39
+ import { boundListResult, DEFAULT_FIND_CHAR_BUDGET } from '../core/entity-operations.js';
40
+ import { handoffDiffPreviewNote } from '../core/handoff-snapshot.js';
38
41
  import { BootstrapInterviewAnswerSchema, AssignmentStatusSchema, AgentRunStatusSchema, AgentRunTransportSchema, ActionRequiredStatusSchema, ActionRequiredKindSchema } from '../core/schema.js';
39
42
  import { SCHEMA_VERSION, createToolErrorResponse, normaliseFormat, renderContextForMcp, } from './mcp.js';
40
43
  function normalizeBootstrapInterviewAnswersArg(value) {
@@ -60,6 +63,33 @@ function normalizeBootstrapInterviewAudienceArg(value) {
60
63
  }
61
64
  return 'any';
62
65
  }
66
+ /**
67
+ * trp#449 class (pln#542): bound an arbitrary object payload by repeatedly
68
+ * halving its largest array field until the serialized size fits the budget.
69
+ * Returns the bounded payload plus a per-field omitted count so the caller
70
+ * can advertise what was dropped.
71
+ */
72
+ function boundObjectArrays(payload, charBudget) {
73
+ const omitted = {};
74
+ let current = { ...payload };
75
+ while (JSON.stringify(current).length > charBudget) {
76
+ let largestKey;
77
+ let largestLen = 1;
78
+ for (const [key, value] of Object.entries(current)) {
79
+ if (Array.isArray(value) && value.length > largestLen) {
80
+ largestKey = key;
81
+ largestLen = value.length;
82
+ }
83
+ }
84
+ if (!largestKey)
85
+ break;
86
+ const arr = current[largestKey];
87
+ const newLen = Math.max(1, Math.floor(arr.length / 2));
88
+ omitted[largestKey] = (omitted[largestKey] ?? 0) + (arr.length - newLen);
89
+ current = { ...current, [largestKey]: arr.slice(0, newLen) };
90
+ }
91
+ return { payload: current, omitted };
92
+ }
63
93
  function getReviewAssignee(tags) {
64
94
  for (const tag of tags) {
65
95
  if (tag.startsWith('assignee:')) {
@@ -70,20 +100,38 @@ function getReviewAssignee(tags) {
70
100
  }
71
101
  export function handleMcpReadToolCall(name, args = {}, context = {}) {
72
102
  const baseCwd = context.cwd ?? process.cwd();
73
- let cwd = name === 'bclaw_switch'
74
- ? baseCwd
75
- : 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;
76
109
  // If a project param is provided, resolve it to an actual cwd override.
77
110
  // resolveProjectCwd unifies cross_project_links (siblings/peers) AND
78
111
  // workspace store-chain children. Throws on unknown project — surfaces
79
112
  // visibly as a tool error rather than silently falling back to the
80
113
  // current project, which would mislead the caller.
114
+ // Precedence rule: once routing succeeds, per-handler "project as filter"
115
+ // logic is skipped (routing already scopes to the right store) — pln#359.
81
116
  const projectArg = args.project;
82
117
  const targetProjectArg = name === 'bclaw_switch' ? undefined : projectArg;
118
+ let projectRoutingApplied = false;
83
119
  if (targetProjectArg) {
84
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
+ }
129
+ projectRoutingApplied = true;
85
130
  }
86
131
  if (name === 'bclaw_get_context') {
132
+ // pln#542: budget_tokens caps the relevance-ranked fill (~4 chars/token).
133
+ // Explicit maxChars wins when both are given.
134
+ const budgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
87
135
  const result = buildContext({
88
136
  target: args.path,
89
137
  project: targetProjectArg,
@@ -93,7 +141,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
93
141
  profile: args.profile,
94
142
  includePending: args.includePending,
95
143
  maxItems: args.maxItems,
96
- maxChars: args.maxChars,
144
+ maxChars: args.maxChars ?? (budgetTokens ? budgetTokens * 4 : undefined),
97
145
  digest: args.digest,
98
146
  sinceSession: args.since_session,
99
147
  bootstrap: args.bootstrap,
@@ -133,10 +181,22 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
133
181
  suggestions.push('\n💡 Tip: Use bclaw_get_capabilities, bclaw_list_tools, or bclaw_search_tools for detailed discovery');
134
182
  enrichedContent = content + suggestions.join('\n');
135
183
  }
136
- // Check for unseen events from other agents
137
- const agentName = args.agent ?? resolveCurrentAgentName(cwd);
138
- const unseenEvents = readUnseenEvents(agentName, cwd);
139
- const notifications = buildNotificationSummary(unseenEvents);
184
+ // Unseen-event notifications. When buildContext already computed the
185
+ // event-cursor diff (the converged novelty mechanism, pln#542), reuse its
186
+ // histogram the cursor was advanced by that read, so a second
187
+ // readUnseenEvents would observe nothing.
188
+ let notifications;
189
+ let unseenEventCount;
190
+ if (result.context_diff?.source === 'event_cursor') {
191
+ notifications = result.context_diff.event_summary;
192
+ unseenEventCount = result.context_diff.unseen_event_count;
193
+ }
194
+ else {
195
+ const agentName = args.agent ?? resolveCurrentAgentName(cwd);
196
+ const unseenEvents = readUnseenEvents(agentName, cwd);
197
+ notifications = buildNotificationSummary(unseenEvents);
198
+ unseenEventCount = unseenEvents.length;
199
+ }
140
200
  return {
141
201
  content: [{ type: 'text', text: enrichedContent || 'No relevant memory found.' }],
142
202
  structuredContent: {
@@ -151,7 +211,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
151
211
  name: tool.name,
152
212
  type: tool.type,
153
213
  })),
154
- ...(notifications ? { pending_notifications: notifications, unseen_event_count: unseenEvents.length } : {}),
214
+ ...(notifications ? { pending_notifications: notifications, unseen_event_count: unseenEventCount } : {}),
155
215
  },
156
216
  };
157
217
  }
@@ -197,12 +257,38 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
197
257
  lines.push(`Suggestion: ${suggestion}`);
198
258
  }
199
259
  }
200
- if (handoff.snapshot?.diff) {
201
- lines.push('', 'Uncommitted Git Diff:', handoff.snapshot.diff);
260
+ // trp#449 class (pln#542): the embedded git diff is unbounded — cap it so
261
+ // a large handoff snapshot never overflows the MCP token budget.
262
+ // budget_tokens tightens the cap (~4 chars/token).
263
+ const handoffBudgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
264
+ const diffCharBudget = handoffBudgetTokens ? Math.min(handoffBudgetTokens * 4, DEFAULT_FIND_CHAR_BUDGET) : DEFAULT_FIND_CHAR_BUDGET;
265
+ let boundedHandoff = handoff;
266
+ let diffTruncated = false;
267
+ if (handoff.snapshot?.diff && handoff.snapshot.diff.length > diffCharBudget) {
268
+ diffTruncated = true;
269
+ boundedHandoff = {
270
+ ...handoff,
271
+ snapshot: {
272
+ ...handoff.snapshot,
273
+ diff: `${handoff.snapshot.diff.slice(0, diffCharBudget)}\n… [diff truncated to ${diffCharBudget} chars — read the worktree branch for the full diff]`,
274
+ },
275
+ };
276
+ }
277
+ if (boundedHandoff.snapshot?.diff) {
278
+ lines.push('', 'Uncommitted Git Diff:', boundedHandoff.snapshot.diff);
279
+ // pln#569 — the inline diff is a capped preview when a digest is present;
280
+ // tell the reader the full diff lives on the worktree branch.
281
+ const note = handoffDiffPreviewNote(boundedHandoff.snapshot);
282
+ if (!diffTruncated && note)
283
+ lines.push(note);
202
284
  }
203
285
  return {
204
286
  content: [{ type: 'text', text: lines.join('\n') }],
205
- structuredContent: { handoff, schema_version: SCHEMA_VERSION },
287
+ structuredContent: {
288
+ handoff: boundedHandoff,
289
+ ...(diffTruncated ? { diff_truncated: true } : {}),
290
+ schema_version: SCHEMA_VERSION,
291
+ },
206
292
  };
207
293
  }
208
294
  if (name === 'bclaw_bootstrap') {
@@ -210,6 +296,23 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
210
296
  if (args.apply && args.uninstall) {
211
297
  throw new Error('bclaw_bootstrap does not allow apply and uninstall at the same time.');
212
298
  }
299
+ // Mirror the CLI confirmAction gate: apply/uninstall mutate canonical
300
+ // memory, so they refuse without an explicit yes:true instead of relying
301
+ // on the host honouring the headlessApproval annotation.
302
+ if ((args.apply || args.uninstall) && args.yes !== true) {
303
+ const action = args.uninstall ? 'uninstall' : 'apply';
304
+ return {
305
+ content: [{
306
+ type: 'text',
307
+ text: `bclaw_bootstrap ${action} modifies canonical memory and requires explicit confirmation. Confirm with the user, then re-call with yes: true. No changes were made.`,
308
+ }],
309
+ structuredContent: {
310
+ confirmation_required: true,
311
+ action,
312
+ schema_version: SCHEMA_VERSION,
313
+ },
314
+ };
315
+ }
213
316
  if (args.uninstall) {
214
317
  const result = uninstallBootstrapImport(cwd);
215
318
  const text = !result.receipt
@@ -371,11 +474,34 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
371
474
  return !plan || plan.status === 'todo';
372
475
  }).length
373
476
  : 0;
477
+ // pln#559 step 3 — attention_required is now the FULL composite the
478
+ // Attention section already displays in the extension: pending actions
479
+ // + non-auto (human-review) candidates + blocked assignments + stale
480
+ // runs. The 2026-06-10 calibration: the badge previously read only
481
+ // pendingActions and chronically undercounted while the Attention
482
+ // section showed N >> badge. The badge must NEVER be smaller than the
483
+ // section header it represents.
484
+ const pendingCandidates = listCandidates('pending', cwd);
485
+ const pendingHumanCandidates = pendingCandidates.filter((c) => resolvedSource(c) !== 'auto');
486
+ const allAssignments = listAssignments(cwd);
487
+ const blockedAssignments = allAssignments.filter((a) => a.status === 'blocked');
488
+ const allRuns = listAgentRuns(cwd);
489
+ const staleRuns = allRuns.filter((r) => r.status === 'blocked' || r.status === 'waiting_input' || r.status === 'failed');
490
+ const attentionRequiredComposite = pendingActions.length +
491
+ pendingHumanCandidates.length +
492
+ blockedAssignments.length +
493
+ staleRuns.length;
374
494
  const summary = {
375
495
  project_id: config.project_id,
376
496
  agent,
377
497
  current_host: currentHost,
378
- attention_required: pendingActions.length,
498
+ attention_required: attentionRequiredComposite,
499
+ attention_breakdown: {
500
+ pending_actions: pendingActions.length,
501
+ pending_human_candidates: pendingHumanCandidates.length,
502
+ blocked_assignments: blockedAssignments.length,
503
+ stale_runs: staleRuns.length,
504
+ },
379
505
  in_progress: activeClaims.length,
380
506
  plans: {
381
507
  in_progress: state.plan_items.filter((p) => p.status === 'in_progress').length,
@@ -491,9 +617,22 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
491
617
  lines.push(`- ${other.name}: ${other.claim_count} claim(s) on ${other.scopes.join(', ')}`);
492
618
  }
493
619
  }
620
+ // trp#449 class (pln#542): the board aggregates many unbounded arrays —
621
+ // bound the structured payload by size. budget_tokens tightens the cap.
622
+ const boardBudgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
623
+ const boardCharBudget = boardBudgetTokens ? Math.min(boardBudgetTokens * 4, DEFAULT_FIND_CHAR_BUDGET) : DEFAULT_FIND_CHAR_BUDGET;
624
+ const { payload: boundedBoard, omitted } = boundObjectArrays(board, boardCharBudget);
494
625
  return {
495
626
  content: [{ type: 'text', text: lines.join('\n') }],
496
- structuredContent: { ...board },
627
+ structuredContent: {
628
+ ...boundedBoard,
629
+ ...(Object.keys(omitted).length > 0
630
+ ? {
631
+ omitted_for_size: omitted,
632
+ hint: `Payload size-bounded: ${Object.entries(omitted).map(([k, n]) => `${n} ${k}`).join(', ')} omitted. Use bclaw_find(entity=…) with filters to read the full lists.`,
633
+ }
634
+ : {}),
635
+ },
497
636
  };
498
637
  }
499
638
  if (name === 'bclaw_search') {
@@ -503,19 +642,68 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
503
642
  }
504
643
  const offset = Math.max(0, Number(args.offset) || 0);
505
644
  const limit = typeof args.limit === 'number' ? args.limit : 10;
645
+ const includeLegacy = args.includeLegacy === true || (typeof args.filter === 'object' && args.filter?.includeLegacy === true);
506
646
  const allResults = search({
507
647
  query,
508
648
  section: (args.section ?? args.type),
509
649
  since: args.since,
510
- maxResults: offset + limit,
650
+ maxResults: Number.MAX_SAFE_INTEGER,
651
+ includeLegacy,
511
652
  cwd,
512
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
+ });
513
664
  const total = allResults.length;
514
665
  const page = allResults.slice(offset, offset + limit);
515
- const lines = page.map((result) => `[${result.id}] (${result.section}) score=${result.score.toFixed(2)}: ${result.text.slice(0, 120)}`);
666
+ // trp#449 class — bound the page by size (pln#542). budget_tokens tightens
667
+ // the cap (~4 chars/token); the default mirrors bclaw_find's budget.
668
+ const budgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
669
+ const charBudget = budgetTokens ? Math.min(budgetTokens * 4, DEFAULT_FIND_CHAR_BUDGET) : DEFAULT_FIND_CHAR_BUDGET;
670
+ const bounded = boundListResult({ entity: 'search_result', total, items: page }, offset, charBudget);
671
+ const lines = bounded.items.map((result) => `[${result.id}] (${result.section}) score=${result.score.toFixed(2)}: ${result.text.slice(0, 120)}`);
672
+ const nextActions = bounded.has_more
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
+ }]
688
+ : [];
689
+ const legacyNote = excludedLegacy > 0 ? ` (${excludedLegacy} legacy result(s) excluded; pass includeLegacy=true to include them)` : '';
516
690
  return {
517
- content: [{ type: 'text', text: page.length > 0 ? lines.join('\n') : 'No results found.' }],
518
- structuredContent: { total, offset, limit, results: page },
691
+ content: [{ type: 'text', text: bounded.items.length > 0 ? `${lines.join('\n')}${legacyNote}` : `No results found.${legacyNote}` }],
692
+ structuredContent: {
693
+ total,
694
+ offset,
695
+ limit,
696
+ results: bounded.items,
697
+ returned: bounded.returned,
698
+ excluded_legacy: excludedLegacy,
699
+ resolved_project: resolvedProject ?? { path: cwd },
700
+ active_source: activeSource,
701
+ has_more: bounded.has_more,
702
+ ...(bounded.next_offset !== undefined ? { next_offset: bounded.next_offset } : {}),
703
+ ...(bounded.omitted_for_size ? { omitted_for_size: bounded.omitted_for_size } : {}),
704
+ ...(bounded.hint ? { hint: bounded.hint } : {}),
705
+ next_actions: nextActions,
706
+ },
519
707
  };
520
708
  }
521
709
  if (name === 'bclaw_estimation_report') {
@@ -563,7 +751,10 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
563
751
  const assignee = String(args.assignee).toLowerCase();
564
752
  plans = plans.filter((plan) => plan.assignee?.toLowerCase() === assignee);
565
753
  }
566
- if (args.project) {
754
+ if (args.project && !projectRoutingApplied) {
755
+ // Only filter by plan.project metadata when routing was NOT applied.
756
+ // When routing succeeded, cwd is already scoped to the target project store,
757
+ // so filtering by plan.project label would incorrectly narrow results.
567
758
  const project = String(args.project).toLowerCase();
568
759
  plans = plans.filter((plan) => plan.project?.toLowerCase() === project);
569
760
  }
@@ -863,23 +1054,28 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
863
1054
  // committed but never called bclaw_assignment_update) and surfaces
864
1055
  // delivered_but_unverified for spawns past the 60s grace with no
865
1056
  // life-sign — see runtime_note run_77e65e77 for the empirical case.
866
- try {
867
- if (runId) {
868
- reconcileAgentRun(runId, cwd);
869
- }
870
- else if (assignmentId) {
871
- for (const run of listAgentRuns(cwd, { assignment_id: assignmentId })) {
872
- reconcileAgentRun(run.id, cwd);
1057
+ // Observer mode (BRAINCLAW_OBSERVER=1) suppresses this pre-read sweep —
1058
+ // a dashboard fetching assignment events must not be allowed to transition
1059
+ // agent_run records as a side effect of the read.
1060
+ if (!isObserverMode()) {
1061
+ try {
1062
+ if (runId) {
1063
+ reconcileAgentRun(runId, cwd);
873
1064
  }
874
- }
875
- else if (claimId) {
876
- for (const run of listAgentRuns(cwd, { claim_id: claimId })) {
877
- reconcileAgentRun(run.id, cwd);
1065
+ else if (assignmentId) {
1066
+ for (const run of listAgentRuns(cwd, { assignment_id: assignmentId })) {
1067
+ reconcileAgentRun(run.id, cwd);
1068
+ }
1069
+ }
1070
+ else if (claimId) {
1071
+ for (const run of listAgentRuns(cwd, { claim_id: claimId })) {
1072
+ reconcileAgentRun(run.id, cwd);
1073
+ }
878
1074
  }
879
1075
  }
1076
+ catch { /* defensive: never block events query on reconcile failure */ }
880
1077
  }
881
- catch { /* defensive: never block events query on reconcile failure */ }
882
- let events = queryRuntimeEvents({
1078
+ const events = queryRuntimeEvents({
883
1079
  ...(id ? { id } : {}),
884
1080
  ...(assignmentId ? { assignment_id: assignmentId } : {}),
885
1081
  ...(runId ? { run_id: runId } : {}),
@@ -1267,7 +1463,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1267
1463
  if (name === 'bclaw_switch') {
1268
1464
  if (args.list === true) {
1269
1465
  try {
1270
- const result = listAvailableProjects(cwd);
1466
+ const result = listAvailableProjectsForSession(cwd, context.connectionSessionId);
1271
1467
  const lines = result.projects.map(p => {
1272
1468
  const marker = p.active ? '→' : ' ';
1273
1469
  const label = p.name ? `${p.name} (${p.relative_path})` : p.relative_path;
@@ -1284,7 +1480,9 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1284
1480
  }
1285
1481
  if (args.clear === true) {
1286
1482
  try {
1287
- const session = loadCurrentSession(cwd);
1483
+ const session = context.connectionSessionId
1484
+ ? loadSessionById(context.connectionSessionId, cwd)
1485
+ : loadCurrentSession(cwd);
1288
1486
  if (session?.active_project) {
1289
1487
  const { active_project: _removed, ...rest } = session;
1290
1488
  saveCurrentSession(rest, cwd);
@@ -1303,7 +1501,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1303
1501
  return createToolErrorResponse('validation_error', 'Missing required argument: project (or use list=true / clear=true)');
1304
1502
  }
1305
1503
  try {
1306
- const result = switchProject(projectRef, { cwd, sessionOnly: true });
1504
+ const result = switchProject(projectRef, { cwd, sessionOnly: true, sessionId: context.connectionSessionId });
1307
1505
  const text = `✔ Switched to ${result.name ? `"${result.name}"` : result.path} (${result.scope}-scoped)`;
1308
1506
  return {
1309
1507
  content: [{ type: 'text', text }],
@@ -1601,6 +1799,17 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1601
1799
  return handleMcpReadToolCall('bclaw_get_agent_board', args, context);
1602
1800
  case 'board_summary':
1603
1801
  return handleMcpReadToolCall('bclaw_get_agent_board_summary', args, context);
1802
+ case 'cross_project': {
1803
+ // pln#558 step 3 — lightweight endpoint for the VS Code extension's
1804
+ // SYSTEM section: returns linked_projects + incoming_signals only,
1805
+ // so the dashboard no longer pulls the full coordination snapshot
1806
+ // just to render two summary lists.
1807
+ const snap = buildCrossProjectSnapshot(cwd);
1808
+ return {
1809
+ content: [{ type: 'text', text: JSON.stringify(snap, null, 2) }],
1810
+ structuredContent: snap,
1811
+ };
1812
+ }
1604
1813
  case 'delta': {
1605
1814
  const since = args.since;
1606
1815
  if (typeof since !== 'string' || !since) {
@@ -1609,7 +1818,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1609
1818
  return handleMcpReadToolCall('bclaw_get_context', { ...args, since_session: since }, context);
1610
1819
  }
1611
1820
  default:
1612
- throw new Error(`bclaw_context: unknown kind '${kind}'. Expected memory | execution | board | board_summary | delta.`);
1821
+ throw new Error(`bclaw_context: unknown kind '${kind}'. Expected memory | execution | board | board_summary | cross_project | delta.`);
1613
1822
  }
1614
1823
  }
1615
1824
  if (name === 'bclaw_get_thread') {
@@ -1635,11 +1844,13 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1635
1844
  }
1636
1845
  const tailLogLines = typeof args.tail_log_lines === 'number' ? args.tail_log_lines : undefined;
1637
1846
  const stallThresholdMs = typeof args.stall_threshold_ms === 'number' ? args.stall_threshold_ms : undefined;
1847
+ const baseRef = typeof args.base_ref === 'string' && args.base_ref ? args.base_ref : undefined;
1638
1848
  const status = getDispatchStatus({
1639
1849
  target_id: targetId,
1640
1850
  cwd,
1641
1851
  tail_log_lines: tailLogLines,
1642
1852
  stall_threshold_ms: stallThresholdMs,
1853
+ base_ref: baseRef,
1643
1854
  });
1644
1855
  // Text view: short, single-screen summary so an agent can decide what to do
1645
1856
  // without parsing the structured payload.
@@ -1658,6 +1869,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1658
1869
  `Runtime: pid=${status.runtime.pid ?? '-'} alive=${status.runtime.pid_alive ?? 'unknown'} ack=${status.runtime.ack_file.exists}`,
1659
1870
  ` stdout: ${status.runtime.log_files.stdout?.exists ? `${status.runtime.log_files.stdout.size_bytes}B` : 'absent'}`,
1660
1871
  ` stderr: ${status.runtime.log_files.stderr?.exists ? `${status.runtime.log_files.stderr.size_bytes}B` : 'absent'}`,
1872
+ ` git: commits_ahead=${status.runtime.commits_ahead ?? 'n/a'} dirty_tracked=${status.runtime.dirty_tracked ?? 'n/a'}`,
1661
1873
  ];
1662
1874
  return {
1663
1875
  content: [{ type: 'text', text: lines.join('\n') }],