brainclaw 1.26.2 → 1.27.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 (86) 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 +87 -14
  8. package/dist/commands/mcp-catalog.js +42 -18
  9. package/dist/commands/mcp-schemas.generated.js +44 -0
  10. package/dist/commands/mcp-write-claims.js +128 -1
  11. package/dist/commands/mcp-write-coordination.js +146 -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 +160 -14
  25. package/dist/core/execution-contract.js +345 -0
  26. package/dist/core/execution.js +130 -16
  27. package/dist/core/harness-adapters/base.js +150 -0
  28. package/dist/core/harness-adapters/claude.js +39 -0
  29. package/dist/core/harness-adapters/codex.js +57 -0
  30. package/dist/core/harness-adapters/harvest.js +109 -0
  31. package/dist/core/harness-adapters/index.js +8 -0
  32. package/dist/core/harness-adapters/prompt-only.js +13 -0
  33. package/dist/core/harness-adapters/registry.js +48 -0
  34. package/dist/core/harness-adapters/result.js +33 -0
  35. package/dist/core/harness-adapters/types.js +2 -0
  36. package/dist/core/ideation-loop-close.js +25 -2
  37. package/dist/core/instruction-templates.js +3 -2
  38. package/dist/core/loop-turn-dispatch.js +207 -0
  39. package/dist/core/loops/artifact-contract.js +11 -0
  40. package/dist/core/loops/attempt-authority.js +476 -0
  41. package/dist/core/loops/attempt-generations.js +509 -0
  42. package/dist/core/loops/attempt-reservation.js +197 -35
  43. package/dist/core/loops/attempt-rollout.js +404 -0
  44. package/dist/core/loops/attempt-takeover.js +155 -0
  45. package/dist/core/loops/bootstrap-acquire.js +7 -3
  46. package/dist/core/loops/evidence.js +187 -0
  47. package/dist/core/loops/facade-schema.js +41 -10
  48. package/dist/core/loops/gate-policy.js +485 -0
  49. package/dist/core/loops/impl-bind.js +37 -79
  50. package/dist/core/loops/index.js +9 -0
  51. package/dist/core/loops/iteration-engine.js +31 -19
  52. package/dist/core/loops/kind-policies.js +90 -0
  53. package/dist/core/loops/lock.js +71 -13
  54. package/dist/core/loops/reconcile-turn.js +235 -18
  55. package/dist/core/loops/result-reducers.js +99 -10
  56. package/dist/core/loops/store.js +30 -3
  57. package/dist/core/loops/turn-execution.js +480 -0
  58. package/dist/core/loops/types.js +113 -2
  59. package/dist/core/loops/verbs.js +332 -99
  60. package/dist/core/loops/verify-command.js +31 -8
  61. package/dist/core/loops/workspace-digest.js +54 -0
  62. package/dist/core/review-loop-close.js +25 -3
  63. package/dist/core/review-loop-turn-dispatch.js +210 -161
  64. package/dist/core/runtime-signals.js +62 -25
  65. package/dist/core/schema.js +35 -0
  66. package/dist/core/spawn-check.js +3 -2
  67. package/dist/core/upgrades/backup.js +27 -4
  68. package/dist/facts.js +7 -6
  69. package/dist/facts.json +6 -5
  70. package/docs/cli.md +49 -1
  71. package/docs/concepts/attempt-authority.md +407 -0
  72. package/docs/concepts/evidence-attestations.md +135 -0
  73. package/docs/concepts/execution-contract.md +166 -0
  74. package/docs/concepts/harness-adapters.md +166 -0
  75. package/docs/concepts/ideation-loop.md +5 -4
  76. package/docs/concepts/loop-engine.md +302 -113
  77. package/docs/index.md +4 -1
  78. package/docs/integrations/codex.md +3 -3
  79. package/docs/integrations/mcp.md +59 -5
  80. package/docs/loops/debug.md +144 -0
  81. package/docs/loops/ideation.md +158 -0
  82. package/docs/loops/implementation.md +154 -0
  83. package/docs/loops/research.md +136 -0
  84. package/docs/loops/review.md +200 -0
  85. package/docs/mcp-schema-changelog.md +14 -5
  86. 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).
