@mingchuno/agent-workflows 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -6
- package/dist/src/adapters/agents.js +6 -3
- package/dist/src/adapters/hosting.js +16 -10
- package/dist/src/adapters/sdk-protocol.d.ts +3 -3
- package/dist/src/adapters/sdk-protocol.js +9 -7
- package/dist/src/attribution.d.ts +9 -0
- package/dist/src/attribution.js +57 -0
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +80 -25
- package/dist/src/config.d.ts +24 -30
- package/dist/src/config.js +32 -27
- package/dist/src/defaults.d.ts +2 -0
- package/dist/src/defaults.js +2 -0
- package/dist/src/domain.d.ts +31 -4
- package/dist/src/domain.js +10 -3
- package/dist/src/evidence.d.ts +54 -0
- package/dist/src/evidence.js +214 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/invocation.d.ts +25 -0
- package/dist/src/invocation.js +166 -0
- package/dist/src/operations.d.ts +8 -2
- package/dist/src/operations.js +100 -136
- package/dist/src/prompts.d.ts +28 -0
- package/dist/src/prompts.js +63 -0
- package/dist/src/recovery.d.ts +19 -0
- package/dist/src/recovery.js +99 -0
- package/dist/src/runner.d.ts +7 -0
- package/dist/src/runner.js +170 -18
- package/dist/src/runtime/process.d.ts +2 -0
- package/dist/src/runtime/process.js +41 -12
- package/dist/src/store.d.ts +21 -2
- package/dist/src/store.js +122 -1
- package/dist/src/tui/actions.d.ts +16 -0
- package/dist/src/tui/actions.js +23 -0
- package/dist/src/tui/constants.d.ts +6 -0
- package/dist/src/tui/constants.js +3 -0
- package/dist/src/{tui-data.d.ts → tui/data.d.ts} +10 -7
- package/dist/src/tui/data.js +146 -0
- package/dist/src/tui/dialogs.d.ts +17 -0
- package/dist/src/tui/dialogs.js +149 -0
- package/dist/src/tui/format.d.ts +7 -0
- package/dist/src/tui/format.js +62 -0
- package/dist/src/tui/index.d.ts +3 -0
- package/dist/src/tui/index.js +2 -0
- package/dist/src/tui/layout.d.ts +25 -0
- package/dist/src/tui/layout.js +36 -0
- package/dist/src/tui/log-file.d.ts +26 -0
- package/dist/src/tui/log-file.js +156 -0
- package/dist/src/tui/log.d.ts +11 -0
- package/dist/src/tui/log.js +90 -0
- package/dist/src/tui/monitor.d.ts +10 -0
- package/dist/src/tui/monitor.js +284 -0
- package/dist/src/tui/notifications.d.ts +23 -0
- package/dist/src/tui/notifications.js +104 -0
- package/dist/src/tui/text.d.ts +3 -0
- package/dist/src/tui/text.js +10 -0
- package/dist/src/tui/use-log-controller.d.ts +27 -0
- package/dist/src/tui/use-log-controller.js +192 -0
- package/dist/src/tui/views.d.ts +25 -0
- package/dist/src/tui/views.js +327 -0
- package/dist/src/workspace.js +21 -8
- package/docs/api.md +132 -8
- package/docs/architecture.md +21 -4
- package/docs/configuration.md +181 -8
- package/docs/database.md +7 -0
- package/docs/operations.md +160 -5
- package/docs/providers.md +58 -2
- package/docs/releases.md +34 -79
- package/examples/config.ts +6 -6
- package/examples/run.ts +4 -1
- package/package.json +4 -2
- package/dist/src/tui-data.js +0 -89
- package/dist/src/tui.d.ts +0 -5
- package/dist/src/tui.js +0 -69
- package/docs/acceptance.md +0 -35
package/dist/src/operations.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { Project, Stage } from "./config.js";
|
|
2
2
|
import { type AgentAdapter, type HostingAdapter, type RunRecord, type Workspace } from "./domain.js";
|
|
3
|
+
import { type InvocationTask } from "./invocation.js";
|
|
3
4
|
import type { Store } from "./store.js";
|
|
5
|
+
export type { InvocationTask } from "./invocation.js";
|
|
4
6
|
export interface OperationDependencies {
|
|
7
|
+
promptBaseDirectory?: string;
|
|
5
8
|
store: Store;
|
|
6
9
|
project: Project;
|
|
7
10
|
workspace: Workspace;
|
|
@@ -10,6 +13,8 @@ export interface OperationDependencies {
|
|
|
10
13
|
artifacts: string;
|
|
11
14
|
signal: AbortSignal;
|
|
12
15
|
redact: (text: string) => string;
|
|
16
|
+
beforeStep?: () => Promise<void>;
|
|
17
|
+
executionFingerprint?: () => Promise<string>;
|
|
13
18
|
}
|
|
14
19
|
/** Reusable durable coding operations. Call from a registered DBOS workflow. */
|
|
15
20
|
export declare class Operations {
|
|
@@ -19,11 +24,12 @@ export declare class Operations {
|
|
|
19
24
|
step<T>(name: string, operation: (run: RunRecord) => Promise<T>): Promise<T>;
|
|
20
25
|
eligible(): Promise<boolean>;
|
|
21
26
|
prepare(): Promise<void>;
|
|
22
|
-
invoke(name: string, stage: Stage,
|
|
23
|
-
private prepareInvocationPrompt;
|
|
27
|
+
invoke(name: string, stage: Stage, task: InvocationTask): Promise<string>;
|
|
24
28
|
private saveImplementationSnapshot;
|
|
29
|
+
private acceptContribution;
|
|
25
30
|
implement(): Promise<void>;
|
|
26
31
|
validate(): Promise<boolean>;
|
|
32
|
+
private prepareEvidence;
|
|
27
33
|
writePublication(): Promise<void>;
|
|
28
34
|
commit(): Promise<void>;
|
|
29
35
|
push(): Promise<void>;
|
package/dist/src/operations.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { appendFile, mkdir
|
|
3
|
-
import { join
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
4
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
5
|
-
import {
|
|
5
|
+
import { contributingProviders, finalizeCommitMessage } from "./attribution.js";
|
|
6
6
|
import { BlockedError, isBlockedError, publicationSchema, reviewSchema, } from "./domain.js";
|
|
7
|
+
import { captureEvidence, evidenceContext, } from "./evidence.js";
|
|
8
|
+
import { invokeStage } from "./invocation.js";
|
|
9
|
+
import { defaultStagePrompts } from "./prompts.js";
|
|
10
|
+
import { publicationSteps } from "./recovery.js";
|
|
7
11
|
import { command } from "./runtime/process.js";
|
|
12
|
+
const maxStepAttempts = 3;
|
|
8
13
|
/** Reusable durable coding operations. Call from a registered DBOS workflow. */
|
|
9
14
|
export class Operations {
|
|
10
15
|
runId;
|
|
@@ -17,10 +22,12 @@ export class Operations {
|
|
|
17
22
|
return DBOS.runStep(async () => {
|
|
18
23
|
const { store, signal } = this.dependencies;
|
|
19
24
|
signal.throwIfAborted();
|
|
25
|
+
await this.dependencies.beforeStep?.();
|
|
20
26
|
const run = await store.run(this.runId);
|
|
21
27
|
await store.patchRun(this.runId, { phase: name });
|
|
22
28
|
await store.emit(this.runId, "step", {
|
|
23
29
|
name,
|
|
30
|
+
executionId: DBOS.workflowID,
|
|
24
31
|
stepId: DBOS.stepID,
|
|
25
32
|
attempt: DBOS.stepStatus?.currentAttempt ?? 1,
|
|
26
33
|
status: "running",
|
|
@@ -31,6 +38,7 @@ export class Operations {
|
|
|
31
38
|
const result = await operation(run);
|
|
32
39
|
await store.emit(this.runId, "step", {
|
|
33
40
|
name,
|
|
41
|
+
executionId: DBOS.workflowID,
|
|
34
42
|
stepId: DBOS.stepID,
|
|
35
43
|
attempt: DBOS.stepStatus?.currentAttempt ?? 1,
|
|
36
44
|
status: "completed",
|
|
@@ -38,8 +46,13 @@ export class Operations {
|
|
|
38
46
|
return result;
|
|
39
47
|
}
|
|
40
48
|
catch (error) {
|
|
49
|
+
if (!publicationSteps.includes(name) ||
|
|
50
|
+
isBlockedError(error) ||
|
|
51
|
+
DBOS.stepStatus?.currentAttempt === maxStepAttempts)
|
|
52
|
+
await store.patchRun(this.runId, { failedStep: DBOS.stepID });
|
|
41
53
|
await store.emit(this.runId, "step", {
|
|
42
54
|
name,
|
|
55
|
+
executionId: DBOS.workflowID,
|
|
43
56
|
stepId: DBOS.stepID,
|
|
44
57
|
attempt: DBOS.stepStatus?.currentAttempt ?? 1,
|
|
45
58
|
status: "failed",
|
|
@@ -52,12 +65,8 @@ export class Operations {
|
|
|
52
65
|
}
|
|
53
66
|
}, {
|
|
54
67
|
name,
|
|
55
|
-
retriesAllowed:
|
|
56
|
-
|
|
57
|
-
"change-request",
|
|
58
|
-
"review-publication",
|
|
59
|
-
].includes(name),
|
|
60
|
-
maxAttempts: 3,
|
|
68
|
+
retriesAllowed: publicationSteps.includes(name),
|
|
69
|
+
maxAttempts: maxStepAttempts,
|
|
61
70
|
intervalSeconds: 0.2,
|
|
62
71
|
backoffRate: 2,
|
|
63
72
|
shouldRetry: (error) => !isBlockedError(error),
|
|
@@ -75,138 +84,58 @@ export class Operations {
|
|
|
75
84
|
await this.step("prepare", async (run) => {
|
|
76
85
|
const { workspace, project, store } = this.dependencies;
|
|
77
86
|
const snapshot = await workspace.prepare(project, run.branch, this.dependencies.signal);
|
|
87
|
+
const execution = run.executions?.at(-1);
|
|
88
|
+
if (execution?.recoverySupported &&
|
|
89
|
+
this.dependencies.executionFingerprint)
|
|
90
|
+
execution.fingerprint = await this.dependencies.executionFingerprint();
|
|
78
91
|
await store.patchRun(run.id, {
|
|
79
92
|
base: snapshot.head,
|
|
80
93
|
snapshot,
|
|
81
94
|
outcome: "running",
|
|
95
|
+
executions: run.executions,
|
|
82
96
|
});
|
|
83
97
|
});
|
|
84
98
|
}
|
|
85
|
-
async invoke(name, stage,
|
|
86
|
-
return this.step(name,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
invocation.sessionState = "unavailable";
|
|
96
|
-
await store.saveInvocation(invocation);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
throw new BlockedError(`Interrupted agent stage ${name}; inspect existing sessions before explicit retry`);
|
|
100
|
-
}
|
|
101
|
-
if (!run.snapshot)
|
|
102
|
-
throw new BlockedError("Missing workspace snapshot");
|
|
103
|
-
await workspace.verify(project, run.snapshot);
|
|
104
|
-
const profile = resolveProfile(project.agent, stage.profile);
|
|
105
|
-
const adapter = agents[profile.provider];
|
|
106
|
-
if (!adapter)
|
|
107
|
-
throw new Error(`Missing agent adapter ${profile.provider}`);
|
|
108
|
-
const invocationSignal = AbortSignal.any([
|
|
109
|
-
signal,
|
|
110
|
-
AbortSignal.timeout(stage.timeoutMs),
|
|
111
|
-
]);
|
|
112
|
-
const effective = await adapter.validate(profile, invocationSignal);
|
|
113
|
-
const { skills, fullPrompt } = await this.prepareInvocationPrompt(stage, () => prompt(run));
|
|
114
|
-
const id = randomUUID();
|
|
115
|
-
const directory = join(this.dependencies.artifacts, run.id);
|
|
116
|
-
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
117
|
-
const record = {
|
|
118
|
-
id,
|
|
119
|
-
runId: run.id,
|
|
120
|
-
projectId: project.id,
|
|
121
|
-
step: name,
|
|
122
|
-
stepId: DBOS.stepID,
|
|
123
|
-
attempt: previous.length + 1,
|
|
124
|
-
provider: profile.provider,
|
|
125
|
-
sessionId: null,
|
|
126
|
-
sessionState: "pending",
|
|
127
|
-
requested: profile,
|
|
128
|
-
effective,
|
|
129
|
-
prompt: redact(fullPrompt),
|
|
130
|
-
skills: skills.map((skill) => ({
|
|
131
|
-
...skill,
|
|
132
|
-
content: redact(skill.content),
|
|
133
|
-
})),
|
|
134
|
-
outcome: "running",
|
|
135
|
-
startedAt: new Date().toISOString(),
|
|
136
|
-
log: join(directory, `${id}.jsonl`),
|
|
137
|
-
};
|
|
138
|
-
await store.saveInvocation(record);
|
|
139
|
-
try {
|
|
140
|
-
const output = await adapter.invoke({
|
|
141
|
-
id,
|
|
142
|
-
runId: run.id,
|
|
143
|
-
step: name,
|
|
144
|
-
cwd: project.checkout,
|
|
145
|
-
prompt: fullPrompt,
|
|
146
|
-
profile,
|
|
147
|
-
skills: skills.map((skill) => skill.path),
|
|
148
|
-
processFile: record.log + ".process.json",
|
|
149
|
-
readOnly,
|
|
150
|
-
signal: invocationSignal,
|
|
151
|
-
timeoutMs: stage.timeoutMs,
|
|
152
|
-
session: async (sessionId) => {
|
|
153
|
-
record.sessionId = sessionId;
|
|
154
|
-
record.sessionState = "available";
|
|
155
|
-
await store.saveInvocation(record);
|
|
156
|
-
},
|
|
157
|
-
event: async (event) => {
|
|
158
|
-
await appendFile(record.log, redact(JSON.stringify(event)) + "\n", {
|
|
159
|
-
mode: 0o600,
|
|
160
|
-
});
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
invocationSignal.throwIfAborted();
|
|
164
|
-
if (readOnly)
|
|
165
|
-
await workspace.verify(project, run.snapshot);
|
|
166
|
-
else
|
|
167
|
-
await this.saveImplementationSnapshot(run.id, run.snapshot);
|
|
168
|
-
record.outcome = "completed";
|
|
169
|
-
return redact(output);
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
record.outcome = "failed";
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
finally {
|
|
176
|
-
record.finishedAt = new Date().toISOString();
|
|
177
|
-
if (!record.sessionId)
|
|
178
|
-
record.sessionState = "unavailable";
|
|
179
|
-
await store.saveInvocation(record);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
async prepareInvocationPrompt(stage, prompt) {
|
|
184
|
-
const { project } = this.dependencies;
|
|
185
|
-
const skills = await Promise.all(stage.skills.map(async (path) => {
|
|
186
|
-
const absolute = resolve(project.checkout, path);
|
|
187
|
-
const content = await readFile(absolute, "utf8");
|
|
188
|
-
return {
|
|
189
|
-
path: absolute,
|
|
190
|
-
sha256: createHash("sha256").update(content).digest("hex"),
|
|
191
|
-
content,
|
|
192
|
-
};
|
|
99
|
+
async invoke(name, stage, task) {
|
|
100
|
+
return this.step(name, (run) => invokeStage({
|
|
101
|
+
run,
|
|
102
|
+
name,
|
|
103
|
+
stage,
|
|
104
|
+
task,
|
|
105
|
+
stepId: DBOS.stepID,
|
|
106
|
+
dependencies: this.dependencies,
|
|
107
|
+
saveImplementationSnapshot: (runId, expected, provider) => this.saveImplementationSnapshot(runId, expected, provider),
|
|
108
|
+
acceptContribution: (runId, candidate) => this.acceptContribution(runId, candidate),
|
|
193
109
|
}));
|
|
194
|
-
const fullPrompt = [
|
|
195
|
-
stage.prompt,
|
|
196
|
-
prompt(),
|
|
197
|
-
...skills.map((skill) => `Apply this selected skill (${skill.path}):\n${skill.content}`),
|
|
198
|
-
].join("\n\n");
|
|
199
|
-
return { skills, fullPrompt };
|
|
200
110
|
}
|
|
201
|
-
async saveImplementationSnapshot(runId, expected) {
|
|
111
|
+
async saveImplementationSnapshot(runId, expected, provider) {
|
|
202
112
|
const { workspace, project, store } = this.dependencies;
|
|
203
113
|
const snapshot = await workspace.inspect(project);
|
|
204
114
|
if (snapshot.head !== expected.head || snapshot.branch !== expected.branch)
|
|
205
115
|
throw new BlockedError("Agent changed branch or committed unexpectedly");
|
|
206
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
|
+
});
|
|
207
133
|
}
|
|
208
134
|
async implement() {
|
|
209
|
-
await this.invoke("implementation", this.dependencies.project.stages.implementation,
|
|
135
|
+
await this.invoke("implementation", this.dependencies.project.stages.implementation, {
|
|
136
|
+
defaultPrompt: defaultStagePrompts.implementation,
|
|
137
|
+
context: (run) => `Issue: ${JSON.stringify(run.issue)}`,
|
|
138
|
+
});
|
|
210
139
|
}
|
|
211
140
|
async validate() {
|
|
212
141
|
return this.step("validation", async (run) => {
|
|
@@ -266,11 +195,41 @@ export class Operations {
|
|
|
266
195
|
return true;
|
|
267
196
|
});
|
|
268
197
|
}
|
|
198
|
+
async prepareEvidence(name, published = false) {
|
|
199
|
+
return this.step(name, async (run) => {
|
|
200
|
+
const { project, workspace, artifacts, signal } = this.dependencies;
|
|
201
|
+
if (!run.snapshot)
|
|
202
|
+
throw new BlockedError("Missing workspace snapshot");
|
|
203
|
+
if (published && (!run.base || !run.head))
|
|
204
|
+
throw new BlockedError("Missing published revisions");
|
|
205
|
+
await workspace.verify(project, run.snapshot);
|
|
206
|
+
const evidence = await captureEvidence({
|
|
207
|
+
project,
|
|
208
|
+
snapshot: run.snapshot,
|
|
209
|
+
directory: join(artifacts, run.id, `${name}-${randomUUID()}`),
|
|
210
|
+
signal,
|
|
211
|
+
revisions: published ? { base: run.base, head: run.head } : undefined,
|
|
212
|
+
});
|
|
213
|
+
await workspace.verify(project, run.snapshot);
|
|
214
|
+
return evidence;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
269
217
|
async writePublication() {
|
|
270
|
-
const
|
|
218
|
+
const evidence = await this.prepareEvidence("publication-input");
|
|
219
|
+
const output = await this.invoke("publication", this.dependencies.project.stages.publication, {
|
|
220
|
+
defaultPrompt: defaultStagePrompts.publication,
|
|
221
|
+
readOnly: true,
|
|
222
|
+
outputContract: publicationSchema,
|
|
223
|
+
evidence,
|
|
224
|
+
context: (run) => `${evidenceContext(evidence)}\nIssue: ${JSON.stringify(run.issue)}\nValidation: ${JSON.stringify(run.validation ?? [])}`,
|
|
225
|
+
});
|
|
271
226
|
await this.step("publication-content", async (run) => {
|
|
227
|
+
if (!run.snapshot)
|
|
228
|
+
throw new Error("Missing publication snapshot");
|
|
229
|
+
const providers = contributingProviders(run.contributionCandidates ?? [], run.snapshot);
|
|
272
230
|
await this.dependencies.store.patchRun(run.id, {
|
|
273
|
-
publication: publicationSchema.parse(JSON.parse(output)),
|
|
231
|
+
publication: finalizeCommitMessage(publicationSchema.parse(JSON.parse(output)), this.dependencies.project, providers, run.id),
|
|
232
|
+
contributingProviders: providers,
|
|
274
233
|
});
|
|
275
234
|
});
|
|
276
235
|
}
|
|
@@ -314,19 +273,22 @@ export class Operations {
|
|
|
314
273
|
});
|
|
315
274
|
}
|
|
316
275
|
async review() {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
276
|
+
const evidence = await this.prepareEvidence("review-input", true);
|
|
277
|
+
const output = await this.invoke("review", this.dependencies.project.stages.review, {
|
|
278
|
+
defaultPrompt: defaultStagePrompts.review,
|
|
279
|
+
readOnly: true,
|
|
280
|
+
outputContract: reviewSchema,
|
|
281
|
+
evidence,
|
|
282
|
+
context: (run) => `${evidenceContext(evidence)}\nIssue: ${JSON.stringify(run.issue)}\nValidation: ${JSON.stringify(run.validation ?? [])}\nSet complete=false with limitations if any required evidence cannot be inspected. Never report an incomplete review as clean. Use null for finding path/line where no valid added-line location exists.`,
|
|
323
283
|
});
|
|
324
|
-
const output = await this.invoke("review", this.dependencies.project.stages.review, (run) => `Independently review this published revision without editing files. Return ONLY JSON {"summary":"...","findings":[{"body":"...","path":"optional relative path","line":1}]}. Omit path/line where no valid added-line location exists.\nIssue: ${JSON.stringify(run.issue)}\nExact head: ${run.head}\nValidation: ${JSON.stringify(run.validation ?? [])}\nPublished diff:\n${diff}`, true);
|
|
325
284
|
await this.step("review-content", async (run) => {
|
|
285
|
+
const review = reviewSchema.parse(JSON.parse(output));
|
|
326
286
|
await this.dependencies.store.patchRun(run.id, {
|
|
327
|
-
review
|
|
287
|
+
review,
|
|
328
288
|
reviewHead: run.head,
|
|
329
289
|
});
|
|
290
|
+
if (!review.complete)
|
|
291
|
+
throw new BlockedError("Incomplete review; partial findings and limitations retained locally");
|
|
330
292
|
});
|
|
331
293
|
}
|
|
332
294
|
async publishReview() {
|
|
@@ -334,6 +296,8 @@ export class Operations {
|
|
|
334
296
|
const { hosting, project } = this.dependencies;
|
|
335
297
|
if (!run.change || !run.review || !run.reviewHead || !run.base)
|
|
336
298
|
throw new Error("Missing review");
|
|
299
|
+
if (run.review.complete !== true)
|
|
300
|
+
throw new BlockedError("Incomplete or historical review; use retry for a fresh inspection");
|
|
337
301
|
if ((await hosting.head(run.change)) !== run.reviewHead)
|
|
338
302
|
throw new BlockedError("Review stale: remote head changed");
|
|
339
303
|
const diff = (await command("git", ["diff", "--unified=0", run.base, run.reviewHead], { cwd: project.checkout })).stdout;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Project, Stage } from "./config.js";
|
|
2
|
+
export declare const defaultStagePrompts: {
|
|
3
|
+
readonly implementation: `Implement the supplied issue in the current checkout. Follow repository
|
|
4
|
+
instructions and existing conventions. Keep changes focused on the issue's
|
|
5
|
+
requirements, and add or update tests where needed to verify the behavior.`;
|
|
6
|
+
readonly publication: `Prepare a Git commit message and a pull request or merge request title and
|
|
7
|
+
description for the supplied changes. Follow repository conventions. Describe
|
|
8
|
+
what changed and why, summarize the recorded validation accurately, and state
|
|
9
|
+
material limitations. Do not claim checks passed unless the supplied evidence
|
|
10
|
+
shows they ran and passed.`;
|
|
11
|
+
readonly review: `Independently review the supplied published changes against the issue's
|
|
12
|
+
requirements and repository conventions. Inspect the change artifacts and
|
|
13
|
+
relevant source for correctness, regressions, and missing validation. Report
|
|
14
|
+
actionable findings with supporting locations where possible. State any gaps
|
|
15
|
+
in inspection explicitly; do not present an incomplete review as a clean review.`;
|
|
16
|
+
};
|
|
17
|
+
export interface ResolvedPrompt {
|
|
18
|
+
source: "default" | "inline" | "file";
|
|
19
|
+
path?: string;
|
|
20
|
+
content: string;
|
|
21
|
+
sha256: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function sha256(content: string | Buffer): string;
|
|
24
|
+
/** Resolve overrides once. Defaults belong to the caller, not the stage name. */
|
|
25
|
+
export declare function resolveStagePrompt(stage: Stage, defaultTask: string, baseDirectory?: string): ResolvedPrompt;
|
|
26
|
+
export declare function projectPrompts(project: Project, baseDirectory?: string): {
|
|
27
|
+
[k: string]: ResolvedPrompt;
|
|
28
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, resolve } from "node:path";
|
|
4
|
+
export const defaultStagePrompts = {
|
|
5
|
+
implementation: `Implement the supplied issue in the current checkout. Follow repository
|
|
6
|
+
instructions and existing conventions. Keep changes focused on the issue's
|
|
7
|
+
requirements, and add or update tests where needed to verify the behavior.`,
|
|
8
|
+
publication: `Prepare a Git commit message and a pull request or merge request title and
|
|
9
|
+
description for the supplied changes. Follow repository conventions. Describe
|
|
10
|
+
what changed and why, summarize the recorded validation accurately, and state
|
|
11
|
+
material limitations. Do not claim checks passed unless the supplied evidence
|
|
12
|
+
shows they ran and passed.`,
|
|
13
|
+
review: `Independently review the supplied published changes against the issue's
|
|
14
|
+
requirements and repository conventions. Inspect the change artifacts and
|
|
15
|
+
relevant source for correctness, regressions, and missing validation. Report
|
|
16
|
+
actionable findings with supporting locations where possible. State any gaps
|
|
17
|
+
in inspection explicitly; do not present an incomplete review as a clean review.`,
|
|
18
|
+
};
|
|
19
|
+
export function sha256(content) {
|
|
20
|
+
return createHash("sha256").update(content).digest("hex");
|
|
21
|
+
}
|
|
22
|
+
const overrides = new WeakMap();
|
|
23
|
+
/** Resolve overrides once. Defaults belong to the caller, not the stage name. */
|
|
24
|
+
export function resolveStagePrompt(stage, defaultTask, baseDirectory) {
|
|
25
|
+
const cached = overrides.get(stage);
|
|
26
|
+
if (cached)
|
|
27
|
+
return cached;
|
|
28
|
+
if (stage.prompt !== undefined && stage.promptFile !== undefined)
|
|
29
|
+
throw new Error("Specify either prompt or promptFile, never both");
|
|
30
|
+
let content = stage.prompt ?? defaultTask;
|
|
31
|
+
let path;
|
|
32
|
+
if (stage.promptFile !== undefined) {
|
|
33
|
+
if (!isAbsolute(stage.promptFile) && !baseDirectory)
|
|
34
|
+
throw new Error("Relative promptFile requires an explicit base directory");
|
|
35
|
+
path = resolve(baseDirectory ?? "/", stage.promptFile);
|
|
36
|
+
try {
|
|
37
|
+
content = new TextDecoder("utf-8", {
|
|
38
|
+
fatal: true,
|
|
39
|
+
ignoreBOM: true,
|
|
40
|
+
}).decode(readFileSync(path));
|
|
41
|
+
}
|
|
42
|
+
catch (cause) {
|
|
43
|
+
throw new Error(`Cannot read UTF-8 promptFile ${path}`, { cause });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!content.trim())
|
|
47
|
+
throw new Error(`Stage prompt must be nonblank${path ? `: ${path}` : ""}`);
|
|
48
|
+
const result = {
|
|
49
|
+
source: path ? "file" : stage.prompt !== undefined ? "inline" : "default",
|
|
50
|
+
...(path ? { path } : {}),
|
|
51
|
+
content,
|
|
52
|
+
sha256: sha256(content),
|
|
53
|
+
};
|
|
54
|
+
if (result.source === "file")
|
|
55
|
+
overrides.set(stage, result);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
export function projectPrompts(project, baseDirectory) {
|
|
59
|
+
return Object.fromEntries(Object.entries(defaultStagePrompts).map(([name, task]) => [
|
|
60
|
+
name,
|
|
61
|
+
resolveStagePrompt(project.stages[name], task, baseDirectory),
|
|
62
|
+
]));
|
|
63
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Project } from "./config.js";
|
|
2
|
+
import { type HostingAdapter, type RunRecord, type Workspace } from "./domain.js";
|
|
3
|
+
import type { Store } from "./store.js";
|
|
4
|
+
export declare const publicationSteps: readonly string[];
|
|
5
|
+
export declare function recoveryUnavailable(run: RunRecord): string | undefined;
|
|
6
|
+
/** Hash execution inputs without reading credential environment values. */
|
|
7
|
+
export declare function executionFingerprint(project: Project, workflowVersion: string, signal?: AbortSignal): Promise<string>;
|
|
8
|
+
interface RecoveryDependencies {
|
|
9
|
+
project: Project;
|
|
10
|
+
workspace: Workspace;
|
|
11
|
+
hosting: HostingAdapter;
|
|
12
|
+
store: Pick<Store, "invocations">;
|
|
13
|
+
stateDirectory: string;
|
|
14
|
+
workflowVersion: string;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}
|
|
17
|
+
/** Read-only checks shared by admission and the first non-replayed operation. */
|
|
18
|
+
export declare function verifyPublicationRecovery(run: RunRecord, dependencies: RecoveryDependencies): Promise<void>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { BlockedError, } from "./domain.js";
|
|
5
|
+
import { verifyEvidence } from "./evidence.js";
|
|
6
|
+
import { projectPrompts } from "./prompts.js";
|
|
7
|
+
import { assertProcessesStopped } from "./runtime/ownership.js";
|
|
8
|
+
import { command } from "./runtime/process.js";
|
|
9
|
+
export const publicationSteps = [
|
|
10
|
+
"push",
|
|
11
|
+
"change-request",
|
|
12
|
+
"review-publication",
|
|
13
|
+
];
|
|
14
|
+
export function recoveryUnavailable(run) {
|
|
15
|
+
if (run.outcome !== "failed")
|
|
16
|
+
return "Only failed publication runs can be recovered";
|
|
17
|
+
if (!publicationSteps.includes(run.phase))
|
|
18
|
+
return `Recovery does not support ${run.phase}`;
|
|
19
|
+
const execution = run.executions?.at(-1);
|
|
20
|
+
if (!execution?.fingerprint || execution.failedStep === undefined)
|
|
21
|
+
return "Run has no recovery metadata; use retry for a fresh attempt";
|
|
22
|
+
if (!execution.recoverySupported)
|
|
23
|
+
return "Publication recovery supports the default workflow only";
|
|
24
|
+
if (!run.head || !run.base || !run.snapshot || !run.publication)
|
|
25
|
+
return "Missing committed publication evidence";
|
|
26
|
+
if (run.phase === "review-publication" &&
|
|
27
|
+
(!run.change || !run.review || run.reviewHead !== run.head))
|
|
28
|
+
return "Missing review for the published revision";
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Hash execution inputs without reading credential environment values. */
|
|
32
|
+
export async function executionFingerprint(project, workflowVersion, signal) {
|
|
33
|
+
const { pollIntervalMs: _poll, labels: _labels, branchTemplate: _branch, hosting, ...execution } = project;
|
|
34
|
+
const { tokenEnv: _token, ...destination } = hosting;
|
|
35
|
+
const prompts = projectPrompts(project);
|
|
36
|
+
const remote = await command("git", ["remote", "get-url", "--push", "--all", project.remote], { cwd: project.checkout, signal });
|
|
37
|
+
const fetchRemote = await command("git", ["remote", "get-url", "--all", project.remote], { cwd: project.checkout, signal });
|
|
38
|
+
return createHash("sha256")
|
|
39
|
+
.update(JSON.stringify({
|
|
40
|
+
version: 2,
|
|
41
|
+
workflowVersion,
|
|
42
|
+
execution,
|
|
43
|
+
destination,
|
|
44
|
+
prompts,
|
|
45
|
+
remote: remote.stdout,
|
|
46
|
+
fetchRemote: fetchRemote.stdout,
|
|
47
|
+
}))
|
|
48
|
+
.digest("hex");
|
|
49
|
+
}
|
|
50
|
+
/** Read-only checks shared by admission and the first non-replayed operation. */
|
|
51
|
+
export async function verifyPublicationRecovery(run, dependencies) {
|
|
52
|
+
const { project, workspace, hosting, store, stateDirectory, workflowVersion, signal, } = dependencies;
|
|
53
|
+
if (run.checkout !== project.checkout)
|
|
54
|
+
throw new BlockedError("Configured checkout differs from the recorded run checkout");
|
|
55
|
+
const fingerprint = await executionFingerprint(project, workflowVersion, signal);
|
|
56
|
+
if (fingerprint !== run.executions?.at(-1)?.fingerprint)
|
|
57
|
+
throw new BlockedError("Execution configuration or prompts changed; use retry");
|
|
58
|
+
await assertProcessesStopped(resolve(stateDirectory, run.id));
|
|
59
|
+
const gitDirectory = (await command("git", ["rev-parse", "--absolute-git-dir"], {
|
|
60
|
+
cwd: project.checkout,
|
|
61
|
+
signal,
|
|
62
|
+
})).stdout.trim();
|
|
63
|
+
await assertProcessesStopped(resolve(gitDirectory, "agent-workflows-processes"));
|
|
64
|
+
await workspace.check(project);
|
|
65
|
+
const snapshot = await workspace.inspect(project);
|
|
66
|
+
if (snapshot.branch !== run.branch || snapshot.head !== run.head)
|
|
67
|
+
throw new BlockedError("Recovery requires the original branch and commit");
|
|
68
|
+
await workspace.verify(project, run.snapshot);
|
|
69
|
+
await command("git", ["cat-file", "-e", `${run.base}^{commit}`], {
|
|
70
|
+
cwd: project.checkout,
|
|
71
|
+
signal,
|
|
72
|
+
});
|
|
73
|
+
const invocations = await store.invocations(run.id);
|
|
74
|
+
for (const invocation of invocations) {
|
|
75
|
+
if (invocation.evidence)
|
|
76
|
+
await verifyEvidence(invocation.evidence);
|
|
77
|
+
}
|
|
78
|
+
for (const path of [
|
|
79
|
+
...invocations.map((item) => item.log),
|
|
80
|
+
...(run.validation ?? []).map((item) => item.log),
|
|
81
|
+
]) {
|
|
82
|
+
try {
|
|
83
|
+
await access(path);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new BlockedError(`Recovery artifact unavailable: ${path}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const remote = (await command("git", ["ls-remote", "--heads", project.remote, `refs/heads/${run.branch}`], { cwd: project.checkout, signal })).stdout.trim();
|
|
90
|
+
if ((remote && remote.split(/\s/)[0] !== run.head) ||
|
|
91
|
+
(!remote && run.phase !== "push"))
|
|
92
|
+
throw new BlockedError("Published remote revision changed; recovery refused");
|
|
93
|
+
const change = await hosting.findChange(run.branch);
|
|
94
|
+
if (change && change.head !== run.head)
|
|
95
|
+
throw new BlockedError("Existing change request has a different head");
|
|
96
|
+
if (run.phase === "review-publication" &&
|
|
97
|
+
(!run.change || (await hosting.head(run.change)) !== run.reviewHead))
|
|
98
|
+
throw new BlockedError("Review stale: remote head changed");
|
|
99
|
+
}
|
package/dist/src/runner.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { type AgentAdapter, type HostingAdapter, type Workspace } from "./domain
|
|
|
3
3
|
import { Operations } from "./operations.js";
|
|
4
4
|
import { Store } from "./store.js";
|
|
5
5
|
export interface RunnerOptions {
|
|
6
|
+
pathBaseDirectory?: string;
|
|
7
|
+
promptBaseDirectory?: string;
|
|
6
8
|
config: Configuration;
|
|
7
9
|
databaseUrl: string;
|
|
8
10
|
hosting: (project: Project) => HostingAdapter;
|
|
@@ -18,6 +20,7 @@ export declare class Runner {
|
|
|
18
20
|
private readonly controllers;
|
|
19
21
|
private readonly active;
|
|
20
22
|
private readonly hosting;
|
|
23
|
+
private readonly promptBaseDirectory;
|
|
21
24
|
private workflow;
|
|
22
25
|
private readonly ownership;
|
|
23
26
|
private stopping;
|
|
@@ -36,12 +39,16 @@ export declare class Runner {
|
|
|
36
39
|
private processCommands;
|
|
37
40
|
private pollDueProjects;
|
|
38
41
|
private dispatchRuns;
|
|
42
|
+
private dispatch;
|
|
39
43
|
private recordWorkflowFailure;
|
|
40
44
|
private execute;
|
|
41
45
|
private executeOwned;
|
|
42
46
|
pause(projectId: string): Promise<void>;
|
|
47
|
+
private initializeExecution;
|
|
43
48
|
resume(projectId: string): Promise<void>;
|
|
44
49
|
stop(runId: string): Promise<void>;
|
|
45
50
|
retry(runId: string, commandId?: string): Promise<string>;
|
|
51
|
+
recover(runId: string, commandId?: string): Promise<string>;
|
|
52
|
+
private checkRecoveryState;
|
|
46
53
|
shutdown(): Promise<void>;
|
|
47
54
|
}
|