brainclaw 1.28.1 → 1.28.3

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 (39) hide show
  1. package/dist/brainclaw-vscode.vsix +0 -0
  2. package/dist/commands/code-map.js +2 -0
  3. package/dist/commands/doctor.js +1 -0
  4. package/dist/commands/harvest.js +32 -43
  5. package/dist/commands/loops-handlers.js +66 -3
  6. package/dist/commands/mcp-catalog.js +2 -2
  7. package/dist/commands/mcp-write-coordination.js +426 -141
  8. package/dist/commands/mcp-write-entities.js +5 -2
  9. package/dist/commands/mcp.js +57 -11
  10. package/dist/core/agentrun-reconciler.js +138 -4
  11. package/dist/core/claims.js +4 -1
  12. package/dist/core/code-map/aggregate.js +20 -7
  13. package/dist/core/code-map/backend.js +25 -7
  14. package/dist/core/code-map/cascade-jobs.js +174 -0
  15. package/dist/core/code-map/cascade-worker.js +15 -0
  16. package/dist/core/code-map/cascade.js +63 -26
  17. package/dist/core/code-map/query.js +6 -3
  18. package/dist/core/entity-operations.js +18 -4
  19. package/dist/core/execution-adapters.js +23 -8
  20. package/dist/core/hygiene-policy.js +2 -1
  21. package/dist/core/loop-turn-dispatch.js +18 -1
  22. package/dist/core/loops/attempt-authority.js +22 -4
  23. package/dist/core/loops/attempt-generations.js +17 -4
  24. package/dist/core/loops/attempt-reservation.js +14 -1
  25. package/dist/core/loops/attempt-takeover.js +173 -76
  26. package/dist/core/loops/reconcile-turn.js +224 -26
  27. package/dist/core/loops/result-reducers.js +8 -8
  28. package/dist/core/loops/turn-execution.js +38 -19
  29. package/dist/core/loops/types.js +3 -0
  30. package/dist/core/loops/verbs.js +1 -1
  31. package/dist/core/spawn-check.js +9 -1
  32. package/dist/facts.js +8 -8
  33. package/dist/facts.json +7 -7
  34. package/docs/cli.md +2 -2
  35. package/docs/code-map.md +30 -9
  36. package/docs/concepts/loop-engine.md +5 -0
  37. package/docs/integrations/mcp.md +2 -2
  38. package/docs/mcp-schema-changelog.md +30 -0
  39. package/package.json +1 -1
Binary file
@@ -108,6 +108,8 @@ function printStatus(status, options) {
108
108
  }
109
109
  console.log('Code Map status');
110
110
  console.log(` Store: ${status.store_exists ? 'present' : 'absent'}`);
111
+ console.log(` Root: ${status.resolution.project_root}`);
112
+ console.log(` Path: ${status.resolution.store_path}`);
111
113
  console.log(` ${badgeLine(status.freshness_badge)}`);
