@osovv/vv-opencode 1.5.0 → 1.6.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.
- package/CHANGELOG.md +9 -0
- package/README.md +39 -8
- package/dist/lib/agent-tool-catalog.d.ts +144 -0
- package/dist/lib/agent-tool-catalog.js +2551 -0
- package/dist/lib/agent-tool-catalog.js.map +1 -0
- package/dist/lib/agent-tool-contract.d.ts +200 -0
- package/dist/lib/agent-tool-contract.js +407 -0
- package/dist/lib/agent-tool-contract.js.map +1 -0
- package/dist/lib/workflow-contract.d.ts +12 -0
- package/dist/lib/workflow-contract.js +71 -18
- package/dist/lib/workflow-contract.js.map +1 -1
- package/dist/plugins/hashline-edit/index.js +102 -97
- package/dist/plugins/hashline-edit/index.js.map +1 -1
- package/dist/plugins/hashline-edit/normalize-edits.d.ts +2 -8
- package/dist/plugins/hashline-edit/normalize-edits.js +32 -17
- package/dist/plugins/hashline-edit/normalize-edits.js.map +1 -1
- package/dist/plugins/hashline-edit/schemas.d.ts +246 -0
- package/dist/plugins/hashline-edit/schemas.js +659 -0
- package/dist/plugins/hashline-edit/schemas.js.map +1 -0
- package/dist/plugins/hashline-edit/str-replace-editor.d.ts +6 -17
- package/dist/plugins/hashline-edit/str-replace-editor.js +19 -21
- package/dist/plugins/hashline-edit/str-replace-editor.js.map +1 -1
- package/dist/plugins/hashline-edit/tool-description.d.ts +1 -0
- package/dist/plugins/hashline-edit/tool-description.js +20 -4
- package/dist/plugins/hashline-edit/tool-description.js.map +1 -1
- package/dist/plugins/web-tools/fetch-service.d.ts +8 -5
- package/dist/plugins/web-tools/fetch-service.js +30 -45
- package/dist/plugins/web-tools/fetch-service.js.map +1 -1
- package/dist/plugins/web-tools/index.js +23 -5
- package/dist/plugins/web-tools/index.js.map +1 -1
- package/dist/plugins/web-tools/schemas.d.ts +469 -0
- package/dist/plugins/web-tools/schemas.js +523 -0
- package/dist/plugins/web-tools/schemas.js.map +1 -0
- package/dist/plugins/web-tools/search-service.d.ts +6 -3
- package/dist/plugins/web-tools/search-service.js +24 -29
- package/dist/plugins/web-tools/search-service.js.map +1 -1
- package/dist/plugins/workflow/authority.d.ts +6 -0
- package/dist/plugins/workflow/authority.js +45 -10
- package/dist/plugins/workflow/authority.js.map +1 -1
- package/dist/plugins/workflow/authorization.js +16 -8
- package/dist/plugins/workflow/authorization.js.map +1 -1
- package/dist/plugins/workflow/checkpoints.d.ts +41 -2
- package/dist/plugins/workflow/checkpoints.js +176 -47
- package/dist/plugins/workflow/checkpoints.js.map +1 -1
- package/dist/plugins/workflow/delegated.d.ts +15 -1
- package/dist/plugins/workflow/delegated.js +65 -35
- package/dist/plugins/workflow/delegated.js.map +1 -1
- package/dist/plugins/workflow/execution.d.ts +15 -30
- package/dist/plugins/workflow/execution.js +133 -33
- package/dist/plugins/workflow/execution.js.map +1 -1
- package/dist/plugins/workflow/index.js +177 -112
- package/dist/plugins/workflow/index.js.map +1 -1
- package/dist/plugins/workflow/input-validation.d.ts +48 -0
- package/dist/plugins/workflow/input-validation.js +536 -0
- package/dist/plugins/workflow/input-validation.js.map +1 -0
- package/dist/plugins/workflow/inspection.d.ts +60 -0
- package/dist/plugins/workflow/inspection.js +316 -0
- package/dist/plugins/workflow/inspection.js.map +1 -0
- package/dist/plugins/workflow/protocol.d.ts +2 -0
- package/dist/plugins/workflow/protocol.js +26 -4
- package/dist/plugins/workflow/protocol.js.map +1 -1
- package/dist/plugins/workflow/recovery.d.ts +2 -1
- package/dist/plugins/workflow/recovery.js +79 -12
- package/dist/plugins/workflow/recovery.js.map +1 -1
- package/dist/plugins/workflow/repair.d.ts +1 -1
- package/dist/plugins/workflow/repair.js +9 -10
- package/dist/plugins/workflow/repair.js.map +1 -1
- package/dist/plugins/workflow/results.d.ts +2566 -0
- package/dist/plugins/workflow/results.js +1111 -0
- package/dist/plugins/workflow/results.js.map +1 -0
- package/dist/plugins/workflow/schemas.d.ts +214 -24
- package/dist/plugins/workflow/schemas.js +350 -89
- package/dist/plugins/workflow/schemas.js.map +1 -1
- package/dist/plugins/workflow/state.d.ts +3 -2
- package/dist/plugins/workflow/state.js +15 -8
- package/dist/plugins/workflow/state.js.map +1 -1
- package/dist/plugins/workflow/system-instruction.md +17 -1
- package/dist/plugins/workflow/tooling.d.ts +2 -18
- package/dist/plugins/workflow/tooling.js +477 -460
- package/dist/plugins/workflow/tooling.js.map +1 -1
- package/dist/plugins/workflow/transactions.d.ts +13 -0
- package/dist/plugins/workflow/transactions.js +13 -3
- package/dist/plugins/workflow/transactions.js.map +1 -1
- package/package.json +6 -3
- package/schemas/vvoc/v3.json +1 -1
- package/templates/agents/vv-code-reviewer.md +13 -4
- package/templates/agents/vv-implementer.md +16 -9
- package/templates/agents/vv-spec-reviewer.md +13 -4
- package/templates/skills/vv-execute/SKILL.md +3 -3
- package/templates/skills/vv-execute/references/tool-contracts.md +1750 -0
- package/templates/skills/vv-review/SKILL.md +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// FILE: src/plugins/workflow/tooling.ts
|
|
2
|
-
// VERSION: 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
|
|
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
|
|
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-
|
|
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,
|
|
33
|
-
import { authorizeReworkFromFailedCheckpoint,
|
|
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
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
94
|
-
verification:
|
|
95
|
-
writeScope:
|
|
96
|
-
dependsOn:
|
|
97
|
-
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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 =
|
|
148
|
-
const 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
|
|
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 =
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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:
|
|
299
|
+
description: WORKFLOW_TOOL_DESCRIPTIONS.work_item_open,
|
|
290
300
|
execute: (args, context, overrideStore) => {
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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 (
|
|
317
|
-
const descriptor =
|
|
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 (!
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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,
|
|
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:
|
|
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 =
|
|
448
|
-
|
|
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:
|
|
474
|
+
description: WORKFLOW_TOOL_DESCRIPTIONS.work_item_close,
|
|
475
475
|
execute: (args, context, overrideStore) => {
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
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:
|
|
515
|
+
description: WORKFLOW_TOOL_DESCRIPTIONS.work_item_decide,
|
|
520
516
|
async execute(args, context, overrideStore) {
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
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 =
|
|
537
|
-
const 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 =
|
|
576
|
-
const diagnosis =
|
|
577
|
-
const changedCondition =
|
|
578
|
-
const verification =
|
|
579
|
-
const userMessageId =
|
|
580
|
-
const 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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
687
|
+
const concernsDisposition = parsed.concernsDisposition;
|
|
719
688
|
const decided = decideDelegatedWorkItem(s, {
|
|
720
689
|
sessionId: context.sessionId,
|
|
721
690
|
workItemId,
|
|
722
|
-
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:
|
|
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 =
|
|
778
|
-
const 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
|
-
|
|
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:
|
|
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:
|
|
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 =
|
|
816
|
-
|
|
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 =
|
|
850
|
-
|
|
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 =
|
|
890
|
-
const recoveryId =
|
|
891
|
-
const diagnosis =
|
|
892
|
-
const changedCondition =
|
|
893
|
-
const verification =
|
|
894
|
-
const 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
|
|
976
|
-
evidence:
|
|
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 =
|
|
991
|
-
const messageId =
|
|
992
|
-
const
|
|
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 =
|
|
1109
|
-
const approvalId =
|
|
1110
|
-
const stage =
|
|
1111
|
-
const artifactPath =
|
|
1112
|
-
const 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 =
|
|
1152
|
-
const 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
|
-
|
|
1163
|
-
|
|
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:
|
|
1111
|
+
reason: reason || "revoked",
|
|
1168
1112
|
})
|
|
1169
1113
|
: narrowAdvanceAuthority({
|
|
1170
1114
|
authority,
|
|
1171
1115
|
revocationId,
|
|
1172
|
-
reason:
|
|
1173
|
-
narrowedStages:
|
|
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:
|
|
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 =
|
|
1222
|
-
if (action === "register" &&
|
|
1223
|
-
const 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 (!
|
|
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:
|
|
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
|
|
1279
|
-
if (
|
|
1221
|
+
const runId = parsed.runId?.trim();
|
|
1222
|
+
if (runId) {
|
|
1280
1223
|
const data = s.getStoreData();
|
|
1281
|
-
const execution = findExecution(data,
|
|
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
|
-
//
|
|
1242
|
+
// generic-only actions stay on non-native executions.
|
|
1284
1243
|
const authorityAction = action === "authorize" || action === "record_approval" || action === "revoke_authority";
|
|
1285
|
-
|
|
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
|
|
1290
|
-
args,
|
|
1316
|
+
runId,
|
|
1317
|
+
args: parsed,
|
|
1291
1318
|
...(options ? { control: options } : {}),
|
|
1292
1319
|
});
|
|
1293
1320
|
}
|
|
1294
1321
|
}
|
|
1295
|
-
const
|
|
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
|
-
...(
|
|
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 =
|
|
1364
|
-
const diagnosis =
|
|
1365
|
-
const changedCondition =
|
|
1366
|
-
const verification =
|
|
1367
|
-
const userMessageId =
|
|
1368
|
-
|
|
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
|