@osovv/vv-opencode 1.4.2 → 1.4.3-rc.1

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 (57) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +20 -6
  3. package/dist/lib/orchestration.js +12 -3
  4. package/dist/lib/orchestration.js.map +1 -1
  5. package/dist/lib/spec-lint.d.ts +2 -64
  6. package/dist/lib/spec-lint.js +47 -45
  7. package/dist/lib/spec-lint.js.map +1 -1
  8. package/dist/lib/workflow-contract.d.ts +277 -0
  9. package/dist/lib/workflow-contract.js +636 -0
  10. package/dist/lib/workflow-contract.js.map +1 -0
  11. package/dist/plugins/system-context-injection/index.js +15 -7
  12. package/dist/plugins/system-context-injection/index.js.map +1 -1
  13. package/dist/plugins/workflow/authority.d.ts +99 -0
  14. package/dist/plugins/workflow/authority.js +435 -0
  15. package/dist/plugins/workflow/authority.js.map +1 -0
  16. package/dist/plugins/workflow/checkpoint-io.d.ts +10 -1
  17. package/dist/plugins/workflow/checkpoint-io.js +22 -3
  18. package/dist/plugins/workflow/checkpoint-io.js.map +1 -1
  19. package/dist/plugins/workflow/checkpoints.d.ts +58 -1
  20. package/dist/plugins/workflow/checkpoints.js +362 -34
  21. package/dist/plugins/workflow/checkpoints.js.map +1 -1
  22. package/dist/plugins/workflow/delegated.d.ts +150 -2
  23. package/dist/plugins/workflow/delegated.js +551 -15
  24. package/dist/plugins/workflow/delegated.js.map +1 -1
  25. package/dist/plugins/workflow/execution.d.ts +361 -0
  26. package/dist/plugins/workflow/execution.js +1807 -0
  27. package/dist/plugins/workflow/execution.js.map +1 -0
  28. package/dist/plugins/workflow/index.js +539 -80
  29. package/dist/plugins/workflow/index.js.map +1 -1
  30. package/dist/plugins/workflow/persistence.d.ts +18 -4
  31. package/dist/plugins/workflow/persistence.js +439 -42
  32. package/dist/plugins/workflow/persistence.js.map +1 -1
  33. package/dist/plugins/workflow/repair.d.ts +2 -0
  34. package/dist/plugins/workflow/repair.js +16 -8
  35. package/dist/plugins/workflow/repair.js.map +1 -1
  36. package/dist/plugins/workflow/snapshots.js +3 -3
  37. package/dist/plugins/workflow/snapshots.js.map +1 -1
  38. package/dist/plugins/workflow/state.d.ts +13 -0
  39. package/dist/plugins/workflow/state.js +37 -7
  40. package/dist/plugins/workflow/state.js.map +1 -1
  41. package/dist/plugins/workflow/system-instruction.md +15 -1
  42. package/dist/plugins/workflow/tooling.d.ts +81 -6
  43. package/dist/plugins/workflow/tooling.js +956 -23
  44. package/dist/plugins/workflow/tooling.js.map +1 -1
  45. package/dist/plugins/workflow/transactions.d.ts +54 -0
  46. package/dist/plugins/workflow/transactions.js +147 -0
  47. package/dist/plugins/workflow/transactions.js.map +1 -0
  48. package/package.json +1 -1
  49. package/schemas/vvoc/v3.json +1 -1
  50. package/templates/agents/vv-code-reviewer.md +1 -0
  51. package/templates/agents/vv-controller.md +26 -13
  52. package/templates/agents/vv-implementer.md +1 -0
  53. package/templates/agents/vv-spec-reviewer.md +1 -0
  54. package/templates/skills/vv-execute/SKILL.md +6 -5
  55. package/templates/skills/vv-plan/SKILL.md +1 -1
  56. package/templates/skills/vv-review/SKILL.md +1 -0
  57. package/templates/skills/vv-spec/SKILL.md +2 -2
