brainclaw 1.5.5 → 1.6.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 (43) hide show
  1. package/dist/brainclaw-vscode.vsix +0 -0
  2. package/dist/cli.js +124 -7
  3. package/dist/commands/bootstrap-loop.js +206 -0
  4. package/dist/commands/loop.js +156 -0
  5. package/dist/commands/loops-handlers.js +110 -55
  6. package/dist/commands/mcp-read-handlers.js +37 -0
  7. package/dist/commands/mcp.js +621 -202
  8. package/dist/commands/questions.js +180 -0
  9. package/dist/commands/reply.js +190 -0
  10. package/dist/commands/session-end.js +105 -3
  11. package/dist/commands/session-start.js +32 -53
  12. package/dist/commands/switch.js +17 -1
  13. package/dist/core/agentrun-reconciler.js +65 -0
  14. package/dist/core/claims.js +29 -0
  15. package/dist/core/dispatch-status.js +219 -0
  16. package/dist/core/entity-operations.js +128 -9
  17. package/dist/core/execution-adapters.js +38 -2
  18. package/dist/core/facade-schema.js +55 -0
  19. package/dist/core/federation-cloud.js +27 -12
  20. package/dist/core/federation-materialize.js +57 -0
  21. package/dist/core/instruction-templates.js +2 -0
  22. package/dist/core/loops/bootstrap-acquire.js +195 -0
  23. package/dist/core/loops/facade-schema.js +68 -1
  24. package/dist/core/loops/hooks/bootstrap-write.js +144 -0
  25. package/dist/core/loops/hooks/notify-operator.js +148 -0
  26. package/dist/core/loops/hooks/survey-source-reader.js +256 -0
  27. package/dist/core/loops/index.js +8 -2
  28. package/dist/core/loops/next-expected.js +63 -0
  29. package/dist/core/loops/presets/bootstrap.js +75 -0
  30. package/dist/core/loops/presets/index.js +16 -0
  31. package/dist/core/loops/store.js +224 -4
  32. package/dist/core/loops/types.js +346 -1
  33. package/dist/core/loops/verbs.js +739 -6
  34. package/dist/core/schema.js +28 -2
  35. package/dist/core/state.js +62 -0
  36. package/dist/facts.js +7 -5
  37. package/dist/facts.json +6 -4
  38. package/docs/concepts/dispatch-lifecycle.md +228 -0
  39. package/docs/concepts/loop-engine.md +55 -0
  40. package/docs/concepts/multi-agent-workflows.md +167 -166
  41. package/docs/concepts/troubleshooting.md +10 -2
  42. package/docs/integrations/overview.md +14 -12
  43. package/package.json +1 -1
@@ -1,8 +1,11 @@
1
1
  import { ZodError } from 'zod';
2
2
  import { listAgentRuns } from '../core/agentruns.js';
3
3
  import { reconcileAgentRun } from '../core/agentrun-reconciler.js';
4
- import { add_artifact, advance, closeLoop, complete_turn, getLoop, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, listLoopEvents, listLoops, LockLostError, LockTimeoutError, openLoop, pause, resume, turn, VersionConflictError, withLoopLock, } from '../core/loops/index.js';
4
+ import { add_artifact, advance, AwaitingFileApplyApprovalError, closeLoop, complete_turn, computeNextExpected, getLoop, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, listLoopEvents, listLoops, LockLostError, LockTimeoutError, openLoop, pause, provideInput, requestInput, resume, sweepPauseTimeouts, turn, VersionConflictError, withLoopLock, } from '../core/loops/index.js';
5
5
  import { BclawLoopRequestSchema, BCLAW_LOOP_INTENTS, } from '../core/loops/facade-schema.js';
6
+ // NextExpectedHint type now lives in src/core/loops/next-expected.ts
7
+ // (hoisted per can_e57c7782 follow-up so MCP facade + CLI share the
8
+ // same contract). Imported above.
6
9
  function resolveActor(req, defaultActor) {
7
10
  const agentId = req.agentId?.trim() || defaultActor;
8
11
  const actor = req.agent?.trim() || req.agentId?.trim() || defaultActor;
@@ -128,68 +131,51 @@ function withLockedLoopMutation(req, agentId, cwd, work) {
128
131
  // Best-effort fence at entry; see SLOT_BOUND_INTENTS / fence-check
129
132
  // discipline comment above for why mid-verb re-checks are deferred.
130
133
  fenceCheck();
134
+ // pln#508 step 3 follow-up (can_810ff9ec): run the timeout sweep
135
+ // INSIDE the loop lock for mutating intents. Previously the sweep
136
+ // ran at facade entry (before lock acquisition), which could race
137
+ // with concurrent writers and turn the caller's expected_version
138
+ // into a sweep-induced version_conflict. Now: sweep writes happen
139
+ // under the same lock as the caller's verb — single-writer
140
+ // serialization preserved. If the sweep bumps the version, the
141
+ // caller's verb sees the post-sweep state (e.g. their question
142
+ // already timed out → provide_input legitimately returns
143
+ // unknown_question, which is correct semantics).
144
+ trySweepLoopTimeouts(req.loop_id, cwd);
131
145
  return work();
132
146
  },
133
147
  });
