brainclaw 1.16.0 → 1.18.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 (85) hide show
  1. package/README.md +22 -8
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-review.js +2 -2
  4. package/dist/commands/code-map.js +4 -1
  5. package/dist/commands/codev.js +61 -30
  6. package/dist/commands/doctor.js +14 -1
  7. package/dist/commands/harvest.js +241 -25
  8. package/dist/commands/inbox.js +10 -4
  9. package/dist/commands/loop.js +2 -2
  10. package/dist/commands/loops-handlers.js +82 -1
  11. package/dist/commands/mcp-catalog.js +12 -4
  12. package/dist/commands/mcp-read-handlers.js +90 -7
  13. package/dist/commands/mcp-schemas.generated.js +3 -0
  14. package/dist/commands/mcp-write-coordination.js +159 -40
  15. package/dist/commands/mcp.js +11 -2
  16. package/dist/core/agent-capability.js +7 -2
  17. package/dist/core/agent-files.js +53 -2
  18. package/dist/core/agent-integrations.js +1 -0
  19. package/dist/core/agentrun-reconciler.js +171 -7
  20. package/dist/core/agentruns.js +6 -1
  21. package/dist/core/code-map/aggregate.js +473 -0
  22. package/dist/core/code-map/backend.js +36 -10
  23. package/dist/core/code-map/freshness.js +36 -1
  24. package/dist/core/code-map/lang/c/imports.scm +12 -0
  25. package/dist/core/code-map/lang/c/index.js +150 -0
  26. package/dist/core/code-map/lang/c/tags.scm +68 -0
  27. package/dist/core/code-map/lang/cpp/imports.scm +14 -0
  28. package/dist/core/code-map/lang/cpp/index.js +149 -0
  29. package/dist/core/code-map/lang/cpp/tags.scm +87 -0
  30. package/dist/core/code-map/lang/csharp/imports.scm +20 -0
  31. package/dist/core/code-map/lang/csharp/index.js +224 -0
  32. package/dist/core/code-map/lang/csharp/tags.scm +63 -0
  33. package/dist/core/code-map/lang/go/imports.scm +13 -0
  34. package/dist/core/code-map/lang/go/index.js +139 -0
  35. package/dist/core/code-map/lang/go/tags.scm +36 -0
  36. package/dist/core/code-map/lang/providers.js +12 -1
  37. package/dist/core/code-map/lang/ruby/imports.scm +24 -0
  38. package/dist/core/code-map/lang/ruby/index.js +198 -0
  39. package/dist/core/code-map/lang/ruby/tags.scm +49 -0
  40. package/dist/core/code-map/lang/rust/imports.scm +44 -0
  41. package/dist/core/code-map/lang/rust/index.js +136 -0
  42. package/dist/core/code-map/lang/rust/tags.scm +47 -0
  43. package/dist/core/code-map/query.js +229 -80
  44. package/dist/core/code-map/types.js +18 -0
  45. package/dist/core/code-map/work-section.js +8 -7
  46. package/dist/core/codev-responses.js +16 -0
  47. package/dist/core/dispatcher.js +209 -29
  48. package/dist/core/execution-adapters.js +29 -3
  49. package/dist/core/ideation-loop-close.js +124 -0
  50. package/dist/core/loops/artifact-resolver.js +197 -0
  51. package/dist/core/loops/attempt-reservation.js +576 -0
  52. package/dist/core/loops/commit-intent.js +494 -0
  53. package/dist/core/loops/facade-schema.js +48 -0
  54. package/dist/core/loops/impl-bind.js +144 -0
  55. package/dist/core/loops/index.js +1 -1
  56. package/dist/core/loops/iteration-engine.js +29 -0
  57. package/dist/core/loops/lock.js +14 -0
  58. package/dist/core/loops/project-resolution.js +157 -0
  59. package/dist/core/loops/reconcile-turn.js +369 -0
  60. package/dist/core/loops/result-reducers.js +88 -0
  61. package/dist/core/loops/store.js +46 -7
  62. package/dist/core/loops/types.js +139 -11
  63. package/dist/core/loops/verbs.js +9 -3
  64. package/dist/core/loops/verify-command.js +209 -0
  65. package/dist/core/messaging.js +58 -5
  66. package/dist/core/review-loop-close.js +106 -34
  67. package/dist/core/review-loop-turn-dispatch.js +445 -0
  68. package/dist/core/runtime-signals.js +68 -0
  69. package/dist/core/schema.js +34 -0
  70. package/dist/core/worktree.js +240 -22
  71. package/dist/facts.js +10 -10
  72. package/dist/facts.json +9 -9
  73. package/dist/wasm/tree-sitter-c.wasm +0 -0
  74. package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
  75. package/dist/wasm/tree-sitter-cpp.wasm +0 -0
  76. package/dist/wasm/tree-sitter-go.wasm +0 -0
  77. package/dist/wasm/tree-sitter-ruby.wasm +0 -0
  78. package/dist/wasm/tree-sitter-rust.wasm +0 -0
  79. package/docs/cli.md +1 -1
  80. package/docs/code-map.md +22 -6
  81. package/docs/concepts/loop-engine.md +28 -2
  82. package/docs/concepts/observer-protocol.md +22 -0
  83. package/docs/integrations/codex.md +19 -3
  84. package/docs/mcp-schema-changelog.md +43 -1
  85. package/package.json +1 -1
