@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,10 +1,10 @@
1
1
  // FILE: src/plugins/workflow/tooling.ts
2
- // VERSION: 0.5.0
2
+ // VERSION: 0.6.0
3
3
  // START_MODULE_CONTRACT
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; 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. Tool argument shapes are single-sourced from src/plugins/workflow/schemas.ts.
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/plugins/workflow/schemas.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
4
+ // PURPOSE: Provide work-item tooling handlers that wrap explicit workflow state operations with structured protocol-friendly responses under strict branch-aware input validation.
5
+ // SCOPE: work_item_open, work_item_list, and work_item_close tool definitions with standalone/generic open validation and mode-specific serialization including recovery-aware progress summaries; generic execution registration/append through an optional execution descriptor or runId with typed sources, boundaries, tasks, and checkpoints; 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. Every handler validates raw arguments through validateWorkflowToolInput before dispatch or mutation and then consumes the parsed canonical values; provided-plan references/hashes are trimmed so source identity stays idempotent, run session ownership is resolved before any source-specific diagnostic or action discrimination, source-dependent known fields are rejected only after an owned run's source is resolved, authority replay/extension compares the supplied scope against the recorded scope, and unknown runs fail as lookup failures rather than missing native-only fields. Tool argument shapes are single-sourced from src/plugins/workflow/schemas.ts.
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/plugins/workflow/input-validation.ts, src/plugins/workflow/schemas.ts, src/plugins/workflow/results.ts, src/lib/agent-tool-contract.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-WORKFLOW-CONTRACT, M-AGENT-TOOL-CONTRACT, M-PLUGIN-WORKFLOW
8
8
  // ROLE: RUNTIME
9
9
  // MAP_MODE: EXPORTS
10
10
  // END_MODULE_CONTRACT
@@ -13,8 +13,8 @@
13
13
  // WorkflowToolContext - Minimal execution context required by workflow tools.
14
14
  // WorkflowToolDefinition - Deterministic tool definition shape with an optionally async execute handler.
15
15
  // DelegatedControlOptions - Optional read-only authorization lookup bound to the plugin SDK client.
16
- // createWorkItemOpenTool - Creates work_item_open tool wrapper around explicit openWorkItem contract including standalone delegated tasks.
17
- // createWorkItemListTool - Creates work_item_list tool wrapper with mode, round metadata, delegated acceptance and recovery state, and registered plan runs.
16
+ // createWorkItemOpenTool - Creates work_item_open tool wrapper around explicit openWorkItem contract including standalone delegated tasks and generic execution registration/append.
17
+ // createWorkItemListTool - Creates work_item_list tool wrapper returning the additive read-only inspection payload (items, plan runs, executions, loaded contract identity).
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
20
  // createWorkCheckpointTool - Creates work_checkpoint control wrapper around plan registration, checkpoint start, verify, checkpoint recovery, and generic checkpoint/completion/amendment/authority actions.
@@ -27,107 +27,192 @@
27
27
  // END_MODULE_MAP
28
28
  //
29
29
  // START_CHANGE_SUMMARY
30
- // LAST_CHANGE: [C-WORKFLOW-INDEX-REDUCE - Argument shapes moved to single-sourced schemas.ts with z.infer types re-exported here; accepted shapes are unchanged.]
30
+ // LAST_CHANGE: [C-AGENT-TOOL-CONTRACTS T-004 - work_item_list delegates to the read-only inspection owner; every handler that reports a delegated next action (open, failure context, recovery) now composes the same deriveDelegatedGuidance as the list so responses cannot contradict it, and generic register/amend plus checkpoint amend/complete execution views pass live store data so task status derives from current acceptance. Prior T-003: every handler result is finalized so failures carry a stable category. A missing trusted workspace root/plan loader/authorization lookup is a distinct host_context failure instead of INVALID_INPUT.]
31
31
  // END_CHANGE_SUMMARY
32
- import { closeWorkItem, getReviewRound, listWorkItems, openWorkItem, } from "./state.js";
33
- import { authorizeReworkFromFailedCheckpoint, getDelegatedRunView, recoverDelegatedCheckpoint, registerDelegatedPlan, startDelegatedCheckpoint, verifyDelegatedCheckpoint, } from "./checkpoints.js";
32
+ import { closeWorkItem, openWorkItem, } from "./state.js";
33
+ import { authorizeReworkFromFailedCheckpoint, recoverDelegatedCheckpoint, registerDelegatedPlan, startDelegatedCheckpoint, verifyDelegatedCheckpoint, } from "./checkpoints.js";
34
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";
35
+ import { addAuthorityInStore, addReserveDebitInStore, addStageApprovalInStore, appendExecutionWorkInStore, completeExecutionInStore, findExecution, getExecutionView, latestAttemptView, recordGenericReviewerResultInStore, recoverGenericCheckpointInStore, putAuthorityInStore, registerExecutionInStore, startGenericCheckpointInStore, } from "./execution.js";
36
+ import { advanceUnitsAvailable, authorityScopeDifferences, effectiveAuthorityStages, extendAdvanceAuthority, grantAdvanceAuthority, proposeReserveDebit, proposeStageApproval, revokeAdvanceAuthority, narrowAdvanceAuthority, } from "./authority.js";
37
+ import { validateExecutionBoundary, validateWorkflowCheckpointContract, } from "../../lib/workflow-contract.js";
38
+ import { WORKFLOW_TOOL_DESCRIPTIONS, validateWorkflowToolInput } from "./input-validation.js";
39
+ import { getWorkflowInspection, deriveDelegatedGuidance, serializeWorkItem } from "./inspection.js";
40
+ import { finalizeWorkflowResult, workflowHostContextFailure, workflowInputFailure, } from "./results.js";
37
41
  function coerceNonEmptyString(value) {
38
42
  if (typeof value !== "string")
39
43
  return undefined;
40
44
  const trimmed = value.trim();
41
45
  return trimmed || undefined;
42
46
  }
