@h-rig/cli 0.0.6-alpha.9 → 0.0.6-alpha.90

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 (114) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/build-rig-binaries.d.ts +2 -0
  3. package/dist/bin/rig.d.ts +2 -0
  4. package/dist/bin/rig.js +11052 -6668
  5. package/dist/src/app/board.d.ts +23 -0
  6. package/dist/src/app/board.js +1786 -0
  7. package/dist/src/app/drone-ui.d.ts +37 -0
  8. package/dist/src/app/drone-ui.js +294 -0
  9. package/dist/src/app/theme.d.ts +47 -0
  10. package/dist/src/app/theme.js +150 -0
  11. package/dist/src/commands/_async-ui.d.ts +13 -0
  12. package/dist/src/commands/_async-ui.js +223 -0
  13. package/dist/src/commands/_authority-runs.d.ts +22 -0
  14. package/dist/src/commands/_authority-runs.js +2 -3
  15. package/dist/src/commands/_cli-format.d.ts +49 -0
  16. package/dist/src/commands/_cli-format.js +447 -0
  17. package/dist/src/commands/_connection-state.d.ts +44 -0
  18. package/dist/src/commands/_connection-state.js +30 -11
  19. package/dist/src/commands/_doctor-checks.d.ts +52 -0
  20. package/dist/src/commands/_doctor-checks.js +180 -44
  21. package/dist/src/commands/_help-catalog.d.ts +51 -0
  22. package/dist/src/commands/_help-catalog.js +442 -0
  23. package/dist/src/commands/_json-output.d.ts +11 -0
  24. package/dist/src/commands/_json-output.js +56 -0
  25. package/dist/src/commands/_operator-surface.d.ts +34 -0
  26. package/dist/src/commands/_operator-surface.js +220 -0
  27. package/dist/src/commands/_operator-view.d.ts +30 -0
  28. package/dist/src/commands/_operator-view.js +803 -73
  29. package/dist/src/commands/_parsers.d.ts +15 -0
  30. package/dist/src/commands/_parsers.js +18 -11
  31. package/dist/src/commands/_paths.d.ts +11 -0
  32. package/dist/src/commands/_pi-frontend.d.ts +27 -0
  33. package/dist/src/commands/_pi-frontend.js +742 -0
  34. package/dist/src/commands/_pi-install.d.ts +42 -0
  35. package/dist/src/commands/_pi-install.js +5 -4
  36. package/dist/src/commands/_policy.d.ts +8 -0
  37. package/dist/src/commands/_policy.js +12 -5
  38. package/dist/src/commands/_preflight.d.ts +22 -0
  39. package/dist/src/commands/_preflight.js +190 -128
  40. package/dist/src/commands/_probes.d.ts +1 -0
  41. package/dist/src/commands/_run-driver-helpers.d.ts +99 -0
  42. package/dist/src/commands/_run-driver-helpers.js +75 -22
  43. package/dist/src/commands/_run-replay.d.ts +24 -0
  44. package/dist/src/commands/_run-replay.js +142 -0
  45. package/dist/src/commands/_server-client.d.ts +186 -0
  46. package/dist/src/commands/_server-client.js +360 -61
  47. package/dist/src/commands/_snapshot-upload.d.ts +39 -0
  48. package/dist/src/commands/_snapshot-upload.js +163 -39
  49. package/dist/src/commands/_spinner.d.ts +25 -0
  50. package/dist/src/commands/_spinner.js +65 -0
  51. package/dist/src/commands/_task-picker.d.ts +9 -0
  52. package/dist/src/commands/_task-picker.js +172 -19
  53. package/dist/src/commands/agent.d.ts +3 -0
  54. package/dist/src/commands/agent.js +39 -20
  55. package/dist/src/commands/browser.d.ts +65 -0
  56. package/dist/src/commands/browser.js +334 -51
  57. package/dist/src/commands/connect.d.ts +7 -0
  58. package/dist/src/commands/connect.js +272 -33
  59. package/dist/src/commands/dist.d.ts +28 -0
  60. package/dist/src/commands/dist.js +19 -12
  61. package/dist/src/commands/doctor.d.ts +3 -0
  62. package/dist/src/commands/doctor.js +378 -45
  63. package/dist/src/commands/github.d.ts +3 -0
  64. package/dist/src/commands/github.js +375 -53
  65. package/dist/src/commands/inbox.d.ts +30 -0
  66. package/dist/src/commands/inbox.js +760 -70
  67. package/dist/src/commands/init.d.ts +74 -0
  68. package/dist/src/commands/init.js +963 -119
  69. package/dist/src/commands/inspect.d.ts +3 -0
  70. package/dist/src/commands/inspect.js +588 -32
  71. package/dist/src/commands/inspector.d.ts +3 -0
  72. package/dist/src/commands/inspector.js +20 -13
  73. package/dist/src/commands/pi.d.ts +3 -0
  74. package/dist/src/commands/pi.js +177 -0
  75. package/dist/src/commands/plugin.d.ts +16 -0
  76. package/dist/src/commands/plugin.js +95 -27
  77. package/dist/src/commands/profile-and-review.d.ts +4 -0
  78. package/dist/src/commands/profile-and-review.js +26 -19
  79. package/dist/src/commands/queue.d.ts +3 -0
  80. package/dist/src/commands/queue.js +32 -12
  81. package/dist/src/commands/remote.d.ts +3 -0
  82. package/dist/src/commands/remote.js +43 -36
  83. package/dist/src/commands/repo-git-harness.d.ts +5 -0
  84. package/dist/src/commands/repo-git-harness.js +22 -15
  85. package/dist/src/commands/run.d.ts +3 -0
  86. package/dist/src/commands/run.js +1379 -159
  87. package/dist/src/commands/server.d.ts +7 -0
  88. package/dist/src/commands/server.js +502 -57
  89. package/dist/src/commands/setup.d.ts +16 -0
  90. package/dist/src/commands/setup.js +390 -63
  91. package/dist/src/commands/stats.d.ts +12 -0
  92. package/dist/src/commands/stats.js +1051 -0
  93. package/dist/src/commands/task-report-bug.d.ts +19 -0
  94. package/dist/src/commands/task-report-bug.js +260 -62
  95. package/dist/src/commands/task-run-driver.d.ts +132 -0
  96. package/dist/src/commands/task-run-driver.js +886 -130
  97. package/dist/src/commands/task.d.ts +14 -0
  98. package/dist/src/commands/task.js +1644 -314
  99. package/dist/src/commands/test.d.ts +3 -0
  100. package/dist/src/commands/test.js +15 -8
  101. package/dist/src/commands/workspace.d.ts +3 -0
  102. package/dist/src/commands/workspace.js +18 -11
  103. package/dist/src/commands.d.ts +29 -0
  104. package/dist/src/commands.js +10967 -6637
  105. package/dist/src/index.d.ts +4 -0
  106. package/dist/src/index.js +10168 -5787
  107. package/dist/src/launcher.d.ts +61 -0
  108. package/dist/src/launcher.js +77 -13
  109. package/dist/src/report-bug.d.ts +44 -0
  110. package/dist/src/report-bug.js +3 -3
  111. package/dist/src/runner.d.ts +47 -0
  112. package/dist/src/runner.js +16 -22
  113. package/dist/src/withMutedConsole.d.ts +2 -0
  114. package/package.json +13 -6
@@ -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 {
@@ -271,11 +382,23 @@ async function requestServerJson(context, pathname, init = {}) {
271
382
  })() : null;
272
383
  if (!response.ok) {
273
384
  const diagnostics = diagnosticMessage(payload);
274
- const detail = diagnostics ?? (text || response.statusText);
275
- throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
385
+ const rawDetail = diagnostics ?? (text || response.statusText);
386
+ const detail = diagnostics ? rawDetail : rawDetail.split(`
387
+ `).map((line) => line.trim()).find((line) => line.length > 0)?.slice(0, 200) ?? response.statusText;
388
+ const failure = await buildServerFailureContext(context.projectRoot, server);
389
+ throw new CliError(`Rig server request failed (${response.status}): ${detail}
390
+ ${failure.contextLine}`, 1, { hint: failure.hint });
276
391
  }
277
392
  return payload;
278
393
  }
394
+ async function listRunsViaServer(context, options = {}) {
395
+ const url = new URL("http://rig.local/api/runs");
396
+ if (options.limit !== undefined)
397
+ url.searchParams.set("limit", String(options.limit));
398
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
399
+ const runs = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.runs) ? payload.runs : [];
400
+ return runs.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
401
+ }
279
402
  async function getRunDetailsViaServer(context, runId) {
280
403
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}`);
281
404
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
@@ -289,6 +412,47 @@ async function getRunLogsViaServer(context, runId, options = {}) {
289
412
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
290
413
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
291
414
  }
415
+ async function getRunTimelineViaServer(context, runId, options = {}) {
416
+ const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
417
+ if (options.limit !== undefined)
418
+ url.searchParams.set("limit", String(options.limit));
419
+ if (options.cursor)
420
+ url.searchParams.set("cursor", options.cursor);
421
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
422
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
423
+ }
424
+ var RESUMABLE_RUN_STATUSES = new Set([
425
+ "created",
426
+ "preparing",
427
+ "running",
428
+ "validating",
429
+ "reviewing",
430
+ "stopped",
431
+ "failed",
432
+ "needs-attention",
433
+ "needs_attention"
434
+ ]);
435
+ async function resumeRunViaServer(context, runId, options) {
436
+ let targetRunId = runId?.trim() || null;
437
+ if (!targetRunId) {
438
+ const candidates = (await listRunsViaServer(context)).filter((run) => RESUMABLE_RUN_STATUSES.has(String(run.status ?? ""))).sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")));
439
+ targetRunId = typeof candidates[0]?.runId === "string" ? candidates[0].runId : null;
440
+ }
441
+ if (!targetRunId) {
442
+ 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>`." });
443
+ }
444
+ const payload = await requestServerJson(context, "/api/runs/resume", {
445
+ method: "POST",
446
+ headers: { "content-type": "application/json" },
447
+ body: JSON.stringify({ runId: targetRunId, createdAt: new Date().toISOString(), restart: options.restart })
448
+ });
449
+ const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
450
+ if (record.ok === false) {
451
+ const message = typeof record.error === "string" && record.error.trim() ? record.error : "run resume failed";
452
+ throw new CliError(`${options.restart ? "restart" : "resume"} failed for ${targetRunId}: ${message}`, 1);
453
+ }
454
+ return { ok: true, runId: targetRunId, ...record };
455
+ }
292
456
  async function stopRunViaServer(context, runId) {
293
457
  const payload = await requestServerJson(context, "/api/runs/stop", {
294
458
  method: "POST",
@@ -305,10 +469,155 @@ async function steerRunViaServer(context, runId, message) {
305
469
  });
306
470
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
307
471
  }
472
+ async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
473
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
474
+ method: "POST",
475
+ headers: { "content-type": "application/json" },
476
+ body: JSON.stringify({ text, streamingBehavior })
477
+ });
478
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
479
+ }
308
480
 
