@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,14 +1,43 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { runCommandBatch, } from "../workflows/command-batch.js";
|
|
3
|
+
import { action, agent, compute, defineWorkflow, includeWorkflow, includedResult, } from "../workflows/definition.js";
|
|
3
4
|
import { digest } from "../workflows/human-decision.js";
|
|
4
5
|
import autodocWorkflow, {} from "./autodoc.workflow.js";
|
|
6
|
+
import { parseAutoimplementConcurrency, parseCiInspectionBatch, parsePublishedRepositories, parseVerificationCommandPlan, reviewerCommand, } from "./autoimplement-command-batches.js";
|
|
5
7
|
import autoplanWorkflow, {} from "./autoplan.workflow.js";
|
|
6
8
|
import planApprovalWorkflow, {} from "./plan-approval.workflow.js";
|
|
7
|
-
const FIVE_MINUTES_MS = 5 * 60_000;
|
|
8
|
-
const TEN_MINUTES_MS = 10 * 60_000;
|
|
9
9
|
const MAX_BLOCKER_CHALLENGES = 3;
|
|
10
10
|
const MAX_CHALLENGE_ITEMS = 5;
|
|
11
11
|
const MAX_CHALLENGE_TEXT = 500;
|
|
12
|
+
const MAX_TIMEOUT_FALLBACKS = 3;
|
|
13
|
+
const MAX_TIMEOUT_FALLBACK_EVIDENCE = 8;
|
|
14
|
+
const TIMEOUT_FALLBACK_SOURCES = [
|
|
15
|
+
"implement",
|
|
16
|
+
"planVerification",
|
|
17
|
+
"verify",
|
|
18
|
+
"fix",
|
|
19
|
+
"publish",
|
|
20
|
+
"addressP2",
|
|
21
|
+
"verifyP2",
|
|
22
|
+
"inspectComments",
|
|
23
|
+
"inspectCi",
|
|
24
|
+
"opportunisticTest",
|
|
25
|
+
"finalizeDelivery",
|
|
26
|
+
];
|
|
27
|
+
const WORK_ATTEMPT_NODES = ["implement", "fix", "addressP2"];
|
|
28
|
+
const TIMEOUT_FALLBACK_ROUTES = {
|
|
29
|
+
implement: ["retry", "replan", "blocked"],
|
|
30
|
+
planVerification: ["retry", "verify", "replan", "blocked"],
|
|
31
|
+
verify: ["retry", "verify", "replan", "blocked"],
|
|
32
|
+
fix: ["retry", "replan", "blocked"],
|
|
33
|
+
publish: ["retry", "replan", "blocked"],
|
|
34
|
+
addressP2: ["retry", "replan", "blocked"],
|
|
35
|
+
verifyP2: ["retry", "replan", "blocked"],
|
|
36
|
+
inspectComments: ["retry", "review", "ci", "replan", "blocked"],
|
|
37
|
+
inspectCi: ["retry", "ci", "deliver", "replan", "blocked"],
|
|
38
|
+
opportunisticTest: ["retry", "ci", "deliver", "replan", "blocked"],
|
|
39
|
+
finalizeDelivery: ["retry", "deliver", "replan", "blocked"],
|
|
40
|
+
};
|
|
12
41
|
function requireRecord(value, label) {
|
|
13
42
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
14
43
|
throw new Error(`${label} must be an object`);
|
|
@@ -21,6 +50,12 @@ function requireString(value, label) {
|
|
|
21
50
|
}
|
|
22
51
|
return value.trim();
|
|
23
52
|
}
|
|
53
|
+
function requireStringArray(value, label) {
|
|
54
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
55
|
+
throw new Error(`${label} must be an array of strings`);
|
|
56
|
+
}
|
|
57
|
+
return [...value];
|
|
58
|
+
}
|
|
24
59
|
function boundedChallengeItems(value, label) {
|
|
25
60
|
if (!Array.isArray(value) || value.length > MAX_CHALLENGE_ITEMS) {
|
|
26
61
|
throw new Error(`${label} must be an array with at most ${MAX_CHALLENGE_ITEMS} items`);
|
|
@@ -33,6 +68,148 @@ function boundedChallengeItems(value, label) {
|
|
|
33
68
|
return text;
|
|
34
69
|
});
|
|
35
70
|
}
|
|
71
|
+
function isTimeoutFallbackSource(nodeId) {
|
|
72
|
+
return TIMEOUT_FALLBACK_SOURCES.includes(nodeId);
|
|
73
|
+
}
|
|
74
|
+
function latestTimedOutStep(context) {
|
|
75
|
+
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
76
|
+
const step = context.state.steps[index];
|
|
77
|
+
if (step?.outcome === "timed_out" && isTimeoutFallbackSource(step.nodeId))
|
|
78
|
+
return step;
|
|
79
|
+
}
|
|
80
|
+
throw new Error("No supported timed-out Autoimplement step is available");
|
|
81
|
+
}
|
|
82
|
+
function latestStepIndex(context, predicate) {
|
|
83
|
+
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
84
|
+
const step = context.state.steps[index];
|
|
85
|
+
if (step && predicate(step))
|
|
86
|
+
return index;
|
|
87
|
+
}
|
|
88
|
+
return -1;
|
|
89
|
+
}
|
|
90
|
+
function latestWorkAttemptIndex(context) {
|
|
91
|
+
return latestStepIndex(context, (step) => WORK_ATTEMPT_NODES.includes(step.nodeId));
|
|
92
|
+
}
|
|
93
|
+
function hasCurrentAcceptedWork(context) {
|
|
94
|
+
const index = latestWorkAttemptIndex(context);
|
|
95
|
+
return index >= 0 && context.state.steps[index]?.outcome === "ok";
|
|
96
|
+
}
|
|
97
|
+
function hasCurrentPublication(context) {
|
|
98
|
+
const workIndex = latestWorkAttemptIndex(context);
|
|
99
|
+
const publicationIndex = latestStepIndex(context, (step) => {
|
|
100
|
+
if (step.outcome !== "ok")
|
|
101
|
+
return false;
|
|
102
|
+
if (step.nodeId === "publish")
|
|
103
|
+
return true;
|
|
104
|
+
if (step.nodeId !== "verifyP2")
|
|
105
|
+
return false;
|
|
106
|
+
const output = step.output;
|
|
107
|
+
return output?.passed === true;
|
|
108
|
+
});
|
|
109
|
+
return publicationIndex > workIndex;
|
|
110
|
+
}
|
|
111
|
+
function parseTimeoutFallback(value, context) {
|
|
112
|
+
const result = requireRecord(value, "timeout fallback");
|
|
113
|
+
const routes = [
|
|
114
|
+
"retry",
|
|
115
|
+
"verify",
|
|
116
|
+
"review",
|
|
117
|
+
"ci",
|
|
118
|
+
"deliver",
|
|
119
|
+
"replan",
|
|
120
|
+
"blocked",
|
|
121
|
+
];
|
|
122
|
+
if (!routes.includes(result.route)) {
|
|
123
|
+
throw new Error(`timeout fallback route must be one of ${routes.join(", ")}`);
|
|
124
|
+
}
|
|
125
|
+
const route = result.route;
|
|
126
|
+
const reason = requireString(result.reason, "timeout fallback reason");
|
|
127
|
+
const evidence = requireStringArray(result.evidence, "timeout fallback evidence").map((item, index) => requireString(item, `timeout fallback evidence[${index}]`));
|
|
128
|
+
if (evidence.length === 0 || evidence.length > MAX_TIMEOUT_FALLBACK_EVIDENCE) {
|
|
129
|
+
throw new Error(`timeout fallback evidence must contain 1 through ${MAX_TIMEOUT_FALLBACK_EVIDENCE} items`);
|
|
130
|
+
}
|
|
131
|
+
const timedOut = latestTimedOutStep(context);
|
|
132
|
+
const source = timedOut.nodeId;
|
|
133
|
+
if (!TIMEOUT_FALLBACK_ROUTES[source].includes(route)) {
|
|
134
|
+
throw new Error(`timeout fallback route ${route} is not safe after timed-out ${source}`);
|
|
135
|
+
}
|
|
136
|
+
if (route === "verify" && !hasCurrentAcceptedWork(context)) {
|
|
137
|
+
throw new Error("timeout fallback cannot route to verification without current accepted work");
|
|
138
|
+
}
|
|
139
|
+
if (["review", "ci", "deliver"].includes(route) && !hasCurrentPublication(context)) {
|
|
140
|
+
throw new Error("timeout fallback cannot move past publication without a current published head");
|
|
141
|
+
}
|
|
142
|
+
if (route === "ci") {
|
|
143
|
+
const comments = context.outputs.inspectComments;
|
|
144
|
+
if (comments?.route !== "ci") {
|
|
145
|
+
throw new Error("timeout fallback cannot route to CI before comment inspection completed");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (route === "deliver") {
|
|
149
|
+
const ci = (context.outputs.assessTrackedCi ?? context.outputs.inspectCi);
|
|
150
|
+
const classification = context.outputs.classifyCi;
|
|
151
|
+
if (ci?.route !== "green" && classification?.route !== "unrelated") {
|
|
152
|
+
throw new Error("timeout fallback cannot route to delivery before CI is ready");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { route, reason, evidence };
|
|
156
|
+
}
|
|
157
|
+
function timeoutFallbackTarget(context) {
|
|
158
|
+
const fallback = context.outputs.timeoutFallback;
|
|
159
|
+
if (fallback.route !== "retry") {
|
|
160
|
+
const routes = {
|
|
161
|
+
verify: "planVerification",
|
|
162
|
+
review: "selectReviewCommands",
|
|
163
|
+
ci: "inspectCi",
|
|
164
|
+
deliver: "finalizeDelivery",
|
|
165
|
+
replan: "redesign",
|
|
166
|
+
blocked: "blocked",
|
|
167
|
+
};
|
|
168
|
+
return { route: routes[fallback.route] };
|
|
169
|
+
}
|
|
170
|
+
return { route: latestTimedOutStep(context).nodeId };
|
|
171
|
+
}
|
|
172
|
+
function timeoutFallbackGuard(context) {
|
|
173
|
+
const timeout = latestTimedOutStep(context);
|
|
174
|
+
const attempts = context.state.steps.filter((step) => step.nodeId === "timeoutFallback" && step.outcome === "ok").length;
|
|
175
|
+
if (attempts >= MAX_TIMEOUT_FALLBACKS) {
|
|
176
|
+
const timeouts = context.state.steps
|
|
177
|
+
.filter((step) => step.outcome === "timed_out" && isTimeoutFallbackSource(step.nodeId))
|
|
178
|
+
.map((step) => ({
|
|
179
|
+
nodeId: step.nodeId,
|
|
180
|
+
attemptId: step.attemptId,
|
|
181
|
+
error: step.error,
|
|
182
|
+
}));
|
|
183
|
+
return {
|
|
184
|
+
route: "blocked",
|
|
185
|
+
reason: `Autoimplement reached the ${MAX_TIMEOUT_FALLBACKS}-fallback timeout safety limit.`,
|
|
186
|
+
evidence: {
|
|
187
|
+
attempts,
|
|
188
|
+
limit: MAX_TIMEOUT_FALLBACKS,
|
|
189
|
+
timeouts,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
route: "recover",
|
|
195
|
+
attempt: attempts + 1,
|
|
196
|
+
limit: MAX_TIMEOUT_FALLBACKS,
|
|
197
|
+
timeout: {
|
|
198
|
+
nodeId: timeout.nodeId,
|
|
199
|
+
attemptId: timeout.attemptId,
|
|
200
|
+
error: timeout.error,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function throwLatestSupportedFailure(context) {
|
|
205
|
+
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
206
|
+
const step = context.state.steps[index];
|
|
207
|
+
if (step?.outcome === "failed" && isTimeoutFallbackSource(step.nodeId)) {
|
|
208
|
+
throw new Error(step.error ?? `Autoimplement node failed: ${step.nodeId}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
throw new Error("No supported failed Autoimplement step is available");
|
|
212
|
+
}
|
|
36
213
|
function parseBlockerChallenge(value) {
|
|
37
214
|
const result = requireRecord(value, "blocker challenge");
|
|
38
215
|
if (result.route !== "continue" && result.route !== "blocked") {
|
|
@@ -120,6 +297,7 @@ function parseInput(value) {
|
|
|
120
297
|
documents: [...raw.documents],
|
|
121
298
|
};
|
|
122
299
|
}
|
|
300
|
+
const concurrency = parseAutoimplementConcurrency(input.concurrency);
|
|
123
301
|
let approval;
|
|
124
302
|
if (input.approval !== undefined) {
|
|
125
303
|
const raw = requireRecord(input.approval, "autoimplement approval");
|
|
@@ -149,6 +327,7 @@ function parseInput(value) {
|
|
|
149
327
|
...(documents !== undefined ? { documents: [...documents] } : {}),
|
|
150
328
|
...(documentation !== undefined ? { documentation } : {}),
|
|
151
329
|
...(approval !== undefined ? { approval } : {}),
|
|
330
|
+
concurrency,
|
|
152
331
|
};
|
|
153
332
|
}
|
|
154
333
|
function parseExistingPlan(value) {
|
|
@@ -187,58 +366,50 @@ function parseRoute(value, routes, label) {
|
|
|
187
366
|
}
|
|
188
367
|
return { ...record, route: record.route };
|
|
189
368
|
}
|
|
190
|
-
function
|
|
191
|
-
|
|
192
|
-
if (command.command !== options.command) {
|
|
193
|
-
throw new Error(`${options.label} command must be ${options.command}`);
|
|
194
|
-
}
|
|
195
|
-
if (!Array.isArray(command.args) || command.args.some((arg) => typeof arg !== "string")) {
|
|
196
|
-
throw new Error(`${options.label} args must be an array of strings`);
|
|
197
|
-
}
|
|
198
|
-
const args = [...command.args];
|
|
199
|
-
if (!options.validateArgs(args))
|
|
200
|
-
throw new Error(`${options.label} args are not allowed`);
|
|
201
|
-
const cwd = requireString(command.cwd, `${options.label} cwd`);
|
|
202
|
-
if (!path.isAbsolute(cwd))
|
|
203
|
-
throw new Error(`${options.label} cwd must be absolute`);
|
|
204
|
-
const timeoutMs = command.timeoutMs;
|
|
205
|
-
if (typeof timeoutMs !== "number" ||
|
|
206
|
-
!Number.isInteger(timeoutMs) ||
|
|
207
|
-
timeoutMs <= 0 ||
|
|
208
|
-
timeoutMs > options.maxTimeoutMs) {
|
|
209
|
-
throw new Error(`${options.label} timeoutMs must be at most ${options.maxTimeoutMs}`);
|
|
210
|
-
}
|
|
211
|
-
return { command: options.command, args, cwd, timeoutMs };
|
|
369
|
+
function concurrency(context) {
|
|
370
|
+
return parseAutoimplementConcurrency(context.input.concurrency);
|
|
212
371
|
}
|
|
213
|
-
function
|
|
214
|
-
return
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
372
|
+
async function runAutoimplementBatch(context, kind, commands, maxConcurrency) {
|
|
373
|
+
return await runCommandBatch({ items: commands, maxConcurrency: Math.min(maxConcurrency, Math.max(1, commands.length)) }, {
|
|
374
|
+
signal: context.signal,
|
|
375
|
+
onItemSettled: async (result, completed, total) => {
|
|
376
|
+
if (context.signal.aborted)
|
|
377
|
+
return;
|
|
378
|
+
try {
|
|
379
|
+
await context.publishUpdate({
|
|
380
|
+
type: "command-batch.item",
|
|
381
|
+
key: `${kind}/${result.id}`,
|
|
382
|
+
data: {
|
|
383
|
+
schema: "pi-workflows.command-batch-item.v1",
|
|
384
|
+
batchKind: kind,
|
|
385
|
+
itemId: result.id,
|
|
386
|
+
outcome: result.outcome,
|
|
387
|
+
completed,
|
|
388
|
+
total,
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
if (!context.signal.aborted)
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
221
396
|
},
|
|
222
397
|
});
|
|
223
398
|
}
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
});
|
|
399
|
+
function commandBatchTimeoutMs(commands, maxConcurrency) {
|
|
400
|
+
if (commands.length === 0)
|
|
401
|
+
return 10_000;
|
|
402
|
+
const concurrency = Math.min(maxConcurrency, commands.length);
|
|
403
|
+
const waves = Math.ceil(commands.length / concurrency);
|
|
404
|
+
const longestItem = Math.max(...commands.map((command) => command.timeoutMs));
|
|
405
|
+
return waves * longestItem + 10_000;
|
|
232
406
|
}
|
|
233
|
-
function
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
allowNonZeroExit: true,
|
|
240
|
-
maxOutputChars: 1_000_000,
|
|
241
|
-
};
|
|
407
|
+
function reviewBatchNeedsRepair(result) {
|
|
408
|
+
return result.items.some((item) => item.outcome === "timedOut" ||
|
|
409
|
+
item.outcome === "cancelled" ||
|
|
410
|
+
(item.outcome === "failed" && item.exitCode === null) ||
|
|
411
|
+
item.stdoutTruncated ||
|
|
412
|
+
item.stderrTruncated);
|
|
242
413
|
}
|
|
243
414
|
function latestOutput(context, nodeIds) {
|
|
244
415
|
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
@@ -246,22 +417,11 @@ function latestOutput(context, nodeIds) {
|
|
|
246
417
|
if (step && nodeIds.includes(step.nodeId))
|
|
247
418
|
return step.output;
|
|
248
419
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
253
|
-
const step = context.state.steps[index];
|
|
254
|
-
if (!step)
|
|
255
|
-
continue;
|
|
256
|
-
if (step.nodeId === "repairCiCommand")
|
|
257
|
-
return step.output;
|
|
258
|
-
if (step.nodeId === "inspectCi") {
|
|
259
|
-
const output = step.output;
|
|
260
|
-
if (output.trackingCommand !== undefined)
|
|
261
|
-
return output.trackingCommand;
|
|
262
|
-
}
|
|
420
|
+
for (const nodeId of nodeIds) {
|
|
421
|
+
if (context.outputs[nodeId] !== undefined)
|
|
422
|
+
return context.outputs[nodeId];
|
|
263
423
|
}
|
|
264
|
-
throw new Error(
|
|
424
|
+
throw new Error(`No output found for ${nodeIds.join(" or ")}`);
|
|
265
425
|
}
|
|
266
426
|
function currentPlan(context) {
|
|
267
427
|
const adopted = context.outputs.adoptPlan;
|
|
@@ -305,6 +465,7 @@ function latestBlockerClaim(context) {
|
|
|
305
465
|
"assessTrackedCi",
|
|
306
466
|
"classifyCi",
|
|
307
467
|
"finalizeDelivery",
|
|
468
|
+
"timeoutFallback",
|
|
308
469
|
];
|
|
309
470
|
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
310
471
|
const step = context.state.steps[index];
|
|
@@ -337,6 +498,7 @@ function latestIssue(context) {
|
|
|
337
498
|
"triageReview",
|
|
338
499
|
"inspectComments",
|
|
339
500
|
"classifyCi",
|
|
501
|
+
"timeoutFallback",
|
|
340
502
|
"adoptPlan",
|
|
341
503
|
];
|
|
342
504
|
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
@@ -357,19 +519,277 @@ function parseFinding(value, severity) {
|
|
|
357
519
|
summary: requireString(finding.summary, `${severity} finding summary`),
|
|
358
520
|
};
|
|
359
521
|
}
|
|
360
|
-
function
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
522
|
+
function parseVerificationForContext(value, context) {
|
|
523
|
+
const plan = parseVerificationCommandPlan(value);
|
|
524
|
+
const implementation = latestOutput(context, ["implement"]);
|
|
525
|
+
const reported = Array.isArray(implementation.repositories)
|
|
526
|
+
? implementation.repositories.filter((entry) => typeof entry === "string")
|
|
527
|
+
: [];
|
|
528
|
+
const request = context.input;
|
|
529
|
+
const roots = new Set((reported.length > 0 ? reported : [request.repository ?? process.cwd()]).map((entry) => path.resolve(entry)));
|
|
530
|
+
for (const command of plan.commands) {
|
|
531
|
+
const cwd = path.resolve(command.cwd);
|
|
532
|
+
if (!roots.delete(cwd)) {
|
|
533
|
+
throw new Error(`verification command cwd was not reported by implementation: ${command.cwd}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (roots.size > 0) {
|
|
537
|
+
throw new Error(`verification plan is missing reported repositories: ${[...roots].join(", ")}`);
|
|
538
|
+
}
|
|
539
|
+
return plan;
|
|
540
|
+
}
|
|
541
|
+
function currentPublishedRepositories(context) {
|
|
542
|
+
return latestOutput(context, ["verifyP2", "publish"]);
|
|
543
|
+
}
|
|
544
|
+
function parseDeliveryRepository(value, index) {
|
|
545
|
+
const result = requireRecord(value, `delivery repositories[${index}]`);
|
|
546
|
+
const repository = requireString(result.repository, `delivery repositories[${index}].repository`);
|
|
547
|
+
if (!path.isAbsolute(repository)) {
|
|
548
|
+
throw new Error(`delivery repositories[${index}].repository must be absolute`);
|
|
549
|
+
}
|
|
550
|
+
if (typeof result.merged !== "boolean") {
|
|
551
|
+
throw new Error(`delivery repositories[${index}].merged must be a boolean`);
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
repository: path.resolve(repository),
|
|
555
|
+
pr: requireString(result.pr, `delivery repositories[${index}].pr`),
|
|
556
|
+
merged: result.merged,
|
|
557
|
+
reportComment: requireString(result.reportComment, `delivery repositories[${index}].reportComment`),
|
|
558
|
+
reason: requireString(result.reason, `delivery repositories[${index}].reason`),
|
|
367
559
|
};
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
560
|
+
}
|
|
561
|
+
function parseDeliveryResult(value, context) {
|
|
562
|
+
const result = requireRecord(value, "delivery result");
|
|
563
|
+
if (result.status !== "completed" && result.status !== "blocked") {
|
|
564
|
+
throw new Error("delivery status must be completed or blocked");
|
|
565
|
+
}
|
|
566
|
+
const request = context.input;
|
|
567
|
+
if (request.merge !== true && result.merged === true) {
|
|
568
|
+
throw new Error("delivery cannot merge without explicit merge: true");
|
|
569
|
+
}
|
|
570
|
+
if (result.status === "blocked")
|
|
571
|
+
return result;
|
|
572
|
+
if (typeof result.merged !== "boolean") {
|
|
573
|
+
throw new Error("completed delivery merged must be a boolean");
|
|
574
|
+
}
|
|
575
|
+
const pr = requireString(result.pr, "completed delivery pr");
|
|
576
|
+
const reportComment = requireString(result.reportComment, "completed delivery reportComment");
|
|
577
|
+
const reason = requireString(result.reason, "completed delivery reason");
|
|
578
|
+
const published = currentPublishedRepositories(context).repositories;
|
|
579
|
+
let repositories;
|
|
580
|
+
if (result.repositories === undefined) {
|
|
581
|
+
if (published.length !== 1) {
|
|
582
|
+
throw new Error("completed delivery repositories must cover every published repository");
|
|
583
|
+
}
|
|
584
|
+
const only = published[0];
|
|
585
|
+
if (only === undefined)
|
|
586
|
+
throw new Error("completed delivery has no published repository");
|
|
587
|
+
repositories = [
|
|
588
|
+
{
|
|
589
|
+
repository: path.resolve(only.repository),
|
|
590
|
+
pr,
|
|
591
|
+
merged: result.merged,
|
|
592
|
+
reportComment,
|
|
593
|
+
reason,
|
|
594
|
+
},
|
|
595
|
+
];
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
if (!Array.isArray(result.repositories)) {
|
|
599
|
+
throw new Error("completed delivery repositories must be an array");
|
|
600
|
+
}
|
|
601
|
+
repositories = result.repositories.map(parseDeliveryRepository);
|
|
602
|
+
}
|
|
603
|
+
const actual = new Map();
|
|
604
|
+
for (const repository of repositories) {
|
|
605
|
+
if (actual.has(repository.repository)) {
|
|
606
|
+
throw new Error(`completed delivery repository is duplicated: ${repository.repository}`);
|
|
607
|
+
}
|
|
608
|
+
actual.set(repository.repository, repository);
|
|
609
|
+
}
|
|
610
|
+
const mergeExpected = request.merge === true;
|
|
611
|
+
for (const expected of published) {
|
|
612
|
+
const repository = actual.get(path.resolve(expected.repository));
|
|
613
|
+
if (repository === undefined || repository.pr !== expected.pr) {
|
|
614
|
+
throw new Error(`completed delivery does not match published repository and PR: ${expected.repository}`);
|
|
615
|
+
}
|
|
616
|
+
if (repository.merged !== mergeExpected) {
|
|
617
|
+
throw new Error(`completed delivery merge result does not match merge policy: ${expected.repository}`);
|
|
618
|
+
}
|
|
619
|
+
actual.delete(repository.repository);
|
|
620
|
+
}
|
|
621
|
+
if (actual.size > 0) {
|
|
622
|
+
throw new Error(`completed delivery contains unpublished repositories: ${[...actual.keys()].join(", ")}`);
|
|
623
|
+
}
|
|
624
|
+
const firstPublished = published[0];
|
|
625
|
+
const first = firstPublished === undefined
|
|
626
|
+
? undefined
|
|
627
|
+
: repositories.find((repository) => repository.repository === path.resolve(firstPublished.repository));
|
|
628
|
+
if (first === undefined ||
|
|
629
|
+
first.pr !== pr ||
|
|
630
|
+
first.merged !== result.merged ||
|
|
631
|
+
first.reportComment !== reportComment) {
|
|
632
|
+
throw new Error("completed delivery top-level compatibility fields must match the first result");
|
|
633
|
+
}
|
|
634
|
+
return { status: "completed", merged: result.merged, pr, reportComment, reason, repositories };
|
|
635
|
+
}
|
|
636
|
+
function parseP2Verification(value, context) {
|
|
637
|
+
const result = requireRecord(value, "P2 verification");
|
|
638
|
+
if (typeof result.passed !== "boolean") {
|
|
639
|
+
throw new Error("P2 verification passed must be a boolean");
|
|
640
|
+
}
|
|
641
|
+
if (result.pushed !== true) {
|
|
642
|
+
throw new Error("P2 verification pushed must be true");
|
|
643
|
+
}
|
|
644
|
+
const refreshed = parsePublishedRepositories(result);
|
|
645
|
+
const previous = latestOutput(context, ["publish"]);
|
|
646
|
+
const expected = new Map(previous.repositories.map((repository) => [repository.id, repository]));
|
|
647
|
+
for (const repository of refreshed.repositories) {
|
|
648
|
+
const prior = expected.get(repository.id);
|
|
649
|
+
if (prior === undefined ||
|
|
650
|
+
prior.repository !== repository.repository ||
|
|
651
|
+
prior.branch !== repository.branch ||
|
|
652
|
+
prior.baseBranch !== repository.baseBranch ||
|
|
653
|
+
prior.pr !== repository.pr ||
|
|
654
|
+
prior.dependencyFingerprint !== repository.dependencyFingerprint) {
|
|
655
|
+
throw new Error(`P2 verification repository does not match publication: ${repository.id}`);
|
|
656
|
+
}
|
|
657
|
+
expected.delete(repository.id);
|
|
658
|
+
}
|
|
659
|
+
if (expected.size > 0) {
|
|
660
|
+
throw new Error(`P2 verification is missing repository ids: ${[...expected.keys()].join(", ")}`);
|
|
661
|
+
}
|
|
662
|
+
return { ...result, repositories: refreshed.repositories };
|
|
663
|
+
}
|
|
664
|
+
function parseCiInspectionForPublished(value, context) {
|
|
665
|
+
const inspected = parseCiInspectionBatch(value);
|
|
666
|
+
const published = currentPublishedRepositories(context);
|
|
667
|
+
const expected = new Map(published.repositories.map((repository) => [repository.id, repository]));
|
|
668
|
+
for (const target of inspected.targets) {
|
|
669
|
+
const repository = expected.get(target.id);
|
|
670
|
+
if (repository === undefined ||
|
|
671
|
+
repository.repository !== target.repository ||
|
|
672
|
+
repository.headRevision !== target.headRevision ||
|
|
673
|
+
repository.pr !== target.pr) {
|
|
674
|
+
throw new Error(`CI target does not match the published repository and head: ${target.id} (${JSON.stringify({ target, repository })})`);
|
|
675
|
+
}
|
|
676
|
+
expected.delete(target.id);
|
|
677
|
+
}
|
|
678
|
+
if (expected.size > 0) {
|
|
679
|
+
throw new Error(`CI inspection is missing repository ids: ${[...expected.keys()].join(", ")}`);
|
|
680
|
+
}
|
|
681
|
+
return inspected;
|
|
682
|
+
}
|
|
683
|
+
function parseTrackedCiAssessment(value, context) {
|
|
684
|
+
const result = requireRecord(value, "tracked CI assessment");
|
|
685
|
+
const inspected = latestOutput(context, ["inspectCi"]);
|
|
686
|
+
const execution = latestOutput(context, ["trackCi"]);
|
|
687
|
+
const expectedIds = execution.batch.items.map((item) => item.id);
|
|
688
|
+
if (!Array.isArray(result.targets)) {
|
|
689
|
+
throw new Error("tracked CI assessment targets must be an array");
|
|
690
|
+
}
|
|
691
|
+
const seen = new Set();
|
|
692
|
+
const targets = result.targets.map((entry, index) => {
|
|
693
|
+
const target = requireRecord(entry, `tracked CI assessment targets[${index}]`);
|
|
694
|
+
const id = requireString(target.id, `tracked CI assessment targets[${index}].id`);
|
|
695
|
+
if (seen.has(id))
|
|
696
|
+
throw new Error(`tracked CI assessment target is duplicated: ${id}`);
|
|
697
|
+
seen.add(id);
|
|
698
|
+
if (target.route !== "green" &&
|
|
699
|
+
target.route !== "failed" &&
|
|
700
|
+
target.route !== "pending" &&
|
|
701
|
+
target.route !== "unavailable") {
|
|
702
|
+
throw new Error(`tracked CI assessment targets[${index}].route is invalid`);
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
id,
|
|
706
|
+
route: target.route,
|
|
707
|
+
reason: requireString(target.reason, `tracked CI assessment targets[${index}].reason`),
|
|
708
|
+
};
|
|
709
|
+
});
|
|
710
|
+
const missing = expectedIds.filter((id) => !seen.has(id));
|
|
711
|
+
const unexpected = [...seen].filter((id) => !expectedIds.includes(id));
|
|
712
|
+
if (missing.length > 0 || unexpected.length > 0 || targets.length !== expectedIds.length) {
|
|
713
|
+
throw new Error(`tracked CI assessment targets must exactly cover watched ids; missing: ${missing.join(", ") || "none"}; unexpected: ${unexpected.join(", ") || "none"}`);
|
|
714
|
+
}
|
|
715
|
+
const trackedRoutes = new Map(targets.map((target) => [target.id, target.route]));
|
|
716
|
+
const routes = inspected.targets.map((target) => trackedRoutes.get(target.id) ?? target.route);
|
|
717
|
+
const route = routes.includes("failed")
|
|
718
|
+
? "failed"
|
|
719
|
+
: routes.includes("pending")
|
|
720
|
+
? "pending"
|
|
721
|
+
: routes.includes("unavailable")
|
|
722
|
+
? "unavailable"
|
|
723
|
+
: "green";
|
|
724
|
+
if (result.route !== route) {
|
|
725
|
+
throw new Error(`tracked CI assessment route must be ${route}`);
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
...result,
|
|
729
|
+
route,
|
|
730
|
+
reason: requireString(result.reason, "tracked CI assessment reason"),
|
|
731
|
+
targets,
|
|
732
|
+
relatedFailures: requireStringArray(result.relatedFailures ?? [], "tracked CI assessment relatedFailures"),
|
|
733
|
+
unrelatedFailures: requireStringArray(result.unrelatedFailures ?? [], "tracked CI assessment unrelatedFailures"),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function selectReviewCommands(context) {
|
|
737
|
+
const published = latestOutput(context, ["publish"]);
|
|
738
|
+
const reviewed = reviewRounds(context).flatMap((round) => round.repositories ?? []);
|
|
739
|
+
const repositories = published.repositories.filter((repository) => !reviewed.some((entry) => entry.id === repository.id &&
|
|
740
|
+
entry.headRevision === repository.headRevision &&
|
|
741
|
+
entry.dependencyFingerprint === repository.dependencyFingerprint &&
|
|
742
|
+
entry.invocationSucceeded));
|
|
743
|
+
return {
|
|
744
|
+
route: repositories.length === 0 ? "reuse" : "run",
|
|
745
|
+
repositories,
|
|
746
|
+
commands: repositories.map(reviewerCommand),
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function parseReviewAssessment(value, context) {
|
|
750
|
+
const review = requireRecord(value, "review assessment");
|
|
751
|
+
if (!Array.isArray(review.repositories)) {
|
|
752
|
+
throw new Error("review repositories must be an array");
|
|
753
|
+
}
|
|
754
|
+
const selected = latestOutput(context, ["selectReviewCommands"]);
|
|
755
|
+
const expected = new Map(selected.repositories.map((repository) => [repository.id, repository]));
|
|
756
|
+
const repositories = review.repositories.map((value, index) => {
|
|
757
|
+
const raw = requireRecord(value, `review repositories[${index}]`);
|
|
758
|
+
const id = requireString(raw.id, `review repositories[${index}].id`);
|
|
759
|
+
const published = expected.get(id);
|
|
760
|
+
if (published === undefined)
|
|
761
|
+
throw new Error(`review repository id was not in the batch: ${id}`);
|
|
762
|
+
expected.delete(id);
|
|
763
|
+
const parseList = (key, severity) => {
|
|
764
|
+
const list = raw[key];
|
|
765
|
+
if (!Array.isArray(list))
|
|
766
|
+
throw new Error(`review repositories[${index}].${key} must be an array`);
|
|
767
|
+
return list.map((item) => parseFinding(item, severity));
|
|
768
|
+
};
|
|
769
|
+
return {
|
|
770
|
+
id,
|
|
771
|
+
repository: published.repository,
|
|
772
|
+
baseBranch: published.baseBranch,
|
|
773
|
+
headRevision: published.headRevision,
|
|
774
|
+
...(published.dependencyFingerprint !== undefined
|
|
775
|
+
? { dependencyFingerprint: published.dependencyFingerprint }
|
|
776
|
+
: {}),
|
|
777
|
+
invocationSucceeded: raw.invocationSucceeded === true,
|
|
778
|
+
p0: parseList("p0", "P0"),
|
|
779
|
+
p1: parseList("p1", "P1"),
|
|
780
|
+
p2: parseList("p2", "P2"),
|
|
781
|
+
lower: parseList("lower", "lower"),
|
|
782
|
+
reason: requireString(raw.reason, `review repositories[${index}].reason`),
|
|
783
|
+
};
|
|
784
|
+
});
|
|
785
|
+
if (expected.size > 0) {
|
|
786
|
+
throw new Error(`review assessment is missing repository ids: ${[...expected.keys()].join(", ")}`);
|
|
787
|
+
}
|
|
788
|
+
const p0 = repositories.flatMap((entry) => entry.p0);
|
|
789
|
+
const p1 = repositories.flatMap((entry) => entry.p1);
|
|
790
|
+
const p2 = repositories.flatMap((entry) => entry.p2);
|
|
791
|
+
const lower = repositories.flatMap((entry) => entry.lower);
|
|
792
|
+
const invocationSucceeded = repositories.every((entry) => entry.invocationSucceeded);
|
|
373
793
|
const route = !invocationSucceeded
|
|
374
794
|
? "command_error"
|
|
375
795
|
: p0.length + p1.length > 0
|
|
@@ -385,6 +805,7 @@ function parseReviewAssessment(value) {
|
|
|
385
805
|
p2,
|
|
386
806
|
lower,
|
|
387
807
|
reason: requireString(review.reason, "review reason"),
|
|
808
|
+
repositories,
|
|
388
809
|
};
|
|
389
810
|
}
|
|
390
811
|
function reviewRounds(context) {
|
|
@@ -392,8 +813,37 @@ function reviewRounds(context) {
|
|
|
392
813
|
.filter((step) => step.nodeId === "assessReview" && step.outcome === "ok")
|
|
393
814
|
.map((step) => step.output);
|
|
394
815
|
}
|
|
816
|
+
function reviewRoundsForOutput(context) {
|
|
817
|
+
const rounds = reviewRounds(context);
|
|
818
|
+
const repositoryIds = new Set(rounds.flatMap((round) => (round.repositories ?? []).map((repository) => repository.id)));
|
|
819
|
+
if (repositoryIds.size > 1)
|
|
820
|
+
return rounds;
|
|
821
|
+
return rounds.map(({ repositories: _repositories, ...round }) => round);
|
|
822
|
+
}
|
|
823
|
+
function ciForOutput(context) {
|
|
824
|
+
const result = latestOutput(context, ["assessTrackedCi", "inspectCi"]);
|
|
825
|
+
const targets = result.targets;
|
|
826
|
+
if (!Array.isArray(targets) || targets.length !== 1)
|
|
827
|
+
return result;
|
|
828
|
+
const { targets: _targets, ...aggregate } = result;
|
|
829
|
+
if (result.route === "green" || result.route === "failed" || result.route === "unavailable") {
|
|
830
|
+
const target = targets[0];
|
|
831
|
+
if (target !== null && typeof target === "object" && !Array.isArray(target)) {
|
|
832
|
+
const record = target;
|
|
833
|
+
return {
|
|
834
|
+
...aggregate,
|
|
835
|
+
reason: aggregate.reason ?? record.reason,
|
|
836
|
+
relatedFailures: aggregate.relatedFailures ?? record.relatedFailures ?? [],
|
|
837
|
+
unrelatedFailures: aggregate.unrelatedFailures ?? record.unrelatedFailures ?? [],
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return aggregate;
|
|
842
|
+
}
|
|
395
843
|
function latestBlockedReason(context) {
|
|
396
844
|
const candidates = [
|
|
845
|
+
"timeoutFallbackGuard",
|
|
846
|
+
"timeoutFallback",
|
|
397
847
|
"challengeBlockerGuard",
|
|
398
848
|
"challengeBlocker",
|
|
399
849
|
"finalizeDelivery",
|
|
@@ -574,8 +1024,58 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
574
1024
|
: { route: "redesign", replans, limit };
|
|
575
1025
|
},
|
|
576
1026
|
}),
|
|
1027
|
+
timeoutFallbackGuard: compute({
|
|
1028
|
+
run: timeoutFallbackGuard,
|
|
1029
|
+
}),
|
|
1030
|
+
timeoutFallback: agent({
|
|
1031
|
+
timeoutMs: 30 * 60_000,
|
|
1032
|
+
statusDetail: "choosing a safe timeout fallback",
|
|
1033
|
+
prompt: (context) => {
|
|
1034
|
+
const request = context.input;
|
|
1035
|
+
const guard = context.outputs.timeoutFallbackGuard;
|
|
1036
|
+
const previousFallbacks = context.state.steps
|
|
1037
|
+
.filter((step) => step.nodeId === "timeoutFallback" && step.outcome === "ok")
|
|
1038
|
+
.map((step) => step.output);
|
|
1039
|
+
return [
|
|
1040
|
+
"A bounded Autoimplement agent step timed out. Choose the safest existing workflow stage to run next instead of ending the run blindly.",
|
|
1041
|
+
"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.",
|
|
1042
|
+
"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.",
|
|
1043
|
+
"Do not assume that the timed-out step failed or completed. Use accepted workflow outputs and durable repository or pull-request state.",
|
|
1044
|
+
"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.",
|
|
1045
|
+
"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.",
|
|
1046
|
+
"Do not skip required implementation, verification, review, CI, authorization, or delivery checks.",
|
|
1047
|
+
`Task: ${request.task}`,
|
|
1048
|
+
`Approved plan: ${JSON.stringify(currentPlan(context))}`,
|
|
1049
|
+
`Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
|
|
1050
|
+
`Timeout: ${JSON.stringify(guard)}`,
|
|
1051
|
+
`Accepted outputs: ${JSON.stringify(context.outputs)}`,
|
|
1052
|
+
`Previous fallback results: ${JSON.stringify(previousFallbacks)}`,
|
|
1053
|
+
`Recent workflow attempts: ${JSON.stringify(recentWorkflowAttempts(context))}`,
|
|
1054
|
+
].join("\n");
|
|
1055
|
+
},
|
|
1056
|
+
expectedOutput: `{ "route": "retry" | "verify" | "review" | "ci" | "deliver" | "replan" | "blocked", "reason": "why this is the safest next stage", "evidence": ["state inspected before choosing the route"] }`,
|
|
1057
|
+
validate: parseTimeoutFallback,
|
|
1058
|
+
}),
|
|
1059
|
+
routeTimeoutFallback: compute({
|
|
1060
|
+
run: timeoutFallbackTarget,
|
|
1061
|
+
}),
|
|
1062
|
+
propagateSupportedFailure: compute({
|
|
1063
|
+
run: throwLatestSupportedFailure,
|
|
1064
|
+
}),
|
|
1065
|
+
routeVerifyP2Result: compute({
|
|
1066
|
+
run: ({ outputs }) => outputs.verifyP2,
|
|
1067
|
+
}),
|
|
1068
|
+
routeInspectCommentsResult: compute({
|
|
1069
|
+
run: ({ outputs }) => outputs.inspectComments,
|
|
1070
|
+
}),
|
|
1071
|
+
routeInspectCiResult: compute({
|
|
1072
|
+
run: ({ outputs }) => outputs.inspectCi,
|
|
1073
|
+
}),
|
|
1074
|
+
routeFinalizeDeliveryResult: compute({
|
|
1075
|
+
run: ({ outputs }) => outputs.finalizeDelivery,
|
|
1076
|
+
}),
|
|
577
1077
|
implement: agent({
|
|
578
|
-
timeoutMs: 60 * 60_000,
|
|
1078
|
+
timeoutMs: 8 * 60 * 60_000,
|
|
579
1079
|
statusDetail: "implementing",
|
|
580
1080
|
prompt: (context) => {
|
|
581
1081
|
const request = context.input;
|
|
@@ -584,12 +1084,14 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
584
1084
|
`Plan: ${JSON.stringify(currentPlan(context))}`,
|
|
585
1085
|
`Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
|
|
586
1086
|
`Constraints: ${JSON.stringify(request.constraints ?? [])}`,
|
|
1087
|
+
"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.",
|
|
587
1088
|
"Follow repository instructions and use the most elegant long-term production-ready implementation without unnecessary work.",
|
|
588
1089
|
"If implementation exposes a new design or scope problem, report it precisely instead of forcing the old plan.",
|
|
1090
|
+
"Report every changed repository as an absolute path so independent verification can be bounded safely.",
|
|
589
1091
|
"Do not merge yet.",
|
|
590
1092
|
].join("\n");
|
|
591
1093
|
},
|
|
592
|
-
expectedOutput: `{ "status": "implemented" | "issue" | "blocked", "summary": "work completed or issue", "files": ["changed file"], "issueKind": "design" | "implementation" | null, "evidence": "new evidence" }`,
|
|
1094
|
+
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" }`,
|
|
593
1095
|
validate: (value) => requireRecord(value, "implementation result"),
|
|
594
1096
|
}),
|
|
595
1097
|
classifyImplementation: agent({
|
|
@@ -650,14 +1152,39 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
650
1152
|
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"] }`,
|
|
651
1153
|
validate: parseBlockerChallenge,
|
|
652
1154
|
}),
|
|
653
|
-
|
|
654
|
-
timeoutMs:
|
|
655
|
-
statusDetail: "
|
|
1155
|
+
planVerification: agent({
|
|
1156
|
+
timeoutMs: 15 * 60_000,
|
|
1157
|
+
statusDetail: "planning independent verification commands",
|
|
656
1158
|
prompt: () => [
|
|
657
|
-
"
|
|
658
|
-
"
|
|
659
|
-
"
|
|
660
|
-
"
|
|
1159
|
+
"Select the required local verification commands for the implementation.",
|
|
1160
|
+
"Return one command per independent repository working directory.",
|
|
1161
|
+
"Use exact executables and argument arrays without shell wrappers, environment overrides, stdin, Git or GitHub mutations, package publication, deployment, merge, or release commands.",
|
|
1162
|
+
"Use absolute repository paths, explicit timeouts no longer than 2700000ms, and maxOutputChars no larger than 1000000.",
|
|
1163
|
+
"List checks that cannot run locally under untested.",
|
|
1164
|
+
].join("\n"),
|
|
1165
|
+
expectedOutput: `{ "commands": [{ "id": "stable-id", "command": "npm", "args": ["run", "check"], "cwd": "/absolute/repository", "timeoutMs": 2700000, "maxOutputChars": 1000000 }], "untested": ["remaining check"] }`,
|
|
1166
|
+
validate: parseVerificationForContext,
|
|
1167
|
+
}),
|
|
1168
|
+
runVerification: action({
|
|
1169
|
+
timeoutMs: (context) => {
|
|
1170
|
+
const plan = latestOutput(context, ["planVerification"]);
|
|
1171
|
+
return commandBatchTimeoutMs(plan.commands, concurrency(context).verification);
|
|
1172
|
+
},
|
|
1173
|
+
statusDetail: "running independent verification commands",
|
|
1174
|
+
run: async (context) => {
|
|
1175
|
+
const plan = latestOutput(context, ["planVerification"]);
|
|
1176
|
+
return await runAutoimplementBatch(context, "verification", plan.commands, concurrency(context).verification);
|
|
1177
|
+
},
|
|
1178
|
+
}),
|
|
1179
|
+
verify: agent({
|
|
1180
|
+
timeoutMs: 20 * 60_000,
|
|
1181
|
+
statusDetail: "assessing verification",
|
|
1182
|
+
prompt: (context) => [
|
|
1183
|
+
"Assess the completed local verification commands.",
|
|
1184
|
+
"Set passed true only when every required command succeeded without truncated output.",
|
|
1185
|
+
"Report exact command outcomes, failures, and checks that still need remote verification.",
|
|
1186
|
+
`Command plan: ${JSON.stringify(latestOutput(context, ["planVerification"]))}`,
|
|
1187
|
+
`Command results: ${JSON.stringify(latestOutput(context, ["runVerification"]))}`,
|
|
661
1188
|
].join("\n"),
|
|
662
1189
|
expectedOutput: `{ "passed": true | false, "commands": [{ "command": "exact command", "outcome": "result" }], "failures": ["failure"], "untested": ["remaining check"] }`,
|
|
663
1190
|
validate: (value) => requireRecord(value, "verification result"),
|
|
@@ -680,6 +1207,7 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
680
1207
|
statusDetail: "fixing",
|
|
681
1208
|
prompt: (context) => [
|
|
682
1209
|
"Fix the current implementation issue without expanding the approved design.",
|
|
1210
|
+
"Inspect the current diff and commits first. Continue any partial fix and change only work that is still missing.",
|
|
683
1211
|
`Issue: ${JSON.stringify(latestIssue(context))}`,
|
|
684
1212
|
`Current plan: ${JSON.stringify(currentPlan(context))}`,
|
|
685
1213
|
"Stop after the fix so verification can run again.",
|
|
@@ -694,79 +1222,61 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
694
1222
|
const request = input;
|
|
695
1223
|
return [
|
|
696
1224
|
"Commit and push the verified implementation before review.",
|
|
1225
|
+
"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.",
|
|
697
1226
|
"Use the existing implementation-plan PR when one exists. Otherwise open a PR and use the pr-description skill for its body.",
|
|
698
1227
|
"Inspect the complete public diff before every push or PR mutation.",
|
|
699
|
-
|
|
1228
|
+
"Report every repository that received a pushed pull request with its absolute repository path, branch, base branch, pushed head revision, and PR URL.",
|
|
1229
|
+
"Include dependencyFingerprint only when a declared dependency result is relevant to review reuse.",
|
|
1230
|
+
`Requested base branch: ${request.baseBranch ?? "discover each repository default branch"}.`,
|
|
700
1231
|
"Do not merge yet.",
|
|
701
1232
|
].join("\n");
|
|
702
1233
|
},
|
|
703
|
-
expectedOutput: `{ "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true }`,
|
|
704
|
-
validate:
|
|
1234
|
+
expectedOutput: `{ "repositories": [{ "repository": "/absolute/repository", "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true, "dependencyFingerprint": "optional digest" }] }`,
|
|
1235
|
+
validate: parsePublishedRepositories,
|
|
705
1236
|
}),
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
prompt: ({ outputs, input }) => {
|
|
709
|
-
const published = outputs.publish;
|
|
710
|
-
const request = input;
|
|
711
|
-
return [
|
|
712
|
-
"Write the exact Pi Reviewer command for the pushed branch.",
|
|
713
|
-
"The executable must be pi-reviewer. Use its configured model and thinking settings.",
|
|
714
|
-
"Use the repository base branch and an absolute repository working directory.",
|
|
715
|
-
"Set timeoutMs to at most 600000.",
|
|
716
|
-
`Published branch: ${JSON.stringify(published)}`,
|
|
717
|
-
`Repository hint: ${request.repository ?? "current repository"}`,
|
|
718
|
-
].join("\n");
|
|
719
|
-
},
|
|
720
|
-
expectedOutput: `{ "command": "pi-reviewer", "args": ["--base", "main"], "cwd": "/absolute/repository", "timeoutMs": 600000 }`,
|
|
721
|
-
validate: parseReviewerCommand,
|
|
722
|
-
}),
|
|
723
|
-
runReview: shell({
|
|
724
|
-
statusDetail: "running Pi Reviewer",
|
|
725
|
-
timeoutMs: TEN_MINUTES_MS + 10_000,
|
|
726
|
-
exec: (context) => commandExecution(latestOutput(context, ["authorReviewCommand", "repairReviewCommand"])),
|
|
1237
|
+
selectReviewCommands: compute({
|
|
1238
|
+
run: selectReviewCommands,
|
|
727
1239
|
}),
|
|
728
|
-
|
|
729
|
-
statusDetail: "
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
return
|
|
733
|
-
"The Pi Reviewer invocation failed. Diagnose the exact command, arguments, base branch, working directory, and error.",
|
|
734
|
-
"Write a corrected pi-reviewer command. Do not substitute codex review or another reviewer.",
|
|
735
|
-
"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.",
|
|
736
|
-
`Failed result: ${JSON.stringify(failed)}`,
|
|
737
|
-
].join("\n");
|
|
1240
|
+
runReview: action({
|
|
1241
|
+
statusDetail: "running pi-reviewer commands",
|
|
1242
|
+
timeoutMs: (context) => {
|
|
1243
|
+
const selected = latestOutput(context, ["selectReviewCommands"]);
|
|
1244
|
+
return commandBatchTimeoutMs(selected.commands, concurrency(context).reviewer);
|
|
738
1245
|
},
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
return {
|
|
744
|
-
route: "blocked",
|
|
745
|
-
reason: requireString(result.reason, "reviewer command blocker"),
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
if (result.route !== "retry")
|
|
749
|
-
throw new Error("reviewer command repair route must be retry or blocked");
|
|
750
|
-
return {
|
|
751
|
-
route: "retry",
|
|
752
|
-
...parseReviewerCommand(result),
|
|
753
|
-
reason: requireString(result.reason, "reviewer command repair reason"),
|
|
754
|
-
};
|
|
1246
|
+
run: async (context) => {
|
|
1247
|
+
const selected = latestOutput(context, ["selectReviewCommands"]);
|
|
1248
|
+
const batch = await runAutoimplementBatch(context, "review", selected.commands, concurrency(context).reviewer);
|
|
1249
|
+
return { route: reviewBatchNeedsRepair(batch) ? "repair" : "assess", batch };
|
|
755
1250
|
},
|
|
756
1251
|
}),
|
|
1252
|
+
repairReviewCommand: agent({
|
|
1253
|
+
statusDetail: "repairing reviewer prerequisites",
|
|
1254
|
+
prompt: (context) => [
|
|
1255
|
+
"One or more pi-reviewer commands failed, timed out, or returned truncated output.",
|
|
1256
|
+
"Diagnose and fix only local reviewer prerequisites or configuration that are in scope.",
|
|
1257
|
+
"Do not change the deterministic executable, base branch, or repository command shape, and do not substitute another reviewer.",
|
|
1258
|
+
"Choose retry only when the same commands can now produce complete reviews. Choose blocked when pi-reviewer or required configuration remains unavailable.",
|
|
1259
|
+
`Failed batch: ${JSON.stringify(context.outputs.runReview)}`,
|
|
1260
|
+
].join("\n"),
|
|
1261
|
+
expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis and action" }`,
|
|
1262
|
+
validate: (value) => parseRoute(value, ["retry", "blocked"], "reviewer command repair"),
|
|
1263
|
+
}),
|
|
757
1264
|
assessReview: agent({
|
|
758
1265
|
statusDetail: "assessing reviewer findings",
|
|
759
1266
|
prompt: (context) => {
|
|
760
|
-
const
|
|
1267
|
+
const selected = latestOutput(context, ["selectReviewCommands"]);
|
|
1268
|
+
const execution = latestOutput(context, ["runReview"]);
|
|
761
1269
|
return [
|
|
762
|
-
"Assess
|
|
763
|
-
"
|
|
764
|
-
"
|
|
1270
|
+
"Assess each completed pi-reviewer result separately.",
|
|
1271
|
+
"Return one repository entry for every selected command, using the exact repository id.",
|
|
1272
|
+
"Set invocationSucceeded false when a complete valid review was not produced.",
|
|
1273
|
+
"Record every finding under P0, P1, P2, or lower and mark it as design or implementation.",
|
|
765
1274
|
"Do not promote P2 findings to P1 merely to force another review round.",
|
|
766
|
-
`
|
|
1275
|
+
`Selected repositories: ${JSON.stringify(selected.repositories)}`,
|
|
1276
|
+
`Reviewer results: ${JSON.stringify(execution.batch)}`,
|
|
767
1277
|
].join("\n");
|
|
768
1278
|
},
|
|
769
|
-
expectedOutput: `{ "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }`,
|
|
1279
|
+
expectedOutput: `{ "repositories": [{ "id": "repository-id", "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }], "reason": "batch assessment" }`,
|
|
770
1280
|
validate: parseReviewAssessment,
|
|
771
1281
|
}),
|
|
772
1282
|
triageReview: compute({
|
|
@@ -785,7 +1295,8 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
785
1295
|
statusDetail: "addressing P2 findings",
|
|
786
1296
|
prompt: ({ outputs }) => [
|
|
787
1297
|
"Address valid P2 findings from the last review when the improvement is proportionate and in scope.",
|
|
788
|
-
"
|
|
1298
|
+
"Inspect the current diff and commits first. Do not repeat a P2 change that is already present.",
|
|
1299
|
+
"Do not rerun pi-reviewer solely because P2 work changes files. Verification will run once, then the workflow continues.",
|
|
789
1300
|
`Review: ${JSON.stringify(outputs.assessReview)}`,
|
|
790
1301
|
].join("\n"),
|
|
791
1302
|
expectedOutput: `{ "addressed": ["P2 change"], "skipped": [{ "finding": "finding", "reason": "why" }] }`,
|
|
@@ -796,19 +1307,22 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
796
1307
|
statusDetail: "verifying P2 changes",
|
|
797
1308
|
prompt: () => [
|
|
798
1309
|
"Run focused verification for the P2 changes and push the verified result.",
|
|
799
|
-
"
|
|
1310
|
+
"Inspect the local and remote heads first. Do not push again when the verified head is already remote.",
|
|
1311
|
+
"Do not run pi-reviewer again because the previous round had no P0 or P1 findings.",
|
|
1312
|
+
"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.",
|
|
800
1313
|
"Report exact commands and outcomes.",
|
|
801
1314
|
].join("\n"),
|
|
802
|
-
expectedOutput: `{ "passed": true | false, "commands": [{ "command": "command", "outcome": "result" }], "pushed": true }`,
|
|
803
|
-
validate:
|
|
1315
|
+
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" }] }`,
|
|
1316
|
+
validate: parseP2Verification,
|
|
804
1317
|
}),
|
|
805
1318
|
inspectComments: agent({
|
|
806
1319
|
timeoutMs: 20 * 60_000,
|
|
807
1320
|
statusDetail: "checking PR comments",
|
|
808
|
-
prompt: () => [
|
|
809
|
-
"Inspect current inline review comments and PR issue comments.",
|
|
810
|
-
"Reply to and resolve every comment. Ignore stale or irrelevant comments only after explaining why.",
|
|
811
|
-
"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.",
|
|
1321
|
+
prompt: (context) => [
|
|
1322
|
+
"Inspect current inline review comments and PR issue comments for every published pull request.",
|
|
1323
|
+
"Handle pull requests one at a time. Reply to and resolve every comment. Ignore stale or irrelevant comments only after explaining why.",
|
|
1324
|
+
"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.",
|
|
1325
|
+
`Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
|
|
812
1326
|
].join("\n"),
|
|
813
1327
|
expectedOutput: `{ "route": "redesign" | "fix" | "ci" | "blocked", "summary": "comment status", "evidence": ["comment or response"] }`,
|
|
814
1328
|
validate: (value) => parseRoute(value, ["redesign", "fix", "ci", "blocked"], "PR comment assessment"),
|
|
@@ -816,60 +1330,61 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
816
1330
|
inspectCi: agent({
|
|
817
1331
|
timeoutMs: 10 * 60_000,
|
|
818
1332
|
statusDetail: "checking CI",
|
|
819
|
-
prompt: () => [
|
|
820
|
-
"Inspect
|
|
821
|
-
"
|
|
822
|
-
"
|
|
823
|
-
"
|
|
1333
|
+
prompt: (context) => [
|
|
1334
|
+
"Inspect every published pull request once without waiting for completion.",
|
|
1335
|
+
"Return one target per repository and current PR head.",
|
|
1336
|
+
"Choose green, failed, pending, or unavailable for each target.",
|
|
1337
|
+
"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.",
|
|
1338
|
+
"Separate failures caused by this change from unrelated failures. Do not invent an ETA.",
|
|
1339
|
+
`Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
|
|
824
1340
|
].join("\n"),
|
|
825
|
-
expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "relatedFailures": ["failure"], "unrelatedFailures": ["failure"], "trackingCommand": { "command": "gh", "args": ["pr", "checks", "--watch"], "cwd": "/absolute/repository", "timeoutMs": 300000
|
|
826
|
-
validate:
|
|
827
|
-
const result = parseRoute(value, ["green", "failed", "pending", "unavailable"], "CI inspection");
|
|
828
|
-
if (result.route === "pending")
|
|
829
|
-
result.trackingCommand = parseCiCommand(result.trackingCommand);
|
|
830
|
-
return result;
|
|
831
|
-
},
|
|
1341
|
+
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 } }] }`,
|
|
1342
|
+
validate: parseCiInspectionForPublished,
|
|
832
1343
|
}),
|
|
833
|
-
trackCi:
|
|
834
|
-
statusDetail: "tracking CI
|
|
835
|
-
timeoutMs:
|
|
836
|
-
|
|
1344
|
+
trackCi: action({
|
|
1345
|
+
statusDetail: "tracking pending CI commands",
|
|
1346
|
+
timeoutMs: (context) => {
|
|
1347
|
+
const inspected = latestOutput(context, ["inspectCi"]);
|
|
1348
|
+
const commands = inspected.targets.flatMap((target) => target.trackingCommand === undefined ? [] : [target.trackingCommand]);
|
|
1349
|
+
return commandBatchTimeoutMs(commands, concurrency(context).ciWatch);
|
|
1350
|
+
},
|
|
1351
|
+
run: async (context) => {
|
|
1352
|
+
const inspected = latestOutput(context, ["inspectCi"]);
|
|
1353
|
+
const commands = inspected.targets.flatMap((target) => target.trackingCommand === undefined ? [] : [target.trackingCommand]);
|
|
1354
|
+
const batch = await runAutoimplementBatch(context, "ciWatch", commands, concurrency(context).ciWatch);
|
|
1355
|
+
const needsRepair = batch.items.some((item) => (item.outcome === "failed" && item.exitCode === null) ||
|
|
1356
|
+
item.stdoutTruncated ||
|
|
1357
|
+
item.stderrTruncated);
|
|
1358
|
+
return { route: needsRepair ? "repair" : "assess", batch };
|
|
1359
|
+
},
|
|
837
1360
|
}),
|
|
838
1361
|
repairCiCommand: agent({
|
|
839
|
-
statusDetail: "
|
|
1362
|
+
statusDetail: "repairing CI watch prerequisites",
|
|
840
1363
|
prompt: (context) => [
|
|
841
|
-
"
|
|
842
|
-
"
|
|
843
|
-
"
|
|
844
|
-
|
|
1364
|
+
"One or more supported CI watch commands failed or returned truncated output.",
|
|
1365
|
+
"Diagnose and fix only local gh prerequisites or authentication that are already authorized.",
|
|
1366
|
+
"Do not change the PR identity or substitute another command form.",
|
|
1367
|
+
"Choose retry only when the same validated commands can now provide useful status. Choose blocked otherwise.",
|
|
1368
|
+
`Failure: ${JSON.stringify(context.outputs.trackCi)}`,
|
|
845
1369
|
].join("\n"),
|
|
846
|
-
expectedOutput: `{ "route": "retry" | "blocked", "
|
|
847
|
-
validate: (value) =>
|
|
848
|
-
const result = requireRecord(value, "CI command repair");
|
|
849
|
-
if (result.route === "blocked") {
|
|
850
|
-
return { route: "blocked", reason: requireString(result.reason, "CI command blocker") };
|
|
851
|
-
}
|
|
852
|
-
if (result.route !== "retry")
|
|
853
|
-
throw new Error("CI command repair route must be retry or blocked");
|
|
854
|
-
return {
|
|
855
|
-
route: "retry",
|
|
856
|
-
...parseCiCommand(result),
|
|
857
|
-
reason: requireString(result.reason, "CI command repair reason"),
|
|
858
|
-
};
|
|
859
|
-
},
|
|
1370
|
+
expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis" }`,
|
|
1371
|
+
validate: (value) => parseRoute(value, ["retry", "blocked"], "CI command repair"),
|
|
860
1372
|
}),
|
|
861
1373
|
assessTrackedCi: agent({
|
|
862
1374
|
statusDetail: "assessing tracked CI",
|
|
863
1375
|
prompt: (context) => {
|
|
864
|
-
const
|
|
1376
|
+
const inspected = latestOutput(context, ["inspectCi"]);
|
|
1377
|
+
const execution = latestOutput(context, ["trackCi"]);
|
|
865
1378
|
return [
|
|
866
|
-
"Assess
|
|
867
|
-
"
|
|
868
|
-
|
|
1379
|
+
"Assess every CI watch result without starting another wait.",
|
|
1380
|
+
"Return one target result for every watched PR and an aggregate route of green, failed, pending, or unavailable.",
|
|
1381
|
+
"A timed-out watch normally remains pending. Separate related from unrelated failures. Do not invent an ETA.",
|
|
1382
|
+
`Initial inspection: ${JSON.stringify(inspected)}`,
|
|
1383
|
+
`Tracking results: ${JSON.stringify(execution.batch)}`,
|
|
869
1384
|
].join("\n");
|
|
870
1385
|
},
|
|
871
|
-
expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "relatedFailures": ["failure"], "unrelatedFailures": ["failure"] }`,
|
|
872
|
-
validate:
|
|
1386
|
+
expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "targets": [{ "id": "repository-id", "route": "green" | "failed" | "pending" | "unavailable", "reason": "status" }], "relatedFailures": ["failure"], "unrelatedFailures": ["failure"] }`,
|
|
1387
|
+
validate: parseTrackedCiAssessment,
|
|
873
1388
|
}),
|
|
874
1389
|
opportunisticTest: agent({
|
|
875
1390
|
timeoutMs: 30 * 60_000,
|
|
@@ -895,30 +1410,23 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
895
1410
|
}),
|
|
896
1411
|
finalizeDelivery: agent({
|
|
897
1412
|
timeoutMs: 30 * 60_000,
|
|
898
|
-
statusDetail: "finalizing
|
|
899
|
-
prompt: (
|
|
900
|
-
const request = input;
|
|
1413
|
+
statusDetail: "finalizing PRs",
|
|
1414
|
+
prompt: (context) => {
|
|
1415
|
+
const request = context.input;
|
|
901
1416
|
return [
|
|
902
1417
|
request.merge === false
|
|
903
|
-
? "Leave
|
|
904
|
-
: "
|
|
905
|
-
"
|
|
906
|
-
"
|
|
1418
|
+
? "Leave every verified PR ready without merging because input disabled merge."
|
|
1419
|
+
: "Handle verified PRs one at a time and merge each unless repository policy or explicit user instructions prohibit it.",
|
|
1420
|
+
"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.",
|
|
1421
|
+
"Use each repository's required merge method.",
|
|
1422
|
+
"Post a final report with the implementation summary and exact validation commands on every PR only when that report is missing.",
|
|
1423
|
+
"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.",
|
|
907
1424
|
"Return blocked instead of claiming completion when a required merge or report action fails.",
|
|
1425
|
+
`Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
|
|
908
1426
|
].join("\n");
|
|
909
1427
|
},
|
|
910
|
-
expectedOutput: `{ "status": "completed" | "blocked", "merged": true | false, "pr": "URL", "reportComment": "URL or summary", "reason": "result" }`,
|
|
911
|
-
validate:
|
|
912
|
-
const result = requireRecord(value, "delivery result");
|
|
913
|
-
if (result.status !== "completed" && result.status !== "blocked") {
|
|
914
|
-
throw new Error("delivery status must be completed or blocked");
|
|
915
|
-
}
|
|
916
|
-
const request = context.input;
|
|
917
|
-
if (request.merge !== true && result.merged === true) {
|
|
918
|
-
throw new Error("delivery cannot merge without explicit merge: true");
|
|
919
|
-
}
|
|
920
|
-
return result;
|
|
921
|
-
},
|
|
1428
|
+
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" }] }`,
|
|
1429
|
+
validate: parseDeliveryResult,
|
|
922
1430
|
}),
|
|
923
1431
|
blocked: compute({
|
|
924
1432
|
run: (context) => {
|
|
@@ -939,11 +1447,11 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
939
1447
|
status: "completed",
|
|
940
1448
|
task: request.task,
|
|
941
1449
|
plan: currentPlan(context),
|
|
942
|
-
implementation: context
|
|
943
|
-
verification: context
|
|
944
|
-
reviewRounds:
|
|
945
|
-
ci: context
|
|
946
|
-
delivery: context
|
|
1450
|
+
implementation: latestOutput(context, ["implement"]),
|
|
1451
|
+
verification: latestOutput(context, ["verifyP2", "verify"]),
|
|
1452
|
+
reviewRounds: reviewRoundsForOutput(context),
|
|
1453
|
+
ci: ciForOutput(context),
|
|
1454
|
+
delivery: latestOutput(context, ["finalizeDelivery"]),
|
|
947
1455
|
};
|
|
948
1456
|
},
|
|
949
1457
|
}),
|
|
@@ -986,13 +1494,51 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
986
1494
|
from: "replanGuard",
|
|
987
1495
|
switch: { on: "$.route", cases: { redesign: "redesign", blocked: "blocked" } },
|
|
988
1496
|
},
|
|
989
|
-
{ from: "implement", to: "classifyImplementation" },
|
|
990
1497
|
{
|
|
991
|
-
from: "
|
|
1498
|
+
from: "timeoutFallbackGuard",
|
|
1499
|
+
switch: { on: "$.route", cases: { recover: "timeoutFallback", blocked: "blocked" } },
|
|
1500
|
+
},
|
|
1501
|
+
{ from: "timeoutFallback", to: "routeTimeoutFallback" },
|
|
1502
|
+
{ from: "propagateSupportedFailure", to: "blocked" },
|
|
1503
|
+
{
|
|
1504
|
+
from: "routeTimeoutFallback",
|
|
992
1505
|
switch: {
|
|
993
1506
|
on: "$.route",
|
|
994
1507
|
cases: {
|
|
1508
|
+
implement: "implement",
|
|
1509
|
+
planVerification: "planVerification",
|
|
995
1510
|
verify: "verify",
|
|
1511
|
+
fix: "fix",
|
|
1512
|
+
publish: "publish",
|
|
1513
|
+
addressP2: "addressP2",
|
|
1514
|
+
verifyP2: "verifyP2",
|
|
1515
|
+
inspectComments: "inspectComments",
|
|
1516
|
+
inspectCi: "inspectCi",
|
|
1517
|
+
opportunisticTest: "opportunisticTest",
|
|
1518
|
+
finalizeDelivery: "finalizeDelivery",
|
|
1519
|
+
selectReviewCommands: "selectReviewCommands",
|
|
1520
|
+
redesign: "redesign",
|
|
1521
|
+
blocked: "blocked",
|
|
1522
|
+
},
|
|
1523
|
+
},
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
from: "implement",
|
|
1527
|
+
switch: {
|
|
1528
|
+
on: "$result.outcome",
|
|
1529
|
+
cases: {
|
|
1530
|
+
ok: "classifyImplementation",
|
|
1531
|
+
timed_out: "timeoutFallbackGuard",
|
|
1532
|
+
failed: "propagateSupportedFailure",
|
|
1533
|
+
},
|
|
1534
|
+
},
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
from: "classifyImplementation",
|
|
1538
|
+
switch: {
|
|
1539
|
+
on: "$.route",
|
|
1540
|
+
cases: {
|
|
1541
|
+
verify: "planVerification",
|
|
996
1542
|
redesign: "redesign",
|
|
997
1543
|
fix: "fix",
|
|
998
1544
|
blocked: "challengeBlockerGuard",
|
|
@@ -1007,7 +1553,29 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1007
1553
|
from: "challengeBlocker",
|
|
1008
1554
|
switch: { on: "$.route", cases: { continue: "redesign", blocked: "blocked" } },
|
|
1009
1555
|
},
|
|
1010
|
-
{
|
|
1556
|
+
{
|
|
1557
|
+
from: "planVerification",
|
|
1558
|
+
switch: {
|
|
1559
|
+
on: "$result.outcome",
|
|
1560
|
+
cases: {
|
|
1561
|
+
ok: "runVerification",
|
|
1562
|
+
timed_out: "timeoutFallbackGuard",
|
|
1563
|
+
failed: "propagateSupportedFailure",
|
|
1564
|
+
},
|
|
1565
|
+
},
|
|
1566
|
+
},
|
|
1567
|
+
{ from: "runVerification", to: "verify" },
|
|
1568
|
+
{
|
|
1569
|
+
from: "verify",
|
|
1570
|
+
switch: {
|
|
1571
|
+
on: "$result.outcome",
|
|
1572
|
+
cases: {
|
|
1573
|
+
ok: "classifyVerification",
|
|
1574
|
+
timed_out: "timeoutFallbackGuard",
|
|
1575
|
+
failed: "propagateSupportedFailure",
|
|
1576
|
+
},
|
|
1577
|
+
},
|
|
1578
|
+
},
|
|
1011
1579
|
{
|
|
1012
1580
|
from: "classifyVerification",
|
|
1013
1581
|
switch: {
|
|
@@ -1020,20 +1588,36 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1020
1588
|
},
|
|
1021
1589
|
},
|
|
1022
1590
|
},
|
|
1023
|
-
{ from: "fix", to: "verify" },
|
|
1024
|
-
{ from: "publish", to: "authorReviewCommand" },
|
|
1025
|
-
{ from: "authorReviewCommand", to: "runReview" },
|
|
1026
1591
|
{
|
|
1027
|
-
from: "
|
|
1592
|
+
from: "fix",
|
|
1593
|
+
switch: {
|
|
1594
|
+
on: "$result.outcome",
|
|
1595
|
+
cases: {
|
|
1596
|
+
ok: "planVerification",
|
|
1597
|
+
timed_out: "timeoutFallbackGuard",
|
|
1598
|
+
failed: "propagateSupportedFailure",
|
|
1599
|
+
},
|
|
1600
|
+
},
|
|
1601
|
+
},
|
|
1602
|
+
{
|
|
1603
|
+
from: "publish",
|
|
1028
1604
|
switch: {
|
|
1029
1605
|
on: "$result.outcome",
|
|
1030
1606
|
cases: {
|
|
1031
|
-
ok: "
|
|
1032
|
-
|
|
1033
|
-
|
|
1607
|
+
ok: "selectReviewCommands",
|
|
1608
|
+
timed_out: "timeoutFallbackGuard",
|
|
1609
|
+
failed: "propagateSupportedFailure",
|
|
1034
1610
|
},
|
|
1035
1611
|
},
|
|
1036
1612
|
},
|
|
1613
|
+
{
|
|
1614
|
+
from: "selectReviewCommands",
|
|
1615
|
+
switch: { on: "$.route", cases: { run: "runReview", reuse: "inspectComments" } },
|
|
1616
|
+
},
|
|
1617
|
+
{
|
|
1618
|
+
from: "runReview",
|
|
1619
|
+
switch: { on: "$.route", cases: { assess: "assessReview", repair: "repairReviewCommand" } },
|
|
1620
|
+
},
|
|
1037
1621
|
{
|
|
1038
1622
|
from: "repairReviewCommand",
|
|
1039
1623
|
switch: {
|
|
@@ -1057,13 +1641,45 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1057
1641
|
from: "triageReview",
|
|
1058
1642
|
switch: { on: "$.route", cases: { redesign: "redesign", fix: "fix" } },
|
|
1059
1643
|
},
|
|
1060
|
-
{
|
|
1644
|
+
{
|
|
1645
|
+
from: "addressP2",
|
|
1646
|
+
switch: {
|
|
1647
|
+
on: "$result.outcome",
|
|
1648
|
+
cases: {
|
|
1649
|
+
ok: "verifyP2",
|
|
1650
|
+
timed_out: "timeoutFallbackGuard",
|
|
1651
|
+
failed: "propagateSupportedFailure",
|
|
1652
|
+
},
|
|
1653
|
+
},
|
|
1654
|
+
},
|
|
1061
1655
|
{
|
|
1062
1656
|
from: "verifyP2",
|
|
1657
|
+
switch: {
|
|
1658
|
+
on: "$result.outcome",
|
|
1659
|
+
cases: {
|
|
1660
|
+
ok: "routeVerifyP2Result",
|
|
1661
|
+
timed_out: "timeoutFallbackGuard",
|
|
1662
|
+
failed: "propagateSupportedFailure",
|
|
1663
|
+
},
|
|
1664
|
+
},
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
from: "routeVerifyP2Result",
|
|
1063
1668
|
switch: { on: "$.passed", cases: { true: "inspectComments", false: "fix" } },
|
|
1064
1669
|
},
|
|
1065
1670
|
{
|
|
1066
1671
|
from: "inspectComments",
|
|
1672
|
+
switch: {
|
|
1673
|
+
on: "$result.outcome",
|
|
1674
|
+
cases: {
|
|
1675
|
+
ok: "routeInspectCommentsResult",
|
|
1676
|
+
timed_out: "timeoutFallbackGuard",
|
|
1677
|
+
failed: "propagateSupportedFailure",
|
|
1678
|
+
},
|
|
1679
|
+
},
|
|
1680
|
+
},
|
|
1681
|
+
{
|
|
1682
|
+
from: "routeInspectCommentsResult",
|
|
1067
1683
|
switch: {
|
|
1068
1684
|
on: "$.route",
|
|
1069
1685
|
cases: {
|
|
@@ -1076,6 +1692,17 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1076
1692
|
},
|
|
1077
1693
|
{
|
|
1078
1694
|
from: "inspectCi",
|
|
1695
|
+
switch: {
|
|
1696
|
+
on: "$result.outcome",
|
|
1697
|
+
cases: {
|
|
1698
|
+
ok: "routeInspectCiResult",
|
|
1699
|
+
timed_out: "timeoutFallbackGuard",
|
|
1700
|
+
failed: "propagateSupportedFailure",
|
|
1701
|
+
},
|
|
1702
|
+
},
|
|
1703
|
+
},
|
|
1704
|
+
{
|
|
1705
|
+
from: "routeInspectCiResult",
|
|
1079
1706
|
switch: {
|
|
1080
1707
|
on: "$.route",
|
|
1081
1708
|
cases: {
|
|
@@ -1088,10 +1715,7 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1088
1715
|
},
|
|
1089
1716
|
{
|
|
1090
1717
|
from: "trackCi",
|
|
1091
|
-
switch: {
|
|
1092
|
-
on: "$result.outcome",
|
|
1093
|
-
cases: { ok: "assessTrackedCi", failed: "repairCiCommand", timed_out: "opportunisticTest" },
|
|
1094
|
-
},
|
|
1718
|
+
switch: { on: "$.route", cases: { assess: "assessTrackedCi", repair: "repairCiCommand" } },
|
|
1095
1719
|
},
|
|
1096
1720
|
{
|
|
1097
1721
|
from: "repairCiCommand",
|
|
@@ -1112,7 +1736,17 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1112
1736
|
},
|
|
1113
1737
|
},
|
|
1114
1738
|
},
|
|
1115
|
-
{
|
|
1739
|
+
{
|
|
1740
|
+
from: "opportunisticTest",
|
|
1741
|
+
switch: {
|
|
1742
|
+
on: "$result.outcome",
|
|
1743
|
+
cases: {
|
|
1744
|
+
ok: "inspectCi",
|
|
1745
|
+
timed_out: "timeoutFallbackGuard",
|
|
1746
|
+
failed: "propagateSupportedFailure",
|
|
1747
|
+
},
|
|
1748
|
+
},
|
|
1749
|
+
},
|
|
1116
1750
|
{
|
|
1117
1751
|
from: "classifyCi",
|
|
1118
1752
|
switch: {
|
|
@@ -1127,6 +1761,17 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1127
1761
|
},
|
|
1128
1762
|
{
|
|
1129
1763
|
from: "finalizeDelivery",
|
|
1764
|
+
switch: {
|
|
1765
|
+
on: "$result.outcome",
|
|
1766
|
+
cases: {
|
|
1767
|
+
ok: "routeFinalizeDeliveryResult",
|
|
1768
|
+
timed_out: "timeoutFallbackGuard",
|
|
1769
|
+
failed: "propagateSupportedFailure",
|
|
1770
|
+
},
|
|
1771
|
+
},
|
|
1772
|
+
},
|
|
1773
|
+
{
|
|
1774
|
+
from: "routeFinalizeDeliveryResult",
|
|
1130
1775
|
switch: {
|
|
1131
1776
|
on: "$.status",
|
|
1132
1777
|
cases: { completed: "finalize", blocked: "challengeBlockerGuard" },
|