@h-rig/cli 0.0.6-alpha.7 → 0.0.6-alpha.71

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 (53) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/rig.js +4507 -1506
  3. package/dist/src/commands/_async-ui.js +152 -0
  4. package/dist/src/commands/_authority-runs.js +2 -3
  5. package/dist/src/commands/_cli-format.js +369 -0
  6. package/dist/src/commands/_connection-state.js +30 -11
  7. package/dist/src/commands/_doctor-checks.js +177 -43
  8. package/dist/src/commands/_help-catalog.js +485 -0
  9. package/dist/src/commands/_json-output.js +56 -0
  10. package/dist/src/commands/_operator-surface.js +220 -0
  11. package/dist/src/commands/_operator-view.js +595 -72
  12. package/dist/src/commands/_parsers.js +18 -11
  13. package/dist/src/commands/_pi-frontend.js +411 -0
  14. package/dist/src/commands/_pi-install.js +4 -3
  15. package/dist/src/commands/_policy.js +12 -5
  16. package/dist/src/commands/_preflight.js +187 -127
  17. package/dist/src/commands/_run-driver-helpers.js +75 -22
  18. package/dist/src/commands/_run-replay.js +142 -0
  19. package/dist/src/commands/_server-client.js +343 -60
  20. package/dist/src/commands/_snapshot-upload.js +160 -38
  21. package/dist/src/commands/_spinner.js +65 -0
  22. package/dist/src/commands/_task-picker.js +44 -16
  23. package/dist/src/commands/agent.js +39 -20
  24. package/dist/src/commands/browser.js +28 -21
  25. package/dist/src/commands/connect.js +146 -33
  26. package/dist/src/commands/dist.js +19 -12
  27. package/dist/src/commands/doctor.js +304 -44
  28. package/dist/src/commands/github.js +301 -52
  29. package/dist/src/commands/inbox.js +679 -72
  30. package/dist/src/commands/init.js +622 -118
  31. package/dist/src/commands/inspect.js +515 -32
  32. package/dist/src/commands/inspector.js +20 -13
  33. package/dist/src/commands/pi.js +177 -0
  34. package/dist/src/commands/plugin.js +95 -27
  35. package/dist/src/commands/profile-and-review.js +26 -19
  36. package/dist/src/commands/queue.js +32 -12
  37. package/dist/src/commands/remote.js +43 -36
  38. package/dist/src/commands/repo-git-harness.js +22 -15
  39. package/dist/src/commands/run.js +1162 -158
  40. package/dist/src/commands/server.js +373 -56
  41. package/dist/src/commands/setup.js +316 -62
  42. package/dist/src/commands/stats.js +1030 -0
  43. package/dist/src/commands/task-report-bug.js +29 -22
  44. package/dist/src/commands/task-run-driver.js +862 -129
  45. package/dist/src/commands/task.js +1423 -311
  46. package/dist/src/commands/test.js +15 -8
  47. package/dist/src/commands/workspace.js +18 -11
  48. package/dist/src/commands.js +4446 -1499
  49. package/dist/src/index.js +4502 -1504
  50. package/dist/src/launcher.js +77 -13
  51. package/dist/src/report-bug.js +3 -3
  52. package/dist/src/runner.js +16 -22
  53. package/package.json +10 -5
@@ -1,17 +1,22 @@
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
- import { CliError } from "@rig/runtime/control-plane/errors";
7
+ import { CliError as RuntimeCliError } 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
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
10
+
11
+ class CliError extends RuntimeCliError {
12
+ hint;
13
+ constructor(message, exitCode = 1, options = {}) {
14
+ super(message, exitCode);
15
+ if (options.hint?.trim()) {
16
+ this.hint = options.hint.trim();
17
+ }
18
+ }
19
+ }
15
20
  function takeFlag(args, flag) {
16
21
  const rest = [];
17
22
  let value = false;
@@ -32,7 +37,7 @@ function takeOption(args, option) {
32
37
  if (current === option) {
33
38
  const next = args[index + 1];
34
39
  if (!next || next.startsWith("-")) {
35
- throw new CliError(`Missing value for ${option}`);
40
+ throw new CliError(`Missing value for ${option}`, 1, { hint: `Provide a value after ${option}, e.g. \`${option} <value>\`.` });
36
41
  }
37
42
  value = next;
38
43
  index += 1;
@@ -54,23 +59,19 @@ Usage: ${usage}`);
54
59
  // packages/cli/src/commands/run.ts
55
60
  import {
56
61
  listAuthorityRuns,
57
- readAuthorityRun,
58
- readJsonlFile,
59
- resolveAuthorityRunDir
62
+ readAuthorityRun as readAuthorityRun2
60
63
  } from "@rig/runtime/control-plane/authority-files";
61
64
  import {
62
65
  cleanupRunState,
63
66
  deleteRunState,
64
67
  listOpenEpics,
65
68
  resolveDefaultEpic,
66
- runResume,
67
- runRestart,
68
69
  runStatus,
69
70
  runStop,
70
71
  startRun,
71
72
  defaultStartRunOptions
72
73
  } from "@rig/runtime/control-plane/native/run-ops";
73
- import { loadRuntimeContextFromEnv as loadRuntimeContextFromEnv2 } from "@rig/runtime/control-plane/runtime/context";
74
+ import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
74
75
 
75
76
  // packages/cli/src/commands/_parsers.ts
76
77
  function parsePositiveInt(value, option, fallback) {
@@ -79,13 +80,12 @@ function parsePositiveInt(value, option, fallback) {
79
80
  }
80
81
  const parsed = Number.parseInt(value, 10);
81
82
  if (!Number.isFinite(parsed) || parsed <= 0) {
82
- throw new CliError2(`Invalid ${option} value: ${value}`);
83
+ throw new CliError(`Invalid ${option} value: ${value}`, 1, { hint: `Pass a positive integer, e.g. \`${option} 10\`.` });
83
84
  }
84
85
  return parsed;
85
86
  }
86
87
 
87
88
  // packages/cli/src/commands/_server-client.ts
88
- import { spawnSync } from "child_process";
89
89
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
90
90
  import { resolve as resolve2 } from "path";
91
91
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
@@ -112,9 +112,14 @@ function readJsonFile(path) {
112
112
  try {
113
113
  return JSON.parse(readFileSync(path, "utf8"));
114
114
  } catch (error) {
115
- throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
115
+ throw new CliError(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1, { hint: "Fix or delete that file, then re-select a server with `rig server use <alias|local>`." });
116
116
  }
117
117
  }
