@osovv/vv-opencode 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +41 -10
  3. package/dist/lib/agent-tool-catalog.d.ts +144 -0
  4. package/dist/lib/agent-tool-catalog.js +2551 -0
  5. package/dist/lib/agent-tool-catalog.js.map +1 -0
  6. package/dist/lib/agent-tool-contract.d.ts +200 -0
  7. package/dist/lib/agent-tool-contract.js +407 -0
  8. package/dist/lib/agent-tool-contract.js.map +1 -0
  9. package/dist/lib/workflow-contract.d.ts +12 -0
  10. package/dist/lib/workflow-contract.js +71 -18
  11. package/dist/lib/workflow-contract.js.map +1 -1
  12. package/dist/plugins/hashline-edit/index.js +102 -97
  13. package/dist/plugins/hashline-edit/index.js.map +1 -1
  14. package/dist/plugins/hashline-edit/normalize-edits.d.ts +2 -8
  15. package/dist/plugins/hashline-edit/normalize-edits.js +32 -17
  16. package/dist/plugins/hashline-edit/normalize-edits.js.map +1 -1
  17. package/dist/plugins/hashline-edit/schemas.d.ts +246 -0
  18. package/dist/plugins/hashline-edit/schemas.js +659 -0
  19. package/dist/plugins/hashline-edit/schemas.js.map +1 -0
  20. package/dist/plugins/hashline-edit/str-replace-editor.d.ts +6 -17
  21. package/dist/plugins/hashline-edit/str-replace-editor.js +19 -21
  22. package/dist/plugins/hashline-edit/str-replace-editor.js.map +1 -1
  23. package/dist/plugins/hashline-edit/tool-description.d.ts +1 -0
  24. package/dist/plugins/hashline-edit/tool-description.js +20 -4
  25. package/dist/plugins/hashline-edit/tool-description.js.map +1 -1
  26. package/dist/plugins/system-context-injection/index.js +9 -4
  27. package/dist/plugins/system-context-injection/index.js.map +1 -1
  28. package/dist/plugins/web-tools/fetch-service.d.ts +8 -5
  29. package/dist/plugins/web-tools/fetch-service.js +30 -45
  30. package/dist/plugins/web-tools/fetch-service.js.map +1 -1
  31. package/dist/plugins/web-tools/index.js +23 -5
  32. package/dist/plugins/web-tools/index.js.map +1 -1
  33. package/dist/plugins/web-tools/schemas.d.ts +469 -0
  34. package/dist/plugins/web-tools/schemas.js +523 -0
  35. package/dist/plugins/web-tools/schemas.js.map +1 -0
  36. package/dist/plugins/web-tools/search-service.d.ts +6 -3
  37. package/dist/plugins/web-tools/search-service.js +24 -29
  38. package/dist/plugins/web-tools/search-service.js.map +1 -1
  39. package/dist/plugins/workflow/authority.d.ts +6 -0
  40. package/dist/plugins/workflow/authority.js +45 -10
  41. package/dist/plugins/workflow/authority.js.map +1 -1
  42. package/dist/plugins/workflow/authorization.js +16 -8
  43. package/dist/plugins/workflow/authorization.js.map +1 -1
  44. package/dist/plugins/workflow/checkpoints.d.ts +41 -2
  45. package/dist/plugins/workflow/checkpoints.js +176 -47
  46. package/dist/plugins/workflow/checkpoints.js.map +1 -1
  47. package/dist/plugins/workflow/delegated.d.ts +15 -1
  48. package/dist/plugins/workflow/delegated.js +65 -35
  49. package/dist/plugins/workflow/delegated.js.map +1 -1
  50. package/dist/plugins/workflow/execution.d.ts +15 -30
  51. package/dist/plugins/workflow/execution.js +133 -33
  52. package/dist/plugins/workflow/execution.js.map +1 -1
  53. package/dist/plugins/workflow/index.js +177 -112
  54. package/dist/plugins/workflow/index.js.map +1 -1
  55. package/dist/plugins/workflow/input-validation.d.ts +48 -0
  56. package/dist/plugins/workflow/input-validation.js +536 -0
  57. package/dist/plugins/workflow/input-validation.js.map +1 -0
  58. package/dist/plugins/workflow/inspection.d.ts +60 -0
  59. package/dist/plugins/workflow/inspection.js +316 -0
  60. package/dist/plugins/workflow/inspection.js.map +1 -0
  61. package/dist/plugins/workflow/protocol.d.ts +2 -0
  62. package/dist/plugins/workflow/protocol.js +26 -4
  63. package/dist/plugins/workflow/protocol.js.map +1 -1
  64. package/dist/plugins/workflow/recovery.d.ts +2 -1
  65. package/dist/plugins/workflow/recovery.js +79 -12
  66. package/dist/plugins/workflow/recovery.js.map +1 -1
  67. package/dist/plugins/workflow/repair.d.ts +1 -1
  68. package/dist/plugins/workflow/repair.js +9 -10
  69. package/dist/plugins/workflow/repair.js.map +1 -1
  70. package/dist/plugins/workflow/results.d.ts +2566 -0
  71. package/dist/plugins/workflow/results.js +1111 -0
  72. package/dist/plugins/workflow/results.js.map +1 -0
  73. package/dist/plugins/workflow/schemas.d.ts +214 -24
  74. package/dist/plugins/workflow/schemas.js +350 -89
  75. package/dist/plugins/workflow/schemas.js.map +1 -1
  76. package/dist/plugins/workflow/state.d.ts +3 -2
  77. package/dist/plugins/workflow/state.js +15 -8
  78. package/dist/plugins/workflow/state.js.map +1 -1
  79. package/dist/plugins/workflow/system-instruction.md +17 -1
  80. package/dist/plugins/workflow/tooling.d.ts +2 -18
  81. package/dist/plugins/workflow/tooling.js +477 -460
  82. package/dist/plugins/workflow/tooling.js.map +1 -1
  83. package/dist/plugins/workflow/transactions.d.ts +13 -0
  84. package/dist/plugins/workflow/transactions.js +13 -3
  85. package/dist/plugins/workflow/transactions.js.map +1 -1
  86. package/package.json +6 -3
  87. package/schemas/vvoc/v3.json +1 -1
  88. package/templates/agents/vv-code-reviewer.md +13 -4
  89. package/templates/agents/vv-controller.md +17 -0
  90. package/templates/agents/vv-implementer.md +20 -9
  91. package/templates/agents/vv-spec-reviewer.md +13 -4
  92. package/templates/skills/vv-execute/SKILL.md +3 -3
  93. package/templates/skills/vv-execute/references/tool-contracts.md +1750 -0
  94. package/templates/skills/vv-review/SKILL.md +1 -1
