@h-rig/cli 0.0.6-alpha.4 → 0.0.6-alpha.40

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.
Files changed (47) hide show
  1. package/dist/bin/rig.js +3947 -1190
  2. package/dist/src/commands/_cli-format.js +369 -0
  3. package/dist/src/commands/_connection-state.js +1 -3
  4. package/dist/src/commands/_doctor-checks.js +13 -27
  5. package/dist/src/commands/_help-catalog.js +445 -0
  6. package/dist/src/commands/_operator-surface.js +220 -0
  7. package/dist/src/commands/_operator-view.js +942 -56
  8. package/dist/src/commands/_parsers.js +0 -2
  9. package/dist/src/commands/_pi-frontend.js +906 -0
  10. package/dist/src/commands/_pi-install.js +4 -3
  11. package/dist/src/commands/_pi-worker-bridge-extension.js +826 -0
  12. package/dist/src/commands/_policy.js +0 -2
  13. package/dist/src/commands/_preflight.js +32 -109
  14. package/dist/src/commands/_run-driver-helpers.js +21 -2
  15. package/dist/src/commands/_server-client.js +152 -31
  16. package/dist/src/commands/_snapshot-upload.js +8 -23
  17. package/dist/src/commands/_task-picker.js +44 -16
  18. package/dist/src/commands/agent.js +8 -9
  19. package/dist/src/commands/browser.js +4 -6
  20. package/dist/src/commands/connect.js +132 -25
  21. package/dist/src/commands/dist.js +4 -6
  22. package/dist/src/commands/doctor.js +13 -27
  23. package/dist/src/commands/github.js +10 -25
  24. package/dist/src/commands/inbox.js +351 -31
  25. package/dist/src/commands/init.js +298 -71
  26. package/dist/src/commands/inspect.js +237 -23
  27. package/dist/src/commands/inspector.js +2 -4
  28. package/dist/src/commands/pi.js +168 -0
  29. package/dist/src/commands/plugin.js +81 -22
  30. package/dist/src/commands/profile-and-review.js +8 -10
  31. package/dist/src/commands/queue.js +2 -3
  32. package/dist/src/commands/remote.js +18 -20
  33. package/dist/src/commands/repo-git-harness.js +6 -8
  34. package/dist/src/commands/run.js +1240 -123
  35. package/dist/src/commands/server.js +222 -33
  36. package/dist/src/commands/setup.js +17 -37
  37. package/dist/src/commands/task-report-bug.js +5 -7
  38. package/dist/src/commands/task-run-driver.js +649 -71
  39. package/dist/src/commands/task.js +1679 -252
  40. package/dist/src/commands/test.js +3 -5
  41. package/dist/src/commands/workspace.js +4 -6
  42. package/dist/src/commands.js +3927 -1164
  43. package/dist/src/index.js +3940 -1186
  44. package/dist/src/launcher.js +5 -3
  45. package/dist/src/report-bug.js +3 -3
  46. package/dist/src/runner.js +5 -19
  47. package/package.json +6 -4
@@ -1,15 +1,11 @@
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
8
6
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
9
7
  import { CliError } from "@rig/runtime/control-plane/errors";
10
8
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
11
- import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
12
- import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
13
9
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
14
10
  import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
15
11
  function takeFlag(args, flag) {
@@ -54,9 +50,7 @@ Usage: ${usage}`);
54
50
  // packages/cli/src/commands/run.ts
55
51
  import {
56
52
  listAuthorityRuns,
57
- readAuthorityRun,
58
- readJsonlFile,
59
- resolveAuthorityRunDir
53
+ readAuthorityRun
60
54
  } from "@rig/runtime/control-plane/authority-files";
61
55
  import {
62
56
  cleanupRunState,
@@ -70,7 +64,7 @@ import {
70
64
  startRun,
71
65
  defaultStartRunOptions
72
66
  } from "@rig/runtime/control-plane/native/run-ops";
73
- import { loadRuntimeContextFromEnv as loadRuntimeContextFromEnv2 } from "@rig/runtime/control-plane/runtime/context";
67
+ import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
74
68
 
75
69
  // packages/cli/src/commands/_parsers.ts
76
70
  function parsePositiveInt(value, option, fallback) {
@@ -85,7 +79,6 @@ function parsePositiveInt(value, option, fallback) {
85
79
  }
86
80
 
87
81
  // packages/cli/src/commands/_server-client.ts
88
- import { spawnSync } from "child_process";
89
82
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
90
83
  import { resolve as resolve2 } from "path";
91
84
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
@@ -167,13 +160,13 @@ function resolveSelectedConnection(projectRoot, options = {}) {
167
160
  const global = readGlobalConnections(options);
168
161
  const connection = global.connections[repo.selected];
169
162
  if (!connection) {
170
- throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
163
+ throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
171
164
  }
172
165
  return { alias: repo.selected, connection };
173
166
  }
174
167
 
175
168
  // packages/cli/src/commands/_server-client.ts
176
- var cachedGitHubBearerToken;
169
+ var scopedGitHubBearerTokens = new Map;
177
170
  function cleanToken(value) {
178
171
  const trimmed = value?.trim();
179
172
  return trimmed ? trimmed : null;
@@ -190,25 +183,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
190
183
  }
191
184
  }
192
185
  function readGitHubBearerTokenForRemote(projectRoot) {
193
- if (cachedGitHubBearerToken !== undefined)
194
- return cachedGitHubBearerToken;
186
+ const scopedKey = resolve2(projectRoot);
187
+ if (scopedGitHubBearerTokens.has(scopedKey))
188
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
195
189
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
196
- if (privateSession) {
197
- cachedGitHubBearerToken = privateSession;
198
- return cachedGitHubBearerToken;
199
- }
200
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
201
- if (envToken) {
202
- cachedGitHubBearerToken = envToken;
203
- return cachedGitHubBearerToken;
204
- }
205
- const result = spawnSync("gh", ["auth", "token"], {
206
- encoding: "utf8",
207
- timeout: 5000,
208
- stdio: ["ignore", "pipe", "ignore"]
209
- });
210
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
211
- return cachedGitHubBearerToken;
190
+ if (privateSession)
191
+ return privateSession;
192
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
212
193
  }
213
194
  async function ensureServerForCli(projectRoot) {
214
195
  try {
@@ -276,6 +257,14 @@ async function requestServerJson(context, pathname, init = {}) {
276
257
  }
277
258
  return payload;
278
259
  }
260
+ async function listRunsViaServer(context, options = {}) {
261
+ const url = new URL("http://rig.local/api/runs");
262
+ if (options.limit !== undefined)
263
+ url.searchParams.set("limit", String(options.limit));
264
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
265
+ const runs = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.runs) ? payload.runs : [];
266
+ return runs.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
267
+ }
279
268
  async function getRunDetailsViaServer(context, runId) {
280
269
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}`);
281
270
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
@@ -289,6 +278,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
289
278
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
290
279
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
291
280
  }
281
+ async function getRunTimelineViaServer(context, runId, options = {}) {
282
+ const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
283
+ if (options.limit !== undefined)
284
+ url.searchParams.set("limit", String(options.limit));
285
+ if (options.cursor)
286
+ url.searchParams.set("cursor", options.cursor);
287
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
288
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
289
+ }
292
290
  async function stopRunViaServer(context, runId) {
293
291
  const payload = await requestServerJson(context, "/api/runs/stop", {
294
292
  method: "POST",
@@ -305,10 +303,69 @@ async function steerRunViaServer(context, runId, message) {
305
303
  });
306
304
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
307
305
  }
306
+ async function getRunPiSessionViaServer(context, runId) {
307
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi`);
308
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
309
+ }
310
+ async function getRunPiMessagesViaServer(context, runId) {
311
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/messages`);
312
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { messages: [] };
313
+ }
314
+ async function getRunPiStatusViaServer(context, runId) {
315
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/status`);
316
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
317
+ }
318
+ async function getRunPiCommandsViaServer(context, runId) {
319
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
320
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
321
+ }
322
+ async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
323
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
324
+ method: "POST",
325
+ headers: { "content-type": "application/json" },
326
+ body: JSON.stringify({ text, streamingBehavior })
327
+ });
328
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
329
+ }
330
+ async function sendRunPiShellViaServer(context, runId, text) {
331
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/shell`, {
332
+ method: "POST",
333
+ headers: { "content-type": "application/json" },
334
+ body: JSON.stringify({ text })
335
+ });
336
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
337
+ }
338
+ async function runRunPiCommandViaServer(context, runId, text) {
339
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands/run`, {
340
+ method: "POST",
341
+ headers: { "content-type": "application/json" },
342
+ body: JSON.stringify({ text })
343
+ });
344
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { type: "done" };
345
+ }
346
+ async function respondRunPiExtensionUiViaServer(context, runId, requestId, valueOrCancel) {
347
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/extension-ui/respond`, {
348
+ method: "POST",
349
+ headers: { "content-type": "application/json" },
350
+ body: JSON.stringify({ requestId, ...valueOrCancel })
351
+ });
352
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
353
+ }
354
+ async function abortRunPiViaServer(context, runId) {
355
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/abort`, { method: "POST" });
356
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { aborted: true };
357
+ }
358
+ async function buildRunPiEventsWebSocketUrl(context, runId) {
359
+ const server = await ensureServerForCli(context.projectRoot);
360
+ const url = new URL(`${server.baseUrl.replace(/\/+$/, "")}/api/runs/${encodeURIComponent(runId)}/pi/events`);
361
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
362
+ if (server.authToken)
363
+ url.searchParams.set("token", server.authToken);
364
+ return url.toString();
365
+ }
308
366
 
309
- // packages/cli/src/commands/_operator-view.ts
367
+ // packages/cli/src/commands/_operator-surface.ts
310
368
  import { createInterface } from "readline";
311
- var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
312
369
  var CANONICAL_STAGES = [
313
370
  "Connect",
314
371
  "GitHub/task sync",
@@ -323,18 +380,834 @@ var CANONICAL_STAGES = [
323
380
  "Merge",
324
381
  "Complete"
325
382
  ];
383
+ function logDetail(log) {
384
+ return typeof log.detail === "string" ? log.detail.trim() : "";
385
+ }
386
+ function parseProviderProtocolLog(title, detail) {
387
+ if (title.trim().toLowerCase() !== "agent output")
388
+ return null;
389
+ if (!detail.startsWith("{") || !detail.endsWith("}"))
390
+ return null;
391
+ try {
392
+ const record = JSON.parse(detail);
393
+ if (!record || typeof record !== "object" || Array.isArray(record))
394
+ return null;
395
+ const type = record.type;
396
+ return typeof type === "string" && [
397
+ "assistant",
398
+ "message_start",
399
+ "message_update",
400
+ "message_end",
401
+ "stream_event",
402
+ "tool_result",
403
+ "tool_execution_start",
404
+ "tool_execution_update",
405
+ "tool_execution_end",
406
+ "turn_start",
407
+ "turn_end"
408
+ ].includes(type) ? record : null;
409
+ } catch {
410
+ return null;
411
+ }
412
+ }
413
+ function renderProviderProtocolLog(record) {
414
+ const type = typeof record.type === "string" ? record.type : "";
415
+ if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
416
+ const toolName = String(record.toolName ?? record.name ?? "tool");
417
+ 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";
418
+ return `[Pi tool] ${toolName} ${status}`;
419
+ }
420
+ return null;
421
+ }
422
+ function entryId(entry, fallback) {
423
+ return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
424
+ }
326
425
  function renderOperatorSnapshot(snapshot) {
327
426
  const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
328
427
  const runId = String(run.runId ?? run.id ?? "run");
329
428
  const status = String(run.status ?? "unknown");
330
429
  const logs = snapshot.logs ?? [];
430
+ const latestByStage = new Map;
431
+ for (const log of logs) {
432
+ const title = String(log.title ?? "").toLowerCase();
433
+ const stageName = String(log.stage ?? "").toLowerCase();
434
+ const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
435
+ if (stage)
436
+ latestByStage.set(stage, log);
437
+ }
331
438
  const stageLines = CANONICAL_STAGES.flatMap((stage) => {
332
- const match = logs.find((log) => String(log.title ?? "").toLowerCase() === stage.toLowerCase() || String(log.stage ?? "").toLowerCase() === stage.toLowerCase());
333
- return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
439
+ const match = latestByStage.get(stage);
440
+ return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
334
441
  });
335
442
  return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
336
443
  `);
