@h-rig/cli 0.0.6-alpha.63 → 0.0.6-alpha.65

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 (46) hide show
  1. package/dist/bin/rig.js +1349 -1599
  2. package/dist/src/commands/_connection-state.js +14 -5
  3. package/dist/src/commands/_doctor-checks.js +71 -11
  4. package/dist/src/commands/_help-catalog.js +99 -60
  5. package/dist/src/commands/_json-output.js +56 -0
  6. package/dist/src/commands/_operator-view.js +97 -784
  7. package/dist/src/commands/_parsers.js +18 -9
  8. package/dist/src/commands/_pi-frontend.js +96 -788
  9. package/dist/src/commands/_policy.js +12 -3
  10. package/dist/src/commands/_preflight.js +73 -13
  11. package/dist/src/commands/_run-driver-helpers.js +31 -5
  12. package/dist/src/commands/_run-replay.js +2 -2
  13. package/dist/src/commands/_server-client.js +76 -16
  14. package/dist/src/commands/_snapshot-upload.js +70 -10
  15. package/dist/src/commands/agent.js +21 -11
  16. package/dist/src/commands/browser.js +24 -15
  17. package/dist/src/commands/connect.js +22 -17
  18. package/dist/src/commands/dist.js +15 -6
  19. package/dist/src/commands/doctor.js +70 -10
  20. package/dist/src/commands/github.js +79 -19
  21. package/dist/src/commands/inbox.js +215 -131
  22. package/dist/src/commands/init.js +202 -35
  23. package/dist/src/commands/inspect.js +77 -17
  24. package/dist/src/commands/inspector.js +18 -9
  25. package/dist/src/commands/pi.js +18 -9
  26. package/dist/src/commands/plugin.js +19 -10
  27. package/dist/src/commands/profile-and-review.js +22 -13
  28. package/dist/src/commands/queue.js +19 -9
  29. package/dist/src/commands/remote.js +25 -16
  30. package/dist/src/commands/repo-git-harness.js +18 -9
  31. package/dist/src/commands/run.js +158 -805
  32. package/dist/src/commands/server.js +85 -25
  33. package/dist/src/commands/setup.js +73 -13
  34. package/dist/src/commands/stats.js +681 -0
  35. package/dist/src/commands/task-report-bug.js +24 -15
  36. package/dist/src/commands/task-run-driver.js +121 -49
  37. package/dist/src/commands/task.js +254 -893
  38. package/dist/src/commands/test.js +12 -3
  39. package/dist/src/commands/workspace.js +14 -5
  40. package/dist/src/commands.js +1339 -1650
  41. package/dist/src/index.js +1349 -1599
  42. package/dist/src/launcher.js +72 -10
  43. package/dist/src/runner.js +14 -5
  44. package/package.json +10 -7
  45. package/dist/src/commands/_pi-remote-session.js +0 -771
  46. package/dist/src/commands/_pi-worker-bridge-extension.js +0 -834
@@ -7,10 +7,19 @@ import { cancel as cancel2, confirm, isCancel as isCancel2 } from "@clack/prompt
7
7
 
8
8
  // packages/cli/src/runner.ts
9
9
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
10
- import { CliError } from "@rig/runtime/control-plane/errors";
10
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
11
11
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
12
12
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
13
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
13
+
14
+ class CliError extends RuntimeCliError {
15
+ hint;
16
+ constructor(message, exitCode = 1, options = {}) {
17
+ super(message, exitCode);
18
+ if (options.hint?.trim()) {
19
+ this.hint = options.hint.trim();
20
+ }
21
+ }
22
+ }
14
23
  function takeFlag(args, flag) {
15
24
  const rest = [];
16
25
  let value = false;
@@ -31,7 +40,7 @@ function takeOption(args, option) {
31
40
  if (current === option) {
32
41
  const next = args[index + 1];
33
42
  if (!next || next.startsWith("-")) {
34
- throw new CliError(`Missing value for ${option}`);
43
+ throw new CliError(`Missing value for ${option}`, 1, { hint: `Provide a value after ${option}, e.g. \`${option} <value>\`.` });
35
44
  }
36
45
  value = next;
37
46
  index += 1;
@@ -124,7 +133,7 @@ function readJsonFile(path) {
124
133
  try {
125
134
  return JSON.parse(readFileSync(path, "utf8"));
126
135
  } catch (error) {
127
- throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
136
+ 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>`." });
128
137
  }
129
138
  }
130
139
  function writeJsonFile(path, value) {
@@ -188,7 +197,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
188
197
  const global = readGlobalConnections(options);
189
198
  const connection = global.connections[repo.selected];
190
199
  if (!connection) {
191
- throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
200
+ throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
192
201
  }
193
202
  return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
194
203
  }
@@ -264,7 +273,7 @@ async function ensureServerForCli(projectRoot) {
264
273
  };
265
274
  } catch (error) {
266
275
  if (error instanceof Error) {
267
- throw new CliError2(error.message, 1);
276
+ throw new CliError(error.message, 1);
268
277
  }
269
278
  throw error;
270
279
  }
@@ -324,15 +333,64 @@ function diagnosticMessage(payload) {
324
333
  });
325
334
  return messages.length > 0 ? messages.join("; ") : null;
326
335
  }
336
+ var serverReachabilityCache = new Map;
337
+ async function probeServerReachability(baseUrl, authToken) {
338
+ try {
339
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
340
+ headers: mergeHeaders(undefined, authToken),
341
+ signal: AbortSignal.timeout(1500)
342
+ });
343
+ return response.ok;
344
+ } catch {
345
+ return false;
346
+ }
347
+ }
348
+ function cachedServerReachability(projectRoot, baseUrl, authToken) {
349
+ const key = resolve2(projectRoot);
350
+ const cached = serverReachabilityCache.get(key);
351
+ if (cached)
352
+ return cached;
353
+ const probe = probeServerReachability(baseUrl, authToken);
354
+ serverReachabilityCache.set(key, probe);
355
+ return probe;
356
+ }
357
+ function describeSelectedServer(projectRoot, server) {
358
+ try {
359
+ const selected = resolveSelectedConnection(projectRoot);
360
+ if (selected) {
361
+ return {
362
+ alias: selected.alias,
363
+ target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
364
+ };
365
+ }
366
+ } catch {}
367
+ return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
368
+ }
369
+ async function buildServerFailureContext(projectRoot, server) {
370
+ const { alias, target } = describeSelectedServer(projectRoot, server);
371
+ const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
372
+ const reachability = reachable ? "server is reachable" : "server is unreachable";
373
+ return {
374
+ contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
375
+ hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
376
+ };
377
+ }
327
378
  async function requestServerJson(context, pathname, init = {}) {
328
379
  const server = await ensureServerForCli(context.projectRoot);
329
380
  const headers = mergeHeaders(init.headers, server.authToken);
330
381
  if (server.serverProjectRoot)
331
382
  headers.set("x-rig-project-root", server.serverProjectRoot);
332
- const response = await fetch(`${server.baseUrl}${pathname}`, {
333
- ...init,
334
- headers
335
- });
383
+ let response;
384
+ try {
385
+ response = await fetch(`${server.baseUrl}${pathname}`, {
386
+ ...init,
387
+ headers
388
+ });
389
+ } catch (error) {
390
+ const failure = await buildServerFailureContext(context.projectRoot, server);
391
+ throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
392
+ ${failure.contextLine}`, 1, { hint: failure.hint });
393
+ }
336
394
  const text = await response.text();
337
395
  const payload = text.trim().length > 0 ? (() => {
338
396
  try {
@@ -344,7 +402,9 @@ async function requestServerJson(context, pathname, init = {}) {
344
402
  if (!response.ok) {
345
403
  const diagnostics = diagnosticMessage(payload);
346
404
  const detail = diagnostics ?? (text || response.statusText);
347
- throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
405
+ const failure = await buildServerFailureContext(context.projectRoot, server);
406
+ throw new CliError(`Rig server request failed (${response.status}): ${detail}
407
+ ${failure.contextLine}`, 1, { hint: failure.hint });
348
408
  }
349
409
  return payload;
350
410
  }
@@ -353,7 +413,7 @@ async function listWorkspaceTasksViaServer(context, filters = {}) {
353
413
  appendTaskFilterParams(url, filters);
354
414
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
355
415
  if (!Array.isArray(payload)) {
356
- throw new CliError2("Rig server returned an invalid task list payload.", 1);
416
+ throw new CliError("Rig server returned an invalid task list payload.", 1, { hint: "Check the selected server with `rig server status`; mixed CLI/server versions can mismatch \u2014 try `rig doctor`." });
357
417
  }
358
418
  return payload.flatMap((entry) => entry && typeof entry === "object" && !Array.isArray(entry) ? [entry] : []);
359
419
  }
@@ -369,7 +429,7 @@ async function selectNextWorkspaceTaskViaServer(context, filters = {}) {
369
429
  appendTaskFilterParams(url, filters);
370
430
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
371
431
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
372
- throw new CliError2("Rig server returned an invalid next-task payload.", 1);
432
+ throw new CliError("Rig server returned an invalid next-task payload.", 1, { hint: "Check the selected server with `rig server status`; try `rig task list` to see the raw task set." });
373
433
  }
374
434
  const record = payload;
375
435
  const rawTask = record.task;
@@ -415,26 +475,6 @@ async function steerRunViaServer(context, runId, message) {
415
475
  });
416
476
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
417
477
  }
418
- async function getRunPiSessionViaServer(context, runId) {
419
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi`);
420
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
421
- }
422
- async function getRunPiMessagesViaServer(context, runId) {
423
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/messages`);
424
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { messages: [] };
425
- }
426
- async function getRunPiStatusViaServer(context, runId) {
427
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/status`);
428
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
429
- }
430
- async function getRunPiCommandsViaServer(context, runId) {
431
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
432
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
433
- }
434
- async function getRunPiCapabilitiesViaServer(context, runId) {
435
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/capabilities`);
436
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { capabilities: null };
437
- }
438
478
  async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
439
479
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
440
480
  method: "POST",
@@ -443,44 +483,6 @@ async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior)
443
483
  });
444
484
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
445
485
  }
