brainclaw 1.26.2 → 1.28.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 (88) hide show
  1. package/README.md +13 -0
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-coordination.js +65 -1
  4. package/dist/commands/attempt-authority.js +80 -0
  5. package/dist/commands/harvest.js +140 -61
  6. package/dist/commands/loop.js +34 -0
  7. package/dist/commands/loops-handlers.js +143 -15
  8. package/dist/commands/mcp-catalog.js +52 -18
  9. package/dist/commands/mcp-schemas.generated.js +64 -0
  10. package/dist/commands/mcp-write-claims.js +128 -1
  11. package/dist/commands/mcp-write-coordination.js +149 -76
  12. package/dist/core/agent-capability.js +1 -1
  13. package/dist/core/agentrun-reconciler.js +148 -22
  14. package/dist/core/agentruns.js +254 -29
  15. package/dist/core/assignment-request-schema.js +7 -0
  16. package/dist/core/assignment-sweeper.js +5 -3
  17. package/dist/core/assignments.js +131 -33
  18. package/dist/core/claim-request-schema.js +7 -0
  19. package/dist/core/claims.js +53 -2
  20. package/dist/core/dispatch-status.js +16 -6
  21. package/dist/core/dispatcher.js +51 -51
  22. package/dist/core/entity-operations.js +20 -0
  23. package/dist/core/events.js +4 -0
  24. package/dist/core/execution-adapters.js +189 -14
  25. package/dist/core/execution-contract.js +345 -0
  26. package/dist/core/execution.js +130 -16
  27. package/dist/core/facade-schema.js +3 -0
  28. package/dist/core/harness-adapters/base.js +150 -0
  29. package/dist/core/harness-adapters/claude.js +39 -0
  30. package/dist/core/harness-adapters/codex.js +57 -0
  31. package/dist/core/harness-adapters/harvest.js +109 -0
  32. package/dist/core/harness-adapters/index.js +8 -0
  33. package/dist/core/harness-adapters/prompt-only.js +13 -0
  34. package/dist/core/harness-adapters/registry.js +48 -0
  35. package/dist/core/harness-adapters/result.js +33 -0
  36. package/dist/core/harness-adapters/types.js +2 -0
  37. package/dist/core/ideation-loop-close.js +25 -2
  38. package/dist/core/instruction-templates.js +3 -2
  39. package/dist/core/loop-turn-dispatch.js +235 -0
  40. package/dist/core/loops/artifact-contract.js +11 -0
  41. package/dist/core/loops/attempt-authority.js +496 -0
  42. package/dist/core/loops/attempt-generations.js +509 -0
  43. package/dist/core/loops/attempt-reservation.js +197 -35
  44. package/dist/core/loops/attempt-rollout.js +404 -0
  45. package/dist/core/loops/attempt-takeover.js +155 -0
  46. package/dist/core/loops/bootstrap-acquire.js +7 -3
  47. package/dist/core/loops/brief-assembly.js +21 -4
  48. package/dist/core/loops/evidence.js +188 -0
  49. package/dist/core/loops/facade-schema.js +75 -11
  50. package/dist/core/loops/gate-policy.js +533 -0
  51. package/dist/core/loops/impl-bind.js +91 -81
  52. package/dist/core/loops/index.js +9 -0
  53. package/dist/core/loops/iteration-engine.js +31 -19
  54. package/dist/core/loops/kind-policies.js +90 -0
  55. package/dist/core/loops/lock.js +71 -13
  56. package/dist/core/loops/reconcile-turn.js +237 -18
  57. package/dist/core/loops/result-reducers.js +113 -10
  58. package/dist/core/loops/store.js +34 -3
  59. package/dist/core/loops/turn-execution.js +480 -0
  60. package/dist/core/loops/types.js +127 -3
  61. package/dist/core/loops/verbs.js +335 -99
  62. package/dist/core/loops/verify-command.js +105 -20
  63. package/dist/core/loops/workspace-digest.js +54 -0
  64. package/dist/core/review-loop-close.js +25 -3
  65. package/dist/core/review-loop-turn-dispatch.js +210 -161
  66. package/dist/core/runtime-signals.js +62 -25
  67. package/dist/core/schema.js +40 -0
  68. package/dist/core/spawn-check.js +3 -2
  69. package/dist/core/upgrades/backup.js +27 -4
  70. package/dist/facts.js +9 -8
  71. package/dist/facts.json +8 -7
  72. package/docs/cli.md +49 -1
  73. package/docs/concepts/attempt-authority.md +407 -0
  74. package/docs/concepts/evidence-attestations.md +135 -0
  75. package/docs/concepts/execution-contract.md +166 -0
  76. package/docs/concepts/harness-adapters.md +166 -0
  77. package/docs/concepts/ideation-loop.md +5 -4
  78. package/docs/concepts/loop-engine.md +302 -113
  79. package/docs/index.md +4 -1
  80. package/docs/integrations/codex.md +3 -3
  81. package/docs/integrations/mcp.md +59 -5
  82. package/docs/loops/debug.md +144 -0
  83. package/docs/loops/ideation.md +158 -0
  84. package/docs/loops/implementation.md +174 -0
  85. package/docs/loops/research.md +136 -0
  86. package/docs/loops/review.md +200 -0
  87. package/docs/mcp-schema-changelog.md +18 -5
  88. package/package.json +1 -1