309
- // packages/cli/src/commands/_operator-view.ts
481
+ // packages/cli/src/commands/_run-replay.ts
482
+ import { existsSync as existsSync3, readdirSync } from "fs";
483
+ import { join, resolve as resolve3 } from "path";
484
+ import {
485
+ readAuthorityRun,
486
+ readJsonlFile,
487
+ runJournalPath
488
+ } from "@rig/runtime/control-plane/authority-files";
489
+ function asRecord(value) {
490
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
491
+ }
492
+ function text(value) {
493
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
494
+ }
495
+ function snippet(value, max = 120) {
496
+ const raw = text(value);
497
+ if (!raw)
498
+ return null;
499
+ const flat = raw.replace(/\s+/g, " ");
500
+ return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
501
+ }
502
+ function summarizeLifecycleEntry(entry) {
503
+ const type = text(entry.type) ?? "event";
504
+ switch (type) {
505
+ case "status": {
506
+ const anchor = text(entry.sessionId);
507
+ return [
508
+ `status \u2192 ${text(entry.status) ?? "(unknown)"}`,
509
+ snippet(entry.detail) ? `\u2014 ${snippet(entry.detail)}` : null,
510
+ anchor ? `[session ${anchor}]` : null
511
+ ].filter(Boolean).join(" ");
512
+ }
513
+ case "timeline-entry": {
514
+ const payload = asRecord(entry.payload) ?? {};
515
+ const kind = text(payload.type) ?? "entry";
516
+ const body = snippet(payload.title) ?? snippet(payload.text) ?? snippet(payload.detail);
517
+ const state = text(payload.state);
518
+ return [`timeline ${kind}`, body ? `\u2014 ${body}` : null, state ? `(${state})` : null].filter(Boolean).join(" ");
519
+ }
520
+ case "log-entry": {
521
+ const payload = asRecord(entry.payload) ?? {};
522
+ const title = snippet(payload.title) ?? "log";
523
+ const detail = snippet(payload.detail);
524
+ return [`log ${title}`, detail ? `\u2014 ${detail}` : null].filter(Boolean).join(" ");
525
+ }
526
+ case "record-patch": {
527
+ const patch = asRecord(entry.patch) ?? {};
528
+ const keys = Object.keys(patch);
529
+ const shown = keys.slice(0, 8).join(", ");
530
+ return `record-patch {${shown}${keys.length > 8 ? `, +${keys.length - 8} more` : ""}}`;
531
+ }
532
+ case "timeline":
533
+ return "timeline updated (signal)";
534
+ case "log":
535
+ return `log signal${snippet(entry.title) ? ` \u2014 ${snippet(entry.title)}` : ""}`;
536
+ case "completed":
537
+ return "run completed";
538
+ case "failed":
539
+ return `run failed${snippet(entry.error) ? ` \u2014 ${snippet(entry.error)}` : ""}`;
540
+ default:
541
+ return type;
542
+ }
543
+ }
544
+ function summarizeSessionEntry(entry) {
545
+ const type = text(entry.type) ?? "entry";
546
+ const message = asRecord(entry.message);
547
+ const role = text(message?.role);
548
+ const content = message?.content;
549
+ let body = null;
550
+ if (typeof content === "string") {
551
+ body = snippet(content);
552
+ } else if (Array.isArray(content)) {
553
+ body = snippet(content.map((part) => text(asRecord(part)?.text) ?? "").filter(Boolean).join(" "));
554
+ }
555
+ return [
556
+ `pi ${type}`,
557
+ role ? `(${role})` : null,
558
+ body ? `\u2014 ${body}` : null
559
+ ].filter(Boolean).join(" ");
560
+ }
561
+ function sessionEntryTimestamp(entry) {
562
+ return text(entry.timestamp) ?? text(entry.at) ?? text(entry.createdAt) ?? null;
563
+ }
564
+ function resolveRunSessionFile(record) {
565
+ const piSession = record?.piSession ?? null;
566
+ if (!piSession)
567
+ return null;
568
+ const recorded = text(piSession.sessionFile);
569
+ if (recorded && existsSync3(recorded)) {
570
+ return recorded;
571
+ }
572
+ const cwd = text(piSession.cwd);
573
+ const sessionId = text(piSession.sessionId);
574
+ if (!cwd || !sessionId)
575
+ return null;
576
+ const sessionDir = resolve3(cwd, ".rig", "session");
577
+ try {
578
+ const match = readdirSync(sessionDir).find((name) => name.endsWith(`_${sessionId}.jsonl`));
579
+ return match ? join(sessionDir, match) : null;
580
+ } catch {
581
+ return null;
582
+ }
583
+ }
584
+ function buildRunReplay(projectRoot, runId, options = {}) {
585
+ const logPath = runJournalPath(projectRoot, runId);
586
+ const record = readAuthorityRun(projectRoot, runId);
587
+ const lifecycleEntries = readJsonlFile(logPath).map((entry) => asRecord(entry)).filter((entry) => entry !== null);
588
+ const merged = lifecycleEntries.map((entry) => ({
589
+ at: text(entry.at),
590
+ source: "run",
591
+ summary: summarizeLifecycleEntry(entry)
592
+ }));
593
+ let sessionFile = null;
594
+ let sessionEntryCount = 0;
595
+ if (options.withSession) {
596
+ sessionFile = resolveRunSessionFile(record);
597
+ if (sessionFile) {
598
+ let lastSeenAt = null;
599
+ const sessionLines = readJsonlFile(sessionFile).map((entry) => asRecord(entry)).filter((entry) => entry !== null).map((entry) => {
600
+ lastSeenAt = sessionEntryTimestamp(entry) ?? lastSeenAt;
601
+ return { at: lastSeenAt, source: "session", summary: summarizeSessionEntry(entry) };
602
+ });
603
+ sessionEntryCount = sessionLines.length;
604
+ merged.push(...sessionLines);
605
+ merged.sort((left, right) => (left.at ?? "").localeCompare(right.at ?? ""));
606
+ }
607
+ }
608
+ const lines = merged.map((line) => `${line.at ?? "(no timestamp) "} ${line.source === "run" ? "run " : "session"} ${line.summary}`);
609
+ return {
610
+ runId,
611
+ logPath,
612
+ sessionFile,
613
+ entryCount: lifecycleEntries.length,
614
+ sessionEntryCount,
615
+ lines
616
+ };
617
+ }
618
+
619
+ // packages/cli/src/commands/_operator-surface.ts
310
620
  import { createInterface } from "readline";
311
- var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
312
621
  var CANONICAL_STAGES = [
313
622
  "Connect",
314
623
  "GitHub/task sync",
@@ -323,18 +632,585 @@ var CANONICAL_STAGES = [
323
632
  "Merge",
324
633
  "Complete"
325
634
  ];
635
+ function logDetail(log) {
636
+ return typeof log.detail === "string" ? log.detail.trim() : "";
637
+ }
638
+ function parseProviderProtocolLog(title, detail) {
639
+ if (title.trim().toLowerCase() !== "agent output")
640
+ return null;
641
+ if (!detail.startsWith("{") || !detail.endsWith("}"))
642
+ return null;
643
+ try {
644
+ const record = JSON.parse(detail);
645
+ if (!record || typeof record !== "object" || Array.isArray(record))
646
+ return null;
647
+ const type = record.type;
648
+ return typeof type === "string" && [
649
+ "assistant",
650
+ "message_start",
651
+ "message_update",
652
+ "message_end",
653
+ "stream_event",
654
+ "tool_result",
655
+ "tool_execution_start",
656
+ "tool_execution_update",
657
+ "tool_execution_end",
658
+ "turn_start",
659
+ "turn_end"
660
+ ].includes(type) ? record : null;
661
+ } catch {
662
+ return null;
663
+ }
664
+ }
665
+ function renderProviderProtocolLog(record) {
666
+ const type = typeof record.type === "string" ? record.type : "";
667
+ if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
668
+ const toolName = String(record.toolName ?? record.name ?? "tool");
669
+ 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";
670
+ return `[Pi tool] ${toolName} ${status}`;
671
+ }
672
+ return null;
673
+ }
674
+ function entryId(entry, fallback) {
675
+ return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
676
+ }
326
677
  function renderOperatorSnapshot(snapshot) {
327
678
  const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
328
679
  const runId = String(run.runId ?? run.id ?? "run");
329
680
  const status = String(run.status ?? "unknown");
330
681
  const logs = snapshot.logs ?? [];
682
+ const latestByStage = new Map;
683
+ for (const log of logs) {
684
+ const title = String(log.title ?? "").toLowerCase();
685
+ const stageName = String(log.stage ?? "").toLowerCase();
686
+ const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
687
+ if (stage)
688
+ latestByStage.set(stage, log);
689
+ }
331
690
  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)}`] : [];
691
+ const match = latestByStage.get(stage);
692
+ return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
334
693
  });
335
694
  return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
336
695
  `);