@@ -1,30 +1,34 @@
1
1
  // FILE: src/plugins/workflow/index.ts
2
- // VERSION: 0.8.0
2
+ // VERSION: 0.9.1
3
3
  // START_MODULE_CONTRACT
4
- // PURPOSE: Register workflow tools and enforcement while injecting only startup-profile-compatible vv-controller guidance, including delegated control tools with bounded recovery, host-call-bound attempts, terminal report-rejection settlement, and checkpoint reviewer linkage.
5
- // SCOPE: work_item_open/list/close registration, delegated-only work_item_decide and work_checkpoint registration with root-session authorization and an SDK-backed read-only authorization-message lookup for user-authorized recovery, tracked launch validation with delegated barriers and overlapping-write gates, live host-call bindings that convert supported foreground vv-implementer task launches into failed delegated attempts on confirmed host-terminal errors, result normalization and bounded same-session continuation with explicit hard-stop suppression, callID-bound delegated attempt results, terminal settlement of protocol-invalid reports as report_rejected attempts through staged persistence, checkpoint reviewer bookkeeping, round aggregation with bounded excerpts, implementation round limits, checked persistence, and profile-selected chat.message guidance. Tool argument schemas come from schemas.ts; the authorization guard and message lookups from authorization.ts; staged transactions and committed recovery from recovery.ts.
6
- // DEPENDS: [@opencode-ai/plugin, src/lib/config-layers.ts, src/lib/orchestration.ts, src/lib/plugin-toggle-config.ts, src/plugins/workflow/authorization.ts, src/plugins/workflow/checkpoint-io.ts, src/plugins/workflow/checkpoints.ts, src/plugins/workflow/delegated.ts, src/plugins/workflow/persistence.ts, src/plugins/workflow/protocol.ts, src/plugins/workflow/recovery.ts, src/plugins/workflow/repair.ts, src/plugins/workflow/schemas.ts, src/plugins/workflow/state.ts, src/plugins/workflow/tooling.ts, src/plugins/workflow/transitions.ts]
7
- // LINKS: M-PLUGIN-WORKFLOW, M-ORCHESTRATION-PROFILES, M-WORKFLOW-PROTOCOL, M-WORKFLOW-REPAIR, M-WORKFLOW-STATE, M-WORKFLOW-TRANSITIONS, M-WORKFLOW-TOOLING, M-WORKFLOW-PERSISTENCE, M-WORKFLOW-DELEGATED, M-WORKFLOW-CHECKPOINTS, V-M-PLUGIN-WORKFLOW
4
+ // PURPOSE: Register workflow tools and enforcement while injecting only startup-profile-compatible vv-controller guidance, including profile-independent control tools with bounded recovery, host-call-bound attempts, terminal report-rejection settlement, checkpoint reviewer linkage, and strict owned-tool contract publication/validation.
5
+ // SCOPE: work_item_open/list/close registration, profile-independent work_item_decide and work_checkpoint registration with root-session authorization and an SDK-backed read-only authorization-message lookup for user-authorized recovery, owned tool.definition publication of strict input JSON Schemas plus early validateWorkflowToolInput guards in tool.execute.before (without touching task-launch hooks), pre-dispatch structural validation of mutation tools, tracked launch validation with delegated barriers, overlapping-write gates, and authoritative native sealed-run rejection, live host-call bindings that convert supported foreground vv-implementer task launches into failed delegated attempts on confirmed host-terminal errors, result normalization and bounded same-session continuation with explicit hard-stop suppression, callID-bound delegated attempt results, terminal settlement of protocol-invalid reports as report_rejected attempts through staged persistence, checkpoint reviewer bookkeeping, round aggregation with bounded excerpts, implementation round limits, checked persistence, and profile-selected chat.message guidance. Tool argument schemas come from schemas.ts; branch-aware validation from input-validation.ts; the authorization guard and message lookups from authorization.ts; staged transactions and committed recovery from recovery.ts.
6
+ // DEPENDS: [@opencode-ai/plugin, src/lib/agent-tool-contract.ts, src/lib/config-layers.ts, src/lib/orchestration.ts, src/lib/plugin-toggle-config.ts, src/plugins/workflow/authorization.ts, src/plugins/workflow/checkpoint-io.ts, src/plugins/workflow/checkpoints.ts, src/plugins/workflow/delegated.ts, src/plugins/workflow/input-validation.ts, src/plugins/workflow/persistence.ts, src/plugins/workflow/protocol.ts, src/plugins/workflow/recovery.ts, src/plugins/workflow/repair.ts, src/plugins/workflow/results.ts, src/plugins/workflow/schemas.ts, src/plugins/workflow/state.ts, src/plugins/workflow/tooling.ts, src/plugins/workflow/transitions.ts]
7
+ // LINKS: M-PLUGIN-WORKFLOW, M-ORCHESTRATION-PROFILES, M-WORKFLOW-PROTOCOL, M-WORKFLOW-REPAIR, M-WORKFLOW-STATE, M-WORKFLOW-TRANSITIONS, M-WORKFLOW-TOOLING, M-WORKFLOW-PERSISTENCE, M-WORKFLOW-DELEGATED, M-AGENT-TOOL-CONTRACT, V-M-PLUGIN-WORKFLOW
8
8
  // ROLE: RUNTIME