@@ -14,7 +14,7 @@
14
14
  import crypto from 'node:crypto';
15
15
  import { buildClaimEnvPrefix } from '../core/execution-profile.js';
16
16
  import { resolveProjectCwd } from '../core/cross-project.js';
17
- import { attachAssignmentMessageToClaim, createCoordinatorClaim, linkClaimToAssignment, listClaims, saveClaim, } from '../core/claims.js';
17
+ import { attachAssignmentMessageToClaim, createCoordinatorClaim, linkClaimToAssignment, listClaims, releaseClaimIfActive, saveClaim, } from '../core/claims.js';
18
18
  import { ensureAgentRegisteredForDispatch, findAgentIdentityById, findAgentIdentityByName, } from '../core/agent-registry.js';
19
19
  import { appendAuditEntry } from '../core/audit.js';
20
20
  import { nowISO } from '../core/ids.js';
@@ -26,10 +26,13 @@ import { agentValidationFailedWarning, consultAutoExecuteNoOpWarning, planAlread
26
26
  import { ackMessage, getThread, hasActiveAssignment, sendMessage } from '../core/messaging.js';
27
27
  import { dispatch, dispatchReview, generateDispatchBrief } from '../core/dispatcher.js';
28
28
  import { CoordinateRequestSchema } from '../core/facade-schema.js';
29
- import { buildInvokeCommand, getCapabilityProfile, getSpawnableAgents, resolveModel, validateAgentForDispatch, } from '../core/agent-capability.js';
29
+ import { getCapabilityProfile, getSpawnableAgents, resolveModel, validateAgentForDispatch, } from '../core/agent-capability.js';
30
+ import { buildHarnessInvocation, resolveHarnessBinding } from '../core/harness-adapters/index.js';
30
31
  import { attemptExecution } from '../core/execution.js';
31
32
  import { createAgentRun, transitionAgentRun } from '../core/agentruns.js';
32
33
  import { prepareTurnOwnedReviewDispatch, turnOwnedReviewEnabled } from '../core/review-loop-turn-dispatch.js';
34
+ import { prepareTurnExecution } from '../core/loops/turn-execution.js';
35
+ import { removeWorktree } from '../core/worktree.js';
33
36
  import { createAssignment, generateAssignmentId, patchAssignmentMessageId, transitionAssignment, } from '../core/assignments.js';
34
37
  import { createToolErrorResponse, toolResponse, } from './mcp-contract.js';
35
38
  import { handleMcpReadToolCall } from './mcp-read-handlers.js';
@@ -492,16 +495,16 @@ export async function handleBclawCoordinate(args, ctx) {
492
495
  if (execResult.error)
493
496
  opts.warnings.push(`${entry.agent}: ${execResult.error}`);
494
497
  if (turnEcho) {
495
- // pln#630 risk #1 — a turn-owned reviewer's run was ALREADY created (`created`) by
496
- // prepareTurnOwnedReviewDispatch. Do NOT mint a second run here (double-mint). Transition
497
- // the deterministic run → running on a real spawn (mirrors dispatchReviewLoopTurn); leave
498
+ // A turn-owned loop run was ALREADY created (`created`) before crossing.
499
+ // Do NOT mint a second run here (double-mint). Transition the
500
+ // deterministic run → running on a real spawn; leave
498
501
  // it `created` otherwise so the no-sentinel legacy fallback (turnOwnedLaneEvidence) + the
499
502
  // pre-run lease reconciler govern it. Non-turn-owned entries take the unchanged else-branch.
500
503
  if (execResult.execution_status === 'delivered_and_started') {
501
504
  try {
502
505
  transitionAgentRun(turnEcho.run_id, 'running', {
503
506
  actor: opts.senderAgent, actor_id: opts.senderAgentId, pid: execResult.pid,
504
- status_reason: 'turn-owned reviewer spawned by coordinator',
507
+ status_reason: 'turn-owned loop worker spawned by coordinator',
505
508
  }, opts.cwd);
506
509
  }
507
510
  catch { /* best-effort — the reconciler converges if this races */ }
@@ -626,6 +629,8 @@ export async function handleBclawCoordinate(args, ctx) {
626
629
  scope: options?.scope,
627
630
  worktreePath: options?.worktreePath,
628
631
  assignmentId: options?.assignmentId,
632
+ executionContractRef: options?.executionContractRef,
633
+ attemptFence: options?.attemptFence,
629
634
  // pln#638 PR-6b — the envelope must read the TARGET project's store: on a
630
635
  // cross-project dispatch, defaulting to process.cwd() would inline the
631
636
  // WRONG project's constraints/traps into the worker's brief.
@@ -659,7 +664,7 @@ export async function handleBclawCoordinate(args, ctx) {
659
664
  }, dispatchCwd);
660
665
  artifacts.push({ type: 'message', id: msgResult.id });
661
666
  side_effects.push({ action: 'create', entity: 'message', id: msgResult.id });
662
- const invoke = buildInvokeCommand(input.agent, input.text, {
667
+ const invoke = buildHarnessInvocation(input.agent, input.text, {
663
668
  mode: input.commandMode ?? 'worker',
664
669
  // pln#520/#606 — decouple model from agent identity. req.model is the
665
670
  // override link; when unset, resolveModel intentionally falls back to
@@ -670,7 +675,8 @@ export async function handleBclawCoordinate(args, ctx) {
670
675
  // (gpt-5.6-luna review). Flows to both the manual commandHint and the
671
676
  // auto-spawn path (runCoordinateExecution reuses this invoke).
672
677
  model: resolveModel(input.agent, { override: req.model }),
673
- });
678
+ binding: input.harnessBinding,
679
+ })?.invoke;
674
680
  // Build env prefix for claim routing — centralised in
675
681
  // execution-profile.ts:buildClaimEnvPrefix as of pln#496 step
676
682
  // stp_a9afe59d (handles all five shells, not just Windows/POSIX).
@@ -996,6 +1002,7 @@ export async function handleBclawCoordinate(args, ctx) {
996
1002
  created_by: creatorActor,
997
1003
  slots,
998
1004
  mode: req.review_mode ?? 'asymmetric',
1005
+ linked: req.linked,
999
1006
  }, dispatchCwd);
1000
1007
  out.loopId = loop.id;
1001
1008
  out.artifacts.push({ type: 'loop', id: loop.id });
@@ -1024,7 +1031,10 @@ export async function handleBclawCoordinate(args, ctx) {
1024
1031
  // no ids, so a harvest could only match reviewer slots by agent name —
1025
1032
  // which completes the WRONG slot in symmetric (multi-reviewer) mode.
1026
1033
  try {
1027
- const reviewScope = `review-loop:${loop.id}`;
1034
+ // One claim owns one reviewer slot. A loop-wide claim cannot safely
1035
+ // represent parallel assignments because Claim has one assignment_id
1036
+ // projection; keep the parseable loop prefix and add slot identity.
1037
+ const reviewScope = `review-loop:${loop.id}:slot:${slot.slot_id}`;
1028
1038
  const reviewDescription = `Review loop turn for ${loop.id} slot ${slot.slot_id} phase findings. `
1029
1039
  + `Mode: ${advanced.loop.protocol?.review_mode ?? 'asymmetric'}. ${req.task}`;
1030
1040
  const claimResult = createCoordinatorClaim({
@@ -1046,6 +1056,8 @@ export async function handleBclawCoordinate(args, ctx) {
1046
1056
  });
1047
1057
  let reviewAssignmentId;
1048
1058
  let reviewTurnEcho;
1059
+ let reviewExecutionContractRef;
1060
+ let reviewWorktreePath = claimResult.worktreePath;
1049
1061
  // pln#630 — turn-own the INITIAL reviewer dispatch (same default + kill-switch as the
1050
1062
  // fix cycle). Skipped for cross-project reviews (no local worktree/sentinel → they never
1051
1063
  // spawn here). WON: prepare minted the DETERMINISTIC assignment + run + turn()-bound the
@@ -1070,12 +1082,25 @@ export async function handleBclawCoordinate(args, ctx) {
1070
1082
  dispatcherAgent: senderAgent,
1071
1083
  dispatcherAgentId: senderAgentId,
1072
1084
  sessionId: connectionSessionId,
1085
+ model: resolveModel(slot.agent ?? '', { override: req.model }),
1073
1086
  isReviewer: true,
1074
1087
  cwd: dispatchCwd,
1075
1088
  });
1076
1089
  if (prep.kind === 'won') {
1077
1090
  reviewAssignmentId = prep.assignmentId; // deterministic — harvest correlates on it
1078
- reviewTurnEcho = { turn_id: prep.turnId, run_id: prep.runId, nonce: prep.nonce };
1091
+ reviewExecutionContractRef = prep.executionContractRef;
1092
+ reviewTurnEcho = {
1093
+ turn_id: prep.turnId,
1094
+ run_id: prep.runId,
1095
+ nonce: prep.nonce,
1096
+ ...(prep.executionContractRef ? {
1097
+ contract_hash: prep.executionContractRef.hash,
1098
+ capability_snapshot_hash: prep.executionContractRef.snapshot_hash,
1099
+ } : {}),
1100
+ ...(prep.attemptEpoch !== undefined ? { attempt_epoch: prep.attemptEpoch } : {}),
1101
+ ...(prep.workspaceDigest ? { workspace_digest: prep.workspaceDigest } : {}),
1102
+ };
1103
+ reviewWorktreePath = prep.workspacePath;
1079
1104
  out.artifacts.push({ type: 'assignment', id: prep.assignmentId });
1080
1105
  usedTurnOwned = true; // prepare already created the assignment + run + bound the slot
1081
1106
  }
@@ -1124,8 +1149,16 @@ export async function handleBclawCoordinate(args, ctx) {
1124
1149
  const reviewBrief = buildCoordinateBrief(slot.agent ?? '', reviewDescription + reviewVerdictBriefSuffix, {
1125
1150
  claimId: claimResult.claimId,
1126
1151
  scope: reviewScope,
1127
- worktreePath: claimResult.worktreePath,
1152
+ worktreePath: reviewWorktreePath,
1128
1153
  assignmentId: reviewAssignmentId,
1154
+ executionContractRef: reviewExecutionContractRef,
1155
+ attemptFence: reviewTurnEcho?.attempt_epoch !== undefined && reviewTurnEcho.workspace_digest ? {
1156
+ turn_id: reviewTurnEcho.turn_id,
1157
+ run_id: reviewTurnEcho.run_id,
1158
+ nonce: reviewTurnEcho.nonce,
1159
+ attempt_epoch: reviewTurnEcho.attempt_epoch,
1160
+ workspace_digest: reviewTurnEcho.workspace_digest,
1161
+ } : undefined,
1129
1162
  });
1130
1163
  const queued = queueCoordinateMessage({
1131
1164
  agent: slot.agent ?? '',
@@ -1145,7 +1178,7 @@ export async function handleBclawCoordinate(args, ctx) {
1145
1178
  scope: reviewScope,
1146
1179
  claim_id: claimResult.claimId,
1147
1180
  ...(reviewAssignmentId ? { assignment_id: reviewAssignmentId } : {}),
1148
- worktree_path: claimResult.worktreePath,
1181
+ worktree_path: reviewWorktreePath,
1149
1182
  },
1150
1183
  commandMode: 'worker',
1151
1184
  });
@@ -1164,7 +1197,7 @@ export async function handleBclawCoordinate(args, ctx) {
1164
1197
  out.preparedReviews.push({
1165
1198
  entry: queued.entry,
1166
1199
  invoke: queued.invoke,
1167
- worktreePath: claimResult.worktreePath,
1200
+ worktreePath: reviewWorktreePath,
1168
1201
  turnEcho: reviewTurnEcho,
1169
1202
  });
1170
1203
  }
@@ -1441,7 +1474,7 @@ export async function handleBclawCoordinate(args, ctx) {
1441
1474
  // pln#513 step 2 — labelled block (ideate:) so the bootstrap
1442
1475
  // join-or-lock path can break out early after assigning result.
1443
1476
  const loopsModuleRef = await import('../core/loops/index.js');
1444
- const { openLoop, add_artifact, advance, turn, getLoop, buildIdeationBrief } = loopsModuleRef;
1477
+ const { openLoop, add_artifact, advance, getLoop, buildIdeationBrief } = loopsModuleRef;
1445
1478
  const presetSelected = req.preset
1446
1479
  ? (await import('../core/loops/presets/index.js')).PRESETS[req.preset]
1447
1480
  : undefined;
@@ -1553,6 +1586,7 @@ export async function handleBclawCoordinate(args, ctx) {
1553
1586
  goal: req.scope,
1554
1587
  created_by: creatorActor,
1555
1588
  slots,
1589
+ linked: req.linked,
1556
1590
  ...(presetSelected
1557
1591
  ? {
1558
1592
  phases: presetSelected.phases,
@@ -1645,6 +1679,7 @@ export async function handleBclawCoordinate(args, ctx) {
1645
1679
  category,
1646
1680
  text: r.text,
1647
1681
  score: r.score,
1682
+ relatedPaths: r.related_paths,
1648
1683
  }));
1649
1684
  },
1650
1685
  };
@@ -1708,55 +1743,86 @@ export async function handleBclawCoordinate(args, ctx) {
1708
1743
  entity: 'claim',
1709
1744
  id: claimResult.claimId,
1710
1745
  });
1711
- let criticAssignmentId;
1712
- try {
1713
- const preId = generateAssignmentId(dispatchCwd);
1714
- const assignment = createAssignment({
1715
- id: preId.id,
1716
- short_label: preId.short_label,
1717
- claim_id: claimResult.claimId,
1718
- agent: slot.agent,
1719
- dispatcher_agent: senderAgent,
1720
- dispatcher_session_id: connectionSessionId,
1721
- scope: criticScope,
1722
- description: criticDescription,
1723
- tags: ['coordinate', 'ideate', 'loop'],
1724
- }, dispatchCwd);
1725
- criticAssignmentId = assignment.id;
1726
- artifacts.push({ type: 'assignment', id: assignment.id });
1727
- }
1728
- catch (asgErr) {
1729
- warnings.push(`ideate assignment creation failed for slot ${slot.slot_id}: ${asgErr instanceof Error ? asgErr.message : String(asgErr)}`);
1730
- }
1731
- // pln#629 — bind the slot to its claim/assignment NOW that both
1732
- // exist (mirrors the review path, pln#628 BLOCKING 2). The turn()
1733
- // used to fire BEFORE the assignment was created, leaving
1734
- // slot.assignment_id undefined: bclaw_loop get's reconcile then
1735
- // skipped the critic slot (loops-handlers.ts `if (!assignmentId)
1736
- // continue`) and dispatch_status(lop_) resolved no assignment, so
1737
- // ideate loops could never be reconciled (trp_dfe0b941 /
1738
- // trp_2187b340 / trp_1de94516). Runs even if assignment creation
1739
- // failed (undefined id → legacy agent-match fallback, as review).
1740
- turn({
1741
- id: loopId,
1746
+ // P0C / dec#171 — ideation crosses the same attempt fence as every
1747
+ // worker-backed LoopKind phase. Assignment, AgentRun, claim binding
1748
+ // and slot binding are all durable before the irreversible crossing.
1749
+ const criticModel = resolveModel(slot.agent, { override: req.model });
1750
+ const criticHarnessBinding = resolveHarnessBinding(slot.agent, criticModel);
1751
+ const attempt = prepareTurnExecution({
1752
+ kind: 'ideation',
1753
+ loop_id: loopId,
1742
1754
  slot_id: slot.slot_id,
1743
- actor: creatorActor,
1744
- input: briefResult.text,
1745
- assignment_id: criticAssignmentId,
1755
+ phase: advancedLoop.current_phase,
1756
+ agent: slot.agent,
1757
+ agent_id: slot.agent_id,
1746
1758
  claim_id: claimResult.claimId,
1747
- }, dispatchCwd);
1759
+ dispatcher_agent: senderAgent,
1760
+ dispatcher_agent_id: senderAgentId,
1761
+ dispatcher_session_id: connectionSessionId,
1762
+ scope: criticScope,
1763
+ description: criticDescription,
1764
+ task: briefResult.text,
1765
+ cwd: dispatchCwd,
1766
+ worktree_path: claimResult.worktreePath,
1767
+ model: criticModel,
1768
+ harness_binding: criticHarnessBinding,
1769
+ assignment_tags: ['coordinate', 'ideate', 'loop', 'turn-owned'],
1770
+ run_tags: ['turn-owned', 'ideate', 'loop'],
1771
+ });
1772
+ if (attempt.kind !== 'won') {
1773
+ warnings.push(`ideate attempt denied for slot ${slot.slot_id}: ${attempt.reason}; no worker spawned`);
1774
+ // A pre-identity refusal or an authority owned by another claim
1775
+ // cannot use this freshly created lane claim. Release only claims
1776
+ // created by THIS call; a reused claim may belong to a concurrent
1777
+ // winner and must remain intact. Repairable/same-claim authority is
1778
+ // also retained so projections can be replayed safely.
1779
+ if (attempt.claim_disposition === 'release' && !claimResult.reusedExisting) {
1780
+ try {
1781
+ const released = releaseClaimIfActive(claimResult.claimId, dispatchCwd);
1782
+ if (released.released) {
1783
+ side_effects.push({ action: 'release', entity: 'claim', id: claimResult.claimId });
1784
+ if (claimResult.worktreePath) {
1785
+ try {
1786
+ removeWorktree(dispatchCwd, claimResult.worktreePath, { force: true });
1787
+ }
1788
+ catch (cleanupError) {
1789
+ warnings.push(`ideate denied-claim worktree cleanup failed for ${claimResult.claimId}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
1790
+ }
1791
+ }
1792
+ }
1793
+ }
1794
+ catch (cleanupError) {
1795
+ warnings.push(`ideate denied-claim cleanup failed for ${claimResult.claimId}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
1796
+ }
1797
+ }
1798
+ continue;
1799
+ }
1800
+ const criticAssignmentId = attempt.assignment_id;
1801
+ const criticTurnEcho = {
1802
+ turn_id: attempt.turn_id,
1803
+ run_id: attempt.run_id,
1804
+ nonce: attempt.nonce,
1805
+ ...(attempt.execution_contract_ref ? {
1806
+ contract_hash: attempt.execution_contract_ref.hash,
1807
+ capability_snapshot_hash: attempt.execution_contract_ref.snapshot_hash,
1808
+ } : {}),
1809
+ ...(attempt.attempt_epoch !== undefined ? { attempt_epoch: attempt.attempt_epoch } : {}),
1810
+ ...(attempt.workspace_digest ? { workspace_digest: attempt.workspace_digest } : {}),
1811
+ };
1812
+ artifacts.push({ type: 'assignment', id: criticAssignmentId });
1748
1813
  // pln#626 Phase 2 — the critique-only contract must reach the
1749
1814
  // DELIVERED brief, not just the claim record: buildCoordinateBrief
1750
1815
  // wraps this in a worker envelope, so prepend the constraint + the
1751
1816
  // reply path (MCP complete_turn, or LANE-RESULT.json for a sandboxed
1752
1817
  // critic without brainclaw MCP) ahead of the ideation brief body.
1753
1818
  const criticTaskText = `CRITIQUE-ONLY TASK — do NOT edit code or commit. Read the proposal below and reply with your critique: `
1754
- + `call bclaw_loop(intent='complete_turn') if you have brainclaw MCP, otherwise write your critique to LANE-RESULT.json in your worktree root (the coordinator harvests it).\n\n`
1819
+ + `call bclaw_loop(intent='complete_turn') if you have brainclaw MCP, otherwise write LANE-RESULT.json in your worktree root with `
1820
+ + `"status":"completed", "artifact_type":"critique", and the full critique in "body" (the coordinator harvests it).\n\n`
1755
1821
  + briefResult.text;
1756
1822
  const criticBrief = buildCoordinateBrief(slot.agent, criticTaskText, {
1757
1823
  claimId: claimResult.claimId,
1758
1824
  scope: criticScope,
1759
- worktreePath: claimResult.worktreePath,
1825
+ worktreePath: attempt.workspace_path,
1760
1826
  assignmentId: criticAssignmentId,
1761
1827
  // The ideation brief above already inlines a BM25-selected,
1762
1828
  // budget-managed memory bundle (with its own truncation warning).
@@ -1764,6 +1830,14 @@ export async function handleBclawCoordinate(args, ctx) {
1764
1830
  // and break the documented ~48K content cap + dispatch-envelope
1765
1831
  // math pinned by ideation-loop-e2e.
1766
1832
  contextEnvelope: false,
1833
+ executionContractRef: attempt.execution_contract_ref,
1834
+ attemptFence: attempt.attempt_epoch !== undefined && attempt.workspace_digest ? {
1835
+ turn_id: attempt.turn_id,
1836
+ run_id: attempt.run_id,
1837
+ nonce: attempt.nonce,
1838
+ attempt_epoch: attempt.attempt_epoch,
1839
+ workspace_digest: attempt.workspace_digest,
1840
+ } : undefined,
1767
1841
  });
1768
1842
  const queued = queueCoordinateMessage({
1769
1843
  agent: slot.agent,
@@ -1783,9 +1857,10 @@ export async function handleBclawCoordinate(args, ctx) {
1783
1857
  iteration: advancedLoop.iteration_count,
1784
1858
  proposal_artifact_id: proposalArtifactId,
1785
1859
  ...(criticAssignmentId ? { assignment_id: criticAssignmentId } : {}),
1786
- worktree_path: claimResult.worktreePath,
1860
+ worktree_path: attempt.workspace_path,
1787
1861
  },
1788
1862
  commandMode: 'worker',
1863
+ harnessBinding: criticHarnessBinding,
1789
1864
  });
1790
1865
  if (criticAssignmentId) {
1791
1866
  try {
@@ -1802,7 +1877,8 @@ export async function handleBclawCoordinate(args, ctx) {
1802
1877
  preparedCritics.push({
1803
1878
  entry: queued.entry,
1804
1879
  invoke: queued.invoke,
1805
- worktreePath: claimResult.worktreePath,
1880
+ worktreePath: attempt.workspace_path,
1881
+ turnEcho: criticTurnEcho,
1806
1882
  });
1807
1883
  dispatchedCritics += 1;
1808
1884
  }
@@ -1966,34 +2042,31 @@ export async function handleBclawCoordinate(args, ctx) {
1966
2042
  }
1967
2043
  export async function handleBclawLoop(args, ctx) {
1968
2044
  const { cwd, connectionSessionId } = ctx;
1969
- // pln#542: intent='open' is no longer exposed standalone over MCP — it
1970
- // creates a loop without dispatching the first turn (the documented
1971
- // anti-pattern, now removed instead of documented). Internal callers
1972
- // (bclaw_coordinate, CLI bootstrap) use core openLoop directly.
1973
- if (args?.intent === 'open') {
1974
- return {
1975
- response: createToolErrorResponse('intent_not_exposed', "bclaw_loop(intent='open') is not exposed standalone: it creates a loop structure without dispatching any turn, so the work never starts. Use bclaw_coordinate(intent='review', open_loop=true, targetAgents=[…]) or bclaw_coordinate(intent='ideate') — they open the loop AND dispatch the first turn."),
1976
- };
1977
- }
1978
- // pln#632 `bind` SPAWNE de vrais workers (il dispatche la séquence liée de la
1979
- // boucle), donc il est protégé au barreau 'trusted' comme les autres surfaces de
1980
- // dispatch.
1981
- //
1982
- // LA BRANCHE `turn && dispatch === true` A ÉTÉ RETIRÉE (pln#626 phase 4). Le drapeau
1983
- // `TurnInput.dispatch` était déclaré, transporté jusqu'à `turn()` — et JAMAIS LU. Une
1984
- // porte de confiance sur un no-op est pire qu'absente : elle fait croire qu'un chemin
1985
- // sensible est gardé, et un lecteur qui la voit conclut à tort que `dispatch: true` a
1986
- // un effet. Le drapeau lui-même est supprimé dans le même commit ; garder la porte
1987
- // aurait laissé la fausse impression intacte.
1988
- if (args?.intent === 'bind') {
1989
- const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
2045
+ // Direct open is public for implementation/research/debug orchestration.
2046
+ // The facade schema requires allow_orphan=true, making the caller explicitly
2047
+ // own the subsequent bind/turn/dispatch. Review and ideation still have the
2048
+ // higher-level coordinate shortcuts that open and dispatch together.
2049
+ // `turn(dispatch=true)` is now a REAL generic spawn path (claim + immutable
2050
+ // attempt + inbox + execution adapter). Plain turn remains a pure Loop Engine
2051
+ // mutation. `bind` is also engine-only; takeover and real turn dispatch are
2052
+ // the two trusted authority-changing paths here.
2053
+ const targetCwd = resolveProjectCwd(args?.project, cwd);
2054
+ let effectiveArgs = args;
2055
+ if (args?.intent === 'takeover' || (args?.intent === 'turn' && args?.dispatch === true)) {
2056
+ const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', targetCwd, connectionSessionId);
1990
2057
  if (resolved.error) {
1991
2058
  return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
1992
2059
  }
2060
+ if ((args.intent === 'takeover' || args.intent === 'turn') && resolved.identity) {
2061
+ effectiveArgs = {
2062
+ ...args,
2063
+ agent: resolved.identity.agent_name,
2064
+ agentId: resolved.identity.agent_id,
2065
+ };
2066
+ }
1993
2067
  }
1994
2068
  const { handleBclawLoop: runLoopIntent } = await import('./loops-handlers.js');
1995
- const targetCwd = resolveProjectCwd(args?.project, cwd);
1996
- const result = await runLoopIntent({ args: args, cwd: targetCwd, sessionId: connectionSessionId });
2069
+ const result = await runLoopIntent({ args: effectiveArgs, cwd: targetCwd, sessionId: connectionSessionId });
1997
2070
  return {
1998
2071
  response: toolResponse({
1999
2072
  content: [{ type: 'text', text: result.summary }],
@@ -166,7 +166,7 @@ const PROFILES = {
166
166
  // giving Codex the same session-lifecycle wiring as Claude Code.
167
167
  hasMcp: true, hasHooks: true, hasAutoApprove: false, hasSkills: true, hasRules: true,
168
168
  instructionFile: 'AGENTS.md', sharedInstructionFile: true, mcpConfigScope: 'machine', templateTier: 'A',
169
- role_capabilities: ['execute', 'review'],
169
+ role_capabilities: ['execute', 'review', 'consult'],
170
170
  runtime: { mcp_direct: true, hooks: true, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: true },
171
171
  max_concurrent_tasks: 5,
172
172
  // pln#475: prefer stdin_pipe to avoid Windows cmd.exe arg-parsing breaking