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
@@ -0,0 +1,480 @@
1
+ /** Common projections-before-crossing contract for every worker-backed LoopKind phase. */
2
+ import { ensureAgentRunProjection } from '../agentruns.js';
3
+ import { ensureAssignmentProjection } from '../assignments.js';
4
+ import { ensureClaimAssignmentBinding, loadClaim } from '../claims.js';
5
+ import { CapabilityRequirementSchema, ExecutionContractSchema, attestHarnessContractAcceptance, executionContractRef, resolveCapabilitySnapshot, validateWorkerContractAcceptance, HarnessCapabilityBindingSchema, } from '../execution-contract.js';
6
+ import { resolveHarnessBinding } from '../harness-adapters/index.js';
7
+ import { bindTurnProjection } from './verbs.js';
8
+ import { deriveChildIds, getReservation, launchGrant, resolveTurnId } from './attempt-reservation.js';
9
+ import { bootstrapAttemptAuthorityV2, crossActiveAttemptGenerationV2, executionContractForGeneration, prepareAttempt, projectAndCross, } from './attempt-authority.js';
10
+ import { resolveTurnGenerationChain } from './attempt-generations.js';
11
+ import { readLocalAuthorityHome, resolveActiveAttemptRollout } from './attempt-rollout.js';
12
+ import { getLoop } from './store.js';
13
+ import { phasePolicy } from './kind-policies.js';
14
+ /** Idempotently materialize every child projection required before launch. */
15
+ export function ensureTurnExecutionProjections(reservation, input, cwd) {
16
+ if (reservation.claim_id === '')
17
+ throw new Error('attempt reservation has no claim');
18
+ ensureAssignmentProjection({
19
+ id: input.assignment_id,
20
+ short_label: input.assignment_id,
21
+ claim_id: reservation.claim_id,
22
+ agent: input.agent,
23
+ agent_id: input.agent_id,
24
+ dispatcher_agent: input.dispatcher_agent,
25
+ dispatcher_session_id: input.dispatcher_session_id,
26
+ scope: input.scope,
27
+ description: input.description,
28
+ // Assignment is the stable logical attempt. Its contract projection stays
29
+ // generation-zero; each physical AgentRun below carries its own contract.
30
+ execution_contract_ref: reservation.execution_contract_ref,
31
+ capability_snapshot: reservation.capability_snapshot,
32
+ tags: input.assignment_tags ?? ['coordinate', 'loop', 'turn-owned'],
33
+ }, cwd);
34
+ input.on_projection?.('assignment');
35
+ ensureAgentRunProjection({
36
+ id: input.run_id,
37
+ short_label: input.run_id,
38
+ assignment_id: input.assignment_id,
39
+ claim_id: reservation.claim_id,
40
+ attempt_index: input.attempt_index ?? 1,
41
+ agent: input.agent,
42
+ agent_id: input.agent_id,
43
+ transport: 'cli_spawn',
44
+ status: 'created',
45
+ scope: input.scope,
46
+ description: input.description,
47
+ worktree_path: input.worktree_path,
48
+ execution_contract_ref: input.execution_contract_ref ?? reservation.execution_contract_ref,
49
+ capability_snapshot: input.capability_snapshot ?? reservation.capability_snapshot,
50
+ tags: input.run_tags ?? ['turn-owned', 'loop'],
51
+ }, cwd);
52
+ input.on_projection?.('run');
53
+ ensureClaimAssignmentBinding(reservation.claim_id, input.assignment_id, cwd, {
54
+ worktreePath: input.worktree_path,
55
+ });
56
+ input.on_projection?.('claim_binding');
57
+ bindTurnProjection({
58
+ id: input.loop_id,
59
+ slot_id: input.slot_id,
60
+ actor: input.dispatcher_agent_id ?? input.dispatcher_agent,
61
+ input: input.task,
62
+ turn_id: input.turn_id,
63
+ assignment_id: input.assignment_id,
64
+ claim_id: reservation.claim_id,
65
+ }, cwd);
66
+ input.on_projection?.('slot_binding');
67
+ input.on_projection?.('before_crossing');
68
+ }
69
+ function preconditionDenied(reason) {
70
+ return { kind: 'denied', reason, code: 'precondition', authority_claimed: false, claim_disposition: 'release' };
71
+ }
72
+ function authorityDenied(input, turnId, reason) {
73
+ const reservation = getReservation(turnId, input.cwd);
74
+ const ownsAuthority = reservation?.claim_id === input.claim_id;
75
+ const crossed = reservation?.launch?.status === 'crossed';
76
+ return {
77
+ kind: 'denied',
78
+ reason,
79
+ code: ownsAuthority ? (crossed ? 'already_crossed' : 'repairable') : 'authority_conflict',
80
+ authority_claimed: ownsAuthority,
81
+ claim_disposition: ownsAuthority ? 'retain' : 'release',
82
+ turn_id: turnId,
83
+ };
84
+ }
85
+ function defaultRequiredRole(kind, phase) {
86
+ if (kind === 'review')
87
+ return phase === 'author_response' ? 'execute' : 'review';
88
+ if (kind === 'ideation')
89
+ return 'review';
90
+ if (kind === 'research')
91
+ return 'consult';
92
+ return 'execute';
93
+ }
94
+ /**
95
+ * Common worker-attempt path. It refuses engine/manual phases and never advances
96
+ * a phase or evaluates a gate; those decisions stay in the Loop Engine.
97
+ * The first phase in a `(loop, slot, iteration)` retains the historical identity.
98
+ * If the same slot executes another worker phase without an iteration bump, the
99
+ * resolver uses a phase-qualified identity while preserving compatible legacy
100
+ * reservations for crash and upgrade replay.
101
+ */
102
+ export function prepareTurnExecution(input) {
103
+ const loop = getLoop(input.loop_id, input.cwd);
104
+ if (!loop)
105
+ return preconditionDenied(`loop ${input.loop_id} not found`);
106
+ if (loop.kind !== input.kind)
107
+ return preconditionDenied(`loop kind mismatch: ${loop.kind} != ${input.kind}`);
108
+ if (loop.status !== 'open')
109
+ return preconditionDenied(`loop ${loop.id} is ${loop.status}, not open`);
110
+ if (loop.current_phase !== input.phase) {
111
+ return preconditionDenied(`phase mismatch: loop is '${loop.current_phase}', attempt requested '${input.phase}'`);
112
+ }
113
+ const execution = phasePolicy(input.kind, input.phase);
114
+ if (!execution)
115
+ return preconditionDenied(`unknown ${input.kind} phase '${input.phase}'`);
116
+ if (execution.execution !== 'worker') {
117
+ return preconditionDenied(`${input.kind}.${input.phase} is ${execution.execution}, not a worker phase`);
118
+ }
119
+ const iteration = loop.iteration_count;
120
+ const slot = loop.slots.find((candidate) => candidate.slot_id === input.slot_id);
121
+ if (!slot)
122
+ return preconditionDenied(`slot ${input.slot_id} not found in loop ${loop.id}`);
123
+ const turnId = resolveTurnId({
124
+ loop_id: loop.id,
125
+ slot_id: input.slot_id,
126
+ phase: input.phase,
127
+ iteration,
128
+ current_turn_id: slot.current_turn_id,
129
+ }, input.cwd);
130
+ const childIds = deriveChildIds(turnId);
131
+ const existingReservation = getReservation(turnId, input.cwd);
132
+ if (slot.agent !== undefined && slot.agent !== input.agent) {
133
+ return preconditionDenied(`slot ${input.slot_id} belongs to agent '${slot.agent}', not '${input.agent}'`);
134
+ }
135
+ if (slot.agent_id !== undefined && slot.agent_id !== input.agent_id) {
136
+ return preconditionDenied(`slot ${input.slot_id} belongs to agent_id '${slot.agent_id}', not '${input.agent_id ?? 'none'}'`);
137
+ }
138
+ if (slot.claim_id !== undefined && slot.claim_id !== input.claim_id) {
139
+ return preconditionDenied(`slot ${input.slot_id} is bound to claim ${slot.claim_id}, not ${input.claim_id}`);
140
+ }
141
+ if (slot.current_turn_id !== undefined
142
+ && slot.current_turn_id !== turnId
143
+ && ['assigned', 'working', 'waiting_input'].includes(slot.status)) {
144
+ return preconditionDenied(`slot ${input.slot_id} has active turn ${slot.current_turn_id}, not ${turnId}`);
145
+ }
146
+ const claim = (() => {
147
+ try {
148
+ return loadClaim(input.claim_id, input.cwd);
149
+ }
150
+ catch {
151
+ return undefined;
152
+ }
153
+ })();
154
+ if (!claim)
155
+ return preconditionDenied(`claim ${input.claim_id} not found`);
156
+ if (claim.status !== 'active')
157
+ return preconditionDenied(`claim ${input.claim_id} is ${claim.status}, not active`);
158
+ if (claim.agent !== input.agent) {
159
+ return preconditionDenied(`claim ${input.claim_id} belongs to agent '${claim.agent}', not '${input.agent}'`);
160
+ }
161
+ if (claim.scope !== input.scope) {
162
+ return preconditionDenied(`claim ${input.claim_id} covers '${claim.scope}', not '${input.scope}'`);
163
+ }
164
+ if (claim.worktree_path && input.worktree_path && claim.worktree_path !== input.worktree_path) {
165
+ return preconditionDenied(`claim ${input.claim_id} worktree does not match the dispatch worktree`);
166
+ }
167
+ const dispatchLeaseMs = input.dispatch_lease_ms ?? 30 * 60_000;
168
+ const grantLeaseMs = input.grant_lease_ms ?? 10 * 60_000;
169
+ const hasCompleteExistingContract = Boolean(existingReservation?.execution_contract
170
+ && existingReservation.execution_contract_ref
171
+ && existingReservation.capability_snapshot);
172
+ const hasPartialExistingContract = Boolean(existingReservation) && !hasCompleteExistingContract && Boolean(existingReservation?.execution_contract
173
+ || existingReservation?.execution_contract_ref
174
+ || existingReservation?.capability_snapshot);
175
+ if (hasPartialExistingContract) {
176
+ return preconditionDenied('existing reservation has an incomplete execution-contract triplet');
177
+ }
178
+ const legacyUncontracted = Boolean(existingReservation) && !hasCompleteExistingContract;
179
+ const capabilityRequirement = CapabilityRequirementSchema.parse(input.capability_requirement
180
+ ?? existingReservation?.execution_contract?.capability_requirement
181
+ ?? {
182
+ roles: [defaultRequiredRole(input.kind, input.phase)],
183
+ required_surfaces: ['cli_spawn'],
184
+ // The transport requirement is CLI spawnability. Some integrations (for
185
+ // example Cline) expose a spawnable CLI while their native interaction
186
+ // surface remains an extension; callers can still constrain that native
187
+ // surface explicitly through capability_requirement.execution_surfaces.
188
+ execution_surfaces: [],
189
+ model: input.model,
190
+ required_tools: [],
191
+ });
192
+ if (input.capability_requirement
193
+ && existingReservation?.execution_contract
194
+ && JSON.stringify(capabilityRequirement) !== JSON.stringify(existingReservation.execution_contract.capability_requirement)) {
195
+ return preconditionDenied('capability requirement differs from the immutable existing execution contract');
196
+ }
197
+ let requestedHarnessBinding;
198
+ try {
199
+ const resolved = input.harness_binding ?? resolveHarnessBinding(input.agent, input.model);
200
+ requestedHarnessBinding = HarnessCapabilityBindingSchema.parse(resolved);
201
+ }
202
+ catch (error) {
203
+ return preconditionDenied(error instanceof Error ? error.message : String(error));
204
+ }
205
+ const frozenHarnessBinding = existingReservation?.capability_snapshot?.resolved.harness;
206
+ if (frozenHarnessBinding && JSON.stringify(frozenHarnessBinding) !== JSON.stringify(requestedHarnessBinding)) {
207
+ return preconditionDenied(`harness binding differs from immutable capability snapshot: frozen `
208
+ + `${frozenHarnessBinding.adapter_id}@${frozenHarnessBinding.adapter_version}, requested `
209
+ + `${requestedHarnessBinding.adapter_id}@${requestedHarnessBinding.adapter_version}`);
210
+ }
211
+ const capabilitySnapshot = existingReservation?.capability_snapshot
212
+ ?? resolveCapabilitySnapshot(input.agent, capabilityRequirement, input.agent_id, requestedHarnessBinding);
213
+ if (!capabilitySnapshot.accepted) {
214
+ const reasons = capabilitySnapshot.reasons.map((reason) => reason.code).join(', ');
215
+ return preconditionDenied(`capability requirements rejected for ${input.agent}: ${reasons}`);
216
+ }
217
+ const effectiveCompletionMode = existingReservation?.completion_mode ?? execution.completion_mode;
218
+ const effectiveExpectedArtifacts = existingReservation?.expected_artifacts ?? execution.expected_artifacts;
219
+ const contract = legacyUncontracted ? undefined : (existingReservation?.execution_contract ?? ExecutionContractSchema.parse({
220
+ schema_version: 1,
221
+ minimum_reader_version: 1,
222
+ identity: {
223
+ loop_id: loop.id,
224
+ turn_id: turnId,
225
+ logical_attempt_epoch: existingReservation?.epoch ?? 0,
226
+ assignment_id: childIds.assignment_id,
227
+ run_id: childIds.run_id,
228
+ kind: input.kind,
229
+ phase: input.phase,
230
+ iteration,
231
+ },
232
+ artifact_contract: {
233
+ completion_mode: effectiveCompletionMode,
234
+ expected_artifacts: effectiveExpectedArtifacts,
235
+ },
236
+ capability_requirement: capabilityRequirement,
237
+ workspace_policy: {
238
+ scope: input.scope,
239
+ cwd: input.cwd,
240
+ worktree_path: input.worktree_path,
241
+ isolation: input.workspace_policy?.isolation ?? (input.worktree_path ? 'worktree' : 'shared_checkout'),
242
+ write_access: input.workspace_policy?.write_access ?? 'workspace',
243
+ },
244
+ timeout_policy: {
245
+ dispatch_lease_ms: dispatchLeaseMs,
246
+ grant_lease_ms: grantLeaseMs,
247
+ },
248
+ evidence_policy: {
249
+ require_turn_id: true,
250
+ require_run_id: true,
251
+ require_nonce: true,
252
+ artifact_hash: 'optional',
253
+ },
254
+ protocol: { name: 'attempt-authority', minimum_version: 1 },
255
+ }));
256
+ const contractRef = contract
257
+ ? (existingReservation?.execution_contract_ref ?? executionContractRef(contract, capabilitySnapshot))
258
+ : undefined;
259
+ const v2 = resolveTurnGenerationChain(input.cwd, turnId);
260
+ const activeRollout = resolveActiveAttemptRollout(input.cwd);
261
+ const localHome = readLocalAuthorityHome(input.cwd);
262
+ if (activeRollout && (!localHome || !contractRef)) {
263
+ return preconditionDenied(!localHome
264
+ ? 'AttemptAuthority v2 rollout is active but this store/device has no local authority_home'
265
+ : 'AttemptAuthority v2 rollout is active but the turn has no immutable execution contract');
266
+ }
267
+ // Crash-safe v1→v2 cutover repair. The legacy launch fence is crossed before
268
+ // the immutable generation-zero cells are published. If the coordinator dies
269
+ // in that narrow window, a replay must materialize/adopt the v2 anchor so the
270
+ // turn can be fenced/taken over. It MUST NOT return spawn authority: the
271
+ // pre-crash caller may have received the crossed grant, so only a successor
272
+ // generation can safely recover liveness without a duplicate launch.
273
+ if (activeRollout
274
+ && localHome
275
+ && contractRef
276
+ && !v2
277
+ && existingReservation?.decision === 'committed'
278
+ && launchGrant(turnId, input.cwd)?.status === 'crossed') {
279
+ try {
280
+ bootstrapAttemptAuthorityV2({
281
+ turn_id: turnId,
282
+ authority_home: localHome,
283
+ actor: input.dispatcher_agent_id ?? input.dispatcher_agent,
284
+ writer_id: input.dispatcher_agent_id ?? input.dispatcher_agent,
285
+ cwd: input.cwd,
286
+ workspace_path: input.worktree_path ?? input.cwd,
287
+ on_stage: (stage) => input.on_cutover_stage?.(stage === 'initial_anchored' ? 'initial_anchored' : 'v2_anchored'),
288
+ });
289
+ return authorityDenied(input, turnId, 'repaired v1→v2 cutover after a crossed legacy launch; spawn authority remains fenced — use takeover');
290
+ }
291
+ catch (error) {
292
+ return authorityDenied(input, turnId, error instanceof Error ? error.message : String(error));
293
+ }
294
+ }
295
+ if (v2?.status === 'active' && v2.latest_generation.attempt_epoch === 0) {
296
+ if (activeRollout && localHome && contractRef) {
297
+ try {
298
+ bootstrapAttemptAuthorityV2({
299
+ turn_id: turnId,
300
+ authority_home: localHome,
301
+ actor: input.dispatcher_agent_id ?? input.dispatcher_agent,
302
+ writer_id: input.dispatcher_agent_id ?? input.dispatcher_agent,
303
+ cwd: input.cwd,
304
+ workspace_path: v2.latest_generation.workspace_path,
305
+ });
306
+ }
307
+ catch (error) {
308
+ return authorityDenied(input, turnId, error instanceof Error ? error.message : String(error));
309
+ }
310
+ }
311
+ return authorityDenied(input, turnId, 'generation zero launch is already crossed; use takeover for recovery');
312
+ }
313
+ if (input.accepted_execution_contract && !contractRef) {
314
+ return preconditionDenied('legacy uncontracted reservation cannot claim worker contract acceptance');
315
+ }
316
+ if (contractRef && !(v2?.status === 'active' && v2.latest_generation.attempt_epoch > 0)) {
317
+ let accepted;
318
+ try {
319
+ accepted = input.accepted_execution_contract
320
+ ?? attestHarnessContractAcceptance(contractRef, capabilitySnapshot, requestedHarnessBinding);
321
+ }
322
+ catch (error) {
323
+ return preconditionDenied(`worker contract acceptance unavailable before crossing: ${error instanceof Error ? error.message : String(error)}`);
324
+ }
325
+ const acceptance = validateWorkerContractAcceptance(contractRef, accepted, undefined);
326
+ if (acceptance.kind !== 'accepted') {
327
+ return preconditionDenied(`worker contract mismatch before crossing: expected ${contractRef.hash}/${contractRef.snapshot_hash}, accepted ${accepted.contract_hash}/${accepted.capability_snapshot_hash}; abort and reselect`);
328
+ }
329
+ }
330
+ // A takeover closes the old epoch and embeds a fresh active generation in
331
+ // close(epoch). Re-entering the common worker path projects that generation
332
+ // and contends on its immutable launch cell; the first caller wins spawn
333
+ // authority, every replay is adopted and MUST NOT spawn.
334
+ if (v2?.status === 'active' && v2.latest_generation.attempt_epoch > 0) {
335
+ const reservation = existingReservation;
336
+ if (!reservation || !localHome) {
337
+ return authorityDenied(input, turnId, 'AttemptAuthority v2 authority_home is unavailable');
338
+ }
339
+ try {
340
+ const generation = v2.latest_generation;
341
+ const generationContract = executionContractForGeneration(reservation, generation);
342
+ let accepted;
343
+ try {
344
+ accepted = input.accepted_execution_contract
345
+ ?? attestHarnessContractAcceptance(generationContract.ref, reservation.capability_snapshot, requestedHarnessBinding);
346
+ }
347
+ catch (error) {
348
+ return preconditionDenied(`worker contract acceptance unavailable before crossing: ${error instanceof Error ? error.message : String(error)}`);
349
+ }
350
+ const acceptance = validateWorkerContractAcceptance(generationContract.ref, accepted, undefined);
351
+ if (acceptance.kind !== 'accepted') {
352
+ return preconditionDenied('worker rejected the active generation execution contract before crossing; abort and reselect');
353
+ }
354
+ ensureTurnExecutionProjections(reservation, {
355
+ loop_id: loop.id,
356
+ slot_id: input.slot_id,
357
+ turn_id: turnId,
358
+ assignment_id: generation.assignment_id,
359
+ run_id: generation.run_id,
360
+ agent: input.agent,
361
+ agent_id: input.agent_id,
362
+ dispatcher_agent: input.dispatcher_agent,
363
+ dispatcher_agent_id: input.dispatcher_agent_id,
364
+ dispatcher_session_id: input.dispatcher_session_id,
365
+ scope: input.scope,
366
+ description: input.description,
367
+ task: input.task,
368
+ worktree_path: generation.workspace_path,
369
+ assignment_tags: input.assignment_tags,
370
+ run_tags: [...(input.run_tags ?? ['turn-owned', 'loop']), `attempt-generation:${generation.attempt_epoch}`],
371
+ attempt_index: generation.attempt_epoch + 1,
372
+ execution_contract_ref: generationContract.ref,
373
+ capability_snapshot: reservation.capability_snapshot,
374
+ on_projection: input.on_projection,
375
+ }, input.cwd);
376
+ const crossing = crossActiveAttemptGenerationV2(turnId, generation.attempt_epoch, localHome, input.dispatcher_agent_id ?? input.dispatcher_agent, input.dispatcher_agent_id ?? input.dispatcher_agent, input.cwd);
377
+ if (!crossing.won)
378
+ return authorityDenied(input, turnId, 'attempt generation launch already crossed');
379
+ return {
380
+ kind: 'won',
381
+ turn_id: turnId,
382
+ assignment_id: generation.assignment_id,
383
+ run_id: generation.run_id,
384
+ nonce: generation.launch_nonce,
385
+ attempt_epoch: generation.attempt_epoch,
386
+ workspace_digest: generation.workspace_digest,
387
+ workspace_path: generation.workspace_path,
388
+ contract_status: 'contracted',
389
+ execution_contract_ref: generationContract.ref,
390
+ capability_snapshot: reservation.capability_snapshot,
391
+ };
392
+ }
393
+ catch (error) {
394
+ return authorityDenied(input, turnId, error instanceof Error ? error.message : String(error));
395
+ }
396
+ }
397
+ try {
398
+ const now = Date.now();
399
+ const prepared = prepareAttempt({
400
+ turn_id: turnId,
401
+ loop_id: loop.id,
402
+ slot_id: input.slot_id,
403
+ target_slot_generation: iteration,
404
+ loop_version_at_reserve: loop.version,
405
+ agent: input.agent,
406
+ agent_id: input.agent_id,
407
+ claim_id: input.claim_id,
408
+ phase: input.phase,
409
+ iteration,
410
+ completion_mode: effectiveCompletionMode,
411
+ expected_artifacts: effectiveExpectedArtifacts,
412
+ execution_contract: contract,
413
+ execution_contract_ref: contractRef,
414
+ capability_snapshot: contractRef ? capabilitySnapshot : undefined,
415
+ store_root: input.cwd,
416
+ cwd: input.cwd,
417
+ lease_deadline: new Date(now + dispatchLeaseMs).toISOString(),
418
+ grant_lease_deadline: new Date(now + grantLeaseMs).toISOString(),
419
+ authority_actor: input.dispatcher_agent_id ?? input.dispatcher_agent,
420
+ on_stage: input.on_authority_stage,
421
+ }, input.cwd);
422
+ if (prepared.launch_status !== 'armed') {
423
+ return authorityDenied(input, turnId, `launch grant is ${prepared.launch_status}`);
424
+ }
425
+ const crossing = projectAndCross(prepared, (reservation) => ensureTurnExecutionProjections(reservation, {
426
+ loop_id: loop.id,
427
+ slot_id: input.slot_id,
428
+ turn_id: turnId,
429
+ assignment_id: childIds.assignment_id,
430
+ run_id: childIds.run_id,
431
+ agent: input.agent,
432
+ agent_id: input.agent_id,
433
+ dispatcher_agent: input.dispatcher_agent,
434
+ dispatcher_agent_id: input.dispatcher_agent_id,
435
+ dispatcher_session_id: input.dispatcher_session_id,
436
+ scope: input.scope,
437
+ description: input.description,
438
+ task: input.task,
439
+ worktree_path: input.worktree_path,
440
+ assignment_tags: input.assignment_tags,
441
+ run_tags: input.run_tags,
442
+ on_projection: input.on_projection,
443
+ }, input.cwd), input.cwd, input.dispatcher_agent_id ?? input.dispatcher_agent);
444
+ if (crossing.kind !== 'won')
445
+ return authorityDenied(input, turnId, 'launch grant already crossed');
446
+ input.on_cutover_stage?.('legacy_crossed');
447
+ let generationFence = {};
448
+ if (activeRollout && localHome && contractRef) {
449
+ const generation = bootstrapAttemptAuthorityV2({
450
+ turn_id: turnId,
451
+ authority_home: localHome,
452
+ actor: input.dispatcher_agent_id ?? input.dispatcher_agent,
453
+ writer_id: input.dispatcher_agent_id ?? input.dispatcher_agent,
454
+ cwd: input.cwd,
455
+ workspace_path: input.worktree_path ?? input.cwd,
456
+ on_stage: (stage) => input.on_cutover_stage?.(stage === 'initial_anchored' ? 'initial_anchored' : 'v2_anchored'),
457
+ });
458
+ generationFence = {
459
+ attempt_epoch: generation.attempt_epoch,
460
+ workspace_digest: generation.workspace_digest,
461
+ };
462
+ }
463
+ return {
464
+ kind: 'won',
465
+ turn_id: turnId,
466
+ assignment_id: childIds.assignment_id,
467
+ run_id: childIds.run_id,
468
+ nonce: prepared.token,
469
+ contract_status: contractRef ? 'contracted' : 'legacy_uncontracted',
470
+ execution_contract_ref: contractRef,
471
+ capability_snapshot: contractRef ? capabilitySnapshot : undefined,
472
+ workspace_path: input.worktree_path ?? input.cwd,
473
+ ...generationFence,
474
+ };
475
+ }
476
+ catch (error) {
477
+ return authorityDenied(input, turnId, error instanceof Error ? error.message : String(error));
478
+ }
479
+ }
480
+ //# sourceMappingURL=turn-execution.js.map
@@ -312,6 +312,74 @@ export const REF_BASED_ARTIFACT_TYPES = new Set([
312
312
  'signals_report',
313
313
  'file_diff',
314
314
  ]);
