@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/README.md
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
# Agent Workflows
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
A local TypeScript SDK and CLI/TUI that turns labelled GitHub or GitLab issues into draft change requests with independent agent reviews. DBOS persists execution; each project uses its existing checkout, with one task at a time. Separate projects progress concurrently.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
Supports Codex and Copilot SDKs, GitHub, and GitLab.com/self-hosted GitLab. Human review and merging remain separate.
|
|
8
|
+
|
|
9
|
+
## Why use this SDK over DBOS directly?
|
|
10
|
+
|
|
11
|
+
DBOS provides durable execution, checkpoints and queues. This SDK supplies the
|
|
12
|
+
coding-workflow behavior a client would otherwise build around it:
|
|
13
|
+
|
|
14
|
+
- **Ready-made operations:** issue intake, agent implementation, validation,
|
|
15
|
+
draft PR/MR publication and independent review, with Codex/Copilot and
|
|
16
|
+
GitHub/GitLab adapters.
|
|
17
|
+
- **Checkout and recovery rules:** exclusive checkout ownership, process
|
|
18
|
+
cancellation, preservation of unfinished work, and reconciliation of interrupted
|
|
19
|
+
commits, pushes and publication.
|
|
20
|
+
- **Application controls and evidence:** explicit run outcomes, safe retry
|
|
21
|
+
admission, session/log records, and a shared SDK, CLI and terminal monitor.
|
|
22
|
+
|
|
23
|
+
Clients configure providers and compose operations while DBOS remains responsible
|
|
24
|
+
for durability. [Custom workflows can call DBOS directly](docs/api.md#dbos-sdk-direct-usage).
|
|
25
|
+
Use this SDK when its existing-checkout, one-task-per-project model fits; use DBOS
|
|
26
|
+
directly when you need general-purpose workflows or want to own these policies.
|
|
6
27
|
|
|
7
28
|
## Quickstart
|
|
8
29
|
|
|
@@ -17,7 +38,12 @@ agent-workflows init
|
|
|
17
38
|
|
|
18
39
|
For SDK use, install locally with `npm install @mingchuno/agent-workflows`. Both interfaces ship in the same package.
|
|
19
40
|
|
|
20
|
-
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.
|
|
21
47
|
|
|
22
48
|
```sh
|
|
23
49
|
agent-workflows run
|
|
@@ -26,6 +52,10 @@ agent-workflows monitor
|
|
|
26
52
|
agent-workflows status --json
|
|
27
53
|
```
|
|
28
54
|
|
|
55
|
+
Alternatively, load database, hosting and application variables from one explicit
|
|
56
|
+
file: `agent-workflows --env-file ./runner.env run`. Existing shell values win,
|
|
57
|
+
including empty strings. See [environment file examples and boundaries](docs/configuration.md#cli-environment-files).
|
|
58
|
+
|
|
29
59
|
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.
|
|
30
60
|
|
|
31
61
|
## SDK
|
|
@@ -65,10 +95,11 @@ Use `pnpm start --help` to run the CLI from source. `pnpm build` emits the runti
|
|
|
65
95
|
Schema changes and database upgrades: [database maintenance](docs/database.md). Biome formats and lints supported source/configuration files; Markdown and YAML are maintained manually.
|
|
66
96
|
|
|
67
97
|
- [Configuration and profiles](docs/configuration.md)
|
|
98
|
+
- [Default stage prompts](docs/configuration.md#default-stage-prompts)
|
|
68
99
|
- [Public SDK API and composition](docs/api.md)
|
|
100
|
+
- [DBOS SDK direct usage](docs/api.md#dbos-sdk-direct-usage)
|
|
69
101
|
- [Authentication and provider capabilities](docs/providers.md)
|
|
70
102
|
- [CLI, TUI, observability and recovery](docs/operations.md)
|
|
71
|
-
- [
|
|
72
|
-
- [
|
|
73
|
-
- [
|
|
74
|
-
- [Specification](https://github.com/mingchuno/agent-workflows/issues/1)
|
|
103
|
+
- [Observability Landscape](docs/operations.md#observability-landscape)
|
|
104
|
+
- [Architecture](docs/architecture.md)
|
|
105
|
+
- [Releases](docs/releases.md)
|
|
@@ -2,6 +2,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { defaultStageTimeoutMs } from "../defaults.js";
|
|
5
6
|
import { command } from "../runtime/process.js";
|
|
6
7
|
/** Runs each SDK invocation in an owned process group; no cross-stage session reuse. */
|
|
7
8
|
export class SDKAgent {
|
|
@@ -65,9 +66,9 @@ export class SDKAgent {
|
|
|
65
66
|
cwd: invocation.cwd,
|
|
66
67
|
prompt: invocation.prompt,
|
|
67
68
|
profile: invocation.profile,
|
|
68
|
-
|
|
69
|
+
outputSchema: invocation.outputSchema,
|
|
69
70
|
readOnly: invocation.readOnly,
|
|
70
|
-
timeoutMs: invocation.timeoutMs ??
|
|
71
|
+
timeoutMs: invocation.timeoutMs ?? defaultStageTimeoutMs,
|
|
71
72
|
}, invocation);
|
|
72
73
|
}
|
|
73
74
|
async worker(input, invocation, signal = invocation?.signal) {
|
|
@@ -97,7 +98,9 @@ export class SDKAgent {
|
|
|
97
98
|
]),
|
|
98
99
|
captureOutput: false,
|
|
99
100
|
processFile: invocation?.processFile,
|
|
100
|
-
timeoutMs: invocation
|
|
101
|
+
timeoutMs: invocation
|
|
102
|
+
? (invocation.timeoutMs ?? defaultStageTimeoutMs)
|
|
103
|
+
: 30_000,
|
|
101
104
|
onOutput: (chunk) => {
|
|
102
105
|
buffer += chunk;
|
|
103
106
|
for (;;) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Gitlab } from "@gitbeaker/rest";
|
|
2
2
|
import { Octokit } from "@octokit/rest";
|
|
3
3
|
import { BlockedError, } from "../domain.js";
|
|
4
|
+
const hostingRequestTimeoutMs = 30_000;
|
|
5
|
+
const hostingPageSize = 100;
|
|
6
|
+
const minimumGitLabMajorVersion = 17;
|
|
7
|
+
const maximumGitLabMajorVersion = 19;
|
|
4
8
|
function origin(value) {
|
|
5
9
|
const url = new URL(value);
|
|
6
10
|
if (url.username || url.password || url.search || url.hash)
|
|
@@ -70,7 +74,7 @@ export class GitHubHosting {
|
|
|
70
74
|
baseUrl: host === "https://github.com"
|
|
71
75
|
? "https://api.github.com"
|
|
72
76
|
: `${host}/api/v3`,
|
|
73
|
-
request: { timeout:
|
|
77
|
+
request: { timeout: hostingRequestTimeoutMs, redirect: "error" },
|
|
74
78
|
});
|
|
75
79
|
}
|
|
76
80
|
async listIssues(labels) {
|
|
@@ -78,7 +82,7 @@ export class GitHubHosting {
|
|
|
78
82
|
...this.repo,
|
|
79
83
|
state: "open",
|
|
80
84
|
labels: labels.join(","),
|
|
81
|
-
per_page:
|
|
85
|
+
per_page: hostingPageSize,
|
|
82
86
|
});
|
|
83
87
|
return issues
|
|
84
88
|
.filter((issue) => !issue.pull_request)
|
|
@@ -112,7 +116,7 @@ export class GitHubHosting {
|
|
|
112
116
|
...this.repo,
|
|
113
117
|
head: `${this.repo.owner}:${branch}`,
|
|
114
118
|
state: "all",
|
|
115
|
-
per_page:
|
|
119
|
+
per_page: hostingPageSize,
|
|
116
120
|
});
|
|
117
121
|
if (changes.length > 1)
|
|
118
122
|
throw new BlockedError("Multiple change requests match branch");
|
|
@@ -139,7 +143,7 @@ export class GitHubHosting {
|
|
|
139
143
|
const reviews = await this.client.paginate(this.client.pulls.listReviews, {
|
|
140
144
|
...this.repo,
|
|
141
145
|
pull_number: input.change.id,
|
|
142
|
-
per_page:
|
|
146
|
+
per_page: hostingPageSize,
|
|
143
147
|
});
|
|
144
148
|
if (reviews.some((review) => review.body?.includes(marker(input.runId))))
|
|
145
149
|
return;
|
|
@@ -172,21 +176,23 @@ export class GitLabHosting {
|
|
|
172
176
|
this.client = new Gitlab({
|
|
173
177
|
host,
|
|
174
178
|
token: token(project),
|
|
175
|
-
queryTimeout:
|
|
179
|
+
queryTimeout: hostingRequestTimeoutMs,
|
|
176
180
|
});
|
|
177
181
|
}
|
|
178
182
|
async preflight() {
|
|
179
183
|
const metadata = await this.client.Metadata.show();
|
|
180
184
|
const major = Number(metadata.version.split(".")[0]);
|
|
181
|
-
if (!Number.isInteger(major) ||
|
|
182
|
-
|
|
185
|
+
if (!Number.isInteger(major) ||
|
|
186
|
+
major < minimumGitLabMajorVersion ||
|
|
187
|
+
major > maximumGitLabMajorVersion)
|
|
188
|
+
throw new Error(`Supported GitLab versions are ${minimumGitLabMajorVersion}.x–${maximumGitLabMajorVersion}.x; instance reports ${metadata.version}`);
|
|
183
189
|
}
|
|
184
190
|
async listIssues(labels) {
|
|
185
191
|
const issues = await this.client.Issues.all({
|
|
186
192
|
projectId: this.repository,
|
|
187
193
|
state: "opened",
|
|
188
194
|
labels: labels.join(","),
|
|
189
|
-
perPage:
|
|
195
|
+
perPage: hostingPageSize,
|
|
190
196
|
});
|
|
191
197
|
return issues.map((issue) => ({
|
|
192
198
|
id: String(issue.id),
|
|
@@ -216,7 +222,7 @@ export class GitLabHosting {
|
|
|
216
222
|
const changes = await this.client.MergeRequests.all({
|
|
217
223
|
projectId: this.repository,
|
|
218
224
|
sourceBranch: branch,
|
|
219
|
-
perPage:
|
|
225
|
+
perPage: hostingPageSize,
|
|
220
226
|
});
|
|
221
227
|
if (changes.length > 1)
|
|
222
228
|
throw new BlockedError("Multiple merge requests match branch");
|
|
@@ -237,7 +243,7 @@ export class GitLabHosting {
|
|
|
237
243
|
.sha;
|
|
238
244
|
}
|
|
239
245
|
async publishReview(input) {
|
|
240
|
-
const notes = await this.client.MergeRequestNotes.all(this.repository, input.change.id, { perPage:
|
|
246
|
+
const notes = await this.client.MergeRequestNotes.all(this.repository, input.change.id, { perPage: hostingPageSize });
|
|
241
247
|
if (notes.some((note) => note.body.includes(marker(input.runId))))
|
|
242
248
|
return;
|
|
243
249
|
const change = await this.client.MergeRequests.show(this.repository, input.change.id);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ModelInfo, SessionConfig } from "@github/copilot-sdk";
|
|
2
|
-
import type { ThreadEvent, ThreadOptions } from "@openai/codex-sdk";
|
|
2
|
+
import type { ThreadEvent, ThreadOptions, TurnOptions } from "@openai/codex-sdk";
|
|
3
3
|
import type { AgentProfile } from "../config.js";
|
|
4
4
|
export interface WorkerInput {
|
|
5
5
|
provider: "codex" | "copilot";
|
|
@@ -8,7 +8,7 @@ export interface WorkerInput {
|
|
|
8
8
|
cwd: string;
|
|
9
9
|
prompt: string;
|
|
10
10
|
profile: AgentProfile;
|
|
11
|
-
|
|
11
|
+
outputSchema?: unknown;
|
|
12
12
|
readOnly: boolean;
|
|
13
13
|
processFile?: string;
|
|
14
14
|
timeoutMs?: number;
|
|
@@ -16,7 +16,7 @@ export interface WorkerInput {
|
|
|
16
16
|
export type Emit = (type: string, value: unknown) => void;
|
|
17
17
|
export interface CodexClient {
|
|
18
18
|
startThread(options: ThreadOptions): {
|
|
19
|
-
runStreamed(prompt: string): Promise<{
|
|
19
|
+
runStreamed(prompt: string, options?: TurnOptions): Promise<{
|
|
20
20
|
events: AsyncIterable<ThreadEvent>;
|
|
21
21
|
}>;
|
|
22
22
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defaultStageTimeoutMs } from "../defaults.js";
|
|
2
2
|
export async function runCodex(client, input, emit) {
|
|
3
3
|
const thread = client.startThread({
|
|
4
4
|
workingDirectory: input.cwd,
|
|
@@ -8,7 +8,9 @@ export async function runCodex(client, input, emit) {
|
|
|
8
8
|
sandboxMode: input.readOnly ? "read-only" : "workspace-write",
|
|
9
9
|
approvalPolicy: "never",
|
|
10
10
|
});
|
|
11
|
-
const turn = await thread.runStreamed(input.prompt
|
|
11
|
+
const turn = await thread.runStreamed(input.prompt, {
|
|
12
|
+
outputSchema: input.outputSchema,
|
|
13
|
+
});
|
|
12
14
|
let output = "";
|
|
13
15
|
for await (const event of turn.events) {
|
|
14
16
|
if (event.type === "thread.started")
|
|
@@ -39,17 +41,17 @@ export async function runCopilot(client, input, emit) {
|
|
|
39
41
|
reasoningEffort: input.profile
|
|
40
42
|
.reasoningEffort,
|
|
41
43
|
workingDirectory: input.cwd,
|
|
42
|
-
skillDirectories: input.skills.map(dirname),
|
|
43
44
|
infiniteSessions: input.profile.context
|
|
44
45
|
? { enabled: true, ...input.profile.context }
|
|
45
46
|
: undefined,
|
|
46
|
-
onPermissionRequest: async (request) =>
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
onPermissionRequest: async (request) => request.managedApprovalRequired ||
|
|
48
|
+
(input.readOnly && request.kind !== "read")
|
|
49
|
+
? { kind: "reject" }
|
|
50
|
+
: { kind: "approve-once" },
|
|
49
51
|
});
|
|
50
52
|
emit("session", session.sessionId);
|
|
51
53
|
session.on((event) => emit("event", event));
|
|
52
|
-
const response = await session.sendAndWait({ prompt: input.prompt }, input.timeoutMs ??
|
|
54
|
+
const response = await session.sendAndWait({ prompt: input.prompt }, input.timeoutMs ?? defaultStageTimeoutMs);
|
|
53
55
|
emit("result", response?.data.content ?? "");
|
|
54
56
|
await session.disconnect();
|
|
55
57
|
}
|
|
@@ -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.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env -S node --
|
|
2
2
|
export {};
|
package/dist/src/cli.js
CHANGED
|
@@ -1,21 +1,63 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env -S node --
|
|
2
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
5
|
+
import { parseEnv } from "node:util";
|
|
4
6
|
import { Command } from "commander";
|
|
5
7
|
import { render } from "ink";
|
|
6
8
|
import React from "react";
|
|
7
9
|
import { createAgents } from "./adapters/agents.js";
|
|
8
10
|
import { createHosting } from "./adapters/hosting.js";
|
|
9
11
|
import { configSchema } from "./config.js";
|
|
12
|
+
import { defaultValidationTimeoutMs } from "./defaults.js";
|
|
10
13
|
import { Runner } from "./runner.js";
|
|
11
14
|
import { Store } from "./store.js";
|
|
12
|
-
import { Monitor } from "./tui.js";
|
|
15
|
+
import { createTerminalNotificationWriter, Monitor } from "./tui/index.js";
|
|
16
|
+
const launchDirectory = process.cwd();
|
|
13
17
|
const program = new Command()
|
|
14
18
|
.name("agent-workflows")
|
|
15
19
|
.description("Local durable issue-to-review workflows")
|
|
16
|
-
.option("-c, --config <file>", "configuration path", "agent-workflows.json")
|
|
20
|
+
.option("-c, --config <file>", "configuration path", "agent-workflows.json")
|
|
21
|
+
.option("--config-base-directory <directory>", "base directory for paths contained in configuration")
|
|
22
|
+
.option("--env-file <path>", "load literal dotenv values; existing environment wins")
|
|
23
|
+
.hook("preAction", async () => {
|
|
24
|
+
const path = program.opts().envFile;
|
|
25
|
+
if (path === undefined)
|
|
26
|
+
return;
|
|
27
|
+
const file = resolve(launchDirectory, path);
|
|
28
|
+
let contents;
|
|
29
|
+
try {
|
|
30
|
+
contents = await readFile(file, "utf8");
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new Error(`Cannot read environment file ${file} (${error.code ?? "read failed"})`);
|
|
34
|
+
}
|
|
35
|
+
for (const [name, value] of Object.entries(parseEnv(contents))) {
|
|
36
|
+
if (process.env[name] === undefined)
|
|
37
|
+
process.env[name] = value;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
17
40
|
async function configuration() {
|
|
18
|
-
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;
|
|
19
61
|
}
|
|
20
62
|
function databaseUrl(config) {
|
|
21
63
|
const value = process.env[config.databaseUrlEnv];
|
|
@@ -38,14 +80,18 @@ program
|
|
|
38
80
|
.command("init")
|
|
39
81
|
.description("Write a configuration scaffold without overwriting files")
|
|
40
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) || ".";
|
|
41
87
|
const config = {
|
|
42
88
|
id: "local",
|
|
43
89
|
databaseUrlEnv: "AGENT_WORKFLOWS_DATABASE_URL",
|
|
44
|
-
stateDirectory:
|
|
90
|
+
stateDirectory: portable(externalState),
|
|
45
91
|
projects: [
|
|
46
92
|
{
|
|
47
93
|
id: "example",
|
|
48
|
-
checkout:
|
|
94
|
+
checkout: portable(checkout),
|
|
49
95
|
hosting: {
|
|
50
96
|
provider: "github",
|
|
51
97
|
origin: "https://github.com",
|
|
@@ -55,25 +101,24 @@ program
|
|
|
55
101
|
labels: ["ready-for-agent"],
|
|
56
102
|
baseBranch: "main",
|
|
57
103
|
branchTemplate: "agent/{issue}-{attempt}",
|
|
58
|
-
|
|
104
|
+
includeAgentCoAuthors: true,
|
|
59
105
|
agent: { provider: "codex" },
|
|
60
|
-
validation: [
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
writing: {
|
|
66
|
-
prompt: "Describe the actual changes and validation accurately.",
|
|
67
|
-
},
|
|
68
|
-
review: {
|
|
69
|
-
prompt: "Review correctness, safety and the issue acceptance criteria.",
|
|
106
|
+
validation: [
|
|
107
|
+
{
|
|
108
|
+
command: "pnpm",
|
|
109
|
+
args: ["test"],
|
|
110
|
+
timeoutMs: defaultValidationTimeoutMs,
|
|
70
111
|
},
|
|
71
|
-
|
|
112
|
+
],
|
|
113
|
+
stages: { implementation: {}, publication: {}, review: {} },
|
|
72
114
|
},
|
|
73
115
|
],
|
|
74
116
|
};
|
|
75
|
-
await writeFile(
|
|
76
|
-
|
|
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.");
|
|
77
122
|
});
|
|
78
123
|
program
|
|
79
124
|
.command("run")
|
|
@@ -88,6 +133,7 @@ program
|
|
|
88
133
|
config.projects = config.projects.filter((project) => wanted.has(project.id));
|
|
89
134
|
}
|
|
90
135
|
const runner = new Runner({
|
|
136
|
+
pathBaseDirectory: configBaseDirectory(),
|
|
91
137
|
config,
|
|
92
138
|
databaseUrl: databaseUrl(config),
|
|
93
139
|
hosting: createHosting,
|
|
@@ -126,6 +172,7 @@ program
|
|
|
126
172
|
program.command("inspect <run>").action(async (run) => withStore(async (store) => console.log(JSON.stringify({
|
|
127
173
|
run: await store.run(run),
|
|
128
174
|
invocations: await store.invocations(run),
|
|
175
|
+
recovery: await store.recoveryPlan(run),
|
|
129
176
|
}, null, 2))));
|
|
130
177
|
program
|
|
131
178
|
.command("logs <run>")
|
|
@@ -149,7 +196,7 @@ program
|
|
|
149
196
|
}
|
|
150
197
|
}
|
|
151
198
|
}));
|
|
152
|
-
for (const kind of ["pause", "resume", "stop", "retry"])
|
|
199
|
+
for (const kind of ["pause", "resume", "stop", "retry", "recover"])
|
|
153
200
|
program
|
|
154
201
|
.command(`${kind} <target>`)
|
|
155
202
|
.description(`${kind} project or run through the active runner`)
|
|
@@ -159,11 +206,19 @@ for (const kind of ["pause", "resume", "stop", "retry"])
|
|
|
159
206
|
status: "pending",
|
|
160
207
|
}));
|
|
161
208
|
}));
|
|
162
|
-
program
|
|
163
|
-
|
|
209
|
+
program
|
|
210
|
+
.command("monitor")
|
|
211
|
+
.option("--notify", "notify when an observed execution reaches an outcome")
|
|
212
|
+
.action(async (options) => {
|
|
213
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
164
214
|
throw new Error("Monitor requires an interactive terminal; use status --json instead");
|
|
165
215
|
await withStore(async (store) => {
|
|
166
|
-
await render(React.createElement(Monitor, {
|
|
216
|
+
await render(React.createElement(Monitor, {
|
|
217
|
+
source: store,
|
|
218
|
+
notificationWriter: options.notify
|
|
219
|
+
? createTerminalNotificationWriter()
|
|
220
|
+
: undefined,
|
|
221
|
+
}), { alternateScreen: true }).waitUntilExit();
|
|
167
222
|
});
|
|
168
223
|
});
|
|
169
224
|
try {
|
package/dist/src/config.d.ts
CHANGED
|
@@ -25,8 +25,8 @@ export declare const stageSchema: z.ZodObject<{
|
|
|
25
25
|
bufferExhaustionThreshold: z.ZodOptional<z.ZodNumber>;
|
|
26
26
|
}, z.core.$strict>>>;
|
|
27
27
|
}, z.core.$strict>>;
|
|
28
|
-
prompt: z.
|
|
29
|
-
|
|
28
|
+
prompt: z.ZodOptional<z.ZodString>;
|
|
29
|
+
promptFile: z.ZodOptional<z.ZodString>;
|
|
30
30
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
31
31
|
}, z.core.$strict>;
|
|
32
32
|
export declare const projectSchema: z.ZodObject<{
|
|
@@ -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";
|
|
@@ -67,8 +64,8 @@ export declare const projectSchema: z.ZodObject<{
|
|
|
67
64
|
bufferExhaustionThreshold: z.ZodOptional<z.ZodNumber>;
|
|
68
65
|
}, z.core.$strict>>;
|
|
69
66
|
}, z.core.$strict>;
|
|
70
|
-
stages: z.
|
|
71
|
-
implementation: z.
|
|
67
|
+
stages: z.ZodPrefault<z.ZodObject<{
|
|
68
|
+
implementation: z.ZodPrefault<z.ZodObject<{
|
|
72
69
|
profile: z.ZodOptional<z.ZodObject<{
|
|
73
70
|
provider: z.ZodOptional<z.ZodEnum<{
|
|
74
71
|
codex: "codex";
|
|
@@ -81,11 +78,11 @@ export declare const projectSchema: z.ZodObject<{
|
|
|
81
78
|
bufferExhaustionThreshold: z.ZodOptional<z.ZodNumber>;
|
|
82
79
|
}, z.core.$strict>>>;
|
|
83
80
|
}, z.core.$strict>>;
|
|
84
|
-
prompt: z.
|
|
85
|
-
|
|
81
|
+
prompt: z.ZodOptional<z.ZodString>;
|
|
82
|
+
promptFile: z.ZodOptional<z.ZodString>;
|
|
86
83
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
87
84
|
}, z.core.$strict>>;
|
|
88
|
-
|
|
85
|
+
publication: z.ZodPrefault<z.ZodObject<{
|
|
89
86
|
profile: z.ZodOptional<z.ZodObject<{
|
|
90
87
|
provider: z.ZodOptional<z.ZodEnum<{
|
|
91
88
|
codex: "codex";
|
|
@@ -98,11 +95,11 @@ export declare const projectSchema: z.ZodObject<{
|
|
|
98
95
|
bufferExhaustionThreshold: z.ZodOptional<z.ZodNumber>;
|
|
99
96
|
}, z.core.$strict>>>;
|
|
100
97
|
}, z.core.$strict>>;
|
|
101
|
-
prompt: z.
|
|
102
|
-
|
|
98
|
+
prompt: z.ZodOptional<z.ZodString>;
|
|
99
|
+
promptFile: z.ZodOptional<z.ZodString>;
|
|
103
100
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
104
101
|
}, z.core.$strict>>;
|
|
105
|
-
review: z.
|
|
102
|
+
review: z.ZodPrefault<z.ZodObject<{
|
|
106
103
|
profile: z.ZodOptional<z.ZodObject<{
|
|
107
104
|
provider: z.ZodOptional<z.ZodEnum<{
|
|
108
105
|
codex: "codex";
|
|
@@ -115,8 +112,8 @@ export declare const projectSchema: z.ZodObject<{
|
|
|
115
112
|
bufferExhaustionThreshold: z.ZodOptional<z.ZodNumber>;
|
|
116
113
|
}, z.core.$strict>>>;
|
|
117
114
|
}, z.core.$strict>>;
|
|
118
|
-
prompt: z.
|
|
119
|
-
|
|
115
|
+
prompt: z.ZodOptional<z.ZodString>;
|
|
116
|
+
promptFile: z.ZodOptional<z.ZodString>;
|
|
120
117
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
121
118
|
}, z.core.$strict>>;
|
|
122
119
|
}, z.core.$strict>>;
|
|
@@ -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";
|
|
@@ -163,8 +157,8 @@ export declare const configSchema: z.ZodObject<{
|
|
|
163
157
|
bufferExhaustionThreshold: z.ZodOptional<z.ZodNumber>;
|
|
164
158
|
}, z.core.$strict>>;
|
|
165
159
|
}, z.core.$strict>;
|
|
166
|
-
stages: z.
|
|
167
|
-
implementation: z.
|
|
160
|
+
stages: z.ZodPrefault<z.ZodObject<{
|
|
161
|
+
implementation: z.ZodPrefault<z.ZodObject<{
|
|
168
162
|
profile: z.ZodOptional<z.ZodObject<{
|
|
169
163
|
provider: z.ZodOptional<z.ZodEnum<{
|
|
170
164
|
codex: "codex";
|
|
@@ -177,11 +171,11 @@ export declare const configSchema: z.ZodObject<{
|
|
|
177
171
|
bufferExhaustionThreshold: z.ZodOptional<z.ZodNumber>;
|
|
178
172
|
}, z.core.$strict>>>;
|
|
179
173
|
}, z.core.$strict>>;
|
|
180
|
-
prompt: z.
|
|
181
|
-
|
|
174
|
+
prompt: z.ZodOptional<z.ZodString>;
|
|
175
|
+
promptFile: z.ZodOptional<z.ZodString>;
|
|
182
176
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
183
177
|
}, z.core.$strict>>;
|
|
184
|
-
|
|
178
|
+
publication: z.ZodPrefault<z.ZodObject<{
|
|
185
179
|
profile: z.ZodOptional<z.ZodObject<{
|
|
186
180
|
provider: z.ZodOptional<z.ZodEnum<{
|
|
187
181
|
codex: "codex";
|
|
@@ -194,11 +188,11 @@ export declare const configSchema: z.ZodObject<{
|
|
|
194
188
|
bufferExhaustionThreshold: z.ZodOptional<z.ZodNumber>;
|
|
195
189
|
}, z.core.$strict>>>;
|
|
196
190
|
}, z.core.$strict>>;
|
|
197
|
-
prompt: z.
|
|
198
|
-
|
|
191
|
+
prompt: z.ZodOptional<z.ZodString>;
|
|
192
|
+
promptFile: z.ZodOptional<z.ZodString>;
|
|
199
193
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
200
194
|
}, z.core.$strict>>;
|
|
201
|
-
review: z.
|
|
195
|
+
review: z.ZodPrefault<z.ZodObject<{
|
|
202
196
|
profile: z.ZodOptional<z.ZodObject<{
|
|
203
197
|
provider: z.ZodOptional<z.ZodEnum<{
|
|
204
198
|
codex: "codex";
|
|
@@ -211,8 +205,8 @@ export declare const configSchema: z.ZodObject<{
|
|
|
211
205
|
bufferExhaustionThreshold: z.ZodOptional<z.ZodNumber>;
|
|
212
206
|
}, z.core.$strict>>>;
|
|
213
207
|
}, z.core.$strict>>;
|
|
214
|
-
prompt: z.
|
|
215
|
-
|
|
208
|
+
prompt: z.ZodOptional<z.ZodString>;
|
|
209
|
+
promptFile: z.ZodOptional<z.ZodString>;
|
|
216
210
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
217
211
|
}, z.core.$strict>>;
|
|
218
212
|
}, z.core.$strict>>;
|