@mingchuno/agent-workflows 0.1.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 (62) hide show
  1. package/LICENCE +21 -0
  2. package/README.md +74 -0
  3. package/dist/drizzle/0000_initial.sql +45 -0
  4. package/dist/drizzle/meta/0000_snapshot.json +264 -0
  5. package/dist/drizzle/meta/_journal.json +13 -0
  6. package/dist/src/adapters/agent-worker.d.ts +1 -0
  7. package/dist/src/adapters/agent-worker.js +16 -0
  8. package/dist/src/adapters/agents.d.ts +24 -0
  9. package/dist/src/adapters/agents.js +142 -0
  10. package/dist/src/adapters/hosting.d.ts +33 -0
  11. package/dist/src/adapters/hosting.js +275 -0
  12. package/dist/src/adapters/sdk-protocol.d.ts +43 -0
  13. package/dist/src/adapters/sdk-protocol.js +64 -0
  14. package/dist/src/cli.d.ts +2 -0
  15. package/dist/src/cli.js +175 -0
  16. package/dist/src/config.d.ts +224 -0
  17. package/dist/src/config.js +82 -0
  18. package/dist/src/db/locks.d.ts +4 -0
  19. package/dist/src/db/locks.js +14 -0
  20. package/dist/src/db/migrate.d.ts +1 -0
  21. package/dist/src/db/migrate.js +12 -0
  22. package/dist/src/db/migrations.d.ts +2 -0
  23. package/dist/src/db/migrations.js +22 -0
  24. package/dist/src/db/schema.d.ts +486 -0
  25. package/dist/src/db/schema.js +46 -0
  26. package/dist/src/domain.d.ts +133 -0
  27. package/dist/src/domain.js +24 -0
  28. package/dist/src/index.d.ts +8 -0
  29. package/dist/src/index.js +8 -0
  30. package/dist/src/operations.d.ts +35 -0
  31. package/dist/src/operations.js +378 -0
  32. package/dist/src/run-record.d.ts +7 -0
  33. package/dist/src/run-record.js +19 -0
  34. package/dist/src/runner.d.ts +47 -0
  35. package/dist/src/runner.js +370 -0
  36. package/dist/src/runtime/ownership.d.ts +8 -0
  37. package/dist/src/runtime/ownership.js +84 -0
  38. package/dist/src/runtime/process.d.ts +18 -0
  39. package/dist/src/runtime/process.js +98 -0
  40. package/dist/src/runtime/redaction.d.ts +8 -0
  41. package/dist/src/runtime/redaction.js +33 -0
  42. package/dist/src/store.d.ts +87 -0
  43. package/dist/src/store.js +355 -0
  44. package/dist/src/tui-data.d.ts +25 -0
  45. package/dist/src/tui-data.js +89 -0
  46. package/dist/src/tui.d.ts +5 -0
  47. package/dist/src/tui.js +69 -0
  48. package/dist/src/workspace.d.ts +16 -0
  49. package/dist/src/workspace.js +186 -0
  50. package/docs/acceptance.md +35 -0
  51. package/docs/api.md +64 -0
  52. package/docs/architecture.md +24 -0
  53. package/docs/configuration.md +41 -0
  54. package/docs/database.md +28 -0
  55. package/docs/operations.md +46 -0
  56. package/docs/providers.md +49 -0
  57. package/docs/releases.md +89 -0
  58. package/examples/config.ts +57 -0
  59. package/examples/custom-workflow.ts +32 -0
  60. package/examples/observe.ts +18 -0
  61. package/examples/run.ts +31 -0
  62. package/package.json +78 -0
