@h-rig/cli 0.0.6-alpha.64 → 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
@@ -4,10 +4,19 @@ import { createInterface as createInterface2 } from "readline/promises";
4
4
 
5
5
  // packages/cli/src/runner.ts
6
6
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
7
- import { CliError } from "@rig/runtime/control-plane/errors";
7
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
8
8
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
9
9
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
10
- 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
+ }
11
20
  function takeFlag(args, flag) {
12
21
  const rest = [];
13
22
  let value = false;
@@ -28,7 +37,7 @@ function takeOption(args, option) {
28
37
  if (current === option) {
29
38
  const next = args[index + 1];
30
39
  if (!next || next.startsWith("-")) {
31
- 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>\`.` });
32
41
  }
33
42
  value = next;
34
43
  index += 1;
@@ -73,7 +82,7 @@ function parsePositiveInt(value, option, fallback) {
73
82
  }
74
83
  const parsed = Number.parseInt(value, 10);
75
84
  if (!Number.isFinite(parsed) || parsed <= 0) {
76
- throw new CliError2(`Invalid ${option} value: ${value}`);
85
+ throw new CliError(`Invalid ${option} value: ${value}`, 1, { hint: `Pass a positive integer, e.g. \`${option} 10\`.` });
77
86
  }
78
87
  return parsed;
79
88
  }
@@ -105,7 +114,7 @@ function readJsonFile(path) {
105
114
  try {
106
115
  return JSON.parse(readFileSync(path, "utf8"));
107
116
  } catch (error) {
108
- throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
117
+ 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>`." });
109
118
  }
110
119
  }
111
120
  function writeJsonFile(path, value) {
@@ -169,7 +178,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
169
178
  const global = readGlobalConnections(options);
170
179
  const connection = global.connections[repo.selected];
171
180
  if (!connection) {
172
- throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
181
+ throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
173
182
  }
174
183
  return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
175
184
  }
@@ -242,7 +251,7 @@ async function ensureServerForCli(projectRoot) {
242
251
  };
243
252
  } catch (error) {
244
253
  if (error instanceof Error) {
245
- throw new CliError2(error.message, 1);
254
+ throw new CliError(error.message, 1);
246
255
  }
247
256
  throw error;
248
257
  }
@@ -292,15 +301,64 @@ function diagnosticMessage(payload) {
292
301
  });
293
302
  return messages.length > 0 ? messages.join("; ") : null;
294
303
  }
304
+ var serverReachabilityCache = new Map;
305
+ async function probeServerReachability(baseUrl, authToken) {
306
+ try {
307
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
308
+ headers: mergeHeaders(undefined, authToken),
309
+ signal: AbortSignal.timeout(1500)
310
+ });
311
+ return response.ok;
312
+ } catch {
313
+ return false;
314
+ }
315
+ }
316
+ function cachedServerReachability(projectRoot, baseUrl, authToken) {
317
+ const key = resolve2(projectRoot);
318
+ const cached = serverReachabilityCache.get(key);
319
+ if (cached)
320
+ return cached;
321
+ const probe = probeServerReachability(baseUrl, authToken);
322
+ serverReachabilityCache.set(key, probe);
323
+ return probe;
324
+ }
325
+ function describeSelectedServer(projectRoot, server) {
326
+ try {
327
+ const selected = resolveSelectedConnection(projectRoot);
328
+ if (selected) {
329
+ return {
330
+ alias: selected.alias,
331
+ target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
332
+ };
333
+ }
334
+ } catch {}
335
+ return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
336
+ }
337
+ async function buildServerFailureContext(projectRoot, server) {
338
+ const { alias, target } = describeSelectedServer(projectRoot, server);
339
+ const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
340
+ const reachability = reachable ? "server is reachable" : "server is unreachable";
341
+ return {
342
+ contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
343
+ hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
344
+ };
345
+ }
295
346
  async function requestServerJson(context, pathname, init = {}) {
296
347
  const server = await ensureServerForCli(context.projectRoot);
297
348
  const headers = mergeHeaders(init.headers, server.authToken);
298
349
  if (server.serverProjectRoot)
299
350
  headers.set("x-rig-project-root", server.serverProjectRoot);
300
- const response = await fetch(`${server.baseUrl}${pathname}`, {
301
- ...init,
302
- headers
303
- });
351
+ let response;
352
+ try {
353
+ response = await fetch(`${server.baseUrl}${pathname}`, {
354
+ ...init,
355
+ headers
356
+ });
357
+ } catch (error) {
358
+ const failure = await buildServerFailureContext(context.projectRoot, server);
359
+ throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
360
+ ${failure.contextLine}`, 1, { hint: failure.hint });
361
+ }
304
362
  const text = await response.text();
305
363
  const payload = text.trim().length > 0 ? (() => {
306
364
  try {
@@ -312,7 +370,9 @@ async function requestServerJson(context, pathname, init = {}) {
312
370
  if (!response.ok) {
313
371
  const diagnostics = diagnosticMessage(payload);
314
372
  const detail = diagnostics ?? (text || response.statusText);
315
- throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
373
+ const failure = await buildServerFailureContext(context.projectRoot, server);
374
+ throw new CliError(`Rig server request failed (${response.status}): ${detail}
375
+ ${failure.contextLine}`, 1, { hint: failure.hint });
316
376
  }
317
377
  return payload;
318
378
  }
@@ -362,26 +422,6 @@ async function steerRunViaServer(context, runId, message) {
362
422
  });
363
423
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
364
424
  }
365
- async function getRunPiSessionViaServer(context, runId) {
366
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi`);
367
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
368
- }
369
- async function getRunPiMessagesViaServer(context, runId) {
370
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/messages`);
371
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { messages: [] };
372
- }
373
- async function getRunPiStatusViaServer(context, runId) {
374
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/status`);
375
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
376
- }
377
- async function getRunPiCommandsViaServer(context, runId) {
378
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
379
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
380
- }
381
- async function getRunPiCapabilitiesViaServer(context, runId) {
382
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/capabilities`);
383
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { capabilities: null };
384
- }
385
425
  async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
386
426
  const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
387
427
  method: "POST",
@@ -390,44 +430,6 @@ async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior)
390
430
  });
391
431
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
392
432
  }
