@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
@@ -19,7 +19,7 @@
19
19
  import { tool } from "@opencode-ai/plugin";
20
20
  import { attemptTrackedResultRepair, detectExplicitHardStopStatus, isTrackedResultRepairEligible, unwrapResumableTaskResult, } from "./repair.js";
21
21
  import { parseResultBlock, parseWorkItemHeader, TRACKED_SUBAGENT_NAMES, } from "./protocol.js";
22
- import { applyTrackedResult, createRecordLookupKey, createWorkflowResultExcerpt, beginTrackedLaunch, createWorkItemStore, getReviewRound, getWorkItem, revertReviewerLaunch, } from "./state.js";
22
+ import { applyTrackedResult, createRecordLookupKey, createWorkflowResultExcerpt, beginTrackedLaunch, createWorkItemStore, createWorkItemStoreView, getReviewRound, getWorkItem, revertReviewerLaunch, } from "./state.js";
23
23
  import { applyDelegatedLaunchFailure, applyDelegatedReportRejection, applyDelegatedResult, beginDelegatedLaunch, revertInFlightDelegatedLaunches, summarizeDelegatedProgress, } from "./delegated.js";
24
24
  import { checkpointBarrierUnsatisfied, findOverlappingInFlightReview, recordCheckpointReviewerLaunch, recordCheckpointReviewerResult, } from "./checkpoints.js";
25
25
  import { getAllowedNextAgents, getAttemptedImplementationRound, getReviewerRoleForAgent, shouldBlockRound, } from "./transitions.js";
@@ -30,6 +30,8 @@ import { resolveOrchestrationPolicy, } from "../../lib/orchestration.js";
30
30
  import { isVvocPluginEnabled } from "../../lib/plugin-toggle-config.js";
31
31
  import { deleteWorkflowSessionDir, hydrateWorkflowStateChecked, snapshotWorkflowStateChecked, } from "./persistence.js";
32
32
  import { loadApprovedDelegatedPlan } from "./checkpoint-io.js";
33
+ import { isTaskLaunchableInStore } from "./execution.js";
34
+ import { runWorkflowTransaction, WorkflowTransactionQueue } from "./transactions.js";
33
35
  const z = tool.schema;
34
36
  const TRACKED_SUBAGENT_SET = new Set(TRACKED_SUBAGENT_NAMES);
35
37
  const WORK_ITEM_MISSING_MARKER = "__VVOC" + "_SECRET_BEARER_TOKEN_a6f582092f05__";
@@ -76,19 +78,26 @@ ordinary attempts without acceptance, diagnose it yourself and call work_item_de
76
78
  "recover", the terminal attempt number, a diagnosis, the changed condition or approach, the
77
79
  required verification references, and a stable recoveryId. A recovery resumes the same work item
78
80
  and its ordinary budget; when a further attempt is necessary it grants exactly one. The single
79
- autonomous grant per target is consumed on first use, and any further unit requires a fresh
80
- root-user message referenced by userMessageId — the runtime verifies that message's role, session
81
- identity, and timing, never its wording. Recovery never accepts a result, replaces a reviewer, or
81
+ autonomous grant per target is consumed on first use. After that, a further unit requires either a
82
+ fresh root-user message referenced by userMessageId — validated for role, session identity, and
83
+ timing, never wording — or a recorded advance authority referenced by runId and authorityId whose
84
+ finite shared reserve still has a unit. Recovery never accepts a result, replaces a reviewer, or
82
85
  changes declared scope. A rejected report (a worker execution that finished with a protocol-invalid
83
86
  result) is recorded with bounded diagnostics and has the same recovery path; it never becomes DONE.
84
87
 