134
148
  }
135
- /**
136
- * `NextExpectedHint`self-describing hint to the caller about the most
137
- * natural next intent. Kept conservative for the MVP: we look at the loop's
138
- * status + slot states and pick the smallest correct action.
139
- */
140
- function computeNextExpected(loop) {
141
- if (loop.status === 'completed' || loop.status === 'cancelled' || loop.status === 'blocked') {
142
- return null;
143
- }
144
- if (loop.status === 'paused') {
145
- return null;
146
- }
147
- const currentPhaseSlots = loop.slots.filter((s) => (s.phase ?? loop.current_phase) === loop.current_phase);
148
- const openSlots = currentPhaseSlots.filter((s) => s.status === 'open');
149
- if (openSlots.length > 0) {
150
- const first = openSlots[0];
151
- return {
152
- action: 'turn',
153
- intent: 'bclaw_loop.turn',
154
- phase: loop.current_phase,
155
- slot_id: first.slot_id,
156
- role: first.role,
157
- blocking_on: openSlots.map((s) => s.slot_id),
158
- };
159
- }
160
- const assignedOrWorking = currentPhaseSlots.filter((s) => s.status === 'assigned' || s.status === 'working');
161
- if (assignedOrWorking.length > 0) {
162
- return {
163
- action: 'complete_turn',
164
- intent: 'bclaw_loop.complete_turn',
165
- phase: loop.current_phase,
166
- slot_id: assignedOrWorking[0].slot_id,
167
- role: assignedOrWorking[0].role,
168
- blocking_on: assignedOrWorking.map((s) => s.slot_id),
169
- };
170
- }
171
- const phaseNames = loop.phases.map((p) => p.name);
172
- const currentIndex = phaseNames.indexOf(loop.current_phase);
173
- if (currentIndex >= 0 && currentIndex + 1 < phaseNames.length) {
174
- return {
175
- action: 'advance',
176
- intent: 'bclaw_loop.advance',
177
- from_phase: loop.current_phase,
178
- to_phase: phaseNames[currentIndex + 1],
179
- blocking_on: [],
180
- };
181
- }
182
- return {
183
- action: 'close',
184
- intent: 'bclaw_loop.close',
185
- reason: 'terminal_phase_reached',
186
- blocking_on: [],
187
- };
188
- }
149
+ // computeNextExpected lives in src/core/loops/next-expected.ts (hoisted
150
+ // per can_e57c7782 follow-up same contract is now shared by both the
151
+ // MCP facade here and the CLI `brainclaw reply` command).
189
152
  function summarizeLoop(loop, autoClosed) {
190
153
  const suffix = autoClosed ? ' (auto-closed)' : '';
191
154
  return `✔ loop ${loop.id} [${loop.kind}] phase=${loop.current_phase} status=${loop.status}${suffix}`;
192
155
  }
