@osovv/vv-opencode 1.4.5 → 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 +23 -0
- package/README.md +47 -16
- package/dist/commands/patch-provider.d.ts +5 -5
- package/dist/commands/patch-provider.js +10 -6
- package/dist/commands/patch-provider.js.map +1 -1
- 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/opencode/agent-registrations.d.ts +33 -0
- package/dist/lib/opencode/agent-registrations.js +469 -0
- package/dist/lib/opencode/agent-registrations.js.map +1 -0
- package/dist/lib/opencode/inspection.d.ts +84 -0
- package/dist/lib/opencode/inspection.js +367 -0
- package/dist/lib/opencode/inspection.js.map +1 -0
- package/dist/lib/opencode/model-overrides.d.ts +19 -0
- package/dist/lib/opencode/model-overrides.js +221 -0
- package/dist/lib/opencode/model-overrides.js.map +1 -0
- package/dist/lib/opencode/paths.d.ts +21 -0
- package/dist/lib/opencode/paths.js +62 -0
- package/dist/lib/opencode/paths.js.map +1 -0
- package/dist/lib/opencode/plugin-registration.d.ts +17 -0
- package/dist/lib/opencode/plugin-registration.js +210 -0
- package/dist/lib/opencode/plugin-registration.js.map +1 -0
- package/dist/lib/opencode/shared-utils.d.ts +30 -0
- package/dist/lib/opencode/shared-utils.js +183 -0
- package/dist/lib/opencode/shared-utils.js.map +1 -0
- package/dist/lib/opencode/vvoc-config-io.d.ts +9 -0
- package/dist/lib/opencode/vvoc-config-io.js +58 -0
- package/dist/lib/opencode/vvoc-config-io.js.map +1 -0
- package/dist/lib/opencode.d.ts +15 -174
- package/dist/lib/opencode.js +19 -1387
- package/dist/lib/opencode.js.map +1 -1
- package/dist/lib/spec-lint/cross-file-rules.d.ts +2 -0
- package/dist/lib/spec-lint/cross-file-rules.js +33 -0
- package/dist/lib/spec-lint/cross-file-rules.js.map +1 -0
- package/dist/lib/spec-lint/delegated-extraction.d.ts +8 -0
- package/dist/lib/spec-lint/delegated-extraction.js +112 -0
- package/dist/lib/spec-lint/delegated-extraction.js.map +1 -0
- package/dist/lib/spec-lint/lint.d.ts +7 -0
- package/dist/lib/spec-lint/lint.js +92 -0
- package/dist/lib/spec-lint/lint.js.map +1 -0
- package/dist/lib/spec-lint/parser.d.ts +101 -0
- package/dist/lib/spec-lint/parser.js +482 -0
- package/dist/lib/spec-lint/parser.js.map +1 -0
- package/dist/lib/spec-lint/plan-rules.d.ts +38 -0
- package/dist/lib/spec-lint/plan-rules.js +745 -0
- package/dist/lib/spec-lint/plan-rules.js.map +1 -0
- package/dist/lib/spec-lint/spec-rules.d.ts +8 -0
- package/dist/lib/spec-lint/spec-rules.js +188 -0
- package/dist/lib/spec-lint/spec-rules.js.map +1 -0
- package/dist/lib/spec-lint.d.ts +4 -63
- package/dist/lib/spec-lint.js +13 -1505
- package/dist/lib/spec-lint.js.map +1 -1
- package/dist/lib/vvoc-preset-registry.d.ts +7 -7
- package/dist/lib/vvoc-preset-registry.js +8 -8
- package/dist/lib/vvoc-preset-registry.js.map +1 -1
- 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.d.ts +30 -0
- package/dist/plugins/workflow/authorization.js +147 -0
- package/dist/plugins/workflow/authorization.js.map +1 -0
- 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 +197 -435
- 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 +18 -0
- package/dist/plugins/workflow/recovery.js +217 -0
- package/dist/plugins/workflow/recovery.js.map +1 -0
- 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 +398 -0
- package/dist/plugins/workflow/schemas.js +399 -0
- package/dist/plugins/workflow/schemas.js.map +1 -0
- 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 +11 -82
- package/dist/plugins/workflow/tooling.js +478 -461
- 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,42 +1,46 @@
|
|
|
1
1
|
// FILE: src/plugins/workflow/index.ts
|
|
2
|
-
// VERSION: 0.
|
|
2
|
+
// VERSION: 0.9.1
|
|
3
3
|
// START_MODULE_CONTRACT
|
|
4
|
-
// PURPOSE: Register workflow tools and enforcement while injecting only startup-profile-compatible vv-controller guidance, including
|
|
5
|
-
// SCOPE: work_item_open/list/close registration,
|
|
6
|
-
// DEPENDS: [@opencode-ai/plugin, src/lib/config-layers.ts, src/lib/orchestration.ts, src/lib/plugin-toggle-config.ts, src/plugins/workflow/checkpoint-io.ts, src/plugins/workflow/checkpoints.ts, src/plugins/workflow/delegated.ts, src/plugins/workflow/persistence.ts, src/plugins/workflow/protocol.ts, src/plugins/workflow/repair.ts, src/plugins/workflow/state.ts, src/plugins/workflow/tooling.ts, src/plugins/workflow/transitions.ts]
|
|
7
|
-
// LINKS: M-PLUGIN-WORKFLOW, M-ORCHESTRATION-PROFILES, M-WORKFLOW-PROTOCOL, M-WORKFLOW-REPAIR, M-WORKFLOW-STATE, M-WORKFLOW-TRANSITIONS, M-WORKFLOW-TOOLING, M-WORKFLOW-PERSISTENCE, M-WORKFLOW-DELEGATED, M-
|
|
4
|
+
// PURPOSE: Register workflow tools and enforcement while injecting only startup-profile-compatible vv-controller guidance, including profile-independent control tools with bounded recovery, host-call-bound attempts, terminal report-rejection settlement, checkpoint reviewer linkage, and strict owned-tool contract publication/validation.
|
|
5
|
+
// SCOPE: work_item_open/list/close registration, profile-independent work_item_decide and work_checkpoint registration with root-session authorization and an SDK-backed read-only authorization-message lookup for user-authorized recovery, owned tool.definition publication of strict input JSON Schemas plus early validateWorkflowToolInput guards in tool.execute.before (without touching task-launch hooks), pre-dispatch structural validation of mutation tools, tracked launch validation with delegated barriers, overlapping-write gates, and authoritative native sealed-run rejection, live host-call bindings that convert supported foreground vv-implementer task launches into failed delegated attempts on confirmed host-terminal errors, result normalization and bounded same-session continuation with explicit hard-stop suppression, callID-bound delegated attempt results, terminal settlement of protocol-invalid reports as report_rejected attempts through staged persistence, checkpoint reviewer bookkeeping, round aggregation with bounded excerpts, implementation round limits, checked persistence, and profile-selected chat.message guidance. Tool argument schemas come from schemas.ts; branch-aware validation from input-validation.ts; the authorization guard and message lookups from authorization.ts; staged transactions and committed recovery from recovery.ts.
|
|
6
|
+
// DEPENDS: [@opencode-ai/plugin, src/lib/agent-tool-contract.ts, src/lib/config-layers.ts, src/lib/orchestration.ts, src/lib/plugin-toggle-config.ts, src/plugins/workflow/authorization.ts, src/plugins/workflow/checkpoint-io.ts, src/plugins/workflow/checkpoints.ts, src/plugins/workflow/delegated.ts, src/plugins/workflow/input-validation.ts, src/plugins/workflow/persistence.ts, src/plugins/workflow/protocol.ts, src/plugins/workflow/recovery.ts, src/plugins/workflow/repair.ts, src/plugins/workflow/results.ts, src/plugins/workflow/schemas.ts, src/plugins/workflow/state.ts, src/plugins/workflow/tooling.ts, src/plugins/workflow/transitions.ts]
|
|
7
|
+
// LINKS: M-PLUGIN-WORKFLOW, M-ORCHESTRATION-PROFILES, M-WORKFLOW-PROTOCOL, M-WORKFLOW-REPAIR, M-WORKFLOW-STATE, M-WORKFLOW-TRANSITIONS, M-WORKFLOW-TOOLING, M-WORKFLOW-PERSISTENCE, M-WORKFLOW-DELEGATED, M-AGENT-TOOL-CONTRACT, V-M-PLUGIN-WORKFLOW
|
|
8
8
|
// ROLE: RUNTIME
|
|
9
9
|
// MAP_MODE: EXPORTS
|
|
10
10
|
// END_MODULE_CONTRACT
|
|
11
11
|
//
|
|
12
12
|
// START_MODULE_MAP
|
|
13
|
-
// WorkflowPlugin - Registers workflow work-item tools,
|
|
13
|
+
// WorkflowPlugin - Registers workflow work-item tools, profile-independent control tools whose execution remains root-session gated, owned tool.definition/execute.before contract hooks, tracked task protocol enforcement with callID-bound delegated attempts, bounded recovery with durable persist-and-rollback commits, terminal report-rejection settlement, checkpoint linkage, live host-call failure bindings, and primary-session workflow guidance injection.
|
|
14
14
|
// END_MODULE_MAP
|
|
15
15
|
//
|
|
16
16
|
// START_CHANGE_SUMMARY
|
|
17
|
-
// LAST_CHANGE: [C-
|
|
17
|
+
// LAST_CHANGE: [C-AGENT-TOOL-CONTRACTS T-007 - Appended one shared tracked result-protocol instruction, generated from protocol.ts status vocabularies, to the review-only/selective/delegated guidance so common launch/result rules and the on-demand reference path are available without the native execution skill; the profile-specific ownership text and the tracked system instruction are otherwise unchanged. Prior T-004: removed the redundant registration spread and made the native launch hook reject an authoritatively sealed plan run.]
|
|
18
18
|
// END_CHANGE_SUMMARY
|
|
19
19
|
import { tool } from "@opencode-ai/plugin";
|
|
20
20
|
import { attemptTrackedResultRepair, detectExplicitHardStopStatus, isTrackedResultRepairEligible, unwrapResumableTaskResult, } from "./repair.js";
|
|
21
|
-
import { parseResultBlock, parseWorkItemHeader, TRACKED_SUBAGENT_NAMES, } from "./protocol.js";
|
|
22
|
-
import { applyTrackedResult, createRecordLookupKey, createWorkflowResultExcerpt, beginTrackedLaunch, createWorkItemStore,
|
|
21
|
+
import { describeStatusVocabulary, parseResultBlock, parseWorkItemHeader, TRACKED_SUBAGENT_NAMES, } from "./protocol.js";
|
|
22
|
+
import { applyTrackedResult, createRecordLookupKey, createWorkflowResultExcerpt, beginTrackedLaunch, createWorkItemStore, getReviewRound, getWorkItem, revertReviewerLaunch, } from "./state.js";
|
|
23
23
|
import { applyDelegatedLaunchFailure, applyDelegatedReportRejection, applyDelegatedResult, beginDelegatedLaunch, revertInFlightDelegatedLaunches, summarizeDelegatedProgress, } from "./delegated.js";
|
|
24
24
|
import { checkpointBarrierUnsatisfied, findOverlappingInFlightReview, recordCheckpointReviewerLaunch, recordCheckpointReviewerResult, } from "./checkpoints.js";
|
|
25
25
|
import { getAllowedNextAgents, getAttemptedImplementationRound, getReviewerRoleForAgent, shouldBlockRound, } from "./transitions.js";
|
|
26
26
|
import { createWorkItemCloseTool, createWorkItemDecideTool, createWorkItemListTool, createWorkItemOpenTool, createWorkCheckpointTool, } from "./tooling.js";
|
|
27
|
+
import { workCheckpointArgs, workItemCloseArgs, workItemDecideArgs, workItemListArgs, workItemOpenArgs, } from "./schemas.js";
|
|
28
|
+
import { isWorkflowToolId, validateWorkflowToolInput, workflowToolContracts, } from "./input-validation.js";
|
|
29
|
+
import { ContractInputError, createToolDefinitionAdapter } from "../../lib/agent-tool-contract.js";
|
|
30
|
+
import { serializeWorkflowResult, workflowInputFailure } from "./results.js";
|
|
31
|
+
import { deriveDelegatedGuidance } from "./inspection.js";
|
|
32
|
+
import { assertWorkflowToolAccess, createWorkflowAuthorization, shouldInjectForAgent, } from "./authorization.js";
|
|
33
|
+
import { createRecoverySupport } from "./recovery.js";
|
|
27
34
|
import workflowSystemInstructionTemplate from "./system-instruction.md?raw";
|
|
28
35
|
import { loadVvocConfig } from "../../lib/config-layers.js";
|
|
29
36
|
import { resolveOrchestrationPolicy, } from "../../lib/orchestration.js";
|
|
30
37
|
import { isVvocPluginEnabled } from "../../lib/plugin-toggle-config.js";
|
|
31
38
|
import { deleteWorkflowSessionDir, hydrateWorkflowStateChecked, snapshotWorkflowStateChecked, } from "./persistence.js";
|
|
32
39
|
import { loadApprovedDelegatedPlan } from "./checkpoint-io.js";
|
|
33
|
-
import { isTaskLaunchableInStore } from "./execution.js";
|
|
34
|
-
import { runWorkflowTransaction, WorkflowTransactionQueue } from "./transactions.js";
|
|
35
|
-
const z = tool.schema;
|
|
40
|
+
import { isTaskLaunchableInStore, latestAttemptView } from "./execution.js";
|
|
36
41
|
const TRACKED_SUBAGENT_SET = new Set(TRACKED_SUBAGENT_NAMES);
|
|
37
42
|
const WORK_ITEM_MISSING_MARKER = "__VVOC" + "_SECRET_BEARER_TOKEN_a6f582092f05__";
|
|
38
43
|
const INVALID_NEXT_AGENT_MARKER = "__VVOC" + "_SECRET_BEARER_TOKEN_513fa2de603d__";
|
|
39
|
-
const WORKFLOW_CONTROLLER_AGENT = "vv-controller";
|
|
40
44
|
const REVIEW_ONLY_WORKFLOW_SYSTEM_INSTRUCTION = `
|
|
41
45
|
<workflow_protocol>
|
|
42
46
|
Tracked review-only mechanics are available when independent evaluation is explicitly requested or
|
|
@@ -103,17 +107,29 @@ packets, while keeping planning artifacts, acceptance decisions, and verificatio
|
|
|
103
107
|
session.
|
|
104
108
|
</workflow_protocol>
|
|
105
109
|
`.trim();
|
|
110
|
+
// Common result-protocol guidance appended to the non-tracked profile
|
|
111
|
+
// instructions. It states shared launch/result rules only: it does not select a
|
|
112
|
+
// source or lifecycle, and it adds no delegation or review obligation. Status
|
|
113
|
+
// vocabularies come from protocol.ts so the text cannot drift from the parser.
|
|
114
|
+
const TRACKED_RESULT_PROTOCOL_INSTRUCTION = `
|
|
115
|
+
<tracked_result_protocol>
|
|
116
|
+
A tracked subagent result begins on its first line with the protocol top block — no preface, prose, or code fence — followed by a blank line and the body. Use the exact VVOC_WORK_ITEM_ID returned by work_item_open for that assignment; never reuse a sample id from another task.
|
|
117
|
+
- vv-implementer: VVOC_STATUS ${describeStatusVocabulary("vv-implementer")}, with a required VVOC_ROUTE.
|
|
118
|
+
- vv-spec-reviewer / vv-code-reviewer: VVOC_STATUS ${describeStatusVocabulary("vv-spec-reviewer")}; a reviewer result carries no route.
|
|
119
|
+
A result whose first field names a different work item is a work-item mismatch, not a malformed header, and is never relabeled to the expected id. Inspect work_item_list before retrying to recover the current identity, state, attempt, and remaining budget. Common tool calls follow the published input schemas, and work_item_list reports the loaded contract revision and the on-demand reference path at contract.referencePath.
|
|
120
|
+
</tracked_result_protocol>
|
|
121
|
+
`.trim();
|
|
106
122
|
/** Returns the exact workflow instruction compatible with one resolved policy. */
|
|
107
123
|
function getWorkflowSystemInstruction(policy) {
|
|
108
124
|
switch (policy.workflowGuidance) {
|
|
109
125
|
case "review-only":
|
|
110
|
-
return REVIEW_ONLY_WORKFLOW_SYSTEM_INSTRUCTION
|
|
126
|
+
return `${REVIEW_ONLY_WORKFLOW_SYSTEM_INSTRUCTION}\n\n${TRACKED_RESULT_PROTOCOL_INSTRUCTION}`;
|
|
111
127
|
case "selective":
|
|
112
|
-
return SELECTIVE_WORKFLOW_SYSTEM_INSTRUCTION
|
|
128
|
+
return `${SELECTIVE_WORKFLOW_SYSTEM_INSTRUCTION}\n\n${TRACKED_RESULT_PROTOCOL_INSTRUCTION}`;
|
|
113
129
|
case "tracked":
|
|
114
130
|
return workflowSystemInstructionTemplate.trim();
|
|
115
131
|
case "delegated":
|
|
116
|
-
return DELEGATED_WORKFLOW_SYSTEM_INSTRUCTION
|
|
132
|
+
return `${DELEGATED_WORKFLOW_SYSTEM_INSTRUCTION}\n\n${TRACKED_RESULT_PROTOCOL_INSTRUCTION}`;
|
|
117
133
|
}
|
|
118
134
|
}
|
|
119
135
|
function isTrackedSubagent(value) {
|
|
@@ -180,19 +196,6 @@ function delegatedHostFailurePrefix(childSessionId) {
|
|
|
180
196
|
return `Subagent failed (task_id: ${childSessionId}): `;
|
|
181
197
|
}
|
|
182
198
|
// END_BLOCK_DELEGATED_FAILURE_BINDING_TYPES
|
|
183
|
-
function canUseWorkflowTools(agentName) {
|
|
184
|
-
return agentName === WORKFLOW_CONTROLLER_AGENT;
|
|
185
|
-
}
|
|
186
|
-
function assertWorkflowToolAccess(agentName, toolName) {
|
|
187
|
-
if (canUseWorkflowTools(agentName)) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
const resolvedAgent = agentName?.trim() || "unknown-agent";
|
|
191
|
-
throw new Error(`WORKFLOW_TOOL_DENIED: ${toolName} is only available to ${WORKFLOW_CONTROLLER_AGENT} sessions. Current agent: ${resolvedAgent}.`);
|
|
192
|
-
}
|
|
193
|
-
function shouldInjectForAgent(agentName) {
|
|
194
|
-
return canUseWorkflowTools(agentName);
|
|
195
|
-
}
|
|
196
199
|
function appendSystemInstruction(existingSystem, instruction) {
|
|
197
200
|
if (!existingSystem?.trim()) {
|
|
198
201
|
return instruction;
|
|
@@ -202,8 +205,11 @@ function appendSystemInstruction(existingSystem, instruction) {
|
|
|
202
205
|
}
|
|
203
206
|
return `${existingSystem.trim()}\n\n${instruction}`;
|
|
204
207
|
}
|
|
205
|
-
|
|
206
|
-
|
|
208
|
+
// Serialize every public workflow result through the shared results contract so
|
|
209
|
+
// failures gain a stable category and post-side-effect reporting failures stay
|
|
210
|
+
// bounded and truthful instead of throwing raw after a committed mutation.
|
|
211
|
+
function stringifyToolOutput(value, outcome) {
|
|
212
|
+
return serializeWorkflowResult(value, outcome !== undefined ? { outcome } : undefined);
|
|
207
213
|
}
|
|
208
214
|
function createRoundLimitMessage(record, attemptedRound) {
|
|
209
215
|
return [
|
|
@@ -625,221 +631,21 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
625
631
|
}
|
|
626
632
|
// END_BLOCK_DELEGATED_FAILURE_BINDING
|
|
627
633
|
// START_BLOCK_DELEGATED_AUTHORIZATION
|
|
628
|
-
// New control mutations require the primary vv-controller session
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
contextWorkspace.worktree !== trustedWorkspaceRoot) {
|
|
639
|
-
throw new Error(`CONTROL_DENIED: ${toolName} workspace ${contextWorkspace.worktree} does not match the trusted plugin workspace.`);
|
|
640
|
-
}
|
|
641
|
-
if (contextWorkspace.worktree === undefined &&
|
|
642
|
-
contextWorkspace.directory !== undefined &&
|
|
643
|
-
contextWorkspace.directory !== directory) {
|
|
644
|
-
throw new Error(`CONTROL_DENIED: ${toolName} directory ${contextWorkspace.directory} does not match the trusted plugin directory.`);
|
|
645
|
-
}
|
|
646
|
-
if (invalidHydrationSessions.has(sessionId)) {
|
|
647
|
-
throw new Error(`CONTROL_DENIED: persisted workflow state for session ${sessionId} is invalid; resolve or remove it before new control mutations.`);
|
|
648
|
-
}
|
|
649
|
-
let parentID;
|
|
650
|
-
try {
|
|
651
|
-
const response = await client.session.get({ path: { id: sessionId } });
|
|
652
|
-
if (!response.data) {
|
|
653
|
-
throw new Error("missing session payload");
|
|
654
|
-
}
|
|
655
|
-
parentID = response.data.parentID;
|
|
656
|
-
}
|
|
657
|
-
catch (error) {
|
|
658
|
-
throw new Error(`CONTROL_DENIED: ${toolName} requires root-session identity for ${sessionId}, which could not be verified: ${error.message}`);
|
|
659
|
-
}
|
|
660
|
-
if (parentID !== undefined && parentID !== null && parentID !== "") {
|
|
661
|
-
throw new Error(`CONTROL_DENIED: ${toolName} may only run in the root session; session ${sessionId} is a child of ${parentID}.`);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
634
|
+
// New control mutations require the primary vv-controller session; the
|
|
635
|
+
// guard, its root-session identity check, and the SDK-backed message
|
|
636
|
+
// lookups live in authorization.js over this explicit plugin context.
|
|
637
|
+
const { assertPrimaryControllerMutation, lookupRecoveryUserMessage, lookupAuthorityMessage } = createWorkflowAuthorization({
|
|
638
|
+
client,
|
|
639
|
+
directory,
|
|
640
|
+
worktree,
|
|
641
|
+
trustedWorkspaceRoot,
|
|
642
|
+
invalidHydrationSessions,
|
|
643
|
+
});
|
|
664
644
|
// END_BLOCK_DELEGATED_AUTHORIZATION
|
|
665
645
|
// START_BLOCK_RECOVERY_SUPPORT
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
|
|
669
|
-
// failures throw so the domain reports AUTHORIZATION_LOOKUP_FAILED instead
|
|
670
|
-
// of conflating an unreachable lookup with a nonexistent message.
|
|
671
|
-
const lookupRecoveryUserMessage = async (sessionId, messageId) => {
|
|
672
|
-
const response = await client.session.message({
|
|
673
|
-
path: { id: sessionId, messageID: messageId },
|
|
674
|
-
query: { directory },
|
|
675
|
-
});
|
|
676
|
-
if (response.error || !response.data) {
|
|
677
|
-
const errorName = response.error?.name;
|
|
678
|
-
if (errorName && errorName !== "NotFound") {
|
|
679
|
-
throw new Error(`session message lookup failed: ${errorName}`);
|
|
680
|
-
}
|
|
681
|
-
return undefined;
|
|
682
|
-
}
|
|
683
|
-
const info = response.data.info;
|
|
684
|
-
return {
|
|
685
|
-
role: info.role,
|
|
686
|
-
sessionID: info.sessionID,
|
|
687
|
-
id: info.id,
|
|
688
|
-
timeCreatedMs: typeof info.time?.created === "number" && Number.isFinite(info.time.created)
|
|
689
|
-
? info.time.created
|
|
690
|
-
: undefined,
|
|
691
|
-
};
|
|
692
|
-
};
|
|
693
|
-
// Advance-authority provenance uses the same pinned SDK message response.
|
|
694
|
-
// Only verified identity/timing/eligibility metadata leaves this lookup; raw
|
|
695
|
-
// user text is never persisted or logged.
|
|
696
|
-
const lookupAuthorityMessage = async (input) => {
|
|
697
|
-
const response = await client.session.message({
|
|
698
|
-
path: { id: input.sessionId, messageID: input.messageId },
|
|
699
|
-
query: { directory },
|
|
700
|
-
});
|
|
701
|
-
if (response.error || !response.data) {
|
|
702
|
-
const errorName = response.error?.name;
|
|
703
|
-
if (errorName && errorName !== "NotFound") {
|
|
704
|
-
throw new Error(`authority message lookup failed: ${errorName}`);
|
|
705
|
-
}
|
|
706
|
-
return undefined;
|
|
707
|
-
}
|
|
708
|
-
const info = response.data.info;
|
|
709
|
-
const parts = Array.isArray(response.data.parts) ? response.data.parts : [];
|
|
710
|
-
const textParts = parts
|
|
711
|
-
.map((part) => part)
|
|
712
|
-
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
713
|
-
.map((part) => part.text);
|
|
714
|
-
return {
|
|
715
|
-
messageId: info.id ?? input.messageId,
|
|
716
|
-
sessionId: info.sessionID ?? input.sessionId,
|
|
717
|
-
role: info.role === "user" ? "user" : "assistant",
|
|
718
|
-
createdMs: typeof info.time?.created === "number" && Number.isFinite(info.time.created)
|
|
719
|
-
? info.time.created
|
|
720
|
-
: 0,
|
|
721
|
-
ignored: info.ignored === true,
|
|
722
|
-
syntheticOnly: false,
|
|
723
|
-
textParts,
|
|
724
|
-
};
|
|
725
|
-
};
|
|
726
|
-
// Per-session serialization for generic mutating tool calls. The staged
|
|
727
|
-
// snapshot persists before the committed state is published, so a failed
|
|
728
|
-
// write never exposes new obligations, authority, or launch permissions.
|
|
729
|
-
const workflowTransactions = new WorkflowTransactionQueue();
|
|
730
|
-
async function commitGenericToolResult(sessionId, run) {
|
|
731
|
-
const liveStore = stores.get(sessionId);
|
|
732
|
-
if (!liveStore) {
|
|
733
|
-
return {
|
|
734
|
-
ok: false,
|
|
735
|
-
errorCode: "SESSION_MISMATCH",
|
|
736
|
-
message: `no live workflow store for session ${sessionId}`,
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
if (invalidHydrationSessions.has(sessionId)) {
|
|
740
|
-
return {
|
|
741
|
-
ok: false,
|
|
742
|
-
errorCode: "INVALID_STATE",
|
|
743
|
-
message: `persisted workflow state for session ${sessionId} is invalid`,
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
const outcome = await runWorkflowTransaction({
|
|
747
|
-
queue: workflowTransactions,
|
|
748
|
-
sessionId,
|
|
749
|
-
getData: () => liveStore.getStoreData(),
|
|
750
|
-
operation: async (staged) => {
|
|
751
|
-
const result = await run(createWorkItemStoreView(staged));
|
|
752
|
-
if (result.ok !== true) {
|
|
753
|
-
// Validation failed: persist nothing and publish nothing.
|
|
754
|
-
return { result, skipPersist: true };
|
|
755
|
-
}
|
|
756
|
-
return { result };
|
|
757
|
-
},
|
|
758
|
-
});
|
|
759
|
-
if (!outcome.ok) {
|
|
760
|
-
void client.app
|
|
761
|
-
.log({
|
|
762
|
-
body: {
|
|
763
|
-
service: "workflow",
|
|
764
|
-
level: "error",
|
|
765
|
-
message: "[workflow][generic][BLOCK_GENERIC_COMMIT] persistence failed",
|
|
766
|
-
extra: { sessionID: sessionId, error: outcome.error.slice(0, 300) },
|
|
767
|
-
},
|
|
768
|
-
})
|
|
769
|
-
.catch(() => undefined);
|
|
770
|
-
return {
|
|
771
|
-
ok: false,
|
|
772
|
-
errorCode: "PERSISTENCE_FAILED",
|
|
773
|
-
message: `generic workflow mutation could not be persisted: ${outcome.error}`,
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
return outcome.result;
|
|
777
|
-
}
|
|
778
|
-
/**
|
|
779
|
-
* Execute one recovery mutation durably on the live store: the domain
|
|
780
|
-
* reducer applies to the live entry (re-prechecking after any authorization
|
|
781
|
-
* await), and the whole live store is then persisted synchronously. If the
|
|
782
|
-
* write fails, the captured prior entry is restored so no unpersisted launch
|
|
783
|
-
* permission is ever exposed — there is no await between mutation, persist,
|
|
784
|
-
* and rollback, so no concurrent actor can observe the intermediate state,
|
|
785
|
-
* and no stale whole-store snapshot can regress concurrent transitions.
|
|
786
|
-
*/
|
|
787
|
-
async function executeCommittedRecovery(sessionId, runRecovery, captureRestore, wasApplied) {
|
|
788
|
-
const liveStore = stores.get(sessionId);
|
|
789
|
-
if (!liveStore || invalidHydrationSessions.has(sessionId)) {
|
|
790
|
-
throw new Error(`CONTROL_DENIED: persisted workflow state for session ${sessionId} is invalid; resolve or remove it before new control mutations.`);
|
|
791
|
-
}
|
|
792
|
-
const restore = captureRestore();
|
|
793
|
-
const result = await runRecovery(liveStore);
|
|
794
|
-
if (!wasApplied(result)) {
|
|
795
|
-
return result;
|
|
796
|
-
}
|
|
797
|
-
const persisted = snapshotWorkflowStateChecked(sessionId, liveStore.getStoreData());
|
|
798
|
-
if (!persisted.ok) {
|
|
799
|
-
restore();
|
|
800
|
-
void client.app
|
|
801
|
-
.log({
|
|
802
|
-
body: {
|
|
803
|
-
service: "workflow",
|
|
804
|
-
level: "error",
|
|
805
|
-
message: "[workflow][recovery][BLOCK_RECOVERY_COMMIT] recovery persistence failed",
|
|
806
|
-
extra: { sessionID: sessionId, error: persisted.error.slice(0, 300) },
|
|
807
|
-
},
|
|
808
|
-
})
|
|
809
|
-
.catch(() => undefined);
|
|
810
|
-
throw new Error(`PERSISTENCE_FAILED: recovery applied in memory but could not be persisted and was rolled back: ${persisted.error}`);
|
|
811
|
-
}
|
|
812
|
-
return result;
|
|
813
|
-
}
|
|
814
|
-
/** Capture and restore one work-item record entry for recovery rollback. */
|
|
815
|
-
function captureRecordRestore(sessionId, workItemId) {
|
|
816
|
-
const liveStore = stores.get(sessionId);
|
|
817
|
-
if (!liveStore)
|
|
818
|
-
return () => undefined;
|
|
819
|
-
const liveData = liveStore.getStoreData();
|
|
820
|
-
const lookupKey = createRecordLookupKey(sessionId, workItemId);
|
|
821
|
-
const prior = liveData.records.get(lookupKey);
|
|
822
|
-
return () => {
|
|
823
|
-
if (prior) {
|
|
824
|
-
liveData.records.set(lookupKey, prior);
|
|
825
|
-
}
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
/** Capture and restore one checkpoint entry for recovery rollback. */
|
|
829
|
-
function captureCheckpointRestore(sessionId, runId, checkpointId) {
|
|
830
|
-
const liveStore = stores.get(sessionId);
|
|
831
|
-
if (!liveStore)
|
|
832
|
-
return () => undefined;
|
|
833
|
-
const run = liveStore.getStoreData().planRuns.get(runId);
|
|
834
|
-
if (!run)
|
|
835
|
-
return () => undefined;
|
|
836
|
-
const priorCheckpoint = run.checkpoints.get(checkpointId);
|
|
837
|
-
return () => {
|
|
838
|
-
if (priorCheckpoint) {
|
|
839
|
-
run.checkpoints.set(checkpointId, priorCheckpoint);
|
|
840
|
-
}
|
|
841
|
-
};
|
|
842
|
-
}
|
|
646
|
+
// Staged generic transactions and durably committed recovery with rollback
|
|
647
|
+
// live in recovery.js over this explicit persistence seam.
|
|
648
|
+
const { commitGenericToolResult, executeCommittedRecovery, captureRecordRestore, captureCheckpointRestore, } = createRecoverySupport({ client, stores, invalidHydrationSessions });
|
|
843
649
|
// END_BLOCK_RECOVERY_SUPPORT
|
|
844
650
|
// START_BLOCK_CHECKPOINT_LINKAGE
|
|
845
651
|
/** Find the in-flight checkpoint generation whose linked review item matches. */
|
|
@@ -870,66 +676,46 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
870
676
|
lookupUserMessage: lookupRecoveryUserMessage,
|
|
871
677
|
lookupAuthorityMessage,
|
|
872
678
|
});
|
|
679
|
+
// START_BLOCK_TOOL_CONTRACT_HOOKS
|
|
680
|
+
// Owned-only contract publication and early structural/branch validation.
|
|
681
|
+
// The tool.definition adapter publishes the strict input JSON Schema through
|
|
682
|
+
// the host's observable jsonSchema member (T-001-proven seam) without
|
|
683
|
+
// replacing the host decoder; the execute.before guard rejects invalid
|
|
684
|
+
// raw arguments with bounded tokenized paths before any handler runs and
|
|
685
|
+
// never touches the task-launch hooks below.
|
|
686
|
+
const toolDefinitionAdapter = createToolDefinitionAdapter([...workflowToolContracts]);
|
|
687
|
+
// END_BLOCK_TOOL_CONTRACT_HOOKS
|
|
873
688
|
return {
|
|
874
689
|
tool: {
|
|
875
690
|
work_item_open: tool({
|
|
876
691
|
description: workItemOpenTool.description,
|
|
877
|
-
args:
|
|
878
|
-
items: z.array(z.object({
|
|
879
|
-
key: z.string(),
|
|
880
|
-
title: z.string(),
|
|
881
|
-
mode: z.string(),
|
|
882
|
-
requiredReviewers: z.array(z.string()),
|
|
883
|
-
writeScope: z.array(z.string()).optional(),
|
|
884
|
-
planRunId: z.string().optional(),
|
|
885
|
-
planTaskId: z.string().optional(),
|
|
886
|
-
taskId: z.string().optional(),
|
|
887
|
-
goal: z.string().optional(),
|
|
888
|
-
acceptanceCriteria: z.array(z.string()).optional(),
|
|
889
|
-
verification: z.array(z.string()).optional(),
|
|
890
|
-
dependsOn: z.array(z.string()).optional(),
|
|
891
|
-
blockedBy: z.array(z.string()).optional(),
|
|
892
|
-
})),
|
|
893
|
-
execution: z
|
|
894
|
-
.object({
|
|
895
|
-
executionKey: z.string(),
|
|
896
|
-
source: z.record(z.string(), z.unknown()),
|
|
897
|
-
goal: z.string(),
|
|
898
|
-
boundary: z.object({
|
|
899
|
-
files: z.array(z.string()),
|
|
900
|
-
directories: z.array(z.string()),
|
|
901
|
-
}),
|
|
902
|
-
checkpoints: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
903
|
-
})
|
|
904
|
-
.optional(),
|
|
905
|
-
runId: z.string().optional(),
|
|
906
|
-
amendmentId: z.string().optional(),
|
|
907
|
-
rationale: z.string().optional(),
|
|
908
|
-
},
|
|
692
|
+
args: workItemOpenArgs,
|
|
909
693
|
async execute(args, context) {
|
|
910
694
|
assertWorkflowToolAccess(context.agent, "work_item_open");
|
|
911
|
-
const
|
|
695
|
+
const validation = validateWorkflowToolInput("work_item_open", args);
|
|
696
|
+
if (!validation.ok) {
|
|
697
|
+
return stringifyToolOutput(workflowInputFailure("work_item_open", context.sessionID, validation.issues));
|
|
698
|
+
}
|
|
699
|
+
const isGeneric = validation.data.execution !== undefined || validation.data.runId !== undefined;
|
|
912
700
|
if (isGeneric) {
|
|
913
701
|
// Hydrate/validate the session store before the transaction boundary
|
|
914
702
|
// reads it, matching every other tool entry point.
|
|
915
703
|
getOrCreateStore(context.sessionID);
|
|
916
|
-
const result = await commitGenericToolResult(context.sessionID, (view) => workItemOpenTool.execute(args, {
|
|
704
|
+
const result = await commitGenericToolResult(context.sessionID, "work_item_open", (view) => workItemOpenTool.execute(args, {
|
|
917
705
|
sessionId: context.sessionID,
|
|
918
706
|
workspaceRoot: trustedWorkspaceRoot,
|
|
919
707
|
}, view));
|
|
920
|
-
return stringifyToolOutput(result);
|
|
708
|
+
return stringifyToolOutput(result, result.ok === true ? "committed" : "not_applied");
|
|
921
709
|
}
|
|
922
710
|
const sessionStore = getOrCreateStore(context.sessionID);
|
|
923
711
|
const opened = workItemOpenTool.execute(args, { sessionId: context.sessionID }, sessionStore);
|
|
924
|
-
snapshotSession(context.sessionID);
|
|
925
|
-
return stringifyToolOutput(opened);
|
|
712
|
+
const openedPersisted = snapshotSession(context.sessionID);
|
|
713
|
+
return stringifyToolOutput(opened, opened.ok === false ? "not_applied" : openedPersisted.ok ? "committed" : "unknown");
|
|
926
714
|
},
|
|
927
715
|
}),
|
|
928
716
|
work_item_list: tool({
|
|
929
717
|
description: workItemListTool.description,
|
|
930
|
-
args:
|
|
931
|
-
includeClosed: z.boolean().optional(),
|
|
932
|
-
},
|
|
718
|
+
args: workItemListArgs,
|
|
933
719
|
async execute(args, context) {
|
|
934
720
|
assertWorkflowToolAccess(context.agent, "work_item_list");
|
|
935
721
|
const sessionStore = getOrCreateStore(context.sessionID);
|
|
@@ -938,166 +724,133 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
938
724
|
}),
|
|
939
725
|
work_item_close: tool({
|
|
940
726
|
description: workItemCloseTool.description,
|
|
941
|
-
args:
|
|
942
|
-
workItemId: z.string(),
|
|
943
|
-
},
|
|
727
|
+
args: workItemCloseArgs,
|
|
944
728
|
async execute(args, context) {
|
|
945
729
|
assertWorkflowToolAccess(context.agent, "work_item_close");
|
|
946
730
|
const sessionStore = getOrCreateStore(context.sessionID);
|
|
947
731
|
const closed = workItemCloseTool.execute(args, { sessionId: context.sessionID }, sessionStore);
|
|
948
|
-
snapshotSession(context.sessionID);
|
|
949
|
-
return stringifyToolOutput(closed);
|
|
732
|
+
const closedPersisted = snapshotSession(context.sessionID);
|
|
733
|
+
return stringifyToolOutput(closed, closed.ok === false ? "not_applied" : closedPersisted.ok ? "committed" : "unknown");
|
|
950
734
|
},
|
|
951
735
|
}),
|
|
952
736
|
// Control tools are registered independent of the startup profile; they
|
|
953
737
|
// remain root-session, agent, workspace, and invalid-hydration gated at
|
|
954
738
|
// execution time.
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
const sessionStore = getOrCreateStore(context.sessionID);
|
|
978
|
-
await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_item_decide");
|
|
979
|
-
// Recovery is the only decision family that must persist
|
|
980
|
-
// before it exposes new launch permissions. Authority-bearing
|
|
981
|
-
// recovery stages the work-item recovery and the reserve debit in
|
|
982
|
-
// one serialized transaction so they commit or fail together.
|
|
983
|
-
if (args.decision === "recover") {
|
|
984
|
-
if (args.authorityId !== undefined) {
|
|
985
|
-
const staged = await commitGenericToolResult(context.sessionID, (view) => workItemDecideTool.execute(args, { sessionId: context.sessionID }, view));
|
|
986
|
-
return stringifyToolOutput(staged);
|
|
987
|
-
}
|
|
988
|
-
const workItemId = String(args.workItemId ?? "");
|
|
989
|
-
const recovered = await executeCommittedRecovery(context.sessionID, (liveStore) => workItemDecideTool.execute(args, { sessionId: context.sessionID }, liveStore), () => captureRecordRestore(context.sessionID, workItemId), (result) => result.ok === true);
|
|
990
|
-
return stringifyToolOutput(recovered);
|
|
991
|
-
}
|
|
992
|
-
const decided = await workItemDecideTool.execute(args, { sessionId: context.sessionID }, sessionStore);
|
|
993
|
-
if (decided.ok) {
|
|
994
|
-
const persisted = snapshotSession(context.sessionID);
|
|
995
|
-
if (!persisted.ok) {
|
|
996
|
-
throw new Error(`PERSISTENCE_FAILED: decision applied in memory but could not be persisted: ${persisted.error}`);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
return stringifyToolOutput(decided);
|
|
1000
|
-
},
|
|
1001
|
-
}),
|
|
1002
|
-
work_checkpoint: tool({
|
|
1003
|
-
description: workCheckpointTool.description,
|
|
1004
|
-
args: {
|
|
1005
|
-
action: z.enum([
|
|
1006
|
-
"register",
|
|
1007
|
-
"start",
|
|
1008
|
-
"verify",
|
|
1009
|
-
"recover",
|
|
1010
|
-
"review",
|
|
1011
|
-
"bind",
|
|
1012
|
-
"complete",
|
|
1013
|
-
"amend",
|
|
1014
|
-
"authorize",
|
|
1015
|
-
"record_approval",
|
|
1016
|
-
"revoke_authority",
|
|
1017
|
-
]),
|
|
1018
|
-
planPath: z.string().optional(),
|
|
1019
|
-
runId: z.string().optional(),
|
|
1020
|
-
checkpointId: z.string().optional(),
|
|
1021
|
-
complete: z.boolean().optional(),
|
|
1022
|
-
diagnosis: z.string().optional(),
|
|
1023
|
-
changedCondition: z.string().optional(),
|
|
1024
|
-
verification: z.array(z.string()).optional(),
|
|
1025
|
-
recoveryId: z.string().optional(),
|
|
1026
|
-
userMessageId: z.string().optional(),
|
|
1027
|
-
checkpoints: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
1028
|
-
tasks: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
1029
|
-
amendmentId: z.string().optional(),
|
|
1030
|
-
rationale: z.string().optional(),
|
|
1031
|
-
startFingerprint: z.string().optional(),
|
|
1032
|
-
reviewer: z.string().optional(),
|
|
1033
|
-
authorityId: z.string().optional(),
|
|
1034
|
-
messageId: z.string().optional(),
|
|
1035
|
-
approvalId: z.string().optional(),
|
|
1036
|
-
stage: z.string().optional(),
|
|
1037
|
-
stages: z.array(z.string()).optional(),
|
|
1038
|
-
decisionScope: z.string().optional(),
|
|
1039
|
-
fileBoundary: z.array(z.string()).optional(),
|
|
1040
|
-
reservedStops: z.array(z.string()).optional(),
|
|
1041
|
-
artifactPath: z.string().optional(),
|
|
1042
|
-
artifactSha256: z.string().optional(),
|
|
1043
|
-
revocationId: z.string().optional(),
|
|
1044
|
-
},
|
|
1045
|
-
async execute(args, context) {
|
|
1046
|
-
// Resolve the store first so invalid persisted state is detected
|
|
1047
|
-
// before the authorization check reports it as a control denial.
|
|
1048
|
-
const sessionStore = getOrCreateStore(context.sessionID);
|
|
1049
|
-
await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_checkpoint");
|
|
1050
|
-
const toolContext = {
|
|
1051
|
-
sessionId: context.sessionID,
|
|
1052
|
-
workspaceRoot: trustedWorkspaceRoot,
|
|
1053
|
-
loadPlan: async (planPath, workspaceRoot) => {
|
|
1054
|
-
const loaded = await loadApprovedDelegatedPlan({ workspaceRoot, planPath });
|
|
1055
|
-
return loaded.ok ? loaded.plan : { loadError: `${loaded.code}: ${loaded.message}` };
|
|
1056
|
-
},
|
|
1057
|
-
};
|
|
1058
|
-
// Generic (non-native) executions route through the atomic
|
|
1059
|
-
// transaction boundary: staged persist, then publish.
|
|
1060
|
-
const runIdArg = String(args.runId ?? "");
|
|
1061
|
-
const liveExecution = runIdArg
|
|
1062
|
-
? sessionStore.getStoreData().executions.get(runIdArg)
|
|
1063
|
-
: undefined;
|
|
1064
|
-
const authorityAction = args.action === "authorize" ||
|
|
1065
|
-
args.action === "record_approval" ||
|
|
1066
|
-
args.action === "revoke_authority";
|
|
1067
|
-
const genericAction = args.action !== "register" &&
|
|
1068
|
-
liveExecution !== undefined &&
|
|
1069
|
-
(liveExecution.source.kind !== "native-package" || authorityAction);
|
|
1070
|
-
const nativeAuthorityRecover = args.action === "recover" &&
|
|
1071
|
-
args.authorityId !== undefined &&
|
|
1072
|
-
liveExecution !== undefined;
|
|
1073
|
-
const genericRegister = args.action === "register" && !args.planPath;
|
|
1074
|
-
if (genericAction || genericRegister || nativeAuthorityRecover) {
|
|
1075
|
-
const result = await commitGenericToolResult(context.sessionID, (view) => workCheckpointTool.execute(args, toolContext, view));
|
|
1076
|
-
return stringifyToolOutput(result);
|
|
739
|
+
work_item_decide: tool({
|
|
740
|
+
description: workItemDecideTool.description,
|
|
741
|
+
args: workItemDecideArgs,
|
|
742
|
+
async execute(args, context) {
|
|
743
|
+
// Resolve the store first so invalid persisted state is detected
|
|
744
|
+
// before the authorization check reports it as a control denial.
|
|
745
|
+
const sessionStore = getOrCreateStore(context.sessionID);
|
|
746
|
+
await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_item_decide");
|
|
747
|
+
const validation = validateWorkflowToolInput("work_item_decide", args);
|
|
748
|
+
if (!validation.ok) {
|
|
749
|
+
return stringifyToolOutput(workflowInputFailure("work_item_decide", context.sessionID, validation.issues));
|
|
750
|
+
}
|
|
751
|
+
// Recovery is the only decision family that must persist
|
|
752
|
+
// before it exposes new launch permissions. Authority-bearing
|
|
753
|
+
// recovery stages the work-item recovery and the reserve debit in
|
|
754
|
+
// one serialized transaction so they commit or fail together.
|
|
755
|
+
// Routing uses the same validated/normalized values the handler consumes.
|
|
756
|
+
const parsedDecide = validation.data;
|
|
757
|
+
if (parsedDecide.decision === "recover") {
|
|
758
|
+
if (parsedDecide.authorityId !== undefined) {
|
|
759
|
+
const staged = await commitGenericToolResult(context.sessionID, "work_item_decide", (view) => workItemDecideTool.execute(args, { sessionId: context.sessionID }, view));
|
|
760
|
+
return stringifyToolOutput(staged, staged.ok === true ? "committed" : "not_applied");
|
|
1077
761
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
762
|
+
const workItemId = parsedDecide.workItemId.trim();
|
|
763
|
+
const recovered = await executeCommittedRecovery(context.sessionID, (liveStore) => workItemDecideTool.execute(args, { sessionId: context.sessionID }, liveStore), () => captureRecordRestore(context.sessionID, workItemId), (result) => result.ok === true);
|
|
764
|
+
// executeCommittedRecovery returns a success only after a durable
|
|
765
|
+
// write, and throws (rollback) if the write fails.
|
|
766
|
+
return stringifyToolOutput(recovered, recovered.ok === true ? "committed" : "not_applied");
|
|
767
|
+
}
|
|
768
|
+
const decided = await workItemDecideTool.execute(args, { sessionId: context.sessionID }, sessionStore);
|
|
769
|
+
if (decided.ok) {
|
|
770
|
+
const persisted = snapshotSession(context.sessionID);
|
|
771
|
+
if (!persisted.ok) {
|
|
772
|
+
throw new Error(`PERSISTENCE_FAILED: decision applied in memory but could not be persisted: ${persisted.error}`);
|
|
1087
773
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
774
|
+
}
|
|
775
|
+
return stringifyToolOutput(decided, decided.ok === true ? "committed" : "not_applied");
|
|
776
|
+
},
|
|
777
|
+
}),
|
|
778
|
+
work_checkpoint: tool({
|
|
779
|
+
description: workCheckpointTool.description,
|
|
780
|
+
args: workCheckpointArgs,
|
|
781
|
+
async execute(args, context) {
|
|
782
|
+
// Resolve the store first so invalid persisted state is detected
|
|
783
|
+
// before the authorization check reports it as a control denial.
|
|
784
|
+
const sessionStore = getOrCreateStore(context.sessionID);
|
|
785
|
+
await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_checkpoint");
|
|
786
|
+
const validation = validateWorkflowToolInput("work_checkpoint", args);
|
|
787
|
+
if (!validation.ok) {
|
|
788
|
+
return stringifyToolOutput(workflowInputFailure("work_checkpoint", context.sessionID, validation.issues));
|
|
789
|
+
}
|
|
790
|
+
const parsedArgs = validation.data;
|
|
791
|
+
const toolContext = {
|
|
792
|
+
sessionId: context.sessionID,
|
|
793
|
+
workspaceRoot: trustedWorkspaceRoot,
|
|
794
|
+
loadPlan: async (planPath, workspaceRoot) => {
|
|
795
|
+
const loaded = await loadApprovedDelegatedPlan({ workspaceRoot, planPath });
|
|
796
|
+
return loaded.ok ? loaded.plan : { loadError: `${loaded.code}: ${loaded.message}` };
|
|
797
|
+
},
|
|
798
|
+
};
|
|
799
|
+
// Generic (non-native) executions route through the atomic
|
|
800
|
+
// transaction boundary: staged persist, then publish. Routing uses
|
|
801
|
+
// the same trimmed runId the handler looks up, not a raw caller
|
|
802
|
+
// field that could select a different persistence path.
|
|
803
|
+
const runIdArg = parsedArgs.runId?.trim() ?? "";
|
|
804
|
+
const liveExecution = runIdArg
|
|
805
|
+
? sessionStore.getStoreData().executions.get(runIdArg)
|
|
806
|
+
: undefined;
|
|
807
|
+
const authorityAction = parsedArgs.action === "authorize" ||
|
|
808
|
+
parsedArgs.action === "record_approval" ||
|
|
809
|
+
parsedArgs.action === "revoke_authority";
|
|
810
|
+
const genericAction = parsedArgs.action !== "register" &&
|
|
811
|
+
liveExecution !== undefined &&
|
|
812
|
+
(liveExecution.source.kind !== "native-package" || authorityAction);
|
|
813
|
+
const nativeAuthorityRecover = parsedArgs.action === "recover" &&
|
|
814
|
+
parsedArgs.authorityId !== undefined &&
|
|
815
|
+
liveExecution !== undefined;
|
|
816
|
+
const genericRegister = parsedArgs.action === "register" && parsedArgs.planPath === undefined;
|
|
817
|
+
if (genericAction || genericRegister || nativeAuthorityRecover) {
|
|
818
|
+
const result = await commitGenericToolResult(context.sessionID, "work_checkpoint", (view) => workCheckpointTool.execute(args, toolContext, view));
|
|
819
|
+
return stringifyToolOutput(result, result.ok === true ? "committed" : "not_applied");
|
|
820
|
+
}
|
|
821
|
+
// Checkpoint recovery commits durably on the live store and
|
|
822
|
+
// rolls the checkpoint entry back on write failure, so a
|
|
823
|
+
// granted generation is never exposed before its state is
|
|
824
|
+
// durably recorded.
|
|
825
|
+
if (parsedArgs.action === "recover") {
|
|
826
|
+
const runId = parsedArgs.runId ?? "";
|
|
827
|
+
const checkpointId = parsedArgs.checkpointId ?? "";
|
|
828
|
+
const recovered = await executeCommittedRecovery(context.sessionID, (liveStore) => workCheckpointTool.execute(args, toolContext, liveStore), () => captureCheckpointRestore(context.sessionID, runId, checkpointId), (result) => result.ok === true);
|
|
829
|
+
return stringifyToolOutput(recovered, recovered.ok === true ? "committed" : "not_applied");
|
|
830
|
+
}
|
|
831
|
+
const result = await workCheckpointTool.execute({ ...args }, toolContext, sessionStore);
|
|
832
|
+
if (result.ok) {
|
|
833
|
+
const persisted = snapshotSession(context.sessionID);
|
|
834
|
+
if (!persisted.ok) {
|
|
835
|
+
throw new Error(`PERSISTENCE_FAILED: checkpoint change applied in memory but could not be persisted: ${persisted.error}`);
|
|
1094
836
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
}
|
|
1098
|
-
},
|
|
837
|
+
}
|
|
838
|
+
return stringifyToolOutput(result, result.ok === true ? "committed" : "not_applied");
|
|
839
|
+
},
|
|
840
|
+
}),
|
|
1099
841
|
},
|
|
842
|
+
"tool.definition": toolDefinitionAdapter,
|
|
1100
843
|
"tool.execute.before": async (input, output) => {
|
|
844
|
+
// Owned workflow tools get strict structural plus branch validation on
|
|
845
|
+
// the raw forwarded arguments before their handlers run. This runs before
|
|
846
|
+
// the non-task early return and never mutates output.args.
|
|
847
|
+
if (isWorkflowToolId(input.tool)) {
|
|
848
|
+
const validation = validateWorkflowToolInput(input.tool, output.args);
|
|
849
|
+
if (!validation.ok) {
|
|
850
|
+
throw new ContractInputError(input.tool, validation.issues);
|
|
851
|
+
}
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
1101
854
|
if (input.tool !== "task") {
|
|
1102
855
|
return;
|
|
1103
856
|
}
|
|
@@ -1248,6 +1001,9 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
1248
1001
|
const planRunId = workItem.delegated?.planRunId;
|
|
1249
1002
|
if (planRunId) {
|
|
1250
1003
|
const run = data.planRuns.get(planRunId);
|
|
1004
|
+
if (run?.status === "sealed") {
|
|
1005
|
+
throw new Error(`LAUNCH_REJECTED_SEALED: run ${planRunId} is sealed and cannot launch ${workItem.workItemId}.`);
|
|
1006
|
+
}
|
|
1251
1007
|
const binding = run
|
|
1252
1008
|
? [...run.tasks.values()].find((task) => task.workItemId === workItem.workItemId)
|
|
1253
1009
|
: undefined;
|
|
@@ -1489,12 +1245,18 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
1489
1245
|
? [
|
|
1490
1246
|
`Report rejected: attempt ${settledAttempt} of ${header.value} settled as report_rejected with bounded diagnostics retained.`,
|
|
1491
1247
|
(() => {
|
|
1492
|
-
const
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1248
|
+
const store = getOrCreateStore(input.sessionID);
|
|
1249
|
+
const settledRecord = getWorkItem(store, input.sessionID, header.value);
|
|
1250
|
+
const guidance = settledRecord
|
|
1251
|
+
? deriveDelegatedGuidance({
|
|
1252
|
+
record: settledRecord,
|
|
1253
|
+
progress: summarizeDelegatedProgress(settledRecord),
|
|
1254
|
+
latest: latestAttemptView(settledRecord),
|
|
1255
|
+
context: { data: store.getStoreData(), sessionId: input.sessionID },
|
|
1256
|
+
})
|
|
1495
1257
|
: undefined;
|
|
1496
|
-
return `Next action: ${
|
|
1497
|
-
|
|
1258
|
+
return `Next action: ${guidance?.nextAction ?? "inspect work_item_list"}${guidance?.nextAction === "recover" ||
|
|
1259
|
+
guidance?.nextAction === "recover_with_user_authorization"
|
|
1498
1260
|
? ' through work_item_decide decision "recover"'
|
|
1499
1261
|
: ""}.`;
|
|
1500
1262
|
})(),
|