85
- Register the supported approved native plan package once with work_checkpoint register (the
86
- spec/plan pair under .vvoc/specs/); foreign lifecycle plans are not convertible into it. Start
87
- each declared review checkpoint only after its prerequisite tasks are accepted, and run the due
88
- checkpoint before dependent waves. A checkpoint passes only when every declared reviewer passes
89
- against the pinned snapshot; a closed review-only FAIL report is a findings result, not approval.
90
- A checkpoint generation that stopped or exhausted its two ordinary generations recovers through
91
- work_checkpoint action "recover" with the same bounded fields. Do not write source files yourself:
88
+ Requirements may come from the supported approved native spec/plan package under .vvoc/specs/, a
89
+ provided plan reference, or the current conversation. Register a native package once with
90
+ work_checkpoint register (planPath); start and verify declared checkpoints only after their
91
+ prerequisite tasks are accepted. A provided-plan or conversation-scoped run is registered once with
92
+ work_item_open carrying an execution descriptor (executionKey, source, goal, boundary) and its
93
+ first task batch; later tasks append with the same runId plus amendmentId and rationale, and generic
94
+ review obligations run through work_checkpoint start/review/complete. A selected source stays
95
+ authoritative: a failed native package is never silently reopened as weaker generic execution, and
96
+ a provided document is never converted or executed as commands. A checkpoint passes only when
97
+ every declared reviewer passes against the pinned snapshot; a closed review-only FAIL report is a
98
+ findings result, not approval. A checkpoint generation that stopped or exhausted its ordinary
99
+ generations recovers through work_checkpoint action "recover" with the same bounded fields and the
100
+ same authority options. Do not write source files yourself:
92
101
  delegate implementation edits, including fixes requested by reviewers, through bounded task
93
102
  packets, while keeping planning artifacts, acceptance decisions, and verification commands in this
94
103
  session.
@@ -264,7 +273,6 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
264
273
  return {};
265
274
  const resolvedPolicy = resolveOrchestrationPolicy(vvoc.config);
266
275
  const workflowSystemInstruction = getWorkflowSystemInstruction(resolvedPolicy);
267
- const delegatedProfileActive = resolvedPolicy.profile === "delegated";
268
276
  const trustedWorkspaceRoot = worktree || directory;
269
277
  // START_BLOCK_PERSISTENCE_SETUP
270
278
  // Each session (main or subagent) gets its own isolated store.
@@ -682,6 +690,91 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
682
690
  : undefined,
683
691
  };
684
692
  };