337
444
  }
445
+ function createPiRunStreamRenderer(output = process.stdout) {
446
+ let lastSnapshot = "";
447
+ const assistantTextById = new Map;
448
+ const seenTimeline = new Set;
449
+ const seenLogs = new Set;
450
+ const writeLine = (line) => output.write(`${line}
451
+ `);
452
+ return {
453
+ renderSnapshot(snapshot) {
454
+ const rendered = renderOperatorSnapshot(snapshot);
455
+ if (rendered && rendered !== lastSnapshot) {
456
+ writeLine(rendered);
457
+ lastSnapshot = rendered;
458
+ }
459
+ },
460
+ renderTimeline(entries) {
461
+ for (const [index, entry] of entries.entries()) {
462
+ const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
463
+ if (entry.type === "assistant_message" && typeof entry.text === "string") {
464
+ const text = entry.text;
465
+ const previousText = assistantTextById.get(id) ?? "";
466
+ if (!previousText && text.trim()) {
467
+ writeLine("[Pi assistant]");
468
+ }
469
+ if (text.startsWith(previousText)) {
470
+ const delta = text.slice(previousText.length);
471
+ if (delta)
472
+ output.write(delta);
473
+ } else if (text.trim() && text !== previousText) {
474
+ if (previousText)
475
+ writeLine(`
476
+ [Pi assistant]`);
477
+ output.write(text);
478
+ }
479
+ assistantTextById.set(id, text);
480
+ continue;
481
+ }
482
+ if (seenTimeline.has(id))
483
+ continue;
484
+ seenTimeline.add(id);
485
+ if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
486
+ writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
487
+ continue;
488
+ }
489
+ if (entry.type === "timeline_warning") {
490
+ writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
491
+ continue;
492
+ }
493
+ if (entry.type === "action") {
494
+ const text = String(entry.detail ?? entry.message ?? entry.title ?? "").trim();
495
+ if (text)
496
+ writeLine(`[Rig action] ${text}`);
497
+ continue;
498
+ }
499
+ if (entry.type === "user_message") {
500
+ const text = String(entry.text ?? entry.message ?? entry.detail ?? "").trim();
501
+ if (text)
502
+ writeLine(`[Operator] ${text}`);
503
+ continue;
504
+ }
505
+ const fallback = String(entry.detail ?? entry.message ?? entry.text ?? entry.title ?? "").trim();
506
+ if (fallback)
507
+ writeLine(`[${String(entry.type ?? "timeline")}] ${fallback}`);
508
+ }
509
+ },
510
+ renderLogs(entries) {
511
+ for (const [index, entry] of entries.entries()) {
512
+ const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
513
+ if (seenLogs.has(id))
514
+ continue;
515
+ seenLogs.add(id);
516
+ const title = String(entry.title ?? "");
517
+ if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
518
+ continue;
519
+ const detail = logDetail(entry);
520
+ if (!detail)
521
+ continue;
522
+ const protocolRecord = parseProviderProtocolLog(title, detail);
523
+ if (protocolRecord) {
524
+ const protocolLine = renderProviderProtocolLog(protocolRecord);
525
+ if (protocolLine)
526
+ writeLine(protocolLine);
527
+ continue;
528
+ }
529
+ writeLine(`[${title || "Rig log"}] ${detail}`);
530
+ }
531
+ }
532
+ };
533
+ }
534
+ function createOperatorSurface(options = {}) {
535
+ const input = options.input ?? process.stdin;
536
+ const output = options.output ?? process.stdout;
537
+ const errorOutput = options.errorOutput ?? process.stderr;
538
+ const renderer = createPiRunStreamRenderer(output);
539
+ const writeLine = (line) => output.write(`${line}
540
+ `);
541
+ return {
542
+ mode: "pi-compatible-text",
543
+ ...renderer,
544
+ info: writeLine,
545
+ error: (message) => errorOutput.write(`${message}
546
+ `),
547
+ attachCommandInput(handler) {
548
+ if (options.interactive === false || !input.isTTY)
549
+ return null;
550
+ const rl = createInterface({ input, output: process.stdout, terminal: false });
551
+ rl.on("line", (line) => {
552
+ Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
553
+ });
554
+ return { close: () => rl.close() };
555
+ }
556
+ };
557
+ }
558
+
559
+ // packages/cli/src/commands/_pi-frontend.ts
560
+ import { mkdtempSync, rmSync } from "fs";
561
+ import { tmpdir } from "os";
562
+ import { join } from "path";
563
+ import { main as runPiMain } from "@earendil-works/pi-coding-agent";
564
+
565
+ // packages/cli/src/commands/_pi-worker-bridge-extension.ts
566
+ var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
567
+ var MAX_TRANSCRIPT_LINES = 120;
568
+ function recordOf(value) {
569
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
570
+ }
571
+ function asText(value) {
572
+ if (typeof value === "string")
573
+ return value;
574
+ if (value === null || value === undefined)
575
+ return "";
576
+ if (typeof value === "number" || typeof value === "boolean")
577
+ return String(value);
578
+ try {
579
+ return JSON.stringify(value);
580
+ } catch {
581
+ return String(value);
582
+ }
583
+ }
584
+ function textFromContent(content) {
585
+ if (typeof content === "string")
586
+ return content;
587
+ if (!Array.isArray(content))
588
+ return asText(content);
589
+ return content.flatMap((part) => {
590
+ const item = recordOf(part);
591
+ if (!item)
592
+ return [];
593
+ if (typeof item.text === "string")
594
+ return [item.text];
595
+ if (typeof item.content === "string")
596
+ return [item.content];
597
+ if (item.type === "toolCall")
598
+ return [`\u23FA ${String(item.name ?? "tool")} ${asText(item.arguments ?? "")}`.trim()];
599
+ if (item.type === "toolResult")
600
+ return [`\u21B3 ${asText(item.content ?? item.result ?? "")}`.trim()];
601
+ return [];
602
+ }).join(`
603
+ `);
604
+ }
605
+ function appendTranscript(state, label, text) {
606
+ const trimmed = text.trimEnd();
607
+ if (!trimmed)
608
+ return;
609
+ const lines = trimmed.split(/\r?\n/);
610
+ state.transcript.push(`${label}: ${lines[0] ?? ""}`);
611
+ for (const line of lines.slice(1))
612
+ state.transcript.push(` ${line}`);
613
+ if (state.transcript.length > MAX_TRANSCRIPT_LINES) {
614
+ state.transcript.splice(0, state.transcript.length - MAX_TRANSCRIPT_LINES);
615
+ }
616
+ }
617
+ function nativePiUi(ctx) {
618
+ const ui = ctx.ui;
619
+ return typeof ui.emitSessionEvent === "function" && typeof ui.appendSessionMessages === "function" ? ui : null;
620
+ }
621
+ function syncNativeDisplayCwd(ctx, state) {
622
+ const ui = nativePiUi(ctx);
623
+ if (ui?.setDisplayCwd && state.cwd)
624
+ ui.setDisplayCwd(state.cwd);
625
+ }
626
+ function parseExtensionUiRequest(value) {
627
+ const request = recordOf(value) ?? {};
628
+ const requestId = String(request.requestId ?? request.id ?? `ui-${Date.now()}`);
629
+ const method = String(request.method ?? request.type ?? "input");
630
+ const prompt = asText(request.prompt ?? request.message ?? request.title ?? method);
631
+ const rawOptions = Array.isArray(request.options) ? request.options : Array.isArray(request.items) ? request.items : [];
632
+ const options = rawOptions.map((option) => {
633
+ const record = recordOf(option);
634
+ return record ? asText(record.label ?? record.value ?? record.name ?? option) : asText(option);
635
+ }).filter(Boolean);
636
+ return { requestId, method, prompt, options };
637
+ }
638
+ function renderBridgeWidget(state) {
639
+ const statusParts = [
640
+ state.wsConnected ? "live" : "connecting",
641
+ state.status,
642
+ state.model,
643
+ state.cwd
644
+ ].filter(Boolean);
645
+ const lines = [`Rig worker session \xB7 ${statusParts.join(" \xB7 ")}`];
646
+ if (state.activity)
647
+ lines.push(state.activity);
648
+ if (state.commands.length > 0) {
649
+ lines.push(`Worker commands: ${state.commands.slice(0, 10).join(", ")}${state.commands.length > 10 ? ", \u2026" : ""}`);
650
+ }
651
+ lines.push("");
652
+ if (state.transcript.length > 0) {
653
+ lines.push(...state.transcript.slice(-MAX_TRANSCRIPT_LINES));
654
+ } else {
655
+ lines.push("Waiting for the worker session transcript\u2026 (/detach exits and leaves the worker running)");
656
+ }
657
+ if (state.pendingUi) {
658
+ lines.push("");
659
+ lines.push(`Worker needs input \xB7 ${state.pendingUi.method}`);
660
+ lines.push(state.pendingUi.prompt);
661
+ state.pendingUi.options.forEach((option, index) => lines.push(`${index + 1}. ${option}`));
662
+ lines.push("Reply in the editor below. /cancel dismisses this request.");
663
+ }
664
+ return lines;
665
+ }
666
+ function reportBridgeError(ctx, state, message) {
667
+ appendTranscript(state, "Error", message);
668
+ state.status = message;
669
+ try {
670
+ ctx.ui.notify(message, "error");
671
+ } catch {}
672
+ updatePiUi(ctx, state);
673
+ }
674
+ function updatePiUi(ctx, state) {
675
+ ctx.ui.setTitle("Rig \xB7 worker session");
676
+ ctx.ui.setStatus("rig-worker-pi", state.wsConnected ? "worker session live" : state.status);
677
+ syncNativeDisplayCwd(ctx, state);
678
+ if (state.nativeStream && nativePiUi(ctx)) {
679
+ ctx.ui.setWidget("rig-worker-pi-transcript", undefined);
680
+ return;
681
+ }
682
+ ctx.ui.setWorkingVisible(false);
683
+ ctx.ui.setWidget("rig-worker-pi-transcript", renderBridgeWidget(state), { placement: "aboveEditor" });
684
+ }
685
+ function applyStatus(state, payload) {
686
+ const status = recordOf(payload.status) ?? payload;
687
+ state.streaming = status.isStreaming === true || status.isCompacting === true || status.isBashRunning === true;
688
+ state.cwd = typeof status.cwd === "string" ? status.cwd : state.cwd;
689
+ state.model = typeof status.model === "string" ? status.model : state.model;
690
+ const pending = typeof status.pendingMessageCount === "number" ? status.pendingMessageCount : 0;
691
+ state.status = `${state.streaming ? "streaming" : "idle"}${pending ? ` \xB7 ${pending} queued` : ""}`;
692
+ }
693
+ function applyMessage(state, message) {
694
+ const record = recordOf(message);
695
+ if (!record)
696
+ return;
697
+ const role = String(record.role ?? "system");
698
+ const label = role === "assistant" ? "Pi" : role === "user" ? "You" : role === "tool" || role === "toolResult" ? "Tool" : "System";
699
+ appendTranscript(state, label, textFromContent(record.content ?? record.message ?? record.text ?? ""));
700
+ }
701
+ function applyPiEvent(ctx, state, eventValue) {
702
+ const event = recordOf(eventValue);
703
+ if (!event)
704
+ return;
705
+ const type = String(event.type ?? "event");
706
+ if (type === "agent_start") {
707
+ state.streaming = true;
708
+ state.status = "streaming";
709
+ } else if (type === "agent_end") {
710
+ state.streaming = false;
711
+ state.status = "idle";
712
+ } else if (type === "queue_update") {
713
+ const steering = Array.isArray(event.steering) ? event.steering.length : 0;
714
+ const followUp = Array.isArray(event.followUp) ? event.followUp.length : 0;
715
+ state.status = `queued \xB7 steer ${steering} \xB7 follow-up ${followUp}`;
716
+ }
717
+ const native = nativePiUi(ctx);
718
+ if (state.nativeStream && native?.emitSessionEvent) {
719
+ native.emitSessionEvent(eventValue);
720
+ return;
721
+ }
722
+ if (type === "agent_end") {
723
+ appendTranscript(state, "System", "Agent turn complete.");
724
+ return;
725
+ }
726
+ if (type === "message_start" || type === "message_end" || type === "turn_end") {
727
+ applyMessage(state, event.message);
728
+ return;
729
+ }
730
+ if (type === "message_update") {
731
+ const assistantEvent = recordOf(event.assistantMessageEvent);
732
+ const delta = typeof assistantEvent?.delta === "string" ? assistantEvent.delta : typeof assistantEvent?.text === "string" ? assistantEvent.text : "";
733
+ if (delta)
734
+ appendTranscript(state, assistantEvent?.type === "thinking_delta" ? "Thinking" : "Pi", delta);
735
+ return;
736
+ }
737
+ if (type === "tool_execution_start") {
738
+ appendTranscript(state, "Tool", `${String(event.toolName ?? "tool")} ${asText(event.args ?? "")}`.trim());
739
+ return;
740
+ }
741
+ if (type === "tool_execution_update") {
742
+ appendTranscript(state, "Tool", asText(event.partialResult ?? ""));
743
+ return;
744
+ }
745
+ if (type === "tool_execution_end") {
746
+ appendTranscript(state, event.isError === true ? "Error" : "Tool", asText(event.result ?? `${String(event.toolName ?? "tool")} complete`));
747
+ }
748
+ }
749
+ function firstPendingShell(state) {
750
+ return state.pendingShells[0];
751
+ }
752
+ function finishPendingShell(state, shell, result) {
753
+ const index = state.pendingShells.indexOf(shell);
754
+ if (index !== -1)
755
+ state.pendingShells.splice(index, 1);
756
+ shell.resolve(result);
757
+ }
758
+ function failPendingShell(state, shell, error) {
759
+ const index = state.pendingShells.indexOf(shell);
760
+ if (index !== -1)
761
+ state.pendingShells.splice(index, 1);
762
+ shell.reject(error);
763
+ }
764
+ function applyUiEvent(state, value) {
765
+ const event = recordOf(value);
766
+ if (!event)
767
+ return;
768
+ const type = String(event.type ?? "ui");
769
+ if (type === "shell.chunk") {
770
+ const pending = firstPendingShell(state);
771
+ const chunk = asText(event.chunk);
772
+ if (pending) {
773
+ pending.sawChunk = true;
774
+ pending.onData(Buffer.from(chunk));
775
+ } else {
776
+ appendTranscript(state, "Tool", chunk);
777
+ }
778
+ return;
779
+ }
780
+ if (type === "shell.end") {
781
+ const pending = firstPendingShell(state);
782
+ const output = asText(event.output ?? "");
783
+ const exitCode = typeof event.exitCode === "number" ? event.exitCode : event.isError === true ? 1 : 0;
784
+ if (pending) {
785
+ if (output && !pending.sawChunk)
786
+ pending.onData(Buffer.from(output));
787
+ finishPendingShell(state, pending, { exitCode });
788
+ } else {
789
+ appendTranscript(state, event.isError === true ? "Error" : "Tool", output || `exit ${String(exitCode)}`);
790
+ }
791
+ return;
792
+ }
793
+ if (type === "shell.start") {
794
+ if (!firstPendingShell(state))
795
+ appendTranscript(state, "Tool", `$ ${asText(event.command)}`);
796
+ return;
797
+ }
798
+ appendTranscript(state, "System", `${type}: ${asText(event)}`);
799
+ }
800
+ function applyEnvelope(ctx, state, envelopeValue) {
801
+ const envelope = recordOf(envelopeValue);
802
+ if (!envelope)
803
+ return;
804
+ const type = String(envelope.type ?? "");
805
+ if (type === "ready") {
806
+ const metadata = recordOf(envelope.metadata);
807
+ state.cwd = typeof metadata?.cwd === "string" ? metadata.cwd : state.cwd;
808
+ state.status = "worker Pi daemon ready";
809
+ if (!state.nativeStream)
810
+ appendTranscript(state, "System", "Connected to worker Pi daemon.");
811
+ } else if (type === "status.update") {
812
+ applyStatus(state, envelope);
813
+ } else if (type === "activity.update") {
814
+ const activity = recordOf(envelope.activity);
815
+ state.activity = [activity?.label, activity?.detail].map(asText).filter(Boolean).join(" \u2014 ");
816
+ } else if (type === "extension_ui_request") {
817
+ state.pendingUi = parseExtensionUiRequest(envelope.request);
818
+ appendTranscript(state, "System", `Extension UI request: ${state.pendingUi.prompt}`);
819
+ } else if (type === "pi.ui_event") {
820
+ applyUiEvent(state, envelope.event);
821
+ } else if (type === "pi.event") {
822
+ applyPiEvent(ctx, state, envelope.event);
823
+ } else if (type === "error") {
824
+ appendTranscript(state, "Error", asText(envelope.message ?? envelope.detail ?? "unknown error"));
825
+ }
826
+ syncNativeDisplayCwd(ctx, state);
827
+ }
828
+ function resolveAttachReadyTimeoutMs() {
829
+ const raw = Number.parseInt(process.env.RIG_PI_ATTACH_TIMEOUT_MS ?? "", 10);
830
+ return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60000;
831
+ }
832
+ function formatElapsed(sinceMs) {
833
+ const totalSeconds = Math.floor((Date.now() - sinceMs) / 1000);
834
+ const minutes = Math.floor(totalSeconds / 60);
835
+ const seconds = totalSeconds % 60;
836
+ return minutes > 0 ? `${minutes}m${String(seconds).padStart(2, "0")}s` : `${seconds}s`;
837
+ }
838
+ async function waitForWorkerReady(options, ctx, state) {
839
+ const startedAt = Date.now();
840
+ const deadline = startedAt + resolveAttachReadyTimeoutMs();
841
+ let consecutiveFailures = 0;
842
+ while (true) {
843
+ let requestFailed = false;
844
+ const session = await getRunPiSessionViaServer(options.context, options.runId).catch((error) => {
845
+ requestFailed = true;
846
+ return {
847
+ ready: false,
848
+ status: error instanceof Error ? error.message : String(error),
849
+ retryAfterMs: 1000
850
+ };
851
+ });
852
+ if (session.ready === false) {
853
+ consecutiveFailures = requestFailed ? consecutiveFailures + 1 : 0;
854
+ const status = String(session.status ?? "starting");
855
+ if (TERMINAL_RUN_STATUSES.has(status.toLowerCase())) {
856
+ reportBridgeError(ctx, state, `Run ended before worker Pi daemon became ready: ${status}. Inspect with \`rig run show ${options.runId}\`; restart with \`rig task run --task <id>\`.`);
857
+ return false;
858
+ }
859
+ if (consecutiveFailures >= 5) {
860
+ state.status = `Rig server unreachable \xB7 retrying (${formatElapsed(startedAt)}) \xB7 /detach to exit`;
861
+ } else {
862
+ state.status = `waiting for worker Pi daemon \xB7 ${status} \xB7 ${formatElapsed(startedAt)} \xB7 /detach to exit`;
863
+ }
864
+ updatePiUi(ctx, state);
865
+ if (Date.now() >= deadline) {
866
+ reportBridgeError(ctx, state, `Worker Pi daemon did not become ready within ${formatElapsed(startedAt)} (last status: ${status}). ` + `Check \`rig run show ${options.runId}\` and \`rig doctor\`; the run may have been restarted by a server deploy. ` + "Set RIG_PI_ATTACH_TIMEOUT_MS to wait longer.");
867
+ return false;
868
+ }
869
+ await Bun.sleep(typeof session.retryAfterMs === "number" ? session.retryAfterMs : 750);
870
+ continue;
871
+ }
872
+ const sessionRecord = recordOf(session) ?? {};
873
+ applyEnvelope(ctx, state, { type: "ready", metadata: sessionRecord.metadata ?? sessionRecord });
874
+ updatePiUi(ctx, state);
875
+ return true;
876
+ }
877
+ }
878
+ function parseWsPayload(message) {
879
+ if (typeof message.data === "string")
880
+ return JSON.parse(message.data);
881
+ return JSON.parse(Buffer.from(message.data).toString("utf8"));
882
+ }
883
+ var BRIDGE_LOCAL_COMMANDS = new Set(["detach", "quit", "q", "stop"]);
884
+ function registerDaemonCommandsNatively(pi, options, ctx, state, commands, registered) {
885
+ for (const command of commands) {
886
+ const record = recordOf(command);
887
+ const name = typeof record?.name === "string" ? record.name : "";
888
+ if (!name || registered.has(name) || BRIDGE_LOCAL_COMMANDS.has(name))
889
+ continue;
890
+ registered.add(name);
891
+ const description = typeof record?.description === "string" ? record.description : undefined;
892
+ const source = typeof record?.source === "string" ? record.source : "worker";
893
+ try {
894
+ pi.registerCommand(name, {
895
+ description: `[worker ${source}] ${description ?? ""}`.trim(),
896
+ handler: async (args) => {
897
+ const text = `/${name}${args ? ` ${args}` : ""}`;
898
+ appendTranscript(state, "You", text);
899
+ try {
900
+ const result = await runRunPiCommandViaServer(options.context, options.runId, text);
901
+ const message = typeof result.message === "string" ? result.message : "worker command accepted";
902
+ appendTranscript(state, "System", message);
903
+ if (state.nativeStream)
904
+ ctx.ui.notify(message, "info");
905
+ } catch (error) {
906
+ reportBridgeError(ctx, state, error instanceof Error ? error.message : String(error));
907
+ }
908
+ updatePiUi(ctx, state);
909
+ }
910
+ });
911
+ } catch {}
912
+ }
913
+ }
914
+ async function connectWorkerStream(options, pi, ctx, state, registeredDaemonCommands) {
915
+ const ready = await waitForWorkerReady(options, ctx, state);
916
+ if (!ready)
917
+ return;
918
+ let catchupDone = false;
919
+ const buffered = [];
920
+ const wsUrl = await buildRunPiEventsWebSocketUrl(options.context, options.runId);
921
+ const socket = new WebSocket(wsUrl);
922
+ const closePromise = new Promise((resolve3) => {
923
+ socket.onopen = () => {
924
+ state.wsConnected = true;
925
+ state.status = "live worker Pi WebSocket connected";
926
+ updatePiUi(ctx, state);
927
+ };
928
+ socket.onmessage = (message) => {
929
+ try {
930
+ const payload = parseWsPayload(message);
931
+ if (!catchupDone)
932
+ buffered.push(payload);
933
+ else {
934
+ applyEnvelope(ctx, state, payload);
935
+ updatePiUi(ctx, state);
936
+ }
937
+ } catch (error) {
938
+ appendTranscript(state, "Error", `Unparseable worker Pi event: ${error instanceof Error ? error.message : String(error)}`);
939
+ updatePiUi(ctx, state);
940
+ }
941
+ };
942
+ socket.onerror = () => socket.close();
943
+ socket.onclose = () => {
944
+ state.wsConnected = false;
945
+ state.status = "worker Pi WebSocket disconnected";
946
+ updatePiUi(ctx, state);
947
+ resolve3();
948
+ };
949
+ });
950
+ try {
951
+ const [messagesPayload, statusPayload, commandsPayload] = await Promise.all([
952
+ getRunPiMessagesViaServer(options.context, options.runId),
953
+ getRunPiStatusViaServer(options.context, options.runId),
954
+ getRunPiCommandsViaServer(options.context, options.runId)
955
+ ]);
956
+ const messages = Array.isArray(messagesPayload.messages) ? messagesPayload.messages : [];
957
+ const native = nativePiUi(ctx);
958
+ if (state.nativeStream && native?.appendSessionMessages)
959
+ native.appendSessionMessages(messages);
960
+ else
961
+ for (const message of messages)
962
+ applyMessage(state, message);
963
+ applyStatus(state, statusPayload);
964
+ const commands = Array.isArray(commandsPayload.commands) ? commandsPayload.commands : [];
965
+ state.commands = commands.flatMap((command) => {
966
+ const record = recordOf(command);
967
+ return typeof record?.name === "string" ? [`/${record.name}`] : [];
968
+ });
969
+ registerDaemonCommandsNatively(pi, options, ctx, state, commands, registeredDaemonCommands);
970
+ catchupDone = true;
971
+ for (const payload of buffered.splice(0))
972
+ applyEnvelope(ctx, state, payload);
973
+ updatePiUi(ctx, state);
974
+ } catch (error) {
975
+ appendTranscript(state, "Error", `Worker Pi catch-up failed: ${error instanceof Error ? error.message : String(error)}`);
976
+ catchupDone = true;
977
+ updatePiUi(ctx, state);
978
+ }
979
+ await closePromise;
980
+ }
981
+ function createRemoteBashOperations(options, state, excludeFromContext) {
982
+ return {
983
+ exec(command, _cwd, execOptions) {
984
+ return new Promise((resolve3, reject) => {
985
+ const pending = {
986
+ command,
987
+ onData: execOptions.onData,
988
+ resolve: resolve3,
989
+ reject,
990
+ sawChunk: false
991
+ };
992
+ const cleanup = () => {
993
+ execOptions.signal?.removeEventListener("abort", onAbort);
994
+ if (timer)
995
+ clearTimeout(timer);
996
+ };
997
+ const onAbort = () => {
998
+ cleanup();
999
+ failPendingShell(state, pending, new Error("Remote worker shell command aborted locally."));
1000
+ };
1001
+ const timeoutMs = typeof execOptions.timeout === "number" && execOptions.timeout > 0 ? execOptions.timeout : 0;
1002
+ const timer = timeoutMs > 0 ? setTimeout(() => {
1003
+ cleanup();
1004
+ failPendingShell(state, pending, new Error(`Remote worker shell command timed out after ${timeoutMs}ms.`));
1005
+ }, timeoutMs) : null;
1006
+ const wrappedResolve = pending.resolve;
1007
+ const wrappedReject = pending.reject;
1008
+ pending.resolve = (result) => {
1009
+ cleanup();
1010
+ wrappedResolve(result);
1011
+ };
1012
+ pending.reject = (error) => {
1013
+ cleanup();
1014
+ wrappedReject(error);
1015
+ };
1016
+ execOptions.signal?.addEventListener("abort", onAbort, { once: true });
1017
+ state.pendingShells.push(pending);
1018
+ sendRunPiShellViaServer(options.context, options.runId, `${excludeFromContext ? "!!" : "!"}${command}`).catch((error) => {
1019
+ cleanup();
1020
+ failPendingShell(state, pending, error instanceof Error ? error : new Error(String(error)));
1021
+ });
1022
+ });
1023
+ }
1024
+ };
1025
+ }
1026
+ async function answerPendingUi(options, state, line) {
1027
+ const pending = state.pendingUi;
1028
+ if (!pending)
1029
+ return false;
1030
+ if (line === "/cancel") {
1031
+ await respondRunPiExtensionUiViaServer(options.context, options.runId, pending.requestId, { cancelled: true });
1032
+ } else if (pending.method === "confirm") {
1033
+ const confirmed = /^(y|yes|true|1)$/i.test(line);
1034
+ await respondRunPiExtensionUiViaServer(options.context, options.runId, pending.requestId, { value: confirmed, confirmed });
1035
+ } else if (pending.options.length > 0 && /^\d+$/.test(line)) {
1036
+ const selected = pending.options[Math.max(0, Number(line) - 1)] ?? line;
1037
+ await respondRunPiExtensionUiViaServer(options.context, options.runId, pending.requestId, { value: selected });
1038
+ } else {
1039
+ await respondRunPiExtensionUiViaServer(options.context, options.runId, pending.requestId, { value: line });
1040
+ }
1041
+ appendTranscript(state, "System", `Responded to extension UI request ${pending.requestId}.`);
1042
+ state.pendingUi = null;
1043
+ return true;
1044
+ }
1045
+ async function routeInput(options, ctx, state, line) {
1046
+ const text = line.trim();
1047
+ if (!text)
1048
+ return;
1049
+ if (await answerPendingUi(options, state, text)) {
1050
+ updatePiUi(ctx, state);
1051
+ return;
1052
+ }
1053
+ if (text === "/detach" || text === "/quit" || text === "/q") {
1054
+ appendTranscript(state, "System", "Detached locally; worker Pi daemon continues.");
1055
+ updatePiUi(ctx, state);
1056
+ ctx.shutdown();
1057
+ return;
1058
+ }
1059
+ if (text === "/stop") {
1060
+ await abortRunPiViaServer(options.context, options.runId);
1061
+ appendTranscript(state, "System", "Stop requested for worker Pi daemon.");
1062
+ updatePiUi(ctx, state);
1063
+ ctx.shutdown();
1064
+ return;
1065
+ }
1066
+ if (text.startsWith("!")) {
1067
+ appendTranscript(state, "You", text);
1068
+ await sendRunPiShellViaServer(options.context, options.runId, text);
1069
+ } else if (text.startsWith("/")) {
1070
+ appendTranscript(state, "You", text);
1071
+ const result = await runRunPiCommandViaServer(options.context, options.runId, text);
1072
+ const message = typeof result.message === "string" ? result.message : "worker command accepted";
1073
+ appendTranscript(state, "System", message);
1074
+ } else {
1075
+ appendTranscript(state, "You", text);
1076
+ await sendRunPiPromptViaServer(options.context, options.runId, text, state.streaming ? "steer" : undefined);
1077
+ }
1078
+ updatePiUi(ctx, state);
1079
+ }
1080
+ function createRigWorkerPiBridgeExtension(options) {
1081
+ return (pi) => {
1082
+ const state = {
1083
+ transcript: [],
1084
+ status: "starting worker Pi daemon bridge",
1085
+ activity: "",
1086
+ cwd: "",
1087
+ model: "",
1088
+ commands: [],
1089
+ streaming: false,
1090
+ pendingUi: null,
1091
+ pendingShells: [],
1092
+ wsConnected: false,
1093
+ nativeStream: false
1094
+ };
1095
+ if (options.initialMessageSent)
1096
+ appendTranscript(state, "System", "Initial message sent to worker Pi daemon.");
1097
+ const registeredDaemonCommands = new Set;
1098
+ let nativePiUiContextAvailable = false;
1099
+ pi.on("user_bash", (event) => {
1100
+ state.nativeStream = Boolean(state.nativeStream || nativePiUiContextAvailable);
1101
+ return { operations: createRemoteBashOperations(options, state, event.excludeFromContext === true) };
1102
+ });
1103
+ pi.on("session_start", async (_event, ctx) => {
1104
+ nativePiUiContextAvailable = Boolean(nativePiUi(ctx));
1105
+ state.nativeStream = nativePiUiContextAvailable;
1106
+ updatePiUi(ctx, state);
1107
+ ctx.ui.notify(nativePiUiContextAvailable ? "Connected to the Rig worker session (native stream). /detach exits, /stop cancels the run." : "Connected to the Rig worker session (transcript view). /detach exits, /stop cancels the run.", "info");
1108
+ ctx.ui.onTerminalInput((data) => {
1109
+ if (data.includes("\x04")) {
1110
+ ctx.shutdown();
1111
+ return { consume: true };
1112
+ }
1113
+ if (!data.includes("\r") && !data.includes(`
1114
+ `))
1115
+ return;
1116
+ const inlineText = data.replace(/[\r\n]+/g, "").trim();
1117
+ const editorText = ctx.ui.getEditorText().trim();
1118
+ const text = [editorText, inlineText].filter(Boolean).join(" ").trim();
1119
+ if (!text)
1120
+ return;
1121
+ if (text.startsWith("!"))
1122
+ return;
1123
+ ctx.ui.setEditorText("");
1124
+ routeInput(options, ctx, state, text).catch((error) => {
1125
+ appendTranscript(state, "Error", error instanceof Error ? error.message : String(error));
1126
+ updatePiUi(ctx, state);
1127
+ });
1128
+ return { consume: true };
1129
+ });
1130
+ connectWorkerStream(options, pi, ctx, state, registeredDaemonCommands).catch((error) => {
1131
+ appendTranscript(state, "Error", error instanceof Error ? error.message : String(error));
1132
+ updatePiUi(ctx, state);
1133
+ });
1134
+ });
1135
+ pi.on("session_shutdown", () => {});
1136
+ };
1137
+ }
1138
+
1139
+ // packages/cli/src/commands/_pi-frontend.ts
1140
+ function setTemporaryEnv(updates) {
1141
+ const previous = new Map;
1142
+ for (const [key, value] of Object.entries(updates)) {
1143
+ previous.set(key, process.env[key]);
1144
+ process.env[key] = value;
1145
+ }
1146
+ return () => {
1147
+ for (const [key, value] of previous) {
1148
+ if (value === undefined)
1149
+ delete process.env[key];
1150
+ else
1151
+ process.env[key] = value;
1152
+ }
1153
+ };
1154
+ }
1155
+ async function attachRunBundledPiFrontend(context, input) {
1156
+ const tempRoot = mkdtempSync(join(tmpdir(), "rig-pi-frontend-"));
1157
+ const cwd = join(tempRoot, "workspace");
1158
+ const agentDir = join(tempRoot, "agent");
1159
+ const sessionDir = join(tempRoot, "sessions");
1160
+ const previousCwd = process.cwd();
1161
+ const restoreEnv = setTemporaryEnv({
1162
+ PI_CODING_AGENT_DIR: agentDir,
1163
+ PI_CODING_AGENT_SESSION_DIR: sessionDir,
1164
+ PI_OFFLINE: "1",
1165
+ PI_SKIP_VERSION_CHECK: "1"
1166
+ });
1167
+ let detached = false;
1168
+ try {
1169
+ await Bun.$`mkdir -p ${cwd} ${agentDir} ${sessionDir}`.quiet();
1170
+ process.chdir(cwd);
1171
+ await runPiMain([
1172
+ "--offline",
1173
+ "--no-session",
1174
+ "--no-tools",
1175
+ "--no-builtin-tools",
1176
+ "--no-skills",
1177
+ "--no-context-files",
1178
+ "--no-approve"
1179
+ ], {
1180
+ extensionFactories: [
1181
+ createRigWorkerPiBridgeExtension({
1182
+ context,
1183
+ runId: input.runId,
1184
+ initialMessageSent: input.steered === true
1185
+ })
1186
+ ]
1187
+ });
1188
+ detached = true;
1189
+ } finally {
1190
+ process.chdir(previousCwd);
1191
+ restoreEnv();
1192
+ rmSync(tempRoot, { recursive: true, force: true });
1193
+ }
1194
+ let run = { runId: input.runId, status: "unknown" };
1195
+ try {
1196
+ run = await getRunDetailsViaServer(context, input.runId);
1197
+ } catch {}
1198
+ return {
1199
+ run,
1200
+ logs: [],
1201
+ timeline: [],
1202
+ timelineCursor: null,
1203
+ steered: input.steered === true,
1204
+ detached,
1205
+ rendered: "actual bundled Pi frontend hosted Rig worker Pi bridge extension"
1206
+ };
1207
+ }
1208
+
1209
+ // packages/cli/src/commands/_operator-view.ts
1210
+ var TERMINAL_RUN_STATUSES2 = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
338
1211
  function runStatusFromPayload(payload) {
339
1212
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
340
1213
  return String(run.status ?? "unknown").toLowerCase();
@@ -356,56 +1229,268 @@ async function applyOperatorCommand(context, input, deps = {}) {
356
1229
  await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
357
1230
  return { action: "continue", message: "Steering message queued." };
358
1231
  }
359
- async function readOperatorSnapshot(context, runId) {
1232
+ async function readOperatorSnapshot(context, runId, options = {}) {
360
1233
  const run = await getRunDetailsViaServer(context, runId);
361
1234
  const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
362
- const entries = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
363
- return { run, logs: entries, rendered: renderOperatorSnapshot({ run, logs: entries }) };
1235
+ const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
1236
+ entries: [{
1237
+ id: `timeline-unavailable:${runId}`,
1238
+ type: "timeline_warning",
1239
+ detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
1240
+ createdAt: new Date().toISOString()
1241
+ }],
1242
+ nextCursor: options.timelineCursor ?? null
1243
+ }));
1244
+ const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
1245
+ const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1246
+ const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
1247
+ return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
364
1248
  }