446
- async function sendRunPiShellViaServer(context, runId, text) {
447
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/shell`, {
448
- method: "POST",
449
- headers: { "content-type": "application/json" },
450
- body: JSON.stringify({ text })
451
- });
452
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
453
- }
454
- async function runRunPiCommandViaServer(context, runId, text) {
455
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands/run`, {
456
- method: "POST",
457
- headers: { "content-type": "application/json" },
458
- body: JSON.stringify({ text })
459
- });
460
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { type: "done" };
461
- }
462
- async function respondRunPiExtensionUiViaServer(context, runId, requestId, valueOrCancel) {
463
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/extension-ui/respond`, {
464
- method: "POST",
465
- headers: { "content-type": "application/json" },
466
- body: JSON.stringify({ requestId, ...valueOrCancel })
467
- });
468
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
469
- }
470
- async function abortRunPiViaServer(context, runId) {
471
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/abort`, { method: "POST" });
472
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { aborted: true };
473
- }
474
- async function buildRunPiEventsWebSocketUrl(context, runId) {
475
- const server = await ensureServerForCli(context.projectRoot);
476
- const url = new URL(`${server.baseUrl.replace(/\/+$/, "")}/api/runs/${encodeURIComponent(runId)}/pi/events`);
477
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
478
- if (server.authToken)
479
- url.searchParams.set("token", server.authToken);
480
- if (server.serverProjectRoot)
481
- url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
482
- return url.toString();
483
- }
484
486
  async function submitTaskRunViaServer(context, input) {
485
487
  const isTaskRun = Boolean(input.taskId);
486
488
  const endpoint = isTaskRun ? "/api/runs/task" : "/api/runs/adhoc";
@@ -504,11 +506,11 @@ async function submitTaskRunViaServer(context, input) {
504
506
  })
505
507
  });
506
508
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
507
- throw new CliError2("Rig server returned an invalid run submission payload.", 1);
509
+ throw new CliError("Rig server returned an invalid run submission payload.", 1, { hint: "Check `rig server status` and retry; `rig run list` shows whether the run was created anyway." });
508
510
  }
509
511
  const runId = payload.runId;
510
512
  if (typeof runId !== "string" || runId.trim().length === 0) {
511
- throw new CliError2("Rig server returned no runId for the submitted run.", 1);
513
+ throw new CliError("Rig server returned no runId for the submitted run.", 1, { hint: "Check `rig run list` \u2014 the run may still have been created; otherwise retry the submission." });
512
514
  }
513
515
  return { runId };
514
516
  }
@@ -678,9 +680,9 @@ async function runFastTaskRunPreflight(context, options = {}) {
678
680
  if (failures.length > 0) {
679
681
  const summary = failures.map((check) => `${check.label}${check.detail ? `: ${check.detail}` : ""}`).join("; ");
680
682
  if (failures.some((check) => check.id === "duplicate-active-run") && taskId) {
681
- throw new CliError2(`Task ${taskId} already has an active Rig run. ${summary}`, 1);
683
+ throw new CliError(`Task ${taskId} already has an active Rig run. ${summary}`, 1, { hint: `Attach to it with \`rig run attach <run-id> --follow\`, or stop it first with \`rig run stop <run-id>\`.` });
682
684
  }
683
- throw new CliError2(`Task run preflight failed: ${summary}`, 1);
685
+ throw new CliError(`Task run preflight failed: ${summary}`, 1, { hint: "Run `rig doctor` to diagnose, then retry `rig task run`." });
684
686
  }
685
687
  return { ok: true, checks };
686
688
  }
@@ -697,7 +699,7 @@ async function runProjectMainSyncPreflight(context, options) {
697
699
  runBootstrap: async () => {
698
700
  const bootstrap = await context.runCommand(["bun", "run", "bootstrap"]);
699
701
  if (bootstrap.exitCode !== 0) {
700
- throw new CliError2(bootstrap.stderr || bootstrap.stdout || "bun run bootstrap failed during project pre-run sync", bootstrap.exitCode || 1);
702
+ throw new CliError(bootstrap.stderr || bootstrap.stdout || "bun run bootstrap failed during project pre-run sync", bootstrap.exitCode || 1);
701
703
  }
702
704
  }
703
705
  });