@@ -0,0 +1,1807 @@
1
+ // FILE: src/plugins/workflow/execution.ts
2
+ // VERSION: 1.0.0
3
+ // START_MODULE_CONTRACT
4
+ // PURPOSE: One durable execution registry for native-package, provided-plan, and conversation-scoped runs with immutable source identity, incremental append-only task/checkpoint contracts, exact-reference validation, explicit replacement/split lineage, and shared budget carry-over.
5
+ // SCOPE: Deterministic run identity, source-switch rejection, atomic batch registration and append with full validation before any record is published, exact adoption of compatible unbound work items, quiescent-task replacement/split with aggregate criteria/scope preservation and inherited attempt/rework/recovery budget, native plan-run compatibility migration, sealing, and read-only run views. Domain review generation, authority, and SDK transport live in sibling modules.
6
+ // DEPENDS: [node:crypto, node:path, src/lib/workflow-contract.ts, src/plugins/workflow/state.ts]
7
+ // LINKS: [M-WORKFLOW-EXECUTION, M-WORKFLOW-CONTRACT, M-WORKFLOW-STATE, M-WORKFLOW-DELEGATED, V-M-WORKFLOW-EXECUTION]
8
+ // ROLE: RUNTIME
9
+ // MAP_MODE: EXPORTS
10
+ // END_MODULE_CONTRACT
11
+ //
12
+ // START_MODULE_MAP
13
+ // WorkflowTaskBinding - One registered task contract bound to its work item with lineage and status.
14
+ // WorkflowTaskStatus - Lifecycle of one task binding.
15
+ // WorkflowCheckpointReviewerOutcome - One reviewer's recorded outcome inside a generation.
16
+ // WorkflowCheckpointReviewState - Current in-flight generic review generation state.
17
+ // WorkflowCheckpointHistoryEntry - One settled generic generation outcome.
18
+ // WorkflowCheckpointRecoveryEntry - One recorded generic checkpoint exhaustion recovery.
19
+ // WorkflowCheckpointBinding - One registered checkpoint contract bound to its execution revision.
20
+ // WorkflowExecutionRecord - Authoritative registry entry for one common execution.
21
+ // RegisterExecutionTaskInput - One task contract plus an optional exact unbound work item to adopt.
22
+ // RegisterExecutionInput - Generic registration payload for one execution and its first batch.
23
+ // RegisterExecutionResult - Registration outcome with idempotent reuse or a coded rejection.
24
+ // AppendExecutionWorkInput - Append-only amendment of tasks and checkpoints.
25
+ // AppendExecutionWorkResult - Amendment outcome with the new revision or a coded rejection.
26
+ // AdoptWorkItemsInput - Exact adoption of existing compatible unbound work items.
27
+ // AdoptWorkItemsResult - Adoption outcome with the bound identities or a coded rejection.
28
+ // SplitExecutionTaskInput - Replacement/split of one quiescent unaccepted task.
29
+ // SplitExecutionTaskResult - Replacement outcome with descendant identities or a coded rejection.
30
+ // ExecutionMutationErrorCode - Coded rejection families for registry mutations.
31
+ // deriveExecutionRunId - Deterministic common run identity from session, key, and source.
32
+ // cloneWorkflowExecution - Deep clone of one execution record for staged commits.
33
+ // findExecution - Look up one execution by run id.
34
+ // findExecutionByKey - Look up one execution by session and stable execution key.
35
+ // ensureNativeExecutions - Compatibility view that materializes registry entries for existing native runs.
36
+ // registerExecutionInStore - Atomically register or idempotently reuse one execution and its first batch.
37
+ // appendExecutionWorkInStore - Append validated tasks/checkpoints without replaying accepted work.
38
+ // adoptWorkItemsInStore - Bind exact compatible unbound work items without resetting counters.
39
+ // splitExecutionTaskInStore - Replace one quiescent unaccepted task with explicit descendants sharing its budget.
40
+ // sealExecutionInStore - Seal a completed execution against implicit reopening.
41
+ // getExecutionView - Read-only serialization of one execution for tooling output.
42
+ // GENERIC_CHECKPOINT_GENERATIONS - Ordinary generic checkpoint generations before authorized recovery.
43
+ // StartGenericCheckpointResult - Generic checkpoint start outcome.
44
+ // RecordGenericReviewerResultResult - Generic reviewer result recording outcome.
45
+ // CompleteExecutionErrorCode - Completion rejection families.
46
+ // CompleteExecutionResult - Completion outcome with honest review status.
47
+ // startGenericCheckpointInStore - Start one generic checkpoint generation after covered tasks are accepted.
48
+ // recordGenericReviewerResultInStore - Record one reviewer outcome and settle the generation.
49
+ // recoverGenericCheckpointInStore - Grant one exhaustion-recovery generation under a recorded advance unit.
50
+ // isTaskLaunchableInStore - Whether declared dependencies and barriers allow a task launch.
51
+ // completeExecutionInStore - Complete a generic execution when all tasks and obligations hold.
52
+ // registerExecution - Store-surface registration wrapper.
53
+ // appendExecutionWork - Store-surface amendment wrapper.
54
+ // adoptWorkItems - Store-surface adoption wrapper.
55
+ // splitExecutionTask - Store-surface replacement/split wrapper.
56
+ // delegatedAttemptBudget - Re-exported budget helper for inherited lineage budgets.
57
+ // addAuthorityInStore - Persist one advance-authority record and message claim.
58
+ // addStageApprovalInStore - Persist one recorded stage approval.
59
+ // addReserveDebitInStore - Persist one advance-reserve debit.
60
+ // putAuthorityInStore - Replace or append one authority record.
61
+ // ExecutionMutationErrorCode - Coded rejection families for registry mutations.
62
+ // END_MODULE_MAP
63
+ //
64
+ // START_CHANGE_SUMMARY
65
+ // LAST_CHANGE: [C-WORKFLOW-PLAN-INDEPENDENCE - Initial common execution registry: source-independent identity, append-only amendments, lineage, and native compatibility migration.]
66
+ // END_CHANGE_SUMMARY
67
+ import { createHash } from "node:crypto";
68
+ import { isAbsolute } from "node:path";
69
+ import { isBoundedWorkflowId, taskContractsFromNativeDefinition, checkpointContractsFromNativeDefinition, validateExecutionBoundary, validateWorkflowCheckpointContract, validateWorkflowContractGraph, validateWorkflowTaskContract, WORKFLOW_TEXT_MAX_CHARS, } from "../../lib/workflow-contract.js";
70
+ import { nativeExecutionSource } from "./checkpoint-io.js";
71
+ import { delegatedAttemptBudget } from "./delegated.js";
72
+ import { createRecordLookupKey, openWorkItemInStore, } from "./state.js";
73
+ // END_BLOCK_EXECUTION_TYPES
74
+ // START_BLOCK_EXECUTION_HELPERS
75
+ function toIsoNow() {
76
+ return new Date().toISOString();
77
+ }
78
+ function cloneTaskBinding(binding) {
79
+ return {
80
+ ...binding,
81
+ contract: {
82
+ ...binding.contract,
83
+ acceptanceCriteria: [...binding.contract.acceptanceCriteria],
84
+ verification: [...binding.contract.verification],
85
+ writeScope: [...binding.contract.writeScope],
86
+ dependsOn: [...binding.contract.dependsOn],
87
+ blockedBy: [...binding.contract.blockedBy],
88
+ requiredReviewers: [...binding.contract.requiredReviewers],
89
+ },
90
+ ...(binding.supersededBy ? { supersededBy: [...binding.supersededBy] } : {}),
91
+ };
92
+ }
93
+ function cloneCheckpointBinding(binding) {
94
+ return {
95
+ ...binding,
96
+ contract: {
97
+ ...binding.contract,
98
+ covers: [...binding.contract.covers],
99
+ scope: [...binding.contract.scope],
100
+ requiredReviewers: [...binding.contract.requiredReviewers],
101
+ acceptance: [...binding.contract.acceptance],
102
+ verification: [...binding.contract.verification],
103
+ dependsOn: [...binding.contract.dependsOn],
104
+ },
105
+ ...(binding.currentReview
106
+ ? {
107
+ currentReview: {
108
+ ...binding.currentReview,
109
+ coveredAttemptIds: [...binding.currentReview.coveredAttemptIds],
110
+ results: { ...binding.currentReview.results },
111
+ },
112
+ }
113
+ : {}),
114
+ ...(binding.history ? { history: binding.history.map((entry) => ({ ...entry })) } : {}),
115
+ ...(binding.recoveryHistory
116
+ ? {
117
+ recoveryHistory: binding.recoveryHistory.map((entry) => ({
118
+ ...entry,
119
+ verification: [...entry.verification],
120
+ })),
121
+ }
122
+ : {}),
123
+ };
124
+ }
125
+ /** Deep clone of one execution record for staged persistence commits. */
126
+ export function cloneWorkflowExecution(execution) {
127
+ return {
128
+ ...execution,
129
+ boundary: {
130
+ files: [...execution.boundary.files],
131
+ directories: [...execution.boundary.directories],
132
+ },
133
+ tasks: new Map([...execution.tasks].map(([id, binding]) => [id, cloneTaskBinding(binding)])),
134
+ checkpoints: new Map([...execution.checkpoints].map(([id, binding]) => [id, cloneCheckpointBinding(binding)])),
135
+ lineage: execution.lineage.map((entry) => ({
136
+ ...entry,
137
+ childTaskIds: [...entry.childTaskIds],
138
+ })),
139
+ authority: execution.authority.map((record) => ({
140
+ ...record,
141
+ scope: {
142
+ ...record.scope,
143
+ stages: [...record.scope.stages],
144
+ fileBoundary: [...record.scope.fileBoundary],
145
+ reservedStops: [...record.scope.reservedStops],
146
+ },
147
+ extensions: record.extensions.map((extension) => ({ ...extension })),
148
+ revocations: record.revocations.map((revocation) => ({
149
+ ...revocation,
150
+ ...(revocation.narrowedStages ? { narrowedStages: [...revocation.narrowedStages] } : {}),
151
+ })),
152
+ })),
153
+ stageApprovals: execution.stageApprovals.map((approval) => ({ ...approval })),
154
+ reserveDebits: execution.reserveDebits.map((debit) => ({ ...debit })),
155
+ };
156
+ }
157
+ function sourceIdentityOf(source) {
158
+ switch (source.kind) {
159
+ case "native-package":
160
+ return `native\u0000${source.planPath}\u0000${source.planSha256}\u0000${source.specSha256}`;
161
+ case "provided-plan":
162
+ return `provided\u0000${source.reference}\u0000${source.sha256 ?? ""}`;
163
+ case "conversation-scoped":
164
+ return "conversation-scoped";
165
+ }
166
+ }
167
+ /** Deterministic common run identity from session, stable execution key, and source identity. */
168
+ export function deriveExecutionRunId(sessionId, executionKey, source) {
169
+ return `run-${createHash("sha256")
170
+ .update(`${sessionId}\n${executionKey}\n${sourceIdentityOf(source)}`)
171
+ .digest("hex")
172
+ .slice(0, 16)}`;
173
+ }
174
+ /** Look up one execution by run id. */
175
+ export function findExecution(data, runId) {
176
+ return data.executions.get(runId);
177
+ }
178
+ /** Look up one execution by session and stable execution key. */
179
+ export function findExecutionByKey(data, sessionId, executionKey) {
180
+ for (const execution of data.executions.values()) {
181
+ if (execution.sessionId === sessionId && execution.executionKey === executionKey) {
182
+ return execution;
183
+ }
184
+ }
185
+ return undefined;
186
+ }
187
+ function findRecord(data, sessionId, workItemId) {
188
+ return data.records.get(createRecordLookupKey(sessionId, workItemId));
189
+ }
190
+ function workItemKey(executionKey, taskId) {
191
+ return `exec:${executionKey}:task:${taskId}`;
192
+ }
193
+ function autoCheckpoint(task) {
194
+ return {
195
+ checkpointId: `review-${task.taskId}`,
196
+ kind: "milestone",
197
+ covers: [task.taskId],
198
+ scope: [...task.writeScope],
199
+ requiredReviewers: [...task.requiredReviewers],
200
+ acceptance: [...task.acceptanceCriteria],
201
+ verification: [...task.verification],
202
+ origin: "controller",
203
+ dependsOn: [],
204
+ };
205
+ }
206
+ /** Remove work items created during a failed staged batch so nothing is published. */
207
+ function rollbackCreatedWorkItems(data, sessionId, createdKeys) {
208
+ const index = data.keyIndexBySession.get(sessionId);
209
+ for (const key of createdKeys) {
210
+ const workItemId = index?.get(key);
211
+ if (!workItemId)
212
+ continue;
213
+ index?.delete(key);
214
+ data.records.delete(createRecordLookupKey(sessionId, workItemId));
215
+ }
216
+ }
217
+ function bindingIsBoundElsewhere(data, sessionId, workItemId, runId) {
218
+ for (const execution of data.executions.values()) {
219
+ if (execution.runId === runId)
220
+ continue;
221
+ for (const binding of execution.tasks.values()) {
222
+ if (binding.workItemId === workItemId && execution.sessionId === sessionId)
223
+ return true;
224
+ }
225
+ }
226
+ for (const run of data.planRuns.values()) {
227
+ if (run.sessionId !== sessionId)
228
+ continue;
229
+ for (const task of run.tasks.values()) {
230
+ if (task.workItemId === workItemId)
231
+ return true;
232
+ }
233
+ }
234
+ return false;
235
+ }
236
+ function taskStatusOf(record) {
237
+ if (!record)
238
+ return undefined;
239
+ const delegated = record.delegated;
240
+ if (!delegated)
241
+ return undefined;
242
+ if (record.state === "closed")
243
+ return "accepted";
244
+ const accepted = delegated.acceptances.some((acceptance) => !acceptance.revokedAt);
245
+ if (accepted)
246
+ return "accepted";
247
+ if (delegated.attempts.some((attempt) => attempt.status === "in_flight"))
248
+ return "launched";
249
+ return "pending";
250
+ }
251
+ // END_BLOCK_EXECUTION_INPUTS
252
+ // START_BLOCK_SOURCE_VALIDATION
253
+ function validateSource(source) {
254
+ if (source === null || typeof source !== "object") {
255
+ return { ok: false, message: "source must be an object" };
256
+ }
257
+ switch (source.kind) {
258
+ case "native-package":
259
+ if (typeof source.planPath !== "string" ||
260
+ source.planPath.trim() === "" ||
261
+ typeof source.specPath !== "string" ||
262
+ source.specPath.trim() === "" ||
263
+ typeof source.planSha256 !== "string" ||
264
+ source.planSha256.trim() === "" ||
265
+ typeof source.specSha256 !== "string" ||
266
+ source.specSha256.trim() === "") {
267
+ return { ok: false, message: "native-package source requires plan/spec paths and hashes" };
268
+ }
269
+ return { ok: true };
270
+ case "provided-plan":
271
+ if (typeof source.reference !== "string" ||
272
+ source.reference.trim() === "" ||
273
+ source.reference.trim().length > WORKFLOW_TEXT_MAX_CHARS) {
274
+ return { ok: false, message: "provided-plan source requires a bounded reference" };
275
+ }
276
+ if (source.sha256 !== undefined &&
277
+ (typeof source.sha256 !== "string" || source.sha256.trim() === "")) {
278
+ return {
279
+ ok: false,
280
+ message: "provided-plan sha256 must be a non-empty string when present",
281
+ };
282
+ }
283
+ return { ok: true };
284
+ case "conversation-scoped":
285
+ return { ok: true };
286
+ default:
287
+ return {
288
+ ok: false,
289
+ message: "source.kind must be native-package, provided-plan, or conversation-scoped",
290
+ };
291
+ }
292
+ }
293
+ // END_BLOCK_SOURCE_VALIDATION
294
+ // START_CONTRACT: registerExecutionInStore
295
+ // PURPOSE: Atomically register a common execution and its first task/checkpoint batch without launching agents.
296
+ // INPUTS: { data: WorkItemStoreData - backing store data, input: RegisterExecutionInput - validated registration payload }
297
+ // OUTPUTS: { RegisterExecutionResult - created/reused execution or a coded rejection }
298
+ // SIDE_EFFECTS: [Creates execution registry entry and delegated work items; nothing is published when validation fails]
299
+ // LINKS: [M-WORKFLOW-EXECUTION, validateWorkflowContractGraph, openWorkItemInStore]
300
+ // END_CONTRACT: registerExecutionInStore
301
+ export function registerExecutionInStore(data, input) {
302
+ if (typeof input.sessionId !== "string" || input.sessionId.trim() === "") {
303
+ return {
304
+ ok: false,
305
+ errorCode: "INVALID_INPUT",
306
+ message: "sessionId must be a non-empty string",
307
+ };
308
+ }
309
+ if (typeof input.workspaceRoot !== "string" || !isAbsolute(input.workspaceRoot)) {
310
+ return {
311
+ ok: false,
312
+ errorCode: "INVALID_INPUT",
313
+ message: "workspaceRoot must be an absolute path",
314
+ };
315
+ }
316
+ if (!isBoundedWorkflowId(input.executionKey)) {
317
+ return {
318
+ ok: false,
319
+ errorCode: "INVALID_INPUT",
320
+ message: "executionKey must be a bounded identity",
321
+ };
322
+ }
323
+ const goal = typeof input.goal === "string" ? input.goal.trim() : "";
324
+ if (goal === "" || goal.length > WORKFLOW_TEXT_MAX_CHARS) {
325
+ return { ok: false, errorCode: "INVALID_INPUT", message: "goal must be bounded and non-empty" };
326
+ }
327
+ const sourceCheck = validateSource(input.source);
328
+ if (!sourceCheck.ok) {
329
+ return { ok: false, errorCode: "INVALID_INPUT", message: sourceCheck.message };
330
+ }
331
+ if (!Array.isArray(input.tasks) || input.tasks.length === 0) {
332
+ return {
333
+ ok: false,
334
+ errorCode: "INVALID_INPUT",
335
+ message: "at least one task contract is required",
336
+ };
337
+ }
338
+ const boundaryCheck = validateExecutionBoundary(input.boundary);
339
+ if (!boundaryCheck.ok) {
340
+ return {
341
+ ok: false,
342
+ errorCode: "INVALID_INPUT",
343
+ message: boundaryCheck.problems.map((problem) => problem.message).join("; "),
344
+ };
345
+ }
346
+ const boundary = boundaryCheck.value;
347
+ const tasks = [];
348
+ for (const entry of input.tasks) {
349
+ const validated = validateWorkflowTaskContract(entry.contract);
350
+ if (!validated.ok) {
351
+ return {
352
+ ok: false,
353
+ errorCode: "INVALID_INPUT",
354
+ message: validated.problems.map((problem) => problem.message).join("; "),
355
+ };
356
+ }
357
+ tasks.push(validated.value);
358
+ }
359
+ const checkpoints = [];
360
+ for (const entry of input.checkpoints ?? []) {
361
+ const validated = validateWorkflowCheckpointContract(entry);
362
+ if (!validated.ok) {
363
+ return {
364
+ ok: false,
365
+ errorCode: "INVALID_INPUT",
366
+ message: validated.problems.map((problem) => problem.message).join("; "),
367
+ };
368
+ }
369
+ checkpoints.push(validated.value);
370
+ }
371
+ for (const task of tasks) {
372
+ if (task.requiredReviewers.length > 0)
373
+ checkpoints.push(autoCheckpoint(task));
374
+ }
375
+ const graph = validateWorkflowContractGraph({ tasks, checkpoints, boundary });
376
+ if (!graph.ok) {
377
+ const first = graph.problems[0];
378
+ return {
379
+ ok: false,
380
+ errorCode: graphErrorCode(first.code),
381
+ message: graph.problems.map((problem) => problem.message).join("; "),
382
+ };
383
+ }
384
+ const existing = findExecutionByKey(data, input.sessionId, input.executionKey);
385
+ if (existing) {
386
+ if (sourceIdentityOf(existing.source) !== sourceIdentityOf(input.source)) {
387
+ return {
388
+ ok: false,
389
+ errorCode: "SOURCE_SWITCH",
390
+ message: `execution key ${input.executionKey} is already bound to ${existing.source.kind} source`,
391
+ };
392
+ }
393
+ const sameTasks = existing.tasks.size === tasks.length &&
394
+ tasks.every((task) => {
395
+ const binding = existing.tasks.get(task.taskId);
396
+ return binding !== undefined && JSON.stringify(binding.contract) === JSON.stringify(task);
397
+ });
398
+ const sameBoundary = JSON.stringify(existing.boundary) === JSON.stringify(boundary);
399
+ if (existing.goal !== goal || !sameTasks || !sameBoundary) {
400
+ return {
401
+ ok: false,
402
+ errorCode: "EXECUTION_KEY_CONFLICT",
403
+ message: `execution key ${input.executionKey} is already registered with a different contract`,
404
+ };
405
+ }
406
+ return {
407
+ ok: true,
408
+ runId: existing.runId,
409
+ reused: true,
410
+ execution: cloneWorkflowExecution(existing),
411
+ };
412
+ }
413
+ const runId = deriveExecutionRunId(input.sessionId, input.executionKey, input.source);
414
+ const now = toIsoNow();
415
+ const execution = {
416
+ runId,
417
+ sessionId: input.sessionId,
418
+ workspaceRoot: input.workspaceRoot,
419
+ executionKey: input.executionKey,
420
+ source: input.source,
421
+ goal,
422
+ boundary,
423
+ revision: 1,
424
+ state: "preparing",
425
+ createdAt: now,
426
+ updatedAt: now,
427
+ tasks: new Map(),
428
+ checkpoints: new Map(),
429
+ lineage: [
430
+ {
431
+ lineageId: `lineage-${runId}-initial`,
432
+ kind: "initial",
433
+ amendmentId: "initial",
434
+ childTaskIds: tasks.map((task) => task.taskId),
435
+ previousRevision: 0,
436
+ newRevision: 1,
437
+ rationale: "Initial registration",
438
+ createdAt: now,
439
+ },
440
+ ],
441
+ authority: [],
442
+ stageApprovals: [],
443
+ reserveDebits: [],
444
+ };
445
+ const stagedBindings = [];
446
+ const createdKeys = [];
447
+ const adoptedWorkItemIds = new Set();
448
+ const failRegister = (errorCode, message) => {
449
+ rollbackCreatedWorkItems(data, input.sessionId, createdKeys);
450
+ return { ok: false, errorCode, message };
451
+ };
452
+ for (let index = 0; index < tasks.length; index++) {
453
+ const task = tasks[index];
454
+ const requestedWorkItemId = input.tasks[index]?.workItemId;
455
+ if (requestedWorkItemId !== undefined) {
456
+ if (adoptedWorkItemIds.has(requestedWorkItemId)) {
457
+ return failRegister("DUPLICATE_ID", `work item ${requestedWorkItemId} is adopted for more than one task`);
458
+ }
459
+ adoptedWorkItemIds.add(requestedWorkItemId);
460
+ const record = findRecord(data, input.sessionId, requestedWorkItemId);
461
+ if (!record) {
462
+ return failRegister("WORK_ITEM_NOT_FOUND", `no work item ${requestedWorkItemId} to adopt for task ${task.taskId}`);
463
+ }
464
+ if (record.mode !== "delegated" || !record.delegated) {
465
+ return failRegister("ADOPTION_MISMATCH", `${requestedWorkItemId} is not delegated work`);
466
+ }
467
+ if (bindingIsBoundElsewhere(data, input.sessionId, requestedWorkItemId, runId)) {
468
+ return failRegister("WORK_ITEM_ALREADY_BOUND", `${requestedWorkItemId} is already bound to another execution`);
469
+ }
470
+ if (JSON.stringify([...record.delegated.writeScope].sort()) !==
471
+ JSON.stringify([...task.writeScope].sort())) {
472
+ return failRegister("ADOPTION_MISMATCH", `${requestedWorkItemId} write scope does not match task ${task.taskId}`);
473
+ }
474
+ stagedBindings.push({
475
+ taskId: task.taskId,
476
+ workItemId: requestedWorkItemId,
477
+ contract: task,
478
+ origin: "controller",
479
+ revision: 1,
480
+ status: taskStatusOf(record) ?? "pending",
481
+ });
482
+ continue;
483
+ }
484
+ const created = openWorkItemInStore(data, {
485
+ sessionId: input.sessionId,
486
+ key: workItemKey(input.executionKey, task.taskId),
487
+ title: task.title,
488
+ mode: "delegated",
489
+ requiredReviewers: [],
490
+ writeScope: task.writeScope,
491
+ });
492
+ if (!created.ok) {
493
+ return failRegister("TASK_BINDING_FAILED", created.message);
494
+ }
495
+ createdKeys.push(workItemKey(input.executionKey, task.taskId));
496
+ stagedBindings.push({
497
+ taskId: task.taskId,
498
+ workItemId: created.record.workItemId,
499
+ contract: task,
500
+ origin: "controller",
501
+ revision: 1,
502
+ status: "pending",
503
+ });
504
+ }
505
+ // Publish all bindings only after every item was created or adopted.
506
+ for (const binding of stagedBindings)
507
+ execution.tasks.set(binding.taskId, binding);
508
+ for (const checkpoint of checkpoints) {
509
+ execution.checkpoints.set(checkpoint.checkpointId, {
510
+ checkpointId: checkpoint.checkpointId,
511
+ contract: checkpoint,
512
+ revision: 1,
513
+ });
514
+ }
515
+ // All validation and item creation succeeded; publish the registry entry.
516
+ execution.state = "active";
517
+ data.executions.set(runId, execution);
518
+ return { ok: true, runId, reused: false, execution: cloneWorkflowExecution(execution) };
519
+ }
520
+ function graphErrorCode(code) {
521
+ switch (code) {
522
+ case "DUPLICATE_ID":
523
+ return "DUPLICATE_ID";
524
+ case "UNKNOWN_REFERENCE":
525
+ return "UNKNOWN_REFERENCE";
526
+ case "CYCLIC_DEPENDENCY":
527
+ return "CYCLIC_DEPENDENCY";
528
+ case "OUT_OF_BOUNDARY":
529
+ return "OUT_OF_BOUNDARY";
530
+ case "SELF_REFERENCE":
531
+ return "CYCLIC_DEPENDENCY";
532
+ default:
533
+ return "INVALID_INPUT";
534
+ }
535
+ }
536
+ // START_CONTRACT: appendExecutionWorkInStore
537
+ // PURPOSE: Append validated dependency-ready tasks/checkpoints without replaying accepted work or installing retroactive barriers.
538
+ // INPUTS: { data: WorkItemStoreData - backing store data, input: AppendExecutionWorkInput - amendment payload }
539
+ // OUTPUTS: { AppendExecutionWorkResult - new revision or a coded rejection }
540
+ // SIDE_EFFECTS: [Creates work items and mutates the execution registry only after full batch validation]
541
+ // LINKS: [M-WORKFLOW-EXECUTION]
542
+ // END_CONTRACT: appendExecutionWorkInStore
543
+ export function appendExecutionWorkInStore(data, input) {
544
+ const foundExecution = findExecution(data, input.runId);
545
+ // Mutate a clone so staged transactions and accidental callers never mutate
546
+ // shared live state before the snapshot is persisted.
547
+ const execution = foundExecution ? cloneWorkflowExecution(foundExecution) : undefined;
548
+ if (!execution) {
549
+ return { ok: false, errorCode: "EXECUTION_NOT_FOUND", message: `no execution ${input.runId}` };
550
+ }
551
+ if (execution.sessionId !== input.sessionId) {
552
+ return {
553
+ ok: false,
554
+ errorCode: "SESSION_MISMATCH",
555
+ message: "execution belongs to another session",
556
+ };
557
+ }
558
+ if (execution.state === "sealed") {
559
+ return {
560
+ ok: false,
561
+ errorCode: "EXECUTION_SEALED",
562
+ message: `execution ${input.runId} is sealed`,
563
+ };
564
+ }
565
+ if (execution.source.kind === "native-package") {
566
+ return {
567
+ ok: false,
568
+ errorCode: "NATIVE_REPLACEMENT",
569
+ message: "native structural amendments require the native source lifecycle",
570
+ };
571
+ }
572
+ if (!isBoundedWorkflowId(input.amendmentId)) {
573
+ return {
574
+ ok: false,
575
+ errorCode: "INVALID_INPUT",
576
+ message: "amendmentId must be a bounded identity",
577
+ };
578
+ }
579
+ const rationale = typeof input.rationale === "string" ? input.rationale.trim() : "";
580
+ if (rationale === "" || rationale.length > WORKFLOW_TEXT_MAX_CHARS) {
581
+ return {
582
+ ok: false,
583
+ errorCode: "INVALID_INPUT",
584
+ message: "rationale must be bounded and non-empty",
585
+ };
586
+ }
587
+ const newTasks = [];
588
+ for (const entry of input.tasks ?? []) {
589
+ const validated = validateWorkflowTaskContract(entry.contract);
590
+ if (!validated.ok) {
591
+ return {
592
+ ok: false,
593
+ errorCode: "INVALID_INPUT",
594
+ message: validated.problems.map((problem) => problem.message).join("; "),
595
+ };
596
+ }
597
+ newTasks.push(validated.value);
598
+ }
599
+ const newCheckpoints = [];
600
+ for (const entry of input.checkpoints ?? []) {
601
+ const validated = validateWorkflowCheckpointContract(entry);
602
+ if (!validated.ok) {
603
+ return {
604
+ ok: false,
605
+ errorCode: "INVALID_INPUT",
606
+ message: validated.problems.map((problem) => problem.message).join("; "),
607
+ };
608
+ }
609
+ newCheckpoints.push(validated.value);
610
+ }
611
+ for (const task of newTasks) {
612
+ if (task.requiredReviewers.length > 0)
613
+ newCheckpoints.push(autoCheckpoint(task));
614
+ }
615
+ if (newTasks.length === 0 && newCheckpoints.length === 0) {
616
+ return { ok: false, errorCode: "INVALID_INPUT", message: "amendment declares no additions" };
617
+ }
618
+ const existingTasks = [...execution.tasks.values()].map((binding) => binding.contract);
619
+ const existingCheckpoints = [...execution.checkpoints.values()].map((binding) => binding.contract);
620
+ const combinedTasks = [...existingTasks, ...newTasks];
621
+ const combinedCheckpoints = [...existingCheckpoints, ...newCheckpoints];
622
+ for (const task of newTasks) {
623
+ if (execution.tasks.has(task.taskId)) {
624
+ return {
625
+ ok: false,
626
+ errorCode: "DUPLICATE_ID",
627
+ message: `task ${task.taskId} already exists in ${input.runId}`,
628
+ };
629
+ }
630
+ }
631
+ for (const checkpoint of newCheckpoints) {
632
+ if (execution.checkpoints.has(checkpoint.checkpointId)) {
633
+ return {
634
+ ok: false,
635
+ errorCode: "DUPLICATE_ID",
636
+ message: `checkpoint ${checkpoint.checkpointId} already exists in ${input.runId}`,
637
+ };
638
+ }
639
+ for (const covered of checkpoint.covers) {
640
+ const binding = execution.tasks.get(covered);
641
+ if (binding && (binding.status === "launched" || binding.status === "accepted")) {
642
+ return {
643
+ ok: false,
644
+ errorCode: "RETROACTIVE_BARRIER",
645
+ message: `new checkpoint ${checkpoint.checkpointId} cannot retroactively cover ${binding.status} task ${covered}`,
646
+ };
647
+ }
648
+ }
649
+ }
650
+ for (const task of newTasks) {
651
+ for (const barrier of task.blockedBy) {
652
+ if (execution.checkpoints.has(barrier) &&
653
+ !newCheckpoints.some((c) => c.checkpointId === barrier)) {
654
+ return {
655
+ ok: false,
656
+ errorCode: "RETROACTIVE_BARRIER",
657
+ message: `new task ${task.taskId} cannot be blocked by pre-existing checkpoint ${barrier}`,
658
+ };
659
+ }
660
+ }
661
+ }
662
+ const graph = validateWorkflowContractGraph({
663
+ tasks: combinedTasks,
664
+ checkpoints: combinedCheckpoints,
665
+ boundary: execution.boundary,
666
+ });
667
+ if (!graph.ok) {
668
+ return {
669
+ ok: false,
670
+ errorCode: graphErrorCode(graph.problems[0].code),
671
+ message: graph.problems.map((problem) => problem.message).join("; "),
672
+ };
673
+ }
674
+ const revision = execution.revision + 1;
675
+ const now = toIsoNow();
676
+ const stagedTaskBindings = [];
677
+ const createdKeys = [];
678
+ for (const task of newTasks) {
679
+ const created = openWorkItemInStore(data, {
680
+ sessionId: execution.sessionId,
681
+ key: workItemKey(execution.executionKey, task.taskId),
682
+ title: task.title,
683
+ mode: "delegated",
684
+ requiredReviewers: [],
685
+ writeScope: task.writeScope,
686
+ });
687
+ if (!created.ok) {
688
+ rollbackCreatedWorkItems(data, execution.sessionId, createdKeys);
689
+ return { ok: false, errorCode: "TASK_BINDING_FAILED", message: created.message };
690
+ }
691
+ createdKeys.push(workItemKey(execution.executionKey, task.taskId));
692
+ stagedTaskBindings.push({
693
+ taskId: task.taskId,
694
+ workItemId: created.record.workItemId,
695
+ contract: task,
696
+ origin: "controller",
697
+ revision,
698
+ status: "pending",
699
+ });
700
+ }
701
+ // Publish the batch only after every work item was created.
702
+ for (const binding of stagedTaskBindings)
703
+ execution.tasks.set(binding.taskId, binding);
704
+ for (const checkpoint of newCheckpoints) {
705
+ execution.checkpoints.set(checkpoint.checkpointId, {
706
+ checkpointId: checkpoint.checkpointId,
707
+ contract: checkpoint,
708
+ revision,
709
+ });
710
+ }
711
+ execution.revision = revision;
712
+ execution.updatedAt = now;
713
+ execution.lineage.push({
714
+ lineageId: `lineage-${execution.runId}-${input.amendmentId}`,
715
+ kind: "amendment",
716
+ amendmentId: input.amendmentId,
717
+ childTaskIds: newTasks.map((task) => task.taskId),
718
+ previousRevision: revision - 1,
719
+ newRevision: revision,
720
+ rationale,
721
+ createdAt: now,
722
+ });
723
+ data.executions.set(execution.runId, execution);
724
+ return { ok: true, revision, execution: cloneWorkflowExecution(execution) };
725
+ }
726
+ // START_CONTRACT: adoptWorkItemsInStore
727
+ // PURPOSE: Bind exact compatible unbound standalone work items into an execution without resetting counters or history.
728
+ // INPUTS: { data: WorkItemStoreData - backing store data, input: AdoptWorkItemsInput - run and exact work item/task pairs }
729
+ // OUTPUTS: { AdoptWorkItemsResult - updated execution or a coded rejection }
730
+ // SIDE_EFFECTS: [Rebinds existing task bindings to the supplied work items]
731
+ // LINKS: [M-WORKFLOW-EXECUTION]
732
+ // END_CONTRACT: adoptWorkItemsInStore
733
+ export function adoptWorkItemsInStore(data, input) {
734
+ const foundExecution = findExecution(data, input.runId);
735
+ // Mutate a clone so staged transactions and accidental callers never mutate
736
+ // shared live state before the snapshot is persisted.
737
+ const execution = foundExecution ? cloneWorkflowExecution(foundExecution) : undefined;
738
+ if (!execution) {
739
+ return { ok: false, errorCode: "EXECUTION_NOT_FOUND", message: `no execution ${input.runId}` };
740
+ }
741
+ if (execution.sessionId !== input.sessionId) {
742
+ return {
743
+ ok: false,
744
+ errorCode: "SESSION_MISMATCH",
745
+ message: "execution belongs to another session",
746
+ };
747
+ }
748
+ if (execution.state === "sealed") {
749
+ return {
750
+ ok: false,
751
+ errorCode: "EXECUTION_SEALED",
752
+ message: `execution ${input.runId} is sealed`,
753
+ };
754
+ }
755
+ if (!Array.isArray(input.workItems) || input.workItems.length === 0) {
756
+ return {
757
+ ok: false,
758
+ errorCode: "INVALID_INPUT",
759
+ message: "at least one work item binding is required",
760
+ };
761
+ }
762
+ const seenTaskIds = new Set();
763
+ const seenWorkItemIds = new Set();
764
+ for (const entry of input.workItems) {
765
+ const binding = execution.tasks.get(entry.taskId);
766
+ if (!binding) {
767
+ return {
768
+ ok: false,
769
+ errorCode: "TASK_NOT_FOUND",
770
+ message: `no task ${entry.taskId} in ${input.runId}`,
771
+ };
772
+ }
773
+ if (seenTaskIds.has(entry.taskId) || seenWorkItemIds.has(entry.workItemId)) {
774
+ return {
775
+ ok: false,
776
+ errorCode: "DUPLICATE_ID",
777
+ message: `adoption batch repeats ${entry.taskId} or ${entry.workItemId}`,
778
+ };
779
+ }
780
+ seenTaskIds.add(entry.taskId);
781
+ seenWorkItemIds.add(entry.workItemId);
782
+ for (const other of execution.tasks.values()) {
783
+ if (other.workItemId === entry.workItemId && other.taskId !== entry.taskId) {
784
+ return {
785
+ ok: false,
786
+ errorCode: "DUPLICATE_ID",
787
+ message: `work item ${entry.workItemId} is already bound to task ${other.taskId}`,
788
+ };
789
+ }
790
+ }
791
+ const record = findRecord(data, execution.sessionId, entry.workItemId);
792
+ if (!record) {
793
+ return {
794
+ ok: false,
795
+ errorCode: "WORK_ITEM_NOT_FOUND",
796
+ message: `no work item ${entry.workItemId}`,
797
+ };
798
+ }
799
+ if (record.mode !== "delegated" || !record.delegated) {
800
+ return {
801
+ ok: false,
802
+ errorCode: "ADOPTION_MISMATCH",
803
+ message: `${entry.workItemId} is not delegated work`,
804
+ };
805
+ }
806
+ if (bindingIsBoundElsewhere(data, execution.sessionId, entry.workItemId, execution.runId)) {
807
+ return {
808
+ ok: false,
809
+ errorCode: "WORK_ITEM_ALREADY_BOUND",
810
+ message: `${entry.workItemId} is already bound to another execution`,
811
+ };
812
+ }
813
+ const declared = [...record.delegated.writeScope].sort();
814
+ const expected = [...binding.contract.writeScope].sort();
815
+ if (JSON.stringify(declared) !== JSON.stringify(expected)) {
816
+ return {
817
+ ok: false,
818
+ errorCode: "ADOPTION_MISMATCH",
819
+ message: `${entry.workItemId} write scope does not match task ${entry.taskId}`,
820
+ };
821
+ }
822
+ if (record.sessionId !== execution.sessionId) {
823
+ return {
824
+ ok: false,
825
+ errorCode: "SESSION_MISMATCH",
826
+ message: `${entry.workItemId} belongs to another session`,
827
+ };
828
+ }
829
+ }
830
+ for (const entry of input.workItems) {
831
+ const binding = execution.tasks.get(entry.taskId);
832
+ if (!binding)
833
+ continue;
834
+ const record = findRecord(data, execution.sessionId, entry.workItemId);
835
+ execution.tasks.set(entry.taskId, {
836
+ ...binding,
837
+ workItemId: entry.workItemId,
838
+ status: taskStatusOf(record) ?? binding.status,
839
+ });
840
+ }
841
+ execution.updatedAt = toIsoNow();
842
+ data.executions.set(execution.runId, execution);
843
+ return { ok: true, execution: cloneWorkflowExecution(execution) };
844
+ }
845
+ // START_CONTRACT: splitExecutionTaskInStore
846
+ // PURPOSE: Replace one quiescent unaccepted generic task with explicit descendants that preserve aggregate criteria/scope and share the parent budget.
847
+ // INPUTS: { data: WorkItemStoreData - backing store data, input: SplitExecutionTaskInput - parent identity and descendant contracts }
848
+ // OUTPUTS: { SplitExecutionTaskResult - descendant ids or a coded rejection }
849
+ // SIDE_EFFECTS: [Marks the parent superseded, creates descendant work items inheriting consumed budget, rewires explicit dependents]
850
+ // LINKS: [M-WORKFLOW-EXECUTION, delegatedAttemptBudget]
851
+ // END_CONTRACT: splitExecutionTaskInStore
852
+ export function splitExecutionTaskInStore(data, input) {
853
+ const foundExecution = findExecution(data, input.runId);
854
+ // Mutate a clone so staged transactions and accidental callers never mutate
855
+ // shared live state before the snapshot is persisted.
856
+ const execution = foundExecution ? cloneWorkflowExecution(foundExecution) : undefined;
857
+ if (!execution) {
858
+ return { ok: false, errorCode: "EXECUTION_NOT_FOUND", message: `no execution ${input.runId}` };
859
+ }
860
+ if (execution.sessionId !== input.sessionId) {
861
+ return {
862
+ ok: false,
863
+ errorCode: "SESSION_MISMATCH",
864
+ message: "execution belongs to another session",
865
+ };
866
+ }
867
+ if (execution.state === "sealed") {
868
+ return {
869
+ ok: false,
870
+ errorCode: "EXECUTION_SEALED",
871
+ message: `execution ${input.runId} is sealed`,
872
+ };
873
+ }
874
+ if (execution.source.kind === "native-package") {
875
+ return {
876
+ ok: false,
877
+ errorCode: "NATIVE_REPLACEMENT",
878
+ message: "native structural amendments require the native source lifecycle",
879
+ };
880
+ }
881
+ const parent = execution.tasks.get(input.parentTaskId);
882
+ if (!parent) {
883
+ return { ok: false, errorCode: "TASK_NOT_FOUND", message: `no task ${input.parentTaskId}` };
884
+ }
885
+ if (parent.supersededBy) {
886
+ return {
887
+ ok: false,
888
+ errorCode: "TASK_SUPERSEDED",
889
+ message: `task ${input.parentTaskId} is already superseded`,
890
+ };
891
+ }
892
+ if (parent.status === "accepted") {
893
+ return {
894
+ ok: false,
895
+ errorCode: "TASK_ALREADY_ACCEPTED",
896
+ message: `task ${input.parentTaskId} is accepted`,
897
+ };
898
+ }
899
+ if (parent.status === "launched") {
900
+ return {
901
+ ok: false,
902
+ errorCode: "TASK_NOT_QUIESCENT",
903
+ message: `task ${input.parentTaskId} is launched`,
904
+ };
905
+ }
906
+ const children = [];
907
+ for (const child of input.children ?? []) {
908
+ const validated = validateWorkflowTaskContract(child);
909
+ if (!validated.ok) {
910
+ return {
911
+ ok: false,
912
+ errorCode: "INVALID_INPUT",
913
+ message: validated.problems.map((problem) => problem.message).join("; "),
914
+ };
915
+ }
916
+ if (execution.tasks.has(validated.value.taskId)) {
917
+ return {
918
+ ok: false,
919
+ errorCode: "DUPLICATE_ID",
920
+ message: `descendant ${validated.value.taskId} already exists`,
921
+ };
922
+ }
923
+ children.push(validated.value);
924
+ }
925
+ if (children.length === 0) {
926
+ return { ok: false, errorCode: "INVALID_INPUT", message: "split declares no descendants" };
927
+ }
928
+ if (!isBoundedWorkflowId(input.amendmentId)) {
929
+ return {
930
+ ok: false,
931
+ errorCode: "INVALID_INPUT",
932
+ message: "amendmentId must be a bounded identity",
933
+ };
934
+ }
935
+ const splitRationale = typeof input.rationale === "string" ? input.rationale.trim() : "";
936
+ if (splitRationale === "" || splitRationale.length > WORKFLOW_TEXT_MAX_CHARS) {
937
+ return {
938
+ ok: false,
939
+ errorCode: "INVALID_INPUT",
940
+ message: "rationale must be bounded and non-empty",
941
+ };
942
+ }
943
+ const childCriteria = new Set(children.flatMap((child) => child.acceptanceCriteria));
944
+ for (const criterion of parent.contract.acceptanceCriteria) {
945
+ if (!childCriteria.has(criterion)) {
946
+ return {
947
+ ok: false,
948
+ errorCode: "CRITERIA_NOT_PRESERVED",
949
+ message: `descendants do not retain parent criterion ${JSON.stringify(criterion)}`,
950
+ };
951
+ }
952
+ }
953
+ const childScope = new Set(children.flatMap((child) => child.writeScope));
954
+ for (const file of parent.contract.writeScope) {
955
+ if (!childScope.has(file)) {
956
+ return {
957
+ ok: false,
958
+ errorCode: "SCOPE_NOT_PRESERVED",
959
+ message: `descendants do not cover parent write scope ${JSON.stringify(file)}`,
960
+ };
961
+ }
962
+ }
963
+ const now = toIsoNow();
964
+ const revision = execution.revision + 1;
965
+ const parentRecord = findRecord(data, execution.sessionId, parent.workItemId);
966
+ if (!parentRecord) {
967
+ return {
968
+ ok: false,
969
+ errorCode: "WORK_ITEM_NOT_FOUND",
970
+ message: `missing parent work item ${parent.workItemId}`,
971
+ };
972
+ }
973
+ const parentLedger = parentRecord.delegated;
974
+ // Share the parent's remaining allowance across descendants: the aggregate
975
+ // remaining attempts across children never exceeds the parent's remaining
976
+ // attempts, so a split is not a fresh allowance per child.
977
+ const parentRemaining = parentLedger
978
+ ? Math.max(0, delegatedAttemptBudget(parentLedger) - parentLedger.attempts.length)
979
+ : 0;
980
+ const grantedRemaining = Math.min(parentRemaining, children.length);
981
+ const childIds = [];
982
+ const stagedTaskBindings = [];
983
+ const createdKeys = [];
984
+ let childIndex = 0;
985
+ for (const child of children) {
986
+ const carriedAttempts = Math.max(0, 2 - (childIndex < grantedRemaining ? 1 : 0));
987
+ childIndex += 1;
988
+ const created = openWorkItemInStore(data, {
989
+ sessionId: execution.sessionId,
990
+ key: workItemKey(execution.executionKey, child.taskId),
991
+ title: child.title,
992
+ mode: "delegated",
993
+ requiredReviewers: [],
994
+ writeScope: child.writeScope,
995
+ });
996
+ if (!created.ok) {
997
+ rollbackCreatedWorkItems(data, execution.sessionId, createdKeys);
998
+ return { ok: false, errorCode: "TASK_BINDING_FAILED", message: created.message };
999
+ }
1000
+ createdKeys.push(workItemKey(execution.executionKey, child.taskId));
1001
+ const lookup = createRecordLookupKey(execution.sessionId, created.record.workItemId);
1002
+ const stored = data.records.get(lookup);
1003
+ if (stored) {
1004
+ // Descendants inherit only the parent's consumed attempt count as
1005
+ // historical carried attempts. Decisions, acceptances, rework grants,
1006
+ // and recovery grants are not copied, so no child gains a fresh budget.
1007
+ data.records.set(lookup, {
1008
+ ...stored,
1009
+ delegated: {
1010
+ writeScope: [...child.writeScope],
1011
+ attempts: Array.from({ length: carriedAttempts }, (_, attemptIndex) => ({
1012
+ attempt: attemptIndex + 1,
1013
+ callId: `carried-${execution.runId}-${child.taskId}-${attemptIndex + 1}`,
1014
+ launchedAt: now,
1015
+ status: "completed",
1016
+ resultStatus: "BLOCKED",
1017
+ completedAt: now,
1018
+ })),
1019
+ decisions: [],
1020
+ acceptances: [],
1021
+ reworkHistory: [],
1022
+ recoveryHistory: [],
1023
+ },
1024
+ });
1025
+ }
1026
+ childIds.push(child.taskId);
1027
+ stagedTaskBindings.push({
1028
+ taskId: child.taskId,
1029
+ workItemId: created.record.workItemId,
1030
+ contract: child,
1031
+ origin: "controller",
1032
+ revision,
1033
+ status: "pending",
1034
+ parentTaskId: parent.taskId,
1035
+ });
1036
+ }
1037
+ // Build the post-split graph and validate it before committing: children may
1038
+ // not reference unknown tasks, create cycles, or leave the boundary.
1039
+ const rewiredDependents = [];
1040
+ for (const [taskId, binding] of execution.tasks) {
1041
+ if (taskId === parent.taskId)
1042
+ continue;
1043
+ if (!binding.contract.dependsOn.includes(parent.taskId))
1044
+ continue;
1045
+ const dependsOn = binding.contract.dependsOn.flatMap((dependency) => dependency === parent.taskId ? childIds : [dependency]);
1046
+ rewiredDependents.push({
1047
+ ...binding,
1048
+ contract: { ...binding.contract, dependsOn: [...new Set(dependsOn)] },
1049
+ });
1050
+ }
1051
+ const combinedTasks = [
1052
+ ...[...execution.tasks.values()]
1053
+ .filter((binding) => binding.taskId !== parent.taskId)
1054
+ .map((binding) => rewiredDependents.find((entry) => entry.taskId === binding.taskId)?.contract ??
1055
+ binding.contract),
1056
+ ...children,
1057
+ ];
1058
+ const combinedCheckpoints = [...execution.checkpoints.values()].map((checkpoint) => checkpoint.contract);
1059
+ const graph = validateWorkflowContractGraph({
1060
+ tasks: combinedTasks,
1061
+ checkpoints: combinedCheckpoints,
1062
+ boundary: execution.boundary,
1063
+ });
1064
+ if (!graph.ok) {
1065
+ rollbackCreatedWorkItems(data, execution.sessionId, createdKeys);
1066
+ return {
1067
+ ok: false,
1068
+ errorCode: graphErrorCode(graph.problems[0].code),
1069
+ message: graph.problems.map((problem) => problem.message).join("; "),
1070
+ };
1071
+ }
1072
+ for (const binding of stagedTaskBindings)
1073
+ execution.tasks.set(binding.taskId, binding);
1074
+ execution.tasks.set(parent.taskId, {
1075
+ ...parent,
1076
+ status: "superseded",
1077
+ supersededBy: childIds,
1078
+ revision,
1079
+ });
1080
+ for (const binding of rewiredDependents)
1081
+ execution.tasks.set(binding.taskId, binding);
1082
+ execution.revision = revision;
1083
+ execution.updatedAt = now;
1084
+ execution.lineage.push({
1085
+ lineageId: `lineage-${execution.runId}-${input.amendmentId}`,
1086
+ kind: "split",
1087
+ amendmentId: input.amendmentId,
1088
+ parentTaskId: parent.taskId,
1089
+ childTaskIds: childIds,
1090
+ previousRevision: revision - 1,
1091
+ newRevision: revision,
1092
+ rationale: splitRationale,
1093
+ createdAt: now,
1094
+ });
1095
+ data.executions.set(execution.runId, execution);
1096
+ return { ok: true, childTaskIds: childIds, execution: cloneWorkflowExecution(execution) };
1097
+ }
1098
+ // START_CONTRACT: sealExecutionInStore
1099
+ // PURPOSE: Seal an execution so later additions and implicit reopening are rejected.
1100
+ // INPUTS: { data: WorkItemStoreData - backing store data, input: { sessionId, runId } - target identity }
1101
+ // OUTPUTS: { { ok: true; execution } | { ok: false; errorCode; message } - sealing outcome }
1102
+ // SIDE_EFFECTS: [Mutates the execution lifecycle state]
1103
+ // LINKS: [M-WORKFLOW-EXECUTION]
1104
+ // END_CONTRACT: sealExecutionInStore
1105
+ export function sealExecutionInStore(data, input) {
1106
+ const foundExecution = findExecution(data, input.runId);
1107
+ // Mutate a clone so staged transactions and accidental callers never mutate
1108
+ // shared live state before the snapshot is persisted.
1109
+ const execution = foundExecution ? cloneWorkflowExecution(foundExecution) : undefined;
1110
+ if (!execution) {
1111
+ return { ok: false, errorCode: "EXECUTION_NOT_FOUND", message: `no execution ${input.runId}` };
1112
+ }
1113
+ if (execution.sessionId !== input.sessionId) {
1114
+ return {
1115
+ ok: false,
1116
+ errorCode: "SESSION_MISMATCH",
1117
+ message: "execution belongs to another session",
1118
+ };
1119
+ }
1120
+ execution.state = "sealed";
1121
+ execution.sealedAt = toIsoNow();
1122
+ execution.updatedAt = execution.sealedAt;
1123
+ data.executions.set(execution.runId, execution);
1124
+ return { ok: true, execution: cloneWorkflowExecution(execution) };
1125
+ }
1126
+ // START_CONTRACT: ensureNativeExecutions
1127
+ // PURPOSE: Materialize registry entries for existing native plan runs without inventing authority or altering native counters.
1128
+ // INPUTS: { data: WorkItemStoreData - backing store data }
1129
+ // OUTPUTS: { void - registry entries are added for native runs that lack one }
1130
+ // SIDE_EFFECTS: [Adds compatibility execution records derived from existing native runs]
1131
+ // LINKS: [M-WORKFLOW-EXECUTION, nativeExecutionSource]
1132
+ // END_CONTRACT: ensureNativeExecutions
1133
+ export function ensureNativeExecutions(data) {
1134
+ for (const run of data.planRuns.values()) {
1135
+ if (data.executions.has(run.runId))
1136
+ continue;
1137
+ const tasks = taskContractsFromNativeDefinition(run.definition);
1138
+ const checkpoints = checkpointContractsFromNativeDefinition(run.definition);
1139
+ const execution = {
1140
+ runId: run.runId,
1141
+ sessionId: run.sessionId,
1142
+ workspaceRoot: run.workspaceRoot,
1143
+ executionKey: `native:${run.planPath}`,
1144
+ source: nativeExecutionSource(run),
1145
+ goal: `Native approved plan run ${run.runId}`,
1146
+ boundary: {
1147
+ files: [...new Set(tasks.flatMap((task) => task.writeScope))],
1148
+ directories: [],
1149
+ },
1150
+ revision: 1,
1151
+ state: run.status === "sealed" ? "sealed" : "active",
1152
+ createdAt: run.registeredAt,
1153
+ updatedAt: run.registeredAt,
1154
+ tasks: new Map(tasks.map((task) => [
1155
+ task.taskId,
1156
+ {
1157
+ taskId: task.taskId,
1158
+ workItemId: run.tasks.get(task.taskId)?.workItemId ?? "",
1159
+ contract: task,
1160
+ origin: "source",
1161
+ revision: 1,
1162
+ status: "pending",
1163
+ },
1164
+ ])),
1165
+ checkpoints: new Map(checkpoints.map((checkpoint) => [
1166
+ checkpoint.checkpointId,
1167
+ { checkpointId: checkpoint.checkpointId, contract: checkpoint, revision: 1 },
1168
+ ])),
1169
+ lineage: [],
1170
+ authority: [],
1171
+ stageApprovals: [],
1172
+ reserveDebits: [],
1173
+ };
1174
+ data.executions.set(run.runId, execution);
1175
+ }
1176
+ }
1177
+ // START_CONTRACT: getExecutionView
1178
+ // PURPOSE: Serialize one execution into a stable read-only view for tooling output.
1179
+ // INPUTS: { execution: WorkflowExecutionRecord - registry entry }
1180
+ // OUTPUTS: { object - plain serializable view including budget-relevant status }
1181
+ // SIDE_EFFECTS: [none]
1182
+ // LINKS: [M-WORKFLOW-EXECUTION]
1183
+ // END_CONTRACT: getExecutionView
1184
+ export function getExecutionView(execution) {
1185
+ return {
1186
+ runId: execution.runId,
1187
+ sessionId: execution.sessionId,
1188
+ executionKey: execution.executionKey,
1189
+ sourceKind: execution.source.kind,
1190
+ goal: execution.goal,
1191
+ state: execution.state,
1192
+ revision: execution.revision,
1193
+ tasks: [...execution.tasks.values()].map((binding) => ({
1194
+ taskId: binding.taskId,
1195
+ workItemId: binding.workItemId,
1196
+ status: binding.status,
1197
+ requiredReviewers: [...binding.contract.requiredReviewers],
1198
+ dependsOn: [...binding.contract.dependsOn],
1199
+ blockedBy: [...binding.contract.blockedBy],
1200
+ })),
1201
+ checkpoints: [...execution.checkpoints.values()].map((binding) => ({
1202
+ checkpointId: binding.checkpointId,
1203
+ kind: binding.contract.kind,
1204
+ covers: [...binding.contract.covers],
1205
+ requiredReviewers: [...binding.contract.requiredReviewers],
1206
+ ...(binding.status ? { status: binding.status } : {}),
1207
+ ...(binding.currentReview
1208
+ ? {
1209
+ reviewWorkItemId: binding.currentReview.reviewWorkItemId,
1210
+ generation: binding.currentReview.generation,
1211
+ }
1212
+ : {}),
1213
+ ...(binding.recoveryHistory ? { recoveryCount: binding.recoveryHistory.length } : {}),
1214
+ })),
1215
+ };
1216
+ }
1217
+ // START_BLOCK_EXECUTION_PUBLIC_WRAPPERS
1218
+ /** Register one execution through the store surface used by the plugin and tests. */
1219
+ export function registerExecution(store, input) {
1220
+ return registerExecutionInStore(store.getStoreData(), input);
1221
+ }
1222
+ /** Append validated work through the store surface. */
1223
+ export function appendExecutionWork(store, input) {
1224
+ return appendExecutionWorkInStore(store.getStoreData(), input);
1225
+ }
1226
+ /** Adopt exact compatible unbound work items through the store surface. */
1227
+ export function adoptWorkItems(store, input) {
1228
+ return adoptWorkItemsInStore(store.getStoreData(), input);
1229
+ }
1230
+ /** Split one quiescent task through the store surface. */
1231
+ export function splitExecutionTask(store, input) {
1232
+ return splitExecutionTaskInStore(store.getStoreData(), input);
1233
+ }
1234
+ // END_BLOCK_EXECUTION_PUBLIC_WRAPPERS
1235
+ // START_BLOCK_GENERIC_REVIEWS
1236
+ /** Ordinary generic checkpoint generations before an authorized recovery is required. */
1237
+ export const GENERIC_CHECKPOINT_GENERATIONS = 2;
1238
+ function isTaskAccepted(data, execution, taskId) {
1239
+ const binding = execution.tasks.get(taskId);
1240
+ if (!binding)
1241
+ return false;
1242
+ const record = findRecord(data, execution.sessionId, binding.workItemId);
1243
+ if (!record?.delegated)
1244
+ return false;
1245
+ return record.delegated.acceptances.some((acceptance) => !acceptance.revokedAt);
1246
+ }
1247
+ function coveredAttemptIds(data, execution, checkpoint) {
1248
+ const ids = [];
1249
+ for (const taskId of checkpoint.contract.covers) {
1250
+ const binding = execution.tasks.get(taskId);
1251
+ if (!binding)
1252
+ continue;
1253
+ const record = findRecord(data, execution.sessionId, binding.workItemId);
1254
+ const acceptance = record?.delegated?.acceptances.find((entry) => !entry.revokedAt);
1255
+ if (acceptance)
1256
+ ids.push(`${binding.workItemId}#${acceptance.attempt}`);
1257
+ }
1258
+ return ids;
1259
+ }
1260
+ function findCheckpoint(execution, checkpointId) {
1261
+ return execution.checkpoints.get(checkpointId);
1262
+ }
1263
+ /**
1264
+ * Start one generic checkpoint generation after every covered task is accepted.
1265
+ * The caller supplies the freshly captured fingerprint (the runtime captures a
1266
+ * real filesystem snapshot; pure domain tests may pass a deterministic token).
1267
+ */
1268
+ export function startGenericCheckpointInStore(data, input) {
1269
+ const foundExecution = findExecution(data, input.runId);
1270
+ // Mutate a clone so staged transactions and accidental callers never mutate
1271
+ // shared live state before the snapshot is persisted.
1272
+ const execution = foundExecution ? cloneWorkflowExecution(foundExecution) : undefined;
1273
+ if (!execution) {
1274
+ return { ok: false, errorCode: "EXECUTION_NOT_FOUND", message: `no execution ${input.runId}` };
1275
+ }
1276
+ if (execution.sessionId !== input.sessionId) {
1277
+ return {
1278
+ ok: false,
1279
+ errorCode: "SESSION_MISMATCH",
1280
+ message: "execution belongs to another session",
1281
+ };
1282
+ }
1283
+ if (execution.state === "sealed") {
1284
+ return {
1285
+ ok: false,
1286
+ errorCode: "EXECUTION_SEALED",
1287
+ message: `execution ${input.runId} is sealed`,
1288
+ };
1289
+ }
1290
+ const checkpoint = findCheckpoint(execution, input.checkpointId);
1291
+ if (!checkpoint) {
1292
+ return {
1293
+ ok: false,
1294
+ errorCode: "UNKNOWN_REFERENCE",
1295
+ message: `no checkpoint ${input.checkpointId} in ${input.runId}`,
1296
+ };
1297
+ }
1298
+ if (checkpoint.contract.requiredReviewers.length === 0) {
1299
+ return {
1300
+ ok: false,
1301
+ errorCode: "INVALID_INPUT",
1302
+ message: "an assigned checkpoint requires a non-empty reviewer set",
1303
+ };
1304
+ }
1305
+ if (checkpoint.currentReview) {
1306
+ return {
1307
+ ok: false,
1308
+ errorCode: "INVALID_INPUT",
1309
+ message: `checkpoint ${input.checkpointId} already has an in-flight review generation`,
1310
+ };
1311
+ }
1312
+ if (checkpoint.status === "passed") {
1313
+ return {
1314
+ ok: false,
1315
+ errorCode: "INVALID_INPUT",
1316
+ message: `checkpoint ${input.checkpointId} already passed; re-review requires an explicit amendment or recovery`,
1317
+ };
1318
+ }
1319
+ if (checkpoint.stoppedAtGeneration !== undefined) {
1320
+ return {
1321
+ ok: false,
1322
+ errorCode: "INVALID_INPUT",
1323
+ message: `checkpoint ${input.checkpointId} is stopped after a reviewer NEEDS_CONTEXT; recover it before starting again`,
1324
+ };
1325
+ }
1326
+ const advanceGrants = (checkpoint.recoveryHistory ?? []).filter((entry) => entry.kind === "advance_grant").length;
1327
+ if ((checkpoint.attempts ?? 0) >= GENERIC_CHECKPOINT_GENERATIONS + advanceGrants) {
1328
+ return {
1329
+ ok: false,
1330
+ errorCode: "INVALID_INPUT",
1331
+ message: `checkpoint ${input.checkpointId} exhausted its ordinary generations; explicit recovery is required`,
1332
+ };
1333
+ }
1334
+ for (const taskId of checkpoint.contract.covers) {
1335
+ if (!execution.tasks.has(taskId)) {
1336
+ return {
1337
+ ok: false,
1338
+ errorCode: "UNKNOWN_REFERENCE",
1339
+ message: `checkpoint ${input.checkpointId} covers unknown task ${taskId}`,
1340
+ };
1341
+ }
1342
+ if (!isTaskAccepted(data, execution, taskId)) {
1343
+ return {
1344
+ ok: false,
1345
+ errorCode: "EXECUTION_INCOMPLETE",
1346
+ message: `checkpoint ${input.checkpointId} requires accepted task ${taskId} before it may start`,
1347
+ };
1348
+ }
1349
+ }
1350
+ const covered = coveredAttemptIds(data, execution, checkpoint);
1351
+ // Every start gets a fresh monotonic generation identity so a resumed
1352
+ // generation never reuses the stopped review work item.
1353
+ const generation = (checkpoint.starts ?? 0) + 1;
1354
+ // Open a real review_only work item so reviewer results are recorded through
1355
+ // the tracked launch/result pipeline and bound to this generation by exact
1356
+ // work-item identity rather than asserted by the controller.
1357
+ const opened = openWorkItemInStore(data, {
1358
+ sessionId: input.sessionId,
1359
+ key: `exec:${execution.executionKey}:review:${input.checkpointId}:${generation}`,
1360
+ title: `Review ${input.checkpointId} (generation ${generation})`,
1361
+ mode: "review_only",
1362
+ requiredReviewers: [...checkpoint.contract.requiredReviewers],
1363
+ });
1364
+ if (!opened.ok) {
1365
+ return { ok: false, errorCode: "TASK_BINDING_FAILED", message: opened.message };
1366
+ }
1367
+ const reviewWorkItemId = opened.record.workItemId;
1368
+ const updated = {
1369
+ ...checkpoint,
1370
+ status: "in_review",
1371
+ starts: generation,
1372
+ currentReview: {
1373
+ reviewWorkItemId,
1374
+ generation,
1375
+ startedAt: toIsoNow(),
1376
+ startFingerprint: input.startFingerprint ?? `generic:${execution.revision}:${covered.join(",")}`,
1377
+ coveredAttemptIds: covered,
1378
+ results: {},
1379
+ },
1380
+ };
1381
+ execution.checkpoints.set(input.checkpointId, updated);
1382
+ execution.updatedAt = toIsoNow();
1383
+ data.executions.set(execution.runId, execution);
1384
+ return {
1385
+ ok: true,
1386
+ checkpoint: cloneCheckpointBinding(updated),
1387
+ reviewers: [...checkpoint.contract.requiredReviewers],
1388
+ coveredAttemptIds: covered,
1389
+ reviewWorkItemId,
1390
+ header: opened.header,
1391
+ };
1392
+ }
1393
+ /** Record one generic reviewer outcome and settle the generation when all reviewers reported. */
1394
+ export function recordGenericReviewerResultInStore(data, input) {
1395
+ const foundExecution = findExecution(data, input.runId);
1396
+ // Mutate a clone so staged transactions and accidental callers never mutate
1397
+ // shared live state before the snapshot is persisted.
1398
+ const execution = foundExecution ? cloneWorkflowExecution(foundExecution) : undefined;
1399
+ if (!execution) {
1400
+ return { ok: false, errorCode: "EXECUTION_NOT_FOUND", message: `no execution ${input.runId}` };
1401
+ }
1402
+ if (execution.sessionId !== input.sessionId) {
1403
+ return {
1404
+ ok: false,
1405
+ errorCode: "SESSION_MISMATCH",
1406
+ message: "execution belongs to another session",
1407
+ };
1408
+ }
1409
+ const checkpoint = findCheckpoint(execution, input.checkpointId);
1410
+ const review = checkpoint?.currentReview;
1411
+ if (!checkpoint || !review) {
1412
+ return {
1413
+ ok: false,
1414
+ errorCode: "INVALID_INPUT",
1415
+ message: "no in-flight generic review generation",
1416
+ };
1417
+ }
1418
+ const reviewRecord = findRecord(data, input.sessionId, review.reviewWorkItemId);
1419
+ if (!reviewRecord?.currentRound) {
1420
+ return {
1421
+ ok: false,
1422
+ errorCode: "INVALID_INPUT",
1423
+ message: `no linked review round ${review.reviewWorkItemId}`,
1424
+ };
1425
+ }
1426
+ const targets = input.reviewer ? [input.reviewer] : [...checkpoint.contract.requiredReviewers];
1427
+ for (const reviewer of targets) {
1428
+ if (!checkpoint.contract.requiredReviewers.includes(reviewer)) {
1429
+ return {
1430
+ ok: false,
1431
+ errorCode: "INVALID_INPUT",
1432
+ message: `${reviewer} is not required for checkpoint ${input.checkpointId}`,
1433
+ };
1434
+ }
1435
+ if (review.results[reviewer])
1436
+ continue;
1437
+ // The status is read from the linked review_only work item's recorded
1438
+ // reviewer result; a caller cannot assert a reviewer outcome directly.
1439
+ const recorded = reviewRecord.currentRound.results[reviewer];
1440
+ if (!recorded) {
1441
+ if (input.reviewer) {
1442
+ return {
1443
+ ok: false,
1444
+ errorCode: "INVALID_INPUT",
1445
+ message: `${reviewer} has no recorded reviewer result on ${review.reviewWorkItemId}`,
1446
+ };
1447
+ }
1448
+ continue;
1449
+ }
1450
+ review.results[reviewer] = {
1451
+ status: recorded.status,
1452
+ recordedAt: recorded.completedAt,
1453
+ };
1454
+ }
1455
+ const allReported = checkpoint.contract.requiredReviewers.every((reviewer) => review.results[reviewer] !== undefined);
1456
+ // A reviewer NEEDS_CONTEXT is a hard stop, not a failed generation: settle it
1457
+ // as a recoverable "stopped" outcome without consuming a generation, matching
1458
+ // the native checkpoint semantics.
1459
+ const hasNeedsContext = checkpoint.contract.requiredReviewers.some((reviewer) => review.results[reviewer]?.status === "NEEDS_CONTEXT");
1460
+ if (hasNeedsContext) {
1461
+ const history = [...(checkpoint.history ?? [])];
1462
+ history.push({
1463
+ generation: review.generation,
1464
+ outcome: "stopped",
1465
+ fingerprint: review.startFingerprint,
1466
+ completedAt: toIsoNow(),
1467
+ });
1468
+ const stopped = {
1469
+ ...checkpoint,
1470
+ status: "failed",
1471
+ stoppedAtGeneration: review.generation,
1472
+ currentReview: undefined,
1473
+ history,
1474
+ };
1475
+ execution.checkpoints.set(input.checkpointId, stopped);
1476
+ execution.updatedAt = toIsoNow();
1477
+ data.executions.set(execution.runId, execution);
1478
+ return {
1479
+ ok: true,
1480
+ outcome: "stopped",
1481
+ checkpoint: cloneCheckpointBinding(stopped),
1482
+ };
1483
+ }
1484
+ if (!allReported) {
1485
+ execution.updatedAt = toIsoNow();
1486
+ data.executions.set(execution.runId, execution);
1487
+ return { ok: true, outcome: "in_progress", checkpoint: cloneCheckpointBinding(checkpoint) };
1488
+ }
1489
+ const passed = checkpoint.contract.requiredReviewers.every((reviewer) => review.results[reviewer]?.status === "PASS");
1490
+ const history = [...(checkpoint.history ?? [])];
1491
+ history.push({
1492
+ generation: review.generation,
1493
+ outcome: passed ? "passed" : "failed",
1494
+ fingerprint: review.startFingerprint,
1495
+ completedAt: toIsoNow(),
1496
+ });
1497
+ const settled = {
1498
+ ...checkpoint,
1499
+ status: passed ? "passed" : "failed",
1500
+ attempts: (checkpoint.attempts ?? 0) + 1,
1501
+ ...(passed ? { passedRevision: execution.revision } : {}),
1502
+ currentReview: undefined,
1503
+ history,
1504
+ };
1505
+ execution.checkpoints.set(input.checkpointId, settled);
1506
+ execution.updatedAt = toIsoNow();
1507
+ data.executions.set(execution.runId, execution);
1508
+ return {
1509
+ ok: true,
1510
+ outcome: passed ? "passed" : "failed",
1511
+ checkpoint: cloneCheckpointBinding(settled),
1512
+ };
1513
+ }
1514
+ /** Recover a stopped generic checkpoint (cost-free resume) or one exhausted generation under a recorded advance unit. */
1515
+ export function recoverGenericCheckpointInStore(data, input) {
1516
+ const found = findExecution(data, input.runId);
1517
+ const execution = found ? cloneWorkflowExecution(found) : undefined;
1518
+ if (!execution) {
1519
+ return { ok: false, errorCode: "EXECUTION_NOT_FOUND", message: `no execution ${input.runId}` };
1520
+ }
1521
+ if (execution.sessionId !== input.sessionId) {
1522
+ return {
1523
+ ok: false,
1524
+ errorCode: "SESSION_MISMATCH",
1525
+ message: "execution belongs to another session",
1526
+ };
1527
+ }
1528
+ if (execution.state === "sealed") {
1529
+ return {
1530
+ ok: false,
1531
+ errorCode: "EXECUTION_SEALED",
1532
+ message: `execution ${input.runId} is sealed`,
1533
+ };
1534
+ }
1535
+ const checkpoint = findCheckpoint(execution, input.checkpointId);
1536
+ if (!checkpoint) {
1537
+ return {
1538
+ ok: false,
1539
+ errorCode: "UNKNOWN_REFERENCE",
1540
+ message: `no checkpoint ${input.checkpointId} in ${input.runId}`,
1541
+ };
1542
+ }
1543
+ if (checkpoint.currentReview) {
1544
+ return {
1545
+ ok: false,
1546
+ errorCode: "INVALID_INPUT",
1547
+ message: `checkpoint ${input.checkpointId} has an in-flight review generation`,
1548
+ };
1549
+ }
1550
+ const recoveryHistory = checkpoint.recoveryHistory ?? [];
1551
+ if (recoveryHistory.some((entry) => entry.recoveryId === input.recoveryId)) {
1552
+ return {
1553
+ ok: false,
1554
+ errorCode: "DUPLICATE_ID",
1555
+ message: `recovery ${input.recoveryId} is already recorded for ${input.checkpointId}`,
1556
+ };
1557
+ }
1558
+ const stopped = checkpoint.stoppedAtGeneration !== undefined;
1559
+ const advanceGrants = recoveryHistory.filter((entry) => entry.kind === "advance_grant").length;
1560
+ const budget = GENERIC_CHECKPOINT_GENERATIONS + advanceGrants;
1561
+ if (!stopped && (checkpoint.attempts ?? 0) < budget) {
1562
+ return {
1563
+ ok: false,
1564
+ errorCode: "INVALID_INPUT",
1565
+ message: `checkpoint ${input.checkpointId} is not exhausted; ordinary generations remain`,
1566
+ };
1567
+ }
1568
+ if (!stopped && input.authorityGrant !== true) {
1569
+ return {
1570
+ ok: false,
1571
+ errorCode: "INVALID_INPUT",
1572
+ message: `checkpoint ${input.checkpointId} requires one advance-authority unit to recover`,
1573
+ };
1574
+ }
1575
+ const kind = stopped ? "resume" : "advance_grant";
1576
+ const updated = {
1577
+ ...checkpoint,
1578
+ ...(stopped ? { stoppedAtGeneration: undefined } : {}),
1579
+ recoveryHistory: [
1580
+ ...recoveryHistory,
1581
+ {
1582
+ recoveryId: input.recoveryId,
1583
+ kind,
1584
+ diagnosis: input.diagnosis.trim(),
1585
+ changedCondition: input.changedCondition.trim(),
1586
+ verification: input.verification.map((entry) => entry.trim()),
1587
+ recoveredAt: toIsoNow(),
1588
+ },
1589
+ ],
1590
+ };
1591
+ execution.checkpoints.set(input.checkpointId, updated);
1592
+ execution.updatedAt = toIsoNow();
1593
+ data.executions.set(execution.runId, execution);
1594
+ return { ok: true, checkpoint: cloneCheckpointBinding(updated), kind };
1595
+ }
1596
+ /** Whether a task's declared dependencies and barriers currently allow launch. */
1597
+ export function isTaskLaunchableInStore(data, input) {
1598
+ const foundExecution = findExecution(data, input.runId);
1599
+ // Mutate a clone so staged transactions and accidental callers never mutate
1600
+ // shared live state before the snapshot is persisted.
1601
+ const execution = foundExecution ? cloneWorkflowExecution(foundExecution) : undefined;
1602
+ if (!execution || execution.sessionId !== input.sessionId) {
1603
+ return { ok: false, reason: "TASK_NOT_FOUND", message: `no execution ${input.runId}` };
1604
+ }
1605
+ const binding = execution.tasks.get(input.taskId);
1606
+ if (!binding) {
1607
+ return { ok: false, reason: "TASK_NOT_FOUND", message: `no task ${input.taskId}` };
1608
+ }
1609
+ for (const dependency of binding.contract.dependsOn) {
1610
+ if (!isTaskAccepted(data, execution, dependency)) {
1611
+ return {
1612
+ ok: false,
1613
+ reason: "DEPENDENCIES_UNMET",
1614
+ message: `task ${input.taskId} waits on unaccepted task ${dependency}`,
1615
+ };
1616
+ }
1617
+ }
1618
+ for (const barrier of binding.contract.blockedBy) {
1619
+ const checkpoint = findCheckpoint(execution, barrier);
1620
+ if (!checkpoint || checkpoint.status !== "passed") {
1621
+ return {
1622
+ ok: false,
1623
+ reason: "BARRIER_UNSATISFIED",
1624
+ message: `task ${input.taskId} waits on unpassed checkpoint ${barrier}`,
1625
+ };
1626
+ }
1627
+ }
1628
+ return { ok: true };
1629
+ }
1630
+ /**
1631
+ * Complete a generic execution only when every active task is currently
1632
+ * accepted and every assigned review barrier passed for the current contract
1633
+ * revision. No final checkpoint is required when none was assigned.
1634
+ */
1635
+ export function completeExecutionInStore(data, input) {
1636
+ const foundExecution = findExecution(data, input.runId);
1637
+ // Mutate a clone so staged transactions and accidental callers never mutate
1638
+ // shared live state before the snapshot is persisted.
1639
+ const execution = foundExecution ? cloneWorkflowExecution(foundExecution) : undefined;
1640
+ if (!execution) {
1641
+ return { ok: false, errorCode: "EXECUTION_NOT_FOUND", message: `no execution ${input.runId}` };
1642
+ }
1643
+ if (execution.sessionId !== input.sessionId) {
1644
+ return {
1645
+ ok: false,
1646
+ errorCode: "SESSION_MISMATCH",
1647
+ message: "execution belongs to another session",
1648
+ };
1649
+ }
1650
+ if (execution.state === "sealed") {
1651
+ return {
1652
+ ok: false,
1653
+ errorCode: "EXECUTION_SEALED",
1654
+ message: `execution ${input.runId} is sealed`,
1655
+ };
1656
+ }
1657
+ if (typeof input.rationale !== "string" || input.rationale.trim() === "") {
1658
+ return {
1659
+ ok: false,
1660
+ errorCode: "INVALID_INPUT",
1661
+ message: "completion requires a non-empty rationale",
1662
+ };
1663
+ }
1664
+ const activeTasks = [...execution.tasks.values()].filter((binding) => binding.status !== "superseded");
1665
+ for (const binding of activeTasks) {
1666
+ if (!isTaskAccepted(data, execution, binding.taskId)) {
1667
+ return {
1668
+ ok: false,
1669
+ errorCode: "EXECUTION_INCOMPLETE",
1670
+ message: `task ${binding.taskId} is not accepted`,
1671
+ };
1672
+ }
1673
+ }
1674
+ for (const checkpoint of execution.checkpoints.values()) {
1675
+ if (checkpoint.status !== "passed") {
1676
+ return {
1677
+ ok: false,
1678
+ errorCode: "EXECUTION_INCOMPLETE",
1679
+ message: `assigned checkpoint ${checkpoint.checkpointId} has not passed`,
1680
+ };
1681
+ }
1682
+ }
1683
+ const finalCheckpoints = [...execution.checkpoints.values()].filter((checkpoint) => checkpoint.contract.kind === "final");
1684
+ for (const finalCheckpoint of finalCheckpoints) {
1685
+ if (finalCheckpoint.passedRevision !== execution.revision) {
1686
+ return {
1687
+ ok: false,
1688
+ errorCode: "EXECUTION_INCOMPLETE",
1689
+ message: `final checkpoint ${finalCheckpoint.checkpointId} does not cover the current revision ${execution.revision}`,
1690
+ };
1691
+ }
1692
+ for (const binding of activeTasks) {
1693
+ if (!finalCheckpoint.contract.covers.includes(binding.taskId)) {
1694
+ return {
1695
+ ok: false,
1696
+ errorCode: "EXECUTION_INCOMPLETE",
1697
+ message: `final checkpoint ${finalCheckpoint.checkpointId} does not cover current task ${binding.taskId}`,
1698
+ };
1699
+ }
1700
+ for (const file of binding.contract.writeScope) {
1701
+ if (!finalCheckpoint.contract.scope.includes(file)) {
1702
+ return {
1703
+ ok: false,
1704
+ errorCode: "EXECUTION_INCOMPLETE",
1705
+ message: `final checkpoint ${finalCheckpoint.checkpointId} does not cover current scope ${file}`,
1706
+ };
1707
+ }
1708
+ }
1709
+ }
1710
+ }
1711
+ const sealed = sealExecutionInStore(data, { sessionId: input.sessionId, runId: input.runId });
1712
+ if (!sealed.ok)
1713
+ return { ok: false, errorCode: sealed.errorCode, message: sealed.message };
1714
+ const passedCheckpoints = [...sealed.execution.checkpoints.values()].filter((checkpoint) => checkpoint.status === "passed");
1715
+ const activeBindings = [...sealed.execution.tasks.values()].filter((binding) => binding.status !== "superseded");
1716
+ // Independent review is claimed only when every active task is covered by a
1717
+ // passed checkpoint; mixed or partial coverage is controller-accepted.
1718
+ const reviewStatus = activeBindings.length > 0 &&
1719
+ passedCheckpoints.length > 0 &&
1720
+ activeBindings.every((binding) => passedCheckpoints.some((checkpoint) => checkpoint.contract.covers.includes(binding.taskId)))
1721
+ ? "independently_reviewed"
1722
+ : "controller_accepted";
1723
+ return { ok: true, reviewStatus, execution: sealed.execution };
1724
+ }
1725
+ // END_BLOCK_GENERIC_REVIEWS
1726
+ // START_BLOCK_AUTHORITY_PERSISTENCE
1727
+ function mutateExecution(data, sessionId, runId, mutate) {
1728
+ const found = findExecution(data, runId);
1729
+ if (!found) {
1730
+ return { ok: false, errorCode: "EXECUTION_NOT_FOUND", message: `no execution ${runId}` };
1731
+ }
1732
+ if (found.sessionId !== sessionId) {
1733
+ return {
1734
+ ok: false,
1735
+ errorCode: "SESSION_MISMATCH",
1736
+ message: "execution belongs to another session",
1737
+ };
1738
+ }
1739
+ const execution = cloneWorkflowExecution(found);
1740
+ const applied = mutate(execution);
1741
+ if (!applied.ok)
1742
+ return applied;
1743
+ execution.updatedAt = toIsoNow();
1744
+ data.executions.set(execution.runId, execution);
1745
+ return { ok: true, execution };
1746
+ }
1747
+ /** Persist one advance-authority record and its session-wide message claim atomically. */
1748
+ export function addAuthorityInStore(data, input) {
1749
+ return mutateExecution(data, input.sessionId, input.runId, (execution) => {
1750
+ if (execution.authority.some((entry) => entry.authorityId === input.authority.authorityId)) {
1751
+ return {
1752
+ ok: false,
1753
+ errorCode: "DUPLICATE_ID",
1754
+ message: `authority ${input.authority.authorityId} already recorded`,
1755
+ };
1756
+ }
1757
+ execution.authority.push(input.authority);
1758
+ data.messageClaims.set(input.claim.messageId, input.claim);
1759
+ return { ok: true };
1760
+ });
1761
+ }
1762
+ /** Persist one recorded stage approval on its execution. */
1763
+ export function addStageApprovalInStore(data, input) {
1764
+ return mutateExecution(data, input.sessionId, input.runId, (execution) => {
1765
+ if (execution.stageApprovals.some((entry) => entry.approvalId === input.approval.approvalId)) {
1766
+ return {
1767
+ ok: false,
1768
+ errorCode: "DUPLICATE_ID",
1769
+ message: `approval ${input.approval.approvalId} already recorded`,
1770
+ };
1771
+ }
1772
+ execution.stageApprovals.push(input.approval);
1773
+ return { ok: true };
1774
+ });
1775
+ }
1776
+ /** Persist one reserve debit on its execution. */
1777
+ export function addReserveDebitInStore(data, input) {
1778
+ return mutateExecution(data, input.sessionId, input.runId, (execution) => {
1779
+ if (execution.reserveDebits.some((entry) => entry.recoveryId === input.debit.recoveryId)) {
1780
+ return {
1781
+ ok: false,
1782
+ errorCode: "DUPLICATE_ID",
1783
+ message: `reserve debit ${input.debit.recoveryId} already recorded`,
1784
+ };
1785
+ }
1786
+ execution.reserveDebits.push(input.debit);
1787
+ return { ok: true };
1788
+ });
1789
+ }
1790
+ /** Replace or append one authority record, preserving its reserve history. */
1791
+ export function putAuthorityInStore(data, input) {
1792
+ return mutateExecution(data, input.sessionId, input.runId, (execution) => {
1793
+ const index = execution.authority.findIndex((entry) => entry.authorityId === input.authority.authorityId);
1794
+ if (index >= 0) {
1795
+ execution.authority[index] = input.authority;
1796
+ }
1797
+ else {
1798
+ execution.authority.push(input.authority);
1799
+ }
1800
+ return { ok: true };
1801
+ });
1802
+ }
1803
+ // END_BLOCK_AUTHORITY_PERSISTENCE
1804
+ // Re-exported helper so review/completion modules can reason about inherited
1805
+ // budgets without importing delegated.ts directly.
1806
+ export { delegatedAttemptBudget };
1807
+ //# sourceMappingURL=execution.js.map