@@ -38,7 +38,8 @@ import { loadClaim, releaseClaim } from './claims.js';
38
38
  import { loadAssignment } from './assignments.js';
39
39
  import { createRuntimeEvent } from './events.js';
40
40
  import { nowISO } from './ids.js';
41
- import { readHeartbeat, readLogTail, signalExists, latestActivityMs } from './runtime-signals.js';
41
+ import { readHeartbeat, readLogTail, signalExists, latestActivityMs, readCompletionSignals } from './runtime-signals.js';
42
+ import { findReservationByRunId, evidenceMatchesAttempt, launchGrant, revokeLaunchGrant } from './loops/attempt-reservation.js';
42
43
  // ── Constants ──────────────────────────────────────────────────────────────
43
44
  /**
44
45
  * Minimum age before a run is eligible for reconciliation. Below this, the
@@ -188,9 +189,51 @@ export function collectEvidence(run, cwd, options) {
188
189
  fs_activity_age_ms = now - lastFs;
189
190
  }
190
191
  catch { /* defensive */ }
192
+ // pln#630 PR2b-c (§13 R3) — read-strict gate. A run owned by a turn-attempt
193
+ // reservation is completed ONLY on turn-keyed evidence: a `completed` sentinel
194
+ // body whose turn_id/run_id/nonce match the attempt's CURRENT launch
195
+ // generation. A bare assignment-keyed presence sentinel — or a stale prior
196
+ // generation's body — never counts (closes phantom-completion). Legacy runs
197
+ // (no owning reservation) keep presence-based acceptance.
198
+ let turn_owned = false;
199
+ let turn_keyed_completed = false;
200
+ try {
201
+ const reservation = findReservationByRunId(run.id, cwd);
202
+ if (reservation) {
203
+ turn_owned = true;
204
+ const bodies = readCompletionSignals(signalRoot, run.assignment_id);
205
+ // Evidence must be turn-keyed AND carry the RIGHT status (a `.completed`
206
+ // file whose body says status:'failed' is not completion evidence —
207
+ // read-strict trusts the body, not the filename, review PR2b-c #C2).
208
+ const matchedCompleted = bodies.completed !== undefined
209
+ && bodies.completed.status === 'completed'
210
+ && evidenceMatchesAttempt(reservation, bodies.completed);
211
+ const matchedFailed = bodies.failed !== undefined
212
+ && bodies.failed.status === 'failed'
213
+ && evidenceMatchesAttempt(reservation, bodies.failed);
214
+ if (matchedCompleted && matchedFailed) {
215
+ // §13 R4 — a completed+failed contradiction WITHHOLDS both (never a
216
+ // silent accept). Conflict-event journaling is deferred to
217
+ // reconcileTurn (PR3); collectEvidence stays side-effect-free.
218
+ turn_keyed_completed = false;
219
+ failed_signal = false;
220
+ }
221
+ else {
222
+ turn_keyed_completed = matchedCompleted;
223
+ // Read-strict governs FAILURE too for a turn-owned run: override the
224
+ // presence-based failed_signal so a stale/legacy bare `.failed` marker
225
+ // cannot phantom-FAIL a healthy generation (symmetric to completed,
226
+ // review PR2b-c #B). A genuine death with no turn-keyed evidence still
227
+ // fails via the process-dead + stale path below.
228
+ failed_signal = matchedFailed;
229
+ }
230
+ }
231
+ }
232
+ catch { /* defensive — fall back to legacy (non-turn-owned) behavior */ }
191
233
  return {
192
234
  age_ms, has_post_start_commit, claim_released, assignment_completed, process_alive,
193
235
  completed_signal, failed_signal, heartbeat_exists, heartbeat_age_ms, fs_activity_age_ms,
236
+ turn_owned, turn_keyed_completed,
194
237
  };
195
238
  }
