@osovv/vv-opencode 1.4.2 → 1.4.3-rc.1
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 +20 -6
- package/dist/lib/orchestration.js +12 -3
- 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/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 +15 -7
- 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 +58 -1
- package/dist/plugins/workflow/checkpoints.js +362 -34
- package/dist/plugins/workflow/checkpoints.js.map +1 -1
- package/dist/plugins/workflow/delegated.d.ts +150 -2
- package/dist/plugins/workflow/delegated.js +551 -15
- 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 +539 -80
- package/dist/plugins/workflow/index.js.map +1 -1
- package/dist/plugins/workflow/persistence.d.ts +18 -4
- package/dist/plugins/workflow/persistence.js +439 -42
- package/dist/plugins/workflow/persistence.js.map +1 -1
- package/dist/plugins/workflow/repair.d.ts +2 -0
- package/dist/plugins/workflow/repair.js +16 -8
- package/dist/plugins/workflow/repair.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 +37 -7
- 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 +81 -6
- package/dist/plugins/workflow/tooling.js +956 -23
- 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-code-reviewer.md +1 -0
- package/templates/agents/vv-controller.md +26 -13
- package/templates/agents/vv-implementer.md +1 -0
- package/templates/agents/vv-spec-reviewer.md +1 -0
- package/templates/skills/vv-execute/SKILL.md +6 -5
- package/templates/skills/vv-plan/SKILL.md +1 -1
- package/templates/skills/vv-review/SKILL.md +1 -0
- package/templates/skills/vv-spec/SKILL.md +2 -2
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// FILE: src/plugins/workflow/tooling.ts
|
|
2
|
-
// VERSION: 0.
|
|
2
|
+
// VERSION: 0.4.0
|
|
3
3
|
// START_MODULE_CONTRACT
|
|
4
4
|
// PURPOSE: Provide work-item tooling handlers that wrap explicit workflow state operations with structured protocol-friendly responses.
|
|
5
|
-
// SCOPE: work_item_open, work_item_list, and work_item_close tool definitions with delegated-mode open validation and mode-specific serialization; work_item_decide and work_checkpoint control-tool definitions wrapping delegated decisions, plan registration, checkpoint start/verify,
|
|
6
|
-
// DEPENDS: [src/plugins/workflow/checkpoint-io.ts, src/plugins/workflow/checkpoints.ts, src/plugins/workflow/delegated.ts, src/plugins/workflow/state.ts]
|
|
7
|
-
// LINKS: M-WORKFLOW-TOOLING, M-WORKFLOW-DELEGATED, M-WORKFLOW-CHECKPOINTS, M-PLUGIN-WORKFLOW
|
|
5
|
+
// SCOPE: work_item_open, work_item_list, and work_item_close tool definitions with delegated-mode open validation and mode-specific serialization including recovery-aware progress summaries; generic execution registration/append through an optional execution descriptor or runId; work_item_decide and work_checkpoint control-tool definitions wrapping delegated decisions, native plan registration, checkpoint start/verify, failed-checkpoint rework authorization, bounded recover for stopped or exhausted targets with optional root-user message authorization through a read-only lookup, and the generic (non-native) checkpoint, completion, amendment, advance-authority, stage-approval, and revocation actions.
|
|
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/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
|
|
8
8
|
// ROLE: RUNTIME
|
|
9
9
|
// MAP_MODE: EXPORTS
|
|
10
10
|
// END_MODULE_CONTRACT
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
// START_MODULE_MAP
|
|
13
13
|
// WorkflowToolContext - Minimal execution context required by workflow tools.
|
|
14
14
|
// WorkflowToolDefinition - Deterministic tool definition shape with an optionally async execute handler.
|
|
15
|
+
// DelegatedControlOptions - Optional read-only authorization lookup bound to the plugin SDK client.
|
|
15
16
|
// createWorkItemOpenTool - Creates work_item_open tool wrapper around explicit openWorkItem contract including standalone delegated tasks.
|
|
16
|
-
// createWorkItemListTool - Creates work_item_list tool wrapper with mode, round metadata, delegated acceptance state, and registered plan runs.
|
|
17
|
+
// createWorkItemListTool - Creates work_item_list tool wrapper with mode, round metadata, delegated acceptance and recovery state, and registered plan runs.
|
|
17
18
|
// createWorkItemCloseTool - Creates work_item_close tool wrapper with ready_to_close gating responses.
|
|
18
|
-
// createWorkItemDecideTool - Creates work_item_decide control wrapper around decideDelegatedWorkItem and
|
|
19
|
-
// createWorkCheckpointTool - Creates work_checkpoint control wrapper around plan registration, checkpoint start, and
|
|
19
|
+
// createWorkItemDecideTool - Creates work_item_decide control wrapper around decideDelegatedWorkItem, rework authorization, and bounded recovery.
|
|
20
|
+
// createWorkCheckpointTool - Creates work_checkpoint control wrapper around plan registration, checkpoint start, verify, checkpoint recovery, and generic checkpoint/completion/amendment/authority actions.
|
|
21
|
+
// DelegatedControlOptions - Optional read-only authorization lookups bound to the plugin SDK client.
|
|
20
22
|
// DecideArgs - work_item_decide tool argument shape.
|
|
21
23
|
// CheckpointArgs - work_checkpoint tool argument shape.
|
|
22
24
|
// WorkCheckpointRegisterInput - Register-action input discriminator.
|
|
@@ -25,11 +27,13 @@
|
|
|
25
27
|
// END_MODULE_MAP
|
|
26
28
|
//
|
|
27
29
|
// START_CHANGE_SUMMARY
|
|
28
|
-
// LAST_CHANGE: [C-
|
|
30
|
+
// LAST_CHANGE: [C-WORKFLOW-PLAN-INDEPENDENCE - Added generic execution registration/append through work_item_open and generic checkpoint, completion, amendment, advance-authority, stage-approval, and revocation actions through work_checkpoint, plus the SDK-backed authority-message lookup seam.]
|
|
29
31
|
// END_CHANGE_SUMMARY
|
|
30
32
|
import { closeWorkItem, getReviewRound, listWorkItems, openWorkItem, } from "./state.js";
|
|
31
|
-
import { authorizeReworkFromFailedCheckpoint, getDelegatedRunView, registerDelegatedPlan, startDelegatedCheckpoint, verifyDelegatedCheckpoint, } from "./checkpoints.js";
|
|
32
|
-
import { currentDelegatedAcceptance, decideDelegatedWorkItem } from "./delegated.js";
|
|
33
|
+
import { authorizeReworkFromFailedCheckpoint, getDelegatedRunView, recoverDelegatedCheckpoint, registerDelegatedPlan, startDelegatedCheckpoint, verifyDelegatedCheckpoint, } from "./checkpoints.js";
|
|
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";
|
|
33
37
|
function coerceNonEmptyString(value) {
|
|
34
38
|
if (typeof value !== "string")
|
|
35
39
|
return undefined;
|
|
@@ -56,6 +60,89 @@ function canonicalizeReviewers(value) {
|
|
|
56
60
|
return left === "spec" ? -1 : 1;
|
|
57
61
|
});
|
|
58
62
|
}
|
|
63
|
+
// START_BLOCK_GENERIC_NORMALIZATION
|
|
64
|
+
function isWorkflowReviewer(value) {
|
|
65
|
+
return value === "spec" || value === "code";
|
|
66
|
+
}
|
|
67
|
+
function stringList(value) {
|
|
68
|
+
return Array.isArray(value) ? value.map(String) : [];
|
|
69
|
+
}
|
|
70
|
+
/** Normalize one tool item into a bounded generic task contract. */
|
|
71
|
+
function normalizeTaskContract(item) {
|
|
72
|
+
const taskId = coerceNonEmptyString(item.taskId) ?? coerceNonEmptyString(item.key);
|
|
73
|
+
const title = coerceNonEmptyString(item.title) ?? taskId;
|
|
74
|
+
if (!taskId || !title) {
|
|
75
|
+
return { ok: false, message: "each task item requires a non-empty taskId (or key) and title" };
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(item.requiredReviewers)) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
message: "requiredReviewers must be declared explicitly for each task (use [] for no independent review)",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
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
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
contract: {
|
|
90
|
+
taskId,
|
|
91
|
+
title,
|
|
92
|
+
goal: coerceNonEmptyString(item.goal) ?? title,
|
|
93
|
+
acceptanceCriteria: stringList(item.acceptanceCriteria),
|
|
94
|
+
verification: stringList(item.verification),
|
|
95
|
+
writeScope: stringList(item.writeScope),
|
|
96
|
+
dependsOn: stringList(item.dependsOn),
|
|
97
|
+
blockedBy: stringList(item.blockedBy),
|
|
98
|
+
requiredReviewers: [...reviewers],
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function normalizeExecutionSource(raw) {
|
|
103
|
+
if (raw === null || typeof raw !== "object")
|
|
104
|
+
return undefined;
|
|
105
|
+
const candidate = raw;
|
|
106
|
+
if (candidate.kind === "conversation-scoped")
|
|
107
|
+
return { kind: "conversation-scoped" };
|
|
108
|
+
if (candidate.kind === "provided-plan") {
|
|
109
|
+
const reference = coerceNonEmptyString(candidate.reference);
|
|
110
|
+
if (!reference)
|
|
111
|
+
return undefined;
|
|
112
|
+
const sha256 = coerceNonEmptyString(candidate.sha256);
|
|
113
|
+
return { kind: "provided-plan", reference, ...(sha256 ? { sha256 } : {}) };
|
|
114
|
+
}
|
|
115
|
+
// Native packages must register through work_checkpoint register with a
|
|
116
|
+
// planPath; the generic descriptor never fabricates a native source.
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
function normalizeBoundary(raw) {
|
|
120
|
+
if (raw === null || typeof raw !== "object")
|
|
121
|
+
return undefined;
|
|
122
|
+
const candidate = raw;
|
|
123
|
+
const files = stringList(candidate.files);
|
|
124
|
+
const directories = stringList(candidate.directories);
|
|
125
|
+
if (files.length === 0 && directories.length === 0)
|
|
126
|
+
return undefined;
|
|
127
|
+
return { files, directories };
|
|
128
|
+
}
|
|
129
|
+
function normalizeCheckpointContracts(raw) {
|
|
130
|
+
return Array.isArray(raw) ? raw : [];
|
|
131
|
+
}
|
|
132
|
+
function taskContractsFromItems(items) {
|
|
133
|
+
const contracts = [];
|
|
134
|
+
for (const item of items) {
|
|
135
|
+
const normalized = normalizeTaskContract(item);
|
|
136
|
+
if (!normalized.ok)
|
|
137
|
+
return { ok: false, message: normalized.message };
|
|
138
|
+
contracts.push(normalized.contract);
|
|
139
|
+
}
|
|
140
|
+
if (contracts.length === 0) {
|
|
141
|
+
return { ok: false, message: "at least one task item is required" };
|
|
142
|
+
}
|
|
143
|
+
return { ok: true, contracts };
|
|
144
|
+
}
|
|
145
|
+
// END_BLOCK_GENERIC_NORMALIZATION
|
|
59
146
|
function normalizeOpenInputItem(item, sessionId) {
|
|
60
147
|
const key = coerceNonEmptyString(item.key);
|
|
61
148
|
const title = coerceNonEmptyString(item.title);
|
|
@@ -171,11 +258,24 @@ function serializeWorkItem(record) {
|
|
|
171
258
|
accepted: currentDelegatedAcceptance(record) !== undefined,
|
|
172
259
|
acceptedAttempt: currentDelegatedAcceptance(record)?.attempt,
|
|
173
260
|
reworkCount: record.delegated.reworkHistory.length,
|
|
261
|
+
...serializeProgress(record),
|
|
174
262
|
},
|
|
175
263
|
}
|
|
176
264
|
: {}),
|
|
177
265
|
};
|
|
178
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
|
+
}
|
|
179
279
|
// START_CONTRACT: createWorkItemOpenTool
|
|
180
280
|
// PURPOSE: Build work_item_open handler that supports deterministic batch idempotent open operations with explicit workflow intent.
|
|
181
281
|
// INPUTS: { store: WorkItemStore - workflow in-memory store }
|
|
@@ -189,6 +289,114 @@ export function createWorkItemOpenTool(store) {
|
|
|
189
289
|
description: "Open one or more workflow work items idempotently with explicit mode and requiredReviewers.",
|
|
190
290
|
execute: (args, context, overrideStore) => {
|
|
191
291
|
const inputItems = Array.isArray(args.items) ? args.items : [];
|
|
292
|
+
const runIdArg = coerceNonEmptyString(args.runId);
|
|
293
|
+
const executionArg = args.execution;
|
|
294
|
+
if (executionArg !== undefined && runIdArg !== undefined) {
|
|
295
|
+
return {
|
|
296
|
+
tool: "work_item_open",
|
|
297
|
+
sessionId: context.sessionId,
|
|
298
|
+
ok: false,
|
|
299
|
+
errorCode: "INVALID_INPUT",
|
|
300
|
+
message: "INVALID_INPUT: execution and runId are mutually exclusive",
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (executionArg !== undefined || runIdArg !== undefined) {
|
|
304
|
+
const s = overrideStore ?? store;
|
|
305
|
+
const data = s.getStoreData();
|
|
306
|
+
const contracts = taskContractsFromItems(inputItems);
|
|
307
|
+
if (!contracts.ok) {
|
|
308
|
+
return {
|
|
309
|
+
tool: "work_item_open",
|
|
310
|
+
sessionId: context.sessionId,
|
|
311
|
+
ok: false,
|
|
312
|
+
errorCode: "INVALID_INPUT",
|
|
313
|
+
message: `INVALID_INPUT: ${contracts.message}`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (executionArg !== undefined) {
|
|
317
|
+
const descriptor = executionArg !== null && typeof executionArg === "object"
|
|
318
|
+
? executionArg
|
|
319
|
+
: {};
|
|
320
|
+
const executionKey = coerceNonEmptyString(descriptor.executionKey);
|
|
321
|
+
const source = normalizeExecutionSource(descriptor.source);
|
|
322
|
+
const boundary = normalizeBoundary(descriptor.boundary);
|
|
323
|
+
const goal = coerceNonEmptyString(descriptor.goal);
|
|
324
|
+
const workspaceRoot = coerceNonEmptyString(context.workspaceRoot);
|
|
325
|
+
if (!executionKey || !source || !boundary || !goal || !workspaceRoot) {
|
|
326
|
+
return {
|
|
327
|
+
tool: "work_item_open",
|
|
328
|
+
sessionId: context.sessionId,
|
|
329
|
+
ok: false,
|
|
330
|
+
errorCode: "INVALID_INPUT",
|
|
331
|
+
message: "INVALID_INPUT: execution requires executionKey, source, goal, boundary, and the trusted workspace root",
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const registered = registerExecutionInStore(data, {
|
|
335
|
+
sessionId: context.sessionId,
|
|
336
|
+
workspaceRoot,
|
|
337
|
+
executionKey,
|
|
338
|
+
source,
|
|
339
|
+
goal,
|
|
340
|
+
boundary,
|
|
341
|
+
tasks: contracts.contracts.map((contract) => ({ contract })),
|
|
342
|
+
checkpoints: normalizeCheckpointContracts(descriptor.checkpoints),
|
|
343
|
+
});
|
|
344
|
+
if (!registered.ok) {
|
|
345
|
+
return {
|
|
346
|
+
tool: "work_item_open",
|
|
347
|
+
sessionId: context.sessionId,
|
|
348
|
+
ok: false,
|
|
349
|
+
errorCode: registered.errorCode,
|
|
350
|
+
message: registered.message,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
tool: "work_item_open",
|
|
355
|
+
sessionId: context.sessionId,
|
|
356
|
+
ok: true,
|
|
357
|
+
action: "register",
|
|
358
|
+
runId: registered.runId,
|
|
359
|
+
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
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const appended = appendExecutionWorkInStore(data, {
|
|
375
|
+
sessionId: context.sessionId,
|
|
376
|
+
runId: runIdArg,
|
|
377
|
+
amendmentId,
|
|
378
|
+
rationale,
|
|
379
|
+
tasks: contracts.contracts.map((contract) => ({ contract })),
|
|
380
|
+
});
|
|
381
|
+
if (!appended.ok) {
|
|
382
|
+
return {
|
|
383
|
+
tool: "work_item_open",
|
|
384
|
+
sessionId: context.sessionId,
|
|
385
|
+
ok: false,
|
|
386
|
+
errorCode: appended.errorCode,
|
|
387
|
+
message: appended.message,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
tool: "work_item_open",
|
|
392
|
+
sessionId: context.sessionId,
|
|
393
|
+
ok: true,
|
|
394
|
+
action: "amend",
|
|
395
|
+
runId: runIdArg,
|
|
396
|
+
revision: appended.revision,
|
|
397
|
+
execution: getExecutionView(appended.execution),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
192
400
|
const results = inputItems.map((item) => {
|
|
193
401
|
const normalized = normalizeOpenInputItem(item, context.sessionId);
|
|
194
402
|
if (!normalized.ok) {
|
|
@@ -299,17 +507,17 @@ export function createWorkItemCloseTool(store) {
|
|
|
299
507
|
};
|
|
300
508
|
}
|
|
301
509
|
// START_CONTRACT: createWorkItemDecideTool
|
|
302
|
-
// PURPOSE: Build work_item_decide handler wrapping explicit controller acceptance, change requests,
|
|
303
|
-
// INPUTS: { store: WorkItemStore - workflow in-memory store }
|
|
304
|
-
// OUTPUTS: { WorkflowToolDefinition<DecideArgs, Record<string, unknown
|
|
510
|
+
// PURPOSE: Build work_item_decide handler wrapping explicit controller acceptance, change requests, checkpoint-authorized rework, and bounded recovery.
|
|
511
|
+
// INPUTS: { store: WorkItemStore - workflow in-memory store, options?: DelegatedControlOptions - optional read-only authorization lookup }
|
|
512
|
+
// OUTPUTS: { WorkflowToolDefinition<DecideArgs, Promise<Record<string, unknown>>> - async executable control tool definition }
|
|
305
513
|
// SIDE_EFFECTS: [Mutates delegated work-item state through the domain layer]
|
|
306
514
|
// LINKS: [M-WORKFLOW-TOOLING, M-WORKFLOW-DELEGATED, M-WORKFLOW-CHECKPOINTS]
|
|
307
515
|
// END_CONTRACT: createWorkItemDecideTool
|
|
308
|
-
export function createWorkItemDecideTool(store) {
|
|
516
|
+
export function createWorkItemDecideTool(store, options) {
|
|
309
517
|
return {
|
|
310
518
|
name: "work_item_decide",
|
|
311
|
-
description: "Accept or request changes for the current completed delegated attempt,
|
|
312
|
-
execute
|
|
519
|
+
description: "Accept or request changes for the current completed delegated attempt, authorize bounded rework of an accepted task from a failed checkpoint, or recover a stopped or exhausted unaccepted task with a bounded diagnosis and changed condition.",
|
|
520
|
+
async execute(args, context, overrideStore) {
|
|
313
521
|
const s = overrideStore ?? store;
|
|
314
522
|
const workItemId = coerceNonEmptyString(args.workItemId);
|
|
315
523
|
const decision = args.decision;
|
|
@@ -363,13 +571,148 @@ export function createWorkItemDecideTool(store) {
|
|
|
363
571
|
state: s.getWorkItem(context.sessionId, workItemId)?.state,
|
|
364
572
|
};
|
|
365
573
|
}
|
|
574
|
+
if (decision === "recover") {
|
|
575
|
+
const recoveryId = coerceNonEmptyString(args.recoveryId);
|
|
576
|
+
const diagnosis = typeof args.diagnosis === "string" ? args.diagnosis : "";
|
|
577
|
+
const changedCondition = typeof args.changedCondition === "string" ? args.changedCondition : "";
|
|
578
|
+
const verification = Array.isArray(args.verification) ? args.verification.map(String) : [];
|
|
579
|
+
const userMessageId = coerceNonEmptyString(args.userMessageId);
|
|
580
|
+
const authorityId = coerceNonEmptyString(args.authorityId);
|
|
581
|
+
if (!recoveryId) {
|
|
582
|
+
return {
|
|
583
|
+
tool: "work_item_decide",
|
|
584
|
+
sessionId: context.sessionId,
|
|
585
|
+
ok: false,
|
|
586
|
+
errorCode: "INVALID_INPUT",
|
|
587
|
+
message: "INVALID_INPUT: recover requires a stable recoveryId, diagnosis, changedCondition, and verification references",
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
let advanceGrantApproved = false;
|
|
591
|
+
let advanceRunId;
|
|
592
|
+
if (authorityId) {
|
|
593
|
+
advanceRunId = coerceNonEmptyString(args.runId);
|
|
594
|
+
if (!advanceRunId) {
|
|
595
|
+
return {
|
|
596
|
+
tool: "work_item_decide",
|
|
597
|
+
sessionId: context.sessionId,
|
|
598
|
+
ok: false,
|
|
599
|
+
errorCode: "INVALID_INPUT",
|
|
600
|
+
message: "INVALID_INPUT: authorityId recovery requires the owning runId",
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
const execution = findExecution(s.getStoreData(), advanceRunId);
|
|
604
|
+
const authority = execution?.authority.find((entry) => entry.authorityId === authorityId);
|
|
605
|
+
if (!execution || !authority) {
|
|
606
|
+
return {
|
|
607
|
+
tool: "work_item_decide",
|
|
608
|
+
sessionId: context.sessionId,
|
|
609
|
+
ok: false,
|
|
610
|
+
errorCode: "AUTHORITY_NOT_FOUND",
|
|
611
|
+
message: `AUTHORITY_NOT_FOUND: no recorded authority ${authorityId} for ${advanceRunId}`,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
// Scope binding: the recovered item must be a task of the execution
|
|
615
|
+
// whose reserve funds the recovery.
|
|
616
|
+
if (![...execution.tasks.values()].some((task) => task.workItemId === workItemId)) {
|
|
617
|
+
return {
|
|
618
|
+
tool: "work_item_decide",
|
|
619
|
+
sessionId: context.sessionId,
|
|
620
|
+
ok: false,
|
|
621
|
+
errorCode: "INVALID_INPUT",
|
|
622
|
+
message: `INVALID_INPUT: work item ${workItemId} is not a task of execution ${advanceRunId}`,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const proposed = proposeReserveDebit({
|
|
626
|
+
authority,
|
|
627
|
+
debits: execution.reserveDebits,
|
|
628
|
+
recoveryId,
|
|
629
|
+
targetKind: "task",
|
|
630
|
+
targetId: workItemId,
|
|
631
|
+
});
|
|
632
|
+
// A failed proposal only means no advance unit is available; the
|
|
633
|
+
// recovery may still be a cost-free resume or use an ordinary
|
|
634
|
+
// allowance, so let the domain reducer decide.
|
|
635
|
+
advanceGrantApproved = proposed.ok;
|
|
636
|
+
}
|
|
637
|
+
const recovered = await recoverDelegatedWorkItem(s, {
|
|
638
|
+
sessionId: context.sessionId,
|
|
639
|
+
workItemId,
|
|
640
|
+
attempt: args.attempt,
|
|
641
|
+
diagnosis,
|
|
642
|
+
changedCondition,
|
|
643
|
+
verification,
|
|
644
|
+
recoveryId,
|
|
645
|
+
...(advanceGrantApproved ? { advanceGrantApproved: true } : {}),
|
|
646
|
+
...(userMessageId !== undefined
|
|
647
|
+
? { userMessageId, lookupUserMessage: options?.lookupUserMessage }
|
|
648
|
+
: {}),
|
|
649
|
+
});
|
|
650
|
+
if (!recovered.ok) {
|
|
651
|
+
return {
|
|
652
|
+
tool: "work_item_decide",
|
|
653
|
+
sessionId: context.sessionId,
|
|
654
|
+
ok: false,
|
|
655
|
+
errorCode: recovered.errorCode,
|
|
656
|
+
message: recovered.message,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (recovered.kind === "advance_grant" && advanceRunId !== undefined) {
|
|
660
|
+
const execution = findExecution(s.getStoreData(), advanceRunId);
|
|
661
|
+
const authority = execution?.authority.find((entry) => entry.authorityId === authorityId);
|
|
662
|
+
const proposed = authority
|
|
663
|
+
? proposeReserveDebit({
|
|
664
|
+
authority,
|
|
665
|
+
debits: execution?.reserveDebits ?? [],
|
|
666
|
+
recoveryId,
|
|
667
|
+
targetKind: "task",
|
|
668
|
+
targetId: workItemId,
|
|
669
|
+
})
|
|
670
|
+
: undefined;
|
|
671
|
+
if (!proposed?.ok) {
|
|
672
|
+
return {
|
|
673
|
+
tool: "work_item_decide",
|
|
674
|
+
sessionId: context.sessionId,
|
|
675
|
+
ok: false,
|
|
676
|
+
errorCode: proposed?.code ?? "RESERVE_EXHAUSTED",
|
|
677
|
+
message: proposed?.message ?? "advance recovery debit could not be recorded with the grant",
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
const stored = addReserveDebitInStore(s.getStoreData(), {
|
|
681
|
+
sessionId: context.sessionId,
|
|
682
|
+
runId: advanceRunId,
|
|
683
|
+
debit: proposed.value,
|
|
684
|
+
});
|
|
685
|
+
if (!stored.ok) {
|
|
686
|
+
return {
|
|
687
|
+
tool: "work_item_decide",
|
|
688
|
+
sessionId: context.sessionId,
|
|
689
|
+
ok: false,
|
|
690
|
+
errorCode: stored.errorCode,
|
|
691
|
+
message: stored.message,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
tool: "work_item_decide",
|
|
697
|
+
sessionId: context.sessionId,
|
|
698
|
+
ok: true,
|
|
699
|
+
action: "recover",
|
|
700
|
+
workItemId,
|
|
701
|
+
recoveryId: recovered.recoveryId,
|
|
702
|
+
kind: recovered.kind,
|
|
703
|
+
attemptBudget: recovered.attemptBudget,
|
|
704
|
+
remainingAttempts: recovered.remainingAttempts,
|
|
705
|
+
state: recovered.record.state,
|
|
706
|
+
nextAction: summarizeDelegatedProgress(recovered.record).nextAction,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
366
709
|
if (decision !== "accept" && decision !== "request_changes") {
|
|
367
710
|
return {
|
|
368
711
|
tool: "work_item_decide",
|
|
369
712
|
sessionId: context.sessionId,
|
|
370
713
|
ok: false,
|
|
371
714
|
errorCode: "INVALID_INPUT",
|
|
372
|
-
message: "INVALID_INPUT: decision must be accept, request_changes, or
|
|
715
|
+
message: "INVALID_INPUT: decision must be accept, request_changes, rework, or recover",
|
|
373
716
|
};
|
|
374
717
|
}
|
|
375
718
|
const concernsDisposition = typeof args.concernsDisposition === "string" ? args.concernsDisposition : undefined;
|
|
@@ -404,21 +747,479 @@ export function createWorkItemDecideTool(store) {
|
|
|
404
747
|
},
|
|
405
748
|
};
|
|
406
749
|
}
|
|
750
|
+
// START_CONTRACT: executeGenericCheckpoint
|
|
751
|
+
// PURPOSE: Execute one generic (non-native) checkpoint/authority action against the common execution registry.
|
|
752
|
+
// INPUTS: { data, sessionId, runId, args, control? }
|
|
753
|
+
// OUTPUTS: { Promise<Record<string, unknown>> - structured tool result }
|
|
754
|
+
// SIDE_EFFECTS: [Mutates the common execution registry and may consult the SDK-backed authority lookup]
|
|
755
|
+
// LINKS: [M-WORKFLOW-TOOLING, M-WORKFLOW-EXECUTION, M-WORKFLOW-AUTHORITY]
|
|
756
|
+
// END_CONTRACT: executeGenericCheckpoint
|
|
757
|
+
async function executeGenericCheckpoint(options) {
|
|
758
|
+
const { data, sessionId, runId, args } = options;
|
|
759
|
+
const base = { tool: "work_checkpoint", sessionId, runId };
|
|
760
|
+
const execution = findExecution(data, runId);
|
|
761
|
+
if (!execution) {
|
|
762
|
+
return {
|
|
763
|
+
...base,
|
|
764
|
+
ok: false,
|
|
765
|
+
errorCode: "EXECUTION_NOT_FOUND",
|
|
766
|
+
message: `no execution ${runId}`,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
if (execution.sessionId !== sessionId) {
|
|
770
|
+
return {
|
|
771
|
+
...base,
|
|
772
|
+
ok: false,
|
|
773
|
+
errorCode: "SESSION_MISMATCH",
|
|
774
|
+
message: "execution belongs to another session",
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const amendmentId = coerceNonEmptyString(args.amendmentId);
|
|
778
|
+
const rationale = coerceNonEmptyString(args.rationale);
|
|
779
|
+
switch (args.action) {
|
|
780
|
+
case "register":
|
|
781
|
+
case "amend": {
|
|
782
|
+
if (!amendmentId || !rationale) {
|
|
783
|
+
return {
|
|
784
|
+
...base,
|
|
785
|
+
ok: false,
|
|
786
|
+
errorCode: "INVALID_INPUT",
|
|
787
|
+
message: "generic register/amend requires amendmentId and rationale",
|
|
788
|
+
};
|
|
789
|
+
}
|
|
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
|
+
const appended = appendExecutionWorkInStore(data, {
|
|
796
|
+
sessionId,
|
|
797
|
+
runId,
|
|
798
|
+
amendmentId,
|
|
799
|
+
rationale,
|
|
800
|
+
tasks: contracts.contracts.map((contract) => ({ contract })),
|
|
801
|
+
checkpoints: normalizeCheckpointContracts(args.checkpoints),
|
|
802
|
+
});
|
|
803
|
+
if (!appended.ok) {
|
|
804
|
+
return { ...base, ok: false, errorCode: appended.errorCode, message: appended.message };
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
...base,
|
|
808
|
+
ok: true,
|
|
809
|
+
action: "amend",
|
|
810
|
+
revision: appended.revision,
|
|
811
|
+
execution: getExecutionView(appended.execution),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
case "start": {
|
|
815
|
+
const checkpointId = coerceNonEmptyString(args.checkpointId);
|
|
816
|
+
if (!checkpointId) {
|
|
817
|
+
return {
|
|
818
|
+
...base,
|
|
819
|
+
ok: false,
|
|
820
|
+
errorCode: "INVALID_INPUT",
|
|
821
|
+
message: "start requires checkpointId",
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
const startFingerprint = coerceNonEmptyString(args.startFingerprint);
|
|
825
|
+
const started = startGenericCheckpointInStore(data, {
|
|
826
|
+
sessionId,
|
|
827
|
+
runId,
|
|
828
|
+
checkpointId,
|
|
829
|
+
...(startFingerprint ? { startFingerprint } : {}),
|
|
830
|
+
});
|
|
831
|
+
if (!started.ok) {
|
|
832
|
+
return { ...base, ok: false, errorCode: started.errorCode, message: started.message };
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
...base,
|
|
836
|
+
ok: true,
|
|
837
|
+
action: "start",
|
|
838
|
+
checkpointId,
|
|
839
|
+
generation: started.checkpoint.currentReview?.generation,
|
|
840
|
+
reviewWorkItemId: started.reviewWorkItemId,
|
|
841
|
+
header: started.header,
|
|
842
|
+
reviewersToLaunch: started.reviewers,
|
|
843
|
+
coveredAttemptIds: started.coveredAttemptIds,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
case "review":
|
|
847
|
+
case "bind":
|
|
848
|
+
case "verify": {
|
|
849
|
+
const checkpointId = coerceNonEmptyString(args.checkpointId);
|
|
850
|
+
if (!checkpointId) {
|
|
851
|
+
return {
|
|
852
|
+
...base,
|
|
853
|
+
ok: false,
|
|
854
|
+
errorCode: "INVALID_INPUT",
|
|
855
|
+
message: "checkpointId is required",
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
const reviewer = coerceNonEmptyString(args.reviewer);
|
|
859
|
+
if (reviewer !== undefined && reviewer !== "spec" && reviewer !== "code") {
|
|
860
|
+
return {
|
|
861
|
+
...base,
|
|
862
|
+
ok: false,
|
|
863
|
+
errorCode: "INVALID_INPUT",
|
|
864
|
+
message: "reviewer must be spec or code",
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
// The reviewer status is read from the linked review_only work item's
|
|
868
|
+
// recorded round; callers cannot assert a reviewer outcome directly.
|
|
869
|
+
const recorded = recordGenericReviewerResultInStore(data, {
|
|
870
|
+
sessionId,
|
|
871
|
+
runId,
|
|
872
|
+
checkpointId,
|
|
873
|
+
...(reviewer ? { reviewer } : {}),
|
|
874
|
+
});
|
|
875
|
+
if (!recorded.ok) {
|
|
876
|
+
return { ...base, ok: false, errorCode: recorded.errorCode, message: recorded.message };
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
...base,
|
|
880
|
+
ok: true,
|
|
881
|
+
action: args.action,
|
|
882
|
+
checkpointId,
|
|
883
|
+
...(reviewer ? { reviewer } : {}),
|
|
884
|
+
outcome: recorded.outcome,
|
|
885
|
+
checkpointStatus: recorded.checkpoint.status,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
case "recover": {
|
|
889
|
+
const checkpointId = coerceNonEmptyString(args.checkpointId);
|
|
890
|
+
const recoveryId = coerceNonEmptyString(args.recoveryId);
|
|
891
|
+
const diagnosis = typeof args.diagnosis === "string" ? args.diagnosis : "";
|
|
892
|
+
const changedCondition = typeof args.changedCondition === "string" ? args.changedCondition : "";
|
|
893
|
+
const verification = stringList(args.verification);
|
|
894
|
+
const authorityId = coerceNonEmptyString(args.authorityId);
|
|
895
|
+
if (!checkpointId || !recoveryId) {
|
|
896
|
+
return {
|
|
897
|
+
...base,
|
|
898
|
+
ok: false,
|
|
899
|
+
errorCode: "INVALID_INPUT",
|
|
900
|
+
message: "generic checkpoint recovery requires checkpointId and recoveryId",
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
const checkpointBinding = execution.checkpoints.get(checkpointId);
|
|
904
|
+
const stopped = checkpointBinding?.stoppedAtGeneration !== undefined;
|
|
905
|
+
// Validate and reserve the advance unit BEFORE mutating the checkpoint,
|
|
906
|
+
// so a failed debit can never leave a granted generation behind.
|
|
907
|
+
let reservedDebit;
|
|
908
|
+
if (!stopped) {
|
|
909
|
+
if (!authorityId) {
|
|
910
|
+
return {
|
|
911
|
+
...base,
|
|
912
|
+
ok: false,
|
|
913
|
+
errorCode: "INVALID_INPUT",
|
|
914
|
+
message: "an exhausted generic checkpoint requires an authorityId to recover",
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
const authority = execution.authority.find((entry) => entry.authorityId === authorityId);
|
|
918
|
+
if (!authority) {
|
|
919
|
+
return {
|
|
920
|
+
...base,
|
|
921
|
+
ok: false,
|
|
922
|
+
errorCode: "AUTHORITY_NOT_FOUND",
|
|
923
|
+
message: `AUTHORITY_NOT_FOUND: no recorded authority ${authorityId}`,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
const proposed = proposeReserveDebit({
|
|
927
|
+
authority,
|
|
928
|
+
debits: execution.reserveDebits,
|
|
929
|
+
recoveryId,
|
|
930
|
+
targetKind: "checkpoint",
|
|
931
|
+
targetId: checkpointId,
|
|
932
|
+
});
|
|
933
|
+
if (!proposed.ok) {
|
|
934
|
+
return { ...base, ok: false, errorCode: proposed.code, message: proposed.message };
|
|
935
|
+
}
|
|
936
|
+
reservedDebit = proposed.value;
|
|
937
|
+
}
|
|
938
|
+
const recovered = recoverGenericCheckpointInStore(data, {
|
|
939
|
+
sessionId,
|
|
940
|
+
runId,
|
|
941
|
+
checkpointId,
|
|
942
|
+
recoveryId,
|
|
943
|
+
diagnosis,
|
|
944
|
+
changedCondition,
|
|
945
|
+
verification,
|
|
946
|
+
authorityGrant: reservedDebit !== undefined,
|
|
947
|
+
});
|
|
948
|
+
if (!recovered.ok) {
|
|
949
|
+
return { ...base, ok: false, errorCode: recovered.errorCode, message: recovered.message };
|
|
950
|
+
}
|
|
951
|
+
if (recovered.kind === "advance_grant" && reservedDebit) {
|
|
952
|
+
const stored = addReserveDebitInStore(data, {
|
|
953
|
+
sessionId,
|
|
954
|
+
runId,
|
|
955
|
+
debit: reservedDebit,
|
|
956
|
+
});
|
|
957
|
+
if (!stored.ok) {
|
|
958
|
+
return { ...base, ok: false, errorCode: stored.errorCode, message: stored.message };
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
...base,
|
|
963
|
+
ok: true,
|
|
964
|
+
action: "recover",
|
|
965
|
+
checkpointId,
|
|
966
|
+
recoveryId,
|
|
967
|
+
kind: recovered.kind,
|
|
968
|
+
checkpointStatus: recovered.checkpoint.status,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
case "complete": {
|
|
972
|
+
const completed = completeExecutionInStore(data, {
|
|
973
|
+
sessionId,
|
|
974
|
+
runId,
|
|
975
|
+
rationale: rationale ?? "Generic execution completed with controller acceptance.",
|
|
976
|
+
evidence: stringList(args.verification),
|
|
977
|
+
});
|
|
978
|
+
if (!completed.ok) {
|
|
979
|
+
return { ...base, ok: false, errorCode: completed.errorCode, message: completed.message };
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
...base,
|
|
983
|
+
ok: true,
|
|
984
|
+
action: "complete",
|
|
985
|
+
reviewStatus: completed.reviewStatus,
|
|
986
|
+
execution: getExecutionView(completed.execution),
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
case "authorize": {
|
|
990
|
+
const authorityId = coerceNonEmptyString(args.authorityId);
|
|
991
|
+
const messageId = coerceNonEmptyString(args.messageId);
|
|
992
|
+
const stagesArg = Array.isArray(args.stages) ? args.stages : [];
|
|
993
|
+
const stages = stagesArg.filter((stage) => stage === "specification" ||
|
|
994
|
+
stage === "planning" ||
|
|
995
|
+
stage === "implementation" ||
|
|
996
|
+
stage === "verification");
|
|
997
|
+
if (!authorityId || !messageId || stages.length === 0) {
|
|
998
|
+
return {
|
|
999
|
+
...base,
|
|
1000
|
+
ok: false,
|
|
1001
|
+
errorCode: "INVALID_INPUT",
|
|
1002
|
+
message: "authorize requires authorityId, messageId, and explicit stages",
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
const lookup = options.control?.lookupAuthorityMessage;
|
|
1006
|
+
if (!lookup) {
|
|
1007
|
+
return {
|
|
1008
|
+
...base,
|
|
1009
|
+
ok: false,
|
|
1010
|
+
errorCode: "INVALID_INPUT",
|
|
1011
|
+
message: "authorize requires the SDK-backed authorization lookup",
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
const message = await lookup({ sessionId, runId, messageId });
|
|
1015
|
+
if (!message) {
|
|
1016
|
+
return {
|
|
1017
|
+
...base,
|
|
1018
|
+
ok: false,
|
|
1019
|
+
errorCode: "AUTHORITY_DENIED",
|
|
1020
|
+
message: "authorization message could not be verified as an eligible root-user instruction",
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
const existingAuthority = execution.authority.find((entry) => entry.authorityId === authorityId);
|
|
1024
|
+
if (existingAuthority) {
|
|
1025
|
+
if (existingAuthority.grantedByMessageId === messageId) {
|
|
1026
|
+
// Idempotent replay of the originating registration.
|
|
1027
|
+
return {
|
|
1028
|
+
...base,
|
|
1029
|
+
ok: true,
|
|
1030
|
+
action: "authorize",
|
|
1031
|
+
authorityId,
|
|
1032
|
+
reused: true,
|
|
1033
|
+
units: existingAuthority.initialUnits,
|
|
1034
|
+
availableUnits: advanceUnitsAvailable(existingAuthority, execution.reserveDebits),
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
const extended = extendAdvanceAuthority({
|
|
1038
|
+
authority: existingAuthority,
|
|
1039
|
+
extensionId: `ext-${messageId}`,
|
|
1040
|
+
sessionId,
|
|
1041
|
+
message,
|
|
1042
|
+
messageClaims: data.messageClaims,
|
|
1043
|
+
});
|
|
1044
|
+
if (!extended.ok) {
|
|
1045
|
+
return { ...base, ok: false, errorCode: extended.code, message: extended.message };
|
|
1046
|
+
}
|
|
1047
|
+
const updated = {
|
|
1048
|
+
...existingAuthority,
|
|
1049
|
+
extensions: [...existingAuthority.extensions, extended.value],
|
|
1050
|
+
};
|
|
1051
|
+
const storedExtension = putAuthorityInStore(data, { sessionId, runId, authority: updated });
|
|
1052
|
+
if (!storedExtension.ok) {
|
|
1053
|
+
return {
|
|
1054
|
+
...base,
|
|
1055
|
+
ok: false,
|
|
1056
|
+
errorCode: storedExtension.errorCode,
|
|
1057
|
+
message: storedExtension.message,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
...base,
|
|
1062
|
+
ok: true,
|
|
1063
|
+
action: "authorize",
|
|
1064
|
+
authorityId,
|
|
1065
|
+
extensions: updated.extensions.length,
|
|
1066
|
+
availableUnits: advanceUnitsAvailable(updated, execution.reserveDebits),
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
const granted = grantAdvanceAuthority({
|
|
1070
|
+
authorityId,
|
|
1071
|
+
runId,
|
|
1072
|
+
sessionId,
|
|
1073
|
+
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
|
+
},
|
|
1083
|
+
existingAuthorities: execution.authority,
|
|
1084
|
+
messageClaims: data.messageClaims,
|
|
1085
|
+
});
|
|
1086
|
+
if (!granted.ok) {
|
|
1087
|
+
return { ...base, ok: false, errorCode: granted.code, message: granted.message };
|
|
1088
|
+
}
|
|
1089
|
+
const stored = addAuthorityInStore(data, {
|
|
1090
|
+
sessionId,
|
|
1091
|
+
runId,
|
|
1092
|
+
authority: granted.value.record,
|
|
1093
|
+
claim: granted.value.claim,
|
|
1094
|
+
});
|
|
1095
|
+
if (!stored.ok) {
|
|
1096
|
+
return { ...base, ok: false, errorCode: stored.errorCode, message: stored.message };
|
|
1097
|
+
}
|
|
1098
|
+
return {
|
|
1099
|
+
...base,
|
|
1100
|
+
ok: true,
|
|
1101
|
+
action: "authorize",
|
|
1102
|
+
authorityId,
|
|
1103
|
+
units: granted.value.record.initialUnits,
|
|
1104
|
+
availableUnits: advanceUnitsAvailable(granted.value.record, []),
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
case "record_approval": {
|
|
1108
|
+
const authorityId = coerceNonEmptyString(args.authorityId);
|
|
1109
|
+
const approvalId = coerceNonEmptyString(args.approvalId);
|
|
1110
|
+
const stage = coerceNonEmptyString(args.stage);
|
|
1111
|
+
const artifactPath = coerceNonEmptyString(args.artifactPath);
|
|
1112
|
+
const artifactSha256 = coerceNonEmptyString(args.artifactSha256);
|
|
1113
|
+
const authority = execution.authority.find((entry) => entry.authorityId === authorityId);
|
|
1114
|
+
if (!authority || !approvalId || !stage || !artifactPath || !artifactSha256) {
|
|
1115
|
+
return {
|
|
1116
|
+
...base,
|
|
1117
|
+
ok: false,
|
|
1118
|
+
errorCode: "INVALID_INPUT",
|
|
1119
|
+
message: "record_approval requires a recorded authorityId, approvalId, stage, artifactPath, and artifactSha256",
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
const approval = proposeStageApproval({
|
|
1123
|
+
authority,
|
|
1124
|
+
approvalId,
|
|
1125
|
+
stage,
|
|
1126
|
+
artifactPath,
|
|
1127
|
+
artifactSha256,
|
|
1128
|
+
provenance: "controller_delegated",
|
|
1129
|
+
});
|
|
1130
|
+
if (!approval.ok) {
|
|
1131
|
+
return { ...base, ok: false, errorCode: approval.code, message: approval.message };
|
|
1132
|
+
}
|
|
1133
|
+
const stored = addStageApprovalInStore(data, {
|
|
1134
|
+
sessionId,
|
|
1135
|
+
runId,
|
|
1136
|
+
approval: approval.value,
|
|
1137
|
+
});
|
|
1138
|
+
if (!stored.ok) {
|
|
1139
|
+
return { ...base, ok: false, errorCode: stored.errorCode, message: stored.message };
|
|
1140
|
+
}
|
|
1141
|
+
return {
|
|
1142
|
+
...base,
|
|
1143
|
+
ok: true,
|
|
1144
|
+
action: "record_approval",
|
|
1145
|
+
approvalId,
|
|
1146
|
+
stage,
|
|
1147
|
+
provenance: approval.value.provenance,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
case "revoke_authority": {
|
|
1151
|
+
const authorityId = coerceNonEmptyString(args.authorityId);
|
|
1152
|
+
const revocationId = coerceNonEmptyString(args.revocationId);
|
|
1153
|
+
const authority = execution.authority.find((entry) => entry.authorityId === authorityId);
|
|
1154
|
+
if (!authority || !revocationId) {
|
|
1155
|
+
return {
|
|
1156
|
+
...base,
|
|
1157
|
+
ok: false,
|
|
1158
|
+
errorCode: "INVALID_INPUT",
|
|
1159
|
+
message: "revoke_authority requires a recorded authorityId and a revocationId",
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
const stagesArg = Array.isArray(args.stages) ? args.stages : undefined;
|
|
1163
|
+
const revoked = stagesArg === undefined || stagesArg.length === 0
|
|
1164
|
+
? revokeAdvanceAuthority({
|
|
1165
|
+
authority,
|
|
1166
|
+
revocationId,
|
|
1167
|
+
reason: coerceNonEmptyString(args.rationale) ?? "revoked",
|
|
1168
|
+
})
|
|
1169
|
+
: narrowAdvanceAuthority({
|
|
1170
|
+
authority,
|
|
1171
|
+
revocationId,
|
|
1172
|
+
reason: coerceNonEmptyString(args.rationale) ?? "narrowed",
|
|
1173
|
+
narrowedStages: stagesArg.filter((stage) => stage === "specification" ||
|
|
1174
|
+
stage === "planning" ||
|
|
1175
|
+
stage === "implementation" ||
|
|
1176
|
+
stage === "verification"),
|
|
1177
|
+
});
|
|
1178
|
+
if (!revoked.ok) {
|
|
1179
|
+
return { ...base, ok: false, errorCode: revoked.code, message: revoked.message };
|
|
1180
|
+
}
|
|
1181
|
+
const updated = {
|
|
1182
|
+
...authority,
|
|
1183
|
+
revocations: [...authority.revocations, revoked.value],
|
|
1184
|
+
};
|
|
1185
|
+
const stored = putAuthorityInStore(data, { sessionId, runId, authority: updated });
|
|
1186
|
+
if (!stored.ok) {
|
|
1187
|
+
return { ...base, ok: false, errorCode: stored.errorCode, message: stored.message };
|
|
1188
|
+
}
|
|
1189
|
+
return {
|
|
1190
|
+
...base,
|
|
1191
|
+
ok: true,
|
|
1192
|
+
action: "revoke_authority",
|
|
1193
|
+
authorityId,
|
|
1194
|
+
kind: revoked.value.kind,
|
|
1195
|
+
availableUnits: advanceUnitsAvailable(updated, data.executions.get(runId)?.reserveDebits ?? []),
|
|
1196
|
+
stages: effectiveAuthorityStages(updated),
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
default:
|
|
1200
|
+
return {
|
|
1201
|
+
...base,
|
|
1202
|
+
ok: false,
|
|
1203
|
+
errorCode: "INVALID_INPUT",
|
|
1204
|
+
message: `unsupported generic checkpoint action ${String(args.action)}`,
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
407
1208
|
// START_CONTRACT: createWorkCheckpointTool
|
|
408
|
-
// PURPOSE: Build work_checkpoint handler wrapping plan registration, checkpoint start,
|
|
409
|
-
// INPUTS: { store: WorkItemStore - workflow in-memory store }
|
|
1209
|
+
// PURPOSE: Build work_checkpoint handler wrapping plan registration, checkpoint start, fingerprint-verified outcomes, and bounded checkpoint recovery.
|
|
1210
|
+
// INPUTS: { store: WorkItemStore - workflow in-memory store, options?: DelegatedControlOptions - optional read-only authorization lookup }
|
|
410
1211
|
// OUTPUTS: { WorkflowToolDefinition<CheckpointArgs, Promise<Record<string, unknown>>> - async executable control tool definition }
|
|
411
1212
|
// SIDE_EFFECTS: [Registers plan runs and mutates checkpoint state through the domain layer]
|
|
412
1213
|
// LINKS: [M-WORKFLOW-TOOLING, M-WORKFLOW-CHECKPOINTS, M-WORKFLOW-DELEGATED]
|
|
413
1214
|
// END_CONTRACT: createWorkCheckpointTool
|
|
414
|
-
export function createWorkCheckpointTool(store) {
|
|
1215
|
+
export function createWorkCheckpointTool(store, options) {
|
|
415
1216
|
return {
|
|
416
1217
|
name: "work_checkpoint",
|
|
417
|
-
description: "Register an approved delegated plan, start a declared review checkpoint,
|
|
1218
|
+
description: "Register an approved delegated plan, start a declared review checkpoint, verify checkpoint outcomes, or recover a stopped or generation-exhausted checkpoint; verify with complete: true seals a finished final checkpoint.",
|
|
418
1219
|
async execute(args, context, overrideStore) {
|
|
419
1220
|
const s = overrideStore ?? store;
|
|
420
1221
|
const action = args.action;
|
|
421
|
-
if (action === "register") {
|
|
1222
|
+
if (action === "register" && coerceNonEmptyString(args.planPath)) {
|
|
422
1223
|
const planPath = coerceNonEmptyString(args.planPath);
|
|
423
1224
|
const workspaceRoot = coerceNonEmptyString(context.workspaceRoot);
|
|
424
1225
|
if (!planPath || !workspaceRoot) {
|
|
@@ -474,6 +1275,23 @@ export function createWorkCheckpointTool(store) {
|
|
|
474
1275
|
checkpoints: registered.run.checkpoints.size,
|
|
475
1276
|
};
|
|
476
1277
|
}
|
|
1278
|
+
const genericRunId = coerceNonEmptyString(args.runId);
|
|
1279
|
+
if (genericRunId) {
|
|
1280
|
+
const data = s.getStoreData();
|
|
1281
|
+
const execution = findExecution(data, genericRunId);
|
|
1282
|
+
// Authority actions apply to any execution, including native runs;
|
|
1283
|
+
// other generic actions stay on non-native executions.
|
|
1284
|
+
const authorityAction = action === "authorize" || action === "record_approval" || action === "revoke_authority";
|
|
1285
|
+
if (execution && (execution.source.kind !== "native-package" || authorityAction)) {
|
|
1286
|
+
return executeGenericCheckpoint({
|
|
1287
|
+
data,
|
|
1288
|
+
sessionId: context.sessionId,
|
|
1289
|
+
runId: genericRunId,
|
|
1290
|
+
args,
|
|
1291
|
+
...(options ? { control: options } : {}),
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
477
1295
|
const runId = coerceNonEmptyString(args.runId);
|
|
478
1296
|
const checkpointId = coerceNonEmptyString(args.checkpointId);
|
|
479
1297
|
if (!runId || !checkpointId) {
|
|
@@ -541,12 +1359,127 @@ export function createWorkCheckpointTool(store) {
|
|
|
541
1359
|
...(verified.sealedRun !== undefined ? { sealedRun: verified.sealedRun } : {}),
|
|
542
1360
|
};
|
|
543
1361
|
}
|
|
1362
|
+
if (action === "recover") {
|
|
1363
|
+
const recoveryId = coerceNonEmptyString(args.recoveryId);
|
|
1364
|
+
const diagnosis = typeof args.diagnosis === "string" ? args.diagnosis : "";
|
|
1365
|
+
const changedCondition = typeof args.changedCondition === "string" ? args.changedCondition : "";
|
|
1366
|
+
const verification = Array.isArray(args.verification) ? args.verification.map(String) : [];
|
|
1367
|
+
const userMessageId = coerceNonEmptyString(args.userMessageId);
|
|
1368
|
+
if (!recoveryId) {
|
|
1369
|
+
return {
|
|
1370
|
+
tool: "work_checkpoint",
|
|
1371
|
+
sessionId: context.sessionId,
|
|
1372
|
+
ok: false,
|
|
1373
|
+
errorCode: "INVALID_INPUT",
|
|
1374
|
+
message: "INVALID_INPUT: recover requires a stable recoveryId, diagnosis, changedCondition, and verification references",
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
const authorityId = coerceNonEmptyString(args.authorityId);
|
|
1378
|
+
let advanceGrantApproved = false;
|
|
1379
|
+
if (authorityId) {
|
|
1380
|
+
const execution = findExecution(s.getStoreData(), runId);
|
|
1381
|
+
const authority = execution?.authority.find((entry) => entry.authorityId === authorityId);
|
|
1382
|
+
if (!execution || !authority) {
|
|
1383
|
+
return {
|
|
1384
|
+
tool: "work_checkpoint",
|
|
1385
|
+
sessionId: context.sessionId,
|
|
1386
|
+
ok: false,
|
|
1387
|
+
errorCode: "AUTHORITY_NOT_FOUND",
|
|
1388
|
+
message: `AUTHORITY_NOT_FOUND: no recorded authority ${authorityId} for ${runId}`,
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
const proposed = proposeReserveDebit({
|
|
1392
|
+
authority,
|
|
1393
|
+
debits: execution.reserveDebits,
|
|
1394
|
+
recoveryId,
|
|
1395
|
+
targetKind: "checkpoint",
|
|
1396
|
+
targetId: checkpointId,
|
|
1397
|
+
});
|
|
1398
|
+
// A failed proposal only means no advance unit is available; the
|
|
1399
|
+
// recovery may still be a cost-free resume or an ordinary grant.
|
|
1400
|
+
advanceGrantApproved = proposed.ok;
|
|
1401
|
+
}
|
|
1402
|
+
const recovered = await recoverDelegatedCheckpoint(s, {
|
|
1403
|
+
sessionId: context.sessionId,
|
|
1404
|
+
runId,
|
|
1405
|
+
checkpointId,
|
|
1406
|
+
diagnosis,
|
|
1407
|
+
changedCondition,
|
|
1408
|
+
verification,
|
|
1409
|
+
recoveryId,
|
|
1410
|
+
...(advanceGrantApproved ? { advanceGrantApproved: true } : {}),
|
|
1411
|
+
...(userMessageId !== undefined
|
|
1412
|
+
? { userMessageId, lookupUserMessage: options?.lookupUserMessage }
|
|
1413
|
+
: {}),
|
|
1414
|
+
});
|
|
1415
|
+
if (!recovered.ok) {
|
|
1416
|
+
return {
|
|
1417
|
+
tool: "work_checkpoint",
|
|
1418
|
+
sessionId: context.sessionId,
|
|
1419
|
+
ok: false,
|
|
1420
|
+
errorCode: recovered.errorCode,
|
|
1421
|
+
message: recovered.message,
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
if (recovered.kind === "advance_grant" && authorityId) {
|
|
1425
|
+
const execution = findExecution(s.getStoreData(), runId);
|
|
1426
|
+
const authority = execution?.authority.find((entry) => entry.authorityId === authorityId);
|
|
1427
|
+
const proposed = authority
|
|
1428
|
+
? proposeReserveDebit({
|
|
1429
|
+
authority,
|
|
1430
|
+
debits: execution?.reserveDebits ?? [],
|
|
1431
|
+
recoveryId,
|
|
1432
|
+
targetKind: "checkpoint",
|
|
1433
|
+
targetId: checkpointId,
|
|
1434
|
+
})
|
|
1435
|
+
: undefined;
|
|
1436
|
+
if (!proposed?.ok) {
|
|
1437
|
+
return {
|
|
1438
|
+
tool: "work_checkpoint",
|
|
1439
|
+
sessionId: context.sessionId,
|
|
1440
|
+
ok: false,
|
|
1441
|
+
errorCode: proposed?.code ?? "RESERVE_EXHAUSTED",
|
|
1442
|
+
message: proposed?.message ?? "advance checkpoint recovery debit could not be recorded",
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
const stored = addReserveDebitInStore(s.getStoreData(), {
|
|
1446
|
+
sessionId: context.sessionId,
|
|
1447
|
+
runId,
|
|
1448
|
+
debit: proposed.value,
|
|
1449
|
+
});
|
|
1450
|
+
if (!stored.ok) {
|
|
1451
|
+
return {
|
|
1452
|
+
tool: "work_checkpoint",
|
|
1453
|
+
sessionId: context.sessionId,
|
|
1454
|
+
ok: false,
|
|
1455
|
+
errorCode: stored.errorCode,
|
|
1456
|
+
message: stored.message,
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return {
|
|
1461
|
+
tool: "work_checkpoint",
|
|
1462
|
+
sessionId: context.sessionId,
|
|
1463
|
+
ok: true,
|
|
1464
|
+
action: "recover",
|
|
1465
|
+
runId,
|
|
1466
|
+
checkpointId,
|
|
1467
|
+
recoveryId: recovered.recoveryId,
|
|
1468
|
+
kind: recovered.kind,
|
|
1469
|
+
generationBudget: recovered.generationBudget,
|
|
1470
|
+
...(recovered.settledStoppedGeneration !== undefined
|
|
1471
|
+
? { settledStoppedGeneration: recovered.settledStoppedGeneration }
|
|
1472
|
+
: {}),
|
|
1473
|
+
checkpointStatus: recovered.checkpoint.status,
|
|
1474
|
+
lastOutcome: recovered.checkpoint.lastOutcome,
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
544
1477
|
return {
|
|
545
1478
|
tool: "work_checkpoint",
|
|
546
1479
|
sessionId: context.sessionId,
|
|
547
1480
|
ok: false,
|
|
548
1481
|
errorCode: "INVALID_INPUT",
|
|
549
|
-
message: "INVALID_INPUT: action must be register, start, or
|
|
1482
|
+
message: "INVALID_INPUT: action must be register, start, verify, or recover",
|
|
550
1483
|
};
|
|
551
1484
|
},
|
|
552
1485
|
};
|