43
- function isWorkItemMode(value) {
44
- return value === "implementation" || value === "review_only" || value === "delegated";
47
+ function invalidInput(tool, sessionId, issues) {
48
+ // Preserve the exact bounded tokenized issues; the early execute.before hook
49
+ // throws the same ContractInputError form, so hook and handler agree.
50
+ return workflowInputFailure(tool, sessionId, issues);
45
51
  }
46
- function isReviewerRole(value) {
47
- return value === "spec" || value === "code";
52
+ /** A missing trusted workspace root/plan loader/authorization lookup is host context, not caller input. */
53
+ function hostContextInvalid(tool, sessionId, message) {
54
+ return workflowHostContextFailure(tool, sessionId, message);
48
55
  }
49
- function canonicalizeReviewers(value) {
50
- if (!Array.isArray(value) || value.length === 0)
51
- return undefined;
52
- if (!value.every(isReviewerRole))
53
- return undefined;
54
- const unique = new Set(value);
55
- if (unique.size !== value.length)
56
- return undefined;
57
- return [...value].sort((left, right) => {
58
- if (left === right)
59
- return 0;
60
- return left === "spec" ? -1 : 1;
56
+ // START_BLOCK_RESULT_FINALIZATION
57
+ /**
58
+ * Finalize one handler result so failures carry a stable category, bounded
59
+ * prerequisite, and (where owned state is known) concrete current-state,
60
+ * attempt, status, and budget context. Batch item failures are finalized too.
61
+ */
62
+ function withFinalizedResult(definition) {
63
+ const execute = definition.execute;
64
+ return {
65
+ ...definition,
66
+ execute: (args, context, store) => finalizeWorkflowResult(execute(args, context, store)),
67
+ };
68
+ }
69
+ /** Async variant of `withFinalizedResult` for control tools. */
70
+ function withFinalizedAsyncResult(definition) {
71
+ const execute = definition.execute;
72
+ return {
73
+ ...definition,
74
+ async execute(args, context, store) {
75
+ return finalizeWorkflowResult(await execute(args, context, store));
76
+ },
77
+ };
78
+ }
79
+ /**
80
+ * Compose the shared delegated guidance for one owned record so every response
81
+ * that reports a next action (list, open, failure, recovery) uses the same
82
+ * snapshot-level gates and can never contradict the list view.
83
+ */
84
+ function delegatedGuidanceFor(store, sessionId, record) {
85
+ return deriveDelegatedGuidance({
86
+ record,
87
+ progress: summarizeDelegatedProgress(record),
88
+ latest: latestAttemptView(record),
89
+ context: { data: store.getStoreData(), sessionId },
61
90
  });
62
91
  }
63
- // START_BLOCK_GENERIC_NORMALIZATION
64
- function isWorkflowReviewer(value) {
65
- return value === "spec" || value === "code";
92
+ /**
93
+ * Bounded current-state context for an owned same-session work-item failure.
94
+ * Read only after access checks; it never dumps the private store or foreign
95
+ * records, and it is composed from existing eligibility helpers.
96
+ */
97
+ function ownedWorkItemFailureContext(store, sessionId, workItemId, errorCode) {
98
+ const record = store.getWorkItem(sessionId, workItemId);
99
+ if (!record)
100
+ return {};
101
+ const context = { state: record.state };
102
+ if (record.currentRound) {
103
+ context.pendingReviewers = [...record.currentRound.pendingReviewers];
104
+ }
105
+ if (record.delegated) {
106
+ const progress = summarizeDelegatedProgress(record);
107
+ const guidance = delegatedGuidanceFor(store, sessionId, record);
108
+ context.attemptBudget = progress.attemptBudget;
109
+ context.remainingAttempts = progress.remainingAttempts;
110
+ context.recoveryCount = progress.recoveryGrants;
111
+ context.nextAction = guidance.nextAction;
112
+ if (guidance.nextAction === "launch_blocked" && guidance.prerequisites.length > 0) {
113
+ context.prerequisite = guidance.prerequisites[0];
114
+ }
115
+ const attempts = record.delegated.attempts;
116
+ const latest = attempts.length > 0 ? attempts[attempts.length - 1] : undefined;
117
+ if (latest) {
118
+ context.attempt = latest.attempt;
119
+ context.attemptStatus = latest.status;
120
+ if (latest.resultStatus !== undefined)
121
+ context.resultStatus = latest.resultStatus;
122
+ }
123
+ }
124
+ const prerequisite = ownedPrerequisite(record, errorCode);
125
+ if (prerequisite !== undefined && context.prerequisite === undefined) {
126
+ context.prerequisite = prerequisite;
127
+ }
128
+ return context;
66
129
  }
67
- function stringList(value) {
68
- return Array.isArray(value) ? value.map(String) : [];
130
+ /**
131
+ * Real unmet prerequisite for the record's current state and the failure code.
132
+ * Family-specific codes keep their precise condition; otherwise the current
133
+ * owned state determines the prerequisite.
134
+ */
135
+ function ownedPrerequisite(record, errorCode) {
136
+ switch (errorCode) {
137
+ case "CONCERNS_DISPOSITION_REQUIRED":
138
+ return "an explicit concernsDisposition for the DONE_WITH_CONCERNS attempt";
139
+ case "UNEXPECTED_CONCERNS_DISPOSITION":
140
+ return "an attempt that completed DONE_WITH_CONCERNS";
141
+ case "INVALID_ATTEMPT":
142
+ case "ATTEMPT_MISMATCH":
143
+ return "the current completed attempt number for this work item";
144
+ case "ATTEMPT_NOT_TERMINAL":
145
+ return "a terminal targeted attempt";
146
+ case "ATTEMPTS_EXHAUSTED":
147
+ return "an explicit recovery grant or checkpoint-authorized rework";
148
+ case "AUTONOMOUS_GRANT_EXHAUSTED":
149
+ return "a recorded advance authority or a fresh root-user message";
150
+ default:
151
+ break;
152
+ }
153
+ if (record.delegated) {
154
+ if (record.state === "ready_to_close")
155
+ return undefined;
156
+ if (record.state === "awaiting_acceptance") {
157
+ return "a controller accept/request_changes decision for the current completed attempt";
158
+ }
159
+ if (record.state === "blocked" || record.state === "needs_context") {
160
+ return "a bounded recovery (or an authorized advance) for the stopped attempt";
161
+ }
162
+ if (record.currentRound && record.currentRound.pendingReviewers.length > 0) {
163
+ return "a completed review round from every required reviewer";
164
+ }
165
+ if (currentDelegatedAcceptance(record) === undefined) {
166
+ return "a current controller acceptance for the latest completed attempt";
167
+ }
168
+ return "a current controller decision, recovery, or rework authorization for this delegated item";
169
+ }
170
+ if (record.state === "awaiting_reviews" ||
171
+ (record.currentRound && record.currentRound.pendingReviewers.length > 0)) {
172
+ return "a completed review round from every required reviewer";
173
+ }
174
+ return undefined;
69
175
  }
70
- /** Normalize one tool item into a bounded generic task contract. */
176
+ // END_BLOCK_RESULT_FINALIZATION
177
+ // START_BLOCK_GENERIC_NORMALIZATION
178
+ /** Normalize one structurally validated generic task item into a bounded task contract. */
71
179
  function normalizeTaskContract(item) {
72
180
  const taskId = coerceNonEmptyString(item.taskId) ?? coerceNonEmptyString(item.key);
73
181
  const title = coerceNonEmptyString(item.title) ?? taskId;
74
182
  if (!taskId || !title) {
75
183
  return { ok: false, message: "each task item requires a non-empty taskId (or key) and title" };
76
184
  }
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
- }
185
+ // Structural validation guarantees an explicit unique spec/code reviewer array.
83
186
  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
187
  return {
88
188
  ok: true,
89
189
  contract: {
90
190
  taskId,
91
191
  title,
92
192
  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),
193
+ acceptanceCriteria: [...(item.acceptanceCriteria ?? [])],
194
+ verification: [...(item.verification ?? [])],
195
+ writeScope: [...(item.writeScope ?? [])],
196
+ dependsOn: [...(item.dependsOn ?? [])],
197
+ blockedBy: [...(item.blockedBy ?? [])],
98
198
  requiredReviewers: [...reviewers],
99
199
  },
100
200
  };
101
201
  }
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 } : {}) };
202
+ /** Convert typed checkpoint inputs into validated common checkpoint contracts. */
203
+ function normalizeCheckpointInputContracts(raw) {
204
+ const contracts = [];
205
+ for (const entry of raw ?? []) {
206
+ const validated = validateWorkflowCheckpointContract(entry);
207
+ if (!validated.ok) {
208
+ return {
209
+ ok: false,
210
+ message: validated.problems.map((problem) => problem.message).join("; "),
211
+ };
212
+ }
213
+ contracts.push(validated.value);
114
214
  }
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 : [];
215
+ return { ok: true, contracts };
131
216
  }
132
217
  function taskContractsFromItems(items) {
133
218
  const contracts = [];
@@ -142,10 +227,26 @@ function taskContractsFromItems(items) {
142
227
  }
143
228
  return { ok: true, contracts };
144
229
  }
230
+ /**
231
+ * Field-specific canonicalization of a structurally validated generic source.
232
+ * Only the documented provided-plan reference and optional hash are trimmed, so
233
+ * a canonical and a whitespace-varied valid call keep the same source identity
234
+ * and idempotency. Native bindings and workflow identities are never rewritten.
235
+ */
236
+ function canonicalizeExecutionSource(source) {
237
+ if (source.kind === "conversation-scoped")
238
+ return { kind: "conversation-scoped" };
239
+ return {
240
+ kind: "provided-plan",
241
+ reference: source.reference.trim(),
242
+ ...(source.sha256 !== undefined ? { sha256: source.sha256.trim() } : {}),
243
+ };
244
+ }
145
245
  // END_BLOCK_GENERIC_NORMALIZATION
246
+ /** Map a structurally validated standalone item into explicit openWorkItem input. */
146
247
  function normalizeOpenInputItem(item, sessionId) {
147
- const key = coerceNonEmptyString(item.key);
148
- const title = coerceNonEmptyString(item.title);
248
+ const key = item.key.trim();
249
+ const title = item.title.trim();
149
250
  if (!key || !title) {
150
251
  return {
151
252
  ok: false,
@@ -153,39 +254,7 @@ function normalizeOpenInputItem(item, sessionId) {
153
254
  message: "INVALID_INPUT: key and title must be non-empty strings",
154
255
  };
155
256
  }
156
- if (!isWorkItemMode(item.mode)) {
157
- return {
158
- ok: false,
159
- errorCode: "INVALID_INPUT",
160
- message: "INVALID_INPUT: mode must be implementation, review_only, or delegated",
161
- };
162
- }
163
257
  if (item.mode === "delegated") {
164
- const reviewers = Array.isArray(item.requiredReviewers) ? item.requiredReviewers : [];
165
- if (reviewers.length !== 0) {
166
- return {
167
- ok: false,
168
- errorCode: "INVALID_INPUT",
169
- message: "INVALID_INPUT: delegated mode requires an explicitly empty requiredReviewers array",
170
- };
171
- }
172
- const writeScope = Array.isArray(item.writeScope) ? item.writeScope.map(String) : [];
173
- if (writeScope.length === 0) {
174
- return {
175
- ok: false,
176
- errorCode: "INVALID_INPUT",
177
- message: "INVALID_INPUT: delegated mode requires a non-empty writeScope of workspace-relative files",
178
- };
179
- }
180
- const planRunId = coerceNonEmptyString(item.planRunId);
181
- const planTaskId = coerceNonEmptyString(item.planTaskId);
182
- if ((planRunId === undefined) !== (planTaskId === undefined)) {
183
- return {
184
- ok: false,
185
- errorCode: "INVALID_INPUT",
186
- message: "INVALID_INPUT: planRunId and planTaskId must be provided together",
187
- };
188
- }
189
258
  return {
190
259
  ok: true,
191
260
  input: {
@@ -194,28 +263,18 @@ function normalizeOpenInputItem(item, sessionId) {
194
263
  title,
195
264
  mode: "delegated",
196
265
  requiredReviewers: [],
197
- writeScope,
198
- ...(planRunId && planTaskId ? { planRunId, planTaskId } : {}),
266
+ writeScope: [...(item.writeScope ?? [])],
267
+ ...(item.planRunId && item.planTaskId
268
+ ? { planRunId: item.planRunId, planTaskId: item.planTaskId }
269
+ : {}),
199
270
  },
200
271
  };
201
272
  }
202
- const requiredReviewers = canonicalizeReviewers(item.requiredReviewers);
203
- if (!requiredReviewers) {
204
- return {
205
- ok: false,
206
- errorCode: "INVALID_INPUT",
207
- message: "INVALID_INPUT: requiredReviewers must be a non-empty array containing unique spec/code reviewers",
208
- };
209
- }
210
- if (item.writeScope !== undefined ||
211
- item.planRunId !== undefined ||
212
- item.planTaskId !== undefined) {
213
- return {
214
- ok: false,
215
- errorCode: "INVALID_INPUT",
216
- message: `INVALID_INPUT: writeScope and plan bindings are only valid for delegated mode, not ${item.mode}`,
217
- };
218
- }
273
+ const requiredReviewers = [...item.requiredReviewers].sort((left, right) => {
274
+ if (left === right)
275
+ return 0;
276
+ return left === "spec" ? -1 : 1;
277
+ });
219
278
  return {
220
279
  ok: true,
221
280
  input: {
@@ -227,80 +286,25 @@ function normalizeOpenInputItem(item, sessionId) {
227
286
  },
228
287
  };
229
288
  }
230
- function serializeWorkItem(record) {
231
- return {
232
- workItemId: record.workItemId,
233
- header: `VVOC_WORK_ITEM_ID: ${record.workItemId}`,
234
- key: record.key,
235
- title: record.title,
236
- mode: record.mode,
237
- requiredReviewers: record.requiredReviewers,
238
- state: record.state,
239
- specReviewCount: record.specReviewCount,
240
- codeReviewCount: record.codeReviewCount,
241
- reviewRound: getReviewRound(record),
242
- currentRound: record.currentRound,
243
- ...(record.resultExcerpt ? { resultExcerpt: record.resultExcerpt } : {}),
244
- completedReviewRoundCount: record.completedReviewRoundCount,
245
- createdAt: record.createdAt,
246
- updatedAt: record.updatedAt,
247
- ...(record.closedAt ? { closedAt: record.closedAt } : {}),
248
- ...(record.delegated
249
- ? {
250
- delegated: {
251
- writeScope: record.delegated.writeScope,
252
- ...(record.delegated.planRunId
253
- ? { planRunId: record.delegated.planRunId, planTaskId: record.delegated.planTaskId }
254
- : {}),
255
- attempts: record.delegated.attempts.length,
256
- inFlightAttempt: record.delegated.attempts.some((attempt) => attempt.status === "in_flight"),
257
- decisions: record.delegated.decisions.length,
258
- accepted: currentDelegatedAcceptance(record) !== undefined,
259
- acceptedAttempt: currentDelegatedAcceptance(record)?.attempt,
260
- reworkCount: record.delegated.reworkHistory.length,
261
- ...serializeProgress(record),
262
- },
263
- }
264
- : {}),
265
- };
266
- }
267
- /** Recovery-aware progress fields shared by work-item serialization. */
268
- function serializeProgress(record) {
269
- const progress = summarizeDelegatedProgress(record);
270
- return {
271
- attemptBudget: progress.attemptBudget,
272
- remainingAttempts: progress.remainingAttempts,
273
- recoveryCount: progress.recoveryGrants,
274
- autonomousGrantConsumed: progress.autonomousGrantConsumed,
275
- reportRejectionCount: progress.reportRejectedAttempts,
276
- nextAction: progress.nextAction,
277
- };
278
- }
279
289
  // START_CONTRACT: createWorkItemOpenTool
280
- // PURPOSE: Build work_item_open handler that supports deterministic batch idempotent open operations with explicit workflow intent.
290
+ // PURPOSE: Build work_item_open handler that supports deterministic batch idempotent open operations with explicit workflow intent under whole-request structural validation.
281
291
  // INPUTS: { store: WorkItemStore - workflow in-memory store }
282
292
  // OUTPUTS: { WorkflowToolDefinition<OpenToolInput, unknown> - executable tool definition }
283
- // SIDE_EFFECTS: [Mutates in-memory work-item store through open operations]
284
- // LINKS: [M-WORKFLOW-TOOLING, M-WORKFLOW-STATE]
293
+ // SIDE_EFFECTS: [Mutates in-memory work-item store through open operations only after structural validation passes]
294
+ // LINKS: [M-WORKFLOW-TOOLING, M-WORKFLOW-STATE, validateWorkflowToolInput]
285
295
  // END_CONTRACT: createWorkItemOpenTool
286
296
  export function createWorkItemOpenTool(store) {
287
- return {
297
+ return withFinalizedResult({
288
298
  name: "work_item_open",
289
- description: "Open one or more workflow work items idempotently with explicit mode and requiredReviewers.",
299
+ description: WORKFLOW_TOOL_DESCRIPTIONS.work_item_open,
290
300
  execute: (args, context, overrideStore) => {
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
- };
301
+ const validation = validateWorkflowToolInput("work_item_open", args);
302
+ if (!validation.ok) {
303
+ return invalidInput("work_item_open", context.sessionId, validation.issues);
302
304
  }
303
- if (executionArg !== undefined || runIdArg !== undefined) {
305
+ const parsed = validation.data;
306
+ const inputItems = parsed.items;
307
+ if (parsed.execution !== undefined || parsed.runId !== undefined) {
304
308
  const s = overrideStore ?? store;
305
309
  const data = s.getStoreData();
306
310
  const contracts = taskContractsFromItems(inputItems);
@@ -313,33 +317,41 @@ export function createWorkItemOpenTool(store) {
313
317
  message: `INVALID_INPUT: ${contracts.message}`,
314
318
  };
315
319
  }
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);
320
+ if (parsed.execution !== undefined) {
321
+ const descriptor = parsed.execution;
324
322
  const workspaceRoot = coerceNonEmptyString(context.workspaceRoot);
325
- if (!executionKey || !source || !boundary || !goal || !workspaceRoot) {
323
+ if (!workspaceRoot) {
324
+ return hostContextInvalid("work_item_open", context.sessionId, "execution registration requires the trusted workspace root from the plugin context");
325
+ }
326
+ const boundary = validateExecutionBoundary(descriptor.boundary);
327
+ if (!boundary.ok) {
326
328
  return {
327
329
  tool: "work_item_open",
328
330
  sessionId: context.sessionId,
329
331
  ok: false,
330
332
  errorCode: "INVALID_INPUT",
331
- message: "INVALID_INPUT: execution requires executionKey, source, goal, boundary, and the trusted workspace root",
333
+ message: `INVALID_INPUT: ${boundary.problems.map((problem) => problem.message).join("; ")}`,
334
+ };
335
+ }
336
+ const checkpointContracts = normalizeCheckpointInputContracts(descriptor.checkpoints);
337
+ if (!checkpointContracts.ok) {
338
+ return {
339
+ tool: "work_item_open",
340
+ sessionId: context.sessionId,
341
+ ok: false,
342
+ errorCode: "INVALID_INPUT",
343
+ message: `INVALID_INPUT: ${checkpointContracts.message}`,
332
344
  };
333
345
  }
334
346
  const registered = registerExecutionInStore(data, {
335
347
  sessionId: context.sessionId,
336
348
  workspaceRoot,
337
- executionKey,
338
- source,
339
- goal,
340
- boundary,
349
+ executionKey: descriptor.executionKey.trim(),
350
+ source: canonicalizeExecutionSource(descriptor.source),
351
+ goal: descriptor.goal.trim(),
352
+ boundary: boundary.value,
341
353
  tasks: contracts.contracts.map((contract) => ({ contract })),
342
- checkpoints: normalizeCheckpointContracts(descriptor.checkpoints),
354
+ checkpoints: checkpointContracts.contracts,
343
355
  });
344
356
  if (!registered.ok) {
345
357
  return {
@@ -357,23 +369,15 @@ export function createWorkItemOpenTool(store) {
357
369
  action: "register",
358
370
  runId: registered.runId,
359
371
  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
+ execution: getExecutionView(registered.execution, data),
372
373
  };
373
374
  }
375
+ const runId = parsed.runId.trim();
376
+ const amendmentId = parsed.amendmentId.trim();
377
+ const rationale = parsed.rationale;
374
378
  const appended = appendExecutionWorkInStore(data, {
375
379
  sessionId: context.sessionId,
376
- runId: runIdArg,
380
+ runId,
377
381
  amendmentId,
378
382
  rationale,
379
383
  tasks: contracts.contracts.map((contract) => ({ contract })),
@@ -392,11 +396,13 @@ export function createWorkItemOpenTool(store) {
392
396
  sessionId: context.sessionId,
393
397
  ok: true,
394
398
  action: "amend",
395
- runId: runIdArg,
399
+ runId,
396
400
  revision: appended.revision,
397
- execution: getExecutionView(appended.execution),
401
+ execution: getExecutionView(appended.execution, data),
398
402
  };
399
403
  }
404
+ // Structural validation already accepted every item; remaining per-item
405
+ // failures are domain outcomes (idempotency conflicts, state rules).
400
406
  const results = inputItems.map((item) => {
401
407
  const normalized = normalizeOpenInputItem(item, context.sessionId);
402
408
  if (!normalized.ok) {
@@ -406,7 +412,8 @@ export function createWorkItemOpenTool(store) {
406
412
  message: normalized.message,
407
413
  };
408
414
  }
409
- const opened = openWorkItem(overrideStore ?? store, normalized.input);
415
+ const targetStore = overrideStore ?? store;
416
+ const opened = openWorkItem(targetStore, normalized.input);
410
417
  if (!opened.ok) {
411
418
  return {
412
419
  ok: false,
@@ -418,9 +425,10 @@ export function createWorkItemOpenTool(store) {
418
425
  return {
419
426
  ok: true,
420
427
  reused: opened.reused,
421
- workItemId: opened.record.workItemId,
422
- header: opened.header,
423
- ...serializeWorkItem(opened.record),
428
+ ...serializeWorkItem(opened.record, {
429
+ data: targetStore.getStoreData(),
430
+ sessionId: context.sessionId,
431
+ }),
424
432
  };
425
433
  });
426
434
  return {
@@ -429,37 +437,29 @@ export function createWorkItemOpenTool(store) {
429
437
  items: results,
430
438
  };
431
439
  },
432
- };
440
+ });
433
441
  }
434
442
  // START_CONTRACT: createWorkItemListTool
435
- // PURPOSE: Build work_item_list handler that returns current work items, explicit review-round metadata, and registered plan runs.
443
+ // PURPOSE: Build work_item_list handler that returns current work items, explicit review-round metadata, registered plan runs, generic/native execution views, and loaded contract identity.
436
444
  // INPUTS: { store: WorkItemStore - workflow in-memory store }
437
445
  // OUTPUTS: { WorkflowToolDefinition<ListArgs, unknown> - executable tool definition }
438
446
  // SIDE_EFFECTS: [none]
439
- // LINKS: [M-WORKFLOW-TOOLING, M-WORKFLOW-STATE, M-WORKFLOW-CHECKPOINTS]
447
+ // LINKS: [M-WORKFLOW-TOOLING, M-WORKFLOW-STATE, M-WORKFLOW-CHECKPOINTS, M-WORKFLOW-EXECUTION]
440
448
  // END_CONTRACT: createWorkItemListTool
441
449
  export function createWorkItemListTool(store) {
442
- return {
450
+ return withFinalizedResult({
443
451
  name: "work_item_list",
444
- description: "List workflow work items for the current session.",
452
+ description: WORKFLOW_TOOL_DESCRIPTIONS.work_item_list,
445
453
  execute: (args, context, overrideStore) => {
454
+ const validation = validateWorkflowToolInput("work_item_list", args);
455
+ if (!validation.ok) {
456
+ return invalidInput("work_item_list", context.sessionId, validation.issues);
457
+ }
446
458
  const s = overrideStore ?? store;
447
- const includeClosed = args.includeClosed === true;
448
- const records = listWorkItems(s, context.sessionId, { includeClosed });
449
- const data = s.getStoreData();
450
- const planRuns = [...data.planRuns.values()]
451
- .filter((run) => run.sessionId === context.sessionId)
452
- .map((run) => getDelegatedRunView(data, run.runId))
453
- .filter((view) => view !== undefined);
454
- return {
455
- tool: "work_item_list",
456
- sessionId: context.sessionId,
457
- includeClosed,
458
- items: records.map(serializeWorkItem),
459
- ...(planRuns.length > 0 ? { planRuns } : {}),
460
- };
459
+ const includeClosed = validation.data.includeClosed === true;
460
+ return getWorkflowInspection(s, context.sessionId, { includeClosed });
461
461
  },
462
- };
462
+ });
463
463
  }
464
464
  // START_CONTRACT: createWorkItemCloseTool
465
465
  // PURPOSE: Build work_item_close handler that closes a ready_to_close work item.
@@ -469,21 +469,16 @@ export function createWorkItemListTool(store) {
469
469
  // LINKS: [M-WORKFLOW-TOOLING, M-WORKFLOW-STATE]
470
470
  // END_CONTRACT: createWorkItemCloseTool
471
471
  export function createWorkItemCloseTool(store) {
472
- return {
472
+ return withFinalizedResult({
473
473
  name: "work_item_close",
474
- description: "Close a workflow work item by id when it is ready_to_close.",
474
+ description: WORKFLOW_TOOL_DESCRIPTIONS.work_item_close,
475
475
  execute: (args, context, overrideStore) => {
476
- const s = overrideStore ?? store;
477
- const workItemId = coerceNonEmptyString(args.workItemId);
478
- if (!workItemId) {
479
- return {
480
- tool: "work_item_close",
481
- sessionId: context.sessionId,
482
- ok: false,
483
- errorCode: "INVALID_INPUT",
484
- message: "INVALID_INPUT: workItemId must be a non-empty string",
485
- };
476
+ const validation = validateWorkflowToolInput("work_item_close", args);
477
+ if (!validation.ok) {
478
+ return invalidInput("work_item_close", context.sessionId, validation.issues);
486
479
  }
480
+ const s = overrideStore ?? store;
481
+ const workItemId = validation.data.workItemId.trim();
487
482
  const closed = closeWorkItem(s, context.sessionId, workItemId);
488
483
  if (!closed.ok) {
489
484
  return {
@@ -492,6 +487,7 @@ export function createWorkItemCloseTool(store) {
492
487
  ok: false,
493
488
  errorCode: closed.errorCode,
494
489
  message: closed.message,
490
+ ...ownedWorkItemFailureContext(s, context.sessionId, workItemId, closed.errorCode),
495
491
  };
496
492
  }
497
493
  return {
@@ -504,46 +500,33 @@ export function createWorkItemCloseTool(store) {
504
500
  closedAt: closed.record.closedAt,
505
501
  };
506
502
  },
507
- };
503
+ });
508
504
  }
509
505
  // START_CONTRACT: createWorkItemDecideTool
510
- // PURPOSE: Build work_item_decide handler wrapping explicit controller acceptance, change requests, checkpoint-authorized rework, and bounded recovery.
506
+ // PURPOSE: Build work_item_decide handler wrapping explicit controller acceptance, change requests, checkpoint-authorized rework, and bounded recovery under branch-aware input validation.
511
507
  // INPUTS: { store: WorkItemStore - workflow in-memory store, options?: DelegatedControlOptions - optional read-only authorization lookup }
512
508
  // OUTPUTS: { WorkflowToolDefinition<DecideArgs, Promise<Record<string, unknown>>> - async executable control tool definition }
513
- // SIDE_EFFECTS: [Mutates delegated work-item state through the domain layer]
514
- // LINKS: [M-WORKFLOW-TOOLING, M-WORKFLOW-DELEGATED, M-WORKFLOW-CHECKPOINTS]
509
+ // SIDE_EFFECTS: [Mutates delegated work-item state through the domain layer only after structural validation]
510
+ // LINKS: [M-WORKFLOW-TOOLING, M-WORKFLOW-DELEGATED, M-WORKFLOW-CHECKPOINTS, validateWorkflowToolInput]
515
511
  // END_CONTRACT: createWorkItemDecideTool
516
512
  export function createWorkItemDecideTool(store, options) {
517
- return {
513
+ return withFinalizedAsyncResult({
518
514
  name: "work_item_decide",
519
- description: "Accept or request changes for the current completed delegated attempt, authorize bounded rework of an accepted task from a failed checkpoint, or recover a stopped or exhausted unaccepted task with a bounded diagnosis and changed condition.",
515
+ description: WORKFLOW_TOOL_DESCRIPTIONS.work_item_decide,
520
516
  async execute(args, context, overrideStore) {
521
- const s = overrideStore ?? store;
522
- const workItemId = coerceNonEmptyString(args.workItemId);
523
- const decision = args.decision;
524
- const rationale = typeof args.rationale === "string" ? args.rationale : "";
525
- const evidence = Array.isArray(args.evidence) ? args.evidence.map(String) : [];
526
- if (!workItemId || typeof args.attempt !== "number" || !Number.isInteger(args.attempt)) {
527
- return {
528
- tool: "work_item_decide",
529
- sessionId: context.sessionId,
530
- ok: false,
531
- errorCode: "INVALID_INPUT",
532
- message: "INVALID_INPUT: workItemId and a positive integer attempt are required",
533
- };
517
+ const validation = validateWorkflowToolInput("work_item_decide", args);
518
+ if (!validation.ok) {
519
+ return invalidInput("work_item_decide", context.sessionId, validation.issues);
534
520
  }
521
+ const parsed = validation.data;
522
+ const s = overrideStore ?? store;
523
+ const workItemId = parsed.workItemId.trim();
524
+ const decision = parsed.decision;
525
+ const rationale = parsed.rationale ?? "";
526
+ const evidence = [...(parsed.evidence ?? [])];
535
527
  if (decision === "rework") {
536
- const runId = coerceNonEmptyString(args.runId);
537
- const checkpointId = coerceNonEmptyString(args.checkpointId);
538
- if (!runId || !checkpointId) {
539
- return {
540
- tool: "work_item_decide",
541
- sessionId: context.sessionId,
542
- ok: false,
543
- errorCode: "INVALID_INPUT",
544
- message: "INVALID_INPUT: rework requires runId and checkpointId of the failed checkpoint",
545
- };
546
- }
528
+ const runId = parsed.runId.trim();
529
+ const checkpointId = parsed.checkpointId.trim();
547
530
  const reworked = authorizeReworkFromFailedCheckpoint(s, {
548
531
  sessionId: context.sessionId,
549
532
  workItemId,
@@ -558,6 +541,7 @@ export function createWorkItemDecideTool(store, options) {
558
541
  ok: false,
559
542
  errorCode: reworked.errorCode,
560
543
  message: reworked.message,
544
+ ...ownedWorkItemFailureContext(s, context.sessionId, workItemId, reworked.errorCode),
561
545
  };
562
546
  }
563
547
  return {
@@ -572,25 +556,16 @@ export function createWorkItemDecideTool(store, options) {
572
556
  };
573
557
  }
574
558
  if (decision === "recover") {
575
- const recoveryId = coerceNonEmptyString(args.recoveryId);
576
- const diagnosis = typeof args.diagnosis === "string" ? args.diagnosis : "";
577
- const changedCondition = typeof args.changedCondition === "string" ? args.changedCondition : "";
578
- const verification = Array.isArray(args.verification) ? args.verification.map(String) : [];
579
- const userMessageId = coerceNonEmptyString(args.userMessageId);
580
- const authorityId = coerceNonEmptyString(args.authorityId);
581
- if (!recoveryId) {
582
- return {
583
- tool: "work_item_decide",
584
- sessionId: context.sessionId,
585
- ok: false,
586
- errorCode: "INVALID_INPUT",
587
- message: "INVALID_INPUT: recover requires a stable recoveryId, diagnosis, changedCondition, and verification references",
588
- };
589
- }
559
+ const recoveryId = parsed.recoveryId.trim();
560
+ const diagnosis = parsed.diagnosis;
561
+ const changedCondition = parsed.changedCondition;
562
+ const verification = [...(parsed.verification ?? [])];
563
+ const userMessageId = parsed.userMessageId?.trim();
564
+ const authorityId = parsed.authorityId?.trim();
590
565
  let advanceGrantApproved = false;
591
566
  let advanceRunId;
592
567
  if (authorityId) {
593
- advanceRunId = coerceNonEmptyString(args.runId);
568
+ advanceRunId = parsed.runId?.trim();
594
569
  if (!advanceRunId) {
595
570
  return {
596
571
  tool: "work_item_decide",
@@ -637,7 +612,7 @@ export function createWorkItemDecideTool(store, options) {
637
612
  const recovered = await recoverDelegatedWorkItem(s, {
638
613
  sessionId: context.sessionId,
639
614
  workItemId,
640
- attempt: args.attempt,
615
+ attempt: parsed.attempt,
641
616
  diagnosis,
642
617
  changedCondition,
643
618
  verification,
@@ -654,6 +629,7 @@ export function createWorkItemDecideTool(store, options) {
654
629
  ok: false,
655
630
  errorCode: recovered.errorCode,
656
631
  message: recovered.message,
632
+ ...ownedWorkItemFailureContext(s, context.sessionId, workItemId, recovered.errorCode),
657
633
  };
658
634
  }
659
635
  if (recovered.kind === "advance_grant" && advanceRunId !== undefined) {
@@ -675,6 +651,7 @@ export function createWorkItemDecideTool(store, options) {
675
651
  ok: false,
676
652
  errorCode: proposed?.code ?? "RESERVE_EXHAUSTED",
677
653
  message: proposed?.message ?? "advance recovery debit could not be recorded with the grant",
654
+ ...ownedWorkItemFailureContext(s, context.sessionId, workItemId, proposed?.code ?? "RESERVE_EXHAUSTED"),
678
655
  };
679
656
  }
680
657
  const stored = addReserveDebitInStore(s.getStoreData(), {
@@ -689,6 +666,7 @@ export function createWorkItemDecideTool(store, options) {
689
666
  ok: false,
690
667
  errorCode: stored.errorCode,
691
668
  message: stored.message,
669
+ ...ownedWorkItemFailureContext(s, context.sessionId, workItemId, stored.errorCode),
692
670
  };
693
671
  }
694
672
  }
@@ -703,23 +681,14 @@ export function createWorkItemDecideTool(store, options) {
703
681
  attemptBudget: recovered.attemptBudget,
704
682
  remainingAttempts: recovered.remainingAttempts,
705
683
  state: recovered.record.state,
706
- nextAction: summarizeDelegatedProgress(recovered.record).nextAction,
707
- };
708
- }
709
- if (decision !== "accept" && decision !== "request_changes") {
710
- return {
711
- tool: "work_item_decide",
712
- sessionId: context.sessionId,
713
- ok: false,
714
- errorCode: "INVALID_INPUT",
715
- message: "INVALID_INPUT: decision must be accept, request_changes, rework, or recover",
684
+ nextAction: delegatedGuidanceFor(s, context.sessionId, recovered.record).nextAction,
716
685
  };
717
686
  }
718
- const concernsDisposition = typeof args.concernsDisposition === "string" ? args.concernsDisposition : undefined;
687
+ const concernsDisposition = parsed.concernsDisposition;
719
688
  const decided = decideDelegatedWorkItem(s, {
720
689
  sessionId: context.sessionId,
721
690
  workItemId,
722
- attempt: args.attempt,
691
+ attempt: parsed.attempt,
723
692
  decision,
724
693
  rationale,
725
694
  evidence,
@@ -732,6 +701,7 @@ export function createWorkItemDecideTool(store, options) {
732
701
  ok: false,
733
702
  errorCode: decided.errorCode,
734
703
  message: decided.message,
704
+ ...ownedWorkItemFailureContext(s, context.sessionId, workItemId, decided.errorCode),
735
705
  };
736
706
  }
737
707
  return {
@@ -740,12 +710,12 @@ export function createWorkItemDecideTool(store, options) {
740
710
  ok: true,
741
711
  action: decision,
742
712
  workItemId,
743
- attempt: args.attempt,
713
+ attempt: parsed.attempt,
744
714
  decisionId: decided.decisionId,
745
715
  state: decided.record.state,
746
716
  };
747
717
  },
748
- };
718
+ });
749
719
  }
750
720
  // START_CONTRACT: executeGenericCheckpoint
751
721
  // PURPOSE: Execute one generic (non-native) checkpoint/authority action against the common execution registry.
@@ -774,31 +744,34 @@ async function executeGenericCheckpoint(options) {
774
744
  message: "execution belongs to another session",
775
745
  };
776
746
  }
777
- const amendmentId = coerceNonEmptyString(args.amendmentId);
778
- const rationale = coerceNonEmptyString(args.rationale);
747
+ const amendmentId = args.amendmentId?.trim();
748
+ const rationale = args.rationale?.trim();
779
749
  switch (args.action) {
780
750
  case "register":
781
751
  case "amend": {
782
- if (!amendmentId || !rationale) {
752
+ // Branch validation guarantees runId, amendmentId, and rationale for
753
+ // generic register/amend; tasks/checkpoints arrive as typed closed shapes.
754
+ const items = args.tasks ?? [];
755
+ const contracts = items.length > 0 ? taskContractsFromItems(items) : { ok: true, contracts: [] };
756
+ if (!contracts.ok) {
757
+ return { ...base, ok: false, errorCode: "INVALID_INPUT", message: contracts.message };
758
+ }
759
+ const checkpointContracts = normalizeCheckpointInputContracts(args.checkpoints);
760
+ if (!checkpointContracts.ok) {
783
761
  return {
784
762
  ...base,
785
763
  ok: false,
786
764
  errorCode: "INVALID_INPUT",
787
- message: "generic register/amend requires amendmentId and rationale",
765
+ message: `INVALID_INPUT: ${checkpointContracts.message}`,
788
766
  };
789
767
  }
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
768
  const appended = appendExecutionWorkInStore(data, {
796
769
  sessionId,
797
770
  runId,
798
- amendmentId,
799
- rationale,
771
+ amendmentId: amendmentId,
772
+ rationale: rationale,
800
773
  tasks: contracts.contracts.map((contract) => ({ contract })),
801
- checkpoints: normalizeCheckpointContracts(args.checkpoints),
774
+ checkpoints: checkpointContracts.contracts,
802
775
  });
803
776
  if (!appended.ok) {
804
777
  return { ...base, ok: false, errorCode: appended.errorCode, message: appended.message };
@@ -808,20 +781,12 @@ async function executeGenericCheckpoint(options) {
808
781
  ok: true,
809
782
  action: "amend",
810
783
  revision: appended.revision,
811
- execution: getExecutionView(appended.execution),
784
+ execution: getExecutionView(appended.execution, data),
812
785
  };
813
786
  }
814
787
  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);
788
+ const checkpointId = args.checkpointId.trim();
789
+ const startFingerprint = args.startFingerprint?.trim();
825
790
  const started = startGenericCheckpointInStore(data, {
826
791
  sessionId,
827
792
  runId,
@@ -846,24 +811,8 @@ async function executeGenericCheckpoint(options) {
846
811
  case "review":
847
812
  case "bind":
848
813
  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
- }
814
+ const checkpointId = args.checkpointId.trim();
815
+ const reviewer = args.reviewer;
867
816
  // The reviewer status is read from the linked review_only work item's
868
817
  // recorded round; callers cannot assert a reviewer outcome directly.
869
818
  const recorded = recordGenericReviewerResultInStore(data, {
@@ -886,20 +835,12 @@ async function executeGenericCheckpoint(options) {
886
835
  };
887
836
  }
888
837
  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
- }
838
+ const checkpointId = args.checkpointId.trim();
839
+ const recoveryId = args.recoveryId.trim();
840
+ const diagnosis = args.diagnosis;
841
+ const changedCondition = args.changedCondition;
842
+ const verification = [...(args.verification ?? [])];
843
+ const authorityId = args.authorityId?.trim();
903
844
  const checkpointBinding = execution.checkpoints.get(checkpointId);
904
845
  const stopped = checkpointBinding?.stoppedAtGeneration !== undefined;
905
846
  // Validate and reserve the advance unit BEFORE mutating the checkpoint,
@@ -972,8 +913,8 @@ async function executeGenericCheckpoint(options) {
972
913
  const completed = completeExecutionInStore(data, {
973
914
  sessionId,
974
915
  runId,
975
- rationale: rationale ?? "Generic execution completed with controller acceptance.",
976
- evidence: stringList(args.verification),
916
+ rationale: rationale || "Generic execution completed with controller acceptance.",
917
+ evidence: [...(args.verification ?? [])],
977
918
  });
978
919
  if (!completed.ok) {
979
920
  return { ...base, ok: false, errorCode: completed.errorCode, message: completed.message };
@@ -983,17 +924,13 @@ async function executeGenericCheckpoint(options) {
983
924
  ok: true,
984
925
  action: "complete",
985
926
  reviewStatus: completed.reviewStatus,
986
- execution: getExecutionView(completed.execution),
927
+ execution: getExecutionView(completed.execution, data),
987
928
  };
988
929
  }
989
930
  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");
931
+ const authorityId = args.authorityId.trim();
932
+ const messageId = args.messageId.trim();
933
+ const stages = [...(args.stages ?? [])];
997
934
  if (!authorityId || !messageId || stages.length === 0) {
998
935
  return {
999
936
  ...base,
@@ -1002,14 +939,15 @@ async function executeGenericCheckpoint(options) {
1002
939
  message: "authorize requires authorityId, messageId, and explicit stages",
1003
940
  };
1004
941
  }
942
+ const suppliedScope = {
943
+ stages,
944
+ decisionScope: (args.decisionScope ?? "").trim(),
945
+ fileBoundary: [...(args.fileBoundary ?? [])],
946
+ reservedStops: [...(args.reservedStops ?? [])],
947
+ };
1005
948
  const lookup = options.control?.lookupAuthorityMessage;
1006
949
  if (!lookup) {
1007
- return {
1008
- ...base,
1009
- ok: false,
1010
- errorCode: "INVALID_INPUT",
1011
- message: "authorize requires the SDK-backed authorization lookup",
1012
- };
950
+ return hostContextInvalid("work_checkpoint", sessionId, "authorize requires the SDK-backed authorization lookup from the plugin context");
1013
951
  }
1014
952
  const message = await lookup({ sessionId, runId, messageId });
1015
953
  if (!message) {
@@ -1022,6 +960,17 @@ async function executeGenericCheckpoint(options) {
1022
960
  }
1023
961
  const existingAuthority = execution.authority.find((entry) => entry.authorityId === authorityId);
1024
962
  if (existingAuthority) {
963
+ // Idempotent reuse and finite extension never silently accept a
964
+ // supplied scope that contradicts the recorded scope.
965
+ const scopeDifferences = authorityScopeDifferences(suppliedScope, existingAuthority.scope);
966
+ if (scopeDifferences.length > 0) {
967
+ return {
968
+ ...base,
969
+ ok: false,
970
+ errorCode: "INVALID_INPUT",
971
+ message: `INVALID_INPUT: supplied scope differs from the recorded scope for authority ${authorityId} (${scopeDifferences.join(", ")}); scope changes are unsupported on replay or extension`,
972
+ };
973
+ }
1025
974
  if (existingAuthority.grantedByMessageId === messageId) {
1026
975
  // Idempotent replay of the originating registration.
1027
976
  return {
@@ -1071,15 +1020,7 @@ async function executeGenericCheckpoint(options) {
1071
1020
  runId,
1072
1021
  sessionId,
1073
1022
  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
- },
1023
+ scope: suppliedScope,
1083
1024
  existingAuthorities: execution.authority,
1084
1025
  messageClaims: data.messageClaims,
1085
1026
  });
@@ -1105,11 +1046,11 @@ async function executeGenericCheckpoint(options) {
1105
1046
  };
1106
1047
  }
1107
1048
  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);
1049
+ const authorityId = args.authorityId.trim();
1050
+ const approvalId = args.approvalId.trim();
1051
+ const stage = args.stage;
1052
+ const artifactPath = args.artifactPath.trim();
1053
+ const artifactSha256 = args.artifactSha256.trim();
1113
1054
  const authority = execution.authority.find((entry) => entry.authorityId === authorityId);
1114
1055
  if (!authority || !approvalId || !stage || !artifactPath || !artifactSha256) {
1115
1056
  return {
@@ -1148,8 +1089,8 @@ async function executeGenericCheckpoint(options) {
1148
1089
  };
1149
1090
  }
1150
1091
  case "revoke_authority": {
1151
- const authorityId = coerceNonEmptyString(args.authorityId);
1152
- const revocationId = coerceNonEmptyString(args.revocationId);
1092
+ const authorityId = args.authorityId.trim();
1093
+ const revocationId = args.revocationId.trim();
1153
1094
  const authority = execution.authority.find((entry) => entry.authorityId === authorityId);
1154
1095
  if (!authority || !revocationId) {
1155
1096
  return {
@@ -1159,21 +1100,21 @@ async function executeGenericCheckpoint(options) {
1159
1100
  message: "revoke_authority requires a recorded authorityId and a revocationId",
1160
1101
  };
1161
1102
  }
1162
- const stagesArg = Array.isArray(args.stages) ? args.stages : undefined;
1163
- const revoked = stagesArg === undefined || stagesArg.length === 0
1103
+ // Supplied stages were validated as canonical values before any lookup;
1104
+ // absent or empty stages keep the documented full-revocation default.
1105
+ const narrowedStages = args.stages;
1106
+ const reason = args.rationale?.trim();
1107
+ const revoked = narrowedStages === undefined || narrowedStages.length === 0
1164
1108
  ? revokeAdvanceAuthority({
1165
1109
  authority,
1166
1110
  revocationId,
1167
- reason: coerceNonEmptyString(args.rationale) ?? "revoked",
1111
+ reason: reason || "revoked",
1168
1112
  })
1169
1113
  : narrowAdvanceAuthority({
1170
1114
  authority,
1171
1115
  revocationId,
1172
- reason: coerceNonEmptyString(args.rationale) ?? "narrowed",
1173
- narrowedStages: stagesArg.filter((stage) => stage === "specification" ||
1174
- stage === "planning" ||
1175
- stage === "implementation" ||
1176
- stage === "verification"),
1116
+ reason: reason || "narrowed",
1117
+ narrowedStages: [...narrowedStages],
1177
1118
  });
1178
1119
  if (!revoked.ok) {
1179
1120
  return { ...base, ok: false, errorCode: revoked.code, message: revoked.message };
@@ -1206,40 +1147,42 @@ async function executeGenericCheckpoint(options) {
1206
1147
  }
1207
1148
  }
1208
1149
  // START_CONTRACT: createWorkCheckpointTool
1209
- // PURPOSE: Build work_checkpoint handler wrapping plan registration, checkpoint start, fingerprint-verified outcomes, and bounded checkpoint recovery.
1150
+ // PURPOSE: Build work_checkpoint handler wrapping plan registration, checkpoint start, fingerprint-verified outcomes, and bounded checkpoint recovery under branch-aware input validation with source-resolved run routing.
1210
1151
  // INPUTS: { store: WorkItemStore - workflow in-memory store, options?: DelegatedControlOptions - optional read-only authorization lookup }
1211
1152
  // OUTPUTS: { WorkflowToolDefinition<CheckpointArgs, Promise<Record<string, unknown>>> - async executable control tool definition }
1212
- // SIDE_EFFECTS: [Registers plan runs and mutates checkpoint state through the domain layer]
1213
- // LINKS: [M-WORKFLOW-TOOLING, M-WORKFLOW-CHECKPOINTS, M-WORKFLOW-DELEGATED]
1153
+ // SIDE_EFFECTS: [Registers plan runs and mutates checkpoint state through the domain layer only after structural validation]
1154
+ // LINKS: [M-WORKFLOW-TOOLING, M-WORKFLOW-CHECKPOINTS, M-WORKFLOW-DELEGATED, validateWorkflowToolInput]
1214
1155
  // END_CONTRACT: createWorkCheckpointTool
1215
1156
  export function createWorkCheckpointTool(store, options) {
1216
- return {
1157
+ return withFinalizedAsyncResult({
1217
1158
  name: "work_checkpoint",
1218
- description: "Register an approved delegated plan, start a declared review checkpoint, verify checkpoint outcomes, or recover a stopped or generation-exhausted checkpoint; verify with complete: true seals a finished final checkpoint.",
1159
+ description: WORKFLOW_TOOL_DESCRIPTIONS.work_checkpoint,
1219
1160
  async execute(args, context, overrideStore) {
1161
+ const validation = validateWorkflowToolInput("work_checkpoint", args);
1162
+ if (!validation.ok) {
1163
+ return invalidInput("work_checkpoint", context.sessionId, validation.issues);
1164
+ }
1165
+ const parsed = validation.data;
1220
1166
  const s = overrideStore ?? store;
1221
- const action = args.action;
1222
- if (action === "register" && coerceNonEmptyString(args.planPath)) {
1223
- const planPath = coerceNonEmptyString(args.planPath);
1167
+ const action = parsed.action;
1168
+ if (action === "register" && parsed.planPath !== undefined) {
1169
+ const planPath = parsed.planPath.trim();
1224
1170
  const workspaceRoot = coerceNonEmptyString(context.workspaceRoot);
1225
- if (!planPath || !workspaceRoot) {
1171
+ if (!workspaceRoot) {
1172
+ return hostContextInvalid("work_checkpoint", context.sessionId, "register requires the trusted workspace root from the plugin context");
1173
+ }
1174
+ if (!planPath) {
1226
1175
  return {
1227
1176
  tool: "work_checkpoint",
1228
1177
  sessionId: context.sessionId,
1229
1178
  ok: false,
1230
1179
  errorCode: "INVALID_INPUT",
1231
- message: "INVALID_INPUT: register requires a planPath and the trusted workspace root",
1180
+ message: "INVALID_INPUT: planPath must be a non-empty string",
1232
1181
  };
1233
1182
  }
1234
1183
  const load = context.loadPlan;
1235
1184
  if (typeof load !== "function") {
1236
- return {
1237
- tool: "work_checkpoint",
1238
- sessionId: context.sessionId,
1239
- ok: false,
1240
- errorCode: "INVALID_INPUT",
1241
- message: "INVALID_INPUT: register requires a plan loader bound to the plugin context",
1242
- };
1185
+ return hostContextInvalid("work_checkpoint", context.sessionId, "register requires a plan loader bound to the plugin context");
1243
1186
  }
1244
1187
  const loaded = await load(planPath, workspaceRoot);
1245
1188
  if ("loadError" in loaded) {
@@ -1275,25 +1218,108 @@ export function createWorkCheckpointTool(store, options) {
1275
1218
  checkpoints: registered.run.checkpoints.size,
1276
1219
  };
1277
1220
  }
1278
- const genericRunId = coerceNonEmptyString(args.runId);
1279
- if (genericRunId) {
1221
+ const runId = parsed.runId?.trim();
1222
+ if (runId) {
1280
1223
  const data = s.getStoreData();
1281
- const execution = findExecution(data, genericRunId);
1224
+ const execution = findExecution(data, runId);
1225
+ const planRun = data.planRuns.get(runId);
1226
+ // Resolve session ownership before any source-specific diagnostic or
1227
+ // generic/native action discrimination. Both the common execution
1228
+ // registry and the legacy/native planRun fallback are guarded, so a
1229
+ // foreign caller cannot infer the run's source (or which fields an
1230
+ // action consumes) from which error it receives.
1231
+ const knownOwnerSessionId = execution?.sessionId ?? planRun?.sessionId;
1232
+ if (knownOwnerSessionId !== undefined && knownOwnerSessionId !== context.sessionId) {
1233
+ return {
1234
+ tool: "work_checkpoint",
1235
+ sessionId: context.sessionId,
1236
+ ok: false,
1237
+ errorCode: "SESSION_MISMATCH",
1238
+ message: "run belongs to another session",
1239
+ };
1240
+ }
1282
1241
  // Authority actions apply to any execution, including native runs;
1283
- // other generic actions stay on non-native executions.
1242
+ // generic-only actions stay on non-native executions.
1284
1243
  const authorityAction = action === "authorize" || action === "record_approval" || action === "revoke_authority";
1285
- if (execution && (execution.source.kind !== "native-package" || authorityAction)) {
1244
+ const genericOnlyAction = action === "register" ||
1245
+ action === "amend" ||
1246
+ action === "review" ||
1247
+ action === "bind" ||
1248
+ action === "complete";
1249
+ const nativeRun = execution?.source.kind === "native-package" ||
1250
+ (execution === undefined && planRun !== undefined);
1251
+ if (genericOnlyAction && nativeRun) {
1252
+ return {
1253
+ tool: "work_checkpoint",
1254
+ sessionId: context.sessionId,
1255
+ ok: false,
1256
+ errorCode: "INVALID_INPUT",
1257
+ message: `INVALID_INPUT: action ${action} is not supported for native-package run ${runId}`,
1258
+ };
1259
+ }
1260
+ // Source-dependent known fields: only reject them for an owned run whose
1261
+ // source is known, so an unknown run stays a lookup failure rather than
1262
+ // a fabricated native-argument error.
1263
+ if (nativeRun) {
1264
+ if (action === "start" && parsed.startFingerprint !== undefined) {
1265
+ return invalidInput("work_checkpoint", context.sessionId, [
1266
+ {
1267
+ code: "invalid_value",
1268
+ path: "startFingerprint",
1269
+ message: "startFingerprint is only consumed by a generic checkpoint start",
1270
+ expected: "omit startFingerprint for a native-package run",
1271
+ },
1272
+ ]);
1273
+ }
1274
+ if (action === "verify" && parsed.reviewer !== undefined) {
1275
+ return invalidInput("work_checkpoint", context.sessionId, [
1276
+ {
1277
+ code: "invalid_value",
1278
+ path: "reviewer",
1279
+ message: "reviewer is only consumed by generic review/bind/verify; a native verify seals through complete",
1280
+ expected: "omit reviewer for a native-package run",
1281
+ },
1282
+ ]);
1283
+ }
1284
+ }
1285
+ else if (execution !== undefined) {
1286
+ if (action === "verify" && parsed.complete !== undefined) {
1287
+ return invalidInput("work_checkpoint", context.sessionId, [
1288
+ {
1289
+ code: "invalid_value",
1290
+ path: "complete",
1291
+ message: "complete only seals a native-package final checkpoint; generic verify records linked reviewer outcomes",
1292
+ expected: "omit complete for a generic execution",
1293
+ },
1294
+ ]);
1295
+ }
1296
+ if (action === "recover" && parsed.userMessageId !== undefined) {
1297
+ return invalidInput("work_checkpoint", context.sessionId, [
1298
+ {
1299
+ code: "invalid_value",
1300
+ path: "userMessageId",
1301
+ message: "generic checkpoint recovery is authorized by an advance authority; a root-user message is only consumed by native-package checkpoint recovery",
1302
+ expected: "authorityId for generic checkpoint recovery",
1303
+ },
1304
+ ]);
1305
+ }
1306
+ }
1307
+ // Resolve the known run source before source-specific checks. Unknown
1308
+ // runs reach executeGenericCheckpoint (or the native domain below) and
1309
+ // are reported as lookup failures, never as missing native-only fields.
1310
+ if (authorityAction ||
1311
+ genericOnlyAction ||
1312
+ (execution !== undefined && execution.source.kind !== "native-package")) {
1286
1313
  return executeGenericCheckpoint({
1287
1314
  data,
1288
1315
  sessionId: context.sessionId,
1289
- runId: genericRunId,
1290
- args,
1316
+ runId,
1317
+ args: parsed,
1291
1318
  ...(options ? { control: options } : {}),
1292
1319
  });
1293
1320
  }
1294
1321
  }
1295
- const runId = coerceNonEmptyString(args.runId);
1296
- const checkpointId = coerceNonEmptyString(args.checkpointId);
1322
+ const checkpointId = parsed.checkpointId?.trim();
1297
1323
  if (!runId || !checkpointId) {
1298
1324
  return {
1299
1325
  tool: "work_checkpoint",
@@ -1336,7 +1362,7 @@ export function createWorkCheckpointTool(store, options) {
1336
1362
  sessionId: context.sessionId,
1337
1363
  runId,
1338
1364
  checkpointId,
1339
- ...(args.complete === true ? { complete: true } : {}),
1365
+ ...(parsed.complete === true ? { complete: true } : {}),
1340
1366
  });
1341
1367
  if (!verified.ok) {
1342
1368
  return {
@@ -1360,21 +1386,12 @@ export function createWorkCheckpointTool(store, options) {
1360
1386
  };
1361
1387
  }
1362
1388
  if (action === "recover") {
1363
- const recoveryId = coerceNonEmptyString(args.recoveryId);
1364
- const diagnosis = typeof args.diagnosis === "string" ? args.diagnosis : "";
1365
- const changedCondition = typeof args.changedCondition === "string" ? args.changedCondition : "";
1366
- const verification = Array.isArray(args.verification) ? args.verification.map(String) : [];
1367
- const userMessageId = coerceNonEmptyString(args.userMessageId);
1368
- if (!recoveryId) {
1369
- return {
1370
- tool: "work_checkpoint",
1371
- sessionId: context.sessionId,
1372
- ok: false,
1373
- errorCode: "INVALID_INPUT",
1374
- message: "INVALID_INPUT: recover requires a stable recoveryId, diagnosis, changedCondition, and verification references",
1375
- };
1376
- }
1377
- const authorityId = coerceNonEmptyString(args.authorityId);
1389
+ const recoveryId = parsed.recoveryId.trim();
1390
+ const diagnosis = parsed.diagnosis;
1391
+ const changedCondition = parsed.changedCondition;
1392
+ const verification = [...(parsed.verification ?? [])];
1393
+ const userMessageId = parsed.userMessageId?.trim();
1394
+ const authorityId = parsed.authorityId?.trim();
1378
1395
  let advanceGrantApproved = false;
1379
1396
  if (authorityId) {
1380
1397
  const execution = findExecution(s.getStoreData(), runId);
@@ -1482,6 +1499,6 @@ export function createWorkCheckpointTool(store, options) {
1482
1499
  message: "INVALID_INPUT: action must be register, start, verify, or recover",
1483
1500
  };
1484
1501
  },
1485
- };
1502
+ });
1486
1503
  }
1487
1504
  //# sourceMappingURL=tooling.js.map