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 +161 -47
- package/dist/cli/commands/answer.js +61 -0
- package/dist/cli/commands/cancel.js +11 -0
- package/dist/cli/commands/help.js +87 -0
- package/dist/cli/commands/pr/create.js +57 -0
- package/dist/cli/commands/pr/draft.js +57 -0
- package/dist/cli/commands/pr/index.js +66 -0
- package/dist/cli/commands/pr/publish.js +45 -0
- package/dist/cli/commands/pr/shared.js +79 -0
- package/dist/cli/commands/pr/status.js +64 -0
- package/dist/cli/commands/resume.js +11 -0
- package/dist/cli/commands/run.js +31 -12
- package/dist/cli/commands/status.js +12 -0
- package/dist/cli/index.js +7 -1
- package/dist/core/config-loader.js +67 -0
- package/dist/core/task-runner.js +48 -0
- package/dist/state/schema.js +8 -1
- package/dist/utils/gh.js +35 -0
- package/dist/utils/last-run.js +7 -0
- package/dist/utils/select-run.js +18 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,86 +1,200 @@
|
|
|
1
1
|
# orca
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
8
|
+
npm install -g orcastrator
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
##
|
|
11
|
+
## Run A Goal
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
Start with a plain-language goal:
|
|
16
14
|
|
|
17
15
|
```bash
|
|
18
|
-
orca
|
|
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
|
-
|
|
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
|
|
26
|
+
orca --spec ./specs/feature.md
|
|
27
|
+
orca --plan ./specs/feature.md
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
If you only want planning (no execution):
|
|
30
31
|
|
|
31
32
|
```bash
|
|
32
|
-
orca --spec ./specs/
|
|
33
|
+
orca plan --spec ./specs/feature.md
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
## Run Management
|
|
36
37
|
|
|
37
38
|
```bash
|
|
38
|
-
orca
|
|
39
|
-
orca status --
|
|
40
|
-
orca status
|
|
41
|
-
|
|
42
|
-
orca
|
|
43
|
-
|
|
44
|
-
orca
|
|
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
|
-
|
|
54
|
+
## PR Workflow
|
|
48
55
|
|
|
49
56
|
```bash
|
|
50
|
-
orca
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
66
|
+
## Config
|
|
57
67
|
|
|
58
|
-
|
|
68
|
+
Orca auto-discovers config in this order:
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
1. `~/.orca/config.js`
|
|
71
|
+
2. `./orca.config.js` or `./orca.config.ts`
|
|
72
|
+
3. `--config <path>` (if passed)
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
89
|
+
## Reference
|
|
67
90
|
|
|
68
|
-
|
|
69
|
-
<spec-slug>-<timestamp-ms>-<4char-hex>
|
|
70
|
-
```
|
|
91
|
+
### Flags
|
|
71
92
|
|
|
72
|
-
|
|
93
|
+
Global:
|
|
73
94
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
138
|
+
- positional: `[run-id] [answer]`
|
|
139
|
+
- `--run <id>`
|
|
80
140
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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
|
|
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
|
|
191
|
-
|
|
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 (
|
|
213
|
+
if (inputSpecPath && inlineTask) {
|
|
195
214
|
throw new Error("--spec is mutually exclusive with --task / --prompt.");
|
|
196
215
|
}
|
|
197
|
-
await runCommandHandler(
|
|
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.
|
|
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
|
+
}
|
package/dist/core/task-runner.js
CHANGED
|
@@ -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
|
}
|
package/dist/state/schema.js
CHANGED
|
@@ -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([
|
|
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),
|
package/dist/utils/gh.js
ADDED
|
@@ -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,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.
|
|
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": {
|