brainclaw 1.28.3 → 1.28.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +6 -0
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-code-map.js +1 -0
  4. package/dist/commands/code-map.js +5 -1
  5. package/dist/commands/harvest.js +67 -25
  6. package/dist/commands/loops-handlers.js +28 -1
  7. package/dist/commands/mcp-catalog.js +32 -4
  8. package/dist/commands/mcp-read-handlers.js +15 -1
  9. package/dist/commands/mcp-schemas.generated.js +13 -0
  10. package/dist/commands/mcp-write-coordination.js +84 -22
  11. package/dist/commands/mcp-write-memory.js +87 -1
  12. package/dist/commands/mcp.js +60 -11
  13. package/dist/commands/switch.js +8 -1
  14. package/dist/core/code-map/backend.js +19 -5
  15. package/dist/core/code-map/refresh-jobs.js +158 -0
  16. package/dist/core/code-map/refresh-worker.js +12 -0
  17. package/dist/core/context.js +16 -3
  18. package/dist/core/dispatch-status.js +36 -14
  19. package/dist/core/dispatcher.js +28 -20
  20. package/dist/core/entity-operations.js +62 -4
  21. package/dist/core/entity-registry.js +3 -3
  22. package/dist/core/execution-adapters.js +10 -0
  23. package/dist/core/facade-schema.js +10 -0
  24. package/dist/core/ideation-loop-close.js +3 -1
  25. package/dist/core/lane-result-file.js +72 -0
  26. package/dist/core/loop-turn-dispatch.js +2 -0
  27. package/dist/core/loops/brief-assembly.js +19 -11
  28. package/dist/core/loops/next-expected.js +56 -1
  29. package/dist/core/loops/reconcile-turn.js +8 -0
  30. package/dist/core/loops/result-reducers.js +14 -12
  31. package/dist/core/loops/store.js +4 -0
  32. package/dist/core/loops/types.js +14 -2
  33. package/dist/core/loops/verbs.js +8 -1
  34. package/dist/core/loops/worker-reply-contract.js +1 -1
  35. package/dist/core/protocol-tool-policy.js +1 -0
  36. package/dist/core/review-loop-turn-dispatch.js +1 -0
  37. package/dist/core/schema.js +24 -1
  38. package/dist/core/search.js +3 -2
  39. package/dist/core/worktree.js +14 -7
  40. package/dist/facts.js +14 -13
  41. package/dist/facts.json +13 -12
  42. package/docs/cli.md +37 -4
  43. package/docs/code-map.md +30 -18
  44. package/docs/concepts/ideation-loop.md +35 -14
  45. package/docs/integrations/mcp.md +17 -7
  46. package/docs/mcp-schema-changelog.md +63 -6
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -282,6 +282,12 @@ per-phase memory filters. The shared controls are `open`, `turn`,
282
282
  implementation also adds `bind` and `verify`. `request_input` /
283
283
  `provide_input` are cross-cutting clarification primitives for any workflow.
284
284
 
285
+ Ideation is sequential and conversational by default: critic B sees and
286
+ challenges critic A's contribution, critic C sees both, then the champion
287
+ revises and the ordered participants run another bounded round. The three
288
+ critic slots may all use the same installed agent with distinct persisted
289
+ perspectives; `ideation_schedule="parallel"` is an explicit fan-out option.
290
+
285
291
  Every worker-backed phase, across all five workflows, is launched from one
286
292
  immutable [execution contract](docs/concepts/execution-contract.md): exact
287
293
  identity, artifact expectations, workspace policy, capability snapshot and