693
+ // Advance-authority provenance uses the same pinned SDK message response.
694
+ // Only verified identity/timing/eligibility metadata leaves this lookup; raw
695
+ // user text is never persisted or logged.
696
+ const lookupAuthorityMessage = async (input) => {
697
+ const response = await client.session.message({
698
+ path: { id: input.sessionId, messageID: input.messageId },
699
+ query: { directory },
700
+ });
701
+ if (response.error || !response.data) {
702
+ const errorName = response.error?.name;
703
+ if (errorName && errorName !== "NotFound") {
704
+ throw new Error(`authority message lookup failed: ${errorName}`);
705
+ }
706
+ return undefined;
707
+ }
708
+ const info = response.data.info;
709
+ const parts = Array.isArray(response.data.parts) ? response.data.parts : [];
710
+ const textParts = parts
711
+ .map((part) => part)
712
+ .filter((part) => part.type === "text" && typeof part.text === "string")
713
+ .map((part) => part.text);
714
+ return {
715
+ messageId: info.id ?? input.messageId,
716
+ sessionId: info.sessionID ?? input.sessionId,
717
+ role: info.role === "user" ? "user" : "assistant",
718
+ createdMs: typeof info.time?.created === "number" && Number.isFinite(info.time.created)
719
+ ? info.time.created
720
+ : 0,
721
+ ignored: info.ignored === true,
722
+ syntheticOnly: false,
723
+ textParts,
724
+ };
725
+ };
726
+ // Per-session serialization for generic mutating tool calls. The staged
727
+ // snapshot persists before the committed state is published, so a failed
728
+ // write never exposes new obligations, authority, or launch permissions.
729
+ const workflowTransactions = new WorkflowTransactionQueue();
730
+ async function commitGenericToolResult(sessionId, run) {
731
+ const liveStore = stores.get(sessionId);
732
+ if (!liveStore) {
733
+ return {
734
+ ok: false,
735
+ errorCode: "SESSION_MISMATCH",
736
+ message: `no live workflow store for session ${sessionId}`,
737
+ };
738
+ }
739
+ if (invalidHydrationSessions.has(sessionId)) {
740
+ return {
741
+ ok: false,
742
+ errorCode: "INVALID_STATE",
743
+ message: `persisted workflow state for session ${sessionId} is invalid`,
744
+ };
745
+ }
746
+ const outcome = await runWorkflowTransaction({
747
+ queue: workflowTransactions,
748
+ sessionId,
749
+ getData: () => liveStore.getStoreData(),
750
+ operation: async (staged) => {
751
+ const result = await run(createWorkItemStoreView(staged));
752
+ if (result.ok !== true) {
753
+ // Validation failed: persist nothing and publish nothing.
754
+ return { result, skipPersist: true };
755
+ }
756
+ return { result };
757
+ },
758
+ });
759
+ if (!outcome.ok) {
760
+ void client.app
761
+ .log({
762
+ body: {
763
+ service: "workflow",
764
+ level: "error",
765
+ message: "[workflow][generic][BLOCK_GENERIC_COMMIT] persistence failed",
766
+ extra: { sessionID: sessionId, error: outcome.error.slice(0, 300) },
767
+ },
768
+ })
769
+ .catch(() => undefined);
770
+ return {
771
+ ok: false,
772
+ errorCode: "PERSISTENCE_FAILED",
773
+ message: `generic workflow mutation could not be persisted: ${outcome.error}`,
774
+ };
775
+ }
776
+ return outcome.result;
777
+ }
685
778
  /**
686
779
  * Execute one recovery mutation durably on the live store: the domain
687
780
  * reducer applies to the live entry (re-prechecking after any authorization
@@ -775,6 +868,7 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
775
868
  });
776
869
  const workCheckpointTool = createWorkCheckpointTool(dummyStore, {
777
870
  lookupUserMessage: lookupRecoveryUserMessage,
871
+ lookupAuthorityMessage,
778
872
  });
779
873
  return {
780
874
  tool: {
@@ -789,10 +883,42 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
789
883
  writeScope: z.array(z.string()).optional(),
790
884
  planRunId: z.string().optional(),
791
885
  planTaskId: z.string().optional(),
886
+ taskId: z.string().optional(),
887
+ goal: z.string().optional(),
888
+ acceptanceCriteria: z.array(z.string()).optional(),
889
+ verification: z.array(z.string()).optional(),
890
+ dependsOn: z.array(z.string()).optional(),
891
+ blockedBy: z.array(z.string()).optional(),
792
892
  })),
893
+ execution: z
894
+ .object({
895
+ executionKey: z.string(),
896
+ source: z.record(z.string(), z.unknown()),
897
+ goal: z.string(),
898
+ boundary: z.object({
899
+ files: z.array(z.string()),
900
+ directories: z.array(z.string()),
901
+ }),
902
+ checkpoints: z.array(z.record(z.string(), z.unknown())).optional(),
903
+ })
904
+ .optional(),
905
+ runId: z.string().optional(),
906
+ amendmentId: z.string().optional(),
907
+ rationale: z.string().optional(),
793
908
  },
794
909
  async execute(args, context) {
795
910
  assertWorkflowToolAccess(context.agent, "work_item_open");
911
+ const isGeneric = args.execution !== undefined || args.runId !== undefined;
912
+ if (isGeneric) {
913
+ // Hydrate/validate the session store before the transaction boundary
914
+ // reads it, matching every other tool entry point.
915
+ getOrCreateStore(context.sessionID);
916
+ const result = await commitGenericToolResult(context.sessionID, (view) => workItemOpenTool.execute(args, {
917
+ sessionId: context.sessionID,
918
+ workspaceRoot: trustedWorkspaceRoot,
919
+ }, view));
920
+ return stringifyToolOutput(result);
921
+ }
796
922
  const sessionStore = getOrCreateStore(context.sessionID);
797
923
  const opened = workItemOpenTool.execute(args, { sessionId: context.sessionID }, sessionStore);
798
924
  snapshotSession(context.sessionID);
@@ -823,101 +949,153 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
823
949
  return stringifyToolOutput(closed);
824
950
  },
825
951
  }),
826
- // The delegated control tools are registered only under the delegated
827
- // startup profile; other profiles keep their current tool-schema footprint.
828
- ...(delegatedProfileActive
829
- ? {
830
- work_item_decide: tool({
831
- description: workItemDecideTool.description,
832
- args: {
833
- workItemId: z.string(),
834
- attempt: z.number().int().min(1),
835
- decision: z.enum(["accept", "request_changes", "rework", "recover"]),
836
- rationale: z.string().optional(),
837
- evidence: z.array(z.string()).optional(),
838
- concernsDisposition: z.string().optional(),
839
- runId: z.string().optional(),
840
- checkpointId: z.string().optional(),
841
- diagnosis: z.string().optional(),
842
- changedCondition: z.string().optional(),
843
- verification: z.array(z.string()).optional(),
844
- recoveryId: z.string().optional(),
845
- userMessageId: z.string().optional(),
846
- },
847
- async execute(args, context) {
848
- // Resolve the store first so invalid persisted state is detected
849
- // before the authorization check reports it as a control denial.
850
- const sessionStore = getOrCreateStore(context.sessionID);
851
- await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_item_decide");
852
- // Recovery is the only decision family that must persist
853
- // before it exposes new launch permissions, so it commits
854
- // durably on the live store and rolls back on write failure.
855
- if (args.decision === "recover") {
856
- const workItemId = String(args.workItemId ?? "");
857
- const recovered = await executeCommittedRecovery(context.sessionID, (liveStore) => workItemDecideTool.execute(args, { sessionId: context.sessionID }, liveStore), () => captureRecordRestore(context.sessionID, workItemId), (result) => result.ok === true);
858
- return stringifyToolOutput(recovered);
859
- }
860
- const decided = await workItemDecideTool.execute(args, { sessionId: context.sessionID }, sessionStore);
861
- if (decided.ok) {
862
- const persisted = snapshotSession(context.sessionID);
863
- if (!persisted.ok) {
864
- throw new Error(`PERSISTENCE_FAILED: decision applied in memory but could not be persisted: ${persisted.error}`);
865
- }
866
- }
867
- return stringifyToolOutput(decided);
868
- },
869
- }),
870
- work_checkpoint: tool({
871
- description: workCheckpointTool.description,
872
- args: {
873
- action: z.enum(["register", "start", "verify", "recover"]),
874
- planPath: z.string().optional(),
875
- runId: z.string().optional(),
876
- checkpointId: z.string().optional(),
877
- complete: z.boolean().optional(),
878
- diagnosis: z.string().optional(),
879
- changedCondition: z.string().optional(),
880
- verification: z.array(z.string()).optional(),
881
- recoveryId: z.string().optional(),
882
- userMessageId: z.string().optional(),
883
- },
884
- async execute(args, context) {
885
- // Resolve the store first so invalid persisted state is detected
886
- // before the authorization check reports it as a control denial.
887
- const sessionStore = getOrCreateStore(context.sessionID);
888
- await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_checkpoint");
889
- const toolContext = {
890
- sessionId: context.sessionID,
891
- workspaceRoot: trustedWorkspaceRoot,
892
- loadPlan: async (planPath, workspaceRoot) => {
893
- const loaded = await loadApprovedDelegatedPlan({ workspaceRoot, planPath });
894
- return loaded.ok
895
- ? loaded.plan
896
- : { loadError: `${loaded.code}: ${loaded.message}` };
897
- },
898
- };
899
- // Checkpoint recovery commits durably on the live store and
900
- // rolls the checkpoint entry back on write failure, so a
901
- // granted generation is never exposed before its state is
902
- // durably recorded.
903
- if (args.action === "recover") {
904
- const runId = String(args.runId ?? "");
905
- const checkpointId = String(args.checkpointId ?? "");
906
- const recovered = await executeCommittedRecovery(context.sessionID, (liveStore) => workCheckpointTool.execute(args, toolContext, liveStore), () => captureCheckpointRestore(context.sessionID, runId, checkpointId), (result) => result.ok === true);
907
- return stringifyToolOutput(recovered);
952
+ // Control tools are registered independent of the startup profile; they
953
+ // remain root-session, agent, workspace, and invalid-hydration gated at
954
+ // execution time.
955
+ ...{
956
+ work_item_decide: tool({
957
+ description: workItemDecideTool.description,
958
+ args: {
959
+ workItemId: z.string(),
960
+ attempt: z.number().int().min(1),
961
+ decision: z.enum(["accept", "request_changes", "rework", "recover"]),
962
+ rationale: z.string().optional(),
963
+ evidence: z.array(z.string()).optional(),
964
+ concernsDisposition: z.string().optional(),
965
+ runId: z.string().optional(),
966
+ checkpointId: z.string().optional(),
967
+ diagnosis: z.string().optional(),
968
+ changedCondition: z.string().optional(),
969
+ verification: z.array(z.string()).optional(),
970
+ recoveryId: z.string().optional(),
971
+ userMessageId: z.string().optional(),
972
+ authorityId: z.string().optional(),
973
+ },
974
+ async execute(args, context) {
975
+ // Resolve the store first so invalid persisted state is detected
976
+ // before the authorization check reports it as a control denial.
977
+ const sessionStore = getOrCreateStore(context.sessionID);
978
+ await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_item_decide");
979
+ // Recovery is the only decision family that must persist
980
+ // before it exposes new launch permissions. Authority-bearing
981
+ // recovery stages the work-item recovery and the reserve debit in
982
+ // one serialized transaction so they commit or fail together.
983
+ if (args.decision === "recover") {
984
+ if (args.authorityId !== undefined) {
985
+ const staged = await commitGenericToolResult(context.sessionID, (view) => workItemDecideTool.execute(args, { sessionId: context.sessionID }, view));
986
+ return stringifyToolOutput(staged);
908
987
  }
909
- const result = await workCheckpointTool.execute({ ...args }, toolContext, sessionStore);
910
- if (result.ok) {
911
- const persisted = snapshotSession(context.sessionID);
912
- if (!persisted.ok) {
913
- throw new Error(`PERSISTENCE_FAILED: checkpoint change applied in memory but could not be persisted: ${persisted.error}`);
914
- }
988
+ const workItemId = String(args.workItemId ?? "");
989
+ const recovered = await executeCommittedRecovery(context.sessionID, (liveStore) => workItemDecideTool.execute(args, { sessionId: context.sessionID }, liveStore), () => captureRecordRestore(context.sessionID, workItemId), (result) => result.ok === true);
990
+ return stringifyToolOutput(recovered);
991
+ }
992
+ const decided = await workItemDecideTool.execute(args, { sessionId: context.sessionID }, sessionStore);
993
+ if (decided.ok) {
994
+ const persisted = snapshotSession(context.sessionID);
995
+ if (!persisted.ok) {
996
+ throw new Error(`PERSISTENCE_FAILED: decision applied in memory but could not be persisted: ${persisted.error}`);
915
997
  }
998
+ }
999
+ return stringifyToolOutput(decided);
1000
+ },
1001
+ }),
1002
+ work_checkpoint: tool({
1003
+ description: workCheckpointTool.description,
1004
+ args: {
1005
+ action: z.enum([
1006
+ "register",
1007
+ "start",
1008
+ "verify",
1009
+ "recover",
1010
+ "review",
1011
+ "bind",
1012
+ "complete",
1013
+ "amend",
1014
+ "authorize",
1015
+ "record_approval",
1016
+ "revoke_authority",
1017
+ ]),
1018
+ planPath: z.string().optional(),
1019
+ runId: z.string().optional(),
1020
+ checkpointId: z.string().optional(),
1021
+ complete: z.boolean().optional(),
1022
+ diagnosis: z.string().optional(),
1023
+ changedCondition: z.string().optional(),
1024
+ verification: z.array(z.string()).optional(),
1025
+ recoveryId: z.string().optional(),
1026
+ userMessageId: z.string().optional(),
1027
+ checkpoints: z.array(z.record(z.string(), z.unknown())).optional(),
1028
+ tasks: z.array(z.record(z.string(), z.unknown())).optional(),
1029
+ amendmentId: z.string().optional(),
1030
+ rationale: z.string().optional(),
1031
+ startFingerprint: z.string().optional(),
1032
+ reviewer: z.string().optional(),
1033
+ authorityId: z.string().optional(),
1034
+ messageId: z.string().optional(),
1035
+ approvalId: z.string().optional(),
1036
+ stage: z.string().optional(),
1037
+ stages: z.array(z.string()).optional(),
1038
+ decisionScope: z.string().optional(),
1039
+ fileBoundary: z.array(z.string()).optional(),
1040
+ reservedStops: z.array(z.string()).optional(),
1041
+ artifactPath: z.string().optional(),
1042
+ artifactSha256: z.string().optional(),
1043
+ revocationId: z.string().optional(),
1044
+ },
1045
+ async execute(args, context) {
1046
+ // Resolve the store first so invalid persisted state is detected
1047
+ // before the authorization check reports it as a control denial.
1048
+ const sessionStore = getOrCreateStore(context.sessionID);
1049
+ await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_checkpoint");
1050
+ const toolContext = {
1051
+ sessionId: context.sessionID,
1052
+ workspaceRoot: trustedWorkspaceRoot,
1053
+ loadPlan: async (planPath, workspaceRoot) => {
1054
+ const loaded = await loadApprovedDelegatedPlan({ workspaceRoot, planPath });
1055
+ return loaded.ok ? loaded.plan : { loadError: `${loaded.code}: ${loaded.message}` };
1056
+ },
1057
+ };
1058
+ // Generic (non-native) executions route through the atomic
1059
+ // transaction boundary: staged persist, then publish.
1060
+ const runIdArg = String(args.runId ?? "");
1061
+ const liveExecution = runIdArg
1062
+ ? sessionStore.getStoreData().executions.get(runIdArg)
1063
+ : undefined;
1064
+ const authorityAction = args.action === "authorize" ||
1065
+ args.action === "record_approval" ||
1066
+ args.action === "revoke_authority";
1067
+ const genericAction = args.action !== "register" &&
1068
+ liveExecution !== undefined &&
1069
+ (liveExecution.source.kind !== "native-package" || authorityAction);
1070
+ const nativeAuthorityRecover = args.action === "recover" &&
1071
+ args.authorityId !== undefined &&
1072
+ liveExecution !== undefined;
1073
+ const genericRegister = args.action === "register" && !args.planPath;
1074
+ if (genericAction || genericRegister || nativeAuthorityRecover) {
1075
+ const result = await commitGenericToolResult(context.sessionID, (view) => workCheckpointTool.execute(args, toolContext, view));
916
1076
  return stringifyToolOutput(result);
917
- },
918
- }),
919
- }
920
- : {}),
1077
+ }
1078
+ // Checkpoint recovery commits durably on the live store and
1079
+ // rolls the checkpoint entry back on write failure, so a
1080
+ // granted generation is never exposed before its state is
1081
+ // durably recorded.
1082
+ if (args.action === "recover") {
1083
+ const runId = String(args.runId ?? "");
1084
+ const checkpointId = String(args.checkpointId ?? "");
1085
+ const recovered = await executeCommittedRecovery(context.sessionID, (liveStore) => workCheckpointTool.execute(args, toolContext, liveStore), () => captureCheckpointRestore(context.sessionID, runId, checkpointId), (result) => result.ok === true);
1086
+ return stringifyToolOutput(recovered);
1087
+ }
1088
+ const result = await workCheckpointTool.execute({ ...args }, toolContext, sessionStore);
1089
+ if (result.ok) {
1090
+ const persisted = snapshotSession(context.sessionID);
1091
+ if (!persisted.ok) {
1092
+ throw new Error(`PERSISTENCE_FAILED: checkpoint change applied in memory but could not be persisted: ${persisted.error}`);
1093
+ }
1094
+ }
1095
+ return stringifyToolOutput(result);
1096
+ },
1097
+ }),
1098
+ },
921
1099
  },
922
1100
  "tool.execute.before": async (input, output) => {
923
1101
  if (input.tool !== "task") {
@@ -964,6 +1142,41 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
964
1142
  });
965
1143
  throw new Error(`${WORK_ITEM_MISSING_MARKER} LAUNCH_REJECTED_UNKNOWN_WORK_ITEM: no open work item ${header.value} exists in this session. Use work_item_open first or check state with work_item_list.`);
966
1144
  }
1145
+ // Generic executions enforce declared task dependencies and checkpoint
1146
+ // barriers at launch, not only at completion.
1147
+ if (subagentType === "vv-implementer") {
1148
+ const data = sessionStore.getStoreData();
1149
+ for (const execution of data.executions.values()) {
1150
+ if (execution.sessionId !== input.sessionID)
1151
+ continue;
1152
+ const boundTask = [...execution.tasks.values()].find((binding) => binding.workItemId === workItem.workItemId);
1153
+ if (!boundTask)
1154
+ continue;
1155
+ const launchable = isTaskLaunchableInStore(data, {
1156
+ sessionId: input.sessionID,
1157
+ runId: execution.runId,
1158
+ taskId: boundTask.taskId,
1159
+ });
1160
+ if (!launchable.ok) {
1161
+ await client.app.log({
1162
+ body: {
1163
+ service: "workflow",
1164
+ level: "warn",
1165
+ message: "[workflow][launchValidation][BLOCK_VALIDATE_LAUNCH] dependency gate",
1166
+ extra: {
1167
+ sessionID: input.sessionID,
1168
+ workItemId: workItem.workItemId,
1169
+ runId: execution.runId,
1170
+ taskId: boundTask.taskId,
1171
+ reason: launchable.reason,
1172
+ },
1173
+ },
1174
+ });
1175
+ throw new Error(`${INVALID_NEXT_AGENT_MARKER} LAUNCH_REJECTED_DEPENDENCY: task ${boundTask.taskId} cannot start yet: ${launchable.message}`);
1176
+ }
1177
+ break;
1178
+ }
1179
+ }
967
1180
  const allowedNextAgents = getAllowedNextAgents(workItem);
968
1181
  const reviewRound = getReviewRound(workItem);
969
1182
  const attemptedRound = subagentType === "vv-implementer" ? getAttemptedImplementationRound(workItem) : reviewRound;