118
+ function writeJsonFile(path, value) {
119
+ mkdirSync(dirname(path), { recursive: true });
120
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
121
+ `, "utf8");
122
+ }
118
123
  function normalizeConnection(value) {
119
124
  if (!value || typeof value !== "object" || Array.isArray(value))
120
125
  return null;
@@ -155,25 +160,47 @@ function readRepoConnection(projectRoot) {
155
160
  return {
156
161
  selected,
157
162
  project: typeof record.project === "string" ? record.project : undefined,
158
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
163
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
164
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
159
165
  };
160
166
  }
167
+ function writeRepoConnection(projectRoot, state) {
168
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
169
+ }
161
170
  function resolveSelectedConnection(projectRoot, options = {}) {
162
171
  const repo = readRepoConnection(projectRoot);
163
172
  if (!repo)
164
173
  return null;
165
174
  if (repo.selected === "local")
166
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
175
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
167
176
  const global = readGlobalConnections(options);
168
177
  const connection = global.connections[repo.selected];
169
178
  if (!connection) {
170
- throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
179
+ throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
171
180
  }
172
- return { alias: repo.selected, connection };
181
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
182
+ }
183
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
184
+ const repo = readRepoConnection(projectRoot);
185
+ if (!repo)
186
+ return;
187
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
188
+ }
189
+ function isRemoteConnectionSelected(projectRoot) {
190
+ return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
173
191
  }
174
192
 
175
193
  // packages/cli/src/commands/_server-client.ts
176
- var cachedGitHubBearerToken;
194
+ var scopedGitHubBearerTokens = new Map;
195
+ var serverPhaseListener = null;
196
+ function setServerPhaseListener(listener) {
197
+ const previous = serverPhaseListener;
198
+ serverPhaseListener = listener;
199
+ return previous;
200
+ }
201
+ function reportServerPhase(label) {
202
+ serverPhaseListener?.(label);
203
+ }
177
204
  function cleanToken(value) {
178
205
  const trimmed = value?.trim();
179
206
  return trimmed ? trimmed : null;
@@ -190,49 +217,80 @@ function readPrivateRemoteSessionToken(projectRoot) {
190
217
  }
191
218
  }
192
219
  function readGitHubBearerTokenForRemote(projectRoot) {
193
- if (cachedGitHubBearerToken !== undefined)
194
- return cachedGitHubBearerToken;
220
+ const scopedKey = resolve2(projectRoot);
221
+ if (scopedGitHubBearerTokens.has(scopedKey))
222
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
195
223
  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;
224
+ if (privateSession)
225
+ return privateSession;
226
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
227
+ }
228
+ function readStoredGitHubAuthToken(projectRoot) {
229
+ const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
230
+ if (!existsSync2(path))
231
+ return null;
232
+ try {
233
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
234
+ return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
239
+ function readLocalConnectionFallbackToken(projectRoot) {
240
+ return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
212
241
  }
213
242
  async function ensureServerForCli(projectRoot) {
214
243
  try {
215
244
  const selected = resolveSelectedConnection(projectRoot);
216
245
  if (selected?.connection.kind === "remote") {
246
+ reportServerPhase(`Connecting to ${selected.alias}\u2026`);
247
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
248
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
217
249
  return {
218
250
  baseUrl: selected.connection.baseUrl,
219
- authToken: readGitHubBearerTokenForRemote(projectRoot),
220
- connectionKind: "remote"
251
+ authToken,
252
+ connectionKind: "remote",
253
+ serverProjectRoot
221
254
  };
222
255
  }
256
+ reportServerPhase("Starting local Rig server\u2026");
223
257
  const connection = await ensureLocalRigServerConnection(projectRoot);
224
258
  return {
225
259
  baseUrl: connection.baseUrl,
226
- authToken: connection.authToken,
227
- connectionKind: "local"
260
+ authToken: connection.authToken ?? readLocalConnectionFallbackToken(projectRoot),
261
+ connectionKind: "local",
262
+ serverProjectRoot: resolve2(projectRoot)
228
263
  };
229
264
  } catch (error) {
230
265
  if (error instanceof Error) {
231
- throw new CliError2(error.message, 1);
266
+ throw new CliError(error.message, 1);
232
267
  }
233
268
  throw error;
234
269
  }
235
270
  }
271
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
272
+ const repo = readRepoConnection(projectRoot);
273
+ const slug = repo?.project?.trim();
274
+ if (!slug)
275
+ return null;
276
+ try {
277
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
278
+ headers: mergeHeaders(undefined, authToken)
279
+ });
280
+ if (!response.ok)
281
+ return null;
282
+ const payload = await response.json();
283
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
284
+ const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
285
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
286
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
287
+ if (path)
288
+ writeRepoServerProjectRoot(projectRoot, path);
289
+ return path;
290
+ } catch {
291
+ return null;
292
+ }
293
+ }
236
294
  function mergeHeaders(headers, authToken) {
237
295
  const merged = new Headers(headers);
238
296
  if (authToken) {
@@ -255,12 +313,65 @@ function diagnosticMessage(payload) {
255
313
  });
256
314
  return messages.length > 0 ? messages.join("; ") : null;
257
315
  }
316
+ var serverReachabilityCache = new Map;
317
+ async function probeServerReachability(baseUrl, authToken) {
318
+ try {
319
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
320
+ headers: mergeHeaders(undefined, authToken),
321
+ signal: AbortSignal.timeout(1500)
322
+ });
323
+ return response.ok;
324
+ } catch {
325
+ return false;
326
+ }
327
+ }
328
+ function cachedServerReachability(projectRoot, baseUrl, authToken) {
329
+ const key = resolve2(projectRoot);
330
+ const cached = serverReachabilityCache.get(key);
331
+ if (cached)
332
+ return cached;
333
+ const probe = probeServerReachability(baseUrl, authToken);
334
+ serverReachabilityCache.set(key, probe);
335
+ return probe;
336
+ }
337
+ function describeSelectedServer(projectRoot, server) {
338
+ try {
339
+ const selected = resolveSelectedConnection(projectRoot);
340
+ if (selected) {
341
+ return {
342
+ alias: selected.alias,
343
+ target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
344
+ };
345
+ }
346
+ } catch {}
347
+ return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
348
+ }
349
+ async function buildServerFailureContext(projectRoot, server) {
350
+ const { alias, target } = describeSelectedServer(projectRoot, server);
351
+ const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
352
+ const reachability = reachable ? "server is reachable" : "server is unreachable";
353
+ return {
354
+ contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
355
+ hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
356
+ };
357
+ }
258
358
  async function requestServerJson(context, pathname, init = {}) {
259
359
  const server = await ensureServerForCli(context.projectRoot);
260
- const response = await fetch(`${server.baseUrl}${pathname}`, {
261
- ...init,
262
- headers: mergeHeaders(init.headers, server.authToken)
263
- });
360
+ const headers = mergeHeaders(init.headers, server.authToken);
361
+ if (server.serverProjectRoot)
362
+ headers.set("x-rig-project-root", server.serverProjectRoot);
363
+ reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
364
+ let response;
365
+ try {
366
+ response = await fetch(`${server.baseUrl}${pathname}`, {
367
+ ...init,
368
+ headers
369
+ });
370
+ } catch (error) {
371
+ const failure = await buildServerFailureContext(context.projectRoot, server);
372
+ throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
373
+ ${failure.contextLine}`, 1, { hint: failure.hint });
374
+ }
264
375
  const text = await response.text();