156
+ /**
157
+ * pln#508 step 3 — lazy pause-timeout reconcile at facade entry.
158
+ *
159
+ * Phase 0 spec §6: any time the facade is invoked against a loop_id, sweep
160
+ * the target loop for timed-out operator_question artifacts BEFORE
161
+ * dispatching the intent. The downstream verb then sees the corrected state
162
+ * (e.g. a `cancel_loop` timeout already fired → mutating intents will get
163
+ * the natural `already cancelled` error from assertMutable; a `use_default`
164
+ * timeout already fired → open_questions reflects the synthesized answer).
165
+ *
166
+ * Best-effort: wrapped in try/catch so any reconcile error (corrupt loop on
167
+ * disk, fs hiccup, …) never blocks the facade. The handler proceeds with
168
+ * stale state in that case, which is no worse than the pre-step-3 behavior.
169
+ *
170
+ * Mirrors the lazy-reconcile pattern used by `agentrun-reconciler.ts` for
171
+ * agent_run silent completion (see entity-operations.ts loadAgentRunsWithReconciliation).
172
+ */
173
+ function trySweepLoopTimeouts(loop_id, cwd) {
174
+ try {
175
+ sweepPauseTimeouts(loop_id, undefined, cwd);
176
+ }
177
+ catch { /* best-effort: never block facade on sweep errors */ }
178
+ }
193
179
  export function handleBclawLoop(options) {
194
180
  const startMs = Date.now();
195
181
  const defaultActor = options.defaultActor ?? 'bclaw_loop';
@@ -204,6 +190,15 @@ export function handleBclawLoop(options) {
204
190
  return errorResponse(req.intent, 'validation_error', semanticError, Date.now() - startMs);
205
191
  }
206
192
  const { actor, agentId } = resolveActor(req, defaultActor);
193
+ // pln#508 step 3 — lazy pause-timeout reconcile at facade entry. Only
194
+ // for the `get` read-only intent (no withLockedLoopMutation wrapper).
195
+ // Mutating intents sweep INSIDE withLockedLoopMutation to keep all
196
+ // writes under the same loop lock as the caller's verb — see
197
+ // can_810ff9ec follow-up. `open` has no existing loop_id; `list`
198
+ // enumerates many loops (unbounded fan-out, not in scope).
199
+ if (req.intent === 'get' && typeof req.loop_id === 'string') {
200
+ trySweepLoopTimeouts(req.loop_id, options.cwd);
201
+ }
207
202
  try {
208
203
  switch (req.intent) {
209
204
  case 'open': {
@@ -309,6 +304,7 @@ export function handleBclawLoop(options) {
309
304
  type: req.artifact.type,
310
305
  body: req.artifact.body,
311
306
  ref: req.artifact.ref,
307
+ addresses_critique: req.artifact.addresses_critique,
312
308
  }
313
309
  : undefined,
314
310
  actor,
@@ -343,6 +339,7 @@ export function handleBclawLoop(options) {
343
339
  body: req.artifact.body,
344
340
  produced_by: req.artifact.produced_by,
345
341
  ref: req.artifact.ref,
342
+ addresses_critique: req.artifact.addresses_critique,
346
343
  },
347
344
  actor,
348
345
  }, options.cwd);
@@ -366,6 +363,54 @@ export function handleBclawLoop(options) {
366
363
  return successResponse('resume', { loop, next_expected: computeNextExpected(loop) }, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(loop));
367
364
  });
368
365
  }