@@ -1027,684 +1029,8 @@ async function selectTaskWithTextPicker(tasks, io = {}) {
1027
1029
  import { mkdtempSync, rmSync } from "fs";
1028
1030
  import { tmpdir } from "os";
1029
1031
  import { join } from "path";
1030
- import {
1031
- createAgentSessionFromServices,
1032
- createAgentSessionServices,
1033
- main as runPiMain
1034
- } from "@earendil-works/pi-coding-agent";
1035
-
1036
- // packages/cli/src/commands/_pi-remote-session.ts
1037
- import { AgentSession as PiAgentSession, expandPromptTemplate } from "@earendil-works/pi-coding-agent";
1038
- var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
1039
- function defaultTransport() {
1040
- return {
1041
- getSession: getRunPiSessionViaServer,
1042
- getMessages: getRunPiMessagesViaServer,
1043
- getStatus: getRunPiStatusViaServer,
1044
- getCommands: getRunPiCommandsViaServer,
1045
- getCapabilities: getRunPiCapabilitiesViaServer,
1046
- sendPrompt: sendRunPiPromptViaServer,
1047
- sendShell: sendRunPiShellViaServer,
1048
- runCommand: runRunPiCommandViaServer,
1049
- abort: abortRunPiViaServer,
1050
- buildEventsWebSocketUrl: buildRunPiEventsWebSocketUrl
1051
- };
1052
- }
1053
- function recordOf(value) {
1054
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1055
- }
1056
- function emptyRemoteStatus() {
1057
- return {
1058
- isStreaming: false,
1059
- isCompacting: false,
1060
- isBashRunning: false,
1061
- pendingMessageCount: 0,
1062
- steeringMessages: [],
1063
- followUpMessages: [],
1064
- model: null,
1065
- thinkingLevel: null,
1066
- sessionName: null,
1067
- cwd: null,
1068
- stats: null,
1069
- contextUsage: null
1070
- };
1071
- }
1072
- function resolveAttachReadyTimeoutMs() {
1073
- const raw = Number.parseInt(process.env.RIG_PI_ATTACH_TIMEOUT_MS ?? "", 10);
1074
- return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60000;
1075
- }
1076
-
1077
- class RigRemoteSessionController {
1078
- context;
1079
- runId;
1080
- status = emptyRemoteStatus();
1081
- transport;
1082
- hooks = {};
1083
- session = null;
1084
- socket = null;
1085
- closed = false;
1086
- pendingShells = [];
1087
- pendingCompactions = [];
1088
- constructor(input) {
1089
- this.context = input.context;
1090
- this.runId = input.runId;
1091
- this.transport = input.transport ?? defaultTransport();
1092
- }
1093
- ingestEnvelope(envelopeValue) {
1094
- this.applyEnvelope(envelopeValue);
1095
- }
1096
- bindSession(session) {
1097
- this.session = session;
1098
- }
1099
- setUiHooks(hooks) {
1100
- this.hooks = hooks;
1101
- }
1102
- async connect() {
1103
- const ready = await this.waitForReady();
1104
- if (!ready || this.closed)
1105
- return;
1106
- let catchupDone = false;
1107
- const buffered = [];
1108
- const wsUrl = await this.transport.buildEventsWebSocketUrl(this.context, this.runId);
1109
- const socket = new WebSocket(wsUrl);
1110
- this.socket = socket;
1111
- socket.onopen = () => {
1112
- this.hooks.onConnectionChange?.(true);
1113
- this.hooks.onStatusText?.("worker session live");
1114
- };
1115
- socket.onmessage = (message2) => {
1116
- try {
1117
- const payload = typeof message2.data === "string" ? JSON.parse(message2.data) : JSON.parse(Buffer.from(message2.data).toString("utf8"));
1118
- if (!catchupDone)
1119
- buffered.push(payload);
1120
- else
1121
- this.applyEnvelope(payload);
1122
- } catch (error) {
1123
- this.hooks.onError?.(`Unparseable worker Pi event: ${error instanceof Error ? error.message : String(error)}`);
1124
- }
1125
- };
1126
- socket.onerror = () => socket.close();
1127
- socket.onclose = () => {
1128
- this.hooks.onConnectionChange?.(false);
1129
- if (!this.closed)
1130
- this.hooks.onStatusText?.("worker Pi WebSocket disconnected");
1131
- };
1132
- try {
1133
- const [messagesPayload, statusPayload, commandsPayload, capabilitiesPayload] = await Promise.all([
1134
- this.transport.getMessages(this.context, this.runId),
1135
- this.transport.getStatus(this.context, this.runId),
1136
- this.transport.getCommands(this.context, this.runId),
1137
- this.transport.getCapabilities(this.context, this.runId).catch(() => ({ capabilities: null }))
1138
- ]);
1139
- const messages = Array.isArray(messagesPayload.messages) ? messagesPayload.messages : [];
1140
- this.applyStatusPayload(statusPayload);
1141
- this.session?.replaceRemoteMessages(messages);
1142
- const commands = Array.isArray(commandsPayload.commands) ? commandsPayload.commands : [];
1143
- const capabilities = capabilitiesPayload.capabilities && typeof capabilitiesPayload.capabilities === "object" && !Array.isArray(capabilitiesPayload.capabilities) ? capabilitiesPayload.capabilities : null;
1144
- this.hooks.onCatchUp?.(messages, commands, capabilities);
1145
- catchupDone = true;
1146
- for (const payload of buffered.splice(0))
1147
- this.applyEnvelope(payload);
1148
- } catch (error) {
1149
- catchupDone = true;
1150
- this.hooks.onError?.(`Worker Pi catch-up failed: ${error instanceof Error ? error.message : String(error)}`);
1151
- }
1152
- }
1153
- close() {
1154
- this.closed = true;
1155
- this.socket?.close();
1156
- for (const shell of this.pendingShells.splice(0))
1157
- shell.reject(new Error("Remote session closed."));
1158
- for (const compaction of this.pendingCompactions.splice(0))
1159
- compaction.reject(new Error("Remote session closed."));
1160
- }
1161
- async sendPrompt(text, streamingBehavior) {
1162
- await this.transport.sendPrompt(this.context, this.runId, text, streamingBehavior);
1163
- }
1164
- async sendCommand(text) {
1165
- const result = await this.transport.runCommand(this.context, this.runId, text);
1166
- return typeof result.message === "string" ? result.message : "worker command accepted";
1167
- }
1168
- async sendShell(text) {
1169
- await this.transport.sendShell(this.context, this.runId, text);
1170
- }
1171
- async abort() {
1172
- await this.transport.abort(this.context, this.runId);
1173
- }
1174
- registerPendingShell(shell) {
1175
- this.pendingShells.push(shell);
1176
- }
1177
- failPendingShell(shell, error) {
1178
- const index = this.pendingShells.indexOf(shell);
1179
- if (index !== -1)
1180
- this.pendingShells.splice(index, 1);
1181
- shell.reject(error);
1182
- }
1183
- registerPendingCompaction(pending) {
1184
- this.pendingCompactions.push(pending);
1185
- }
1186
- async waitForReady() {
1187
- const startedAt = Date.now();
1188
- const deadline = startedAt + resolveAttachReadyTimeoutMs();
1189
- let consecutiveFailures = 0;
1190
- while (!this.closed) {
1191
- let requestFailed = false;
1192
- const session = await this.transport.getSession(this.context, this.runId).catch((error) => {
1193
- requestFailed = true;
1194
- return { ready: false, status: error instanceof Error ? error.message : String(error), retryAfterMs: 1000 };
1195
- });
1196
- if (session.ready !== false)
1197
- return true;
1198
- consecutiveFailures = requestFailed ? consecutiveFailures + 1 : 0;
1199
- const status = String(session.status ?? "starting");
1200
- if (TERMINAL_RUN_STATUSES.has(status.toLowerCase())) {
1201
- this.hooks.onError?.(`Run ended before the worker Pi daemon became ready: ${status}. Inspect with \`rig run show ${this.runId}\`.`);
1202
- return false;
1203
- }
1204
- this.hooks.onStatusText?.(consecutiveFailures >= 5 ? "Rig server unreachable \xB7 retrying \xB7 /detach to exit" : `waiting for worker Pi daemon \xB7 ${status} \xB7 /detach to exit`);
1205
- if (Date.now() >= deadline) {
1206
- this.hooks.onError?.(`Worker Pi daemon did not become ready (last status: ${status}). Set RIG_PI_ATTACH_TIMEOUT_MS to wait longer.`);
1207
- return false;
1208
- }
1209
- await Bun.sleep(typeof session.retryAfterMs === "number" ? session.retryAfterMs : 750);
1210
- }
1211
- return false;
1212
- }
1213
- applyEnvelope(envelopeValue) {
1214
- const envelope = recordOf(envelopeValue);
1215
- if (!envelope)
1216
- return;
1217
- const type = String(envelope.type ?? "");
1218
- if (type === "status.update") {
1219
- this.applyStatusPayload(envelope);
1220
- return;
1221
- }
1222
- if (type === "activity.update") {
1223
- const activity = recordOf(envelope.activity);
1224
- this.hooks.onActivity?.(String(activity?.label ?? ""), activity?.detail ? String(activity.detail) : undefined);
1225
- return;
1226
- }
1227
- if (type === "extension_ui_request") {
1228
- const request = recordOf(envelope.request);
1229
- if (request)
1230
- this.hooks.onExtensionUiRequest?.(request);
1231
- return;
1232
- }
1233
- if (type === "pi.ui_event") {
1234
- this.applyShellUiEvent(envelope.event);
1235
- return;
1236
- }
1237
- if (type === "pi.event") {
1238
- this.session?.handleRemoteSessionEvent(envelope.event);
1239
- this.settlePendingCompaction(envelope.event);
1240
- return;
1241
- }
1242
- if (type === "error") {
1243
- this.hooks.onError?.(String(envelope.message ?? envelope.detail ?? "unknown worker error"));
1244
- }
1245
- }
1246
- applyStatusPayload(payload) {
1247
- const status = recordOf(payload.status) ?? payload;
1248
- const next = this.status;
1249
- next.isStreaming = status.isStreaming === true;
1250
- next.isCompacting = status.isCompacting === true;
1251
- next.isBashRunning = status.isBashRunning === true;
1252
- next.pendingMessageCount = typeof status.pendingMessageCount === "number" ? status.pendingMessageCount : 0;
1253
- next.steeringMessages = Array.isArray(status.steeringMessages) ? status.steeringMessages.map(String) : [];
1254
- next.followUpMessages = Array.isArray(status.followUpMessages) ? status.followUpMessages.map(String) : [];
1255
- next.model = recordOf(status.model) ?? next.model;
1256
- next.thinkingLevel = typeof status.thinkingLevel === "string" ? status.thinkingLevel : next.thinkingLevel;
1257
- next.sessionName = typeof status.sessionName === "string" ? status.sessionName : next.sessionName;
1258
- next.cwd = typeof status.cwd === "string" ? status.cwd : next.cwd;
1259
- next.stats = recordOf(status.stats) ?? next.stats;
1260
- next.contextUsage = recordOf(status.contextUsage) ?? next.contextUsage;
1261
- }
1262
- applyShellUiEvent(value) {
1263
- const event = recordOf(value);
1264
- if (!event)
1265
- return;
1266
- const type = String(event.type ?? "");
1267
- const pending = this.pendingShells[0];
1268
- if (type === "shell.chunk" && pending) {
1269
- pending.sawChunk = true;
1270
- pending.onData(Buffer.from(String(event.chunk ?? "")));
1271
- return;
1272
- }
1273
- if (type === "shell.end" && pending) {
1274
- const output = String(event.output ?? "");
1275
- if (output && !pending.sawChunk)
1276
- pending.onData(Buffer.from(output));
1277
- const index = this.pendingShells.indexOf(pending);
1278
- if (index !== -1)
1279
- this.pendingShells.splice(index, 1);
1280
- pending.resolve({ exitCode: typeof event.exitCode === "number" ? event.exitCode : event.isError === true ? 1 : 0 });
1281
- }
1282
- }
1283
- settlePendingCompaction(eventValue) {
1284
- const event = recordOf(eventValue);
1285
- if (!event)
1286
- return;
1287
- const type = String(event.type ?? "");
1288
- if (type !== "compaction_end" || this.pendingCompactions.length === 0)
1289
- return;
1290
- const pending = this.pendingCompactions.shift();
1291
- const result = recordOf(event.result);
1292
- if (result)
1293
- pending.resolve(result);
1294
- else if (event.aborted === true)
1295
- pending.reject(new Error("Compaction aborted on the worker."));
1296
- else
1297
- pending.resolve({});
1298
- }
1299
- }
1300
-
1301
- class RigRemoteAgentSession extends PiAgentSession {
1302
- remote;
1303
- constructor(config, remote) {
1304
- super(config);
1305
- this.remote = remote;
1306
- remote.bindSession(this);
1307
- }
1308
- handleRemoteSessionEvent(eventValue) {
1309
- const event = recordOf(eventValue);
1310
- if (!event)
1311
- return;
1312
- const type = String(event.type ?? "");
1313
- if ((type === "message_end" || type === "turn_end") && recordOf(event.message)) {
1314
- this.agent.state.messages = [...this.agent.state.messages, event.message];
1315
- }
1316
- this._emit(eventValue);
1317
- }
1318
- replaceRemoteMessages(messages) {
1319
- this.agent.state.messages = messages;
1320
- }
1321
- async expandLocalInput(text) {
1322
- let current = text;
1323
- const runner = this.extensionRunner;
1324
- if (runner.hasHandlers("input")) {
1325
- const result = await runner.emitInput(current, undefined, "interactive");
1326
- if (result.action === "handled")
1327
- return null;
1328
- if (result.action === "transform")
1329
- current = result.text;
1330
- }
1331
- current = this._expandSkillCommand(current);
1332
- current = expandPromptTemplate(current, [...this.promptTemplates]);
1333
- return current;
1334
- }
1335
- async prompt(text, options) {
1336
- const trimmed = text.trim();
1337
- if (!trimmed)
1338
- return;
1339
- if (trimmed.startsWith("/")) {
1340
- if (await this._tryExecuteExtensionCommand(trimmed))
1341
- return;
1342
- const expanded2 = await this.expandLocalInput(trimmed);
1343
- if (expanded2 === null)
1344
- return;
1345
- if (expanded2 !== trimmed) {
1346
- const behavior2 = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
1347
- options?.preflightResult?.(true);
1348
- await this.remote.sendPrompt(expanded2, behavior2);
1349
- return;
1350
- }
1351
- await this.remote.sendCommand(trimmed);
1352
- return;
1353
- }
1354
- if (trimmed.startsWith("!")) {
1355
- await this.remote.sendShell(trimmed);
1356
- return;
1357
- }
1358
- const expanded = await this.expandLocalInput(trimmed);
1359
- if (expanded === null)
1360
- return;
1361
- const behavior = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
1362
- options?.preflightResult?.(true);
1363
- await this.remote.sendPrompt(expanded, behavior);
1364
- }
1365
- async steer(text) {
1366
- const trimmed = text.trim();
1367
- if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
1368
- return;
1369
- const expanded = await this.expandLocalInput(trimmed);
1370
- if (expanded === null)
1371
- return;
1372
- await this.remote.sendPrompt(expanded, "steer");
1373
- }
1374
- async followUp(text) {
1375
- const trimmed = text.trim();
1376
- if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
1377
- return;
1378
- const expanded = await this.expandLocalInput(trimmed);
1379
- if (expanded === null)
1380
- return;
1381
- await this.remote.sendPrompt(expanded, "followUp");
1382
- }
1383
- async abort() {
1384
- await this.remote.abort();
1385
- }
1386
- async compact(customInstructions) {
1387
- const pending = new Promise((resolve3, reject) => {
1388
- this.remote.registerPendingCompaction({ resolve: resolve3, reject });
1389
- });
1390
- await this.remote.sendCommand(`/compact${customInstructions ? ` ${customInstructions}` : ""}`);
1391
- return pending;
1392
- }
1393
- clearQueue() {
1394
- const cleared = {
1395
- steering: [...this.remote.status.steeringMessages],
1396
- followUp: [...this.remote.status.followUpMessages]
1397
- };
1398
- this.remote.status.steeringMessages = [];
1399
- this.remote.status.followUpMessages = [];
1400
- this.remote.status.pendingMessageCount = 0;
1401
- this.remote.sendCommand("/queue-clear").catch(() => {});
1402
- return cleared;
1403
- }
1404
- get isStreaming() {
1405
- return this.remote.status.isStreaming;
1406
- }
1407
- get isCompacting() {
1408
- return this.remote.status.isCompacting;
1409
- }
1410
- get isBashRunning() {
1411
- return this.remote.status.isBashRunning;
1412
- }
1413
- get pendingMessageCount() {
1414
- return this.remote.status.pendingMessageCount;
1415
- }
1416
- getSteeringMessages() {
1417
- return this.remote.status.steeringMessages;
1418
- }
1419
- getFollowUpMessages() {
1420
- return this.remote.status.followUpMessages;
1421
- }
1422
- get model() {
1423
- return this.remote.status.model ?? super.model;
1424
- }
1425
- async setModel(model) {
1426
- await this.remote.sendCommand(`/model ${model.provider}/${model.id}`);
1427
- this.remote.status.model = model;
1428
- }
1429
- get thinkingLevel() {
1430
- return this.remote.status.thinkingLevel ?? super.thinkingLevel;
1431
- }
1432
- setThinkingLevel(level) {
1433
- this.remote.status.thinkingLevel = level;
1434
- this.remote.sendCommand(`/thinking ${level}`).catch(() => {});
1435
- }
1436
- get sessionName() {
1437
- return this.remote.status.sessionName ?? super.sessionName;
1438
- }
1439
- setSessionName(name) {
1440
- this.remote.status.sessionName = name;
1441
- this.remote.sendCommand(`/name ${name}`).catch(() => {});
1442
- }
1443
- getSessionStats() {
1444
- return this.remote.status.stats ?? super.getSessionStats();
1445
- }
1446
- getContextUsage() {
1447
- return this.remote.status.contextUsage ?? super.getContextUsage();
1448
- }
1449
- dispose() {
1450
- this.remote.close();
1451
- super.dispose();
1452
- }
1453
- }
1454
- function createRemoteBashOperations(controller, excludeFromContext) {
1455
- return {
1456
- exec(command, _cwd, execOptions) {
1457
- return new Promise((resolve3, reject) => {
1458
- const pending = { onData: execOptions.onData, resolve: resolve3, reject, sawChunk: false };
1459
- const cleanup = () => {
1460
- execOptions.signal?.removeEventListener("abort", onAbort);
1461
- if (timer)
1462
- clearTimeout(timer);
1463
- };
1464
- const onAbort = () => {
1465
- cleanup();
1466
- controller.failPendingShell(pending, new Error("Remote worker shell command aborted locally."));
1467
- };
1468
- const timeoutMs = typeof execOptions.timeout === "number" && execOptions.timeout > 0 ? execOptions.timeout : 0;
1469
- const timer = timeoutMs > 0 ? setTimeout(() => {
1470
- cleanup();
1471
- controller.failPendingShell(pending, new Error(`Remote worker shell command timed out after ${timeoutMs}ms.`));
1472
- }, timeoutMs) : null;
1473
- const wrappedResolve = pending.resolve;
1474
- const wrappedReject = pending.reject;
1475
- pending.resolve = (result) => {
1476
- cleanup();
1477
- wrappedResolve(result);
1478
- };
1479
- pending.reject = (error) => {
1480
- cleanup();
1481
- wrappedReject(error);
1482
- };
1483
- execOptions.signal?.addEventListener("abort", onAbort, { once: true });
1484
- controller.registerPendingShell(pending);
1485
- controller.sendShell(`${excludeFromContext ? "!!" : "!"}${command}`).catch((error) => {
1486
- cleanup();
1487
- controller.failPendingShell(pending, error instanceof Error ? error : new Error(String(error)));
1488
- });
1489
- });
1490
- }
1491
- };
1492
- }
1493
-
1494
- // packages/cli/src/commands/_spinner.ts
1495
- var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1496
-
1497
- // packages/cli/src/commands/_pi-worker-bridge-extension.ts
1498
- function recordOf2(value) {
1499
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1500
- }
1501
- function asText(value) {
1502
- if (typeof value === "string")
1503
- return value;
1504
- if (value === null || value === undefined)
1505
- return "";
1506
- if (typeof value === "number" || typeof value === "boolean")
1507
- return String(value);
1508
- try {
1509
- return JSON.stringify(value);
1510
- } catch {
1511
- return String(value);
1512
- }
1513
- }
1514
- function names(value, key = "name") {
1515
- if (!Array.isArray(value))
1516
- return [];
1517
- return value.flatMap((entry) => {
1518
- if (typeof entry === "string")
1519
- return [entry];
1520
- const record = recordOf2(entry);
1521
- const name = record?.[key];
1522
- return typeof name === "string" ? [name] : [];
1523
- });
1524
- }
1525
- function renderWorkerCapabilities(capabilities) {
1526
- const lines = ["Worker session capabilities (in effect for this run)"];
1527
- const tools = Array.isArray(capabilities.tools) ? capabilities.tools : [];
1528
- const activeTools = tools.flatMap((tool) => {
1529
- const record = recordOf2(tool);
1530
- return record && record.active === true && typeof record.name === "string" ? [record.name] : [];
1531
- });
1532
- const inactiveCount = tools.length - activeTools.length;
1533
- lines.push(` tools ${activeTools.join(", ") || "(none)"}${inactiveCount > 0 ? ` (+${inactiveCount} inactive)` : ""}`);
1534
- const extensions = Array.isArray(capabilities.extensions) ? capabilities.extensions : [];
1535
- const extensionLabels = extensions.flatMap((entry) => {
1536
- const record = recordOf2(entry);
1537
- if (!record)
1538
- return [];
1539
- const path = typeof record.path === "string" ? record.path : "";
1540
- const short = path.split("/").filter(Boolean).slice(-2).join("/") || path || "extension";
1541
- return [short];
1542
- });
1543
- lines.push(` extensions ${extensionLabels.join(", ") || "(none)"}`);
1544
- const hookEvents = names(capabilities.hookEvents);
1545
- const hookList = Array.isArray(capabilities.hookEvents) ? capabilities.hookEvents.map(asText).filter(Boolean) : hookEvents;
1546
- lines.push(` hooks ${hookList.join(", ") || "(none)"}`);
1547
- lines.push(` skills ${names(capabilities.skills).join(", ") || "(none)"}`);
1548
- lines.push(` prompts ${names(capabilities.prompts).join(", ") || "(none)"}`);
1549
- const model = typeof capabilities.model === "string" ? capabilities.model : "(unknown)";
1550
- const thinking = typeof capabilities.thinkingLevel === "string" ? capabilities.thinkingLevel : "";
1551
- const cwd = typeof capabilities.cwd === "string" ? capabilities.cwd : "";
1552
- lines.push(` model ${model}${thinking ? ` \xB7 ${thinking}` : ""}`);
1553
- if (cwd)
1554
- lines.push(` cwd ${cwd}`);
1555
- lines.push(" (worker commands are in your palette as [worker \u2026] \xB7 /worker shows this again)");
1556
- return lines;
1557
- }
1558
- async function answerExtensionUiRequest(options, ctx, request) {
1559
- const requestId = String(request.requestId ?? request.id ?? `ui-${Date.now()}`);
1560
- const method = String(request.method ?? request.type ?? "input");
1561
- const prompt = asText(request.prompt ?? request.message ?? request.title ?? method);
1562
- const rawOptions = Array.isArray(request.options) ? request.options : Array.isArray(request.items) ? request.items : [];
1563
- const choices = rawOptions.map((option) => {
1564
- const record = recordOf2(option);
1565
- return record ? asText(record.label ?? record.value ?? record.name ?? option) : asText(option);
1566
- }).filter(Boolean);
1567
- try {
1568
- if (method === "confirm") {
1569
- const confirmed = await ctx.ui.confirm("Worker request", prompt);
1570
- await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, { value: confirmed, confirmed });
1571
- return;
1572
- }
1573
- if (choices.length > 0) {
1574
- const selected = await ctx.ui.select(prompt, choices);
1575
- await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, selected === undefined ? { cancelled: true } : { value: selected });
1576
- return;
1577
- }
1578
- const value = await ctx.ui.input("Worker request", prompt);
1579
- await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, value === undefined ? { cancelled: true } : { value });
1580
- } catch (error) {
1581
- ctx.ui.notify(`Failed to answer worker UI request: ${error instanceof Error ? error.message : String(error)}`, "error");
1582
- }
1583
- }
1584
- var LOCALLY_OWNED_COMMANDS = new Set(["detach", "quit", "q", "stop", "worker", "settings", "model", "thinking", "compact", "session", "name", "reload"]);
1585
- function registerDaemonCommands(pi, options, ctx, commands, registered) {
1586
- for (const command of commands) {
1587
- const record = recordOf2(command);
1588
- const name = typeof record?.name === "string" ? record.name : "";
1589
- const source = typeof record?.source === "string" ? record.source : "worker";
1590
- if (!name || source === "builtin" || registered.has(name) || LOCALLY_OWNED_COMMANDS.has(name))
1591
- continue;
1592
- registered.add(name);
1593
- const description = typeof record?.description === "string" ? record.description : undefined;
1594
- try {
1595
- pi.registerCommand(name, {
1596
- description: `[worker ${source}] ${description ?? ""}`.trim(),
1597
- handler: async (args) => {
1598
- try {
1599
- const message2 = await options.controller.sendCommand(`/${name}${args ? ` ${args}` : ""}`);
1600
- ctx.ui.notify(message2, "info");
1601
- } catch (error) {
1602
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1603
- }
1604
- }
1605
- });
1606
- } catch {}
1607
- }
1608
- }
1609
- function createRigWorkerPiBridgeExtension(options) {
1610
- return (pi) => {
1611
- const registeredDaemonCommands = new Set;
1612
- let capabilityLines = null;
1613
- let statusText = "connecting to worker session";
1614
- let busy = true;
1615
- let connected = false;
1616
- let frame = 0;
1617
- let spinnerTimer = null;
1618
- const renderStatus = (ctx) => {
1619
- const prefix = busy ? `${SPINNER_FRAMES[frame]} ` : connected ? "\u25CF " : "\u25CB ";
1620
- ctx.ui.setStatus("rig-worker-pi", `${prefix}${statusText}`);
1621
- };
1622
- const setStatus = (ctx, text, isBusy) => {
1623
- statusText = text;
1624
- busy = isBusy;
1625
- renderStatus(ctx);
1626
- };
1627
- pi.registerCommand("detach", {
1628
- description: "Detach from this run; the worker keeps going",
1629
- handler: async (_args, ctx) => {
1630
- ctx.ui.notify("Detached locally; the worker Pi daemon continues.", "info");
1631
- ctx.shutdown();
1632
- }
1633
- });
1634
- pi.registerCommand("stop", {
1635
- description: "Stop the worker Pi run and detach",
1636
- handler: async (_args, ctx) => {
1637
- await options.controller.abort();
1638
- ctx.ui.notify("Stop requested for the worker Pi daemon.", "info");
1639
- ctx.shutdown();
1640
- }
1641
- });
1642
- pi.registerCommand("worker", {
1643
- description: "Show the worker session's real capabilities (tools, extensions, hooks, skills)",
1644
- handler: async (_args, ctx) => {
1645
- if (capabilityLines)
1646
- ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
1647
- else
1648
- ctx.ui.notify("Worker capabilities are not available yet (still connecting).", "info");
1649
- }
1650
- });
1651
- pi.on("user_bash", (event) => ({
1652
- operations: createRemoteBashOperations(options.controller, event.excludeFromContext === true)
1653
- }));
1654
- pi.on("session_start", async (_event, ctx) => {
1655
- ctx.ui.setTitle("Rig \xB7 enriched bundled Pi");
1656
- setStatus(ctx, "waiting for worker Pi daemon", true);
1657
- spinnerTimer = setInterval(() => {
1658
- frame = (frame + 1) % SPINNER_FRAMES.length;
1659
- if (busy)
1660
- renderStatus(ctx);
1661
- }, 150);
1662
- ctx.ui.notify(`Enriched bundled Pi \u2014 native UI + Rig layers, worker brain. Attached to run ${options.runId}. /worker shows live capabilities \xB7 /detach exits \xB7 /stop cancels.`, "info");
1663
- if (options.initialMessageSent)
1664
- ctx.ui.notify("Initial message sent to the worker Pi daemon.", "info");
1665
- const nativeUi = ctx.ui;
1666
- options.controller.setUiHooks({
1667
- onStatusText: (text) => setStatus(ctx, text, !connected),
1668
- onActivity: (label, detail) => {
1669
- const active = label !== "idle" && !/complete|ready/i.test(label);
1670
- setStatus(ctx, [label, detail].filter(Boolean).join(" \u2014 "), active);
1671
- if (/agent running|tool:/.test(label))
1672
- ctx.ui.setWidget("rig-worker-capabilities", undefined);
1673
- },
1674
- onConnectionChange: (nextConnected) => {
1675
- connected = nextConnected;
1676
- setStatus(ctx, nextConnected ? "worker session live" : "worker session disconnected", false);
1677
- },
1678
- onError: (message2) => ctx.ui.notify(message2, "error"),
1679
- onExtensionUiRequest: (request) => {
1680
- answerExtensionUiRequest(options, ctx, request);
1681
- },
1682
- onCatchUp: (messages, commands, capabilities) => {
1683
- if (nativeUi.appendSessionMessages)
1684
- nativeUi.appendSessionMessages(messages);
1685
- const cwd = options.controller.status.cwd;
1686
- if (nativeUi.setDisplayCwd && cwd)
1687
- nativeUi.setDisplayCwd(cwd);
1688
- registerDaemonCommands(pi, options, ctx, commands, registeredDaemonCommands);
1689
- if (capabilities) {
1690
- capabilityLines = renderWorkerCapabilities(capabilities);
1691
- ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
1692
- }
1693
- }
1694
- });
1695
- options.controller.connect().catch((error) => {
1696
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1697
- });
1698
- });
1699
- pi.on("session_shutdown", () => {
1700
- if (spinnerTimer)
1701
- clearInterval(spinnerTimer);
1702
- options.controller.close();
1703
- });
1704
- };
1705
- }
1706
-
1707
- // packages/cli/src/commands/_pi-frontend.ts
1032
+ import { main as runPiMain } from "@earendil-works/pi-coding-agent";
1033
+ import createPiRigExtension from "@rig/pi-rig";
1708
1034
  function setTemporaryEnv(updates) {
1709
1035
  const previous = new Map;
1710
1036
  for (const [key, value] of Object.entries(updates)) {
@@ -1720,51 +1046,38 @@ function setTemporaryEnv(updates) {
1720
1046
  }
1721
1047
  };
1722
1048
  }
