orc-brain 1.0.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/dist/index.js ADDED
@@ -0,0 +1,757 @@
1
+ /** `orc` CLI — a thin client over the orc-brain HTTP API (§9). */
2
+ import { execFileSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, mkdtempSync, } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { Command } from "commander";
8
+ import { checkProviderEnv, liveAuthCheck, runDoctorSync, } from "@orc-brain/core";
9
+ import { api, streamEvents } from "./client.js";
10
+ /** Prints JSON when `--json`, otherwise the provided human renderer. */
11
+ function output(json, data, human) {
12
+ if (json)
13
+ console.log(JSON.stringify(data, null, 2));
14
+ else
15
+ human();
16
+ }
17
+ /** This package's root (one level above `src/` in dev, `dist/` when built). */
18
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
19
+ /** CLI version, read from package.json so `orc --version` tracks releases. */
20
+ const VERSION = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8")).version;
21
+ /**
22
+ * Locates the built web UI: `<pkg>/ui` in the published npm package (bundled
23
+ * at `prepack`), `packages/ui/dist` when running from the monorepo.
24
+ */
25
+ function resolveUiDist() {
26
+ return [join(pkgRoot, "ui"), join(pkgRoot, "..", "ui", "dist")].find((dir) => existsSync(join(dir, "index.html")));
27
+ }
28
+ /** Builds the `orc` command tree. */
29
+ export function buildCli() {
30
+ const program = new Command();
31
+ program
32
+ .name("orc")
33
+ .description("Local orchestrator brain for Claude Code sub-agents")
34
+ .version(VERSION);
35
+ // --- orc serve -----------------------------------------------------------
36
+ program
37
+ .command("serve")
38
+ .description("Start the orchestrator daemon + web UI")
39
+ .option("--port <port>", "port to bind", "4173")
40
+ .option("--state-dir <dir>", "state directory (.orc)")
41
+ .action(async (opts) => {
42
+ // Refuse to start on API-key billing (§2 preflight).
43
+ const env = checkProviderEnv();
44
+ if (!env.ok) {
45
+ console.error(`Refusing to start: unset ${env.offenders.join(", ")} — ` +
46
+ `these switch billing off the Max subscription (§2).`);
47
+ process.exit(1);
48
+ }
49
+ const { createServer } = await import("@orc-brain/server");
50
+ const app = createServer({
51
+ stateDir: opts.stateDir,
52
+ uiDist: resolveUiDist(),
53
+ });
54
+ await app.listen({ port: Number(opts.port), host: "127.0.0.1" });
55
+ console.error(`orc serve listening on http://127.0.0.1:${opts.port}`);
56
+ });
57
+ // --- orc doctor ----------------------------------------------------------
58
+ program
59
+ .command("doctor")
60
+ .description("Verify CLI auth (subscription, no API key), versions, git")
61
+ .option("--live", "also run a live subscription/auth probe")
62
+ .option("--json", "output JSON")
63
+ .action(async (opts) => {
64
+ const checks = runDoctorSync();
65
+ const live = opts.live ? await liveAuthCheck() : undefined;
66
+ const allOk = checks.every((c) => c.ok) && (!live || live.ok);
67
+ output(!!opts.json, { checks, live }, () => {
68
+ for (const c of checks) {
69
+ console.log(`${c.ok ? "✓" : "✗"} ${c.name} — ${c.detail}`);
70
+ }
71
+ if (live) {
72
+ console.log(`${live.ok ? "✓" : "✗"} live auth — ` +
73
+ (live.ok
74
+ ? `apiKeySource=${live.apiKeySource ?? "?"} model=${live.model ?? "?"}`
75
+ : (live.error ?? "failed")));
76
+ }
77
+ });
78
+ process.exit(allOk ? 0 : 1);
79
+ });
80
+ // --- orc status ----------------------------------------------------------
81
+ program
82
+ .command("status [run-id]")
83
+ .description("Run state, scopes, budget, active subagents")
84
+ .option("--watch", "stream live updates")
85
+ .option("--json", "output JSON")
86
+ .action(async (runId, opts) => {
87
+ const id = runId ?? (await latestRunId());
88
+ if (!id) {
89
+ console.error("no runs found");
90
+ process.exit(1);
91
+ }
92
+ await printStatus(id, !!opts.json);
93
+ if (opts.watch) {
94
+ await streamEvents(`?run_id=${id}`, () => void printStatus(id, false));
95
+ }
96
+ });
97
+ // --- orc tail ------------------------------------------------------------
98
+ program
99
+ .command("tail <task-id>")
100
+ .description("Live transcript of one subagent")
101
+ .option("-f, --follow", "follow the stream")
102
+ .action(async (taskId, opts) => {
103
+ const { task } = await api(`/api/tasks/${taskId}`);
104
+ if (!opts.follow) {
105
+ console.log(`session ${task.session_id ?? "(pending)"}`);
106
+ return;
107
+ }
108
+ const runId = await latestRunId();
109
+ await streamEvents(runId ? `?run_id=${runId}` : "", (e) => {
110
+ const d = e.data;
111
+ const p = d?.payload;
112
+ if (!p || p.task_id !== taskId)
113
+ return;
114
+ if (e.event === "tool.call")
115
+ console.log(` ⚙ ${p.tool_name}: ${p.input_summary}`);
116
+ else if (e.event === "text.delta")
117
+ process.stdout.write(String(p.delta));
118
+ else if (e.event === "task.state")
119
+ console.log(`\n[${p.status}]`);
120
+ });
121
+ });
122
+ // --- orc run start|pause|resume|stop ------------------------------------
123
+ const run = program.command("run").description("Run lifecycle");
124
+ run
125
+ .command("start <goal-id>")
126
+ .option("--budget <usd>", "run budget in USD", "25")
127
+ .option("--concurrency <n>", "max concurrent workers")
128
+ .action(async (goalId, opts) => {
129
+ const res = await api("/api/runs", {
130
+ method: "POST",
131
+ body: JSON.stringify({
132
+ goal_id: goalId,
133
+ budget_usd: Number(opts.budget),
134
+ concurrency_limit: opts.concurrency
135
+ ? Number(opts.concurrency)
136
+ : undefined,
137
+ }),
138
+ });
139
+ console.log(res.run.id);
140
+ });
141
+ for (const verb of ["pause", "resume", "stop"]) {
142
+ run.command(`${verb} <run-id>`).action(async (runId) => {
143
+ const path = verb === "stop" ? "pause" : verb;
144
+ await api(`/api/runs/${runId}/${path}`, { method: "POST" });
145
+ console.log(`${verb} ${runId} ok`);
146
+ });
147
+ }
148
+ // orc run mode <run-id> <supervised|unattended> (autonomous-loop.md §3.5)
149
+ run
150
+ .command("mode <run-id> <mode>")
151
+ .description("Set autonomous-loop approval mode: supervised | unattended")
152
+ .action(async (runId, mode) => {
153
+ const res = await api(`/api/runs/${runId}/mode`, {
154
+ method: "POST",
155
+ body: JSON.stringify({ mode }),
156
+ });
157
+ console.log(`run ${runId} mode=${res.mode}`);
158
+ });
159
+ // --- orc panic -----------------------------------------------------------
160
+ program
161
+ .command("panic")
162
+ .description("Kill switch: interrupt everything")
163
+ .action(async () => {
164
+ const res = await api("/api/panic", {
165
+ method: "POST",
166
+ });
167
+ console.log(`panicked; aborted ${res.aborted.length} run(s)`);
168
+ });
169
+ // --- orc purge -----------------------------------------------------------
170
+ program
171
+ .command("purge")
172
+ .description("Wipe the orc database (goals, runs, tasks, reports, events)")
173
+ .option("--keep-projects", "keep the project registry")
174
+ .option("--yes", "confirm — required, purge is irreversible")
175
+ .action(async (opts) => {
176
+ if (!opts.yes) {
177
+ console.error("purge deletes every goal, run, task, report and event" +
178
+ (opts.keepProjects ? " (projects kept)" : ", including projects") +
179
+ ". Re-run with --yes to confirm.");
180
+ process.exit(1);
181
+ }
182
+ const { deleted } = await api("/api/purge", {
183
+ method: "POST",
184
+ body: JSON.stringify({ keep_projects: !!opts.keepProjects }),
185
+ });
186
+ const total = Object.values(deleted).reduce((a, b) => a + b, 0);
187
+ console.log(`purged ${total} row(s):`);
188
+ for (const [table, n] of Object.entries(deleted)) {
189
+ if (n > 0)
190
+ console.log(` ${table}: ${n}`);
191
+ }
192
+ });
193
+ // --- orc plan ------------------------------------------------------------
194
+ const plan = program.command("plan").description("Planner (§3)");
195
+ plan
196
+ .command("run <goal-id>", { isDefault: true })
197
+ .description("Run the Planner → proposed scopes + tasks")
198
+ .option("--json", "output JSON")
199
+ .action(async (goalId, opts) => {
200
+ const res = await api(`/api/goals/${goalId}/plan`, { method: "POST" });
201
+ output(!!opts.json, res, () => {
202
+ console.log(`planned ${res.scopes.length} scope(s), ${res.tasks.length} task(s):`);
203
+ for (const s of res.scopes)
204
+ console.log(` ${s.id} ${s.name}`);
205
+ console.log(`approve with: orc approve ${goalId}`);
206
+ });
207
+ });
208
+ plan
209
+ .command("show <goal-id>")
210
+ .description("Render the proposed scopes, boundaries, and tasks")
211
+ .option("--json", "output JSON")
212
+ .action(async (goalId, opts) => {
213
+ const data = await api(`/api/goals/${goalId}`);
214
+ output(!!opts.json, data, () => {
215
+ console.log(`${data.goal.title} [${data.goal.status}]`);
216
+ for (const s of data.scopes) {
217
+ console.log(`\n▸ ${s.name} [${s.status}] env=${s.environment} ` +
218
+ `tier=${s.model_tier} mode=${s.permission_mode}`);
219
+ console.log(` paths: ${s.path_allowlist.join(", ")}`);
220
+ for (const t of data.tasks.filter((t) => t.scope_id === s.id)) {
221
+ console.log(` · ${t.title} (${t.task_type}) [${t.status}]`);
222
+ }
223
+ }
224
+ });
225
+ });
226
+ plan
227
+ .command("cancel <goal-id>")
228
+ .description("Cancel the proposed plan: drop proposed scopes, back to draft")
229
+ .action(async (goalId) => {
230
+ await api(`/api/goals/${goalId}/plan`, { method: "DELETE" });
231
+ console.log(`plan cancelled; goal ${goalId} is draft again`);
232
+ });
233
+ plan
234
+ .command("edit <goal-id>")
235
+ .description("Edit the proposed plan JSON in $EDITOR, re-validate & apply")
236
+ .action(async (goalId) => {
237
+ const data = await api(`/api/goals/${goalId}`);
238
+ // Reconstruct an editable Plan (scopes with nested tasks by title).
239
+ const planScopes = data.scopes.map((s) => ({
240
+ name: s.name,
241
+ description: s.description,
242
+ path_allowlist: s.path_allowlist,
243
+ path_denylist: s.path_denylist,
244
+ allowed_tools: s.allowed_tools,
245
+ disallowed_tools: s.disallowed_tools,
246
+ model_tier: s.model_tier,
247
+ environment: s.environment,
248
+ permission_mode: s.permission_mode,
249
+ forbidden_actions: s.forbidden_actions,
250
+ success_criteria: s.success_criteria,
251
+ max_budget_usd: s.max_budget_usd,
252
+ tasks: data.tasks
253
+ .filter((t) => t.scope_id === s.id)
254
+ .map((t) => ({
255
+ title: t.title,
256
+ prompt: t.prompt,
257
+ task_type: t.task_type,
258
+ })),
259
+ }));
260
+ const file = join(mkdtempSync(join(tmpdir(), "orc-plan-")), "plan.json");
261
+ writeFileSync(file, JSON.stringify({ scopes: planScopes }, null, 2));
262
+ const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
263
+ execFileSync(editor, [file], { stdio: "inherit" });
264
+ const edited = JSON.parse(readFileSync(file, "utf8"));
265
+ try {
266
+ const res = await api(`/api/goals/${goalId}/plan`, { method: "PUT", body: JSON.stringify(edited) });
267
+ console.log(`re-materialized ${res.scopes.length} scope(s)`);
268
+ }
269
+ catch (err) {
270
+ console.error(err instanceof Error ? err.message : String(err));
271
+ process.exit(2);
272
+ }
273
+ });
274
+ // --- orc approve ---------------------------------------------------------
275
+ program
276
+ .command("approve <goal-id>")
277
+ .description("Approve all proposed scopes of a goal (or selected --scope)")
278
+ .option("--scope <id...>", "approve only these scope ids")
279
+ .option("--start", "then start an unattended run with the project defaults (spec 002 §R5)")
280
+ .action(async (goalId, opts) => {
281
+ if (opts.scope?.length) {
282
+ for (const id of opts.scope) {
283
+ await api(`/api/scopes/${id}/approve`, { method: "POST" });
284
+ }
285
+ console.log(`approved ${opts.scope.length} scope(s)`);
286
+ return;
287
+ }
288
+ const res = await api(`/api/goals/${goalId}/approve`, {
289
+ method: "POST",
290
+ body: JSON.stringify({ start_run: !!opts.start }),
291
+ });
292
+ console.log(`approved ${res.approved.length} scope(s)`);
293
+ if (res.run)
294
+ console.log(`run ${res.run.id} started (unattended)`);
295
+ });
296
+ // --- orc project add|list|show|rm|gc (spec 002 §R6, §R12) -----------------
297
+ const project = program
298
+ .command("project")
299
+ .description("Registered local repositories orc operates on (spec 002)");
300
+ project
301
+ .command("add [path]")
302
+ .description("Register a local git repository as a project (default: current directory)")
303
+ .option("--name <name>", "display name (default: directory basename)")
304
+ .option("--mode <mode>", "execution mode: worktree | in_repo", "in_repo")
305
+ .option("--budget <usd>", "default run budget in USD")
306
+ .option("--concurrency <n>", "default per-run concurrency")
307
+ .option("--auto-merge", "merge settled scope branches into the base branch automatically")
308
+ .action(async (path, opts) => {
309
+ const repoRoot = resolve(path ?? process.cwd());
310
+ const res = await api("/api/projects", {
311
+ method: "POST",
312
+ body: JSON.stringify({
313
+ repo_root: repoRoot,
314
+ name: opts.name,
315
+ execution_mode: opts.mode,
316
+ auto_merge: !!opts.autoMerge,
317
+ default_budget_usd: opts.budget ? Number(opts.budget) : undefined,
318
+ default_concurrency: opts.concurrency
319
+ ? Number(opts.concurrency)
320
+ : undefined,
321
+ }),
322
+ });
323
+ console.log(`${res.project.id} ${res.project.repo_root}`);
324
+ });
325
+ project.command("list").action(async () => {
326
+ const { projects } = await api("/api/projects");
327
+ for (const p of projects) {
328
+ console.log(`${p.id} ${p.execution_mode.padEnd(8)} ${p.name} ${p.repo_root}`);
329
+ }
330
+ });
331
+ project.command("show <id>").action(async (id) => {
332
+ console.log(JSON.stringify(await api(`/api/projects/${id}`), null, 2));
333
+ });
334
+ project.command("rm <id>").action(async (id) => {
335
+ await api(`/api/projects/${id}`, { method: "DELETE" });
336
+ console.log(`removed ${id}`);
337
+ });
338
+ project
339
+ .command("gc <id>")
340
+ .description("Remove orphaned worktrees of a project (branches are kept)")
341
+ .option("--prune-merged", "also delete orc/* branches fully merged into the current checkout")
342
+ .action(async (id, opts) => {
343
+ const res = await api(`/api/projects/${id}/gc`, {
344
+ method: "POST",
345
+ body: JSON.stringify({ prune_merged: !!opts.pruneMerged }),
346
+ });
347
+ console.log(`removed ${res.removed.length} orphaned worktree(s)`);
348
+ for (const p of res.removed)
349
+ console.log(` ${p}`);
350
+ if (opts.pruneMerged) {
351
+ console.log(`pruned ${res.pruned_branches.length} merged branch(es)`);
352
+ for (const b of res.pruned_branches)
353
+ console.log(` ${b}`);
354
+ }
355
+ });
356
+ // --- orc feature (spec 002 §R6): objective in, plan awaits approval -------
357
+ program
358
+ .command("feature <project-id> <objective...>")
359
+ .description("Create a goal under a project and start planning it")
360
+ .action(async (projectId, objectiveWords) => {
361
+ const objective = objectiveWords.join(" ");
362
+ const { goal } = await api(`/api/projects/${projectId}/goals`, { method: "POST", body: JSON.stringify({ objective }) });
363
+ console.log(goal.id);
364
+ console.log(`planning started; review with: orc plan show ${goal.id} ` +
365
+ `then approve with: orc approve ${goal.id} --start`);
366
+ });
367
+ // --- orc plugin (spec 003 §R8): declarations file + secrets ---------------
368
+ // Declarations are edited locally in `<state-dir>/plugins.json` (the server
369
+ // reads it at startup); secrets go through the API so the running server
370
+ // registers redaction/stripping immediately.
371
+ const plugin = program
372
+ .command("plugin")
373
+ .description("Plugins: declared in <state-dir>/plugins.json (spec 003)");
374
+ plugin
375
+ .command("list")
376
+ .description("Loaded plugins and their status (from the running server)")
377
+ .option("--json", "output JSON")
378
+ .action(async (opts) => {
379
+ const { plugins } = await api("/api/plugins");
380
+ output(!!opts.json, { plugins }, () => {
381
+ if (!plugins.length) {
382
+ return console.log("no plugins declared — add one with: orc plugin add linear");
383
+ }
384
+ for (const p of plugins) {
385
+ console.log(`${p.name.padEnd(12)} ${(p.version ?? "-").padEnd(8)} ` +
386
+ `${p.status.padEnd(9)} ${p.capabilities.join(",") || "-"}` +
387
+ (p.error ? ` (${p.error})` : ""));
388
+ }
389
+ });
390
+ });
391
+ plugin
392
+ .command("add <specifier>")
393
+ .description("Declare a plugin: a builtin alias (linear) or an absolute module path")
394
+ .option("--name <name>", "plugin name (default: the builtin alias)")
395
+ .option("--disable", "declare it disabled")
396
+ .option("--state-dir <dir>", "state directory (.orc)")
397
+ .action(async (specifier, opts) => {
398
+ const name = opts.name ?? specifier;
399
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
400
+ console.error(`plugin name must be kebab-case — pass one with --name (got "${name}")`);
401
+ process.exit(1);
402
+ }
403
+ const file = pluginsFilePath(opts.stateDir);
404
+ const decls = readPluginsFile(file);
405
+ if (decls.plugins.some((p) => p.name === name)) {
406
+ console.error(`plugin "${name}" already declared in ${file}`);
407
+ process.exit(1);
408
+ }
409
+ decls.plugins.push({ name, specifier, enabled: !opts.disable });
410
+ writePluginsFile(file, decls);
411
+ console.log(`declared "${name}" in ${file}`);
412
+ console.log("restart `orc serve` to load it");
413
+ });
414
+ plugin
415
+ .command("rm <name>")
416
+ .option("--state-dir <dir>", "state directory (.orc)")
417
+ .action(async (name, opts) => {
418
+ const file = pluginsFilePath(opts.stateDir);
419
+ const decls = readPluginsFile(file);
420
+ const before = decls.plugins.length;
421
+ decls.plugins = decls.plugins.filter((p) => p.name !== name);
422
+ if (decls.plugins.length === before) {
423
+ console.error(`plugin "${name}" not declared in ${file}`);
424
+ process.exit(1);
425
+ }
426
+ writePluginsFile(file, decls);
427
+ console.log(`removed "${name}"; restart \`orc serve\` to apply`);
428
+ });
429
+ for (const verb of ["enable", "disable"]) {
430
+ plugin
431
+ .command(`${verb} <name>`)
432
+ .option("--state-dir <dir>", "state directory (.orc)")
433
+ .action(async (name, opts) => {
434
+ const file = pluginsFilePath(opts.stateDir);
435
+ const decls = readPluginsFile(file);
436
+ const entry = decls.plugins.find((p) => p.name === name);
437
+ if (!entry) {
438
+ console.error(`plugin "${name}" not declared in ${file}`);
439
+ process.exit(1);
440
+ }
441
+ entry.enabled = verb === "enable";
442
+ writePluginsFile(file, decls);
443
+ console.log(`${verb}d "${name}"; restart \`orc serve\` to apply`);
444
+ });
445
+ }
446
+ const secret = plugin
447
+ .command("secret")
448
+ .description("Plugin secrets (stored in <state-dir>/secrets.json, 0600)");
449
+ secret
450
+ .command("set <plugin> <key>")
451
+ .description("Set a secret; the value is read from stdin, never argv")
452
+ .action(async (pluginName, key) => {
453
+ const value = await readSecretFromStdin(`value for ${key}: `);
454
+ if (!value) {
455
+ console.error("empty value; aborted");
456
+ process.exit(1);
457
+ }
458
+ await api(`/api/plugins/${pluginName}/secrets`, {
459
+ method: "POST",
460
+ body: JSON.stringify({ key, value }),
461
+ });
462
+ console.log(`${key} set for ${pluginName}`);
463
+ });
464
+ secret
465
+ .command("unset <plugin> <key>")
466
+ .action(async (pluginName, key) => {
467
+ await api(`/api/plugins/${pluginName}/secrets/${key}`, {
468
+ method: "DELETE",
469
+ });
470
+ console.log(`${key} unset for ${pluginName}`);
471
+ });
472
+ // --- orc provider (spec 003 §R8): browse & import external tasks ----------
473
+ const provider = program
474
+ .command("provider")
475
+ .description("Task providers exposed by plugins (spec 003)");
476
+ provider.command("list").action(async () => {
477
+ const { providers } = await api("/api/providers");
478
+ if (!providers.length)
479
+ return console.log("no active task providers");
480
+ for (const p of providers) {
481
+ console.log(`${p.name} ${p.capabilities.join(",")}`);
482
+ }
483
+ });
484
+ provider
485
+ .command("tasks <name>")
486
+ .description("List external tasks from a provider")
487
+ .option("--search <q>", "search text")
488
+ .option("--mine", "only tasks assigned to me")
489
+ .option("--state <state>", "filter by workflow state name")
490
+ .option("--team <team>", "filter by team key or name")
491
+ .option("--limit <n>", "max results")
492
+ .option("--json", "output JSON")
493
+ .action(async (name, opts) => {
494
+ const params = new URLSearchParams();
495
+ if (opts.search)
496
+ params.set("search", opts.search);
497
+ if (opts.mine)
498
+ params.set("assigned_to_me", "true");
499
+ if (opts.state)
500
+ params.set("state", opts.state);
501
+ if (opts.team)
502
+ params.set("team", opts.team);
503
+ if (opts.limit)
504
+ params.set("limit", opts.limit);
505
+ const qs = params.toString();
506
+ const { tasks } = await api(`/api/providers/${name}/tasks${qs ? `?${qs}` : ""}`);
507
+ output(!!opts.json, { tasks }, () => {
508
+ if (!tasks.length)
509
+ return console.log("no tasks found");
510
+ for (const t of tasks) {
511
+ console.log(`${t.identifier.padEnd(10)} ${t.state.padEnd(12)} ${t.title} ${t.url}`);
512
+ }
513
+ });
514
+ });
515
+ provider
516
+ .command("import <name> <external-id>")
517
+ .description("Import an external task as a goal and start planning it")
518
+ .requiredOption("--project <project-id>", "target project")
519
+ .action(async (name, externalId, opts) => {
520
+ const { goal } = await api(`/api/providers/${name}/import`, {
521
+ method: "POST",
522
+ body: JSON.stringify({
523
+ external_id: externalId,
524
+ project_id: opts.project,
525
+ }),
526
+ });
527
+ console.log(goal.id);
528
+ console.log(`planning started; review with: orc plan show ${goal.id} ` +
529
+ `then approve with: orc approve ${goal.id} --start`);
530
+ });
531
+ // --- orc goal new|list|show ----------------------------------------------
532
+ const goal = program.command("goal").description("Goal management");
533
+ goal
534
+ .command("new")
535
+ .description("Define a goal from a JSON file or flags")
536
+ .option("-f, --file <path>", "JSON file with the goal definition")
537
+ .option("--title <title>")
538
+ .option("--objective <text>")
539
+ .option("--repo <path>", "repo root", process.cwd())
540
+ .action(async (opts) => {
541
+ const body = opts.file
542
+ ? JSON.parse(readFileSync(opts.file, "utf8"))
543
+ : {
544
+ title: opts.title ?? "untitled goal",
545
+ objective: opts.objective ?? "",
546
+ success_criteria: [],
547
+ constraints: [],
548
+ out_of_scope: [],
549
+ repo_root: opts.repo,
550
+ };
551
+ const { goal: created } = await api("/api/goals", { method: "POST", body: JSON.stringify(body) });
552
+ console.log(created.id);
553
+ });
554
+ goal.command("list").action(async () => {
555
+ const { goals } = await api("/api/goals");
556
+ for (const g of goals)
557
+ console.log(`${g.id} ${g.status.padEnd(16)} ${g.title}`);
558
+ });
559
+ goal.command("show <id>").action(async (id) => {
560
+ console.log(JSON.stringify(await api(`/api/goals/${id}`), null, 2));
561
+ });
562
+ // --- orc tasks -----------------------------------------------------------
563
+ program
564
+ .command("tasks [run-id]")
565
+ .description("List tasks, optionally filtered by state")
566
+ .option("--state <state>", "running|blocked|failed|done|…")
567
+ .option("--json", "output JSON")
568
+ .action(async (runId, opts) => {
569
+ const id = runId ?? (await latestRunId());
570
+ if (!id)
571
+ return console.error("no runs found");
572
+ const q = opts.state ? `?state=${opts.state}` : "";
573
+ const { tasks } = await api(`/api/runs/${id}/tasks${q}`);
574
+ output(!!opts.json, { tasks }, () => {
575
+ for (const t of tasks) {
576
+ console.log(`${t.id} ${t.status.padEnd(9)} ${(t.model_used ?? "-").padEnd(7)} ${t.title}`);
577
+ }
578
+ });
579
+ });
580
+ // --- orc blocked ---------------------------------------------------------
581
+ const blocked = program
582
+ .command("blocked")
583
+ .description("Pending escalations awaiting operator resolution (§8.5)");
584
+ blocked
585
+ .command("list [run-id]", { isDefault: true })
586
+ .option("--json", "output JSON")
587
+ .action(async (runId, opts) => {
588
+ const q = runId ? `?run_id=${runId}` : "";
589
+ const { escalations } = await api(`/api/blocked${q}`);
590
+ output(!!opts.json, { escalations }, () => {
591
+ if (!escalations.length)
592
+ return console.log("no blocked tasks");
593
+ for (const e of escalations) {
594
+ console.log(`${e.id} ${e.rule_id} ${e.tool_name}: ${e.input_summary}`);
595
+ }
596
+ console.log("resolve: orc blocked resolve <id> --approve-once|--deny|--skip [--msg ...]");
597
+ });
598
+ });
599
+ blocked
600
+ .command("resolve <escalation-id>")
601
+ .description("Resolve a blocked escalation")
602
+ .option("--approve-once", "single-use exemption, re-queue")
603
+ .option("--deny", "deny & instruct, re-queue with guidance")
604
+ .option("--skip", "skip the task")
605
+ .option("--msg <text>", "guidance message")
606
+ .action(async (escId, opts) => {
607
+ const action = opts.approveOnce
608
+ ? "approve_once"
609
+ : opts.skip
610
+ ? "skip_task"
611
+ : "deny_instruct";
612
+ await api(`/api/escalations/${escId}/resolve`, {
613
+ method: "POST",
614
+ body: JSON.stringify({ action, message: opts.msg }),
615
+ });
616
+ console.log(`resolved ${escId} (${action})`);
617
+ });
618
+ // --- orc budget ----------------------------------------------------------
619
+ const budget = program.command("budget").description("Budget (§7)");
620
+ budget
621
+ .command("show <run-id>")
622
+ .option("--json", "output JSON")
623
+ .action(async (runId, opts) => {
624
+ const data = await api(`/api/runs/${runId}/status`);
625
+ output(!!opts.json, data, () => {
626
+ const r = data.run;
627
+ const pct = r.budget_usd > 0
628
+ ? Math.min(100, (r.budget_spent_usd / r.budget_usd) * 100)
629
+ : 0;
630
+ console.log(`budget ${pct.toFixed(0)}% used — $${r.budget_spent_usd.toFixed(4)} / $${r.budget_usd} (${r.budget_state})`);
631
+ });
632
+ });
633
+ budget
634
+ .command("set <run-id>")
635
+ .requiredOption("--usd <usd>", "new budget ceiling in USD")
636
+ .action(async (runId, opts) => {
637
+ await api(`/api/runs/${runId}/budget`, {
638
+ method: "POST",
639
+ body: JSON.stringify({ usd: Number(opts.usd) }),
640
+ });
641
+ console.log(`budget set to $${opts.usd}`);
642
+ });
643
+ // --- orc report ----------------------------------------------------------
644
+ program
645
+ .command("report [run-id]")
646
+ .description("Show the latest report or force generation (§11)")
647
+ .option("--now", "generate a fresh report now")
648
+ .option("--json", "output JSON")
649
+ .action(async (runId, opts) => {
650
+ const id = runId ?? (await latestRunId());
651
+ if (!id)
652
+ return console.error("no runs found");
653
+ if (opts.now) {
654
+ const { report } = await api(`/api/runs/${id}/reports`, { method: "POST" });
655
+ return output(!!opts.json, { report }, () => console.log(report.content_md));
656
+ }
657
+ const { reports } = await api(`/api/runs/${id}/reports`);
658
+ const latest = reports[0];
659
+ output(!!opts.json, { report: latest }, () => console.log(latest ? latest.content_md : "(no reports yet)"));
660
+ });
661
+ // --- orc audit tail ------------------------------------------------------
662
+ const audit = program.command("audit").description("Audit log");
663
+ audit
664
+ .command("tail <run-id>")
665
+ .option("--rule <id>", "filter by rule id")
666
+ .action(async (runId, opts) => {
667
+ const { events } = await api(`/api/audit/${runId}`);
668
+ for (const e of events) {
669
+ if (opts.rule && e.rule_id !== opts.rule)
670
+ continue;
671
+ console.log(JSON.stringify(e));
672
+ }
673
+ });
674
+ return program;
675
+ }
676
+ /** Resolves `<state-dir>/plugins.json` (default state dir: `<cwd>/.orc`). */
677
+ function pluginsFilePath(stateDir) {
678
+ return join(resolve(stateDir ?? join(process.cwd(), ".orc")), "plugins.json");
679
+ }
680
+ /** Reads the declarations file, tolerating a missing one. */
681
+ function readPluginsFile(path) {
682
+ if (!existsSync(path))
683
+ return { plugins: [] };
684
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
685
+ return { plugins: Array.isArray(parsed.plugins) ? parsed.plugins : [] };
686
+ }
687
+ /** Writes the declarations file, creating the state dir if needed. */
688
+ function writePluginsFile(path, data) {
689
+ mkdirSync(dirname(path), { recursive: true });
690
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
691
+ }
692
+ /**
693
+ * Reads a secret from stdin (spec 003 §R8 — never argv, which leaks to `ps`).
694
+ * Piped input is read whole; interactive input is prompted without echo.
695
+ */
696
+ async function readSecretFromStdin(promptText) {
697
+ if (!process.stdin.isTTY) {
698
+ const chunks = [];
699
+ for await (const chunk of process.stdin)
700
+ chunks.push(chunk);
701
+ return Buffer.concat(chunks).toString("utf8").trim();
702
+ }
703
+ process.stderr.write(promptText);
704
+ return await new Promise((resolvePromise) => {
705
+ const stdin = process.stdin;
706
+ stdin.setRawMode(true);
707
+ stdin.resume();
708
+ let value = "";
709
+ const onData = (chunk) => {
710
+ for (const ch of chunk.toString("utf8")) {
711
+ if (ch === "\n" || ch === "\r") {
712
+ stdin.setRawMode(false);
713
+ stdin.pause();
714
+ stdin.off("data", onData);
715
+ process.stderr.write("\n");
716
+ resolvePromise(value);
717
+ return;
718
+ }
719
+ if (ch === "\u0003")
720
+ process.exit(130); // ^C
721
+ if (ch === "\u007f" || ch === "\b")
722
+ value = value.slice(0, -1);
723
+ else
724
+ value += ch;
725
+ }
726
+ };
727
+ stdin.on("data", onData);
728
+ });
729
+ }
730
+ /** Resolves the most recent run id from the API. */
731
+ async function latestRunId() {
732
+ try {
733
+ const { runs } = await api("/api/runs");
734
+ return runs[0]?.id;
735
+ }
736
+ catch {
737
+ return undefined;
738
+ }
739
+ }
740
+ /** Fetches and renders a run's status. */
741
+ async function printStatus(runId, json) {
742
+ const data = await api(`/api/runs/${runId}/status`);
743
+ output(json, data, () => {
744
+ const { run, spent_usd, in_flight, task_counts } = data;
745
+ const pct = run.budget_usd > 0
746
+ ? Math.min(100, (spent_usd / run.budget_usd) * 100)
747
+ : 0;
748
+ console.log(`run ${runId} [${run.state}] budget ${pct.toFixed(0)}% used ` +
749
+ `($${spent_usd.toFixed(4)}/$${run.budget_usd}, ${run.budget_state}) · ${in_flight} in-flight`);
750
+ const counts = Object.entries(task_counts)
751
+ .map(([k, v]) => `${k}:${v}`)
752
+ .join(" ");
753
+ if (counts)
754
+ console.log(`tasks: ${counts}`);
755
+ });
756
+ }
757
+ //# sourceMappingURL=index.js.map