@@ -0,0 +1,275 @@
1
+ import { Gitlab } from "@gitbeaker/rest";
2
+ import { Octokit } from "@octokit/rest";
3
+ import { BlockedError, } from "../domain.js";
4
+ function origin(value) {
5
+ const url = new URL(value);
6
+ if (url.username || url.password || url.search || url.hash)
7
+ throw new Error("Hosting origin must not contain credentials, query or fragment");
8
+ if (url.protocol !== "https:" &&
9
+ !(url.protocol === "http:" &&
10
+ ["127.0.0.1", "localhost"].includes(url.hostname)))
11
+ throw new Error("Hosting origin requires HTTPS");
12
+ return url.href.replace(/\/$/, "");
13
+ }
14
+ function token(project) {
15
+ const value = process.env[project.hosting.tokenEnv];
16
+ if (!value)
17
+ throw new Error(`Missing credential environment variable: ${project.hosting.tokenEnv}`);
18
+ return value;
19
+ }
20
+ function marker(runId, suffix = "") {
21
+ return `<!-- agent-workflows:${runId}${suffix} -->`;
22
+ }
23
+ export function inlineFindings(review, diff) {
24
+ const lines = new Map();
25
+ let path = "", line = 0;
26
+ for (const text of diff.split("\n")) {
27
+ if (text.startsWith("+++ b/")) {
28
+ path = text.slice(6);
29
+ lines.set(path, new Set());
30
+ }
31
+ else if (text.startsWith("@@")) {
32
+ line = Number(/\+(\d+)/.exec(text)?.[1] ?? 0);
33
+ }
34
+ else if (text.startsWith("+")) {
35
+ lines.get(path)?.add(line);
36
+ line++;
37
+ }
38
+ else if (!text.startsWith("-") && !text.startsWith("\\"))
39
+ line++;
40
+ }
41
+ return review.findings.filter((finding) => !!finding.path &&
42
+ !!finding.line &&
43
+ !!lines.get(finding.path)?.has(finding.line));
44
+ }
45
+ function reviewPresentation(input) {
46
+ const inline = inlineFindings(input.review, input.diff);
47
+ const body = [
48
+ `Review of ${input.head}`,
49
+ input.review.summary,
50
+ ...input.review.findings
51
+ .filter((finding) => !inline.includes(finding))
52
+ .map((finding) => finding.body),
53
+ marker(input.runId),
54
+ ].join("\n\n");
55
+ return { inline, body };
56
+ }
57
+ export class GitHubHosting {
58
+ identity;
59
+ client;
60
+ repo;
61
+ constructor(project) {
62
+ const host = origin(project.hosting.origin);
63
+ const [owner, repo, ...extra] = project.hosting.repository.split("/");
64
+ if (!owner || !repo || extra.length)
65
+ throw new Error("GitHub repository must be owner/repo");
66
+ this.repo = { owner, repo };
67
+ this.identity = `${host}/${owner}/${repo}`;
68
+ this.client = new Octokit({
69
+ auth: token(project),
70
+ baseUrl: host === "https://github.com"
71
+ ? "https://api.github.com"
72
+ : `${host}/api/v3`,
73
+ request: { timeout: 30000, redirect: "error" },
74
+ });
75
+ }
76
+ async listIssues(labels) {
77
+ const issues = await this.client.paginate(this.client.issues.listForRepo, {
78
+ ...this.repo,
79
+ state: "open",
80
+ labels: labels.join(","),
81
+ per_page: 100,
82
+ });
83
+ return issues
84
+ .filter((issue) => !issue.pull_request)
85
+ .map((issue) => ({
86
+ id: String(issue.id),
87
+ number: issue.number,
88
+ title: issue.title,
89
+ body: issue.body ?? "",
90
+ url: issue.html_url,
91
+ labels: issue.labels.map((label) => typeof label === "string" ? label : (label.name ?? "")),
92
+ open: issue.state === "open",
93
+ }));
94
+ }
95
+ async getIssue(number) {
96
+ const { data } = await this.client.issues.get({
97
+ ...this.repo,
98
+ issue_number: number,
99
+ });
100
+ return {
101
+ id: String(data.id),
102
+ number: data.number,
103
+ title: data.title,
104
+ body: data.body ?? "",
105
+ url: data.html_url,
106
+ labels: data.labels.map((label) => typeof label === "string" ? label : (label.name ?? "")),
107
+ open: data.state === "open" && !data.pull_request,
108
+ };
109
+ }
110
+ async findChange(branch) {
111
+ const changes = await this.client.paginate(this.client.pulls.list, {
112
+ ...this.repo,
113
+ head: `${this.repo.owner}:${branch}`,
114
+ state: "all",
115
+ per_page: 100,
116
+ });
117
+ if (changes.length > 1)
118
+ throw new BlockedError("Multiple change requests match branch");
119
+ const change = changes[0];
120
+ return change
121
+ ? { id: change.number, url: change.html_url, head: change.head.sha }
122
+ : undefined;
123
+ }
124
+ async createChange(input) {
125
+ const { data } = await this.client.pulls.create({
126
+ ...this.repo,
127
+ head: input.branch,
128
+ base: input.base,
129
+ title: input.publication.title,
130
+ body: `${input.publication.description}\n\nRefs ${input.issue.url}\n${marker(input.runId)}`,
131
+ draft: true,
132
+ });
133
+ return { id: data.number, url: data.html_url, head: data.head.sha };
134
+ }
135
+ async head(change) {
136
+ return (await this.client.pulls.get({ ...this.repo, pull_number: change.id })).data.head.sha;
137
+ }
138
+ async publishReview(input) {
139
+ const reviews = await this.client.paginate(this.client.pulls.listReviews, {
140
+ ...this.repo,
141
+ pull_number: input.change.id,
142
+ per_page: 100,
143
+ });
144
+ if (reviews.some((review) => review.body?.includes(marker(input.runId))))
145
+ return;
146
+ if ((await this.head(input.change)) !== input.head)
147
+ throw new BlockedError("Review stale: head changed");
148
+ const { inline, body } = reviewPresentation(input);
149
+ await this.client.pulls.createReview({
150
+ ...this.repo,
151
+ pull_number: input.change.id,
152
+ commit_id: input.head,
153
+ event: "COMMENT",
154
+ body,
155
+ comments: inline.map((finding) => ({
156
+ path: finding.path,
157
+ line: finding.line,
158
+ side: "RIGHT",
159
+ body: finding.body,
160
+ })),
161
+ });
162
+ }
163
+ }
164
+ export class GitLabHosting {
165
+ identity;
166
+ client;
167
+ repository;
168
+ constructor(project) {
169
+ const host = origin(project.hosting.origin);
170
+ this.repository = project.hosting.repository;
171
+ this.identity = `${host}/${this.repository}`;
172
+ this.client = new Gitlab({
173
+ host,
174
+ token: token(project),
175
+ queryTimeout: 30000,
176
+ });
177
+ }
178
+ async preflight() {
179
+ const metadata = await this.client.Metadata.show();
180
+ 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}`);
183
+ }
184
+ async listIssues(labels) {
185
+ const issues = await this.client.Issues.all({
186
+ projectId: this.repository,
187
+ state: "opened",
188
+ labels: labels.join(","),
189
+ perPage: 100,
190
+ });
191
+ return issues.map((issue) => ({
192
+ id: String(issue.id),
193
+ number: issue.iid,
194
+ title: issue.title,
195
+ body: issue.description ?? "",
196
+ url: issue.web_url,
197
+ labels: issue.labels,
198
+ open: issue.state === "opened",
199
+ }));
200
+ }
201
+ async getIssue(number) {
202
+ const issue = await this.client.Issues.show(number, {
203
+ projectId: this.repository,
204
+ });
205
+ return {
206
+ id: String(issue.id),
207
+ number: issue.iid,
208
+ title: issue.title,
209
+ body: issue.description ?? "",
210
+ url: issue.web_url,
211
+ labels: issue.labels.map((label) => typeof label === "string" ? label : label.name),
212
+ open: issue.state === "opened",
213
+ };
214
+ }
215
+ async findChange(branch) {
216
+ const changes = await this.client.MergeRequests.all({
217
+ projectId: this.repository,
218
+ sourceBranch: branch,
219
+ perPage: 100,
220
+ });
221
+ if (changes.length > 1)
222
+ throw new BlockedError("Multiple merge requests match branch");
223
+ const change = changes[0];
224
+ return change
225
+ ? { id: change.iid, url: change.web_url, head: change.sha }
226
+ : undefined;
227
+ }
228
+ async createChange(input) {
229
+ const change = await this.client.MergeRequests.create(this.repository, input.branch, input.base, `Draft: ${input.publication.title}`, {
230
+ description: `${input.publication.description}\n\nRefs ${input.issue.url}\n${marker(input.runId)}`,
231
+ removeSourceBranch: false,
232
+ });
233
+ return { id: change.iid, url: change.web_url, head: change.sha };
234
+ }
235
+ async head(change) {
236
+ return (await this.client.MergeRequests.show(this.repository, change.id))
237
+ .sha;
238
+ }
239
+ async publishReview(input) {
240
+ const notes = await this.client.MergeRequestNotes.all(this.repository, input.change.id, { perPage: 100 });
241
+ if (notes.some((note) => note.body.includes(marker(input.runId))))
242
+ return;
243
+ const change = await this.client.MergeRequests.show(this.repository, input.change.id);
244
+ if (change.sha !== input.head)
245
+ throw new BlockedError("Review stale: head changed");
246
+ const { inline, body } = reviewPresentation(input);
247
+ for (const [index, finding] of inline.entries()) {
248
+ const tag = marker(input.runId, `:inline:${index}`);
249
+ if (notes.some((note) => note.body.includes(tag)))
250
+ continue;
251
+ const refs = change.diff_refs;
252
+ if (!refs)
253
+ throw new BlockedError("GitLab diff refs unavailable");
254
+ await this.client.MergeRequestDiscussions.create(this.repository, input.change.id, `${finding.body}\n\n${tag}`, {
255
+ position: {
256
+ positionType: "text",
257
+ baseSha: refs.base_sha,
258
+ startSha: refs.start_sha,
259
+ headSha: input.head,
260
+ newPath: finding.path,
261
+ oldPath: finding.path,
262
+ newLine: String(finding.line),
263
+ },
264
+ });
265
+ }
266
+ if ((await this.head(input.change)) !== input.head)
267
+ throw new BlockedError("Review stale: head changed");
268
+ await this.client.MergeRequestNotes.create(this.repository, input.change.id, body);
269
+ }
270
+ }
271
+ export function createHosting(project) {
272
+ return project.hosting.provider === "github"
273
+ ? new GitHubHosting(project)
274
+ : new GitLabHosting(project);
275
+ }
@@ -0,0 +1,43 @@
1
+ import type { ModelInfo, SessionConfig } from "@github/copilot-sdk";
2
+ import type { ThreadEvent, ThreadOptions } from "@openai/codex-sdk";
3
+ import type { AgentProfile } from "../config.js";
4
+ export interface WorkerInput {
5
+ provider: "codex" | "copilot";
6
+ operation: string;
7
+ id: string;
8
+ cwd: string;
9
+ prompt: string;
10
+ profile: AgentProfile;
11
+ skills: string[];
12
+ readOnly: boolean;
13
+ processFile?: string;
14
+ timeoutMs?: number;
15
+ }
16
+ export type Emit = (type: string, value: unknown) => void;
17
+ export interface CodexClient {
18
+ startThread(options: ThreadOptions): {
19
+ runStreamed(prompt: string): Promise<{
20
+ events: AsyncIterable<ThreadEvent>;
21
+ }>;
22
+ };
23
+ }
24
+ export declare function runCodex(client: CodexClient, input: WorkerInput, emit: Emit): Promise<void>;
25
+ export interface CopilotSessionClient {
26
+ sessionId: string;
27
+ on(handler: (event: unknown) => void): unknown;
28
+ sendAndWait(message: {
29
+ prompt: string;
30
+ }, timeout: number): Promise<{
31
+ data: {
32
+ content: string;
33
+ };
34
+ } | undefined>;
35
+ disconnect(): Promise<void>;
36
+ }
37
+ export interface CopilotClientContract {
38
+ listModels(): Promise<ModelInfo[]>;
39
+ createSession(options: SessionConfig): Promise<CopilotSessionClient>;
40
+ stop(): Promise<Error[]>;
41
+ forceStop(): Promise<void>;
42
+ }
43
+ export declare function runCopilot(client: CopilotClientContract, input: WorkerInput, emit: Emit): Promise<void>;
@@ -0,0 +1,64 @@
1
+ import { dirname } from "node:path";
2
+ export async function runCodex(client, input, emit) {
3
+ const thread = client.startThread({
4
+ workingDirectory: input.cwd,
5
+ model: input.profile.model,
6
+ modelReasoningEffort: input.profile
7
+ .reasoningEffort,
8
+ sandboxMode: input.readOnly ? "read-only" : "workspace-write",
9
+ approvalPolicy: "never",
10
+ });
11
+ const turn = await thread.runStreamed(input.prompt);
12
+ let output = "";
13
+ for await (const event of turn.events) {
14
+ if (event.type === "thread.started")
15
+ emit("session", event.thread_id);
16
+ emit("event", event);
17
+ if (event.type === "item.completed" && event.item.type === "agent_message")
18
+ output = event.item.text;
19
+ if (event.type === "turn.failed")
20
+ throw new Error(event.error.message);
21
+ if (event.type === "error")
22
+ throw new Error(event.message);
23
+ }
24
+ emit("result", output);
25
+ }
26
+ export async function runCopilot(client, input, emit) {
27
+ try {
28
+ if (input.operation === "models") {
29
+ emit("result", JSON.stringify((await client.listModels()).map((model) => ({
30
+ id: model.id,
31
+ reasoningEfforts: model.supportedReasoningEfforts ?? [],
32
+ contextWindowTokens: model.capabilities.limits.max_context_window_tokens,
33
+ }))));
34
+ return;
35
+ }
36
+ const session = await client.createSession({
37
+ sessionId: input.id,
38
+ model: input.profile.model,
39
+ reasoningEffort: input.profile
40
+ .reasoningEffort,
41
+ workingDirectory: input.cwd,
42
+ skillDirectories: input.skills.map(dirname),
43
+ infiniteSessions: input.profile.context
44
+ ? { enabled: true, ...input.profile.context }
45
+ : 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" },
49
+ });
50
+ emit("session", session.sessionId);
51
+ session.on((event) => emit("event", event));
52
+ const response = await session.sendAndWait({ prompt: input.prompt }, input.timeoutMs ?? 1_800_000);
53
+ emit("result", response?.data.content ?? "");
54
+ await session.disconnect();
55
+ }
56
+ finally {
57
+ const errors = await client.stop();
58
+ if (errors.length) {
59
+ await client.forceStop();
60
+ // biome-ignore lint/correctness/noUnsafeFinally: Preserve the existing shutdown-failure contract.
61
+ throw new AggregateError(errors, "Copilot shutdown failed");
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { Command } from "commander";
5
+ import { render } from "ink";
6
+ import React from "react";
7
+ import { createAgents } from "./adapters/agents.js";
8
+ import { createHosting } from "./adapters/hosting.js";
9
+ import { configSchema } from "./config.js";
10
+ import { Runner } from "./runner.js";
11
+ import { Store } from "./store.js";
12
+ import { Monitor } from "./tui.js";
13
+ const program = new Command()
14
+ .name("agent-workflows")
15
+ .description("Local durable issue-to-review workflows")
16
+ .option("-c, --config <file>", "configuration path", "agent-workflows.json");
17
+ async function configuration() {
18
+ return configSchema.parse(JSON.parse(await readFile(resolve(program.opts().config), "utf8")));
19
+ }
20
+ function databaseUrl(config) {
21
+ const value = process.env[config.databaseUrlEnv];
22
+ if (!value)
23
+ throw new Error(`Set ${config.databaseUrlEnv} to a PostgreSQL URL`);
24
+ return value;
25
+ }
26
+ async function withStore(action) {
27
+ const config = await configuration();
28
+ const store = new Store(databaseUrl(config), config.id);
29
+ try {
30
+ await store.initialize();
31
+ await action(store);
32
+ }
33
+ finally {
34
+ await store.close();
35
+ }
36
+ }
37
+ program
38
+ .command("init")
39
+ .description("Write a configuration scaffold without overwriting files")
40
+ .action(async () => {
41
+ const config = {
42
+ id: "local",
43
+ databaseUrlEnv: "AGENT_WORKFLOWS_DATABASE_URL",
44
+ stateDirectory: resolve(".agent-workflows"),
45
+ projects: [
46
+ {
47
+ id: "example",
48
+ checkout: process.cwd(),
49
+ hosting: {
50
+ provider: "github",
51
+ origin: "https://github.com",
52
+ repository: "OWNER/REPOSITORY",
53
+ tokenEnv: "GITHUB_TOKEN",
54
+ },
55
+ labels: ["ready-for-agent"],
56
+ baseBranch: "main",
57
+ branchTemplate: "agent/{issue}-{attempt}",
58
+ gitIdentity: { name: "YOUR NAME", email: "you@example.com" },
59
+ 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.",
70
+ },
71
+ },
72
+ },
73
+ ],
74
+ };
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.");
77
+ });
78
+ program
79
+ .command("run")
80
+ .option("-p, --project <ids...>", "run selected project IDs")
81
+ .action(async (options) => {
82
+ const config = await configuration();
83
+ if (options.project) {
84
+ const wanted = new Set(options.project);
85
+ for (const id of wanted)
86
+ if (!config.projects.some((project) => project.id === id))
87
+ throw new Error(`Unknown project ${id}`);
88
+ config.projects = config.projects.filter((project) => wanted.has(project.id));
89
+ }
90
+ const runner = new Runner({
91
+ config,
92
+ databaseUrl: databaseUrl(config),
93
+ hosting: createHosting,
94
+ agents: createAgents(),
95
+ });
96
+ try {
97
+ await runner.start();
98
+ console.log(`Runner ${config.id} started. Use status or monitor in another terminal.`);
99
+ await new Promise((resolve) => {
100
+ process.once("SIGINT", resolve);
101
+ process.once("SIGTERM", resolve);
102
+ });
103
+ }
104
+ finally {
105
+ await runner.shutdown();
106
+ }
107
+ });
108
+ program
109
+ .command("status")
110
+ .option("--json", "machine-readable output")
111
+ .action(async (options) => withStore(async (store) => {
112
+ const status = {
113
+ projects: await store.projects(),
114
+ runs: await store.runs(),
115
+ commands: await store.commands(),
116
+ };
117
+ if (options.json)
118
+ console.log(JSON.stringify(status));
119
+ else
120
+ for (const project of status.projects) {
121
+ console.log(`${project.id}: ${project.blocked ? "blocked" : project.paused ? "paused" : "enabled"}`);
122
+ for (const run of status.runs.filter((run) => run.projectId === project.id))
123
+ console.log(` ${run.id} #${run.issue.number} attempt ${run.attempt}: ${run.outcome} / ${run.phase}`);
124
+ }
125
+ }));
126
+ program.command("inspect <run>").action(async (run) => withStore(async (store) => console.log(JSON.stringify({
127
+ run: await store.run(run),
128
+ invocations: await store.invocations(run),
129
+ }, null, 2))));
130
+ program
131
+ .command("logs <run>")
132
+ .option("--invocation <id>", "specific invocation")
133
+ .action(async (run, options) => withStore(async (store) => {
134
+ const record = await store.run(run);
135
+ const invocations = (await store.invocations(run)).filter((item) => !options.invocation || item.id === options.invocation);
136
+ const paths = [
137
+ ...invocations.map((item) => item.log),
138
+ ...(!options.invocation
139
+ ? (record.validation?.map((check) => check.log) ?? [])
140
+ : []),
141
+ ];
142
+ for (const path of paths) {
143
+ console.log(`--- ${path}`);
144
+ try {
145
+ console.log(await readFile(path, "utf8"));
146
+ }
147
+ catch {
148
+ console.log("Artifact unavailable");
149
+ }
150
+ }
151
+ }));
152
+ for (const kind of ["pause", "resume", "stop", "retry"])
153
+ program
154
+ .command(`${kind} <target>`)
155
+ .description(`${kind} project or run through the active runner`)
156
+ .action(async (target) => withStore(async (store) => {
157
+ console.log(JSON.stringify({
158
+ commandId: await store.request(kind, target),
159
+ status: "pending",
160
+ }));
161
+ }));
162
+ program.command("monitor").action(async () => {
163
+ if (!process.stdin.isTTY)
164
+ throw new Error("Monitor requires an interactive terminal; use status --json instead");
165
+ await withStore(async (store) => {
166
+ await render(React.createElement(Monitor, { source: store })).waitUntilExit();
167
+ });
168
+ });
169
+ try {
170
+ await program.parseAsync();
171
+ }
172
+ catch (error) {
173
+ console.error(error instanceof Error ? error.message : String(error));
174
+ process.exitCode = 1;
175
+ }