@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.
Files changed (76) hide show
  1. package/README.md +37 -6
  2. package/dist/src/adapters/agents.js +6 -3
  3. package/dist/src/adapters/hosting.js +16 -10
  4. package/dist/src/adapters/sdk-protocol.d.ts +3 -3
  5. package/dist/src/adapters/sdk-protocol.js +9 -7
  6. package/dist/src/attribution.d.ts +9 -0
  7. package/dist/src/attribution.js +57 -0
  8. package/dist/src/cli.d.ts +1 -1
  9. package/dist/src/cli.js +80 -25
  10. package/dist/src/config.d.ts +24 -30
  11. package/dist/src/config.js +32 -27
  12. package/dist/src/defaults.d.ts +2 -0
  13. package/dist/src/defaults.js +2 -0
  14. package/dist/src/domain.d.ts +31 -4
  15. package/dist/src/domain.js +10 -3
  16. package/dist/src/evidence.d.ts +54 -0
  17. package/dist/src/evidence.js +214 -0
  18. package/dist/src/index.d.ts +1 -0
  19. package/dist/src/index.js +1 -0
  20. package/dist/src/invocation.d.ts +25 -0
  21. package/dist/src/invocation.js +166 -0
  22. package/dist/src/operations.d.ts +8 -2
  23. package/dist/src/operations.js +100 -136
  24. package/dist/src/prompts.d.ts +28 -0
  25. package/dist/src/prompts.js +63 -0
  26. package/dist/src/recovery.d.ts +19 -0
  27. package/dist/src/recovery.js +99 -0
  28. package/dist/src/runner.d.ts +7 -0
  29. package/dist/src/runner.js +170 -18
  30. package/dist/src/runtime/process.d.ts +2 -0
  31. package/dist/src/runtime/process.js +41 -12
  32. package/dist/src/store.d.ts +21 -2
  33. package/dist/src/store.js +122 -1
  34. package/dist/src/tui/actions.d.ts +16 -0
  35. package/dist/src/tui/actions.js +23 -0
  36. package/dist/src/tui/constants.d.ts +6 -0
  37. package/dist/src/tui/constants.js +3 -0
  38. package/dist/src/{tui-data.d.ts → tui/data.d.ts} +10 -7
  39. package/dist/src/tui/data.js +146 -0
  40. package/dist/src/tui/dialogs.d.ts +17 -0
  41. package/dist/src/tui/dialogs.js +149 -0
  42. package/dist/src/tui/format.d.ts +7 -0
  43. package/dist/src/tui/format.js +62 -0
  44. package/dist/src/tui/index.d.ts +3 -0
  45. package/dist/src/tui/index.js +2 -0
  46. package/dist/src/tui/layout.d.ts +25 -0
  47. package/dist/src/tui/layout.js +36 -0
  48. package/dist/src/tui/log-file.d.ts +26 -0
  49. package/dist/src/tui/log-file.js +156 -0
  50. package/dist/src/tui/log.d.ts +11 -0
  51. package/dist/src/tui/log.js +90 -0
  52. package/dist/src/tui/monitor.d.ts +10 -0
  53. package/dist/src/tui/monitor.js +284 -0
  54. package/dist/src/tui/notifications.d.ts +23 -0
  55. package/dist/src/tui/notifications.js +104 -0
  56. package/dist/src/tui/text.d.ts +3 -0
  57. package/dist/src/tui/text.js +10 -0
  58. package/dist/src/tui/use-log-controller.d.ts +27 -0
  59. package/dist/src/tui/use-log-controller.js +192 -0
  60. package/dist/src/tui/views.d.ts +25 -0
  61. package/dist/src/tui/views.js +327 -0
  62. package/dist/src/workspace.js +21 -8
  63. package/docs/api.md +132 -8
  64. package/docs/architecture.md +21 -4
  65. package/docs/configuration.md +181 -8
  66. package/docs/database.md +7 -0
  67. package/docs/operations.md +160 -5
  68. package/docs/providers.md +58 -2
  69. package/docs/releases.md +34 -79
  70. package/examples/config.ts +6 -6
  71. package/examples/run.ts +4 -1
  72. package/package.json +4 -2
  73. package/dist/src/tui-data.js +0 -89
  74. package/dist/src/tui.d.ts +0 -5
  75. package/dist/src/tui.js +0 -69
  76. package/docs/acceptance.md +0 -35
