@h-rig/cli 0.0.6-alpha.2 → 0.0.6-alpha.20

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.
@@ -1,7 +1,5 @@
1
1
  // @bun
2
2
  // packages/cli/src/commands/run.ts
3
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
4
- import { resolve as resolve3 } from "path";
5
3
  import { createInterface as createInterface2 } from "readline/promises";
6
4
 
7
5
  // packages/cli/src/runner.ts
@@ -12,6 +10,9 @@ import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
12
10
  import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
13
11
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
14
12
  import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
13
+ function formatCommand(parts) {
14
+ return parts.map((part) => /[^a-zA-Z0-9_./:-]/.test(part) ? JSON.stringify(part) : part).join(" ");
15
+ }
15
16
  function takeFlag(args, flag) {
16
17
  const rest = [];
17
18
  let value = false;
@@ -54,9 +55,7 @@ Usage: ${usage}`);
54
55
  // packages/cli/src/commands/run.ts
55
56
  import {
56
57
  listAuthorityRuns,
57
- readAuthorityRun,
58
- readJsonlFile,
59
- resolveAuthorityRunDir
58
+ readAuthorityRun
60
59
  } from "@rig/runtime/control-plane/authority-files";
61
60
  import {
62
61
  cleanupRunState,
@@ -64,6 +63,7 @@ import {
64
63
  listOpenEpics,
65
64
  resolveDefaultEpic,
66
65
  runResume,
66
+ runRestart,
67
67
  runStatus,
68
68
  runStop,
69
69
  startRun,
@@ -84,7 +84,6 @@ function parsePositiveInt(value, option, fallback) {
84
84
  }
85
85
 
86
86
  // packages/cli/src/commands/_server-client.ts
87
- import { spawnSync } from "child_process";
88
87
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
89
88
  import { resolve as resolve2 } from "path";
90
89
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
@@ -172,7 +171,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
172
171
  }
173
172
 
174
173
  // packages/cli/src/commands/_server-client.ts
175
- var cachedGitHubBearerToken;
174
+ var scopedGitHubBearerTokens = new Map;
176
175
  function cleanToken(value) {
177
176
  const trimmed = value?.trim();
178
177
  return trimmed ? trimmed : null;
@@ -189,25 +188,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
189
188
  }
190
189
  }
191
190
  function readGitHubBearerTokenForRemote(projectRoot) {
192
- if (cachedGitHubBearerToken !== undefined)
193
- return cachedGitHubBearerToken;
191
+ const scopedKey = resolve2(projectRoot);
192
+ if (scopedGitHubBearerTokens.has(scopedKey))
193
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
194
194
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
195
- if (privateSession) {
196
- cachedGitHubBearerToken = privateSession;
197
- return cachedGitHubBearerToken;
198
- }
199
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
200
- if (envToken) {
201
- cachedGitHubBearerToken = envToken;
202
- return cachedGitHubBearerToken;
203
- }
204
- const result = spawnSync("gh", ["auth", "token"], {
205
- encoding: "utf8",
206
- timeout: 5000,
207
- stdio: ["ignore", "pipe", "ignore"]
208
- });
209
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
210
- return cachedGitHubBearerToken;
195
+ if (privateSession)
196
+ return privateSession;
197
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
211
198
  }
212
199
  async function ensureServerForCli(projectRoot) {
213
200
  try {
@@ -275,6 +262,14 @@ async function requestServerJson(context, pathname, init = {}) {
275
262
  }
276
263
  return payload;
277
264
  }
265
+ async function listRunsViaServer(context, options = {}) {
266
+ const url = new URL("http://rig.local/api/runs");
267
+ if (options.limit !== undefined)
268
+ url.searchParams.set("limit", String(options.limit));
269
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
270
+ const runs = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.runs) ? payload.runs : [];
271
+ return runs.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
272
+ }
278
273
  async function getRunDetailsViaServer(context, runId) {
279
274
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}`);
280
275
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
@@ -288,6 +283,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
288
283
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
289
284
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
290
285
  }
