@osolmaz/pi-workflows 0.11.2 → 0.12.0
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 +11 -7
- 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 +29 -3
- package/dist/builtins/autoimplement.workflow.js +876 -231
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +3 -1
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +2 -0
- package/dist/builtins/index.js +1 -0
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/sanity-check-session.d.ts +17 -0
- package/dist/builtins/sanity-check-session.js +168 -0
- package/dist/builtins/sanity-check-session.js.map +1 -0
- package/dist/builtins/sanity-check.workflow.d.ts +83 -0
- package/dist/builtins/sanity-check.workflow.js +398 -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.js +7 -7
- 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 +286 -47
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/step-message.d.ts +1 -0
- package/dist/extension/step-message.js.map +1 -1
- package/dist/herdr/setup.js +4 -4
- 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/engine.js +8 -4
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -0
- package/dist/workflows/index.js +1 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +2 -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 +3 -1
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +4 -3
- 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 +7 -7
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +3 -3
- package/docs/MONITOR.md +2 -2
- package/docs/WORKFLOW_COMPOSITION.md +2 -2
- 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-sanity-check-plan.md +175 -0
- package/docs/run-bundles.md +3 -1
- package/docs/workflows.md +62 -19
- 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 +1 -1
- package/schemas/decision-presentation-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v2.schema.json +1 -1
- 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 +1 -1
- package/schemas/human-decision-delivery-v1.schema.json +1 -1
- package/schemas/human-decision-delivery-v2.schema.json +1 -1
- package/schemas/human-decision-receipt-v1.schema.json +1 -1
- package/schemas/human-decision-receipt-v2.schema.json +1 -1
- package/schemas/human-decision-request-v1.schema.json +1 -1
- package/schemas/human-decision-request-v2.schema.json +1 -1
- package/schemas/human-decision-resolution-v1.schema.json +1 -1
- package/schemas/human-decision-resolution-v2.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 +66 -12
- package/skills/autoplan/SKILL.md +26 -2
- package/skills/monitor/SKILL.md +35 -21
- package/skills/pi-workflows/SKILL.md +19 -5
- package/src/builtins/autoimplement-command-batches.ts +364 -0
- package/src/builtins/autoimplement.workflow.ts +1065 -261
- package/src/builtins/catalog.ts +3 -1
- package/src/builtins/index.ts +13 -0
- package/src/builtins/sanity-check-session.ts +205 -0
- package/src/builtins/sanity-check.workflow.ts +624 -0
- package/src/controllers/sqlite.ts +580 -7
- package/src/extension/decision-channels.ts +7 -7
- package/src/extension/deferred-turn-coordinator.ts +171 -0
- package/src/extension/deferred-turn.ts +166 -0
- package/src/extension/index.ts +359 -49
- package/src/extension/step-message.ts +1 -0
- package/src/herdr/setup.ts +4 -4
- package/src/workflows/command-batch.ts +254 -0
- package/src/workflows/engine.ts +8 -5
- package/src/workflows/index.ts +15 -0
- package/src/workflows/schema.ts +2 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +3 -1
- package/src/workflows/types.ts +4 -3
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import {
|
|
3
|
+
runCommandBatch,
|
|
4
|
+
type CommandBatchItem,
|
|
5
|
+
type CommandBatchResult,
|
|
6
|
+
} from "../workflows/command-batch.js";
|
|
7
|
+
import {
|
|
8
|
+
action,
|
|
3
9
|
agent,
|
|
4
10
|
compute,
|
|
5
11
|
defineWorkflow,
|
|
6
12
|
includeWorkflow,
|
|
7
13
|
includedResult,
|
|
8
|
-
shell,
|
|
9
14
|
} from "../workflows/definition.js";
|
|
10
15
|
import { digest } from "../workflows/human-decision.js";
|
|
11
|
-
import type {
|
|
12
|
-
ShellActionExecution,
|
|
13
|
-
ShellActionResult,
|
|
14
|
-
WorkflowNodeContext,
|
|
15
|
-
} from "../workflows/types.js";
|
|
16
|
+
import type { WorkflowActionContext, WorkflowNodeContext } from "../workflows/types.js";
|
|
16
17
|
import autodocWorkflow, { type AutodocInput } from "./autodoc.workflow.js";
|
|
18
|
+
import {
|
|
19
|
+
parseAutoimplementConcurrency,
|
|
20
|
+
parseCiInspectionBatch,
|
|
21
|
+
parsePublishedRepositories,
|
|
22
|
+
parseVerificationCommandPlan,
|
|
23
|
+
reviewerCommand,
|
|
24
|
+
type AutoimplementConcurrency,
|
|
25
|
+
type CiInspectionBatch,
|
|
26
|
+
type PublishedRepositories,
|
|
27
|
+
type PublishedRepository,
|
|
28
|
+
type VerificationCommandPlan,
|
|
29
|
+
} from "./autoimplement-command-batches.js";
|
|
17
30
|
import autoplanWorkflow, { type AutoplanInput } from "./autoplan.workflow.js";
|
|
18
31
|
import planApprovalWorkflow, { type PlanApprovalInput } from "./plan-approval.workflow.js";
|
|
19
32
|
|
|
@@ -35,6 +48,7 @@ export type AutoimplementInput = {
|
|
|
35
48
|
audience: string;
|
|
36
49
|
maxReplans: number;
|
|
37
50
|
};
|
|
51
|
+
concurrency?: Partial<AutoimplementConcurrency>;
|
|
38
52
|
};
|
|
39
53
|
|
|
40
54
|
export type ExistingPlanDiscovery = {
|
|
@@ -46,19 +60,26 @@ export type ExistingPlanDiscovery = {
|
|
|
46
60
|
evidence: unknown;
|
|
47
61
|
};
|
|
48
62
|
|
|
49
|
-
type StructuredCommand = {
|
|
50
|
-
command: string;
|
|
51
|
-
args: string[];
|
|
52
|
-
cwd: string;
|
|
53
|
-
timeoutMs: number;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
63
|
type ReviewFinding = {
|
|
57
64
|
severity: "P0" | "P1" | "P2" | "lower";
|
|
58
65
|
kind: "design" | "implementation";
|
|
59
66
|
summary: string;
|
|
60
67
|
};
|
|
61
68
|
|
|
69
|
+
type RepositoryReviewAssessment = {
|
|
70
|
+
id: string;
|
|
71
|
+
repository: string;
|
|
72
|
+
baseBranch: string;
|
|
73
|
+
headRevision: string;
|
|
74
|
+
dependencyFingerprint?: string;
|
|
75
|
+
invocationSucceeded: boolean;
|
|
76
|
+
p0: ReviewFinding[];
|
|
77
|
+
p1: ReviewFinding[];
|
|
78
|
+
p2: ReviewFinding[];
|
|
79
|
+
lower: ReviewFinding[];
|
|
80
|
+
reason: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
62
83
|
type ReviewAssessment = {
|
|
63
84
|
route: "critical" | "p2" | "clean" | "command_error";
|
|
64
85
|
invocationSucceeded: boolean;
|
|
@@ -67,6 +88,7 @@ type ReviewAssessment = {
|
|
|
67
88
|
p2: ReviewFinding[];
|
|
68
89
|
lower: ReviewFinding[];
|
|
69
90
|
reason: string;
|
|
91
|
+
repositories?: RepositoryReviewAssessment[];
|
|
70
92
|
};
|
|
71
93
|
|
|
72
94
|
export type AutoimplementCompleted = {
|
|
@@ -98,11 +120,49 @@ type BlockerChallenge = {
|
|
|
98
120
|
evidence: string[];
|
|
99
121
|
};
|
|
100
122
|
|
|
101
|
-
const FIVE_MINUTES_MS = 5 * 60_000;
|
|
102
|
-
const TEN_MINUTES_MS = 10 * 60_000;
|
|
103
123
|
const MAX_BLOCKER_CHALLENGES = 3;
|
|
104
124
|
const MAX_CHALLENGE_ITEMS = 5;
|
|
105
125
|
const MAX_CHALLENGE_TEXT = 500;
|
|
126
|
+
const MAX_TIMEOUT_FALLBACKS = 3;
|
|
127
|
+
const MAX_TIMEOUT_FALLBACK_EVIDENCE = 8;
|
|
128
|
+
const TIMEOUT_FALLBACK_SOURCES = [
|
|
129
|
+
"implement",
|
|
130
|
+
"planVerification",
|
|
131
|
+
"verify",
|
|
132
|
+
"fix",
|
|
133
|
+
"publish",
|
|
134
|
+
"addressP2",
|
|
135
|
+
"verifyP2",
|
|
136
|
+
"inspectComments",
|
|
137
|
+
"inspectCi",
|
|
138
|
+
"opportunisticTest",
|
|
139
|
+
"finalizeDelivery",
|
|
140
|
+
] as const;
|
|
141
|
+
|
|
142
|
+
const WORK_ATTEMPT_NODES = ["implement", "fix", "addressP2"] as const;
|
|
143
|
+
|
|
144
|
+
type TimeoutFallbackSource = (typeof TIMEOUT_FALLBACK_SOURCES)[number];
|
|
145
|
+
type TimeoutFallbackRoute = "retry" | "verify" | "review" | "ci" | "deliver" | "replan" | "blocked";
|
|
146
|
+
|
|
147
|
+
const TIMEOUT_FALLBACK_ROUTES: Record<TimeoutFallbackSource, readonly TimeoutFallbackRoute[]> = {
|
|
148
|
+
implement: ["retry", "replan", "blocked"],
|
|
149
|
+
planVerification: ["retry", "verify", "replan", "blocked"],
|
|
150
|
+
verify: ["retry", "verify", "replan", "blocked"],
|
|
151
|
+
fix: ["retry", "replan", "blocked"],
|
|
152
|
+
publish: ["retry", "replan", "blocked"],
|
|
153
|
+
addressP2: ["retry", "replan", "blocked"],
|
|
154
|
+
verifyP2: ["retry", "replan", "blocked"],
|
|
155
|
+
inspectComments: ["retry", "review", "ci", "replan", "blocked"],
|
|
156
|
+
inspectCi: ["retry", "ci", "deliver", "replan", "blocked"],
|
|
157
|
+
opportunisticTest: ["retry", "ci", "deliver", "replan", "blocked"],
|
|
158
|
+
finalizeDelivery: ["retry", "deliver", "replan", "blocked"],
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
type TimeoutFallbackResult = {
|
|
162
|
+
route: TimeoutFallbackRoute;
|
|
163
|
+
reason: string;
|
|
164
|
+
evidence: string[];
|
|
165
|
+
};
|
|
106
166
|
|
|
107
167
|
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
108
168
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -118,6 +178,13 @@ function requireString(value: unknown, label: string): string {
|
|
|
118
178
|
return value.trim();
|
|
119
179
|
}
|
|
120
180
|
|
|
181
|
+
function requireStringArray(value: unknown, label: string): string[] {
|
|
182
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
183
|
+
throw new Error(`${label} must be an array of strings`);
|
|
184
|
+
}
|
|
185
|
+
return [...value] as string[];
|
|
186
|
+
}
|
|
187
|
+
|
|
121
188
|
function boundedChallengeItems(value: unknown, label: string): string[] {
|
|
122
189
|
if (!Array.isArray(value) || value.length > MAX_CHALLENGE_ITEMS) {
|
|
123
190
|
throw new Error(`${label} must be an array with at most ${MAX_CHALLENGE_ITEMS} items`);
|
|
@@ -131,6 +198,170 @@ function boundedChallengeItems(value: unknown, label: string): string[] {
|
|
|
131
198
|
});
|
|
132
199
|
}
|
|
133
200
|
|
|
201
|
+
function isTimeoutFallbackSource(nodeId: string): nodeId is TimeoutFallbackSource {
|
|
202
|
+
return (TIMEOUT_FALLBACK_SOURCES as readonly string[]).includes(nodeId);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function latestTimedOutStep(context: WorkflowNodeContext) {
|
|
206
|
+
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
207
|
+
const step = context.state.steps[index];
|
|
208
|
+
if (step?.outcome === "timed_out" && isTimeoutFallbackSource(step.nodeId)) return step;
|
|
209
|
+
}
|
|
210
|
+
throw new Error("No supported timed-out Autoimplement step is available");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function latestStepIndex(
|
|
214
|
+
context: WorkflowNodeContext,
|
|
215
|
+
predicate: (step: WorkflowNodeContext["state"]["steps"][number]) => boolean,
|
|
216
|
+
): number {
|
|
217
|
+
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
218
|
+
const step = context.state.steps[index];
|
|
219
|
+
if (step && predicate(step)) return index;
|
|
220
|
+
}
|
|
221
|
+
return -1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function latestWorkAttemptIndex(context: WorkflowNodeContext): number {
|
|
225
|
+
return latestStepIndex(context, (step) =>
|
|
226
|
+
(WORK_ATTEMPT_NODES as readonly string[]).includes(step.nodeId),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function hasCurrentAcceptedWork(context: WorkflowNodeContext): boolean {
|
|
231
|
+
const index = latestWorkAttemptIndex(context);
|
|
232
|
+
return index >= 0 && context.state.steps[index]?.outcome === "ok";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function hasCurrentPublication(context: WorkflowNodeContext): boolean {
|
|
236
|
+
const workIndex = latestWorkAttemptIndex(context);
|
|
237
|
+
const publicationIndex = latestStepIndex(context, (step) => {
|
|
238
|
+
if (step.outcome !== "ok") return false;
|
|
239
|
+
if (step.nodeId === "publish") return true;
|
|
240
|
+
if (step.nodeId !== "verifyP2") return false;
|
|
241
|
+
const output = step.output as { passed?: unknown } | null;
|
|
242
|
+
return output?.passed === true;
|
|
243
|
+
});
|
|
244
|
+
return publicationIndex > workIndex;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function parseTimeoutFallback(value: unknown, context: WorkflowNodeContext): TimeoutFallbackResult {
|
|
248
|
+
const result = requireRecord(value, "timeout fallback");
|
|
249
|
+
const routes: TimeoutFallbackRoute[] = [
|
|
250
|
+
"retry",
|
|
251
|
+
"verify",
|
|
252
|
+
"review",
|
|
253
|
+
"ci",
|
|
254
|
+
"deliver",
|
|
255
|
+
"replan",
|
|
256
|
+
"blocked",
|
|
257
|
+
];
|
|
258
|
+
if (!routes.includes(result.route as TimeoutFallbackRoute)) {
|
|
259
|
+
throw new Error(`timeout fallback route must be one of ${routes.join(", ")}`);
|
|
260
|
+
}
|
|
261
|
+
const route = result.route as TimeoutFallbackRoute;
|
|
262
|
+
const reason = requireString(result.reason, "timeout fallback reason");
|
|
263
|
+
const evidence = requireStringArray(result.evidence, "timeout fallback evidence").map(
|
|
264
|
+
(item, index) => requireString(item, `timeout fallback evidence[${index}]`),
|
|
265
|
+
);
|
|
266
|
+
if (evidence.length === 0 || evidence.length > MAX_TIMEOUT_FALLBACK_EVIDENCE) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`timeout fallback evidence must contain 1 through ${MAX_TIMEOUT_FALLBACK_EVIDENCE} items`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const timedOut = latestTimedOutStep(context);
|
|
273
|
+
const source = timedOut.nodeId as TimeoutFallbackSource;
|
|
274
|
+
if (!TIMEOUT_FALLBACK_ROUTES[source].includes(route)) {
|
|
275
|
+
throw new Error(`timeout fallback route ${route} is not safe after timed-out ${source}`);
|
|
276
|
+
}
|
|
277
|
+
if (route === "verify" && !hasCurrentAcceptedWork(context)) {
|
|
278
|
+
throw new Error("timeout fallback cannot route to verification without current accepted work");
|
|
279
|
+
}
|
|
280
|
+
if (["review", "ci", "deliver"].includes(route) && !hasCurrentPublication(context)) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
"timeout fallback cannot move past publication without a current published head",
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (route === "ci") {
|
|
286
|
+
const comments = context.outputs.inspectComments as { route?: unknown } | undefined;
|
|
287
|
+
if (comments?.route !== "ci") {
|
|
288
|
+
throw new Error("timeout fallback cannot route to CI before comment inspection completed");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (route === "deliver") {
|
|
292
|
+
const ci = (context.outputs.assessTrackedCi ?? context.outputs.inspectCi) as
|
|
293
|
+
| { route?: unknown }
|
|
294
|
+
| undefined;
|
|
295
|
+
const classification = context.outputs.classifyCi as { route?: unknown } | undefined;
|
|
296
|
+
if (ci?.route !== "green" && classification?.route !== "unrelated") {
|
|
297
|
+
throw new Error("timeout fallback cannot route to delivery before CI is ready");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return { route, reason, evidence };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function timeoutFallbackTarget(context: WorkflowNodeContext): { route: string } {
|
|
305
|
+
const fallback = context.outputs.timeoutFallback as TimeoutFallbackResult;
|
|
306
|
+
if (fallback.route !== "retry") {
|
|
307
|
+
const routes: Record<Exclude<TimeoutFallbackRoute, "retry">, string> = {
|
|
308
|
+
verify: "planVerification",
|
|
309
|
+
review: "selectReviewCommands",
|
|
310
|
+
ci: "inspectCi",
|
|
311
|
+
deliver: "finalizeDelivery",
|
|
312
|
+
replan: "redesign",
|
|
313
|
+
blocked: "blocked",
|
|
314
|
+
};
|
|
315
|
+
return { route: routes[fallback.route] };
|
|
316
|
+
}
|
|
317
|
+
return { route: latestTimedOutStep(context).nodeId };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function timeoutFallbackGuard(context: WorkflowNodeContext) {
|
|
321
|
+
const timeout = latestTimedOutStep(context);
|
|
322
|
+
const attempts = context.state.steps.filter(
|
|
323
|
+
(step) => step.nodeId === "timeoutFallback" && step.outcome === "ok",
|
|
324
|
+
).length;
|
|
325
|
+
if (attempts >= MAX_TIMEOUT_FALLBACKS) {
|
|
326
|
+
const timeouts = context.state.steps
|
|
327
|
+
.filter((step) => step.outcome === "timed_out" && isTimeoutFallbackSource(step.nodeId))
|
|
328
|
+
.map((step) => ({
|
|
329
|
+
nodeId: step.nodeId,
|
|
330
|
+
attemptId: step.attemptId,
|
|
331
|
+
error: step.error,
|
|
332
|
+
}));
|
|
333
|
+
return {
|
|
334
|
+
route: "blocked",
|
|
335
|
+
reason: `Autoimplement reached the ${MAX_TIMEOUT_FALLBACKS}-fallback timeout safety limit.`,
|
|
336
|
+
evidence: {
|
|
337
|
+
attempts,
|
|
338
|
+
limit: MAX_TIMEOUT_FALLBACKS,
|
|
339
|
+
timeouts,
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
route: "recover",
|
|
345
|
+
attempt: attempts + 1,
|
|
346
|
+
limit: MAX_TIMEOUT_FALLBACKS,
|
|
347
|
+
timeout: {
|
|
348
|
+
nodeId: timeout.nodeId,
|
|
349
|
+
attemptId: timeout.attemptId,
|
|
350
|
+
error: timeout.error,
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function throwLatestSupportedFailure(context: WorkflowNodeContext): never {
|
|
356
|
+
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
357
|
+
const step = context.state.steps[index];
|
|
358
|
+
if (step?.outcome === "failed" && isTimeoutFallbackSource(step.nodeId)) {
|
|
359
|
+
throw new Error(step.error ?? `Autoimplement node failed: ${step.nodeId}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
throw new Error("No supported failed Autoimplement step is available");
|
|
363
|
+
}
|
|
364
|
+
|
|
134
365
|
function parseBlockerChallenge(value: unknown): BlockerChallenge {
|
|
135
366
|
const result = requireRecord(value, "blocker challenge");
|
|
136
367
|
if (result.route !== "continue" && result.route !== "blocked") {
|
|
@@ -233,6 +464,7 @@ function parseInput(value: unknown): AutoimplementInput {
|
|
|
233
464
|
documents: [...raw.documents] as string[],
|
|
234
465
|
};
|
|
235
466
|
}
|
|
467
|
+
const concurrency = parseAutoimplementConcurrency(input.concurrency);
|
|
236
468
|
let approval: AutoimplementInput["approval"];
|
|
237
469
|
if (input.approval !== undefined) {
|
|
238
470
|
const raw = requireRecord(input.approval, "autoimplement approval");
|
|
@@ -264,6 +496,7 @@ function parseInput(value: unknown): AutoimplementInput {
|
|
|
264
496
|
...(documents !== undefined ? { documents: [...documents] as string[] } : {}),
|
|
265
497
|
...(documentation !== undefined ? { documentation } : {}),
|
|
266
498
|
...(approval !== undefined ? { approval } : {}),
|
|
499
|
+
concurrency,
|
|
267
500
|
};
|
|
268
501
|
}
|
|
269
502
|
|
|
@@ -312,70 +545,71 @@ function parseRoute<T extends string>(
|
|
|
312
545
|
return { ...record, route: record.route as T };
|
|
313
546
|
}
|
|
314
547
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (!Array.isArray(command.args) || command.args.some((arg) => typeof arg !== "string")) {
|
|
329
|
-
throw new Error(`${options.label} args must be an array of strings`);
|
|
330
|
-
}
|
|
331
|
-
const args = [...command.args] as string[];
|
|
332
|
-
if (!options.validateArgs(args)) throw new Error(`${options.label} args are not allowed`);
|
|
333
|
-
const cwd = requireString(command.cwd, `${options.label} cwd`);
|
|
334
|
-
if (!path.isAbsolute(cwd)) throw new Error(`${options.label} cwd must be absolute`);
|
|
335
|
-
const timeoutMs = command.timeoutMs;
|
|
336
|
-
if (
|
|
337
|
-
typeof timeoutMs !== "number" ||
|
|
338
|
-
!Number.isInteger(timeoutMs) ||
|
|
339
|
-
timeoutMs <= 0 ||
|
|
340
|
-
timeoutMs > options.maxTimeoutMs
|
|
341
|
-
) {
|
|
342
|
-
throw new Error(`${options.label} timeoutMs must be at most ${options.maxTimeoutMs}`);
|
|
343
|
-
}
|
|
344
|
-
return { command: options.command, args, cwd, timeoutMs };
|
|
548
|
+
type ReviewCommandSelection = {
|
|
549
|
+
route: "run" | "reuse";
|
|
550
|
+
repositories: PublishedRepository[];
|
|
551
|
+
commands: CommandBatchItem[];
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
type BatchExecution = {
|
|
555
|
+
route: "assess" | "repair";
|
|
556
|
+
batch: CommandBatchResult;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
function concurrency(context: WorkflowNodeContext): AutoimplementConcurrency {
|
|
560
|
+
return parseAutoimplementConcurrency((context.input as AutoimplementInput).concurrency);
|
|
345
561
|
}
|
|
346
562
|
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
563
|
+
async function runAutoimplementBatch(
|
|
564
|
+
context: WorkflowActionContext,
|
|
565
|
+
kind: "review" | "ciWatch" | "verification",
|
|
566
|
+
commands: CommandBatchItem[],
|
|
567
|
+
maxConcurrency: number,
|
|
568
|
+
): Promise<CommandBatchResult> {
|
|
569
|
+
return await runCommandBatch(
|
|
570
|
+
{ items: commands, maxConcurrency: Math.min(maxConcurrency, Math.max(1, commands.length)) },
|
|
571
|
+
{
|
|
572
|
+
signal: context.signal,
|
|
573
|
+
onItemSettled: async (result, completed, total) => {
|
|
574
|
+
if (context.signal.aborted) return;
|
|
575
|
+
try {
|
|
576
|
+
await context.publishUpdate({
|
|
577
|
+
type: "command-batch.item",
|
|
578
|
+
key: `${kind}/${result.id}`,
|
|
579
|
+
data: {
|
|
580
|
+
schema: "pi-workflows.command-batch-item.v1",
|
|
581
|
+
batchKind: kind,
|
|
582
|
+
itemId: result.id,
|
|
583
|
+
outcome: result.outcome,
|
|
584
|
+
completed,
|
|
585
|
+
total,
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
} catch (error) {
|
|
589
|
+
if (!context.signal.aborted) throw error;
|
|
590
|
+
}
|
|
591
|
+
},
|
|
355
592
|
},
|
|
356
|
-
|
|
593
|
+
);
|
|
357
594
|
}
|
|
358
595
|
|
|
359
|
-
function
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
(args[0] === "pr" && args[1] === "checks" && args.includes("--watch")) ||
|
|
366
|
-
(args[0] === "run" && args[1] === "watch"),
|
|
367
|
-
});
|
|
596
|
+
function commandBatchTimeoutMs(commands: CommandBatchItem[], maxConcurrency: number): number {
|
|
597
|
+
if (commands.length === 0) return 10_000;
|
|
598
|
+
const concurrency = Math.min(maxConcurrency, commands.length);
|
|
599
|
+
const waves = Math.ceil(commands.length / concurrency);
|
|
600
|
+
const longestItem = Math.max(...commands.map((command) => command.timeoutMs));
|
|
601
|
+
return waves * longestItem + 10_000;
|
|
368
602
|
}
|
|
369
603
|
|
|
370
|
-
function
|
|
371
|
-
return
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
604
|
+
function reviewBatchNeedsRepair(result: CommandBatchResult): boolean {
|
|
605
|
+
return result.items.some(
|
|
606
|
+
(item) =>
|
|
607
|
+
item.outcome === "timedOut" ||
|
|
608
|
+
item.outcome === "cancelled" ||
|
|
609
|
+
(item.outcome === "failed" && item.exitCode === null) ||
|
|
610
|
+
item.stdoutTruncated ||
|
|
611
|
+
item.stderrTruncated,
|
|
612
|
+
);
|
|
379
613
|
}
|
|
380
614
|
|
|
381
615
|
function latestOutput<T>(context: WorkflowNodeContext, nodeIds: string[]): T {
|
|
@@ -383,20 +617,10 @@ function latestOutput<T>(context: WorkflowNodeContext, nodeIds: string[]): T {
|
|
|
383
617
|
const step = context.state.steps[index];
|
|
384
618
|
if (step && nodeIds.includes(step.nodeId)) return step.output as T;
|
|
385
619
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
function latestCiCommand(context: WorkflowNodeContext): StructuredCommand {
|
|
390
|
-
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
391
|
-
const step = context.state.steps[index];
|
|
392
|
-
if (!step) continue;
|
|
393
|
-
if (step.nodeId === "repairCiCommand") return step.output as StructuredCommand;
|
|
394
|
-
if (step.nodeId === "inspectCi") {
|
|
395
|
-
const output = step.output as { trackingCommand?: StructuredCommand };
|
|
396
|
-
if (output.trackingCommand !== undefined) return output.trackingCommand;
|
|
397
|
-
}
|
|
620
|
+
for (const nodeId of nodeIds) {
|
|
621
|
+
if (context.outputs[nodeId] !== undefined) return context.outputs[nodeId] as T;
|
|
398
622
|
}
|
|
399
|
-
throw new Error(
|
|
623
|
+
throw new Error(`No output found for ${nodeIds.join(" or ")}`);
|
|
400
624
|
}
|
|
401
625
|
|
|
402
626
|
function currentPlan(context: WorkflowNodeContext): unknown {
|
|
@@ -444,6 +668,7 @@ function latestBlockerClaim(context: WorkflowNodeContext): unknown {
|
|
|
444
668
|
"assessTrackedCi",
|
|
445
669
|
"classifyCi",
|
|
446
670
|
"finalizeDelivery",
|
|
671
|
+
"timeoutFallback",
|
|
447
672
|
];
|
|
448
673
|
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
449
674
|
const step = context.state.steps[index];
|
|
@@ -480,6 +705,7 @@ function latestIssue(context: WorkflowNodeContext): unknown {
|
|
|
480
705
|
"triageReview",
|
|
481
706
|
"inspectComments",
|
|
482
707
|
"classifyCi",
|
|
708
|
+
"timeoutFallback",
|
|
483
709
|
"adoptPlan",
|
|
484
710
|
];
|
|
485
711
|
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
@@ -501,18 +727,356 @@ function parseFinding(value: unknown, severity: ReviewFinding["severity"]): Revi
|
|
|
501
727
|
};
|
|
502
728
|
}
|
|
503
729
|
|
|
504
|
-
function
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
730
|
+
function parseVerificationForContext(
|
|
731
|
+
value: unknown,
|
|
732
|
+
context: WorkflowNodeContext,
|
|
733
|
+
): VerificationCommandPlan {
|
|
734
|
+
const plan = parseVerificationCommandPlan(value);
|
|
735
|
+
const implementation = latestOutput<Record<string, unknown>>(context, ["implement"]);
|
|
736
|
+
const reported = Array.isArray(implementation.repositories)
|
|
737
|
+
? implementation.repositories.filter((entry): entry is string => typeof entry === "string")
|
|
738
|
+
: [];
|
|
739
|
+
const request = context.input as AutoimplementInput;
|
|
740
|
+
const roots = new Set(
|
|
741
|
+
(reported.length > 0 ? reported : [request.repository ?? process.cwd()]).map((entry) =>
|
|
742
|
+
path.resolve(entry),
|
|
743
|
+
),
|
|
744
|
+
);
|
|
745
|
+
for (const command of plan.commands) {
|
|
746
|
+
const cwd = path.resolve(command.cwd);
|
|
747
|
+
if (!roots.delete(cwd)) {
|
|
748
|
+
throw new Error(
|
|
749
|
+
`verification command cwd was not reported by implementation: ${command.cwd}`,
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (roots.size > 0) {
|
|
754
|
+
throw new Error(`verification plan is missing reported repositories: ${[...roots].join(", ")}`);
|
|
755
|
+
}
|
|
756
|
+
return plan;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function currentPublishedRepositories(context: WorkflowNodeContext): PublishedRepositories {
|
|
760
|
+
return latestOutput<PublishedRepositories>(context, ["verifyP2", "publish"]);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
type DeliveryRepositoryResult = {
|
|
764
|
+
repository: string;
|
|
765
|
+
pr: string;
|
|
766
|
+
merged: boolean;
|
|
767
|
+
reportComment: string;
|
|
768
|
+
reason: string;
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
function parseDeliveryRepository(value: unknown, index: number): DeliveryRepositoryResult {
|
|
772
|
+
const result = requireRecord(value, `delivery repositories[${index}]`);
|
|
773
|
+
const repository = requireString(result.repository, `delivery repositories[${index}].repository`);
|
|
774
|
+
if (!path.isAbsolute(repository)) {
|
|
775
|
+
throw new Error(`delivery repositories[${index}].repository must be absolute`);
|
|
776
|
+
}
|
|
777
|
+
if (typeof result.merged !== "boolean") {
|
|
778
|
+
throw new Error(`delivery repositories[${index}].merged must be a boolean`);
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
repository: path.resolve(repository),
|
|
782
|
+
pr: requireString(result.pr, `delivery repositories[${index}].pr`),
|
|
783
|
+
merged: result.merged,
|
|
784
|
+
reportComment: requireString(
|
|
785
|
+
result.reportComment,
|
|
786
|
+
`delivery repositories[${index}].reportComment`,
|
|
787
|
+
),
|
|
788
|
+
reason: requireString(result.reason, `delivery repositories[${index}].reason`),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function parseDeliveryResult(
|
|
793
|
+
value: unknown,
|
|
794
|
+
context: WorkflowNodeContext,
|
|
795
|
+
): Record<string, unknown> {
|
|
796
|
+
const result = requireRecord(value, "delivery result");
|
|
797
|
+
if (result.status !== "completed" && result.status !== "blocked") {
|
|
798
|
+
throw new Error("delivery status must be completed or blocked");
|
|
799
|
+
}
|
|
800
|
+
const request = context.input as AutoimplementInput;
|
|
801
|
+
if (request.merge !== true && result.merged === true) {
|
|
802
|
+
throw new Error("delivery cannot merge without explicit merge: true");
|
|
803
|
+
}
|
|
804
|
+
if (result.status === "blocked") return result;
|
|
805
|
+
if (typeof result.merged !== "boolean") {
|
|
806
|
+
throw new Error("completed delivery merged must be a boolean");
|
|
807
|
+
}
|
|
808
|
+
const pr = requireString(result.pr, "completed delivery pr");
|
|
809
|
+
const reportComment = requireString(result.reportComment, "completed delivery reportComment");
|
|
810
|
+
const reason = requireString(result.reason, "completed delivery reason");
|
|
811
|
+
const published = currentPublishedRepositories(context).repositories;
|
|
812
|
+
let repositories: DeliveryRepositoryResult[];
|
|
813
|
+
if (result.repositories === undefined) {
|
|
814
|
+
if (published.length !== 1) {
|
|
815
|
+
throw new Error("completed delivery repositories must cover every published repository");
|
|
816
|
+
}
|
|
817
|
+
const only = published[0];
|
|
818
|
+
if (only === undefined) throw new Error("completed delivery has no published repository");
|
|
819
|
+
repositories = [
|
|
820
|
+
{
|
|
821
|
+
repository: path.resolve(only.repository),
|
|
822
|
+
pr,
|
|
823
|
+
merged: result.merged,
|
|
824
|
+
reportComment,
|
|
825
|
+
reason,
|
|
826
|
+
},
|
|
827
|
+
];
|
|
828
|
+
} else {
|
|
829
|
+
if (!Array.isArray(result.repositories)) {
|
|
830
|
+
throw new Error("completed delivery repositories must be an array");
|
|
831
|
+
}
|
|
832
|
+
repositories = result.repositories.map(parseDeliveryRepository);
|
|
833
|
+
}
|
|
834
|
+
const actual = new Map<string, DeliveryRepositoryResult>();
|
|
835
|
+
for (const repository of repositories) {
|
|
836
|
+
if (actual.has(repository.repository)) {
|
|
837
|
+
throw new Error(`completed delivery repository is duplicated: ${repository.repository}`);
|
|
838
|
+
}
|
|
839
|
+
actual.set(repository.repository, repository);
|
|
840
|
+
}
|
|
841
|
+
const mergeExpected = request.merge === true;
|
|
842
|
+
for (const expected of published) {
|
|
843
|
+
const repository = actual.get(path.resolve(expected.repository));
|
|
844
|
+
if (repository === undefined || repository.pr !== expected.pr) {
|
|
845
|
+
throw new Error(
|
|
846
|
+
`completed delivery does not match published repository and PR: ${expected.repository}`,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
if (repository.merged !== mergeExpected) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
`completed delivery merge result does not match merge policy: ${expected.repository}`,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
actual.delete(repository.repository);
|
|
855
|
+
}
|
|
856
|
+
if (actual.size > 0) {
|
|
857
|
+
throw new Error(
|
|
858
|
+
`completed delivery contains unpublished repositories: ${[...actual.keys()].join(", ")}`,
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
const firstPublished = published[0];
|
|
862
|
+
const first =
|
|
863
|
+
firstPublished === undefined
|
|
864
|
+
? undefined
|
|
865
|
+
: repositories.find(
|
|
866
|
+
(repository) => repository.repository === path.resolve(firstPublished.repository),
|
|
867
|
+
);
|
|
868
|
+
if (
|
|
869
|
+
first === undefined ||
|
|
870
|
+
first.pr !== pr ||
|
|
871
|
+
first.merged !== result.merged ||
|
|
872
|
+
first.reportComment !== reportComment
|
|
873
|
+
) {
|
|
874
|
+
throw new Error(
|
|
875
|
+
"completed delivery top-level compatibility fields must match the first result",
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
return { status: "completed", merged: result.merged, pr, reportComment, reason, repositories };
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function parseP2Verification(
|
|
882
|
+
value: unknown,
|
|
883
|
+
context: WorkflowNodeContext,
|
|
884
|
+
): Record<string, unknown> {
|
|
885
|
+
const result = requireRecord(value, "P2 verification");
|
|
886
|
+
if (typeof result.passed !== "boolean") {
|
|
887
|
+
throw new Error("P2 verification passed must be a boolean");
|
|
888
|
+
}
|
|
889
|
+
if (result.pushed !== true) {
|
|
890
|
+
throw new Error("P2 verification pushed must be true");
|
|
891
|
+
}
|
|
892
|
+
const refreshed = parsePublishedRepositories(result);
|
|
893
|
+
const previous = latestOutput<PublishedRepositories>(context, ["publish"]);
|
|
894
|
+
const expected = new Map(
|
|
895
|
+
previous.repositories.map((repository) => [repository.id, repository] as const),
|
|
896
|
+
);
|
|
897
|
+
for (const repository of refreshed.repositories) {
|
|
898
|
+
const prior = expected.get(repository.id);
|
|
899
|
+
if (
|
|
900
|
+
prior === undefined ||
|
|
901
|
+
prior.repository !== repository.repository ||
|
|
902
|
+
prior.branch !== repository.branch ||
|
|
903
|
+
prior.baseBranch !== repository.baseBranch ||
|
|
904
|
+
prior.pr !== repository.pr ||
|
|
905
|
+
prior.dependencyFingerprint !== repository.dependencyFingerprint
|
|
906
|
+
) {
|
|
907
|
+
throw new Error(`P2 verification repository does not match publication: ${repository.id}`);
|
|
908
|
+
}
|
|
909
|
+
expected.delete(repository.id);
|
|
910
|
+
}
|
|
911
|
+
if (expected.size > 0) {
|
|
912
|
+
throw new Error(
|
|
913
|
+
`P2 verification is missing repository ids: ${[...expected.keys()].join(", ")}`,
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
return { ...result, repositories: refreshed.repositories };
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function parseCiInspectionForPublished(
|
|
920
|
+
value: unknown,
|
|
921
|
+
context: WorkflowNodeContext,
|
|
922
|
+
): CiInspectionBatch {
|
|
923
|
+
const inspected = parseCiInspectionBatch(value);
|
|
924
|
+
const published = currentPublishedRepositories(context);
|
|
925
|
+
const expected = new Map(
|
|
926
|
+
published.repositories.map((repository) => [repository.id, repository] as const),
|
|
927
|
+
);
|
|
928
|
+
for (const target of inspected.targets) {
|
|
929
|
+
const repository = expected.get(target.id);
|
|
930
|
+
if (
|
|
931
|
+
repository === undefined ||
|
|
932
|
+
repository.repository !== target.repository ||
|
|
933
|
+
repository.headRevision !== target.headRevision ||
|
|
934
|
+
repository.pr !== target.pr
|
|
935
|
+
) {
|
|
936
|
+
throw new Error(
|
|
937
|
+
`CI target does not match the published repository and head: ${target.id} (${JSON.stringify({ target, repository })})`,
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
expected.delete(target.id);
|
|
941
|
+
}
|
|
942
|
+
if (expected.size > 0) {
|
|
943
|
+
throw new Error(`CI inspection is missing repository ids: ${[...expected.keys()].join(", ")}`);
|
|
944
|
+
}
|
|
945
|
+
return inspected;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function parseTrackedCiAssessment(
|
|
949
|
+
value: unknown,
|
|
950
|
+
context: WorkflowNodeContext,
|
|
951
|
+
): Record<string, unknown> & { route: CiInspectionBatch["route"] } {
|
|
952
|
+
const result = requireRecord(value, "tracked CI assessment");
|
|
953
|
+
const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
|
|
954
|
+
const execution = latestOutput<BatchExecution>(context, ["trackCi"]);
|
|
955
|
+
const expectedIds = execution.batch.items.map((item) => item.id);
|
|
956
|
+
if (!Array.isArray(result.targets)) {
|
|
957
|
+
throw new Error("tracked CI assessment targets must be an array");
|
|
958
|
+
}
|
|
959
|
+
const seen = new Set<string>();
|
|
960
|
+
const targets = result.targets.map((entry, index) => {
|
|
961
|
+
const target = requireRecord(entry, `tracked CI assessment targets[${index}]`);
|
|
962
|
+
const id = requireString(target.id, `tracked CI assessment targets[${index}].id`);
|
|
963
|
+
if (seen.has(id)) throw new Error(`tracked CI assessment target is duplicated: ${id}`);
|
|
964
|
+
seen.add(id);
|
|
965
|
+
if (
|
|
966
|
+
target.route !== "green" &&
|
|
967
|
+
target.route !== "failed" &&
|
|
968
|
+
target.route !== "pending" &&
|
|
969
|
+
target.route !== "unavailable"
|
|
970
|
+
) {
|
|
971
|
+
throw new Error(`tracked CI assessment targets[${index}].route is invalid`);
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
id,
|
|
975
|
+
route: target.route,
|
|
976
|
+
reason: requireString(target.reason, `tracked CI assessment targets[${index}].reason`),
|
|
977
|
+
};
|
|
978
|
+
});
|
|
979
|
+
const missing = expectedIds.filter((id) => !seen.has(id));
|
|
980
|
+
const unexpected = [...seen].filter((id) => !expectedIds.includes(id));
|
|
981
|
+
if (missing.length > 0 || unexpected.length > 0 || targets.length !== expectedIds.length) {
|
|
982
|
+
throw new Error(
|
|
983
|
+
`tracked CI assessment targets must exactly cover watched ids; missing: ${missing.join(", ") || "none"}; unexpected: ${unexpected.join(", ") || "none"}`,
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
const trackedRoutes = new Map(targets.map((target) => [target.id, target.route] as const));
|
|
987
|
+
const routes = inspected.targets.map((target) => trackedRoutes.get(target.id) ?? target.route);
|
|
988
|
+
const route = routes.includes("failed")
|
|
989
|
+
? "failed"
|
|
990
|
+
: routes.includes("pending")
|
|
991
|
+
? "pending"
|
|
992
|
+
: routes.includes("unavailable")
|
|
993
|
+
? "unavailable"
|
|
994
|
+
: "green";
|
|
995
|
+
if (result.route !== route) {
|
|
996
|
+
throw new Error(`tracked CI assessment route must be ${route}`);
|
|
997
|
+
}
|
|
998
|
+
return {
|
|
999
|
+
...result,
|
|
1000
|
+
route,
|
|
1001
|
+
reason: requireString(result.reason, "tracked CI assessment reason"),
|
|
1002
|
+
targets,
|
|
1003
|
+
relatedFailures: requireStringArray(
|
|
1004
|
+
result.relatedFailures ?? [],
|
|
1005
|
+
"tracked CI assessment relatedFailures",
|
|
1006
|
+
),
|
|
1007
|
+
unrelatedFailures: requireStringArray(
|
|
1008
|
+
result.unrelatedFailures ?? [],
|
|
1009
|
+
"tracked CI assessment unrelatedFailures",
|
|
1010
|
+
),
|
|
510
1011
|
};
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const
|
|
515
|
-
const
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function selectReviewCommands(context: WorkflowNodeContext): ReviewCommandSelection {
|
|
1015
|
+
const published = latestOutput<PublishedRepositories>(context, ["publish"]);
|
|
1016
|
+
const reviewed = reviewRounds(context).flatMap((round) => round.repositories ?? []);
|
|
1017
|
+
const repositories = published.repositories.filter(
|
|
1018
|
+
(repository) =>
|
|
1019
|
+
!reviewed.some(
|
|
1020
|
+
(entry) =>
|
|
1021
|
+
entry.id === repository.id &&
|
|
1022
|
+
entry.headRevision === repository.headRevision &&
|
|
1023
|
+
entry.dependencyFingerprint === repository.dependencyFingerprint &&
|
|
1024
|
+
entry.invocationSucceeded,
|
|
1025
|
+
),
|
|
1026
|
+
);
|
|
1027
|
+
return {
|
|
1028
|
+
route: repositories.length === 0 ? "reuse" : "run",
|
|
1029
|
+
repositories,
|
|
1030
|
+
commands: repositories.map(reviewerCommand),
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function parseReviewAssessment(value: unknown, context: WorkflowNodeContext): ReviewAssessment {
|
|
1035
|
+
const review = requireRecord(value, "review assessment");
|
|
1036
|
+
if (!Array.isArray(review.repositories)) {
|
|
1037
|
+
throw new Error("review repositories must be an array");
|
|
1038
|
+
}
|
|
1039
|
+
const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
|
|
1040
|
+
const expected = new Map(selected.repositories.map((repository) => [repository.id, repository]));
|
|
1041
|
+
const repositories = review.repositories.map((value, index) => {
|
|
1042
|
+
const raw = requireRecord(value, `review repositories[${index}]`);
|
|
1043
|
+
const id = requireString(raw.id, `review repositories[${index}].id`);
|
|
1044
|
+
const published = expected.get(id);
|
|
1045
|
+
if (published === undefined)
|
|
1046
|
+
throw new Error(`review repository id was not in the batch: ${id}`);
|
|
1047
|
+
expected.delete(id);
|
|
1048
|
+
const parseList = (key: "p0" | "p1" | "p2" | "lower", severity: ReviewFinding["severity"]) => {
|
|
1049
|
+
const list = raw[key];
|
|
1050
|
+
if (!Array.isArray(list))
|
|
1051
|
+
throw new Error(`review repositories[${index}].${key} must be an array`);
|
|
1052
|
+
return list.map((item) => parseFinding(item, severity));
|
|
1053
|
+
};
|
|
1054
|
+
return {
|
|
1055
|
+
id,
|
|
1056
|
+
repository: published.repository,
|
|
1057
|
+
baseBranch: published.baseBranch,
|
|
1058
|
+
headRevision: published.headRevision,
|
|
1059
|
+
...(published.dependencyFingerprint !== undefined
|
|
1060
|
+
? { dependencyFingerprint: published.dependencyFingerprint }
|
|
1061
|
+
: {}),
|
|
1062
|
+
invocationSucceeded: raw.invocationSucceeded === true,
|
|
1063
|
+
p0: parseList("p0", "P0"),
|
|
1064
|
+
p1: parseList("p1", "P1"),
|
|
1065
|
+
p2: parseList("p2", "P2"),
|
|
1066
|
+
lower: parseList("lower", "lower"),
|
|
1067
|
+
reason: requireString(raw.reason, `review repositories[${index}].reason`),
|
|
1068
|
+
} satisfies RepositoryReviewAssessment;
|
|
1069
|
+
});
|
|
1070
|
+
if (expected.size > 0) {
|
|
1071
|
+
throw new Error(
|
|
1072
|
+
`review assessment is missing repository ids: ${[...expected.keys()].join(", ")}`,
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
const p0 = repositories.flatMap((entry) => entry.p0);
|
|
1076
|
+
const p1 = repositories.flatMap((entry) => entry.p1);
|
|
1077
|
+
const p2 = repositories.flatMap((entry) => entry.p2);
|
|
1078
|
+
const lower = repositories.flatMap((entry) => entry.lower);
|
|
1079
|
+
const invocationSucceeded = repositories.every((entry) => entry.invocationSucceeded);
|
|
516
1080
|
const route = !invocationSucceeded
|
|
517
1081
|
? "command_error"
|
|
518
1082
|
: p0.length + p1.length > 0
|
|
@@ -528,6 +1092,7 @@ function parseReviewAssessment(value: unknown): ReviewAssessment {
|
|
|
528
1092
|
p2,
|
|
529
1093
|
lower,
|
|
530
1094
|
reason: requireString(review.reason, "review reason"),
|
|
1095
|
+
repositories,
|
|
531
1096
|
};
|
|
532
1097
|
}
|
|
533
1098
|
|
|
@@ -537,8 +1102,39 @@ function reviewRounds(context: WorkflowNodeContext): ReviewAssessment[] {
|
|
|
537
1102
|
.map((step) => step.output as ReviewAssessment);
|
|
538
1103
|
}
|
|
539
1104
|
|
|
1105
|
+
function reviewRoundsForOutput(context: WorkflowNodeContext): ReviewAssessment[] {
|
|
1106
|
+
const rounds = reviewRounds(context);
|
|
1107
|
+
const repositoryIds = new Set(
|
|
1108
|
+
rounds.flatMap((round) => (round.repositories ?? []).map((repository) => repository.id)),
|
|
1109
|
+
);
|
|
1110
|
+
if (repositoryIds.size > 1) return rounds;
|
|
1111
|
+
return rounds.map(({ repositories: _repositories, ...round }) => round);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function ciForOutput(context: WorkflowNodeContext): unknown {
|
|
1115
|
+
const result = latestOutput<Record<string, unknown>>(context, ["assessTrackedCi", "inspectCi"]);
|
|
1116
|
+
const targets = result.targets;
|
|
1117
|
+
if (!Array.isArray(targets) || targets.length !== 1) return result;
|
|
1118
|
+
const { targets: _targets, ...aggregate } = result;
|
|
1119
|
+
if (result.route === "green" || result.route === "failed" || result.route === "unavailable") {
|
|
1120
|
+
const target = targets[0];
|
|
1121
|
+
if (target !== null && typeof target === "object" && !Array.isArray(target)) {
|
|
1122
|
+
const record = target as Record<string, unknown>;
|
|
1123
|
+
return {
|
|
1124
|
+
...aggregate,
|
|
1125
|
+
reason: aggregate.reason ?? record.reason,
|
|
1126
|
+
relatedFailures: aggregate.relatedFailures ?? record.relatedFailures ?? [],
|
|
1127
|
+
unrelatedFailures: aggregate.unrelatedFailures ?? record.unrelatedFailures ?? [],
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return aggregate;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
540
1134
|
function latestBlockedReason(context: WorkflowNodeContext): { reason: string; evidence: unknown } {
|
|
541
1135
|
const candidates = [
|
|
1136
|
+
"timeoutFallbackGuard",
|
|
1137
|
+
"timeoutFallback",
|
|
542
1138
|
"challengeBlockerGuard",
|
|
543
1139
|
"challengeBlocker",
|
|
544
1140
|
"finalizeDelivery",
|
|
@@ -723,8 +1319,58 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
723
1319
|
: { route: "redesign", replans, limit };
|
|
724
1320
|
},
|
|
725
1321
|
}),
|
|
1322
|
+
timeoutFallbackGuard: compute({
|
|
1323
|
+
run: timeoutFallbackGuard,
|
|
1324
|
+
}),
|
|
1325
|
+
timeoutFallback: agent({
|
|
1326
|
+
timeoutMs: 30 * 60_000,
|
|
1327
|
+
statusDetail: "choosing a safe timeout fallback",
|
|
1328
|
+
prompt: (context) => {
|
|
1329
|
+
const request = context.input as AutoimplementInput;
|
|
1330
|
+
const guard = context.outputs.timeoutFallbackGuard;
|
|
1331
|
+
const previousFallbacks = context.state.steps
|
|
1332
|
+
.filter((step) => step.nodeId === "timeoutFallback" && step.outcome === "ok")
|
|
1333
|
+
.map((step) => step.output);
|
|
1334
|
+
return [
|
|
1335
|
+
"A bounded Autoimplement agent step timed out. Choose the safest existing workflow stage to run next instead of ending the run blindly.",
|
|
1336
|
+
"This is a read-only fallback step. Inspect state, but do not edit files, run mutating commands, commit, push, open or update a pull request, post comments, merge, deploy, or release.",
|
|
1337
|
+
"Inspect the current repository worktree, branch, diff, and commits. Inspect the remote branch, pull request, review, CI, merge, and final report when they exist and affect the next route.",
|
|
1338
|
+
"Do not assume that the timed-out step failed or completed. Use accepted workflow outputs and durable repository or pull-request state.",
|
|
1339
|
+
"Before any forward route, confirm that its accepted output belongs to the current work attempt and that observed local and remote heads match the accepted publication. Otherwise retry, replan, or block.",
|
|
1340
|
+
"Choose retry only when the timed-out stage must run again. Choose verify when accepted implementation output exists and verification is next. Choose review when accepted publication output exists. Choose ci only after comment inspection routed to CI. Choose deliver only after CI is green or classified unrelated. Choose replan when evidence invalidates the approved plan. Choose blocked only when no safe route exists.",
|
|
1341
|
+
"Do not skip required implementation, verification, review, CI, authorization, or delivery checks.",
|
|
1342
|
+
`Task: ${request.task}`,
|
|
1343
|
+
`Approved plan: ${JSON.stringify(currentPlan(context))}`,
|
|
1344
|
+
`Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
|
|
1345
|
+
`Timeout: ${JSON.stringify(guard)}`,
|
|
1346
|
+
`Accepted outputs: ${JSON.stringify(context.outputs)}`,
|
|
1347
|
+
`Previous fallback results: ${JSON.stringify(previousFallbacks)}`,
|
|
1348
|
+
`Recent workflow attempts: ${JSON.stringify(recentWorkflowAttempts(context))}`,
|
|
1349
|
+
].join("\n");
|
|
1350
|
+
},
|
|
1351
|
+
expectedOutput: `{ "route": "retry" | "verify" | "review" | "ci" | "deliver" | "replan" | "blocked", "reason": "why this is the safest next stage", "evidence": ["state inspected before choosing the route"] }`,
|
|
1352
|
+
validate: parseTimeoutFallback,
|
|
1353
|
+
}),
|
|
1354
|
+
routeTimeoutFallback: compute({
|
|
1355
|
+
run: timeoutFallbackTarget,
|
|
1356
|
+
}),
|
|
1357
|
+
propagateSupportedFailure: compute({
|
|
1358
|
+
run: throwLatestSupportedFailure,
|
|
1359
|
+
}),
|
|
1360
|
+
routeVerifyP2Result: compute({
|
|
1361
|
+
run: ({ outputs }) => outputs.verifyP2,
|
|
1362
|
+
}),
|
|
1363
|
+
routeInspectCommentsResult: compute({
|
|
1364
|
+
run: ({ outputs }) => outputs.inspectComments,
|
|
1365
|
+
}),
|
|
1366
|
+
routeInspectCiResult: compute({
|
|
1367
|
+
run: ({ outputs }) => outputs.inspectCi,
|
|
1368
|
+
}),
|
|
1369
|
+
routeFinalizeDeliveryResult: compute({
|
|
1370
|
+
run: ({ outputs }) => outputs.finalizeDelivery,
|
|
1371
|
+
}),
|
|
726
1372
|
implement: agent({
|
|
727
|
-
timeoutMs: 60 * 60_000,
|
|
1373
|
+
timeoutMs: 8 * 60 * 60_000,
|
|
728
1374
|
statusDetail: "implementing",
|
|
729
1375
|
prompt: (context) => {
|
|
730
1376
|
const request = context.input as AutoimplementInput;
|
|
@@ -733,12 +1379,14 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
733
1379
|
`Plan: ${JSON.stringify(currentPlan(context))}`,
|
|
734
1380
|
`Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
|
|
735
1381
|
`Constraints: ${JSON.stringify(request.constraints ?? [])}`,
|
|
1382
|
+
"Before changing files, inspect the current worktree, diff, commits, branch, remote state, and matching pull request. Continue existing work and do not repeat completed effects.",
|
|
736
1383
|
"Follow repository instructions and use the most elegant long-term production-ready implementation without unnecessary work.",
|
|
737
1384
|
"If implementation exposes a new design or scope problem, report it precisely instead of forcing the old plan.",
|
|
1385
|
+
"Report every changed repository as an absolute path so independent verification can be bounded safely.",
|
|
738
1386
|
"Do not merge yet.",
|
|
739
1387
|
].join("\n");
|
|
740
1388
|
},
|
|
741
|
-
expectedOutput: `{ "status": "implemented" | "issue" | "blocked", "summary": "work completed or issue", "files": ["changed file"], "issueKind": "design" | "implementation" | null, "evidence": "new evidence" }`,
|
|
1389
|
+
expectedOutput: `{ "status": "implemented" | "issue" | "blocked", "summary": "work completed or issue", "files": ["changed file"], "repositories": ["absolute repository path changed"], "issueKind": "design" | "implementation" | null, "evidence": "new evidence" }`,
|
|
742
1390
|
validate: (value) => requireRecord(value, "implementation result"),
|
|
743
1391
|
}),
|
|
744
1392
|
classifyImplementation: agent({
|
|
@@ -805,15 +1453,46 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
805
1453
|
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
1454
|
validate: parseBlockerChallenge,
|
|
807
1455
|
}),
|
|
808
|
-
|
|
809
|
-
timeoutMs:
|
|
810
|
-
statusDetail: "
|
|
1456
|
+
planVerification: agent({
|
|
1457
|
+
timeoutMs: 15 * 60_000,
|
|
1458
|
+
statusDetail: "planning independent verification commands",
|
|
811
1459
|
prompt: () =>
|
|
812
1460
|
[
|
|
813
|
-
"
|
|
814
|
-
"
|
|
815
|
-
"
|
|
816
|
-
"
|
|
1461
|
+
"Select the required local verification commands for the implementation.",
|
|
1462
|
+
"Return one command per independent repository working directory.",
|
|
1463
|
+
"Use exact executables and argument arrays without shell wrappers, environment overrides, stdin, Git or GitHub mutations, package publication, deployment, merge, or release commands.",
|
|
1464
|
+
"Use absolute repository paths, explicit timeouts no longer than 2700000ms, and maxOutputChars no larger than 1000000.",
|
|
1465
|
+
"List checks that cannot run locally under untested.",
|
|
1466
|
+
].join("\n"),
|
|
1467
|
+
expectedOutput: `{ "commands": [{ "id": "stable-id", "command": "npm", "args": ["run", "check"], "cwd": "/absolute/repository", "timeoutMs": 2700000, "maxOutputChars": 1000000 }], "untested": ["remaining check"] }`,
|
|
1468
|
+
validate: parseVerificationForContext,
|
|
1469
|
+
}),
|
|
1470
|
+
runVerification: action({
|
|
1471
|
+
timeoutMs: (context) => {
|
|
1472
|
+
const plan = latestOutput<VerificationCommandPlan>(context, ["planVerification"]);
|
|
1473
|
+
return commandBatchTimeoutMs(plan.commands, concurrency(context).verification);
|
|
1474
|
+
},
|
|
1475
|
+
statusDetail: "running independent verification commands",
|
|
1476
|
+
run: async (context) => {
|
|
1477
|
+
const plan = latestOutput<VerificationCommandPlan>(context, ["planVerification"]);
|
|
1478
|
+
return await runAutoimplementBatch(
|
|
1479
|
+
context,
|
|
1480
|
+
"verification",
|
|
1481
|
+
plan.commands,
|
|
1482
|
+
concurrency(context).verification,
|
|
1483
|
+
);
|
|
1484
|
+
},
|
|
1485
|
+
}),
|
|
1486
|
+
verify: agent({
|
|
1487
|
+
timeoutMs: 20 * 60_000,
|
|
1488
|
+
statusDetail: "assessing verification",
|
|
1489
|
+
prompt: (context) =>
|
|
1490
|
+
[
|
|
1491
|
+
"Assess the completed local verification commands.",
|
|
1492
|
+
"Set passed true only when every required command succeeded without truncated output.",
|
|
1493
|
+
"Report exact command outcomes, failures, and checks that still need remote verification.",
|
|
1494
|
+
`Command plan: ${JSON.stringify(latestOutput(context, ["planVerification"]))}`,
|
|
1495
|
+
`Command results: ${JSON.stringify(latestOutput(context, ["runVerification"]))}`,
|
|
817
1496
|
].join("\n"),
|
|
818
1497
|
expectedOutput: `{ "passed": true | false, "commands": [{ "command": "exact command", "outcome": "result" }], "failures": ["failure"], "untested": ["remaining check"] }`,
|
|
819
1498
|
validate: (value) => requireRecord(value, "verification result"),
|
|
@@ -843,6 +1522,7 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
843
1522
|
prompt: (context) =>
|
|
844
1523
|
[
|
|
845
1524
|
"Fix the current implementation issue without expanding the approved design.",
|
|
1525
|
+
"Inspect the current diff and commits first. Continue any partial fix and change only work that is still missing.",
|
|
846
1526
|
`Issue: ${JSON.stringify(latestIssue(context))}`,
|
|
847
1527
|
`Current plan: ${JSON.stringify(currentPlan(context))}`,
|
|
848
1528
|
"Stop after the fix so verification can run again.",
|
|
@@ -857,82 +1537,68 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
857
1537
|
const request = input as AutoimplementInput;
|
|
858
1538
|
return [
|
|
859
1539
|
"Commit and push the verified implementation before review.",
|
|
1540
|
+
"Inspect the branch, local and remote heads, and matching pull requests first. Do not push an already-pushed head or create a second pull request for the same branch and base.",
|
|
860
1541
|
"Use the existing implementation-plan PR when one exists. Otherwise open a PR and use the pr-description skill for its body.",
|
|
861
1542
|
"Inspect the complete public diff before every push or PR mutation.",
|
|
862
|
-
|
|
1543
|
+
"Report every repository that received a pushed pull request with its absolute repository path, branch, base branch, pushed head revision, and PR URL.",
|
|
1544
|
+
"Include dependencyFingerprint only when a declared dependency result is relevant to review reuse.",
|
|
1545
|
+
`Requested base branch: ${request.baseBranch ?? "discover each repository default branch"}.`,
|
|
863
1546
|
"Do not merge yet.",
|
|
864
1547
|
].join("\n");
|
|
865
1548
|
},
|
|
866
|
-
expectedOutput: `{ "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true }`,
|
|
867
|
-
validate:
|
|
868
|
-
}),
|
|
869
|
-
authorReviewCommand: agent({
|
|
870
|
-
statusDetail: "writing reviewer command",
|
|
871
|
-
prompt: ({ outputs, input }) => {
|
|
872
|
-
const published = outputs.publish as Record<string, unknown>;
|
|
873
|
-
const request = input as AutoimplementInput;
|
|
874
|
-
return [
|
|
875
|
-
"Write the exact Pi Reviewer command for the pushed branch.",
|
|
876
|
-
"The executable must be pi-reviewer. Use its configured model and thinking settings.",
|
|
877
|
-
"Use the repository base branch and an absolute repository working directory.",
|
|
878
|
-
"Set timeoutMs to at most 600000.",
|
|
879
|
-
`Published branch: ${JSON.stringify(published)}`,
|
|
880
|
-
`Repository hint: ${request.repository ?? "current repository"}`,
|
|
881
|
-
].join("\n");
|
|
882
|
-
},
|
|
883
|
-
expectedOutput: `{ "command": "pi-reviewer", "args": ["--base", "main"], "cwd": "/absolute/repository", "timeoutMs": 600000 }`,
|
|
884
|
-
validate: parseReviewerCommand,
|
|
1549
|
+
expectedOutput: `{ "repositories": [{ "repository": "/absolute/repository", "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true, "dependencyFingerprint": "optional digest" }] }`,
|
|
1550
|
+
validate: parsePublishedRepositories,
|
|
885
1551
|
}),
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
timeoutMs: TEN_MINUTES_MS + 10_000,
|
|
889
|
-
exec: (context) =>
|
|
890
|
-
commandExecution(
|
|
891
|
-
latestOutput<StructuredCommand>(context, ["authorReviewCommand", "repairReviewCommand"]),
|
|
892
|
-
),
|
|
1552
|
+
selectReviewCommands: compute({
|
|
1553
|
+
run: selectReviewCommands,
|
|
893
1554
|
}),
|
|
894
|
-
|
|
895
|
-
statusDetail: "
|
|
896
|
-
|
|
897
|
-
const
|
|
898
|
-
return
|
|
899
|
-
"The Pi Reviewer invocation failed. Diagnose the exact command, arguments, base branch, working directory, and error.",
|
|
900
|
-
"Write a corrected pi-reviewer command. Do not substitute codex review or another reviewer.",
|
|
901
|
-
"If Pi Reviewer or its configuration is missing, report that through the same command shape only when another valid invocation exists; otherwise the next assessment must block.",
|
|
902
|
-
`Failed result: ${JSON.stringify(failed)}`,
|
|
903
|
-
].join("\n");
|
|
1555
|
+
runReview: action({
|
|
1556
|
+
statusDetail: "running pi-reviewer commands",
|
|
1557
|
+
timeoutMs: (context) => {
|
|
1558
|
+
const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
|
|
1559
|
+
return commandBatchTimeoutMs(selected.commands, concurrency(context).reviewer);
|
|
904
1560
|
},
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
}
|
|
914
|
-
if (result.route !== "retry")
|
|
915
|
-
throw new Error("reviewer command repair route must be retry or blocked");
|
|
916
|
-
return {
|
|
917
|
-
route: "retry",
|
|
918
|
-
...parseReviewerCommand(result),
|
|
919
|
-
reason: requireString(result.reason, "reviewer command repair reason"),
|
|
920
|
-
};
|
|
1561
|
+
run: async (context): Promise<BatchExecution> => {
|
|
1562
|
+
const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
|
|
1563
|
+
const batch = await runAutoimplementBatch(
|
|
1564
|
+
context,
|
|
1565
|
+
"review",
|
|
1566
|
+
selected.commands,
|
|
1567
|
+
concurrency(context).reviewer,
|
|
1568
|
+
);
|
|
1569
|
+
return { route: reviewBatchNeedsRepair(batch) ? "repair" : "assess", batch };
|
|
921
1570
|
},
|
|
922
1571
|
}),
|
|
1572
|
+
repairReviewCommand: agent({
|
|
1573
|
+
statusDetail: "repairing reviewer prerequisites",
|
|
1574
|
+
prompt: (context) =>
|
|
1575
|
+
[
|
|
1576
|
+
"One or more pi-reviewer commands failed, timed out, or returned truncated output.",
|
|
1577
|
+
"Diagnose and fix only local reviewer prerequisites or configuration that are in scope.",
|
|
1578
|
+
"Do not change the deterministic executable, base branch, or repository command shape, and do not substitute another reviewer.",
|
|
1579
|
+
"Choose retry only when the same commands can now produce complete reviews. Choose blocked when pi-reviewer or required configuration remains unavailable.",
|
|
1580
|
+
`Failed batch: ${JSON.stringify(context.outputs.runReview)}`,
|
|
1581
|
+
].join("\n"),
|
|
1582
|
+
expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis and action" }`,
|
|
1583
|
+
validate: (value) =>
|
|
1584
|
+
parseRoute(value, ["retry", "blocked"] as const, "reviewer command repair"),
|
|
1585
|
+
}),
|
|
923
1586
|
assessReview: agent({
|
|
924
1587
|
statusDetail: "assessing reviewer findings",
|
|
925
1588
|
prompt: (context) => {
|
|
926
|
-
const
|
|
1589
|
+
const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
|
|
1590
|
+
const execution = latestOutput<BatchExecution>(context, ["runReview"]);
|
|
927
1591
|
return [
|
|
928
|
-
"Assess
|
|
929
|
-
"
|
|
930
|
-
"
|
|
1592
|
+
"Assess each completed pi-reviewer result separately.",
|
|
1593
|
+
"Return one repository entry for every selected command, using the exact repository id.",
|
|
1594
|
+
"Set invocationSucceeded false when a complete valid review was not produced.",
|
|
1595
|
+
"Record every finding under P0, P1, P2, or lower and mark it as design or implementation.",
|
|
931
1596
|
"Do not promote P2 findings to P1 merely to force another review round.",
|
|
932
|
-
`
|
|
1597
|
+
`Selected repositories: ${JSON.stringify(selected.repositories)}`,
|
|
1598
|
+
`Reviewer results: ${JSON.stringify(execution.batch)}`,
|
|
933
1599
|
].join("\n");
|
|
934
1600
|
},
|
|
935
|
-
expectedOutput: `{ "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }`,
|
|
1601
|
+
expectedOutput: `{ "repositories": [{ "id": "repository-id", "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }], "reason": "batch assessment" }`,
|
|
936
1602
|
validate: parseReviewAssessment,
|
|
937
1603
|
}),
|
|
938
1604
|
triageReview: compute({
|
|
@@ -952,7 +1618,8 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
952
1618
|
prompt: ({ outputs }) =>
|
|
953
1619
|
[
|
|
954
1620
|
"Address valid P2 findings from the last review when the improvement is proportionate and in scope.",
|
|
955
|
-
"
|
|
1621
|
+
"Inspect the current diff and commits first. Do not repeat a P2 change that is already present.",
|
|
1622
|
+
"Do not rerun pi-reviewer solely because P2 work changes files. Verification will run once, then the workflow continues.",
|
|
956
1623
|
`Review: ${JSON.stringify(outputs.assessReview)}`,
|
|
957
1624
|
].join("\n"),
|
|
958
1625
|
expectedOutput: `{ "addressed": ["P2 change"], "skipped": [{ "finding": "finding", "reason": "why" }] }`,
|
|
@@ -964,20 +1631,23 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
964
1631
|
prompt: () =>
|
|
965
1632
|
[
|
|
966
1633
|
"Run focused verification for the P2 changes and push the verified result.",
|
|
967
|
-
"
|
|
1634
|
+
"Inspect the local and remote heads first. Do not push again when the verified head is already remote.",
|
|
1635
|
+
"Do not run pi-reviewer again because the previous round had no P0 or P1 findings.",
|
|
1636
|
+
"Re-observe every published PR after the push and return its current repository, branch, base branch, head revision, PR URL, pushed status, and unchanged dependency fingerprint.",
|
|
968
1637
|
"Report exact commands and outcomes.",
|
|
969
1638
|
].join("\n"),
|
|
970
|
-
expectedOutput: `{ "passed": true | false, "commands": [{ "command": "command", "outcome": "result" }], "pushed": true }`,
|
|
971
|
-
validate:
|
|
1639
|
+
expectedOutput: `{ "passed": true | false, "commands": [{ "command": "command", "outcome": "result" }], "pushed": true, "repositories": [{ "repository": "/absolute/repository", "branch": "branch", "baseBranch": "base", "headRevision": "current pushed revision", "pr": "URL", "pushed": true, "dependencyFingerprint": "optional fingerprint" }] }`,
|
|
1640
|
+
validate: parseP2Verification,
|
|
972
1641
|
}),
|
|
973
1642
|
inspectComments: agent({
|
|
974
1643
|
timeoutMs: 20 * 60_000,
|
|
975
1644
|
statusDetail: "checking PR comments",
|
|
976
|
-
prompt: () =>
|
|
1645
|
+
prompt: (context) =>
|
|
977
1646
|
[
|
|
978
|
-
"Inspect current inline review comments and PR issue comments.",
|
|
979
|
-
"Reply to and resolve every comment. Ignore stale or irrelevant comments only after explaining why.",
|
|
980
|
-
"Choose redesign for a valid design issue, fix for a local code issue, ci when no actionable comment remains, or blocked for an external blocker.",
|
|
1647
|
+
"Inspect current inline review comments and PR issue comments for every published pull request.",
|
|
1648
|
+
"Handle pull requests one at a time. Reply to and resolve every comment. Ignore stale or irrelevant comments only after explaining why.",
|
|
1649
|
+
"Choose redesign for a valid design issue, fix for a local code issue, ci when no actionable comment remains on any PR, or blocked for an external blocker.",
|
|
1650
|
+
`Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
|
|
981
1651
|
].join("\n"),
|
|
982
1652
|
expectedOutput: `{ "route": "redesign" | "fix" | "ci" | "blocked", "summary": "comment status", "evidence": ["comment or response"] }`,
|
|
983
1653
|
validate: (value) =>
|
|
@@ -986,71 +1656,75 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
986
1656
|
inspectCi: agent({
|
|
987
1657
|
timeoutMs: 10 * 60_000,
|
|
988
1658
|
statusDetail: "checking CI",
|
|
989
|
-
prompt: () =>
|
|
1659
|
+
prompt: (context) =>
|
|
990
1660
|
[
|
|
991
|
-
"Inspect
|
|
992
|
-
"
|
|
993
|
-
"
|
|
994
|
-
"
|
|
1661
|
+
"Inspect every published pull request once without waiting for completion.",
|
|
1662
|
+
"Return one target per repository and current PR head.",
|
|
1663
|
+
"Choose green, failed, pending, or unavailable for each target.",
|
|
1664
|
+
"When pending, provide an exact supported gh pr checks --watch or gh run watch command with the repository id, absolute repository cwd, timeoutMs at most 300000, and maxOutputChars at most 1000000. The workflow binds it to the target PR before execution.",
|
|
1665
|
+
"Separate failures caused by this change from unrelated failures. Do not invent an ETA.",
|
|
1666
|
+
`Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
|
|
995
1667
|
].join("\n"),
|
|
996
|
-
expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "relatedFailures": ["failure"], "unrelatedFailures": ["failure"], "trackingCommand": { "command": "gh", "args": ["pr", "checks", "--watch"], "cwd": "/absolute/repository", "timeoutMs": 300000
|
|
997
|
-
validate:
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1668
|
+
expectedOutput: `{ "targets": [{ "repository": "/absolute/repository", "headRevision": "revision", "pr": "URL", "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "relatedFailures": ["failure"], "unrelatedFailures": ["failure"], "trackingCommand": { "id": "repository-id", "command": "gh", "args": ["pr", "checks", "PR URL", "--watch"], "cwd": "/absolute/repository", "timeoutMs": 300000, "maxOutputChars": 1000000 } }] }`,
|
|
1669
|
+
validate: parseCiInspectionForPublished,
|
|
1670
|
+
}),
|
|
1671
|
+
trackCi: action({
|
|
1672
|
+
statusDetail: "tracking pending CI commands",
|
|
1673
|
+
timeoutMs: (context) => {
|
|
1674
|
+
const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
|
|
1675
|
+
const commands = inspected.targets.flatMap((target) =>
|
|
1676
|
+
target.trackingCommand === undefined ? [] : [target.trackingCommand],
|
|
1002
1677
|
);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1678
|
+
return commandBatchTimeoutMs(commands, concurrency(context).ciWatch);
|
|
1679
|
+
},
|
|
1680
|
+
run: async (context): Promise<BatchExecution> => {
|
|
1681
|
+
const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
|
|
1682
|
+
const commands = inspected.targets.flatMap((target) =>
|
|
1683
|
+
target.trackingCommand === undefined ? [] : [target.trackingCommand],
|
|
1684
|
+
);
|
|
1685
|
+
const batch = await runAutoimplementBatch(
|
|
1686
|
+
context,
|
|
1687
|
+
"ciWatch",
|
|
1688
|
+
commands,
|
|
1689
|
+
concurrency(context).ciWatch,
|
|
1690
|
+
);
|
|
1691
|
+
const needsRepair = batch.items.some(
|
|
1692
|
+
(item) =>
|
|
1693
|
+
(item.outcome === "failed" && item.exitCode === null) ||
|
|
1694
|
+
item.stdoutTruncated ||
|
|
1695
|
+
item.stderrTruncated,
|
|
1696
|
+
);
|
|
1697
|
+
return { route: needsRepair ? "repair" : "assess", batch };
|
|
1006
1698
|
},
|
|
1007
|
-
}),
|
|
1008
|
-
trackCi: shell({
|
|
1009
|
-
statusDetail: "tracking CI for at most five minutes",
|
|
1010
|
-
timeoutMs: FIVE_MINUTES_MS + 10_000,
|
|
1011
|
-
exec: (context) => commandExecution(latestCiCommand(context)),
|
|
1012
1699
|
}),
|
|
1013
1700
|
repairCiCommand: agent({
|
|
1014
|
-
statusDetail: "
|
|
1701
|
+
statusDetail: "repairing CI watch prerequisites",
|
|
1015
1702
|
prompt: (context) =>
|
|
1016
1703
|
[
|
|
1017
|
-
"
|
|
1018
|
-
"
|
|
1019
|
-
"
|
|
1020
|
-
|
|
1704
|
+
"One or more supported CI watch commands failed or returned truncated output.",
|
|
1705
|
+
"Diagnose and fix only local gh prerequisites or authentication that are already authorized.",
|
|
1706
|
+
"Do not change the PR identity or substitute another command form.",
|
|
1707
|
+
"Choose retry only when the same validated commands can now provide useful status. Choose blocked otherwise.",
|
|
1708
|
+
`Failure: ${JSON.stringify(context.outputs.trackCi)}`,
|
|
1021
1709
|
].join("\n"),
|
|
1022
|
-
expectedOutput: `{ "route": "retry" | "blocked", "
|
|
1023
|
-
validate: (value) =>
|
|
1024
|
-
const result = requireRecord(value, "CI command repair");
|
|
1025
|
-
if (result.route === "blocked") {
|
|
1026
|
-
return { route: "blocked", reason: requireString(result.reason, "CI command blocker") };
|
|
1027
|
-
}
|
|
1028
|
-
if (result.route !== "retry")
|
|
1029
|
-
throw new Error("CI command repair route must be retry or blocked");
|
|
1030
|
-
return {
|
|
1031
|
-
route: "retry",
|
|
1032
|
-
...parseCiCommand(result),
|
|
1033
|
-
reason: requireString(result.reason, "CI command repair reason"),
|
|
1034
|
-
};
|
|
1035
|
-
},
|
|
1710
|
+
expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis" }`,
|
|
1711
|
+
validate: (value) => parseRoute(value, ["retry", "blocked"] as const, "CI command repair"),
|
|
1036
1712
|
}),
|
|
1037
1713
|
assessTrackedCi: agent({
|
|
1038
1714
|
statusDetail: "assessing tracked CI",
|
|
1039
1715
|
prompt: (context) => {
|
|
1040
|
-
const
|
|
1716
|
+
const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
|
|
1717
|
+
const execution = latestOutput<BatchExecution>(context, ["trackCi"]);
|
|
1041
1718
|
return [
|
|
1042
|
-
"Assess
|
|
1043
|
-
"
|
|
1044
|
-
|
|
1719
|
+
"Assess every CI watch result without starting another wait.",
|
|
1720
|
+
"Return one target result for every watched PR and an aggregate route of green, failed, pending, or unavailable.",
|
|
1721
|
+
"A timed-out watch normally remains pending. Separate related from unrelated failures. Do not invent an ETA.",
|
|
1722
|
+
`Initial inspection: ${JSON.stringify(inspected)}`,
|
|
1723
|
+
`Tracking results: ${JSON.stringify(execution.batch)}`,
|
|
1045
1724
|
].join("\n");
|
|
1046
1725
|
},
|
|
1047
|
-
expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "relatedFailures": ["failure"], "unrelatedFailures": ["failure"] }`,
|
|
1048
|
-
validate:
|
|
1049
|
-
parseRoute(
|
|
1050
|
-
value,
|
|
1051
|
-
["green", "failed", "pending", "unavailable"] as const,
|
|
1052
|
-
"tracked CI assessment",
|
|
1053
|
-
),
|
|
1726
|
+
expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "targets": [{ "id": "repository-id", "route": "green" | "failed" | "pending" | "unavailable", "reason": "status" }], "relatedFailures": ["failure"], "unrelatedFailures": ["failure"] }`,
|
|
1727
|
+
validate: parseTrackedCiAssessment,
|
|
1054
1728
|
}),
|
|
1055
1729
|
opportunisticTest: agent({
|
|
1056
1730
|
timeoutMs: 30 * 60_000,
|
|
@@ -1083,30 +1757,23 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1083
1757
|
}),
|
|
1084
1758
|
finalizeDelivery: agent({
|
|
1085
1759
|
timeoutMs: 30 * 60_000,
|
|
1086
|
-
statusDetail: "finalizing
|
|
1087
|
-
prompt: (
|
|
1088
|
-
const request = input as AutoimplementInput;
|
|
1760
|
+
statusDetail: "finalizing PRs",
|
|
1761
|
+
prompt: (context) => {
|
|
1762
|
+
const request = context.input as AutoimplementInput;
|
|
1089
1763
|
return [
|
|
1090
1764
|
request.merge === false
|
|
1091
|
-
? "Leave
|
|
1092
|
-
: "
|
|
1093
|
-
"
|
|
1094
|
-
"
|
|
1765
|
+
? "Leave every verified PR ready without merging because input disabled merge."
|
|
1766
|
+
: "Handle verified PRs one at a time and merge each unless repository policy or explicit user instructions prohibit it.",
|
|
1767
|
+
"Before each mutation, inspect the current PR head, merge state, and existing final report. Do not merge an already merged expected head or post a duplicate report.",
|
|
1768
|
+
"Use each repository's required merge method.",
|
|
1769
|
+
"Post a final report with the implementation summary and exact validation commands on every PR only when that report is missing.",
|
|
1770
|
+
"Keep the existing top-level merged, pr, reportComment, and reason fields. For several PRs, use the first PR for the top-level compatibility fields and include every result under repositories.",
|
|
1095
1771
|
"Return blocked instead of claiming completion when a required merge or report action fails.",
|
|
1772
|
+
`Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
|
|
1096
1773
|
].join("\n");
|
|
1097
1774
|
},
|
|
1098
|
-
expectedOutput: `{ "status": "completed" | "blocked", "merged": true | false, "pr": "URL", "reportComment": "URL or summary", "reason": "result" }`,
|
|
1099
|
-
validate:
|
|
1100
|
-
const result = requireRecord(value, "delivery result");
|
|
1101
|
-
if (result.status !== "completed" && result.status !== "blocked") {
|
|
1102
|
-
throw new Error("delivery status must be completed or blocked");
|
|
1103
|
-
}
|
|
1104
|
-
const request = context.input as AutoimplementInput;
|
|
1105
|
-
if (request.merge !== true && result.merged === true) {
|
|
1106
|
-
throw new Error("delivery cannot merge without explicit merge: true");
|
|
1107
|
-
}
|
|
1108
|
-
return result;
|
|
1109
|
-
},
|
|
1775
|
+
expectedOutput: `{ "status": "completed" | "blocked", "merged": true | false, "pr": "first PR URL", "reportComment": "first report URL or summary", "reason": "aggregate result", "repositories": [{ "repository": "/absolute/repository", "pr": "URL", "merged": true | false, "reportComment": "URL or summary", "reason": "result" }] }`,
|
|
1776
|
+
validate: parseDeliveryResult,
|
|
1110
1777
|
}),
|
|
1111
1778
|
blocked: compute({
|
|
1112
1779
|
run: (context) => {
|
|
@@ -1127,11 +1794,11 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1127
1794
|
status: "completed",
|
|
1128
1795
|
task: request.task,
|
|
1129
1796
|
plan: currentPlan(context),
|
|
1130
|
-
implementation: context
|
|
1131
|
-
verification: context
|
|
1132
|
-
reviewRounds:
|
|
1133
|
-
ci: context
|
|
1134
|
-
delivery: context
|
|
1797
|
+
implementation: latestOutput(context, ["implement"]),
|
|
1798
|
+
verification: latestOutput(context, ["verifyP2", "verify"]),
|
|
1799
|
+
reviewRounds: reviewRoundsForOutput(context),
|
|
1800
|
+
ci: ciForOutput(context),
|
|
1801
|
+
delivery: latestOutput(context, ["finalizeDelivery"]),
|
|
1135
1802
|
} satisfies AutoimplementCompleted;
|
|
1136
1803
|
},
|
|
1137
1804
|
}),
|
|
@@ -1174,13 +1841,51 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1174
1841
|
from: "replanGuard",
|
|
1175
1842
|
switch: { on: "$.route", cases: { redesign: "redesign", blocked: "blocked" } },
|
|
1176
1843
|
},
|
|
1177
|
-
{ from: "implement", to: "classifyImplementation" },
|
|
1178
1844
|
{
|
|
1179
|
-
from: "
|
|
1845
|
+
from: "timeoutFallbackGuard",
|
|
1846
|
+
switch: { on: "$.route", cases: { recover: "timeoutFallback", blocked: "blocked" } },
|
|
1847
|
+
},
|
|
1848
|
+
{ from: "timeoutFallback", to: "routeTimeoutFallback" },
|
|
1849
|
+
{ from: "propagateSupportedFailure", to: "blocked" },
|
|
1850
|
+
{
|
|
1851
|
+
from: "routeTimeoutFallback",
|
|
1180
1852
|
switch: {
|
|
1181
1853
|
on: "$.route",
|
|
1182
1854
|
cases: {
|
|
1855
|
+
implement: "implement",
|
|
1856
|
+
planVerification: "planVerification",
|
|
1183
1857
|
verify: "verify",
|
|
1858
|
+
fix: "fix",
|
|
1859
|
+
publish: "publish",
|
|
1860
|
+
addressP2: "addressP2",
|
|
1861
|
+
verifyP2: "verifyP2",
|
|
1862
|
+
inspectComments: "inspectComments",
|
|
1863
|
+
inspectCi: "inspectCi",
|
|
1864
|
+
opportunisticTest: "opportunisticTest",
|
|
1865
|
+
finalizeDelivery: "finalizeDelivery",
|
|
1866
|
+
selectReviewCommands: "selectReviewCommands",
|
|
1867
|
+
redesign: "redesign",
|
|
1868
|
+
blocked: "blocked",
|
|
1869
|
+
},
|
|
1870
|
+
},
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
from: "implement",
|
|
1874
|
+
switch: {
|
|
1875
|
+
on: "$result.outcome",
|
|
1876
|
+
cases: {
|
|
1877
|
+
ok: "classifyImplementation",
|
|
1878
|
+
timed_out: "timeoutFallbackGuard",
|
|
1879
|
+
failed: "propagateSupportedFailure",
|
|
1880
|
+
},
|
|
1881
|
+
},
|
|
1882
|
+
},
|
|
1883
|
+
{
|
|
1884
|
+
from: "classifyImplementation",
|
|
1885
|
+
switch: {
|
|
1886
|
+
on: "$.route",
|
|
1887
|
+
cases: {
|
|
1888
|
+
verify: "planVerification",
|
|
1184
1889
|
redesign: "redesign",
|
|
1185
1890
|
fix: "fix",
|
|
1186
1891
|
blocked: "challengeBlockerGuard",
|
|
@@ -1195,7 +1900,29 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1195
1900
|
from: "challengeBlocker",
|
|
1196
1901
|
switch: { on: "$.route", cases: { continue: "redesign", blocked: "blocked" } },
|
|
1197
1902
|
},
|
|
1198
|
-
{
|
|
1903
|
+
{
|
|
1904
|
+
from: "planVerification",
|
|
1905
|
+
switch: {
|
|
1906
|
+
on: "$result.outcome",
|
|
1907
|
+
cases: {
|
|
1908
|
+
ok: "runVerification",
|
|
1909
|
+
timed_out: "timeoutFallbackGuard",
|
|
1910
|
+
failed: "propagateSupportedFailure",
|
|
1911
|
+
},
|
|
1912
|
+
},
|
|
1913
|
+
},
|
|
1914
|
+
{ from: "runVerification", to: "verify" },
|
|
1915
|
+
{
|
|
1916
|
+
from: "verify",
|
|
1917
|
+
switch: {
|
|
1918
|
+
on: "$result.outcome",
|
|
1919
|
+
cases: {
|
|
1920
|
+
ok: "classifyVerification",
|
|
1921
|
+
timed_out: "timeoutFallbackGuard",
|
|
1922
|
+
failed: "propagateSupportedFailure",
|
|
1923
|
+
},
|
|
1924
|
+
},
|
|
1925
|
+
},
|
|
1199
1926
|
{
|
|
1200
1927
|
from: "classifyVerification",
|
|
1201
1928
|
switch: {
|
|
@@ -1208,20 +1935,36 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1208
1935
|
},
|
|
1209
1936
|
},
|
|
1210
1937
|
},
|
|
1211
|
-
{ from: "fix", to: "verify" },
|
|
1212
|
-
{ from: "publish", to: "authorReviewCommand" },
|
|
1213
|
-
{ from: "authorReviewCommand", to: "runReview" },
|
|
1214
1938
|
{
|
|
1215
|
-
from: "
|
|
1939
|
+
from: "fix",
|
|
1940
|
+
switch: {
|
|
1941
|
+
on: "$result.outcome",
|
|
1942
|
+
cases: {
|
|
1943
|
+
ok: "planVerification",
|
|
1944
|
+
timed_out: "timeoutFallbackGuard",
|
|
1945
|
+
failed: "propagateSupportedFailure",
|
|
1946
|
+
},
|
|
1947
|
+
},
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
from: "publish",
|
|
1216
1951
|
switch: {
|
|
1217
1952
|
on: "$result.outcome",
|
|
1218
1953
|
cases: {
|
|
1219
|
-
ok: "
|
|
1220
|
-
|
|
1221
|
-
|
|
1954
|
+
ok: "selectReviewCommands",
|
|
1955
|
+
timed_out: "timeoutFallbackGuard",
|
|
1956
|
+
failed: "propagateSupportedFailure",
|
|
1222
1957
|
},
|
|
1223
1958
|
},
|
|
1224
1959
|
},
|
|
1960
|
+
{
|
|
1961
|
+
from: "selectReviewCommands",
|
|
1962
|
+
switch: { on: "$.route", cases: { run: "runReview", reuse: "inspectComments" } },
|
|
1963
|
+
},
|
|
1964
|
+
{
|
|
1965
|
+
from: "runReview",
|
|
1966
|
+
switch: { on: "$.route", cases: { assess: "assessReview", repair: "repairReviewCommand" } },
|
|
1967
|
+
},
|
|
1225
1968
|
{
|
|
1226
1969
|
from: "repairReviewCommand",
|
|
1227
1970
|
switch: {
|
|
@@ -1245,13 +1988,45 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1245
1988
|
from: "triageReview",
|
|
1246
1989
|
switch: { on: "$.route", cases: { redesign: "redesign", fix: "fix" } },
|
|
1247
1990
|
},
|
|
1248
|
-
{
|
|
1991
|
+
{
|
|
1992
|
+
from: "addressP2",
|
|
1993
|
+
switch: {
|
|
1994
|
+
on: "$result.outcome",
|
|
1995
|
+
cases: {
|
|
1996
|
+
ok: "verifyP2",
|
|
1997
|
+
timed_out: "timeoutFallbackGuard",
|
|
1998
|
+
failed: "propagateSupportedFailure",
|
|
1999
|
+
},
|
|
2000
|
+
},
|
|
2001
|
+
},
|
|
1249
2002
|
{
|
|
1250
2003
|
from: "verifyP2",
|
|
2004
|
+
switch: {
|
|
2005
|
+
on: "$result.outcome",
|
|
2006
|
+
cases: {
|
|
2007
|
+
ok: "routeVerifyP2Result",
|
|
2008
|
+
timed_out: "timeoutFallbackGuard",
|
|
2009
|
+
failed: "propagateSupportedFailure",
|
|
2010
|
+
},
|
|
2011
|
+
},
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
from: "routeVerifyP2Result",
|
|
1251
2015
|
switch: { on: "$.passed", cases: { true: "inspectComments", false: "fix" } },
|
|
1252
2016
|
},
|
|
1253
2017
|
{
|
|
1254
2018
|
from: "inspectComments",
|
|
2019
|
+
switch: {
|
|
2020
|
+
on: "$result.outcome",
|
|
2021
|
+
cases: {
|
|
2022
|
+
ok: "routeInspectCommentsResult",
|
|
2023
|
+
timed_out: "timeoutFallbackGuard",
|
|
2024
|
+
failed: "propagateSupportedFailure",
|
|
2025
|
+
},
|
|
2026
|
+
},
|
|
2027
|
+
},
|
|
2028
|
+
{
|
|
2029
|
+
from: "routeInspectCommentsResult",
|
|
1255
2030
|
switch: {
|
|
1256
2031
|
on: "$.route",
|
|
1257
2032
|
cases: {
|
|
@@ -1264,6 +2039,17 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1264
2039
|
},
|
|
1265
2040
|
{
|
|
1266
2041
|
from: "inspectCi",
|
|
2042
|
+
switch: {
|
|
2043
|
+
on: "$result.outcome",
|
|
2044
|
+
cases: {
|
|
2045
|
+
ok: "routeInspectCiResult",
|
|
2046
|
+
timed_out: "timeoutFallbackGuard",
|
|
2047
|
+
failed: "propagateSupportedFailure",
|
|
2048
|
+
},
|
|
2049
|
+
},
|
|
2050
|
+
},
|
|
2051
|
+
{
|
|
2052
|
+
from: "routeInspectCiResult",
|
|
1267
2053
|
switch: {
|
|
1268
2054
|
on: "$.route",
|
|
1269
2055
|
cases: {
|
|
@@ -1276,10 +2062,7 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1276
2062
|
},
|
|
1277
2063
|
{
|
|
1278
2064
|
from: "trackCi",
|
|
1279
|
-
switch: {
|
|
1280
|
-
on: "$result.outcome",
|
|
1281
|
-
cases: { ok: "assessTrackedCi", failed: "repairCiCommand", timed_out: "opportunisticTest" },
|
|
1282
|
-
},
|
|
2065
|
+
switch: { on: "$.route", cases: { assess: "assessTrackedCi", repair: "repairCiCommand" } },
|
|
1283
2066
|
},
|
|
1284
2067
|
{
|
|
1285
2068
|
from: "repairCiCommand",
|
|
@@ -1300,7 +2083,17 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1300
2083
|
},
|
|
1301
2084
|
},
|
|
1302
2085
|
},
|
|
1303
|
-
{
|
|
2086
|
+
{
|
|
2087
|
+
from: "opportunisticTest",
|
|
2088
|
+
switch: {
|
|
2089
|
+
on: "$result.outcome",
|
|
2090
|
+
cases: {
|
|
2091
|
+
ok: "inspectCi",
|
|
2092
|
+
timed_out: "timeoutFallbackGuard",
|
|
2093
|
+
failed: "propagateSupportedFailure",
|
|
2094
|
+
},
|
|
2095
|
+
},
|
|
2096
|
+
},
|
|
1304
2097
|
{
|
|
1305
2098
|
from: "classifyCi",
|
|
1306
2099
|
switch: {
|
|
@@ -1315,6 +2108,17 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1315
2108
|
},
|
|
1316
2109
|
{
|
|
1317
2110
|
from: "finalizeDelivery",
|
|
2111
|
+
switch: {
|
|
2112
|
+
on: "$result.outcome",
|
|
2113
|
+
cases: {
|
|
2114
|
+
ok: "routeFinalizeDeliveryResult",
|
|
2115
|
+
timed_out: "timeoutFallbackGuard",
|
|
2116
|
+
failed: "propagateSupportedFailure",
|
|
2117
|
+
},
|
|
2118
|
+
},
|
|
2119
|
+
},
|
|
2120
|
+
{
|
|
2121
|
+
from: "routeFinalizeDeliveryResult",
|
|
1318
2122
|
switch: {
|
|
1319
2123
|
on: "$.status",
|
|
1320
2124
|
cases: { completed: "finalize", blocked: "challengeBlockerGuard" },
|