@h-rig/cli 0.0.6-alpha.5 → 0.0.6-alpha.51

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 (50) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/rig.js +4129 -1207
  3. package/dist/src/commands/_cli-format.js +369 -0
  4. package/dist/src/commands/_connection-state.js +12 -6
  5. package/dist/src/commands/_doctor-checks.js +79 -34
  6. package/dist/src/commands/_help-catalog.js +445 -0
  7. package/dist/src/commands/_operator-surface.js +220 -0
  8. package/dist/src/commands/_operator-view.js +1124 -64
  9. package/dist/src/commands/_parsers.js +0 -2
  10. package/dist/src/commands/_pi-frontend.js +1080 -0
  11. package/dist/src/commands/_pi-install.js +4 -3
  12. package/dist/src/commands/_pi-remote-session.js +771 -0
  13. package/dist/src/commands/_pi-worker-bridge-extension.js +834 -0
  14. package/dist/src/commands/_policy.js +0 -2
  15. package/dist/src/commands/_preflight.js +98 -116
  16. package/dist/src/commands/_run-driver-helpers.js +2 -2
  17. package/dist/src/commands/_server-client.js +225 -48
  18. package/dist/src/commands/_snapshot-upload.js +74 -30
  19. package/dist/src/commands/_spinner.js +63 -0
  20. package/dist/src/commands/_task-picker.js +44 -16
  21. package/dist/src/commands/agent.js +8 -9
  22. package/dist/src/commands/browser.js +4 -6
  23. package/dist/src/commands/connect.js +134 -26
  24. package/dist/src/commands/dist.js +4 -6
  25. package/dist/src/commands/doctor.js +79 -34
  26. package/dist/src/commands/github.js +76 -32
  27. package/dist/src/commands/inbox.js +410 -31
  28. package/dist/src/commands/init.js +398 -90
  29. package/dist/src/commands/inspect.js +296 -23
  30. package/dist/src/commands/inspector.js +2 -4
  31. package/dist/src/commands/pi.js +168 -0
  32. package/dist/src/commands/plugin.js +81 -22
  33. package/dist/src/commands/profile-and-review.js +8 -10
  34. package/dist/src/commands/queue.js +2 -3
  35. package/dist/src/commands/remote.js +18 -20
  36. package/dist/src/commands/repo-git-harness.js +6 -8
  37. package/dist/src/commands/run.js +1422 -131
  38. package/dist/src/commands/server.js +280 -40
  39. package/dist/src/commands/setup.js +84 -45
  40. package/dist/src/commands/task-report-bug.js +5 -7
  41. package/dist/src/commands/task-run-driver.js +676 -70
  42. package/dist/src/commands/task.js +1861 -260
  43. package/dist/src/commands/test.js +3 -5
  44. package/dist/src/commands/workspace.js +4 -6
  45. package/dist/src/commands.js +4111 -1183
  46. package/dist/src/index.js +4122 -1203
  47. package/dist/src/launcher.js +5 -3
  48. package/dist/src/report-bug.js +3 -3
  49. package/dist/src/runner.js +5 -19
  50. package/package.json +7 -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";
@@ -115,6 +108,11 @@ function readJsonFile(path) {
115
108
  throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
116
109
  }
117
110
  }
111
+ function writeJsonFile(path, value) {
112
+ mkdirSync(dirname(path), { recursive: true });
113
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
114
+ `, "utf8");
115
+ }
118
116
  function normalizeConnection(value) {
119
117
  if (!value || typeof value !== "object" || Array.isArray(value))
120
118
  return null;
@@ -155,25 +153,35 @@ function readRepoConnection(projectRoot) {
155
153
  return {
156
154
  selected,
157
155
  project: typeof record.project === "string" ? record.project : undefined,
158
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
156
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
157
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
159
158
  };
160
159
  }
160
+ function writeRepoConnection(projectRoot, state) {
161
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
162
+ }
161
163
  function resolveSelectedConnection(projectRoot, options = {}) {
162
164
  const repo = readRepoConnection(projectRoot);
163
165
  if (!repo)
164
166
  return null;
165
167
  if (repo.selected === "local")
166
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
168
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
167
169
  const global = readGlobalConnections(options);
168
170
  const connection = global.connections[repo.selected];
169
171
  if (!connection) {
170
- throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
172
+ throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
171
173
  }
172
- return { alias: repo.selected, connection };
174
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
175
+ }
176
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
177
+ const repo = readRepoConnection(projectRoot);
178
+ if (!repo)
179
+ return;
180
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
173
181
  }
174
182
 
175
183
  // packages/cli/src/commands/_server-client.ts
176
- var cachedGitHubBearerToken;
184
+ var scopedGitHubBearerTokens = new Map;
177
185
  function cleanToken(value) {
178
186
  const trimmed = value?.trim();
179
187
  return trimmed ? trimmed : null;
@@ -190,41 +198,47 @@ function readPrivateRemoteSessionToken(projectRoot) {
190
198
  }
191
199
  }
192
200
  function readGitHubBearerTokenForRemote(projectRoot) {
193
- if (cachedGitHubBearerToken !== undefined)
194
- return cachedGitHubBearerToken;
201
+ const scopedKey = resolve2(projectRoot);
202
+ if (scopedGitHubBearerTokens.has(scopedKey))
203
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
195
204
  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;
205
+ if (privateSession)
206
+ return privateSession;
207
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
208
+ }
209
+ function readStoredGitHubAuthToken(projectRoot) {
210
+ const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
211
+ if (!existsSync2(path))
212
+ return null;
213
+ try {
214
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
215
+ return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+ function readLocalConnectionFallbackToken(projectRoot) {
221
+ return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
212
222
  }
213
223
  async function ensureServerForCli(projectRoot) {
214
224
  try {
215
225
  const selected = resolveSelectedConnection(projectRoot);
216
226
  if (selected?.connection.kind === "remote") {
227
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
228
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
217
229
  return {
218
230
  baseUrl: selected.connection.baseUrl,
219
- authToken: readGitHubBearerTokenForRemote(projectRoot),
220
- connectionKind: "remote"
231
+ authToken,
232
+ connectionKind: "remote",
233
+ serverProjectRoot
221
234
  };
222
235
  }
223
236
  const connection = await ensureLocalRigServerConnection(projectRoot);
224
237
  return {
225
238
  baseUrl: connection.baseUrl,
226
- authToken: connection.authToken,
227
- connectionKind: "local"
239
+ authToken: connection.authToken ?? readLocalConnectionFallbackToken(projectRoot),
240
+ connectionKind: "local",
241
+ serverProjectRoot: resolve2(projectRoot)
228
242
  };
229
243
  } catch (error) {
230
244
  if (error instanceof Error) {
@@ -233,6 +247,29 @@ async function ensureServerForCli(projectRoot) {
233
247
  throw error;
234
248
  }
235
249
  }
250
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
251
+ const repo = readRepoConnection(projectRoot);
252
+ const slug = repo?.project?.trim();
253
+ if (!slug)
254
+ return null;
255
+ try {
256
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
257
+ headers: mergeHeaders(undefined, authToken)
258
+ });
259
+ if (!response.ok)
260
+ return null;
261
+ const payload = await response.json();
262
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
263
+ const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
264
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
265
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
266
+ if (path)
267
+ writeRepoServerProjectRoot(projectRoot, path);
268
+ return path;
269
+ } catch {
270
+ return null;
271
+ }
272
+ }
236
273
  function mergeHeaders(headers, authToken) {
237
274
  const merged = new Headers(headers);
238
275
  if (authToken) {
@@ -257,9 +294,12 @@ function diagnosticMessage(payload) {
257
294
  }
258
295
  async function requestServerJson(context, pathname, init = {}) {
259
296
  const server = await ensureServerForCli(context.projectRoot);
297
+ const headers = mergeHeaders(init.headers, server.authToken);
298
+ if (server.serverProjectRoot)
299
+ headers.set("x-rig-project-root", server.serverProjectRoot);
260
300
  const response = await fetch(`${server.baseUrl}${pathname}`, {
261
301
  ...init,
262
- headers: mergeHeaders(init.headers, server.authToken)
302
+ headers
263
303
  });
264
304
  const text = await response.text();
265
305
  const payload = text.trim().length > 0 ? (() => {
@@ -276,6 +316,14 @@ async function requestServerJson(context, pathname, init = {}) {
276
316
  }
277
317
  return payload;
278
318
  }
319
+ async function listRunsViaServer(context, options = {}) {
320
+ const url = new URL("http://rig.local/api/runs");
321
+ if (options.limit !== undefined)
322
+ url.searchParams.set("limit", String(options.limit));
323
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
324
+ const runs = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.runs) ? payload.runs : [];
325
+ return runs.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
326
+ }
279
327
  async function getRunDetailsViaServer(context, runId) {
280
328
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}`);
281
329
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
@@ -289,6 +337,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
289
337
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
290
338
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
291
339
  }
