@osolmaz/pi-workflows 0.11.2 → 0.12.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/README.md +15 -10
- package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
- package/dist/builtins/autoimplement-command-batches.js +258 -0
- package/dist/builtins/autoimplement-command-batches.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +509 -124
- package/dist/builtins/autoimplement.workflow.js +884 -327
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +5 -3
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +4 -1
- package/dist/builtins/index.js +3 -1
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +2 -4
- package/dist/builtins/monitor.workflow.js +26 -128
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/pi-agent-group.d.ts +72 -0
- package/dist/builtins/pi-agent-group.js +1087 -0
- package/dist/builtins/pi-agent-group.js.map +1 -0
- package/dist/builtins/plan-approval.workflow.d.ts +39 -5
- package/dist/builtins/plan-approval.workflow.js +92 -14
- package/dist/builtins/plan-approval.workflow.js.map +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +301 -0
- package/dist/builtins/plan-change.workflow.js +256 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.js +2 -2
- package/dist/builtins/plan-presentation.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.d.ts +85 -0
- package/dist/builtins/sanity-check.workflow.js +482 -0
- package/dist/builtins/sanity-check.workflow.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +90 -2
- package/dist/controllers/sqlite.js +380 -5
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +2 -2
- package/dist/extension/decision-channels.js +29 -35
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
- package/dist/extension/deferred-turn-coordinator.js +143 -0
- package/dist/extension/deferred-turn-coordinator.js.map +1 -0
- package/dist/extension/deferred-turn.d.ts +44 -0
- package/dist/extension/deferred-turn.js +110 -0
- package/dist/extension/deferred-turn.js.map +1 -0
- package/dist/extension/index.js +348 -80
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -2
- package/dist/extension/step-message.d.ts +1 -0
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +23 -3
- package/dist/extension/widget.js.map +1 -1
- package/dist/herdr/setup.js +4 -4
- package/dist/render/graph-render.js +1 -2
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/render.js +7 -6
- package/dist/viewer/render.js.map +1 -1
- package/dist/workflows/catalog.js +7 -2
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/command-batch.d.ts +38 -0
- package/dist/workflows/command-batch.js +176 -0
- package/dist/workflows/command-batch.js.map +1 -0
- package/dist/workflows/composition.js +8 -0
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/decision-presentation.d.ts +1 -1
- package/dist/workflows/decision-presentation.js +51 -38
- package/dist/workflows/decision-presentation.js.map +1 -1
- package/dist/workflows/engine.d.ts +2 -2
- package/dist/workflows/engine.js +22 -17
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +13 -0
- package/dist/workflows/errors.js +15 -0
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +16 -4
- package/dist/workflows/human-decision.js +175 -72
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -2
- package/dist/workflows/index.js +2 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/progress.d.ts +1 -0
- package/dist/workflows/progress.js +15 -3
- package/dist/workflows/progress.js.map +1 -1
- package/dist/workflows/schema.js +12 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/shell.d.ts +4 -0
- package/dist/workflows/shell.js +6 -0
- package/dist/workflows/shell.js.map +1 -1
- package/dist/workflows/store.js +8 -1
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +37 -48
- package/docs/2026-08-18-herdr-piw-plan.md +1 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
- package/docs/CONTROLLERS.md +2 -2
- package/docs/DEFERRED_TURNS.md +298 -0
- package/docs/DESIGN_PHILOSOPHY.md +1 -1
- package/docs/HUMAN_DECISIONS.md +30 -40
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +17 -27
- package/docs/MONITOR.md +7 -13
- package/docs/WORKFLOW_COMPOSITION.md +10 -9
- package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
- package/docs/WORKFLOW_UPDATES.md +28 -5
- package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
- package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
- package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
- package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
- package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
- package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
- package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
- package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
- package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
- package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
- package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
- package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +283 -0
- package/docs/run-bundles.md +8 -6
- package/docs/workflows.md +85 -22
- package/examples/workflows/approved-plan.workflow.ts +19 -46
- package/examples/workflows/command-batch.workflow.ts +50 -0
- package/examples/workflows/sanity-check.workflow.ts +1 -0
- package/herdr-plugin.toml +3 -3
- package/package.json +7 -7
- package/schemas/decision-presentation-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v1.schema.json +16 -4
- package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
- package/schemas/human-decision-cancellation-v1.schema.json +1 -1
- package/schemas/human-decision-continuation-v1.schema.json +11 -2
- package/schemas/human-decision-delivery-v1.schema.json +9 -1
- package/schemas/human-decision-receipt-v1.schema.json +9 -1
- package/schemas/human-decision-request-v1.schema.json +25 -5
- package/schemas/human-decision-resolution-v1.schema.json +1 -1
- package/schemas/human-decision-settlement-v1.schema.json +1 -1
- package/skills/autodoc/SKILL.md +33 -2
- package/skills/autoimplement/SKILL.md +93 -12
- package/skills/autoplan/SKILL.md +26 -2
- package/skills/monitor/SKILL.md +66 -24
- package/skills/pi-workflows/SKILL.md +21 -6
- package/skills/sanity-check/SKILL.md +44 -0
- package/src/builtins/autoimplement-command-batches.ts +364 -0
- package/src/builtins/autoimplement.workflow.ts +1075 -370
- package/src/builtins/catalog.ts +5 -3
- package/src/builtins/index.ts +24 -0
- package/src/builtins/monitor.workflow.ts +27 -150
- package/src/builtins/pi-agent-group.ts +1407 -0
- package/src/builtins/plan-approval.workflow.ts +157 -24
- package/src/builtins/plan-change.workflow.ts +321 -0
- package/src/builtins/plan-presentation.ts +2 -2
- package/src/builtins/sanity-check.workflow.ts +769 -0
- package/src/controllers/sqlite.ts +580 -7
- package/src/extension/decision-channels.ts +36 -66
- package/src/extension/deferred-turn-coordinator.ts +171 -0
- package/src/extension/deferred-turn.ts +166 -0
- package/src/extension/index.ts +438 -90
- package/src/extension/session-events.ts +2 -2
- package/src/extension/step-message.ts +1 -0
- package/src/extension/widget.ts +24 -5
- package/src/herdr/setup.ts +4 -4
- package/src/render/graph-render.ts +1 -2
- package/src/viewer/render.ts +7 -6
- package/src/workflows/catalog.ts +7 -2
- package/src/workflows/command-batch.ts +254 -0
- package/src/workflows/composition.ts +9 -0
- package/src/workflows/decision-presentation.ts +56 -43
- package/src/workflows/engine.ts +25 -20
- package/src/workflows/errors.ts +24 -0
- package/src/workflows/human-decision.ts +218 -101
- package/src/workflows/index.ts +20 -11
- package/src/workflows/progress.ts +18 -3
- package/src/workflows/schema.ts +19 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +8 -1
- package/src/workflows/types.ts +43 -59
- package/schemas/human-decision-accepted-v2.schema.json +0 -50
- package/schemas/human-decision-delivery-v2.schema.json +0 -36
- package/schemas/human-decision-receipt-v2.schema.json +0 -39
- package/schemas/human-decision-request-v2.schema.json +0 -69
- package/schemas/human-decision-resolution-v2.schema.json +0 -27
|
@@ -9,25 +9,56 @@ import {
|
|
|
9
9
|
import type { HumanDecisionReceipt, HumanDecisionResponse } from "../workflows/types.js";
|
|
10
10
|
import { presentPlan } from "./plan-presentation.js";
|
|
11
11
|
|
|
12
|
+
const DEFAULT_APPROVAL_AUDIENCE = "operator";
|
|
13
|
+
const DEFAULT_APPROVAL_TIMEOUT_MINUTES = 10;
|
|
14
|
+
const DEFAULT_MAX_REPLANS = 3;
|
|
15
|
+
const MAX_APPROVAL_TIMEOUT_MINUTES = 24 * 60;
|
|
16
|
+
const MAX_REPLANS = 20;
|
|
17
|
+
|
|
18
|
+
export type PlanApprovalMode = "auto" | "required" | "skip";
|
|
19
|
+
|
|
20
|
+
export type PlanApprovalPolicy = {
|
|
21
|
+
mode?: PlanApprovalMode;
|
|
22
|
+
audience?: string;
|
|
23
|
+
timeoutMinutes?: number;
|
|
24
|
+
maxReplans?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ResolvedPlanApprovalPolicy = {
|
|
28
|
+
mode: PlanApprovalMode;
|
|
29
|
+
audience: string;
|
|
30
|
+
timeoutMinutes?: number;
|
|
31
|
+
maxReplans: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
12
34
|
export type PlanApprovalInput = {
|
|
13
35
|
task: string;
|
|
14
36
|
plan: unknown;
|
|
15
37
|
planDigest: string;
|
|
16
|
-
|
|
38
|
+
approval?: PlanApprovalPolicy;
|
|
17
39
|
revision?: number;
|
|
18
40
|
};
|
|
19
41
|
|
|
42
|
+
export type NormalizedPlanApprovalInput = Omit<PlanApprovalInput, "approval"> & {
|
|
43
|
+
approval: ResolvedPlanApprovalPolicy;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type PlanApprovalResolution =
|
|
47
|
+
| { provenance: "skipped"; revision: number }
|
|
48
|
+
| { provenance: "human"; decision: HumanDecisionReceipt }
|
|
49
|
+
| { provenance: "timeout"; decision: HumanDecisionReceipt };
|
|
50
|
+
|
|
20
51
|
export type PlanApprovalContinue = {
|
|
21
52
|
status: "continue";
|
|
22
53
|
plan: unknown;
|
|
23
54
|
planDigest: string;
|
|
24
|
-
|
|
55
|
+
resolution: PlanApprovalResolution;
|
|
25
56
|
};
|
|
26
57
|
|
|
27
58
|
export type PlanApprovalStop = {
|
|
28
59
|
status: "stop";
|
|
29
60
|
planDigest: string;
|
|
30
|
-
decision: HumanDecisionReceipt;
|
|
61
|
+
resolution: { provenance: "human"; decision: HumanDecisionReceipt };
|
|
31
62
|
};
|
|
32
63
|
|
|
33
64
|
export type PlanApprovalReplan = {
|
|
@@ -35,7 +66,7 @@ export type PlanApprovalReplan = {
|
|
|
35
66
|
plan: unknown;
|
|
36
67
|
planDigest: string;
|
|
37
68
|
instructions: string;
|
|
38
|
-
decision: HumanDecisionReceipt;
|
|
69
|
+
resolution: { provenance: "human"; decision: HumanDecisionReceipt };
|
|
39
70
|
};
|
|
40
71
|
|
|
41
72
|
const planChoices = defineHumanChoices({
|
|
@@ -66,8 +97,78 @@ function requireString(value: unknown, label: string): string {
|
|
|
66
97
|
return value.trim();
|
|
67
98
|
}
|
|
68
99
|
|
|
69
|
-
function
|
|
100
|
+
function requireExactKeys(
|
|
101
|
+
value: Record<string, unknown>,
|
|
102
|
+
allowed: readonly string[],
|
|
103
|
+
label: string,
|
|
104
|
+
): void {
|
|
105
|
+
const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
106
|
+
if (unexpected.length > 0) {
|
|
107
|
+
throw new Error(`${label} has unknown field ${unexpected[0]}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function parsePlanApprovalPolicy(value: unknown): ResolvedPlanApprovalPolicy {
|
|
112
|
+
if (value === undefined) {
|
|
113
|
+
return {
|
|
114
|
+
mode: "auto",
|
|
115
|
+
audience: DEFAULT_APPROVAL_AUDIENCE,
|
|
116
|
+
timeoutMinutes: DEFAULT_APPROVAL_TIMEOUT_MINUTES,
|
|
117
|
+
maxReplans: DEFAULT_MAX_REPLANS,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const policy = requireRecord(value, "plan approval policy");
|
|
121
|
+
requireExactKeys(
|
|
122
|
+
policy,
|
|
123
|
+
["mode", "audience", "timeoutMinutes", "maxReplans"],
|
|
124
|
+
"plan approval policy",
|
|
125
|
+
);
|
|
126
|
+
const mode = policy.mode ?? "auto";
|
|
127
|
+
if (mode !== "auto" && mode !== "required" && mode !== "skip") {
|
|
128
|
+
throw new Error("plan approval mode must be auto, required, or skip");
|
|
129
|
+
}
|
|
130
|
+
if (mode !== "auto" && policy.timeoutMinutes !== undefined) {
|
|
131
|
+
throw new Error("plan approval timeoutMinutes is available only in auto mode");
|
|
132
|
+
}
|
|
133
|
+
const timeoutMinutes = policy.timeoutMinutes ?? DEFAULT_APPROVAL_TIMEOUT_MINUTES;
|
|
134
|
+
if (
|
|
135
|
+
mode === "auto" &&
|
|
136
|
+
(!Number.isInteger(timeoutMinutes) ||
|
|
137
|
+
(timeoutMinutes as number) < 1 ||
|
|
138
|
+
(timeoutMinutes as number) > MAX_APPROVAL_TIMEOUT_MINUTES)
|
|
139
|
+
) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`plan approval timeoutMinutes must be from 1 through ${MAX_APPROVAL_TIMEOUT_MINUTES}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const maxReplans = policy.maxReplans ?? DEFAULT_MAX_REPLANS;
|
|
145
|
+
if (
|
|
146
|
+
!Number.isInteger(maxReplans) ||
|
|
147
|
+
(maxReplans as number) < 1 ||
|
|
148
|
+
(maxReplans as number) > MAX_REPLANS
|
|
149
|
+
) {
|
|
150
|
+
throw new Error(`plan approval maxReplans must be from 1 through ${MAX_REPLANS}`);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
mode,
|
|
154
|
+
audience:
|
|
155
|
+
mode === "skip"
|
|
156
|
+
? DEFAULT_APPROVAL_AUDIENCE
|
|
157
|
+
: policy.audience === undefined
|
|
158
|
+
? DEFAULT_APPROVAL_AUDIENCE
|
|
159
|
+
: requireString(policy.audience, "plan approval audience"),
|
|
160
|
+
...(mode === "auto" ? { timeoutMinutes: timeoutMinutes as number } : {}),
|
|
161
|
+
maxReplans: maxReplans as number,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseInput(value: unknown): NormalizedPlanApprovalInput {
|
|
70
166
|
const input = requireRecord(value, "plan approval input");
|
|
167
|
+
requireExactKeys(
|
|
168
|
+
input,
|
|
169
|
+
["task", "plan", "planDigest", "approval", "revision"],
|
|
170
|
+
"plan approval input",
|
|
171
|
+
);
|
|
71
172
|
const revision = input.revision;
|
|
72
173
|
if (revision !== undefined && (!Number.isInteger(revision) || (revision as number) < 1)) {
|
|
73
174
|
throw new Error("plan approval revision must be a positive integer");
|
|
@@ -77,29 +178,40 @@ function parseInput(value: unknown): PlanApprovalInput {
|
|
|
77
178
|
task: requireString(input.task, "plan approval task"),
|
|
78
179
|
plan: input.plan,
|
|
79
180
|
planDigest: requireString(input.planDigest, "plan approval digest"),
|
|
80
|
-
|
|
181
|
+
approval: parsePlanApprovalPolicy(input.approval),
|
|
81
182
|
...(revision !== undefined ? { revision: revision as number } : {}),
|
|
82
183
|
};
|
|
83
184
|
}
|
|
84
185
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
throw new Error("plan approval continuation is missing its accepted decision");
|
|
88
|
-
return value;
|
|
186
|
+
function gateResponse(value: unknown): HumanDecisionResponse {
|
|
187
|
+
return value as HumanDecisionResponse;
|
|
89
188
|
}
|
|
90
189
|
|
|
91
|
-
function
|
|
92
|
-
|
|
190
|
+
function decisionResolution(
|
|
191
|
+
value: HumanDecisionReceipt | undefined,
|
|
192
|
+
):
|
|
193
|
+
| { provenance: "human"; decision: HumanDecisionReceipt }
|
|
194
|
+
| { provenance: "timeout"; decision: HumanDecisionReceipt } {
|
|
195
|
+
if (value === undefined) throw new Error("plan approval decision receipt is missing");
|
|
196
|
+
return { provenance: value.provenance, decision: value };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function humanResolution(value: HumanDecisionReceipt | undefined): {
|
|
200
|
+
provenance: "human";
|
|
201
|
+
decision: HumanDecisionReceipt;
|
|
202
|
+
} {
|
|
203
|
+
const resolution = decisionResolution(value);
|
|
204
|
+
if (resolution.provenance !== "human") {
|
|
205
|
+
throw new Error("plan approval stop and replan require a human decision");
|
|
206
|
+
}
|
|
207
|
+
return resolution;
|
|
93
208
|
}
|
|
94
209
|
|
|
95
210
|
export const planApprovalWorkflow = defineWorkflow({
|
|
96
211
|
source: import.meta.url,
|
|
97
|
-
contractId: "pi-workflows.plan-approval.v1",
|
|
98
212
|
name: "plan-approval",
|
|
99
213
|
input: parseInput,
|
|
100
|
-
|
|
101
|
-
startAt: "approve",
|
|
102
|
-
maxSteps: 8,
|
|
214
|
+
startAt: "routePolicy",
|
|
103
215
|
exits: {
|
|
104
216
|
continue: {
|
|
105
217
|
from: "continued",
|
|
@@ -115,11 +227,25 @@ export const planApprovalWorkflow = defineWorkflow({
|
|
|
115
227
|
},
|
|
116
228
|
},
|
|
117
229
|
nodes: {
|
|
230
|
+
routePolicy: compute({
|
|
231
|
+
run: ({ input }) => ({
|
|
232
|
+
route: (input as NormalizedPlanApprovalInput).approval.mode === "skip" ? "skip" : "ask",
|
|
233
|
+
}),
|
|
234
|
+
}),
|
|
118
235
|
approve: humanDecision({
|
|
119
|
-
audience: ({ input }) => (input as
|
|
236
|
+
audience: ({ input }) => (input as NormalizedPlanApprovalInput).approval.audience,
|
|
120
237
|
choices: planChoices,
|
|
238
|
+
onTimeout: ({ input }) => {
|
|
239
|
+
const policy = (input as NormalizedPlanApprovalInput).approval;
|
|
240
|
+
return policy.mode === "auto"
|
|
241
|
+
? {
|
|
242
|
+
afterMs: (policy.timeoutMinutes ?? DEFAULT_APPROVAL_TIMEOUT_MINUTES) * 60_000,
|
|
243
|
+
response: { choice: "continue" },
|
|
244
|
+
}
|
|
245
|
+
: undefined;
|
|
246
|
+
},
|
|
121
247
|
request: ({ input }) => {
|
|
122
|
-
const request = input as
|
|
248
|
+
const request = input as NormalizedPlanApprovalInput;
|
|
123
249
|
const revision = request.revision ?? 1;
|
|
124
250
|
return {
|
|
125
251
|
title: "Approve the implementation plan",
|
|
@@ -141,28 +267,31 @@ export const planApprovalWorkflow = defineWorkflow({
|
|
|
141
267
|
}),
|
|
142
268
|
continued: compute({
|
|
143
269
|
run: ({ input, state }) => {
|
|
144
|
-
const request = input as
|
|
270
|
+
const request = input as NormalizedPlanApprovalInput;
|
|
145
271
|
return {
|
|
146
272
|
status: "continue",
|
|
147
273
|
plan: request.plan,
|
|
148
274
|
planDigest: request.planDigest,
|
|
149
|
-
|
|
275
|
+
resolution:
|
|
276
|
+
request.approval.mode === "skip"
|
|
277
|
+
? { provenance: "skipped", revision: request.revision ?? 1 }
|
|
278
|
+
: decisionResolution(state.humanDecision),
|
|
150
279
|
} satisfies PlanApprovalContinue;
|
|
151
280
|
},
|
|
152
281
|
}),
|
|
153
282
|
stopped: compute({
|
|
154
283
|
run: ({ input, state }) => {
|
|
155
|
-
const request = input as
|
|
284
|
+
const request = input as NormalizedPlanApprovalInput;
|
|
156
285
|
return {
|
|
157
286
|
status: "stop",
|
|
158
287
|
planDigest: request.planDigest,
|
|
159
|
-
|
|
288
|
+
resolution: humanResolution(state.humanDecision),
|
|
160
289
|
} satisfies PlanApprovalStop;
|
|
161
290
|
},
|
|
162
291
|
}),
|
|
163
292
|
replan: compute({
|
|
164
293
|
run: ({ input, outputs, state }) => {
|
|
165
|
-
const request = input as
|
|
294
|
+
const request = input as NormalizedPlanApprovalInput;
|
|
166
295
|
const response = gateResponse(outputs.approve);
|
|
167
296
|
const instructions = response.input?.instructions;
|
|
168
297
|
if (typeof instructions !== "string" || instructions.length === 0) {
|
|
@@ -173,12 +302,16 @@ export const planApprovalWorkflow = defineWorkflow({
|
|
|
173
302
|
plan: request.plan,
|
|
174
303
|
planDigest: request.planDigest,
|
|
175
304
|
instructions,
|
|
176
|
-
|
|
305
|
+
resolution: humanResolution(state.humanDecision),
|
|
177
306
|
} satisfies PlanApprovalReplan;
|
|
178
307
|
},
|
|
179
308
|
}),
|
|
180
309
|
},
|
|
181
310
|
edges: [
|
|
311
|
+
{
|
|
312
|
+
from: "routePolicy",
|
|
313
|
+
switch: { on: "$.route", cases: { ask: "approve", skip: "continued" } },
|
|
314
|
+
},
|
|
182
315
|
humanDecisionEdge({
|
|
183
316
|
from: "approve",
|
|
184
317
|
choices: planChoices,
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compute,
|
|
3
|
+
defineWorkflow,
|
|
4
|
+
includeWorkflow,
|
|
5
|
+
includedResult,
|
|
6
|
+
} from "../workflows/definition.js";
|
|
7
|
+
import type { WorkflowNodeContext } from "../workflows/types.js";
|
|
8
|
+
import autodocWorkflow, { type AutodocInput, type DocumentedPlan } from "./autodoc.workflow.js";
|
|
9
|
+
import autoplanWorkflow, { type AutoplanInput, type AutoplanReady } from "./autoplan.workflow.js";
|
|
10
|
+
import planApprovalWorkflow, {
|
|
11
|
+
parsePlanApprovalPolicy,
|
|
12
|
+
type PlanApprovalContinue,
|
|
13
|
+
type NormalizedPlanApprovalInput,
|
|
14
|
+
type PlanApprovalPolicy,
|
|
15
|
+
type PlanApprovalResolution,
|
|
16
|
+
type ResolvedPlanApprovalPolicy,
|
|
17
|
+
} from "./plan-approval.workflow.js";
|
|
18
|
+
|
|
19
|
+
export type PlanChangeInput = {
|
|
20
|
+
task: string;
|
|
21
|
+
scope?: string;
|
|
22
|
+
constraints?: string[];
|
|
23
|
+
repository?: string;
|
|
24
|
+
documents?: string[];
|
|
25
|
+
previousPlan?: unknown;
|
|
26
|
+
newEvidence?: unknown;
|
|
27
|
+
approval?: PlanApprovalPolicy;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type NormalizedPlanChangeInput = Omit<PlanChangeInput, "approval"> & {
|
|
31
|
+
approval: ResolvedPlanApprovalPolicy;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type PlanChangeReady = {
|
|
35
|
+
status: "ready";
|
|
36
|
+
plan: unknown;
|
|
37
|
+
planDigest: string;
|
|
38
|
+
documents: string[];
|
|
39
|
+
revision: number;
|
|
40
|
+
approval: PlanApprovalResolution;
|
|
41
|
+
documentation: DocumentedPlan["documentation"];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type PlanChangeBlocked = {
|
|
45
|
+
status: "blocked";
|
|
46
|
+
reason: string;
|
|
47
|
+
evidence: unknown;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
51
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
52
|
+
throw new Error(`${label} must be an object`);
|
|
53
|
+
}
|
|
54
|
+
return value as Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function requireString(value: unknown, label: string): string {
|
|
58
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
59
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
60
|
+
}
|
|
61
|
+
return value.trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function requireExactKeys(
|
|
65
|
+
value: Record<string, unknown>,
|
|
66
|
+
allowed: readonly string[],
|
|
67
|
+
label: string,
|
|
68
|
+
): void {
|
|
69
|
+
const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
70
|
+
if (unexpected.length > 0) throw new Error(`${label} has unknown field ${unexpected[0]}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseStringArray(value: unknown, label: string): string[] | undefined {
|
|
74
|
+
if (value === undefined) return undefined;
|
|
75
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
76
|
+
throw new Error(`${label} must be an array of strings`);
|
|
77
|
+
}
|
|
78
|
+
return [...value] as string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseInput(value: unknown): NormalizedPlanChangeInput {
|
|
82
|
+
const input = requireRecord(value, "plan change input");
|
|
83
|
+
requireExactKeys(
|
|
84
|
+
input,
|
|
85
|
+
[
|
|
86
|
+
"task",
|
|
87
|
+
"scope",
|
|
88
|
+
"constraints",
|
|
89
|
+
"repository",
|
|
90
|
+
"documents",
|
|
91
|
+
"previousPlan",
|
|
92
|
+
"newEvidence",
|
|
93
|
+
"approval",
|
|
94
|
+
],
|
|
95
|
+
"plan change input",
|
|
96
|
+
);
|
|
97
|
+
const constraints = parseStringArray(input.constraints, "plan change constraints");
|
|
98
|
+
const documents = parseStringArray(input.documents, "plan change documents");
|
|
99
|
+
return {
|
|
100
|
+
task: requireString(input.task, "plan change task"),
|
|
101
|
+
...(input.scope !== undefined
|
|
102
|
+
? { scope: requireString(input.scope, "plan change scope") }
|
|
103
|
+
: {}),
|
|
104
|
+
...(constraints !== undefined ? { constraints } : {}),
|
|
105
|
+
...(input.repository !== undefined
|
|
106
|
+
? { repository: requireString(input.repository, "plan change repository") }
|
|
107
|
+
: {}),
|
|
108
|
+
...(documents !== undefined ? { documents } : {}),
|
|
109
|
+
...(input.previousPlan !== undefined ? { previousPlan: input.previousPlan } : {}),
|
|
110
|
+
...(input.newEvidence !== undefined ? { newEvidence: input.newEvidence } : {}),
|
|
111
|
+
approval: parsePlanApprovalPolicy(input.approval),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function currentDesign(context: Pick<WorkflowNodeContext, "outputs">): AutoplanReady {
|
|
116
|
+
const result = includedResult(autoplanWorkflow, context.outputs.design);
|
|
117
|
+
if (result.exit !== "ready") throw new Error("plan change design did not return a ready plan");
|
|
118
|
+
return result.output;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function currentDocumentation(context: Pick<WorkflowNodeContext, "outputs">): DocumentedPlan {
|
|
122
|
+
const result = includedResult(autodocWorkflow, context.outputs.documentation);
|
|
123
|
+
if (result.exit !== "ready") {
|
|
124
|
+
throw new Error("plan change documentation did not return a ready plan");
|
|
125
|
+
}
|
|
126
|
+
return result.output;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function currentApproval(context: Pick<WorkflowNodeContext, "outputs">): PlanApprovalContinue {
|
|
130
|
+
const result = includedResult(planApprovalWorkflow, context.outputs.approval);
|
|
131
|
+
if (result.exit !== "continue") {
|
|
132
|
+
throw new Error("plan change approval did not return continue");
|
|
133
|
+
}
|
|
134
|
+
return result.output;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function latestReplanInstructions(outputs: Record<string, unknown>): string | undefined {
|
|
138
|
+
if (outputs.approval === undefined) return undefined;
|
|
139
|
+
const result = includedResult(planApprovalWorkflow, outputs.approval);
|
|
140
|
+
return result.exit === "replan" ? result.output.instructions : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function blockedResult(context: WorkflowNodeContext): PlanChangeBlocked {
|
|
144
|
+
if (context.outputs.approval !== undefined) {
|
|
145
|
+
const approval = includedResult(planApprovalWorkflow, context.outputs.approval);
|
|
146
|
+
if (approval.exit === "stop") {
|
|
147
|
+
return {
|
|
148
|
+
status: "blocked",
|
|
149
|
+
reason: "The operator stopped the proposed plan change.",
|
|
150
|
+
evidence: approval.output,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const candidates = ["replanGuard", "assessPlan", "approval", "documentation", "design"];
|
|
155
|
+
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
156
|
+
const step = context.state.steps[index];
|
|
157
|
+
if (step === undefined || !candidates.some((candidate) => step.nodeId.startsWith(candidate))) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const output = step.output as Record<string, unknown> | null;
|
|
161
|
+
if (output !== null && typeof output === "object") {
|
|
162
|
+
const reason = output.reason;
|
|
163
|
+
if (typeof reason === "string" && reason.length > 0) {
|
|
164
|
+
return { status: "blocked", reason, evidence: step.output };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
status: "blocked",
|
|
170
|
+
reason: "The plan change could not continue within its configured policy.",
|
|
171
|
+
evidence: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const planChangeWorkflow = defineWorkflow({
|
|
176
|
+
source: import.meta.url,
|
|
177
|
+
name: "plan-change",
|
|
178
|
+
input: parseInput,
|
|
179
|
+
startAt: "start",
|
|
180
|
+
maxSteps: 400,
|
|
181
|
+
includes: {
|
|
182
|
+
design: includeWorkflow(autoplanWorkflow, {
|
|
183
|
+
input: ({ input, outputs }): AutoplanInput => {
|
|
184
|
+
const request = input as NormalizedPlanChangeInput;
|
|
185
|
+
const prior =
|
|
186
|
+
outputs.design === undefined
|
|
187
|
+
? request.previousPlan
|
|
188
|
+
: (() => {
|
|
189
|
+
const result = includedResult(autoplanWorkflow, outputs.design);
|
|
190
|
+
return result.exit === "ready" ? result.output.plan : request.previousPlan;
|
|
191
|
+
})();
|
|
192
|
+
const instructions = latestReplanInstructions(outputs);
|
|
193
|
+
return {
|
|
194
|
+
problem: request.task,
|
|
195
|
+
...(request.scope !== undefined ? { scope: request.scope } : {}),
|
|
196
|
+
...(request.constraints !== undefined ? { constraints: request.constraints } : {}),
|
|
197
|
+
...(prior !== undefined ? { previousPlan: prior } : {}),
|
|
198
|
+
...(instructions === undefined
|
|
199
|
+
? request.newEvidence !== undefined
|
|
200
|
+
? { newEvidence: request.newEvidence }
|
|
201
|
+
: {}
|
|
202
|
+
: {
|
|
203
|
+
newEvidence: {
|
|
204
|
+
priorEvidence: request.newEvidence,
|
|
205
|
+
operatorInstructions: instructions,
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
documentation: includeWorkflow(autodocWorkflow, {
|
|
212
|
+
input: (context): AutodocInput => {
|
|
213
|
+
const request = context.input as NormalizedPlanChangeInput;
|
|
214
|
+
const design = currentDesign(context);
|
|
215
|
+
return {
|
|
216
|
+
task: request.task,
|
|
217
|
+
plan: design.plan,
|
|
218
|
+
...(request.repository !== undefined ? { repository: request.repository } : {}),
|
|
219
|
+
...(request.documents !== undefined ? { documents: request.documents } : {}),
|
|
220
|
+
evidence: request.newEvidence,
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
}),
|
|
224
|
+
approval: includeWorkflow(planApprovalWorkflow, {
|
|
225
|
+
input: (context): NormalizedPlanApprovalInput => {
|
|
226
|
+
const request = context.input as NormalizedPlanChangeInput;
|
|
227
|
+
const documented = currentDocumentation(context);
|
|
228
|
+
const revision =
|
|
229
|
+
context.state.steps.filter((step) => step.nodeId === "approval/routePolicy").length + 1;
|
|
230
|
+
return {
|
|
231
|
+
task: request.task,
|
|
232
|
+
plan: documented.plan,
|
|
233
|
+
planDigest: documented.planDigest,
|
|
234
|
+
approval: request.approval,
|
|
235
|
+
revision,
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
},
|
|
240
|
+
exits: {
|
|
241
|
+
ready: {
|
|
242
|
+
from: "finalize",
|
|
243
|
+
validate: (value: unknown): PlanChangeReady => value as PlanChangeReady,
|
|
244
|
+
},
|
|
245
|
+
blocked: {
|
|
246
|
+
from: "blocked",
|
|
247
|
+
validate: (value: unknown): PlanChangeBlocked => value as PlanChangeBlocked,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
nodes: {
|
|
251
|
+
start: compute({ run: () => ({ route: "design" }) }),
|
|
252
|
+
assessPlan: compute({
|
|
253
|
+
run: (context) => {
|
|
254
|
+
const request = context.input as NormalizedPlanChangeInput;
|
|
255
|
+
const design = currentDesign(context);
|
|
256
|
+
return request.previousPlan !== undefined && design.changed !== true
|
|
257
|
+
? {
|
|
258
|
+
route: "blocked",
|
|
259
|
+
reason: "Planning returned the same plan for the unresolved evidence.",
|
|
260
|
+
evidence: design,
|
|
261
|
+
}
|
|
262
|
+
: { route: "document", planDigest: design.planDigest };
|
|
263
|
+
},
|
|
264
|
+
}),
|
|
265
|
+
replanGuard: compute({
|
|
266
|
+
run: (context) => {
|
|
267
|
+
const request = context.input as NormalizedPlanChangeInput;
|
|
268
|
+
const replans = context.state.steps.filter(
|
|
269
|
+
(step) => step.nodeId === "approval/replan",
|
|
270
|
+
).length;
|
|
271
|
+
return replans > request.approval.maxReplans
|
|
272
|
+
? {
|
|
273
|
+
route: "blocked",
|
|
274
|
+
reason: `Plan approval reached the ${request.approval.maxReplans}-replan safety limit.`,
|
|
275
|
+
replans,
|
|
276
|
+
limit: request.approval.maxReplans,
|
|
277
|
+
}
|
|
278
|
+
: { route: "design", replans, limit: request.approval.maxReplans };
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
finalize: compute({
|
|
282
|
+
run: (context) => {
|
|
283
|
+
const documented = currentDocumentation(context);
|
|
284
|
+
const approval = currentApproval(context);
|
|
285
|
+
const revision = context.state.steps.filter(
|
|
286
|
+
(step) => step.nodeId === "approval/routePolicy",
|
|
287
|
+
).length;
|
|
288
|
+
return {
|
|
289
|
+
status: "ready",
|
|
290
|
+
plan: documented.plan,
|
|
291
|
+
planDigest: documented.planDigest,
|
|
292
|
+
documents: documented.documentation.files,
|
|
293
|
+
revision,
|
|
294
|
+
approval: approval.resolution,
|
|
295
|
+
documentation: documented.documentation,
|
|
296
|
+
} satisfies PlanChangeReady;
|
|
297
|
+
},
|
|
298
|
+
}),
|
|
299
|
+
blocked: compute({ run: blockedResult }),
|
|
300
|
+
},
|
|
301
|
+
edges: [
|
|
302
|
+
{ from: "start", to: "design" },
|
|
303
|
+
{ from: "design.ready", to: "assessPlan" },
|
|
304
|
+
{ from: "design.blocked", to: "blocked" },
|
|
305
|
+
{
|
|
306
|
+
from: "assessPlan",
|
|
307
|
+
switch: { on: "$.route", cases: { document: "documentation", blocked: "blocked" } },
|
|
308
|
+
},
|
|
309
|
+
{ from: "documentation.ready", to: "approval" },
|
|
310
|
+
{ from: "documentation.blocked", to: "blocked" },
|
|
311
|
+
{ from: "approval.continue", to: "finalize" },
|
|
312
|
+
{ from: "approval.stop", to: "blocked" },
|
|
313
|
+
{ from: "approval.replan", to: "replanGuard" },
|
|
314
|
+
{
|
|
315
|
+
from: "replanGuard",
|
|
316
|
+
switch: { on: "$.route", cases: { design: "design", blocked: "blocked" } },
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
export default planChangeWorkflow;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
legacyDecisionPresentation,
|
|
3
2
|
normalizeDecisionPresentation,
|
|
3
|
+
valueDecisionPresentation,
|
|
4
4
|
} from "../workflows/decision-presentation.js";
|
|
5
5
|
import type { DecisionPresentation } from "../workflows/types.js";
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ export function presentPlan(input: {
|
|
|
10
10
|
planDigest: string;
|
|
11
11
|
revision: number;
|
|
12
12
|
}): DecisionPresentation {
|
|
13
|
-
const plan =
|
|
13
|
+
const plan = valueDecisionPresentation(input.plan);
|
|
14
14
|
const summary =
|
|
15
15
|
readableSummary(planSummary(input.plan)) ??
|
|
16
16
|
readableSummary(`Review the implementation plan for ${input.task}.`)!;
|