@osovv/vv-opencode 1.4.3-rc.0 → 1.4.3

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 (53) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +15 -6
  3. package/dist/lib/orchestration.js +9 -5
  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/vvoc-preset-registry.d.ts +13 -1
  9. package/dist/lib/vvoc-preset-registry.js +11 -1
  10. package/dist/lib/vvoc-preset-registry.js.map +1 -1
  11. package/dist/lib/workflow-contract.d.ts +277 -0
  12. package/dist/lib/workflow-contract.js +636 -0
  13. package/dist/lib/workflow-contract.js.map +1 -0
  14. package/dist/plugins/system-context-injection/index.js +8 -0
  15. package/dist/plugins/system-context-injection/index.js.map +1 -1
  16. package/dist/plugins/workflow/authority.d.ts +99 -0
  17. package/dist/plugins/workflow/authority.js +435 -0
  18. package/dist/plugins/workflow/authority.js.map +1 -0
  19. package/dist/plugins/workflow/checkpoint-io.d.ts +10 -1
  20. package/dist/plugins/workflow/checkpoint-io.js +22 -3
  21. package/dist/plugins/workflow/checkpoint-io.js.map +1 -1
  22. package/dist/plugins/workflow/checkpoints.d.ts +5 -0
  23. package/dist/plugins/workflow/checkpoints.js +15 -4
  24. package/dist/plugins/workflow/checkpoints.js.map +1 -1
  25. package/dist/plugins/workflow/delegated.d.ts +7 -1
  26. package/dist/plugins/workflow/delegated.js +12 -6
  27. package/dist/plugins/workflow/delegated.js.map +1 -1
  28. package/dist/plugins/workflow/execution.d.ts +361 -0
  29. package/dist/plugins/workflow/execution.js +1807 -0
  30. package/dist/plugins/workflow/execution.js.map +1 -0
  31. package/dist/plugins/workflow/index.js +317 -104
  32. package/dist/plugins/workflow/index.js.map +1 -1
  33. package/dist/plugins/workflow/persistence.d.ts +15 -3
  34. package/dist/plugins/workflow/persistence.js +270 -34
  35. package/dist/plugins/workflow/persistence.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 +22 -5
  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 +46 -1
  43. package/dist/plugins/workflow/tooling.js +822 -6
  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-controller.md +12 -7
  51. package/templates/skills/vv-execute/SKILL.md +4 -4
  52. package/templates/skills/vv-plan/SKILL.md +1 -1
  53. package/templates/skills/vv-spec/SKILL.md +2 -2
@@ -2,9 +2,9 @@
2
2
  // VERSION: 0.4.0
3
3
  // START_MODULE_CONTRACT
4
4
  // PURPOSE: Provide work-item tooling handlers that wrap explicit workflow state operations with structured protocol-friendly responses.
5
- // SCOPE: work_item_open, work_item_list, and work_item_close tool definitions with delegated-mode open validation and mode-specific serialization including recovery-aware progress summaries; work_item_decide and work_checkpoint control-tool definitions wrapping delegated decisions, plan registration, checkpoint start/verify, failed-checkpoint rework authorization, and bounded recover for stopped or exhausted targets with optional root-user message authorization through a read-only lookup.
6
- // DEPENDS: [src/plugins/workflow/checkpoint-io.ts, src/plugins/workflow/checkpoints.ts, src/plugins/workflow/delegated.ts, src/plugins/workflow/state.ts]
7
- // LINKS: M-WORKFLOW-TOOLING, M-WORKFLOW-DELEGATED, M-WORKFLOW-CHECKPOINTS, M-PLUGIN-WORKFLOW
5
+ // SCOPE: work_item_open, work_item_list, and work_item_close tool definitions with delegated-mode open validation and mode-specific serialization including recovery-aware progress summaries; generic execution registration/append through an optional execution descriptor or runId; work_item_decide and work_checkpoint control-tool definitions wrapping delegated decisions, native plan registration, checkpoint start/verify, failed-checkpoint rework authorization, bounded recover for stopped or exhausted targets with optional root-user message authorization through a read-only lookup, and the generic (non-native) checkpoint, completion, amendment, advance-authority, stage-approval, and revocation actions.
6
+ // DEPENDS: [src/plugins/workflow/checkpoint-io.ts, src/plugins/workflow/checkpoints.ts, src/plugins/workflow/delegated.ts, src/plugins/workflow/execution.ts, src/plugins/workflow/authority.ts, src/lib/workflow-contract.ts, src/plugins/workflow/state.ts]
7
+ // LINKS: M-WORKFLOW-TOOLING, M-WORKFLOW-DELEGATED, M-WORKFLOW-CHECKPOINTS, M-WORKFLOW-EXECUTION, M-WORKFLOW-AUTHORITY, M-PLUGIN-WORKFLOW
8
8
  // ROLE: RUNTIME
9
9
  // MAP_MODE: EXPORTS
10
10
  // END_MODULE_CONTRACT
@@ -17,7 +17,8 @@
17
17
  // createWorkItemListTool - Creates work_item_list tool wrapper with mode, round metadata, delegated acceptance and recovery state, and registered plan runs.
18
18
  // createWorkItemCloseTool - Creates work_item_close tool wrapper with ready_to_close gating responses.
19
19
  // createWorkItemDecideTool - Creates work_item_decide control wrapper around decideDelegatedWorkItem, rework authorization, and bounded recovery.
20
- // createWorkCheckpointTool - Creates work_checkpoint control wrapper around plan registration, checkpoint start, verify, and checkpoint recovery.
20
+ // createWorkCheckpointTool - Creates work_checkpoint control wrapper around plan registration, checkpoint start, verify, checkpoint recovery, and generic checkpoint/completion/amendment/authority actions.
21
+ // DelegatedControlOptions - Optional read-only authorization lookups bound to the plugin SDK client.
21
22
  // DecideArgs - work_item_decide tool argument shape.