340
+ async function getRunTimelineViaServer(context, runId, options = {}) {
341
+ const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
342
+ if (options.limit !== undefined)
343
+ url.searchParams.set("limit", String(options.limit));
344
+ if (options.cursor)
345
+ url.searchParams.set("cursor", options.cursor);
346
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
347
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
348
+ }
292
349
  async function stopRunViaServer(context, runId) {
293
350
  const payload = await requestServerJson(context, "/api/runs/stop", {
294
351
  method: "POST",
@@ -305,10 +362,75 @@ async function steerRunViaServer(context, runId, message) {
305
362
  });
306
363
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
307
364
  }
365
+ async function getRunPiSessionViaServer(context, runId) {
366
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi`);
367
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
368
+ }
369
+ async function getRunPiMessagesViaServer(context, runId) {
370
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/messages`);
371
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { messages: [] };
372
+ }
373
+ async function getRunPiStatusViaServer(context, runId) {
374
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/status`);
375
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
376
+ }
377
+ async function getRunPiCommandsViaServer(context, runId) {
378
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
379
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
380
+ }
381
+ async function getRunPiCapabilitiesViaServer(context, runId) {
382
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/capabilities`);
383
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { capabilities: null };
384
+ }
385
+ async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
386
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
387
+ method: "POST",
388
+ headers: { "content-type": "application/json" },
389
+ body: JSON.stringify({ text, streamingBehavior })
390
+ });
391
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
392
+ }
393
+ async function sendRunPiShellViaServer(context, runId, text) {
394
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/shell`, {
395
+ method: "POST",
396
+ headers: { "content-type": "application/json" },
397
+ body: JSON.stringify({ text })
398
+ });
399
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
400
+ }
401
+ async function runRunPiCommandViaServer(context, runId, text) {
402
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands/run`, {
403
+ method: "POST",
404
+ headers: { "content-type": "application/json" },
405
+ body: JSON.stringify({ text })
406
+ });
407
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { type: "done" };
408
+ }
409
+ async function respondRunPiExtensionUiViaServer(context, runId, requestId, valueOrCancel) {
410
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/extension-ui/respond`, {
411
+ method: "POST",
412
+ headers: { "content-type": "application/json" },
413
+ body: JSON.stringify({ requestId, ...valueOrCancel })
414
+ });
415
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
416
+ }
417
+ async function abortRunPiViaServer(context, runId) {
418
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/abort`, { method: "POST" });
419
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { aborted: true };
420
+ }
421
+ async function buildRunPiEventsWebSocketUrl(context, runId) {
422
+ const server = await ensureServerForCli(context.projectRoot);
423
+ const url = new URL(`${server.baseUrl.replace(/\/+$/, "")}/api/runs/${encodeURIComponent(runId)}/pi/events`);
424
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
425
+ if (server.authToken)
426
+ url.searchParams.set("token", server.authToken);
427
+ if (server.serverProjectRoot)
428
+ url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
429
+ return url.toString();
430
+ }
308
431
 
309
- // packages/cli/src/commands/_operator-view.ts
432
+ // packages/cli/src/commands/_operator-surface.ts
310
433
  import { createInterface } from "readline";
311
- var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
312
434
  var CANONICAL_STAGES = [
313
435
  "Connect",
314
436
  "GitHub/task sync",
@@ -323,18 +445,943 @@ var CANONICAL_STAGES = [
323
445
  "Merge",
324
446
  "Complete"
325
447
  ];
448
+ function logDetail(log) {
449
+ return typeof log.detail === "string" ? log.detail.trim() : "";
450
+ }
451
+ function parseProviderProtocolLog(title, detail) {
452
+ if (title.trim().toLowerCase() !== "agent output")
453
+ return null;
454
+ if (!detail.startsWith("{") || !detail.endsWith("}"))
455
+ return null;
456
+ try {
457
+ const record = JSON.parse(detail);
458
+ if (!record || typeof record !== "object" || Array.isArray(record))
459
+ return null;
460
+ const type = record.type;
461
+ return typeof type === "string" && [
462
+ "assistant",
463
+ "message_start",
464
+ "message_update",
465
+ "message_end",
466
+ "stream_event",
467
+ "tool_result",
468
+ "tool_execution_start",
469
+ "tool_execution_update",
470
+ "tool_execution_end",
471
+ "turn_start",
472
+ "turn_end"
473
+ ].includes(type) ? record : null;
474
+ } catch {
475
+ return null;
476
+ }
477
+ }
478
+ function renderProviderProtocolLog(record) {
479
+ const type = typeof record.type === "string" ? record.type : "";
480
+ if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
481
+ const toolName = String(record.toolName ?? record.name ?? "tool");
482
+ 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";
483
+ return `[Pi tool] ${toolName} ${status}`;
484
+ }
485
+ return null;
486
+ }
487
+ function entryId(entry, fallback) {
488
+ return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
489
+ }
326
490
  function renderOperatorSnapshot(snapshot) {
327
491
  const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
328
492
  const runId = String(run.runId ?? run.id ?? "run");
329
493
  const status = String(run.status ?? "unknown");
330
494
  const logs = snapshot.logs ?? [];
495
+ const latestByStage = new Map;
496
+ for (const log of logs) {
497
+ const title = String(log.title ?? "").toLowerCase();
498
+ const stageName = String(log.stage ?? "").toLowerCase();
499
+ const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
500
+ if (stage)
501
+ latestByStage.set(stage, log);
502
+ }
331
503
  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)}`] : [];
504
+ const match = latestByStage.get(stage);
505
+ return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
334
506
  });
335
507
  return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
336
508
  `);
337
509
  }