393
- async function sendRunPiShellViaServer(context, runId, text) {
394
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/shell`, {
395
- method: "POST",
396
- headers: { "content-type": "application/json" },
397
- body: JSON.stringify({ text })
398
- });
399
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
400
- }
401
- async function runRunPiCommandViaServer(context, runId, text) {
402
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands/run`, {
403
- method: "POST",
404
- headers: { "content-type": "application/json" },
405
- body: JSON.stringify({ text })
406
- });
407
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { type: "done" };
408
- }
409
- async function respondRunPiExtensionUiViaServer(context, runId, requestId, valueOrCancel) {
410
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/extension-ui/respond`, {
411
- method: "POST",
412
- headers: { "content-type": "application/json" },
413
- body: JSON.stringify({ requestId, ...valueOrCancel })
414
- });
415
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
416
- }
417
- async function abortRunPiViaServer(context, runId) {
418
- const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/abort`, { method: "POST" });
419
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { aborted: true };
420
- }
421
- async function buildRunPiEventsWebSocketUrl(context, runId) {
422
- const server = await ensureServerForCli(context.projectRoot);
423
- const url = new URL(`${server.baseUrl.replace(/\/+$/, "")}/api/runs/${encodeURIComponent(runId)}/pi/events`);
424
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
425
- if (server.authToken)
426
- url.searchParams.set("token", server.authToken);
427
- if (server.serverProjectRoot)
428
- url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
429
- return url.toString();
430
- }
431
433
 
432
434
  // packages/cli/src/commands/_run-replay.ts
433
435
  import { existsSync as existsSync3, readdirSync } from "fs";
@@ -435,7 +437,7 @@ import { join, resolve as resolve3 } from "path";
435
437
  import {
436
438
  readAuthorityRun,
437
439
  readJsonlFile,
438
- runLifecycleLogPath
440
+ runJournalPath
439
441
  } from "@rig/runtime/control-plane/authority-files";
440
442
  function asRecord(value) {
441
443
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -533,7 +535,7 @@ function resolveRunSessionFile(record) {
533
535
  }
534
536
  }