22
23
  // CheckpointArgs - work_checkpoint tool argument shape.
23
24
  // WorkCheckpointRegisterInput - Register-action input discriminator.
@@ -26,11 +27,13 @@
26
27
  // END_MODULE_MAP
27
28
  //
28
29
  // START_CHANGE_SUMMARY
29
- // LAST_CHANGE: [C-WORKFLOW-BOUNDED-RECOVERY-R1 - Added decision recover and work_checkpoint action recover with bounded recovery text, stable recoveryId, optional userMessageId authorization, and recovery-aware inspection output.]
30
+ // LAST_CHANGE: [C-WORKFLOW-PLAN-INDEPENDENCE - Added generic execution registration/append through work_item_open and generic checkpoint, completion, amendment, advance-authority, stage-approval, and revocation actions through work_checkpoint, plus the SDK-backed authority-message lookup seam.]
30
31
  // END_CHANGE_SUMMARY
31
32
  import { closeWorkItem, getReviewRound, listWorkItems, openWorkItem, } from "./state.js";
32
33
  import { authorizeReworkFromFailedCheckpoint, getDelegatedRunView, recoverDelegatedCheckpoint, registerDelegatedPlan, startDelegatedCheckpoint, verifyDelegatedCheckpoint, } from "./checkpoints.js";
33
34
  import { currentDelegatedAcceptance, decideDelegatedWorkItem, recoverDelegatedWorkItem, summarizeDelegatedProgress, } from "./delegated.js";
35
+ import { addAuthorityInStore, addReserveDebitInStore, addStageApprovalInStore, appendExecutionWorkInStore, completeExecutionInStore, findExecution, getExecutionView, recordGenericReviewerResultInStore, recoverGenericCheckpointInStore, putAuthorityInStore, registerExecutionInStore, startGenericCheckpointInStore, } from "./execution.js";
36
+ import { advanceUnitsAvailable, effectiveAuthorityStages, extendAdvanceAuthority, grantAdvanceAuthority, proposeReserveDebit, proposeStageApproval, revokeAdvanceAuthority, narrowAdvanceAuthority, } from "./authority.js";
34
37
  function coerceNonEmptyString(value) {
35
38
  if (typeof value !== "string")
36
39
  return undefined;
@@ -57,6 +60,89 @@ function canonicalizeReviewers(value) {
57
60
  return left === "spec" ? -1 : 1;
58
61
  });
59
62
  }