@@ -1024,7 +1030,10 @@ export async function handleBclawCoordinate(args, ctx) {
1024
1030
  // no ids, so a harvest could only match reviewer slots by agent name —
1025
1031
  // which completes the WRONG slot in symmetric (multi-reviewer) mode.
1026
1032
  try {
1027
- const reviewScope = `review-loop:${loop.id}`;
1033
+ // One claim owns one reviewer slot. A loop-wide claim cannot safely
1034
+ // represent parallel assignments because Claim has one assignment_id
1035
+ // projection; keep the parseable loop prefix and add slot identity.
1036
+ const reviewScope = `review-loop:${loop.id}:slot:${slot.slot_id}`;
1028
1037
  const reviewDescription = `Review loop turn for ${loop.id} slot ${slot.slot_id} phase findings. `
1029
1038
  + `Mode: ${advanced.loop.protocol?.review_mode ?? 'asymmetric'}. ${req.task}`;
1030
1039
  const claimResult = createCoordinatorClaim({
@@ -1046,6 +1055,8 @@ export async function handleBclawCoordinate(args, ctx) {
1046
1055
  });
1047
1056
  let reviewAssignmentId;
1048
1057
  let reviewTurnEcho;
1058
+ let reviewExecutionContractRef;
1059
+ let reviewWorktreePath = claimResult.worktreePath;
1049
1060
  // pln#630 — turn-own the INITIAL reviewer dispatch (same default + kill-switch as the
1050
1061
  // fix cycle). Skipped for cross-project reviews (no local worktree/sentinel → they never
1051
1062
  // spawn here). WON: prepare minted the DETERMINISTIC assignment + run + turn()-bound the
@@ -1070,12 +1081,25 @@ export async function handleBclawCoordinate(args, ctx) {
1070
1081
  dispatcherAgent: senderAgent,
1071
1082
  dispatcherAgentId: senderAgentId,
1072
1083
  sessionId: connectionSessionId,
1084
+ model: resolveModel(slot.agent ?? '', { override: req.model }),
1073
1085
  isReviewer: true,
1074
1086
  cwd: dispatchCwd,
1075
1087
  });
1076
1088
  if (prep.kind === 'won') {
1077
1089
  reviewAssignmentId = prep.assignmentId; // deterministic — harvest correlates on it
1078
- reviewTurnEcho = { turn_id: prep.turnId, run_id: prep.runId, nonce: prep.nonce };
1090
+ reviewExecutionContractRef = prep.executionContractRef;
1091
+ reviewTurnEcho = {
1092
+ turn_id: prep.turnId,
1093
+ run_id: prep.runId,
1094
+ nonce: prep.nonce,
1095
+ ...(prep.executionContractRef ? {
1096
+ contract_hash: prep.executionContractRef.hash,
1097
+ capability_snapshot_hash: prep.executionContractRef.snapshot_hash,
1098
+ } : {}),
1099
+ ...(prep.attemptEpoch !== undefined ? { attempt_epoch: prep.attemptEpoch } : {}),
1100
+ ...(prep.workspaceDigest ? { workspace_digest: prep.workspaceDigest } : {}),
1101
+ };
1102
+ reviewWorktreePath = prep.workspacePath;
1079
1103
  out.artifacts.push({ type: 'assignment', id: prep.assignmentId });
1080
1104
  usedTurnOwned = true; // prepare already created the assignment + run + bound the slot
1081
1105
  }
@@ -1124,8 +1148,16 @@ export async function handleBclawCoordinate(args, ctx) {
1124
1148
  const reviewBrief = buildCoordinateBrief(slot.agent ?? '', reviewDescription + reviewVerdictBriefSuffix, {
1125
1149
  claimId: claimResult.claimId,
1126
1150
  scope: reviewScope,
1127
- worktreePath: claimResult.worktreePath,
1151
+ worktreePath: reviewWorktreePath,
1128
1152
  assignmentId: reviewAssignmentId,
1153
+ executionContractRef: reviewExecutionContractRef,
1154
+ attemptFence: reviewTurnEcho?.attempt_epoch !== undefined && reviewTurnEcho.workspace_digest ? {
1155
+ turn_id: reviewTurnEcho.turn_id,
1156
+ run_id: reviewTurnEcho.run_id,
1157
+ nonce: reviewTurnEcho.nonce,
1158
+ attempt_epoch: reviewTurnEcho.attempt_epoch,
1159
+ workspace_digest: reviewTurnEcho.workspace_digest,
1160
+ } : undefined,
1129
1161
  });
1130
1162
  const queued = queueCoordinateMessage({
1131
1163
  agent: slot.agent ?? '',
@@ -1145,7 +1177,7 @@ export async function handleBclawCoordinate(args, ctx) {
1145
1177
  scope: reviewScope,
1146
1178
  claim_id: claimResult.claimId,
1147
1179
  ...(reviewAssignmentId ? { assignment_id: reviewAssignmentId } : {}),
1148
- worktree_path: claimResult.worktreePath,
1180
+ worktree_path: reviewWorktreePath,
1149
1181
  },
1150
1182
  commandMode: 'worker',
1151
1183
  });
@@ -1164,7 +1196,7 @@ export async function handleBclawCoordinate(args, ctx) {
1164
1196
  out.preparedReviews.push({
1165
1197
  entry: queued.entry,
1166
1198
  invoke: queued.invoke,
1167
- worktreePath: claimResult.worktreePath,
1199
+ worktreePath: reviewWorktreePath,
1168
1200
  turnEcho: reviewTurnEcho,
1169
1201
  });
1170
1202
  }
@@ -1441,7 +1473,7 @@ export async function handleBclawCoordinate(args, ctx) {
1441
1473
  // pln#513 step 2 — labelled block (ideate:) so the bootstrap
1442
1474
  // join-or-lock path can break out early after assigning result.
1443
1475
  const loopsModuleRef = await import('../core/loops/index.js');
1444
- const { openLoop, add_artifact, advance, turn, getLoop, buildIdeationBrief } = loopsModuleRef;
1476
+ const { openLoop, add_artifact, advance, getLoop, buildIdeationBrief } = loopsModuleRef;
1445
1477
  const presetSelected = req.preset
1446
1478
  ? (await import('../core/loops/presets/index.js')).PRESETS[req.preset]
1447
1479
  : undefined;
@@ -1708,55 +1740,86 @@ export async function handleBclawCoordinate(args, ctx) {
1708
1740
  entity: 'claim',
1709
1741
  id: claimResult.claimId,
1710
1742
  });
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,
1743
+ // P0C / dec#171 — ideation crosses the same attempt fence as every
1744
+ // worker-backed LoopKind phase. Assignment, AgentRun, claim binding
1745
+ // and slot binding are all durable before the irreversible crossing.
1746
+ const criticModel = resolveModel(slot.agent, { override: req.model });
1747
+ const criticHarnessBinding = resolveHarnessBinding(slot.agent, criticModel);
1748
+ const attempt = prepareTurnExecution({
1749
+ kind: 'ideation',
1750
+ loop_id: loopId,
1742
1751
  slot_id: slot.slot_id,
1743
- actor: creatorActor,
1744
- input: briefResult.text,
1745
- assignment_id: criticAssignmentId,
1752
+ phase: advancedLoop.current_phase,
1753
+ agent: slot.agent,
1754
+ agent_id: slot.agent_id,
1746
1755
  claim_id: claimResult.claimId,
1747
- }, dispatchCwd);
1756
+ dispatcher_agent: senderAgent,
1757
+ dispatcher_agent_id: senderAgentId,
1758
+ dispatcher_session_id: connectionSessionId,
1759
+ scope: criticScope,
1760
+ description: criticDescription,
1761
+ task: briefResult.text,
1762
+ cwd: dispatchCwd,
1763
+ worktree_path: claimResult.worktreePath,
1764
+ model: criticModel,
1765
+ harness_binding: criticHarnessBinding,
1766
+ assignment_tags: ['coordinate', 'ideate', 'loop', 'turn-owned'],
1767
+ run_tags: ['turn-owned', 'ideate', 'loop'],
1768
+ });
1769
+ if (attempt.kind !== 'won') {
1770
+ warnings.push(`ideate attempt denied for slot ${slot.slot_id}: ${attempt.reason}; no worker spawned`);
1771
+ // A pre-identity refusal or an authority owned by another claim
1772
+ // cannot use this freshly created lane claim. Release only claims
1773
+ // created by THIS call; a reused claim may belong to a concurrent
1774
+ // winner and must remain intact. Repairable/same-claim authority is
1775
+ // also retained so projections can be replayed safely.
1776
+ if (attempt.claim_disposition === 'release' && !claimResult.reusedExisting) {
1777
+ try {
1778
+ const released = releaseClaimIfActive(claimResult.claimId, dispatchCwd);
1779
+ if (released.released) {
1780
+ side_effects.push({ action: 'release', entity: 'claim', id: claimResult.claimId });
1781
+ if (claimResult.worktreePath) {
1782
+ try {
1783
+ removeWorktree(dispatchCwd, claimResult.worktreePath, { force: true });
1784
+ }
1785
+ catch (cleanupError) {
1786
+ warnings.push(`ideate denied-claim worktree cleanup failed for ${claimResult.claimId}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
1787
+ }
1788
+ }
1789
+ }
1790
+ }
1791
+ catch (cleanupError) {
1792
+ warnings.push(`ideate denied-claim cleanup failed for ${claimResult.claimId}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
1793
+ }
1794
+ }
1795
+ continue;
1796
+ }
1797
+ const criticAssignmentId = attempt.assignment_id;
1798
+ const criticTurnEcho = {
1799
+ turn_id: attempt.turn_id,
1800
+ run_id: attempt.run_id,
1801
+ nonce: attempt.nonce,
1802
+ ...(attempt.execution_contract_ref ? {
1803
+ contract_hash: attempt.execution_contract_ref.hash,
1804
+ capability_snapshot_hash: attempt.execution_contract_ref.snapshot_hash,
1805
+ } : {}),
1806
+ ...(attempt.attempt_epoch !== undefined ? { attempt_epoch: attempt.attempt_epoch } : {}),
1807
+ ...(attempt.workspace_digest ? { workspace_digest: attempt.workspace_digest } : {}),
1808
+ };
1809
+ artifacts.push({ type: 'assignment', id: criticAssignmentId });
1748
1810
  // pln#626 Phase 2 — the critique-only contract must reach the
1749
1811
  // DELIVERED brief, not just the claim record: buildCoordinateBrief
1750
1812
  // wraps this in a worker envelope, so prepend the constraint + the
1751
1813
  // reply path (MCP complete_turn, or LANE-RESULT.json for a sandboxed
1752
1814
  // critic without brainclaw MCP) ahead of the ideation brief body.
1753
1815
  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`
1816
+ + `call bclaw_loop(intent='complete_turn') if you have brainclaw MCP, otherwise write LANE-RESULT.json in your worktree root with `
1817
+ + `"status":"completed", "artifact_type":"critique", and the full critique in "body" (the coordinator harvests it).\n\n`
1755
1818
  + briefResult.text;
1756
1819
  const criticBrief = buildCoordinateBrief(slot.agent, criticTaskText, {
1757
1820
  claimId: claimResult.claimId,
1758
1821
  scope: criticScope,
1759
- worktreePath: claimResult.worktreePath,
1822
+ worktreePath: attempt.workspace_path,
1760
1823
  assignmentId: criticAssignmentId,
1761
1824
  // The ideation brief above already inlines a BM25-selected,
1762
1825
  // budget-managed memory bundle (with its own truncation warning).
@@ -1764,6 +1827,14 @@ export async function handleBclawCoordinate(args, ctx) {
1764
1827
  // and break the documented ~48K content cap + dispatch-envelope
1765
1828
  // math pinned by ideation-loop-e2e.
1766
1829
  contextEnvelope: false,
1830
+ executionContractRef: attempt.execution_contract_ref,
1831
+ attemptFence: attempt.attempt_epoch !== undefined && attempt.workspace_digest ? {
1832
+ turn_id: attempt.turn_id,
1833
+ run_id: attempt.run_id,
1834
+ nonce: attempt.nonce,
1835
+ attempt_epoch: attempt.attempt_epoch,
1836
+ workspace_digest: attempt.workspace_digest,
1837
+ } : undefined,
1767
1838
  });
1768
1839
  const queued = queueCoordinateMessage({
1769
1840
  agent: slot.agent,
@@ -1783,9 +1854,10 @@ export async function handleBclawCoordinate(args, ctx) {
1783
1854
  iteration: advancedLoop.iteration_count,
1784
1855
  proposal_artifact_id: proposalArtifactId,
1785
1856
  ...(criticAssignmentId ? { assignment_id: criticAssignmentId } : {}),
1786
- worktree_path: claimResult.worktreePath,
1857
+ worktree_path: attempt.workspace_path,
1787
1858
  },
1788
1859
  commandMode: 'worker',
1860
+ harnessBinding: criticHarnessBinding,
1789
1861
  });
1790
1862
  if (criticAssignmentId) {
1791
1863
  try {
@@ -1802,7 +1874,8 @@ export async function handleBclawCoordinate(args, ctx) {
1802
1874
  preparedCritics.push({
1803
1875
  entry: queued.entry,
1804
1876
  invoke: queued.invoke,
1805
- worktreePath: claimResult.worktreePath,
1877
+ worktreePath: attempt.workspace_path,
1878
+ turnEcho: criticTurnEcho,
1806
1879
  });
1807
1880
  dispatchedCritics += 1;
1808
1881
  }
@@ -1966,34 +2039,31 @@ export async function handleBclawCoordinate(args, ctx) {
1966
2039
  }
1967
2040
  export async function handleBclawLoop(args, ctx) {
1968
2041
  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);
2042
+ // Direct open is public for implementation/research/debug orchestration.
2043
+ // The facade schema requires allow_orphan=true, making the caller explicitly
2044
+ // own the subsequent bind/turn/dispatch. Review and ideation still have the
2045
+ // higher-level coordinate shortcuts that open and dispatch together.
2046
+ // `turn(dispatch=true)` is now a REAL generic spawn path (claim + immutable
2047
+ // attempt + inbox + execution adapter). Plain turn remains a pure Loop Engine
2048
+ // mutation. `bind` is also engine-only; takeover and real turn dispatch are
2049
+ // the two trusted authority-changing paths here.
2050
+ const targetCwd = resolveProjectCwd(args?.project, cwd);
2051
+ let effectiveArgs = args;
2052
+ if (args?.intent === 'takeover' || (args?.intent === 'turn' && args?.dispatch === true)) {
2053
+ const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', targetCwd, connectionSessionId);
1990
2054
  if (resolved.error) {
1991
2055
  return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
1992
2056
  }
2057
+ if ((args.intent === 'takeover' || args.intent === 'turn') && resolved.identity) {
2058
+ effectiveArgs = {
2059
+ ...args,
2060
+ agent: resolved.identity.agent_name,
2061
+ agentId: resolved.identity.agent_id,
2062
+ };
2063
+ }
1993
2064
  }
1994
2065
  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 });
2066
+ const result = await runLoopIntent({ args: effectiveArgs, cwd: targetCwd, sessionId: connectionSessionId });
1997
2067
  return {
1998
2068
  response: toolResponse({
1999
2069
  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