@mingchuno/agent-workflows 0.2.0 → 0.4.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 +31 -15
- package/dist/src/attribution.d.ts +9 -0
- package/dist/src/attribution.js +57 -0
- package/dist/src/cli-config.d.ts +2 -0
- package/dist/src/cli-config.js +35 -0
- package/dist/src/cli.js +50 -31
- 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 +25 -10
- package/docs/configuration.md +78 -27
- package/docs/database.md +2 -18
- package/docs/operations.md +54 -22
- package/docs/providers.md +2 -2
- package/examples/config.ts +4 -4
- package/examples/run.ts +4 -1
- package/package.json +1 -1
- package/docs/architecture.md +0 -41
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
|
|
@@ -47,9 +52,10 @@ agent-workflows monitor
|
|
|
47
52
|
agent-workflows status --json
|
|
48
53
|
```
|
|
49
54
|
|
|
50
|
-
Alternatively,
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
Alternatively, set `"envFile": "./runner.env"` at the top level of
|
|
56
|
+
`agent-workflows.json` to load database, hosting and application variables from
|
|
57
|
+
one explicit file. Existing process values win, including empty strings. See
|
|
58
|
+
[environment file examples and boundaries](docs/configuration.md#cli-environment-files).
|
|
53
59
|
|
|
54
60
|
The runner fetches the configured base, creates a branch, implements an eligible issue, validates it, generates publication text, commits and pushes, creates a draft PR/MR, and publishes an independent review of its exact head. It never merges. Initial use should target a repository and issue you explicitly intend to automate; running the CLI authorizes these effects and agent usage.
|
|
55
61
|
|
|
@@ -71,6 +77,24 @@ await runner.shutdown();
|
|
|
71
77
|
|
|
72
78
|
[Custom workflow](examples/custom-workflow.ts), [complete runner](examples/run.ts), [configuration](examples/config.ts), and [inspection](examples/observe.ts) examples are type-checked with the library. The custom workflow is also exercised using controlled providers.
|
|
73
79
|
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
### Using the package
|
|
83
|
+
|
|
84
|
+
- [Configuration and profiles](docs/configuration.md)
|
|
85
|
+
- [Default stage prompts](docs/configuration.md#default-stage-prompts)
|
|
86
|
+
- [Public SDK API and composition](docs/api.md)
|
|
87
|
+
- [DBOS SDK direct usage](docs/api.md#dbos-sdk-direct-usage)
|
|
88
|
+
- [Authentication and provider capabilities](docs/providers.md)
|
|
89
|
+
- [CLI, TUI, observability, and recovery](docs/operations.md)
|
|
90
|
+
- [Observability landscape](docs/operations.md#observability-landscape)
|
|
91
|
+
|
|
92
|
+
### Maintaining the project
|
|
93
|
+
|
|
94
|
+
- [Architecture decisions](docs/adr/README.md)
|
|
95
|
+
- [Database maintenance](docs/database.md)
|
|
96
|
+
- [Release process](docs/releases.md)
|
|
97
|
+
|
|
74
98
|
## Development and review
|
|
75
99
|
|
|
76
100
|
[mise](https://mise.jdx.dev/getting-started.html) pins the development Node and pnpm versions. Activate it in your shell or prefix commands with `mise exec --`.
|
|
@@ -87,14 +111,6 @@ Use `pnpm start --help` to run the CLI from source. `pnpm build` emits the runti
|
|
|
87
111
|
|
|
88
112
|
`pnpm test` builds the application, then starts and removes a disposable real PostgreSQL database using `initdb`, `pg_ctl`, and `createdb` on PATH. Alternatively, set `TEST_DATABASE_URL` to a disposable database whose role can create test databases. Tests use real temporary Git repositories and controlled adapters/HTTP servers; they make no paid agent calls or writes to real hosting providers.
|
|
89
113
|
|
|
90
|
-
Schema changes and database upgrades: [database maintenance](docs/database.md).
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
- [Default stage prompts](docs/configuration.md#default-stage-prompts)
|
|
94
|
-
- [Public SDK API and composition](docs/api.md)
|
|
95
|
-
- [DBOS SDK direct usage](docs/api.md#dbos-sdk-direct-usage)
|
|
96
|
-
- [Authentication and provider capabilities](docs/providers.md)
|
|
97
|
-
- [CLI, TUI, observability and recovery](docs/operations.md)
|
|
98
|
-
- [Observability Landscape](docs/operations.md#observability-landscape)
|
|
99
|
-
- [Architecture](docs/architecture.md)
|
|
100
|
-
- [Releases](docs/releases.md)
|
|
114
|
+
Schema changes and database upgrades: [database maintenance](docs/database.md).
|
|
115
|
+
Biome formats and lints supported source/configuration files; Markdown and YAML
|
|
116
|
+
are maintained manually.
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { parseEnv } from "node:util";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { configSchema } from "./config.js";
|
|
5
|
+
const cliConfigurationSchema = configSchema.extend({
|
|
6
|
+
envFile: z
|
|
7
|
+
.string()
|
|
8
|
+
.refine((path) => path.trim().length > 0, "Environment file path must be nonblank")
|
|
9
|
+
.optional(),
|
|
10
|
+
});
|
|
11
|
+
export async function readCliConfiguration(path, resolveEnvironmentFile) {
|
|
12
|
+
const { envFile, ...configuration } = cliConfigurationSchema.parse(JSON.parse(await readFile(path, "utf8")));
|
|
13
|
+
if (envFile !== undefined)
|
|
14
|
+
await loadEnvironmentFile(resolveEnvironmentFile(envFile));
|
|
15
|
+
return configuration;
|
|
16
|
+
}
|
|
17
|
+
async function loadEnvironmentFile(path) {
|
|
18
|
+
let contents;
|
|
19
|
+
try {
|
|
20
|
+
contents = await readFile(path, "utf8");
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw new Error(`Cannot read environment file ${path} (${error.code ?? "read failed"})`);
|
|
24
|
+
}
|
|
25
|
+
let values;
|
|
26
|
+
try {
|
|
27
|
+
values = parseEnv(contents);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new Error(`Cannot parse environment file ${path}`, { cause: error });
|
|
31
|
+
}
|
|
32
|
+
for (const [name, value] of Object.entries(values))
|
|
33
|
+
if (value !== undefined && process.env[name] === undefined)
|
|
34
|
+
process.env[name] = value;
|
|
35
|
+
}
|
package/dist/src/cli.js
CHANGED
|
@@ -1,42 +1,44 @@
|
|
|
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 { parseEnv } from "node:util";
|
|
4
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { render } from "ink";
|
|
7
7
|
import React from "react";
|
|
8
8
|
import { createAgents } from "./adapters/agents.js";
|
|
9
9
|
import { createHosting } from "./adapters/hosting.js";
|
|
10
|
-
import {
|
|
10
|
+
import { readCliConfiguration } from "./cli-config.js";
|
|
11
11
|
import { defaultValidationTimeoutMs } from "./defaults.js";
|
|
12
12
|
import { Runner } from "./runner.js";
|
|
13
13
|
import { Store } from "./store.js";
|
|
14
|
-
import { Monitor } from "./tui/index.js";
|
|
14
|
+
import { createTerminalNotificationWriter, Monitor } from "./tui/index.js";
|
|
15
15
|
const launchDirectory = process.cwd();
|
|
16
16
|
const program = new Command()
|
|
17
17
|
.name("agent-workflows")
|
|
18
18
|
.description("Local durable issue-to-review workflows")
|
|
19
19
|
.option("-c, --config <file>", "configuration path", "agent-workflows.json")
|
|
20
|
-
.option("--
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
.option("--config-base-directory <directory>", "base directory for paths contained in configuration");
|
|
21
|
+
async function configuration() {
|
|
22
|
+
return readCliConfiguration(configPath(), (path) => resolve(configBaseDirectory(), path));
|
|
23
|
+
}
|
|
24
|
+
function configPath() {
|
|
25
|
+
return resolve(launchDirectory, program.opts().config);
|
|
26
|
+
}
|
|
27
|
+
function configBaseDirectory() {
|
|
28
|
+
const selected = program.opts().configBaseDirectory;
|
|
29
|
+
const path = selected
|
|
30
|
+
? resolve(launchDirectory, selected)
|
|
31
|
+
: dirname(configPath());
|
|
32
|
+
let canonical;
|
|
27
33
|
try {
|
|
28
|
-
|
|
34
|
+
canonical = realpathSync(path);
|
|
29
35
|
}
|
|
30
36
|
catch (error) {
|
|
31
|
-
throw new Error(`
|
|
37
|
+
throw new Error(`Configuration path base is not an existing directory: ${path}`, { cause: error });
|
|
32
38
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
async function configuration() {
|
|
39
|
-
return configSchema.parse(JSON.parse(await readFile(resolve(program.opts().config), "utf8")));
|
|
39
|
+
if (!statSync(canonical).isDirectory())
|
|
40
|
+
throw new Error(`Configuration path base is not an existing directory: ${path}`);
|
|
41
|
+
return canonical;
|
|
40
42
|
}
|
|
41
43
|
function databaseUrl(config) {
|
|
42
44
|
const value = process.env[config.databaseUrlEnv];
|
|
@@ -46,6 +48,9 @@ function databaseUrl(config) {
|
|
|
46
48
|
}
|
|
47
49
|
async function withStore(action) {
|
|
48
50
|
const config = await configuration();
|
|
51
|
+
await withConfiguredStore(config, action);
|
|
52
|
+
}
|
|
53
|
+
async function withConfiguredStore(config, action) {
|
|
49
54
|
const store = new Store(databaseUrl(config), config.id);
|
|
50
55
|
try {
|
|
51
56
|
await store.initialize();
|
|
@@ -59,14 +64,18 @@ program
|
|
|
59
64
|
.command("init")
|
|
60
65
|
.description("Write a configuration scaffold without overwriting files")
|
|
61
66
|
.action(async () => {
|
|
67
|
+
const base = configBaseDirectory();
|
|
68
|
+
const checkout = process.cwd();
|
|
69
|
+
const externalState = resolve(checkout, "..", `${basename(checkout)}.agent-workflows`);
|
|
70
|
+
const portable = (path) => relative(base, path) || ".";
|
|
62
71
|
const config = {
|
|
63
72
|
id: "local",
|
|
64
73
|
databaseUrlEnv: "AGENT_WORKFLOWS_DATABASE_URL",
|
|
65
|
-
stateDirectory:
|
|
74
|
+
stateDirectory: portable(externalState),
|
|
66
75
|
projects: [
|
|
67
76
|
{
|
|
68
77
|
id: "example",
|
|
69
|
-
checkout:
|
|
78
|
+
checkout: portable(checkout),
|
|
70
79
|
hosting: {
|
|
71
80
|
provider: "github",
|
|
72
81
|
origin: "https://github.com",
|
|
@@ -76,7 +85,7 @@ program
|
|
|
76
85
|
labels: ["ready-for-agent"],
|
|
77
86
|
baseBranch: "main",
|
|
78
87
|
branchTemplate: "agent/{issue}-{attempt}",
|
|
79
|
-
|
|
88
|
+
includeAgentCoAuthors: true,
|
|
80
89
|
agent: { provider: "codex" },
|
|
81
90
|
validation: [
|
|
82
91
|
{
|
|
@@ -89,8 +98,11 @@ program
|
|
|
89
98
|
},
|
|
90
99
|
],
|
|
91
100
|
};
|
|
92
|
-
await writeFile(
|
|
93
|
-
|
|
101
|
+
await writeFile(configPath(), JSON.stringify(config, null, 2) + "\n", {
|
|
102
|
+
flag: "wx",
|
|
103
|
+
mode: 0o600,
|
|
104
|
+
});
|
|
105
|
+
console.log("Created configuration. Set repository and credentials before running.");
|
|
94
106
|
});
|
|
95
107
|
program
|
|
96
108
|
.command("run")
|
|
@@ -105,7 +117,7 @@ program
|
|
|
105
117
|
config.projects = config.projects.filter((project) => wanted.has(project.id));
|
|
106
118
|
}
|
|
107
119
|
const runner = new Runner({
|
|
108
|
-
|
|
120
|
+
pathBaseDirectory: configBaseDirectory(),
|
|
109
121
|
config,
|
|
110
122
|
databaseUrl: databaseUrl(config),
|
|
111
123
|
hosting: createHosting,
|
|
@@ -178,13 +190,20 @@ for (const kind of ["pause", "resume", "stop", "retry", "recover"])
|
|
|
178
190
|
status: "pending",
|
|
179
191
|
}));
|
|
180
192
|
}));
|
|
181
|
-
program
|
|
193
|
+
program
|
|
194
|
+
.command("monitor")
|
|
195
|
+
.option("--notify", "notify when an observed execution reaches an outcome")
|
|
196
|
+
.action(async (options) => {
|
|
197
|
+
const config = await configuration();
|
|
182
198
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
183
199
|
throw new Error("Monitor requires an interactive terminal; use status --json instead");
|
|
184
|
-
await
|
|
185
|
-
await render(React.createElement(Monitor, {
|
|
186
|
-
|
|
187
|
-
|
|
200
|
+
await withConfiguredStore(config, async (store) => {
|
|
201
|
+
await render(React.createElement(Monitor, {
|
|
202
|
+
source: store,
|
|
203
|
+
notificationWriter: options.notify
|
|
204
|
+
? createTerminalNotificationWriter()
|
|
205
|
+
: undefined,
|
|
206
|
+
}), { alternateScreen: true }).waitUntilExit();
|
|
188
207
|
});
|
|
189
208
|
});
|
|
190
209
|
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;
|