510
+ function createPiRunStreamRenderer(output = process.stdout) {
511
+ let lastSnapshot = "";
512
+ const assistantTextById = new Map;
513
+ const seenTimeline = new Set;
514
+ const seenLogs = new Set;
515
+ const writeLine = (line) => output.write(`${line}
516
+ `);
517
+ return {
518
+ renderSnapshot(snapshot) {
519
+ const rendered = renderOperatorSnapshot(snapshot);
520
+ if (rendered && rendered !== lastSnapshot) {
521
+ writeLine(rendered);
522
+ lastSnapshot = rendered;
523
+ }
524
+ },
525
+ renderTimeline(entries) {
526
+ for (const [index, entry] of entries.entries()) {
527
+ const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
528
+ if (entry.type === "assistant_message" && typeof entry.text === "string") {
529
+ const text = entry.text;
530
+ const previousText = assistantTextById.get(id) ?? "";
531
+ if (!previousText && text.trim()) {
532
+ writeLine("[Pi assistant]");
533
+ }
534
+ if (text.startsWith(previousText)) {
535
+ const delta = text.slice(previousText.length);
536
+ if (delta)
537
+ output.write(delta);
538
+ } else if (text.trim() && text !== previousText) {
539
+ if (previousText)
540
+ writeLine(`
541
+ [Pi assistant]`);
542
+ output.write(text);
543
+ }
544
+ assistantTextById.set(id, text);
545
+ continue;
546
+ }
547
+ if (seenTimeline.has(id))
548
+ continue;
549
+ seenTimeline.add(id);
550
+ if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
551
+ writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
552
+ continue;
553
+ }
554
+ if (entry.type === "timeline_warning") {
555
+ writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
556
+ continue;
557
+ }
558
+ if (entry.type === "action") {
559
+ const text = String(entry.detail ?? entry.message ?? entry.title ?? "").trim();
560
+ if (text)
561
+ writeLine(`[Rig action] ${text}`);
562
+ continue;
563
+ }
564
+ if (entry.type === "user_message") {
565
+ const text = String(entry.text ?? entry.message ?? entry.detail ?? "").trim();
566
+ if (text)
567
+ writeLine(`[Operator] ${text}`);
568
+ continue;
569
+ }
570
+ const fallback = String(entry.detail ?? entry.message ?? entry.text ?? entry.title ?? "").trim();
571
+ if (fallback)
572
+ writeLine(`[${String(entry.type ?? "timeline")}] ${fallback}`);
573
+ }
574
+ },
575
+ renderLogs(entries) {
576
+ for (const [index, entry] of entries.entries()) {
577
+ const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
578
+ if (seenLogs.has(id))
579
+ continue;
580
+ seenLogs.add(id);
581
+ const title = String(entry.title ?? "");
582
+ if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
583
+ continue;
584
+ const detail = logDetail(entry);
585
+ if (!detail)
586
+ continue;
587
+ const protocolRecord = parseProviderProtocolLog(title, detail);
588
+ if (protocolRecord) {
589
+ const protocolLine = renderProviderProtocolLog(protocolRecord);
590
+ if (protocolLine)
591
+ writeLine(protocolLine);
592
+ continue;
593
+ }
594
+ writeLine(`[${title || "Rig log"}] ${detail}`);
595
+ }
596
+ }
597
+ };
598
+ }
599
+ function createOperatorSurface(options = {}) {
600
+ const input = options.input ?? process.stdin;
601
+ const output = options.output ?? process.stdout;
602
+ const errorOutput = options.errorOutput ?? process.stderr;
603
+ const renderer = createPiRunStreamRenderer(output);
604
+ const writeLine = (line) => output.write(`${line}
605
+ `);
606
+ return {
607
+ mode: "pi-compatible-text",
608
+ ...renderer,
609
+ info: writeLine,
610
+ error: (message) => errorOutput.write(`${message}
611
+ `),
612
+ attachCommandInput(handler) {
613
+ if (options.interactive === false || !input.isTTY)
614
+ return null;
615
+ const rl = createInterface({ input, output: process.stdout, terminal: false });
616
+ rl.on("line", (line) => {
617
+ Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
618
+ });
619
+ return { close: () => rl.close() };
620
+ }
621
+ };
622
+ }
623
+
624
+ // packages/cli/src/commands/_pi-frontend.ts
625
+ import { mkdtempSync, rmSync } from "fs";
626
+ import { tmpdir } from "os";
627
+ import { join } from "path";
628
+ import {
629
+ createAgentSessionFromServices,
630
+ createAgentSessionServices,
631
+ main as runPiMain
632
+ } from "@earendil-works/pi-coding-agent";
633
+
634
+ // packages/cli/src/commands/_pi-remote-session.ts
635
+ import { AgentSession as PiAgentSession, expandPromptTemplate } from "@earendil-works/pi-coding-agent";
636
+ var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
637
+ function defaultTransport() {
638
+ return {
639
+ getSession: getRunPiSessionViaServer,
640
+ getMessages: getRunPiMessagesViaServer,
641
+ getStatus: getRunPiStatusViaServer,
642
+ getCommands: getRunPiCommandsViaServer,
643
+ getCapabilities: getRunPiCapabilitiesViaServer,
644
+ sendPrompt: sendRunPiPromptViaServer,
645
+ sendShell: sendRunPiShellViaServer,
646
+ runCommand: runRunPiCommandViaServer,
647
+ abort: abortRunPiViaServer,
648
+ buildEventsWebSocketUrl: buildRunPiEventsWebSocketUrl
649
+ };
650
+ }
651
+ function recordOf(value) {
652
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
653
+ }
654
+ function emptyRemoteStatus() {
655
+ return {
656
+ isStreaming: false,
657
+ isCompacting: false,
658
+ isBashRunning: false,
659
+ pendingMessageCount: 0,
660
+ steeringMessages: [],
661
+ followUpMessages: [],
662
+ model: null,
663
+ thinkingLevel: null,
664
+ sessionName: null,
665
+ cwd: null,
666
+ stats: null,
667
+ contextUsage: null
668
+ };
669
+ }
670
+ function resolveAttachReadyTimeoutMs() {
671
+ const raw = Number.parseInt(process.env.RIG_PI_ATTACH_TIMEOUT_MS ?? "", 10);
672
+ return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60000;
673
+ }
674
+
675
+ class RigRemoteSessionController {
676
+ context;
677
+ runId;
678
+ status = emptyRemoteStatus();
679
+ transport;
680
+ hooks = {};
681
+ session = null;
682
+ socket = null;
683
+ closed = false;
684
+ pendingShells = [];
685
+ pendingCompactions = [];
686
+ constructor(input) {
687
+ this.context = input.context;
688
+ this.runId = input.runId;
689
+ this.transport = input.transport ?? defaultTransport();
690
+ }
691
+ ingestEnvelope(envelopeValue) {
692
+ this.applyEnvelope(envelopeValue);
693
+ }
694
+ bindSession(session) {
695
+ this.session = session;
696
+ }
697
+ setUiHooks(hooks) {
698
+ this.hooks = hooks;
699
+ }
700
+ async connect() {
701
+ const ready = await this.waitForReady();
702
+ if (!ready || this.closed)
703
+ return;
704
+ let catchupDone = false;
705
+ const buffered = [];
706
+ const wsUrl = await this.transport.buildEventsWebSocketUrl(this.context, this.runId);
707
+ const socket = new WebSocket(wsUrl);
708
+ this.socket = socket;
709
+ socket.onopen = () => {
710
+ this.hooks.onConnectionChange?.(true);
711
+ this.hooks.onStatusText?.("worker session live");
712
+ };
713
+ socket.onmessage = (message) => {
714
+ try {
715
+ const payload = typeof message.data === "string" ? JSON.parse(message.data) : JSON.parse(Buffer.from(message.data).toString("utf8"));
716
+ if (!catchupDone)
717
+ buffered.push(payload);
718
+ else
719
+ this.applyEnvelope(payload);
720
+ } catch (error) {
721
+ this.hooks.onError?.(`Unparseable worker Pi event: ${error instanceof Error ? error.message : String(error)}`);
722
+ }
723
+ };
724
+ socket.onerror = () => socket.close();
725
+ socket.onclose = () => {
726
+ this.hooks.onConnectionChange?.(false);
727
+ if (!this.closed)
728
+ this.hooks.onStatusText?.("worker Pi WebSocket disconnected");
729
+ };
730
+ try {
731
+ const [messagesPayload, statusPayload, commandsPayload, capabilitiesPayload] = await Promise.all([
732
+ this.transport.getMessages(this.context, this.runId),
733
+ this.transport.getStatus(this.context, this.runId),
734
+ this.transport.getCommands(this.context, this.runId),
735
+ this.transport.getCapabilities(this.context, this.runId).catch(() => ({ capabilities: null }))
736
+ ]);
737
+ const messages = Array.isArray(messagesPayload.messages) ? messagesPayload.messages : [];
738
+ this.applyStatusPayload(statusPayload);
739
+ this.session?.replaceRemoteMessages(messages);
740
+ const commands = Array.isArray(commandsPayload.commands) ? commandsPayload.commands : [];
741
+ const capabilities = capabilitiesPayload.capabilities && typeof capabilitiesPayload.capabilities === "object" && !Array.isArray(capabilitiesPayload.capabilities) ? capabilitiesPayload.capabilities : null;
742
+ this.hooks.onCatchUp?.(messages, commands, capabilities);
743
+ catchupDone = true;
744
+ for (const payload of buffered.splice(0))
745
+ this.applyEnvelope(payload);
746
+ } catch (error) {
747
+ catchupDone = true;
748
+ this.hooks.onError?.(`Worker Pi catch-up failed: ${error instanceof Error ? error.message : String(error)}`);
749
+ }
750
+ }
751
+ close() {
752
+ this.closed = true;
753
+ this.socket?.close();
754
+ for (const shell of this.pendingShells.splice(0))
755
+ shell.reject(new Error("Remote session closed."));
756
+ for (const compaction of this.pendingCompactions.splice(0))
757
+ compaction.reject(new Error("Remote session closed."));
758
+ }
759
+ async sendPrompt(text, streamingBehavior) {
760
+ await this.transport.sendPrompt(this.context, this.runId, text, streamingBehavior);
761
+ }
762
+ async sendCommand(text) {
763
+ const result = await this.transport.runCommand(this.context, this.runId, text);
764
+ return typeof result.message === "string" ? result.message : "worker command accepted";
765
+ }
766
+ async sendShell(text) {
767
+ await this.transport.sendShell(this.context, this.runId, text);
768
+ }
769
+ async abort() {
770
+ await this.transport.abort(this.context, this.runId);
771
+ }
772
+ registerPendingShell(shell) {
773
+ this.pendingShells.push(shell);
774
+ }
775
+ failPendingShell(shell, error) {
776
+ const index = this.pendingShells.indexOf(shell);
777
+ if (index !== -1)
778
+ this.pendingShells.splice(index, 1);
779
+ shell.reject(error);
780
+ }
781
+ registerPendingCompaction(pending) {
782
+ this.pendingCompactions.push(pending);
783
+ }
784
+ async waitForReady() {
785
+ const startedAt = Date.now();
786
+ const deadline = startedAt + resolveAttachReadyTimeoutMs();
787
+ let consecutiveFailures = 0;
788
+ while (!this.closed) {
789
+ let requestFailed = false;
790
+ const session = await this.transport.getSession(this.context, this.runId).catch((error) => {
791
+ requestFailed = true;
792
+ return { ready: false, status: error instanceof Error ? error.message : String(error), retryAfterMs: 1000 };
793
+ });
794
+ if (session.ready !== false)
795
+ return true;
796
+ consecutiveFailures = requestFailed ? consecutiveFailures + 1 : 0;
797
+ const status = String(session.status ?? "starting");
798
+ if (TERMINAL_RUN_STATUSES.has(status.toLowerCase())) {
799
+ this.hooks.onError?.(`Run ended before the worker Pi daemon became ready: ${status}. Inspect with \`rig run show ${this.runId}\`.`);
800
+ return false;
801
+ }
802
+ this.hooks.onStatusText?.(consecutiveFailures >= 5 ? "Rig server unreachable \xB7 retrying \xB7 /detach to exit" : `waiting for worker Pi daemon \xB7 ${status} \xB7 /detach to exit`);
803
+ if (Date.now() >= deadline) {
804
+ this.hooks.onError?.(`Worker Pi daemon did not become ready (last status: ${status}). Set RIG_PI_ATTACH_TIMEOUT_MS to wait longer.`);
805
+ return false;
806
+ }
807
+ await Bun.sleep(typeof session.retryAfterMs === "number" ? session.retryAfterMs : 750);
808
+ }
809
+ return false;
810
+ }
811
+ applyEnvelope(envelopeValue) {
812
+ const envelope = recordOf(envelopeValue);
813
+ if (!envelope)
814
+ return;
815
+ const type = String(envelope.type ?? "");
816
+ if (type === "status.update") {
817
+ this.applyStatusPayload(envelope);
818
+ return;
819
+ }
820
+ if (type === "activity.update") {
821
+ const activity = recordOf(envelope.activity);
822
+ this.hooks.onActivity?.(String(activity?.label ?? ""), activity?.detail ? String(activity.detail) : undefined);
823
+ return;
824
+ }
825
+ if (type === "extension_ui_request") {
826
+ const request = recordOf(envelope.request);
827
+ if (request)
828
+ this.hooks.onExtensionUiRequest?.(request);
829
+ return;
830
+ }
831
+ if (type === "pi.ui_event") {
832
+ this.applyShellUiEvent(envelope.event);
833
+ return;
834
+ }
835
+ if (type === "pi.event") {
836
+ this.session?.handleRemoteSessionEvent(envelope.event);
837
+ this.settlePendingCompaction(envelope.event);
838
+ return;
839
+ }
840
+ if (type === "error") {
841
+ this.hooks.onError?.(String(envelope.message ?? envelope.detail ?? "unknown worker error"));
842
+ }
843
+ }
844
+ applyStatusPayload(payload) {
845
+ const status = recordOf(payload.status) ?? payload;
846
+ const next = this.status;
847
+ next.isStreaming = status.isStreaming === true;
848
+ next.isCompacting = status.isCompacting === true;
849
+ next.isBashRunning = status.isBashRunning === true;
850
+ next.pendingMessageCount = typeof status.pendingMessageCount === "number" ? status.pendingMessageCount : 0;
851
+ next.steeringMessages = Array.isArray(status.steeringMessages) ? status.steeringMessages.map(String) : [];
852
+ next.followUpMessages = Array.isArray(status.followUpMessages) ? status.followUpMessages.map(String) : [];
853
+ next.model = recordOf(status.model) ?? next.model;
854
+ next.thinkingLevel = typeof status.thinkingLevel === "string" ? status.thinkingLevel : next.thinkingLevel;
855
+ next.sessionName = typeof status.sessionName === "string" ? status.sessionName : next.sessionName;
856
+ next.cwd = typeof status.cwd === "string" ? status.cwd : next.cwd;
857
+ next.stats = recordOf(status.stats) ?? next.stats;
858
+ next.contextUsage = recordOf(status.contextUsage) ?? next.contextUsage;
859
+ }
860
+ applyShellUiEvent(value) {
861
+ const event = recordOf(value);
862
+ if (!event)
863
+ return;
864
+ const type = String(event.type ?? "");
865
+ const pending = this.pendingShells[0];
866
+ if (type === "shell.chunk" && pending) {
867
+ pending.sawChunk = true;
868
+ pending.onData(Buffer.from(String(event.chunk ?? "")));
869
+ return;
870
+ }
871
+ if (type === "shell.end" && pending) {
872
+ const output = String(event.output ?? "");
873
+ if (output && !pending.sawChunk)
874
+ pending.onData(Buffer.from(output));
875
+ const index = this.pendingShells.indexOf(pending);
876
+ if (index !== -1)
877
+ this.pendingShells.splice(index, 1);
878
+ pending.resolve({ exitCode: typeof event.exitCode === "number" ? event.exitCode : event.isError === true ? 1 : 0 });
879
+ }
880
+ }
881
+ settlePendingCompaction(eventValue) {
882
+ const event = recordOf(eventValue);
883
+ if (!event)
884
+ return;
885
+ const type = String(event.type ?? "");
886
+ if (type !== "compaction_end" || this.pendingCompactions.length === 0)
887
+ return;
888
+ const pending = this.pendingCompactions.shift();
889
+ const result = recordOf(event.result);
890
+ if (result)
891
+ pending.resolve(result);
892
+ else if (event.aborted === true)
893
+ pending.reject(new Error("Compaction aborted on the worker."));
894
+ else
895
+ pending.resolve({});
896
+ }
897
+ }
898
+
899
+ class RigRemoteAgentSession extends PiAgentSession {
900
+ remote;
901
+ constructor(config, remote) {
902
+ super(config);
903
+ this.remote = remote;
904
+ remote.bindSession(this);
905
+ }
906
+ handleRemoteSessionEvent(eventValue) {
907
+ const event = recordOf(eventValue);
908
+ if (!event)
909
+ return;
910
+ const type = String(event.type ?? "");
911
+ if ((type === "message_end" || type === "turn_end") && recordOf(event.message)) {
912
+ this.agent.state.messages = [...this.agent.state.messages, event.message];
913
+ }
914
+ this._emit(eventValue);
915
+ }
916
+ replaceRemoteMessages(messages) {
917
+ this.agent.state.messages = messages;
918
+ }
919
+ async expandLocalInput(text) {
920
+ let current = text;
921
+ const runner = this.extensionRunner;
922
+ if (runner.hasHandlers("input")) {
923
+ const result = await runner.emitInput(current, undefined, "interactive");
924
+ if (result.action === "handled")
925
+ return null;
926
+ if (result.action === "transform")
927
+ current = result.text;
928
+ }
929
+ current = this._expandSkillCommand(current);
930
+ current = expandPromptTemplate(current, [...this.promptTemplates]);
931
+ return current;
932
+ }
933
+ async prompt(text, options) {
934
+ const trimmed = text.trim();
935
+ if (!trimmed)
936
+ return;
937
+ if (trimmed.startsWith("/")) {
938
+ if (await this._tryExecuteExtensionCommand(trimmed))
939
+ return;
940
+ const expanded2 = await this.expandLocalInput(trimmed);
941
+ if (expanded2 === null)
942
+ return;
943
+ if (expanded2 !== trimmed) {
944
+ const behavior2 = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
945
+ options?.preflightResult?.(true);
946
+ await this.remote.sendPrompt(expanded2, behavior2);
947
+ return;
948
+ }
949
+ await this.remote.sendCommand(trimmed);
950
+ return;
951
+ }
952
+ if (trimmed.startsWith("!")) {
953
+ await this.remote.sendShell(trimmed);
954
+ return;
955
+ }
956
+ const expanded = await this.expandLocalInput(trimmed);
957
+ if (expanded === null)
958
+ return;
959
+ const behavior = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
960
+ options?.preflightResult?.(true);
961
+ await this.remote.sendPrompt(expanded, behavior);
962
+ }
963
+ async steer(text) {
964
+ const trimmed = text.trim();
965
+ if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
966
+ return;
967
+ const expanded = await this.expandLocalInput(trimmed);
968
+ if (expanded === null)
969
+ return;
970
+ await this.remote.sendPrompt(expanded, "steer");
971
+ }
972
+ async followUp(text) {
973
+ const trimmed = text.trim();
974
+ if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
975
+ return;
976
+ const expanded = await this.expandLocalInput(trimmed);
977
+ if (expanded === null)
978
+ return;
979
+ await this.remote.sendPrompt(expanded, "followUp");
980
+ }
981
+ async abort() {
982
+ await this.remote.abort();
983
+ }
984
+ async compact(customInstructions) {
985
+ const pending = new Promise((resolve3, reject) => {
986
+ this.remote.registerPendingCompaction({ resolve: resolve3, reject });
987
+ });
988
+ await this.remote.sendCommand(`/compact${customInstructions ? ` ${customInstructions}` : ""}`);
989
+ return pending;
990
+ }
991
+ clearQueue() {
992
+ const cleared = {
993
+ steering: [...this.remote.status.steeringMessages],
994
+ followUp: [...this.remote.status.followUpMessages]
995
+ };
996
+ this.remote.status.steeringMessages = [];
997
+ this.remote.status.followUpMessages = [];
998
+ this.remote.status.pendingMessageCount = 0;
999
+ this.remote.sendCommand("/queue-clear").catch(() => {});
1000
+ return cleared;
1001
+ }
1002
+ get isStreaming() {
1003
+ return this.remote.status.isStreaming;
1004
+ }
1005
+ get isCompacting() {
1006
+ return this.remote.status.isCompacting;
1007
+ }
1008
+ get isBashRunning() {
1009
+ return this.remote.status.isBashRunning;
1010
+ }
1011
+ get pendingMessageCount() {
1012
+ return this.remote.status.pendingMessageCount;
1013
+ }
1014
+ getSteeringMessages() {
1015
+ return this.remote.status.steeringMessages;
1016
+ }
1017
+ getFollowUpMessages() {
1018
+ return this.remote.status.followUpMessages;
1019
+ }
1020
+ get model() {
1021
+ return this.remote.status.model ?? super.model;
1022
+ }
1023
+ async setModel(model) {
1024
+ await this.remote.sendCommand(`/model ${model.provider}/${model.id}`);
1025
+ this.remote.status.model = model;
1026
+ }
1027
+ get thinkingLevel() {
1028
+ return this.remote.status.thinkingLevel ?? super.thinkingLevel;
1029
+ }
1030
+ setThinkingLevel(level) {
1031
+ this.remote.status.thinkingLevel = level;
1032
+ this.remote.sendCommand(`/thinking ${level}`).catch(() => {});
1033
+ }
1034
+ get sessionName() {
1035
+ return this.remote.status.sessionName ?? super.sessionName;
1036
+ }
1037
+ setSessionName(name) {
1038
+ this.remote.status.sessionName = name;
1039
+ this.remote.sendCommand(`/name ${name}`).catch(() => {});
1040
+ }
1041
+ getSessionStats() {
1042
+ return this.remote.status.stats ?? super.getSessionStats();
1043
+ }
1044
+ getContextUsage() {
1045
+ return this.remote.status.contextUsage ?? super.getContextUsage();
1046
+ }
1047
+ dispose() {
1048
+ this.remote.close();
1049
+ super.dispose();
1050
+ }
1051
+ }
1052
+ function createRemoteBashOperations(controller, excludeFromContext) {
1053
+ return {
1054
+ exec(command, _cwd, execOptions) {
1055
+ return new Promise((resolve3, reject) => {
1056
+ const pending = { onData: execOptions.onData, resolve: resolve3, reject, sawChunk: false };
1057
+ const cleanup = () => {
1058
+ execOptions.signal?.removeEventListener("abort", onAbort);
1059
+ if (timer)
1060
+ clearTimeout(timer);
1061
+ };
1062
+ const onAbort = () => {
1063
+ cleanup();
1064
+ controller.failPendingShell(pending, new Error("Remote worker shell command aborted locally."));
1065
+ };
1066
+ const timeoutMs = typeof execOptions.timeout === "number" && execOptions.timeout > 0 ? execOptions.timeout : 0;
1067
+ const timer = timeoutMs > 0 ? setTimeout(() => {
1068
+ cleanup();
1069
+ controller.failPendingShell(pending, new Error(`Remote worker shell command timed out after ${timeoutMs}ms.`));
1070
+ }, timeoutMs) : null;
1071
+ const wrappedResolve = pending.resolve;
1072
+ const wrappedReject = pending.reject;
1073
+ pending.resolve = (result) => {
1074
+ cleanup();
1075
+ wrappedResolve(result);
1076
+ };
1077
+ pending.reject = (error) => {
1078
+ cleanup();
1079
+ wrappedReject(error);
1080
+ };
1081
+ execOptions.signal?.addEventListener("abort", onAbort, { once: true });
1082
+ controller.registerPendingShell(pending);
1083
+ controller.sendShell(`${excludeFromContext ? "!!" : "!"}${command}`).catch((error) => {
1084
+ cleanup();
1085
+ controller.failPendingShell(pending, error instanceof Error ? error : new Error(String(error)));
1086
+ });
1087
+ });
1088
+ }
1089
+ };
1090
+ }
1091
+
1092
+ // packages/cli/src/commands/_spinner.ts
1093
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1094
+
1095
+ // packages/cli/src/commands/_pi-worker-bridge-extension.ts
1096
+ function recordOf2(value) {
1097
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1098
+ }
1099
+ function asText(value) {
1100
+ if (typeof value === "string")
1101
+ return value;
1102
+ if (value === null || value === undefined)
1103
+ return "";
1104
+ if (typeof value === "number" || typeof value === "boolean")
1105
+ return String(value);
1106
+ try {
1107
+ return JSON.stringify(value);
1108
+ } catch {
1109
+ return String(value);
1110
+ }
1111
+ }
1112
+ function names(value, key = "name") {
1113
+ if (!Array.isArray(value))
1114
+ return [];
1115
+ return value.flatMap((entry) => {
1116
+ if (typeof entry === "string")
1117
+ return [entry];
1118
+ const record = recordOf2(entry);
1119
+ const name = record?.[key];
1120
+ return typeof name === "string" ? [name] : [];
1121
+ });
1122
+ }
1123
+ function renderWorkerCapabilities(capabilities) {
1124
+ const lines = ["Worker session capabilities (in effect for this run)"];
1125
+ const tools = Array.isArray(capabilities.tools) ? capabilities.tools : [];
1126
+ const activeTools = tools.flatMap((tool) => {
1127
+ const record = recordOf2(tool);
1128
+ return record && record.active === true && typeof record.name === "string" ? [record.name] : [];
1129
+ });
1130
+ const inactiveCount = tools.length - activeTools.length;
1131
+ lines.push(` tools ${activeTools.join(", ") || "(none)"}${inactiveCount > 0 ? ` (+${inactiveCount} inactive)` : ""}`);
1132
+ const extensions = Array.isArray(capabilities.extensions) ? capabilities.extensions : [];
1133
+ const extensionLabels = extensions.flatMap((entry) => {
1134
+ const record = recordOf2(entry);
1135
+ if (!record)
1136
+ return [];
1137
+ const path = typeof record.path === "string" ? record.path : "";
1138
+ const short = path.split("/").filter(Boolean).slice(-2).join("/") || path || "extension";
1139
+ return [short];
1140
+ });
1141
+ lines.push(` extensions ${extensionLabels.join(", ") || "(none)"}`);
1142
+ const hookEvents = names(capabilities.hookEvents);
1143
+ const hookList = Array.isArray(capabilities.hookEvents) ? capabilities.hookEvents.map(asText).filter(Boolean) : hookEvents;
1144
+ lines.push(` hooks ${hookList.join(", ") || "(none)"}`);
1145
+ lines.push(` skills ${names(capabilities.skills).join(", ") || "(none)"}`);
1146
+ lines.push(` prompts ${names(capabilities.prompts).join(", ") || "(none)"}`);
1147
+ const model = typeof capabilities.model === "string" ? capabilities.model : "(unknown)";
1148
+ const thinking = typeof capabilities.thinkingLevel === "string" ? capabilities.thinkingLevel : "";
1149
+ const cwd = typeof capabilities.cwd === "string" ? capabilities.cwd : "";
1150
+ lines.push(` model ${model}${thinking ? ` \xB7 ${thinking}` : ""}`);
1151
+ if (cwd)
1152
+ lines.push(` cwd ${cwd}`);
1153
+ lines.push(" (worker commands are in your palette as [worker \u2026] \xB7 /worker shows this again)");
1154
+ return lines;
1155
+ }
1156
+ async function answerExtensionUiRequest(options, ctx, request) {
1157
+ const requestId = String(request.requestId ?? request.id ?? `ui-${Date.now()}`);
1158
+ const method = String(request.method ?? request.type ?? "input");
1159
+ const prompt = asText(request.prompt ?? request.message ?? request.title ?? method);
1160
+ const rawOptions = Array.isArray(request.options) ? request.options : Array.isArray(request.items) ? request.items : [];
1161
+ const choices = rawOptions.map((option) => {
1162
+ const record = recordOf2(option);
1163
+ return record ? asText(record.label ?? record.value ?? record.name ?? option) : asText(option);
1164
+ }).filter(Boolean);
1165
+ try {
1166
+ if (method === "confirm") {
1167
+ const confirmed = await ctx.ui.confirm("Worker request", prompt);
1168
+ await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, { value: confirmed, confirmed });
1169
+ return;
1170
+ }
1171
+ if (choices.length > 0) {
1172
+ const selected = await ctx.ui.select(prompt, choices);
1173
+ await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, selected === undefined ? { cancelled: true } : { value: selected });
1174
+ return;
1175
+ }
1176
+ const value = await ctx.ui.input("Worker request", prompt);
1177
+ await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, value === undefined ? { cancelled: true } : { value });
1178
+ } catch (error) {
1179
+ ctx.ui.notify(`Failed to answer worker UI request: ${error instanceof Error ? error.message : String(error)}`, "error");
1180
+ }
1181
+ }
1182
+ var LOCALLY_OWNED_COMMANDS = new Set(["detach", "quit", "q", "stop", "worker", "settings", "model", "thinking", "compact", "session", "name", "reload"]);
1183
+ function registerDaemonCommands(pi, options, ctx, commands, registered) {
1184
+ for (const command of commands) {
1185
+ const record = recordOf2(command);
1186
+ const name = typeof record?.name === "string" ? record.name : "";
1187
+ const source = typeof record?.source === "string" ? record.source : "worker";
1188
+ if (!name || source === "builtin" || registered.has(name) || LOCALLY_OWNED_COMMANDS.has(name))
1189
+ continue;
1190
+ registered.add(name);
1191
+ const description = typeof record?.description === "string" ? record.description : undefined;
1192
+ try {
1193
+ pi.registerCommand(name, {
1194
+ description: `[worker ${source}] ${description ?? ""}`.trim(),
1195
+ handler: async (args) => {
1196
+ try {
1197
+ const message = await options.controller.sendCommand(`/${name}${args ? ` ${args}` : ""}`);
1198
+ ctx.ui.notify(message, "info");
1199
+ } catch (error) {
1200
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1201
+ }
1202
+ }
1203
+ });
1204
+ } catch {}
1205
+ }
1206
+ }
1207
+ function createRigWorkerPiBridgeExtension(options) {
1208
+ return (pi) => {
1209
+ const registeredDaemonCommands = new Set;
1210
+ let capabilityLines = null;
1211
+ let statusText = "connecting to worker session";
1212
+ let busy = true;
1213
+ let connected = false;
1214
+ let frame = 0;
1215
+ let spinnerTimer = null;
1216
+ const renderStatus = (ctx) => {
1217
+ const prefix = busy ? `${SPINNER_FRAMES[frame]} ` : connected ? "\u25CF " : "\u25CB ";
1218
+ ctx.ui.setStatus("rig-worker-pi", `${prefix}${statusText}`);
1219
+ };
1220
+ const setStatus = (ctx, text, isBusy) => {
1221
+ statusText = text;
1222
+ busy = isBusy;
1223
+ renderStatus(ctx);
1224
+ };
1225
+ pi.registerCommand("detach", {
1226
+ description: "Detach from this run; the worker keeps going",
1227
+ handler: async (_args, ctx) => {
1228
+ ctx.ui.notify("Detached locally; the worker Pi daemon continues.", "info");
1229
+ ctx.shutdown();
1230
+ }
1231
+ });
1232
+ pi.registerCommand("stop", {
1233
+ description: "Stop the worker Pi run and detach",
1234
+ handler: async (_args, ctx) => {
1235
+ await options.controller.abort();
1236
+ ctx.ui.notify("Stop requested for the worker Pi daemon.", "info");
1237
+ ctx.shutdown();
1238
+ }
1239
+ });
1240
+ pi.registerCommand("worker", {
1241
+ description: "Show the worker session's real capabilities (tools, extensions, hooks, skills)",
1242
+ handler: async (_args, ctx) => {
1243
+ if (capabilityLines)
1244
+ ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
1245
+ else
1246
+ ctx.ui.notify("Worker capabilities are not available yet (still connecting).", "info");
1247
+ }
1248
+ });
1249
+ pi.on("user_bash", (event) => ({
1250
+ operations: createRemoteBashOperations(options.controller, event.excludeFromContext === true)
1251
+ }));
1252
+ pi.on("session_start", async (_event, ctx) => {
1253
+ ctx.ui.setTitle("Rig \xB7 enriched bundled Pi");
1254
+ setStatus(ctx, "waiting for worker Pi daemon", true);
1255
+ spinnerTimer = setInterval(() => {
1256
+ frame = (frame + 1) % SPINNER_FRAMES.length;
1257
+ if (busy)
1258
+ renderStatus(ctx);
1259
+ }, 150);
1260
+ ctx.ui.notify(`Enriched bundled Pi \u2014 native UI + Rig layers, worker brain. Attached to run ${options.runId}. /worker shows live capabilities \xB7 /detach exits \xB7 /stop cancels.`, "info");
1261
+ if (options.initialMessageSent)
1262
+ ctx.ui.notify("Initial message sent to the worker Pi daemon.", "info");
1263
+ const nativeUi = ctx.ui;
1264
+ options.controller.setUiHooks({
1265
+ onStatusText: (text) => setStatus(ctx, text, !connected),
1266
+ onActivity: (label, detail) => {
1267
+ const active = label !== "idle" && !/complete|ready/i.test(label);
1268
+ setStatus(ctx, [label, detail].filter(Boolean).join(" \u2014 "), active);
1269
+ if (/agent running|tool:/.test(label))
1270
+ ctx.ui.setWidget("rig-worker-capabilities", undefined);
1271
+ },
1272
+ onConnectionChange: (nextConnected) => {
1273
+ connected = nextConnected;
1274
+ setStatus(ctx, nextConnected ? "worker session live" : "worker session disconnected", false);
1275
+ },
1276
+ onError: (message) => ctx.ui.notify(message, "error"),
1277
+ onExtensionUiRequest: (request) => {
1278
+ answerExtensionUiRequest(options, ctx, request);
1279
+ },
1280
+ onCatchUp: (messages, commands, capabilities) => {
1281
+ if (nativeUi.appendSessionMessages)
1282
+ nativeUi.appendSessionMessages(messages);
1283
+ const cwd = options.controller.status.cwd;
1284
+ if (nativeUi.setDisplayCwd && cwd)
1285
+ nativeUi.setDisplayCwd(cwd);
1286
+ registerDaemonCommands(pi, options, ctx, commands, registeredDaemonCommands);
1287
+ if (capabilities) {
1288
+ capabilityLines = renderWorkerCapabilities(capabilities);
1289
+ ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
1290
+ }
1291
+ }
1292
+ });
1293
+ options.controller.connect().catch((error) => {
1294
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1295
+ });
1296
+ });
1297
+ pi.on("session_shutdown", () => {
1298
+ if (spinnerTimer)
1299
+ clearInterval(spinnerTimer);
1300
+ options.controller.close();
1301
+ });
1302
+ };
1303
+ }
1304
+
1305
+ // packages/cli/src/commands/_pi-frontend.ts
1306
+ function setTemporaryEnv(updates) {
1307
+ const previous = new Map;
1308
+ for (const [key, value] of Object.entries(updates)) {
1309
+ previous.set(key, process.env[key]);
1310
+ process.env[key] = value;
1311
+ }
1312
+ return () => {
1313
+ for (const [key, value] of previous) {
1314
+ if (value === undefined)
1315
+ delete process.env[key];
1316
+ else
1317
+ process.env[key] = value;
1318
+ }
1319
+ };
1320
+ }
1321
+ async function attachRunBundledPiFrontend(context, input) {
1322
+ const tempSessionDir = mkdtempSync(join(tmpdir(), "rig-pi-frontend-sessions-"));
1323
+ const restoreEnv = setTemporaryEnv({
1324
+ PI_CODING_AGENT_SESSION_DIR: tempSessionDir,
1325
+ PI_SKIP_VERSION_CHECK: "1",
1326
+ PI_HIDDEN_COMMANDS: "import,fork,clone,tree,new,resume,trust"
1327
+ });
1328
+ const controller = new RigRemoteSessionController({ context, runId: input.runId });
1329
+ const createRemoteRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
1330
+ const services = await createAgentSessionServices({
1331
+ cwd,
1332
+ agentDir,
1333
+ resourceLoaderOptions: {
1334
+ extensionFactories: [
1335
+ createRigWorkerPiBridgeExtension({
1336
+ context,
1337
+ controller,
1338
+ runId: input.runId,
1339
+ initialMessageSent: input.steered === true
1340
+ })
1341
+ ],
1342
+ noExtensions: true,
1343
+ noSkills: true,
1344
+ noPromptTemplates: true,
1345
+ noContextFiles: true
1346
+ }
1347
+ });
1348
+ const created = await createAgentSessionFromServices({
1349
+ services,
1350
+ sessionManager,
1351
+ sessionStartEvent,
1352
+ noTools: "all",
1353
+ sessionFactory: (config) => new RigRemoteAgentSession(config, controller)
1354
+ });
1355
+ return { ...created, services, diagnostics: services.diagnostics };
1356
+ };
1357
+ let detached = false;
1358
+ try {
1359
+ await runPiMain([], {
1360
+ createRuntimeOverride: () => createRemoteRuntime
1361
+ });
1362
+ detached = true;
1363
+ } finally {
1364
+ restoreEnv();
1365
+ controller.close();
1366
+ rmSync(tempSessionDir, { recursive: true, force: true });
1367
+ }
1368
+ let run = { runId: input.runId, status: "unknown" };
1369
+ try {
1370
+ run = await getRunDetailsViaServer(context, input.runId);
1371
+ } catch {}
1372
+ return {
1373
+ run,
1374
+ logs: [],
1375
+ timeline: [],
1376
+ timelineCursor: null,
1377
+ steered: input.steered === true,
1378
+ detached,
1379
+ rendered: "enriched bundled Pi frontend with remote worker session runtime"
1380
+ };
1381
+ }
1382
+
1383
+ // packages/cli/src/commands/_operator-view.ts
1384
+ var TERMINAL_RUN_STATUSES2 = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
338
1385
  function runStatusFromPayload(payload) {
339
1386
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
340
1387
  return String(run.status ?? "unknown").toLowerCase();
@@ -356,56 +1403,268 @@ async function applyOperatorCommand(context, input, deps = {}) {
356
1403
  await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
357
1404
  return { action: "continue", message: "Steering message queued." };
358
1405
  }
359
- async function readOperatorSnapshot(context, runId) {
1406
+ async function readOperatorSnapshot(context, runId, options = {}) {
360
1407
  const run = await getRunDetailsViaServer(context, runId);
361
1408
  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 }) };