1049
+ function buildOperatorPiEnv(input) {
1050
+ return {
1051
+ PI_CODING_AGENT_SESSION_DIR: input.sessionDir,
1052
+ PI_SKIP_VERSION_CHECK: "1",
1053
+ RIG_PI_OPERATOR_SESSION: "1",
1054
+ RIG_RUN_ID: input.runId,
1055
+ RIG_SERVER_URL: input.serverUrl,
1056
+ ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {},
1057
+ ...input.serverProjectRoot ? { RIG_PROJECT_ROOT: input.serverProjectRoot } : {}
1058
+ };
1059
+ }
1723
1060
  async function attachRunBundledPiFrontend(context, input) {
1724
1061
  const tempSessionDir = mkdtempSync(join(tmpdir(), "rig-pi-frontend-sessions-"));
1725
- const restoreEnv = setTemporaryEnv({
1726
- PI_CODING_AGENT_SESSION_DIR: tempSessionDir,
1727
- PI_SKIP_VERSION_CHECK: "1",
1728
- PI_HIDDEN_COMMANDS: "import,fork,clone,tree,new,resume,trust"
1729
- });
1730
- const controller = new RigRemoteSessionController({ context, runId: input.runId });
1731
- const createRemoteRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
1732
- const services = await createAgentSessionServices({
1733
- cwd,
1734
- agentDir,
1735
- resourceLoaderOptions: {
1736
- extensionFactories: [
1737
- createRigWorkerPiBridgeExtension({
1738
- context,
1739
- controller,
1740
- runId: input.runId,
1741
- initialMessageSent: input.steered === true
1742
- })
1743
- ],
1744
- noExtensions: true,
1745
- noSkills: true,
1746
- noPromptTemplates: true,
1747
- noContextFiles: true
1748
- }
1749
- });
1750
- const created = await createAgentSessionFromServices({
1751
- services,
1752
- sessionManager,
1753
- sessionStartEvent,
1754
- noTools: "all",
1755
- sessionFactory: (config) => new RigRemoteAgentSession(config, controller)
1756
- });
1757
- return { ...created, services, diagnostics: services.diagnostics };
1062
+ const server = await ensureServerForCli(context.projectRoot);
1063
+ const restoreEnv = setTemporaryEnv(buildOperatorPiEnv({
1064
+ runId: input.runId,
1065
+ serverUrl: server.baseUrl,
1066
+ authToken: server.authToken,
1067
+ serverProjectRoot: server.serverProjectRoot,
1068
+ sessionDir: tempSessionDir
1069
+ }));
1070
+ const piRigExtensionFactory = (pi) => {
1071
+ createPiRigExtension(pi);
1758
1072
  };
1759
1073
  let detached = false;
1760
1074
  try {
1761
1075
  await runPiMain([], {
1762
- createRuntimeOverride: () => createRemoteRuntime
1076
+ extensionFactories: [piRigExtensionFactory]
1763
1077
  });
1764
1078
  detached = true;
1765
1079
  } finally {
1766
1080
  restoreEnv();
1767
- controller.close();
1768
1081
  rmSync(tempSessionDir, { recursive: true, force: true });
1769
1082
  }
1770
1083
  let run = { runId: input.runId, status: "unknown" };
@@ -1778,12 +1091,12 @@ async function attachRunBundledPiFrontend(context, input) {
1778
1091
  timelineCursor: null,
1779
1092
  steered: input.steered === true,
1780
1093
  detached,
1781
- rendered: "enriched bundled Pi frontend with remote worker session runtime"
1094
+ rendered: "stock Pi operator console with the pi-rig extension"
1782
1095
  };
1783
1096
  }