265
376
  const payload = text.trim().length > 0 ? (() => {
266
377
  try {
@@ -272,10 +383,20 @@ async function requestServerJson(context, pathname, init = {}) {
272
383
  if (!response.ok) {
273
384
  const diagnostics = diagnosticMessage(payload);
274
385
  const detail = diagnostics ?? (text || response.statusText);
275
- throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
386
+ const failure = await buildServerFailureContext(context.projectRoot, server);
387
+ throw new CliError(`Rig server request failed (${response.status}): ${detail}
388
+ ${failure.contextLine}`, 1, { hint: failure.hint });
276
389
  }
277
390
  return payload;
278
391
  }
392
+ async function listRunsViaServer(context, options = {}) {
393
+ const url = new URL("http://rig.local/api/runs");
394
+ if (options.limit !== undefined)
395
+ url.searchParams.set("limit", String(options.limit));
396
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
397
+ const runs = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.runs) ? payload.runs : [];
398
+ return runs.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
399
+ }
279
400
  async function getRunDetailsViaServer(context, runId) {
280
401
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}`);
281
402
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
@@ -289,6 +410,47 @@ async function getRunLogsViaServer(context, runId, options = {}) {
289
410
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
290
411
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
291
412
  }
413
+ async function getRunTimelineViaServer(context, runId, options = {}) {
414
+ const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
415
+ if (options.limit !== undefined)
416
+ url.searchParams.set("limit", String(options.limit));
417
+ if (options.cursor)
418
+ url.searchParams.set("cursor", options.cursor);
419
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
420
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
421
+ }
422
+ var RESUMABLE_RUN_STATUSES = new Set([
423
+ "created",
424
+ "preparing",
425
+ "running",
426
+ "validating",
427
+ "reviewing",
428
+ "stopped",
429
+ "failed",
430
+ "needs-attention",
431
+ "needs_attention"
432
+ ]);
433
+ async function resumeRunViaServer(context, runId, options) {
434
+ let targetRunId = runId?.trim() || null;
435
+ if (!targetRunId) {
436
+ const candidates = (await listRunsViaServer(context)).filter((run) => RESUMABLE_RUN_STATUSES.has(String(run.status ?? ""))).sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")));
437
+ targetRunId = typeof candidates[0]?.runId === "string" ? candidates[0].runId : null;
438
+ }
439
+ if (!targetRunId) {
440
+ throw new CliError(options.restart ? "No run is available to restart." : "No resumable run is available.", 2, { hint: "List runs with `rig run list`, then pass an explicit id: `rig run resume <run-id>`." });
441
+ }
442
+ const payload = await requestServerJson(context, "/api/runs/resume", {
443
+ method: "POST",
444
+ headers: { "content-type": "application/json" },
445
+ body: JSON.stringify({ runId: targetRunId, createdAt: new Date().toISOString(), restart: options.restart })
446
+ });
447
+ const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
448
+ if (record.ok === false) {
449
+ const message = typeof record.error === "string" && record.error.trim() ? record.error : "run resume failed";
450
+ throw new CliError(`${options.restart ? "restart" : "resume"} failed for ${targetRunId}: ${message}`, 1);
451
+ }
452
+ return { ok: true, runId: targetRunId, ...record };
453
+ }
292
454
  async function stopRunViaServer(context, runId) {
293
455
  const payload = await requestServerJson(context, "/api/runs/stop", {
294
456
  method: "POST",
@@ -305,10 +467,155 @@ async function steerRunViaServer(context, runId, message) {
305
467
  });
306
468
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
307
469
  }
470
+ async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
471
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
472
+ method: "POST",
473
+ headers: { "content-type": "application/json" },
474
+ body: JSON.stringify({ text, streamingBehavior })
475
+ });
476
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
477
+ }
308
478
 
309
- // packages/cli/src/commands/_operator-view.ts
479
+ // packages/cli/src/commands/_run-replay.ts
480
+ import { existsSync as existsSync3, readdirSync } from "fs";
481
+ import { join, resolve as resolve3 } from "path";
482
+ import {
483
+ readAuthorityRun,
484
+ readJsonlFile,
485
+ runJournalPath
486
+ } from "@rig/runtime/control-plane/authority-files";
487
+ function asRecord(value) {
488
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
489
+ }
490
+ function text(value) {
491
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
492
+ }
493
+ function snippet(value, max = 120) {
494
+ const raw = text(value);
495
+ if (!raw)
496
+ return null;
497
+ const flat = raw.replace(/\s+/g, " ");
498
+ return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
499
+ }
500
+ function summarizeLifecycleEntry(entry) {
501
+ const type = text(entry.type) ?? "event";
502
+ switch (type) {
503
+ case "status": {
504
+ const anchor = text(entry.sessionId);
505
+ return [
506
+ `status \u2192 ${text(entry.status) ?? "(unknown)"}`,
507
+ snippet(entry.detail) ? `\u2014 ${snippet(entry.detail)}` : null,
508
+ anchor ? `[session ${anchor}]` : null
509
+ ].filter(Boolean).join(" ");
510
+ }
511
+ case "timeline-entry": {
512
+ const payload = asRecord(entry.payload) ?? {};
513
+ const kind = text(payload.type) ?? "entry";
514
+ const body = snippet(payload.title) ?? snippet(payload.text) ?? snippet(payload.detail);
515
+ const state = text(payload.state);
516
+ return [`timeline ${kind}`, body ? `\u2014 ${body}` : null, state ? `(${state})` : null].filter(Boolean).join(" ");
517
+ }
518
+ case "log-entry": {
519
+ const payload = asRecord(entry.payload) ?? {};
520
+ const title = snippet(payload.title) ?? "log";
521
+ const detail = snippet(payload.detail);
522
+ return [`log ${title}`, detail ? `\u2014 ${detail}` : null].filter(Boolean).join(" ");
523
+ }
524
+ case "record-patch": {
525
+ const patch = asRecord(entry.patch) ?? {};
526
+ const keys = Object.keys(patch);
527
+ const shown = keys.slice(0, 8).join(", ");
528
+ return `record-patch {${shown}${keys.length > 8 ? `, +${keys.length - 8} more` : ""}}`;
529
+ }
530
+ case "timeline":
531
+ return "timeline updated (signal)";
532
+ case "log":
533
+ return `log signal${snippet(entry.title) ? ` \u2014 ${snippet(entry.title)}` : ""}`;
534
+ case "completed":
535
+ return "run completed";
536
+ case "failed":
537
+ return `run failed${snippet(entry.error) ? ` \u2014 ${snippet(entry.error)}` : ""}`;
538
+ default:
539
+ return type;
540
+ }
541
+ }
542
+ function summarizeSessionEntry(entry) {
543
+ const type = text(entry.type) ?? "entry";
544
+ const message = asRecord(entry.message);
545
+ const role = text(message?.role);
546
+ const content = message?.content;
547
+ let body = null;
548
+ if (typeof content === "string") {
549
+ body = snippet(content);
550
+ } else if (Array.isArray(content)) {
551
+ body = snippet(content.map((part) => text(asRecord(part)?.text) ?? "").filter(Boolean).join(" "));
552
+ }
553
+ return [
554
+ `pi ${type}`,
555
+ role ? `(${role})` : null,
556
+ body ? `\u2014 ${body}` : null
557
+ ].filter(Boolean).join(" ");
558
+ }
559
+ function sessionEntryTimestamp(entry) {
560
+ return text(entry.timestamp) ?? text(entry.at) ?? text(entry.createdAt) ?? null;
561
+ }
562
+ function resolveRunSessionFile(record) {
563
+ const piSession = record?.piSession ?? null;
564
+ if (!piSession)
565
+ return null;
566
+ const recorded = text(piSession.sessionFile);
567
+ if (recorded && existsSync3(recorded)) {
568
+ return recorded;
569
+ }
570
+ const cwd = text(piSession.cwd);
571
+ const sessionId = text(piSession.sessionId);
572
+ if (!cwd || !sessionId)
573
+ return null;
574
+ const sessionDir = resolve3(cwd, ".rig", "session");
575
+ try {
576
+ const match = readdirSync(sessionDir).find((name) => name.endsWith(`_${sessionId}.jsonl`));
577
+ return match ? join(sessionDir, match) : null;
578
+ } catch {
579
+ return null;
580
+ }
581
+ }
582
+ function buildRunReplay(projectRoot, runId, options = {}) {
583
+ const logPath = runJournalPath(projectRoot, runId);
584
+ const record = readAuthorityRun(projectRoot, runId);
585
+ const lifecycleEntries = readJsonlFile(logPath).map((entry) => asRecord(entry)).filter((entry) => entry !== null);
586
+ const merged = lifecycleEntries.map((entry) => ({
587
+ at: text(entry.at),
588
+ source: "run",
589
+ summary: summarizeLifecycleEntry(entry)
590
+ }));
591
+ let sessionFile = null;
592
+ let sessionEntryCount = 0;
593
+ if (options.withSession) {
594
+ sessionFile = resolveRunSessionFile(record);
595
+ if (sessionFile) {
596
+ let lastSeenAt = null;
597
+ const sessionLines = readJsonlFile(sessionFile).map((entry) => asRecord(entry)).filter((entry) => entry !== null).map((entry) => {
598
+ lastSeenAt = sessionEntryTimestamp(entry) ?? lastSeenAt;
599
+ return { at: lastSeenAt, source: "session", summary: summarizeSessionEntry(entry) };
600
+ });
601
+ sessionEntryCount = sessionLines.length;
602
+ merged.push(...sessionLines);
603
+ merged.sort((left, right) => (left.at ?? "").localeCompare(right.at ?? ""));
604
+ }
605
+ }
606
+ const lines = merged.map((line) => `${line.at ?? "(no timestamp) "} ${line.source === "run" ? "run " : "session"} ${line.summary}`);
607
+ return {
608
+ runId,
609
+ logPath,
610
+ sessionFile,
611
+ entryCount: lifecycleEntries.length,
612
+ sessionEntryCount,
613
+ lines
614
+ };
615
+ }
616
+
617
+ // packages/cli/src/commands/_operator-surface.ts
310
618
  import { createInterface } from "readline";
311
- var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
312
619
  var CANONICAL_STAGES = [
313
620
  "Connect",
314
621
  "GitHub/task sync",
@@ -323,18 +630,380 @@ var CANONICAL_STAGES = [
323
630
  "Merge",
324
631
  "Complete"
325
632
  ];
633
+ function logDetail(log) {
634
+ return typeof log.detail === "string" ? log.detail.trim() : "";
635
+ }
636
+ function parseProviderProtocolLog(title, detail) {
637
+ if (title.trim().toLowerCase() !== "agent output")
638
+ return null;
639
+ if (!detail.startsWith("{") || !detail.endsWith("}"))
640
+ return null;
641
+ try {
642
+ const record = JSON.parse(detail);
643
+ if (!record || typeof record !== "object" || Array.isArray(record))
644
+ return null;
645
+ const type = record.type;
646
+ return typeof type === "string" && [
647
+ "assistant",
648
+ "message_start",
649
+ "message_update",
650
+ "message_end",
651
+ "stream_event",
652
+ "tool_result",
653
+ "tool_execution_start",
654
+ "tool_execution_update",
655
+ "tool_execution_end",
656
+ "turn_start",
657
+ "turn_end"
658
+ ].includes(type) ? record : null;
659
+ } catch {
660
+ return null;
661
+ }
662
+ }
663
+ function renderProviderProtocolLog(record) {
664
+ const type = typeof record.type === "string" ? record.type : "";
665
+ if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
666
+ const toolName = String(record.toolName ?? record.name ?? "tool");
667
+ 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";
668
+ return `[Pi tool] ${toolName} ${status}`;
669
+ }
670
+ return null;
671
+ }
672
+ function entryId(entry, fallback) {
673
+ return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
674
+ }
326
675
  function renderOperatorSnapshot(snapshot) {
327
676
  const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
328
677
  const runId = String(run.runId ?? run.id ?? "run");
329
678
  const status = String(run.status ?? "unknown");
330
679
  const logs = snapshot.logs ?? [];
680
+ const latestByStage = new Map;
681
+ for (const log of logs) {
682
+ const title = String(log.title ?? "").toLowerCase();
683
+ const stageName = String(log.stage ?? "").toLowerCase();
684
+ const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
685
+ if (stage)
686
+ latestByStage.set(stage, log);
687
+ }
331
688
  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)}`] : [];
689
+ const match = latestByStage.get(stage);
690
+ return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
334
691
  });
335
692
  return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