365
1249
  async function attachRunOperatorView(context, input) {
366
1250
  let steered = false;
367
1251
  if (input.message?.trim()) {
368
- await steerRunViaServer(context, input.runId, input.message.trim());
1252
+ await sendRunPiPromptViaServer(context, input.runId, input.message.trim(), "steer").catch(() => steerRunViaServer(context, input.runId, input.message.trim()));
369
1253
  steered = true;
370
1254
  }
1255
+ if (input.follow && !input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
1256
+ return attachRunBundledPiFrontend(context, {
1257
+ runId: input.runId,
1258
+ steered
1259
+ });
1260
+ }
1261
+ const surface = createOperatorSurface({ interactive: input.interactive !== false });
371
1262
  let snapshot = await readOperatorSnapshot(context, input.runId);
372
1263
  if (context.outputMode === "text") {
373
- console.log(snapshot.rendered);
1264
+ surface.renderSnapshot(snapshot);
1265
+ surface.renderTimeline(snapshot.timeline);
1266
+ surface.renderLogs(snapshot.logs);
374
1267
  if (steered)
375
- console.log("Steering message queued.");
1268
+ surface.info("Message submitted to worker Pi.");
376
1269
  }
377
1270
  let detached = false;
378
- let rl = null;
1271
+ let commandInput = null;
379
1272
  if (input.follow && !input.once && context.outputMode === "text") {
380
1273
  if (input.interactive !== false && process.stdin.isTTY) {
381
- console.log("Controls: /user <message>, /stop, /detach");
382
- rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
383
- rl.on("line", (line) => {
384
- applyOperatorCommand(context, { runId: input.runId, line }).then((result) => {
385
- if (result.message)
386
- console.log(result.message);
387
- if (result.action === "detach" || result.action === "stopped") {
388
- detached = true;
389
- rl?.close();
390
- }
391
- }).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
1274
+ surface.info("Controls: /user <message>, /stop, /detach");
1275
+ commandInput = surface.attachCommandInput(async (line) => {
1276
+ const result = await applyOperatorCommand(context, { runId: input.runId, line });
1277
+ if (result.message)
1278
+ surface.info(result.message);
1279
+ if (result.action === "detach" || result.action === "stopped") {
1280
+ detached = true;
1281
+ commandInput?.close();
1282
+ }
392
1283
  });
393
1284
  }
394
- let lastRendered = snapshot.rendered;
395
1285
  const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
396
- while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
1286
+ let timelineCursor = snapshot.timelineCursor;
1287
+ while (!detached && !TERMINAL_RUN_STATUSES2.has(runStatusFromPayload(snapshot.run))) {
397
1288
  await Bun.sleep(pollMs);
398
- snapshot = await readOperatorSnapshot(context, input.runId);
399
- if (snapshot.rendered !== lastRendered) {
400
- console.log(snapshot.rendered);
401
- lastRendered = snapshot.rendered;
402
- }
1289
+ snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
1290
+ timelineCursor = snapshot.timelineCursor;
1291
+ surface.renderSnapshot(snapshot);
1292
+ surface.renderTimeline(snapshot.timeline);
1293
+ surface.renderLogs(snapshot.logs);
403
1294
  }
404
- rl?.close();
1295
+ commandInput?.close();
405
1296
  }
406
1297
  return { ...snapshot, steered, detached };
407
1298
  }
408
1299
 
1300
+ // packages/cli/src/commands/_cli-format.ts
1301
+ import { log, note } from "@clack/prompts";
1302
+ import pc from "picocolors";
1303
+ function stringField(record, key, fallback = "") {
1304
+ const value = record[key];
1305
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
1306
+ }
1307
+ function rawObject(record) {
1308
+ const raw = record.raw;
1309
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
1310
+ }
1311
+ function truncate(value, width) {
1312
+ if (value.length <= width)
1313
+ return value;
1314
+ if (width <= 1)
1315
+ return "\u2026";
1316
+ return `${value.slice(0, width - 1)}\u2026`;
1317
+ }
1318
+ function pad(value, width) {
1319
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
1320
+ }
1321
+ function statusColor(status) {
1322
+ const normalized = status.toLowerCase();
1323
+ if (["completed", "merged", "closed", "done", "accepted", "pass", "selected", "approved"].includes(normalized))
1324
+ return pc.green;
1325
+ if (["failed", "needs_attention", "needs-attention", "blocked", "error", "rejected"].includes(normalized))
1326
+ return pc.red;
1327
+ if (["running", "reviewing", "validating", "in_progress", "in-progress", "remote"].includes(normalized))
1328
+ return pc.cyan;
1329
+ if (["ready", "open", "queued", "created", "preparing", "local", "pending"].includes(normalized))
1330
+ return pc.yellow;
1331
+ return pc.dim;
1332
+ }
1333
+ function compactDate(value) {
1334
+ if (!value.trim())
1335
+ return "";
1336
+ const parsed = Date.parse(value);
1337
+ if (!Number.isFinite(parsed))
1338
+ return value;
1339
+ return new Date(parsed).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
1340
+ }
1341
+ function firstString(record, keys, fallback = "") {
1342
+ for (const key of keys) {
1343
+ const value = stringField(record, key);
1344
+ if (value)
1345
+ return value;
1346
+ }
1347
+ return fallback;
1348
+ }
1349
+ function runIdOf(run) {
1350
+ return firstString(run, ["runId", "id"], "(unknown-run)");
1351
+ }
1352
+ function taskIdOf(run) {
1353
+ return firstString(run, ["taskId", "task", "task_id"]);
1354
+ }
1355
+ function runTitleOf(run) {
1356
+ return firstString(run, ["title", "summary", "name"], taskIdOf(run) || "(untitled)");
1357
+ }
1358
+ function shouldUseClackOutput() {
1359
+ return Boolean(process.stdout.isTTY) && process.env.RIG_CLI_PLAIN_HELP !== "1";
1360
+ }
1361
+ function printFormattedOutput(message, options = {}) {
1362
+ if (!shouldUseClackOutput()) {
1363
+ console.log(message);
1364
+ return;
1365
+ }
1366
+ if (options.title)
1367
+ note(message, options.title);
1368
+ else
1369
+ log.message(message);
1370
+ }
1371
+ function formatStatusPill(status) {
1372
+ const label = status || "unknown";
1373
+ return statusColor(label)(`\u25CF ${label}`);
1374
+ }
1375
+ function formatSection(title, subtitle) {
1376
+ return `${pc.bold(pc.cyan("\u25C6"))} ${pc.bold(title)}${subtitle ? pc.dim(` \u2014 ${subtitle}`) : ""}`;
1377
+ }
1378
+ function formatSuccessCard(title, rows = []) {
1379
+ const body = rows.filter(([, value]) => value !== undefined && value !== null && String(value).length > 0).map(([key, value]) => `${pc.dim("\u2502")} ${pc.dim(key.padEnd(12))} ${value}`);
1380
+ return [formatSection(title), ...body].join(`
1381
+ `);
1382
+ }
1383
+ function formatNextSteps(steps) {
1384
+ if (steps.length === 0)
1385
+ return [];
1386
+ return [pc.bold("Next"), ...steps.map((step) => `${pc.dim("\u203A")} ${step}`)];
1387
+ }
1388
+ function formatRunList(runs, options = {}) {
1389
+ if (runs.length === 0) {
1390
+ return [
1391
+ formatSection("Runs", "none recorded"),
1392
+ options.source === "server" ? pc.dim("No runs recorded on the selected Rig server.") : pc.dim("No runs recorded in .rig/runs."),
1393
+ "",
1394
+ ...formatNextSteps(["Start one: `rig task run --next`", "Check server: `rig server status`"])
1395
+ ].join(`
1396
+ `);
1397
+ }
1398
+ const rows = runs.map((run) => {
1399
+ const runId = stringField(run, "runId", stringField(run, "id", "(unknown-run)"));
1400
+ const status = stringField(run, "status", "unknown");
1401
+ const taskId = stringField(run, "taskId", "");
1402
+ const title = stringField(run, "title", taskId || "(untitled)");
1403
+ const runtime = stringField(run, "runtimeAdapter", "");
1404
+ return { runId, status, title, runtime };
1405
+ });
1406
+ const idWidth = Math.min(36, Math.max(6, ...rows.map((row) => row.runId.length)));
1407
+ const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
1408
+ const header = `${pc.bold(pad("RUN", idWidth))} ${pc.bold(pad("STATUS", statusWidth))} ${pc.bold("TITLE")}`;
1409
+ const body = rows.map((row) => [
1410
+ pc.bold(pad(truncate(row.runId, idWidth), idWidth)),
1411
+ statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
1412
+ `${row.title}${row.runtime ? pc.dim(` ${row.runtime}`) : ""}`
1413
+ ].join(" "));
1414
+ return [formatSection("Runs", options.source === "server" ? "selected server" : "local state"), header, ...body, "", ...formatNextSteps(["Follow live: `rig run attach <run-id> --follow`", "Details: `rig run show <run-id>`"])].join(`
1415
+ `);
1416
+ }
1417
+ function formatRunCard(run, options = {}) {
1418
+ const raw = rawObject(run);
1419
+ const merged = { ...raw, ...run };
1420
+ const runId = runIdOf(merged);
1421
+ const status = firstString(merged, ["status"], "unknown");
1422
+ const taskId = taskIdOf(merged);
1423
+ const title = runTitleOf(merged);
1424
+ const runtime = firstString(merged, ["runtimeAdapter", "runtime", "adapter"]);
1425
+ const mode = firstString(merged, ["runtimeMode", "mode"]);
1426
+ const interaction = firstString(merged, ["interactionMode"]);
1427
+ const created = compactDate(firstString(merged, ["createdAt"]));
1428
+ const started = compactDate(firstString(merged, ["startedAt"]));
1429
+ const updated = compactDate(firstString(merged, ["updatedAt"]));
1430
+ const completed = compactDate(firstString(merged, ["completedAt", "finishedAt"]));
1431
+ const worktree = firstString(merged, ["worktreePath", "cwd", "projectRoot"]);
1432
+ const piSession = merged.piSession && typeof merged.piSession === "object" && !Array.isArray(merged.piSession) ? firstString(merged.piSession, ["sessionId", "id"]) : "";
1433
+ const timeline = Array.isArray(merged.timeline) ? merged.timeline.length : null;
1434
+ const approvals = Array.isArray(merged.approvals) ? merged.approvals.length : null;
1435
+ const inputs = Array.isArray(merged.userInputs) ? merged.userInputs.length : null;
1436
+ const rows = [
1437
+ ["run", pc.bold(runId)],
1438
+ ["status", formatStatusPill(status)],
1439
+ ["task", taskId],
1440
+ ["title", title],
1441
+ ["runtime", [runtime, mode, interaction].filter(Boolean).join(" \xB7 ")],
1442
+ ["created", created],
1443
+ ["started", started],
1444
+ ["updated", updated],
1445
+ ["completed", completed],
1446
+ ["worktree", worktree],
1447
+ ["pi", piSession],
1448
+ ["timeline", timeline],
1449
+ ["approvals", approvals],
1450
+ ["inputs", inputs]
1451
+ ];
1452
+ return [
1453
+ formatSuccessCard(options.title ?? "Run details", rows),
1454
+ "",
1455
+ ...formatNextSteps([`Follow live: \`rig run attach ${runId} --follow\``, `Raw payload: \`rig run show ${runId} --raw\``])
1456
+ ].join(`
1457
+ `);
1458
+ }
1459
+ function formatRunStatus(summary, options = {}) {
1460
+ const activeRuns = summary.activeRuns ?? [];
1461
+ const recentRuns = summary.recentRuns ?? [];
1462
+ const lines = [formatSection("Run status", options.source === "server" ? "selected server" : "local state")];
1463
+ lines.push("", pc.bold(`Active runs (${activeRuns.length})`));
1464
+ if (activeRuns.length === 0) {
1465
+ lines.push(pc.dim("No active runs."));
1466
+ } else {
1467
+ for (const run of activeRuns) {
1468
+ lines.push(formatRunSummaryLine(run));
1469
+ }
1470
+ }
1471
+ lines.push("", pc.bold(`Recent runs (${recentRuns.length})`));
1472
+ if (recentRuns.length === 0) {
1473
+ lines.push(pc.dim("No recent terminal runs."));
1474
+ } else {
1475
+ for (const run of recentRuns.slice(0, 10)) {
1476
+ lines.push(formatRunSummaryLine(run));
1477
+ }
1478
+ }
1479
+ lines.push("", ...formatNextSteps(["Start work: `rig task run --next`", "Attach: `rig run attach <run-id> --follow`", "Details: `rig run show <run-id>`"]));
1480
+ return lines.join(`
1481
+ `);
1482
+ }
1483
+ function formatRunSummaryLine(run) {
1484
+ const record = run;
1485
+ const runId = runIdOf(record);
1486
+ const status = firstString(record, ["status"], "unknown");
1487
+ const taskId = taskIdOf(record);
1488
+ const title = runTitleOf(record);
1489
+ const runtime = firstString(record, ["runtimeAdapter", "runtime", "adapter"]);
1490
+ const descriptor = [taskId, title].filter(Boolean).join(" \xB7 ");
1491
+ return `${pc.dim("\u2502")} ${pc.bold(runId)} ${formatStatusPill(status)} ${descriptor}${runtime ? pc.dim(` ${runtime}`) : ""}`;
1492
+ }
1493
+
409
1494
  // packages/cli/src/commands/run.ts
410
1495
  function normalizeRemoteRunDetails(payload) {
411
1496
  const run = payload.run;
@@ -418,6 +1503,25 @@ function normalizeRemoteRunDetails(payload) {
418
1503
  ...Array.isArray(payload.userInputs) ? { userInputs: payload.userInputs } : {}
419
1504
  };
420
1505
  }
1506
+ var REMOTE_TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged"]);
1507
+ function isRemoteConnectionSelected(projectRoot) {
1508
+ return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
1509
+ }
1510
+ async function listRunsForSelectedConnection(context, options = {}) {
1511
+ if (isRemoteConnectionSelected(context.projectRoot)) {
1512
+ return { runs: await listRunsViaServer(context, options), source: "server" };
1513
+ }
1514
+ return { runs: listAuthorityRuns(context.projectRoot), source: "local" };
1515
+ }
1516
+ function runStringField(run, key, fallback = "") {
1517
+ const value = run[key];
1518
+ return typeof value === "string" && value.trim() ? value : fallback;
1519
+ }
1520
+ function buildServerRunStatus(runs) {
1521
+ const activeRuns = runs.filter((run) => !REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
1522
+ const recentRuns = runs.filter((run) => REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
1523
+ return { activeRuns, recentRuns, runs };
1524
+ }
421
1525
  function shouldPromptForEpicSelection(context, command, promptEpic, noEpicPrompt) {
422
1526
  if (noEpicPrompt) {
423
1527
  return false;
@@ -479,21 +1583,15 @@ async function promptForEpicSelection(projectRoot, command) {
479
1583
  }
480
1584
  async function executeRun(context, args) {
481
1585
  const [command = "status", ...rest] = args;
482
- const runtimeContext = loadRuntimeContextFromEnv2() ?? undefined;
1586
+ const runtimeContext = loadRuntimeContextFromEnv() ?? undefined;
483
1587
  switch (command) {
484
1588
  case "list": {
485
- requireNoExtraArgs(rest, "bun run rig run list");
486
- const runs = listAuthorityRuns(context.projectRoot);
1589
+ requireNoExtraArgs(rest, "rig run list");
1590
+ const { runs, source } = await listRunsForSelectedConnection(context, { limit: 100 });
487
1591
  if (context.outputMode === "text") {
488
- if (runs.length === 0) {
489
- console.log("No runs recorded in .rig/runs.");
490
- } else {
491
- for (const run of runs) {
492
- console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.title}`);
493
- }
494
- }
1592
+ printFormattedOutput(formatRunList(runs, { source }));
495
1593
  }
496
- return { ok: true, group: "run", command, details: { runs } };
1594
+ return { ok: true, group: "run", command, details: { runs, source } };
497
1595
  }
498
1596
  case "delete": {
499
1597
  let pending = rest;
@@ -501,7 +1599,7 @@ async function executeRun(context, args) {
501
1599
  pending = run.rest;
502
1600
  const purgeArtifacts = takeFlag(pending, "--purge-artifacts");
503
1601
  pending = purgeArtifacts.rest;
504
- requireNoExtraArgs(pending, "bun run rig run delete --run <id> [--purge-artifacts]");
1602
+ requireNoExtraArgs(pending, "rig run delete --run <id> [--purge-artifacts]");
505
1603
  if (!run.value) {
506
1604
  throw new CliError2("run delete requires --run <id>.");
507
1605
  }
@@ -531,7 +1629,7 @@ async function executeRun(context, args) {
531
1629
  pending = keepRuntimes.rest;
532
1630
  const keepQueue = takeFlag(pending, "--keep-queue");
533
1631
  pending = keepQueue.rest;
534
- requireNoExtraArgs(pending, "bun run rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
1632
+ requireNoExtraArgs(pending, "rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
535
1633
  if (!all.value) {
536
1634
  throw new CliError2("run cleanup currently requires --all.");
537
1635
  }
@@ -550,20 +1648,25 @@ async function executeRun(context, args) {
550
1648
  }
551
1649
  case "show": {
552
1650
  let pending = rest;
1651
+ const rawResult = takeFlag(pending, "--raw");
1652
+ pending = rawResult.rest;
553
1653
  const run = takeOption(pending, "--run");
554
1654
  pending = run.rest;
555
- requireNoExtraArgs(pending, "bun run rig run show --run <id>");
556
- if (!run.value) {
557
- throw new CliError2("run show requires --run <id>.");
1655
+ const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
1656
+ const extra = positionalRunId ? pending.slice(1) : pending;
1657
+ requireNoExtraArgs(extra, "rig run show <id>|--run <id> [--raw]");
1658
+ const runId = run.value ?? positionalRunId;
1659
+ if (!runId) {
1660
+ throw new CliError2("run show requires a run id.");
558
1661
  }
559
- const record = readAuthorityRun(context.projectRoot, run.value) ?? normalizeRemoteRunDetails(await getRunDetailsViaServer(context, run.value).catch(() => ({})));
1662
+ const record = readAuthorityRun(context.projectRoot, runId) ?? normalizeRemoteRunDetails(await getRunDetailsViaServer(context, runId).catch(() => ({})));
560
1663
  if (!record) {
561
- throw new CliError2(`Run not found: ${run.value}`, 2);
1664
+ throw new CliError2(`Run not found: ${runId}`, 2);
562
1665
  }
563
1666
  if (context.outputMode === "text") {
564
- console.log(JSON.stringify(record, null, 2));
1667
+ printFormattedOutput(rawResult.value ? JSON.stringify(record, null, 2) : formatRunCard(record));
565
1668
  }
566
- return { ok: true, group: "run", command, details: record };
1669
+ return { ok: true, group: "run", command, details: { ...record, rawOutput: rawResult.value } };
567
1670
  }
568
1671
  case "timeline": {
569
1672
  let pending = rest;
@@ -571,38 +1674,28 @@ async function executeRun(context, args) {
571
1674
  pending = run.rest;
572
1675
  const follow = takeFlag(pending, "--follow");
573
1676
  pending = follow.rest;
574
- requireNoExtraArgs(pending, "bun run rig run timeline --run <id> [--follow]");
1677
+ requireNoExtraArgs(pending, "rig run timeline --run <id> [--follow]");
575
1678
  if (!run.value) {
576
1679
  throw new CliError2("run timeline requires --run <id>.");
577
1680
  }
578
- const timelinePath = resolve3(resolveAuthorityRunDir(context.projectRoot, run.value), "timeline.jsonl");
579
- const printEvents = () => {
580
- const events2 = readJsonlFile(timelinePath);
581
- if (context.outputMode === "text") {
582
- for (const event of events2) {
583
- console.log(JSON.stringify(event));
584
- }
585
- }
586
- return events2;
587
- };
588
- const events = printEvents();
1681
+ const renderer = createPiRunStreamRenderer();
1682
+ let cursor = null;
1683
+ const page = await getRunTimelineViaServer(context, run.value, { limit: 500 });
1684
+ const events = Array.isArray(page.entries) ? page.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1685
+ cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
1686
+ if (context.outputMode === "text") {
1687
+ renderer.renderTimeline(events);
1688
+ }
589
1689
  if (follow.value && context.outputMode === "text") {
590
- let lastLength = existsSync3(timelinePath) ? readFileSync3(timelinePath, "utf8").length : 0;
591
1690
  while (true) {
592
1691
  await Bun.sleep(1000);
593
- if (!existsSync3(timelinePath))
594
- continue;
595
- const next = readFileSync3(timelinePath, "utf8");
596
- if (next.length <= lastLength)
597
- continue;
598
- const delta = next.slice(lastLength);
599
- lastLength = next.length;
600
- for (const line of delta.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) {
601
- console.log(line);
602
- }
1692
+ const nextPage = await getRunTimelineViaServer(context, run.value, { limit: 500, ...cursor ? { cursor } : {} });
1693
+ const nextEvents = Array.isArray(nextPage.entries) ? nextPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1694
+ cursor = typeof nextPage.nextCursor === "string" ? nextPage.nextCursor : cursor;
1695
+ renderer.renderTimeline(nextEvents);
603
1696
  }
604
1697
  }
605
- return { ok: true, group: "run", command, details: { runId: run.value, events } };
1698
+ return { ok: true, group: "run", command, details: { runId: run.value, events, cursor } };
606
1699
  }
607
1700
  case "attach": {
608
1701
  let pending = rest;
@@ -618,41 +1711,38 @@ async function executeRun(context, args) {
618
1711
  pending = pollMs.rest;
619
1712
  const positionalRunId = pending.length > 0 ? pending[0] : undefined;
620
1713
  const extra = positionalRunId ? pending.slice(1) : pending;
621
- requireNoExtraArgs(extra, "bun run rig run attach <run-id>|--run <run-id> [--message <text>] [--once|--follow] [--poll-ms <ms>]");
1714
+ requireNoExtraArgs(extra, "rig run attach <run-id>|--run <run-id> [--message <text>] [--once|--follow] [--poll-ms <ms>]");
622
1715
  const runId = runOption.value ?? positionalRunId;
623
1716
  if (!runId) {
624
1717
  throw new CliError2("run attach requires a run id.", 2);
625
1718
  }
1719
+ let steered = false;
1720
+ if (messageOption.value?.trim()) {
1721
+ await steerRunViaServer(context, runId, messageOption.value.trim());
1722
+ steered = true;
1723
+ }
626
1724
  const attached = await attachRunOperatorView(context, {
627
1725
  runId,
628
- message: messageOption.value ?? null,
1726
+ message: null,
629
1727
  once: once.value,
630
1728
  follow: follow.value,
631
1729
  pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)
632
1730
  });
633
- return { ok: true, group: "run", command, details: attached };
1731
+ return { ok: true, group: "run", command, details: { ...attached, steered: attached.steered || steered } };
634
1732
  }
635
1733
  case "status": {
636
- requireNoExtraArgs(rest, "bun run rig run status");
1734
+ requireNoExtraArgs(rest, "rig run status");
637
1735
  if (context.dryRun) {
638
1736
  if (context.outputMode === "text") {
639
1737
  console.log("[dry-run] rig run status");
640
1738
  }
641
1739
  return { ok: true, group: "run", command };
642
1740
  }
643
- const summary = runStatus(context.projectRoot, runtimeContext);
1741
+ const summary = isRemoteConnectionSelected(context.projectRoot) ? buildServerRunStatus(await listRunsViaServer(context, { limit: 100 })) : runStatus(context.projectRoot, runtimeContext);
1742
+ const activeRuns = Array.isArray(summary.activeRuns) ? summary.activeRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
1743
+ const recentRuns = Array.isArray(summary.recentRuns) ? summary.recentRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
644
1744
  if (context.outputMode === "text") {
645
- console.log(`Active runs: ${summary.activeRuns.length}`);
646
- for (const run of summary.activeRuns) {
647
- console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.taskId ?? run.title}`);
648
- }
649
- if (summary.recentRuns.length > 0) {
650
- console.log("");
651
- console.log("Recent runs:");
652
- for (const run of summary.recentRuns) {
653
- console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.taskId ?? run.title}`);
654
- }
655
- }
1745
+ printFormattedOutput(formatRunStatus({ activeRuns, recentRuns, runs: Array.isArray(summary.runs) ? summary.runs : [...activeRuns, ...recentRuns] }, { source: isRemoteConnectionSelected(context.projectRoot) ? "server" : "local" }));
656
1746
  }
657
1747
  return { ok: true, group: "run", command, details: summary };
658
1748
  }
@@ -676,7 +1766,7 @@ async function executeRun(context, args) {
676
1766
  pending = pollResult.rest;
677
1767
  const noServerResult = takeFlag(pending, "--no-server");
678
1768
  pending = noServerResult.rest;
679
- requireNoExtraArgs(pending, "bun run rig run start [--epic <id>] [--prompt-epic|--no-epic-prompt] [--ws-port <n>] [--server-host <host>] [--server-port <n>] [--poll-ms <n>] [--no-server]");
1769
+ requireNoExtraArgs(pending, "rig run start [--epic <id>] [--prompt-epic|--no-epic-prompt] [--ws-port <n>] [--server-host <host>] [--server-port <n>] [--poll-ms <n>] [--no-server]");
680
1770
  if (promptEpicResult.value && noEpicPromptResult.value) {
681
1771
  throw new CliError2("Cannot use --prompt-epic and --no-epic-prompt together.");
682
1772
  }
@@ -724,7 +1814,7 @@ async function executeRun(context, args) {
724
1814
  };
725
1815
  }
726
1816
  case "resume": {
727
- requireNoExtraArgs(rest, "bun run rig run resume");
1817
+ requireNoExtraArgs(rest, "rig run resume");
728
1818
  if (context.dryRun) {
729
1819
  if (context.outputMode === "text") {
730
1820
  console.log("[dry-run] rig run resume");
@@ -738,7 +1828,7 @@ async function executeRun(context, args) {
738
1828
  return { ok: true, group: "run", command, details: resumed };
739
1829
  }
740
1830
  case "restart": {
741
- requireNoExtraArgs(rest, "bun run rig run restart");
1831
+ requireNoExtraArgs(rest, "rig run restart");
742
1832
  if (context.dryRun) {
743
1833
  if (context.outputMode === "text") {
744
1834
  console.log("[dry-run] rig run restart");
@@ -751,11 +1841,38 @@ async function executeRun(context, args) {
751
1841
  }
752
1842
  return { ok: true, group: "run", command, details: restarted };
753
1843
  }
1844
+ case "steer": {
1845
+ const runOption = takeOption(rest, "--run");
1846
+ const messageOption = takeOption(runOption.rest, "--message");
1847
+ const shortMessageOption = takeOption(messageOption.rest, "-m");
1848
+ const positionalRunId = shortMessageOption.rest.length > 0 ? shortMessageOption.rest[0] : undefined;
1849
+ const extra = positionalRunId ? shortMessageOption.rest.slice(1) : shortMessageOption.rest;
1850
+ requireNoExtraArgs(extra, "rig run steer [<run-id>|--run <id>] --message <text>");
1851
+ const runId = runOption.value ?? positionalRunId;
1852
+ const message = messageOption.value ?? shortMessageOption.value;
1853
+ if (!runId) {
1854
+ throw new CliError2("run steer requires a run id (positional or --run <id>).", 2);
1855
+ }
1856
+ if (!message?.trim()) {
1857
+ throw new CliError2("run steer requires --message <text>.", 2);
1858
+ }
1859
+ if (context.dryRun) {
1860
+ if (context.outputMode === "text") {
1861
+ console.log(`[dry-run] rig run steer ${runId} --message ${JSON.stringify(message)}`);
1862
+ }
1863
+ return { ok: true, group: "run", command, details: { runId, dryRun: true } };
1864
+ }
1865
+ await steerRunViaServer(context, runId, message.trim());
1866
+ if (context.outputMode === "text") {
1867
+ console.log(`Steering message queued for ${runId}.`);
1868
+ }
1869
+ return { ok: true, group: "run", command, details: { runId, queued: true } };
1870
+ }
754
1871
  case "stop": {
755
1872
  const runOption = takeOption(rest, "--run");
756
1873
  const positionalRunId = runOption.rest.length > 0 ? runOption.rest[0] : undefined;
757
1874
  const extra = positionalRunId ? runOption.rest.slice(1) : runOption.rest;
758
- requireNoExtraArgs(extra, "bun run rig run stop [<run-id>|--run <id>]");
1875
+ requireNoExtraArgs(extra, "rig run stop [<run-id>|--run <id>]");
759
1876
  const runId = runOption.value ?? positionalRunId;
760
1877
  if (context.dryRun) {
761
1878
  return {