9
9
  // MAP_MODE: EXPORTS
10
10
  // END_MODULE_CONTRACT
11
11
  //
12
12
  // START_MODULE_MAP
13
- // WorkflowPlugin - Registers workflow work-item tools, delegated control tools under the delegated profile, tracked task protocol enforcement with callID-bound delegated attempts, bounded recovery with durable persist-and-rollback commits, terminal report-rejection settlement, checkpoint linkage, live host-call failure bindings, and primary-session workflow guidance injection.
13
+ // WorkflowPlugin - Registers workflow work-item tools, profile-independent control tools whose execution remains root-session gated, owned tool.definition/execute.before contract hooks, tracked task protocol enforcement with callID-bound delegated attempts, bounded recovery with durable persist-and-rollback commits, terminal report-rejection settlement, checkpoint linkage, live host-call failure bindings, and primary-session workflow guidance injection.
14
14
  // END_MODULE_MAP
15
15
  //
16
16
  // START_CHANGE_SUMMARY
17
- // LAST_CHANGE: [C-WORKFLOW-INDEX-REDUCE - Moved the five tool argument schemas into schemas.ts with z.infer types, and extracted the authorization guard, message lookups, staged transactions, and committed recovery into authorization.ts and recovery.ts over explicit context; registered shapes and behavior are unchanged.]
17
+ // LAST_CHANGE: [C-AGENT-TOOL-CONTRACTS T-007 - Appended one shared tracked result-protocol instruction, generated from protocol.ts status vocabularies, to the review-only/selective/delegated guidance so common launch/result rules and the on-demand reference path are available without the native execution skill; the profile-specific ownership text and the tracked system instruction are otherwise unchanged. Prior T-004: removed the redundant registration spread and made the native launch hook reject an authoritatively sealed plan run.]
18
18
  // END_CHANGE_SUMMARY
19
19
  import { tool } from "@opencode-ai/plugin";
20
20
  import { attemptTrackedResultRepair, detectExplicitHardStopStatus, isTrackedResultRepairEligible, unwrapResumableTaskResult, } from "./repair.js";
