@osolmaz/pi-workflows 0.10.0 → 0.11.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 +32 -14
- package/dist/builtins/autodoc.workflow.d.ts +58 -0
- package/dist/builtins/autodoc.workflow.js +266 -0
- package/dist/builtins/autodoc.workflow.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +212 -11
- package/dist/builtins/autoimplement.workflow.js +431 -22
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/{autodevise.workflow.d.ts → autoplan.workflow.d.ts} +9 -9
- package/dist/builtins/{autodevise.workflow.js → autoplan.workflow.js} +20 -20
- package/dist/builtins/autoplan.workflow.js.map +1 -0
- package/dist/builtins/catalog.js +8 -4
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +6 -3
- package/dist/builtins/index.js +5 -2
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +4 -0
- package/dist/builtins/monitor.workflow.js +134 -9
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/plan-approval.workflow.d.ts +83 -0
- package/dist/builtins/plan-approval.workflow.js +148 -0
- package/dist/builtins/plan-approval.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.d.ts +7 -0
- package/dist/builtins/plan-presentation.js +44 -0
- package/dist/builtins/plan-presentation.js.map +1 -0
- package/dist/extension/decision-channels.d.ts +132 -0
- package/dist/extension/decision-channels.js +1082 -0
- package/dist/extension/decision-channels.js.map +1 -0
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +426 -11
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/widget.js +26 -2
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-tool.d.ts +1 -37
- package/dist/extension/workflow-tool.js +1 -42
- package/dist/extension/workflow-tool.js.map +1 -1
- package/dist/herdr/setup.d.ts +13 -1
- package/dist/herdr/setup.js +349 -36
- package/dist/herdr/setup.js.map +1 -1
- package/dist/host/rpc-bridge.js +4 -21
- package/dist/host/rpc-bridge.js.map +1 -1
- package/dist/render/graph-render.js +33 -8
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/cli.d.ts +1 -0
- package/dist/viewer/cli.js +21 -10
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +36 -2
- package/dist/viewer/render.js.map +1 -1
- package/dist/workflows/composition.js +15 -1
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/decision-presentation.d.ts +18 -0
- package/dist/workflows/decision-presentation.js +417 -0
- package/dist/workflows/decision-presentation.js.map +1 -0
- package/dist/workflows/engine.d.ts +2 -1
- package/dist/workflows/engine.js +86 -4
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +87 -0
- package/dist/workflows/human-decision.js +583 -0
- package/dist/workflows/human-decision.js.map +1 -0
- package/dist/workflows/index.d.ts +3 -1
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +15 -0
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.js +7 -0
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +72 -0
- package/dist/workflows/tool-input.js +141 -0
- package/dist/workflows/tool-input.js.map +1 -0
- package/dist/workflows/types.d.ts +223 -0
- package/docs/HUMAN_DECISIONS.md +371 -0
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +323 -0
- package/docs/MONITOR.md +17 -6
- package/docs/WORKFLOW_COMPOSITION.md +13 -6
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +296 -0
- package/docs/plans/2026-08-19-human-decision-presentations-plan.md +172 -0
- package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +45 -0
- package/docs/plans/2026-08-19-workflow-composition-plan.md +25 -17
- package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +138 -0
- package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +104 -0
- package/docs/run-bundles.md +23 -2
- package/docs/workflows.md +47 -5
- package/examples/workflows/approved-plan.workflow.ts +58 -0
- package/examples/workflows/autoplan.workflow.ts +1 -0
- package/examples/workflows/human-decision.workflow.ts +62 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +2 -1
- package/schemas/decision-presentation-v1.schema.json +83 -0
- package/schemas/human-decision-accepted-v1.schema.json +44 -0
- package/schemas/human-decision-accepted-v2.schema.json +50 -0
- package/schemas/human-decision-answer-attempt-v1.schema.json +37 -0
- package/schemas/human-decision-cancellation-v1.schema.json +15 -0
- package/schemas/human-decision-continuation-v1.schema.json +16 -0
- package/schemas/human-decision-delivery-v1.schema.json +28 -0
- package/schemas/human-decision-delivery-v2.schema.json +36 -0
- package/schemas/human-decision-receipt-v1.schema.json +33 -0
- package/schemas/human-decision-receipt-v2.schema.json +39 -0
- package/schemas/human-decision-request-v1.schema.json +59 -0
- package/schemas/human-decision-request-v2.schema.json +69 -0
- package/schemas/human-decision-resolution-v1.schema.json +27 -0
- package/schemas/human-decision-resolution-v2.schema.json +27 -0
- package/schemas/human-decision-settlement-v1.schema.json +28 -0
- package/skills/autodoc/SKILL.md +43 -0
- package/skills/autoimplement/SKILL.md +58 -0
- package/skills/autoimplement/agents/openai.yaml +4 -0
- package/skills/autoplan/SKILL.md +25 -0
- package/skills/monitor/SKILL.md +3 -1
- package/skills/pi-workflows/SKILL.md +3 -3
- package/src/builtins/autodoc.workflow.ts +325 -0
- package/src/builtins/autoimplement.workflow.ts +493 -23
- package/src/builtins/{autodevise.workflow.ts → autoplan.workflow.ts} +32 -32
- package/src/builtins/catalog.ts +8 -4
- package/src/builtins/index.ts +25 -6
- package/src/builtins/monitor.workflow.ts +162 -8
- package/src/builtins/plan-approval.workflow.ts +190 -0
- package/src/builtins/plan-presentation.ts +57 -0
- package/src/extension/decision-channels.ts +1533 -0
- package/src/extension/index.ts +499 -9
- package/src/extension/widget.ts +39 -2
- package/src/extension/workflow-tool.ts +5 -81
- package/src/herdr/setup.ts +429 -39
- package/src/host/rpc-bridge.ts +7 -30
- package/src/render/graph-render.ts +40 -3
- package/src/viewer/cli.ts +22 -10
- package/src/viewer/render.ts +56 -3
- package/src/workflows/composition.ts +16 -1
- package/src/workflows/decision-presentation.ts +501 -0
- package/src/workflows/engine.ts +108 -4
- package/src/workflows/human-decision.ts +819 -0
- package/src/workflows/index.ts +69 -0
- package/src/workflows/schema.ts +17 -0
- package/src/workflows/store.ts +8 -0
- package/src/workflows/tool-input.ts +202 -0
- package/src/workflows/types.ts +265 -0
- package/dist/builtins/autodevise.workflow.js.map +0 -1
- package/examples/workflows/autodevise.workflow.ts +0 -1
|
@@ -7,12 +7,15 @@ import {
|
|
|
7
7
|
includedResult,
|
|
8
8
|
shell,
|
|
9
9
|
} from "../workflows/definition.js";
|
|
10
|
+
import { digest } from "../workflows/human-decision.js";
|
|
10
11
|
import type {
|
|
11
12
|
ShellActionExecution,
|
|
12
13
|
ShellActionResult,
|
|
13
14
|
WorkflowNodeContext,
|
|
14
15
|
} from "../workflows/types.js";
|
|
15
|
-
import
|
|
16
|
+
import autodocWorkflow, { type AutodocInput } from "./autodoc.workflow.js";
|
|
17
|
+
import autoplanWorkflow, { type AutoplanInput } from "./autoplan.workflow.js";
|
|
18
|
+
import planApprovalWorkflow, { type PlanApprovalInput } from "./plan-approval.workflow.js";
|
|
16
19
|
|
|
17
20
|
export type AutoimplementInput = {
|
|
18
21
|
task: string;
|
|
@@ -22,6 +25,25 @@ export type AutoimplementInput = {
|
|
|
22
25
|
repository?: string;
|
|
23
26
|
baseBranch?: string;
|
|
24
27
|
merge?: boolean;
|
|
28
|
+
documents?: string[];
|
|
29
|
+
documentation?: {
|
|
30
|
+
status: "current";
|
|
31
|
+
planDigest: string;
|
|
32
|
+
documents: string[];
|
|
33
|
+
};
|
|
34
|
+
approval?: {
|
|
35
|
+
audience: string;
|
|
36
|
+
maxReplans: number;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ExistingPlanDiscovery = {
|
|
41
|
+
route: "found" | "blocked";
|
|
42
|
+
plan?: unknown;
|
|
43
|
+
documentation?: "current" | "missing" | "stale";
|
|
44
|
+
documents: string[];
|
|
45
|
+
reason: string;
|
|
46
|
+
evidence: unknown;
|
|
25
47
|
};
|
|
26
48
|
|
|
27
49
|
type StructuredCommand = {
|
|
@@ -65,8 +87,22 @@ export type AutoimplementBlocked = {
|
|
|
65
87
|
evidence: unknown;
|
|
66
88
|
};
|
|
67
89
|
|
|
90
|
+
type BlockerChallenge = {
|
|
91
|
+
route: "continue" | "blocked";
|
|
92
|
+
blockingNow: boolean;
|
|
93
|
+
outsideAuthority: boolean;
|
|
94
|
+
canProceed: boolean;
|
|
95
|
+
reason: string;
|
|
96
|
+
nextAction: string;
|
|
97
|
+
alternativesChecked: string[];
|
|
98
|
+
evidence: string[];
|
|
99
|
+
};
|
|
100
|
+
|
|
68
101
|
const FIVE_MINUTES_MS = 5 * 60_000;
|
|
69
102
|
const TEN_MINUTES_MS = 10 * 60_000;
|
|
103
|
+
const MAX_BLOCKER_CHALLENGES = 3;
|
|
104
|
+
const MAX_CHALLENGE_ITEMS = 5;
|
|
105
|
+
const MAX_CHALLENGE_TEXT = 500;
|
|
70
106
|
|
|
71
107
|
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
72
108
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -82,6 +118,80 @@ function requireString(value: unknown, label: string): string {
|
|
|
82
118
|
return value.trim();
|
|
83
119
|
}
|
|
84
120
|
|
|
121
|
+
function boundedChallengeItems(value: unknown, label: string): string[] {
|
|
122
|
+
if (!Array.isArray(value) || value.length > MAX_CHALLENGE_ITEMS) {
|
|
123
|
+
throw new Error(`${label} must be an array with at most ${MAX_CHALLENGE_ITEMS} items`);
|
|
124
|
+
}
|
|
125
|
+
return value.map((item, index) => {
|
|
126
|
+
const text = requireString(item, `${label}[${index}]`);
|
|
127
|
+
if (text.length > MAX_CHALLENGE_TEXT) {
|
|
128
|
+
throw new Error(`${label}[${index}] must be at most ${MAX_CHALLENGE_TEXT} characters`);
|
|
129
|
+
}
|
|
130
|
+
return text;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseBlockerChallenge(value: unknown): BlockerChallenge {
|
|
135
|
+
const result = requireRecord(value, "blocker challenge");
|
|
136
|
+
if (result.route !== "continue" && result.route !== "blocked") {
|
|
137
|
+
throw new Error("blocker challenge route must be continue or blocked");
|
|
138
|
+
}
|
|
139
|
+
for (const key of ["blockingNow", "outsideAuthority", "canProceed"] as const) {
|
|
140
|
+
if (typeof result[key] !== "boolean") {
|
|
141
|
+
throw new Error(`blocker challenge ${key} must be a boolean`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const blockingNow = result.blockingNow as boolean;
|
|
145
|
+
const outsideAuthority = result.outsideAuthority as boolean;
|
|
146
|
+
const canProceed = result.canProceed as boolean;
|
|
147
|
+
const reason = requireString(result.reason, "blocker challenge reason");
|
|
148
|
+
if (reason.length > MAX_CHALLENGE_TEXT) {
|
|
149
|
+
throw new Error(`blocker challenge reason must be at most ${MAX_CHALLENGE_TEXT} characters`);
|
|
150
|
+
}
|
|
151
|
+
if (typeof result.nextAction !== "string") {
|
|
152
|
+
throw new Error("blocker challenge nextAction must be a string");
|
|
153
|
+
}
|
|
154
|
+
const nextAction = result.nextAction.trim();
|
|
155
|
+
if (nextAction.length > MAX_CHALLENGE_TEXT) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`blocker challenge nextAction must be at most ${MAX_CHALLENGE_TEXT} characters`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const alternativesChecked = boundedChallengeItems(
|
|
161
|
+
result.alternativesChecked,
|
|
162
|
+
"blocker challenge alternativesChecked",
|
|
163
|
+
);
|
|
164
|
+
const evidence = boundedChallengeItems(result.evidence, "blocker challenge evidence");
|
|
165
|
+
|
|
166
|
+
if (result.route === "blocked") {
|
|
167
|
+
if (
|
|
168
|
+
blockingNow !== true ||
|
|
169
|
+
outsideAuthority !== true ||
|
|
170
|
+
canProceed !== false ||
|
|
171
|
+
nextAction.length > 0 ||
|
|
172
|
+
alternativesChecked.length === 0 ||
|
|
173
|
+
evidence.length === 0
|
|
174
|
+
) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
"blocked challenge requires blockingNow=true, outsideAuthority=true, canProceed=false, an empty nextAction, and concrete alternatives and evidence",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
} else if (canProceed !== true || nextAction.length === 0) {
|
|
180
|
+
throw new Error("continue challenge requires canProceed=true and a practical nextAction");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
route: result.route,
|
|
185
|
+
blockingNow,
|
|
186
|
+
outsideAuthority,
|
|
187
|
+
canProceed,
|
|
188
|
+
reason,
|
|
189
|
+
nextAction,
|
|
190
|
+
alternativesChecked,
|
|
191
|
+
evidence,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
85
195
|
function parseInput(value: unknown): AutoimplementInput {
|
|
86
196
|
const input = requireRecord(value, "autoimplement input");
|
|
87
197
|
const constraints = input.constraints;
|
|
@@ -94,6 +204,51 @@ function parseInput(value: unknown): AutoimplementInput {
|
|
|
94
204
|
if (input.merge !== undefined && typeof input.merge !== "boolean") {
|
|
95
205
|
throw new Error("autoimplement merge must be a boolean");
|
|
96
206
|
}
|
|
207
|
+
const documents = input.documents;
|
|
208
|
+
if (
|
|
209
|
+
documents !== undefined &&
|
|
210
|
+
(!Array.isArray(documents) || documents.some((item) => typeof item !== "string"))
|
|
211
|
+
) {
|
|
212
|
+
throw new Error("autoimplement documents must be an array of strings");
|
|
213
|
+
}
|
|
214
|
+
let documentation: AutoimplementInput["documentation"];
|
|
215
|
+
if (input.documentation !== undefined) {
|
|
216
|
+
if (input.plan === undefined) {
|
|
217
|
+
throw new Error("autoimplement documentation requires an explicit plan");
|
|
218
|
+
}
|
|
219
|
+
const raw = requireRecord(input.documentation, "autoimplement documentation");
|
|
220
|
+
if (raw.status !== "current") {
|
|
221
|
+
throw new Error("autoimplement documentation status must be current");
|
|
222
|
+
}
|
|
223
|
+
const planDigest = requireString(raw.planDigest, "autoimplement documentation planDigest");
|
|
224
|
+
if (planDigest !== digest(input.plan)) {
|
|
225
|
+
throw new Error("autoimplement documentation planDigest does not match the explicit plan");
|
|
226
|
+
}
|
|
227
|
+
if (!Array.isArray(raw.documents) || raw.documents.some((item) => typeof item !== "string")) {
|
|
228
|
+
throw new Error("autoimplement documentation documents must be an array of strings");
|
|
229
|
+
}
|
|
230
|
+
documentation = {
|
|
231
|
+
status: "current",
|
|
232
|
+
planDigest,
|
|
233
|
+
documents: [...raw.documents] as string[],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
let approval: AutoimplementInput["approval"];
|
|
237
|
+
if (input.approval !== undefined) {
|
|
238
|
+
const raw = requireRecord(input.approval, "autoimplement approval");
|
|
239
|
+
const maxReplans = raw.maxReplans ?? 3;
|
|
240
|
+
if (
|
|
241
|
+
!Number.isInteger(maxReplans) ||
|
|
242
|
+
(maxReplans as number) < 1 ||
|
|
243
|
+
(maxReplans as number) > 20
|
|
244
|
+
) {
|
|
245
|
+
throw new Error("autoimplement approval maxReplans must be from 1 through 20");
|
|
246
|
+
}
|
|
247
|
+
approval = {
|
|
248
|
+
audience: requireString(raw.audience, "autoimplement approval audience"),
|
|
249
|
+
maxReplans: maxReplans as number,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
97
252
|
return {
|
|
98
253
|
task: requireString(input.task, "autoimplement task"),
|
|
99
254
|
...(input.plan !== undefined ? { plan: input.plan } : {}),
|
|
@@ -106,6 +261,42 @@ function parseInput(value: unknown): AutoimplementInput {
|
|
|
106
261
|
? { baseBranch: requireString(input.baseBranch, "baseBranch") }
|
|
107
262
|
: {}),
|
|
108
263
|
merge: input.merge === true,
|
|
264
|
+
...(documents !== undefined ? { documents: [...documents] as string[] } : {}),
|
|
265
|
+
...(documentation !== undefined ? { documentation } : {}),
|
|
266
|
+
...(approval !== undefined ? { approval } : {}),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function parseExistingPlan(value: unknown): ExistingPlanDiscovery {
|
|
271
|
+
const result = requireRecord(value, "existing plan discovery");
|
|
272
|
+
if (result.route !== "found" && result.route !== "blocked") {
|
|
273
|
+
throw new Error("existing plan discovery route must be found or blocked");
|
|
274
|
+
}
|
|
275
|
+
if (result.route === "found") {
|
|
276
|
+
if (result.plan === undefined) throw new Error("found plan must include plan");
|
|
277
|
+
if (
|
|
278
|
+
result.documentation !== "current" &&
|
|
279
|
+
result.documentation !== "missing" &&
|
|
280
|
+
result.documentation !== "stale"
|
|
281
|
+
) {
|
|
282
|
+
throw new Error("found plan documentation must be current, missing, or stale");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (
|
|
286
|
+
!Array.isArray(result.documents) ||
|
|
287
|
+
result.documents.some((item) => typeof item !== "string")
|
|
288
|
+
) {
|
|
289
|
+
throw new Error("existing plan documents must be an array of strings");
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
route: result.route,
|
|
293
|
+
...(result.plan !== undefined ? { plan: result.plan } : {}),
|
|
294
|
+
...(result.documentation !== undefined
|
|
295
|
+
? { documentation: result.documentation as "current" | "missing" | "stale" }
|
|
296
|
+
: {}),
|
|
297
|
+
documents: [...result.documents] as string[],
|
|
298
|
+
reason: requireString(result.reason, "existing plan discovery reason"),
|
|
299
|
+
evidence: result.evidence ?? null,
|
|
109
300
|
};
|
|
110
301
|
}
|
|
111
302
|
|
|
@@ -210,11 +401,80 @@ function latestCiCommand(context: WorkflowNodeContext): StructuredCommand {
|
|
|
210
401
|
|
|
211
402
|
function currentPlan(context: WorkflowNodeContext): unknown {
|
|
212
403
|
const adopted = context.outputs.adoptPlan as { plan?: unknown } | undefined;
|
|
213
|
-
|
|
404
|
+
if (adopted?.plan !== undefined) return adopted.plan;
|
|
405
|
+
const documented = context.outputs.documentation as
|
|
406
|
+
| { exit?: string; output?: { plan?: unknown } }
|
|
407
|
+
| undefined;
|
|
408
|
+
if (documented?.exit === "ready" && documented.output?.plan !== undefined) {
|
|
409
|
+
return documented.output.plan;
|
|
410
|
+
}
|
|
411
|
+
const discovered = context.outputs.findPlan as ExistingPlanDiscovery | undefined;
|
|
412
|
+
if (discovered?.route === "found" && discovered.plan !== undefined) return discovered.plan;
|
|
413
|
+
return (context.input as AutoimplementInput).plan;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function currentPlanDigest(context: WorkflowNodeContext): string {
|
|
417
|
+
const adopted = context.outputs.adoptPlan as { planDigest?: unknown } | undefined;
|
|
418
|
+
if (typeof adopted?.planDigest === "string") return adopted.planDigest;
|
|
419
|
+
const documented = context.outputs.documentation as
|
|
420
|
+
| { exit?: string; output?: { planDigest?: unknown } }
|
|
421
|
+
| undefined;
|
|
422
|
+
if (documented?.exit === "ready" && typeof documented.output?.planDigest === "string") {
|
|
423
|
+
return documented.output.planDigest;
|
|
424
|
+
}
|
|
425
|
+
const plan = currentPlan(context);
|
|
426
|
+
if (plan === undefined) throw new Error("autoimplement does not have a selected plan");
|
|
427
|
+
return digest(plan);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function blockerChallenges(context: WorkflowNodeContext): BlockerChallenge[] {
|
|
431
|
+
return context.state.steps
|
|
432
|
+
.filter((step) => step.nodeId === "challengeBlocker" && step.outcome === "ok")
|
|
433
|
+
.map((step) => step.output as BlockerChallenge);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function latestBlockerClaim(context: WorkflowNodeContext): unknown {
|
|
437
|
+
const ids = [
|
|
438
|
+
"classifyImplementation",
|
|
439
|
+
"classifyVerification",
|
|
440
|
+
"repairReviewCommand",
|
|
441
|
+
"inspectComments",
|
|
442
|
+
"inspectCi",
|
|
443
|
+
"repairCiCommand",
|
|
444
|
+
"assessTrackedCi",
|
|
445
|
+
"classifyCi",
|
|
446
|
+
"finalizeDelivery",
|
|
447
|
+
];
|
|
448
|
+
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
449
|
+
const step = context.state.steps[index];
|
|
450
|
+
if (step && ids.includes(step.nodeId)) {
|
|
451
|
+
return { source: step.nodeId, result: step.output };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
throw new Error("No model-generated blocker claim is available to challenge");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function recentWorkflowAttempts(context: WorkflowNodeContext): unknown[] {
|
|
458
|
+
return context.state.steps.slice(-12).map((step) => ({
|
|
459
|
+
nodeId: step.nodeId,
|
|
460
|
+
outcome: step.outcome,
|
|
461
|
+
output: step.output,
|
|
462
|
+
}));
|
|
214
463
|
}
|
|
215
464
|
|
|
216
465
|
function latestIssue(context: WorkflowNodeContext): unknown {
|
|
466
|
+
const approval = context.outputs.approval as
|
|
467
|
+
| { exit?: string; output?: { instructions?: unknown } }
|
|
468
|
+
| undefined;
|
|
469
|
+
if (approval?.exit === "replan" && typeof approval.output?.instructions === "string") {
|
|
470
|
+
return {
|
|
471
|
+
source: "human-replan",
|
|
472
|
+
instructions: approval.output.instructions,
|
|
473
|
+
priorPlanDigest: currentPlanDigest(context),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
217
476
|
const ids = [
|
|
477
|
+
"challengeBlocker",
|
|
218
478
|
"classifyImplementation",
|
|
219
479
|
"classifyVerification",
|
|
220
480
|
"triageReview",
|
|
@@ -279,6 +539,8 @@ function reviewRounds(context: WorkflowNodeContext): ReviewAssessment[] {
|
|
|
279
539
|
|
|
280
540
|
function latestBlockedReason(context: WorkflowNodeContext): { reason: string; evidence: unknown } {
|
|
281
541
|
const candidates = [
|
|
542
|
+
"challengeBlockerGuard",
|
|
543
|
+
"challengeBlocker",
|
|
282
544
|
"finalizeDelivery",
|
|
283
545
|
"inspectCi",
|
|
284
546
|
"assessTrackedCi",
|
|
@@ -287,7 +549,11 @@ function latestBlockedReason(context: WorkflowNodeContext): { reason: string; ev
|
|
|
287
549
|
"classifyImplementation",
|
|
288
550
|
"classifyVerification",
|
|
289
551
|
"triageReview",
|
|
552
|
+
"replanGuard",
|
|
290
553
|
"adoptPlan",
|
|
554
|
+
"findPlan",
|
|
555
|
+
"documentation",
|
|
556
|
+
"approval",
|
|
291
557
|
];
|
|
292
558
|
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
293
559
|
const step = context.state.steps[index];
|
|
@@ -311,12 +577,48 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
311
577
|
presentationPrompt:
|
|
312
578
|
"Summarize what was implemented, the review rounds by severity, the CI result, the PR or merge result, and any remaining limitation. Include exact validation commands.",
|
|
313
579
|
startAt: "prepare",
|
|
314
|
-
maxSteps:
|
|
580
|
+
maxSteps: 240,
|
|
315
581
|
includes: {
|
|
582
|
+
documentation: includeWorkflow(autodocWorkflow, {
|
|
583
|
+
input: (context): AutodocInput => {
|
|
584
|
+
const request = context.input as AutoimplementInput;
|
|
585
|
+
const discovery = context.outputs.findPlan as ExistingPlanDiscovery | undefined;
|
|
586
|
+
const plan = currentPlan(context);
|
|
587
|
+
if (plan === undefined) throw new Error("autoimplement documentation is missing a plan");
|
|
588
|
+
return {
|
|
589
|
+
task: request.task,
|
|
590
|
+
plan,
|
|
591
|
+
...(request.repository !== undefined ? { repository: request.repository } : {}),
|
|
592
|
+
documents:
|
|
593
|
+
request.documents ?? request.documentation?.documents ?? discovery?.documents ?? [],
|
|
594
|
+
evidence: latestIssue(context),
|
|
595
|
+
};
|
|
596
|
+
},
|
|
597
|
+
}),
|
|
598
|
+
approval: includeWorkflow(planApprovalWorkflow, {
|
|
599
|
+
input: (context): PlanApprovalInput => {
|
|
600
|
+
const request = context.input as AutoimplementInput;
|
|
601
|
+
if (request.approval === undefined) {
|
|
602
|
+
throw new Error("autoimplement approval was entered without an approval policy");
|
|
603
|
+
}
|
|
604
|
+
const plan = currentPlan(context);
|
|
605
|
+
if (plan === undefined) throw new Error("autoimplement approval is missing a plan");
|
|
606
|
+
const revisions = context.state.steps.filter(
|
|
607
|
+
(step) => step.nodeId === "approval/approve",
|
|
608
|
+
).length;
|
|
609
|
+
return {
|
|
610
|
+
task: request.task,
|
|
611
|
+
plan,
|
|
612
|
+
planDigest: currentPlanDigest(context),
|
|
613
|
+
audience: request.approval.audience,
|
|
614
|
+
revision: revisions + 1,
|
|
615
|
+
};
|
|
616
|
+
},
|
|
617
|
+
}),
|
|
316
618
|
redesign: includeWorkflow({
|
|
317
|
-
workflow: "
|
|
318
|
-
contract:
|
|
319
|
-
input: (context):
|
|
619
|
+
workflow: "autoplan",
|
|
620
|
+
contract: autoplanWorkflow,
|
|
621
|
+
input: (context): AutoplanInput => {
|
|
320
622
|
const request = context.input as AutoimplementInput;
|
|
321
623
|
return {
|
|
322
624
|
problem: request.task,
|
|
@@ -340,25 +642,87 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
340
642
|
},
|
|
341
643
|
nodes: {
|
|
342
644
|
prepare: compute({
|
|
343
|
-
run: ({ input }) =>
|
|
344
|
-
|
|
345
|
-
|
|
645
|
+
run: ({ input }) => {
|
|
646
|
+
const request = input as AutoimplementInput;
|
|
647
|
+
return {
|
|
648
|
+
route:
|
|
649
|
+
request.plan === undefined
|
|
650
|
+
? "find"
|
|
651
|
+
: request.documentation?.status === "current"
|
|
652
|
+
? "ready"
|
|
653
|
+
: "document",
|
|
654
|
+
};
|
|
655
|
+
},
|
|
656
|
+
}),
|
|
657
|
+
findPlan: agent({
|
|
658
|
+
statusDetail: "finding existing plan",
|
|
659
|
+
prompt: ({ input }) => {
|
|
660
|
+
const request = input as AutoimplementInput;
|
|
661
|
+
return [
|
|
662
|
+
"Find the clear plan that has already been selected for this task.",
|
|
663
|
+
"Use the current conversation context and referenced canonical documents.",
|
|
664
|
+
"Do not devise, improve, replace, document, or implement a plan.",
|
|
665
|
+
"Return blocked when no single clear existing plan can be found.",
|
|
666
|
+
"Report whether its canonical documentation is current, missing, or stale.",
|
|
667
|
+
`Task: ${request.task}`,
|
|
668
|
+
`Repository: ${request.repository ?? "current repository"}`,
|
|
669
|
+
`Referenced documents: ${JSON.stringify(request.documents ?? [])}`,
|
|
670
|
+
].join("\n");
|
|
671
|
+
},
|
|
672
|
+
expectedOutput:
|
|
673
|
+
'{ "route": "found" | "blocked", "plan": {} (required when found), "documentation": "current" | "missing" | "stale" (required when found), "documents": ["canonical file"], "reason": "reason", "evidence": "evidence" }',
|
|
674
|
+
validate: parseExistingPlan,
|
|
675
|
+
}),
|
|
676
|
+
routeFoundPlan: compute({
|
|
677
|
+
run: ({ outputs }) => {
|
|
678
|
+
const discovered = outputs.findPlan as ExistingPlanDiscovery;
|
|
679
|
+
if (discovered.route !== "found" || discovered.plan === undefined) {
|
|
680
|
+
return { route: "blocked", reason: discovered.reason, evidence: discovered.evidence };
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
route: discovered.documentation === "current" ? "ready" : "document",
|
|
684
|
+
plan: discovered.plan,
|
|
685
|
+
reason: discovered.reason,
|
|
686
|
+
evidence: discovered.evidence,
|
|
687
|
+
};
|
|
688
|
+
},
|
|
346
689
|
}),
|
|
347
690
|
adoptPlan: compute({
|
|
348
691
|
run: ({ outputs }) => {
|
|
349
|
-
const result = includedResult(
|
|
692
|
+
const result = includedResult(autoplanWorkflow, outputs.redesign);
|
|
350
693
|
if (result.exit !== "ready") throw new Error("redesign did not return a ready plan");
|
|
351
694
|
return {
|
|
352
|
-
route: result.output.changed ? "
|
|
695
|
+
route: result.output.changed ? "document" : "blocked",
|
|
353
696
|
plan: result.output.plan,
|
|
354
697
|
planDigest: result.output.planDigest,
|
|
355
698
|
changed: result.output.changed,
|
|
356
699
|
reason: result.output.changed
|
|
357
|
-
? "The plan changed in response to new evidence."
|
|
700
|
+
? "The plan changed in response to new evidence and must be documented."
|
|
358
701
|
: "Redesign returned the same plan for the same unresolved evidence.",
|
|
359
702
|
};
|
|
360
703
|
},
|
|
361
704
|
}),
|
|
705
|
+
maybeApproval: compute({
|
|
706
|
+
run: ({ input }) => ({
|
|
707
|
+
route: (input as AutoimplementInput).approval === undefined ? "implement" : "approve",
|
|
708
|
+
}),
|
|
709
|
+
}),
|
|
710
|
+
replanGuard: compute({
|
|
711
|
+
run: (context) => {
|
|
712
|
+
const request = context.input as AutoimplementInput;
|
|
713
|
+
const limit = request.approval?.maxReplans ?? 3;
|
|
714
|
+
const replans = context.state.steps.filter(
|
|
715
|
+
(step) => step.nodeId === "approval/replan",
|
|
716
|
+
).length;
|
|
717
|
+
return replans > limit
|
|
718
|
+
? {
|
|
719
|
+
route: "blocked",
|
|
720
|
+
reason: `Plan approval reached the ${limit}-replan safety limit.`,
|
|
721
|
+
evidence: context.outputs.approval,
|
|
722
|
+
}
|
|
723
|
+
: { route: "redesign", replans, limit };
|
|
724
|
+
},
|
|
725
|
+
}),
|
|
362
726
|
implement: agent({
|
|
363
727
|
timeoutMs: 60 * 60_000,
|
|
364
728
|
statusDetail: "implementing",
|
|
@@ -367,6 +731,8 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
367
731
|
return [
|
|
368
732
|
`Implement this task end-to-end: ${request.task}`,
|
|
369
733
|
`Plan: ${JSON.stringify(currentPlan(context))}`,
|
|
734
|
+
`Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
|
|
735
|
+
`Constraints: ${JSON.stringify(request.constraints ?? [])}`,
|
|
370
736
|
"Follow repository instructions and use the most elegant long-term production-ready implementation without unnecessary work.",
|
|
371
737
|
"If implementation exposes a new design or scope problem, report it precisely instead of forcing the old plan.",
|
|
372
738
|
"Do not merge yet.",
|
|
@@ -394,6 +760,51 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
394
760
|
"implementation assessment",
|
|
395
761
|
),
|
|
396
762
|
}),
|
|
763
|
+
challengeBlockerGuard: compute({
|
|
764
|
+
run: (context) => {
|
|
765
|
+
const challenges = blockerChallenges(context);
|
|
766
|
+
return challenges.length >= MAX_BLOCKER_CHALLENGES
|
|
767
|
+
? {
|
|
768
|
+
route: "blocked",
|
|
769
|
+
reason: `Blocker challenge reached the ${MAX_BLOCKER_CHALLENGES}-attempt workflow safety limit.`,
|
|
770
|
+
evidence: { attempts: challenges.length, challenges },
|
|
771
|
+
}
|
|
772
|
+
: {
|
|
773
|
+
route: "challenge",
|
|
774
|
+
attempt: challenges.length + 1,
|
|
775
|
+
limit: MAX_BLOCKER_CHALLENGES,
|
|
776
|
+
};
|
|
777
|
+
},
|
|
778
|
+
}),
|
|
779
|
+
challengeBlocker: agent({
|
|
780
|
+
statusDetail: "challenging blocker claim",
|
|
781
|
+
prompt: (context) => {
|
|
782
|
+
const request = context.input as AutoimplementInput;
|
|
783
|
+
return [
|
|
784
|
+
"Independently challenge the latest claim that autoimplement is blocked.",
|
|
785
|
+
"Are you really blocked?",
|
|
786
|
+
"Is this really a blocker right now?",
|
|
787
|
+
"Can you find a safe way to move forward and finish this?",
|
|
788
|
+
"Are you getting stuck on something trivial, procedural, reversible, or already authorized?",
|
|
789
|
+
"Inspect the task, approved plan, current result, evidence, scope, authority, previous attempts, and viable alternatives.",
|
|
790
|
+
"Distinguish a true external blocker from ordinary rollout work, local implementation work, a design adjustment, a missing verification step, or a reversible operational task.",
|
|
791
|
+
"A local test failure, stale package, packaging or artifact mismatch, rollback preparation, or deployment procedure is not by itself outside authority.",
|
|
792
|
+
"If a safe deployment and rollback path is already authorized, a supported cutover is work to do, not a blocker.",
|
|
793
|
+
"Confirm blocked only when the issue blocks progress now, is outside authority, and has no safe practical path forward.",
|
|
794
|
+
"Return continue with the next practical action when work can proceed. Keep text concise, with at most five alternatives and five evidence items.",
|
|
795
|
+
`Task: ${request.task}`,
|
|
796
|
+
`Approved plan: ${JSON.stringify(currentPlan(context))}`,
|
|
797
|
+
`Current result and claimed blocker: ${JSON.stringify(latestBlockerClaim(context))}`,
|
|
798
|
+
`Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
|
|
799
|
+
`Constraints and authority: ${JSON.stringify(request.constraints ?? [])}`,
|
|
800
|
+
`Merge authorized: ${request.merge === true}`,
|
|
801
|
+
`Previous blocker challenges: ${JSON.stringify(blockerChallenges(context))}`,
|
|
802
|
+
`Recent workflow attempts: ${JSON.stringify(recentWorkflowAttempts(context))}`,
|
|
803
|
+
].join("\n");
|
|
804
|
+
},
|
|
805
|
+
expectedOutput: `{ "route": "continue" | "blocked", "blockingNow": true | false, "outsideAuthority": true | false, "canProceed": true | false, "reason": "concise reason", "nextAction": "practical action or empty when blocked", "alternativesChecked": ["checked alternative"], "evidence": ["concrete evidence"] }`,
|
|
806
|
+
validate: parseBlockerChallenge,
|
|
807
|
+
}),
|
|
397
808
|
verify: agent({
|
|
398
809
|
timeoutMs: 45 * 60_000,
|
|
399
810
|
statusDetail: "verifying",
|
|
@@ -728,28 +1139,73 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
728
1139
|
edges: [
|
|
729
1140
|
{
|
|
730
1141
|
from: "prepare",
|
|
731
|
-
switch: {
|
|
1142
|
+
switch: {
|
|
1143
|
+
on: "$.route",
|
|
1144
|
+
cases: { find: "findPlan", document: "documentation", ready: "maybeApproval" },
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
from: "findPlan",
|
|
1149
|
+
switch: { on: "$.route", cases: { found: "routeFoundPlan", blocked: "blocked" } },
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
from: "routeFoundPlan",
|
|
1153
|
+
switch: {
|
|
1154
|
+
on: "$.route",
|
|
1155
|
+
cases: { ready: "maybeApproval", document: "documentation", blocked: "blocked" },
|
|
1156
|
+
},
|
|
732
1157
|
},
|
|
733
1158
|
{ from: "redesign.ready", to: "adoptPlan" },
|
|
734
1159
|
{ from: "redesign.blocked", to: "blocked" },
|
|
735
1160
|
{
|
|
736
1161
|
from: "adoptPlan",
|
|
737
|
-
switch: { on: "$.route", cases: {
|
|
1162
|
+
switch: { on: "$.route", cases: { document: "documentation", blocked: "blocked" } },
|
|
1163
|
+
},
|
|
1164
|
+
{ from: "documentation.ready", to: "maybeApproval" },
|
|
1165
|
+
{ from: "documentation.blocked", to: "blocked" },
|
|
1166
|
+
{
|
|
1167
|
+
from: "maybeApproval",
|
|
1168
|
+
switch: { on: "$.route", cases: { approve: "approval", implement: "implement" } },
|
|
1169
|
+
},
|
|
1170
|
+
{ from: "approval.continue", to: "implement" },
|
|
1171
|
+
{ from: "approval.stop", to: "blocked" },
|
|
1172
|
+
{ from: "approval.replan", to: "replanGuard" },
|
|
1173
|
+
{
|
|
1174
|
+
from: "replanGuard",
|
|
1175
|
+
switch: { on: "$.route", cases: { redesign: "redesign", blocked: "blocked" } },
|
|
738
1176
|
},
|
|
739
1177
|
{ from: "implement", to: "classifyImplementation" },
|
|
740
1178
|
{
|
|
741
1179
|
from: "classifyImplementation",
|
|
742
1180
|
switch: {
|
|
743
1181
|
on: "$.route",
|
|
744
|
-
cases: {
|
|
1182
|
+
cases: {
|
|
1183
|
+
verify: "verify",
|
|
1184
|
+
redesign: "redesign",
|
|
1185
|
+
fix: "fix",
|
|
1186
|
+
blocked: "challengeBlockerGuard",
|
|
1187
|
+
},
|
|
745
1188
|
},
|
|
746
1189
|
},
|
|
1190
|
+
{
|
|
1191
|
+
from: "challengeBlockerGuard",
|
|
1192
|
+
switch: { on: "$.route", cases: { challenge: "challengeBlocker", blocked: "blocked" } },
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
from: "challengeBlocker",
|
|
1196
|
+
switch: { on: "$.route", cases: { continue: "redesign", blocked: "blocked" } },
|
|
1197
|
+
},
|
|
747
1198
|
{ from: "verify", to: "classifyVerification" },
|
|
748
1199
|
{
|
|
749
1200
|
from: "classifyVerification",
|
|
750
1201
|
switch: {
|
|
751
1202
|
on: "$.route",
|
|
752
|
-
cases: {
|
|
1203
|
+
cases: {
|
|
1204
|
+
publish: "publish",
|
|
1205
|
+
redesign: "redesign",
|
|
1206
|
+
fix: "fix",
|
|
1207
|
+
blocked: "challengeBlockerGuard",
|
|
1208
|
+
},
|
|
753
1209
|
},
|
|
754
1210
|
},
|
|
755
1211
|
{ from: "fix", to: "verify" },
|
|
@@ -768,7 +1224,10 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
768
1224
|
},
|
|
769
1225
|
{
|
|
770
1226
|
from: "repairReviewCommand",
|
|
771
|
-
switch: {
|
|
1227
|
+
switch: {
|
|
1228
|
+
on: "$.route",
|
|
1229
|
+
cases: { retry: "runReview", blocked: "challengeBlockerGuard" },
|
|
1230
|
+
},
|
|
772
1231
|
},
|
|
773
1232
|
{
|
|
774
1233
|
from: "assessReview",
|
|
@@ -795,7 +1254,12 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
795
1254
|
from: "inspectComments",
|
|
796
1255
|
switch: {
|
|
797
1256
|
on: "$.route",
|
|
798
|
-
cases: {
|
|
1257
|
+
cases: {
|
|
1258
|
+
redesign: "redesign",
|
|
1259
|
+
fix: "fix",
|
|
1260
|
+
ci: "inspectCi",
|
|
1261
|
+
blocked: "challengeBlockerGuard",
|
|
1262
|
+
},
|
|
799
1263
|
},
|
|
800
1264
|
},
|
|
801
1265
|
{
|
|
@@ -806,7 +1270,7 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
806
1270
|
green: "finalizeDelivery",
|
|
807
1271
|
failed: "classifyCi",
|
|
808
1272
|
pending: "trackCi",
|
|
809
|
-
unavailable: "
|
|
1273
|
+
unavailable: "challengeBlockerGuard",
|
|
810
1274
|
},
|
|
811
1275
|
},
|
|
812
1276
|
},
|
|
@@ -819,7 +1283,10 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
819
1283
|
},
|
|
820
1284
|
{
|
|
821
1285
|
from: "repairCiCommand",
|
|
822
|
-
switch: {
|
|
1286
|
+
switch: {
|
|
1287
|
+
on: "$.route",
|
|
1288
|
+
cases: { retry: "trackCi", blocked: "challengeBlockerGuard" },
|
|
1289
|
+
},
|
|
823
1290
|
},
|
|
824
1291
|
{
|
|
825
1292
|
from: "assessTrackedCi",
|
|
@@ -829,7 +1296,7 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
829
1296
|
green: "finalizeDelivery",
|
|
830
1297
|
failed: "classifyCi",
|
|
831
1298
|
pending: "opportunisticTest",
|
|
832
|
-
unavailable: "
|
|
1299
|
+
unavailable: "challengeBlockerGuard",
|
|
833
1300
|
},
|
|
834
1301
|
},
|
|
835
1302
|
},
|
|
@@ -842,13 +1309,16 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
842
1309
|
redesign: "redesign",
|
|
843
1310
|
fix: "fix",
|
|
844
1311
|
unrelated: "finalizeDelivery",
|
|
845
|
-
blocked: "
|
|
1312
|
+
blocked: "challengeBlockerGuard",
|
|
846
1313
|
},
|
|
847
1314
|
},
|
|
848
1315
|
},
|
|
849
1316
|
{
|
|
850
1317
|
from: "finalizeDelivery",
|
|
851
|
-
switch: {
|
|
1318
|
+
switch: {
|
|
1319
|
+
on: "$.status",
|
|
1320
|
+
cases: { completed: "finalize", blocked: "challengeBlockerGuard" },
|
|
1321
|
+
},
|
|
852
1322
|
},
|
|
853
1323
|
],
|
|
854
1324
|
});
|