@mingchuno/agent-workflows 0.2.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 +6 -1
- package/dist/src/attribution.d.ts +9 -0
- package/dist/src/attribution.js +57 -0
- package/dist/src/cli.js +47 -13
- package/dist/src/config.d.ts +2 -8
- package/dist/src/config.js +1 -1
- package/dist/src/domain.d.ts +7 -0
- package/dist/src/invocation.d.ts +4 -3
- package/dist/src/invocation.js +6 -3
- package/dist/src/operations.d.ts +1 -0
- package/dist/src/operations.js +25 -3
- package/dist/src/runner.d.ts +2 -0
- package/dist/src/runner.js +28 -3
- package/dist/src/tui/data.d.ts +2 -1
- package/dist/src/tui/data.js +7 -2
- package/dist/src/tui/index.d.ts +1 -0
- package/dist/src/tui/index.js +1 -0
- package/dist/src/tui/layout.d.ts +1 -1
- package/dist/src/tui/layout.js +2 -2
- package/dist/src/tui/monitor.d.ts +3 -1
- package/dist/src/tui/monitor.js +93 -31
- package/dist/src/tui/notifications.d.ts +23 -0
- package/dist/src/tui/notifications.js +104 -0
- package/dist/src/tui/views.d.ts +10 -2
- package/dist/src/tui/views.js +272 -42
- package/dist/src/workspace.js +21 -8
- package/docs/api.md +15 -4
- package/docs/configuration.md +50 -9
- package/docs/operations.md +45 -5
- package/examples/config.ts +4 -4
- package/examples/run.ts +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,12 @@ agent-workflows init
|
|
|
38
38
|
|
|
39
39
|
For SDK use, install locally with `npm install @mingchuno/agent-workflows`. Both interfaces ship in the same package.
|
|
40
40
|
|
|
41
|
-
Edit `agent-workflows.json`: set the checkout, hosting origin/repository,
|
|
41
|
+
Edit `agent-workflows.json`: set the checkout, hosting origin/repository, agent
|
|
42
|
+
stages and validation commands. Relative checkout, state and prompt-file paths
|
|
43
|
+
resolve from the configuration file's directory. Commits use native Git identity
|
|
44
|
+
and disclose retained writable agent contributions with default-enabled co-author
|
|
45
|
+
trailers. Set the named hosting-token environment variable through your usual
|
|
46
|
+
secret manager; for GitHub, follow the [PAT creation and required permissions](docs/providers.md#create-a-fine-grained-personal-access-token) guide before running. Commit or ignore the configuration before running. Keep the state directory outside managed checkouts, or explicitly Git-ignore it.
|
|
42
47
|
|
|
43
48
|
```sh
|
|
44
49
|
agent-workflows run
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Project } from "./config.js";
|
|
2
|
+
import type { ContributionCandidate, Publication, Snapshot } from "./domain.js";
|
|
3
|
+
export declare const agentCoAuthors: {
|
|
4
|
+
readonly codex: "Codex <noreply@openai.com>";
|
|
5
|
+
readonly copilot: "Copilot <223556219+Copilot@users.noreply.github.com>";
|
|
6
|
+
};
|
|
7
|
+
export type AttributedProvider = keyof typeof agentCoAuthors;
|
|
8
|
+
export declare function contributingProviders(candidates: ContributionCandidate[], final: Snapshot): AttributedProvider[];
|
|
9
|
+
export declare function finalizeCommitMessage(publication: Publication, project: Project, providers: AttributedProvider[], runId: string): Publication;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const agentCoAuthors = {
|
|
2
|
+
codex: "Codex <noreply@openai.com>",
|
|
3
|
+
copilot: "Copilot <223556219+Copilot@users.noreply.github.com>",
|
|
4
|
+
};
|
|
5
|
+
const trailerLine = /^([A-Za-z0-9][A-Za-z0-9-]*):[ \t]*(\S.*)$/;
|
|
6
|
+
function normalizedTrailer(line) {
|
|
7
|
+
const match = trailerLine.exec(line);
|
|
8
|
+
return match ? `${match[1]}:${match[2]}`.toLowerCase() : undefined;
|
|
9
|
+
}
|
|
10
|
+
function retained(candidate, final) {
|
|
11
|
+
const paths = new Set([
|
|
12
|
+
...Object.keys(candidate.beforeFiles),
|
|
13
|
+
...Object.keys(candidate.afterFiles),
|
|
14
|
+
]);
|
|
15
|
+
return [...paths].some((path) => candidate.beforeFiles[path] !== candidate.afterFiles[path] &&
|
|
16
|
+
candidate.beforeFiles[path] !== final.files[path]);
|
|
17
|
+
}
|
|
18
|
+
export function contributingProviders(candidates, final) {
|
|
19
|
+
const providers = new Set();
|
|
20
|
+
for (const candidate of candidates) {
|
|
21
|
+
if (!(candidate.provider in agentCoAuthors) || !retained(candidate, final))
|
|
22
|
+
continue;
|
|
23
|
+
providers.add(candidate.provider);
|
|
24
|
+
}
|
|
25
|
+
return [...providers];
|
|
26
|
+
}
|
|
27
|
+
export function finalizeCommitMessage(publication, project, providers, runId) {
|
|
28
|
+
const identities = project.includeAgentCoAuthors
|
|
29
|
+
? providers.map((provider) => agentCoAuthors[provider])
|
|
30
|
+
: [];
|
|
31
|
+
const matching = new Set(identities.map((identity) => `co-authored-by:${identity}`.toLowerCase()));
|
|
32
|
+
const lines = publication.commitMessage
|
|
33
|
+
.split(/\r?\n/)
|
|
34
|
+
.filter((line) => !line.trim().toLowerCase().startsWith("agent-workflows-run:"));
|
|
35
|
+
while (lines.at(-1)?.trim() === "")
|
|
36
|
+
lines.pop();
|
|
37
|
+
const separator = lines.findLastIndex((line) => line.trim() === "");
|
|
38
|
+
const possibleTrailers = lines.slice(separator + 1);
|
|
39
|
+
const hasTrailerBlock = separator >= 0 &&
|
|
40
|
+
possibleTrailers.length > 0 &&
|
|
41
|
+
possibleTrailers.every((line) => trailerLine.test(line) || /^\s+\S/.test(line));
|
|
42
|
+
const body = hasTrailerBlock ? lines.slice(0, separator) : lines;
|
|
43
|
+
while (body.at(-1)?.trim() === "")
|
|
44
|
+
body.pop();
|
|
45
|
+
const existingTrailers = hasTrailerBlock
|
|
46
|
+
? possibleTrailers.filter((line) => !matching.has(normalizedTrailer(line.trim()) ?? ""))
|
|
47
|
+
: [];
|
|
48
|
+
const finalizedTrailers = [
|
|
49
|
+
...existingTrailers,
|
|
50
|
+
...identities.map((identity) => `Co-authored-by: ${identity}`),
|
|
51
|
+
`Agent-Workflows-Run: ${runId}`,
|
|
52
|
+
];
|
|
53
|
+
return {
|
|
54
|
+
...publication,
|
|
55
|
+
commitMessage: `${body.join("\n")}\n\n${finalizedTrailers.join("\n")}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
package/dist/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --
|
|
2
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { basename, dirname, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
4
5
|
import { parseEnv } from "node:util";
|
|
5
6
|
import { Command } from "commander";
|
|
6
7
|
import { render } from "ink";
|
|
@@ -11,12 +12,13 @@ import { configSchema } from "./config.js";
|
|
|
11
12
|
import { defaultValidationTimeoutMs } from "./defaults.js";
|
|
12
13
|
import { Runner } from "./runner.js";
|
|
13
14
|
import { Store } from "./store.js";
|
|
14
|
-
import { Monitor } from "./tui/index.js";
|
|
15
|
+
import { createTerminalNotificationWriter, Monitor } from "./tui/index.js";
|
|
15
16
|
const launchDirectory = process.cwd();
|
|
16
17
|
const program = new Command()
|
|
17
18
|
.name("agent-workflows")
|
|
18
19
|
.description("Local durable issue-to-review workflows")
|
|
19
20
|
.option("-c, --config <file>", "configuration path", "agent-workflows.json")
|
|
21
|
+
.option("--config-base-directory <directory>", "base directory for paths contained in configuration")
|
|
20
22
|
.option("--env-file <path>", "load literal dotenv values; existing environment wins")
|
|
21
23
|
.hook("preAction", async () => {
|
|
22
24
|
const path = program.opts().envFile;
|
|
@@ -36,7 +38,26 @@ const program = new Command()
|
|
|
36
38
|
}
|
|
37
39
|
});
|
|
38
40
|
async function configuration() {
|
|
39
|
-
return configSchema.parse(JSON.parse(await readFile(
|
|
41
|
+
return configSchema.parse(JSON.parse(await readFile(configPath(), "utf8")));
|
|
42
|
+
}
|
|
43
|
+
function configPath() {
|
|
44
|
+
return resolve(launchDirectory, program.opts().config);
|
|
45
|
+
}
|
|
46
|
+
function configBaseDirectory() {
|
|
47
|
+
const selected = program.opts().configBaseDirectory;
|
|
48
|
+
const path = selected
|
|
49
|
+
? resolve(launchDirectory, selected)
|
|
50
|
+
: dirname(configPath());
|
|
51
|
+
let canonical;
|
|
52
|
+
try {
|
|
53
|
+
canonical = realpathSync(path);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw new Error(`Configuration path base is not an existing directory: ${path}`, { cause: error });
|
|
57
|
+
}
|
|
58
|
+
if (!statSync(canonical).isDirectory())
|
|
59
|
+
throw new Error(`Configuration path base is not an existing directory: ${path}`);
|
|
60
|
+
return canonical;
|
|
40
61
|
}
|
|
41
62
|
function databaseUrl(config) {
|
|
42
63
|
const value = process.env[config.databaseUrlEnv];
|
|
@@ -59,14 +80,18 @@ program
|
|
|
59
80
|
.command("init")
|
|
60
81
|
.description("Write a configuration scaffold without overwriting files")
|
|
61
82
|
.action(async () => {
|
|
83
|
+
const base = configBaseDirectory();
|
|
84
|
+
const checkout = process.cwd();
|
|
85
|
+
const externalState = resolve(checkout, "..", `${basename(checkout)}.agent-workflows`);
|
|
86
|
+
const portable = (path) => relative(base, path) || ".";
|
|
62
87
|
const config = {
|
|
63
88
|
id: "local",
|
|
64
89
|
databaseUrlEnv: "AGENT_WORKFLOWS_DATABASE_URL",
|
|
65
|
-
stateDirectory:
|
|
90
|
+
stateDirectory: portable(externalState),
|
|
66
91
|
projects: [
|
|
67
92
|
{
|
|
68
93
|
id: "example",
|
|
69
|
-
checkout:
|
|
94
|
+
checkout: portable(checkout),
|
|
70
95
|
hosting: {
|
|
71
96
|
provider: "github",
|
|
72
97
|
origin: "https://github.com",
|
|
@@ -76,7 +101,7 @@ program
|
|
|
76
101
|
labels: ["ready-for-agent"],
|
|
77
102
|
baseBranch: "main",
|
|
78
103
|
branchTemplate: "agent/{issue}-{attempt}",
|
|
79
|
-
|
|
104
|
+
includeAgentCoAuthors: true,
|
|
80
105
|
agent: { provider: "codex" },
|
|
81
106
|
validation: [
|
|
82
107
|
{
|
|
@@ -89,8 +114,11 @@ program
|
|
|
89
114
|
},
|
|
90
115
|
],
|
|
91
116
|
};
|
|
92
|
-
await writeFile(
|
|
93
|
-
|
|
117
|
+
await writeFile(configPath(), JSON.stringify(config, null, 2) + "\n", {
|
|
118
|
+
flag: "wx",
|
|
119
|
+
mode: 0o600,
|
|
120
|
+
});
|
|
121
|
+
console.log("Created configuration. Set repository and credentials before running.");
|
|
94
122
|
});
|
|
95
123
|
program
|
|
96
124
|
.command("run")
|
|
@@ -105,7 +133,7 @@ program
|
|
|
105
133
|
config.projects = config.projects.filter((project) => wanted.has(project.id));
|
|
106
134
|
}
|
|
107
135
|
const runner = new Runner({
|
|
108
|
-
|
|
136
|
+
pathBaseDirectory: configBaseDirectory(),
|
|
109
137
|
config,
|
|
110
138
|
databaseUrl: databaseUrl(config),
|
|
111
139
|
hosting: createHosting,
|
|
@@ -178,13 +206,19 @@ for (const kind of ["pause", "resume", "stop", "retry", "recover"])
|
|
|
178
206
|
status: "pending",
|
|
179
207
|
}));
|
|
180
208
|
}));
|
|
181
|
-
program
|
|
209
|
+
program
|
|
210
|
+
.command("monitor")
|
|
211
|
+
.option("--notify", "notify when an observed execution reaches an outcome")
|
|
212
|
+
.action(async (options) => {
|
|
182
213
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
183
214
|
throw new Error("Monitor requires an interactive terminal; use status --json instead");
|
|
184
215
|
await withStore(async (store) => {
|
|
185
|
-
await render(React.createElement(Monitor, {
|
|
186
|
-
|
|
187
|
-
|
|
216
|
+
await render(React.createElement(Monitor, {
|
|
217
|
+
source: store,
|
|
218
|
+
notificationWriter: options.notify
|
|
219
|
+
? createTerminalNotificationWriter()
|
|
220
|
+
: undefined,
|
|
221
|
+
}), { alternateScreen: true }).waitUntilExit();
|
|
188
222
|
});
|
|
189
223
|
});
|
|
190
224
|
try {
|
package/dist/src/config.d.ts
CHANGED
|
@@ -51,10 +51,7 @@ 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
|
-
|
|
55
|
-
name: z.ZodString;
|
|
56
|
-
email: z.ZodEmail;
|
|
57
|
-
}, z.core.$strict>;
|
|
54
|
+
includeAgentCoAuthors: z.ZodDefault<z.ZodBoolean>;
|
|
58
55
|
agent: z.ZodObject<{
|
|
59
56
|
provider: z.ZodEnum<{
|
|
60
57
|
codex: "codex";
|
|
@@ -147,10 +144,7 @@ export declare const configSchema: z.ZodObject<{
|
|
|
147
144
|
args: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
148
145
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
149
146
|
}, z.core.$strict>>>;
|
|
150
|
-
|
|
151
|
-
name: z.ZodString;
|
|
152
|
-
email: z.ZodEmail;
|
|
153
|
-
}, z.core.$strict>;
|
|
147
|
+
includeAgentCoAuthors: z.ZodDefault<z.ZodBoolean>;
|
|
154
148
|
agent: z.ZodObject<{
|
|
155
149
|
provider: z.ZodEnum<{
|
|
156
150
|
codex: "codex";
|
package/dist/src/config.js
CHANGED
|
@@ -60,7 +60,7 @@ export const projectSchema = z.strictObject({
|
|
|
60
60
|
.default(defaultValidationTimeoutMs),
|
|
61
61
|
}))
|
|
62
62
|
.default([]),
|
|
63
|
-
|
|
63
|
+
includeAgentCoAuthors: z.boolean().default(true),
|
|
64
64
|
agent: profileSchema,
|
|
65
65
|
stages: z
|
|
66
66
|
.strictObject({
|
package/dist/src/domain.d.ts
CHANGED
|
@@ -47,6 +47,11 @@ export interface Snapshot {
|
|
|
47
47
|
paths: string[];
|
|
48
48
|
files: Record<string, string | null>;
|
|
49
49
|
}
|
|
50
|
+
export interface ContributionCandidate {
|
|
51
|
+
provider: string;
|
|
52
|
+
beforeFiles: Record<string, string | null>;
|
|
53
|
+
afterFiles: Record<string, string | null>;
|
|
54
|
+
}
|
|
50
55
|
export interface Workspace {
|
|
51
56
|
check(project: Project): Promise<void>;
|
|
52
57
|
prepare(project: Project, branch: string, signal?: AbortSignal): Promise<Snapshot>;
|
|
@@ -140,6 +145,8 @@ export interface RunRecord {
|
|
|
140
145
|
snapshot?: Snapshot;
|
|
141
146
|
validation?: ValidationResult[];
|
|
142
147
|
publication?: Publication;
|
|
148
|
+
contributionCandidates?: ContributionCandidate[];
|
|
149
|
+
contributingProviders?: string[];
|
|
143
150
|
change?: ChangeRequest;
|
|
144
151
|
review?: Review;
|
|
145
152
|
reviewHead?: string;
|
package/dist/src/invocation.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type Stage } from "./config.js";
|
|
3
|
-
import { type RunRecord, type Snapshot } from "./domain.js";
|
|
3
|
+
import { type ContributionCandidate, type RunRecord, type Snapshot } 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,8 +17,9 @@ interface StageExecution {
|
|
|
17
17
|
task: InvocationTask;
|
|
18
18
|
stepId: number;
|
|
19
19
|
dependencies: OperationDependencies;
|
|
20
|
-
saveImplementationSnapshot: (runId: string, snapshot: Snapshot) => Promise<
|
|
20
|
+
saveImplementationSnapshot: (runId: string, snapshot: Snapshot, provider: string) => Promise<ContributionCandidate | undefined>;
|
|
21
|
+
acceptContribution: (runId: string, candidate: ContributionCandidate) => Promise<void>;
|
|
21
22
|
}
|
|
22
23
|
/** One logical stage; only returned format errors admit a second response attempt. */
|
|
23
|
-
export declare function invokeStage({ run, name, stage, task, stepId, dependencies, saveImplementationSnapshot, }: StageExecution): Promise<string>;
|
|
24
|
+
export declare function invokeStage({ run, name, stage, task, stepId, dependencies, saveImplementationSnapshot, acceptContribution, }: StageExecution): Promise<string>;
|
|
24
25
|
export {};
|
package/dist/src/invocation.js
CHANGED
|
@@ -3,12 +3,12 @@ import { appendFile, mkdir } from "node:fs/promises";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { resolveProfile } from "./config.js";
|
|
6
|
-
import { BlockedError } from "./domain.js";
|
|
6
|
+
import { BlockedError, } from "./domain.js";
|
|
7
7
|
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, }) {
|
|
11
|
+
export async function invokeStage({ run, name, stage, task, stepId, dependencies, saveImplementationSnapshot, acceptContribution, }) {
|
|
12
12
|
const { store, project, agents, signal, workspace, redact } = dependencies;
|
|
13
13
|
const previous = (await store.invocations(run.id)).filter((item) => item.stepId === stepId);
|
|
14
14
|
if (previous.length) {
|
|
@@ -56,6 +56,7 @@ export async function invokeStage({ run, name, stage, task, stepId, dependencies
|
|
|
56
56
|
const directory = join(dependencies.artifacts, run.id);
|
|
57
57
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
58
58
|
let correction = "";
|
|
59
|
+
let pendingContribution;
|
|
59
60
|
for (let attempt = 1; attempt <= maxInvocationAttempts; attempt++) {
|
|
60
61
|
invocationSignal.throwIfAborted();
|
|
61
62
|
const expected = (await store.run(run.id)).snapshot;
|
|
@@ -118,7 +119,7 @@ export async function invokeStage({ run, name, stage, task, stepId, dependencies
|
|
|
118
119
|
if (readOnly)
|
|
119
120
|
await workspace.verify(project, expected);
|
|
120
121
|
else
|
|
121
|
-
await saveImplementationSnapshot(run.id, expected);
|
|
122
|
+
pendingContribution = await saveImplementationSnapshot(run.id, expected, profile.provider);
|
|
122
123
|
if (task.evidence)
|
|
123
124
|
await verifyEvidence(task.evidence);
|
|
124
125
|
invocationSignal.throwIfAborted();
|
|
@@ -144,6 +145,8 @@ export async function invokeStage({ run, name, stage, task, stepId, dependencies
|
|
|
144
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)}`;
|
|
145
146
|
continue;
|
|
146
147
|
}
|
|
148
|
+
if (pendingContribution)
|
|
149
|
+
await acceptContribution(run.id, pendingContribution);
|
|
147
150
|
record.outcome = "completed";
|
|
148
151
|
return redact(task.outputContract ? JSON.stringify(parsed) : output);
|
|
149
152
|
}
|
package/dist/src/operations.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare class Operations {
|
|
|
26
26
|
prepare(): Promise<void>;
|
|
27
27
|
invoke(name: string, stage: Stage, task: InvocationTask): Promise<string>;
|
|
28
28
|
private saveImplementationSnapshot;
|
|
29
|
+
private acceptContribution;
|
|
29
30
|
implement(): Promise<void>;
|
|
30
31
|
validate(): Promise<boolean>;
|
|
31
32
|
private prepareEvidence;
|
package/dist/src/operations.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
5
|
+
import { contributingProviders, finalizeCommitMessage } from "./attribution.js";
|
|
5
6
|
import { BlockedError, isBlockedError, publicationSchema, reviewSchema, } from "./domain.js";
|
|
6
7
|
import { captureEvidence, evidenceContext, } from "./evidence.js";
|
|
7
8
|
import { invokeStage } from "./invocation.js";
|
|
@@ -103,15 +104,32 @@ export class Operations {
|
|
|
103
104
|
task,
|
|
104
105
|
stepId: DBOS.stepID,
|
|
105
106
|
dependencies: this.dependencies,
|
|
106
|
-
saveImplementationSnapshot: (runId, expected) => this.saveImplementationSnapshot(runId, expected),
|
|
107
|
+
saveImplementationSnapshot: (runId, expected, provider) => this.saveImplementationSnapshot(runId, expected, provider),
|
|
108
|
+
acceptContribution: (runId, candidate) => this.acceptContribution(runId, candidate),
|
|
107
109
|
}));
|
|
108
110
|
}
|
|
109
|
-
async saveImplementationSnapshot(runId, expected) {
|
|
111
|
+
async saveImplementationSnapshot(runId, expected, provider) {
|
|
110
112
|
const { workspace, project, store } = this.dependencies;
|
|
111
113
|
const snapshot = await workspace.inspect(project);
|
|
112
114
|
if (snapshot.head !== expected.head || snapshot.branch !== expected.branch)
|
|
113
115
|
throw new BlockedError("Agent changed branch or committed unexpectedly");
|
|
114
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
|
+
});
|
|
115
133
|
}
|
|
116
134
|
async implement() {
|
|
117
135
|
await this.invoke("implementation", this.dependencies.project.stages.implementation, {
|
|
@@ -206,8 +224,12 @@ export class Operations {
|
|
|
206
224
|
context: (run) => `${evidenceContext(evidence)}\nIssue: ${JSON.stringify(run.issue)}\nValidation: ${JSON.stringify(run.validation ?? [])}`,
|
|
207
225
|
});
|
|
208
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);
|
|
209
230
|
await this.dependencies.store.patchRun(run.id, {
|
|
210
|
-
publication: publicationSchema.parse(JSON.parse(output)),
|
|
231
|
+
publication: finalizeCommitMessage(publicationSchema.parse(JSON.parse(output)), this.dependencies.project, providers, run.id),
|
|
232
|
+
contributingProviders: providers,
|
|
211
233
|
});
|
|
212
234
|
});
|
|
213
235
|
}
|
package/dist/src/runner.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ 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;
|
|
6
7
|
promptBaseDirectory?: string;
|
|
7
8
|
config: Configuration;
|
|
8
9
|
databaseUrl: string;
|
|
@@ -19,6 +20,7 @@ export declare class Runner {
|
|
|
19
20
|
private readonly controllers;
|
|
20
21
|
private readonly active;
|
|
21
22
|
private readonly hosting;
|
|
23
|
+
private readonly promptBaseDirectory;
|
|
22
24
|
private workflow;
|
|
23
25
|
private readonly ownership;
|
|
24
26
|
private stopping;
|
package/dist/src/runner.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
3
|
import { mkdir, realpath } from "node:fs/promises";
|
|
3
4
|
import { resolve } from "node:path";
|
|
4
5
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
@@ -22,6 +23,7 @@ export class Runner {
|
|
|
22
23
|
controllers = new Map();
|
|
23
24
|
active = new Map();
|
|
24
25
|
hosting = new Map();
|
|
26
|
+
promptBaseDirectory;
|
|
25
27
|
workflow;
|
|
26
28
|
ownership = new CheckoutOwnership();
|
|
27
29
|
stopping = false;
|
|
@@ -33,8 +35,16 @@ export class Runner {
|
|
|
33
35
|
constructor(options) {
|
|
34
36
|
this.options = options;
|
|
35
37
|
this.config = configSchema.parse(options.config);
|
|
38
|
+
const pathBaseDirectory = canonicalDirectory(options.pathBaseDirectory ?? process.cwd(), "Configuration path base");
|
|
39
|
+
this.config.stateDirectory = resolve(pathBaseDirectory, this.config.stateDirectory);
|
|
36
40
|
for (const project of this.config.projects)
|
|
37
|
-
|
|
41
|
+
project.checkout = resolve(pathBaseDirectory, project.checkout);
|
|
42
|
+
const promptBaseDirectory = options.promptBaseDirectory
|
|
43
|
+
? resolve(options.promptBaseDirectory)
|
|
44
|
+
: pathBaseDirectory;
|
|
45
|
+
this.promptBaseDirectory = promptBaseDirectory;
|
|
46
|
+
for (const project of this.config.projects)
|
|
47
|
+
projectPrompts(project, promptBaseDirectory);
|
|
38
48
|
this.store = new Store(options.databaseUrl, this.config.id, this.redact);
|
|
39
49
|
}
|
|
40
50
|
queue(id) {
|
|
@@ -49,7 +59,7 @@ export class Runner {
|
|
|
49
59
|
const ids = new Set();
|
|
50
60
|
for (const project of this.config.projects) {
|
|
51
61
|
project.checkout = await realpath(project.checkout);
|
|
52
|
-
await assertEvidenceDirectory(project.checkout, this.config.stateDirectory);
|
|
62
|
+
this.config.stateDirectory = await assertEvidenceDirectory(project.checkout, this.config.stateDirectory);
|
|
53
63
|
if (canonical.has(project.checkout) || ids.has(project.id))
|
|
54
64
|
throw new Error("Duplicate project identity or canonical checkout");
|
|
55
65
|
canonical.add(project.checkout);
|
|
@@ -277,7 +287,7 @@ export class Runner {
|
|
|
277
287
|
const workspace = this.options.workspace ?? new ExistingCheckout();
|
|
278
288
|
let recoveryChecked = false;
|
|
279
289
|
const operations = new Operations(runId, {
|
|
280
|
-
promptBaseDirectory: this.
|
|
290
|
+
promptBaseDirectory: this.promptBaseDirectory,
|
|
281
291
|
store: this.store,
|
|
282
292
|
project,
|
|
283
293
|
workspace,
|
|
@@ -495,3 +505,18 @@ export class Runner {
|
|
|
495
505
|
await this.store.close();
|
|
496
506
|
}
|
|
497
507
|
}
|
|
508
|
+
function canonicalDirectory(path, label) {
|
|
509
|
+
const resolved = resolve(path);
|
|
510
|
+
let canonical;
|
|
511
|
+
try {
|
|
512
|
+
canonical = realpathSync(resolved);
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
throw new Error(`${label} is not an existing directory: ${resolved}`, {
|
|
516
|
+
cause: error,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
if (!statSync(canonical).isDirectory())
|
|
520
|
+
throw new Error(`${label} is not an existing directory: ${resolved}`);
|
|
521
|
+
return canonical;
|
|
522
|
+
}
|
package/dist/src/tui/data.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RunRecord } from "../domain.js";
|
|
2
2
|
import type { EventRecord, InvocationRecord, ProjectState, Store } from "../store.js";
|
|
3
|
+
import { type ExecutionNotificationWriter } from "./notifications.js";
|
|
3
4
|
export interface MonitorSource {
|
|
4
5
|
projects: Store["projects"];
|
|
5
6
|
runs: Store["runs"];
|
|
@@ -12,7 +13,7 @@ export type MonitorAction = "pause" | "resume" | "stop" | "retry" | "recover";
|
|
|
12
13
|
export declare function useMonitorData(source: MonitorSource, selection: {
|
|
13
14
|
projectId?: string;
|
|
14
15
|
runId?: string;
|
|
15
|
-
}): {
|
|
16
|
+
}, notificationWriter?: ExecutionNotificationWriter): {
|
|
16
17
|
projects: ProjectState[];
|
|
17
18
|
project: ProjectState | undefined;
|
|
18
19
|
projectRuns: RunRecord[];
|
package/dist/src/tui/data.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { tuiRefreshIntervalMs } from "./constants.js";
|
|
3
|
-
|
|
3
|
+
import { ExecutionNotificationObserver, } from "./notifications.js";
|
|
4
|
+
export function useMonitorData(source, selection, notificationWriter) {
|
|
4
5
|
const [projects, setProjects] = useState([]);
|
|
5
6
|
const [runs, setRuns] = useState([]);
|
|
6
7
|
const [detail, setDetail] = useState({ sessions: [], events: [] });
|
|
@@ -10,6 +11,9 @@ export function useMonitorData(source, selection) {
|
|
|
10
11
|
const [pending, setPending] = useState();
|
|
11
12
|
const pendingRef = useRef(undefined);
|
|
12
13
|
const mounted = useRef(true);
|
|
14
|
+
const notificationObserver = useRef(notificationWriter
|
|
15
|
+
? new ExecutionNotificationObserver(notificationWriter)
|
|
16
|
+
: undefined).current;
|
|
13
17
|
const project = projects.find((item) => item.id === selection.projectId) ?? projects[0];
|
|
14
18
|
const projectRuns = runs.filter((item) => item.projectId === project?.id);
|
|
15
19
|
const run = projectRuns.find((item) => item.id === selection.runId) ?? projectRuns[0];
|
|
@@ -38,6 +42,7 @@ export function useMonitorData(source, selection) {
|
|
|
38
42
|
return;
|
|
39
43
|
setProjects(nextProjects);
|
|
40
44
|
setRuns(nextRuns);
|
|
45
|
+
notificationObserver?.observe(nextRuns);
|
|
41
46
|
if (selectedRunId) {
|
|
42
47
|
const [sessions, nextEvents] = await Promise.all([
|
|
43
48
|
source.invocations(selectedRunId),
|
|
@@ -71,7 +76,7 @@ export function useMonitorData(source, selection) {
|
|
|
71
76
|
closed = true;
|
|
72
77
|
clearInterval(timer);
|
|
73
78
|
};
|
|
74
|
-
}, [source, selectedRunId]);
|
|
79
|
+
}, [source, selectedRunId, notificationObserver]);
|
|
75
80
|
useEffect(() => {
|
|
76
81
|
if (!pending || pending === "submitting")
|
|
77
82
|
return;
|
package/dist/src/tui/index.d.ts
CHANGED
package/dist/src/tui/index.js
CHANGED
package/dist/src/tui/layout.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Pane content sizes shared by rendering and keyboard scrolling. */
|
|
2
|
-
export declare function monitorLayout(columns: number, rows: number): {
|
|
2
|
+
export declare function monitorLayout(columns: number, rows: number, compactChrome?: boolean): {
|
|
3
3
|
wide: boolean;
|
|
4
4
|
height: number;
|
|
5
5
|
paneWidth: number;
|
package/dist/src/tui/layout.js
CHANGED
|
@@ -2,9 +2,9 @@ import { screenChromeRows } from "./constants.js";
|
|
|
2
2
|
const panelHorizontalChrome = 4;
|
|
3
3
|
const panelVerticalChrome = 3;
|
|
4
4
|
/** Pane content sizes shared by rendering and keyboard scrolling. */
|
|
5
|
-
export function monitorLayout(columns, rows) {
|
|
5
|
+
export function monitorLayout(columns, rows, compactChrome = false) {
|
|
6
6
|
const wide = columns >= 110;
|
|
7
|
-
const height = Math.max(1, rows - screenChromeRows);
|
|
7
|
+
const height = Math.max(1, rows - (compactChrome ? 4 : screenChromeRows));
|
|
8
8
|
const paneWidth = wide ? Math.floor(columns * 0.43) : columns;
|
|
9
9
|
const summaryWidth = wide ? columns - paneWidth : columns;
|
|
10
10
|
const summaryPanelHeight = wide ? height - 6 : height;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type MonitorSource } from "./data.js";
|
|
2
|
-
|
|
2
|
+
import type { ExecutionNotificationWriter } from "./notifications.js";
|
|
3
|
+
export declare function Monitor({ source, size, notificationWriter, }: {
|
|
3
4
|
source: MonitorSource;
|
|
4
5
|
size?: {
|
|
5
6
|
columns: number;
|
|
6
7
|
rows: number;
|
|
7
8
|
};
|
|
9
|
+
notificationWriter?: ExecutionNotificationWriter;
|
|
8
10
|
}): import("react").JSX.Element;
|