535
537
  function buildRunReplay(projectRoot, runId, options = {}) {
536
- const logPath = runLifecycleLogPath(projectRoot, runId);
538
+ const logPath = runJournalPath(projectRoot, runId);
537
539
  const record = readAuthorityRun(projectRoot, runId);
538
540
  const lifecycleEntries = readJsonlFile(logPath).map((entry) => asRecord(entry)).filter((entry) => entry !== null);
539
541
  const merged = lifecycleEntries.map((entry) => ({
@@ -763,684 +765,8 @@ function createOperatorSurface(options = {}) {
763
765
  import { mkdtempSync, rmSync } from "fs";
764
766
  import { tmpdir } from "os";
765
767
  import { join as join2 } from "path";
766
- import {
767
- createAgentSessionFromServices,
768
- createAgentSessionServices,
769
- main as runPiMain
770
- } from "@earendil-works/pi-coding-agent";
771
-
772
- // packages/cli/src/commands/_pi-remote-session.ts
773
- import { AgentSession as PiAgentSession, expandPromptTemplate } from "@earendil-works/pi-coding-agent";
774
- var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
775
- function defaultTransport() {
776
- return {
777
- getSession: getRunPiSessionViaServer,
778
- getMessages: getRunPiMessagesViaServer,
779
- getStatus: getRunPiStatusViaServer,
780
- getCommands: getRunPiCommandsViaServer,
781
- getCapabilities: getRunPiCapabilitiesViaServer,
782
- sendPrompt: sendRunPiPromptViaServer,
783
- sendShell: sendRunPiShellViaServer,
784
- runCommand: runRunPiCommandViaServer,
785
- abort: abortRunPiViaServer,
786
- buildEventsWebSocketUrl: buildRunPiEventsWebSocketUrl
787
- };
788
- }
789
- function recordOf(value) {
790
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
791
- }
792
- function emptyRemoteStatus() {
793
- return {
794
- isStreaming: false,
795
- isCompacting: false,
796
- isBashRunning: false,
797
- pendingMessageCount: 0,
798
- steeringMessages: [],
799
- followUpMessages: [],
800
- model: null,
801
- thinkingLevel: null,
802
- sessionName: null,
803
- cwd: null,
804
- stats: null,
805
- contextUsage: null
806
- };
807
- }
808
- function resolveAttachReadyTimeoutMs() {
809
- const raw = Number.parseInt(process.env.RIG_PI_ATTACH_TIMEOUT_MS ?? "", 10);
810
- return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60000;
811
- }
812
-
813
- class RigRemoteSessionController {
814
- context;
815
- runId;
816
- status = emptyRemoteStatus();
817
- transport;
818
- hooks = {};
819
- session = null;
820
- socket = null;
821
- closed = false;
822
- pendingShells = [];
823
- pendingCompactions = [];
824
- constructor(input) {
825
- this.context = input.context;
826
- this.runId = input.runId;
827
- this.transport = input.transport ?? defaultTransport();
828
- }
829
- ingestEnvelope(envelopeValue) {
830
- this.applyEnvelope(envelopeValue);
831
- }
832
- bindSession(session) {
833
- this.session = session;
834
- }
835
- setUiHooks(hooks) {
836
- this.hooks = hooks;
837
- }
838
- async connect() {
839
- const ready = await this.waitForReady();
840
- if (!ready || this.closed)
841
- return;
842
- let catchupDone = false;
843
- const buffered = [];
844
- const wsUrl = await this.transport.buildEventsWebSocketUrl(this.context, this.runId);
845
- const socket = new WebSocket(wsUrl);
846
- this.socket = socket;
847
- socket.onopen = () => {
848
- this.hooks.onConnectionChange?.(true);
849
- this.hooks.onStatusText?.("worker session live");
850
- };
851
- socket.onmessage = (message) => {
852
- try {
853
- const payload = typeof message.data === "string" ? JSON.parse(message.data) : JSON.parse(Buffer.from(message.data).toString("utf8"));
854
- if (!catchupDone)
855
- buffered.push(payload);
856
- else
857
- this.applyEnvelope(payload);
858
- } catch (error) {
859
- this.hooks.onError?.(`Unparseable worker Pi event: ${error instanceof Error ? error.message : String(error)}`);
860
- }
861
- };
862
- socket.onerror = () => socket.close();
863
- socket.onclose = () => {
864
- this.hooks.onConnectionChange?.(false);
865
- if (!this.closed)
866
- this.hooks.onStatusText?.("worker Pi WebSocket disconnected");
867
- };
868
- try {
869
- const [messagesPayload, statusPayload, commandsPayload, capabilitiesPayload] = await Promise.all([
870
- this.transport.getMessages(this.context, this.runId),
871
- this.transport.getStatus(this.context, this.runId),
872
- this.transport.getCommands(this.context, this.runId),
873
- this.transport.getCapabilities(this.context, this.runId).catch(() => ({ capabilities: null }))
874
- ]);
875
- const messages = Array.isArray(messagesPayload.messages) ? messagesPayload.messages : [];
876
- this.applyStatusPayload(statusPayload);
877
- this.session?.replaceRemoteMessages(messages);
878
- const commands = Array.isArray(commandsPayload.commands) ? commandsPayload.commands : [];
879
- const capabilities = capabilitiesPayload.capabilities && typeof capabilitiesPayload.capabilities === "object" && !Array.isArray(capabilitiesPayload.capabilities) ? capabilitiesPayload.capabilities : null;
880
- this.hooks.onCatchUp?.(messages, commands, capabilities);
881
- catchupDone = true;
882
- for (const payload of buffered.splice(0))
883
- this.applyEnvelope(payload);
884
- } catch (error) {
885
- catchupDone = true;
886
- this.hooks.onError?.(`Worker Pi catch-up failed: ${error instanceof Error ? error.message : String(error)}`);
887
- }
888
- }
889
- close() {
890
- this.closed = true;
891
- this.socket?.close();
892
- for (const shell of this.pendingShells.splice(0))
893
- shell.reject(new Error("Remote session closed."));
894
- for (const compaction of this.pendingCompactions.splice(0))
895
- compaction.reject(new Error("Remote session closed."));
896
- }
897
- async sendPrompt(text2, streamingBehavior) {
898
- await this.transport.sendPrompt(this.context, this.runId, text2, streamingBehavior);
899
- }
900
- async sendCommand(text2) {
901
- const result = await this.transport.runCommand(this.context, this.runId, text2);
902
- return typeof result.message === "string" ? result.message : "worker command accepted";
903
- }
904
- async sendShell(text2) {
905
- await this.transport.sendShell(this.context, this.runId, text2);
906
- }
907
- async abort() {
908
- await this.transport.abort(this.context, this.runId);
909
- }
910
- registerPendingShell(shell) {
911
- this.pendingShells.push(shell);
912
- }
913
- failPendingShell(shell, error) {
914
- const index = this.pendingShells.indexOf(shell);
915
- if (index !== -1)
916
- this.pendingShells.splice(index, 1);
917
- shell.reject(error);
918
- }
919
- registerPendingCompaction(pending) {
920
- this.pendingCompactions.push(pending);
921
- }
922
- async waitForReady() {
923
- const startedAt = Date.now();
924
- const deadline = startedAt + resolveAttachReadyTimeoutMs();
925
- let consecutiveFailures = 0;
926
- while (!this.closed) {
927
- let requestFailed = false;
928
- const session = await this.transport.getSession(this.context, this.runId).catch((error) => {
929
- requestFailed = true;
930
- return { ready: false, status: error instanceof Error ? error.message : String(error), retryAfterMs: 1000 };
931
- });
932
- if (session.ready !== false)
933
- return true;
934
- consecutiveFailures = requestFailed ? consecutiveFailures + 1 : 0;
935
- const status = String(session.status ?? "starting");
936
- if (TERMINAL_RUN_STATUSES.has(status.toLowerCase())) {
937
- this.hooks.onError?.(`Run ended before the worker Pi daemon became ready: ${status}. Inspect with \`rig run show ${this.runId}\`.`);
938
- return false;
939
- }
940
- 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`);
941
- if (Date.now() >= deadline) {
942
- this.hooks.onError?.(`Worker Pi daemon did not become ready (last status: ${status}). Set RIG_PI_ATTACH_TIMEOUT_MS to wait longer.`);
943
- return false;
944
- }
945
- await Bun.sleep(typeof session.retryAfterMs === "number" ? session.retryAfterMs : 750);
946
- }
947
- return false;
948
- }
949
- applyEnvelope(envelopeValue) {
950
- const envelope = recordOf(envelopeValue);
951
- if (!envelope)
952
- return;
953
- const type = String(envelope.type ?? "");
954
- if (type === "status.update") {
955
- this.applyStatusPayload(envelope);
956
- return;
957
- }
958
- if (type === "activity.update") {
959
- const activity = recordOf(envelope.activity);
960
- this.hooks.onActivity?.(String(activity?.label ?? ""), activity?.detail ? String(activity.detail) : undefined);
961
- return;
962
- }
963
- if (type === "extension_ui_request") {
964
- const request = recordOf(envelope.request);
965
- if (request)
966
- this.hooks.onExtensionUiRequest?.(request);
967
- return;
968
- }
969
- if (type === "pi.ui_event") {
970
- this.applyShellUiEvent(envelope.event);
971
- return;
972
- }
973
- if (type === "pi.event") {
974
- this.session?.handleRemoteSessionEvent(envelope.event);
975
- this.settlePendingCompaction(envelope.event);
976
- return;
977
- }
978
- if (type === "error") {
979
- this.hooks.onError?.(String(envelope.message ?? envelope.detail ?? "unknown worker error"));
980
- }
981
- }
982
- applyStatusPayload(payload) {
983
- const status = recordOf(payload.status) ?? payload;
984
- const next = this.status;
985
- next.isStreaming = status.isStreaming === true;
986
- next.isCompacting = status.isCompacting === true;
987
- next.isBashRunning = status.isBashRunning === true;
988
- next.pendingMessageCount = typeof status.pendingMessageCount === "number" ? status.pendingMessageCount : 0;
989
- next.steeringMessages = Array.isArray(status.steeringMessages) ? status.steeringMessages.map(String) : [];
990
- next.followUpMessages = Array.isArray(status.followUpMessages) ? status.followUpMessages.map(String) : [];
991
- next.model = recordOf(status.model) ?? next.model;
992
- next.thinkingLevel = typeof status.thinkingLevel === "string" ? status.thinkingLevel : next.thinkingLevel;
993
- next.sessionName = typeof status.sessionName === "string" ? status.sessionName : next.sessionName;
994
- next.cwd = typeof status.cwd === "string" ? status.cwd : next.cwd;
995
- next.stats = recordOf(status.stats) ?? next.stats;
996
- next.contextUsage = recordOf(status.contextUsage) ?? next.contextUsage;
997
- }
998
- applyShellUiEvent(value) {
999
- const event = recordOf(value);
1000
- if (!event)
1001
- return;
1002
- const type = String(event.type ?? "");
1003
- const pending = this.pendingShells[0];
1004
- if (type === "shell.chunk" && pending) {
1005
- pending.sawChunk = true;
1006
- pending.onData(Buffer.from(String(event.chunk ?? "")));
1007
- return;
1008
- }
1009
- if (type === "shell.end" && pending) {
1010
- const output = String(event.output ?? "");
1011
- if (output && !pending.sawChunk)
1012
- pending.onData(Buffer.from(output));
1013
- const index = this.pendingShells.indexOf(pending);
1014
- if (index !== -1)
1015
- this.pendingShells.splice(index, 1);
1016
- pending.resolve({ exitCode: typeof event.exitCode === "number" ? event.exitCode : event.isError === true ? 1 : 0 });
1017
- }
1018
- }
1019
- settlePendingCompaction(eventValue) {
1020
- const event = recordOf(eventValue);
1021
- if (!event)
1022
- return;
1023
- const type = String(event.type ?? "");
1024
- if (type !== "compaction_end" || this.pendingCompactions.length === 0)
1025
- return;
1026
- const pending = this.pendingCompactions.shift();
1027
- const result = recordOf(event.result);
1028
- if (result)
1029
- pending.resolve(result);
1030
- else if (event.aborted === true)
1031
- pending.reject(new Error("Compaction aborted on the worker."));
1032
- else
1033
- pending.resolve({});
1034
- }
1035
- }
1036
-
1037
- class RigRemoteAgentSession extends PiAgentSession {
1038
- remote;
1039
- constructor(config, remote) {
1040
- super(config);
1041
- this.remote = remote;
1042
- remote.bindSession(this);
1043
- }
1044
- handleRemoteSessionEvent(eventValue) {
1045
- const event = recordOf(eventValue);
1046
- if (!event)
1047
- return;
1048
- const type = String(event.type ?? "");
1049
- if ((type === "message_end" || type === "turn_end") && recordOf(event.message)) {
1050
- this.agent.state.messages = [...this.agent.state.messages, event.message];
1051
- }
1052
- this._emit(eventValue);
1053
- }
1054
- replaceRemoteMessages(messages) {
1055
- this.agent.state.messages = messages;
1056
- }
1057
- async expandLocalInput(text2) {
1058
- let current = text2;
1059
- const runner = this.extensionRunner;
1060
- if (runner.hasHandlers("input")) {
1061
- const result = await runner.emitInput(current, undefined, "interactive");
1062
- if (result.action === "handled")
1063
- return null;
1064
- if (result.action === "transform")
1065
- current = result.text;
1066
- }
1067
- current = this._expandSkillCommand(current);
1068
- current = expandPromptTemplate(current, [...this.promptTemplates]);
1069
- return current;
1070
- }
1071
- async prompt(text2, options) {
1072
- const trimmed = text2.trim();
1073
- if (!trimmed)
1074
- return;
1075
- if (trimmed.startsWith("/")) {
1076
- if (await this._tryExecuteExtensionCommand(trimmed))
1077
- return;
1078
- const expanded2 = await this.expandLocalInput(trimmed);
1079
- if (expanded2 === null)
1080
- return;
1081
- if (expanded2 !== trimmed) {
1082
- const behavior2 = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
1083
- options?.preflightResult?.(true);
1084
- await this.remote.sendPrompt(expanded2, behavior2);
1085
- return;
1086
- }
1087
- await this.remote.sendCommand(trimmed);
1088
- return;
1089
- }
1090
- if (trimmed.startsWith("!")) {
1091
- await this.remote.sendShell(trimmed);
1092
- return;
1093
- }
1094
- const expanded = await this.expandLocalInput(trimmed);
1095
- if (expanded === null)
1096
- return;
1097
- const behavior = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
1098
- options?.preflightResult?.(true);
1099
- await this.remote.sendPrompt(expanded, behavior);
1100
- }
1101
- async steer(text2) {
1102
- const trimmed = text2.trim();
1103
- if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
1104
- return;
1105
- const expanded = await this.expandLocalInput(trimmed);
1106
- if (expanded === null)
1107
- return;
1108
- await this.remote.sendPrompt(expanded, "steer");
1109
- }
1110
- async followUp(text2) {
1111
- const trimmed = text2.trim();
1112
- if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
1113
- return;
1114
- const expanded = await this.expandLocalInput(trimmed);
1115
- if (expanded === null)
1116
- return;
1117
- await this.remote.sendPrompt(expanded, "followUp");
1118
- }
1119
- async abort() {
1120
- await this.remote.abort();
1121
- }
1122
- async compact(customInstructions) {
1123
- const pending = new Promise((resolve4, reject) => {
1124
- this.remote.registerPendingCompaction({ resolve: resolve4, reject });
1125
- });
1126
- await this.remote.sendCommand(`/compact${customInstructions ? ` ${customInstructions}` : ""}`);
1127
- return pending;
1128
- }
1129
- clearQueue() {
1130
- const cleared = {
1131
- steering: [...this.remote.status.steeringMessages],
1132
- followUp: [...this.remote.status.followUpMessages]
1133
- };
1134
- this.remote.status.steeringMessages = [];
1135
- this.remote.status.followUpMessages = [];
1136
- this.remote.status.pendingMessageCount = 0;
1137
- this.remote.sendCommand("/queue-clear").catch(() => {});
1138
- return cleared;
1139
- }
1140
- get isStreaming() {
1141
- return this.remote.status.isStreaming;
1142
- }
1143
- get isCompacting() {
1144
- return this.remote.status.isCompacting;
1145
- }
1146
- get isBashRunning() {
1147
- return this.remote.status.isBashRunning;
1148
- }
1149
- get pendingMessageCount() {
1150
- return this.remote.status.pendingMessageCount;
1151
- }
1152
- getSteeringMessages() {
1153
- return this.remote.status.steeringMessages;
1154
- }
1155
- getFollowUpMessages() {
1156
- return this.remote.status.followUpMessages;
1157
- }
1158
- get model() {
1159
- return this.remote.status.model ?? super.model;
1160
- }
1161
- async setModel(model) {
1162
- await this.remote.sendCommand(`/model ${model.provider}/${model.id}`);
1163
- this.remote.status.model = model;
1164
- }
1165
- get thinkingLevel() {
1166
- return this.remote.status.thinkingLevel ?? super.thinkingLevel;
1167
- }
1168
- setThinkingLevel(level) {
1169
- this.remote.status.thinkingLevel = level;
1170
- this.remote.sendCommand(`/thinking ${level}`).catch(() => {});
1171
- }
1172
- get sessionName() {
1173
- return this.remote.status.sessionName ?? super.sessionName;
1174
- }
1175
- setSessionName(name) {
1176
- this.remote.status.sessionName = name;
1177
- this.remote.sendCommand(`/name ${name}`).catch(() => {});
1178
- }
1179
- getSessionStats() {
1180
- return this.remote.status.stats ?? super.getSessionStats();
1181
- }
1182
- getContextUsage() {
1183
- return this.remote.status.contextUsage ?? super.getContextUsage();
1184
- }
1185
- dispose() {
1186
- this.remote.close();
1187
- super.dispose();
1188
- }
1189
- }
1190
- function createRemoteBashOperations(controller, excludeFromContext) {
1191
- return {
1192
- exec(command, _cwd, execOptions) {
1193
- return new Promise((resolve4, reject) => {
1194
- const pending = { onData: execOptions.onData, resolve: resolve4, reject, sawChunk: false };
1195
- const cleanup = () => {
1196
- execOptions.signal?.removeEventListener("abort", onAbort);
1197
- if (timer)
1198
- clearTimeout(timer);
1199
- };
1200
- const onAbort = () => {
1201
- cleanup();
1202
- controller.failPendingShell(pending, new Error("Remote worker shell command aborted locally."));
1203
- };
1204
- const timeoutMs = typeof execOptions.timeout === "number" && execOptions.timeout > 0 ? execOptions.timeout : 0;
1205
- const timer = timeoutMs > 0 ? setTimeout(() => {
1206
- cleanup();
1207
- controller.failPendingShell(pending, new Error(`Remote worker shell command timed out after ${timeoutMs}ms.`));
1208
- }, timeoutMs) : null;
1209
- const wrappedResolve = pending.resolve;
1210
- const wrappedReject = pending.reject;
1211
- pending.resolve = (result) => {
1212
- cleanup();
1213
- wrappedResolve(result);
1214
- };
1215
- pending.reject = (error) => {
1216
- cleanup();
1217
- wrappedReject(error);
1218
- };
1219
- execOptions.signal?.addEventListener("abort", onAbort, { once: true });
1220
- controller.registerPendingShell(pending);
1221
- controller.sendShell(`${excludeFromContext ? "!!" : "!"}${command}`).catch((error) => {
1222
- cleanup();
1223
- controller.failPendingShell(pending, error instanceof Error ? error : new Error(String(error)));
1224
- });
1225
- });
1226
- }
1227
- };
1228
- }
1229
-
1230
- // packages/cli/src/commands/_spinner.ts
1231
- var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1232
-
1233
- // packages/cli/src/commands/_pi-worker-bridge-extension.ts
1234
- function recordOf2(value) {
1235
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1236
- }
1237
- function asText(value) {
1238
- if (typeof value === "string")
1239
- return value;
1240
- if (value === null || value === undefined)
1241
- return "";
1242
- if (typeof value === "number" || typeof value === "boolean")
1243
- return String(value);
1244
- try {
1245
- return JSON.stringify(value);
1246
- } catch {
1247
- return String(value);
1248
- }
1249
- }
1250
- function names(value, key = "name") {
1251
- if (!Array.isArray(value))
1252
- return [];
1253
- return value.flatMap((entry) => {
1254
- if (typeof entry === "string")
1255
- return [entry];
1256
- const record = recordOf2(entry);
1257
- const name = record?.[key];
1258
- return typeof name === "string" ? [name] : [];
1259
- });
1260
- }
1261
- function renderWorkerCapabilities(capabilities) {
1262
- const lines = ["Worker session capabilities (in effect for this run)"];
1263
- const tools = Array.isArray(capabilities.tools) ? capabilities.tools : [];
1264
- const activeTools = tools.flatMap((tool) => {
1265
- const record = recordOf2(tool);
1266
- return record && record.active === true && typeof record.name === "string" ? [record.name] : [];
1267
- });
1268
- const inactiveCount = tools.length - activeTools.length;
1269
- lines.push(` tools ${activeTools.join(", ") || "(none)"}${inactiveCount > 0 ? ` (+${inactiveCount} inactive)` : ""}`);
1270
- const extensions = Array.isArray(capabilities.extensions) ? capabilities.extensions : [];
1271
- const extensionLabels = extensions.flatMap((entry) => {
1272
- const record = recordOf2(entry);
1273
- if (!record)
1274
- return [];
1275
- const path = typeof record.path === "string" ? record.path : "";
1276
- const short = path.split("/").filter(Boolean).slice(-2).join("/") || path || "extension";
1277
- return [short];
1278
- });
1279
- lines.push(` extensions ${extensionLabels.join(", ") || "(none)"}`);
1280
- const hookEvents = names(capabilities.hookEvents);
1281
- const hookList = Array.isArray(capabilities.hookEvents) ? capabilities.hookEvents.map(asText).filter(Boolean) : hookEvents;
1282
- lines.push(` hooks ${hookList.join(", ") || "(none)"}`);
1283
- lines.push(` skills ${names(capabilities.skills).join(", ") || "(none)"}`);
1284
- lines.push(` prompts ${names(capabilities.prompts).join(", ") || "(none)"}`);
1285
- const model = typeof capabilities.model === "string" ? capabilities.model : "(unknown)";
1286
- const thinking = typeof capabilities.thinkingLevel === "string" ? capabilities.thinkingLevel : "";
1287
- const cwd = typeof capabilities.cwd === "string" ? capabilities.cwd : "";
1288
- lines.push(` model ${model}${thinking ? ` \xB7 ${thinking}` : ""}`);
1289
- if (cwd)
1290
- lines.push(` cwd ${cwd}`);
1291
- lines.push(" (worker commands are in your palette as [worker \u2026] \xB7 /worker shows this again)");
1292
- return lines;
1293
- }
1294
- async function answerExtensionUiRequest(options, ctx, request) {
1295
- const requestId = String(request.requestId ?? request.id ?? `ui-${Date.now()}`);
1296
- const method = String(request.method ?? request.type ?? "input");
1297
- const prompt = asText(request.prompt ?? request.message ?? request.title ?? method);
1298
- const rawOptions = Array.isArray(request.options) ? request.options : Array.isArray(request.items) ? request.items : [];
1299
- const choices = rawOptions.map((option) => {
1300
- const record = recordOf2(option);
1301
- return record ? asText(record.label ?? record.value ?? record.name ?? option) : asText(option);
1302
- }).filter(Boolean);
1303
- try {
1304
- if (method === "confirm") {
1305
- const confirmed = await ctx.ui.confirm("Worker request", prompt);
1306
- await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, { value: confirmed, confirmed });
1307
- return;
1308
- }
1309
- if (choices.length > 0) {
1310
- const selected = await ctx.ui.select(prompt, choices);
1311
- await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, selected === undefined ? { cancelled: true } : { value: selected });
1312
- return;
1313
- }
1314
- const value = await ctx.ui.input("Worker request", prompt);
1315
- await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, value === undefined ? { cancelled: true } : { value });
1316
- } catch (error) {
1317
- ctx.ui.notify(`Failed to answer worker UI request: ${error instanceof Error ? error.message : String(error)}`, "error");
1318
- }
1319
- }
1320
- var LOCALLY_OWNED_COMMANDS = new Set(["detach", "quit", "q", "stop", "worker", "settings", "model", "thinking", "compact", "session", "name", "reload"]);
1321
- function registerDaemonCommands(pi, options, ctx, commands, registered) {
1322
- for (const command of commands) {
1323
- const record = recordOf2(command);
1324
- const name = typeof record?.name === "string" ? record.name : "";
1325
- const source = typeof record?.source === "string" ? record.source : "worker";
1326
- if (!name || source === "builtin" || registered.has(name) || LOCALLY_OWNED_COMMANDS.has(name))
1327
- continue;
1328
- registered.add(name);
1329
- const description = typeof record?.description === "string" ? record.description : undefined;
1330
- try {
1331
- pi.registerCommand(name, {
1332
- description: `[worker ${source}] ${description ?? ""}`.trim(),
1333
- handler: async (args) => {
1334
- try {
1335
- const message = await options.controller.sendCommand(`/${name}${args ? ` ${args}` : ""}`);
1336
- ctx.ui.notify(message, "info");
1337
- } catch (error) {
1338
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1339
- }
1340
- }
1341
- });
1342
- } catch {}
1343
- }
1344
- }
1345
- function createRigWorkerPiBridgeExtension(options) {
1346
- return (pi) => {
1347
- const registeredDaemonCommands = new Set;
1348
- let capabilityLines = null;
1349
- let statusText = "connecting to worker session";
1350
- let busy = true;
1351
- let connected = false;
1352
- let frame = 0;
1353
- let spinnerTimer = null;
1354
- const renderStatus = (ctx) => {
1355
- const prefix = busy ? `${SPINNER_FRAMES[frame]} ` : connected ? "\u25CF " : "\u25CB ";
1356
- ctx.ui.setStatus("rig-worker-pi", `${prefix}${statusText}`);
1357
- };
1358
- const setStatus = (ctx, text2, isBusy) => {
1359
- statusText = text2;
1360
- busy = isBusy;
1361
- renderStatus(ctx);
1362
- };
1363
- pi.registerCommand("detach", {
1364
- description: "Detach from this run; the worker keeps going",
1365
- handler: async (_args, ctx) => {
1366
- ctx.ui.notify("Detached locally; the worker Pi daemon continues.", "info");
1367
- ctx.shutdown();
1368
- }
1369
- });
1370
- pi.registerCommand("stop", {
1371
- description: "Stop the worker Pi run and detach",
1372
- handler: async (_args, ctx) => {
1373
- await options.controller.abort();
1374
- ctx.ui.notify("Stop requested for the worker Pi daemon.", "info");
1375
- ctx.shutdown();
1376
- }
1377
- });
1378
- pi.registerCommand("worker", {
1379
- description: "Show the worker session's real capabilities (tools, extensions, hooks, skills)",
1380
- handler: async (_args, ctx) => {
1381
- if (capabilityLines)
1382
- ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
1383
- else
1384
- ctx.ui.notify("Worker capabilities are not available yet (still connecting).", "info");
1385
- }
1386
- });
1387
- pi.on("user_bash", (event) => ({
1388
- operations: createRemoteBashOperations(options.controller, event.excludeFromContext === true)
1389
- }));
1390
- pi.on("session_start", async (_event, ctx) => {
1391
- ctx.ui.setTitle("Rig \xB7 enriched bundled Pi");
1392
- setStatus(ctx, "waiting for worker Pi daemon", true);
1393
- spinnerTimer = setInterval(() => {
1394
- frame = (frame + 1) % SPINNER_FRAMES.length;
1395
- if (busy)
1396
- renderStatus(ctx);
1397
- }, 150);
1398
- 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");
1399
- if (options.initialMessageSent)
1400
- ctx.ui.notify("Initial message sent to the worker Pi daemon.", "info");
1401
- const nativeUi = ctx.ui;
1402
- options.controller.setUiHooks({
1403
- onStatusText: (text2) => setStatus(ctx, text2, !connected),
1404
- onActivity: (label, detail) => {
1405
- const active = label !== "idle" && !/complete|ready/i.test(label);
1406
- setStatus(ctx, [label, detail].filter(Boolean).join(" \u2014 "), active);
1407
- if (/agent running|tool:/.test(label))
1408
- ctx.ui.setWidget("rig-worker-capabilities", undefined);
1409
- },
1410
- onConnectionChange: (nextConnected) => {
1411
- connected = nextConnected;
1412
- setStatus(ctx, nextConnected ? "worker session live" : "worker session disconnected", false);
1413
- },
1414
- onError: (message) => ctx.ui.notify(message, "error"),
1415
- onExtensionUiRequest: (request) => {
1416
- answerExtensionUiRequest(options, ctx, request);
1417
- },
1418
- onCatchUp: (messages, commands, capabilities) => {
1419
- if (nativeUi.appendSessionMessages)
1420
- nativeUi.appendSessionMessages(messages);
1421
- const cwd = options.controller.status.cwd;
1422
- if (nativeUi.setDisplayCwd && cwd)
1423
- nativeUi.setDisplayCwd(cwd);
1424
- registerDaemonCommands(pi, options, ctx, commands, registeredDaemonCommands);
1425
- if (capabilities) {
1426
- capabilityLines = renderWorkerCapabilities(capabilities);
1427
- ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
1428
- }
1429
- }
1430
- });
1431
- options.controller.connect().catch((error) => {
1432
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1433
- });
1434
- });
1435
- pi.on("session_shutdown", () => {
1436
- if (spinnerTimer)
1437
- clearInterval(spinnerTimer);
1438
- options.controller.close();
1439
- });
1440
- };
1441
- }
1442
-
1443
- // packages/cli/src/commands/_pi-frontend.ts
768
+ import { main as runPiMain } from "@earendil-works/pi-coding-agent";
769
+ import createPiRigExtension from "@rig/pi-rig";
1444
770
  function setTemporaryEnv(updates) {
1445
771
  const previous = new Map;
1446
772
  for (const [key, value] of Object.entries(updates)) {
@@ -1456,51 +782,38 @@ function setTemporaryEnv(updates) {
1456
782
  }
1457
783
  };
1458
784
  }
785
+ function buildOperatorPiEnv(input) {
786
+ return {
787
+ PI_CODING_AGENT_SESSION_DIR: input.sessionDir,
788
+ PI_SKIP_VERSION_CHECK: "1",
789
+ RIG_PI_OPERATOR_SESSION: "1",
790
+ RIG_RUN_ID: input.runId,
791
+ RIG_SERVER_URL: input.serverUrl,
792
+ ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {},
793
+ ...input.serverProjectRoot ? { RIG_PROJECT_ROOT: input.serverProjectRoot } : {}
794
+ };
795
+ }
1459
796
  async function attachRunBundledPiFrontend(context, input) {
1460
797
  const tempSessionDir = mkdtempSync(join2(tmpdir(), "rig-pi-frontend-sessions-"));
1461
- const restoreEnv = setTemporaryEnv({
1462
- PI_CODING_AGENT_SESSION_DIR: tempSessionDir,
1463
- PI_SKIP_VERSION_CHECK: "1",
1464
- PI_HIDDEN_COMMANDS: "import,fork,clone,tree,new,resume,trust"
1465
- });
1466
- const controller = new RigRemoteSessionController({ context, runId: input.runId });
1467
- const createRemoteRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
1468
- const services = await createAgentSessionServices({
1469
- cwd,
1470
- agentDir,
1471
- resourceLoaderOptions: {
1472
- extensionFactories: [
1473
- createRigWorkerPiBridgeExtension({
1474
- context,
1475
- controller,
1476
- runId: input.runId,
1477
- initialMessageSent: input.steered === true
1478
- })
1479
- ],
1480
- noExtensions: true,
1481
- noSkills: true,
1482
- noPromptTemplates: true,
1483
- noContextFiles: true
1484
- }
1485
- });
1486
- const created = await createAgentSessionFromServices({
1487
- services,
1488
- sessionManager,
1489
- sessionStartEvent,
1490
- noTools: "all",
1491
- sessionFactory: (config) => new RigRemoteAgentSession(config, controller)
1492
- });
1493
- return { ...created, services, diagnostics: services.diagnostics };
798
+ const server = await ensureServerForCli(context.projectRoot);
799
+ const restoreEnv = setTemporaryEnv(buildOperatorPiEnv({
800
+ runId: input.runId,
801
+ serverUrl: server.baseUrl,
802
+ authToken: server.authToken,
803
+ serverProjectRoot: server.serverProjectRoot,
804
+ sessionDir: tempSessionDir
805
+ }));
806
+ const piRigExtensionFactory = (pi) => {
807
+ createPiRigExtension(pi);
1494
808
  };
1495
809
  let detached = false;
1496
810
  try {
1497
811
  await runPiMain([], {
1498
- createRuntimeOverride: () => createRemoteRuntime
812
+ extensionFactories: [piRigExtensionFactory]
1499
813
  });
1500
814
  detached = true;
1501
815
  } finally {
1502
816
  restoreEnv();
1503
- controller.close();
1504
817
  rmSync(tempSessionDir, { recursive: true, force: true });
1505
818
  }
1506
819
  let run = { runId: input.runId, status: "unknown" };
@@ -1514,12 +827,12 @@ async function attachRunBundledPiFrontend(context, input) {
1514
827
  timelineCursor: null,
1515
828
  steered: input.steered === true,
1516
829
  detached,
1517
- rendered: "enriched bundled Pi frontend with remote worker session runtime"
830
+ rendered: "stock Pi operator console with the pi-rig extension"
1518
831
  };
1519
832
  }
1520
833
 
1521
834
  // packages/cli/src/commands/_operator-view.ts
1522
- var TERMINAL_RUN_STATUSES2 = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
835
+ var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
1523
836
  function runStatusFromPayload(payload) {
1524
837
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
1525
838
  return String(run.status ?? "unknown").toLowerCase();
@@ -1596,7 +909,7 @@ async function attachRunOperatorView(context, input) {
1596
909
  }
1597
910
  const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
1598
911
  let timelineCursor = snapshot.timelineCursor;
1599
- while (!detached && !TERMINAL_RUN_STATUSES2.has(runStatusFromPayload(snapshot.run))) {
912
+ while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
1600
913
  await Bun.sleep(pollMs);
1601
914
  snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
1602
915
  timelineCursor = snapshot.timelineCursor;
@@ -1803,6 +1116,44 @@ function formatRunSummaryLine(run) {
1803
1116
  return `${pc.dim("\u2502")} ${pc.bold(runId)} ${formatStatusPill(status)} ${descriptor}${runtime ? pc.dim(` ${runtime}`) : ""}`;
1804
1117
  }
1805
1118
 
1119
+ // packages/cli/src/commands/inbox.ts
1120
+ async function listInboxRecords(context, kind, filters) {
1121
+ const params = new URLSearchParams;
1122
+ if (filters.run)
1123
+ params.set("runId", filters.run);
1124
+ if (filters.task)
1125
+ params.set("taskId", filters.task);
1126
+ const query = params.size > 0 ? `?${params.toString()}` : "";
1127
+ const payload = await requestServerJson(context, `/api/inbox/${kind}${query}`);
1128
+ const records = Array.isArray(payload) ? payload : [];
1129
+ return filters.pendingOnly ? records.filter((entry) => (entry.status ?? "pending") !== "resolved") : records;
1130
+ }
1131
+ async function readPendingInboxCounts(context) {
1132
+ try {
1133
+ const [approvals, inputs] = await Promise.all([
1134
+ listInboxRecords(context, "approvals", { pendingOnly: true }),
1135
+ listInboxRecords(context, "inputs", { pendingOnly: true })
1136
+ ]);
1137
+ return { approvals: approvals.length, inputs: inputs.length };
1138
+ } catch {
1139
+ return null;
1140
+ }
1141
+ }
1142
+ async function printPendingInboxFooter(context) {
1143
+ if (context.outputMode !== "text")
1144
+ return;
1145
+ const counts = await readPendingInboxCounts(context);
1146
+ if (!counts || counts.approvals === 0 && counts.inputs === 0)
1147
+ return;
1148
+ const parts = [];
1149
+ if (counts.approvals > 0)
1150
+ parts.push(`${counts.approvals} approval${counts.approvals === 1 ? "" : "s"}`);
1151
+ if (counts.inputs > 0)
1152
+ parts.push(`${counts.inputs} input request${counts.inputs === 1 ? "" : "s"}`);
1153
+ console.log(`
1154
+ \u26A0 ${parts.join(" and ")} pending \u2014 run \`rig inbox\` to review.`);
1155
+ }
1156
+
1806
1157
  // packages/cli/src/commands/run.ts
1807
1158
  function normalizeRemoteRunDetails(payload) {
1808
1159
  const run = payload.run;
@@ -1854,7 +1205,7 @@ async function promptForEpicSelection(projectRoot, command) {
1854
1205
  options.unshift(defaultEpic);
1855
1206
  }
1856
1207
  if (options.length === 0) {
1857
- throw new CliError2("No open epic found. Pass --epic <id>.");
1208
+ throw new CliError("No open epic found. Pass --epic <id>.", 1, { hint: "Re-run with `rig run start --epic <id>`." });
1858
1209
  }
1859
1210
  console.log(`Select epic for run ${command}:`);
1860
1211
  options.forEach((id, index) => {
@@ -1876,7 +1227,7 @@ async function promptForEpicSelection(projectRoot, command) {
1876
1227
  return fallback ?? options[0];
1877
1228
  }
1878
1229
  if (answer === "q" || answer === "quit") {
1879
- throw new CliError2("Run cancelled by user.");
1230
+ throw new CliError("Run cancelled by user.");
1880
1231
  }
1881
1232
  if (/^\d+$/.test(answer)) {
1882
1233
  const index = Number.parseInt(answer, 10) - 1;
@@ -1902,6 +1253,7 @@ async function executeRun(context, args) {
1902
1253
  const { runs, source } = await listRunsForSelectedConnection(context, { limit: 100 });
1903
1254
  if (context.outputMode === "text") {
1904
1255
  printFormattedOutput(formatRunList(runs, { source }));
1256
+ await printPendingInboxFooter(context);
1905
1257
  }
1906
1258
  return { ok: true, group: "run", command, details: { runs, source } };
1907
1259
  }
@@ -1913,7 +1265,7 @@ async function executeRun(context, args) {
1913
1265
  pending = purgeArtifacts.rest;
1914
1266
  requireNoExtraArgs(pending, "rig run delete --run <id> [--purge-artifacts]");
1915
1267
  if (!run.value) {
1916
- throw new CliError2("run delete requires --run <id>.");
1268
+ 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>`." });
1917
1269
  }
1918
1270
  const result = await deleteRunState(context.projectRoot, {
1919
1271
  runId: run.value,
@@ -1943,7 +1295,7 @@ async function executeRun(context, args) {
1943
1295
  pending = keepQueue.rest;
1944
1296
  requireNoExtraArgs(pending, "rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
1945
1297
  if (!all.value) {
1946
- throw new CliError2("run cleanup currently requires --all.");
1298
+ 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)." });
1947
1299
  }
1948
1300
  const result = await cleanupRunState(context.projectRoot, {
1949
1301
  includeArtifacts: !keepArtifacts.value,
@@ -1969,11 +1321,11 @@ async function executeRun(context, args) {
1969
1321
  requireNoExtraArgs(extra, "rig run show <id>|--run <id> [--raw]");
1970
1322
  const runId = run.value ?? positionalRunId;
1971
1323
  if (!runId) {
1972
- throw new CliError2("run show requires a run id.");
1324
+ 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>`." });
1973
1325
  }
1974
1326
  const record = readAuthorityRun2(context.projectRoot, runId) ?? normalizeRemoteRunDetails(await getRunDetailsViaServer(context, runId).catch(() => ({})));
1975
1327
  if (!record) {
1976
- throw new CliError2(`Run not found: ${runId}`, 2);
1328
+ throw new CliError(`Run not found: ${runId}`, 2, { hint: "Run `rig run list` to see recorded runs." });
1977
1329
  }
1978
1330
  if (context.outputMode === "text") {
1979
1331
  printFormattedOutput(rawResult.value ? JSON.stringify(record, null, 2) : formatRunCard(record));
@@ -1988,7 +1340,7 @@ async function executeRun(context, args) {
1988
1340
  pending = follow.rest;
1989
1341
  requireNoExtraArgs(pending, "rig run timeline --run <id> [--follow]");
1990
1342
  if (!run.value) {
1991
- throw new CliError2("run timeline requires --run <id>.");
1343
+ 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>`." });
1992
1344
  }
1993
1345
  const renderer = createPiRunStreamRenderer();
1994
1346
  let cursor = null;
@@ -2020,11 +1372,11 @@ async function executeRun(context, args) {
2020
1372
  requireNoExtraArgs(extra, "rig run replay <id>|--run <id> [--with-session]");
2021
1373
  const runId = run.value ?? positionalRunId;
2022
1374
  if (!runId) {
2023
- throw new CliError2("run replay requires a run id.", 2);
1375
+ 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>`." });
2024
1376
  }
2025
1377
  const replay = buildRunReplay(context.projectRoot, runId, { withSession: withSession.value });
2026
1378
  if (replay.entryCount === 0 && replay.sessionEntryCount === 0) {
2027
- throw new CliError2(`No run.jsonl lifecycle log found for ${runId} (looked at ${replay.logPath}).`, 2);
1379
+ 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." });
2028
1380
  }
2029
1381
  if (context.outputMode === "text") {
2030
1382
  console.log(`Replay of ${runId} (${replay.entryCount} lifecycle entries${withSession.value ? `, ${replay.sessionEntryCount} session entries` : ""}):`);
@@ -2066,7 +1418,7 @@ async function executeRun(context, args) {
2066
1418
  requireNoExtraArgs(extra, "rig run attach <run-id>|--run <run-id> [--message <text>] [--once|--follow] [--poll-ms <ms>]");
2067
1419
  const runId = runOption.value ?? positionalRunId;
2068
1420
  if (!runId) {
2069
- throw new CliError2("run attach requires a run id.", 2);
1421
+ 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`." });
2070
1422
  }
2071
1423
  let steered = false;
2072
1424
  if (messageOption.value?.trim()) {
@@ -2095,6 +1447,7 @@ async function executeRun(context, args) {
2095
1447
  const recentRuns = Array.isArray(summary.recentRuns) ? summary.recentRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
2096
1448
  if (context.outputMode === "text") {
2097
1449
  printFormattedOutput(formatRunStatus({ activeRuns, recentRuns, runs: Array.isArray(summary.runs) ? summary.runs : [...activeRuns, ...recentRuns] }, { source: isRemoteConnectionSelected(context.projectRoot) ? "server" : "local" }));
1450
+ await printPendingInboxFooter(context);
2098
1451
  }
2099
1452
  return { ok: true, group: "run", command, details: summary };
2100
1453
  }
@@ -2120,10 +1473,10 @@ async function executeRun(context, args) {
2120
1473
  pending = noServerResult.rest;
2121
1474
  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]");
2122
1475
  if (promptEpicResult.value && noEpicPromptResult.value) {
2123
- throw new CliError2("Cannot use --prompt-epic and --no-epic-prompt together.");
1476
+ throw new CliError("Cannot use --prompt-epic and --no-epic-prompt together.", 1, { hint: "Pass only one of --prompt-epic or --no-epic-prompt." });
2124
1477
  }
2125
1478
  if (promptEpicResult.value && (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY)) {
2126
- throw new CliError2("--prompt-epic requires an interactive terminal (TTY) in text mode.");
1479
+ 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>`." });
2127
1480
  }
2128
1481
  let resolvedEpicId = epicResult.value || undefined;
2129
1482
  if (!resolvedEpicId && shouldPromptForEpicSelection(context, command, promptEpicResult.value, noEpicPromptResult.value)) {
@@ -2151,7 +1504,7 @@ async function executeRun(context, args) {
2151
1504
  console.log(`Runs: ${result.runIds.join(", ")}`);
2152
1505
  }
2153
1506
  if (result.exitCode !== 0) {
2154
- throw new CliError2(`run ${command} failed with exit code ${result.exitCode}.`, result.exitCode);
1507
+ 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>`." });
2155
1508
  }
2156
1509
  return {
2157
1510
  ok: true,
@@ -2203,10 +1556,10 @@ async function executeRun(context, args) {
2203
1556
  const runId = runOption.value ?? positionalRunId;
2204
1557
  const message = messageOption.value ?? shortMessageOption.value;
2205
1558
  if (!runId) {
2206
- throw new CliError2("run steer requires a run id (positional or --run <id>).", 2);
1559
+ 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>`." });
2207
1560
  }
2208
1561
  if (!message?.trim()) {
2209
- throw new CliError2("run steer requires --message <text>.", 2);
1562
+ throw new CliError("run steer requires --message <text>.", 2, { hint: 'Re-run as `rig run steer <run-id> --message "your steering note"`.' });
2210
1563
  }
2211
1564
  if (context.dryRun) {
2212
1565
  if (context.outputMode === "text") {
@@ -2242,7 +1595,7 @@ async function executeRun(context, args) {
2242
1595
  }
2243
1596
  const result = await runStop(context.projectRoot);
2244
1597
  if (result.remaining.length > 0) {
2245
- throw new CliError2(`Failed to stop run(s): ${result.remaining.join(", ")}`, 1);
1598
+ 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." });
2246
1599
  }
2247
1600
  if (context.outputMode === "text") {
2248
1601
  console.log(`Stopped process count: ${result.stopped}`);
@@ -2258,7 +1611,7 @@ async function executeRun(context, args) {
2258
1611
  };
2259
1612
  }
2260
1613
  default:
2261
- throw new CliError2(`Unknown run command: ${command}`);
1614
+ throw new CliError(`Unknown run command: ${command}`, 1, { hint: "Run `rig run --help` to list available run commands." });
2262
1615
  }
2263
1616
  }
2264
1617
  export {