@mingchuno/agent-workflows 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -84,6 +84,7 @@ export class SDKAgent {
84
84
  const worker = join(dirname(ownPath), `agent-worker.${source ? "ts" : "js"}`);
85
85
  let buffer = "", output = "", events = Promise.resolve();
86
86
  let eventError;
87
+ let stderr = "";
87
88
  const persistenceFailure = new AbortController();
88
89
  try {
89
90
  await command(process.execPath, [
@@ -97,6 +98,9 @@ export class SDKAgent {
97
98
  ...(signal ? [signal] : []),
98
99
  ]),
99
100
  captureOutput: false,
101
+ onStderr: (chunk) => {
102
+ stderr = (stderr + chunk).slice(-16_384);
103
+ },
100
104
  processFile: invocation?.processFile,
101
105
  timeoutMs: invocation
102
106
  ? (invocation.timeoutMs ?? defaultStageTimeoutMs)
@@ -128,6 +132,11 @@ export class SDKAgent {
128
132
  });
129
133
  }
130
134
  },
135
+ }).catch((error) => {
136
+ const detail = stderr.trim();
137
+ throw detail
138
+ ? new Error(`${String(error)}\nWorker stderr (last 16 KiB):\n${detail}`)
139
+ : error;
131
140
  });
132
141
  await events;
133
142
  if (eventError)
package/dist/src/cli.js CHANGED
@@ -165,6 +165,9 @@ program
165
165
  const record = await store.run(run);
166
166
  const invocations = (await store.invocations(run)).filter((item) => !options.invocation || item.id === options.invocation);
167
167
  const paths = [
168
+ ...(!options.invocation
169
+ ? (record.stageLogs?.map((item) => item.path) ?? [])
170
+ : []),
168
171
  ...invocations.map((item) => item.log),
169
172
  ...(!options.invocation
170
173
  ? (record.validation?.map((check) => check.log) ?? [])
@@ -180,7 +183,7 @@ program
180
183
  }
181
184
  }
182
185
  }));
183
- for (const kind of ["pause", "resume", "stop", "retry", "recover"])
186
+ for (const kind of ["pause", "resume", "stop", "recover"])
184
187
  program
185
188
  .command(`${kind} <target>`)
186
189
  .description(`${kind} project or run through the active runner`)
@@ -190,6 +193,16 @@ for (const kind of ["pause", "resume", "stop", "retry", "recover"])
190
193
  status: "pending",
191
194
  }));
192
195
  }));
196
+ program
197
+ .command("retry <target>")
198
+ .description("retry a run through the active runner")
199
+ .option("--refresh-issue", "use the current hosted issue for the new run")
200
+ .action(async (target, options) => withStore(async (store) => {
201
+ console.log(JSON.stringify({
202
+ commandId: await store.request(options.refreshIssue ? "retry-refresh" : "retry", target),
203
+ status: "pending",
204
+ }));
205
+ }));
193
206
  program
194
207
  .command("monitor")
195
208
  .option("--notify", "notify when an observed execution reaches an outcome")
@@ -51,6 +51,11 @@ export declare const projectSchema: z.ZodObject<{
51
51
  args: z.ZodDefault<z.ZodArray<z.ZodString>>;
52
52
  timeoutMs: z.ZodDefault<z.ZodNumber>;
53
53
  }, z.core.$strict>>>;
54
+ validationProfiles: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
55
+ command: z.ZodString;
56
+ args: z.ZodDefault<z.ZodArray<z.ZodString>>;
57
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
58
+ }, z.core.$strict>>>>;
54
59
  includeAgentCoAuthors: z.ZodDefault<z.ZodBoolean>;