286
+ async function getRunTimelineViaServer(context, runId, options = {}) {
287
+ const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
288
+ if (options.limit !== undefined)
289
+ url.searchParams.set("limit", String(options.limit));
290
+ if (options.cursor)
291
+ url.searchParams.set("cursor", options.cursor);
292
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
293
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
294
+ }
291
295
  async function stopRunViaServer(context, runId) {
292
296
  const payload = await requestServerJson(context, "/api/runs/stop", {
293
297
  method: "POST",
@@ -305,9 +309,8 @@ async function steerRunViaServer(context, runId, message) {
305
309
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
306
310
  }
307
311
 
308
- // packages/cli/src/commands/_operator-view.ts
312
+ // packages/cli/src/commands/_operator-surface.ts
309
313
  import { createInterface } from "readline";
310
- var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
311
314
  var CANONICAL_STAGES = [
312
315
  "Connect",
313
316
  "GitHub/task sync",
@@ -322,18 +325,168 @@ var CANONICAL_STAGES = [
322
325
  "Merge",
323
326
  "Complete"
324
327
  ];
328
+ function logDetail(log) {
329
+ return typeof log.detail === "string" ? log.detail.trim() : "";
330
+ }
331
+ function parseProviderProtocolLog(title, detail) {
332
+ if (title.trim().toLowerCase() !== "agent output")
333
+ return null;
334
+ if (!detail.startsWith("{") || !detail.endsWith("}"))
335
+ return null;
336
+ try {
337
+ const record = JSON.parse(detail);
338
+ if (!record || typeof record !== "object" || Array.isArray(record))
339
+ return null;
340
+ const type = record.type;
341
+ return typeof type === "string" && [
342
+ "assistant",
343
+ "message_start",
344
+ "message_update",
345
+ "message_end",
346
+ "stream_event",
347
+ "tool_result",
348
+ "tool_execution_start",
349
+ "tool_execution_update",
350
+ "tool_execution_end",
351
+ "turn_start",
352
+ "turn_end"
353
+ ].includes(type) ? record : null;
354
+ } catch {
355
+ return null;
356
+ }
357
+ }
358
+ function renderProviderProtocolLog(record) {
359
+ const type = typeof record.type === "string" ? record.type : "";
360
+ if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
361
+ const toolName = String(record.toolName ?? record.name ?? "tool");
362
+ const status = type === "tool_execution_start" ? "started" : type === "tool_execution_end" ? record.isError === true || record.result && typeof record.result === "object" && !Array.isArray(record.result) && record.result.isError === true ? "failed" : "completed" : "running";
363
+ return `[Pi tool] ${toolName} ${status}`;
364
+ }
365
+ return null;
366
+ }
367
+ function entryId(entry, fallback) {
368
+ return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
369
+ }
325
370
  function renderOperatorSnapshot(snapshot) {
326
371
  const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
327
372
  const runId = String(run.runId ?? run.id ?? "run");
328
373
  const status = String(run.status ?? "unknown");
329
374
  const logs = snapshot.logs ?? [];
375
+ const latestByStage = new Map;
376
+ for (const log of logs) {
377
+ const title = String(log.title ?? "").toLowerCase();
378
+ const stageName = String(log.stage ?? "").toLowerCase();
379
+ const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
380
+ if (stage)
381
+ latestByStage.set(stage, log);
382
+ }
330
383
  const stageLines = CANONICAL_STAGES.flatMap((stage) => {
331
- const match = logs.find((log) => String(log.title ?? "").toLowerCase() === stage.toLowerCase() || String(log.stage ?? "").toLowerCase() === stage.toLowerCase());
332
- return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
384
+ const match = latestByStage.get(stage);
385
+ return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
333
386
  });
334
387
  return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
335
388
  `);
336
389
  }
390
+ function createPiRunStreamRenderer(output = process.stdout) {
391
+ let lastSnapshot = "";
392
+ const assistantTextById = new Map;
393
+ const seenTimeline = new Set;
394
+ const seenLogs = new Set;
395
+ const writeLine = (line) => output.write(`${line}
396
+ `);
397
+ return {
398
+ renderSnapshot(snapshot) {
399
+ const rendered = renderOperatorSnapshot(snapshot);
400
+ if (rendered && rendered !== lastSnapshot) {
401
+ writeLine(rendered);
402
+ lastSnapshot = rendered;
403
+ }
404
+ },
405
+ renderTimeline(entries) {
406
+ for (const [index, entry] of entries.entries()) {
407
+ const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
408
+ if (entry.type === "assistant_message" && typeof entry.text === "string") {
409
+ const text = entry.text;
410
+ const previousText = assistantTextById.get(id) ?? "";
411
+ if (!previousText && text.trim()) {
412
+ writeLine("[Pi assistant]");
413
+ }
414
+ if (text.startsWith(previousText)) {
415
+ const delta = text.slice(previousText.length);
416
+ if (delta)
417
+ output.write(delta);
418
+ } else if (text.trim() && text !== previousText) {
419
+ if (previousText)
420
+ writeLine(`
421
+ [Pi assistant]`);
422
+ output.write(text);
423
+ }
424
+ assistantTextById.set(id, text);
425
+ continue;
426
+ }
427
+ if (seenTimeline.has(id))
428
+ continue;
429
+ seenTimeline.add(id);
430
+ if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
431
+ writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
432
+ continue;
433
+ }
434
+ if (entry.type === "timeline_warning") {
435
+ writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
436
+ }
437
+ }
438
+ },
439
+ renderLogs(entries) {
440
+ for (const [index, entry] of entries.entries()) {
441
+ const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
442
+ if (seenLogs.has(id))
443
+ continue;
444
+ seenLogs.add(id);
445
+ const title = String(entry.title ?? "");
446
+ if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
447
+ continue;
448
+ const detail = logDetail(entry);
449
+ if (!detail)
450
+ continue;
451
+ const protocolRecord = parseProviderProtocolLog(title, detail);
452
+ if (protocolRecord) {
453
+ const protocolLine = renderProviderProtocolLog(protocolRecord);
454
+ if (protocolLine)
455
+ writeLine(protocolLine);
456
+ continue;
457
+ }
458
+ writeLine(`[${title || "Rig log"}] ${detail}`);
459
+ }
460
+ }
461
+ };
462
+ }
463
+ function createOperatorSurface(options = {}) {
464
+ const input = options.input ?? process.stdin;
465
+ const output = options.output ?? process.stdout;
466
+ const errorOutput = options.errorOutput ?? process.stderr;
467
+ const renderer = createPiRunStreamRenderer(output);
468
+ const writeLine = (line) => output.write(`${line}
469
+ `);
470
+ return {
471
+ mode: "pi-compatible-text",
472
+ ...renderer,
473
+ info: writeLine,
474
+ error: (message) => errorOutput.write(`${message}
475
+ `),
476
+ attachCommandInput(handler) {
477
+ if (options.interactive === false || !input.isTTY)
478
+ return null;
479
+ const rl = createInterface({ input, output: process.stdout, terminal: false });
480
+ rl.on("line", (line) => {
481
+ Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
482
+ });
483
+ return { close: () => rl.close() };
484
+ }
485
+ };
486
+ }
487
+
488
+ // packages/cli/src/commands/_operator-view.ts
489
+ var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
337
490
  function runStatusFromPayload(payload) {
338
491
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
339
492
  return String(run.status ?? "unknown").toLowerCase();
@@ -355,11 +508,22 @@ async function applyOperatorCommand(context, input, deps = {}) {
355
508
  await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
356
509
  return { action: "continue", message: "Steering message queued." };
357
510
  }
358
- async function readOperatorSnapshot(context, runId) {
511
+ async function readOperatorSnapshot(context, runId, options = {}) {
359
512
  const run = await getRunDetailsViaServer(context, runId);
360
513
  const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
361
- const entries = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
362
- return { run, logs: entries, rendered: renderOperatorSnapshot({ run, logs: entries }) };
514
+ const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
515
+ entries: [{
516
+ id: `timeline-unavailable:${runId}`,
517
+ type: "timeline_warning",
518
+ detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
519
+ createdAt: new Date().toISOString()
520
+ }],
521
+ nextCursor: options.timelineCursor ?? null
522
+ }));
523
+ const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
524
+ const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
525
+ const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
526
+ return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
363
527
  }
364
528
  async function attachRunOperatorView(context, input) {
365
529
  let steered = false;
@@ -367,45 +531,230 @@ async function attachRunOperatorView(context, input) {
367
531
  await steerRunViaServer(context, input.runId, input.message.trim());
368
532
  steered = true;
369
533
  }
534
+ const surface = createOperatorSurface({ interactive: input.interactive !== false });
370
535
  let snapshot = await readOperatorSnapshot(context, input.runId);
371
536
  if (context.outputMode === "text") {
372
- console.log(snapshot.rendered);
537
+ surface.renderSnapshot(snapshot);
538
+ surface.renderTimeline(snapshot.timeline);
539
+ surface.renderLogs(snapshot.logs);
373
540
  if (steered)
374
- console.log("Steering message queued.");
541
+ surface.info("Steering message queued.");
375
542
  }
376
543
  let detached = false;
377
- let rl = null;
544
+ let commandInput = null;
378
545
  if (input.follow && !input.once && context.outputMode === "text") {
379
546
  if (input.interactive !== false && process.stdin.isTTY) {
380
- console.log("Controls: /user <message>, /stop, /detach");
381
- rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
382
- rl.on("line", (line) => {
383
- applyOperatorCommand(context, { runId: input.runId, line }).then((result) => {
384
- if (result.message)
385
- console.log(result.message);
386
- if (result.action === "detach" || result.action === "stopped") {
387
- detached = true;
388
- rl?.close();
389
- }
390
- }).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
547
+ surface.info("Controls: /user <message>, /stop, /detach");
548
+ commandInput = surface.attachCommandInput(async (line) => {
549
+ const result = await applyOperatorCommand(context, { runId: input.runId, line });
550
+ if (result.message)
551
+ surface.info(result.message);
552
+ if (result.action === "detach" || result.action === "stopped") {
553
+ detached = true;
554
+ commandInput?.close();
555
+ }
391
556
  });
392
557
  }
393
- let lastRendered = snapshot.rendered;
394
558
  const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
559
+ let timelineCursor = snapshot.timelineCursor;
395
560
  while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
396
561
  await Bun.sleep(pollMs);
397
- snapshot = await readOperatorSnapshot(context, input.runId);
398
- if (snapshot.rendered !== lastRendered) {
399
- console.log(snapshot.rendered);
400
- lastRendered = snapshot.rendered;
401
- }
562
+ snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
563
+ timelineCursor = snapshot.timelineCursor;
564
+ surface.renderSnapshot(snapshot);
565
+ surface.renderTimeline(snapshot.timeline);
566
+ surface.renderLogs(snapshot.logs);
402
567
  }
403
- rl?.close();
568
+ commandInput?.close();
404
569
  }
405
570
  return { ...snapshot, steered, detached };
406
571
  }
407
572
 
573
+ // packages/cli/src/commands/_cli-format.ts
574
+ import pc from "picocolors";
575
+ function stringField(record, key, fallback = "") {
576
+ const value = record[key];
577
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
578
+ }
579
+ function truncate(value, width) {
580
+ if (value.length <= width)
581
+ return value;
582
+ if (width <= 1)
583
+ return "\u2026";
584
+ return `${value.slice(0, width - 1)}\u2026`;
585
+ }
586
+ function pad(value, width) {
587
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
588
+ }
589
+ function statusColor(status) {
590
+ const normalized = status.toLowerCase();
591
+ if (["completed", "merged", "closed", "done", "accepted"].includes(normalized))
592
+ return pc.green;
593
+ if (["failed", "needs_attention", "needs-attention", "blocked"].includes(normalized))
594
+ return pc.red;
595
+ if (["running", "reviewing", "validating", "in_progress", "in-progress"].includes(normalized))
596
+ return pc.cyan;
597
+ if (["ready", "open", "queued", "created", "preparing"].includes(normalized))
598
+ return pc.yellow;
599
+ return pc.dim;
600
+ }
601
+ function formatRunList(runs, options = {}) {
602
+ if (runs.length === 0) {
603
+ return pc.dim(options.source === "server" ? "No runs recorded on the selected Rig server." : "No runs recorded in .rig/runs.");
604
+ }
605
+ const rows = runs.map((run) => {
606
+ const runId = stringField(run, "runId", stringField(run, "id", "(unknown-run)"));
607
+ const status = stringField(run, "status", "unknown");
608
+ const taskId = stringField(run, "taskId", "");
609
+ const title = stringField(run, "title", taskId || "(untitled)");
610
+ const runtime = stringField(run, "runtimeAdapter", "");
611
+ return { runId, status, title, runtime };
612
+ });
613
+ const idWidth = Math.min(36, Math.max(6, ...rows.map((row) => row.runId.length)));
614
+ const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
615
+ const header = `${pc.bold(pad("RUN", idWidth))} ${pc.bold(pad("STATUS", statusWidth))} ${pc.bold("TITLE")}`;
616
+ const body = rows.map((row) => [
617
+ pc.bold(pad(truncate(row.runId, idWidth), idWidth)),
618
+ statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
619
+ `${row.title}${row.runtime ? pc.dim(` ${row.runtime}`) : ""}`
620
+ ].join(" "));
621
+ return [pc.bold(options.source === "server" ? "Rig runs (server)" : "Rig runs"), header, ...body].join(`
622
+ `);
623
+ }
624
+
625
+ // packages/cli/src/commands/_pi-session.ts
626
+ import { spawn } from "child_process";
627
+
628
+ // packages/cli/src/commands/_pi-install.ts
629
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
630
+ import { resolve as resolve3 } from "path";
631
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
632
+ function resolvePiRigPackageSource(projectRoot, exists = existsSync3) {
633
+ const localPackage = resolve3(projectRoot, "packages", "pi-rig");
634
+ if (exists(resolve3(localPackage, "package.json")))
635
+ return localPackage;
636
+ return `npm:${PI_RIG_PACKAGE_NAME}`;
637
+ }
638
+
639
+ // packages/cli/src/commands/_pi-session.ts
640
+ function buildPiRigSessionEnv(input) {
641
+ return {
642
+ RIG_PROJECT_ROOT: input.projectRoot,
643
+ PROJECT_RIG_ROOT: input.projectRoot,
644
+ RIG_RUN_ID: input.runId,
645
+ RIG_SERVER_RUN_ID: input.runId,
646
+ RIG_RUNTIME_ADAPTER: "pi",
647
+ RIG_SERVER_URL: input.serverUrl,
648
+ RIG_SERVER_BASE_URL: input.serverUrl,
649
+ RIG_STEERING_POLL_MS: process.env.RIG_STEERING_POLL_MS?.trim() || "1000",
650
+ RIG_PI_OPERATOR_SESSION: "1",
651
+ ...input.taskId ? { RIG_TASK_ID: input.taskId } : {},
652
+ ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {}
653
+ };
654
+ }
655
+ function shellBinary(name) {
656
+ const explicit = process.env.RIG_PI_BINARY?.trim();
657
+ if (explicit)
658
+ return explicit;
659
+ return Bun.which(name) || name;
660
+ }
661
+ function buildPiRigSessionCommand(input) {
662
+ const configuredExtension = input.extensionSource ?? process.env.RIG_PI_RIG_EXTENSION_SOURCE?.trim();
663
+ const extensionSource = configuredExtension && configuredExtension.length > 0 ? configuredExtension : resolvePiRigPackageSource(input.projectRoot);
664
+ const initialCommand = `/rig attach ${input.runId}`;
665
+ return [
666
+ shellBinary("pi"),
667
+ "--no-extensions",
668
+ "--extension",
669
+ extensionSource,
670
+ initialCommand
671
+ ];
672
+ }
673
+ async function launchPiRigSession(context, input) {
674
+ if (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY) {
675
+ return { launched: false, exitCode: null, command: [] };
676
+ }
677
+ if (process.env.RIG_DISABLE_PI_LAUNCH === "1") {
678
+ return { launched: false, exitCode: null, command: [] };
679
+ }
680
+ const server = await ensureServerForCli(context.projectRoot);
681
+ const command = buildPiRigSessionCommand({ ...input, projectRoot: context.projectRoot });
682
+ const env = {
683
+ ...process.env,
684
+ ...buildPiRigSessionEnv({
685
+ projectRoot: context.projectRoot,
686
+ runId: input.runId,
687
+ taskId: input.taskId,
688
+ serverUrl: server.baseUrl,
689
+ authToken: server.authToken
690
+ })
691
+ };
692
+ process.stdout.write(`Launching Pi for Rig run ${input.runId}\u2026
693
+ `);
694
+ process.stdout.write(`Pi command: ${formatCommand(command)}
695
+ `);
696
+ const launchedAt = Date.now();
697
+ const child = spawn(command[0], command.slice(1), {
698
+ cwd: context.projectRoot,
699
+ env,
700
+ stdio: "inherit"
701
+ });
702
+ const launchError = await new Promise((resolve4) => {
703
+ child.once("error", (error) => {
704
+ resolve4({ error: error.message });
705
+ });
706
+ child.once("close", (code) => resolve4({ code }));
707
+ });
708
+ if ("error" in launchError) {
709
+ process.stderr.write(`Failed to launch Pi; falling back to Rig attach view: ${launchError.error}
710
+ `);
711
+ return { launched: false, exitCode: null, command, error: launchError.error };
712
+ }
713
+ const exitCode = launchError.code;
714
+ const elapsedMs = Date.now() - launchedAt;
715
+ if (typeof exitCode === "number" && exitCode !== 0 && elapsedMs < 5000) {
716
+ const error = `Pi exited during startup with code ${exitCode}.`;
717
+ process.stderr.write(`${error} Falling back to Rig attach view.
718
+ `);
719
+ return { launched: false, exitCode, command, error };
720
+ }
721
+ return { launched: true, exitCode, command };
722
+ }
723
+
408
724
  // packages/cli/src/commands/run.ts
725
+ function normalizeRemoteRunDetails(payload) {
726
+ const run = payload.run;
727
+ if (!run || typeof run !== "object" || Array.isArray(run))
728
+ return null;
729
+ return {
730
+ ...run,
731
+ ...Array.isArray(payload.timeline) ? { timeline: payload.timeline } : {},
732
+ ...Array.isArray(payload.approvals) ? { approvals: payload.approvals } : {},
733
+ ...Array.isArray(payload.userInputs) ? { userInputs: payload.userInputs } : {}
734
+ };
735
+ }
736
+ var REMOTE_TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged"]);
737
+ function isRemoteConnectionSelected(projectRoot) {
738
+ return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
739
+ }
740
+ async function listRunsForSelectedConnection(context, options = {}) {
741
+ if (isRemoteConnectionSelected(context.projectRoot)) {
742
+ return { runs: await listRunsViaServer(context, options), source: "server" };
743
+ }
744
+ return { runs: listAuthorityRuns(context.projectRoot), source: "local" };
745
+ }
746
+ function runStringField(run, key, fallback = "") {
747
+ const value = run[key];
748
+ return typeof value === "string" && value.trim() ? value : fallback;
749
+ }
750
+ function runDisplayTitle(run) {
751
+ return runStringField(run, "title", runStringField(run, "taskId", "(untitled)"));
752
+ }
753
+ function buildServerRunStatus(runs) {
754
+ const activeRuns = runs.filter((run) => !REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
755
+ const recentRuns = runs.filter((run) => REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
756
+ return { activeRuns, recentRuns, runs };
757
+ }
409
758
  function shouldPromptForEpicSelection(context, command, promptEpic, noEpicPrompt) {
410
759
  if (noEpicPrompt) {
411
760
  return false;
@@ -471,17 +820,11 @@ async function executeRun(context, args) {
471
820
  switch (command) {
472
821
  case "list": {
473
822
  requireNoExtraArgs(rest, "bun run rig run list");
474
- const runs = listAuthorityRuns(context.projectRoot);
823
+ const { runs, source } = await listRunsForSelectedConnection(context, { limit: 100 });
475
824
  if (context.outputMode === "text") {
476
- if (runs.length === 0) {
477
- console.log("No runs recorded in .rig/runs.");
478
- } else {
479
- for (const run of runs) {
480
- console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.title}`);
481
- }
482
- }
825
+ console.log(formatRunList(runs, { source }));
483
826
  }
