@osolmaz/pi-workflows 0.11.2 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -10
- package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
- package/dist/builtins/autoimplement-command-batches.js +258 -0
- package/dist/builtins/autoimplement-command-batches.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +509 -124
- package/dist/builtins/autoimplement.workflow.js +884 -327
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +5 -3
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +4 -1
- package/dist/builtins/index.js +3 -1
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +2 -4
- package/dist/builtins/monitor.workflow.js +26 -128
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/pi-agent-group.d.ts +72 -0
- package/dist/builtins/pi-agent-group.js +1087 -0
- package/dist/builtins/pi-agent-group.js.map +1 -0
- package/dist/builtins/plan-approval.workflow.d.ts +39 -5
- package/dist/builtins/plan-approval.workflow.js +92 -14
- package/dist/builtins/plan-approval.workflow.js.map +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +301 -0
- package/dist/builtins/plan-change.workflow.js +256 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.js +2 -2
- package/dist/builtins/plan-presentation.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.d.ts +85 -0
- package/dist/builtins/sanity-check.workflow.js +482 -0
- package/dist/builtins/sanity-check.workflow.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +90 -2
- package/dist/controllers/sqlite.js +380 -5
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +2 -2
- package/dist/extension/decision-channels.js +29 -35
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
- package/dist/extension/deferred-turn-coordinator.js +143 -0
- package/dist/extension/deferred-turn-coordinator.js.map +1 -0
- package/dist/extension/deferred-turn.d.ts +44 -0
- package/dist/extension/deferred-turn.js +110 -0
- package/dist/extension/deferred-turn.js.map +1 -0
- package/dist/extension/index.js +348 -80
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -2
- package/dist/extension/step-message.d.ts +1 -0
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +23 -3
- package/dist/extension/widget.js.map +1 -1
- package/dist/herdr/setup.js +4 -4
- package/dist/render/graph-render.js +1 -2
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/render.js +7 -6
- package/dist/viewer/render.js.map +1 -1
- package/dist/workflows/catalog.js +7 -2
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/command-batch.d.ts +38 -0
- package/dist/workflows/command-batch.js +176 -0
- package/dist/workflows/command-batch.js.map +1 -0
- package/dist/workflows/composition.js +8 -0
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/decision-presentation.d.ts +1 -1
- package/dist/workflows/decision-presentation.js +51 -38
- package/dist/workflows/decision-presentation.js.map +1 -1
- package/dist/workflows/engine.d.ts +2 -2
- package/dist/workflows/engine.js +22 -17
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +13 -0
- package/dist/workflows/errors.js +15 -0
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +16 -4
- package/dist/workflows/human-decision.js +175 -72
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -2
- package/dist/workflows/index.js +2 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/progress.d.ts +1 -0
- package/dist/workflows/progress.js +15 -3
- package/dist/workflows/progress.js.map +1 -1
- package/dist/workflows/schema.js +12 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/shell.d.ts +4 -0
- package/dist/workflows/shell.js +6 -0
- package/dist/workflows/shell.js.map +1 -1
- package/dist/workflows/store.js +8 -1
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +37 -48
- package/docs/2026-08-18-herdr-piw-plan.md +1 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
- package/docs/CONTROLLERS.md +2 -2
- package/docs/DEFERRED_TURNS.md +298 -0
- package/docs/DESIGN_PHILOSOPHY.md +1 -1
- package/docs/HUMAN_DECISIONS.md +30 -40
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +17 -27
- package/docs/MONITOR.md +7 -13
- package/docs/WORKFLOW_COMPOSITION.md +10 -9
- package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
- package/docs/WORKFLOW_UPDATES.md +28 -5
- package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
- package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
- package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
- package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
- package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
- package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
- package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
- package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
- package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
- package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
- package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
- package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +283 -0
- package/docs/run-bundles.md +8 -6
- package/docs/workflows.md +85 -22
- package/examples/workflows/approved-plan.workflow.ts +19 -46
- package/examples/workflows/command-batch.workflow.ts +50 -0
- package/examples/workflows/sanity-check.workflow.ts +1 -0
- package/herdr-plugin.toml +3 -3
- package/package.json +7 -7
- package/schemas/decision-presentation-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v1.schema.json +16 -4
- package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
- package/schemas/human-decision-cancellation-v1.schema.json +1 -1
- package/schemas/human-decision-continuation-v1.schema.json +11 -2
- package/schemas/human-decision-delivery-v1.schema.json +9 -1
- package/schemas/human-decision-receipt-v1.schema.json +9 -1
- package/schemas/human-decision-request-v1.schema.json +25 -5
- package/schemas/human-decision-resolution-v1.schema.json +1 -1
- package/schemas/human-decision-settlement-v1.schema.json +1 -1
- package/skills/autodoc/SKILL.md +33 -2
- package/skills/autoimplement/SKILL.md +93 -12
- package/skills/autoplan/SKILL.md +26 -2
- package/skills/monitor/SKILL.md +66 -24
- package/skills/pi-workflows/SKILL.md +21 -6
- package/skills/sanity-check/SKILL.md +44 -0
- package/src/builtins/autoimplement-command-batches.ts +364 -0
- package/src/builtins/autoimplement.workflow.ts +1075 -370
- package/src/builtins/catalog.ts +5 -3
- package/src/builtins/index.ts +24 -0
- package/src/builtins/monitor.workflow.ts +27 -150
- package/src/builtins/pi-agent-group.ts +1407 -0
- package/src/builtins/plan-approval.workflow.ts +157 -24
- package/src/builtins/plan-change.workflow.ts +321 -0
- package/src/builtins/plan-presentation.ts +2 -2
- package/src/builtins/sanity-check.workflow.ts +769 -0
- package/src/controllers/sqlite.ts +580 -7
- package/src/extension/decision-channels.ts +36 -66
- package/src/extension/deferred-turn-coordinator.ts +171 -0
- package/src/extension/deferred-turn.ts +166 -0
- package/src/extension/index.ts +438 -90
- package/src/extension/session-events.ts +2 -2
- package/src/extension/step-message.ts +1 -0
- package/src/extension/widget.ts +24 -5
- package/src/herdr/setup.ts +4 -4
- package/src/render/graph-render.ts +1 -2
- package/src/viewer/render.ts +7 -6
- package/src/workflows/catalog.ts +7 -2
- package/src/workflows/command-batch.ts +254 -0
- package/src/workflows/composition.ts +9 -0
- package/src/workflows/decision-presentation.ts +56 -43
- package/src/workflows/engine.ts +25 -20
- package/src/workflows/errors.ts +24 -0
- package/src/workflows/human-decision.ts +218 -101
- package/src/workflows/index.ts +20 -11
- package/src/workflows/progress.ts +18 -3
- package/src/workflows/schema.ts +19 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +8 -1
- package/src/workflows/types.ts +43 -59
- package/schemas/human-decision-accepted-v2.schema.json +0 -50
- package/schemas/human-decision-delivery-v2.schema.json +0 -36
- package/schemas/human-decision-receipt-v2.schema.json +0 -39
- package/schemas/human-decision-request-v2.schema.json +0 -69
- package/schemas/human-decision-resolution-v2.schema.json +0 -27
|
@@ -1,21 +1,34 @@
|
|
|
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";
|
|
17
|
-
import
|
|
18
|
-
|
|
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";
|
|
30
|
+
import { parsePlanApprovalPolicy, type PlanApprovalPolicy } from "./plan-approval.workflow.js";
|
|
31
|
+
import planChangeWorkflow, { type NormalizedPlanChangeInput } from "./plan-change.workflow.js";
|
|
19
32
|
|
|
20
33
|
export type AutoimplementInput = {
|
|
21
34
|
task: string;
|
|
@@ -31,10 +44,8 @@ export type AutoimplementInput = {
|
|
|
31
44
|
planDigest: string;
|
|
32
45
|
documents: string[];
|
|
33
46
|
};
|
|
34
|
-
approval?:
|
|
35
|
-
|
|
36
|
-
maxReplans: number;
|
|
37
|
-
};
|
|
47
|
+
approval?: PlanApprovalPolicy;
|
|
48
|
+
concurrency?: Partial<AutoimplementConcurrency>;
|
|
38
49
|
};
|
|
39
50
|
|
|
40
51
|
export type ExistingPlanDiscovery = {
|
|
@@ -46,19 +57,26 @@ export type ExistingPlanDiscovery = {
|
|
|
46
57
|
evidence: unknown;
|
|
47
58
|
};
|
|
48
59
|
|
|
49
|
-
type StructuredCommand = {
|
|
50
|
-
command: string;
|
|
51
|
-
args: string[];
|
|
52
|
-
cwd: string;
|
|
53
|
-
timeoutMs: number;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
60
|
type ReviewFinding = {
|
|
57
61
|
severity: "P0" | "P1" | "P2" | "lower";
|
|
58
62
|
kind: "design" | "implementation";
|
|
59
63
|
summary: string;
|
|
60
64
|
};
|
|
61
65
|
|
|
66
|
+
type RepositoryReviewAssessment = {
|
|
67
|
+
id: string;
|
|
68
|
+
repository: string;
|
|
69
|
+
baseBranch: string;
|
|
70
|
+
headRevision: string;
|
|
71
|
+
dependencyFingerprint?: string;
|
|
72
|
+
invocationSucceeded: boolean;
|
|
73
|
+
p0: ReviewFinding[];
|
|
74
|
+
p1: ReviewFinding[];
|
|
75
|
+
p2: ReviewFinding[];
|
|
76
|
+
lower: ReviewFinding[];
|
|
77
|
+
reason: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
62
80
|
type ReviewAssessment = {
|
|
63
81
|
route: "critical" | "p2" | "clean" | "command_error";
|
|
64
82
|
invocationSucceeded: boolean;
|
|
@@ -67,6 +85,7 @@ type ReviewAssessment = {
|
|
|
67
85
|
p2: ReviewFinding[];
|
|
68
86
|
lower: ReviewFinding[];
|
|
69
87
|
reason: string;
|
|
88
|
+
repositories?: RepositoryReviewAssessment[];
|
|
70
89
|
};
|
|
71
90
|
|
|
72
91
|
export type AutoimplementCompleted = {
|
|
@@ -98,11 +117,49 @@ type BlockerChallenge = {
|
|
|
98
117
|
evidence: string[];
|
|
99
118
|
};
|
|
100
119
|
|
|
101
|
-
const FIVE_MINUTES_MS = 5 * 60_000;
|
|
102
|
-
const TEN_MINUTES_MS = 10 * 60_000;
|
|
103
120
|
const MAX_BLOCKER_CHALLENGES = 3;
|
|
104
121
|
const MAX_CHALLENGE_ITEMS = 5;
|
|
105
122
|
const MAX_CHALLENGE_TEXT = 500;
|
|
123
|
+
const MAX_TIMEOUT_FALLBACKS = 3;
|
|
124
|
+
const MAX_TIMEOUT_FALLBACK_EVIDENCE = 8;
|
|
125
|
+
const TIMEOUT_FALLBACK_SOURCES = [
|
|
126
|
+
"implement",
|
|
127
|
+
"planVerification",
|
|
128
|
+
"verify",
|
|
129
|
+
"fix",
|
|
130
|
+
"publish",
|
|
131
|
+
"addressP2",
|
|
132
|
+
"verifyP2",
|
|
133
|
+
"inspectComments",
|
|
134
|
+
"inspectCi",
|
|
135
|
+
"opportunisticTest",
|
|
136
|
+
"finalizeDelivery",
|
|
137
|
+
] as const;
|
|
138
|
+
|
|
139
|
+
const WORK_ATTEMPT_NODES = ["implement", "fix", "addressP2"] as const;
|
|
140
|
+
|
|
141
|
+
type TimeoutFallbackSource = (typeof TIMEOUT_FALLBACK_SOURCES)[number];
|
|
142
|
+
type TimeoutFallbackRoute = "retry" | "verify" | "review" | "ci" | "deliver" | "replan" | "blocked";
|
|
143
|
+
|
|
144
|
+
const TIMEOUT_FALLBACK_ROUTES: Record<TimeoutFallbackSource, readonly TimeoutFallbackRoute[]> = {
|
|
145
|
+
implement: ["retry", "replan", "blocked"],
|
|
146
|
+
planVerification: ["retry", "verify", "replan", "blocked"],
|
|
147
|
+
verify: ["retry", "verify", "replan", "blocked"],
|
|
148
|
+
fix: ["retry", "replan", "blocked"],
|
|
149
|
+
publish: ["retry", "replan", "blocked"],
|
|
150
|
+
addressP2: ["retry", "replan", "blocked"],
|
|
151
|
+
verifyP2: ["retry", "replan", "blocked"],
|
|
152
|
+
inspectComments: ["retry", "review", "ci", "replan", "blocked"],
|
|
153
|
+
inspectCi: ["retry", "ci", "deliver", "replan", "blocked"],
|
|
154
|
+
opportunisticTest: ["retry", "ci", "deliver", "replan", "blocked"],
|
|
155
|
+
finalizeDelivery: ["retry", "deliver", "replan", "blocked"],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
type TimeoutFallbackResult = {
|
|
159
|
+
route: TimeoutFallbackRoute;
|
|
160
|
+
reason: string;
|
|
161
|
+
evidence: string[];
|
|
162
|
+
};
|
|
106
163
|
|
|
107
164
|
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
108
165
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -118,6 +175,13 @@ function requireString(value: unknown, label: string): string {
|
|
|
118
175
|
return value.trim();
|
|
119
176
|
}
|
|
120
177
|
|
|
178
|
+
function requireStringArray(value: unknown, label: string): string[] {
|
|
179
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
180
|
+
throw new Error(`${label} must be an array of strings`);
|
|
181
|
+
}
|
|
182
|
+
return [...value] as string[];
|
|
183
|
+
}
|
|
184
|
+
|
|
121
185
|
function boundedChallengeItems(value: unknown, label: string): string[] {
|
|
122
186
|
if (!Array.isArray(value) || value.length > MAX_CHALLENGE_ITEMS) {
|
|
123
187
|
throw new Error(`${label} must be an array with at most ${MAX_CHALLENGE_ITEMS} items`);
|
|
@@ -131,6 +195,170 @@ function boundedChallengeItems(value: unknown, label: string): string[] {
|
|
|
131
195
|
});
|
|
132
196
|
}
|
|
133
197
|
|
|
198
|
+
function isTimeoutFallbackSource(nodeId: string): nodeId is TimeoutFallbackSource {
|
|
199
|
+
return (TIMEOUT_FALLBACK_SOURCES as readonly string[]).includes(nodeId);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function latestTimedOutStep(context: WorkflowNodeContext) {
|
|
203
|
+
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
204
|
+
const step = context.state.steps[index];
|
|
205
|
+
if (step?.outcome === "timed_out" && isTimeoutFallbackSource(step.nodeId)) return step;
|
|
206
|
+
}
|
|
207
|
+
throw new Error("No supported timed-out Autoimplement step is available");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function latestStepIndex(
|
|
211
|
+
context: WorkflowNodeContext,
|
|
212
|
+
predicate: (step: WorkflowNodeContext["state"]["steps"][number]) => boolean,
|
|
213
|
+
): number {
|
|
214
|
+
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
215
|
+
const step = context.state.steps[index];
|
|
216
|
+
if (step && predicate(step)) return index;
|
|
217
|
+
}
|
|
218
|
+
return -1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function latestWorkAttemptIndex(context: WorkflowNodeContext): number {
|
|
222
|
+
return latestStepIndex(context, (step) =>
|
|
223
|
+
(WORK_ATTEMPT_NODES as readonly string[]).includes(step.nodeId),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function hasCurrentAcceptedWork(context: WorkflowNodeContext): boolean {
|
|
228
|
+
const index = latestWorkAttemptIndex(context);
|
|
229
|
+
return index >= 0 && context.state.steps[index]?.outcome === "ok";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function hasCurrentPublication(context: WorkflowNodeContext): boolean {
|
|
233
|
+
const workIndex = latestWorkAttemptIndex(context);
|
|
234
|
+
const publicationIndex = latestStepIndex(context, (step) => {
|
|
235
|
+
if (step.outcome !== "ok") return false;
|
|
236
|
+
if (step.nodeId === "publish") return true;
|
|
237
|
+
if (step.nodeId !== "verifyP2") return false;
|
|
238
|
+
const output = step.output as { passed?: unknown } | null;
|
|
239
|
+
return output?.passed === true;
|
|
240
|
+
});
|
|
241
|
+
return publicationIndex > workIndex;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseTimeoutFallback(value: unknown, context: WorkflowNodeContext): TimeoutFallbackResult {
|
|
245
|
+
const result = requireRecord(value, "timeout fallback");
|
|
246
|
+
const routes: TimeoutFallbackRoute[] = [
|
|
247
|
+
"retry",
|
|
248
|
+
"verify",
|
|
249
|
+
"review",
|
|
250
|
+
"ci",
|
|
251
|
+
"deliver",
|
|
252
|
+
"replan",
|
|
253
|
+
"blocked",
|
|
254
|
+
];
|
|
255
|
+
if (!routes.includes(result.route as TimeoutFallbackRoute)) {
|
|
256
|
+
throw new Error(`timeout fallback route must be one of ${routes.join(", ")}`);
|
|
257
|
+
}
|
|
258
|
+
const route = result.route as TimeoutFallbackRoute;
|
|
259
|
+
const reason = requireString(result.reason, "timeout fallback reason");
|
|
260
|
+
const evidence = requireStringArray(result.evidence, "timeout fallback evidence").map(
|
|
261
|
+
(item, index) => requireString(item, `timeout fallback evidence[${index}]`),
|
|
262
|
+
);
|
|
263
|
+
if (evidence.length === 0 || evidence.length > MAX_TIMEOUT_FALLBACK_EVIDENCE) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`timeout fallback evidence must contain 1 through ${MAX_TIMEOUT_FALLBACK_EVIDENCE} items`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const timedOut = latestTimedOutStep(context);
|
|
270
|
+
const source = timedOut.nodeId as TimeoutFallbackSource;
|
|
271
|
+
if (!TIMEOUT_FALLBACK_ROUTES[source].includes(route)) {
|
|
272
|
+
throw new Error(`timeout fallback route ${route} is not safe after timed-out ${source}`);
|
|
273
|
+
}
|
|
274
|
+
if (route === "verify" && !hasCurrentAcceptedWork(context)) {
|
|
275
|
+
throw new Error("timeout fallback cannot route to verification without current accepted work");
|
|
276
|
+
}
|
|
277
|
+
if (["review", "ci", "deliver"].includes(route) && !hasCurrentPublication(context)) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
"timeout fallback cannot move past publication without a current published head",
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (route === "ci") {
|
|
283
|
+
const comments = context.outputs.inspectComments as { route?: unknown } | undefined;
|
|
284
|
+
if (comments?.route !== "ci") {
|
|
285
|
+
throw new Error("timeout fallback cannot route to CI before comment inspection completed");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (route === "deliver") {
|
|
289
|
+
const ci = (context.outputs.assessTrackedCi ?? context.outputs.inspectCi) as
|
|
290
|
+
| { route?: unknown }
|
|
291
|
+
| undefined;
|
|
292
|
+
const classification = context.outputs.classifyCi as { route?: unknown } | undefined;
|
|
293
|
+
if (ci?.route !== "green" && classification?.route !== "unrelated") {
|
|
294
|
+
throw new Error("timeout fallback cannot route to delivery before CI is ready");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { route, reason, evidence };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function timeoutFallbackTarget(context: WorkflowNodeContext): { route: string } {
|
|
302
|
+
const fallback = context.outputs.timeoutFallback as TimeoutFallbackResult;
|
|
303
|
+
if (fallback.route !== "retry") {
|
|
304
|
+
const routes: Record<Exclude<TimeoutFallbackRoute, "retry">, string> = {
|
|
305
|
+
verify: "planVerification",
|
|
306
|
+
review: "selectReviewCommands",
|
|
307
|
+
ci: "inspectCi",
|
|
308
|
+
deliver: "finalizeDelivery",
|
|
309
|
+
replan: "redesign",
|
|
310
|
+
blocked: "blocked",
|
|
311
|
+
};
|
|
312
|
+
return { route: routes[fallback.route] };
|
|
313
|
+
}
|
|
314
|
+
return { route: latestTimedOutStep(context).nodeId };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function timeoutFallbackGuard(context: WorkflowNodeContext) {
|
|
318
|
+
const timeout = latestTimedOutStep(context);
|
|
319
|
+
const attempts = context.state.steps.filter(
|
|
320
|
+
(step) => step.nodeId === "timeoutFallback" && step.outcome === "ok",
|
|
321
|
+
).length;
|
|
322
|
+
if (attempts >= MAX_TIMEOUT_FALLBACKS) {
|
|
323
|
+
const timeouts = context.state.steps
|
|
324
|
+
.filter((step) => step.outcome === "timed_out" && isTimeoutFallbackSource(step.nodeId))
|
|
325
|
+
.map((step) => ({
|
|
326
|
+
nodeId: step.nodeId,
|
|
327
|
+
attemptId: step.attemptId,
|
|
328
|
+
error: step.error,
|
|
329
|
+
}));
|
|
330
|
+
return {
|
|
331
|
+
route: "blocked",
|
|
332
|
+
reason: `Autoimplement reached the ${MAX_TIMEOUT_FALLBACKS}-fallback timeout safety limit.`,
|
|
333
|
+
evidence: {
|
|
334
|
+
attempts,
|
|
335
|
+
limit: MAX_TIMEOUT_FALLBACKS,
|
|
336
|
+
timeouts,
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
route: "recover",
|
|
342
|
+
attempt: attempts + 1,
|
|
343
|
+
limit: MAX_TIMEOUT_FALLBACKS,
|
|
344
|
+
timeout: {
|
|
345
|
+
nodeId: timeout.nodeId,
|
|
346
|
+
attemptId: timeout.attemptId,
|
|
347
|
+
error: timeout.error,
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function throwLatestSupportedFailure(context: WorkflowNodeContext): never {
|
|
353
|
+
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
354
|
+
const step = context.state.steps[index];
|
|
355
|
+
if (step?.outcome === "failed" && isTimeoutFallbackSource(step.nodeId)) {
|
|
356
|
+
throw new Error(step.error ?? `Autoimplement node failed: ${step.nodeId}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
throw new Error("No supported failed Autoimplement step is available");
|
|
360
|
+
}
|
|
361
|
+
|
|
134
362
|
function parseBlockerChallenge(value: unknown): BlockerChallenge {
|
|
135
363
|
const result = requireRecord(value, "blocker challenge");
|
|
136
364
|
if (result.route !== "continue" && result.route !== "blocked") {
|
|
@@ -233,22 +461,8 @@ function parseInput(value: unknown): AutoimplementInput {
|
|
|
233
461
|
documents: [...raw.documents] as string[],
|
|
234
462
|
};
|
|
235
463
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const raw = requireRecord(input.approval, "autoimplement approval");
|
|
239
|
-
const maxReplans = raw.maxReplans ?? 3;
|
|
240
|
-
if (
|
|
241
|
-
!Number.isInteger(maxReplans) ||
|
|
242
|
-
(maxReplans as number) < 1 ||
|
|
243
|
-
(maxReplans as number) > 20
|
|
244
|
-
) {
|
|
245
|
-
throw new Error("autoimplement approval maxReplans must be from 1 through 20");
|
|
246
|
-
}
|
|
247
|
-
approval = {
|
|
248
|
-
audience: requireString(raw.audience, "autoimplement approval audience"),
|
|
249
|
-
maxReplans: maxReplans as number,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
464
|
+
const concurrency = parseAutoimplementConcurrency(input.concurrency);
|
|
465
|
+
const approval = parsePlanApprovalPolicy(input.approval);
|
|
252
466
|
return {
|
|
253
467
|
task: requireString(input.task, "autoimplement task"),
|
|
254
468
|
...(input.plan !== undefined ? { plan: input.plan } : {}),
|
|
@@ -263,7 +477,8 @@ function parseInput(value: unknown): AutoimplementInput {
|
|
|
263
477
|
merge: input.merge === true,
|
|
264
478
|
...(documents !== undefined ? { documents: [...documents] as string[] } : {}),
|
|
265
479
|
...(documentation !== undefined ? { documentation } : {}),
|
|
266
|
-
|
|
480
|
+
approval,
|
|
481
|
+
concurrency,
|
|
267
482
|
};
|
|
268
483
|
}
|
|
269
484
|
|
|
@@ -312,70 +527,71 @@ function parseRoute<T extends string>(
|
|
|
312
527
|
return { ...record, route: record.route as T };
|
|
313
528
|
}
|
|
314
529
|
|
|
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 };
|
|
530
|
+
type ReviewCommandSelection = {
|
|
531
|
+
route: "run" | "reuse";
|
|
532
|
+
repositories: PublishedRepository[];
|
|
533
|
+
commands: CommandBatchItem[];
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
type BatchExecution = {
|
|
537
|
+
route: "assess" | "repair";
|
|
538
|
+
batch: CommandBatchResult;
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
function concurrency(context: WorkflowNodeContext): AutoimplementConcurrency {
|
|
542
|
+
return parseAutoimplementConcurrency((context.input as AutoimplementInput).concurrency);
|
|
345
543
|
}
|
|
346
544
|
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
545
|
+
async function runAutoimplementBatch(
|
|
546
|
+
context: WorkflowActionContext,
|
|
547
|
+
kind: "review" | "ciWatch" | "verification",
|
|
548
|
+
commands: CommandBatchItem[],
|
|
549
|
+
maxConcurrency: number,
|
|
550
|
+
): Promise<CommandBatchResult> {
|
|
551
|
+
return await runCommandBatch(
|
|
552
|
+
{ items: commands, maxConcurrency: Math.min(maxConcurrency, Math.max(1, commands.length)) },
|
|
553
|
+
{
|
|
554
|
+
signal: context.signal,
|
|
555
|
+
onItemSettled: async (result, completed, total) => {
|
|
556
|
+
if (context.signal.aborted) return;
|
|
557
|
+
try {
|
|
558
|
+
await context.publishUpdate({
|
|
559
|
+
type: "command-batch.item",
|
|
560
|
+
key: `${kind}/${result.id}`,
|
|
561
|
+
data: {
|
|
562
|
+
schema: "pi-workflows.command-batch-item.v1",
|
|
563
|
+
batchKind: kind,
|
|
564
|
+
itemId: result.id,
|
|
565
|
+
outcome: result.outcome,
|
|
566
|
+
completed,
|
|
567
|
+
total,
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (!context.signal.aborted) throw error;
|
|
572
|
+
}
|
|
573
|
+
},
|
|
355
574
|
},
|
|
356
|
-
|
|
575
|
+
);
|
|
357
576
|
}
|
|
358
577
|
|
|
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
|
-
});
|
|
578
|
+
function commandBatchTimeoutMs(commands: CommandBatchItem[], maxConcurrency: number): number {
|
|
579
|
+
if (commands.length === 0) return 10_000;
|
|
580
|
+
const concurrency = Math.min(maxConcurrency, commands.length);
|
|
581
|
+
const waves = Math.ceil(commands.length / concurrency);
|
|
582
|
+
const longestItem = Math.max(...commands.map((command) => command.timeoutMs));
|
|
583
|
+
return waves * longestItem + 10_000;
|
|
368
584
|
}
|
|
369
585
|
|
|
370
|
-
function
|
|
371
|
-
return
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
586
|
+
function reviewBatchNeedsRepair(result: CommandBatchResult): boolean {
|
|
587
|
+
return result.items.some(
|
|
588
|
+
(item) =>
|
|
589
|
+
item.outcome === "timedOut" ||
|
|
590
|
+
item.outcome === "cancelled" ||
|
|
591
|
+
(item.outcome === "failed" && item.exitCode === null) ||
|
|
592
|
+
item.stdoutTruncated ||
|
|
593
|
+
item.stderrTruncated,
|
|
594
|
+
);
|
|
379
595
|
}
|
|
380
596
|
|
|
381
597
|
function latestOutput<T>(context: WorkflowNodeContext, nodeIds: string[]): T {
|
|
@@ -383,20 +599,10 @@ function latestOutput<T>(context: WorkflowNodeContext, nodeIds: string[]): T {
|
|
|
383
599
|
const step = context.state.steps[index];
|
|
384
600
|
if (step && nodeIds.includes(step.nodeId)) return step.output as T;
|
|
385
601
|
}
|
|
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
|
-
}
|
|
602
|
+
for (const nodeId of nodeIds) {
|
|
603
|
+
if (context.outputs[nodeId] !== undefined) return context.outputs[nodeId] as T;
|
|
398
604
|
}
|
|
399
|
-
throw new Error(
|
|
605
|
+
throw new Error(`No output found for ${nodeIds.join(" or ")}`);
|
|
400
606
|
}
|
|
401
607
|
|
|
402
608
|
function currentPlan(context: WorkflowNodeContext): unknown {
|
|
@@ -413,20 +619,6 @@ function currentPlan(context: WorkflowNodeContext): unknown {
|
|
|
413
619
|
return (context.input as AutoimplementInput).plan;
|
|
414
620
|
}
|
|
415
621
|
|
|
416
|
-
function currentPlanDigest(context: WorkflowNodeContext): string {
|
|
417
|
-
const adopted = context.outputs.adoptPlan as { planDigest?: unknown } | undefined;
|
|
418
|
-
if (typeof adopted?.planDigest === "string") return adopted.planDigest;
|
|
419
|
-
const documented = context.outputs.documentation as
|
|
420
|
-
| { exit?: string; output?: { planDigest?: unknown } }
|
|
421
|
-
| undefined;
|
|
422
|
-
if (documented?.exit === "ready" && typeof documented.output?.planDigest === "string") {
|
|
423
|
-
return documented.output.planDigest;
|
|
424
|
-
}
|
|
425
|
-
const plan = currentPlan(context);
|
|
426
|
-
if (plan === undefined) throw new Error("autoimplement does not have a selected plan");
|
|
427
|
-
return digest(plan);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
622
|
function blockerChallenges(context: WorkflowNodeContext): BlockerChallenge[] {
|
|
431
623
|
return context.state.steps
|
|
432
624
|
.filter((step) => step.nodeId === "challengeBlocker" && step.outcome === "ok")
|
|
@@ -444,6 +636,7 @@ function latestBlockerClaim(context: WorkflowNodeContext): unknown {
|
|
|
444
636
|
"assessTrackedCi",
|
|
445
637
|
"classifyCi",
|
|
446
638
|
"finalizeDelivery",
|
|
639
|
+
"timeoutFallback",
|
|
447
640
|
];
|
|
448
641
|
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
449
642
|
const step = context.state.steps[index];
|
|
@@ -463,16 +656,6 @@ function recentWorkflowAttempts(context: WorkflowNodeContext): unknown[] {
|
|
|
463
656
|
}
|
|
464
657
|
|
|
465
658
|
function latestIssue(context: WorkflowNodeContext): unknown {
|
|
466
|
-
const approval = context.outputs.approval as
|
|
467
|
-
| { exit?: string; output?: { instructions?: unknown } }
|
|
468
|
-
| undefined;
|
|
469
|
-
if (approval?.exit === "replan" && typeof approval.output?.instructions === "string") {
|
|
470
|
-
return {
|
|
471
|
-
source: "human-replan",
|
|
472
|
-
instructions: approval.output.instructions,
|
|
473
|
-
priorPlanDigest: currentPlanDigest(context),
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
659
|
const ids = [
|
|
477
660
|
"challengeBlocker",
|
|
478
661
|
"classifyImplementation",
|
|
@@ -480,6 +663,7 @@ function latestIssue(context: WorkflowNodeContext): unknown {
|
|
|
480
663
|
"triageReview",
|
|
481
664
|
"inspectComments",
|
|
482
665
|
"classifyCi",
|
|
666
|
+
"timeoutFallback",
|
|
483
667
|
"adoptPlan",
|
|
484
668
|
];
|
|
485
669
|
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
@@ -501,18 +685,356 @@ function parseFinding(value: unknown, severity: ReviewFinding["severity"]): Revi
|
|
|
501
685
|
};
|
|
502
686
|
}
|
|
503
687
|
|
|
504
|
-
function
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
688
|
+
function parseVerificationForContext(
|
|
689
|
+
value: unknown,
|
|
690
|
+
context: WorkflowNodeContext,
|
|
691
|
+
): VerificationCommandPlan {
|
|
692
|
+
const plan = parseVerificationCommandPlan(value);
|
|
693
|
+
const implementation = latestOutput<Record<string, unknown>>(context, ["implement"]);
|
|
694
|
+
const reported = Array.isArray(implementation.repositories)
|
|
695
|
+
? implementation.repositories.filter((entry): entry is string => typeof entry === "string")
|
|
696
|
+
: [];
|
|
697
|
+
const request = context.input as AutoimplementInput;
|
|
698
|
+
const roots = new Set(
|
|
699
|
+
(reported.length > 0 ? reported : [request.repository ?? process.cwd()]).map((entry) =>
|
|
700
|
+
path.resolve(entry),
|
|
701
|
+
),
|
|
702
|
+
);
|
|
703
|
+
for (const command of plan.commands) {
|
|
704
|
+
const cwd = path.resolve(command.cwd);
|
|
705
|
+
if (!roots.delete(cwd)) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
`verification command cwd was not reported by implementation: ${command.cwd}`,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (roots.size > 0) {
|
|
712
|
+
throw new Error(`verification plan is missing reported repositories: ${[...roots].join(", ")}`);
|
|
713
|
+
}
|
|
714
|
+
return plan;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function currentPublishedRepositories(context: WorkflowNodeContext): PublishedRepositories {
|
|
718
|
+
return latestOutput<PublishedRepositories>(context, ["verifyP2", "publish"]);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
type DeliveryRepositoryResult = {
|
|
722
|
+
repository: string;
|
|
723
|
+
pr: string;
|
|
724
|
+
merged: boolean;
|
|
725
|
+
reportComment: string;
|
|
726
|
+
reason: string;
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
function parseDeliveryRepository(value: unknown, index: number): DeliveryRepositoryResult {
|
|
730
|
+
const result = requireRecord(value, `delivery repositories[${index}]`);
|
|
731
|
+
const repository = requireString(result.repository, `delivery repositories[${index}].repository`);
|
|
732
|
+
if (!path.isAbsolute(repository)) {
|
|
733
|
+
throw new Error(`delivery repositories[${index}].repository must be absolute`);
|
|
734
|
+
}
|
|
735
|
+
if (typeof result.merged !== "boolean") {
|
|
736
|
+
throw new Error(`delivery repositories[${index}].merged must be a boolean`);
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
repository: path.resolve(repository),
|
|
740
|
+
pr: requireString(result.pr, `delivery repositories[${index}].pr`),
|
|
741
|
+
merged: result.merged,
|
|
742
|
+
reportComment: requireString(
|
|
743
|
+
result.reportComment,
|
|
744
|
+
`delivery repositories[${index}].reportComment`,
|
|
745
|
+
),
|
|
746
|
+
reason: requireString(result.reason, `delivery repositories[${index}].reason`),
|
|
510
747
|
};
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function parseDeliveryResult(
|
|
751
|
+
value: unknown,
|
|
752
|
+
context: WorkflowNodeContext,
|
|
753
|
+
): Record<string, unknown> {
|
|
754
|
+
const result = requireRecord(value, "delivery result");
|
|
755
|
+
if (result.status !== "completed" && result.status !== "blocked") {
|
|
756
|
+
throw new Error("delivery status must be completed or blocked");
|
|
757
|
+
}
|
|
758
|
+
const request = context.input as AutoimplementInput;
|
|
759
|
+
if (request.merge !== true && result.merged === true) {
|
|
760
|
+
throw new Error("delivery cannot merge without explicit merge: true");
|
|
761
|
+
}
|
|
762
|
+
if (result.status === "blocked") return result;
|
|
763
|
+
if (typeof result.merged !== "boolean") {
|
|
764
|
+
throw new Error("completed delivery merged must be a boolean");
|
|
765
|
+
}
|
|
766
|
+
const pr = requireString(result.pr, "completed delivery pr");
|
|
767
|
+
const reportComment = requireString(result.reportComment, "completed delivery reportComment");
|
|
768
|
+
const reason = requireString(result.reason, "completed delivery reason");
|
|
769
|
+
const published = currentPublishedRepositories(context).repositories;
|
|
770
|
+
let repositories: DeliveryRepositoryResult[];
|
|
771
|
+
if (result.repositories === undefined) {
|
|
772
|
+
if (published.length !== 1) {
|
|
773
|
+
throw new Error("completed delivery repositories must cover every published repository");
|
|
774
|
+
}
|
|
775
|
+
const only = published[0];
|
|
776
|
+
if (only === undefined) throw new Error("completed delivery has no published repository");
|
|
777
|
+
repositories = [
|
|
778
|
+
{
|
|
779
|
+
repository: path.resolve(only.repository),
|
|
780
|
+
pr,
|
|
781
|
+
merged: result.merged,
|
|
782
|
+
reportComment,
|
|
783
|
+
reason,
|
|
784
|
+
},
|
|
785
|
+
];
|
|
786
|
+
} else {
|
|
787
|
+
if (!Array.isArray(result.repositories)) {
|
|
788
|
+
throw new Error("completed delivery repositories must be an array");
|
|
789
|
+
}
|
|
790
|
+
repositories = result.repositories.map(parseDeliveryRepository);
|
|
791
|
+
}
|
|
792
|
+
const actual = new Map<string, DeliveryRepositoryResult>();
|
|
793
|
+
for (const repository of repositories) {
|
|
794
|
+
if (actual.has(repository.repository)) {
|
|
795
|
+
throw new Error(`completed delivery repository is duplicated: ${repository.repository}`);
|
|
796
|
+
}
|
|
797
|
+
actual.set(repository.repository, repository);
|
|
798
|
+
}
|
|
799
|
+
const mergeExpected = request.merge === true;
|
|
800
|
+
for (const expected of published) {
|
|
801
|
+
const repository = actual.get(path.resolve(expected.repository));
|
|
802
|
+
if (repository === undefined || repository.pr !== expected.pr) {
|
|
803
|
+
throw new Error(
|
|
804
|
+
`completed delivery does not match published repository and PR: ${expected.repository}`,
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
if (repository.merged !== mergeExpected) {
|
|
808
|
+
throw new Error(
|
|
809
|
+
`completed delivery merge result does not match merge policy: ${expected.repository}`,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
actual.delete(repository.repository);
|
|
813
|
+
}
|
|
814
|
+
if (actual.size > 0) {
|
|
815
|
+
throw new Error(
|
|
816
|
+
`completed delivery contains unpublished repositories: ${[...actual.keys()].join(", ")}`,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
const firstPublished = published[0];
|
|
820
|
+
const first =
|
|
821
|
+
firstPublished === undefined
|
|
822
|
+
? undefined
|
|
823
|
+
: repositories.find(
|
|
824
|
+
(repository) => repository.repository === path.resolve(firstPublished.repository),
|
|
825
|
+
);
|
|
826
|
+
if (
|
|
827
|
+
first === undefined ||
|
|
828
|
+
first.pr !== pr ||
|
|
829
|
+
first.merged !== result.merged ||
|
|
830
|
+
first.reportComment !== reportComment
|
|
831
|
+
) {
|
|
832
|
+
throw new Error(
|
|
833
|
+
"completed delivery top-level compatibility fields must match the first result",
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
return { status: "completed", merged: result.merged, pr, reportComment, reason, repositories };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function parseP2Verification(
|
|
840
|
+
value: unknown,
|
|
841
|
+
context: WorkflowNodeContext,
|
|
842
|
+
): Record<string, unknown> {
|
|
843
|
+
const result = requireRecord(value, "P2 verification");
|
|
844
|
+
if (typeof result.passed !== "boolean") {
|
|
845
|
+
throw new Error("P2 verification passed must be a boolean");
|
|
846
|
+
}
|
|
847
|
+
if (result.pushed !== true) {
|
|
848
|
+
throw new Error("P2 verification pushed must be true");
|
|
849
|
+
}
|
|
850
|
+
const refreshed = parsePublishedRepositories(result);
|
|
851
|
+
const previous = latestOutput<PublishedRepositories>(context, ["publish"]);
|
|
852
|
+
const expected = new Map(
|
|
853
|
+
previous.repositories.map((repository) => [repository.id, repository] as const),
|
|
854
|
+
);
|
|
855
|
+
for (const repository of refreshed.repositories) {
|
|
856
|
+
const prior = expected.get(repository.id);
|
|
857
|
+
if (
|
|
858
|
+
prior === undefined ||
|
|
859
|
+
prior.repository !== repository.repository ||
|
|
860
|
+
prior.branch !== repository.branch ||
|
|
861
|
+
prior.baseBranch !== repository.baseBranch ||
|
|
862
|
+
prior.pr !== repository.pr ||
|
|
863
|
+
prior.dependencyFingerprint !== repository.dependencyFingerprint
|
|
864
|
+
) {
|
|
865
|
+
throw new Error(`P2 verification repository does not match publication: ${repository.id}`);
|
|
866
|
+
}
|
|
867
|
+
expected.delete(repository.id);
|
|
868
|
+
}
|
|
869
|
+
if (expected.size > 0) {
|
|
870
|
+
throw new Error(
|
|
871
|
+
`P2 verification is missing repository ids: ${[...expected.keys()].join(", ")}`,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
return { ...result, repositories: refreshed.repositories };
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function parseCiInspectionForPublished(
|
|
878
|
+
value: unknown,
|
|
879
|
+
context: WorkflowNodeContext,
|
|
880
|
+
): CiInspectionBatch {
|
|
881
|
+
const inspected = parseCiInspectionBatch(value);
|
|
882
|
+
const published = currentPublishedRepositories(context);
|
|
883
|
+
const expected = new Map(
|
|
884
|
+
published.repositories.map((repository) => [repository.id, repository] as const),
|
|
885
|
+
);
|
|
886
|
+
for (const target of inspected.targets) {
|
|
887
|
+
const repository = expected.get(target.id);
|
|
888
|
+
if (
|
|
889
|
+
repository === undefined ||
|
|
890
|
+
repository.repository !== target.repository ||
|
|
891
|
+
repository.headRevision !== target.headRevision ||
|
|
892
|
+
repository.pr !== target.pr
|
|
893
|
+
) {
|
|
894
|
+
throw new Error(
|
|
895
|
+
`CI target does not match the published repository and head: ${target.id} (${JSON.stringify({ target, repository })})`,
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
expected.delete(target.id);
|
|
899
|
+
}
|
|
900
|
+
if (expected.size > 0) {
|
|
901
|
+
throw new Error(`CI inspection is missing repository ids: ${[...expected.keys()].join(", ")}`);
|
|
902
|
+
}
|
|
903
|
+
return inspected;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function parseTrackedCiAssessment(
|
|
907
|
+
value: unknown,
|
|
908
|
+
context: WorkflowNodeContext,
|
|
909
|
+
): Record<string, unknown> & { route: CiInspectionBatch["route"] } {
|
|
910
|
+
const result = requireRecord(value, "tracked CI assessment");
|
|
911
|
+
const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
|
|
912
|
+
const execution = latestOutput<BatchExecution>(context, ["trackCi"]);
|
|
913
|
+
const expectedIds = execution.batch.items.map((item) => item.id);
|
|
914
|
+
if (!Array.isArray(result.targets)) {
|
|
915
|
+
throw new Error("tracked CI assessment targets must be an array");
|
|
916
|
+
}
|
|
917
|
+
const seen = new Set<string>();
|
|
918
|
+
const targets = result.targets.map((entry, index) => {
|
|
919
|
+
const target = requireRecord(entry, `tracked CI assessment targets[${index}]`);
|
|
920
|
+
const id = requireString(target.id, `tracked CI assessment targets[${index}].id`);
|
|
921
|
+
if (seen.has(id)) throw new Error(`tracked CI assessment target is duplicated: ${id}`);
|
|
922
|
+
seen.add(id);
|
|
923
|
+
if (
|
|
924
|
+
target.route !== "green" &&
|
|
925
|
+
target.route !== "failed" &&
|
|
926
|
+
target.route !== "pending" &&
|
|
927
|
+
target.route !== "unavailable"
|
|
928
|
+
) {
|
|
929
|
+
throw new Error(`tracked CI assessment targets[${index}].route is invalid`);
|
|
930
|
+
}
|
|
931
|
+
return {
|
|
932
|
+
id,
|
|
933
|
+
route: target.route,
|
|
934
|
+
reason: requireString(target.reason, `tracked CI assessment targets[${index}].reason`),
|
|
935
|
+
};
|
|
936
|
+
});
|
|
937
|
+
const missing = expectedIds.filter((id) => !seen.has(id));
|
|
938
|
+
const unexpected = [...seen].filter((id) => !expectedIds.includes(id));
|
|
939
|
+
if (missing.length > 0 || unexpected.length > 0 || targets.length !== expectedIds.length) {
|
|
940
|
+
throw new Error(
|
|
941
|
+
`tracked CI assessment targets must exactly cover watched ids; missing: ${missing.join(", ") || "none"}; unexpected: ${unexpected.join(", ") || "none"}`,
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
const trackedRoutes = new Map(targets.map((target) => [target.id, target.route] as const));
|
|
945
|
+
const routes = inspected.targets.map((target) => trackedRoutes.get(target.id) ?? target.route);
|
|
946
|
+
const route = routes.includes("failed")
|
|
947
|
+
? "failed"
|
|
948
|
+
: routes.includes("pending")
|
|
949
|
+
? "pending"
|
|
950
|
+
: routes.includes("unavailable")
|
|
951
|
+
? "unavailable"
|
|
952
|
+
: "green";
|
|
953
|
+
if (result.route !== route) {
|
|
954
|
+
throw new Error(`tracked CI assessment route must be ${route}`);
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
...result,
|
|
958
|
+
route,
|
|
959
|
+
reason: requireString(result.reason, "tracked CI assessment reason"),
|
|
960
|
+
targets,
|
|
961
|
+
relatedFailures: requireStringArray(
|
|
962
|
+
result.relatedFailures ?? [],
|
|
963
|
+
"tracked CI assessment relatedFailures",
|
|
964
|
+
),
|
|
965
|
+
unrelatedFailures: requireStringArray(
|
|
966
|
+
result.unrelatedFailures ?? [],
|
|
967
|
+
"tracked CI assessment unrelatedFailures",
|
|
968
|
+
),
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function selectReviewCommands(context: WorkflowNodeContext): ReviewCommandSelection {
|
|
973
|
+
const published = latestOutput<PublishedRepositories>(context, ["publish"]);
|
|
974
|
+
const reviewed = reviewRounds(context).flatMap((round) => round.repositories ?? []);
|
|
975
|
+
const repositories = published.repositories.filter(
|
|
976
|
+
(repository) =>
|
|
977
|
+
!reviewed.some(
|
|
978
|
+
(entry) =>
|
|
979
|
+
entry.id === repository.id &&
|
|
980
|
+
entry.headRevision === repository.headRevision &&
|
|
981
|
+
entry.dependencyFingerprint === repository.dependencyFingerprint &&
|
|
982
|
+
entry.invocationSucceeded,
|
|
983
|
+
),
|
|
984
|
+
);
|
|
985
|
+
return {
|
|
986
|
+
route: repositories.length === 0 ? "reuse" : "run",
|
|
987
|
+
repositories,
|
|
988
|
+
commands: repositories.map(reviewerCommand),
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function parseReviewAssessment(value: unknown, context: WorkflowNodeContext): ReviewAssessment {
|
|
993
|
+
const review = requireRecord(value, "review assessment");
|
|
994
|
+
if (!Array.isArray(review.repositories)) {
|
|
995
|
+
throw new Error("review repositories must be an array");
|
|
996
|
+
}
|
|
997
|
+
const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
|
|
998
|
+
const expected = new Map(selected.repositories.map((repository) => [repository.id, repository]));
|
|
999
|
+
const repositories = review.repositories.map((value, index) => {
|
|
1000
|
+
const raw = requireRecord(value, `review repositories[${index}]`);
|
|
1001
|
+
const id = requireString(raw.id, `review repositories[${index}].id`);
|
|
1002
|
+
const published = expected.get(id);
|
|
1003
|
+
if (published === undefined)
|
|
1004
|
+
throw new Error(`review repository id was not in the batch: ${id}`);
|
|
1005
|
+
expected.delete(id);
|
|
1006
|
+
const parseList = (key: "p0" | "p1" | "p2" | "lower", severity: ReviewFinding["severity"]) => {
|
|
1007
|
+
const list = raw[key];
|
|
1008
|
+
if (!Array.isArray(list))
|
|
1009
|
+
throw new Error(`review repositories[${index}].${key} must be an array`);
|
|
1010
|
+
return list.map((item) => parseFinding(item, severity));
|
|
1011
|
+
};
|
|
1012
|
+
return {
|
|
1013
|
+
id,
|
|
1014
|
+
repository: published.repository,
|
|
1015
|
+
baseBranch: published.baseBranch,
|
|
1016
|
+
headRevision: published.headRevision,
|
|
1017
|
+
...(published.dependencyFingerprint !== undefined
|
|
1018
|
+
? { dependencyFingerprint: published.dependencyFingerprint }
|
|
1019
|
+
: {}),
|
|
1020
|
+
invocationSucceeded: raw.invocationSucceeded === true,
|
|
1021
|
+
p0: parseList("p0", "P0"),
|
|
1022
|
+
p1: parseList("p1", "P1"),
|
|
1023
|
+
p2: parseList("p2", "P2"),
|
|
1024
|
+
lower: parseList("lower", "lower"),
|
|
1025
|
+
reason: requireString(raw.reason, `review repositories[${index}].reason`),
|
|
1026
|
+
} satisfies RepositoryReviewAssessment;
|
|
1027
|
+
});
|
|
1028
|
+
if (expected.size > 0) {
|
|
1029
|
+
throw new Error(
|
|
1030
|
+
`review assessment is missing repository ids: ${[...expected.keys()].join(", ")}`,
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
const p0 = repositories.flatMap((entry) => entry.p0);
|
|
1034
|
+
const p1 = repositories.flatMap((entry) => entry.p1);
|
|
1035
|
+
const p2 = repositories.flatMap((entry) => entry.p2);
|
|
1036
|
+
const lower = repositories.flatMap((entry) => entry.lower);
|
|
1037
|
+
const invocationSucceeded = repositories.every((entry) => entry.invocationSucceeded);
|
|
516
1038
|
const route = !invocationSucceeded
|
|
517
1039
|
? "command_error"
|
|
518
1040
|
: p0.length + p1.length > 0
|
|
@@ -528,6 +1050,7 @@ function parseReviewAssessment(value: unknown): ReviewAssessment {
|
|
|
528
1050
|
p2,
|
|
529
1051
|
lower,
|
|
530
1052
|
reason: requireString(review.reason, "review reason"),
|
|
1053
|
+
repositories,
|
|
531
1054
|
};
|
|
532
1055
|
}
|
|
533
1056
|
|
|
@@ -537,8 +1060,39 @@ function reviewRounds(context: WorkflowNodeContext): ReviewAssessment[] {
|
|
|
537
1060
|
.map((step) => step.output as ReviewAssessment);
|
|
538
1061
|
}
|
|
539
1062
|
|
|
1063
|
+
function reviewRoundsForOutput(context: WorkflowNodeContext): ReviewAssessment[] {
|
|
1064
|
+
const rounds = reviewRounds(context);
|
|
1065
|
+
const repositoryIds = new Set(
|
|
1066
|
+
rounds.flatMap((round) => (round.repositories ?? []).map((repository) => repository.id)),
|
|
1067
|
+
);
|
|
1068
|
+
if (repositoryIds.size > 1) return rounds;
|
|
1069
|
+
return rounds.map(({ repositories: _repositories, ...round }) => round);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function ciForOutput(context: WorkflowNodeContext): unknown {
|
|
1073
|
+
const result = latestOutput<Record<string, unknown>>(context, ["assessTrackedCi", "inspectCi"]);
|
|
1074
|
+
const targets = result.targets;
|
|
1075
|
+
if (!Array.isArray(targets) || targets.length !== 1) return result;
|
|
1076
|
+
const { targets: _targets, ...aggregate } = result;
|
|
1077
|
+
if (result.route === "green" || result.route === "failed" || result.route === "unavailable") {
|
|
1078
|
+
const target = targets[0];
|
|
1079
|
+
if (target !== null && typeof target === "object" && !Array.isArray(target)) {
|
|
1080
|
+
const record = target as Record<string, unknown>;
|
|
1081
|
+
return {
|
|
1082
|
+
...aggregate,
|
|
1083
|
+
reason: aggregate.reason ?? record.reason,
|
|
1084
|
+
relatedFailures: aggregate.relatedFailures ?? record.relatedFailures ?? [],
|
|
1085
|
+
unrelatedFailures: aggregate.unrelatedFailures ?? record.unrelatedFailures ?? [],
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return aggregate;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
540
1092
|
function latestBlockedReason(context: WorkflowNodeContext): { reason: string; evidence: unknown } {
|
|
541
1093
|
const candidates = [
|
|
1094
|
+
"timeoutFallbackGuard",
|
|
1095
|
+
"timeoutFallback",
|
|
542
1096
|
"challengeBlockerGuard",
|
|
543
1097
|
"challengeBlocker",
|
|
544
1098
|
"finalizeDelivery",
|
|
@@ -549,11 +1103,10 @@ function latestBlockedReason(context: WorkflowNodeContext): { reason: string; ev
|
|
|
549
1103
|
"classifyImplementation",
|
|
550
1104
|
"classifyVerification",
|
|
551
1105
|
"triageReview",
|
|
552
|
-
"
|
|
1106
|
+
"redesign",
|
|
553
1107
|
"adoptPlan",
|
|
554
1108
|
"findPlan",
|
|
555
1109
|
"documentation",
|
|
556
|
-
"approval",
|
|
557
1110
|
];
|
|
558
1111
|
for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
|
|
559
1112
|
const step = context.state.steps[index];
|
|
@@ -595,37 +1148,18 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
595
1148
|
};
|
|
596
1149
|
},
|
|
597
1150
|
}),
|
|
598
|
-
|
|
599
|
-
input: (context):
|
|
1151
|
+
redesign: includeWorkflow(planChangeWorkflow, {
|
|
1152
|
+
input: (context): NormalizedPlanChangeInput => {
|
|
600
1153
|
const request = context.input as AutoimplementInput;
|
|
601
|
-
if (request.approval === undefined) {
|
|
602
|
-
throw new Error("autoimplement approval was entered without an approval policy");
|
|
603
|
-
}
|
|
604
|
-
const plan = currentPlan(context);
|
|
605
|
-
if (plan === undefined) throw new Error("autoimplement approval is missing a plan");
|
|
606
|
-
const revisions = context.state.steps.filter(
|
|
607
|
-
(step) => step.nodeId === "approval/approve",
|
|
608
|
-
).length;
|
|
609
1154
|
return {
|
|
610
1155
|
task: request.task,
|
|
611
|
-
plan,
|
|
612
|
-
planDigest: currentPlanDigest(context),
|
|
613
|
-
audience: request.approval.audience,
|
|
614
|
-
revision: revisions + 1,
|
|
615
|
-
};
|
|
616
|
-
},
|
|
617
|
-
}),
|
|
618
|
-
redesign: includeWorkflow({
|
|
619
|
-
workflow: "autoplan",
|
|
620
|
-
contract: autoplanWorkflow,
|
|
621
|
-
input: (context): AutoplanInput => {
|
|
622
|
-
const request = context.input as AutoimplementInput;
|
|
623
|
-
return {
|
|
624
|
-
problem: request.task,
|
|
625
1156
|
...(request.scope !== undefined ? { scope: request.scope } : {}),
|
|
626
1157
|
...(request.constraints !== undefined ? { constraints: request.constraints } : {}),
|
|
1158
|
+
...(request.repository !== undefined ? { repository: request.repository } : {}),
|
|
1159
|
+
documents: request.documents ?? request.documentation?.documents ?? [],
|
|
627
1160
|
...(currentPlan(context) !== undefined ? { previousPlan: currentPlan(context) } : {}),
|
|
628
1161
|
newEvidence: latestIssue(context),
|
|
1162
|
+
approval: parsePlanApprovalPolicy(request.approval),
|
|
629
1163
|
};
|
|
630
1164
|
},
|
|
631
1165
|
}),
|
|
@@ -689,42 +1223,69 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
689
1223
|
}),
|
|
690
1224
|
adoptPlan: compute({
|
|
691
1225
|
run: ({ outputs }) => {
|
|
692
|
-
const result = includedResult(
|
|
1226
|
+
const result = includedResult(planChangeWorkflow, outputs.redesign);
|
|
693
1227
|
if (result.exit !== "ready") throw new Error("redesign did not return a ready plan");
|
|
694
1228
|
return {
|
|
695
|
-
route: result.output.changed ? "document" : "blocked",
|
|
696
1229
|
plan: result.output.plan,
|
|
697
1230
|
planDigest: result.output.planDigest,
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
: "Redesign returned the same plan for the same unresolved evidence.",
|
|
1231
|
+
documents: result.output.documents,
|
|
1232
|
+
approval: result.output.approval,
|
|
1233
|
+
reason: "The changed plan was documented and passed its approval policy.",
|
|
702
1234
|
};
|
|
703
1235
|
},
|
|
704
1236
|
}),
|
|
705
|
-
|
|
706
|
-
run:
|
|
707
|
-
route: (input as AutoimplementInput).approval === undefined ? "implement" : "approve",
|
|
708
|
-
}),
|
|
1237
|
+
timeoutFallbackGuard: compute({
|
|
1238
|
+
run: timeoutFallbackGuard,
|
|
709
1239
|
}),
|
|
710
|
-
|
|
711
|
-
|
|
1240
|
+
timeoutFallback: agent({
|
|
1241
|
+
timeoutMs: 30 * 60_000,
|
|
1242
|
+
statusDetail: "choosing a safe timeout fallback",
|
|
1243
|
+
prompt: (context) => {
|
|
712
1244
|
const request = context.input as AutoimplementInput;
|
|
713
|
-
const
|
|
714
|
-
const
|
|
715
|
-
(step) => step.nodeId === "
|
|
716
|
-
|
|
717
|
-
return
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1245
|
+
const guard = context.outputs.timeoutFallbackGuard;
|
|
1246
|
+
const previousFallbacks = context.state.steps
|
|
1247
|
+
.filter((step) => step.nodeId === "timeoutFallback" && step.outcome === "ok")
|
|
1248
|
+
.map((step) => step.output);
|
|
1249
|
+
return [
|
|
1250
|
+
"A bounded Autoimplement agent step timed out. Choose the safest existing workflow stage to run next instead of ending the run blindly.",
|
|
1251
|
+
"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.",
|
|
1252
|
+
"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.",
|
|
1253
|
+
"Do not assume that the timed-out step failed or completed. Use accepted workflow outputs and durable repository or pull-request state.",
|
|
1254
|
+
"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.",
|
|
1255
|
+
"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.",
|
|
1256
|
+
"Do not skip required implementation, verification, review, CI, authorization, or delivery checks.",
|
|
1257
|
+
`Task: ${request.task}`,
|
|
1258
|
+
`Approved plan: ${JSON.stringify(currentPlan(context))}`,
|
|
1259
|
+
`Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
|
|
1260
|
+
`Timeout: ${JSON.stringify(guard)}`,
|
|
1261
|
+
`Accepted outputs: ${JSON.stringify(context.outputs)}`,
|
|
1262
|
+
`Previous fallback results: ${JSON.stringify(previousFallbacks)}`,
|
|
1263
|
+
`Recent workflow attempts: ${JSON.stringify(recentWorkflowAttempts(context))}`,
|
|
1264
|
+
].join("\n");
|
|
724
1265
|
},
|
|
1266
|
+
expectedOutput: `{ "route": "retry" | "verify" | "review" | "ci" | "deliver" | "replan" | "blocked", "reason": "why this is the safest next stage", "evidence": ["state inspected before choosing the route"] }`,
|
|
1267
|
+
validate: parseTimeoutFallback,
|
|
1268
|
+
}),
|
|
1269
|
+
routeTimeoutFallback: compute({
|
|
1270
|
+
run: timeoutFallbackTarget,
|
|
1271
|
+
}),
|
|
1272
|
+
propagateSupportedFailure: compute({
|
|
1273
|
+
run: throwLatestSupportedFailure,
|
|
1274
|
+
}),
|
|
1275
|
+
routeVerifyP2Result: compute({
|
|
1276
|
+
run: ({ outputs }) => outputs.verifyP2,
|
|
1277
|
+
}),
|
|
1278
|
+
routeInspectCommentsResult: compute({
|
|
1279
|
+
run: ({ outputs }) => outputs.inspectComments,
|
|
1280
|
+
}),
|
|
1281
|
+
routeInspectCiResult: compute({
|
|
1282
|
+
run: ({ outputs }) => outputs.inspectCi,
|
|
1283
|
+
}),
|
|
1284
|
+
routeFinalizeDeliveryResult: compute({
|
|
1285
|
+
run: ({ outputs }) => outputs.finalizeDelivery,
|
|
725
1286
|
}),
|
|
726
1287
|
implement: agent({
|
|
727
|
-
timeoutMs: 60 * 60_000,
|
|
1288
|
+
timeoutMs: 8 * 60 * 60_000,
|
|
728
1289
|
statusDetail: "implementing",
|
|
729
1290
|
prompt: (context) => {
|
|
730
1291
|
const request = context.input as AutoimplementInput;
|
|
@@ -733,12 +1294,14 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
733
1294
|
`Plan: ${JSON.stringify(currentPlan(context))}`,
|
|
734
1295
|
`Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
|
|
735
1296
|
`Constraints: ${JSON.stringify(request.constraints ?? [])}`,
|
|
1297
|
+
"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
1298
|
"Follow repository instructions and use the most elegant long-term production-ready implementation without unnecessary work.",
|
|
737
1299
|
"If implementation exposes a new design or scope problem, report it precisely instead of forcing the old plan.",
|
|
1300
|
+
"Report every changed repository as an absolute path so independent verification can be bounded safely.",
|
|
738
1301
|
"Do not merge yet.",
|
|
739
1302
|
].join("\n");
|
|
740
1303
|
},
|
|
741
|
-
expectedOutput: `{ "status": "implemented" | "issue" | "blocked", "summary": "work completed or issue", "files": ["changed file"], "issueKind": "design" | "implementation" | null, "evidence": "new evidence" }`,
|
|
1304
|
+
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
1305
|
validate: (value) => requireRecord(value, "implementation result"),
|
|
743
1306
|
}),
|
|
744
1307
|
classifyImplementation: agent({
|
|
@@ -805,15 +1368,46 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
805
1368
|
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
1369
|
validate: parseBlockerChallenge,
|
|
807
1370
|
}),
|
|
808
|
-
|
|
809
|
-
timeoutMs:
|
|
810
|
-
statusDetail: "
|
|
1371
|
+
planVerification: agent({
|
|
1372
|
+
timeoutMs: 15 * 60_000,
|
|
1373
|
+
statusDetail: "planning independent verification commands",
|
|
811
1374
|
prompt: () =>
|
|
812
1375
|
[
|
|
813
|
-
"
|
|
814
|
-
"
|
|
815
|
-
"
|
|
816
|
-
"
|
|
1376
|
+
"Select the required local verification commands for the implementation.",
|
|
1377
|
+
"Return one command per independent repository working directory.",
|
|
1378
|
+
"Use exact executables and argument arrays without shell wrappers, environment overrides, stdin, Git or GitHub mutations, package publication, deployment, merge, or release commands.",
|
|
1379
|
+
"Use absolute repository paths, explicit timeouts no longer than 2700000ms, and maxOutputChars no larger than 1000000.",
|
|
1380
|
+
"List checks that cannot run locally under untested.",
|
|
1381
|
+
].join("\n"),
|
|
1382
|
+
expectedOutput: `{ "commands": [{ "id": "stable-id", "command": "npm", "args": ["run", "check"], "cwd": "/absolute/repository", "timeoutMs": 2700000, "maxOutputChars": 1000000 }], "untested": ["remaining check"] }`,
|
|
1383
|
+
validate: parseVerificationForContext,
|
|
1384
|
+
}),
|
|
1385
|
+
runVerification: action({
|
|
1386
|
+
timeoutMs: (context) => {
|
|
1387
|
+
const plan = latestOutput<VerificationCommandPlan>(context, ["planVerification"]);
|
|
1388
|
+
return commandBatchTimeoutMs(plan.commands, concurrency(context).verification);
|
|
1389
|
+
},
|
|
1390
|
+
statusDetail: "running independent verification commands",
|
|
1391
|
+
run: async (context) => {
|
|
1392
|
+
const plan = latestOutput<VerificationCommandPlan>(context, ["planVerification"]);
|
|
1393
|
+
return await runAutoimplementBatch(
|
|
1394
|
+
context,
|
|
1395
|
+
"verification",
|
|
1396
|
+
plan.commands,
|
|
1397
|
+
concurrency(context).verification,
|
|
1398
|
+
);
|
|
1399
|
+
},
|
|
1400
|
+
}),
|
|
1401
|
+
verify: agent({
|
|
1402
|
+
timeoutMs: 20 * 60_000,
|
|
1403
|
+
statusDetail: "assessing verification",
|
|
1404
|
+
prompt: (context) =>
|
|
1405
|
+
[
|
|
1406
|
+
"Assess the completed local verification commands.",
|
|
1407
|
+
"Set passed true only when every required command succeeded without truncated output.",
|
|
1408
|
+
"Report exact command outcomes, failures, and checks that still need remote verification.",
|
|
1409
|
+
`Command plan: ${JSON.stringify(latestOutput(context, ["planVerification"]))}`,
|
|
1410
|
+
`Command results: ${JSON.stringify(latestOutput(context, ["runVerification"]))}`,
|
|
817
1411
|
].join("\n"),
|
|
818
1412
|
expectedOutput: `{ "passed": true | false, "commands": [{ "command": "exact command", "outcome": "result" }], "failures": ["failure"], "untested": ["remaining check"] }`,
|
|
819
1413
|
validate: (value) => requireRecord(value, "verification result"),
|
|
@@ -843,6 +1437,7 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
843
1437
|
prompt: (context) =>
|
|
844
1438
|
[
|
|
845
1439
|
"Fix the current implementation issue without expanding the approved design.",
|
|
1440
|
+
"Inspect the current diff and commits first. Continue any partial fix and change only work that is still missing.",
|
|
846
1441
|
`Issue: ${JSON.stringify(latestIssue(context))}`,
|
|
847
1442
|
`Current plan: ${JSON.stringify(currentPlan(context))}`,
|
|
848
1443
|
"Stop after the fix so verification can run again.",
|
|
@@ -857,82 +1452,68 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
857
1452
|
const request = input as AutoimplementInput;
|
|
858
1453
|
return [
|
|
859
1454
|
"Commit and push the verified implementation before review.",
|
|
1455
|
+
"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
1456
|
"Use the existing implementation-plan PR when one exists. Otherwise open a PR and use the pr-description skill for its body.",
|
|
861
1457
|
"Inspect the complete public diff before every push or PR mutation.",
|
|
862
|
-
|
|
1458
|
+
"Report every repository that received a pushed pull request with its absolute repository path, branch, base branch, pushed head revision, and PR URL.",
|
|
1459
|
+
"Include dependencyFingerprint only when a declared dependency result is relevant to review reuse.",
|
|
1460
|
+
`Requested base branch: ${request.baseBranch ?? "discover each repository default branch"}.`,
|
|
863
1461
|
"Do not merge yet.",
|
|
864
1462
|
].join("\n");
|
|
865
1463
|
},
|
|
866
|
-
expectedOutput: `{ "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true }`,
|
|
867
|
-
validate:
|
|
1464
|
+
expectedOutput: `{ "repositories": [{ "repository": "/absolute/repository", "branch": "branch", "baseBranch": "base", "headRevision": "revision", "pr": "URL", "pushed": true, "dependencyFingerprint": "optional digest" }] }`,
|
|
1465
|
+
validate: parsePublishedRepositories,
|
|
868
1466
|
}),
|
|
869
|
-
|
|
870
|
-
|
|
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,
|
|
1467
|
+
selectReviewCommands: compute({
|
|
1468
|
+
run: selectReviewCommands,
|
|
885
1469
|
}),
|
|
886
|
-
runReview:
|
|
887
|
-
statusDetail: "running
|
|
888
|
-
timeoutMs:
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
latestOutput<StructuredCommand>(context, ["authorReviewCommand", "repairReviewCommand"]),
|
|
892
|
-
),
|
|
893
|
-
}),
|
|
894
|
-
repairReviewCommand: agent({
|
|
895
|
-
statusDetail: "correcting reviewer command",
|
|
896
|
-
prompt: (context) => {
|
|
897
|
-
const failed = context.results.runReview;
|
|
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");
|
|
1470
|
+
runReview: action({
|
|
1471
|
+
statusDetail: "running pi-reviewer commands",
|
|
1472
|
+
timeoutMs: (context) => {
|
|
1473
|
+
const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
|
|
1474
|
+
return commandBatchTimeoutMs(selected.commands, concurrency(context).reviewer);
|
|
904
1475
|
},
|
|
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
|
-
};
|
|
1476
|
+
run: async (context): Promise<BatchExecution> => {
|
|
1477
|
+
const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
|
|
1478
|
+
const batch = await runAutoimplementBatch(
|
|
1479
|
+
context,
|
|
1480
|
+
"review",
|
|
1481
|
+
selected.commands,
|
|
1482
|
+
concurrency(context).reviewer,
|
|
1483
|
+
);
|
|
1484
|
+
return { route: reviewBatchNeedsRepair(batch) ? "repair" : "assess", batch };
|
|
921
1485
|
},
|
|
922
1486
|
}),
|
|
1487
|
+
repairReviewCommand: agent({
|
|
1488
|
+
statusDetail: "repairing reviewer prerequisites",
|
|
1489
|
+
prompt: (context) =>
|
|
1490
|
+
[
|
|
1491
|
+
"One or more pi-reviewer commands failed, timed out, or returned truncated output.",
|
|
1492
|
+
"Diagnose and fix only local reviewer prerequisites or configuration that are in scope.",
|
|
1493
|
+
"Do not change the deterministic executable, base branch, or repository command shape, and do not substitute another reviewer.",
|
|
1494
|
+
"Choose retry only when the same commands can now produce complete reviews. Choose blocked when pi-reviewer or required configuration remains unavailable.",
|
|
1495
|
+
`Failed batch: ${JSON.stringify(context.outputs.runReview)}`,
|
|
1496
|
+
].join("\n"),
|
|
1497
|
+
expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis and action" }`,
|
|
1498
|
+
validate: (value) =>
|
|
1499
|
+
parseRoute(value, ["retry", "blocked"] as const, "reviewer command repair"),
|
|
1500
|
+
}),
|
|
923
1501
|
assessReview: agent({
|
|
924
1502
|
statusDetail: "assessing reviewer findings",
|
|
925
1503
|
prompt: (context) => {
|
|
926
|
-
const
|
|
1504
|
+
const selected = latestOutput<ReviewCommandSelection>(context, ["selectReviewCommands"]);
|
|
1505
|
+
const execution = latestOutput<BatchExecution>(context, ["runReview"]);
|
|
927
1506
|
return [
|
|
928
|
-
"Assess
|
|
929
|
-
"
|
|
930
|
-
"
|
|
1507
|
+
"Assess each completed pi-reviewer result separately.",
|
|
1508
|
+
"Return one repository entry for every selected command, using the exact repository id.",
|
|
1509
|
+
"Set invocationSucceeded false when a complete valid review was not produced.",
|
|
1510
|
+
"Record every finding under P0, P1, P2, or lower and mark it as design or implementation.",
|
|
931
1511
|
"Do not promote P2 findings to P1 merely to force another review round.",
|
|
932
|
-
`
|
|
1512
|
+
`Selected repositories: ${JSON.stringify(selected.repositories)}`,
|
|
1513
|
+
`Reviewer results: ${JSON.stringify(execution.batch)}`,
|
|
933
1514
|
].join("\n");
|
|
934
1515
|
},
|
|
935
|
-
expectedOutput: `{ "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }`,
|
|
1516
|
+
expectedOutput: `{ "repositories": [{ "id": "repository-id", "invocationSucceeded": true | false, "p0": [{ "kind": "design" | "implementation", "summary": "finding" }], "p1": [], "p2": [], "lower": [], "reason": "assessment" }], "reason": "batch assessment" }`,
|
|
936
1517
|
validate: parseReviewAssessment,
|
|
937
1518
|
}),
|
|
938
1519
|
triageReview: compute({
|
|
@@ -952,7 +1533,8 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
952
1533
|
prompt: ({ outputs }) =>
|
|
953
1534
|
[
|
|
954
1535
|
"Address valid P2 findings from the last review when the improvement is proportionate and in scope.",
|
|
955
|
-
"
|
|
1536
|
+
"Inspect the current diff and commits first. Do not repeat a P2 change that is already present.",
|
|
1537
|
+
"Do not rerun pi-reviewer solely because P2 work changes files. Verification will run once, then the workflow continues.",
|
|
956
1538
|
`Review: ${JSON.stringify(outputs.assessReview)}`,
|
|
957
1539
|
].join("\n"),
|
|
958
1540
|
expectedOutput: `{ "addressed": ["P2 change"], "skipped": [{ "finding": "finding", "reason": "why" }] }`,
|
|
@@ -964,20 +1546,23 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
964
1546
|
prompt: () =>
|
|
965
1547
|
[
|
|
966
1548
|
"Run focused verification for the P2 changes and push the verified result.",
|
|
967
|
-
"
|
|
1549
|
+
"Inspect the local and remote heads first. Do not push again when the verified head is already remote.",
|
|
1550
|
+
"Do not run pi-reviewer again because the previous round had no P0 or P1 findings.",
|
|
1551
|
+
"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
1552
|
"Report exact commands and outcomes.",
|
|
969
1553
|
].join("\n"),
|
|
970
|
-
expectedOutput: `{ "passed": true | false, "commands": [{ "command": "command", "outcome": "result" }], "pushed": true }`,
|
|
971
|
-
validate:
|
|
1554
|
+
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" }] }`,
|
|
1555
|
+
validate: parseP2Verification,
|
|
972
1556
|
}),
|
|
973
1557
|
inspectComments: agent({
|
|
974
1558
|
timeoutMs: 20 * 60_000,
|
|
975
1559
|
statusDetail: "checking PR comments",
|
|
976
|
-
prompt: () =>
|
|
1560
|
+
prompt: (context) =>
|
|
977
1561
|
[
|
|
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.",
|
|
1562
|
+
"Inspect current inline review comments and PR issue comments for every published pull request.",
|
|
1563
|
+
"Handle pull requests one at a time. Reply to and resolve every comment. Ignore stale or irrelevant comments only after explaining why.",
|
|
1564
|
+
"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.",
|
|
1565
|
+
`Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
|
|
981
1566
|
].join("\n"),
|
|
982
1567
|
expectedOutput: `{ "route": "redesign" | "fix" | "ci" | "blocked", "summary": "comment status", "evidence": ["comment or response"] }`,
|
|
983
1568
|
validate: (value) =>
|
|
@@ -986,71 +1571,75 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
986
1571
|
inspectCi: agent({
|
|
987
1572
|
timeoutMs: 10 * 60_000,
|
|
988
1573
|
statusDetail: "checking CI",
|
|
989
|
-
prompt: () =>
|
|
1574
|
+
prompt: (context) =>
|
|
990
1575
|
[
|
|
991
|
-
"Inspect
|
|
992
|
-
"
|
|
993
|
-
"
|
|
994
|
-
"
|
|
1576
|
+
"Inspect every published pull request once without waiting for completion.",
|
|
1577
|
+
"Return one target per repository and current PR head.",
|
|
1578
|
+
"Choose green, failed, pending, or unavailable for each target.",
|
|
1579
|
+
"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.",
|
|
1580
|
+
"Separate failures caused by this change from unrelated failures. Do not invent an ETA.",
|
|
1581
|
+
`Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
|
|
995
1582
|
].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
|
-
|
|
1583
|
+
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 } }] }`,
|
|
1584
|
+
validate: parseCiInspectionForPublished,
|
|
1585
|
+
}),
|
|
1586
|
+
trackCi: action({
|
|
1587
|
+
statusDetail: "tracking pending CI commands",
|
|
1588
|
+
timeoutMs: (context) => {
|
|
1589
|
+
const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
|
|
1590
|
+
const commands = inspected.targets.flatMap((target) =>
|
|
1591
|
+
target.trackingCommand === undefined ? [] : [target.trackingCommand],
|
|
1002
1592
|
);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1593
|
+
return commandBatchTimeoutMs(commands, concurrency(context).ciWatch);
|
|
1594
|
+
},
|
|
1595
|
+
run: async (context): Promise<BatchExecution> => {
|
|
1596
|
+
const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
|
|
1597
|
+
const commands = inspected.targets.flatMap((target) =>
|
|
1598
|
+
target.trackingCommand === undefined ? [] : [target.trackingCommand],
|
|
1599
|
+
);
|
|
1600
|
+
const batch = await runAutoimplementBatch(
|
|
1601
|
+
context,
|
|
1602
|
+
"ciWatch",
|
|
1603
|
+
commands,
|
|
1604
|
+
concurrency(context).ciWatch,
|
|
1605
|
+
);
|
|
1606
|
+
const needsRepair = batch.items.some(
|
|
1607
|
+
(item) =>
|
|
1608
|
+
(item.outcome === "failed" && item.exitCode === null) ||
|
|
1609
|
+
item.stdoutTruncated ||
|
|
1610
|
+
item.stderrTruncated,
|
|
1611
|
+
);
|
|
1612
|
+
return { route: needsRepair ? "repair" : "assess", batch };
|
|
1006
1613
|
},
|
|
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
1614
|
}),
|
|
1013
1615
|
repairCiCommand: agent({
|
|
1014
|
-
statusDetail: "
|
|
1616
|
+
statusDetail: "repairing CI watch prerequisites",
|
|
1015
1617
|
prompt: (context) =>
|
|
1016
1618
|
[
|
|
1017
|
-
"
|
|
1018
|
-
"
|
|
1019
|
-
"
|
|
1020
|
-
|
|
1619
|
+
"One or more supported CI watch commands failed or returned truncated output.",
|
|
1620
|
+
"Diagnose and fix only local gh prerequisites or authentication that are already authorized.",
|
|
1621
|
+
"Do not change the PR identity or substitute another command form.",
|
|
1622
|
+
"Choose retry only when the same validated commands can now provide useful status. Choose blocked otherwise.",
|
|
1623
|
+
`Failure: ${JSON.stringify(context.outputs.trackCi)}`,
|
|
1021
1624
|
].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
|
-
},
|
|
1625
|
+
expectedOutput: `{ "route": "retry" | "blocked", "reason": "diagnosis" }`,
|
|
1626
|
+
validate: (value) => parseRoute(value, ["retry", "blocked"] as const, "CI command repair"),
|
|
1036
1627
|
}),
|
|
1037
1628
|
assessTrackedCi: agent({
|
|
1038
1629
|
statusDetail: "assessing tracked CI",
|
|
1039
1630
|
prompt: (context) => {
|
|
1040
|
-
const
|
|
1631
|
+
const inspected = latestOutput<CiInspectionBatch>(context, ["inspectCi"]);
|
|
1632
|
+
const execution = latestOutput<BatchExecution>(context, ["trackCi"]);
|
|
1041
1633
|
return [
|
|
1042
|
-
"Assess
|
|
1043
|
-
"
|
|
1044
|
-
|
|
1634
|
+
"Assess every CI watch result without starting another wait.",
|
|
1635
|
+
"Return one target result for every watched PR and an aggregate route of green, failed, pending, or unavailable.",
|
|
1636
|
+
"A timed-out watch normally remains pending. Separate related from unrelated failures. Do not invent an ETA.",
|
|
1637
|
+
`Initial inspection: ${JSON.stringify(inspected)}`,
|
|
1638
|
+
`Tracking results: ${JSON.stringify(execution.batch)}`,
|
|
1045
1639
|
].join("\n");
|
|
1046
1640
|
},
|
|
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
|
-
),
|
|
1641
|
+
expectedOutput: `{ "route": "green" | "failed" | "pending" | "unavailable", "reason": "status", "targets": [{ "id": "repository-id", "route": "green" | "failed" | "pending" | "unavailable", "reason": "status" }], "relatedFailures": ["failure"], "unrelatedFailures": ["failure"] }`,
|
|
1642
|
+
validate: parseTrackedCiAssessment,
|
|
1054
1643
|
}),
|
|
1055
1644
|
opportunisticTest: agent({
|
|
1056
1645
|
timeoutMs: 30 * 60_000,
|
|
@@ -1083,30 +1672,23 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1083
1672
|
}),
|
|
1084
1673
|
finalizeDelivery: agent({
|
|
1085
1674
|
timeoutMs: 30 * 60_000,
|
|
1086
|
-
statusDetail: "finalizing
|
|
1087
|
-
prompt: (
|
|
1088
|
-
const request = input as AutoimplementInput;
|
|
1675
|
+
statusDetail: "finalizing PRs",
|
|
1676
|
+
prompt: (context) => {
|
|
1677
|
+
const request = context.input as AutoimplementInput;
|
|
1089
1678
|
return [
|
|
1090
1679
|
request.merge === false
|
|
1091
|
-
? "Leave
|
|
1092
|
-
: "
|
|
1093
|
-
"
|
|
1094
|
-
"
|
|
1680
|
+
? "Leave every verified PR ready without merging because input disabled merge."
|
|
1681
|
+
: "Handle verified PRs one at a time and merge each unless repository policy or explicit user instructions prohibit it.",
|
|
1682
|
+
"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.",
|
|
1683
|
+
"Use each repository's required merge method.",
|
|
1684
|
+
"Post a final report with the implementation summary and exact validation commands on every PR only when that report is missing.",
|
|
1685
|
+
"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
1686
|
"Return blocked instead of claiming completion when a required merge or report action fails.",
|
|
1687
|
+
`Published repositories: ${JSON.stringify(currentPublishedRepositories(context))}`,
|
|
1096
1688
|
].join("\n");
|
|
1097
1689
|
},
|
|
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
|
-
},
|
|
1690
|
+
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" }] }`,
|
|
1691
|
+
validate: parseDeliveryResult,
|
|
1110
1692
|
}),
|
|
1111
1693
|
blocked: compute({
|
|
1112
1694
|
run: (context) => {
|
|
@@ -1127,11 +1709,11 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1127
1709
|
status: "completed",
|
|
1128
1710
|
task: request.task,
|
|
1129
1711
|
plan: currentPlan(context),
|
|
1130
|
-
implementation: context
|
|
1131
|
-
verification: context
|
|
1132
|
-
reviewRounds:
|
|
1133
|
-
ci: context
|
|
1134
|
-
delivery: context
|
|
1712
|
+
implementation: latestOutput(context, ["implement"]),
|
|
1713
|
+
verification: latestOutput(context, ["verifyP2", "verify"]),
|
|
1714
|
+
reviewRounds: reviewRoundsForOutput(context),
|
|
1715
|
+
ci: ciForOutput(context),
|
|
1716
|
+
delivery: latestOutput(context, ["finalizeDelivery"]),
|
|
1135
1717
|
} satisfies AutoimplementCompleted;
|
|
1136
1718
|
},
|
|
1137
1719
|
}),
|
|
@@ -1141,7 +1723,7 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1141
1723
|
from: "prepare",
|
|
1142
1724
|
switch: {
|
|
1143
1725
|
on: "$.route",
|
|
1144
|
-
cases: { find: "findPlan", document: "documentation", ready: "
|
|
1726
|
+
cases: { find: "findPlan", document: "documentation", ready: "implement" },
|
|
1145
1727
|
},
|
|
1146
1728
|
},
|
|
1147
1729
|
{
|
|
@@ -1152,35 +1734,59 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1152
1734
|
from: "routeFoundPlan",
|
|
1153
1735
|
switch: {
|
|
1154
1736
|
on: "$.route",
|
|
1155
|
-
cases: { ready: "
|
|
1737
|
+
cases: { ready: "implement", document: "documentation", blocked: "blocked" },
|
|
1156
1738
|
},
|
|
1157
1739
|
},
|
|
1158
1740
|
{ from: "redesign.ready", to: "adoptPlan" },
|
|
1159
1741
|
{ from: "redesign.blocked", to: "blocked" },
|
|
1742
|
+
{ from: "adoptPlan", to: "implement" },
|
|
1743
|
+
{ from: "documentation.ready", to: "implement" },
|
|
1744
|
+
{ from: "documentation.blocked", to: "blocked" },
|
|
1160
1745
|
{
|
|
1161
|
-
from: "
|
|
1162
|
-
switch: { on: "$.route", cases: {
|
|
1746
|
+
from: "timeoutFallbackGuard",
|
|
1747
|
+
switch: { on: "$.route", cases: { recover: "timeoutFallback", blocked: "blocked" } },
|
|
1163
1748
|
},
|
|
1164
|
-
{ from: "
|
|
1165
|
-
{ from: "
|
|
1749
|
+
{ from: "timeoutFallback", to: "routeTimeoutFallback" },
|
|
1750
|
+
{ from: "propagateSupportedFailure", to: "blocked" },
|
|
1166
1751
|
{
|
|
1167
|
-
from: "
|
|
1168
|
-
switch: {
|
|
1752
|
+
from: "routeTimeoutFallback",
|
|
1753
|
+
switch: {
|
|
1754
|
+
on: "$.route",
|
|
1755
|
+
cases: {
|
|
1756
|
+
implement: "implement",
|
|
1757
|
+
planVerification: "planVerification",
|
|
1758
|
+
verify: "verify",
|
|
1759
|
+
fix: "fix",
|
|
1760
|
+
publish: "publish",
|
|
1761
|
+
addressP2: "addressP2",
|
|
1762
|
+
verifyP2: "verifyP2",
|
|
1763
|
+
inspectComments: "inspectComments",
|
|
1764
|
+
inspectCi: "inspectCi",
|
|
1765
|
+
opportunisticTest: "opportunisticTest",
|
|
1766
|
+
finalizeDelivery: "finalizeDelivery",
|
|
1767
|
+
selectReviewCommands: "selectReviewCommands",
|
|
1768
|
+
redesign: "redesign",
|
|
1769
|
+
blocked: "blocked",
|
|
1770
|
+
},
|
|
1771
|
+
},
|
|
1169
1772
|
},
|
|
1170
|
-
{ from: "approval.continue", to: "implement" },
|
|
1171
|
-
{ from: "approval.stop", to: "blocked" },
|
|
1172
|
-
{ from: "approval.replan", to: "replanGuard" },
|
|
1173
1773
|
{
|
|
1174
|
-
from: "
|
|
1175
|
-
switch: {
|
|
1774
|
+
from: "implement",
|
|
1775
|
+
switch: {
|
|
1776
|
+
on: "$result.outcome",
|
|
1777
|
+
cases: {
|
|
1778
|
+
ok: "classifyImplementation",
|
|
1779
|
+
timed_out: "timeoutFallbackGuard",
|
|
1780
|
+
failed: "propagateSupportedFailure",
|
|
1781
|
+
},
|
|
1782
|
+
},
|
|
1176
1783
|
},
|
|
1177
|
-
{ from: "implement", to: "classifyImplementation" },
|
|
1178
1784
|
{
|
|
1179
1785
|
from: "classifyImplementation",
|
|
1180
1786
|
switch: {
|
|
1181
1787
|
on: "$.route",
|
|
1182
1788
|
cases: {
|
|
1183
|
-
verify: "
|
|
1789
|
+
verify: "planVerification",
|
|
1184
1790
|
redesign: "redesign",
|
|
1185
1791
|
fix: "fix",
|
|
1186
1792
|
blocked: "challengeBlockerGuard",
|
|
@@ -1195,7 +1801,29 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1195
1801
|
from: "challengeBlocker",
|
|
1196
1802
|
switch: { on: "$.route", cases: { continue: "redesign", blocked: "blocked" } },
|
|
1197
1803
|
},
|
|
1198
|
-
{
|
|
1804
|
+
{
|
|
1805
|
+
from: "planVerification",
|
|
1806
|
+
switch: {
|
|
1807
|
+
on: "$result.outcome",
|
|
1808
|
+
cases: {
|
|
1809
|
+
ok: "runVerification",
|
|
1810
|
+
timed_out: "timeoutFallbackGuard",
|
|
1811
|
+
failed: "propagateSupportedFailure",
|
|
1812
|
+
},
|
|
1813
|
+
},
|
|
1814
|
+
},
|
|
1815
|
+
{ from: "runVerification", to: "verify" },
|
|
1816
|
+
{
|
|
1817
|
+
from: "verify",
|
|
1818
|
+
switch: {
|
|
1819
|
+
on: "$result.outcome",
|
|
1820
|
+
cases: {
|
|
1821
|
+
ok: "classifyVerification",
|
|
1822
|
+
timed_out: "timeoutFallbackGuard",
|
|
1823
|
+
failed: "propagateSupportedFailure",
|
|
1824
|
+
},
|
|
1825
|
+
},
|
|
1826
|
+
},
|
|
1199
1827
|
{
|
|
1200
1828
|
from: "classifyVerification",
|
|
1201
1829
|
switch: {
|
|
@@ -1208,20 +1836,36 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1208
1836
|
},
|
|
1209
1837
|
},
|
|
1210
1838
|
},
|
|
1211
|
-
{ from: "fix", to: "verify" },
|
|
1212
|
-
{ from: "publish", to: "authorReviewCommand" },
|
|
1213
|
-
{ from: "authorReviewCommand", to: "runReview" },
|
|
1214
1839
|
{
|
|
1215
|
-
from: "
|
|
1840
|
+
from: "fix",
|
|
1841
|
+
switch: {
|
|
1842
|
+
on: "$result.outcome",
|
|
1843
|
+
cases: {
|
|
1844
|
+
ok: "planVerification",
|
|
1845
|
+
timed_out: "timeoutFallbackGuard",
|
|
1846
|
+
failed: "propagateSupportedFailure",
|
|
1847
|
+
},
|
|
1848
|
+
},
|
|
1849
|
+
},
|
|
1850
|
+
{
|
|
1851
|
+
from: "publish",
|
|
1216
1852
|
switch: {
|
|
1217
1853
|
on: "$result.outcome",
|
|
1218
1854
|
cases: {
|
|
1219
|
-
ok: "
|
|
1220
|
-
|
|
1221
|
-
|
|
1855
|
+
ok: "selectReviewCommands",
|
|
1856
|
+
timed_out: "timeoutFallbackGuard",
|
|
1857
|
+
failed: "propagateSupportedFailure",
|
|
1222
1858
|
},
|
|
1223
1859
|
},
|
|
1224
1860
|
},
|
|
1861
|
+
{
|
|
1862
|
+
from: "selectReviewCommands",
|
|
1863
|
+
switch: { on: "$.route", cases: { run: "runReview", reuse: "inspectComments" } },
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
from: "runReview",
|
|
1867
|
+
switch: { on: "$.route", cases: { assess: "assessReview", repair: "repairReviewCommand" } },
|
|
1868
|
+
},
|
|
1225
1869
|
{
|
|
1226
1870
|
from: "repairReviewCommand",
|
|
1227
1871
|
switch: {
|
|
@@ -1245,13 +1889,45 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1245
1889
|
from: "triageReview",
|
|
1246
1890
|
switch: { on: "$.route", cases: { redesign: "redesign", fix: "fix" } },
|
|
1247
1891
|
},
|
|
1248
|
-
{
|
|
1892
|
+
{
|
|
1893
|
+
from: "addressP2",
|
|
1894
|
+
switch: {
|
|
1895
|
+
on: "$result.outcome",
|
|
1896
|
+
cases: {
|
|
1897
|
+
ok: "verifyP2",
|
|
1898
|
+
timed_out: "timeoutFallbackGuard",
|
|
1899
|
+
failed: "propagateSupportedFailure",
|
|
1900
|
+
},
|
|
1901
|
+
},
|
|
1902
|
+
},
|
|
1249
1903
|
{
|
|
1250
1904
|
from: "verifyP2",
|
|
1905
|
+
switch: {
|
|
1906
|
+
on: "$result.outcome",
|
|
1907
|
+
cases: {
|
|
1908
|
+
ok: "routeVerifyP2Result",
|
|
1909
|
+
timed_out: "timeoutFallbackGuard",
|
|
1910
|
+
failed: "propagateSupportedFailure",
|
|
1911
|
+
},
|
|
1912
|
+
},
|
|
1913
|
+
},
|
|
1914
|
+
{
|
|
1915
|
+
from: "routeVerifyP2Result",
|
|
1251
1916
|
switch: { on: "$.passed", cases: { true: "inspectComments", false: "fix" } },
|
|
1252
1917
|
},
|
|
1253
1918
|
{
|
|
1254
1919
|
from: "inspectComments",
|
|
1920
|
+
switch: {
|
|
1921
|
+
on: "$result.outcome",
|
|
1922
|
+
cases: {
|
|
1923
|
+
ok: "routeInspectCommentsResult",
|
|
1924
|
+
timed_out: "timeoutFallbackGuard",
|
|
1925
|
+
failed: "propagateSupportedFailure",
|
|
1926
|
+
},
|
|
1927
|
+
},
|
|
1928
|
+
},
|
|
1929
|
+
{
|
|
1930
|
+
from: "routeInspectCommentsResult",
|
|
1255
1931
|
switch: {
|
|
1256
1932
|
on: "$.route",
|
|
1257
1933
|
cases: {
|
|
@@ -1264,6 +1940,17 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1264
1940
|
},
|
|
1265
1941
|
{
|
|
1266
1942
|
from: "inspectCi",
|
|
1943
|
+
switch: {
|
|
1944
|
+
on: "$result.outcome",
|
|
1945
|
+
cases: {
|
|
1946
|
+
ok: "routeInspectCiResult",
|
|
1947
|
+
timed_out: "timeoutFallbackGuard",
|
|
1948
|
+
failed: "propagateSupportedFailure",
|
|
1949
|
+
},
|
|
1950
|
+
},
|
|
1951
|
+
},
|
|
1952
|
+
{
|
|
1953
|
+
from: "routeInspectCiResult",
|
|
1267
1954
|
switch: {
|
|
1268
1955
|
on: "$.route",
|
|
1269
1956
|
cases: {
|
|
@@ -1276,10 +1963,7 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1276
1963
|
},
|
|
1277
1964
|
{
|
|
1278
1965
|
from: "trackCi",
|
|
1279
|
-
switch: {
|
|
1280
|
-
on: "$result.outcome",
|
|
1281
|
-
cases: { ok: "assessTrackedCi", failed: "repairCiCommand", timed_out: "opportunisticTest" },
|
|
1282
|
-
},
|
|
1966
|
+
switch: { on: "$.route", cases: { assess: "assessTrackedCi", repair: "repairCiCommand" } },
|
|
1283
1967
|
},
|
|
1284
1968
|
{
|
|
1285
1969
|
from: "repairCiCommand",
|
|
@@ -1300,7 +1984,17 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1300
1984
|
},
|
|
1301
1985
|
},
|
|
1302
1986
|
},
|
|
1303
|
-
{
|
|
1987
|
+
{
|
|
1988
|
+
from: "opportunisticTest",
|
|
1989
|
+
switch: {
|
|
1990
|
+
on: "$result.outcome",
|
|
1991
|
+
cases: {
|
|
1992
|
+
ok: "inspectCi",
|
|
1993
|
+
timed_out: "timeoutFallbackGuard",
|
|
1994
|
+
failed: "propagateSupportedFailure",
|
|
1995
|
+
},
|
|
1996
|
+
},
|
|
1997
|
+
},
|
|
1304
1998
|
{
|
|
1305
1999
|
from: "classifyCi",
|
|
1306
2000
|
switch: {
|
|
@@ -1315,6 +2009,17 @@ export const autoimplementWorkflow = defineWorkflow({
|
|
|
1315
2009
|
},
|
|
1316
2010
|
{
|
|
1317
2011
|
from: "finalizeDelivery",
|
|
2012
|
+
switch: {
|
|
2013
|
+
on: "$result.outcome",
|
|
2014
|
+
cases: {
|
|
2015
|
+
ok: "routeFinalizeDeliveryResult",
|
|
2016
|
+
timed_out: "timeoutFallbackGuard",
|
|
2017
|
+
failed: "propagateSupportedFailure",
|
|
2018
|
+
},
|
|
2019
|
+
},
|
|
2020
|
+
},
|
|
2021
|
+
{
|
|
2022
|
+
from: "routeFinalizeDeliveryResult",
|
|
1318
2023
|
switch: {
|
|
1319
2024
|
on: "$.status",
|
|
1320
2025
|
cases: { completed: "finalize", blocked: "challengeBlockerGuard" },
|