1784
1097
 
1785
1098
  // packages/cli/src/commands/_operator-view.ts
1786
- var TERMINAL_RUN_STATUSES2 = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
1099
+ var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
1787
1100
  function runStatusFromPayload(payload) {
1788
1101
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
1789
1102
  return String(run.status ?? "unknown").toLowerCase();
@@ -1860,7 +1173,7 @@ async function attachRunOperatorView(context, input) {
1860
1173
  }
1861
1174
  const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
1862
1175
  let timelineCursor = snapshot.timelineCursor;
1863
- while (!detached && !TERMINAL_RUN_STATUSES2.has(runStatusFromPayload(snapshot.run))) {
1176
+ while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
1864
1177
  await Bun.sleep(pollMs);
1865
1178
  snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
1866
1179
  timelineCursor = snapshot.timelineCursor;
@@ -2042,55 +1355,91 @@ function formatSubmittedRun(input) {
2042
1355
  `);
2043
1356
  }
2044
1357
 
1358
+ // packages/cli/src/commands/inbox.ts
1359
+ async function listInboxRecords(context, kind, filters) {
1360
+ const params = new URLSearchParams;
1361
+ if (filters.run)
1362
+ params.set("runId", filters.run);
1363
+ if (filters.task)
1364
+ params.set("taskId", filters.task);
1365
+ const query = params.size > 0 ? `?${params.toString()}` : "";
1366
+ const payload = await requestServerJson(context, `/api/inbox/${kind}${query}`);
1367
+ const records = Array.isArray(payload) ? payload : [];
1368
+ return filters.pendingOnly ? records.filter((entry) => (entry.status ?? "pending") !== "resolved") : records;
1369
+ }
1370
+ async function readPendingInboxCounts(context) {
1371
+ try {
1372
+ const [approvals, inputs] = await Promise.all([
1373
+ listInboxRecords(context, "approvals", { pendingOnly: true }),
1374
+ listInboxRecords(context, "inputs", { pendingOnly: true })
1375
+ ]);
1376
+ return { approvals: approvals.length, inputs: inputs.length };
1377
+ } catch {
1378
+ return null;
1379
+ }
1380
+ }
1381
+ async function printPendingInboxFooter(context) {
1382
+ if (context.outputMode !== "text")
1383
+ return;
1384
+ const counts = await readPendingInboxCounts(context);
1385
+ if (!counts || counts.approvals === 0 && counts.inputs === 0)
1386
+ return;
1387
+ const parts = [];
1388
+ if (counts.approvals > 0)
1389
+ parts.push(`${counts.approvals} approval${counts.approvals === 1 ? "" : "s"}`);
1390
+ if (counts.inputs > 0)
1391
+ parts.push(`${counts.inputs} input request${counts.inputs === 1 ? "" : "s"}`);
1392
+ console.log(`
1393
+ \u26A0 ${parts.join(" and ")} pending \u2014 run \`rig inbox\` to review.`);
1394
+ }
1395
+
2045
1396
  // packages/cli/src/commands/_help-catalog.ts
2046
1397
  import { intro, log as log2, note as note2, outro } from "@clack/prompts";
2047
1398
  import pc2 from "picocolors";
2048
1399
  var TOP_LEVEL_SECTIONS = [
2049
1400
  {
2050
1401
  title: "Start here",
2051
- subtitle: "one-time setup for a repo",
1402
+ subtitle: "one-time setup, pick a server",
2052
1403
  commands: [
2053
1404
  { command: "rig init", description: "Wizard: config, GitHub auth, task source, server, Pi wiring." },
2054
- { command: "rig doctor", description: "Check every part of the wiring \u2014 run this when anything feels off." },
2055
- { command: "rig server use <alias|local>", description: "Pick which Rig server owns this repo (`rig server list` to see them)." }
1405
+ { command: "rig server use <alias|local>", description: "Pick which Rig server owns this repo (`rig server list` to see them)." },
1406
+ { command: "rig server status", description: "Show the selected server for this repo." }
2056
1407
  ]
2057
1408
  },
2058
1409
  {
2059
1410
  title: "Work",
2060
- subtitle: "find a task and put an agent on it",
1411
+ subtitle: "find a task, put an agent on it, answer what it asks",
2061
1412
  commands: [
2062
1413
  { command: "rig task list", description: "What's on the board (from the selected source/server)." },
2063
- { command: "rig task next", description: "The next ready task, as a card." },
2064
1414
  { command: "rig task run --next", description: "Dispatch an agent; interactive mode opens the native Pi session." },
2065
- { command: "rig task run <id> --detach", description: "Fire-and-forget a specific task." }
1415
+ { command: "rig run status", description: "Active and recent runs at a glance." },
1416
+ { command: "rig run attach <id> --follow", description: "Join the live Pi session (worker keeps running if you /detach)." },
1417
+ { command: "rig inbox approvals", description: "Approvals workers are waiting on (then `rig inbox approve \u2026`)." }
2066
1418
  ]
2067
1419
  },
2068
1420
  {
2069
- title: "Watch & steer",
2070
- subtitle: "live runs are observable and steerable, attached or not",
1421
+ title: "Watch",
1422
+ subtitle: "fleet metrics and per-task forensics",
2071
1423
  commands: [
2072
- { command: "rig run status", description: "Active and recent runs at a glance." },
2073
- { command: "rig run attach <id> --follow", description: "Join the live Pi session (worker keeps running if you /detach)." },
2074
- { command: "rig run steer <id> -m <text>", description: "Drop a message into a live worker without attaching." },
2075
- { command: "rig run stop <id>", description: "Cancel a running worker." }
1424
+ { command: "rig stats [--since 7d]", description: "Fleet metrics: completion/failure rates, median run time, steering, stalls." },
1425
+ { command: "rig inspect logs --task <id>", description: "Latest run log for a task." },
1426
+ { command: "rig inspect diff --task <id>", description: "Changed files for a task." }
2076
1427
  ]
2077
1428
  },
2078
1429
  {
2079
- title: "Unblock & gate",
2080
- subtitle: "answer what workers ask; control what merges",
1430
+ title: "Unblock",
1431
+ subtitle: "diagnose wiring, fix auth",
2081
1432
  commands: [
2082
- { command: "rig inbox approvals", description: "Approvals workers are waiting on (then `rig inbox approve \u2026`)." },
2083
- { command: "rig inbox inputs", description: "Questions workers asked (then `rig inbox respond \u2026`)." },
2084
- { command: "rig review show|set", description: "The completion review gate (Greptile is mandatory on merges)." }
1433
+ { command: "rig doctor", description: "Check every part of the wiring \u2014 run this when anything feels off." },
1434
+ { command: "rig github auth status", description: "GitHub auth state on the selected server." }
2085
1435
  ]
2086
1436
  },
2087
1437
  {
2088
1438
  title: "Extend",
2089
- subtitle: "more Pi, more plugins",
1439
+ subtitle: "plugins contribute validators, hooks, task sources, commands",
2090
1440
  commands: [
2091
- { command: "rig pi search <term>", description: "Discover community Pi extensions on npm." },
2092
- { command: "rig pi add <pkg>", description: "Add a Pi extension to this project (workers pick it up too)." },
2093
- { command: "rig plugin list", description: "What the rig.config.ts plugins contribute." }
1441
+ { command: "rig plugin list", description: "What the rig.config.ts plugins contribute." },
1442
+ { command: "rig plugin run <command-id>", description: "Execute a plugin-contributed CLI command." }
2094
1443
  ]
2095
1444
  }
2096
1445
  ];
@@ -2114,22 +1463,20 @@ var PRIMARY_GROUPS = [
2114
1463
  "rig server use local",
2115
1464
  "rig server start --port 3773"
2116
1465
  ],
2117
- next: ["Use `rig task list` to see server-owned work.", "Use `rig run list` or `rig run attach <id> --follow` to monitor runs."],
2118
- advanced: ["Compatibility alias: `rig connect ...` remains callable."]
1466
+ next: ["Use `rig task list` to see server-owned work.", "Use `rig run list` or `rig run attach <id> --follow` to monitor runs."]
2119
1467
  },