336
693
  `);
337
694
  }
695
+ function createPiRunStreamRenderer(output = process.stdout) {
696
+ let lastSnapshot = "";
697
+ const assistantTextById = new Map;
698
+ const seenTimeline = new Set;
699
+ const seenLogs = new Set;
700
+ const writeLine = (line) => output.write(`${line}
701
+ `);
702
+ return {
703
+ renderSnapshot(snapshot) {
704
+ const rendered = renderOperatorSnapshot(snapshot);
705
+ if (rendered && rendered !== lastSnapshot) {
706
+ writeLine(rendered);
707
+ lastSnapshot = rendered;
708
+ }
709
+ },
710
+ renderTimeline(entries) {
711
+ for (const [index, entry] of entries.entries()) {
712
+ const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
713
+ if (entry.type === "assistant_message" && typeof entry.text === "string") {
714
+ const text2 = entry.text;
715
+ const previousText = assistantTextById.get(id) ?? "";
716
+ if (!previousText && text2.trim()) {
717
+ writeLine("[Pi assistant]");
718
+ }
719
+ if (text2.startsWith(previousText)) {
720
+ const delta = text2.slice(previousText.length);
721
+ if (delta)
722
+ output.write(delta);
723
+ } else if (text2.trim() && text2 !== previousText) {
724
+ if (previousText)
725
+ writeLine(`
726
+ [Pi assistant]`);
727
+ output.write(text2);
728
+ }
729
+ assistantTextById.set(id, text2);
730
+ continue;
731
+ }
732
+ if (seenTimeline.has(id))
733
+ continue;
734
+ seenTimeline.add(id);
735
+ if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
736
+ writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
737
+ continue;
738
+ }
739
+ if (entry.type === "timeline_warning") {
740
+ writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
741
+ continue;
742
+ }
743
+ if (entry.type === "action") {
744
+ const text2 = String(entry.detail ?? entry.message ?? entry.title ?? "").trim();
745
+ if (text2)
746
+ writeLine(`[Rig action] ${text2}`);
747
+ continue;
748
+ }
749
+ if (entry.type === "user_message") {
750
+ const text2 = String(entry.text ?? entry.message ?? entry.detail ?? "").trim();
751
+ if (text2)
752
+ writeLine(`[Operator] ${text2}`);
753
+ continue;
754
+ }
755
+ const fallback = String(entry.detail ?? entry.message ?? entry.text ?? entry.title ?? "").trim();
756
+ if (fallback)
757
+ writeLine(`[${String(entry.type ?? "timeline")}] ${fallback}`);
758
+ }
759
+ },
760
+ renderLogs(entries) {
761
+ for (const [index, entry] of entries.entries()) {
762
+ const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
763
+ if (seenLogs.has(id))
764
+ continue;
765
+ seenLogs.add(id);
766
+ const title = String(entry.title ?? "");
767
+ if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
768
+ continue;
769
+ const detail = logDetail(entry);
770
+ if (!detail)
771
+ continue;
772
+ const protocolRecord = parseProviderProtocolLog(title, detail);
773
+ if (protocolRecord) {
774
+ const protocolLine = renderProviderProtocolLog(protocolRecord);
775
+ if (protocolLine)
776
+ writeLine(protocolLine);
777
+ continue;
778
+ }
779
+ writeLine(`[${title || "Rig log"}] ${detail}`);
780
+ }
781
+ }
782
+ };
783
+ }
784
+ function createOperatorSurface(options = {}) {
785
+ const input = options.input ?? process.stdin;
786
+ const output = options.output ?? process.stdout;
787
+ const errorOutput = options.errorOutput ?? process.stderr;
788
+ const renderer = createPiRunStreamRenderer(output);
789
+ const writeLine = (line) => output.write(`${line}
790
+ `);
791
+ return {
792
+ mode: "pi-compatible-text",
793
+ ...renderer,
794
+ info: writeLine,
795
+ error: (message) => errorOutput.write(`${message}
796
+ `),
797
+ attachCommandInput(handler) {
798
+ if (options.interactive === false || !input.isTTY)
799
+ return null;
800
+ const rl = createInterface({ input, output: process.stdout, terminal: false });
801
+ rl.on("line", (line) => {
802
+ Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
803
+ });
804
+ return { close: () => rl.close() };
805
+ }
806
+ };
807
+ }
808
+
809
+ // packages/cli/src/commands/_pi-frontend.ts
810
+ import { mkdtempSync, rmSync } from "fs";
811
+ import { tmpdir } from "os";
812
+ import { join as join2 } from "path";
813
+ import { main as runPiMain } from "@earendil-works/pi-coding-agent";
814
+ import createPiRigExtension from "@rig/pi-rig";
815
+ function setTemporaryEnv(updates) {
816
+ const previous = new Map;
817
+ for (const [key, value] of Object.entries(updates)) {
818
+ previous.set(key, process.env[key]);
819
+ process.env[key] = value;
820
+ }
821
+ return () => {
822
+ for (const [key, value] of previous) {
823
+ if (value === undefined)
824
+ delete process.env[key];
825
+ else
826
+ process.env[key] = value;
827
+ }
828
+ };
829
+ }
830
+ function buildOperatorPiEnv(input) {
831
+ return {
832
+ PI_CODING_AGENT_SESSION_DIR: input.sessionDir,
833
+ PI_SKIP_VERSION_CHECK: "1",
834
+ RIG_PI_OPERATOR_SESSION: "1",
835
+ RIG_RUN_ID: input.runId,
836
+ RIG_SERVER_URL: input.serverUrl,
837
+ ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {},
838
+ ...input.serverProjectRoot ? { RIG_PROJECT_ROOT: input.serverProjectRoot } : {}
839
+ };
840
+ }
841
+ async function attachRunBundledPiFrontend(context, input) {
842
+ const tempSessionDir = mkdtempSync(join2(tmpdir(), "rig-pi-frontend-sessions-"));
843
+ const server = await ensureServerForCli(context.projectRoot);
844
+ const restoreEnv = setTemporaryEnv(buildOperatorPiEnv({
845
+ runId: input.runId,
846
+ serverUrl: server.baseUrl,
847
+ authToken: server.authToken,
848
+ serverProjectRoot: server.serverProjectRoot,
849
+ sessionDir: tempSessionDir
850
+ }));
851
+ const piRigExtensionFactory = (pi) => {
852
+ createPiRigExtension(pi);
853
+ };
854
+ let detached = false;
855
+ try {
856
+ await runPiMain([
857
+ "--no-extensions",
858
+ "--no-skills",
859
+ "--no-prompt-templates",
860
+ "--no-context-files"
861
+ ], {
862
+ extensionFactories: [piRigExtensionFactory]
863
+ });
864
+ detached = true;
865
+ } finally {
866
+ restoreEnv();
867
+ rmSync(tempSessionDir, { recursive: true, force: true });
868
+ }
869
+ let run = { runId: input.runId, status: "unknown" };
870
+ try {
871
+ run = await getRunDetailsViaServer(context, input.runId);
872
+ } catch {}
873
+ return {
874
+ run,
875
+ logs: [],
876
+ timeline: [],
877
+ timelineCursor: null,
878
+ steered: input.steered === true,
879
+ detached,
880
+ rendered: "stock Pi operator console with the pi-rig extension"
881
+ };
882
+ }
883
+
884
+ // packages/cli/src/commands/_async-ui.ts
885
+ import pc from "picocolors";
886
+
887
+ // packages/cli/src/commands/_spinner.ts
888
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
889
+ function createTtySpinner(input) {
890
+ const output = input.output ?? process.stdout;
891
+ const isTty = output.isTTY === true;
892
+ const frames = input.frames && input.frames.length > 0 ? input.frames : SPINNER_FRAMES;
893
+ let label = input.label;
894
+ let frame = 0;
895
+ let paused = false;
896
+ let stopped = false;
897
+ let lastPrintedLabel = "";
898
+ const render = () => {
899
+ if (stopped || paused)
900
+ return;
901
+ if (!isTty) {
902
+ if (label !== lastPrintedLabel) {
903
+ output.write(`${label}
904
+ `);
905
+ lastPrintedLabel = label;
906
+ }
907
+ return;
908
+ }
909
+ frame = (frame + 1) % frames.length;
910
+ const glyph = frames[frame] ?? frames[0] ?? "";
911
+ output.write(`\r\x1B[2K${input.styleFrame ? input.styleFrame(glyph) : glyph} ${label}`);
912
+ };
913
+ const clearLine = () => {
914
+ if (isTty)
915
+ output.write("\r\x1B[2K");
916
+ };
917
+ render();
918
+ const timer = isTty ? setInterval(render, input.intervalMs ?? 120) : null;
919
+ return {
920
+ setLabel(next) {
921
+ label = next;
922
+ render();
923
+ },
924
+ pause() {
925
+ paused = true;
926
+ clearLine();
927
+ },
928
+ resume() {
929
+ if (stopped)
930
+ return;
931
+ paused = false;
932
+ render();
933
+ },
934
+ stop(finalLine) {
935
+ if (stopped)
936
+ return;
937
+ stopped = true;
938
+ if (timer)
939
+ clearInterval(timer);
940
+ clearLine();
941
+ if (finalLine)
942
+ output.write(`${finalLine}
943
+ `);
944
+ }
945
+ };
946
+ }
947
+
948
+ // packages/cli/src/commands/_async-ui.ts
949
+ var CLACK_SPINNER_FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
950
+ var DONE_SYMBOL = pc.green("\u25C7");
951
+ var FAIL_SYMBOL = pc.red("\u25A0");
952
+ var activeUpdate = null;
953
+ async function withSpinner(label, work, options = {}) {
954
+ if (options.outputMode === "json") {
955
+ return work(() => {});
956
+ }
957
+ if (activeUpdate) {
958
+ const outer = activeUpdate;
959
+ outer(label);
960
+ return work(outer);
961
+ }
962
+ const output = options.output ?? process.stderr;
963
+ const isTty = output.isTTY === true;
964
+ let lastLabel = label;
965
+ if (!isTty) {
966
+ output.write(`${label}
967
+ `);
968
+ const update2 = (next) => {
969
+ lastLabel = next;
970
+ };
971
+ activeUpdate = update2;
972
+ const previousListener2 = setServerPhaseListener(update2);
973
+ try {
974
+ return await work(update2);
975
+ } finally {
976
+ activeUpdate = null;
977
+ setServerPhaseListener(previousListener2);
978
+ }
979
+ }
980
+ const spinner = createTtySpinner({
981
+ label,
982
+ output,
983
+ frames: CLACK_SPINNER_FRAMES,
984
+ styleFrame: (frame) => pc.magenta(frame)
985
+ });
986
+ const update = (next) => {
987
+ lastLabel = next;
988
+ spinner.setLabel(next);
989
+ };
990
+ activeUpdate = update;
991
+ const previousListener = setServerPhaseListener(update);
992
+ try {
993
+ const result = await work(update);
994
+ spinner.stop(options.doneLabel ? `${DONE_SYMBOL} ${options.doneLabel}` : undefined);
995
+ return result;
996
+ } catch (error) {
997
+ spinner.stop(`${FAIL_SYMBOL} ${lastLabel}`);
998
+ throw error;
999
+ } finally {
1000
+ activeUpdate = null;
1001
+ setServerPhaseListener(previousListener);
1002
+ }
1003
+ }
1004
+
1005
+ // packages/cli/src/commands/_operator-view.ts
1006
+ var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
338
1007
  function runStatusFromPayload(payload) {
339
1008
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
340
1009
  return String(run.status ?? "unknown").toLowerCase();
@@ -356,56 +1025,307 @@ async function applyOperatorCommand(context, input, deps = {}) {
356
1025
  await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
357
1026
  return { action: "continue", message: "Steering message queued." };
358
1027
  }
359
- async function readOperatorSnapshot(context, runId) {
1028
+ async function readOperatorSnapshot(context, runId, options = {}) {
360
1029
  const run = await getRunDetailsViaServer(context, runId);
361
1030
  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 }) };
1031
+ const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
1032
+ entries: [{
1033
+ id: `timeline-unavailable:${runId}`,
1034
+ type: "timeline_warning",
1035
+ detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
1036
+ createdAt: new Date().toISOString()
1037
+ }],
1038
+ nextCursor: options.timelineCursor ?? null
1039
+ }));
1040
+ const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
1041
+ const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1042
+ const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
1043
+ return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
364
1044
  }
365
1045
  async function attachRunOperatorView(context, input) {
366
1046
  let steered = false;
367
- if (input.message?.trim()) {
368
- await steerRunViaServer(context, input.runId, input.message.trim());
1047
+ const attachMessage = input.message?.trim();
1048
+ if (attachMessage) {
1049
+ await withSpinner("Queueing steering message\u2026", () => sendRunPiPromptViaServer(context, input.runId, attachMessage, "steer").catch(() => steerRunViaServer(context, input.runId, attachMessage)), { outputMode: context.outputMode });
369
1050
  steered = true;
370
1051
  }
371
- let snapshot = await readOperatorSnapshot(context, input.runId);
1052
+ if (input.follow && !input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
1053
+ return attachRunBundledPiFrontend(context, {
1054
+ runId: input.runId,
1055
+ steered
1056
+ });
1057
+ }
1058
+ const surface = createOperatorSurface({ interactive: input.interactive !== false });
1059
+ let snapshot = await withSpinner(`Connecting to run ${input.runId}\u2026`, () => readOperatorSnapshot(context, input.runId), { outputMode: context.outputMode });
372
1060
  if (context.outputMode === "text") {
373
- console.log(snapshot.rendered);
1061
+ surface.renderSnapshot(snapshot);
1062
+ surface.renderTimeline(snapshot.timeline);
1063
+ surface.renderLogs(snapshot.logs);
374
1064
  if (steered)
375
- console.log("Steering message queued.");
1065
+ surface.info("Message submitted to worker Pi.");
376
1066
  }
377
1067
  let detached = false;
378
- let rl = null;
1068
+ let commandInput = null;
379
1069
  if (input.follow && !input.once && context.outputMode === "text") {
380
1070
  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)}`));