Binary file
@@ -7,6 +7,7 @@ export function registerCodeMapCommands(program) {
7
7
  .option('--json', 'Output as JSON')
8
8
  .option('--all', 'For refresh: enumerate all supported files (full refresh)')
9
9
  .option('--changed', 'For refresh: only changed files (default)')
10
+ .option('--scope <scope>', 'For refresh: changed or all (same selector as MCP)')
10
11
  .option('--cascade', 'For refresh/status in a multi-project workspace: cascade across every nested project (each gets its own store; the root store is scoped to files no child owns)')
11
12
  .option('--limit <n>', 'Max results for find/brief/impact/outline', (v) => parseInt(v, 10))
12
13
  .option('--depth <n>', 'For impact/export: maximum graph depth (export is hard-capped at 4)', (v) => parseInt(v, 10))
@@ -27,7 +27,10 @@ export async function runCodeMap(subcommand, args, options = {}) {
27
27
  return;
28
28
  }
29
29
  if (normalized === 'refresh') {
30
- const scope = options.all ? 'all' : 'changed';
30
+ if (options.scope && options.scope !== 'all' && options.scope !== 'changed') {
31
+ throw new Error('code-map refresh --scope must be "changed" or "all".');
32
+ }
33
+ const scope = options.scope === 'all' || options.all ? 'all' : 'changed';
31
34
  const result = await be.refresh({ scope, cwd, cascade: options.cascade });
32
35
  printRefresh(result, options);
33
36
  return;
@@ -108,6 +111,7 @@ function printStatus(status, options) {
108
111
  }
109
112
  console.log('Code Map status');
110
113
  console.log(` Store: ${status.store_exists ? 'present' : 'absent'}`);
114
+ console.log(` Index: ${status.index_exists ? 'ready' : status.index_manifest_exists ? 'invalid manifest' : 'not built'}`);
111
115
  console.log(` Root: ${status.resolution.project_root}`);
112
116
  console.log(` Path: ${status.resolution.store_path}`);
113
117
  console.log(` ${badgeLine(status.freshness_badge)}`);
@@ -13,7 +13,7 @@
13
13
  import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import { spawnSync } from 'node:child_process';
16
- import { CandidateSchema, LaneResultSchema } from '../core/schema.js';
16
+ import { CandidateSchema } from '../core/schema.js';
17
17
  import { gitEvidence } from '../core/dispatch-status.js';
18
18
  import { listCandidates, listArchivedCandidates, saveCandidate } from '../core/candidates.js';
19
19
  import { createRuntimeEvent } from '../core/events.js';
@@ -26,11 +26,14 @@ import { closeReviewLoopFromLaneResult } from '../core/review-loop-close.js';
26
26
  import { closeIdeationLoopFromLaneResult } from '../core/ideation-loop-close.js';
27
27
  import { dispatchReviewLoopTurn, turnOwnedLoopEnabled } from '../core/review-loop-turn-dispatch.js';
28
28
  import { reconcileTurnOwnedLane, turnOwnedLaneEvidence } from '../core/loops/reconcile-turn.js';
29
+ import { findReservationByAssignmentId } from '../core/loops/attempt-reservation.js';
29
30
  import { getLoop } from '../core/loops/store.js';
31
+ import { computeNextExpected } from '../core/loops/next-expected.js';
30
32
  import { phasePolicy } from '../core/loops/kind-policies.js';
31
33
  import { reconcileClaimConformity } from '../core/claim-conformity.js';
32
34
  import { toWarningDetail } from '../core/warnings.js';
33
35
  import { harvestHarnessObservation } from '../core/harness-adapters/index.js';
36
+ import { LANE_RESULT_FILENAME, resolveLaneResultFile } from '../core/lane-result-file.js';
34
37
  /**
35
38
  * pln#630 PR3a — finalize a TURN-OWNED review lane via the exactly-once `reconcileTurn`
36
39
  * instead of the legacy `closeReviewLoopFromLaneResult`. Returns `undefined` for a legacy
@@ -349,7 +352,7 @@ export function runHarvestCandidates(options = {}) {
349
352
  // ─────────────────────────────────────────────────────────────────────────────
350
353
  /** Conventional path of a worker's lane-result file at the worktree root. */
351
354
  export function getLaneResultPath(worktreePath) {
352
- return path.join(worktreePath, 'LANE-RESULT.json');
355
+ return path.join(worktreePath, LANE_RESULT_FILENAME);
353
356
  }
354
357
  /** Idempotency marker so a lane-result is harvested once. */
355
358
  function laneHarvestedMarkerPath(cwd, assignmentId) {
@@ -365,11 +368,19 @@ function laneHarvestedMarkerPath(cwd, assignmentId) {
365
368
  export function harvestLaneResults(options = {}) {
366
369
  const cwd = options.cwd ?? process.cwd();
367
370
  const agent = options.agent ?? 'coordinator';
368
- const result = { harvested: [], skipped: [], errors: [], warnings: [] };
371
+ const result = { harvested: [], skipped: [], errors: [], warnings: [], continuations: [] };
369
372
  const worktreePaths = resolveLaneScanPaths(options, cwd);
370
373
  for (const worktreePath of worktreePaths) {
371
- const file = getLaneResultPath(worktreePath);
372
- const fileExists = fs.existsSync(file);
374
+ const fileResolution = resolveLaneResultFile(worktreePath, options.assignmentId);
375
+ if (fileResolution.kind === 'invalid') {
376
+ result.errors.push(`Failed to parse ${fileResolution.path}: ${fileResolution.error}`);
377
+ continue;
378
+ }
379
+ if (fileResolution.kind === 'ambiguous') {
380
+ result.errors.push(`Ambiguous lane-result files for ${options.assignmentId ?? worktreePath}: ${fileResolution.paths.join(', ')}`);
381
+ continue;
382
+ }
383
+ const fileExists = fileResolution.kind === 'found';
373
384
  let nativeObservation;
374
385
  if (options.assignmentId) {
375
386
  try {
@@ -384,13 +395,7 @@ export function harvestLaneResults(options = {}) {
384
395
  continue;
385
396
  let lane;
386
397
  if (fileExists) {
387
- try {
388
- lane = LaneResultSchema.parse(JSON.parse(fs.readFileSync(file, 'utf-8')));
389
- }
390
- catch (err) {
391
- result.errors.push(`Failed to parse ${file}: ${err instanceof Error ? err.message : String(err)}`);
392
- continue;
393
- }
398
+ lane = fileResolution.lane;
394
399
  }
395
400
  else {
396
401
  lane = nativeObservation.lane;
@@ -582,6 +587,26 @@ export function harvestLaneResults(options = {}) {
582
587
  }
583
588
  }
584
589
  result.harvested.push(lane);
590
+ if (!options.dryRun) {
591
+ const reservation = findReservationByAssignmentId(lane.assignment_id, cwd);
592
+ if (reservation) {
593
+ try {
594
+ const loop = getLoop(reservation.loop_id, cwd);
595
+ if (loop) {
596
+ result.continuations.push({
597
+ assignment_id: lane.assignment_id,
598
+ loop_id: loop.id,
599
+ next_expected: computeNextExpected(loop),
600
+ });
601
+ }
602
+ }
603
+ catch {
604
+ // Reconciliation already reports corrupt/unreadable loop state as a
605
+ // loud warning. Continuation hints are best-effort and must not turn
606
+ // a successfully harvested result into an uncaught failure.
607
+ }
608
+ }
609
+ }
585
610
  }
586
611
  return result;
587
612
  }
@@ -662,17 +687,18 @@ export function integrateLaneResults(options = {}) {
662
687
  const result = { integrated: [], skipped: [], errors: [], next_turns: [] };
663
688
  const worktreePaths = resolveLaneScanPaths(options, cwd);
664
689
  for (const worktreePath of worktreePaths) {
665
- const file = getLaneResultPath(worktreePath);
666
- if (!fs.existsSync(file))
690
+ const fileResolution = resolveLaneResultFile(worktreePath, options.assignmentId);
691
+ if (fileResolution.kind === 'absent')
692
+ continue;
693
+ if (fileResolution.kind === 'invalid') {
694
+ result.errors.push(`Failed to parse ${fileResolution.path}: ${fileResolution.error}`);
667
695
  continue;
668
- let lane;
669
- try {
670
- lane = LaneResultSchema.parse(JSON.parse(fs.readFileSync(file, 'utf-8')));
671
696
  }
672
- catch (err) {
673
- result.errors.push(`Failed to parse ${file}: ${err instanceof Error ? err.message : String(err)}`);
697
+ if (fileResolution.kind === 'ambiguous') {
698
+ result.errors.push(`Ambiguous lane-result files for ${options.assignmentId ?? worktreePath}: ${fileResolution.paths.join(', ')}`);
674
699
  continue;
675
700
  }
701
+ const lane = fileResolution.lane;
676
702
  if (options.assignmentId && lane.assignment_id !== options.assignmentId)
677
703
  continue;
678
704
  const assignment = loadAssignment(lane.assignment_id, cwd);
@@ -681,6 +707,11 @@ export function integrateLaneResults(options = {}) {
681
707
  result.errors.push(`No assignment record for lane ${lane.assignment_id} — cannot integrate`);
682
708
  continue;
683
709
  }
710
+ const candidateTurnEvidence = turnOwnedLaneEvidence(lane, cwd);
711
+ const candidateOwnedLoop = candidateTurnEvidence ? getLoop(candidateTurnEvidence.reservation.loop_id, cwd) : undefined;
712
+ const ownedTurnEvidence = candidateTurnEvidence && candidateOwnedLoop && turnOwnedLoopEnabled(candidateOwnedLoop.kind)
713
+ ? candidateTurnEvidence
714
+ : undefined;
684
715
  const profile = getCapabilityProfile(assignment.agent);
685
716
  // No profile ⇒ assume it can commit (conservative: don't author for an
686
717
  // unknown agent), so brainclaw only lifecycles.
@@ -726,7 +757,12 @@ export function integrateLaneResults(options = {}) {
726
757
  ...(entry.commit_sha ? [{ type: 'commit', ref: entry.commit_sha, description: 'on-behalf integration commit' }] : []),
727
758
  ...entry.files_changed.slice(0, 50).map((f) => ({ type: 'file', ref: f })),
728
759
  ];
729
- entry.assignment_completed = forceCompleteAssignment(lane.assignment_id, artifacts, `pln#534 on-behalf integration: ${lane.summary.slice(0, 120)}`, actor, cwd);
760
+ // Turn-owned lanes are terminalized only after their artifact contract
761
+ // passes reconcileTurn. A repairable envelope must not complete the
762
+ // Assignment or fail/release its slot before the corrected replay.
763
+ if (!ownedTurnEvidence) {
764
+ entry.assignment_completed = forceCompleteAssignment(lane.assignment_id, artifacts, `pln#534 on-behalf integration: ${lane.summary.slice(0, 120)}`, actor, cwd);
765
+ }
730
766
  // pln#628 Focus 4B — map this lane onto its review loop BEFORE deciding
731
767
  // teardown: PR1 records the verdict + advances (auto-close on approve);
732
768
  // PR2 continues the fix cycle on request_changes (bump round, emit a
@@ -735,11 +771,8 @@ export function integrateLaneResults(options = {}) {
735
771
  // for non-review lanes / lanes without a verdict; never throws.
736
772
  // Legacy ideation lanes still use the historical closer. A turn-owned
737
773
  // lane of any kind is finalized exactly once by reconcileTurn below.
738
- const candidateEvidence = turnOwnedLaneEvidence(lane, cwd);
739
- const ownedLoop = candidateEvidence ? getLoop(candidateEvidence.reservation.loop_id, cwd) : undefined;
740
- const turnOwnedEvidence = candidateEvidence && ownedLoop && turnOwnedLoopEnabled(ownedLoop.kind)
741
- ? candidateEvidence
742
- : undefined;
774
+ const ownedLoop = candidateOwnedLoop;
775
+ const turnOwnedEvidence = ownedTurnEvidence;
743
776
  if (!turnOwnedEvidence) {
744
777
  const ideationClose = closeIdeationLoopFromLaneResult(assignment, lane, actor, cwd);
745
778
  if (ideationClose) {
@@ -781,6 +814,7 @@ export function integrateLaneResults(options = {}) {
781
814
  if (rr.next_turn) {
782
815
  result.next_turns.push({ loop_id: reservation.loop_id, ...rr.next_turn });
783
816
  }
817
+ entry.assignment_completed = loadAssignment(lane.assignment_id, cwd)?.status === 'completed';
784
818
  // Claim/run/assignment settling is OWNED by reconcileTurn, so we do NOT run the
785
819
  // legacy teardown gate — just reflect the resulting claim state. Settlement
786
820
  // semantics (reconcile-turn.ts, review #1): an ACCEPTED lane — approve OR
@@ -1221,6 +1255,7 @@ export async function runHarvestLane(assignmentId, options = {}) {
1221
1255
  // collected but never emitted on ANY channel; the silent half of the
1222
1256
  // 2026-08-02/03 review-loop stalls.
1223
1257
  warnings: result.warnings,
1258
+ continuations: result.continuations,
1224
1259
  }, null, 2));
1225
1260
  return;
1226
1261
  }
@@ -1257,6 +1292,13 @@ export async function runHarvestLane(assignmentId, options = {}) {
1257
1292
  for (const w of result.warnings) {
1258
1293
  console.log(` ⚠ ${w.message}`);
1259
1294
  }
1295
+ for (const continuation of result.continuations) {
1296
+ const next = continuation.next_expected;
1297
+ if (!next)
1298
+ continue;
1299
+ const slot = next.slot_id ? ` slot=${next.slot_id}` : '';
1300
+ console.log(` ↻ Next loop action [${continuation.loop_id}]: ${next.action}${slot}${next.reason ? ` — ${next.reason}` : ''}`);
1301
+ }
1260
1302
  const warnTag = result.warnings.length > 0 ? `, ${result.warnings.length} warning(s)` : '';
1261
1303
  console.log(`\n✔ Lane harvest complete${dryTag}: ${result.harvested.length} harvested, ${result.skipped.length} skipped, ${result.errors.length} error(s)${warnTag}.`);
1262
1304
  }
@@ -23,12 +23,15 @@ function successResponse(intent, result, artifacts, side_effects, warnings, dura
23
23
  const resultLoop = result && typeof result === 'object' && 'loop' in result
24
24
  ? result.loop
25
25
  : undefined;
26
+ const enrichedResult = resultLoop && result && typeof result === 'object'
27
+ ? { ...result, progress: loopProgress(resultLoop) }
28
+ : result;
26
29
  const nextActions = resultLoop ? pipelineNextActions(resultLoop) : [];
27
30
  return {
28
31
  response: {
29
32
  status: 'ok',
30
33
  intent: `bclaw_loop.${intent}`,
31
- result,
34
+ result: enrichedResult,
32
35
  artifacts,
33
36
  side_effects,
34
37
  warnings,
@@ -86,6 +89,30 @@ function pipelineNextActions(loop) {
86
89
  }
87
90
  return [];
88
91
  }
92
+ function loopProgress(loop) {
93
+ const phase = loop.phases.find((candidate) => candidate.name === loop.current_phase);
94
+ const phaseSlots = loop.slots.filter((slot) => (slot.phase ?? loop.current_phase) === loop.current_phase);
95
+ const slotCounts = {};
96
+ for (const slot of phaseSlots)
97
+ slotCounts[slot.status] = (slotCounts[slot.status] ?? 0) + 1;
98
+ const artifactCounts = {};
99
+ for (const artifact of loop.artifacts.filter((item) => item.phase === loop.current_phase)) {
100
+ artifactCounts[artifact.type] = (artifactCounts[artifact.type] ?? 0) + 1;
101
+ }
102
+ const gate = evaluatePhaseAdvanceGate(loop, phase?.advance_gate);
103
+ const activeSlots = phaseSlots.filter((slot) => ['open', 'assigned', 'working', 'waiting_input'].includes(slot.status));
104
+ const stuck = loop.status === 'open' && !gate.advance && activeSlots.length === 0;
105
+ return {
106
+ phase: loop.current_phase,
107
+ iteration: loop.iteration_count,
108
+ slots_by_status: slotCounts,
109
+ artifacts_by_type: artifactCounts,
110
+ gate_met: gate.advance,
111
+ ...(gate.gate_reason ? { gate_reason: gate.gate_reason } : {}),
112
+ stuck,
113
+ ...(stuck ? { recovery: 'Replay a real slot turn with bclaw_loop(intent="turn", slot_id=…); add_artifact cannot satisfy strict evidence.' } : {}),
114
+ };
115
+ }
89
116
  /** Concrete action evaluated by continuation policy; never exposed as an ungoverned hint. */
90
117
  function proposedPipelineActions(loop, cwd) {
91
118
  if (loop.kind === 'ideation') {
@@ -374,11 +374,12 @@ export const MCP_READ_TOOLS = [
374
374
  },
375
375
  {
376
376
  name: 'bclaw_code_status',
377
- description: 'Code Map status for the active session project: store presence, freshness badge, and index stats. Read-only; never refreshes. In a multi-project workspace, cascade=true adds per-child coverage plus progress/terminal diagnostics for the latest durable cascade job.',
377
+ description: 'Code Map status for the active session or explicit project: physical store-path presence, readable-index presence, exact resolution, freshness, stats, and latest durable refresh job. Read-only; never refreshes. In a multi-project workspace, cascade=true adds per-child coverage plus progress/terminal diagnostics for the latest durable cascade job.',
378
378
  annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'auto' },
379
379
  inputSchema: {
380
380
  type: 'object',
381
381
  properties: {
382
+ project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
382
383
  cascade: { type: 'boolean', description: 'Multi-project workspace recap: also report per-child store presence + freshness for every nested project. No-op outside a multi-project workspace.' },
383
384
  },
384
385
  },
@@ -390,6 +391,7 @@ export const MCP_READ_TOOLS = [
390
391
  inputSchema: {
391
392
  type: 'object',
392
393
  properties: {
394
+ project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
393
395
  query: { type: 'string', description: 'Symbol or token to search for (e.g. "App", "useAuth", "dispatch").' },
394
396
  limit: { type: 'number', description: 'Max matches to return.' },
395
397
  },
@@ -403,6 +405,7 @@ export const MCP_READ_TOOLS = [
403
405
  inputSchema: {
404
406
  type: 'object',
405
407
  properties: {
408
+ project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
406
409
  target: { type: 'string', description: 'Symbol name or file path to build a reading brief for.' },
407
410
  limit: { type: 'number', description: 'Max suggested files (hard-capped at 12 by the spec).' },
408
411
  },
@@ -416,6 +419,7 @@ export const MCP_READ_TOOLS = [
416
419
  inputSchema: {
417
420
  type: 'object',
418
421
  properties: {
422
+ project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
419
423
  target: { type: 'string', description: 'Symbol name or source-file path whose impact to inspect.' },
420
424
  depth: { type: 'number', description: 'Maximum graph depth. 1 (default) returns direct dependents only; 2+ opts into transitives. Clamped to 4.' },
421
425
  limit: { type: 'number', description: 'Maximum rows in each dependent section and in naming suggestions. Clamped to 100.' },
@@ -430,6 +434,7 @@ export const MCP_READ_TOOLS = [
430
434
  inputSchema: {
431
435
  type: 'object',
432
436
  properties: {
437
+ project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
433
438
  target: { type: 'string', description: 'Symbol name or source-file path around which to export a local subgraph.' },
434
439
  targetKind: { type: 'string', enum: ['symbol', 'file'], description: 'Optional explicit target kind; otherwise a source path is detected safely.' },
435
440
  direction: { type: 'string', enum: ['outgoing', 'incoming', 'both'], description: 'Which edge direction(s) to follow. Default: both.' },
@@ -449,6 +454,7 @@ export const MCP_READ_TOOLS = [
449
454
  inputSchema: {
450
455
  type: 'object',
451
456
  properties: {
457
+ project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
452
458
  path: { type: 'string', description: 'Workspace-relative source file path (for example `src/app/App.tsx`).' },
453
459
  limit: { type: 'number', description: 'Maximum symbols to return; clamped to the hard cap of 200.' },
454
460
  },
@@ -459,11 +465,12 @@ export const MCP_READ_TOOLS = [
459
465
  const MCP_WRITE_TOOLS = [
460
466
  {
461
467
  name: 'bclaw_code_refresh',
462
- description: 'Rebuild the Code Map index for the active session project. scope="changed" (default) reparses changed files; scope="all" does a full refresh + compaction. In a multi-project workspace, cascade=true starts a durable background job immediately; follow it with bclaw_code_status(cascade=true), which reports progress and terminal per-project diagnostics without an MCP timeout.',
468
+ description: 'Accept a durable background rebuild of the Code Map index for the active or explicit project and return immediately. scope="changed" (default) reparses changed files; scope="all" does a full refresh + compaction. Follow with bclaw_code_status, which reports progress and terminal diagnostics without an MCP timeout. In a multi-project workspace, cascade=true applies the same contract across nested projects.',
463
469
  annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'prompt' },
464
470
  inputSchema: {
465
471
  type: 'object',
466
472
  properties: {
473
+ project: { type: 'string', description: 'Optional project name, id, or workspace-relative path. Overrides the active session project for this call.' },
467
474
  scope: { type: 'string', enum: ['changed', 'all'], description: 'changed (default) reparses changed files only; all does a full refresh with orphan compaction.' },
468
475
  cascade: { type: 'boolean', description: 'Multi-project cascade: refresh every nested brainclaw project + a child-scoped root store. No-op outside a multi-project workspace.' },
469
476
  },
@@ -848,10 +855,12 @@ const MCP_WRITE_TOOLS = [
848
855
  inputSchema: {
849
856
  type: 'object',
850
857
  properties: {
851
- intent: { type: 'string', enum: ['assign', 'consult', 'review', 'reroute', 'summarize', 'ideate'], description: 'Coordination intent. assign/review/reroute and multi-agent ideate spawn worker processes; consult/summarize do not. "assign" creates a claim per target agent and spawns a worker on the brief. "consult" delivers the brief to the target inbox(es) WITHOUT creating claims and WITHOUT spawning — targets pick it up via their own bclaw_work. "review" creates a review candidate (and, with open_loop, a review loop). "ideate" opens an ideation loop with the task as the proposal seed; with targetAgents it advances to critique and SPAWNS one worktree-isolated critic worker per target (autoExecute honored, pln#626 Phase 2), otherwise it opens the loop for the champion to drive manually. "reroute" releases the current claim and reassigns. "summarize" reads a thread and returns a summary.' },
858
+ intent: { type: 'string', enum: ['assign', 'consult', 'review', 'reroute', 'summarize', 'ideate'], description: 'Coordination intent. assign/review/reroute and multi-agent ideate spawn worker processes; consult/summarize do not. "assign" creates a claim per target agent and spawns a worker on the brief. "consult" delivers the brief to the target inbox(es) WITHOUT creating claims and WITHOUT spawning — targets pick it up via their own bclaw_work. "review" creates a review candidate (and, with open_loop, a review loop). "ideate" opens an ideation loop with the task as the proposal seed; with targetAgents it advances to critique and, by default, starts one critic at a time. Set ideation_schedule="parallel" for immediate fan-out. "reroute" releases the current claim and reassigns. "summarize" reads a thread and returns a summary.' },
852
859
  task: { type: 'string', description: 'Brief or task description delivered to target agents. TRANSPORT NOTE (dec#133): a spawned worker\'s capabilities follow its invoke template, not the mere presence of "sandbox". A sandboxed codex worker (`--sandbox workspace-write`, `approval_policy=never`) CAN reach brainclaw MCP — the server runs out-of-sandbox and every tool call is auto-approved — so MCP lifecycle calls (`bclaw_assignment_update`, `bclaw_send_message`, …) do NOT hang. Its one real limit is that `.git` is read-only: it cannot `git commit`, so it must leave fixes uncommitted in the worktree and the coordinator integrates + commits the diff at harvest (never instruct such a worker to commit). Genuinely MCP-less agents (nanoclaw/nemoclaw/picoclaw/zeroclaw) have no MCP at all: for them, prefer file-based protocols (write findings/reply to a markdown file in the worktree; the coordinator harvests it and lifecycle-closes the assignment). See docs/integrations/<agent>.md for the per-agent capability matrix.' },
853
860
  scope: { type: 'string', description: 'File or feature scope. Used as claim scope for assign/reroute; as thread id for summarize if threadId is absent.' },
854
861
  targetAgents: { type: 'array', items: { type: 'string' }, description: 'Agent names to target. If omitted, all spawnable agents are used.' },
862
+ ideation_schedule: { type: 'string', enum: ['sequential', 'parallel'], description: 'For intent=ideate with targetAgents: sequential (default) starts only the first critic; after harvest, drive the next open critic with bclaw_loop turn dispatch=true. parallel starts all critics immediately.' },
863
+ criticPerspectives: { type: 'array', items: { type: 'string' }, description: 'Optional ideation instructions/lenses aligned positionally with targetAgents. If omitted, Brainclaw assigns distinct evidence, failure-mode, and alternative/trade-off lenses.' },
855
864
  constraints: { type: 'object', description: 'Optional structured constraints passed alongside the brief (e.g. deadline, reviewCriteria).' },
856
865
  threadId: { type: 'string', description: 'Thread ID for summarize intent.' },
857
866
  linked: {
@@ -864,7 +873,7 @@ const MCP_WRITE_TOOLS = [
864
873
  },
865
874
  additionalProperties: false,
866
875
  },
867
- autoExecute: { type: 'boolean', description: 'Attempt to spawn target agents after delivery (default: true). Applies to the spawning intents assign/review/reroute AND to multi-agent ideate (with targetAgents, it spawns one worktree-isolated critic worker per target). consult is inbox-only and ignores autoExecute; summarize just reads a thread and ignores it. When false on a spawning intent, returns command_ready_manual with bash commands for the supervisor to run.' },
876
+ autoExecute: { type: 'boolean', description: 'Attempt to spawn target agents after delivery (default: true). Applies to the spawning intents assign/review/reroute and multi-agent ideate. Sequential ideation starts one critic; parallel ideation starts every critic. consult is inbox-only and ignores autoExecute; summarize just reads a thread and ignores it. When false on a spawning intent, returns command_ready_manual with commands for the supervisor to run.' },
868
877
  open_loop: { type: 'boolean', description: 'For intent=review only: also open a review Loop on top of the candidate (author + reviewer slots, advance to `findings`, dispatch turns). Default false — existing review callers are unaffected. See docs/concepts/loop-engine.md §Automation.' },
869
878
  review_mode: { type: 'string', enum: ['asymmetric', 'symmetric'], description: 'Optional review Loop mode when open_loop=true. `asymmetric` (default) keeps the classical author→reviewer handoff; `symmetric` lets each reviewer turn also apply fixes directly, halving round-trips for spec/doc reviews. Ignored when open_loop is false.' },
870
879
  preflight: { type: 'boolean', description: 'pln#533: when open_loop=true, run a trivial validation spawn per reviewer agent BEFORE opening the loop so an environment death (config rejected, auth fail, model mismatch) surfaces instantly with a clear reason instead of a generic loop timeout. Reviewers that fail pre-flight are dropped (with a targeted warning); if all fail, loop creation is skipped. Default true; set false to skip (e.g. you already ran `brainclaw doctor --spawn-check`). Ignored when open_loop is false or BRAINCLAW_NO_SPAWN is set.' },
@@ -999,6 +1008,24 @@ const MCP_WRITE_TOOLS = [
999
1008
  required: [],
1000
1009
  },
1001
1010
  },
1011
+ {
1012
+ name: 'bclaw_harvest',
1013
+ description: 'Harvest worker LANE-RESULT.json envelopes into assignment/loop state. This is MCP parity for `brainclaw harvest` and is distinct from candidate-memory harvest. Use assignmentId for one lane or all=true; integrate=true also performs the CLI --integrate lifecycle. Repairable contract errors leave the loop slot replayable and are returned as warnings.',
1014
+ annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
1015
+ inputSchema: {
1016
+ type: 'object',
1017
+ properties: {
1018
+ assignmentId: { type: 'string', description: 'One Assignment whose lane result should be harvested.' },
1019
+ all: { type: 'boolean', description: 'Scan every managed lane; mutually exclusive with assignmentId.' },
1020
+ worktreePaths: { type: 'array', items: { type: 'string' }, description: 'Optional explicit worktrees to scan.' },
1021
+ dryRun: { type: 'boolean', description: 'Report without writing state.' },
1022
+ integrate: { type: 'boolean', description: 'Also lifecycle/commit-on-behalf like CLI --integrate.' },
1023
+ agent: { type: 'string', description: 'Coordinator agent name.' },
1024
+ agentId: { type: 'string', description: 'Registered coordinator agent id.' },
1025
+ },
1026
+ required: [],
1027
+ },
1028
+ },
1002
1029
  // ── Canonical CRUD verbs (Phase 3 / v1.0 grammar) ──────────────────
1003
1030
  // Promoted to `standard` tier at the v1.0 cut.
1004
1031
  {
@@ -1012,6 +1039,7 @@ const MCP_WRITE_TOOLS = [
1012
1039
  filter: { type: 'object', description: 'Filter keys (ANY entity): status, tag (single tag), tags (array, any-match), author, plan_id, source, auto_generated, limit, offset, includeLegacy (bool, default false), minAutoReflectConfidence (0-1, default 0.6). ENTITY-SCOPED keys (rejected with a validation_error if used with any other entity): assignment_id, claim_id, message_id — ONLY for entity="agent_run"; scope ("project" default | "global", the latter unions the dispatchable catalog + adds dispatchable/registered) and includeReputation (bool — attaches a public reputation summary per agent) — ONLY for entity="agent". Unknown/mis-scoped keys are rejected loudly.' },
1013
1040
  project: { type: 'string', description: 'Optional: name (or path/basename) of a linked project to query. Defaults to the current project. Only cross_project_links (config.yaml) and workspace store-chain children are accepted — list with `brainclaw link list`.' },
1014
1041
  budget_tokens: { type: 'number', description: 'Optional token budget for the page payload (~4 chars/token). Tightens the default size cap; pagination metadata (has_more/next_offset) still applies.' },
1042
+ fields: { type: 'array', items: { type: 'string' }, description: 'Optional field projection for each row, e.g. ["id","status","created_at"].' },
1015
1043
  },
1016
1044
  required: ['entity'],
1017
1045
  },
@@ -232,6 +232,13 @@ function dispatchReadTool(name, args, ctx) {
232
232
  notifications = buildNotificationSummary(unseenEvents);
233
233
  unseenEventCount = unseenEvents.length;
234
234
  }
235
+ const actionableNotificationTypes = new Set(['action', 'assignment', 'claim', 'plan', 'handoff', 'candidate', 'loop']);
236
+ const actionableNotifications = notifications
237
+ ? Object.fromEntries(Object.entries(notifications).filter(([key]) => actionableNotificationTypes.has(key.split(':').at(-1) ?? '')))
238
+ : undefined;
239
+ const actionableCount = actionableNotifications
240
+ ? Object.values(actionableNotifications).reduce((sum, count) => sum + count, 0)
241
+ : 0;
235
242
  return {
236
243
  content: [{ type: 'text', text: enrichedContent || 'No relevant memory found.' }],
237
244
  structuredContent: {
@@ -246,7 +253,14 @@ function dispatchReadTool(name, args, ctx) {
246
253
  name: tool.name,
247
254
  type: tool.type,
248
255
  })),
249
- ...(notifications ? { pending_notifications: notifications, unseen_event_count: unseenEventCount } : {}),
256
+ ...(notifications ? {
257
+ pending_notifications: {
258
+ actionable_count: actionableCount,
259
+ by_type: actionableNotifications ?? {},
260
+ telemetry_events_omitted: Math.max(0, (unseenEventCount ?? 0) - actionableCount),
261
+ },
262
+ unseen_event_count: unseenEventCount,
263
+ } : {}),
250
264
  },
251
265
  };
252
266
  }
@@ -274,6 +274,11 @@ export const generatedSchemas = {
274
274
  "agent_id": {
275
275
  "type": "string"
276
276
  },
277
+ "perspective": {
278
+ "type": "string",
279
+ "minLength": 1,
280
+ "maxLength": 1000
281
+ },
277
282
  "assignment_id": {
278
283
  "type": "string"
279
284
  },
@@ -317,6 +322,14 @@ export const generatedSchemas = {
317
322
  },
318
323
  "current_turn_id": {
319
324
  "type": "string"
325
+ },
326
+ "last_completed_phase": {
327
+ "type": "string"
328
+ },
329
+ "last_completed_iteration": {
330
+ "type": "integer",
331
+ "minimum": 0,
332
+ "maximum": 9007199254740991
320
333
  }
321
334
  },
322
335
  "required": [