21
- import { parseResultBlock, parseWorkItemHeader, TRACKED_SUBAGENT_NAMES, } from "./protocol.js";
21
+ import { describeStatusVocabulary, parseResultBlock, parseWorkItemHeader, TRACKED_SUBAGENT_NAMES, } from "./protocol.js";
22
22
  import { applyTrackedResult, createRecordLookupKey, createWorkflowResultExcerpt, beginTrackedLaunch, createWorkItemStore, 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";
26
26
  import { createWorkItemCloseTool, createWorkItemDecideTool, createWorkItemListTool, createWorkItemOpenTool, createWorkCheckpointTool, } from "./tooling.js";
27
27
  import { workCheckpointArgs, workItemCloseArgs, workItemDecideArgs, workItemListArgs, workItemOpenArgs, } from "./schemas.js";
28
+ import { isWorkflowToolId, validateWorkflowToolInput, workflowToolContracts, } from "./input-validation.js";
29
+ import { ContractInputError, createToolDefinitionAdapter } from "../../lib/agent-tool-contract.js";
30
+ import { serializeWorkflowResult, workflowInputFailure } from "./results.js";
31
+ import { deriveDelegatedGuidance } from "./inspection.js";
28
32
  import { assertWorkflowToolAccess, createWorkflowAuthorization, shouldInjectForAgent, } from "./authorization.js";
29
33
  import { createRecoverySupport } from "./recovery.js";
30
34
  import workflowSystemInstructionTemplate from "./system-instruction.md?raw";
@@ -33,7 +37,7 @@ import { resolveOrchestrationPolicy, } from "../../lib/orchestration.js";
33
37
  import { isVvocPluginEnabled } from "../../lib/plugin-toggle-config.js";
34
38
  import { deleteWorkflowSessionDir, hydrateWorkflowStateChecked, snapshotWorkflowStateChecked, } from "./persistence.js";
35
39
  import { loadApprovedDelegatedPlan } from "./checkpoint-io.js";
36
- import { isTaskLaunchableInStore } from "./execution.js";
40
+ import { isTaskLaunchableInStore, latestAttemptView } from "./execution.js";
37
41
  const TRACKED_SUBAGENT_SET = new Set(TRACKED_SUBAGENT_NAMES);
38
42
  const WORK_ITEM_MISSING_MARKER = "__VVOC" + "_SECRET_BEARER_TOKEN_a6f582092f05__";
39
43
  const INVALID_NEXT_AGENT_MARKER = "__VVOC" + "_SECRET_BEARER_TOKEN_513fa2de603d__";
@@ -103,17 +107,29 @@ packets, while keeping planning artifacts, acceptance decisions, and verificatio
103
107
  session.
104
108
  </workflow_protocol>
105
109
  `.trim();
110
+ // Common result-protocol guidance appended to the non-tracked profile
111
+ // instructions. It states shared launch/result rules only: it does not select a
112
+ // source or lifecycle, and it adds no delegation or review obligation. Status
113
+ // vocabularies come from protocol.ts so the text cannot drift from the parser.
114
+ const TRACKED_RESULT_PROTOCOL_INSTRUCTION = `
115
+ <tracked_result_protocol>
116
+ A tracked subagent result begins on its first line with the protocol top block — no preface, prose, or code fence — followed by a blank line and the body. Use the exact VVOC_WORK_ITEM_ID returned by work_item_open for that assignment; never reuse a sample id from another task.
117
+ - vv-implementer: VVOC_STATUS ${describeStatusVocabulary("vv-implementer")}, with a required VVOC_ROUTE.
118
+ - vv-spec-reviewer / vv-code-reviewer: VVOC_STATUS ${describeStatusVocabulary("vv-spec-reviewer")}; a reviewer result carries no route.
119
+ A result whose first field names a different work item is a work-item mismatch, not a malformed header, and is never relabeled to the expected id. Inspect work_item_list before retrying to recover the current identity, state, attempt, and remaining budget. Common tool calls follow the published input schemas, and work_item_list reports the loaded contract revision and the on-demand reference path at contract.referencePath.
120
+ </tracked_result_protocol>
121
+ `.trim();
106
122
  /** Returns the exact workflow instruction compatible with one resolved policy. */
107
123
  function getWorkflowSystemInstruction(policy) {
108
124
  switch (policy.workflowGuidance) {
109
125
  case "review-only":
110
- return REVIEW_ONLY_WORKFLOW_SYSTEM_INSTRUCTION;
126
+ return `${REVIEW_ONLY_WORKFLOW_SYSTEM_INSTRUCTION}\n\n${TRACKED_RESULT_PROTOCOL_INSTRUCTION}`;
111
127
  case "selective":
112
- return SELECTIVE_WORKFLOW_SYSTEM_INSTRUCTION;
128
+ return `${SELECTIVE_WORKFLOW_SYSTEM_INSTRUCTION}\n\n${TRACKED_RESULT_PROTOCOL_INSTRUCTION}`;
113
129
  case "tracked":
114
130
  return workflowSystemInstructionTemplate.trim();
115
131
  case "delegated":
116
- return DELEGATED_WORKFLOW_SYSTEM_INSTRUCTION;
132
+ return `${DELEGATED_WORKFLOW_SYSTEM_INSTRUCTION}\n\n${TRACKED_RESULT_PROTOCOL_INSTRUCTION}`;
117
133
  }
118
134
  }
119
135
  function isTrackedSubagent(value) {
@@ -189,8 +205,11 @@ function appendSystemInstruction(existingSystem, instruction) {
189
205
  }
190
206
  return `${existingSystem.trim()}\n\n${instruction}`;
191
207
  }
192
- function stringifyToolOutput(value) {
193
- return JSON.stringify(value, null, 2);
208
+ // Serialize every public workflow result through the shared results contract so
209
+ // failures gain a stable category and post-side-effect reporting failures stay
210
+ // bounded and truthful instead of throwing raw after a committed mutation.
211
+ function stringifyToolOutput(value, outcome) {
212
+ return serializeWorkflowResult(value, outcome !== undefined ? { outcome } : undefined);
194
213
  }
195
214
  function createRoundLimitMessage(record, attemptedRound) {
196
215
  return [
@@ -657,6 +676,15 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
657
676
  lookupUserMessage: lookupRecoveryUserMessage,
658
677
  lookupAuthorityMessage,
659
678
  });
679
+ // START_BLOCK_TOOL_CONTRACT_HOOKS
680
+ // Owned-only contract publication and early structural/branch validation.
681
+ // The tool.definition adapter publishes the strict input JSON Schema through
682
+ // the host's observable jsonSchema member (T-001-proven seam) without
683
+ // replacing the host decoder; the execute.before guard rejects invalid
684
+ // raw arguments with bounded tokenized paths before any handler runs and
685
+ // never touches the task-launch hooks below.
686
+ const toolDefinitionAdapter = createToolDefinitionAdapter([...workflowToolContracts]);
687
+ // END_BLOCK_TOOL_CONTRACT_HOOKS
660
688
  return {
661
689
  tool: {
662
690
  work_item_open: tool({
@@ -664,21 +692,25 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
664
692
  args: workItemOpenArgs,
665
693
  async execute(args, context) {
666
694
  assertWorkflowToolAccess(context.agent, "work_item_open");
667
- const isGeneric = args.execution !== undefined || args.runId !== undefined;
695
+ const validation = validateWorkflowToolInput("work_item_open", args);
696
+ if (!validation.ok) {
697
+ return stringifyToolOutput(workflowInputFailure("work_item_open", context.sessionID, validation.issues));
698
+ }
699
+ const isGeneric = validation.data.execution !== undefined || validation.data.runId !== undefined;
668
700
  if (isGeneric) {
669
701
  // Hydrate/validate the session store before the transaction boundary
670
702
  // reads it, matching every other tool entry point.
671
703
  getOrCreateStore(context.sessionID);
672
- const result = await commitGenericToolResult(context.sessionID, (view) => workItemOpenTool.execute(args, {
704
+ const result = await commitGenericToolResult(context.sessionID, "work_item_open", (view) => workItemOpenTool.execute(args, {
673
705
  sessionId: context.sessionID,
674
706
  workspaceRoot: trustedWorkspaceRoot,
675
707
  }, view));
676
- return stringifyToolOutput(result);
708
+ return stringifyToolOutput(result, result.ok === true ? "committed" : "not_applied");
677
709
  }
678
710
  const sessionStore = getOrCreateStore(context.sessionID);
679
711
  const opened = workItemOpenTool.execute(args, { sessionId: context.sessionID }, sessionStore);
680
- snapshotSession(context.sessionID);
681
- return stringifyToolOutput(opened);
712
+ const openedPersisted = snapshotSession(context.sessionID);
713
+ return stringifyToolOutput(opened, opened.ok === false ? "not_applied" : openedPersisted.ok ? "committed" : "unknown");
682
714
  },
683
715
  }),
684
716
  work_item_list: tool({
@@ -697,104 +729,128 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
697
729
  assertWorkflowToolAccess(context.agent, "work_item_close");
698
730
  const sessionStore = getOrCreateStore(context.sessionID);
699
731
  const closed = workItemCloseTool.execute(args, { sessionId: context.sessionID }, sessionStore);
700
- snapshotSession(context.sessionID);
701
- return stringifyToolOutput(closed);
732
+ const closedPersisted = snapshotSession(context.sessionID);
733
+ return stringifyToolOutput(closed, closed.ok === false ? "not_applied" : closedPersisted.ok ? "committed" : "unknown");
702
734
  },
703
735
  }),
704
736
  // Control tools are registered independent of the startup profile; they
705
737
  // remain root-session, agent, workspace, and invalid-hydration gated at
706
738
  // execution time.
707
- ...{
708
- work_item_decide: tool({
709
- description: workItemDecideTool.description,
710
- args: workItemDecideArgs,
711
- async execute(args, context) {
712
- // Resolve the store first so invalid persisted state is detected
713
- // before the authorization check reports it as a control denial.
714
- const sessionStore = getOrCreateStore(context.sessionID);
715
- await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_item_decide");
716
- // Recovery is the only decision family that must persist
717
- // before it exposes new launch permissions. Authority-bearing
718
- // recovery stages the work-item recovery and the reserve debit in
719
- // one serialized transaction so they commit or fail together.
720
- if (args.decision === "recover") {
721
- if (args.authorityId !== undefined) {
722
- const staged = await commitGenericToolResult(context.sessionID, (view) => workItemDecideTool.execute(args, { sessionId: context.sessionID }, view));
723
- return stringifyToolOutput(staged);
724
- }
725
- const workItemId = String(args.workItemId ?? "");
726
- const recovered = await executeCommittedRecovery(context.sessionID, (liveStore) => workItemDecideTool.execute(args, { sessionId: context.sessionID }, liveStore), () => captureRecordRestore(context.sessionID, workItemId), (result) => result.ok === true);
727
- return stringifyToolOutput(recovered);
728
- }
729
- const decided = await workItemDecideTool.execute(args, { sessionId: context.sessionID }, sessionStore);
730
- if (decided.ok) {
731
- const persisted = snapshotSession(context.sessionID);
732
- if (!persisted.ok) {
733
- throw new Error(`PERSISTENCE_FAILED: decision applied in memory but could not be persisted: ${persisted.error}`);
734
- }
735
- }
736
- return stringifyToolOutput(decided);
737
- },
738
- }),
739
- work_checkpoint: tool({
740
- description: workCheckpointTool.description,
741
- args: workCheckpointArgs,
742
- async execute(args, context) {
743
- // Resolve the store first so invalid persisted state is detected
744
- // before the authorization check reports it as a control denial.
745
- const sessionStore = getOrCreateStore(context.sessionID);
746
- await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_checkpoint");
747
- const toolContext = {
748
- sessionId: context.sessionID,
749
- workspaceRoot: trustedWorkspaceRoot,
750
- loadPlan: async (planPath, workspaceRoot) => {
751
- const loaded = await loadApprovedDelegatedPlan({ workspaceRoot, planPath });
752
- return loaded.ok ? loaded.plan : { loadError: `${loaded.code}: ${loaded.message}` };
753
- },
754
- };
755
- // Generic (non-native) executions route through the atomic
756
- // transaction boundary: staged persist, then publish.
757
- const runIdArg = String(args.runId ?? "");
758
- const liveExecution = runIdArg
759
- ? sessionStore.getStoreData().executions.get(runIdArg)
760
- : undefined;
761
- const authorityAction = args.action === "authorize" ||
762
- args.action === "record_approval" ||
763
- args.action === "revoke_authority";
764
- const genericAction = args.action !== "register" &&
765
- liveExecution !== undefined &&
766
- (liveExecution.source.kind !== "native-package" || authorityAction);
767
- const nativeAuthorityRecover = args.action === "recover" &&
768
- args.authorityId !== undefined &&
769
- liveExecution !== undefined;
770
- const genericRegister = args.action === "register" && !args.planPath;
771
- if (genericAction || genericRegister || nativeAuthorityRecover) {
772
- const result = await commitGenericToolResult(context.sessionID, (view) => workCheckpointTool.execute(args, toolContext, view));
773
- return stringifyToolOutput(result);
739
+ work_item_decide: tool({
740
+ description: workItemDecideTool.description,
741
+ args: workItemDecideArgs,
742
+ async execute(args, context) {
743
+ // Resolve the store first so invalid persisted state is detected
744
+ // before the authorization check reports it as a control denial.
745
+ const sessionStore = getOrCreateStore(context.sessionID);
746
+ await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_item_decide");
747
+ const validation = validateWorkflowToolInput("work_item_decide", args);
748
+ if (!validation.ok) {
749
+ return stringifyToolOutput(workflowInputFailure("work_item_decide", context.sessionID, validation.issues));
750
+ }
751
+ // Recovery is the only decision family that must persist
752
+ // before it exposes new launch permissions. Authority-bearing
753
+ // recovery stages the work-item recovery and the reserve debit in
754
+ // one serialized transaction so they commit or fail together.
755
+ // Routing uses the same validated/normalized values the handler consumes.
756
+ const parsedDecide = validation.data;
757
+ if (parsedDecide.decision === "recover") {
758
+ if (parsedDecide.authorityId !== undefined) {
759
+ const staged = await commitGenericToolResult(context.sessionID, "work_item_decide", (view) => workItemDecideTool.execute(args, { sessionId: context.sessionID }, view));
760
+ return stringifyToolOutput(staged, staged.ok === true ? "committed" : "not_applied");
774
761
  }
775
- // Checkpoint recovery commits durably on the live store and
776
- // rolls the checkpoint entry back on write failure, so a
777
- // granted generation is never exposed before its state is
778
- // durably recorded.
779
- if (args.action === "recover") {
780
- const runId = String(args.runId ?? "");
781
- const checkpointId = String(args.checkpointId ?? "");
782
- const recovered = await executeCommittedRecovery(context.sessionID, (liveStore) => workCheckpointTool.execute(args, toolContext, liveStore), () => captureCheckpointRestore(context.sessionID, runId, checkpointId), (result) => result.ok === true);
783
- return stringifyToolOutput(recovered);
762
+ const workItemId = parsedDecide.workItemId.trim();
763
+ const recovered = await executeCommittedRecovery(context.sessionID, (liveStore) => workItemDecideTool.execute(args, { sessionId: context.sessionID }, liveStore), () => captureRecordRestore(context.sessionID, workItemId), (result) => result.ok === true);
764
+ // executeCommittedRecovery returns a success only after a durable
765
+ // write, and throws (rollback) if the write fails.
766
+ return stringifyToolOutput(recovered, recovered.ok === true ? "committed" : "not_applied");
767
+ }
768
+ const decided = await workItemDecideTool.execute(args, { sessionId: context.sessionID }, sessionStore);
769
+ if (decided.ok) {
770
+ const persisted = snapshotSession(context.sessionID);
771
+ if (!persisted.ok) {
772
+ throw new Error(`PERSISTENCE_FAILED: decision applied in memory but could not be persisted: ${persisted.error}`);
784
773
  }
785
- const result = await workCheckpointTool.execute({ ...args }, toolContext, sessionStore);
786
- if (result.ok) {
787
- const persisted = snapshotSession(context.sessionID);
788
- if (!persisted.ok) {
789
- throw new Error(`PERSISTENCE_FAILED: checkpoint change applied in memory but could not be persisted: ${persisted.error}`);
790
- }
774
+ }
775
+ return stringifyToolOutput(decided, decided.ok === true ? "committed" : "not_applied");
776
+ },
777
+ }),
778
+ work_checkpoint: tool({
779
+ description: workCheckpointTool.description,
780
+ args: workCheckpointArgs,
781
+ async execute(args, context) {
782
+ // Resolve the store first so invalid persisted state is detected
783
+ // before the authorization check reports it as a control denial.
784
+ const sessionStore = getOrCreateStore(context.sessionID);
785
+ await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_checkpoint");
786
+ const validation = validateWorkflowToolInput("work_checkpoint", args);
787
+ if (!validation.ok) {
788
+ return stringifyToolOutput(workflowInputFailure("work_checkpoint", context.sessionID, validation.issues));
789
+ }
790
+ const parsedArgs = validation.data;
791
+ const toolContext = {
792
+ sessionId: context.sessionID,
793
+ workspaceRoot: trustedWorkspaceRoot,
794
+ loadPlan: async (planPath, workspaceRoot) => {
795
+ const loaded = await loadApprovedDelegatedPlan({ workspaceRoot, planPath });
796
+ return loaded.ok ? loaded.plan : { loadError: `${loaded.code}: ${loaded.message}` };
797
+ },
798
+ };
799
+ // Generic (non-native) executions route through the atomic
800
+ // transaction boundary: staged persist, then publish. Routing uses
801
+ // the same trimmed runId the handler looks up, not a raw caller
802
+ // field that could select a different persistence path.
803
+ const runIdArg = parsedArgs.runId?.trim() ?? "";
804
+ const liveExecution = runIdArg
805
+ ? sessionStore.getStoreData().executions.get(runIdArg)
806
+ : undefined;
807
+ const authorityAction = parsedArgs.action === "authorize" ||
808
+ parsedArgs.action === "record_approval" ||
809
+ parsedArgs.action === "revoke_authority";
810
+ const genericAction = parsedArgs.action !== "register" &&
811
+ liveExecution !== undefined &&
812
+ (liveExecution.source.kind !== "native-package" || authorityAction);
813
+ const nativeAuthorityRecover = parsedArgs.action === "recover" &&
814
+ parsedArgs.authorityId !== undefined &&
815
+ liveExecution !== undefined;
816
+ const genericRegister = parsedArgs.action === "register" && parsedArgs.planPath === undefined;
817
+ if (genericAction || genericRegister || nativeAuthorityRecover) {
818
+ const result = await commitGenericToolResult(context.sessionID, "work_checkpoint", (view) => workCheckpointTool.execute(args, toolContext, view));
819
+ return stringifyToolOutput(result, result.ok === true ? "committed" : "not_applied");
820
+ }
821
+ // Checkpoint recovery commits durably on the live store and
822
+ // rolls the checkpoint entry back on write failure, so a
823
+ // granted generation is never exposed before its state is
824
+ // durably recorded.
825
+ if (parsedArgs.action === "recover") {
826
+ const runId = parsedArgs.runId ?? "";
827
+ const checkpointId = parsedArgs.checkpointId ?? "";
828
+ const recovered = await executeCommittedRecovery(context.sessionID, (liveStore) => workCheckpointTool.execute(args, toolContext, liveStore), () => captureCheckpointRestore(context.sessionID, runId, checkpointId), (result) => result.ok === true);
829
+ return stringifyToolOutput(recovered, recovered.ok === true ? "committed" : "not_applied");
830
+ }
831
+ const result = await workCheckpointTool.execute({ ...args }, toolContext, sessionStore);
832
+ if (result.ok) {
833
+ const persisted = snapshotSession(context.sessionID);
834
+ if (!persisted.ok) {
835
+ throw new Error(`PERSISTENCE_FAILED: checkpoint change applied in memory but could not be persisted: ${persisted.error}`);
791
836
  }
792
- return stringifyToolOutput(result);
793
- },
794
- }),
795
- },
837
+ }
838
+ return stringifyToolOutput(result, result.ok === true ? "committed" : "not_applied");
839
+ },
840
+ }),
796
841
  },
842
+ "tool.definition": toolDefinitionAdapter,
797
843
  "tool.execute.before": async (input, output) => {
844
+ // Owned workflow tools get strict structural plus branch validation on
845
+ // the raw forwarded arguments before their handlers run. This runs before
846
+ // the non-task early return and never mutates output.args.
847
+ if (isWorkflowToolId(input.tool)) {
848
+ const validation = validateWorkflowToolInput(input.tool, output.args);
849
+ if (!validation.ok) {
850
+ throw new ContractInputError(input.tool, validation.issues);
851
+ }
852
+ return;
853
+ }
798
854
  if (input.tool !== "task") {
799
855
  return;
800
856
  }
@@ -945,6 +1001,9 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
945
1001
  const planRunId = workItem.delegated?.planRunId;
946
1002
  if (planRunId) {
947
1003
  const run = data.planRuns.get(planRunId);
1004
+ if (run?.status === "sealed") {
1005
+ throw new Error(`LAUNCH_REJECTED_SEALED: run ${planRunId} is sealed and cannot launch ${workItem.workItemId}.`);
1006
+ }
948
1007
  const binding = run
949
1008
  ? [...run.tasks.values()].find((task) => task.workItemId === workItem.workItemId)
950
1009
  : undefined;
@@ -1186,12 +1245,18 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
1186
1245
  ? [
1187
1246
  `Report rejected: attempt ${settledAttempt} of ${header.value} settled as report_rejected with bounded diagnostics retained.`,
1188
1247
  (() => {
1189
- const settledRecord = getWorkItem(getOrCreateStore(input.sessionID), input.sessionID, header.value);
1190
- const progress = settledRecord
1191
- ? summarizeDelegatedProgress(settledRecord)
1248
+ const store = getOrCreateStore(input.sessionID);
1249
+ const settledRecord = getWorkItem(store, input.sessionID, header.value);
1250
+ const guidance = settledRecord
1251
+ ? deriveDelegatedGuidance({
1252
+ record: settledRecord,
1253
+ progress: summarizeDelegatedProgress(settledRecord),
1254
+ latest: latestAttemptView(settledRecord),
1255
+ context: { data: store.getStoreData(), sessionId: input.sessionID },
1256
+ })
1192
1257
  : undefined;
1193
- return `Next action: ${progress?.nextAction ?? "inspect work_item_list"}${progress?.nextAction === "recover" ||
1194
- progress?.nextAction === "recover_with_user_authorization"
1258
+ return `Next action: ${guidance?.nextAction ?? "inspect work_item_list"}${guidance?.nextAction === "recover" ||
1259
+ guidance?.nextAction === "recover_with_user_authorization"
1195
1260
  ? ' through work_item_decide decision "recover"'
1196
1261
  : ""}.`;
1197
1262
  })(),