package/README.md CHANGED
@@ -1,8 +1,29 @@
1
1
  # Agent Workflows
2
2
 
3
+ ![Print-shop workbench with a job sheet, a worked proof, and an inspected copy awaiting human approval.](docs/assets/hero.png)
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
- Phase 1 includes Codex and Copilot SDK adapters, GitHub and GitLab.com/self-hosted GitLab adapters, durable publication reconciliation, session observability and explicit operator controls. Human review and merging remain separate.
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, Git identity, agent stages and validation commands. Set the named hosting-token environment variable through your usual secret manager. Commit or ignore the configuration before running. Keep the state directory outside managed checkouts, or explicitly Git-ignore it.
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
- - [Architecture and review evidence](docs/architecture.md)
72
- - [Phase 1 acceptance](docs/acceptance.md)
73
- - [Release setup, first publication and recovery](docs/releases.md)
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
- skills: invocation.skills,
69
+ outputSchema: invocation.outputSchema,
69
70
  readOnly: invocation.readOnly,
70
- timeoutMs: invocation.timeoutMs ?? 1_800_000,
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 ? (invocation.timeoutMs ?? 1_800_000) : 30_000,
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: 30000, redirect: "error" },
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: 100,
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: 100,
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: 100,
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: 30000,
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) || major < 17 || major > 19)
182
- throw new Error(`Supported GitLab versions are 17.x–19.x; instance reports ${metadata.version}`);
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: 100,
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: 100,
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: 100 });
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
- skills: string[];
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 { dirname } from "node:path";
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) => input.readOnly && request.kind !== "read"
47
- ? { kind: "denied-no-approval-rule-and-could-not-request-from-user" }
48
- : { kind: "approved" },
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 ?? 1_800_000);
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(resolve(program.opts().config), "utf8")));
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: resolve(".agent-workflows"),
90
+ stateDirectory: portable(externalState),
45
91
  projects: [
46
92
  {
47
93
  id: "example",
48
- checkout: process.cwd(),
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
- gitIdentity: { name: "YOUR NAME", email: "you@example.com" },
104
+ includeAgentCoAuthors: true,
59
105
  agent: { provider: "codex" },
60
- validation: [{ command: "pnpm", args: ["test"], timeoutMs: 300000 }],
61
- stages: {
62
- implementation: {
63
- prompt: "Implement the issue and preserve repository conventions.",
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(resolve(program.opts().config), JSON.stringify(config, null, 2) + "\n", { flag: "wx", mode: 0o600 });
76
- console.log("Created configuration. Set repository, checkout, identity and credentials before running.");
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.command("monitor").action(async () => {
163
- if (!process.stdin.isTTY)
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, { source: store })).waitUntilExit();
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 {
@@ -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.ZodDefault<z.ZodString>;
29
- skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
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
- gitIdentity: z.ZodObject<{
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.ZodDefault<z.ZodObject<{
71
- implementation: z.ZodDefault<z.ZodObject<{
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.ZodDefault<z.ZodString>;
85
- skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
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
- writing: z.ZodDefault<z.ZodObject<{
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.ZodDefault<z.ZodString>;
102
- skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
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.ZodDefault<z.ZodObject<{
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.ZodDefault<z.ZodString>;
119
- skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
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
- gitIdentity: z.ZodObject<{
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.ZodDefault<z.ZodObject<{
167
- implementation: z.ZodDefault<z.ZodObject<{
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.ZodDefault<z.ZodString>;
181
- skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
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
- writing: z.ZodDefault<z.ZodObject<{
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.ZodDefault<z.ZodString>;
198
- skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
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.ZodDefault<z.ZodObject<{
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.ZodDefault<z.ZodString>;
215
- skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
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>>;