@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
|
@@ -2,9 +2,9 @@
|
|
|
2
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 including recovery-aware progress summaries; work_item_decide and work_checkpoint control-tool definitions wrapping delegated decisions, plan registration, checkpoint start/verify, failed-checkpoint rework authorization,
|
|
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
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
// createWorkItemListTool - Creates work_item_list tool wrapper with mode, round metadata, delegated acceptance and recovery state, and registered plan runs.
|
|
18
18
|
// createWorkItemCloseTool - Creates work_item_close tool wrapper with ready_to_close gating responses.
|
|
19
19
|
// createWorkItemDecideTool - Creates work_item_decide control wrapper around decideDelegatedWorkItem, rework authorization, and bounded recovery.
|
|
20
|
-
// createWorkCheckpointTool - Creates work_checkpoint control wrapper around plan registration, checkpoint start, verify, and checkpoint
|
|
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.
|
|
21
22
|
// DecideArgs - work_item_decide tool argument shape.
|
|
22
23
|
// CheckpointArgs - work_checkpoint tool argument shape.
|
|
23
24
|
// WorkCheckpointRegisterInput - Register-action input discriminator.
|
|
@@ -26,11 +27,13 @@
|
|
|
26
27
|
// END_MODULE_MAP
|
|
27
28
|
//
|
|
28
29
|
// START_CHANGE_SUMMARY
|
|
29
|
-
// LAST_CHANGE: [C-WORKFLOW-
|
|
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.]
|
|
30
31
|
// END_CHANGE_SUMMARY
|
|
31
32
|
import { closeWorkItem, getReviewRound, listWorkItems, openWorkItem, } from "./state.js";
|
|
32
33
|
import { authorizeReworkFromFailedCheckpoint, getDelegatedRunView, recoverDelegatedCheckpoint, registerDelegatedPlan, startDelegatedCheckpoint, verifyDelegatedCheckpoint, } from "./checkpoints.js";
|
|
33
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";
|
|
34
37
|
function coerceNonEmptyString(value) {
|
|
35
38
|
if (typeof value !== "string")
|
|
36
39
|
return undefined;
|
|
@@ -57,6 +60,89 @@ function canonicalizeReviewers(value) {
|
|
|
57
60
|
return left === "spec" ? -1 : 1;
|
|
58
61
|
});
|
|
59
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
|
|
60
146
|
function normalizeOpenInputItem(item, sessionId) {
|
|
61
147
|
const key = coerceNonEmptyString(item.key);
|
|
62
148
|
const title = coerceNonEmptyString(item.title);
|
|
@@ -203,6 +289,114 @@ export function createWorkItemOpenTool(store) {
|
|
|
203
289
|
description: "Open one or more workflow work items idempotently with explicit mode and requiredReviewers.",
|
|
204
290
|
execute: (args, context, overrideStore) => {
|
|
205
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
|
+
}
|
|
206
400
|
const results = inputItems.map((item) => {
|
|
207
401
|
const normalized = normalizeOpenInputItem(item, context.sessionId);
|
|
208
402
|
if (!normalized.ok) {
|
|
@@ -383,6 +577,7 @@ export function createWorkItemDecideTool(store, options) {
|
|
|
383
577
|
const changedCondition = typeof args.changedCondition === "string" ? args.changedCondition : "";
|
|
384
578
|
const verification = Array.isArray(args.verification) ? args.verification.map(String) : [];
|
|
385
579
|
const userMessageId = coerceNonEmptyString(args.userMessageId);
|
|
580
|
+
const authorityId = coerceNonEmptyString(args.authorityId);
|
|
386
581
|
if (!recoveryId) {
|
|
387
582
|
return {
|
|
388
583
|
tool: "work_item_decide",
|
|
@@ -392,6 +587,53 @@ export function createWorkItemDecideTool(store, options) {
|
|
|
392
587
|
message: "INVALID_INPUT: recover requires a stable recoveryId, diagnosis, changedCondition, and verification references",
|
|
393
588
|
};
|
|
394
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
|
+
}
|
|
395
637
|
const recovered = await recoverDelegatedWorkItem(s, {
|
|
396
638
|
sessionId: context.sessionId,
|
|
397
639
|
workItemId,
|
|
@@ -400,6 +642,7 @@ export function createWorkItemDecideTool(store, options) {
|
|
|
400
642
|
changedCondition,
|
|
401
643
|
verification,
|
|
402
644
|
recoveryId,
|
|
645
|
+
...(advanceGrantApproved ? { advanceGrantApproved: true } : {}),
|
|
403
646
|
...(userMessageId !== undefined
|
|
404
647
|
? { userMessageId, lookupUserMessage: options?.lookupUserMessage }
|
|
405
648
|
: {}),
|
|
@@ -413,6 +656,42 @@ export function createWorkItemDecideTool(store, options) {
|
|
|
413
656
|
message: recovered.message,
|
|
414
657
|
};
|
|
415
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
|
+
}
|
|
416
695
|
return {
|
|
417
696
|
tool: "work_item_decide",
|
|
418
697
|
sessionId: context.sessionId,
|
|
@@ -468,6 +747,464 @@ export function createWorkItemDecideTool(store, options) {
|
|
|
468
747
|
},
|
|
469
748
|
};
|
|
470
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
|
+
}
|
|
471
1208
|
// START_CONTRACT: createWorkCheckpointTool
|
|
472
1209
|
// PURPOSE: Build work_checkpoint handler wrapping plan registration, checkpoint start, fingerprint-verified outcomes, and bounded checkpoint recovery.
|
|
473
1210
|
// INPUTS: { store: WorkItemStore - workflow in-memory store, options?: DelegatedControlOptions - optional read-only authorization lookup }
|
|
@@ -482,7 +1219,7 @@ export function createWorkCheckpointTool(store, options) {
|
|
|
482
1219
|
async execute(args, context, overrideStore) {
|
|
483
1220
|
const s = overrideStore ?? store;
|
|
484
1221
|
const action = args.action;
|
|
485
|
-
if (action === "register") {
|
|
1222
|
+
if (action === "register" && coerceNonEmptyString(args.planPath)) {
|
|
486
1223
|
const planPath = coerceNonEmptyString(args.planPath);
|
|
487
1224
|
const workspaceRoot = coerceNonEmptyString(context.workspaceRoot);
|
|
488
1225
|
if (!planPath || !workspaceRoot) {
|
|
@@ -538,6 +1275,23 @@ export function createWorkCheckpointTool(store, options) {
|
|
|
538
1275
|
checkpoints: registered.run.checkpoints.size,
|
|
539
1276
|
};
|
|
540
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
|
+
}
|
|
541
1295
|
const runId = coerceNonEmptyString(args.runId);
|
|
542
1296
|
const checkpointId = coerceNonEmptyString(args.checkpointId);
|
|
543
1297
|
if (!runId || !checkpointId) {
|
|
@@ -620,6 +1374,31 @@ export function createWorkCheckpointTool(store, options) {
|
|
|
620
1374
|
message: "INVALID_INPUT: recover requires a stable recoveryId, diagnosis, changedCondition, and verification references",
|
|
621
1375
|
};
|
|
622
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
|
+
}
|
|
623
1402
|
const recovered = await recoverDelegatedCheckpoint(s, {
|
|
624
1403
|
sessionId: context.sessionId,
|
|
625
1404
|
runId,
|
|
@@ -628,6 +1407,7 @@ export function createWorkCheckpointTool(store, options) {
|
|
|
628
1407
|
changedCondition,
|
|
629
1408
|
verification,
|
|
630
1409
|
recoveryId,
|
|
1410
|
+
...(advanceGrantApproved ? { advanceGrantApproved: true } : {}),
|
|
631
1411
|
...(userMessageId !== undefined
|
|
632
1412
|
? { userMessageId, lookupUserMessage: options?.lookupUserMessage }
|
|
633
1413
|
: {}),
|
|
@@ -641,6 +1421,42 @@ export function createWorkCheckpointTool(store, options) {
|
|
|
641
1421
|
message: recovered.message,
|
|
642
1422
|
};
|
|
643
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
|
+
}
|
|
644
1460
|
return {
|
|
645
1461
|
tool: "work_checkpoint",
|
|
646
1462
|
sessionId: context.sessionId,
|