337
696
  }
697
+ function createPiRunStreamRenderer(output = process.stdout) {
698
+ let lastSnapshot = "";
699
+ const assistantTextById = new Map;
700
+ const seenTimeline = new Set;
701
+ const seenLogs = new Set;
702
+ const writeLine = (line) => output.write(`${line}
703
+ `);
704
+ return {
705
+ renderSnapshot(snapshot) {
706
+ const rendered = renderOperatorSnapshot(snapshot);
707
+ if (rendered && rendered !== lastSnapshot) {
708
+ writeLine(rendered);
709
+ lastSnapshot = rendered;
710
+ }
711
+ },
712
+ renderTimeline(entries) {
713
+ for (const [index, entry] of entries.entries()) {
714
+ const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
715
+ if (entry.type === "assistant_message" && typeof entry.text === "string") {
716
+ const text2 = entry.text;
717
+ const previousText = assistantTextById.get(id) ?? "";
718
+ if (!previousText && text2.trim()) {
719
+ writeLine("[Pi assistant]");
720
+ }
721
+ if (text2.startsWith(previousText)) {
722
+ const delta = text2.slice(previousText.length);
723
+ if (delta)
724
+ output.write(delta);
725
+ } else if (text2.trim() && text2 !== previousText) {
726
+ if (previousText)
727
+ writeLine(`
728
+ [Pi assistant]`);
729
+ output.write(text2);
730
+ }
731
+ assistantTextById.set(id, text2);
732
+ continue;
733
+ }
734
+ if (seenTimeline.has(id))
735
+ continue;
736
+ seenTimeline.add(id);
737
+ if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
738
+ writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
739
+ continue;
740
+ }
741
+ if (entry.type === "timeline_warning") {
742
+ writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
743
+ continue;
744
+ }
745
+ if (entry.type === "action") {
746
+ const text2 = String(entry.detail ?? entry.message ?? entry.title ?? "").trim();
747
+ if (text2)
748
+ writeLine(`[Rig action] ${text2}`);
749
+ continue;
750
+ }
751
+ if (entry.type === "user_message") {
752
+ const text2 = String(entry.text ?? entry.message ?? entry.detail ?? "").trim();
753
+ if (text2)
754
+ writeLine(`[Operator] ${text2}`);
755
+ continue;
756
+ }
757
+ const fallback = String(entry.detail ?? entry.message ?? entry.text ?? entry.title ?? "").trim();
758
+ if (fallback)
759
+ writeLine(`[${String(entry.type ?? "timeline")}] ${fallback}`);
760
+ }
761
+ },
762
+ renderLogs(entries) {
763
+ for (const [index, entry] of entries.entries()) {
764
+ const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
765
+ if (seenLogs.has(id))
766
+ continue;
767
+ seenLogs.add(id);
768
+ const title = String(entry.title ?? "");
769
+ if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
770
+ continue;
771
+ const detail = logDetail(entry);
772
+ if (!detail)
773
+ continue;
774
+ const protocolRecord = parseProviderProtocolLog(title, detail);
775
+ if (protocolRecord) {
776
+ const protocolLine = renderProviderProtocolLog(protocolRecord);
777
+ if (protocolLine)
778
+ writeLine(protocolLine);
779
+ continue;
780
+ }
781
+ writeLine(`[${title || "Rig log"}] ${detail}`);
782
+ }
783
+ }
784
+ };
785
+ }
786
+ function createOperatorSurface(options = {}) {
787
+ const input = options.input ?? process.stdin;
788
+ const output = options.output ?? process.stdout;
789
+ const errorOutput = options.errorOutput ?? process.stderr;
790
+ const renderer = createPiRunStreamRenderer(output);
791
+ const writeLine = (line) => output.write(`${line}
792
+ `);
793
+ return {
794
+ mode: "pi-compatible-text",
795
+ ...renderer,
796
+ info: writeLine,
797
+ error: (message) => errorOutput.write(`${message}
798
+ `),
799
+ attachCommandInput(handler) {
800
+ if (options.interactive === false || !input.isTTY)
801
+ return null;
802
+ const rl = createInterface({ input, output: process.stdout, terminal: false });
803
+ rl.on("line", (line) => {
804
+ Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
805
+ });
806
+ return { close: () => rl.close() };
807
+ }
808
+ };
809
+ }
810
+
811
+ // packages/cli/src/commands/_pi-frontend.ts
812
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
813
+ import { homedir as homedir2, tmpdir } from "os";
814
+ import { join as join2 } from "path";
815
+ import { main as runPiMain } from "@earendil-works/pi-coding-agent";
816
+ import createPiRigExtension from "@rig/pi-rig";
817
+
818
+ // packages/cli/src/commands/_async-ui.ts
819
+ import pc from "picocolors";
820
+
821
+ // packages/cli/src/commands/_spinner.ts
822
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
823
+ function createTtySpinner(input) {
824
+ const output = input.output ?? process.stdout;
825
+ const isTty = output.isTTY === true;
826
+ const frames = input.frames && input.frames.length > 0 ? input.frames : SPINNER_FRAMES;
827
+ let label = input.label;
828
+ let frame = 0;
829
+ let paused = false;
830
+ let stopped = false;
831
+ let lastPrintedLabel = "";
832
+ const render = () => {
833
+ if (stopped || paused)
834
+ return;
835
+ if (!isTty) {
836
+ if (label !== lastPrintedLabel) {
837
+ output.write(`${label}
838
+ `);
839
+ lastPrintedLabel = label;
840
+ }
841
+ return;
842
+ }
843
+ frame = (frame + 1) % frames.length;
844
+ const glyph = frames[frame] ?? frames[0] ?? "";
845
+ output.write(`\r\x1B[2K${input.styleFrame ? input.styleFrame(glyph) : glyph} ${label}`);
846
+ };
847
+ const clearLine = () => {
848
+ if (isTty)
849
+ output.write("\r\x1B[2K");
850
+ };
851
+ render();
852
+ const timer = isTty ? setInterval(render, input.intervalMs ?? 120) : null;
853
+ return {
854
+ setLabel(next) {
855
+ label = next;
856
+ render();
857
+ },
858
+ pause() {
859
+ paused = true;
860
+ clearLine();
861
+ },
862
+ resume() {
863
+ if (stopped)
864
+ return;
865
+ paused = false;
866
+ render();
867
+ },
868
+ stop(finalLine) {
869
+ if (stopped)
870
+ return;
871
+ stopped = true;
872
+ if (timer)
873
+ clearInterval(timer);
874
+ clearLine();
875
+ if (finalLine)
876
+ output.write(`${finalLine}
877
+ `);
878
+ }
879
+ };
880
+ }
881
+
882
+ // packages/cli/src/app/theme.ts
883
+ var RIG_PALETTE = {
884
+ ink: "#f2f3f6",
885
+ ink2: "#aeb0ba",
886
+ ink3: "#6c6e79",
887
+ ink4: "#44464f",
888
+ accent: "#ccff4d",
889
+ accentDim: "#a9d63f",
890
+ cyan: "#56d8ff",
891
+ red: "#ff5d5d",
892
+ yellow: "#ffd24d"
893
+ };
894
+ function hexToRgb(hex) {
895
+ const value = hex.replace("#", "");
896
+ return [
897
+ Number.parseInt(value.slice(0, 2), 16),
898
+ Number.parseInt(value.slice(2, 4), 16),
899
+ Number.parseInt(value.slice(4, 6), 16)
900
+ ];
901
+ }
902
+ function fg(hex) {
903
+ const [r, g, b] = hexToRgb(hex);
904
+ return (text2) => `\x1B[38;2;${r};${g};${b}m${text2}\x1B[39m`;
905
+ }
906
+ var ink = fg(RIG_PALETTE.ink);
907
+ var ink2 = fg(RIG_PALETTE.ink2);
908
+ var ink3 = fg(RIG_PALETTE.ink3);
909
+ var ink4 = fg(RIG_PALETTE.ink4);
910
+ var accent = fg(RIG_PALETTE.accent);
911
+ var accentDim = fg(RIG_PALETTE.accentDim);
912
+ var cyan = fg(RIG_PALETTE.cyan);
913
+ var red = fg(RIG_PALETTE.red);
914
+ var yellow = fg(RIG_PALETTE.yellow);
915
+ function bold(text2) {
916
+ return `\x1B[1m${text2}\x1B[22m`;
917
+ }
918
+ var DRONE_ART = [
919
+ " .-=-. .-=-. ",
920
+ " ( !!! ) ( !!! ) ",
921
+ " '-=-'._ _.'-=-' ",
922
+ " '._ _.' ",
923
+ " '=$$$$$$$=.' ",
924
+ " =$$$$$$$$$$$= ",
925
+ " $$$@@@@@@@@@@$$$ ",
926
+ " $$$@@ @@$$$ ",
927
+ " $$@ ? @$$$ ",
928
+ " $$$@ '-' @$$$ ",
929
+ " $$$@@ @@$$$ ",
930
+ " $$$@@@@@@@@@@$$$ ",
931
+ " =$$$$$$$$$$$= ",
932
+ " '=$$$$$$$=.' ",
933
+ " _.' '._ ",
934
+ " .-=-.' '.-=-. ",
935
+ " ( !!! ) ( !!! ) ",
936
+ " '-=-' '-=-' "
937
+ ];
938
+ var EYE_FRAMES = ["@", "o", "."];
939
+ var DRONE_WIDTH = DRONE_ART[0].length;
940
+ var DRONE_HEIGHT = DRONE_ART.length;
941
+ var MICRO_BLADES = ["---", "\\\\\\", "|||", "///"];
942
+ function microDroneFrame(tick) {
943
+ const blade = MICRO_BLADES[tick % MICRO_BLADES.length];
944
+ const eye = EYE_FRAMES[Math.floor(tick / 2) % EYE_FRAMES.length];
945
+ return `(${blade})${eye}(${blade})`;
946
+ }
947
+ var MICRO_DRONE_FRAMES = Array.from({ length: 12 }, (_, index) => microDroneFrame(index));
948
+ function renderMicroDroneFrame(tick) {
949
+ const blade = MICRO_BLADES[tick % MICRO_BLADES.length];
950
+ const eye = EYE_FRAMES[Math.floor(tick / 2) % EYE_FRAMES.length];
951
+ return `${ink4("(")}${cyan(blade)}${ink4(")")}${bold(accent(eye))}${ink4("(")}${cyan(blade)}${ink4(")")}`;
952
+ }
953
+
954
+ // packages/cli/src/commands/_async-ui.ts
955
+ var DONE_SYMBOL = pc.green("\u25C7");
956
+ var FAIL_SYMBOL = pc.red("\u25A0");
957
+ var activeUpdate = null;
958
+ async function withSpinner(label, work, options = {}) {
959
+ if (options.outputMode === "json") {
960
+ return work(() => {});
961
+ }
962
+ if (activeUpdate) {
963
+ const outer = activeUpdate;
964
+ outer(label);
965
+ return work(outer);
966
+ }
967
+ const output = options.output ?? process.stderr;
968
+ const isTty = output.isTTY === true;
969
+ let lastLabel = label;
970
+ if (!isTty) {
971
+ output.write(`${label}
972
+ `);
973
+ const update2 = (next) => {
974
+ lastLabel = next;
975
+ };
976
+ activeUpdate = update2;
977
+ const previousListener2 = setServerPhaseListener(update2);
978
+ try {
979
+ return await work(update2);
980
+ } finally {
981
+ activeUpdate = null;
982
+ setServerPhaseListener(previousListener2);
983
+ }
984
+ }
985
+ const spinner = createTtySpinner({
986
+ label,
987
+ output,
988
+ frames: MICRO_DRONE_FRAMES,
989
+ styleFrame: (frame) => renderMicroDroneFrame(Math.max(0, MICRO_DRONE_FRAMES.indexOf(frame)))
990
+ });
991
+ const update = (next) => {
992
+ lastLabel = next;
993
+ spinner.setLabel(next);
994
+ };
995
+ activeUpdate = update;
996
+ const previousListener = setServerPhaseListener(update);
997
+ try {
998
+ const result = await work(update);
999
+ spinner.stop(options.doneLabel ? `${DONE_SYMBOL} ${options.doneLabel}` : undefined);
1000
+ return result;
1001
+ } catch (error) {
1002
+ spinner.stop(`${FAIL_SYMBOL} ${lastLabel}`);
1003
+ throw error;
1004
+ } finally {
1005
+ activeUpdate = null;
1006
+ setServerPhaseListener(previousListener);
1007
+ }
1008
+ }
1009
+
1010
+ // packages/cli/src/commands/_pi-frontend.ts
1011
+ function setTemporaryEnv(updates) {
1012
+ const previous = new Map;
1013
+ for (const [key, value] of Object.entries(updates)) {
1014
+ previous.set(key, process.env[key]);
1015
+ process.env[key] = value;
1016
+ }
1017
+ return () => {
1018
+ for (const [key, value] of previous) {
1019
+ if (value === undefined)
1020
+ delete process.env[key];
1021
+ else
1022
+ process.env[key] = value;
1023
+ }
1024
+ };
1025
+ }
1026
+ function buildOperatorPiEnv(input) {
1027
+ return {
1028
+ PI_CODING_AGENT_SESSION_DIR: input.sessionDir,
1029
+ PI_SKIP_VERSION_CHECK: "1",
1030
+ PI_HIDDEN_COMMANDS: "import,fork,clone,tree,new,resume,trust",
1031
+ RIG_PI_OPERATOR_SESSION: "1",
1032
+ RIG_RUN_ID: input.runId,
1033
+ RIG_SERVER_URL: input.serverUrl,
1034
+ ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {},
1035
+ ...input.serverProjectRoot ? { RIG_PROJECT_ROOT: input.serverProjectRoot } : {}
1036
+ };
1037
+ }
1038
+ async function prepareOperatorConsole(context, runId, tempSessionDir) {
1039
+ const server = await ensureServerForCli(context.projectRoot);
1040
+ const localCwd = join2(homedir2(), ".rig", "drones", runId.slice(0, 8));
1041
+ mkdirSync2(localCwd, { recursive: true });
1042
+ trustDroneCwd(localCwd);
1043
+ installRigPiTheme();
1044
+ let sessionFileArg = [];
1045
+ try {
1046
+ const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/session-file`);
1047
+ if (payload?.ok && typeof payload.content === "string" && payload.content.trim()) {
1048
+ const fileName = typeof payload.fileName === "string" && payload.fileName.trim() ? payload.fileName.replace(/[^A-Za-z0-9._-]/g, "_") : `rig-run-${runId}.jsonl`;
1049
+ const localSessionPath = join2(tempSessionDir, fileName);
1050
+ const content = payload.content.split(`
1051
+ `).map((line, index) => {
1052
+ if (index > 0 || !line.trim())
1053
+ return line;
1054
+ try {
1055
+ const header = JSON.parse(line);
1056
+ if (header.type === "session" && typeof header.cwd === "string") {
1057
+ return JSON.stringify({ ...header, cwd: localCwd });
1058
+ }
1059
+ } catch {}
1060
+ return line;
1061
+ }).join(`
1062
+ `);
1063
+ writeFileSync2(localSessionPath, content);
1064
+ sessionFileArg = ["--session", localSessionPath];
1065
+ }
1066
+ } catch {}
1067
+ return { server, sessionFileArg };
1068
+ }
1069
+ var RIG_PI_THEME = {
1070
+ $schema: "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
1071
+ name: "rig",
1072
+ vars: {
1073
+ acid: "#ccff4d",
1074
+ acidDim: "#a9d63f",
1075
+ cyan: "#56d8ff",
1076
+ red: "#ff5d5d",
1077
+ yellow: "#ffd24d",
1078
+ ink: "#f2f3f6",
1079
+ ink2: "#aeb0ba",
1080
+ ink3: "#6c6e79",
1081
+ ink4: "#44464f",
1082
+ panel: "#101115",
1083
+ panelUser: "#14161b",
1084
+ toolPending: "#0e1013",
1085
+ toolSuccess: "#10150c",
1086
+ toolError: "#1a0f0f",
1087
+ customMsg: "#0f1410"
1088
+ },
1089
+ colors: {
1090
+ accent: "acid",
1091
+ border: "ink4",
1092
+ borderAccent: "acid",
1093
+ borderMuted: "ink4",
1094
+ success: "acid",
1095
+ error: "red",
1096
+ warning: "yellow",
1097
+ muted: "ink3",
1098
+ dim: "ink4",
1099
+ text: "ink",
1100
+ thinkingText: "ink3",
1101
+ selectedBg: "panel",
1102
+ userMessageBg: "panelUser",
1103
+ userMessageText: "ink",
1104
+ customMessageBg: "customMsg",
1105
+ customMessageText: "ink2",
1106
+ customMessageLabel: "acidDim",
1107
+ toolPendingBg: "toolPending",
1108
+ toolSuccessBg: "toolSuccess",
1109
+ toolErrorBg: "toolError",
1110
+ toolTitle: "ink",
1111
+ toolOutput: "ink3",
1112
+ mdHeading: "acid",
1113
+ mdLink: "cyan",
1114
+ mdLinkUrl: "ink4",
1115
+ mdCode: "acidDim",
1116
+ mdCodeBlock: "ink2",
1117
+ mdCodeBlockBorder: "ink4",
1118
+ mdQuote: "ink3",
1119
+ mdQuoteBorder: "ink4",
1120
+ mdHr: "ink4",
1121
+ mdListBullet: "acid",
1122
+ toolDiffAdded: "acid",
1123
+ toolDiffRemoved: "red",
1124
+ toolDiffContext: "ink3",
1125
+ syntaxComment: "ink3",
1126
+ syntaxKeyword: "cyan",
1127
+ syntaxFunction: "acid",
1128
+ syntaxVariable: "ink",
1129
+ syntaxString: "acidDim",
1130
+ syntaxNumber: "yellow",
1131
+ syntaxType: "cyan",
1132
+ syntaxOperator: "ink2",
1133
+ syntaxPunctuation: "ink3",
1134
+ thinkingOff: "ink4",
1135
+ thinkingMinimal: "ink3",
1136
+ thinkingLow: "ink2",
1137
+ thinkingMedium: "cyan",
1138
+ thinkingHigh: "acidDim",
1139
+ thinkingXhigh: "acid",
1140
+ bashMode: "cyan"
1141
+ }
1142
+ };
1143
+ function trustDroneCwd(localCwd) {
1144
+ try {
1145
+ const agentDir = join2(homedir2(), ".pi", "agent");
1146
+ mkdirSync2(agentDir, { recursive: true });
1147
+ const trustPath = join2(agentDir, "trust.json");
1148
+ const store = existsSync4(trustPath) ? JSON.parse(readFileSync3(trustPath, "utf8")) : {};
1149
+ if (store[localCwd] !== true) {
1150
+ store[localCwd] = true;
1151
+ writeFileSync2(trustPath, `${JSON.stringify(store, null, "\t")}
1152
+ `);
1153
+ }
1154
+ } catch {}
1155
+ }
1156
+ function installRigPiTheme() {
1157
+ try {
1158
+ const themesDir = join2(homedir2(), ".pi", "agent", "themes");
1159
+ mkdirSync2(themesDir, { recursive: true });
1160
+ const themePath = join2(themesDir, "rig.json");
1161
+ const next = `${JSON.stringify(RIG_PI_THEME, null, "\t")}
1162
+ `;
1163
+ if (!existsSync4(themePath) || readFileSync3(themePath, "utf8") !== next) {
1164
+ writeFileSync2(themePath, next);
1165
+ }
1166
+ } catch {}
1167
+ }
1168
+ async function attachRunBundledPiFrontend(context, input) {
1169
+ const tempSessionDir = mkdtempSync(join2(tmpdir(), "rig-pi-frontend-sessions-"));
1170
+ const { server, sessionFileArg } = await withSpinner(`Opening Pi console for run ${input.runId}\u2026`, () => prepareOperatorConsole(context, input.runId, tempSessionDir), { outputMode: context.outputMode });
1171
+ const restoreEnv = setTemporaryEnv(buildOperatorPiEnv({
1172
+ runId: input.runId,
1173
+ serverUrl: server.baseUrl,
1174
+ authToken: server.authToken,
1175
+ serverProjectRoot: server.serverProjectRoot,
1176
+ sessionDir: tempSessionDir
1177
+ }));
1178
+ const piRigExtensionFactory = (pi) => {
1179
+ createPiRigExtension(pi);
1180
+ };
1181
+ let detached = false;
1182
+ try {
1183
+ await runPiMain([
1184
+ "--no-extensions",
1185
+ "--no-skills",
1186
+ "--no-prompt-templates",
1187
+ "--no-context-files",
1188
+ ...sessionFileArg
1189
+ ], {
1190
+ extensionFactories: [piRigExtensionFactory]
1191
+ });
1192
+ detached = true;
1193
+ } finally {
1194
+ restoreEnv();
1195
+ rmSync(tempSessionDir, { recursive: true, force: true });
1196
+ }
1197
+ let run = { runId: input.runId, status: "unknown" };
1198
+ try {
1199
+ run = await getRunDetailsViaServer(context, input.runId);
1200
+ } catch {}
1201
+ return {
1202
+ run,
1203
+ logs: [],
1204
+ timeline: [],
1205
+ timelineCursor: null,
1206
+ steered: input.steered === true,
1207
+ detached,
1208
+ rendered: "stock Pi operator console with the pi-rig extension"
1209
+ };
1210
+ }
1211
+
1212
+ // packages/cli/src/commands/_operator-view.ts
1213
+ var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
338
1214
  function runStatusFromPayload(payload) {
339
1215
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
340
1216
  return String(run.status ?? "unknown").toLowerCase();
@@ -356,56 +1232,316 @@ async function applyOperatorCommand(context, input, deps = {}) {
356
1232
  await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
357
1233
  return { action: "continue", message: "Steering message queued." };
358
1234
  }
359
- async function readOperatorSnapshot(context, runId) {
1235
+ async function readOperatorSnapshot(context, runId, options = {}) {
360
1236
  const run = await getRunDetailsViaServer(context, runId);
361
1237
  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 }) };
1238
+ const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
1239
+ entries: [{
1240
+ id: `timeline-unavailable:${runId}`,
1241
+ type: "timeline_warning",
1242
+ detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
1243
+ createdAt: new Date().toISOString()
1244
+ }],
1245
+ nextCursor: options.timelineCursor ?? null
1246
+ }));
1247
+ const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
1248
+ const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1249
+ const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
1250
+ return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
364
1251
  }
365
1252
  async function attachRunOperatorView(context, input) {
366
1253
  let steered = false;
367
- if (input.message?.trim()) {
368
- await steerRunViaServer(context, input.runId, input.message.trim());
1254
+ const attachMessage = input.message?.trim();
1255
+ if (attachMessage) {
1256
+ await withSpinner("Queueing steering message\u2026", () => sendRunPiPromptViaServer(context, input.runId, attachMessage, "steer").catch(() => steerRunViaServer(context, input.runId, attachMessage)), { outputMode: context.outputMode });
369
1257
  steered = true;
370
1258
  }
371
- let snapshot = await readOperatorSnapshot(context, input.runId);
1259
+ if (!input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
1260
+ return attachRunBundledPiFrontend(context, {
1261
+ runId: input.runId,
1262
+ steered
1263
+ });
1264
+ }
1265
+ const surface = createOperatorSurface({ interactive: input.interactive !== false });
1266
+ let snapshot = await withSpinner(`Connecting to run ${input.runId}\u2026`, () => readOperatorSnapshot(context, input.runId), { outputMode: context.outputMode });
372
1267
  if (context.outputMode === "text") {
373
- console.log(snapshot.rendered);
1268
+ surface.renderSnapshot(snapshot);
1269
+ surface.renderTimeline(snapshot.timeline);
1270
+ surface.renderLogs(snapshot.logs);
374
1271
  if (steered)
375
- console.log("Steering message queued.");
1272
+ surface.info("Message submitted to worker Pi.");
376
1273
  }
377
1274
  let detached = false;
378
- let rl = null;
1275
+ let commandInput = null;
379
1276
  if (input.follow && !input.once && context.outputMode === "text") {
380
1277
  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)}`));