366
+ case 'request_input': {
367
+ return withLockedLoopMutation(req, agentId, options.cwd, () => {
368
+ const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
369
+ const result = requestInput({
370
+ loop_id: req.loop_id,
371
+ slot_id: req.slot_id,
372
+ phase: req.phase,
373
+ question_text: req.question_text,
374
+ evidence: req.evidence,
375
+ suggested_default: req.suggested_default,
376
+ options: req.options,
377
+ pause_scope: req.pause_scope,
378
+ on_timeout: req.on_timeout,
379
+ timeout_at: req.timeout_at,
380
+ actor,
381
+ }, options.cwd);
382
+ const newEvents = findNewLoopEvents(result.thread.id, beforeEvents, options.cwd);
383
+ return successResponse('request_input', {
384
+ loop: result.thread,
385
+ question_id: result.question_id,
386
+ artifact_id: result.artifact_id,
387
+ next_expected: computeNextExpected(result.thread),
388
+ }, [loopArtifactEntry(result.thread.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', result.thread.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(result.thread));
389
+ });
390
+ }
391
+ case 'provide_input': {
392
+ return withLockedLoopMutation(req, agentId, options.cwd, () => {
393
+ const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
394
+ const result = provideInput({
395
+ loop_id: req.loop_id,
396
+ replies_to: req.replies_to,
397
+ resolved_via: req.resolved_via,
398
+ answer_text: req.answer_text,
399
+ chosen_option_id: req.chosen_option_id,
400
+ by: req.by,
401
+ actor,
402
+ }, options.cwd);
403
+ const newEvents = findNewLoopEvents(result.thread.id, beforeEvents, options.cwd);
404
+ return successResponse('provide_input', {
405
+ loop: result.thread,
406
+ artifact_id: result.artifact_id,
407
+ duplicate: result.duplicate,
408
+ next_expected: computeNextExpected(result.thread),
409
+ }, [loopArtifactEntry(result.thread.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', result.thread.id), ...loopEventSideEffects(newEvents)], result.duplicate
410
+ ? ['provide_input: idempotent replay — replies_to was already resolved; returning existing answer']
411
+ : [], Date.now() - startMs, summarizeLoop(result.thread));
412
+ });
413
+ }
369
414
  case 'close': {
370
415
  return withLockedLoopMutation(req, agentId, options.cwd, () => {
371
416
  const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
@@ -395,6 +440,16 @@ export function handleBclawLoop(options) {
395
440
  if (err instanceof LockLostError) {
396
441
  return errorResponse(req.intent, 'lock_lost', err.message, Date.now() - startMs);
397
442
  }
443
+ if (err instanceof AwaitingFileApplyApprovalError) {
444
+ // pln#512 phase 3 codex review fix #1: surface structurally so callers
445
+ // can branch on the code without parsing the message text.
446
+ return errorResponse(req.intent, 'awaiting_file_apply_approval', err.message, Date.now() - startMs, {
447
+ loop_id: err.loop_id,
448
+ question_id: err.question_id,
449
+ target_path: err.target_path,
450
+ diff_artifact_id: err.diff_artifact_id,
451
+ });
452
+ }
398
453
  const message = err instanceof Error ? err.message : String(err);
399
454
  if (message.includes('unauthorized_slot_write')) {
400
455
  return errorResponse(req.intent, 'unauthorized_slot_write', message, Date.now() - startMs);
@@ -13,6 +13,7 @@ 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 { getDispatchStatus } from '../core/dispatch-status.js';
16
17
  import { listActionRequired } from '../core/actions.js';
17
18
  import { queryRuntimeEvents } from '../core/events.js';
18
19
  import { listSequences, getActiveSequence } from '../core/sequence.js';
@@ -1627,6 +1628,42 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1627
1628
  structuredContent: { thread_id: threadId, total: messages.length, messages, schema_version: SCHEMA_VERSION },
1628
1629
  };
1629
1630
  }
1631
+ if (name === 'bclaw_dispatch_status') {
1632
+ const targetId = String(args.target_id ?? '');
1633
+ if (!targetId) {
1634
+ throw new Error('Missing required argument: target_id (asgn_/clm_/lop_/run_)');
1635
+ }
1636
+ const tailLogLines = typeof args.tail_log_lines === 'number' ? args.tail_log_lines : undefined;
1637
+ const stallThresholdMs = typeof args.stall_threshold_ms === 'number' ? args.stall_threshold_ms : undefined;
1638
+ const status = getDispatchStatus({
1639
+ target_id: targetId,
1640
+ cwd,
1641
+ tail_log_lines: tailLogLines,
1642
+ stall_threshold_ms: stallThresholdMs,
1643
+ });
1644
+ // Text view: short, single-screen summary so an agent can decide what to do
1645
+ // without parsing the structured payload.
1646
+ const lines = [
1647
+ `Dispatch status for ${targetId} (resolved_from=${status.resolved_from})`,
1648
+ `Health: ${status.diagnosis.health}`,
1649
+ `Summary: ${status.diagnosis.summary}`,
1650
+ `Next action: ${status.diagnosis.recommended_next_action}`,
1651
+ '',
1652
+ 'Entities:',
1653
+ ` assignment=${status.entities.assignment_id ?? '-'} (status=${status.assignment?.status ?? '-'})`,
1654
+ ` claim=${status.entities.claim_id ?? '-'} (status=${status.claim?.status ?? '-'})`,
1655
+ ` loop=${status.entities.loop_id ?? '-'} (phase=${status.loop?.current_phase ?? '-'})`,
1656
+ ` run=${status.entities.run_id ?? '-'} (status=${status.agent_run?.status ?? '-'})`,
1657
+ '',
1658
+ `Runtime: pid=${status.runtime.pid ?? '-'} alive=${status.runtime.pid_alive ?? 'unknown'} ack=${status.runtime.ack_file.exists}`,
1659
+ ` stdout: ${status.runtime.log_files.stdout?.exists ? `${status.runtime.log_files.stdout.size_bytes}B` : 'absent'}`,
1660
+ ` stderr: ${status.runtime.log_files.stderr?.exists ? `${status.runtime.log_files.stderr.size_bytes}B` : 'absent'}`,
1661
+ ];
1662
+ return {
1663
+ content: [{ type: 'text', text: lines.join('\n') }],
1664
+ structuredContent: { ...status, schema_version: SCHEMA_VERSION },
1665
+ };
1666
+ }
1630
1667
  throw new Error(`Unknown read tool: ${name}`);
1631
1668
  }
1632
1669
  //# sourceMappingURL=mcp-read-handlers.js.map