63
+ // START_BLOCK_GENERIC_NORMALIZATION
64
+ function isWorkflowReviewer(value) {
65
+ return value === "spec" || value === "code";
66
+ }
67
+ function stringList(value) {
68
+ return Array.isArray(value) ? value.map(String) : [];
69
+ }
70
+ /** Normalize one tool item into a bounded generic task contract. */
71
+ function normalizeTaskContract(item) {
72
+ const taskId = coerceNonEmptyString(item.taskId) ?? coerceNonEmptyString(item.key);
73
+ const title = coerceNonEmptyString(item.title) ?? taskId;
74
+ if (!taskId || !title) {
75
+ return { ok: false, message: "each task item requires a non-empty taskId (or key) and title" };
76
+ }
77
+ if (!Array.isArray(item.requiredReviewers)) {
78
+ return {
79
+ ok: false,
80
+ message: "requiredReviewers must be declared explicitly for each task (use [] for no independent review)",
81
+ };
82
+ }
83
+ const reviewers = item.requiredReviewers;
84
+ if (!reviewers.every(isWorkflowReviewer) || new Set(reviewers).size !== reviewers.length) {
85
+ return { ok: false, message: "requiredReviewers must be a unique spec/code array" };
86
+ }
87
+ return {
88
+ ok: true,
89
+ contract: {
90
+ taskId,
91
+ title,
92
+ goal: coerceNonEmptyString(item.goal) ?? title,
93
+ acceptanceCriteria: stringList(item.acceptanceCriteria),
94
+ verification: stringList(item.verification),
95
+ writeScope: stringList(item.writeScope),
96
+ dependsOn: stringList(item.dependsOn),
97
+ blockedBy: stringList(item.blockedBy),
98
+ requiredReviewers: [...reviewers],
99
+ },
100
+ };
101
+ }
102
+ function normalizeExecutionSource(raw) {
103
+ if (raw === null || typeof raw !== "object")
104
+ return undefined;
105
+ const candidate = raw;
106
+ if (candidate.kind === "conversation-scoped")
107
+ return { kind: "conversation-scoped" };
108
+ if (candidate.kind === "provided-plan") {
109
+ const reference = coerceNonEmptyString(candidate.reference);
110
+ if (!reference)
111
+ return undefined;
112
+ const sha256 = coerceNonEmptyString(candidate.sha256);
113
+ return { kind: "provided-plan", reference, ...(sha256 ? { sha256 } : {}) };
114
+ }
115
+ // Native packages must register through work_checkpoint register with a
116
+ // planPath; the generic descriptor never fabricates a native source.
117
+ return undefined;
118
+ }
119
+ function normalizeBoundary(raw) {
120
+ if (raw === null || typeof raw !== "object")
121
+ return undefined;
122
+ const candidate = raw;
123
+ const files = stringList(candidate.files);
124
+ const directories = stringList(candidate.directories);
125
+ if (files.length === 0 && directories.length === 0)
126
+ return undefined;
127
+ return { files, directories };
128
+ }
129
+ function normalizeCheckpointContracts(raw) {
130
+ return Array.isArray(raw) ? raw : [];
131
+ }
132
+ function taskContractsFromItems(items) {
133
+ const contracts = [];
134
+ for (const item of items) {
135
+ const normalized = normalizeTaskContract(item);
136
+ if (!normalized.ok)
137
+ return { ok: false, message: normalized.message };
138
+ contracts.push(normalized.contract);
139
+ }
140
+ if (contracts.length === 0) {
141
+ return { ok: false, message: "at least one task item is required" };
142
+ }
143
+ return { ok: true, contracts };
144
+ }
145
+ // END_BLOCK_GENERIC_NORMALIZATION
60
146
  function normalizeOpenInputItem(item, sessionId) {
61
147
  const key = coerceNonEmptyString(item.key);
62
148
  const title = coerceNonEmptyString(item.title);
@@ -203,6 +289,114 @@ export function createWorkItemOpenTool(store) {
203
289
  description: "Open one or more workflow work items idempotently with explicit mode and requiredReviewers.",
204
290
  execute: (args, context, overrideStore) => {
205
291
  const inputItems = Array.isArray(args.items) ? args.items : [];
292
+ const runIdArg = coerceNonEmptyString(args.runId);
293
+ const executionArg = args.execution;
294
+ if (executionArg !== undefined && runIdArg !== undefined) {
295
+ return {
296
+ tool: "work_item_open",
297
+ sessionId: context.sessionId,
298
+ ok: false,
299
+ errorCode: "INVALID_INPUT",
300
+ message: "INVALID_INPUT: execution and runId are mutually exclusive",
301
+ };
302
+ }
303
+ if (executionArg !== undefined || runIdArg !== undefined) {
304
+ const s = overrideStore ?? store;
305
+ const data = s.getStoreData();
306
+ const contracts = taskContractsFromItems(inputItems);
307
+ if (!contracts.ok) {
308
+ return {
309
+ tool: "work_item_open",
310
+ sessionId: context.sessionId,
311
+ ok: false,
312
+ errorCode: "INVALID_INPUT",
313
+ message: `INVALID_INPUT: ${contracts.message}`,
314
+ };
315
+ }
316
+ if (executionArg !== undefined) {
317
+ const descriptor = executionArg !== null && typeof executionArg === "object"
318
+ ? executionArg
319
+ : {};
320
+ const executionKey = coerceNonEmptyString(descriptor.executionKey);
321
+ const source = normalizeExecutionSource(descriptor.source);
322
+ const boundary = normalizeBoundary(descriptor.boundary);
323
+ const goal = coerceNonEmptyString(descriptor.goal);
324
+ const workspaceRoot = coerceNonEmptyString(context.workspaceRoot);
325
+ if (!executionKey || !source || !boundary || !goal || !workspaceRoot) {
326
+ return {
327
+ tool: "work_item_open",
328
+ sessionId: context.sessionId,
329
+ ok: false,
330
+ errorCode: "INVALID_INPUT",
331
+ message: "INVALID_INPUT: execution requires executionKey, source, goal, boundary, and the trusted workspace root",
332
+ };
333
+ }
334
+ const registered = registerExecutionInStore(data, {
335
+ sessionId: context.sessionId,
336
+ workspaceRoot,
337
+ executionKey,
338
+ source,
339
+ goal,
340
+ boundary,
341
+ tasks: contracts.contracts.map((contract) => ({ contract })),
342
+ checkpoints: normalizeCheckpointContracts(descriptor.checkpoints),
343
+ });
344
+ if (!registered.ok) {
345
+ return {
346
+ tool: "work_item_open",
347
+ sessionId: context.sessionId,
348
+ ok: false,
349
+ errorCode: registered.errorCode,
350
+ message: registered.message,
351
+ };
352
+ }
353
+ return {
354
+ tool: "work_item_open",
355
+ sessionId: context.sessionId,
356
+ ok: true,
357
+ action: "register",
358
+ runId: registered.runId,
359
+ reused: registered.reused,
360
+ execution: getExecutionView(registered.execution),
361
+ };
362
+ }
363
+ const amendmentId = coerceNonEmptyString(args.amendmentId);
364
+ const rationale = coerceNonEmptyString(args.rationale);
365
+ if (!runIdArg || !amendmentId || !rationale) {
366
+ return {
367
+ tool: "work_item_open",
368
+ sessionId: context.sessionId,
369
+ ok: false,
370
+ errorCode: "INVALID_INPUT",
371
+ message: "INVALID_INPUT: runId append requires amendmentId and rationale",
372
+ };
373
+ }
374
+ const appended = appendExecutionWorkInStore(data, {
375
+ sessionId: context.sessionId,
376
+ runId: runIdArg,
377
+ amendmentId,
378
+ rationale,
379
+ tasks: contracts.contracts.map((contract) => ({ contract })),
380
+ });
381
+ if (!appended.ok) {
382
+ return {
383
+ tool: "work_item_open",
384
+ sessionId: context.sessionId,
385
+ ok: false,
386
+ errorCode: appended.errorCode,
387
+ message: appended.message,
388
+ };
389
+ }
390
+ return {
391
+ tool: "work_item_open",
392
+ sessionId: context.sessionId,
393
+ ok: true,
394
+ action: "amend",
395
+ runId: runIdArg,
396
+ revision: appended.revision,
397
+ execution: getExecutionView(appended.execution),
398
+ };
399
+ }
206
400
  const results = inputItems.map((item) => {
207
401
  const normalized = normalizeOpenInputItem(item, context.sessionId);
208
402
  if (!normalized.ok) {
@@ -383,6 +577,7 @@ export function createWorkItemDecideTool(store, options) {
383
577
  const changedCondition = typeof args.changedCondition === "string" ? args.changedCondition : "";
384
578
  const verification = Array.isArray(args.verification) ? args.verification.map(String) : [];
385
579
  const userMessageId = coerceNonEmptyString(args.userMessageId);
580
+ const authorityId = coerceNonEmptyString(args.authorityId);
386
581
  if (!recoveryId) {
387
582
  return {
388
583
  tool: "work_item_decide",
@@ -392,6 +587,53 @@ export function createWorkItemDecideTool(store, options) {
392
587
  message: "INVALID_INPUT: recover requires a stable recoveryId, diagnosis, changedCondition, and verification references",
393
588
  };
394
589
  }
590
+ let advanceGrantApproved = false;
591
+ let advanceRunId;
592
+ if (authorityId) {
593
+ advanceRunId = coerceNonEmptyString(args.runId);
594
+ if (!advanceRunId) {
595
+ return {
596
+ tool: "work_item_decide",
597
+ sessionId: context.sessionId,
598
+ ok: false,
599
+ errorCode: "INVALID_INPUT",
600
+ message: "INVALID_INPUT: authorityId recovery requires the owning runId",
601
+ };
602
+ }
603
+ const execution = findExecution(s.getStoreData(), advanceRunId);
604
+ const authority = execution?.authority.find((entry) => entry.authorityId === authorityId);
605
+ if (!execution || !authority) {
606
+ return {
607
+ tool: "work_item_decide",
608
+ sessionId: context.sessionId,
609
+ ok: false,
610
+ errorCode: "AUTHORITY_NOT_FOUND",
611
+ message: `AUTHORITY_NOT_FOUND: no recorded authority ${authorityId} for ${advanceRunId}`,
612
+ };
613
+ }
614
+ // Scope binding: the recovered item must be a task of the execution
615
+ // whose reserve funds the recovery.
616
+ if (![...execution.tasks.values()].some((task) => task.workItemId === workItemId)) {
617
+ return {
618
+ tool: "work_item_decide",
619
+ sessionId: context.sessionId,
620
+ ok: false,
621
+ errorCode: "INVALID_INPUT",
622
+ message: `INVALID_INPUT: work item ${workItemId} is not a task of execution ${advanceRunId}`,
623
+ };
624
+ }
625
+ const proposed = proposeReserveDebit({
626
+ authority,
627
+ debits: execution.reserveDebits,
628
+ recoveryId,
629
+ targetKind: "task",
630
+ targetId: workItemId,
631
+ });
632
+ // A failed proposal only means no advance unit is available; the
633
+ // recovery may still be a cost-free resume or use an ordinary
634
+ // allowance, so let the domain reducer decide.
635
+ advanceGrantApproved = proposed.ok;
636
+ }
395
637
  const recovered = await recoverDelegatedWorkItem(s, {
396
638
  sessionId: context.sessionId,
397
639
  workItemId,
@@ -400,6 +642,7 @@ export function createWorkItemDecideTool(store, options) {
400
642
  changedCondition,
401
643
  verification,
402
644
  recoveryId,
645
+ ...(advanceGrantApproved ? { advanceGrantApproved: true } : {}),
403
646
  ...(userMessageId !== undefined
404
647
  ? { userMessageId, lookupUserMessage: options?.lookupUserMessage }
405
648
  : {}),
@@ -413,6 +656,42 @@ export function createWorkItemDecideTool(store, options) {
413
656
  message: recovered.message,
414
657
  };
415
658
  }
659
+ if (recovered.kind === "advance_grant" && advanceRunId !== undefined) {
660
+ const execution = findExecution(s.getStoreData(), advanceRunId);
661
+ const authority = execution?.authority.find((entry) => entry.authorityId === authorityId);
662
+ const proposed = authority
663
+ ? proposeReserveDebit({
664
+ authority,
665
+ debits: execution?.reserveDebits ?? [],
666
+ recoveryId,
667
+ targetKind: "task",
668
+ targetId: workItemId,
669
+ })
670
+ : undefined;
671
+ if (!proposed?.ok) {
672
+ return {
673
+ tool: "work_item_decide",
674
+ sessionId: context.sessionId,
675
+ ok: false,
676
+ errorCode: proposed?.code ?? "RESERVE_EXHAUSTED",
677
+ message: proposed?.message ?? "advance recovery debit could not be recorded with the grant",
678
+ };
679
+ }
680
+ const stored = addReserveDebitInStore(s.getStoreData(), {
681
+ sessionId: context.sessionId,
682
+ runId: advanceRunId,
683
+ debit: proposed.value,
684
+ });
685
+ if (!stored.ok) {
686
+ return {
687
+ tool: "work_item_decide",
688
+ sessionId: context.sessionId,
689
+ ok: false,
690
+ errorCode: stored.errorCode,
691
+ message: stored.message,
692
+ };
693
+ }
694
+ }
416
695
  return {
417
696
  tool: "work_item_decide",
418
697
  sessionId: context.sessionId,
@@ -468,6 +747,464 @@ export function createWorkItemDecideTool(store, options) {
468
747
  },
469
748
  };
470
749
  }
750
+ // START_CONTRACT: executeGenericCheckpoint
751
+ // PURPOSE: Execute one generic (non-native) checkpoint/authority action against the common execution registry.
752
+ // INPUTS: { data, sessionId, runId, args, control? }
753
+ // OUTPUTS: { Promise<Record<string, unknown>> - structured tool result }
754
+ // SIDE_EFFECTS: [Mutates the common execution registry and may consult the SDK-backed authority lookup]
755
+ // LINKS: [M-WORKFLOW-TOOLING, M-WORKFLOW-EXECUTION, M-WORKFLOW-AUTHORITY]
756
+ // END_CONTRACT: executeGenericCheckpoint
757
+ async function executeGenericCheckpoint(options) {
758
+ const { data, sessionId, runId, args } = options;
759
+ const base = { tool: "work_checkpoint", sessionId, runId };
760
+ const execution = findExecution(data, runId);
761
+ if (!execution) {
762
+ return {
763
+ ...base,
764
+ ok: false,
765
+ errorCode: "EXECUTION_NOT_FOUND",
766
+ message: `no execution ${runId}`,
767
+ };
768
+ }
769
+ if (execution.sessionId !== sessionId) {
770
+ return {
771
+ ...base,
772
+ ok: false,
773
+ errorCode: "SESSION_MISMATCH",
774
+ message: "execution belongs to another session",
775
+ };
776
+ }
777
+ const amendmentId = coerceNonEmptyString(args.amendmentId);
778
+ const rationale = coerceNonEmptyString(args.rationale);
779
+ switch (args.action) {
780
+ case "register":
781
+ case "amend": {
782
+ if (!amendmentId || !rationale) {
783
+ return {
784
+ ...base,
785
+ ok: false,
786
+ errorCode: "INVALID_INPUT",
787
+ message: "generic register/amend requires amendmentId and rationale",
788
+ };
789
+ }
790
+ const items = Array.isArray(args.tasks) ? args.tasks : [];
791
+ const contracts = items.length > 0 ? taskContractsFromItems(items) : { ok: true, contracts: [] };
792
+ if (!contracts.ok) {
793
+ return { ...base, ok: false, errorCode: "INVALID_INPUT", message: contracts.message };
794
+ }
795
+ const appended = appendExecutionWorkInStore(data, {
796
+ sessionId,
797
+ runId,
798
+ amendmentId,
799
+ rationale,
800
+ tasks: contracts.contracts.map((contract) => ({ contract })),
801
+ checkpoints: normalizeCheckpointContracts(args.checkpoints),
802
+ });
803
+ if (!appended.ok) {
804
+ return { ...base, ok: false, errorCode: appended.errorCode, message: appended.message };
805
+ }
806
+ return {
807
+ ...base,
808
+ ok: true,
809
+ action: "amend",
810
+ revision: appended.revision,
811
+ execution: getExecutionView(appended.execution),
812
+ };
813
+ }
814
+ case "start": {
815
+ const checkpointId = coerceNonEmptyString(args.checkpointId);
816
+ if (!checkpointId) {
817
+ return {
818
+ ...base,
819
+ ok: false,
820
+ errorCode: "INVALID_INPUT",
821
+ message: "start requires checkpointId",
822
+ };
823
+ }
824
+ const startFingerprint = coerceNonEmptyString(args.startFingerprint);
825
+ const started = startGenericCheckpointInStore(data, {
826
+ sessionId,
827
+ runId,
828
+ checkpointId,
829
+ ...(startFingerprint ? { startFingerprint } : {}),
830
+ });
831
+ if (!started.ok) {
832
+ return { ...base, ok: false, errorCode: started.errorCode, message: started.message };
833
+ }
834
+ return {
835
+ ...base,
836
+ ok: true,
837
+ action: "start",
838
+ checkpointId,
839
+ generation: started.checkpoint.currentReview?.generation,
840
+ reviewWorkItemId: started.reviewWorkItemId,
841
+ header: started.header,
842
+ reviewersToLaunch: started.reviewers,
843
+ coveredAttemptIds: started.coveredAttemptIds,
844
+ };
845
+ }
846
+ case "review":
847
+ case "bind":
848
+ case "verify": {
849
+ const checkpointId = coerceNonEmptyString(args.checkpointId);
850
+ if (!checkpointId) {
851
+ return {
852
+ ...base,
853
+ ok: false,
854
+ errorCode: "INVALID_INPUT",
855
+ message: "checkpointId is required",
856
+ };
857
+ }
858
+ const reviewer = coerceNonEmptyString(args.reviewer);
859
+ if (reviewer !== undefined && reviewer !== "spec" && reviewer !== "code") {
860
+ return {
861
+ ...base,
862
+ ok: false,
863
+ errorCode: "INVALID_INPUT",
864
+ message: "reviewer must be spec or code",
865
+ };
866
+ }
867
+ // The reviewer status is read from the linked review_only work item's
868
+ // recorded round; callers cannot assert a reviewer outcome directly.
869
+ const recorded = recordGenericReviewerResultInStore(data, {
870
+ sessionId,
871
+ runId,
872
+ checkpointId,
873
+ ...(reviewer ? { reviewer } : {}),
874
+ });
875
+ if (!recorded.ok) {
876
+ return { ...base, ok: false, errorCode: recorded.errorCode, message: recorded.message };
877
+ }
878
+ return {
879
+ ...base,
880
+ ok: true,
881
+ action: args.action,
882
+ checkpointId,
883
+ ...(reviewer ? { reviewer } : {}),
884
+ outcome: recorded.outcome,
885
+ checkpointStatus: recorded.checkpoint.status,
886
+ };
887
+ }
888
+ case "recover": {
889
+ const checkpointId = coerceNonEmptyString(args.checkpointId);
890
+ const recoveryId = coerceNonEmptyString(args.recoveryId);
891
+ const diagnosis = typeof args.diagnosis === "string" ? args.diagnosis : "";
892
+ const changedCondition = typeof args.changedCondition === "string" ? args.changedCondition : "";
893
+ const verification = stringList(args.verification);
894
+ const authorityId = coerceNonEmptyString(args.authorityId);
895
+ if (!checkpointId || !recoveryId) {
896
+ return {
897
+ ...base,
898
+ ok: false,
899
+ errorCode: "INVALID_INPUT",
900
+ message: "generic checkpoint recovery requires checkpointId and recoveryId",
901
+ };
902
+ }
903
+ const checkpointBinding = execution.checkpoints.get(checkpointId);
904
+ const stopped = checkpointBinding?.stoppedAtGeneration !== undefined;
905
+ // Validate and reserve the advance unit BEFORE mutating the checkpoint,
906
+ // so a failed debit can never leave a granted generation behind.
907
+ let reservedDebit;
908
+ if (!stopped) {
909
+ if (!authorityId) {
910
+ return {
911
+ ...base,
912
+ ok: false,
913
+ errorCode: "INVALID_INPUT",
914
+ message: "an exhausted generic checkpoint requires an authorityId to recover",
915
+ };
916
+ }
917
+ const authority = execution.authority.find((entry) => entry.authorityId === authorityId);
918
+ if (!authority) {
919
+ return {
920
+ ...base,
921
+ ok: false,
922
+ errorCode: "AUTHORITY_NOT_FOUND",
923
+ message: `AUTHORITY_NOT_FOUND: no recorded authority ${authorityId}`,
924
+ };
925
+ }
926
+ const proposed = proposeReserveDebit({
927
+ authority,
928
+ debits: execution.reserveDebits,
929
+ recoveryId,
930
+ targetKind: "checkpoint",
931
+ targetId: checkpointId,
932
+ });
933
+ if (!proposed.ok) {
934
+ return { ...base, ok: false, errorCode: proposed.code, message: proposed.message };
935
+ }
936
+ reservedDebit = proposed.value;
937
+ }
938
+ const recovered = recoverGenericCheckpointInStore(data, {
939
+ sessionId,
940
+ runId,
941
+ checkpointId,
942
+ recoveryId,
943
+ diagnosis,
944
+ changedCondition,
945
+ verification,
946
+ authorityGrant: reservedDebit !== undefined,
947
+ });
948
+ if (!recovered.ok) {
949
+ return { ...base, ok: false, errorCode: recovered.errorCode, message: recovered.message };
950
+ }
951
+ if (recovered.kind === "advance_grant" && reservedDebit) {
952
+ const stored = addReserveDebitInStore(data, {
953
+ sessionId,
954
+ runId,
955
+ debit: reservedDebit,
956
+ });
957
+ if (!stored.ok) {
958
+ return { ...base, ok: false, errorCode: stored.errorCode, message: stored.message };
959
+ }
960
+ }
961
+ return {
962
+ ...base,
963
+ ok: true,
964
+ action: "recover",
965
+ checkpointId,
966
+ recoveryId,
967
+ kind: recovered.kind,
968
+ checkpointStatus: recovered.checkpoint.status,
969
+ };
970
+ }
971
+ case "complete": {
972
+ const completed = completeExecutionInStore(data, {
973
+ sessionId,
974
+ runId,
975
+ rationale: rationale ?? "Generic execution completed with controller acceptance.",
976
+ evidence: stringList(args.verification),
977
+ });
978
+ if (!completed.ok) {
979
+ return { ...base, ok: false, errorCode: completed.errorCode, message: completed.message };
980
+ }
981
+ return {
982
+ ...base,
983
+ ok: true,
984
+ action: "complete",
985
+ reviewStatus: completed.reviewStatus,
986
+ execution: getExecutionView(completed.execution),
987
+ };
988
+ }
989
+ case "authorize": {
990
+ const authorityId = coerceNonEmptyString(args.authorityId);
991
+ const messageId = coerceNonEmptyString(args.messageId);
992
+ const stagesArg = Array.isArray(args.stages) ? args.stages : [];
993
+ const stages = stagesArg.filter((stage) => stage === "specification" ||
994
+ stage === "planning" ||
995
+ stage === "implementation" ||
996
+ stage === "verification");
997
+ if (!authorityId || !messageId || stages.length === 0) {
998
+ return {
999
+ ...base,
1000
+ ok: false,
1001
+ errorCode: "INVALID_INPUT",
1002
+ message: "authorize requires authorityId, messageId, and explicit stages",
1003
+ };
1004
+ }
1005
+ const lookup = options.control?.lookupAuthorityMessage;
1006
+ if (!lookup) {
1007
+ return {
1008
+ ...base,
1009
+ ok: false,
1010
+ errorCode: "INVALID_INPUT",
1011
+ message: "authorize requires the SDK-backed authorization lookup",
1012
+ };
1013
+ }
1014
+ const message = await lookup({ sessionId, runId, messageId });
1015
+ if (!message) {
1016
+ return {
1017
+ ...base,
1018
+ ok: false,
1019
+ errorCode: "AUTHORITY_DENIED",
1020
+ message: "authorization message could not be verified as an eligible root-user instruction",
1021
+ };
1022
+ }
1023
+ const existingAuthority = execution.authority.find((entry) => entry.authorityId === authorityId);
1024
+ if (existingAuthority) {
1025
+ if (existingAuthority.grantedByMessageId === messageId) {
1026
+ // Idempotent replay of the originating registration.
1027
+ return {
1028
+ ...base,
1029
+ ok: true,
1030
+ action: "authorize",
1031
+ authorityId,
1032
+ reused: true,
1033
+ units: existingAuthority.initialUnits,
1034
+ availableUnits: advanceUnitsAvailable(existingAuthority, execution.reserveDebits),
1035
+ };
1036
+ }
1037
+ const extended = extendAdvanceAuthority({
1038
+ authority: existingAuthority,
1039
+ extensionId: `ext-${messageId}`,
1040
+ sessionId,
1041
+ message,
1042
+ messageClaims: data.messageClaims,
1043
+ });
1044
+ if (!extended.ok) {
1045
+ return { ...base, ok: false, errorCode: extended.code, message: extended.message };
1046
+ }
1047
+ const updated = {
1048
+ ...existingAuthority,
1049
+ extensions: [...existingAuthority.extensions, extended.value],
1050
+ };
1051
+ const storedExtension = putAuthorityInStore(data, { sessionId, runId, authority: updated });
1052
+ if (!storedExtension.ok) {
1053
+ return {
1054
+ ...base,
1055
+ ok: false,
1056
+ errorCode: storedExtension.errorCode,
1057
+ message: storedExtension.message,
1058
+ };
1059
+ }
1060
+ return {
1061
+ ...base,
1062
+ ok: true,
1063
+ action: "authorize",
1064
+ authorityId,
1065
+ extensions: updated.extensions.length,
1066
+ availableUnits: advanceUnitsAvailable(updated, execution.reserveDebits),
1067
+ };
1068
+ }
1069
+ const granted = grantAdvanceAuthority({
1070
+ authorityId,
1071
+ runId,
1072
+ sessionId,
1073
+ message,
1074
+ scope: {
1075
+ stages,
1076
+ decisionScope: coerceNonEmptyString(args.decisionScope) ?? "",
1077
+ fileBoundary: stringList(args.fileBoundary),
1078
+ reservedStops: stringList(args.reservedStops).filter((stage) => stage === "specification" ||
1079
+ stage === "planning" ||
1080
+ stage === "implementation" ||
1081
+ stage === "verification"),
1082
+ },
1083
+ existingAuthorities: execution.authority,
1084
+ messageClaims: data.messageClaims,
1085
+ });
1086
+ if (!granted.ok) {
1087
+ return { ...base, ok: false, errorCode: granted.code, message: granted.message };
1088
+ }
1089
+ const stored = addAuthorityInStore(data, {
1090
+ sessionId,
1091
+ runId,
1092
+ authority: granted.value.record,
1093
+ claim: granted.value.claim,
1094
+ });
1095
+ if (!stored.ok) {
1096
+ return { ...base, ok: false, errorCode: stored.errorCode, message: stored.message };
1097
+ }
1098
+ return {
1099
+ ...base,
1100
+ ok: true,
1101
+ action: "authorize",
1102
+ authorityId,
1103
+ units: granted.value.record.initialUnits,
1104
+ availableUnits: advanceUnitsAvailable(granted.value.record, []),
1105
+ };
1106
+ }
1107
+ case "record_approval": {
1108
+ const authorityId = coerceNonEmptyString(args.authorityId);
1109
+ const approvalId = coerceNonEmptyString(args.approvalId);
1110
+ const stage = coerceNonEmptyString(args.stage);
1111
+ const artifactPath = coerceNonEmptyString(args.artifactPath);
1112
+ const artifactSha256 = coerceNonEmptyString(args.artifactSha256);
1113
+ const authority = execution.authority.find((entry) => entry.authorityId === authorityId);
1114
+ if (!authority || !approvalId || !stage || !artifactPath || !artifactSha256) {
1115
+ return {
1116
+ ...base,
1117
+ ok: false,
1118
+ errorCode: "INVALID_INPUT",
1119
+ message: "record_approval requires a recorded authorityId, approvalId, stage, artifactPath, and artifactSha256",
1120
+ };
1121
+ }
1122
+ const approval = proposeStageApproval({
1123
+ authority,
1124
+ approvalId,
1125
+ stage,
1126
+ artifactPath,
1127
+ artifactSha256,
1128
+ provenance: "controller_delegated",
1129
+ });
1130
+ if (!approval.ok) {
1131
+ return { ...base, ok: false, errorCode: approval.code, message: approval.message };
1132
+ }
1133
+ const stored = addStageApprovalInStore(data, {
1134
+ sessionId,
1135
+ runId,
1136
+ approval: approval.value,
1137
+ });
1138
+ if (!stored.ok) {
1139
+ return { ...base, ok: false, errorCode: stored.errorCode, message: stored.message };
1140
+ }
1141
+ return {
1142
+ ...base,
1143
+ ok: true,
1144
+ action: "record_approval",
1145
+ approvalId,
1146
+ stage,
1147
+ provenance: approval.value.provenance,
1148
+ };
1149
+ }
1150
+ case "revoke_authority": {
1151
+ const authorityId = coerceNonEmptyString(args.authorityId);
1152
+ const revocationId = coerceNonEmptyString(args.revocationId);
1153
+ const authority = execution.authority.find((entry) => entry.authorityId === authorityId);
1154
+ if (!authority || !revocationId) {
1155
+ return {
1156
+ ...base,
1157
+ ok: false,
1158
+ errorCode: "INVALID_INPUT",
1159
+ message: "revoke_authority requires a recorded authorityId and a revocationId",
1160
+ };
1161
+ }
1162
+ const stagesArg = Array.isArray(args.stages) ? args.stages : undefined;
1163
+ const revoked = stagesArg === undefined || stagesArg.length === 0
1164
+ ? revokeAdvanceAuthority({
1165
+ authority,
1166
+ revocationId,
1167
+ reason: coerceNonEmptyString(args.rationale) ?? "revoked",
1168
+ })
1169
+ : narrowAdvanceAuthority({
1170
+ authority,
1171
+ revocationId,
1172
+ reason: coerceNonEmptyString(args.rationale) ?? "narrowed",
1173
+ narrowedStages: stagesArg.filter((stage) => stage === "specification" ||
1174
+ stage === "planning" ||
1175
+ stage === "implementation" ||
1176
+ stage === "verification"),
1177
+ });
1178
+ if (!revoked.ok) {
1179
+ return { ...base, ok: false, errorCode: revoked.code, message: revoked.message };
1180
+ }
1181
+ const updated = {
1182
+ ...authority,
1183
+ revocations: [...authority.revocations, revoked.value],
1184
+ };
1185
+ const stored = putAuthorityInStore(data, { sessionId, runId, authority: updated });
1186
+ if (!stored.ok) {
1187
+ return { ...base, ok: false, errorCode: stored.errorCode, message: stored.message };
1188
+ }
1189
+ return {
1190
+ ...base,
1191
+ ok: true,
1192
+ action: "revoke_authority",
1193
+ authorityId,
1194
+ kind: revoked.value.kind,
1195
+ availableUnits: advanceUnitsAvailable(updated, data.executions.get(runId)?.reserveDebits ?? []),
1196
+ stages: effectiveAuthorityStages(updated),
1197
+ };
1198
+ }
1199
+ default:
1200
+ return {
1201
+ ...base,
1202
+ ok: false,
1203
+ errorCode: "INVALID_INPUT",
1204
+ message: `unsupported generic checkpoint action ${String(args.action)}`,
1205
+ };
1206
+ }
1207
+ }
471
1208
  // START_CONTRACT: createWorkCheckpointTool
472
1209
  // PURPOSE: Build work_checkpoint handler wrapping plan registration, checkpoint start, fingerprint-verified outcomes, and bounded checkpoint recovery.
473
1210
  // INPUTS: { store: WorkItemStore - workflow in-memory store, options?: DelegatedControlOptions - optional read-only authorization lookup }
@@ -482,7 +1219,7 @@ export function createWorkCheckpointTool(store, options) {
482
1219
  async execute(args, context, overrideStore) {
483
1220
  const s = overrideStore ?? store;
484
1221
  const action = args.action;
485
- if (action === "register") {
1222
+ if (action === "register" && coerceNonEmptyString(args.planPath)) {
486
1223
  const planPath = coerceNonEmptyString(args.planPath);
487
1224
  const workspaceRoot = coerceNonEmptyString(context.workspaceRoot);
488
1225
  if (!planPath || !workspaceRoot) {
@@ -538,6 +1275,23 @@ export function createWorkCheckpointTool(store, options) {
538
1275
  checkpoints: registered.run.checkpoints.size,
539
1276
  };
540
1277
  }
1278
+ const genericRunId = coerceNonEmptyString(args.runId);
1279
+ if (genericRunId) {
1280
+ const data = s.getStoreData();
1281
+ const execution = findExecution(data, genericRunId);
1282
+ // Authority actions apply to any execution, including native runs;
1283
+ // other generic actions stay on non-native executions.
1284
+ const authorityAction = action === "authorize" || action === "record_approval" || action === "revoke_authority";
1285
+ if (execution && (execution.source.kind !== "native-package" || authorityAction)) {
1286
+ return executeGenericCheckpoint({
1287
+ data,
1288
+ sessionId: context.sessionId,
1289
+ runId: genericRunId,
1290
+ args,
1291
+ ...(options ? { control: options } : {}),
1292
+ });
1293
+ }
1294
+ }
541
1295
  const runId = coerceNonEmptyString(args.runId);
542
1296
  const checkpointId = coerceNonEmptyString(args.checkpointId);
543
1297
  if (!runId || !checkpointId) {
@@ -620,6 +1374,31 @@ export function createWorkCheckpointTool(store, options) {
620
1374
  message: "INVALID_INPUT: recover requires a stable recoveryId, diagnosis, changedCondition, and verification references",
621
1375
  };
622
1376
  }
1377
+ const authorityId = coerceNonEmptyString(args.authorityId);
1378
+ let advanceGrantApproved = false;
1379
+ if (authorityId) {
1380
+ const execution = findExecution(s.getStoreData(), runId);
1381
+ const authority = execution?.authority.find((entry) => entry.authorityId === authorityId);
1382
+ if (!execution || !authority) {
1383
+ return {
1384
+ tool: "work_checkpoint",
1385
+ sessionId: context.sessionId,
1386
+ ok: false,
1387
+ errorCode: "AUTHORITY_NOT_FOUND",
1388
+ message: `AUTHORITY_NOT_FOUND: no recorded authority ${authorityId} for ${runId}`,
1389
+ };
1390
+ }
1391
+ const proposed = proposeReserveDebit({
1392
+ authority,
1393
+ debits: execution.reserveDebits,
1394
+ recoveryId,
1395
+ targetKind: "checkpoint",
1396
+ targetId: checkpointId,
1397
+ });
1398
+ // A failed proposal only means no advance unit is available; the
1399
+ // recovery may still be a cost-free resume or an ordinary grant.
1400
+ advanceGrantApproved = proposed.ok;
1401
+ }
623
1402
  const recovered = await recoverDelegatedCheckpoint(s, {
624
1403
  sessionId: context.sessionId,
625
1404
  runId,
@@ -628,6 +1407,7 @@ export function createWorkCheckpointTool(store, options) {
628
1407
  changedCondition,
629
1408
  verification,
630
1409
  recoveryId,
1410
+ ...(advanceGrantApproved ? { advanceGrantApproved: true } : {}),
631
1411
  ...(userMessageId !== undefined
632
1412
  ? { userMessageId, lookupUserMessage: options?.lookupUserMessage }
633
1413
  : {}),
@@ -641,6 +1421,42 @@ export function createWorkCheckpointTool(store, options) {
641
1421
  message: recovered.message,
642
1422
  };
643
1423
  }
1424
+ if (recovered.kind === "advance_grant" && authorityId) {
1425
+ const execution = findExecution(s.getStoreData(), runId);
1426
+ const authority = execution?.authority.find((entry) => entry.authorityId === authorityId);
1427
+ const proposed = authority
1428
+ ? proposeReserveDebit({
1429
+ authority,
1430
+ debits: execution?.reserveDebits ?? [],
1431
+ recoveryId,
1432
+ targetKind: "checkpoint",
1433
+ targetId: checkpointId,
1434
+ })
1435
+ : undefined;
1436
+ if (!proposed?.ok) {
1437
+ return {
1438
+ tool: "work_checkpoint",
1439
+ sessionId: context.sessionId,
1440
+ ok: false,
1441
+ errorCode: proposed?.code ?? "RESERVE_EXHAUSTED",
1442
+ message: proposed?.message ?? "advance checkpoint recovery debit could not be recorded",
1443
+ };
1444
+ }
1445
+ const stored = addReserveDebitInStore(s.getStoreData(), {
1446
+ sessionId: context.sessionId,
1447
+ runId,
1448
+ debit: proposed.value,
1449
+ });
1450
+ if (!stored.ok) {
1451
+ return {
1452
+ tool: "work_checkpoint",
1453
+ sessionId: context.sessionId,
1454
+ ok: false,
1455
+ errorCode: stored.errorCode,
1456
+ message: stored.message,
1457
+ };
1458
+ }
1459
+ }
644
1460
  return {
645
1461
  tool: "work_checkpoint",
646
1462
  sessionId: context.sessionId,