196
239
  /**
@@ -209,7 +252,7 @@ function fsActiveWithin(evidence, windowMs) {
209
252
  * Inference only fires after the stale window with no life evidence, so this is
210
253
  * conservative. (Loop auto-close on failure is a follow-up.)
211
254
  */
212
- function cascadeReleaseOnFailure(run, actor, cwd) {
255
+ function cascadeReleaseOnFailure(run, actor, cwd, terminalStatus = 'failed') {
213
256
  if (!run.claim_id)
214
257
  return;
215
258
  try {
@@ -219,8 +262,8 @@ function cascadeReleaseOnFailure(run, actor, cwd) {
219
262
  createRuntimeEvent({
220
263
  agent: actor,
221
264
  session_id: run.session_id,
222
- event_type: 'run_failed',
223
- text: `Auto-released claim ${run.claim_id} after run ${run.id} was reconciled to failed (trp#433 GC cascade)`,
265
+ event_type: terminalStatus === 'cancelled' ? 'run_cancelled' : 'run_failed',
266
+ text: `Auto-released claim ${run.claim_id} after run ${run.id} was reconciled to ${terminalStatus} (trp#433 GC cascade)`,
224
267
  tags: ['reconciler', 'gc', 'claim-release'],
225
268
  assignment_id: run.assignment_id,
226
269
  run_id: run.id,
@@ -232,6 +275,13 @@ function cascadeReleaseOnFailure(run, actor, cwd) {
232
275
  catch { /* best-effort — never let GC break reconciliation */ }
233
276
  }
234
277
  function anyCompletionEvidence(evidence) {
278
+ // pln#630 PR2b-c (§13 R3): a turn-owned run is completed ONLY on turn-keyed
279
+ // evidence — never a bare presence sentinel or an assignment-keyed proxy
280
+ // (claim_released / assignment_completed / post_start_commit). This is the
281
+ // read-strict split that closes the stale-sentinel + presence-only
282
+ // phantom-completion holes. Legacy (non-turn-owned) runs are unchanged.
283
+ if (evidence.turn_owned)
284
+ return evidence.turn_keyed_completed;
235
285
  return evidence.completed_signal
236
286
  || evidence.has_post_start_commit
237
287
  || evidence.claim_released
@@ -345,7 +395,7 @@ export function reconcileAgentRun(runId, cwd, options = {}) {
345
395
  const evidence = {
346
396
  age_ms: 0, has_post_start_commit: false, claim_released: false,
347
397
  assignment_completed: false, process_alive: undefined,
348
- completed_signal: false, failed_signal: false, heartbeat_exists: false,
398
+ completed_signal: false, failed_signal: false, heartbeat_exists: false, turn_owned: false, turn_keyed_completed: false,
349
399
  };
350
400
  return {
351
401
  run_id: runId, action: 'no_op', reason: 'run not found', evidence,
@@ -361,6 +411,18 @@ export function reconcileAgentRun(runId, cwd, options = {}) {
361
411
  evidence, previous_status, current_status: run.status,
362
412
  };
363
413
  }
414
+ // pln#630 PR2c-lease (§4 + R5): a turn-owned run preallocated `created`/
415
+ // `launching` converges on its DISPATCH/LAUNCH LEASE, not the pid/heartbeat
416
+ // heuristics below (which assume a worker already spawned and can emit
417
+ // life-signs). Delegate before the generic evidence path so these runs never
418
+ // orphan `created`/`launching` forever. Inert for non-turn-owned runs
419
+ // (evidence.turn_owned=false → fall through to the unchanged generic path).
420
+ if ((run.status === 'created' || run.status === 'launching') && evidence.turn_owned) {
421
+ const reservation = findReservationByRunId(run.id, cwd);
422
+ if (reservation) {
423
+ return reconcileTurnOwnedPreRunLease(run, reservation, evidence, cwd, options);
424
+ }
425
+ }
364
426
  const grace = options.healthCheckGraceMs ?? DEFAULT_HEALTH_CHECK_GRACE_MS;
365
427
  const stale = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
366
428
  const actor = options.actor ?? 'reconciler';
@@ -450,6 +512,84 @@ export function reconcileAgentRun(runId, cwd, options = {}) {
450
512
  evidence, previous_status, current_status: run.status,
451
513
  };
452
514
  }
515
+ /**
516
+ * pln#630 PR2c-lease (§4 + R5) — converge a TURN-OWNED run still in `created`/
517
+ * `launching` against its dispatch/launch LEASE, not the pid/heartbeat heuristics
518
+ * (which presuppose a spawned, life-sign-emitting worker). Reached only from
519
+ * `reconcileAgentRun`'s turn-owned delegate branch, so it always has the owning
520
+ * reservation + pre-collected evidence.
521
+ *
522
+ * Outcomes (never `completed` — no phantom success from an unspawned/unproven run):
523
+ * - turn-keyed completion present → NO-OP (finalization is reconcileTurn's job, PR3);
524
+ * - within lease → NO-OP (the worker may yet cross the launch fence / start);
525
+ * - past lease + launch grant CROSSED → `failed` / `launch_attempted_unknown`
526
+ * (the worker WAS invoked; its outcome is unknowable, so it must not complete);
527
+ * - past lease + grant not crossed → `cancelled` / `reserved_never_launched`
528
+ * (no launch receipt — nothing ran; matches attemptStatus(revoked)='cancelled').
529
+ */
530
+ function reconcileTurnOwnedPreRunLease(run, reservation, evidence, cwd, options) {
531
+ const now = options.nowMs ?? Date.now();
532
+ const actor = options.actor ?? 'reconciler';
533
+ const previous_status = run.status;
534
+ // A genuine turn-keyed completion is NOT an expiry: never fail/cancel a run
535
+ // that produced accepted evidence. Its finalization (assignment/claim/artifacts)
536
+ // is reconcileTurn's responsibility (PR3); here we simply decline to expire it.
537
+ if (evidence.turn_keyed_completed) {
538
+ return {
539
+ run_id: run.id, action: 'no_op',
540
+ reason: 'turn-keyed completion present — defer finalization to reconcileTurn',
541
+ evidence, previous_status, current_status: run.status,
542
+ };
543
+ }
544
+ // Effective deadline: once armed, the launch lease is the tighter authoritative
545
+ // bound; before arm, the dispatch lease bounds how long a committed reservation
546
+ // may wait to spawn. A non-parseable lease cannot expire a run (fail-safe: leave
547
+ // it for explicit reconcile rather than converge on garbage).
548
+ const leaseISO = reservation.launch?.lease_deadline ?? reservation.lease_deadline;
549
+ const leaseMs = Date.parse(leaseISO);
550
+ if (!Number.isFinite(leaseMs) || now < leaseMs) {
551
+ return {
552
+ run_id: run.id, action: 'no_op',
553
+ reason: Number.isFinite(leaseMs)
554
+ ? `within lease (deadline ${leaseISO})`
555
+ : `lease deadline unparseable (${leaseISO}) — cannot expire`,
556
+ evidence, previous_status, current_status: run.status,
557
+ };
558
+ }
559
+ // Past lease, no accepted completion — the launch-grant status (authoritative,
560
+ // decision-file reconciled) decides the terminal.
561
+ const grant = launchGrant(reservation.turn_id, cwd);
562
+ const crossed = grant?.status === 'crossed';
563
+ const targetStatus = crossed ? 'failed' : 'cancelled';
564
+ const action = crossed ? 'inferred_failed' : 'inferred_cancelled';
565
+ const reason = crossed
566
+ ? `launch_attempted_unknown: launch grant crossed but run never reached running by lease ${leaseISO} — outcome unknown, never completed`
567
+ : `reserved_never_launched: no launch receipt by lease ${leaseISO} (grant=${grant?.status ?? 'none'})`;
568
+ // pln#630 dec#149 R1 (review Finding 2) — make the reserved_never_launched strand REACHABLE
569
+ // through the WIRED lazy reconciler: revoke the still-armed grant so its authoritative status
570
+ // becomes `revoked`. That is what lets reconcileTurn's fix-cycle strand detector see the strand
571
+ // and re-emit, and lets the re-dispatch re-arm a fresh generation (dec#149 R1). Idempotent +
572
+ // best-effort: a race to crossed/revoked is fine (the decision file governs) and must never
573
+ // block the run's terminal transition below.
574
+ if (!crossed && grant?.status === 'armed') {
575
+ try {
576
+ revokeLaunchGrant(reservation.turn_id, grant.epoch, 'reserved_never_launched', cwd, actor);
577
+ }
578
+ catch { /* raced to crossed/revoked — authoritative status governs */ }
579
+ }
580
+ try {
581
+ transitionAgentRun(run.id, targetStatus, { actor, status_reason: reason }, cwd);
582
+ cascadeReleaseOnFailure(run, actor, cwd, targetStatus);
583
+ return { run_id: run.id, action, reason, evidence, previous_status, current_status: targetStatus };
584
+ }
585
+ catch (err) {
586
+ return {
587
+ run_id: run.id, action: 'no_op',
588
+ reason: `lease-expiry transition rejected: ${err instanceof Error ? err.message : String(err)}`,
589
+ evidence, previous_status, current_status: run.status,
590
+ };
591
+ }
592
+ }
453
593
  /**
454
594
  * Read-path reconciliation for a `running` run whose tracked PID reads dead.
455
595
  *
@@ -478,7 +618,7 @@ export function reconcileDeadPidRunningAgentRunAtRead(runId, cwd, options = {})
478
618
  const evidence = {
479
619
  age_ms: 0, has_post_start_commit: false, claim_released: false,
480
620
  assignment_completed: false, process_alive: undefined,
481
- completed_signal: false, failed_signal: false, heartbeat_exists: false,
621
+ completed_signal: false, failed_signal: false, heartbeat_exists: false, turn_owned: false, turn_keyed_completed: false,
482
622
  };
483
623
  return {
484
624
  run_id: runId, action: 'no_op', reason: 'run not found', evidence,
@@ -610,6 +750,30 @@ export function sweepDeadPidRunningAgentRunsAtRead(cwd, options = {}) {
610
750
  .slice(0, limit);
611
751
  return candidates.map((run) => reconcileDeadPidRunningAgentRunAtRead(run.id, cwd, options));
612
752
  }
753
+ /**
754
+ * pln#630 PR2c-lease — lazy read-path sweep for TURN-OWNED runs still in
755
+ * `created`/`launching`, converging them on their dispatch/launch lease via
756
+ * `reconcileAgentRun`'s turn-owned delegate. Strictly turn-owned: a legacy
757
+ * (non-reservation-owned) `created`/`launching` run is SKIPPED here, so this
758
+ * sweep changes nothing for existing runs and is fully inert until live
759
+ * turn-owned dispatch (PR2c-b) mints such a run. Errors are isolated per run.
760
+ */
761
+ export function sweepTurnOwnedPreRunLeaseAtRead(cwd, options = {}) {
762
+ const results = [];
763
+ for (const status of ['created', 'launching']) {
764
+ for (const run of listAgentRuns(cwd, { status })) {
765
+ // Only turn-owned runs converge on the lease here — legacy runs keep their
766
+ // existing (non-read-path) reconciliation route untouched.
767
+ if (!findReservationByRunId(run.id, cwd))
768
+ continue;
769
+ try {
770
+ results.push(reconcileAgentRun(run.id, cwd, options));
771
+ }
772
+ catch { /* best-effort — never block reads on reconciliation errors */ }
773
+ }
774
+ }
775
+ return results;
776
+ }
613
777
  /**
614
778
  * Reconcile every non-terminal agent_run matching `filter`. Useful for
615
779
  * batch sweeps from `bclaw_assignment_events` or `brainclaw doctor --dispatch`.
@@ -628,7 +792,7 @@ export function reconcileAllOpenRuns(cwd, filter = {}, options = {}) {
628
792
  catch {
629
793
  results.push({
630
794
  run_id: run.id, action: 'no_op', reason: 'reconcile threw — skipped',
631
- evidence: { age_ms: 0, has_post_start_commit: false, claim_released: false, assignment_completed: false, process_alive: undefined, completed_signal: false, failed_signal: false, heartbeat_exists: false },
795
+ evidence: { age_ms: 0, has_post_start_commit: false, claim_released: false, assignment_completed: false, process_alive: undefined, completed_signal: false, failed_signal: false, heartbeat_exists: false, turn_owned: false, turn_keyed_completed: false },
632
796
  previous_status: run.status, current_status: run.status,
633
797
  });
634
798
  }
@@ -101,7 +101,12 @@ export function findLatestAgentRunForAssignment(assignmentId, cwd) {
101
101
  })[0];
102
102
  }
103
103
  const VALID_TRANSITIONS = new Map([
104
- ['created', new Set(['launching', 'waiting_input', 'running', 'cancelled', 'interrupted'])],
104
+ // `failed` added (pln#630 PR2c-lease): a turn-owned run preallocated `created`
105
+ // whose launch grant CROSSED but crashed before the created→launching
106
+ // transition is `launch_attempted_unknown` — a failure, reconciled straight
107
+ // from `created`. (reserved_never_launched, where the grant never crossed,
108
+ // goes to `cancelled`, already permitted.)
109
+ ['created', new Set(['launching', 'waiting_input', 'running', 'failed', 'cancelled', 'interrupted'])],
105
110
  ['launching', new Set(['waiting_input', 'running', 'failed', 'cancelled', 'timed_out', 'interrupted'])],
106
111
  ['waiting_input', new Set(['launching', 'running', 'blocked', 'cancelled', 'timed_out', 'interrupted'])],
107
112
  ['running', new Set(['blocked', 'completed', 'failed', 'cancelled', 'timed_out', 'interrupted'])],