315
+ export const EVIDENCE_ATTESTATION_KINDS = ['claim', 'observation', 'verification', 'approval'];
316
+ export const EVIDENCE_PRODUCER_KINDS = ['engine', 'slot', 'coordinator', 'operator'];
317
+ export const EVIDENCE_CHANNELS = [
318
+ 'complete_turn',
319
+ 'reconcile_turn',
320
+ 'verify_command',
321
+ 'add_artifact',
322
+ 'operator_input',
323
+ 'system_hook',
324
+ ];
325
+ export const EvidenceSubjectSchema = z.object({
326
+ loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
327
+ artifact_id: z.string().min(1),
328
+ phase: z.string().min(1),
329
+ iteration: z.number().int().nonnegative(),
330
+ slot_id: z.string().optional(),
331
+ turn_id: z.string().optional(),
332
+ assignment_id: z.string().optional(),
333
+ claim_id: z.string().optional(),
334
+ run_id: z.string().optional(),
335
+ nonce_digest: z.string().regex(/^[0-9a-f]{64}$/).optional(),
336
+ attempt_epoch: z.number().int().nonnegative().optional(),
337
+ execution_contract_hash: z.string().regex(/^[0-9a-f]{64}$/).optional(),
338
+ command_digest: z.string().regex(/^[0-9a-f]{64}$/).optional(),
339
+ workspace_digest: z.string().regex(/^[0-9a-f]{64}$/).optional(),
340
+ });
341
+ export const EvidenceProducerSchema = z.object({
342
+ kind: z.enum(EVIDENCE_PRODUCER_KINDS),
343
+ id: z.string().min(1),
344
+ agent_id: z.string().optional(),
345
+ channel: z.enum(EVIDENCE_CHANNELS),
346
+ });
347
+ export const EvidenceAttestationSchema = z.object({
348
+ kind: z.enum(EVIDENCE_ATTESTATION_KINDS),
349
+ issuer: z.string().min(1),
350
+ issued_at: z.string().datetime(),
351
+ subject_digest: z.string().regex(/^[0-9a-f]{64}$/),
352
+ rights: z.array(z.string().min(1)).min(1),
353
+ });
354
+ /**
355
+ * EvidenceEnvelope v1 is sealed by the loop engine at the artifact commit
356
+ * boundary. The seal is an integrity checksum, not a remote identity
357
+ * signature: ingress schemas never accept this object from a caller.
358
+ */
359
+ export const EvidenceEnvelopeSchema = z.object({
360
+ version: z.literal(1),
361
+ evidence_id: z.string().regex(/^evd_[0-9a-z]+$/),
362
+ evidence_type: z.literal('artifact_commit'),
363
+ policy_version: z.literal('gate-policy-v1'),
364
+ subject: EvidenceSubjectSchema,
365
+ producer: EvidenceProducerSchema,
366
+ artifact_digest: z.string().regex(/^[0-9a-f]{64}$/),
367
+ issued_at: z.string().datetime(),
368
+ observed_at: z.string().datetime(),
369
+ validity: z.object({
370
+ not_before: z.string().datetime(),
371
+ not_after: z.string().datetime().optional(),
372
+ }),
373
+ attestations: z.array(EvidenceAttestationSchema).min(1),
374
+ seal: z.object({
375
+ algorithm: z.literal('sha256'),
376
+ digest: z.string().regex(/^[0-9a-f]{64}$/),
377
+ }),
378
+ });
379
+ export const EvidencePolicyBindingSchema = z.object({
380
+ version: z.literal('gate-policy-v1'),
381
+ mode: z.enum(['shadow', 'strict']),
382
+ });
315
383
  /**
316
384
  * Lookup table mapping known artifact types to their body Zod schemas.
317
385
  * Step 2 handlers (request_input/provide_input) parse `artifact.body` as
@@ -339,6 +407,7 @@ export const REF_BASED_ARTIFACT_TYPES = new Set([
339
407
  */