1071
+ surface.info("Controls: /user <message>, /stop, /detach");
1072
+ commandInput = surface.attachCommandInput(async (line) => {
1073
+ const result = await applyOperatorCommand(context, { runId: input.runId, line });
1074
+ if (result.message)
1075
+ surface.info(result.message);
1076
+ if (result.action === "detach" || result.action === "stopped") {
1077
+ detached = true;
1078
+ commandInput?.close();
1079
+ }
392
1080
  });
393
1081
  }
394
- let lastRendered = snapshot.rendered;
395
1082
  const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
1083
+ let timelineCursor = snapshot.timelineCursor;
396
1084
  while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
397
1085
  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
- }
1086
+ snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
1087
+ timelineCursor = snapshot.timelineCursor;
1088
+ surface.renderSnapshot(snapshot);
1089
+ surface.renderTimeline(snapshot.timeline);
1090
+ surface.renderLogs(snapshot.logs);
403
1091
  }
404
- rl?.close();
1092
+ commandInput?.close();
405
1093
  }
406
1094
  return { ...snapshot, steered, detached };
407
1095
  }
408
1096
 
1097
+ // packages/cli/src/commands/_cli-format.ts
1098
+ import { log, note } from "@clack/prompts";
1099
+ import pc2 from "picocolors";
1100
+ function stringField(record, key, fallback = "") {
1101
+ const value = record[key];
1102
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
1103
+ }
1104
+ function rawObject(record) {
1105
+ const raw = record.raw;
1106
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
1107
+ }
1108
+ function truncate(value, width) {
1109
+ if (value.length <= width)
1110
+ return value;
1111
+ if (width <= 1)
1112
+ return "\u2026";
1113
+ return `${value.slice(0, width - 1)}\u2026`;
1114
+ }
1115
+ function pad(value, width) {
1116
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
1117
+ }
1118
+ function statusColor(status) {
1119
+ const normalized = status.toLowerCase();
1120
+ if (["completed", "merged", "closed", "done", "accepted", "pass", "selected", "approved"].includes(normalized))
1121
+ return pc2.green;
1122
+ if (["failed", "needs_attention", "needs-attention", "blocked", "error", "rejected"].includes(normalized))
1123
+ return pc2.red;
1124
+ if (["running", "reviewing", "validating", "in_progress", "in-progress", "remote"].includes(normalized))
1125
+ return pc2.cyan;
1126
+ if (["ready", "open", "queued", "created", "preparing", "local", "pending"].includes(normalized))
1127
+ return pc2.yellow;
1128
+ return pc2.dim;
1129
+ }
1130
+ function compactDate(value) {
1131
+ if (!value.trim())
1132
+ return "";
1133
+ const parsed = Date.parse(value);
1134
+ if (!Number.isFinite(parsed))
1135
+ return value;
1136
+ return new Date(parsed).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
1137
+ }
1138
+ function firstString(record, keys, fallback = "") {
1139
+ for (const key of keys) {
1140
+ const value = stringField(record, key);
1141
+ if (value)
1142
+ return value;
1143
+ }
1144
+ return fallback;
1145
+ }
1146
+ function runIdOf(run) {
1147
+ return firstString(run, ["runId", "id"], "(unknown-run)");
1148
+ }
1149
+ function taskIdOf(run) {
1150
+ return firstString(run, ["taskId", "task", "task_id"]);
1151
+ }
1152
+ function runTitleOf(run) {
1153
+ return firstString(run, ["title", "summary", "name"], taskIdOf(run) || "(untitled)");
1154
+ }
1155
+ function shouldUseClackOutput() {
1156
+ return Boolean(process.stdout.isTTY) && process.env.RIG_CLI_PLAIN_HELP !== "1";
1157
+ }
1158
+ function printFormattedOutput(message, options = {}) {
1159
+ if (!shouldUseClackOutput()) {
1160
+ console.log(message);
1161
+ return;
1162
+ }
1163
+ if (options.title)
1164
+ note(message, options.title);
1165
+ else
1166
+ log.message(message);
1167
+ }
1168
+ function formatStatusPill(status) {
1169
+ const label = status || "unknown";
1170
+ return statusColor(label)(`\u25CF ${label}`);
1171
+ }
1172
+ function formatSection(title, subtitle) {
1173
+ return `${pc2.bold(pc2.cyan("\u25C6"))} ${pc2.bold(title)}${subtitle ? pc2.dim(` \u2014 ${subtitle}`) : ""}`;
1174
+ }
1175
+ function formatSuccessCard(title, rows = []) {
1176
+ const body = rows.filter(([, value]) => value !== undefined && value !== null && String(value).length > 0).map(([key, value]) => `${pc2.dim("\u2502")} ${pc2.dim(key.padEnd(12))} ${value}`);
1177
+ return [formatSection(title), ...body].join(`
1178
+ `);
1179
+ }
1180
+ function formatNextSteps(steps) {
1181
+ if (steps.length === 0)
1182
+ return [];
1183
+ return [pc2.bold("Next"), ...steps.map((step) => `${pc2.dim("\u203A")} ${step}`)];
1184
+ }
1185
+ function formatRunList(runs, options = {}) {
1186
+ if (runs.length === 0) {
1187
+ return [
1188
+ formatSection("Runs", "none recorded"),
1189
+ options.source === "server" ? pc2.dim("No runs recorded on the selected Rig server.") : pc2.dim("No runs recorded in .rig/runs."),
1190
+ "",
1191
+ ...formatNextSteps(["Start one: `rig task run --next`", "Check server: `rig server status`"])
1192
+ ].join(`
1193
+ `);
1194
+ }
1195
+ const rows = runs.map((run) => {
1196
+ const runId = stringField(run, "runId", stringField(run, "id", "(unknown-run)"));
1197
+ const status = stringField(run, "status", "unknown");
1198
+ const taskId = stringField(run, "taskId", "");
1199
+ const title = stringField(run, "title", taskId || "(untitled)");
1200
+ const runtime = stringField(run, "runtimeAdapter", "");
1201
+ return { runId, status, title, runtime };
1202
+ });
1203
+ const idWidth = Math.min(36, Math.max(6, ...rows.map((row) => row.runId.length)));
1204
+ const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
1205
+ const header = `${pc2.bold(pad("RUN", idWidth))} ${pc2.bold(pad("STATUS", statusWidth))} ${pc2.bold("TITLE")}`;
1206
+ const body = rows.map((row) => [
1207
+ pc2.bold(pad(truncate(row.runId, idWidth), idWidth)),
1208
+ statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
1209
+ `${row.title}${row.runtime ? pc2.dim(` ${row.runtime}`) : ""}`
1210
+ ].join(" "));
1211
+ 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(`
1212
+ `);
1213
+ }
1214
+ function formatRunCard(run, options = {}) {
1215
+ const raw = rawObject(run);
1216
+ const merged = { ...raw, ...run };
1217
+ const runId = runIdOf(merged);
1218
+ const status = firstString(merged, ["status"], "unknown");
1219
+ const taskId = taskIdOf(merged);
1220
+ const title = runTitleOf(merged);
1221
+ const runtime = firstString(merged, ["runtimeAdapter", "runtime", "adapter"]);
1222
+ const mode = firstString(merged, ["runtimeMode", "mode"]);
1223
+ const interaction = firstString(merged, ["interactionMode"]);
1224
+ const created = compactDate(firstString(merged, ["createdAt"]));
1225
+ const started = compactDate(firstString(merged, ["startedAt"]));
1226
+ const updated = compactDate(firstString(merged, ["updatedAt"]));
1227
+ const completed = compactDate(firstString(merged, ["completedAt", "finishedAt"]));
1228
+ const worktree = firstString(merged, ["worktreePath", "cwd", "projectRoot"]);
1229
+ const piSession = merged.piSession && typeof merged.piSession === "object" && !Array.isArray(merged.piSession) ? firstString(merged.piSession, ["sessionId", "id"]) : "";
1230
+ const timeline = Array.isArray(merged.timeline) ? merged.timeline.length : null;
1231
+ const approvals = Array.isArray(merged.approvals) ? merged.approvals.length : null;
1232
+ const inputs = Array.isArray(merged.userInputs) ? merged.userInputs.length : null;
1233
+ const rows = [
1234
+ ["run", pc2.bold(runId)],
1235
+ ["status", formatStatusPill(status)],
1236
+ ["task", taskId],
1237
+ ["title", title],
1238
+ ["runtime", [runtime, mode, interaction].filter(Boolean).join(" \xB7 ")],
1239
+ ["created", created],
1240
+ ["started", started],
1241
+ ["updated", updated],
1242
+ ["completed", completed],
1243
+ ["worktree", worktree],
1244
+ ["pi", piSession],
1245
+ ["timeline", timeline],
1246
+ ["approvals", approvals],
1247
+ ["inputs", inputs]
1248
+ ];
1249
+ return [
1250
+ formatSuccessCard(options.title ?? "Run details", rows),
1251
+ "",
1252
+ ...formatNextSteps([`Follow live: \`rig run attach ${runId} --follow\``, `Raw payload: \`rig run show ${runId} --raw\``])
1253
+ ].join(`
1254
+ `);
1255
+ }
1256
+ function formatRunStatus(summary, options = {}) {
1257
+ const activeRuns = summary.activeRuns ?? [];
1258
+ const recentRuns = summary.recentRuns ?? [];
1259
+ const lines = [formatSection("Run status", options.source === "server" ? "selected server" : "local state")];
1260
+ lines.push("", pc2.bold(`Active runs (${activeRuns.length})`));
1261
+ if (activeRuns.length === 0) {
1262
+ lines.push(pc2.dim("No active runs."));
1263
+ } else {
1264
+ for (const run of activeRuns) {
1265
+ lines.push(formatRunSummaryLine(run));
1266
+ }
1267
+ }
1268
+ lines.push("", pc2.bold(`Recent runs (${recentRuns.length})`));
1269
+ if (recentRuns.length === 0) {
1270
+ lines.push(pc2.dim("No recent terminal runs."));
1271
+ } else {
1272
+ for (const run of recentRuns.slice(0, 10)) {
1273
+ lines.push(formatRunSummaryLine(run));
1274
+ }
1275
+ }
1276
+ lines.push("", ...formatNextSteps(["Start work: `rig task run --next`", "Attach: `rig run attach <run-id> --follow`", "Details: `rig run show <run-id>`"]));
1277
+ return lines.join(`
1278
+ `);
1279
+ }
1280
+ function formatRunSummaryLine(run) {
1281
+ const record = run;
1282
+ const runId = runIdOf(record);
1283
+ const status = firstString(record, ["status"], "unknown");
1284
+ const taskId = taskIdOf(record);
1285
+ const title = runTitleOf(record);
1286
+ const runtime = firstString(record, ["runtimeAdapter", "runtime", "adapter"]);
1287
+ const descriptor = [taskId, title].filter(Boolean).join(" \xB7 ");
1288
+ return `${pc2.dim("\u2502")} ${pc2.bold(runId)} ${formatStatusPill(status)} ${descriptor}${runtime ? pc2.dim(` ${runtime}`) : ""}`;
1289
+ }
1290
+
1291
+ // packages/cli/src/commands/inbox.ts
1292
+ async function listInboxRecords(context, kind, filters) {
1293
+ const params = new URLSearchParams;
1294
+ if (filters.run)
1295
+ params.set("runId", filters.run);
1296
+ if (filters.task)
1297
+ params.set("taskId", filters.task);
1298
+ const query = params.size > 0 ? `?${params.toString()}` : "";
1299
+ const payload = await requestServerJson(context, `/api/inbox/${kind}${query}`);
1300
+ const records = Array.isArray(payload) ? payload : [];
1301
+ return filters.pendingOnly ? records.filter((entry) => (entry.status ?? "pending") !== "resolved") : records;
1302
+ }
1303
+ async function readPendingInboxCounts(context) {
1304
+ try {
1305
+ const [approvals, inputs] = await Promise.all([
1306
+ listInboxRecords(context, "approvals", { pendingOnly: true }),
1307
+ listInboxRecords(context, "inputs", { pendingOnly: true })
1308
+ ]);
1309
+ return { approvals: approvals.length, inputs: inputs.length };
1310
+ } catch {
1311
+ return null;
1312
+ }
1313
+ }
1314
+ async function printPendingInboxFooter(context) {
1315
+ if (context.outputMode !== "text")
1316
+ return;
1317
+ const counts = await readPendingInboxCounts(context);
1318
+ if (!counts || counts.approvals === 0 && counts.inputs === 0)
1319
+ return;
1320
+ const parts = [];
1321
+ if (counts.approvals > 0)
1322
+ parts.push(`${counts.approvals} approval${counts.approvals === 1 ? "" : "s"}`);
1323
+ if (counts.inputs > 0)
1324
+ parts.push(`${counts.inputs} input request${counts.inputs === 1 ? "" : "s"}`);
1325
+ console.log(`
1326
+ \u26A0 ${parts.join(" and ")} pending \u2014 run \`rig inbox\` to review.`);
1327
+ }
1328
+
409
1329
  // packages/cli/src/commands/run.ts