484
- return { ok: true, group: "run", command, details: { runs } };
827
+ return { ok: true, group: "run", command, details: { runs, source } };
485
828
  }
486
829
  case "delete": {
487
830
  let pending = rest;
@@ -544,7 +887,7 @@ async function executeRun(context, args) {
544
887
  if (!run.value) {
545
888
  throw new CliError2("run show requires --run <id>.");
546
889
  }
547
- const record = readAuthorityRun(context.projectRoot, run.value);
890
+ const record = readAuthorityRun(context.projectRoot, run.value) ?? normalizeRemoteRunDetails(await getRunDetailsViaServer(context, run.value).catch(() => ({})));
548
891
  if (!record) {
549
892
  throw new CliError2(`Run not found: ${run.value}`, 2);
550
893
  }
@@ -563,34 +906,24 @@ async function executeRun(context, args) {
563
906
  if (!run.value) {
564
907
  throw new CliError2("run timeline requires --run <id>.");
565
908
  }
566
- const timelinePath = resolve3(resolveAuthorityRunDir(context.projectRoot, run.value), "timeline.jsonl");
567
- const printEvents = () => {
568
- const events2 = readJsonlFile(timelinePath);
569
- if (context.outputMode === "text") {
570
- for (const event of events2) {
571
- console.log(JSON.stringify(event));
572
- }
573
- }
574
- return events2;
575
- };
576
- const events = printEvents();
909
+ const renderer = createPiRunStreamRenderer();
910
+ let cursor = null;
911
+ const page = await getRunTimelineViaServer(context, run.value, { limit: 500 });
912
+ const events = Array.isArray(page.entries) ? page.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
913
+ cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
914
+ if (context.outputMode === "text") {
915
+ renderer.renderTimeline(events);
916
+ }
577
917
  if (follow.value && context.outputMode === "text") {
578
- let lastLength = existsSync3(timelinePath) ? readFileSync3(timelinePath, "utf8").length : 0;
579
918
  while (true) {
580
919
  await Bun.sleep(1000);
581
- if (!existsSync3(timelinePath))
582
- continue;
583
- const next = readFileSync3(timelinePath, "utf8");
584
- if (next.length <= lastLength)
585
- continue;
586
- const delta = next.slice(lastLength);
587
- lastLength = next.length;
588
- for (const line of delta.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) {
589
- console.log(line);
590
- }
920
+ const nextPage = await getRunTimelineViaServer(context, run.value, { limit: 500, ...cursor ? { cursor } : {} });
921
+ const nextEvents = Array.isArray(nextPage.entries) ? nextPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
922
+ cursor = typeof nextPage.nextCursor === "string" ? nextPage.nextCursor : cursor;
923
+ renderer.renderTimeline(nextEvents);
591
924
  }
592
925
  }
593
- return { ok: true, group: "run", command, details: { runId: run.value, events } };
926
+ return { ok: true, group: "run", command, details: { runId: run.value, events, cursor } };
594
927
  }
595
928
  case "attach": {
596
929
  let pending = rest;
@@ -611,14 +944,26 @@ async function executeRun(context, args) {
611
944
  if (!runId) {
612
945
  throw new CliError2("run attach requires a run id.", 2);
613
946
  }
947
+ let steered = false;
948
+ const shouldTryPiAttach = context.outputMode === "text" && follow.value && !once.value && Boolean(process.stdin.isTTY && process.stdout.isTTY) && process.env.RIG_DISABLE_PI_LAUNCH !== "1";
949
+ if (shouldTryPiAttach && messageOption.value?.trim()) {
950
+ await steerRunViaServer(context, runId, messageOption.value.trim());
951
+ steered = true;
952
+ }
953
+ if (shouldTryPiAttach) {
954
+ const piSession = await launchPiRigSession(context, { runId });
955
+ if (piSession.launched) {
956
+ return { ok: true, group: "run", command, details: { runId, steered, mode: "pi", ...piSession } };
957
+ }
958
+ }
614
959
  const attached = await attachRunOperatorView(context, {
615
960
  runId,
616
- message: messageOption.value ?? null,
961
+ message: shouldTryPiAttach ? null : messageOption.value ?? null,
617
962
  once: once.value,
618
963
  follow: follow.value,
619
964
  pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)
620
965
  });
621
- return { ok: true, group: "run", command, details: attached };
966
+ return { ok: true, group: "run", command, details: { ...attached, steered: attached.steered || steered } };
622
967
  }
623
968
  case "status": {
624
969
  requireNoExtraArgs(rest, "bun run rig run status");
@@ -628,17 +973,19 @@ async function executeRun(context, args) {
628
973
  }
629
974
  return { ok: true, group: "run", command };
630
975
  }
631
- const summary = runStatus(context.projectRoot, runtimeContext);
976
+ const summary = isRemoteConnectionSelected(context.projectRoot) ? buildServerRunStatus(await listRunsViaServer(context, { limit: 100 })) : runStatus(context.projectRoot, runtimeContext);
977
+ const activeRuns = Array.isArray(summary.activeRuns) ? summary.activeRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
978
+ const recentRuns = Array.isArray(summary.recentRuns) ? summary.recentRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
632
979
  if (context.outputMode === "text") {
633
- console.log(`Active runs: ${summary.activeRuns.length}`);
634
- for (const run of summary.activeRuns) {
635
- console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.taskId ?? run.title}`);
980
+ console.log(`Active runs: ${activeRuns.length}`);
981
+ for (const run of activeRuns) {
982
+ console.log(`- ${runStringField(run, "runId", "(unknown-run)")} \xB7 ${runStringField(run, "status", "unknown")} \xB7 ${runStringField(run, "taskId", runDisplayTitle(run))}`);
636
983
  }
637
- if (summary.recentRuns.length > 0) {
984
+ if (recentRuns.length > 0) {
638
985
  console.log("");
639
986
  console.log("Recent runs:");
640
- for (const run of summary.recentRuns) {
641
- console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.taskId ?? run.title}`);
987
+ for (const run of recentRuns) {
988
+ console.log(`- ${runStringField(run, "runId", "(unknown-run)")} \xB7 ${runStringField(run, "status", "unknown")} \xB7 ${runStringField(run, "taskId", runDisplayTitle(run))}`);
642
989
  }
643
990
  }
644
991
  }
@@ -725,6 +1072,20 @@ async function executeRun(context, args) {
725
1072
  }
726
1073
  return { ok: true, group: "run", command, details: resumed };
727
1074
  }
1075
+ case "restart": {
1076
+ requireNoExtraArgs(rest, "bun run rig run restart");
1077
+ if (context.dryRun) {
1078
+ if (context.outputMode === "text") {
1079
+ console.log("[dry-run] rig run restart");
1080
+ }
1081
+ return { ok: true, group: "run", command };
1082
+ }
1083
+ const restarted = await runRestart(context.projectRoot, runtimeContext);
1084
+ if (context.outputMode === "text") {
1085
+ console.log(`Restarted run: ${restarted.runId}`);
1086
+ }
1087
+ return { ok: true, group: "run", command, details: restarted };
1088
+ }
728
1089
  case "stop": {
729
1090
  const runOption = takeOption(rest, "--run");
730
1091
  const positionalRunId = runOption.rest.length > 0 ? runOption.rest[0] : undefined;