1278
+ surface.info("Controls: /user <message>, /stop, /detach");
1279
+ commandInput = surface.attachCommandInput(async (line) => {
1280
+ const result = await applyOperatorCommand(context, { runId: input.runId, line });
1281
+ if (result.message)
1282
+ surface.info(result.message);
1283
+ if (result.action === "detach" || result.action === "stopped") {
1284
+ detached = true;
1285
+ commandInput?.close();
1286
+ }
392
1287
  });
393
1288
  }
394
- let lastRendered = snapshot.rendered;
395
1289
  const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
1290
+ let timelineCursor = snapshot.timelineCursor;
396
1291
  while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
397
1292
  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
- }
1293
+ snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
1294
+ timelineCursor = snapshot.timelineCursor;
1295
+ surface.renderSnapshot(snapshot);
1296
+ surface.renderTimeline(snapshot.timeline);
1297
+ surface.renderLogs(snapshot.logs);
403
1298
  }
404
- rl?.close();
1299
+ commandInput?.close();
405
1300
  }
406
1301
  return { ...snapshot, steered, detached };
407
1302
  }
408
1303
 
1304
+ // packages/cli/src/app/drone-ui.ts
1305
+ import { ProcessTerminal, TUI, Text, Input, SelectList, matchesKey } from "@earendil-works/pi-tui";
1306
+ function droneNote(message, title) {
1307
+ if (title)
1308
+ console.log(` ${accentDim("\u25C7")} ${bold(ink2(title))}`);
1309
+ for (const line of message.split(`
1310
+ `)) {
1311
+ console.log(` ${ink4("\u2502")} ${line}`);
1312
+ }
1313
+ }
1314
+
1315
+ // packages/cli/src/commands/_cli-format.ts
1316
+ import pc2 from "picocolors";
1317
+ var themeDim = (value) => ink3(value);
1318
+ var themeFaint = (value) => ink4(value);
1319
+ function stringField(record, key, fallback = "") {
1320
+ const value = record[key];
1321
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
1322
+ }
1323
+ function rawObject(record) {
1324
+ const raw = record.raw;
1325
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
1326
+ }
1327
+ function truncate(value, width) {
1328
+ if (value.length <= width)
1329
+ return value;
1330
+ if (width <= 1)
1331
+ return "\u2026";
1332
+ return `${value.slice(0, width - 1)}\u2026`;
1333
+ }
1334
+ function pad(value, width) {
1335
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
1336
+ }
1337
+ function statusColor(status) {
1338
+ const normalized = status.toLowerCase();
1339
+ if (["completed", "merged", "closed", "done", "accepted", "pass", "selected", "approved", "running"].includes(normalized))
1340
+ return accent;
1341
+ if (["failed", "needs_attention", "needs-attention", "blocked", "error", "rejected"].includes(normalized))
1342
+ return red;
1343
+ if (["reviewing", "validating", "in_progress", "in-progress", "remote", "preparing", "closing-out"].includes(normalized))
1344
+ return cyan;
1345
+ if (["ready", "open", "queued", "created", "local", "pending"].includes(normalized))
1346
+ return yellow;
1347
+ return themeDim;
1348
+ }
1349
+ function compactDate(value) {
1350
+ if (!value.trim())
1351
+ return "";
1352
+ const parsed = Date.parse(value);
1353
+ if (!Number.isFinite(parsed))
1354
+ return value;
1355
+ return new Date(parsed).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
1356
+ }
1357
+ function firstString(record, keys, fallback = "") {
1358
+ for (const key of keys) {
1359
+ const value = stringField(record, key);
1360
+ if (value)
1361
+ return value;
1362
+ }
1363
+ return fallback;
1364
+ }
1365
+ function runIdOf(run) {
1366
+ return firstString(run, ["runId", "id"], "(unknown-run)");
1367
+ }
1368
+ function taskIdOf(run) {
1369
+ return firstString(run, ["taskId", "task", "task_id"]);
1370
+ }
1371
+ function runTitleOf(run) {
1372
+ return firstString(run, ["title", "summary", "name"], taskIdOf(run) || "(untitled)");
1373
+ }
1374
+ function shouldUseClackOutput() {
1375
+ return Boolean(process.stdout.isTTY) && process.env.RIG_CLI_PLAIN_HELP !== "1";
1376
+ }
1377
+ function printFormattedOutput(message, options = {}) {
1378
+ if (!shouldUseClackOutput()) {
1379
+ console.log(message);
1380
+ return;
1381
+ }
1382
+ droneNote(message, options.title);
1383
+ }
1384
+ function formatStatusPill(status) {
1385
+ const label = status || "unknown";
1386
+ return statusColor(label)(`\u25CF ${label}`);
1387
+ }
1388
+ function formatSection(title, subtitle) {
1389
+ return `${pc2.bold(accent("\u25C6"))} ${pc2.bold(title)}${subtitle ? themeDim(` \u2014 ${subtitle}`) : ""}`;
1390
+ }
1391
+ function formatSuccessCard(title, rows = []) {
1392
+ const body = rows.filter(([, value]) => value !== undefined && value !== null && String(value).length > 0).map(([key, value]) => `${themeFaint("\u2502")} ${themeDim(key.padEnd(12))} ${value}`);
1393
+ return [formatSection(title), ...body].join(`
1394
+ `);
1395
+ }
1396
+ function formatNextSteps(steps) {
1397
+ if (steps.length === 0)
1398
+ return [];
1399
+ return [pc2.bold("Next"), ...steps.map((step) => `${accent("\u203A")} ${step}`)];
1400
+ }
1401
+ function formatRunList(runs, options = {}) {
1402
+ if (runs.length === 0) {
1403
+ return [
1404
+ formatSection("Runs", "none recorded"),
1405
+ options.source === "server" ? themeDim("No runs recorded on the selected Rig server.") : themeDim("No runs recorded in .rig/runs."),
1406
+ "",
1407
+ ...formatNextSteps(["Start one: `rig task run --next`", "Check server: `rig server status`"])
1408
+ ].join(`
1409
+ `);
1410
+ }
1411
+ const rows = runs.map((run) => {
1412
+ const runId = stringField(run, "runId", stringField(run, "id", "(unknown-run)"));
1413
+ const status = stringField(run, "status", "unknown");
1414
+ const taskId = stringField(run, "taskId", "");
1415
+ const title = stringField(run, "title", taskId || "(untitled)");
1416
+ const runtime = stringField(run, "runtimeAdapter", "");
1417
+ return { runId, status, title, runtime };
1418
+ });
1419
+ const idWidth = Math.min(36, Math.max(6, ...rows.map((row) => row.runId.length)));
1420
+ const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
1421
+ const header = `${pc2.bold(pad("RUN", idWidth))} ${pc2.bold(pad("STATUS", statusWidth))} ${pc2.bold("TITLE")}`;
1422
+ const body = rows.map((row) => [
1423
+ pc2.bold(pad(truncate(row.runId, idWidth), idWidth)),
1424
+ statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
1425
+ `${row.title}${row.runtime ? themeDim(` ${row.runtime}`) : ""}`
1426
+ ].join(" "));
1427
+ 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(`
1428
+ `);
1429
+ }
1430
+ function formatRunCard(run, options = {}) {
1431
+ const raw = rawObject(run);
1432
+ const merged = { ...raw, ...run };
1433
+ const runId = runIdOf(merged);
1434
+ const status = firstString(merged, ["status"], "unknown");
1435
+ const taskId = taskIdOf(merged);
1436
+ const title = runTitleOf(merged);
1437
+ const runtime = firstString(merged, ["runtimeAdapter", "runtime", "adapter"]);
1438
+ const mode = firstString(merged, ["runtimeMode", "mode"]);
1439
+ const interaction = firstString(merged, ["interactionMode"]);
1440
+ const created = compactDate(firstString(merged, ["createdAt"]));
1441
+ const started = compactDate(firstString(merged, ["startedAt"]));
1442
+ const updated = compactDate(firstString(merged, ["updatedAt"]));
1443
+ const completed = compactDate(firstString(merged, ["completedAt", "finishedAt"]));
1444
+ const worktree = firstString(merged, ["worktreePath", "cwd", "projectRoot"]);
1445
+ const piSession = merged.piSession && typeof merged.piSession === "object" && !Array.isArray(merged.piSession) ? firstString(merged.piSession, ["sessionId", "id"]) : "";
1446
+ const timeline = Array.isArray(merged.timeline) ? merged.timeline.length : null;
1447
+ const approvals = Array.isArray(merged.approvals) ? merged.approvals.length : null;
1448
+ const inputs = Array.isArray(merged.userInputs) ? merged.userInputs.length : null;
1449
+ const rows = [
1450
+ ["run", pc2.bold(runId)],
1451
+ ["status", formatStatusPill(status)],
1452
+ ["task", taskId],
1453
+ ["title", title],
1454
+ ["runtime", [runtime, mode, interaction].filter(Boolean).join(" \xB7 ")],
1455
+ ["created", created],
1456
+ ["started", started],
1457
+ ["updated", updated],
1458
+ ["completed", completed],
1459
+ ["worktree", worktree],
1460
+ ["pi", piSession],
1461
+ ["timeline", timeline],
1462
+ ["approvals", approvals],
1463
+ ["inputs", inputs]
1464
+ ];
1465
+ return [
1466
+ formatSuccessCard(options.title ?? "Run details", rows),
1467
+ "",
1468
+ ...formatNextSteps([`Follow live: \`rig run attach ${runId} --follow\``, `Raw payload: \`rig run show ${runId} --raw\``])
1469
+ ].join(`
1470
+ `);
1471
+ }
1472
+ function formatRunStatus(summary, options = {}) {
1473
+ const activeRuns = summary.activeRuns ?? [];
1474
+ const recentRuns = summary.recentRuns ?? [];
1475
+ const lines = [formatSection("Run status", options.source === "server" ? "selected server" : "local state")];
1476
+ lines.push("", pc2.bold(`Active runs (${activeRuns.length})`));
1477
+ if (activeRuns.length === 0) {
1478
+ lines.push(themeDim("No active runs."));
1479
+ } else {
1480
+ for (const run of activeRuns) {
1481
+ lines.push(formatRunSummaryLine(run));
1482
+ }
1483
+ }
1484
+ lines.push("", pc2.bold(`Recent runs (${recentRuns.length})`));
1485
+ if (recentRuns.length === 0) {
1486
+ lines.push(themeDim("No recent terminal runs."));
1487
+ } else {
1488
+ for (const run of recentRuns.slice(0, 10)) {
1489
+ lines.push(formatRunSummaryLine(run));
1490
+ }
1491
+ }
1492
+ lines.push("", ...formatNextSteps(["Start work: `rig task run --next`", "Attach: `rig run attach <run-id> --follow`", "Details: `rig run show <run-id>`"]));
1493
+ return lines.join(`
1494
+ `);
1495
+ }
1496
+ function formatRunSummaryLine(run) {
1497
+ const record = run;
1498
+ const runId = runIdOf(record);
1499
+ const status = firstString(record, ["status"], "unknown");
1500
+ const taskId = taskIdOf(record);
1501
+ const title = runTitleOf(record);
1502
+ const runtime = firstString(record, ["runtimeAdapter", "runtime", "adapter"]);
1503
+ const descriptor = [taskId, title].filter(Boolean).join(" \xB7 ");
1504
+ return `${themeFaint("\u2502")} ${pc2.bold(runId)} ${formatStatusPill(status)} ${descriptor}${runtime ? themeDim(` ${runtime}`) : ""}`;
1505
+ }
1506
+
1507
+ // packages/cli/src/commands/inbox.ts
1508
+ async function listInboxRecords(context, kind, filters) {
1509
+ const params = new URLSearchParams;
1510
+ if (filters.run)
1511
+ params.set("runId", filters.run);
1512
+ if (filters.task)
1513
+ params.set("taskId", filters.task);
1514
+ const query = params.size > 0 ? `?${params.toString()}` : "";
1515
+ const payload = await requestServerJson(context, `/api/inbox/${kind}${query}`);
1516
+ const records = Array.isArray(payload) ? payload : [];
1517
+ return filters.pendingOnly ? records.filter((entry) => (entry.status ?? "pending") !== "resolved") : records;
1518
+ }
1519
+ async function readPendingInboxCounts(context) {
1520
+ try {
1521
+ const [approvals, inputs] = await Promise.all([
1522
+ listInboxRecords(context, "approvals", { pendingOnly: true }),
1523
+ listInboxRecords(context, "inputs", { pendingOnly: true })
1524
+ ]);
1525
+ return { approvals: approvals.length, inputs: inputs.length };
1526
+ } catch {
1527
+ return null;
1528
+ }
1529
+ }
1530
+ async function printPendingInboxFooter(context) {
1531
+ if (context.outputMode !== "text")
1532
+ return;
1533
+ const counts = await readPendingInboxCounts(context);
1534
+ if (!counts || counts.approvals === 0 && counts.inputs === 0)
1535
+ return;
1536
+ const parts = [];
1537
+ if (counts.approvals > 0)
1538
+ parts.push(`${counts.approvals} approval${counts.approvals === 1 ? "" : "s"}`);
1539
+ if (counts.inputs > 0)
1540
+ parts.push(`${counts.inputs} input request${counts.inputs === 1 ? "" : "s"}`);
1541
+ console.log(`
1542
+ \u26A0 ${parts.join(" and ")} pending \u2014 run \`rig inbox\` to review.`);
1543
+ }
1544
+
409
1545
  // packages/cli/src/commands/run.ts
