orcastrator 0.1.1 → 0.2.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 CHANGED
@@ -1,86 +1,200 @@
1
1
  # orca
2
2
 
3
- Orca is a TypeScript CLI harness for coordinated agent planning and execution. It takes a goal or spec file, uses Claude to decompose it into a task graph, runs a pre-execution review pass with Codex, then executes each task via a persistent Codex thread keeping full context across the entire run.
4
-
5
- **Pipeline:** Claude plans → Codex reviews task graph → Codex executes (persistent thread) → Codex post-run review
3
+ Coordinated agent run harness. Breaks down a goal into a task graph, then executes it end-to-end via a persistent [Codex](https://github.com/ratley/codex-client) session with full context across tasks.
6
4
 
7
5
  ## Install
8
6
 
9
7
  ```bash
10
- bun install
8
+ npm install -g orcastrator
11
9
  ```
12
10
 
13
- ## Usage
11
+ ## Run A Goal
14
12
 
15
- ### Inline goal (no file needed)
13
+ Start with a plain-language goal:
16
14
 
17
15
  ```bash
18
- orca -p "add a settings screen to the iOS app"
19
- orca --prompt "refactor the auth module to use JWTs"
20
- orca --task "write unit tests for the payments service"
16
+ orca "add auth to the app"
21
17
  ```
22
18
 
23
- ### Spec file
19
+ Orca will create a run, plan tasks, execute them, and persist run state.
20
+
21
+ ## Spec And Plan Files
22
+
23
+ Use a spec/plan markdown file when you already have a written breakdown:
24
24
 
25
25
  ```bash
26
- orca run --spec ./specs/myfeature.md
26
+ orca --spec ./specs/feature.md
27
+ orca --plan ./specs/feature.md
27
28
  ```
28
29
 
29
- `run` is optional the above is equivalent to:
30
+ If you only want planning (no execution):
30
31
 
31
32
  ```bash
32
- orca --spec ./specs/myfeature.md
33
+ orca plan --spec ./specs/feature.md
33
34
  ```
34
35
 
35
- ### Other commands
36
+ ## Run Management
36
37
 
37
38
  ```bash
38
- orca plan --spec ./specs/myfeature.md # plan only, no execution
39
- orca status --run <run-id> # show status for a specific run
40
- orca status # list all runs with status
41
- orca list # list all runs
42
- orca resume --run <run-id> # resume an incomplete run
43
- orca cancel --run <run-id> # cancel an active run
44
- orca pr-finalize --run <run-id> # finalize a prepared pull request
39
+ orca status
40
+ orca status --last
41
+ orca status --run <run-id>
42
+
43
+ orca list
44
+
45
+ orca resume --last
46
+ orca resume --run <run-id>
47
+
48
+ orca cancel --last
49
+ orca cancel --run <run-id>
50
+
51
+ orca answer <run-id> "yes, use migration A"
45
52
  ```
46
53
 
47
- ### Hooks
54
+ ## PR Workflow
48
55
 
49
56
  ```bash
50
- orca -p "build X" \
51
- --on-task-complete "echo task done: $ORCA_TASK_NAME" \
52
- --on-complete "open -a Terminal" \
53
- --on-error "say orca failed"
57
+ orca pr
58
+ orca pr draft --run <run-id>
59
+ orca pr create --run <run-id>
60
+ orca pr publish --run <run-id>
61
+ orca pr status --run <run-id>
62
+
63
+ orca pr-finalize --config ./orca.config.js
54
64
  ```
55
65
 
56
- Available hooks: `--on-milestone`, `--on-task-complete`, `--on-task-fail`, `--on-complete`, `--on-error`
66
+ ## Config
57
67
 
58
- ## Run output
68
+ Orca auto-discovers config in this order:
59
69
 
60
- Run state is written to:
70
+ 1. `~/.orca/config.js`
71
+ 2. `./orca.config.js` or `./orca.config.ts`
72
+ 3. `--config <path>` (if passed)
61
73
 
62
- ```text
63
- ~/.orca/runs/<run-id>/status.json
74
+ Later entries override earlier ones.
75
+
76
+ ```js
77
+ // orca.config.js
78
+ export default {
79
+ runsDir: "./.orca/runs",
80
+ sessionLogs: "./session-logs",
81
+ hookCommands: {
82
+ onTaskComplete: "echo task done: $ORCA_TASK_NAME",
83
+ onComplete: "echo run complete",
84
+ onError: "echo run failed"
85
+ }
86
+ };
64
87
  ```
65
88
 
66
- Run IDs follow the format:
89
+ ## Reference
67
90
 
68
- ```text
69
- <spec-slug>-<timestamp-ms>-<4char-hex>
70
- ```
91
+ ### Flags
71
92
 
72
- ## Dev
93
+ Global:
73
94
 
74
- ```bash
75
- bun test # run tests
76
- bun run src/cli/index.ts -p "your goal here" # run without building
77
- ```
95
+ - `-h, --help`
96
+ - `-V, --version`
97
+
98
+ `orca` / `orca run`:
99
+
100
+ - positional: `[goal]`
101
+ - also works: `--task <text>`, `-p, --prompt <text>`
102
+ - `--spec <path>`
103
+ - `--plan <path>`
104
+ - `--config <path>`
105
+ - `--on-milestone <cmd>`
106
+ - `--on-task-complete <cmd>`
107
+ - `--on-task-fail <cmd>`
108
+ - `--on-complete <cmd>`
109
+ - `--on-error <cmd>`
110
+
111
+ `orca plan`:
112
+
113
+ - `--spec <path>`
114
+ - `--config <path>`
115
+ - `--on-milestone <cmd>`
116
+ - `--on-error <cmd>`
117
+
118
+ `orca status`:
119
+
120
+ - `--run <run-id>`
121
+ - `--last`
122
+ - `--config <path>`
123
+
124
+ `orca resume`:
125
+
126
+ - `--run <run-id>`
127
+ - `--last`
128
+ - `--config <path>`
129
+
130
+ `orca cancel`:
131
+
132
+ - `--run <run-id>`
133
+ - `--last`
134
+ - `--config <path>`
135
+
136
+ `orca answer`:
78
137
 
79
- ## Architecture
138
+ - positional: `[run-id] [answer]`
139
+ - `--run <id>`
80
140
 
81
- | Step | Model | What |
82
- |------|-------|------|
83
- | Planning | Claude | Spec → task graph (dependency-aware DAG) |
84
- | Phase 4 review | Codex | Reviews task graph before execution; aborts on hard blockers |
85
- | Execution | Codex | Persistent thread runs each task (full context across tasks) |
86
- | Post-run review | Codex | Reviews all changes after execution completes |
141
+ `orca list`:
142
+
143
+ - `--config <path>`
144
+
145
+ `orca pr draft|create|publish|status`:
146
+
147
+ - `--run <run-id>`
148
+ - `--last`
149
+ - `--config <path>`
150
+
151
+ `orca pr-finalize`:
152
+
153
+ - `--config <path>`
154
+
155
+ `orca setup`:
156
+
157
+ - `--anthropic-key <key>`
158
+ - `--openai-key <key>`
159
+ - `--check`
160
+ - `--global`
161
+ - `--project`
162
+
163
+ ### Hooks
164
+
165
+ Hook names:
166
+
167
+ - `onMilestone`
168
+ - `onTaskComplete`
169
+ - `onTaskFail`
170
+ - `onComplete`
171
+ - `onError`
172
+
173
+ Run hooks from CLI with `--on-...` flags or from config via `hookCommands` / `hooks`.
174
+
175
+ ### Run ID Format
176
+
177
+ Run IDs are generated as:
178
+
179
+ - `<slug>-<unix-ms>-<hex4>`
180
+ - Example: `feature-auth-1766228123456-1a2b`
181
+
182
+ ### Config File Locations
183
+
184
+ - Global: `~/.orca/config.js`
185
+ - Project: `./orca.config.js` or `./orca.config.ts`
186
+ - Explicit: `--config <path>`
187
+
188
+ ### Run State Locations
189
+
190
+ - Run status: `<runsDir>/<run-id>/status.json`
191
+ - Answer payloads: `<runsDir>/<run-id>/answer.txt`
192
+ - `runsDir` defaults to `~/.orca/runs` unless overridden by `ORCA_RUNS_DIR`.
193
+
194
+ ## Development
195
+
196
+ ```bash
197
+ bun install
198
+ bun test
199
+ bun run src/cli/index.ts "your goal here"
200
+ ```
@@ -0,0 +1,61 @@
1
+ import path from "node:path";
2
+ import { promises as fs } from "node:fs";
3
+ import { input } from "@inquirer/prompts";
4
+ import { RunStore } from "../../state/store.js";
5
+ import { selectRun } from "../../utils/select-run.js";
6
+ function createStore() {
7
+ const runsDir = process.env.ORCA_RUNS_DIR;
8
+ return runsDir ? new RunStore(runsDir) : new RunStore();
9
+ }
10
+ function resolveRunId(positionalRunId, optionRunId) {
11
+ if (positionalRunId && optionRunId) {
12
+ throw new Error("positional run-id and --run are mutually exclusive");
13
+ }
14
+ return positionalRunId ?? optionRunId;
15
+ }
16
+ async function resolveAnswer(answerArg) {
17
+ if (answerArg) {
18
+ return answerArg;
19
+ }
20
+ if (!process.stdout.isTTY) {
21
+ throw new Error("no answer provided");
22
+ }
23
+ const value = await input({ message: "Answer:" });
24
+ if (!value) {
25
+ throw new Error("no answer provided");
26
+ }
27
+ return value;
28
+ }
29
+ export async function answerCommandHandler(positionalRunId, answerArg, options) {
30
+ const store = createStore();
31
+ let runId = resolveRunId(positionalRunId, options.run);
32
+ if (!runId) {
33
+ if (!process.stdout.isTTY) {
34
+ throw new Error("no run id provided");
35
+ }
36
+ runId = await selectRun(store) ?? undefined;
37
+ if (!runId) {
38
+ throw new Error("no run id provided");
39
+ }
40
+ }
41
+ const run = await store.getRun(runId);
42
+ if (!run) {
43
+ throw new Error(`Run not found: ${runId}`);
44
+ }
45
+ if (run.overallStatus !== "waiting_for_answer") {
46
+ throw new Error(`Run ${runId} is not waiting for an answer.`);
47
+ }
48
+ const answer = await resolveAnswer(answerArg);
49
+ const answerPath = path.join(store.getRunDir(runId), "answer.txt");
50
+ await fs.mkdir(path.dirname(answerPath), { recursive: true });
51
+ await fs.writeFile(answerPath, `${answer}\n`, "utf8");
52
+ await store.updateRun(runId, { overallStatus: "running" });
53
+ console.log(`Answer submitted. Run ${runId} will resume shortly.`);
54
+ }
55
+ export function registerAnswerCommand(program) {
56
+ program
57
+ .command("answer [run-id] [answer]")
58
+ .description("Submit an answer for a run waiting for input")
59
+ .option("--run <id>", "Run ID waiting for answer")
60
+ .action(async (runId, answer, options) => answerCommandHandler(runId, answer, options));
61
+ }
@@ -1,4 +1,5 @@
1
1
  import { RunStore } from "../../state/store.js";
2
+ import { getLastRun } from "../../utils/last-run.js";
2
3
  function createStore() {
3
4
  const runsDir = process.env.ORCA_RUNS_DIR;
4
5
  return runsDir ? new RunStore(runsDir) : new RunStore();
@@ -14,6 +15,15 @@ function getActiveRuns(runs) {
14
15
  }
15
16
  export async function cancelCommandHandler(options) {
16
17
  const store = createStore();
18
+ if (options.last) {
19
+ const lastRun = await getLastRun(store);
20
+ if (!lastRun) {
21
+ console.error("No runs found.");
22
+ process.exitCode = 1;
23
+ return;
24
+ }
25
+ options.run = lastRun.runId;
26
+ }
17
27
  const knownRuns = await store.listRuns();
18
28
  if (!options.run) {
19
29
  console.error(`Missing required --run <run-id>\nActive runs: ${formatRunIds(getActiveRuns(knownRuns))}`);
@@ -55,6 +65,7 @@ export function registerCancelCommand(program) {
55
65
  .command("cancel")
56
66
  .description("Cancel an active run")
57
67
  .option("--run <run-id>", "Run ID to cancel")
68
+ .option("--last", "Use the most recent run")
58
69
  .option("--config <path>", "Path to orca config file")
59
70
  .action(async (options) => cancelCommandHandler(options));
60
71
  }
@@ -0,0 +1,87 @@
1
+ import chalk from "chalk";
2
+ const HELP_COLUMN_WIDTH = 40;
3
+ function formatHelpLine(command, description) {
4
+ return ` ${chalk.cyan(command.padEnd(HELP_COLUMN_WIDTH, " "))}${chalk.dim(description)}`;
5
+ }
6
+ function printSection(title, entries) {
7
+ console.log(chalk.bold(title));
8
+ for (const entry of entries) {
9
+ console.log(formatHelpLine(entry.command, entry.description));
10
+ }
11
+ console.log("");
12
+ }
13
+ function printStyledHelpPage() {
14
+ console.log("orca — coordinated agent run harness");
15
+ console.log("");
16
+ printSection("RUNNING", [
17
+ { command: 'orca "add auth to the app"', description: "run with inline goal" },
18
+ { command: "orca --plan ./specs/feature.md", description: "run from plan file" },
19
+ { command: "orca plan --spec ./specs/feature.md", description: "plan only, no execution" }
20
+ ]);
21
+ printSection("RUN MANAGEMENT", [
22
+ { command: "orca status", description: "list all runs" },
23
+ { command: "orca status --last", description: "show most recent run" },
24
+ { command: "orca status --run <id>", description: "show run details" },
25
+ { command: "orca resume --last", description: "resume most recent run" },
26
+ { command: "orca resume --run <id>", description: "resume incomplete run" },
27
+ { command: "orca cancel --run <id>", description: "cancel active run" }
28
+ ]);
29
+ printSection("PULL REQUESTS", [
30
+ { command: "orca pr", description: "interactive — pick run + action" },
31
+ { command: "orca pr draft --run <id>", description: "create draft PR" },
32
+ { command: "orca pr create --run <id>", description: "create ready-for-review PR" },
33
+ { command: "orca pr publish --run <id>", description: "publish draft → ready for review" },
34
+ { command: "orca pr status --run <id>", description: "check PR state and CI" }
35
+ ]);
36
+ printSection("SETUP", [
37
+ { command: "orca setup", description: "first-time setup and environment checks" }
38
+ ]);
39
+ printSection("FLAGS", [
40
+ { command: "-p, --prompt <text>", description: "inline task goal (alias: --task)" },
41
+ { command: "--plan, --spec <path>", description: "path to spec or plan file" },
42
+ { command: "--run <id>", description: "specify run by ID" },
43
+ { command: "--last", description: "use the most recent run" },
44
+ {
45
+ command: "--config <path>",
46
+ description: "explicit config file (auto-discovered by default)"
47
+ },
48
+ { command: "--full-auto", description: "skip all questions, proceed autonomously" },
49
+ { command: "--on-complete <cmd>", description: "shell hook on run complete" },
50
+ { command: "--on-error <cmd>", description: "shell hook on run error" },
51
+ { command: "-h, --help", description: "show help for any command" },
52
+ { command: "-V, --version", description: "show version" }
53
+ ]);
54
+ }
55
+ function findCommand(program, pathText) {
56
+ const segments = pathText
57
+ .split(" ")
58
+ .map((segment) => segment.trim())
59
+ .filter((segment) => segment.length > 0);
60
+ let current = program;
61
+ for (const segment of segments) {
62
+ current = current.commands.find((command) => command.name() === segment);
63
+ if (!current) {
64
+ return undefined;
65
+ }
66
+ }
67
+ return current;
68
+ }
69
+ export function registerHelpCommand(program) {
70
+ program.addHelpCommand(false);
71
+ program
72
+ .command("help [command]")
73
+ .description("display help for command")
74
+ .action((commandPath) => {
75
+ if (!commandPath) {
76
+ printStyledHelpPage();
77
+ return;
78
+ }
79
+ const target = findCommand(program, commandPath);
80
+ if (!target) {
81
+ console.error(`error: unknown command '${commandPath}'`);
82
+ process.exitCode = 1;
83
+ return;
84
+ }
85
+ target.outputHelp();
86
+ });
87
+ }
@@ -0,0 +1,57 @@
1
+ import { checkGhCli, runGh } from "../../../utils/gh.js";
2
+ import { buildPrBody, buildPrTitle, createStore, loadRunOrExit, printGhMissingAndExit, resolveRunIdOrExit } from "./shared.js";
3
+ function parsePrUrl(stdout) {
4
+ const lines = stdout
5
+ .split("\n")
6
+ .map((line) => line.trim())
7
+ .filter((line) => line.length > 0);
8
+ return lines.at(-1) ?? null;
9
+ }
10
+ export async function prCreateCommandHandler(options) {
11
+ const runId = await resolveRunIdOrExit(options, "create");
12
+ if (!runId) {
13
+ return;
14
+ }
15
+ const run = await loadRunOrExit(options);
16
+ if (!run) {
17
+ return;
18
+ }
19
+ if (!(await checkGhCli())) {
20
+ printGhMissingAndExit();
21
+ return;
22
+ }
23
+ const title = buildPrTitle(run);
24
+ const body = buildPrBody(run);
25
+ const result = await runGh(["pr", "create", "--title", title, "--body", body]);
26
+ if (result.exitCode !== 0) {
27
+ console.error(result.stderr || result.stdout || "Failed to create PR.");
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+ const url = parsePrUrl(result.stdout);
32
+ if (!url) {
33
+ console.error("PR created, but URL could not be determined.");
34
+ process.exitCode = 1;
35
+ return;
36
+ }
37
+ const store = createStore();
38
+ await store.updateRun(runId, {
39
+ pr: {
40
+ ...run.pr,
41
+ url,
42
+ draftTitle: title,
43
+ draftBody: body,
44
+ readyForFinalize: true
45
+ }
46
+ });
47
+ console.log(url);
48
+ }
49
+ export function registerPrCreateCommand(program) {
50
+ program
51
+ .command("create")
52
+ .description("Create a pull request for a run")
53
+ .option("--run <run-id>", "Run ID to create PR for")
54
+ .option("--last", "Use the most recent run")
55
+ .option("--config <path>", "Path to orca config file")
56
+ .action(async (options) => prCreateCommandHandler(options));
57
+ }
@@ -0,0 +1,57 @@
1
+ import { runGh, checkGhCli } from "../../../utils/gh.js";
2
+ import { buildPrBody, buildPrTitle, createStore, loadRunOrExit, printGhMissingAndExit, resolveRunIdOrExit } from "./shared.js";
3
+ function parsePrUrl(stdout) {
4
+ const lines = stdout
5
+ .split("\n")
6
+ .map((line) => line.trim())
7
+ .filter((line) => line.length > 0);
8
+ return lines.at(-1) ?? null;
9
+ }
10
+ export async function prDraftCommandHandler(options) {
11
+ const runId = await resolveRunIdOrExit(options, "draft");
12
+ if (!runId) {
13
+ return;
14
+ }
15
+ const run = await loadRunOrExit(options);
16
+ if (!run) {
17
+ return;
18
+ }
19
+ if (!(await checkGhCli())) {
20
+ printGhMissingAndExit();
21
+ return;
22
+ }
23
+ const title = buildPrTitle(run);
24
+ const body = buildPrBody(run);
25
+ const result = await runGh(["pr", "create", "--draft", "--title", title, "--body", body]);
26
+ if (result.exitCode !== 0) {
27
+ console.error(result.stderr || result.stdout || "Failed to create draft PR.");
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+ const url = parsePrUrl(result.stdout);
32
+ if (!url) {
33
+ console.error("Draft PR created, but URL could not be determined.");
34
+ process.exitCode = 1;
35
+ return;
36
+ }
37
+ const store = createStore();
38
+ await store.updateRun(runId, {
39
+ pr: {
40
+ ...run.pr,
41
+ url,
42
+ draftTitle: title,
43
+ draftBody: body,
44
+ readyForFinalize: false
45
+ }
46
+ });
47
+ console.log(url);
48
+ }
49
+ export function registerPrDraftCommand(program) {
50
+ program
51
+ .command("draft")
52
+ .description("Create a draft pull request for a run")
53
+ .option("--run <run-id>", "Run ID to create draft PR for")
54
+ .option("--last", "Use the most recent run")
55
+ .option("--config <path>", "Path to orca config file")
56
+ .action(async (options) => prDraftCommandHandler(options));
57
+ }
@@ -0,0 +1,66 @@
1
+ import { select } from "@inquirer/prompts";
2
+ import { prCreateCommandHandler, registerPrCreateCommand } from "./create.js";
3
+ import { prDraftCommandHandler, registerPrDraftCommand } from "./draft.js";
4
+ import { prPublishCommandHandler, registerPrPublishCommand } from "./publish.js";
5
+ import { createStore } from "./shared.js";
6
+ import { prStatusCommandHandler, registerPrStatusCommand } from "./status.js";
7
+ import { selectRun } from "../../../utils/select-run.js";
8
+ export function registerPrCommand(program) {
9
+ const prCommand = program
10
+ .command("pr")
11
+ .description("Pull request workflow commands");
12
+ registerPrDraftCommand(prCommand);
13
+ registerPrCreateCommand(prCommand);
14
+ registerPrPublishCommand(prCommand);
15
+ registerPrStatusCommand(prCommand);
16
+ prCommand
17
+ .command("finalize")
18
+ .description("Deprecated: use `orca pr publish`")
19
+ .requiredOption("--run <run-id>", "")
20
+ .action(async (options) => prPublishCommandHandler(options));
21
+ prCommand.action(async () => {
22
+ if (!process.stdout.isTTY) {
23
+ console.error("Usage: orca pr <draft|create|publish|status> --run <id>");
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ const store = createStore();
28
+ const runId = await selectRun(store);
29
+ if (!runId) {
30
+ return;
31
+ }
32
+ const run = await store.getRun(runId);
33
+ if (!run) {
34
+ return;
35
+ }
36
+ if (run.pr?.url) {
37
+ const state = run.pr.finalizedAt ? "published (ready for review)" : "draft";
38
+ console.log(`PR: ${run.pr.url} [${state}]`);
39
+ }
40
+ else {
41
+ console.log("No PR for this run yet.");
42
+ }
43
+ const action = await select({
44
+ message: "What do you want to do?",
45
+ choices: [
46
+ { name: "Create draft PR", value: "draft" },
47
+ { name: "Create PR (ready for review)", value: "create" },
48
+ { name: "Publish draft → ready for review", value: "publish" },
49
+ { name: "View PR status & CI checks", value: "status" }
50
+ ]
51
+ });
52
+ const options = { run: runId };
53
+ if (action === "draft") {
54
+ await prDraftCommandHandler(options);
55
+ }
56
+ else if (action === "create") {
57
+ await prCreateCommandHandler(options);
58
+ }
59
+ else if (action === "publish") {
60
+ await prPublishCommandHandler(options);
61
+ }
62
+ else if (action === "status") {
63
+ await prStatusCommandHandler(options);
64
+ }
65
+ });
66
+ }
@@ -0,0 +1,45 @@
1
+ import { checkGhCli, runGh } from "../../../utils/gh.js";
2
+ import { createStore, loadRunOrExit, printGhMissingAndExit, resolveRunIdOrExit } from "./shared.js";
3
+ export async function prPublishCommandHandler(options) {
4
+ const runId = await resolveRunIdOrExit(options, "publish");
5
+ if (!runId) {
6
+ return;
7
+ }
8
+ const run = await loadRunOrExit(options);
9
+ if (!run) {
10
+ return;
11
+ }
12
+ if (!(await checkGhCli())) {
13
+ printGhMissingAndExit();
14
+ return;
15
+ }
16
+ if (!run.pr?.url) {
17
+ console.error("No PR found for this run. Use `orca pr draft` or `orca pr create` first.");
18
+ process.exitCode = 1;
19
+ return;
20
+ }
21
+ const result = await runGh(["pr", "ready", run.pr.url]);
22
+ if (result.exitCode !== 0) {
23
+ console.error(result.stderr || result.stdout || "Failed to mark PR ready for review.");
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ const store = createStore();
28
+ await store.updateRun(runId, {
29
+ pr: {
30
+ ...run.pr,
31
+ readyForFinalize: true,
32
+ finalizedAt: new Date().toISOString()
33
+ }
34
+ });
35
+ console.log(`PR marked ready for review: ${run.pr.url}`);
36
+ }
37
+ export function registerPrPublishCommand(program) {
38
+ program
39
+ .command("publish")
40
+ .description("Publish a draft PR (mark ready for review)")
41
+ .option("--run <run-id>", "Run ID to publish PR for")
42
+ .option("--last", "Use the most recent run")
43
+ .option("--config <path>", "Path to orca config file")
44
+ .action(async (options) => prPublishCommandHandler(options));
45
+ }
@@ -0,0 +1,79 @@
1
+ import path from "node:path";
2
+ import { RunStore } from "../../../state/store.js";
3
+ import { getLastRun } from "../../../utils/last-run.js";
4
+ import { selectRun } from "../../../utils/select-run.js";
5
+ export function createStore() {
6
+ const runsDir = process.env.ORCA_RUNS_DIR;
7
+ return runsDir ? new RunStore(runsDir) : new RunStore();
8
+ }
9
+ export function buildPrTitle(run) {
10
+ const specBase = run.specPath ? path.basename(run.specPath) : "";
11
+ const derived = specBase.length > 0 ? specBase : run.runId;
12
+ return `Orca run: ${derived}`;
13
+ }
14
+ export function buildPrBody(run) {
15
+ const completedTasks = run.tasks.filter((task) => task.status === "done");
16
+ const lines = [
17
+ `Automated PR for run \`${run.runId}\`.`,
18
+ "",
19
+ `Spec: \`${run.specPath}\``,
20
+ "",
21
+ "Completed tasks:"
22
+ ];
23
+ if (completedTasks.length === 0) {
24
+ lines.push("- (none)");
25
+ }
26
+ else {
27
+ for (const task of completedTasks) {
28
+ lines.push(`- [x] ${task.name} (${task.id})`);
29
+ }
30
+ }
31
+ return lines.join("\n");
32
+ }
33
+ export async function loadRunOrExit(options) {
34
+ if (!options.run) {
35
+ console.error("Run ID is required.");
36
+ process.exitCode = 1;
37
+ return null;
38
+ }
39
+ const store = createStore();
40
+ const run = await store.getRun(options.run);
41
+ if (!run) {
42
+ console.error(`Run not found: ${options.run}`);
43
+ process.exitCode = 1;
44
+ return null;
45
+ }
46
+ return run;
47
+ }
48
+ export function printGhMissingAndExit() {
49
+ console.error("gh CLI not found. Run `orca setup` to install it.");
50
+ process.exitCode = 1;
51
+ }
52
+ export async function resolveRunIdOrExit(options, commandName) {
53
+ if (options.last) {
54
+ const store = createStore();
55
+ const lastRun = await getLastRun(store);
56
+ if (!lastRun) {
57
+ console.error("No runs found.");
58
+ process.exitCode = 1;
59
+ return null;
60
+ }
61
+ options.run = lastRun.runId;
62
+ return lastRun.runId;
63
+ }
64
+ if (options.run) {
65
+ return options.run;
66
+ }
67
+ if (!process.stdout.isTTY) {
68
+ console.error(`missing --run <run-id>. Usage: orca pr ${commandName} --run <id>`);
69
+ process.exitCode = 1;
70
+ return null;
71
+ }
72
+ const runId = await selectRun(createStore());
73
+ if (!runId) {
74
+ process.exitCode = 1;
75
+ return null;
76
+ }
77
+ options.run = runId;
78
+ return runId;
79
+ }
@@ -0,0 +1,64 @@
1
+ import { checkGhCli, runGh } from "../../../utils/gh.js";
2
+ import { loadRunOrExit, printGhMissingAndExit, resolveRunIdOrExit } from "./shared.js";
3
+ function stringifyCheckStatus(check) {
4
+ const primary = check.status ?? check.state ?? "UNKNOWN";
5
+ const conclusion = check.conclusion ? `/${check.conclusion}` : "";
6
+ return `${primary}${conclusion}`;
7
+ }
8
+ function checkName(check) {
9
+ return check.name ?? check.context ?? "unnamed-check";
10
+ }
11
+ export async function prStatusCommandHandler(options) {
12
+ if (!(await resolveRunIdOrExit(options, "status"))) {
13
+ return;
14
+ }
15
+ const run = await loadRunOrExit(options);
16
+ if (!run) {
17
+ return;
18
+ }
19
+ if (!(await checkGhCli())) {
20
+ printGhMissingAndExit();
21
+ return;
22
+ }
23
+ if (!run.pr?.url) {
24
+ console.error("No PR associated with this run.");
25
+ process.exitCode = 1;
26
+ return;
27
+ }
28
+ const result = await runGh(["pr", "view", run.pr.url, "--json", "state,statusCheckRollup,title,url"]);
29
+ if (result.exitCode !== 0) {
30
+ console.error(result.stderr || result.stdout || "Failed to load PR status.");
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(result.stdout);
37
+ }
38
+ catch {
39
+ console.error("Failed to parse PR status output.");
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+ console.log(`Title: ${parsed.title ?? "-"}`);
44
+ console.log(`State: ${parsed.state ?? "-"}`);
45
+ console.log(`URL: ${parsed.url ?? run.pr.url}`);
46
+ console.log("Checks:");
47
+ const checks = parsed.statusCheckRollup ?? [];
48
+ if (checks.length === 0) {
49
+ console.log("- (none)");
50
+ return;
51
+ }
52
+ for (const check of checks) {
53
+ console.log(`- ${checkName(check)}: ${stringifyCheckStatus(check)}`);
54
+ }
55
+ }
56
+ export function registerPrStatusCommand(program) {
57
+ program
58
+ .command("status")
59
+ .description("Show pull request status for a run")
60
+ .option("--run <run-id>", "Run ID to inspect PR for")
61
+ .option("--last", "Use the most recent run")
62
+ .option("--config <path>", "Path to orca config file")
63
+ .action(async (options) => prStatusCommandHandler(options));
64
+ }
@@ -1,5 +1,6 @@
1
1
  import { runTaskRunner } from "../../core/task-runner.js";
2
2
  import { RunStore } from "../../state/store.js";
3
+ import { getLastRun } from "../../utils/last-run.js";
3
4
  function createStore() {
4
5
  const runsDir = process.env.ORCA_RUNS_DIR;
5
6
  return runsDir ? new RunStore(runsDir) : new RunStore();
@@ -15,6 +16,15 @@ function getActiveRuns(runs) {
15
16
  }
16
17
  export async function resumeCommandHandler(options) {
17
18
  const store = createStore();
19
+ if (options.last) {
20
+ const lastRun = await getLastRun(store);
21
+ if (!lastRun) {
22
+ console.error("No runs found.");
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+ options.run = lastRun.runId;
27
+ }
18
28
  const knownRuns = await store.listRuns();
19
29
  if (!options.run) {
20
30
  console.error(`Missing required --run <run-id>\nActive runs: ${formatRunIds(getActiveRuns(knownRuns))}`);
@@ -62,6 +72,7 @@ export function registerResumeCommand(program) {
62
72
  .command("resume")
63
73
  .description("Resume an incomplete run")
64
74
  .option("--run <run-id>", "Run ID to resume")
75
+ .option("--last", "Use the most recent run")
65
76
  .option("--config <path>", "Path to orca config file")
66
77
  .action(async (options) => resumeCommandHandler(options));
67
78
  }
@@ -4,7 +4,7 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { createCodexSession } from "../../agents/codex/session.js";
7
- import { loadConfig } from "../../core/config-loader.js";
7
+ import { resolveConfig } from "../../core/config-loader.js";
8
8
  import { runPlanner } from "../../core/planner.js";
9
9
  import { runTaskRunner } from "../../core/task-runner.js";
10
10
  import { createOpenclawHookHandler, detectOpenclawAvailability } from "../../hooks/adapters/openclaw.js";
@@ -49,23 +49,30 @@ function buildCliCommandHooks(options) {
49
49
  };
50
50
  }
51
51
  export async function runCommandHandler(options) {
52
- const inlineTask = options.task ?? options.prompt;
53
- if (!options.spec && !inlineTask) {
52
+ const inlineTask = options.task ?? options.prompt ?? options.goal;
53
+ const inputSpecPath = options.spec ?? options.plan;
54
+ if (options.goal !== undefined && (options.task || options.prompt)) {
55
+ throw new Error("positional goal and --task/--prompt are mutually exclusive");
56
+ }
57
+ if (options.goal !== undefined && inputSpecPath) {
58
+ throw new Error("positional goal and --spec/--plan are mutually exclusive");
59
+ }
60
+ if (!inputSpecPath && !inlineTask) {
54
61
  throw new Error("One of --spec, --task, or --prompt (-p) must be provided.");
55
62
  }
56
- if (options.spec && inlineTask) {
63
+ if (inputSpecPath && inlineTask) {
57
64
  throw new Error("--spec is mutually exclusive with --task / --prompt.");
58
65
  }
59
66
  const usesInlineTask = Boolean(inlineTask);
60
67
  const specPath = usesInlineTask
61
68
  ? path.join(os.tmpdir(), `orca-task-${Date.now()}-${randomUUID()}.md`)
62
- : path.resolve(options.spec ?? "");
69
+ : path.resolve(inputSpecPath ?? "");
63
70
  if (usesInlineTask) {
64
71
  await writeFile(specPath, `${inlineTask}\n`, "utf8");
65
72
  }
66
73
  try {
67
74
  await access(specPath, fsConstants.R_OK);
68
- const orcaConfig = await loadConfig(options.config);
75
+ const orcaConfig = await resolveConfig(options.config);
69
76
  const runId = generateRunId(specPath);
70
77
  console.log(`Run ID: ${runId}`);
71
78
  const store = createStore();
@@ -175,9 +182,10 @@ export async function runCommandHandler(options) {
175
182
  }
176
183
  export function registerRunCommand(program) {
177
184
  program
178
- .command("run", { isDefault: true })
185
+ .command("run [goal]", { isDefault: true })
179
186
  .description("Run pre-planning and execution")
180
187
  .option("--spec <path>", "Path to spec markdown file")
188
+ .option("--plan <path>", "Alias for --spec — path to a plan/spec file")
181
189
  .option("--task <text>", "Inline task text (alternative to --spec)")
182
190
  .option("-p, --prompt <text>", "Inline task text (alias for --task)")
183
191
  .option("--config <path>", "Path to orca config file")
@@ -186,14 +194,25 @@ export function registerRunCommand(program) {
186
194
  .option("--on-task-fail <cmd>", "Shell hook command for onTaskFail")
187
195
  .option("--on-complete <cmd>", "Shell hook command for onComplete")
188
196
  .option("--on-error <cmd>", "Shell hook command for onError")
189
- .action(async (commandOptions) => {
190
- const inlineTask = commandOptions.task ?? commandOptions.prompt;
191
- if (!commandOptions.spec && !inlineTask) {
197
+ .action(async (goal, commandOptions) => {
198
+ const normalizedOptions = {
199
+ ...commandOptions,
200
+ ...(goal !== undefined ? { goal } : {})
201
+ };
202
+ const inlineTask = normalizedOptions.task ?? normalizedOptions.prompt ?? normalizedOptions.goal;
203
+ const inputSpecPath = normalizedOptions.spec ?? normalizedOptions.plan;
204
+ if (normalizedOptions.goal !== undefined && (normalizedOptions.task || normalizedOptions.prompt)) {
205
+ throw new Error("positional goal and --task/--prompt are mutually exclusive");
206
+ }
207
+ if (normalizedOptions.goal !== undefined && inputSpecPath) {
208
+ throw new Error("positional goal and --spec/--plan are mutually exclusive");
209
+ }
210
+ if (!inputSpecPath && !inlineTask) {
192
211
  throw new Error("One of --spec, --task, or --prompt (-p) must be provided.");
193
212
  }
194
- if (commandOptions.spec && inlineTask) {
213
+ if (inputSpecPath && inlineTask) {
195
214
  throw new Error("--spec is mutually exclusive with --task / --prompt.");
196
215
  }
197
- await runCommandHandler(commandOptions);
216
+ await runCommandHandler(normalizedOptions);
198
217
  });
199
218
  }
@@ -1,4 +1,5 @@
1
1
  import { RunStore } from "../../state/store.js";
2
+ import { getLastRun } from "../../utils/last-run.js";
2
3
  import { formatRunSummaryTable, listRuns } from "./list.js";
3
4
  function createStore() {
4
5
  const runsDir = process.env.ORCA_RUNS_DIR;
@@ -53,6 +54,16 @@ async function printDetailedRun(run) {
53
54
  console.log(formatTaskTable(run.tasks));
54
55
  }
55
56
  export async function statusCommandHandler(options) {
57
+ if (options.last) {
58
+ const store = createStore();
59
+ const lastRun = await getLastRun(store);
60
+ if (!lastRun) {
61
+ console.error("No runs found.");
62
+ process.exitCode = 1;
63
+ return;
64
+ }
65
+ options.run = lastRun.runId;
66
+ }
56
67
  if (!options.run) {
57
68
  const runs = await listRuns();
58
69
  if (runs.length === 0) {
@@ -77,6 +88,7 @@ export function registerStatusCommand(program) {
77
88
  .command("status")
78
89
  .description("Show run status or list all runs")
79
90
  .option("--run <run-id>", "Run ID to inspect")
91
+ .option("--last", "Use the most recent run")
80
92
  .option("--config <path>", "Path to orca config file")
81
93
  .action(async (options) => statusCommandHandler(options));
82
94
  }
package/dist/cli/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
+ import { registerAnswerCommand } from "./commands/answer.js";
3
4
  import { registerCancelCommand } from "./commands/cancel.js";
5
+ import { registerHelpCommand } from "./commands/help.js";
4
6
  import { registerListCommand } from "./commands/list.js";
7
+ import { registerPrCommand } from "./commands/pr/index.js";
5
8
  import { registerPlanCommand } from "./commands/plan.js";
6
9
  import { registerPrFinalizeCommand } from "./commands/pr-finalize.js";
7
10
  import { registerResumeCommand } from "./commands/resume.js";
@@ -9,13 +12,16 @@ import { registerRunCommand } from "./commands/run.js";
9
12
  import { registerSetupCommand } from "./commands/setup.js";
10
13
  import { registerStatusCommand } from "./commands/status.js";
11
14
  const program = new Command();
12
- program.name("orca").description("Orca CLI: coordinated agent run harness").version("0.1.0");
15
+ program.name("orca").description("Orca CLI: coordinated agent run harness").version("0.2.0");
13
16
  registerRunCommand(program);
17
+ registerAnswerCommand(program);
14
18
  registerPlanCommand(program);
15
19
  registerStatusCommand(program);
16
20
  registerListCommand(program);
17
21
  registerResumeCommand(program);
18
22
  registerCancelCommand(program);
23
+ registerPrCommand(program);
19
24
  registerPrFinalizeCommand(program);
20
25
  registerSetupCommand(program);
26
+ registerHelpCommand(program);
21
27
  await program.parseAsync(process.argv);
@@ -1,5 +1,6 @@
1
1
  import { constants as fsConstants } from "node:fs";
2
2
  import { access } from "node:fs/promises";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { pathToFileURL } from "node:url";
5
6
  function isObject(value) {
@@ -51,3 +52,69 @@ export async function loadConfig(configPath) {
51
52
  const configCandidate = "default" in importedModule ? importedModule.default : importedModule;
52
53
  return coerceConfig(configCandidate);
53
54
  }
55
+ const TOP_LEVEL_SCALARS = ["runsDir", "sessionLogs", "maxRetries", "anthropicApiKey", "openaiApiKey"];
56
+ export function mergeConfigs(...configs) {
57
+ const presentConfigs = configs.filter((config) => config !== undefined);
58
+ if (presentConfigs.length === 0) {
59
+ return undefined;
60
+ }
61
+ const merged = {};
62
+ for (const config of presentConfigs) {
63
+ for (const key of TOP_LEVEL_SCALARS) {
64
+ if (key in config) {
65
+ merged[key] = config[key];
66
+ }
67
+ }
68
+ if (merged.claude !== undefined || config.claude !== undefined) {
69
+ merged.claude = { ...merged.claude, ...config.claude };
70
+ }
71
+ if (merged.codex !== undefined || config.codex !== undefined) {
72
+ merged.codex = { ...merged.codex, ...config.codex };
73
+ }
74
+ if (merged.pr !== undefined || config.pr !== undefined) {
75
+ merged.pr = { ...merged.pr, ...config.pr };
76
+ }
77
+ if (merged.hooks !== undefined || config.hooks !== undefined) {
78
+ merged.hooks = { ...merged.hooks, ...config.hooks };
79
+ }
80
+ if (merged.hookCommands !== undefined || config.hookCommands !== undefined) {
81
+ merged.hookCommands = { ...merged.hookCommands, ...config.hookCommands };
82
+ }
83
+ }
84
+ return merged;
85
+ }
86
+ async function loadOptionalConfig(configPath) {
87
+ const resolvedPath = path.resolve(configPath);
88
+ try {
89
+ await access(resolvedPath, fsConstants.R_OK);
90
+ }
91
+ catch (error) {
92
+ if (error.code === "ENOENT") {
93
+ return undefined;
94
+ }
95
+ throw error;
96
+ }
97
+ return loadConfig(resolvedPath);
98
+ }
99
+ export async function resolveConfigFromPaths(globalConfigPath, projectConfigPath, cliConfigPath) {
100
+ const globalConfig = await loadOptionalConfig(globalConfigPath);
101
+ const projectConfig = await loadOptionalConfig(projectConfigPath);
102
+ const cliConfig = await loadConfig(cliConfigPath);
103
+ return mergeConfigs(globalConfig, projectConfig, cliConfig);
104
+ }
105
+ export async function resolveConfig(cliConfigPath) {
106
+ const globalConfigPath = path.join(os.homedir(), ".orca", "config.js");
107
+ const projectJsConfigPath = path.join(process.cwd(), "orca.config.js");
108
+ const projectTsConfigPath = path.join(process.cwd(), "orca.config.ts");
109
+ let projectConfigPath = projectJsConfigPath;
110
+ try {
111
+ await access(projectJsConfigPath, fsConstants.R_OK);
112
+ }
113
+ catch (error) {
114
+ if (error.code !== "ENOENT") {
115
+ throw error;
116
+ }
117
+ projectConfigPath = projectTsConfigPath;
118
+ }
119
+ return resolveConfigFromPaths(globalConfigPath, projectConfigPath, cliConfigPath);
120
+ }
@@ -1,3 +1,5 @@
1
+ import path from "node:path";
2
+ import { promises as fs } from "node:fs";
1
3
  import { executeTask } from "../agents/claude/session.js";
2
4
  import { getRunnable, validateDAG } from "./dependency-graph.js";
3
5
  import { shouldRetry } from "./retry-policy.js";
@@ -38,6 +40,47 @@ function stripOptionalFields(task, fields) {
38
40
  function hasPendingTasks(tasks) {
39
41
  return tasks.some((task) => task.status === "pending" || task.status === "in_progress");
40
42
  }
43
+ function buildSessionSummary(run) {
44
+ const taskRows = run.tasks.length === 0
45
+ ? "| (none) | - | - | - | - |\n"
46
+ : run.tasks
47
+ .map((task) => {
48
+ return `| ${task.id} | ${task.name} | ${task.status} | ${task.startedAt ?? "-"} | ${task.finishedAt ?? "-"} |`;
49
+ })
50
+ .join("\n");
51
+ return [
52
+ `# Run ${run.runId}`,
53
+ "",
54
+ `- Run ID: \`${run.runId}\``,
55
+ `- Spec Path: \`${run.specPath}\``,
56
+ `- Status: \`${run.overallStatus}\``,
57
+ `- Created At: \`${run.createdAt}\``,
58
+ `- Updated At: \`${run.updatedAt}\``,
59
+ "",
60
+ "## Tasks",
61
+ "",
62
+ "| ID | Name | Status | Started At | Finished At |",
63
+ "| --- | --- | --- | --- | --- |",
64
+ taskRows,
65
+ ""
66
+ ].join("\n");
67
+ }
68
+ async function writeSessionSummary(store, runId, sessionLogsDir) {
69
+ if (!sessionLogsDir) {
70
+ return;
71
+ }
72
+ try {
73
+ const run = await store.getRun(runId);
74
+ if (!run) {
75
+ throw new Error(`Run not found while writing session summary: ${runId}`);
76
+ }
77
+ await fs.mkdir(sessionLogsDir, { recursive: true });
78
+ await fs.writeFile(path.join(sessionLogsDir, `${runId}.md`), buildSessionSummary(run), "utf8");
79
+ }
80
+ catch (error) {
81
+ console.error(`Warning: failed to write session summary for ${runId}: ${toErrorMessage(error)}`);
82
+ }
83
+ }
41
84
  export async function runTaskRunner(options) {
42
85
  const emitHook = options.emitHook ?? defaultEmitHook;
43
86
  const executeTaskFn = options.executeTask ?? executeTaskImpl;
@@ -68,6 +111,7 @@ export async function runTaskRunner(options) {
68
111
  timestamp: new Date().toISOString(),
69
112
  metadata: { overallStatus: "cancelled" }
70
113
  });
114
+ await writeSessionSummary(store, runId, config?.sessionLogs);
71
115
  return;
72
116
  }
73
117
  const runnable = getRunnable(run.tasks);
@@ -92,6 +136,7 @@ export async function runTaskRunner(options) {
92
136
  timestamp: completedAt,
93
137
  metadata: { overallStatus: "completed" }
94
138
  });
139
+ await writeSessionSummary(store, runId, config?.sessionLogs);
95
140
  return;
96
141
  }
97
142
  if (hasFailedTask) {
@@ -113,6 +158,7 @@ export async function runTaskRunner(options) {
113
158
  error: failureMessage,
114
159
  metadata: { overallStatus: "failed" }
115
160
  });
161
+ await writeSessionSummary(store, runId, config?.sessionLogs);
116
162
  return;
117
163
  }
118
164
  if (hasCancelledTask) {
@@ -124,6 +170,7 @@ export async function runTaskRunner(options) {
124
170
  timestamp: new Date().toISOString(),
125
171
  metadata: { overallStatus: "cancelled" }
126
172
  });
173
+ await writeSessionSummary(store, runId, config?.sessionLogs);
127
174
  return;
128
175
  }
129
176
  if (hasPendingTasks(run.tasks)) {
@@ -257,6 +304,7 @@ export async function runTaskRunner(options) {
257
304
  error: errorMessage,
258
305
  metadata: { overallStatus: "failed" }
259
306
  });
307
+ await writeSessionSummary(store, runId, config?.sessionLogs);
260
308
  throw error;
261
309
  }
262
310
  }
@@ -38,7 +38,14 @@ export const RunStatusSchema = z.object({
38
38
  specPath: z.string(),
39
39
  createdAt: z.string(),
40
40
  updatedAt: z.string(),
41
- overallStatus: z.enum(["planning", "running", "completed", "failed", "cancelled"]),
41
+ overallStatus: z.enum([
42
+ "planning",
43
+ "running",
44
+ "waiting_for_answer",
45
+ "completed",
46
+ "failed",
47
+ "cancelled"
48
+ ]),
42
49
  tasks: z.array(TaskSchema),
43
50
  milestones: z.array(z.string()),
44
51
  errors: z.array(ErrorEntrySchema),
@@ -0,0 +1,35 @@
1
+ function decodeStream(stream) {
2
+ if (!stream) {
3
+ return Promise.resolve("");
4
+ }
5
+ return new Response(stream).text();
6
+ }
7
+ export async function checkGhCli() {
8
+ try {
9
+ const proc = Bun.spawn(["which", "gh"], {
10
+ stdout: "pipe",
11
+ stderr: "pipe"
12
+ });
13
+ const exitCode = await proc.exited;
14
+ return exitCode === 0;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ export async function runGh(args) {
21
+ const proc = Bun.spawn(["gh", ...args], {
22
+ stdout: "pipe",
23
+ stderr: "pipe"
24
+ });
25
+ const [stdout, stderr, exitCode] = await Promise.all([
26
+ decodeStream(proc.stdout),
27
+ decodeStream(proc.stderr),
28
+ proc.exited
29
+ ]);
30
+ return {
31
+ stdout,
32
+ stderr,
33
+ exitCode
34
+ };
35
+ }
@@ -0,0 +1,7 @@
1
+ export async function getLastRun(store) {
2
+ const runs = await store.listRuns();
3
+ if (runs.length === 0) {
4
+ return null;
5
+ }
6
+ return runs.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0] ?? null;
7
+ }
@@ -0,0 +1,18 @@
1
+ import { select } from "@inquirer/prompts";
2
+ export async function selectRun(store) {
3
+ if (!process.stdout.isTTY) {
4
+ return null;
5
+ }
6
+ const runs = await store.listRuns();
7
+ if (runs.length === 0) {
8
+ console.error("No runs found.");
9
+ return null;
10
+ }
11
+ const sorted = [...runs].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
12
+ const choices = sorted.map((run) => {
13
+ const prStatus = run.pr?.url ? (run.pr.finalizedAt ? "published" : "draft PR") : "no PR";
14
+ const label = `${run.runId} [${run.overallStatus}] ${prStatus}`;
15
+ return { name: label, value: run.runId };
16
+ });
17
+ return select({ message: "Select a run:", choices });
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orcastrator",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "orca": "./dist/cli/index.js"
@@ -17,9 +17,10 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@anthropic-ai/claude-agent-sdk": "^0.2.47",
20
+ "@inquirer/prompts": "^8.2.1",
20
21
  "chalk": "^5.3.0",
21
- "orca-codex-client": "^0.1.0",
22
22
  "commander": "^13.1.0",
23
+ "orca-codex-client": "^0.1.0",
23
24
  "zod": "^3.24.1"
24
25
  },
25
26
  "devDependencies": {