112
114
  if (status.stats) {
113
115
  console.log(` Files: ${status.stats.files_indexed}`);
@@ -518,6 +518,7 @@ export function runDispatchHealthCheck(options = {}) {
518
518
  };
519
519
  switch (result.action) {
520
520
  case 'inferred_completed':
521
+ case 'reconciled_turn':
521
522
  inferred_completed.push(summary);
522
523
  break;
523
524
  case 'health_check_unverified':
@@ -25,54 +25,43 @@ import { commitWorktreeOnBehalf, worktreesBaseDir, resolveGitToplevel } from '..
25
25
  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
- import { reconcileTurn } from '../core/loops/reconcile-turn.js';
29
- import { findReservationByAssignmentId } from '../core/loops/attempt-reservation.js';
30
- import { resolveTurnGenerationChain } from '../core/loops/attempt-generations.js';
28
+ import { reconcileTurnOwnedLane, turnOwnedLaneEvidence } from '../core/loops/reconcile-turn.js';
31
29
  import { getLoop } from '../core/loops/store.js';
32
30
  import { phasePolicy } from '../core/loops/kind-policies.js';
33
- import { readCompletionSignals } from '../core/runtime-signals.js';
34
31
  import { reconcileClaimConformity } from '../core/claim-conformity.js';
35
32
  import { toWarningDetail } from '../core/warnings.js';
36
33
  import { harvestHarnessObservation } from '../core/harness-adapters/index.js';
37
- function turnOwnedLaneEvidence(lane, cwd) {
38
- const reservation = findReservationByAssignmentId(lane.assignment_id, cwd);
39
- if (!reservation)
40
- return undefined; // legacy lane (no reservation)
41
- const chain = resolveTurnGenerationChain(cwd, reservation.turn_id);
42
- const completion = readCompletionSignals(cwd, reservation.child_ids.assignment_id, chain?.latest_generation.run_id).completed;
43
- const nonce = lane.nonce ?? completion?.nonce;
44
- if (!nonce && !reservation.execution_contract_ref)
45
- return undefined;
46
- return {
47
- reservation,
48
- nonce,
49
- contract_hash: lane.execution_contract_hash ?? completion?.contract_hash,
50
- capability_snapshot_hash: lane.capability_snapshot_hash ?? completion?.capability_snapshot_hash,
51
- };
52
- }
53
- function reconcileTurnOwnedLane(lane, cwd, evidence) {
54
- const ev = evidence ?? turnOwnedLaneEvidence(lane, cwd);
55
- if (!ev)
56
- return undefined; // legacy lane OR no turn-keyed evidence caller runs the legacy path
57
- const { reservation, nonce } = ev;
58
- const enrichedLane = {
59
- ...lane,
60
- turn_id: lane.turn_id ?? reservation.turn_id,
61
- run_id: lane.run_id ?? reservation.child_ids.run_id,
62
- nonce,
63
- execution_contract_hash: lane.execution_contract_hash ?? ev.contract_hash,
64
- capability_snapshot_hash: lane.capability_snapshot_hash ?? ev.capability_snapshot_hash,
65
- };
66
- const loop = getLoop(reservation.loop_id, cwd);
67
- const critiques = loop?.kind === 'ideation'
68
- && reservation.phase === 'critique'
69
- && lane.artifact_type === 'critique'
70
- && (lane.body ?? '').trim().length > 0
71
- ? [{ body: lane.body.trim() }]
72
- : undefined;
73
- const result = reconcileTurn({ turn_id: reservation.turn_id, lane: enrichedLane, cwd, critiques });
74
- return { reservation, result };
75
- }
34
+ /**
35
+ * pln#630 PR3a — finalize a TURN-OWNED review lane via the exactly-once `reconcileTurn`
36
+ * instead of the legacy `closeReviewLoopFromLaneResult`. Returns `undefined` for a legacy
37
+ * (non-reserved) lane so the caller runs the unchanged legacy path this is the
38
+ * exactly-one-finalizer discriminator: a lane is turn-owned iff a reservation OWNS its
39
+ * assignment_id (only the turn-owned dispatch writes a reservation file).
40
+ *
41
+ * Evidence sourcing (the load-bearing subtlety): a real reviewer's LANE-RESULT.json is
42
+ * KEYLESS — the review brief never asks the worker to echo turn_id/run_id/nonce — so
43
+ * read-strict `reconcileTurn` (which matches lane.{turn_id,run_id,nonce} against the
44
+ * attempt) would REJECT it. We source the keys authoritatively: turn_id + run_id are
45
+ * deterministic from the reservation, and the NONCE — the non-derivable proof that THIS
46
+ * launch generation actually ran — comes from the coordinator's completion SENTINEL
47
+ * (written mechanically by the ack-wrapper with the launch-grant token). A caller/test
48
+ * that already supplies keyed lanes is honored (lane.* wins); a stale generation's
49
+ * sentinel carries the old token → still rejected, preserving the anti-stale guarantee.
50
+ */
51
+ /**
52
+ * The turn-owned FINALIZATION discriminator (pln#630, review Finding 1). A lane finalizes via
53
+ * the exactly-once reconcileTurn ONLY if a committed reservation OWNS it AND turn-keyed evidence
54
+ * (the nonce) is available — from the lane or the coordinator's completion SENTINEL. Without the
55
+ * nonce, reconcileTurn's read-strict gate can NEVER converge: this is reachable in production
56
+ * when a turn-owned dispatch WON the fence but did not ack-wrap-spawn (inbox_only / IDE-only
57
+ * reviewer, command_ready_manual, capacity cap, BRAINCLAW_NO_SPAWN, worktree-creation failure) —
58
+ * it minted a reservation but no sentinel will ever be written. Returning undefined there routes
59
+ * the lane to the LEGACY presence-based closer so the loop still converges instead of stalling
60
+ * forever. This is SAFE: the exactly-once SPAWN guarantee is enforced at DISPATCH by the launch
61
+ * fence (already run), so using legacy FINALIZATION for a sentinel-less lane reintroduces no
62
+ * double-spawn; and a sentinel that lands after a legacy close makes a later reconcile a
63
+ * terminal-loop idempotent no-op.
64
+ */
76
65
  /**
77
66
  * Map a `reconcileTurn` result onto the `ReviewLoopCloseResult` shape harvest records for
78
67
  * observability (entry.review_loop / CLI). No keep_claim / next_turn: the request_changes
@@ -9,7 +9,7 @@ import { loadSequence } from '../core/sequence.js';
9
9
  import { createActionRequired, loadActionRequired } from '../core/actions.js';
10
10
  import { selectImplementationReviewer } from '../core/reviewer-policy.js';
11
11
  import { handleBclawCoordinate } from './mcp-write-coordination.js';
12
- import { add_artifact, advance, AwaitingFileApplyApprovalError, closeLoop, complete_turn, computeNextExpected, getLoop, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, listLoopEvents, listLoops, LockLostError, LockTimeoutError, openLoop, pause, provideInput, requestInput, resume, sweepPauseTimeouts, takeoverLoopAttempt, readLocalAuthorityHome, turn, VersionConflictError, withLoopLock, artifactEvidenceDigest, attachContinuationActionRequired, ensureContinuation, } from '../core/loops/index.js';
12
+ import { add_artifact, advance, AwaitingFileApplyApprovalError, closeLoop, complete_turn, computeNextExpected, getLoop, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, listLoopEvents, listLoops, LockLostError, LockTimeoutError, openLoop, pause, provideInput, requestInput, resume, sweepPauseTimeouts, takeoverLoopAttempt, readLocalAuthorityHome, turn, VersionConflictError, withLoopLock, artifactEvidenceDigest, attachContinuationActionRequired, ensureContinuation, deriveWorkerReplyContract, evaluatePhaseAdvanceGate, } from '../core/loops/index.js';
13
13
  import { BclawLoopRequestSchema, BCLAW_LOOP_INTENTS, } from '../core/loops/facade-schema.js';
14
14
  // NextExpectedHint type now lives in src/core/loops/next-expected.ts
15
15
  // (hoisted per can_e57c7782 follow-up so MCP facade + CLI share the
@@ -141,7 +141,7 @@ function proposedPipelineActions(loop, cwd) {
141
141
  }
142
142
  return [];
143
143
  }
144
- function errorResponse(intent, code, message, durationMs, result = null) {
144
+ function errorResponse(intent, code, message, durationMs, result = null, nextActions = []) {
145
145
  return {
146
146
  response: {
147
147
  status: 'error',
@@ -152,10 +152,72 @@ function errorResponse(intent, code, message, durationMs, result = null) {
152
152
  warnings: [],
153
153
  error: `${code}: ${message}`,
154
154
  duration_ms: durationMs,
155
+ ...(nextActions.length > 0 ? { next_actions: nextActions } : {}),
155
156
  },
156
157
  summary: `✘ bclaw_loop[${intent}] ${code}: ${message}`,
157
158
  };
158
159
  }
160
+ function continuationUnavailableDiagnostic(loop, cwd) {
161
+ const phase = loop.phases.find((candidate) => candidate.name === loop.current_phase);
162
+ const gate = phase?.advance_gate;
163
+ const gateOutcome = evaluatePhaseAdvanceGate(loop, gate, cwd);
164
+ const contract = deriveWorkerReplyContract(loop);
165
+ const blockers = [];
166
+ const probableCauses = [];
167
+ const nextActions = [];
168
+ if (!gateOutcome.advance && gateOutcome.gate_reason)
169
+ blockers.push(gateOutcome.gate_reason);
170
+ const assigned = loop.slots.filter((slot) => slot.status === 'assigned' && slot.assignment_id);
171
+ const open = loop.slots.filter((slot) => slot.status === 'open'
172
+ && !(loop.kind === 'ideation' && loop.current_phase === 'critique' && slot.role === 'champion'));
173
+ if (assigned.length > 0) {
174
+ probableCauses.push('one or more dispatched worker results have not converged into gate evidence');
175
+ for (const slot of assigned) {
176
+ nextActions.push({
177
+ tool: 'bclaw_find',
178
+ args: { entity: 'agent_run', filter: { assignment_id: slot.assignment_id, limit: 10 } },
179
+ when: `reconcile and inspect the AgentRun projection for slot ${slot.slot_id}`,
180
+ });
181
+ }
182
+ }
183
+ for (const slot of open) {
184
+ probableCauses.push(`slot ${slot.slot_id} has not been dispatched`);
185
+ nextActions.push({
186
+ tool: 'bclaw_loop',
187
+ args: { intent: 'turn', loop_id: loop.id, slot_id: slot.slot_id, input: loop.goal ?? loop.title, dispatch: true },
188
+ when: `dispatch open slot ${slot.slot_id}`,
189
+ });
190
+ }
191
+ if (loop.kind === 'ideation') {
192
+ const draft = [...loop.artifacts].reverse().find((artifact) => artifact.type === 'plan_draft');
193
+ if (!draft)
194
+ blockers.push('no attested plan_draft artifact is available for continuation');
195
+ if ((loop.linked?.plan_ids?.length ?? 0) === 0)
196
+ blockers.push('the source loop is not linked to a plan');
197
+ if ((loop.linked?.sequence_ids?.length ?? 0) !== 1)
198
+ blockers.push('the source loop must link exactly one implementation sequence');
199
+ }
200
+ else if (loop.kind === 'implementation') {
201
+ const handoff = [...loop.artifacts].reverse().find((artifact) => artifact.type === 'handoff' && artifact.ref);
202
+ if (!handoff)
203
+ blockers.push('no attested handoff with a reviewable ref is available');
204
+ }
205
+ return {
206
+ result: {
207
+ loop_id: loop.id,
208
+ phase: loop.current_phase,
209
+ gate: {
210
+ expected: contract?.requirements ?? (gate ? [gate] : []),
211
+ observed: gateOutcome.gate_reason ?? 'gate satisfied or no phase gate',
212
+ passed: gateOutcome.advance,
213
+ },
214
+ blockers: [...new Set(blockers)],
215
+ probable_causes: [...new Set(probableCauses)],
216
+ next_actions: nextActions,
217
+ },
218
+ next_actions: nextActions,
219
+ };
220
+ }
159
221
  function inferIntent(args) {
160
222
  if (!args || typeof args !== 'object')
161
223
  return 'unknown';
@@ -712,7 +774,8 @@ export async function handleBclawLoop(options) {
712
774
  const actions = proposedPipelineActions(source, options.cwd);
713
775
  const action = actions[req.action_index];
714
776
  if (!action) {
715
- return errorResponse('continue', 'continuation_unavailable', `no executable continuation action ${req.action_index} for ${source.id}`, Date.now() - startMs);
777
+ const diagnostic = continuationUnavailableDiagnostic(source, options.cwd);
778
+ return errorResponse('continue', 'continuation_unavailable', `no executable continuation action ${req.action_index} for ${source.id}; inspect gate/blockers and execute the supplied recovery actions`, Date.now() - startMs, diagnostic.result, diagnostic.next_actions);
716
779
  }
717
780
  const sourceArtifactId = action.args?.linked?.source_artifact_id;
718
781
  const sourceArtifact = source.artifacts.find((artifact) => artifact.artifact_id === sourceArtifactId);
@@ -374,7 +374,7 @@ export const MCP_READ_TOOLS = [
374
374
  },
375
375
  {
376
376
  name: 'bclaw_code_status',
377
- description: 'Code Map status for this project: store presence, freshness badge (fresh / stale_changed_files / stale_extractor / stale_grammar / stale_git_head / 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. In a multi-project workspace, cascade=true adds a per-child recap (which nested projects have a built index vs missing_index).',
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.',
378
378
  annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'auto' },
379
379
  inputSchema: {
380
380
  type: 'object',
@@ -459,7 +459,7 @@ export const MCP_READ_TOOLS = [
459
459
  const MCP_WRITE_TOOLS = [
460
460
  {
461
461
  name: 'bclaw_code_refresh',
462
- 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. In a multi-project workspace, cascade=true refreshes EVERY nested project into its own store + the root store scoped to files no child owns (zero double-indexing) so one call at the root indexes the whole monorepo per-project.',
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.',
463
463
  annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'prompt' },
464
464
  inputSchema: {
465
465
  type: 'object',