1409
+ const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
1410
+ entries: [{
1411
+ id: `timeline-unavailable:${runId}`,
1412
+ type: "timeline_warning",
1413
+ detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
1414
+ createdAt: new Date().toISOString()
1415
+ }],
1416
+ nextCursor: options.timelineCursor ?? null
1417
+ }));
1418
+ const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
1419
+ const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1420
+ const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
1421
+ return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
364
1422
  }
365
1423
  async function attachRunOperatorView(context, input) {
366
1424
  let steered = false;
367
1425
  if (input.message?.trim()) {
368
- await steerRunViaServer(context, input.runId, input.message.trim());
1426
+ await sendRunPiPromptViaServer(context, input.runId, input.message.trim(), "steer").catch(() => steerRunViaServer(context, input.runId, input.message.trim()));
369
1427
  steered = true;
370
1428
  }
1429
+ if (input.follow && !input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
1430
+ return attachRunBundledPiFrontend(context, {
1431
+ runId: input.runId,
1432
+ steered
1433
+ });
1434
+ }
1435
+ const surface = createOperatorSurface({ interactive: input.interactive !== false });
371
1436
  let snapshot = await readOperatorSnapshot(context, input.runId);
372
1437
  if (context.outputMode === "text") {
373
- console.log(snapshot.rendered);
1438
+ surface.renderSnapshot(snapshot);
1439
+ surface.renderTimeline(snapshot.timeline);
1440
+ surface.renderLogs(snapshot.logs);
374
1441
  if (steered)
375
- console.log("Steering message queued.");
1442
+ surface.info("Message submitted to worker Pi.");
376
1443
  }
377
1444
  let detached = false;
378
- let rl = null;
1445
+ let commandInput = null;
379
1446
  if (input.follow && !input.once && context.outputMode === "text") {
380
1447
  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)}`));
1448
+ surface.info("Controls: /user <message>, /stop, /detach");
1449
+ commandInput = surface.attachCommandInput(async (line) => {
1450
+ const result = await applyOperatorCommand(context, { runId: input.runId, line });
1451
+ if (result.message)
1452
+ surface.info(result.message);
1453
+ if (result.action === "detach" || result.action === "stopped") {
1454
+ detached = true;
1455
+ commandInput?.close();
1456
+ }
392
1457
  });
393
1458
  }
394
- let lastRendered = snapshot.rendered;
395
1459
  const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
396
- while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
1460
+ let timelineCursor = snapshot.timelineCursor;
1461
+ while (!detached && !TERMINAL_RUN_STATUSES2.has(runStatusFromPayload(snapshot.run))) {
397
1462
  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
- }
1463
+ snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
1464
+ timelineCursor = snapshot.timelineCursor;
1465
+ surface.renderSnapshot(snapshot);
1466
+ surface.renderTimeline(snapshot.timeline);
1467
+ surface.renderLogs(snapshot.logs);
403
1468
  }
404
- rl?.close();
1469
+ commandInput?.close();
405
1470
  }
406
1471
  return { ...snapshot, steered, detached };
407
1472
  }
408
1473
 
1474
+ // packages/cli/src/commands/_cli-format.ts
1475
+ import { log, note } from "@clack/prompts";
1476
+ import pc from "picocolors";
1477
+ function stringField(record, key, fallback = "") {
1478
+ const value = record[key];
1479
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
1480
+ }
1481
+ function rawObject(record) {
1482
+ const raw = record.raw;
1483
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
1484
+ }
1485
+ function truncate(value, width) {
1486
+ if (value.length <= width)
1487
+ return value;
1488
+ if (width <= 1)
1489
+ return "\u2026";
1490
+ return `${value.slice(0, width - 1)}\u2026`;
1491
+ }
1492
+ function pad(value, width) {
1493
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
1494
+ }
1495
+ function statusColor(status) {
1496
+ const normalized = status.toLowerCase();
1497
+ if (["completed", "merged", "closed", "done", "accepted", "pass", "selected", "approved"].includes(normalized))
1498
+ return pc.green;
1499
+ if (["failed", "needs_attention", "needs-attention", "blocked", "error", "rejected"].includes(normalized))
1500
+ return pc.red;
1501
+ if (["running", "reviewing", "validating", "in_progress", "in-progress", "remote"].includes(normalized))
1502
+ return pc.cyan;
1503
+ if (["ready", "open", "queued", "created", "preparing", "local", "pending"].includes(normalized))
1504
+ return pc.yellow;
1505
+ return pc.dim;
1506
+ }
1507
+ function compactDate(value) {
1508
+ if (!value.trim())
1509
+ return "";
1510
+ const parsed = Date.parse(value);
1511
+ if (!Number.isFinite(parsed))
1512
+ return value;
1513
+ return new Date(parsed).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
1514
+ }
1515
+ function firstString(record, keys, fallback = "") {
1516
+ for (const key of keys) {
1517
+ const value = stringField(record, key);
1518
+ if (value)
1519
+ return value;
1520
+ }
1521
+ return fallback;
1522
+ }
1523
+ function runIdOf(run) {
1524
+ return firstString(run, ["runId", "id"], "(unknown-run)");
1525
+ }
1526
+ function taskIdOf(run) {
1527
+ return firstString(run, ["taskId", "task", "task_id"]);
1528
+ }
1529
+ function runTitleOf(run) {
1530
+ return firstString(run, ["title", "summary", "name"], taskIdOf(run) || "(untitled)");
1531
+ }
1532
+ function shouldUseClackOutput() {
1533
+ return Boolean(process.stdout.isTTY) && process.env.RIG_CLI_PLAIN_HELP !== "1";
1534
+ }
1535
+ function printFormattedOutput(message, options = {}) {
1536
+ if (!shouldUseClackOutput()) {
1537
+ console.log(message);
1538
+ return;
1539
+ }
1540
+ if (options.title)
1541
+ note(message, options.title);
1542
+ else
1543
+ log.message(message);
1544
+ }
1545
+ function formatStatusPill(status) {
1546
+ const label = status || "unknown";
1547
+ return statusColor(label)(`\u25CF ${label}`);
1548
+ }
1549
+ function formatSection(title, subtitle) {
1550
+ return `${pc.bold(pc.cyan("\u25C6"))} ${pc.bold(title)}${subtitle ? pc.dim(` \u2014 ${subtitle}`) : ""}`;
1551
+ }
1552
+ function formatSuccessCard(title, rows = []) {
1553
+ 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}`);
1554
+ return [formatSection(title), ...body].join(`
1555
+ `);
1556
+ }
1557
+ function formatNextSteps(steps) {
1558
+ if (steps.length === 0)
1559
+ return [];
1560
+ return [pc.bold("Next"), ...steps.map((step) => `${pc.dim("\u203A")} ${step}`)];
1561
+ }
1562
+ function formatRunList(runs, options = {}) {
1563
+ if (runs.length === 0) {
1564
+ return [
1565
+ formatSection("Runs", "none recorded"),
1566
+ options.source === "server" ? pc.dim("No runs recorded on the selected Rig server.") : pc.dim("No runs recorded in .rig/runs."),
1567
+ "",
1568
+ ...formatNextSteps(["Start one: `rig task run --next`", "Check server: `rig server status`"])
1569
+ ].join(`
1570
+ `);
1571
+ }
1572
+ const rows = runs.map((run) => {
1573
+ const runId = stringField(run, "runId", stringField(run, "id", "(unknown-run)"));
1574
+ const status = stringField(run, "status", "unknown");
1575
+ const taskId = stringField(run, "taskId", "");
1576
+ const title = stringField(run, "title", taskId || "(untitled)");
1577
+ const runtime = stringField(run, "runtimeAdapter", "");
1578
+ return { runId, status, title, runtime };
1579
+ });
1580
+ const idWidth = Math.min(36, Math.max(6, ...rows.map((row) => row.runId.length)));
1581
+ const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
1582
+ const header = `${pc.bold(pad("RUN", idWidth))} ${pc.bold(pad("STATUS", statusWidth))} ${pc.bold("TITLE")}`;
1583
+ const body = rows.map((row) => [
1584
+ pc.bold(pad(truncate(row.runId, idWidth), idWidth)),
1585
+ statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
1586
+ `${row.title}${row.runtime ? pc.dim(` ${row.runtime}`) : ""}`
1587
+ ].join(" "));
1588
+ 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(`
1589
+ `);
1590
+ }
1591
+ function formatRunCard(run, options = {}) {
1592
+ const raw = rawObject(run);
1593
+ const merged = { ...raw, ...run };
1594
+ const runId = runIdOf(merged);
1595
+ const status = firstString(merged, ["status"], "unknown");
1596
+ const taskId = taskIdOf(merged);
1597
+ const title = runTitleOf(merged);
1598
+ const runtime = firstString(merged, ["runtimeAdapter", "runtime", "adapter"]);
1599
+ const mode = firstString(merged, ["runtimeMode", "mode"]);
1600
+ const interaction = firstString(merged, ["interactionMode"]);
1601
+ const created = compactDate(firstString(merged, ["createdAt"]));
1602
+ const started = compactDate(firstString(merged, ["startedAt"]));
1603
+ const updated = compactDate(firstString(merged, ["updatedAt"]));
1604
+ const completed = compactDate(firstString(merged, ["completedAt", "finishedAt"]));
1605
+ const worktree = firstString(merged, ["worktreePath", "cwd", "projectRoot"]);
1606
+ const piSession = merged.piSession && typeof merged.piSession === "object" && !Array.isArray(merged.piSession) ? firstString(merged.piSession, ["sessionId", "id"]) : "";
1607
+ const timeline = Array.isArray(merged.timeline) ? merged.timeline.length : null;
1608
+ const approvals = Array.isArray(merged.approvals) ? merged.approvals.length : null;
1609
+ const inputs = Array.isArray(merged.userInputs) ? merged.userInputs.length : null;
1610
+ const rows = [
1611
+ ["run", pc.bold(runId)],
1612
+ ["status", formatStatusPill(status)],
1613
+ ["task", taskId],
1614
+ ["title", title],
1615
+ ["runtime", [runtime, mode, interaction].filter(Boolean).join(" \xB7 ")],
1616
+ ["created", created],
1617
+ ["started", started],
1618
+ ["updated", updated],
1619
+ ["completed", completed],
1620
+ ["worktree", worktree],
1621
+ ["pi", piSession],
1622
+ ["timeline", timeline],
1623
+ ["approvals", approvals],
1624
+ ["inputs", inputs]
1625
+ ];
1626
+ return [
1627
+ formatSuccessCard(options.title ?? "Run details", rows),
1628
+ "",
1629
+ ...formatNextSteps([`Follow live: \`rig run attach ${runId} --follow\``, `Raw payload: \`rig run show ${runId} --raw\``])
1630
+ ].join(`
1631
+ `);
1632
+ }
1633
+ function formatRunStatus(summary, options = {}) {
1634
+ const activeRuns = summary.activeRuns ?? [];
1635
+ const recentRuns = summary.recentRuns ?? [];
1636
+ const lines = [formatSection("Run status", options.source === "server" ? "selected server" : "local state")];
1637
+ lines.push("", pc.bold(`Active runs (${activeRuns.length})`));
1638
+ if (activeRuns.length === 0) {
1639
+ lines.push(pc.dim("No active runs."));
1640
+ } else {
1641
+ for (const run of activeRuns) {
1642
+ lines.push(formatRunSummaryLine(run));
1643
+ }
1644
+ }
1645
+ lines.push("", pc.bold(`Recent runs (${recentRuns.length})`));
1646
+ if (recentRuns.length === 0) {
1647
+ lines.push(pc.dim("No recent terminal runs."));
1648
+ } else {
1649
+ for (const run of recentRuns.slice(0, 10)) {
1650
+ lines.push(formatRunSummaryLine(run));
1651
+ }
1652
+ }
1653
+ lines.push("", ...formatNextSteps(["Start work: `rig task run --next`", "Attach: `rig run attach <run-id> --follow`", "Details: `rig run show <run-id>`"]));
1654
+ return lines.join(`
1655
+ `);
1656
+ }
1657
+ function formatRunSummaryLine(run) {
1658
+ const record = run;
1659
+ const runId = runIdOf(record);
1660
+ const status = firstString(record, ["status"], "unknown");
1661
+ const taskId = taskIdOf(record);
1662
+ const title = runTitleOf(record);
1663
+ const runtime = firstString(record, ["runtimeAdapter", "runtime", "adapter"]);
1664
+ const descriptor = [taskId, title].filter(Boolean).join(" \xB7 ");
1665
+ return `${pc.dim("\u2502")} ${pc.bold(runId)} ${formatStatusPill(status)} ${descriptor}${runtime ? pc.dim(` ${runtime}`) : ""}`;
1666
+ }
1667
+
409
1668
  // packages/cli/src/commands/run.ts