55
60
  agent: z.ZodObject<{
56
61
  provider: z.ZodEnum<{
@@ -144,6 +149,11 @@ export declare const configSchema: z.ZodObject<{
144
149
  args: z.ZodDefault<z.ZodArray<z.ZodString>>;
145
150
  timeoutMs: z.ZodDefault<z.ZodNumber>;
146
151
  }, z.core.$strict>>>;
152
+ validationProfiles: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
153
+ command: z.ZodString;
154
+ args: z.ZodDefault<z.ZodArray<z.ZodString>>;
155
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
156
+ }, z.core.$strict>>>>;
147
157
  includeAgentCoAuthors: z.ZodDefault<z.ZodBoolean>;
148
158
  agent: z.ZodObject<{
149
159
  provider: z.ZodEnum<{
@@ -29,6 +29,11 @@ export const stageSchema = z
29
29
  : undefined,
30
30
  })
31
31
  .refine((stage) => stage.prompt === undefined || stage.promptFile === undefined, "Specify either prompt or promptFile, never both");
32
+ const validationCommandSchema = z.strictObject({
33
+ command: z.string().min(1),
34
+ args: z.array(z.string()).default([]),
35
+ timeoutMs: z.number().int().positive().default(defaultValidationTimeoutMs),
36
+ });
32
37
  export const projectSchema = z.strictObject({
33
38
  id: z.string().regex(/^[a-zA-Z0-9_-]+$/),
34
39
  checkout: z.string().min(1),
@@ -49,17 +54,10 @@ export const projectSchema = z.strictObject({
49
54
  .includes("{issue}")
50
55
  .default("agent/{issue}-{attempt}"),
51
56
  pollIntervalMs: z.number().int().min(100).default(30_000),
52
- validation: z
53
- .array(z.strictObject({
54
- command: z.string().min(1),
55
- args: z.array(z.string()).default([]),
56
- timeoutMs: z
57
- .number()
58
- .int()
59
- .positive()
60
- .default(defaultValidationTimeoutMs),
61
- }))
62
- .default([]),
57
+ validation: z.array(validationCommandSchema).default([]),
58
+ validationProfiles: z
59
+ .record(z.string().regex(/^[a-zA-Z0-9_-]+$/), z.array(validationCommandSchema).min(1))
60
+ .default({}),
63
61
  includeAgentCoAuthors: z.boolean().default(true),
64
62
  agent: profileSchema,
65
63
  stages: z
@@ -135,6 +135,7 @@ export interface RunRecord {
135
135
  attempt: number;
136
136
  retryOf?: string;
137
137
  issue: Issue;
138
+ validationProfile?: string;
138
139
  outcome: Outcome;
139
140
  phase: string;
140
141
  createdAt: string;
@@ -152,6 +153,11 @@ export interface RunRecord {
152
153
  reviewHead?: string;
153
154
  error?: string;
154
155
  failedStep?: number;
156
+ stageLogs?: Array<{
157
+ executionId: string;
158
+ step: string;
159
+ path: string;
160
+ }>;
155
161
  executions?: ExecutionRecord[];
156
162
  }
157
163
  export declare class BlockedError extends Error {
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { type Stage } from "./config.js";
3
- import { type ContributionCandidate, type RunRecord, type Snapshot } from "./domain.js";
3
+ import { type RunRecord } from "./domain.js";
4
4
  import { type ChangeEvidence } from "./evidence.js";
5
5
  import type { OperationDependencies } from "./operations.js";
6
6
  export interface InvocationTask {
@@ -17,9 +17,7 @@ interface StageExecution {
17
17
  task: InvocationTask;
18
18
  stepId: number;
19
19
  dependencies: OperationDependencies;
20
- saveImplementationSnapshot: (runId: string, snapshot: Snapshot, provider: string) => Promise<ContributionCandidate | undefined>;
21
- acceptContribution: (runId: string, candidate: ContributionCandidate) => Promise<void>;
22
20
  }
23
21
  /** One logical stage; only returned format errors admit a second response attempt. */
24
- export declare function invokeStage({ run, name, stage, task, stepId, dependencies, saveImplementationSnapshot, acceptContribution, }: StageExecution): Promise<string>;
22
+ export declare function invokeStage(execution: StageExecution): Promise<string>;
25
23
  export {};
@@ -8,8 +8,46 @@ import { verifyEvidence } from "./evidence.js";
8
8
  import { resolveStagePrompt, sha256 } from "./prompts.js";
9
9
  const maxInvocationAttempts = 2;
10
10
  /** One logical stage; only returned format errors admit a second response attempt. */
11
- export async function invokeStage({ run, name, stage, task, stepId, dependencies, saveImplementationSnapshot, acceptContribution, }) {
12
- const { store, project, agents, signal, workspace, redact } = dependencies;
11
+ export async function invokeStage(execution) {
12
+ const { run, name, stepId, dependencies } = execution;
13
+ const directory = join(dependencies.artifacts, run.id);
14
+ await mkdir(directory, { recursive: true, mode: 0o700 });
15
+ const path = join(directory, `stage-${name}-${stepId}.log`);
16
+ const executionId = run.executions?.at(-1)?.id ?? run.id;
17
+ await dependencies.store.patchRun(run.id, {
18
+ stageLogs: [
19
+ ...(run.stageLogs ?? []).filter((item) => item.executionId !== executionId || item.step !== name),
20
+ { executionId, step: name, path },
21
+ ],
22
+ });
23
+ const note = (message) => appendFile(path, `${new Date().toISOString()} ${dependencies.redact(message)}\n`, { mode: 0o600 });
24
+ try {
25
+ await note(`Preparing ${name} agent stage`);
26
+ const prepared = await prepareStage(execution, note);
27
+ await note(`Agent preflight passed: ${prepared.profile.provider}`);
28
+ let attempt = { number: 1, correction: "" };
29
+ while (attempt.number <= maxInvocationAttempts) {
30
+ await note(`Starting invocation ${attempt.number}`);
31
+ const result = await invokeAttempt(execution, prepared, attempt);
32
+ if (result.kind === "completed") {
33
+ await note(`${name} agent stage completed`);
34
+ return result.output;
35
+ }
36
+ attempt = {
37
+ number: attempt.number + 1,
38
+ correction: result.correction,
39
+ contribution: result.contribution,
40
+ };
41
+ }
42
+ throw new Error("Format correction exhausted");
43
+ }
44
+ catch (error) {
45
+ await note(`Failed: ${String(error)}`);
46
+ throw error;
47
+ }
48
+ }
49
+ async function refusePreviousInvocations(execution) {
50
+ const { run, name, stepId, dependencies: { store }, } = execution;
13
51
  const previous = (await store.invocations(run.id)).filter((item) => item.stepId === stepId);
14
52
  if (previous.length) {
15
53
  for (const record of previous) {
@@ -23,11 +61,10 @@ export async function invokeStage({ run, name, stage, task, stepId, dependencies
23
61
  }
24
62
  throw new BlockedError(`Interrupted agent stage ${name}; inspect existing sessions before explicit retry`);
25
63
  }
26
- if (!run.snapshot)
27
- throw new BlockedError("Missing workspace snapshot");
28
- await workspace.verify(project, run.snapshot);
29
- if (task.evidence)
30
- await verifyEvidence(task.evidence);
64
+ }
65
+ function prepareRequest(execution) {
66
+ const { run, stage, task, dependencies } = execution;
67
+ const { project } = dependencies;
31
68
  const resolved = resolveStagePrompt(stage, task.defaultPrompt, dependencies.promptBaseDirectory);
32
69
  const outputSchema = task.outputContract
33
70
  ? z.toJSONSchema(task.outputContract)
@@ -44,6 +81,19 @@ export async function invokeStage({ run, name, stage, task, stepId, dependencies
44
81
  contract,
45
82
  ].join("\n\n");
46
83
  const profile = resolveProfile(project.agent, stage.profile);
84
+ return { resolved, outputSchema, fullPrompt, profile };
85
+ }
86
+ async function prepareStage(execution, note) {
87
+ const { run, stage, task, dependencies } = execution;
88
+ const { project, agents, signal, workspace } = dependencies;
89
+ await refusePreviousInvocations(execution);
90
+ if (!run.snapshot)
91
+ throw new BlockedError("Missing workspace snapshot");
92
+ await workspace.verify(project, run.snapshot);
93
+ if (task.evidence)
94
+ await verifyEvidence(task.evidence);
95
+ const request = prepareRequest(execution);
96
+ const { profile } = request;
47
97
  const adapter = agents[profile.provider];
48
98
  if (!adapter)
49
99
  throw new Error(`Missing agent adapter ${profile.provider}`);
@@ -52,115 +102,162 @@ export async function invokeStage({ run, name, stage, task, stepId, dependencies
52
102
  signal,
53
103
  AbortSignal.timeout(stage.timeoutMs),
54
104
  ]);
105
+ await note(`Validating ${profile.provider} agent profile`);
55
106
  const effective = await adapter.validate(profile, invocationSignal);
56
107
  const directory = join(dependencies.artifacts, run.id);
57
108
  await mkdir(directory, { recursive: true, mode: 0o700 });
58
- let correction = "";
59
- let pendingContribution;
60
- for (let attempt = 1; attempt <= maxInvocationAttempts; attempt++) {
109
+ return {
110
+ ...request,
111
+ adapter,
112
+ effective,
113
+ deadline,
114
+ invocationSignal,
115
+ directory,
116
+ };
117
+ }
118
+ async function invokeAttempt(execution, prepared, attempt) {
119
+ const { run, name, task, stepId, dependencies } = execution;
120
+ const { store, project, workspace, redact } = dependencies;
121
+ const { profile, effective, resolved, outputSchema, fullPrompt, directory, invocationSignal, } = prepared;
122
+ invocationSignal.throwIfAborted();
123
+ const expected = (await store.run(run.id)).snapshot;
124
+ await workspace.verify(project, expected);
125
+ if (task.evidence)
126
+ await verifyEvidence(task.evidence);
127
+ const id = randomUUID();
128
+ const prompt = fullPrompt + attempt.correction;
129
+ const readOnly = task.readOnly === true || attempt.number === maxInvocationAttempts;
130
+ const record = {
131
+ id,
132
+ runId: run.id,
133
+ projectId: project.id,
134
+ step: name,
135
+ stepId,
136
+ attempt: attempt.number,
137
+ provider: profile.provider,
138
+ sessionId: null,
139
+ sessionState: "pending",
140
+ requested: profile,
141
+ effective,
142
+ prompt: redact(prompt),
143
+ taskPrompt: { ...resolved, content: redact(resolved.content) },
144
+ outputContract: outputSchema
145
+ ? sha256(JSON.stringify(outputSchema))
146
+ : undefined,
147
+ evidence: task.evidence,
148
+ outcome: "running",
149
+ startedAt: new Date().toISOString(),
150
+ log: join(directory, `${id}.jsonl`),
151
+ };
152
+ await store.saveInvocation(record);
153
+ try {
154
+ invocationSignal.throwIfAborted();
155
+ const output = await invokeProvider(execution, prepared, {
156
+ record,
157
+ prompt,
158
+ readOnly,
159
+ });
160
+ await appendFile(record.log, "", { mode: 0o600 });
61
161
  invocationSignal.throwIfAborted();
62
- const expected = (await store.run(run.id)).snapshot;
63
- await workspace.verify(project, expected);
162
+ let contribution = attempt.contribution;
163
+ if (readOnly)
164
+ await workspace.verify(project, expected);
165
+ else
166
+ contribution = await saveImplementationSnapshot(run.id, expected, profile.provider, dependencies);
64
167
  if (task.evidence)
65
168
  await verifyEvidence(task.evidence);
66
- const id = randomUUID();
67
- const prompt = fullPrompt + correction;
68
- const readOnly = task.readOnly === true || attempt === maxInvocationAttempts;
69
- const record = {
70
- id,
71
- runId: run.id,
72
- projectId: project.id,
73
- step: name,
74
- stepId,
75
- attempt,
76
- provider: profile.provider,
77
- sessionId: null,
78
- sessionState: "pending",
79
- requested: profile,
80
- effective,
81
- prompt: redact(prompt),
82
- taskPrompt: { ...resolved, content: redact(resolved.content) },
83
- outputContract: outputSchema
84
- ? sha256(JSON.stringify(outputSchema))
85
- : undefined,
86
- evidence: task.evidence,
87
- outcome: "running",
88
- startedAt: new Date().toISOString(),
89
- log: join(directory, `${id}.jsonl`),
169
+ invocationSignal.throwIfAborted();
170
+ const response = validateResponse(output, task.outputContract);
171
+ if (response.kind === "invalid") {
172
+ const correction = await recordInvalidResponse(record, output, response.error, redact);
173
+ return { kind: "correction", correction, contribution };
174
+ }
175
+ if (contribution)
176
+ await acceptContribution(run.id, contribution, dependencies);
177
+ record.outcome = "completed";
178
+ return {
179
+ kind: "completed",
180
+ output: redact(task.outputContract ? JSON.stringify(response.parsed) : output),
90
181
  };
182
+ }
183
+ catch (error) {
184
+ if (record.outcome === "running")
185
+ record.outcome = "failed";
186
+ throw error;
187
+ }
188
+ finally {
189
+ record.finishedAt = new Date().toISOString();
190
+ if (!record.sessionId)
191
+ record.sessionState = "unavailable";
91
192
  await store.saveInvocation(record);
92
- try {
93
- invocationSignal.throwIfAborted();
94
- const output = await adapter.invoke({
95
- id,
96
- runId: run.id,
97
- step: name,
98
- cwd: project.checkout,
99
- prompt,
100
- profile,
101
- outputSchema,
102
- processFile: record.log + ".process.json",
103
- readOnly,
104
- signal: invocationSignal,
105
- timeoutMs: Math.max(1, deadline - Date.now()),
106
- session: async (sessionId) => {
107
- record.sessionId = sessionId;
108
- record.sessionState = "available";
109
- await store.saveInvocation(record);
110
- },
111
- event: async (event) => {
112
- await appendFile(record.log, redact(JSON.stringify(event)) + "\n", {
113
- mode: 0o600,
114
- });
115
- },
193
+ }
194
+ }
195
+ async function invokeProvider(execution, prepared, attempt) {
196
+ const { run, name, dependencies: { project, store, redact }, } = execution;
197
+ const { adapter, profile, outputSchema, invocationSignal, deadline } = prepared;
198
+ const { record, prompt, readOnly } = attempt;
199
+ return adapter.invoke({
200
+ id: record.id,
201
+ runId: run.id,
202
+ step: name,
203
+ cwd: project.checkout,
204
+ prompt,
205
+ profile,
206
+ outputSchema,
207
+ processFile: record.log + ".process.json",
208
+ readOnly,
209
+ signal: invocationSignal,
210
+ timeoutMs: Math.max(1, deadline - Date.now()),
211
+ session: async (sessionId) => {
212
+ record.sessionId = sessionId;
213
+ record.sessionState = "available";
214
+ await store.saveInvocation(record);
215
+ },
216
+ event: async (event) => {
217
+ await appendFile(record.log, redact(JSON.stringify(event)) + "\n", {
218
+ mode: 0o600,
116
219
  });
117
- await appendFile(record.log, "", { mode: 0o600 });
118
- invocationSignal.throwIfAborted();
119
- if (readOnly)
120
- await workspace.verify(project, expected);
121
- else
122
- pendingContribution = await saveImplementationSnapshot(run.id, expected, profile.provider);
123
- if (task.evidence)
124
- await verifyEvidence(task.evidence);
125
- invocationSignal.throwIfAborted();
126
- // Only returned output validation failures qualify for correction.
127
- let parsed;
128
- try {
129
- parsed = task.outputContract
130
- ? task.outputContract.parse(JSON.parse(output))
131
- : undefined;
132
- }
133
- catch (error) {
134
- if (!(error instanceof SyntaxError) && !(error instanceof z.ZodError))
135
- throw error;
136
- record.outcome = "invalid-output";
137
- record.validationError = redact(String(error));
138
- await appendFile(record.log, redact(JSON.stringify({
139
- type: "invalid-output",
140
- output,
141
- error: String(error),
142
- })) + "\n");
143
- if (attempt === maxInvocationAttempts)
144
- throw new Error(`Invalid output after one correction: ${String(error)}`);
145
- correction = `\n\nCorrect the prior response format in this fresh inspection-only session. Do not modify files.\nPrior invalid response:\n${output}\nValidation errors:\n${String(error)}`;
146
- continue;
147
- }
148
- if (pendingContribution)
149
- await acceptContribution(run.id, pendingContribution);
150
- record.outcome = "completed";
151
- return redact(task.outputContract ? JSON.stringify(parsed) : output);
152
- }
153
- catch (error) {
154
- if (record.outcome === "running")
155
- record.outcome = "failed";
220
+ },
221
+ });
222
+ }
223
+ /** Only returned output validation failures qualify for correction. */
224
+ function validateResponse(output, contract) {
225
+ let parsed;
226
+ try {
227
+ parsed = contract ? contract.parse(JSON.parse(output)) : undefined;
228
+ }
229
+ catch (error) {
230
+ if (!(error instanceof SyntaxError) && !(error instanceof z.ZodError))
156
231
  throw error;
157
- }
158
- finally {
159
- record.finishedAt = new Date().toISOString();
160
- if (!record.sessionId)
161
- record.sessionState = "unavailable";
162
- await store.saveInvocation(record);
163
- }
232
+ return { kind: "invalid", error };
164
233
  }
165
- throw new Error("Format correction exhausted");
234
+ return { kind: "valid", parsed };
235
+ }
236
+ async function recordInvalidResponse(record, output, error, redact) {
237
+ record.outcome = "invalid-output";
238
+ record.validationError = redact(String(error));
239
+ await appendFile(record.log, redact(JSON.stringify({ type: "invalid-output", output, error: String(error) })) + "\n");
240
+ if (record.attempt === maxInvocationAttempts)
241
+ throw new Error(`Invalid output after one correction: ${String(error)}`);
242
+ return `\n\nCorrect the prior response format in this fresh inspection-only session. Do not modify files.\nPrior invalid response:\n${output}\nValidation errors:\n${String(error)}`;
243
+ }
244
+ async function saveImplementationSnapshot(runId, expected, provider, dependencies) {
245
+ const { workspace, project, store } = dependencies;
246
+ const snapshot = await workspace.inspect(project);
247
+ if (snapshot.head !== expected.head || snapshot.branch !== expected.branch)
248
+ throw new BlockedError("Agent changed branch or committed unexpectedly");
249
+ await store.patchRun(runId, { snapshot });
250
+ return snapshot.fingerprint === expected.fingerprint
251
+ ? undefined
252
+ : {
253
+ provider,
254
+ beforeFiles: expected.files,
255
+ afterFiles: snapshot.files,
256
+ };
257
+ }
258
+ async function acceptContribution(runId, candidate, dependencies) {
259
+ const run = await dependencies.store.run(runId);
260
+ await dependencies.store.patchRun(runId, {
261
+ contributionCandidates: [...(run.contributionCandidates ?? []), candidate],
262
+ });
166
263
  }
@@ -25,10 +25,9 @@ export declare class Operations {
25
25
  eligible(): Promise<boolean>;
26
26
  prepare(): Promise<void>;
27
27
  invoke(name: string, stage: Stage, task: InvocationTask): Promise<string>;
28
- private saveImplementationSnapshot;
29
- private acceptContribution;
30
28
  implement(): Promise<void>;
31
29
  validate(): Promise<boolean>;
30
+ private recordValidation;
32
31
  private prepareEvidence;
33
32
  writePublication(): Promise<void>;
34
33
  commit(): Promise<void>;
@@ -9,6 +9,7 @@ import { invokeStage } from "./invocation.js";
9
9
  import { defaultStagePrompts } from "./prompts.js";
10
10
  import { publicationSteps } from "./recovery.js";
11
11
  import { command } from "./runtime/process.js";
12
+ import { selectValidation } from "./validation-selection.js";
12
13
  const maxStepAttempts = 3;
13
14
  /** Reusable durable coding operations. Call from a registered DBOS workflow. */
14
15
  export class Operations {
@@ -76,8 +77,14 @@ export class Operations {
76
77
  return this.step("eligibility", async (run) => {
77
78
  const { hosting, project } = this.dependencies;
78
79
  const issue = await hosting.getIssue(run.issue.number);
79
- return (issue.open &&
80
- project.labels.every((label) => issue.labels.includes(label)));
80
+ if (!issue.open ||
81
+ !project.labels.every((label) => issue.labels.includes(label)))
82
+ return false;
83
+ const { profile } = selectValidation(run.issue.body, project);
84
+ await this.dependencies.store.patchRun(run.id, {
85
+ validationProfile: profile,
86
+ });
87
+ return true;
81
88
  });
82
89
  }
83
90
  async prepare() {
@@ -104,33 +111,8 @@ export class Operations {
104
111
  task,
105
112
  stepId: DBOS.stepID,
106
113
  dependencies: this.dependencies,
107
- saveImplementationSnapshot: (runId, expected, provider) => this.saveImplementationSnapshot(runId, expected, provider),
108
- acceptContribution: (runId, candidate) => this.acceptContribution(runId, candidate),
109
114
  }));
110
115
  }
111
- async saveImplementationSnapshot(runId, expected, provider) {
112
- const { workspace, project, store } = this.dependencies;
113
- const snapshot = await workspace.inspect(project);
114
- if (snapshot.head !== expected.head || snapshot.branch !== expected.branch)
115
- throw new BlockedError("Agent changed branch or committed unexpectedly");
116
- await store.patchRun(runId, { snapshot });
117
- return snapshot.fingerprint === expected.fingerprint
118
- ? undefined
119
- : {
120
- provider,
121
- beforeFiles: expected.files,
122
- afterFiles: snapshot.files,
123
- };
124
- }
125
- async acceptContribution(runId, candidate) {
126
- const run = await this.dependencies.store.run(runId);
127
- await this.dependencies.store.patchRun(runId, {
128
- contributionCandidates: [
129
- ...(run.contributionCandidates ?? []),
130
- candidate,
131
- ],
132
- });
133
- }
134
116
  async implement() {
135
117
  await this.invoke("implementation", this.dependencies.project.stages.implementation, {
136
118
  defaultPrompt: defaultStagePrompts.implementation,
@@ -139,7 +121,7 @@ export class Operations {
139
121
  }
140
122
  async validate() {
141
123
  return this.step("validation", async (run) => {
142
- const { project, workspace, store, signal, redact } = this.dependencies;
124
+ const { project, workspace, signal, redact } = this.dependencies;
143
125
  if (!run.snapshot)
144
126
  throw new BlockedError("Missing implementation snapshot");
145
127
  await workspace.verify(project, run.snapshot);
@@ -148,7 +130,8 @@ export class Operations {
148
130
  const validation = [];
149
131
  const directory = join(this.dependencies.artifacts, run.id);
150
132
  await mkdir(directory, { recursive: true, mode: 0o700 });
151
- for (const [index, check] of project.validation.entries()) {
133
+ const checks = selectValidation(run.issue.body, project).commands;
134
+ for (const [index, check] of checks.entries()) {
152
135
  const startedAt = new Date().toISOString();
153
136
  const log = join(directory, `validation-${index}.log`);
154
137
  let captured = "";
@@ -169,25 +152,23 @@ export class Operations {
169
152
  await appendFile(log, redact(captured + "\n" + String(error)), {
170
153
  mode: 0o600,
171
154
  });
172
- validation.push({
155
+ await this.recordValidation(run.id, validation, {
173
156
  ...check,
174
157
  exitCode: -1,
175
158
  log,
176
159
  startedAt,
177
160
  finishedAt: new Date().toISOString(),
178
161
  });
179
- await store.patchRun(run.id, { validation });
180
162
  throw error;
181
163
  }
182
164
  await appendFile(log, redact(captured), { mode: 0o600 });
183
- validation.push({
165
+ await this.recordValidation(run.id, validation, {
184
166
  ...check,
185
167
  exitCode: result.exitCode,
186
168
  log,
187
169
  startedAt,
188
170
  finishedAt: new Date().toISOString(),
189
171
  });
190
- await store.patchRun(run.id, { validation });
191
172
  if (result.exitCode !== 0)
192
173
  throw new Error(`Validation failed: ${check.command} (exit ${result.exitCode})`);
193
174
  }
@@ -195,6 +176,10 @@ export class Operations {
195
176
  return true;
196
177
  });
197
178
  }
179
+ async recordValidation(runId, validation, result) {
180
+ validation.push(result);
181
+ await this.dependencies.store.patchRun(runId, { validation });
182
+ }
198
183
  async prepareEvidence(name, published = false) {
199
184
  return this.step(name, async (run) => {
200
185
  const { project, workspace, artifacts, signal } = this.dependencies;