2120
1468
  {
2121
1469
  name: "task",
2122
1470
  summary: "Find work, start Pi-backed runs, and validate task results.",
2123
1471
  usage: ["rig task <list|next|show|run> [options]"],
2124
1472
  commands: [
2125
- { command: "list [--assignee <login|@me>] [--state open|closed]", description: "List tasks from the selected server/source.", primary: true },
1473
+ { command: "list [--assignee <login|me|@me>] [--state open|closed]", description: "List tasks from the selected server/source.", primary: true },
2126
1474
  { command: "next [filters]", description: "Render the next matching task as a selected-task card.", primary: true },
2127
1475
  { command: "show <id>|--task <id> [--raw]", description: "Show a human task summary; --raw prints the full payload.", primary: true },
2128
1476
  { command: "run [#<issue>|<task-id>|--next|--task <id>]", description: "Submit a task run; interactive follows with bundled Pi.", primary: true },
2129
1477
  { command: "validate|verify [--task <id>]", description: "Run configured task checks/review gates." },
2130
1478
  { command: "details --task <id>", description: "Show full task info from the configured source." },
2131
1479
  { command: "reopen [--task <id> | --all] [--reason <text>]", description: "Reopen closed task(s) in the configured source." },
2132
- { command: "reset --task <id>", description: "Compatibility spelling of `reopen --task <id>`." },
2133
1480
  { command: "artifacts|artifact-dir|artifact-write", description: "Inspect or write task artifacts." },
2134
1481
  { command: "report-bug", description: "Create a structured bug report/task." }
2135
1482
  ],
@@ -2187,15 +1534,54 @@ var PRIMARY_GROUPS = [
2187
1534
  next: ["Rejoin the run after resolving a block: `rig run attach <run-id> --follow`."]
2188
1535
  },
2189
1536
  {
2190
- name: "review",
2191
- summary: "Inspect or change completion review gate policy.",
2192
- usage: ["rig review <show|set>"],
1537
+ name: "stats",
1538
+ summary: "Fleet metrics computed from on-disk run journals (no server required).",
1539
+ usage: ["rig stats [show] [--since <7d|30d|ISO date>]"],
2193
1540
  commands: [
2194
- { command: "show", description: "Show current review gate settings.", primary: true },
2195
- { command: "set <off|advisory|required> [--provider greptile]", description: "Change review strictness/provider.", primary: true }
1541
+ { command: "show [--since <window>]", description: "Total runs, completion/failure/needs-attention rates, median run time, steering, stalls, approvals.", primary: true }
2196
1542
  ],
2197
- examples: ["rig review show", "rig review set required --provider greptile"],
2198
- next: ["Use `rig inbox approvals` for blocked run handoffs."]
1543
+ examples: [
1544
+ "rig stats",
1545
+ "rig stats --since 7d",
1546
+ "rig stats --since 2026-06-01 --json"
1547
+ ],
1548
+ next: ["Inspect outliers with `rig run list` and `rig run show <run-id>`.", "Use `--json` for the schema'd envelope (see docs/cli-json.md)."]
1549
+ },
1550
+ {
1551
+ name: "inspect",
1552
+ summary: "Inspect logs, artifacts, graphs, failures for a task.",
1553
+ usage: ["rig inspect <logs|artifacts|failures|graph|audit|diff> --task <id>"],
1554
+ commands: [
1555
+ { command: "logs --task <id>", description: "Latest run log for a task (local or selected server).", primary: true },
1556
+ { command: "artifacts --task <id>", description: "List the task's completion artifacts.", primary: true },
1557
+ { command: "failures --task <id>", description: "Recorded failures for a task.", primary: true },
1558
+ { command: "diff --task <id>", description: "Changed files for a task.", primary: true },
1559
+ { command: "graph", description: "Task dependency graph." },
1560
+ { command: "audit", description: "Controlled-command audit trail." }
1561
+ ],
1562
+ examples: ["rig inspect logs --task <id>", "rig inspect diff --task <id>"],
1563
+ next: ["Use `rig stats` for fleet-level metrics across runs."]
1564
+ },
1565
+ {
1566
+ name: "repo",
1567
+ summary: "Repository sync/baseline helpers for the Rig-managed checkout.",
1568
+ usage: ["rig repo <sync|reset-baseline>"],
1569
+ commands: [
1570
+ { command: "sync", description: "Sync project repository state.", primary: true },
1571
+ { command: "reset-baseline", description: "Reset the managed baseline for the repo." }
1572
+ ],
1573
+ examples: ["rig repo sync"]
1574
+ },
1575
+ {
1576
+ name: "plugin",
1577
+ summary: "Plugin listing, validation, and plugin-contributed commands.",
1578
+ usage: ["rig plugin <list|validate|run> [options]"],
1579
+ commands: [
1580
+ { command: "list", description: "List plugins declared in rig.config.ts and their contributions.", primary: true },
1581
+ { command: "validate --task <id>", description: "Run plugin-contributed validators for a task.", primary: true },
1582
+ { command: "run <command-id> [args...]", description: "Execute a plugin-contributed CLI command (also callable as `rig <command-id>`)." }
1583
+ ],
1584
+ examples: ["rig plugin list", "rig plugin run <command-id>"]
2199
1585
  },
2200
1586
  {
2201
1587
  name: "init",
@@ -2203,12 +1589,14 @@ var PRIMARY_GROUPS = [
2203
1589
  usage: ["rig init [--yes] [--server local|remote] [--repo owner/repo] [--remote-url <url>]"],
2204
1590
  commands: [
2205
1591
  { command: "init", description: "Interactive setup wizard for a new or existing Rig repo.", primary: true },
1592
+ { command: "init --demo", description: "Offline demo project: files task source + 3 sample tasks, zero GitHub.", primary: true },
2206
1593
  { command: "init --yes", description: "Non-interactive setup using detected/default settings.", primary: true },
2207
1594
  { command: "init --server remote --remote-url <url>", description: "Link this repo to a remote Rig server.", primary: true },
2208
1595
  { command: "init --repair", description: "Repair missing private state without replacing project config." }
2209
1596
  ],
2210
1597
  examples: [
2211
1598
  "rig init",
1599
+ "rig init --demo",
2212
1600
  "rig init --yes --repo humanity-org/humanwork",
2213
1601
  "rig init --server remote --remote-url https://where.rig-does.work --repo owner/repo"
2214
1602
  ],
@@ -2239,23 +1627,19 @@ var PRIMARY_GROUPS = [
2239
1627
  }
2240
1628
  ];
2241
1629
  var ADVANCED_GROUPS = [
2242
- { name: "connect", summary: "Compatibility alias for `rig server` selection commands.", usage: ["rig connect <status|list|add|use>"], commands: [{ command: "status|list|add|use", description: "Use `rig server ...` for the primary UX." }] },
2243
1630
  { name: "setup", summary: "Bootstrap/check local setup.", usage: ["rig setup <bootstrap|check|preflight>"], commands: [{ command: "bootstrap|check|preflight", description: "Setup helpers." }] },
1631
+ { name: "profile", summary: "Runtime profile/model defaults.", usage: ["rig profile <show|set>"], commands: [{ command: "show", description: "Show active profile." }] },
2244
1632
  {
2245
- name: "inspect",
2246
- summary: "Inspect logs, artifacts, graphs, failures for a task.",
2247
- usage: ["rig inspect <logs|artifacts|failures|graph|audit|diff> --task <id>"],
1633
+ name: "review",
1634
+ summary: "Inspect or change completion review gate policy.",
1635
+ usage: ["rig review <show|set>"],
2248
1636
  commands: [
2249
- { command: "logs --task <id>", description: "Latest run log for a task (local or selected server)." },
2250
- { command: "artifacts --task <id>", description: "List the task's completion artifacts." },
2251
- { command: "failures --task <id>", description: "Recorded failures for a task." },
2252
- { command: "graph", description: "Task dependency graph." },
2253
- { command: "audit", description: "Controlled-command audit trail." },
2254
- { command: "diff --task <id>", description: "Changed files for a task." }
2255
- ]
1637
+ { command: "show", description: "Show current review gate settings." },
1638
+ { command: "set <off|advisory|required> [--provider greptile]", description: "Change review strictness/provider." }
1639
+ ],
1640
+ examples: ["rig review show", "rig review set required --provider greptile"],
1641
+ next: ["Use `rig inbox approvals` for blocked run handoffs."]
2256
1642
  },
2257
- { name: "repo", summary: "Repository sync/baseline helpers.", usage: ["rig repo <sync|reset-baseline>"], commands: [{ command: "sync", description: "Sync project repository state." }] },
2258
- { name: "profile", summary: "Runtime profile/model defaults.", usage: ["rig profile <show|set>"], commands: [{ command: "show", description: "Show active profile." }] },
2259
1643
  {
2260
1644
  name: "browser",
2261
1645
  summary: "Browser/app diagnostics for browser-required tasks.",
@@ -2281,16 +1665,6 @@ var ADVANCED_GROUPS = [
2281
1665
  examples: ["rig pi search subagents", "rig pi add pi-subagents", "rig pi list"],
2282
1666
  next: ["Config-managed extensions: declare `runtime: { pi: { packages: [...] } }` in rig.config.ts \u2014 workers pick them up automatically."]
2283
1667
  },
2284
- {
2285
- name: "plugin",
2286
- summary: "Plugin listing, validation, and plugin-contributed commands.",
2287
- usage: ["rig plugin <list|validate|run> [options]"],
2288
- commands: [
2289
- { command: "list", description: "List plugins declared in rig.config.ts and their contributions." },
2290
- { command: "validate --task <id>", description: "Run plugin-contributed validators for a task." },
2291
- { command: "run <command-id> [args...]", description: "Execute a plugin-contributed CLI command (also callable as `rig <command-id>`)." }
2292
- ]
2293
- },
2294
1668
  { name: "queue", summary: "Run task queues locally.", usage: ["rig queue run [options]"], commands: [{ command: "run", description: "Process queue work." }] },
2295
1669
  { name: "agent", summary: "Runtime agent workspace helpers.", usage: ["rig agent <list|prepare|run|cleanup>"], commands: [{ command: "list", description: "List prepared agents." }] },
2296
1670
  { name: "inspector", summary: "Event stream and drift scanners.", usage: ["rig inspector <stream|scan-upstream-drift>"], commands: [{ command: "stream", description: "Stream events." }] },
@@ -2357,7 +1731,7 @@ function renderGroup(group) {
2357
1731
  function renderTopLevelHelp() {
2358
1732
  return [
2359
1733
  `${heading("rig")} ${pc2.dim("\u2014 server-owned task/run control plane for Pi-backed engineering work")}`,
2360
- pc2.dim("Current path: select a server, choose a task, submit a run, attach with native Pi, clear inbox/review gates."),
1734
+ pc2.dim("Current path: select a server, choose a task, submit a run, attach with native Pi, clear inbox gates."),
2361
1735
  "",
2362
1736
  ...TOP_LEVEL_SECTIONS.flatMap((section) => [
2363
1737
  `${pc2.bold(pc2.magenta(`\u25C7 ${section.title}`))} \u2014 ${pc2.dim(section.subtitle)}`,
@@ -2409,7 +1783,7 @@ function printTopLevelHelp(state = {}) {
2409
1783
  commandLine("--dry-run", "Print the command plan without mutating state.")
2410
1784
  ].join(`
2411
1785
  `), "Global options");
2412
- outro("init \u2192 task run \u2192 watch/steer \u2192 inbox/review \u2192 merged.");
1786
+ outro("init \u2192 task run \u2192 watch \u2192 inbox \u2192 merged.");
2413
1787
  }
2414
1788
  function printGroupHelpDocument(groupName) {
2415
1789
  const rendered = renderGroupHelp(groupName) ?? renderTopLevelHelp();
@@ -2450,7 +1824,7 @@ async function readStdin() {
2450
1824
  }
2451
1825
  return Buffer.concat(chunks).toString("utf-8");
2452
1826
  }
2453
- function normalizeAssignedToAlias(value) {
1827
+ function normalizeAssigneeAlias(value) {
2454
1828
  if (!value)
2455
1829
  return;
2456
1830
  return value.trim().toLowerCase() === "me" ? "@me" : value;
@@ -2459,25 +1833,19 @@ function parseTaskFilters(args) {
2459
1833
  let pending = args;
2460
1834
  const assigneeResult = takeOption(pending, "--assignee");
2461
1835
  pending = assigneeResult.rest;
2462
- const assignedToResult = takeOption(pending, "--assigned-to");
2463
- pending = assignedToResult.rest;
2464
1836
  const stateResult = takeOption(pending, "--state");
2465
1837
  pending = stateResult.rest;
2466
1838
  const statusResult = takeOption(pending, "--status");
2467
1839
  pending = statusResult.rest;
2468
1840
  const limitResult = takeOption(pending, "--limit");
2469
1841
  pending = limitResult.rest;
2470
- const normalizedAssignedTo = normalizeAssignedToAlias(assignedToResult.value);
2471
- if (assigneeResult.value && normalizedAssignedTo && assigneeResult.value !== normalizedAssignedTo) {
2472
- throw new CliError2("--assignee and --assigned-to cannot specify different assignees.", 2);
2473
- }
2474
- const assignee = normalizedAssignedTo ?? assigneeResult.value;
1842
+ const assignee = normalizeAssigneeAlias(assigneeResult.value);
2475
1843
  const limit = (() => {
2476
1844
  if (!limitResult.value)
2477
1845
  return;
2478
1846
  const parsed = Number.parseInt(limitResult.value, 10);
2479
1847
  if (!Number.isFinite(parsed) || parsed < 1) {
2480
- throw new CliError2("--limit must be a positive integer.", 2);
1848
+ throw new CliError("--limit must be a positive integer.", 2, { hint: "Re-run with a positive number, e.g. `rig task list --limit 20`." });
2481
1849
  }
2482
1850
  return parsed;
2483
1851
  })();
@@ -2512,7 +1880,7 @@ function normalizePrMode(value) {
2512
1880
  return;
2513
1881
  if (value === "auto" || value === "ask" || value === "off")
2514
1882
  return value;
2515
- throw new CliError2("--pr must be auto, ask, or off.", 2);
1883
+ throw new CliError("--pr must be auto, ask, or off.", 2, { hint: "Re-run with `--pr auto|ask|off`, or pass `--no-pr` to disable the PR." });
2516
1884
  }
2517
1885
  function detectLocalDirtyState(projectRoot) {
2518
1886
  const result = spawnSync("git", ["-C", projectRoot, "status", "--porcelain"], { encoding: "utf8", timeout: 5000 });
@@ -2535,7 +1903,7 @@ function selectedServerKind(projectRoot) {
2535
1903
  }
2536
1904
  async function resolveDirtyBaselineForTaskRun(context, explicit) {
2537
1905
  if (explicit && explicit !== "head" && explicit !== "dirty-snapshot") {
2538
- throw new CliError2("--dirty-baseline must be head or dirty-snapshot.", 2);
1906
+ throw new CliError("--dirty-baseline must be head or dirty-snapshot.", 2, { hint: "Re-run with `--dirty-baseline head` or `--dirty-baseline dirty-snapshot`." });
2539
1907
  }
2540
1908
  if (selectedServerKind(context.projectRoot) !== "local") {
2541
1909
  return { mode: explicit === "dirty-snapshot" ? "dirty-snapshot" : "head", state: null };
@@ -2555,7 +1923,7 @@ async function resolveDirtyBaselineForTaskRun(context, explicit) {
2555
1923
  });
2556
1924
  if (isCancel2(answer)) {
2557
1925
  cancel2("Run cancelled.");
2558
- throw new CliError2("Run cancelled by user.", 1);
1926
+ throw new CliError("Run cancelled by user.", 1);
2559
1927
  }
2560
1928
  return { mode: answer ? "dirty-snapshot" : "head", state };
2561
1929
  }
@@ -2610,11 +1978,12 @@ async function executeTask(context, args, options) {
2610
1978
  const rawResult = takeFlag(pending, "--raw");
2611
1979
  pending = rawResult.rest;
2612
1980
  const { filters, rest: remaining } = parseTaskFilters(pending);
2613
- requireNoExtraArgs(remaining, "rig task list [--raw] [--assignee <login|@me>] [--assigned-to <login|me|@me>] [--state open|closed] [--status <status>] [--limit <n>]");
1981
+ requireNoExtraArgs(remaining, "rig task list [--raw] [--assignee <login|me|@me>] [--state open|closed] [--status <status>] [--limit <n>]");
2614
1982
  const tasks = await listWorkspaceTasksViaServer(context, filters);
2615
1983
  if (context.outputMode === "text") {
2616
1984
  const renderedTasks = rawResult.value ? tasks.map((task) => summarizeTask(task, { raw: true })) : tasks.map((task) => summarizeTask(task));
2617
1985
  printFormattedOutput(formatTaskList(renderedTasks, { raw: rawResult.value }));
1986
+ await printPendingInboxFooter(context);
2618
1987
  }
2619
1988
  return {
2620
1989
  ok: true,
@@ -2633,10 +2002,10 @@ async function executeTask(context, args, options) {
2633
2002
  requireNoExtraArgs(remaining, "rig task show <id>|--task <id> [--raw]");
2634
2003
  const taskId3 = normalizeTaskRunTaskId(taskOption.value ?? positional);
2635
2004
  if (!taskId3)
2636
- throw new CliError2("task show requires a task id.", 2);
2005
+ throw new CliError("task show requires a task id.", 2, { hint: "Run `rig task list` to find ids, then `rig task show <id>`." });
2637
2006
  const task = await getWorkspaceTaskViaServer(context, taskId3);
2638
2007
  if (!task)
2639
- throw new CliError2(`Task not found: ${taskId3}`, 3);
2008
+ throw new CliError(`Task not found: ${taskId3}`, 3, { hint: "Run `rig task list` to see available tasks." });
2640
2009
  const summary = summarizeTask(task, { raw: true });
2641
2010
  if (context.outputMode === "text") {
2642
2011
  printFormattedOutput(rawResult.value ? JSON.stringify(summary, null, 2) : formatTaskDetails(summary));
@@ -2645,7 +2014,7 @@ async function executeTask(context, args, options) {
2645
2014
  }
2646
2015
  case "next": {
2647
2016
  const { filters, rest: remaining } = parseTaskFilters(rest);
2648
- requireNoExtraArgs(remaining, "rig task next [--assignee <login|@me>] [--assigned-to <login|me|@me>] [--state open|closed] [--status <status>] [--limit <n>]");
2017
+ requireNoExtraArgs(remaining, "rig task next [--assignee <login|me|@me>] [--state open|closed] [--status <status>] [--limit <n>]");
2649
2018
  const selected = await selectNextWorkspaceTaskViaServer(context, filters);
2650
2019
  if (context.outputMode === "text") {
2651
2020
  if (selected.task) {
@@ -2700,7 +2069,7 @@ async function executeTask(context, args, options) {
2700
2069
  }
2701
2070
  case "artifact-write": {
2702
2071
  if (rest.length < 1) {
2703
- throw new CliError2(`Usage: rig task artifact-write <filename> [--file <path>]
2072
+ throw new CliError(`Usage: rig task artifact-write <filename> [--file <path>]
2704
2073
  ` + ` Reads content from stdin (or --file), writes to the active task artifact dir.
2705
2074
  ` + " Example: echo '...' | rig task artifact-write collection-audit.md");
2706
2075
  }
@@ -2713,7 +2082,7 @@ async function executeTask(context, args, options) {
2713
2082
  content = await readStdin();
2714
2083
  }
2715
2084
  if (!artifactFilename) {
2716
- throw new CliError2("Usage: rig task artifact-write <filename> [--file path]");
2085
+ throw new CliError("Usage: rig task artifact-write <filename> [--file path]");
2717
2086
  }
2718
2087
  withMutedConsole(context.outputMode === "json", () => taskArtifactWrite(context.projectRoot, artifactFilename, content));
2719
2088
  return { ok: true, group: "task", command, details: { filename: artifactFilename } };
@@ -2722,11 +2091,11 @@ async function executeTask(context, args, options) {
2722
2091
  return options.executeTaskReportBug(context, rest);
2723
2092
  case "lookup": {
2724
2093
  if (rest.length !== 1) {
2725
- throw new CliError2("Usage: rig task lookup <task-id>");
2094
+ throw new CliError("Usage: rig task lookup <task-id>");
2726
2095
  }
2727
2096
  const lookupId = rest[0];
2728
2097
  if (!lookupId) {
2729
- throw new CliError2("Usage: rig task lookup <task-id>");
2098
+ throw new CliError("Usage: rig task lookup <task-id>");
2730
2099
  }
2731
2100
  const result = taskLookup(context.projectRoot, lookupId);
2732
2101
  if (context.outputMode === "text") {
@@ -2736,11 +2105,11 @@ async function executeTask(context, args, options) {
2736
2105
  }
2737
2106
  case "record": {
2738
2107
  if (rest.length < 2) {
2739
- throw new CliError2("Usage: rig task record <decision|failure> <text>");
2108
+ throw new CliError("Usage: rig task record <decision|failure> <text>");
2740
2109
  }
2741
2110
  const type = rest[0];
2742
2111
  if (type !== "decision" && type !== "failure") {
2743
- throw new CliError2("Usage: rig task record <decision|failure> <text>");
2112
+ throw new CliError("Usage: rig task record <decision|failure> <text>");
2744
2113
  }
2745
2114
  withMutedConsole(context.outputMode === "json", () => taskRecord(context.projectRoot, type, rest.slice(1).join(" ")));
2746
2115
  return { ok: true, group: "task", command, details: { type: rest[0] } };
@@ -2783,15 +2152,15 @@ async function executeTask(context, args, options) {
2783
2152
  if (positionalTaskId) {
2784
2153
  pending = pending.slice(1);
2785
2154
  }
2786
- requireNoExtraArgs(pending, "rig task run [#<issue>|<task-id>] [--next] [--task <id>] [--detach] [--assignee <login|@me>] [--assigned-to <login|me|@me>] [--state open|closed] [--status <status>] [--limit <n>] [--title <text>] [--runtime-adapter claude-code|codex|pi] [--model <model>] [--runtime-mode <mode>] [--interaction-mode <mode>] [--initial-prompt <text>] [--pr auto|ask|off] [--no-pr] [--dirty-baseline head|dirty-snapshot] [--skip-project-sync]");
2155
+ requireNoExtraArgs(pending, "rig task run [#<issue>|<task-id>] [--next] [--task <id>] [--detach] [--assignee <login|me|@me>] [--state open|closed] [--status <status>] [--limit <n>] [--title <text>] [--runtime-adapter claude-code|codex|pi] [--model <model>] [--runtime-mode <mode>] [--interaction-mode <mode>] [--initial-prompt <text>] [--pr auto|ask|off] [--no-pr] [--dirty-baseline head|dirty-snapshot] [--skip-project-sync]");
2787
2156
  if (nextResult.value && (taskResult.value || positionalTaskId)) {
2788
- throw new CliError2("task run cannot combine --next with an explicit task id.", 2);
2157
+ throw new CliError("task run cannot combine --next with an explicit task id.", 2, { hint: "Use either `rig task run --next` or `rig task run <id>`." });
2789
2158
  }
2790
2159
  if (taskResult.value && positionalTaskId) {
2791
- throw new CliError2("task run cannot combine positional task id with --task <id>.", 2);
2160
+ throw new CliError("task run cannot combine positional task id with --task <id>.", 2, { hint: "Pass the id once, e.g. `rig task run <id>`." });
2792
2161
  }
2793
2162
  if (prResult.value && noPrResult.value) {
2794
- throw new CliError2("task run cannot combine --pr with --no-pr.", 2);
2163
+ throw new CliError("task run cannot combine --pr with --no-pr.", 2, { hint: "Use `--pr auto|ask|off` or `--no-pr`, not both." });
2795
2164
  }
2796
2165
  let selectedTask = null;
2797
2166
  let selectedTaskId = normalizeTaskRunTaskId(taskResult.value) ?? positionalTaskId;
@@ -2800,18 +2169,18 @@ async function executeTask(context, args, options) {
2800
2169
  selectedTask = selected.task;
2801
2170
  selectedTaskId = selected.task ? readTaskId(selected.task) : null;
2802
2171
  if (!selectedTaskId) {
2803
- throw new CliError2("No matching task found for task run --next.", 3);
2172
+ throw new CliError("No matching task found for task run --next.", 3, { hint: "Run `rig task list` to inspect available work, or relax the filters." });
2804
2173
  }
2805
2174
  }
2806
2175
  if (!selectedTaskId && !initialPromptResult.value && !titleResult.value) {
2807
2176
  if (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY) {
2808
- throw new CliError2("task run requires an interactive terminal to pick a task; pass --next, --task <id>, or --initial-prompt/--title for an ad hoc run.", 2);
2177
+ throw new CliError("task run requires an interactive terminal to pick a task; pass --next, --task <id>, or --initial-prompt/--title for an ad hoc run.", 2);
2809
2178
  }
2810
2179
  const tasks = await listWorkspaceTasksViaServer(context, filterResult.filters);
2811
2180
  selectedTask = await selectTaskWithTextPicker(tasks);
2812
2181
  selectedTaskId = selectedTask ? readTaskId(selectedTask) : null;
2813
2182
  if (!selectedTaskId) {
2814
- throw new CliError2("No task selected.", 3);
2183
+ throw new CliError("No task selected.", 3, { hint: "Run `rig task run --next` for the next ready task, or `rig task run --task <id>`." });
2815
2184
  }
2816
2185
  }
2817
2186
  await runProjectMainSyncPreflight(context, { disabled: skipProjectSyncResult.value });
@@ -2885,7 +2254,7 @@ async function executeTask(context, args, options) {
2885
2254
  }
2886
2255
  const ok = await withMutedConsole(context.outputMode === "json", async () => taskValidate(context.projectRoot, task || undefined, await validatorRegistryForTaskCommands(context.projectRoot)));
2887
2256
  if (!ok) {
2888
- throw new CliError2(`Validation failed for ${task || "active task"}.`, 2);
2257
+ throw new CliError(`Validation failed for ${task || "active task"}.`, 2, { hint: "Inspect failures with `rig inspect failures --task <id>`, fix, then re-run `rig task validate --task <id>`." });
2889
2258
  }
2890
2259
  return { ok: true, group: "task", command, details: { task: task || "active" } };
2891
2260
  }
@@ -2898,21 +2267,10 @@ async function executeTask(context, args, options) {
2898
2267
  }
2899
2268
  const ok = await withMutedConsole(context.outputMode === "json", () => taskVerify(context.projectRoot, task || undefined));
2900
2269
  if (!ok) {
2901
- throw new CliError2(`Verification rejected for ${task || "active task"}.`, 2);
2270
+ throw new CliError(`Verification rejected for ${task || "active task"}.`, 2, { hint: "Check `rig inspect logs --task <id>`, address the rejection, then re-run `rig task verify --task <id>`." });
2902
2271
  }
2903
2272
  return { ok: true, group: "task", command, details: { task: task || "active" } };
2904
2273
  }
2905
- case "reset": {
2906
- const { value: task, rest: remaining } = takeOption(rest, "--task");
2907
- requireNoExtraArgs(remaining, "rig task reset --task <task-id>");
2908
- const requiredTask = requireTask(task, "rig task reset --task <task-id>");
2909
- const summary = withMutedConsole(context.outputMode === "json", () => taskReopen(context.projectRoot, {
2910
- all: false,
2911
- taskId: requiredTask,
2912
- dryRun: false
2913
- }));
2914
- return { ok: true, group: "task", command, details: summary };
2915
- }
2916
2274
  case "details": {
2917
2275
  const { value: task, rest: remaining } = takeOption(rest, "--task");
2918
2276
  requireNoExtraArgs(remaining, "rig task details --task <task-id>");
@@ -2926,7 +2284,7 @@ async function executeTask(context, args, options) {
2926
2284
  const { rest: remaining } = takeOption(allFlag.rest, "--reason");
2927
2285
  requireNoExtraArgs(remaining, "rig task reopen [--task <id> | --all] [--reason <text>]");
2928
2286
  if (!allFlag.value && !task) {
2929
- throw new CliError2("Usage: rig task reopen [--task <id> | --all] [--reason <text>]");
2287
+ throw new CliError("Usage: rig task reopen [--task <id> | --all] [--reason <text>]");
2930
2288
  }
2931
2289
  const summary = withMutedConsole(context.outputMode === "json", () => taskReopen(context.projectRoot, {
2932
2290
  all: allFlag.value,
@@ -2936,7 +2294,10 @@ async function executeTask(context, args, options) {
2936
2294
  return { ok: true, group: "task", command, details: summary };
2937
2295
  }
2938
2296
  default:
2939
- throw new CliError2(`Unknown task command: ${command}`);
2297
+ if (command === "reset") {
2298
+ throw new CliError("Unknown task command: reset", 1, { hint: "Use `rig task reopen --task <id>`." });
2299
+ }
2300
+ throw new CliError(`Unknown task command: ${command}`, 1, { hint: "Run `rig task --help` to list available task commands." });
2940
2301
  }
2941
2302
  }
2942
2303
  export {