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