410
1330
  function normalizeRemoteRunDetails(payload) {
411
1331
  const run = payload.run;
@@ -418,6 +1338,22 @@ function normalizeRemoteRunDetails(payload) {
418
1338
  ...Array.isArray(payload.userInputs) ? { userInputs: payload.userInputs } : {}
419
1339
  };
420
1340
  }
1341
+ var REMOTE_TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged"]);
1342
+ async function listRunsForSelectedConnection(context, options = {}) {
1343
+ if (isRemoteConnectionSelected(context.projectRoot)) {
1344
+ return { runs: await listRunsViaServer(context, options), source: "server" };
1345
+ }
1346
+ return { runs: listAuthorityRuns(context.projectRoot), source: "local" };
1347
+ }
1348
+ function runStringField(run, key, fallback = "") {
1349
+ const value = run[key];
1350
+ return typeof value === "string" && value.trim() ? value : fallback;
1351
+ }
1352
+ function buildServerRunStatus(runs) {
1353
+ const activeRuns = runs.filter((run) => !REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
1354
+ const recentRuns = runs.filter((run) => REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
1355
+ return { activeRuns, recentRuns, runs };
1356
+ }
421
1357
  function shouldPromptForEpicSelection(context, command, promptEpic, noEpicPrompt) {
422
1358
  if (noEpicPrompt) {
423
1359
  return false;
@@ -438,7 +1374,7 @@ async function promptForEpicSelection(projectRoot, command) {
438
1374
  options.unshift(defaultEpic);
439
1375
  }
440
1376
  if (options.length === 0) {
441
- throw new CliError2("No open epic found. Pass --epic <id>.");
1377
+ throw new CliError("No open epic found. Pass --epic <id>.", 1, { hint: "Re-run with `rig run start --epic <id>`." });
442
1378
  }
443
1379
  console.log(`Select epic for run ${command}:`);
444
1380
  options.forEach((id, index) => {
@@ -460,7 +1396,7 @@ async function promptForEpicSelection(projectRoot, command) {
460
1396
  return fallback ?? options[0];
461
1397
  }
462
1398
  if (answer === "q" || answer === "quit") {
463
- throw new CliError2("Run cancelled by user.");
1399
+ throw new CliError("Run cancelled by user.");
464
1400
  }
465
1401
  if (/^\d+$/.test(answer)) {
466
1402
  const index = Number.parseInt(answer, 10) - 1;
@@ -479,21 +1415,16 @@ async function promptForEpicSelection(projectRoot, command) {
479
1415
  }
480
1416
  async function executeRun(context, args) {
481
1417
  const [command = "status", ...rest] = args;
482
- const runtimeContext = loadRuntimeContextFromEnv2() ?? undefined;
1418
+ const runtimeContext = loadRuntimeContextFromEnv() ?? undefined;
483
1419
  switch (command) {
484
1420
  case "list": {
485
- requireNoExtraArgs(rest, "bun run rig run list");
486
- const runs = listAuthorityRuns(context.projectRoot);
1421
+ requireNoExtraArgs(rest, "rig run list");
1422
+ const { runs, source } = isRemoteConnectionSelected(context.projectRoot) ? await withSpinner("Reading runs from server\u2026", () => listRunsForSelectedConnection(context, { limit: 100 }), { outputMode: context.outputMode }) : await listRunsForSelectedConnection(context, { limit: 100 });
487
1423
  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
- }
1424
+ printFormattedOutput(formatRunList(runs, { source }));
1425
+ await printPendingInboxFooter(context);
495
1426
  }
496
- return { ok: true, group: "run", command, details: { runs } };
1427
+ return { ok: true, group: "run", command, details: { runs, source } };
497
1428
  }
498
1429
  case "delete": {
499
1430
  let pending = rest;
@@ -501,9 +1432,9 @@ async function executeRun(context, args) {
501
1432
  pending = run.rest;
502
1433
  const purgeArtifacts = takeFlag(pending, "--purge-artifacts");
503
1434
  pending = purgeArtifacts.rest;
504
- requireNoExtraArgs(pending, "bun run rig run delete --run <id> [--purge-artifacts]");
1435
+ requireNoExtraArgs(pending, "rig run delete --run <id> [--purge-artifacts]");
505
1436
  if (!run.value) {
506
- throw new CliError2("run delete requires --run <id>.");
1437
+ throw new CliError("run delete requires --run <id>.", 1, { hint: "Run `rig run list` to find run ids, then `rig run delete --run <run-id>`." });
507
1438
  }
508
1439
  const result = await deleteRunState(context.projectRoot, {
509
1440
  runId: run.value,
@@ -531,9 +1462,9 @@ async function executeRun(context, args) {
531
1462
  pending = keepRuntimes.rest;
532
1463
  const keepQueue = takeFlag(pending, "--keep-queue");
533
1464
  pending = keepQueue.rest;
534
- requireNoExtraArgs(pending, "bun run rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
1465
+ requireNoExtraArgs(pending, "rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
535
1466
  if (!all.value) {
536
- throw new CliError2("run cleanup currently requires --all.");
1467
+ throw new CliError("run cleanup currently requires --all.", 1, { hint: "Run `rig run cleanup --all` (add --keep-artifacts/--keep-runtimes/--keep-queue to retain state)." });
537
1468
  }
538
1469
  const result = await cleanupRunState(context.projectRoot, {
539
1470
  includeArtifacts: !keepArtifacts.value,
@@ -550,20 +1481,25 @@ async function executeRun(context, args) {
550
1481
  }
551
1482
  case "show": {
552
1483
  let pending = rest;
1484
+ const rawResult = takeFlag(pending, "--raw");
1485
+ pending = rawResult.rest;
553
1486
  const run = takeOption(pending, "--run");
554
1487
  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>.");
1488
+ const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
1489
+ const extra = positionalRunId ? pending.slice(1) : pending;
1490
+ requireNoExtraArgs(extra, "rig run show <id>|--run <id> [--raw]");
1491
+ const runId = run.value ?? positionalRunId;
1492
+ if (!runId) {
1493
+ throw new CliError("run show requires a run id.", 1, { hint: "Run `rig run list` to find run ids, then `rig run show <run-id>`." });
558
1494
  }
559
- const record = readAuthorityRun(context.projectRoot, run.value) ?? normalizeRemoteRunDetails(await getRunDetailsViaServer(context, run.value).catch(() => ({})));
1495
+ const record = readAuthorityRun2(context.projectRoot, runId) ?? normalizeRemoteRunDetails(await withSpinner(`Reading run ${runId} from server\u2026`, () => getRunDetailsViaServer(context, runId).catch(() => ({})), { outputMode: context.outputMode }));
560
1496
  if (!record) {
561
- throw new CliError2(`Run not found: ${run.value}`, 2);
1497
+ throw new CliError(`Run not found: ${runId}`, 2, { hint: "Run `rig run list` to see recorded runs." });
562
1498
  }
563
1499
  if (context.outputMode === "text") {
564
- console.log(JSON.stringify(record, null, 2));
1500
+ printFormattedOutput(rawResult.value ? JSON.stringify(record, null, 2) : formatRunCard(record));
565
1501
  }
566
- return { ok: true, group: "run", command, details: record };
1502
+ return { ok: true, group: "run", command, details: { ...record, rawOutput: rawResult.value } };
567
1503
  }
568
1504
  case "timeline": {
569
1505
  let pending = rest;
@@ -571,38 +1507,69 @@ async function executeRun(context, args) {
571
1507
  pending = run.rest;
572
1508
  const follow = takeFlag(pending, "--follow");
573
1509
  pending = follow.rest;
574
- requireNoExtraArgs(pending, "bun run rig run timeline --run <id> [--follow]");
1510
+ requireNoExtraArgs(pending, "rig run timeline --run <id> [--follow]");
575
1511
  if (!run.value) {
576
- throw new CliError2("run timeline requires --run <id>.");
1512
+ throw new CliError("run timeline requires --run <id>.", 1, { hint: "Run `rig run list` to find run ids, then `rig run timeline --run <run-id>`." });
1513
+ }
1514
+ const renderer = createPiRunStreamRenderer();
1515
+ let cursor = null;
1516
+ const timelineRunId = run.value;
1517
+ const page = await withSpinner(`Reading timeline for ${timelineRunId}\u2026`, () => getRunTimelineViaServer(context, timelineRunId, { limit: 500 }), { outputMode: context.outputMode });
1518
+ const events = Array.isArray(page.entries) ? page.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1519
+ cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
1520
+ if (context.outputMode === "text") {
1521
+ renderer.renderTimeline(events);
577
1522
  }
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();
589
1523
  if (follow.value && context.outputMode === "text") {
590
- let lastLength = existsSync3(timelinePath) ? readFileSync3(timelinePath, "utf8").length : 0;
591
1524
  while (true) {
592
1525
  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
- }
1526
+ const nextPage = await getRunTimelineViaServer(context, run.value, { limit: 500, ...cursor ? { cursor } : {} });
1527
+ const nextEvents = Array.isArray(nextPage.entries) ? nextPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1528
+ cursor = typeof nextPage.nextCursor === "string" ? nextPage.nextCursor : cursor;
1529
+ renderer.renderTimeline(nextEvents);
603
1530
  }
604
1531
  }
605
- return { ok: true, group: "run", command, details: { runId: run.value, events } };
1532
+ return { ok: true, group: "run", command, details: { runId: run.value, events, cursor } };
1533
+ }
1534
+ case "replay": {
1535
+ let pending = rest;
1536
+ const run = takeOption(pending, "--run");
1537
+ pending = run.rest;
1538
+ const withSession = takeFlag(pending, "--with-session");
1539
+ pending = withSession.rest;
1540
+ const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
1541
+ const extra = positionalRunId ? pending.slice(1) : pending;
1542
+ requireNoExtraArgs(extra, "rig run replay <id>|--run <id> [--with-session]");
1543
+ const runId = run.value ?? positionalRunId;
1544
+ if (!runId) {
1545
+ throw new CliError("run replay requires a run id.", 2, { hint: "Run `rig run list` to find run ids, then `rig run replay <run-id>`." });
1546
+ }
1547
+ const replay = buildRunReplay(context.projectRoot, runId, { withSession: withSession.value });
1548
+ if (replay.entryCount === 0 && replay.sessionEntryCount === 0) {
1549
+ throw new CliError(`No run.jsonl lifecycle log found for ${runId} (looked at ${replay.logPath}).`, 2, { hint: "Run `rig run list` to confirm the run id; older runs may predate consolidated run.jsonl logging." });
1550
+ }
1551
+ if (context.outputMode === "text") {
1552
+ console.log(`Replay of ${runId} (${replay.entryCount} lifecycle entries${withSession.value ? `, ${replay.sessionEntryCount} session entries` : ""}):`);
1553
+ for (const line of replay.lines) {
1554
+ console.log(line);
1555
+ }
1556
+ if (withSession.value && !replay.sessionFile) {
1557
+ console.log("(no Pi session file resolvable from the run record; showing run.jsonl only)");
1558
+ }
1559
+ }
1560
+ return {
1561
+ ok: true,
1562
+ group: "run",
1563
+ command,
1564
+ details: {
1565
+ runId,
1566
+ logPath: replay.logPath,
1567
+ sessionFile: replay.sessionFile,
1568
+ entryCount: replay.entryCount,
1569
+ sessionEntryCount: replay.sessionEntryCount,
1570
+ lines: replay.lines
1571
+ }
1572
+ };
606
1573
  }
607
1574
  case "attach": {
608
1575
  let pending = rest;
@@ -618,41 +1585,40 @@ async function executeRun(context, args) {
618
1585
  pending = pollMs.rest;
619
1586
  const positionalRunId = pending.length > 0 ? pending[0] : undefined;
620
1587
  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>]");
1588
+ requireNoExtraArgs(extra, "rig run attach <run-id>|--run <run-id> [--message <text>] [--once|--follow] [--poll-ms <ms>]");
622
1589
  const runId = runOption.value ?? positionalRunId;
623
1590
  if (!runId) {
624
- throw new CliError2("run attach requires a run id.", 2);
1591
+ throw new CliError("run attach requires a run id.", 2, { hint: "Run `rig run list` to find run ids, then `rig run attach <run-id> --follow`." });
1592
+ }
1593
+ let steered = false;
1594
+ const steerMessage = messageOption.value?.trim();
1595
+ if (steerMessage) {
1596
+ await withSpinner("Queueing steering message\u2026", () => steerRunViaServer(context, runId, steerMessage), { outputMode: context.outputMode });
1597
+ steered = true;
625
1598
  }
626
1599
  const attached = await attachRunOperatorView(context, {
627
1600
  runId,
628
- message: messageOption.value ?? null,
1601
+ message: null,
629
1602
  once: once.value,
630
1603
  follow: follow.value,
631
1604
  pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)
632
1605
  });
633
- return { ok: true, group: "run", command, details: attached };
1606
+ return { ok: true, group: "run", command, details: { ...attached, steered: attached.steered || steered } };
634
1607
  }
635
1608
  case "status": {
636
- requireNoExtraArgs(rest, "bun run rig run status");
1609
+ requireNoExtraArgs(rest, "rig run status");
637
1610
  if (context.dryRun) {
638
1611
  if (context.outputMode === "text") {
639
1612
  console.log("[dry-run] rig run status");
640
1613
  }
641
1614
  return { ok: true, group: "run", command };
642
1615
  }
643
- const summary = runStatus(context.projectRoot, runtimeContext);
1616
+ const summary = isRemoteConnectionSelected(context.projectRoot) ? buildServerRunStatus(await withSpinner("Reading run status from server\u2026", () => listRunsViaServer(context, { limit: 100 }), { outputMode: context.outputMode })) : runStatus(context.projectRoot, runtimeContext);
1617
+ const activeRuns = Array.isArray(summary.activeRuns) ? summary.activeRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
1618
+ const recentRuns = Array.isArray(summary.recentRuns) ? summary.recentRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
644
1619
  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
- }
1620
+ printFormattedOutput(formatRunStatus({ activeRuns, recentRuns, runs: Array.isArray(summary.runs) ? summary.runs : [...activeRuns, ...recentRuns] }, { source: isRemoteConnectionSelected(context.projectRoot) ? "server" : "local" }));
1621
+ await printPendingInboxFooter(context);
656
1622
  }
657
1623
  return { ok: true, group: "run", command, details: summary };
658
1624
  }
@@ -676,12 +1642,12 @@ async function executeRun(context, args) {
676
1642
  pending = pollResult.rest;
677
1643
  const noServerResult = takeFlag(pending, "--no-server");
678
1644
  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]");
1645
+ 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
1646
  if (promptEpicResult.value && noEpicPromptResult.value) {
681
- throw new CliError2("Cannot use --prompt-epic and --no-epic-prompt together.");
1647
+ throw new CliError("Cannot use --prompt-epic and --no-epic-prompt together.", 1, { hint: "Pass only one of --prompt-epic or --no-epic-prompt." });
682
1648
  }
683
1649
  if (promptEpicResult.value && (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY)) {
684
- throw new CliError2("--prompt-epic requires an interactive terminal (TTY) in text mode.");
1650
+ throw new CliError("--prompt-epic requires an interactive terminal (TTY) in text mode.", 1, { hint: "Pass the epic explicitly instead: `rig run start --epic <id>`." });
685
1651
  }
686
1652
  let resolvedEpicId = epicResult.value || undefined;
687
1653
  if (!resolvedEpicId && shouldPromptForEpicSelection(context, command, promptEpicResult.value, noEpicPromptResult.value)) {
@@ -709,7 +1675,7 @@ async function executeRun(context, args) {
709
1675
  console.log(`Runs: ${result.runIds.join(", ")}`);
710
1676
  }
711
1677
  if (result.exitCode !== 0) {
712
- throw new CliError2(`run ${command} failed with exit code ${result.exitCode}.`, result.exitCode);
1678
+ throw new CliError(`run ${command} failed with exit code ${result.exitCode}.`, result.exitCode, { hint: "Inspect with `rig run status` and `rig inspect logs --task <id>`." });
713
1679
  }
714
1680
  return {
715
1681
  ok: true,
@@ -724,38 +1690,76 @@ async function executeRun(context, args) {
724
1690
  };
725
1691
  }
726
1692
  case "resume": {
727
- requireNoExtraArgs(rest, "bun run rig run resume");
1693
+ let pending = rest;
1694
+ const runOpt = takeOption(pending, "--run");
1695
+ pending = runOpt.rest;
1696
+ const positional = pending[0] && !pending[0].startsWith("-") ? pending.shift() : undefined;
1697
+ requireNoExtraArgs(pending, "rig run resume [<run-id>] [--run <id>]");
1698
+ const targetRunId = runOpt.value ?? positional ?? null;
728
1699
  if (context.dryRun) {
729
1700
  if (context.outputMode === "text") {
730
1701
  console.log("[dry-run] rig run resume");
731
1702
  }
732
1703
  return { ok: true, group: "run", command };
733
1704
  }
734
- const resumed = await runResume(context.projectRoot, runtimeContext);
1705
+ const resumed = await withSpinner(targetRunId ? `Resuming run ${targetRunId}\u2026` : "Resuming latest resumable run\u2026", () => resumeRunViaServer(context, targetRunId, { restart: false }), { outputMode: context.outputMode });
735
1706
  if (context.outputMode === "text") {
736
1707
  console.log(`Resumed run: ${resumed.runId}`);
737
1708
  }
738
1709
  return { ok: true, group: "run", command, details: resumed };
739
1710
  }
740
1711
  case "restart": {
741
- requireNoExtraArgs(rest, "bun run rig run restart");
1712
+ let pending = rest;
1713
+ const runOpt = takeOption(pending, "--run");
1714
+ pending = runOpt.rest;
1715
+ const positional = pending[0] && !pending[0].startsWith("-") ? pending.shift() : undefined;
1716
+ requireNoExtraArgs(pending, "rig run restart [<run-id>] [--run <id>]");
1717
+ const targetRunId = runOpt.value ?? positional ?? null;
742
1718
  if (context.dryRun) {
743
1719
  if (context.outputMode === "text") {
744
1720
  console.log("[dry-run] rig run restart");
745
1721
  }
746
1722
  return { ok: true, group: "run", command };
747
1723
  }
748
- const restarted = await runRestart(context.projectRoot, runtimeContext);
1724
+ const restarted = await withSpinner(targetRunId ? `Restarting run ${targetRunId}\u2026` : "Restarting latest run\u2026", () => resumeRunViaServer(context, targetRunId, { restart: true }), { outputMode: context.outputMode });
749
1725
  if (context.outputMode === "text") {
750
1726
  console.log(`Restarted run: ${restarted.runId}`);
751
1727
  }
752
1728
  return { ok: true, group: "run", command, details: restarted };
753
1729
  }
1730
+ case "steer": {
1731
+ const runOption = takeOption(rest, "--run");
1732
+ const messageOption = takeOption(runOption.rest, "--message");
1733
+ const shortMessageOption = takeOption(messageOption.rest, "-m");
1734
+ const positionalRunId = shortMessageOption.rest.length > 0 ? shortMessageOption.rest[0] : undefined;
1735
+ const extra = positionalRunId ? shortMessageOption.rest.slice(1) : shortMessageOption.rest;
1736
+ requireNoExtraArgs(extra, "rig run steer [<run-id>|--run <id>] --message <text>");
1737
+ const runId = runOption.value ?? positionalRunId;
1738
+ const message = messageOption.value ?? shortMessageOption.value;
1739
+ if (!runId) {
1740
+ throw new CliError("run steer requires a run id (positional or --run <id>).", 2, { hint: "Run `rig run list` to find run ids, then `rig run steer <run-id> --message <text>`." });
1741
+ }
1742
+ if (!message?.trim()) {
1743
+ throw new CliError("run steer requires --message <text>.", 2, { hint: 'Re-run as `rig run steer <run-id> --message "your steering note"`.' });
1744
+ }
1745
+ if (context.dryRun) {
1746
+ if (context.outputMode === "text") {
1747
+ console.log(`[dry-run] rig run steer ${runId} --message ${JSON.stringify(message)}`);
1748
+ }
1749
+ return { ok: true, group: "run", command, details: { runId, dryRun: true } };
1750
+ }
1751
+ const trimmedMessage = message.trim();
1752
+ await withSpinner("Queueing steering message\u2026", () => steerRunViaServer(context, runId, trimmedMessage), { outputMode: context.outputMode });
1753
+ if (context.outputMode === "text") {
1754
+ console.log(`Steering message queued for ${runId}.`);
1755
+ }
1756
+ return { ok: true, group: "run", command, details: { runId, queued: true } };
1757
+ }
754
1758
  case "stop": {
755
1759
  const runOption = takeOption(rest, "--run");
756
1760
  const positionalRunId = runOption.rest.length > 0 ? runOption.rest[0] : undefined;
757
1761
  const extra = positionalRunId ? runOption.rest.slice(1) : runOption.rest;
758
- requireNoExtraArgs(extra, "bun run rig run stop [<run-id>|--run <id>]");
1762
+ requireNoExtraArgs(extra, "rig run stop [<run-id>|--run <id>]");
759
1763
  const runId = runOption.value ?? positionalRunId;
760
1764
  if (context.dryRun) {
761
1765
  return {
@@ -766,14 +1770,14 @@ async function executeRun(context, args) {
766
1770
  };
767
1771
  }
768
1772
  if (runId) {
769
- const stopped = await stopRunViaServer(context, runId);
1773
+ const stopped = await withSpinner(`Requesting stop for ${runId}\u2026`, () => stopRunViaServer(context, runId), { outputMode: context.outputMode });
770
1774
  if (context.outputMode === "text")
771
1775
  console.log(`Stop requested: ${runId}`);
772
1776
  return { ok: true, group: "run", command, details: stopped };
773
1777
  }
774
1778
  const result = await runStop(context.projectRoot);
775
1779
  if (result.remaining.length > 0) {
776
- throw new CliError2(`Failed to stop run(s): ${result.remaining.join(", ")}`, 1);
1780
+ throw new CliError(`Failed to stop run(s): ${result.remaining.join(", ")}`, 1, { hint: "Check `rig run status`, then retry `rig run stop <run-id>` for the remaining runs." });
777
1781
  }
778
1782
  if (context.outputMode === "text") {
779
1783
  console.log(`Stopped process count: ${result.stopped}`);
@@ -789,7 +1793,7 @@ async function executeRun(context, args) {
789
1793
  };
790
1794
  }
791
1795
  default:
792
- throw new CliError2(`Unknown run command: ${command}`);
1796
+ throw new CliError(`Unknown run command: ${command}`, 1, { hint: "Run `rig run --help` to list available run commands." });
793
1797
  }
794
1798
  }
795
1799
  export {