410
1669
  function normalizeRemoteRunDetails(payload) {
411
1670
  const run = payload.run;
@@ -418,6 +1677,25 @@ function normalizeRemoteRunDetails(payload) {
418
1677
  ...Array.isArray(payload.userInputs) ? { userInputs: payload.userInputs } : {}
419
1678
  };
420
1679
  }
1680
+ var REMOTE_TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged"]);
1681
+ function isRemoteConnectionSelected(projectRoot) {
1682
+ return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
1683
+ }
1684
+ async function listRunsForSelectedConnection(context, options = {}) {
1685
+ if (isRemoteConnectionSelected(context.projectRoot)) {
1686
+ return { runs: await listRunsViaServer(context, options), source: "server" };
1687
+ }
1688
+ return { runs: listAuthorityRuns(context.projectRoot), source: "local" };
1689
+ }
1690
+ function runStringField(run, key, fallback = "") {
1691
+ const value = run[key];
1692
+ return typeof value === "string" && value.trim() ? value : fallback;
1693
+ }
1694
+ function buildServerRunStatus(runs) {
1695
+ const activeRuns = runs.filter((run) => !REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
1696
+ const recentRuns = runs.filter((run) => REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
1697
+ return { activeRuns, recentRuns, runs };
1698
+ }
421
1699
  function shouldPromptForEpicSelection(context, command, promptEpic, noEpicPrompt) {
422
1700
  if (noEpicPrompt) {
423
1701
  return false;
@@ -479,21 +1757,15 @@ async function promptForEpicSelection(projectRoot, command) {
479
1757
  }
480
1758
  async function executeRun(context, args) {
481
1759
  const [command = "status", ...rest] = args;
482
- const runtimeContext = loadRuntimeContextFromEnv2() ?? undefined;
1760
+ const runtimeContext = loadRuntimeContextFromEnv() ?? undefined;
483
1761
  switch (command) {
484
1762
  case "list": {
485
- requireNoExtraArgs(rest, "bun run rig run list");
486
- const runs = listAuthorityRuns(context.projectRoot);
1763
+ requireNoExtraArgs(rest, "rig run list");
1764
+ const { runs, source } = await listRunsForSelectedConnection(context, { limit: 100 });
487
1765
  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
- }
1766
+ printFormattedOutput(formatRunList(runs, { source }));
495
1767
  }
496
- return { ok: true, group: "run", command, details: { runs } };
1768
+ return { ok: true, group: "run", command, details: { runs, source } };
497
1769
  }
498
1770
  case "delete": {
499
1771
  let pending = rest;
@@ -501,7 +1773,7 @@ async function executeRun(context, args) {
501
1773
  pending = run.rest;
502
1774
  const purgeArtifacts = takeFlag(pending, "--purge-artifacts");
503
1775
  pending = purgeArtifacts.rest;
504
- requireNoExtraArgs(pending, "bun run rig run delete --run <id> [--purge-artifacts]");
1776
+ requireNoExtraArgs(pending, "rig run delete --run <id> [--purge-artifacts]");
505
1777
  if (!run.value) {
506
1778
  throw new CliError2("run delete requires --run <id>.");
507
1779
  }
@@ -531,7 +1803,7 @@ async function executeRun(context, args) {
531
1803
  pending = keepRuntimes.rest;
532
1804
  const keepQueue = takeFlag(pending, "--keep-queue");
533
1805
  pending = keepQueue.rest;
534
- requireNoExtraArgs(pending, "bun run rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
1806
+ requireNoExtraArgs(pending, "rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
535
1807
  if (!all.value) {
536
1808
  throw new CliError2("run cleanup currently requires --all.");
537
1809
  }
@@ -550,20 +1822,25 @@ async function executeRun(context, args) {
550
1822
  }
551
1823
  case "show": {
552
1824
  let pending = rest;
1825
+ const rawResult = takeFlag(pending, "--raw");
1826
+ pending = rawResult.rest;
553
1827
  const run = takeOption(pending, "--run");
554
1828
  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>.");
1829
+ const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
1830
+ const extra = positionalRunId ? pending.slice(1) : pending;
1831
+ requireNoExtraArgs(extra, "rig run show <id>|--run <id> [--raw]");
1832
+ const runId = run.value ?? positionalRunId;
1833
+ if (!runId) {
1834
+ throw new CliError2("run show requires a run id.");
558
1835
  }
559
- const record = readAuthorityRun(context.projectRoot, run.value) ?? normalizeRemoteRunDetails(await getRunDetailsViaServer(context, run.value).catch(() => ({})));
1836
+ const record = readAuthorityRun(context.projectRoot, runId) ?? normalizeRemoteRunDetails(await getRunDetailsViaServer(context, runId).catch(() => ({})));
560
1837
  if (!record) {
561
- throw new CliError2(`Run not found: ${run.value}`, 2);
1838
+ throw new CliError2(`Run not found: ${runId}`, 2);
562
1839
  }
563
1840
  if (context.outputMode === "text") {
564
- console.log(JSON.stringify(record, null, 2));
1841
+ printFormattedOutput(rawResult.value ? JSON.stringify(record, null, 2) : formatRunCard(record));
565
1842
  }
566
- return { ok: true, group: "run", command, details: record };
1843
+ return { ok: true, group: "run", command, details: { ...record, rawOutput: rawResult.value } };
567
1844
  }
568
1845
  case "timeline": {
569
1846
  let pending = rest;
@@ -571,38 +1848,28 @@ async function executeRun(context, args) {
571
1848
  pending = run.rest;
572
1849
  const follow = takeFlag(pending, "--follow");
573
1850
  pending = follow.rest;
574
- requireNoExtraArgs(pending, "bun run rig run timeline --run <id> [--follow]");
1851
+ requireNoExtraArgs(pending, "rig run timeline --run <id> [--follow]");
575
1852
  if (!run.value) {
576
1853
  throw new CliError2("run timeline requires --run <id>.");
577
1854
  }
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();
1855
+ const renderer = createPiRunStreamRenderer();
1856
+ let cursor = null;
1857
+ const page = await getRunTimelineViaServer(context, run.value, { limit: 500 });
1858
+ const events = Array.isArray(page.entries) ? page.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1859
+ cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
1860
+ if (context.outputMode === "text") {
1861
+ renderer.renderTimeline(events);
1862
+ }
589
1863
  if (follow.value && context.outputMode === "text") {
590
- let lastLength = existsSync3(timelinePath) ? readFileSync3(timelinePath, "utf8").length : 0;
591
1864
  while (true) {
592
1865
  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
- }
1866
+ const nextPage = await getRunTimelineViaServer(context, run.value, { limit: 500, ...cursor ? { cursor } : {} });
1867
+ const nextEvents = Array.isArray(nextPage.entries) ? nextPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1868
+ cursor = typeof nextPage.nextCursor === "string" ? nextPage.nextCursor : cursor;
1869
+ renderer.renderTimeline(nextEvents);
603
1870
  }
604
1871
  }
605
- return { ok: true, group: "run", command, details: { runId: run.value, events } };
1872
+ return { ok: true, group: "run", command, details: { runId: run.value, events, cursor } };
606
1873
  }
607
1874
  case "attach": {
608
1875
  let pending = rest;
@@ -618,41 +1885,38 @@ async function executeRun(context, args) {
618
1885
  pending = pollMs.rest;
619
1886
  const positionalRunId = pending.length > 0 ? pending[0] : undefined;
620
1887
  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>]");
1888
+ requireNoExtraArgs(extra, "rig run attach <run-id>|--run <run-id> [--message <text>] [--once|--follow] [--poll-ms <ms>]");
622
1889
  const runId = runOption.value ?? positionalRunId;
623
1890
  if (!runId) {
624
1891
  throw new CliError2("run attach requires a run id.", 2);
625
1892
  }
1893
+ let steered = false;
1894
+ if (messageOption.value?.trim()) {
1895
+ await steerRunViaServer(context, runId, messageOption.value.trim());
1896
+ steered = true;
1897
+ }
626
1898
  const attached = await attachRunOperatorView(context, {
627
1899
  runId,
628
- message: messageOption.value ?? null,
1900
+ message: null,
629
1901
  once: once.value,
630
1902
  follow: follow.value,
631
1903
  pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)
632
1904
  });
633
- return { ok: true, group: "run", command, details: attached };
1905
+ return { ok: true, group: "run", command, details: { ...attached, steered: attached.steered || steered } };
634
1906
  }
635
1907
  case "status": {
636
- requireNoExtraArgs(rest, "bun run rig run status");
1908
+ requireNoExtraArgs(rest, "rig run status");
637
1909
  if (context.dryRun) {
638
1910
  if (context.outputMode === "text") {
639
1911
  console.log("[dry-run] rig run status");
640
1912
  }
641
1913
  return { ok: true, group: "run", command };
642
1914
  }
643
- const summary = runStatus(context.projectRoot, runtimeContext);
1915
+ const summary = isRemoteConnectionSelected(context.projectRoot) ? buildServerRunStatus(await listRunsViaServer(context, { limit: 100 })) : runStatus(context.projectRoot, runtimeContext);
1916
+ const activeRuns = Array.isArray(summary.activeRuns) ? summary.activeRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
1917
+ const recentRuns = Array.isArray(summary.recentRuns) ? summary.recentRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
644
1918
  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
- }
1919
+ printFormattedOutput(formatRunStatus({ activeRuns, recentRuns, runs: Array.isArray(summary.runs) ? summary.runs : [...activeRuns, ...recentRuns] }, { source: isRemoteConnectionSelected(context.projectRoot) ? "server" : "local" }));
656
1920
  }
657
1921
  return { ok: true, group: "run", command, details: summary };
658
1922
  }
@@ -676,7 +1940,7 @@ async function executeRun(context, args) {
676
1940
  pending = pollResult.rest;
677
1941
  const noServerResult = takeFlag(pending, "--no-server");
678
1942
  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]");
1943
+ 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
1944
  if (promptEpicResult.value && noEpicPromptResult.value) {
681
1945
  throw new CliError2("Cannot use --prompt-epic and --no-epic-prompt together.");
682
1946
  }
@@ -724,7 +1988,7 @@ async function executeRun(context, args) {
724
1988
  };
725
1989
  }
726
1990
  case "resume": {
727
- requireNoExtraArgs(rest, "bun run rig run resume");
1991
+ requireNoExtraArgs(rest, "rig run resume");
728
1992
  if (context.dryRun) {
729
1993
  if (context.outputMode === "text") {
730
1994
  console.log("[dry-run] rig run resume");
@@ -738,7 +2002,7 @@ async function executeRun(context, args) {
738
2002
  return { ok: true, group: "run", command, details: resumed };
739
2003
  }
740
2004
  case "restart": {
741
- requireNoExtraArgs(rest, "bun run rig run restart");
2005
+ requireNoExtraArgs(rest, "rig run restart");
742
2006
  if (context.dryRun) {
743
2007
  if (context.outputMode === "text") {
744
2008
  console.log("[dry-run] rig run restart");
@@ -751,11 +2015,38 @@ async function executeRun(context, args) {
751
2015
  }
752
2016
  return { ok: true, group: "run", command, details: restarted };
753
2017
  }
2018
+ case "steer": {
2019
+ const runOption = takeOption(rest, "--run");
2020
+ const messageOption = takeOption(runOption.rest, "--message");
2021
+ const shortMessageOption = takeOption(messageOption.rest, "-m");
2022
+ const positionalRunId = shortMessageOption.rest.length > 0 ? shortMessageOption.rest[0] : undefined;
2023
+ const extra = positionalRunId ? shortMessageOption.rest.slice(1) : shortMessageOption.rest;
2024
+ requireNoExtraArgs(extra, "rig run steer [<run-id>|--run <id>] --message <text>");
2025
+ const runId = runOption.value ?? positionalRunId;
2026
+ const message = messageOption.value ?? shortMessageOption.value;
2027
+ if (!runId) {
2028
+ throw new CliError2("run steer requires a run id (positional or --run <id>).", 2);
2029
+ }
2030
+ if (!message?.trim()) {
2031
+ throw new CliError2("run steer requires --message <text>.", 2);
2032
+ }
2033
+ if (context.dryRun) {
2034
+ if (context.outputMode === "text") {
2035
+ console.log(`[dry-run] rig run steer ${runId} --message ${JSON.stringify(message)}`);
2036
+ }
2037
+ return { ok: true, group: "run", command, details: { runId, dryRun: true } };
2038
+ }
2039
+ await steerRunViaServer(context, runId, message.trim());
2040
+ if (context.outputMode === "text") {
2041
+ console.log(`Steering message queued for ${runId}.`);
2042
+ }
2043
+ return { ok: true, group: "run", command, details: { runId, queued: true } };
2044
+ }
754
2045
  case "stop": {
755
2046
  const runOption = takeOption(rest, "--run");
756
2047
  const positionalRunId = runOption.rest.length > 0 ? runOption.rest[0] : undefined;
757
2048
  const extra = positionalRunId ? runOption.rest.slice(1) : runOption.rest;
758
- requireNoExtraArgs(extra, "bun run rig run stop [<run-id>|--run <id>]");
2049
+ requireNoExtraArgs(extra, "rig run stop [<run-id>|--run <id>]");
759
2050
  const runId = runOption.value ?? positionalRunId;
760
2051
  if (context.dryRun) {
761
2052
  return {