410
1546
  function normalizeRemoteRunDetails(payload) {
411
1547
  const run = payload.run;
@@ -418,6 +1554,22 @@ function normalizeRemoteRunDetails(payload) {
418
1554
  ...Array.isArray(payload.userInputs) ? { userInputs: payload.userInputs } : {}
419
1555
  };
420
1556
  }
1557
+ var REMOTE_TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged"]);
1558
+ async function listRunsForSelectedConnection(context, options = {}) {
1559
+ if (isRemoteConnectionSelected(context.projectRoot)) {
1560
+ return { runs: await listRunsViaServer(context, options), source: "server" };
1561
+ }
1562
+ return { runs: listAuthorityRuns(context.projectRoot), source: "local" };
1563
+ }
1564
+ function runStringField(run, key, fallback = "") {
1565
+ const value = run[key];
1566
+ return typeof value === "string" && value.trim() ? value : fallback;
1567
+ }
1568
+ function buildServerRunStatus(runs) {
1569
+ const activeRuns = runs.filter((run) => !REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
1570
+ const recentRuns = runs.filter((run) => REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
1571
+ return { activeRuns, recentRuns, runs };
1572
+ }
421
1573
  function shouldPromptForEpicSelection(context, command, promptEpic, noEpicPrompt) {
422
1574
  if (noEpicPrompt) {
423
1575
  return false;
@@ -438,7 +1590,7 @@ async function promptForEpicSelection(projectRoot, command) {
438
1590
  options.unshift(defaultEpic);
439
1591
  }
440
1592
  if (options.length === 0) {
441
- throw new CliError2("No open epic found. Pass --epic <id>.");
1593
+ throw new CliError("No open epic found. Pass --epic <id>.", 1, { hint: "Re-run with `rig run start --epic <id>`." });
442
1594
  }
443
1595
  console.log(`Select epic for run ${command}:`);
444
1596
  options.forEach((id, index) => {
@@ -460,7 +1612,7 @@ async function promptForEpicSelection(projectRoot, command) {
460
1612
  return fallback ?? options[0];
461
1613
  }
462
1614
  if (answer === "q" || answer === "quit") {
463
- throw new CliError2("Run cancelled by user.");
1615
+ throw new CliError("Run cancelled by user.");
464
1616
  }
465
1617
  if (/^\d+$/.test(answer)) {
466
1618
  const index = Number.parseInt(answer, 10) - 1;
@@ -479,21 +1631,16 @@ async function promptForEpicSelection(projectRoot, command) {
479
1631
  }
480
1632
  async function executeRun(context, args) {
481
1633
  const [command = "status", ...rest] = args;
482
- const runtimeContext = loadRuntimeContextFromEnv2() ?? undefined;
1634
+ const runtimeContext = loadRuntimeContextFromEnv() ?? undefined;
483
1635
  switch (command) {
484
1636
  case "list": {
485
- requireNoExtraArgs(rest, "bun run rig run list");
486
- const runs = listAuthorityRuns(context.projectRoot);
1637
+ requireNoExtraArgs(rest, "rig run list");
1638
+ 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
1639
  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
- }
1640
+ printFormattedOutput(formatRunList(runs, { source }));
1641
+ await printPendingInboxFooter(context);
495
1642
  }
496
- return { ok: true, group: "run", command, details: { runs } };
1643
+ return { ok: true, group: "run", command, details: { runs, source } };
497
1644
  }
498
1645
  case "delete": {
499
1646
  let pending = rest;
@@ -501,9 +1648,9 @@ async function executeRun(context, args) {
501
1648
  pending = run.rest;
502
1649
  const purgeArtifacts = takeFlag(pending, "--purge-artifacts");
503
1650
  pending = purgeArtifacts.rest;
504
- requireNoExtraArgs(pending, "bun run rig run delete --run <id> [--purge-artifacts]");
1651
+ requireNoExtraArgs(pending, "rig run delete --run <id> [--purge-artifacts]");
505
1652
  if (!run.value) {
506
- throw new CliError2("run delete requires --run <id>.");
1653
+ 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
1654
  }
508
1655
  const result = await deleteRunState(context.projectRoot, {
509
1656
  runId: run.value,
@@ -531,9 +1678,9 @@ async function executeRun(context, args) {
531
1678
  pending = keepRuntimes.rest;
532
1679
  const keepQueue = takeFlag(pending, "--keep-queue");
533
1680
  pending = keepQueue.rest;
534
- requireNoExtraArgs(pending, "bun run rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
1681
+ requireNoExtraArgs(pending, "rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
535
1682
  if (!all.value) {
536
- throw new CliError2("run cleanup currently requires --all.");
1683
+ 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
1684
  }
538
1685
  const result = await cleanupRunState(context.projectRoot, {
539
1686
  includeArtifacts: !keepArtifacts.value,
@@ -550,20 +1697,25 @@ async function executeRun(context, args) {
550
1697
  }
551
1698
  case "show": {
552
1699
  let pending = rest;
1700
+ const rawResult = takeFlag(pending, "--raw");
1701
+ pending = rawResult.rest;
553
1702
  const run = takeOption(pending, "--run");
554
1703
  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>.");
1704
+ const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
1705
+ const extra = positionalRunId ? pending.slice(1) : pending;
1706
+ requireNoExtraArgs(extra, "rig run show <id>|--run <id> [--raw]");
1707
+ const runId = run.value ?? positionalRunId;
1708
+ if (!runId) {
1709
+ 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
1710
  }
559
- const record = readAuthorityRun(context.projectRoot, run.value) ?? normalizeRemoteRunDetails(await getRunDetailsViaServer(context, run.value).catch(() => ({})));
1711
+ const record = readAuthorityRun2(context.projectRoot, runId) ?? normalizeRemoteRunDetails(await withSpinner(`Reading run ${runId} from server\u2026`, () => getRunDetailsViaServer(context, runId).catch(() => ({})), { outputMode: context.outputMode }));
560
1712
  if (!record) {
561
- throw new CliError2(`Run not found: ${run.value}`, 2);
1713
+ throw new CliError(`Run not found: ${runId}`, 2, { hint: "Run `rig run list` to see recorded runs." });
562
1714
  }
563
1715
  if (context.outputMode === "text") {
564
- console.log(JSON.stringify(record, null, 2));
1716
+ printFormattedOutput(rawResult.value ? JSON.stringify(record, null, 2) : formatRunCard(record));
565
1717
  }
566
- return { ok: true, group: "run", command, details: record };
1718
+ return { ok: true, group: "run", command, details: { ...record, rawOutput: rawResult.value } };
567
1719
  }
568
1720
  case "timeline": {
569
1721
  let pending = rest;
@@ -571,38 +1723,69 @@ async function executeRun(context, args) {
571
1723
  pending = run.rest;
572
1724
  const follow = takeFlag(pending, "--follow");
573
1725
  pending = follow.rest;
574
- requireNoExtraArgs(pending, "bun run rig run timeline --run <id> [--follow]");
1726
+ requireNoExtraArgs(pending, "rig run timeline --run <id> [--follow]");
575
1727
  if (!run.value) {
576
- throw new CliError2("run timeline requires --run <id>.");
1728
+ 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>`." });
1729
+ }
1730
+ const renderer = createPiRunStreamRenderer();
1731
+ let cursor = null;
1732
+ const timelineRunId = run.value;
1733
+ const page = await withSpinner(`Reading timeline for ${timelineRunId}\u2026`, () => getRunTimelineViaServer(context, timelineRunId, { limit: 500 }), { outputMode: context.outputMode });
1734
+ const events = Array.isArray(page.entries) ? page.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1735
+ cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
1736
+ if (context.outputMode === "text") {
1737
+ renderer.renderTimeline(events);
577
1738
  }
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
1739
  if (follow.value && context.outputMode === "text") {
590
- let lastLength = existsSync3(timelinePath) ? readFileSync3(timelinePath, "utf8").length : 0;
591
1740
  while (true) {
592
1741
  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
- }
1742
+ const nextPage = await getRunTimelineViaServer(context, run.value, { limit: 500, ...cursor ? { cursor } : {} });
1743
+ const nextEvents = Array.isArray(nextPage.entries) ? nextPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1744
+ cursor = typeof nextPage.nextCursor === "string" ? nextPage.nextCursor : cursor;
1745
+ renderer.renderTimeline(nextEvents);
1746
+ }
1747
+ }
1748
+ return { ok: true, group: "run", command, details: { runId: run.value, events, cursor } };
1749
+ }
1750
+ case "replay": {
1751
+ let pending = rest;
1752
+ const run = takeOption(pending, "--run");
1753
+ pending = run.rest;
1754
+ const withSession = takeFlag(pending, "--with-session");
1755
+ pending = withSession.rest;
1756
+ const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
1757
+ const extra = positionalRunId ? pending.slice(1) : pending;
1758
+ requireNoExtraArgs(extra, "rig run replay <id>|--run <id> [--with-session]");
1759
+ const runId = run.value ?? positionalRunId;
1760
+ if (!runId) {
1761
+ 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>`." });
1762
+ }
1763
+ const replay = buildRunReplay(context.projectRoot, runId, { withSession: withSession.value });
1764
+ if (replay.entryCount === 0 && replay.sessionEntryCount === 0) {
1765
+ 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." });
1766
+ }
1767
+ if (context.outputMode === "text") {
1768
+ console.log(`Replay of ${runId} (${replay.entryCount} lifecycle entries${withSession.value ? `, ${replay.sessionEntryCount} session entries` : ""}):`);
1769
+ for (const line of replay.lines) {
1770
+ console.log(line);
1771
+ }
1772
+ if (withSession.value && !replay.sessionFile) {
1773
+ console.log("(no Pi session file resolvable from the run record; showing run.jsonl only)");
603
1774
  }
604
1775
  }
605
- return { ok: true, group: "run", command, details: { runId: run.value, events } };
1776
+ return {
1777
+ ok: true,
1778
+ group: "run",
1779
+ command,
1780
+ details: {
1781
+ runId,
1782
+ logPath: replay.logPath,
1783
+ sessionFile: replay.sessionFile,
1784
+ entryCount: replay.entryCount,
1785
+ sessionEntryCount: replay.sessionEntryCount,
1786
+ lines: replay.lines
1787
+ }
1788
+ };
606
1789
  }
607
1790
  case "attach": {
608
1791
  let pending = rest;
@@ -618,41 +1801,40 @@ async function executeRun(context, args) {
618
1801
  pending = pollMs.rest;
619
1802
  const positionalRunId = pending.length > 0 ? pending[0] : undefined;
620
1803
  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>]");
1804
+ requireNoExtraArgs(extra, "rig run attach <run-id>|--run <run-id> [--message <text>] [--once|--follow] [--poll-ms <ms>]");
622
1805
  const runId = runOption.value ?? positionalRunId;
623
1806
  if (!runId) {
624
- throw new CliError2("run attach requires a run id.", 2);
1807
+ 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`." });
1808
+ }
1809
+ let steered = false;
1810
+ const steerMessage = messageOption.value?.trim();
1811
+ if (steerMessage) {
1812
+ await withSpinner("Queueing steering message\u2026", () => steerRunViaServer(context, runId, steerMessage), { outputMode: context.outputMode });
1813
+ steered = true;
625
1814
  }
626
1815
  const attached = await attachRunOperatorView(context, {
627
1816
  runId,
628
- message: messageOption.value ?? null,
1817
+ message: null,
629
1818
  once: once.value,
630
1819
  follow: follow.value,
631
1820
  pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)
632
1821
  });
633
- return { ok: true, group: "run", command, details: attached };
1822
+ return { ok: true, group: "run", command, details: { ...attached, steered: attached.steered || steered } };
634
1823
  }
635
1824
  case "status": {
636
- requireNoExtraArgs(rest, "bun run rig run status");
1825
+ requireNoExtraArgs(rest, "rig run status");
637
1826
  if (context.dryRun) {
638
1827
  if (context.outputMode === "text") {
639
1828
  console.log("[dry-run] rig run status");
640
1829
  }
641
1830
  return { ok: true, group: "run", command };
642
1831
  }
643
- const summary = runStatus(context.projectRoot, runtimeContext);
1832
+ 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);
1833
+ const activeRuns = Array.isArray(summary.activeRuns) ? summary.activeRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
1834
+ const recentRuns = Array.isArray(summary.recentRuns) ? summary.recentRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
644
1835
  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
- }
1836
+ printFormattedOutput(formatRunStatus({ activeRuns, recentRuns, runs: Array.isArray(summary.runs) ? summary.runs : [...activeRuns, ...recentRuns] }, { source: isRemoteConnectionSelected(context.projectRoot) ? "server" : "local" }));
1837
+ await printPendingInboxFooter(context);
656
1838
  }
657
1839
  return { ok: true, group: "run", command, details: summary };
658
1840
  }
@@ -676,12 +1858,12 @@ async function executeRun(context, args) {
676
1858
  pending = pollResult.rest;
677
1859
  const noServerResult = takeFlag(pending, "--no-server");
678
1860
  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]");
1861
+ 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
1862
  if (promptEpicResult.value && noEpicPromptResult.value) {
681
- throw new CliError2("Cannot use --prompt-epic and --no-epic-prompt together.");
1863
+ 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
1864
  }
683
1865
  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.");
1866
+ 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
1867
  }
686
1868
  let resolvedEpicId = epicResult.value || undefined;
687
1869
  if (!resolvedEpicId && shouldPromptForEpicSelection(context, command, promptEpicResult.value, noEpicPromptResult.value)) {
@@ -709,7 +1891,7 @@ async function executeRun(context, args) {
709
1891
  console.log(`Runs: ${result.runIds.join(", ")}`);
710
1892
  }
711
1893
  if (result.exitCode !== 0) {
712
- throw new CliError2(`run ${command} failed with exit code ${result.exitCode}.`, result.exitCode);
1894
+ 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
1895
  }
714
1896
  return {
715
1897
  ok: true,
@@ -724,38 +1906,76 @@ async function executeRun(context, args) {
724
1906
  };
725
1907
  }
726
1908
  case "resume": {
727
- requireNoExtraArgs(rest, "bun run rig run resume");
1909
+ let pending = rest;
1910
+ const runOpt = takeOption(pending, "--run");
1911
+ pending = runOpt.rest;
1912
+ const positional = pending[0] && !pending[0].startsWith("-") ? pending.shift() : undefined;
1913
+ requireNoExtraArgs(pending, "rig run resume [<run-id>] [--run <id>]");
1914
+ const targetRunId = runOpt.value ?? positional ?? null;
728
1915
  if (context.dryRun) {
729
1916
  if (context.outputMode === "text") {
730
1917
  console.log("[dry-run] rig run resume");
731
1918
  }
732
1919
  return { ok: true, group: "run", command };
733
1920
  }
734
- const resumed = await runResume(context.projectRoot, runtimeContext);
1921
+ const resumed = await withSpinner(targetRunId ? `Resuming run ${targetRunId}\u2026` : "Resuming latest resumable run\u2026", () => resumeRunViaServer(context, targetRunId, { restart: false }), { outputMode: context.outputMode });
735
1922
  if (context.outputMode === "text") {
736
1923
  console.log(`Resumed run: ${resumed.runId}`);
737
1924
  }
738
1925
  return { ok: true, group: "run", command, details: resumed };
739
1926
  }
740
1927
  case "restart": {
741
- requireNoExtraArgs(rest, "bun run rig run restart");
1928
+ let pending = rest;
1929
+ const runOpt = takeOption(pending, "--run");
1930
+ pending = runOpt.rest;
1931
+ const positional = pending[0] && !pending[0].startsWith("-") ? pending.shift() : undefined;
1932
+ requireNoExtraArgs(pending, "rig run restart [<run-id>] [--run <id>]");
1933
+ const targetRunId = runOpt.value ?? positional ?? null;
742
1934
  if (context.dryRun) {
743
1935
  if (context.outputMode === "text") {
744
1936
  console.log("[dry-run] rig run restart");
745
1937
  }
746
1938
  return { ok: true, group: "run", command };
747
1939
  }
748
- const restarted = await runRestart(context.projectRoot, runtimeContext);
1940
+ const restarted = await withSpinner(targetRunId ? `Restarting run ${targetRunId}\u2026` : "Restarting latest run\u2026", () => resumeRunViaServer(context, targetRunId, { restart: true }), { outputMode: context.outputMode });
749
1941
  if (context.outputMode === "text") {
750
1942
  console.log(`Restarted run: ${restarted.runId}`);
751
1943
  }
752
1944
  return { ok: true, group: "run", command, details: restarted };
753
1945
  }
1946
+ case "steer": {
1947
+ const runOption = takeOption(rest, "--run");
1948
+ const messageOption = takeOption(runOption.rest, "--message");
1949
+ const shortMessageOption = takeOption(messageOption.rest, "-m");
1950
+ const positionalRunId = shortMessageOption.rest.length > 0 ? shortMessageOption.rest[0] : undefined;
1951
+ const extra = positionalRunId ? shortMessageOption.rest.slice(1) : shortMessageOption.rest;
1952
+ requireNoExtraArgs(extra, "rig run steer [<run-id>|--run <id>] --message <text>");
1953
+ const runId = runOption.value ?? positionalRunId;
1954
+ const message = messageOption.value ?? shortMessageOption.value;
1955
+ if (!runId) {
1956
+ 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>`." });
1957
+ }
1958
+ if (!message?.trim()) {
1959
+ throw new CliError("run steer requires --message <text>.", 2, { hint: 'Re-run as `rig run steer <run-id> --message "your steering note"`.' });
1960
+ }
1961
+ if (context.dryRun) {
1962
+ if (context.outputMode === "text") {
1963
+ console.log(`[dry-run] rig run steer ${runId} --message ${JSON.stringify(message)}`);
1964
+ }
1965
+ return { ok: true, group: "run", command, details: { runId, dryRun: true } };
1966
+ }
1967
+ const trimmedMessage = message.trim();
1968
+ await withSpinner("Queueing steering message\u2026", () => steerRunViaServer(context, runId, trimmedMessage), { outputMode: context.outputMode });
1969
+ if (context.outputMode === "text") {
1970
+ console.log(`Steering message queued for ${runId}.`);
1971
+ }
1972
+ return { ok: true, group: "run", command, details: { runId, queued: true } };
1973
+ }
754
1974
  case "stop": {
755
1975
  const runOption = takeOption(rest, "--run");
756
1976
  const positionalRunId = runOption.rest.length > 0 ? runOption.rest[0] : undefined;
757
1977
  const extra = positionalRunId ? runOption.rest.slice(1) : runOption.rest;
758
- requireNoExtraArgs(extra, "bun run rig run stop [<run-id>|--run <id>]");
1978
+ requireNoExtraArgs(extra, "rig run stop [<run-id>|--run <id>]");
759
1979
  const runId = runOption.value ?? positionalRunId;
760
1980
  if (context.dryRun) {
761
1981
  return {
@@ -766,14 +1986,14 @@ async function executeRun(context, args) {
766
1986
  };
767
1987
  }
768
1988
  if (runId) {
769
- const stopped = await stopRunViaServer(context, runId);
1989
+ const stopped = await withSpinner(`Requesting stop for ${runId}\u2026`, () => stopRunViaServer(context, runId), { outputMode: context.outputMode });
770
1990
  if (context.outputMode === "text")
771
1991
  console.log(`Stop requested: ${runId}`);
772
1992
  return { ok: true, group: "run", command, details: stopped };
773
1993
  }
774
1994
  const result = await runStop(context.projectRoot);
775
1995
  if (result.remaining.length > 0) {
776
- throw new CliError2(`Failed to stop run(s): ${result.remaining.join(", ")}`, 1);
1996
+ 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
1997
  }
778
1998
  if (context.outputMode === "text") {
779
1999
  console.log(`Stopped process count: ${result.stopped}`);
@@ -789,7 +2009,7 @@ async function executeRun(context, args) {
789
2009
  };
790
2010
  }
791
2011
  default:
792
- throw new CliError2(`Unknown run command: ${command}`);
2012
+ throw new CliError(`Unknown run command: ${command}`, 1, { hint: "Run `rig run --help` to list available run commands." });
793
2013
  }
794
2014
  }
795
2015
  export {