340
408
  export const VerifyReportBodySchema = z.object({
341
409
  command: z.string().min(1),
410
+ command_argv: z.array(z.string()).min(1).optional(),
342
411
  exit_code: z.number().int().nullable(),
343
412
  passed: z.boolean(),
344
413
  duration_ms: z.number().int().nonnegative().optional(),
@@ -346,6 +415,9 @@ export const VerifyReportBodySchema = z.object({
346
415
  timed_out: z.boolean().optional(),
347
416
  stdout_tail: z.string().max(1024).optional(),
348
417
  stderr_tail: z.string().max(1024).optional(),
418
+ command_digest: z.string().regex(/^[0-9a-f]{64}$/).optional(),
419
+ workspace_digest: z.string().regex(/^[0-9a-f]{64}$/).optional(),
420
+ workspace_stable: z.boolean().optional(),
349
421
  });
350
422
  export const KNOWN_ARTIFACT_BODY_SCHEMAS = {
351
423
  // inline JSON body: body = JSON.stringify({ ...fields per OperatorQuestionBodySchema })
@@ -394,6 +466,14 @@ export const LoopArtifactSchema = z
394
466
  * at artifact creation time so callers don't have to track it.
395
467
  */
396
468
  iteration: z.number().int().nonnegative().optional(),
469
+ /**
470
+ * Explicit migration provenance. Optional on read so artifacts persisted
471
+ * before P2 remain valid; use artifactEvidenceProvenance() to infer their
472
+ * effective value without rewriting historical records.
473
+ */
474
+ provenance: z.enum(['legacy', 'attested']).optional(),
475
+ /** Server-sealed provenance and independent attestations (P2). */
476
+ evidence: EvidenceEnvelopeSchema.optional(),
397
477
  })
398
478
  .superRefine((artifact, ctx) => {
399
479
  if (artifact.body !== undefined && Buffer.byteLength(artifact.body, 'utf8') > LOOP_ARTIFACT_BODY_MAX_BYTES) {
@@ -500,6 +580,8 @@ export const LoopThreadSchema = z
500
580
  artifacts: z.array(LoopArtifactSchema),
501
581
  linked: LoopLinksSchema.optional(),
502
582
  stop_condition: StopConditionSchema.optional(),
583
+ /** Absent means a pre-policy/legacy thread. New loops bind this at open. */
584
+ evidence_policy: EvidencePolicyBindingSchema.optional(),
503
585
  /**
504
586
  * pln#508 step 1 — set of unresolved operator_question artifact ids.
505
587
  * The engine maintains this on every request_input / provide_input
@@ -580,6 +662,17 @@ const LoopEventBaseShape = {
580
662
  by: z.string().optional(),
581
663
  mutation_id: z.string().min(1),
582
664
  };
665
+ export const GateDecisionSchema = z.object({
666
+ passed: z.boolean(),
667
+ /** Strict and legacy dimensions remain observable during a shadow rollout. */
668
+ strict_passed: z.boolean().optional(),
669
+ legacy_passed: z.boolean().optional(),
670
+ policy_version: z.enum(['legacy', 'gate-policy-v1']),
671
+ mode: z.enum(['legacy', 'shadow', 'strict']),
672
+ condition_digest: z.string().regex(/^[0-9a-f]{64}$/),
673
+ accepted_evidence_ids: z.array(z.string()),
674
+ rejected: z.array(z.object({ artifact_id: z.string(), reason: z.string() })),
675
+ });
583
676
  export const LoopEventSchema = z.discriminatedUnion('kind', [
584
677
  z.object({
585
678
  ...LoopEventBaseShape,
@@ -594,6 +687,7 @@ export const LoopEventSchema = z.discriminatedUnion('kind', [
594
687
  to_phase: z.string().min(1),
595
688
  iteration: z.number().int().nonnegative(),
596
689
  reason: z.string().optional(),
690
+ gate_decision: GateDecisionSchema.optional(),
597
691
  }),
598
692
  z.object({
599
693
  ...LoopEventBaseShape,
@@ -615,6 +709,19 @@ export const LoopEventSchema = z.discriminatedUnion('kind', [
615
709
  phase: z.string().min(1),
616
710
  turn_id: z.string().min(1),
617
711
  }),
712
+ z.object({
713
+ ...LoopEventBaseShape,
714
+ kind: z.literal('attempt_generation_changed'),
715
+ slot_id: z.string().min(1),
716
+ turn_id: z.string().min(1),
717
+ assignment_id: z.string().min(1),
718
+ from_epoch: z.number().int().nonnegative(),
719
+ to_epoch: z.number().int().positive(),
720
+ from_run_id: z.string().min(1),
721
+ to_run_id: z.string().min(1),
722
+ close_digest: z.string().regex(/^[a-f0-9]{64}$/),
723
+ cause: z.string().min(1),
724
+ }),
618
725
  z.object({
619
726
  ...LoopEventBaseShape,
620
727
  kind: z.literal('turn_completed'),
@@ -631,6 +738,7 @@ export const LoopEventSchema = z.discriminatedUnion('kind', [
631
738
  phase: z.string().min(1),
632
739
  type: z.string().min(1),
633
740
  produced_by: z.string().optional(),
741
+ evidence_id: z.string().optional(),
634
742
  }),
635
743
  z.object({
636
744
  ...LoopEventBaseShape,
@@ -651,6 +759,7 @@ export const LoopEventSchema = z.discriminatedUnion('kind', [
651
759
  kind: z.literal('closed'),
652
760
  final_status: z.enum(['completed', 'cancelled', 'blocked']),
653
761
  reason: z.string().optional(),
762
+ gate_decision: GateDecisionSchema.optional(),
654
763
  }),
655
764
  // pln#492 — system events emitted by the iteration / phase-advance gate.
656
765
  // Carried in the same event journal rather than as artifacts so consumers
@@ -660,6 +769,7 @@ export const LoopEventSchema = z.discriminatedUnion('kind', [
660
769
  kind: z.literal('phase_advance_blocked'),
661
770
  phase: z.string().min(1),
662
771
  gate_reason: z.string().min(1),
772
+ gate_decision: GateDecisionSchema.optional(),
663
773
  }),
664
774
  z.object({
665
775
  ...LoopEventBaseShape,
@@ -783,8 +893,9 @@ export const DEFAULT_PROTOCOLS = {
783
893
  },
784
894
  // pln#609 — implementation loop v2. The loop ADDS to the dispatch pipeline
785
895
  // what it lacked: a deterministic command_green gate + a bounded fix↔verify
786
- // cycle + per-phase context sculpting. `bind` is an ENGINE action (bind
787
- // plan+sequence and dispatch) not narration; execute↔verify iterates until
896
+ // cycle + per-phase context sculpting. `bind` is an engine-only link
897
+ // validation/advance; worker launch belongs to turn(dispatch=true).
898
+ // execute↔verify iterates until
788
899
  // the verify command is green (a passing verify_report this iteration) or
789
900
  // the cycle cap is hit (→ handoff_ready with the red report → blocked).
790
901
  implementation: {