orcastrator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # orca
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
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun install
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Inline goal (no file needed)
16
+
17
+ ```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"
21
+ ```
22
+
23
+ ### Spec file
24
+
25
+ ```bash
26
+ orca run --spec ./specs/myfeature.md
27
+ ```
28
+
29
+ `run` is optional — the above is equivalent to:
30
+
31
+ ```bash
32
+ orca --spec ./specs/myfeature.md
33
+ ```
34
+
35
+ ### Other commands
36
+
37
+ ```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
45
+ ```
46
+
47
+ ### Hooks
48
+
49
+ ```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"
54
+ ```
55
+
56
+ Available hooks: `--on-milestone`, `--on-task-complete`, `--on-task-fail`, `--on-complete`, `--on-error`
57
+
58
+ ## Run output
59
+
60
+ Run state is written to:
61
+
62
+ ```text
63
+ ~/.orca/runs/<run-id>/status.json
64
+ ```
65
+
66
+ Run IDs follow the format:
67
+
68
+ ```text
69
+ <spec-slug>-<timestamp-ms>-<4char-hex>
70
+ ```
71
+
72
+ ## Dev
73
+
74
+ ```bash
75
+ bun test # run tests
76
+ bun run src/cli/index.ts -p "your goal here" # run without building
77
+ ```
78
+
79
+ ## Architecture
80
+
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 |
@@ -0,0 +1,137 @@
1
+ import { unstable_v2_createSession } from "@anthropic-ai/claude-agent-sdk";
2
+ function buildPlanningPrompt(spec, systemContext) {
3
+ return [
4
+ systemContext,
5
+ "You are decomposing a spec into an ordered task graph.",
6
+ "Return a JSON array of tasks.",
7
+ "Each task must include fields: id, name, description, dependencies, acceptance_criteria, status, retries, maxRetries.",
8
+ "Set status to \"pending\", retries to 0, and maxRetries to 3 for every task.",
9
+ "dependencies must be an array of task IDs.",
10
+ "acceptance_criteria must be an array of strings.",
11
+ "Return ONLY valid JSON. No markdown fences. No explanation.",
12
+ "Spec:",
13
+ spec
14
+ ].join("\n\n");
15
+ }
16
+ function buildTaskExecutionPrompt(task, runId, cwd) {
17
+ return [
18
+ "You are Orca's task execution assistant.",
19
+ `Run ID: ${runId}`,
20
+ `Repository CWD: ${cwd}`,
21
+ `Task ID: ${task.id}`,
22
+ `Task Name: ${task.name}`,
23
+ "Task Description:",
24
+ task.description,
25
+ "Acceptance Criteria:",
26
+ ...task.acceptance_criteria.map((criterion, index) => `${index + 1}. ${criterion}`),
27
+ "Execute this task and return ONLY JSON.",
28
+ "JSON schema:",
29
+ "{\"outcome\":\"done\"|\"failed\",\"error\"?:\"string\"}",
30
+ "If you cannot complete the task, return outcome=failed with a short error reason."
31
+ ].join("\n\n");
32
+ }
33
+ function extractAssistantText(message) {
34
+ if (!message || typeof message !== "object") {
35
+ return null;
36
+ }
37
+ const obj = message;
38
+ if (obj.type !== "assistant") {
39
+ return null;
40
+ }
41
+ const blocks = obj.message?.content;
42
+ if (!Array.isArray(blocks)) {
43
+ return null;
44
+ }
45
+ const text = blocks
46
+ .filter((block) => block.type === "text" && typeof block.text === "string")
47
+ .map((block) => block.text)
48
+ .join("\n")
49
+ .trim();
50
+ return text.length > 0 ? text : null;
51
+ }
52
+ function parseTaskArray(raw) {
53
+ const parsed = JSON.parse(raw);
54
+ if (!Array.isArray(parsed)) {
55
+ throw new Error("Claude plan response was not a JSON array");
56
+ }
57
+ return parsed;
58
+ }
59
+ function parseTaskExecution(raw) {
60
+ const parsed = JSON.parse(raw);
61
+ if (!parsed || typeof parsed !== "object") {
62
+ throw new Error("Claude task response was not a JSON object");
63
+ }
64
+ const candidate = parsed;
65
+ if (candidate.outcome !== "done" && candidate.outcome !== "failed") {
66
+ throw new Error("Claude task response missing valid outcome");
67
+ }
68
+ if (candidate.error !== undefined && typeof candidate.error !== "string") {
69
+ throw new Error("Claude task response error must be a string");
70
+ }
71
+ return {
72
+ outcome: candidate.outcome,
73
+ rawResponse: raw,
74
+ ...(typeof candidate.error === "string" ? { error: candidate.error } : {})
75
+ };
76
+ }
77
+ async function collectSessionResult(session) {
78
+ const assistantMessages = [];
79
+ let resultText = null;
80
+ for await (const message of session.stream()) {
81
+ const assistantText = extractAssistantText(message);
82
+ if (assistantText) {
83
+ assistantMessages.push(assistantText);
84
+ }
85
+ if (message.type === "result" &&
86
+ message.subtype === "success" &&
87
+ typeof message.result === "string") {
88
+ resultText = message.result;
89
+ }
90
+ if (message.type === "result" && message.subtype !== "success") {
91
+ const details = "errors" in message ? message.errors.join("; ") : "unknown error";
92
+ throw new Error(`Claude session failed (${message.subtype}): ${details}`);
93
+ }
94
+ }
95
+ const rawResponse = assistantMessages.length > 0
96
+ ? assistantMessages[assistantMessages.length - 1]
97
+ : resultText;
98
+ if (!rawResponse) {
99
+ throw new Error("Claude response was empty");
100
+ }
101
+ return rawResponse;
102
+ }
103
+ function getModel(config) {
104
+ return config?.claude?.model ?? process.env.ORCA_CLAUDE_MODEL ?? "claude-sonnet-4-5";
105
+ }
106
+ export async function planSpec(spec, systemContext) {
107
+ const session = unstable_v2_createSession({
108
+ model: getModel()
109
+ });
110
+ try {
111
+ const streamPromise = collectSessionResult(session);
112
+ await session.send(buildPlanningPrompt(spec, systemContext));
113
+ const rawResponse = await streamPromise;
114
+ return {
115
+ tasks: parseTaskArray(rawResponse),
116
+ rawResponse
117
+ };
118
+ }
119
+ finally {
120
+ session.close();
121
+ }
122
+ }
123
+ export async function executeTask(task, runId, config) {
124
+ const session = unstable_v2_createSession({
125
+ model: getModel(config),
126
+ permissionMode: "bypassPermissions",
127
+ });
128
+ try {
129
+ const streamPromise = collectSessionResult(session);
130
+ await session.send(buildTaskExecutionPrompt(task, runId, process.cwd()));
131
+ const rawResponse = await streamPromise;
132
+ return parseTaskExecution(rawResponse);
133
+ }
134
+ finally {
135
+ session.close();
136
+ }
137
+ }
@@ -0,0 +1 @@
1
+ export { createCodexSession, planSpec, executeTask, } from "./session.js";
@@ -0,0 +1,247 @@
1
+ import { CodexClient } from "orca-codex-client";
2
+ function buildPlanningPrompt(spec, systemContext) {
3
+ return [
4
+ systemContext,
5
+ "You are decomposing a spec into an ordered task graph.",
6
+ "Return a JSON array of tasks.",
7
+ "Each task must include fields: id, name, description, dependencies, acceptance_criteria, status, retries, maxRetries.",
8
+ 'Set status to "pending", retries to 0, and maxRetries to 3 for every task.',
9
+ "dependencies must be an array of task IDs.",
10
+ "acceptance_criteria must be an array of strings.",
11
+ "Return ONLY valid JSON. No markdown fences. No explanation.",
12
+ "Spec:",
13
+ spec,
14
+ ].join("\n\n");
15
+ }
16
+ function buildTaskExecutionPrompt(task, runId, cwd) {
17
+ return [
18
+ "You are Orca's task execution assistant.",
19
+ `Run ID: ${runId}`,
20
+ `Repository CWD: ${cwd}`,
21
+ `Task ID: ${task.id}`,
22
+ `Task Name: ${task.name}`,
23
+ "Task Description:",
24
+ task.description,
25
+ "Acceptance Criteria:",
26
+ ...task.acceptance_criteria.map((criterion, index) => `${index + 1}. ${criterion}`),
27
+ "Execute this task. You have full shell access — run commands, read/write files, and do whatever is needed.",
28
+ "When done, output ONLY JSON on the last line:",
29
+ '{"outcome":"done"|"failed","error"?:"string"}',
30
+ "If you cannot complete the task, return outcome=failed with a short error reason.",
31
+ ].join("\n\n");
32
+ }
33
+ function extractAgentText(result) {
34
+ if (result.agentMessage.length > 0) {
35
+ return result.agentMessage;
36
+ }
37
+ const agentItems = result.items.filter((item) => item.type === "agentMessage");
38
+ if (agentItems.length > 0) {
39
+ const last = agentItems[agentItems.length - 1];
40
+ if (last !== undefined && "text" in last && typeof last.text === "string") {
41
+ return last.text;
42
+ }
43
+ }
44
+ throw new Error("Codex response was empty");
45
+ }
46
+ function extractJson(text) {
47
+ // Try to find JSON in the response — could be wrapped in markdown fences
48
+ const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
49
+ if (fenceMatch?.[1]) {
50
+ return fenceMatch[1].trim();
51
+ }
52
+ // Try the last line (common pattern: explanation then JSON)
53
+ const lines = text.trim().split("\n");
54
+ for (let i = lines.length - 1; i >= 0; i--) {
55
+ const line = lines[i]?.trim() ?? "";
56
+ if (line.startsWith("{") || line.startsWith("[")) {
57
+ try {
58
+ JSON.parse(line);
59
+ return line;
60
+ }
61
+ catch {
62
+ // not valid JSON, keep looking
63
+ }
64
+ }
65
+ }
66
+ // Fall back to entire text
67
+ return text.trim();
68
+ }
69
+ function parseTaskArray(raw) {
70
+ const json = extractJson(raw);
71
+ const parsed = JSON.parse(json);
72
+ if (!Array.isArray(parsed)) {
73
+ throw new Error("Codex plan response was not a JSON array");
74
+ }
75
+ return parsed;
76
+ }
77
+ const POSITIVE_COMPLETION_PATTERNS = [
78
+ /\bdone\b/i,
79
+ /\bcomplet/i,
80
+ /\bsuccess/i,
81
+ /\bwrote\b/i,
82
+ /\bwritten\b/i,
83
+ /\bcreated\b/i,
84
+ /\bfinished\b/i,
85
+ ];
86
+ const FAILURE_PATTERNS = [
87
+ /\berror\b/i,
88
+ /\bfailed?\b/i,
89
+ /\bcannot\b/i,
90
+ /\bunable\b/i,
91
+ /\bpermission denied\b/i,
92
+ ];
93
+ function inferOutcomeFromText(raw) {
94
+ const hasFailure = FAILURE_PATTERNS.some((p) => p.test(raw));
95
+ if (hasFailure) {
96
+ return {
97
+ outcome: "failed",
98
+ rawResponse: raw,
99
+ error: "Codex did not emit a JSON completion marker; inferred failure from response text.",
100
+ };
101
+ }
102
+ // No failure indicators — assume done. Codex often narrates rather than emitting JSON,
103
+ // so false negatives here are more harmful than false positives.
104
+ if (!POSITIVE_COMPLETION_PATTERNS.some((p) => p.test(raw))) {
105
+ console.warn("[orca] Warning: Codex response had no clear completion marker; assuming done.");
106
+ }
107
+ return { outcome: "done", rawResponse: raw };
108
+ }
109
+ function parseTaskExecution(raw) {
110
+ let json;
111
+ let parsed;
112
+ try {
113
+ json = extractJson(raw);
114
+ parsed = JSON.parse(json);
115
+ }
116
+ catch {
117
+ // Codex did not emit a JSON completion marker — fall back to text inference.
118
+ return inferOutcomeFromText(raw);
119
+ }
120
+ if (!parsed || typeof parsed !== "object") {
121
+ return inferOutcomeFromText(raw);
122
+ }
123
+ const candidate = parsed;
124
+ if (candidate.outcome !== "done" && candidate.outcome !== "failed") {
125
+ return inferOutcomeFromText(raw);
126
+ }
127
+ if (candidate.error !== undefined && typeof candidate.error !== "string") {
128
+ throw new Error("Codex task response error must be a string");
129
+ }
130
+ return {
131
+ outcome: candidate.outcome,
132
+ rawResponse: raw,
133
+ ...(typeof candidate.error === "string" ? { error: candidate.error } : {}),
134
+ };
135
+ }
136
+ function getModel(config) {
137
+ return config?.codex?.model ?? process.env.ORCA_CODEX_MODEL ?? "gpt-5.3-codex";
138
+ }
139
+ function getCodexPath() {
140
+ return (process.env.ORCA_CODEX_PATH ??
141
+ `${process.env.HOME}/.nvm/versions/node/v22.22.0/bin/codex`);
142
+ }
143
+ export async function createCodexSession(cwd, config) {
144
+ const client = new CodexClient({
145
+ codexPath: getCodexPath(),
146
+ model: getModel(config),
147
+ cwd,
148
+ approvalPolicy: "never",
149
+ sandbox: "workspace-write",
150
+ });
151
+ await client.connect();
152
+ const thread = await client.startThread({});
153
+ const threadId = thread.id;
154
+ return {
155
+ threadId,
156
+ async planSpec(spec, systemContext) {
157
+ const result = await client.runTurn({
158
+ threadId,
159
+ input: [{ type: "text", text: buildPlanningPrompt(spec, systemContext) }],
160
+ });
161
+ const rawResponse = extractAgentText(result);
162
+ return {
163
+ tasks: parseTaskArray(rawResponse),
164
+ rawResponse,
165
+ };
166
+ },
167
+ async executeTask(task, runId) {
168
+ const result = await client.runTurn({
169
+ threadId,
170
+ input: [
171
+ {
172
+ type: "text",
173
+ text: buildTaskExecutionPrompt(task, runId, cwd),
174
+ },
175
+ ],
176
+ });
177
+ const rawResponse = extractAgentText(result);
178
+ return parseTaskExecution(rawResponse);
179
+ },
180
+ async consultTaskGraph(tasks) {
181
+ const taskGraphJson = JSON.stringify(tasks, null, 2);
182
+ const prompt = [
183
+ "Review this Orca task graph before execution.",
184
+ "Flag any: missing steps, wrong dependency order, tasks that are underdefined, or potential blockers.",
185
+ "",
186
+ "Set ok: false ONLY if there is a hard blocking issue — dependency cycle, circular reference, a task that cannot possibly run as defined, or a critical missing step that would cause the run to fail.",
187
+ "For minor issues (ambiguous wording, style preferences, nice-to-haves): list them in issues but set ok: true.",
188
+ "If the graph looks generally reasonable and executable, set ok: true even if you have minor suggestions.",
189
+ "",
190
+ "Be brief. Output JSON on the last line: { \"issues\": [...], \"ok\": boolean }",
191
+ "",
192
+ "Task graph:",
193
+ taskGraphJson,
194
+ ].join("\n");
195
+ const result = await client.runTurn({
196
+ threadId,
197
+ input: [{ type: "text", text: prompt }],
198
+ });
199
+ const rawResponse = extractAgentText(result);
200
+ const json = extractJson(rawResponse);
201
+ const parsed = JSON.parse(json);
202
+ if (!parsed || typeof parsed !== "object") {
203
+ throw new Error("Codex consultation response was not a JSON object");
204
+ }
205
+ const candidate = parsed;
206
+ return {
207
+ issues: Array.isArray(candidate.issues)
208
+ ? candidate.issues.filter((i) => typeof i === "string")
209
+ : [],
210
+ ok: typeof candidate.ok === "boolean" ? candidate.ok : false,
211
+ };
212
+ },
213
+ async reviewChanges() {
214
+ const result = await client.runReview({
215
+ threadId,
216
+ target: { type: "uncommittedChanges" },
217
+ });
218
+ return result.reviewText;
219
+ },
220
+ async disconnect() {
221
+ await client.disconnect();
222
+ },
223
+ };
224
+ }
225
+ /**
226
+ * Stateless wrappers that match the Claude adapter interface.
227
+ * Each call creates a new client + thread (no persistence).
228
+ * Use createCodexSession() for persistent threads.
229
+ */
230
+ export async function planSpec(spec, systemContext, config) {
231
+ const session = await createCodexSession(process.cwd(), config);
232
+ try {
233
+ return await session.planSpec(spec, systemContext);
234
+ }
235
+ finally {
236
+ await session.disconnect();
237
+ }
238
+ }
239
+ export async function executeTask(task, runId, config) {
240
+ const session = await createCodexSession(process.cwd(), config);
241
+ try {
242
+ return await session.executeTask(task, runId);
243
+ }
244
+ finally {
245
+ await session.disconnect();
246
+ }
247
+ }
@@ -0,0 +1,60 @@
1
+ import { RunStore } from "../../state/store.js";
2
+ function createStore() {
3
+ const runsDir = process.env.ORCA_RUNS_DIR;
4
+ return runsDir ? new RunStore(runsDir) : new RunStore();
5
+ }
6
+ function formatRunIds(runs) {
7
+ if (runs.length === 0) {
8
+ return "(none)";
9
+ }
10
+ return runs.map((run) => run.runId).join(", ");
11
+ }
12
+ function getActiveRuns(runs) {
13
+ return runs.filter((run) => run.overallStatus === "planning" || run.overallStatus === "running");
14
+ }
15
+ export async function cancelCommandHandler(options) {
16
+ const store = createStore();
17
+ const knownRuns = await store.listRuns();
18
+ if (!options.run) {
19
+ console.error(`Missing required --run <run-id>\nActive runs: ${formatRunIds(getActiveRuns(knownRuns))}`);
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+ const run = await store.getRun(options.run);
24
+ if (!run) {
25
+ console.error(`Run not found: ${options.run}\nKnown run IDs: ${formatRunIds(knownRuns)}`);
26
+ process.exitCode = 1;
27
+ return;
28
+ }
29
+ const cancelledAt = new Date().toISOString();
30
+ let cancelledTaskId = null;
31
+ const tasks = run.tasks.map((task) => {
32
+ if (task.status === "in_progress") {
33
+ cancelledTaskId = task.id;
34
+ return {
35
+ ...task,
36
+ status: "cancelled",
37
+ finishedAt: cancelledAt,
38
+ lastError: "Run cancelled"
39
+ };
40
+ }
41
+ return task;
42
+ });
43
+ await store.updateRun(run.runId, {
44
+ overallStatus: "cancelled",
45
+ tasks
46
+ });
47
+ if (cancelledTaskId) {
48
+ console.log(`Cancelled run ${run.runId}; task ${cancelledTaskId} marked cancelled.`);
49
+ return;
50
+ }
51
+ console.log(`Cancelled run ${run.runId}.`);
52
+ }
53
+ export function registerCancelCommand(program) {
54
+ program
55
+ .command("cancel")
56
+ .description("Cancel an active run")
57
+ .option("--run <run-id>", "Run ID to cancel")
58
+ .option("--config <path>", "Path to orca config file")
59
+ .action(async (options) => cancelCommandHandler(options));
60
+ }
@@ -0,0 +1,50 @@
1
+ import { RunStore } from "../../state/store.js";
2
+ function createStore() {
3
+ const runsDir = process.env.ORCA_RUNS_DIR;
4
+ return runsDir ? new RunStore(runsDir) : new RunStore();
5
+ }
6
+ function pad(value, width) {
7
+ return value.padEnd(width, " ");
8
+ }
9
+ function formatTable(headers, rows) {
10
+ const widths = headers.map((header, index) => {
11
+ const rowWidths = rows.map((row) => row[index]?.length ?? 0);
12
+ return Math.max(header.length, ...rowWidths);
13
+ });
14
+ const headerLine = headers.map((header, index) => pad(header, widths[index] ?? header.length)).join(" ");
15
+ const separator = widths.map((width) => "-".repeat(width)).join(" ");
16
+ const body = rows.map((row) => row.map((cell, index) => pad(cell, widths[index] ?? cell.length)).join(" "));
17
+ return [headerLine, separator, ...body].join("\n");
18
+ }
19
+ function toSummaryRow(run) {
20
+ return [
21
+ run.runId,
22
+ run.specPath,
23
+ run.overallStatus,
24
+ run.createdAt
25
+ ];
26
+ }
27
+ export function formatRunSummaryTable(runs) {
28
+ const headers = ["Run ID", "Spec", "Status", "Started"];
29
+ const rows = runs.map(toSummaryRow);
30
+ return formatTable(headers, rows);
31
+ }
32
+ export async function listRuns() {
33
+ const store = createStore();
34
+ return await store.listRuns();
35
+ }
36
+ export async function listCommandHandler(_options) {
37
+ const runs = await listRuns();
38
+ if (runs.length === 0) {
39
+ console.log("No runs found.");
40
+ return;
41
+ }
42
+ console.log(formatRunSummaryTable(runs));
43
+ }
44
+ export function registerListCommand(program) {
45
+ program
46
+ .command("list")
47
+ .description("List all runs in run store")
48
+ .option("--config <path>", "Path to orca config file")
49
+ .action(async (options) => listCommandHandler(options));
50
+ }
@@ -0,0 +1,40 @@
1
+ import { access } from "node:fs/promises";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import path from "node:path";
4
+ import { runPlanner } from "../../core/planner.js";
5
+ import { RunStore } from "../../state/store.js";
6
+ import { generateRunId } from "../../utils/ids.js";
7
+ export async function planCommand(options) {
8
+ const specPath = path.resolve(options.spec);
9
+ await access(specPath, fsConstants.R_OK);
10
+ const runId = generateRunId(specPath);
11
+ console.log(`Run ID: ${runId}`);
12
+ const store = new RunStore();
13
+ await store.createRun(runId, specPath);
14
+ await runPlanner(specPath, store, runId);
15
+ const run = await store.getRun(runId);
16
+ if (!run) {
17
+ throw new Error(`Run not found after planning: ${runId}`);
18
+ }
19
+ console.log("Tasks:");
20
+ for (const task of run.tasks) {
21
+ console.log(`- ${task.name}`);
22
+ }
23
+ console.log(`Run dir: ${store.getRunDir(runId)}`);
24
+ }
25
+ export async function planCommandHandler(options) {
26
+ const commandOptions = options.config
27
+ ? { spec: options.spec, config: options.config }
28
+ : { spec: options.spec };
29
+ await planCommand(commandOptions);
30
+ }
31
+ export function registerPlanCommand(program) {
32
+ program
33
+ .command("plan")
34
+ .description("Run pre-planning and output validated task graph")
35
+ .requiredOption("--spec <path>", "Path to spec markdown file")
36
+ .option("--config <path>", "Path to orca config file")
37
+ .option("--on-milestone <cmd>", "Shell hook command for onMilestone")
38
+ .option("--on-error <cmd>", "Shell hook command for onError")
39
+ .action(async (options) => planCommandHandler(options));
40
+ }
@@ -0,0 +1,11 @@
1
+ export async function prFinalizeCommandHandler(_options) {
2
+ console.log("not yet implemented");
3
+ }
4
+ export function registerPrFinalizeCommand(program) {
5
+ program
6
+ .command("pr-finalize")
7
+ .description("Finalize a prepared pull request")
8
+ .requiredOption("--run <run-id>", "Run ID to finalize PR for")
9
+ .option("--config <path>", "Path to orca config file")
10
+ .action(async (options) => prFinalizeCommandHandler(options));
11
+ }