@h-rig/cli 0.0.6-alpha.21 → 0.0.6-alpha.22

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.
@@ -618,6 +618,7 @@ function buildPiRigBridgeEnv(input) {
618
618
  RIG_SERVER_RUN_ID: input.runId,
619
619
  RIG_TASK_ID: input.taskId,
620
620
  RIG_RUNTIME_ADAPTER: "pi",
621
+ RIG_STEERING_POLL_MS: "0",
621
622
  ...input.serverUrl ? { RIG_SERVER_URL: input.serverUrl } : {},
622
623
  ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {},
623
624
  ...githubBridgeEnv(githubToken)
@@ -1220,6 +1221,69 @@ function appendAssistantTimelineFromRecord(input) {
1220
1221
  }
1221
1222
  return nextAssistantText;
1222
1223
  }
1224
+ function appendPiRpcProtocolLogFromRecord(input) {
1225
+ const type = typeof input.record.type === "string" ? input.record.type : "";
1226
+ if (type === "response") {
1227
+ const command = typeof input.record.command === "string" ? input.record.command : "rpc";
1228
+ const success = input.record.success !== false;
1229
+ if (success && command !== "prompt" && command !== "steer" && command !== "follow_up" && command !== "set_session_name") {
1230
+ return true;
1231
+ }
1232
+ appendRunLog(input.projectRoot, input.runId, {
1233
+ id: input.nextRunLogId(),
1234
+ title: success ? "Pi RPC response" : "Pi RPC error",
1235
+ detail: success ? `${command}: accepted` : `${command}: ${String(input.record.error ?? "failed")}`,
1236
+ tone: success ? "tool" : "error",
1237
+ status: input.status,
1238
+ payload: input.record,
1239
+ createdAt: new Date().toISOString()
1240
+ });
1241
+ emitServerRunEvent({ type: "log", runId: input.runId, title: success ? "Pi RPC response" : "Pi RPC error" });
1242
+ return true;
1243
+ }
1244
+ if (type !== "extension_ui_request")
1245
+ return false;
1246
+ const method = typeof input.record.method === "string" ? input.record.method : "ui";
1247
+ let title = "Pi UI event";
1248
+ let detail = method;
1249
+ let tone = "info";
1250
+ if (method === "notify") {
1251
+ title = "Pi notification";
1252
+ detail = String(input.record.message ?? "");
1253
+ tone = input.record.notifyType === "error" ? "error" : "info";
1254
+ } else if (method === "setStatus") {
1255
+ title = "Pi UI status";
1256
+ detail = `${String(input.record.statusKey ?? "status")}: ${String(input.record.statusText ?? "cleared")}`;
1257
+ tone = "tool";
1258
+ } else if (method === "setWidget") {
1259
+ title = "Pi UI widget";
1260
+ const lines = Array.isArray(input.record.widgetLines) ? input.record.widgetLines.map((line) => String(line)).join(" | ") : "cleared";
1261
+ detail = `${String(input.record.widgetKey ?? "widget")}: ${lines}`.slice(0, 500);
1262
+ tone = "tool";
1263
+ } else if (method === "setTitle") {
1264
+ title = "Pi UI title";
1265
+ detail = String(input.record.title ?? "");
1266
+ tone = "tool";
1267
+ } else if (method === "set_editor_text") {
1268
+ title = "Pi editor update";
1269
+ detail = String(input.record.text ?? "").slice(0, 500);
1270
+ tone = "tool";
1271
+ } else {
1272
+ title = "Pi UI request";
1273
+ detail = `${method}: ${String(input.record.title ?? input.record.message ?? "")}`.trim();
1274
+ }
1275
+ appendRunLog(input.projectRoot, input.runId, {
1276
+ id: input.nextRunLogId(),
1277
+ title,
1278
+ detail,
1279
+ tone,
1280
+ status: input.status,
1281
+ payload: input.record,
1282
+ createdAt: new Date().toISOString()
1283
+ });
1284
+ emitServerRunEvent({ type: "log", runId: input.runId, title });
1285
+ return true;
1286
+ }
1223
1287
  function appendPiToolTimelineFromRecord(input) {
1224
1288
  const type = typeof input.record.type === "string" ? input.record.type : "";
1225
1289
  if (type !== "tool_execution_start" && type !== "tool_execution_update" && type !== "tool_execution_end")
@@ -1238,7 +1302,7 @@ function appendPiToolTimelineFromRecord(input) {
1238
1302
  }
1239
1303
  function isNonRenderablePiProtocolRecord(record) {
1240
1304
  const type = typeof record.type === "string" ? record.type : "";
1241
- return type === "message_start" || type === "message_end" || type === "turn_start" || type === "turn_end" || type === "tool_result" || type === "message_update" && (!record.assistantMessageEvent || typeof record.assistantMessageEvent !== "object" || Array.isArray(record.assistantMessageEvent) || record.assistantMessageEvent.type !== "text_delta");
1305
+ return type === "agent_start" || type === "agent_end" || type === "message_start" || type === "message_end" || type === "turn_start" || type === "turn_end" || type === "tool_result" || type === "message_update" && (!record.assistantMessageEvent || typeof record.assistantMessageEvent !== "object" || Array.isArray(record.assistantMessageEvent) || record.assistantMessageEvent.type !== "text_delta");
1242
1306
  }
1243
1307
  function appendToolTimelineFromLog(input) {
1244
1308
  const title = typeof input.log.title === "string" ? input.log.title : "";
@@ -1401,11 +1465,8 @@ async function executeRigOwnedTaskRun(context, input) {
1401
1465
  ...input.model ? ["--model", input.model] : [],
1402
1466
  "--prompt"
1403
1467
  ] : input.runtimeAdapter === "pi" ? [
1404
- "--print",
1405
- "--verbose",
1406
1468
  "--mode",
1407
- "json",
1408
- "--no-session",
1469
+ "rpc",
1409
1470
  ...input.model ? ["--model", input.model] : []
1410
1471
  ] : [
1411
1472
  "--print",
@@ -1502,7 +1563,7 @@ ${planningClassification.planningRequired ? `Before implementing, write a concis
1502
1563
  projectRoot: context.projectRoot,
1503
1564
  runId: input.runId,
1504
1565
  stage,
1505
- detail: stage === "Launch Pi" ? "Pi runtime bridge starting with pi-rig environment." : stage === "Plan" ? `${planningClassification.planningRequired ? "recorded" : "skipped"} (${planningClassification.reason}; size=${planningClassification.size}; risk=${planningClassification.risk})` : stage === "Implement" ? "Pi implementation pass is running." : null,
1566
+ detail: stage === "Launch Pi" ? "Pi RPC runtime bridge starting with pi-rig environment." : stage === "Plan" ? `${planningClassification.planningRequired ? "recorded" : "skipped"} (${planningClassification.reason}; size=${planningClassification.size}; risk=${planningClassification.risk})` : stage === "Implement" ? "Pi implementation pass is running." : null,
1506
1567
  status: stage === "Implement" || stage === "Launch Pi" ? "running" : "completed"
1507
1568
  });
1508
1569
  }
@@ -1595,6 +1656,7 @@ ${planningClassification.planningRequired ? `Before implementing, write a concis
1595
1656
  detail: detail ?? "Verifier review is running."
1596
1657
  });
1597
1658
  };
1659
+ const nextRunLogId = createRunLogIdFactory(input.runId);
1598
1660
  const handleWrapperEvent = (rawPayload) => {
1599
1661
  let event = null;
1600
1662
  try {
@@ -1729,9 +1791,23 @@ ${planningClassification.planningRequired ? `Before implementing, write a concis
1729
1791
  }
1730
1792
  return true;
1731
1793
  }
1794
+ if (event.type === "pi.rpc.prompt.sent" || event.type === "pi.rpc.steering.delivered" || event.type === "pi.rpc.steering.poll.failed" || event.type === "pi.rpc.extension_ui.cancelled") {
1795
+ const title = event.type === "pi.rpc.prompt.sent" ? "Delivered initial prompt to worker Pi" : event.type === "pi.rpc.steering.delivered" ? "Delivered steering to worker Pi" : event.type === "pi.rpc.steering.poll.failed" ? "Worker Pi steering poll failed" : "Pi RPC UI request auto-cancelled";
1796
+ const detail = event.type === "pi.rpc.prompt.sent" ? `${String(payload.kind ?? "prompt")} prompt (${String(payload.bytes ?? "unknown")} bytes)` : event.type === "pi.rpc.steering.delivered" ? `${String(payload.actor ?? "operator")}: ${String(payload.message ?? "")}`.slice(0, 500) : event.type === "pi.rpc.steering.poll.failed" ? String(payload.error ?? "steering poll failed") : `${String(payload.method ?? "ui")}: ${String(payload.reason ?? "noninteractive worker session")}`;
1797
+ appendRunLog(context.projectRoot, input.runId, {
1798
+ id: nextRunLogId(),
1799
+ title,
1800
+ detail,
1801
+ tone: event.type === "pi.rpc.steering.poll.failed" ? "error" : "info",
1802
+ status: reviewStarted ? "reviewing" : verificationStarted ? "validating" : "running",
1803
+ payload,
1804
+ createdAt: new Date().toISOString()
1805
+ });
1806
+ emitServerRunEvent({ type: "log", runId: input.runId, title });
1807
+ return true;
1808
+ }
1732
1809
  return false;
1733
1810
  };
1734
- const nextRunLogId = createRunLogIdFactory(input.runId);
1735
1811
  const handleAgentStdoutLine = (line) => {
1736
1812
  const trimmed = line.trim();
1737
1813
  if (!trimmed)
@@ -1765,6 +1841,15 @@ ${planningClassification.planningRequired ? `Before implementing, write a concis
1765
1841
  try {
1766
1842
  const record = JSON.parse(trimmed);
1767
1843
  const liveLogStatus = reviewStarted ? "reviewing" : verificationStarted ? "validating" : "running";
1844
+ if (input.runtimeAdapter === "pi" && appendPiRpcProtocolLogFromRecord({
1845
+ projectRoot: context.projectRoot,
1846
+ runId: input.runId,
1847
+ record,
1848
+ status: liveLogStatus,
1849
+ nextRunLogId
1850
+ })) {
1851
+ return;
1852
+ }
1768
1853
  if (input.runtimeAdapter === "pi" && appendPiToolTimelineFromRecord({ projectRoot: context.projectRoot, runId: input.runId, record })) {
1769
1854
  emitServerRunEvent({ type: "timeline", runId: input.runId });
1770
1855
  return;
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // packages/cli/src/commands/task.ts
3
5
  import { readFileSync as readFileSync4 } from "fs";
4
6
  import { spawnSync } from "child_process";
@@ -13,9 +15,6 @@ import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
13
15
  import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
14
16
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
15
17
  import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
16
- function formatCommand(parts) {
17
- return parts.map((part) => /[^a-zA-Z0-9_./:-]/.test(part) ? JSON.stringify(part) : part).join(" ");
18
- }
19
18
  function takeFlag(args, flag) {
20
19
  const rest = [];
21
20
  let value = false;
@@ -412,12 +411,6 @@ async function defaultCommandRunner(command, options = {}) {
412
411
  function resolvePiRigExtensionPath(homeDir) {
413
412
  return resolve3(homeDir, ".pi", "agent", "extensions", "pi-rig");
414
413
  }
415
- function resolvePiRigPackageSource(projectRoot, exists = existsSync3) {
416
- const localPackage = resolve3(projectRoot, "packages", "pi-rig");
417
- if (exists(resolve3(localPackage, "package.json")))
418
- return localPackage;
419
- return `npm:${PI_RIG_PACKAGE_NAME}`;
420
- }
421
414
  function resolvePiHomeDir(inputHomeDir) {
422
415
  return inputHomeDir ?? process.env.RIG_PI_HOME_DIR?.trim() ?? homedir2();
423
416
  }
@@ -976,6 +969,50 @@ async function selectTaskWithTextPicker(tasks, io = {}) {
976
969
 
977
970
  // packages/cli/src/commands/_operator-view.ts
978
971
  var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
972
+ var CANONICAL_STAGES2 = [
973
+ "Connect",
974
+ "GitHub/task sync",
975
+ "Prepare workspace",
976
+ "Launch Pi",
977
+ "Plan",
978
+ "Implement",
979
+ "Validate",
980
+ "Commit",
981
+ "Open PR",
982
+ "Review/CI",
983
+ "Merge",
984
+ "Complete"
985
+ ];
986
+ var GREEN = "\x1B[32m";
987
+ var BLUE = "\x1B[34m";
988
+ var MAGENTA = "\x1B[35m";
989
+ var YELLOW = "\x1B[33m";
990
+ var RED = "\x1B[31m";
991
+ var DIM = "\x1B[2m";
992
+ var BOLD = "\x1B[1m";
993
+ var RESET = "\x1B[0m";
994
+ async function loadPiTuiRuntime() {
995
+ try {
996
+ return await import("@earendil-works/pi-tui");
997
+ } catch {
998
+ const base = new URL("../../../pi/packages/tui/src/", import.meta.url);
999
+ const [tui, input, terminal, keys, utils] = await Promise.all([
1000
+ import(new URL("tui.ts", base).href),
1001
+ import(new URL("components/input.ts", base).href),
1002
+ import(new URL("terminal.ts", base).href),
1003
+ import(new URL("keys.ts", base).href),
1004
+ import(new URL("utils.ts", base).href)
1005
+ ]);
1006
+ return {
1007
+ Container: tui.Container,
1008
+ TUI: tui.TUI,
1009
+ Input: input.Input,
1010
+ ProcessTerminal: terminal.ProcessTerminal,
1011
+ matchesKey: keys.matchesKey,
1012
+ truncateToWidth: utils.truncateToWidth
1013
+ };
1014
+ }
1015
+ }
979
1016
  function runStatusFromPayload(payload) {
980
1017
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
981
1018
  return String(run.status ?? "unknown").toLowerCase();
@@ -1014,12 +1051,201 @@ async function readOperatorSnapshot(context, runId, options = {}) {
1014
1051
  const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
1015
1052
  return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
1016
1053
  }
1054
+ function unwrapRun(runPayload) {
1055
+ return runPayload.run && typeof runPayload.run === "object" && !Array.isArray(runPayload.run) ? runPayload.run : runPayload;
1056
+ }
1057
+ function logDetail2(log) {
1058
+ return typeof log.detail === "string" ? log.detail.trim() : "";
1059
+ }
1060
+ function logTitle(log) {
1061
+ return typeof log.title === "string" ? log.title.trim() : "";
1062
+ }
1063
+ function renderAssistantTextFromTimeline(entries) {
1064
+ const assistant = entries.filter((entry) => entry.type === "assistant_message" && typeof entry.text === "string").at(-1);
1065
+ const text = typeof assistant?.text === "string" ? assistant.text.trimEnd() : "";
1066
+ if (!text)
1067
+ return [];
1068
+ return [`${BLUE}${BOLD}Remote Pi assistant${RESET}`, ...text.split(/\r?\n/).slice(-18)];
1069
+ }
1070
+ function renderToolLines(entries) {
1071
+ return entries.filter((entry) => entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call").slice(-8).map((entry) => `${DIM}[tool]${RESET} ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
1072
+ }
1073
+ function renderStageLines(logs) {
1074
+ const latestByStage = new Map;
1075
+ for (const log of logs) {
1076
+ const title = logTitle(log).toLowerCase();
1077
+ const stageName = String(log.stage ?? "").toLowerCase();
1078
+ const stage = CANONICAL_STAGES2.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
1079
+ if (stage)
1080
+ latestByStage.set(stage, log);
1081
+ }
1082
+ return CANONICAL_STAGES2.map((stage) => {
1083
+ const log = latestByStage.get(stage);
1084
+ const status = String(log?.status ?? "pending");
1085
+ const detail = log ? logDetail2(log) : "";
1086
+ const color = status === "completed" ? GREEN : status === "failed" || status === "rejected" ? RED : status === "pending" ? DIM : YELLOW;
1087
+ const mark = status === "completed" ? "\u2713" : status === "pending" ? "\xB7" : status === "failed" ? "\u2717" : "\u25B6";
1088
+ return `${color}${mark} ${stage}${RESET}${detail ? ` ${DIM}\u2014 ${detail.slice(0, 140)}${RESET}` : ""}`;
1089
+ });
1090
+ }
1091
+ function renderEventLines(logs) {
1092
+ return logs.filter((log) => !CANONICAL_STAGES2.some((stage) => stage.toLowerCase() === logTitle(log).toLowerCase())).slice(-12).flatMap((log) => {
1093
+ const title = logTitle(log) || "Rig event";
1094
+ const detail = logDetail2(log);
1095
+ if (!detail)
1096
+ return [];
1097
+ const tone = String(log.tone ?? "");
1098
+ const color = tone === "error" ? RED : tone === "tool" ? MAGENTA : DIM;
1099
+ return [`${color}[${title}]${RESET} ${detail.slice(0, 220)}`];
1100
+ });
1101
+ }
1102
+
1103
+ class RigRunComponent {
1104
+ truncateToWidth;
1105
+ snapshot = { run: {}, logs: [], timeline: [] };
1106
+ localEvents = [];
1107
+ constructor(truncateToWidth) {
1108
+ this.truncateToWidth = truncateToWidth;
1109
+ }
1110
+ update(snapshot) {
1111
+ this.snapshot = snapshot;
1112
+ }
1113
+ addLocalEvent(message2) {
1114
+ this.localEvents.push(`${new Date().toLocaleTimeString()} ${message2}`);
1115
+ this.localEvents = this.localEvents.slice(-8);
1116
+ }
1117
+ invalidate() {}
1118
+ render(width) {
1119
+ const run = unwrapRun(this.snapshot.run);
1120
+ const runId = String(run.runId ?? run.id ?? "run");
1121
+ const status = String(run.status ?? "unknown");
1122
+ const worker = typeof run.worktreePath === "string" && run.worktreePath.trim() ? run.worktreePath.trim() : typeof run.projectRoot === "string" && run.projectRoot.trim() ? run.projectRoot.trim() : "worker workspace pending";
1123
+ const lines = [
1124
+ `${BOLD}Rig Pi frontend${RESET} ${DIM}(local Pi TUI \u2192 Rig server \u2192 worker Pi backend)${RESET}`,
1125
+ `${BOLD}${runId}${RESET} \xB7 ${status} \xB7 ${DIM}${worker}${RESET}`,
1126
+ "",
1127
+ `${BOLD}Rig flow${RESET}`,
1128
+ ...renderStageLines(this.snapshot.logs ?? []),
1129
+ "",
1130
+ ...renderAssistantTextFromTimeline(this.snapshot.timeline ?? []),
1131
+ ...renderToolLines(this.snapshot.timeline ?? []),
1132
+ "",
1133
+ `${BOLD}Rig / Pi events${RESET}`,
1134
+ ...renderEventLines(this.snapshot.logs ?? []),
1135
+ ...this.localEvents.map((event) => `${GREEN}[frontend]${RESET} ${event}`),
1136
+ ""
1137
+ ];
1138
+ return lines.slice(-42).map((line) => this.truncateToWidth(line, Math.max(10, width)));
1139
+ }
1140
+ }
1141
+
1142
+ class RigInputComponent {
1143
+ matchesKey;
1144
+ truncateToWidth;
1145
+ input;
1146
+ status = "Type text, /skill:..., or remote Pi slash commands. Local: /stop /detach.";
1147
+ constructor(InputCtor, matchesKey, truncateToWidth, onSubmit, onEscape) {
1148
+ this.matchesKey = matchesKey;
1149
+ this.truncateToWidth = truncateToWidth;
1150
+ this.input = new InputCtor;
1151
+ this.input.onSubmit = (value) => {
1152
+ const text = value.trim();
1153
+ this.input.setValue("");
1154
+ if (text)
1155
+ Promise.resolve(onSubmit(text));
1156
+ };
1157
+ this.input.onEscape = onEscape;
1158
+ }
1159
+ handleInput(data) {
1160
+ if (this.matchesKey(data, "ctrl+d")) {
1161
+ this.input.onEscape?.();
1162
+ return;
1163
+ }
1164
+ this.input.handleInput?.(data);
1165
+ }
1166
+ setStatus(status) {
1167
+ this.status = status;
1168
+ }
1169
+ invalidate() {
1170
+ this.input.invalidate();
1171
+ }
1172
+ render(width) {
1173
+ return [
1174
+ `${DIM}${this.truncateToWidth(this.status, Math.max(10, width))}${RESET}`,
1175
+ `${GREEN}${BOLD}You \u2192 worker Pi:${RESET}`,
1176
+ ...this.input.render(width)
1177
+ ];
1178
+ }
1179
+ }
1180
+ async function attachRunPiTuiFrontend(context, input) {
1181
+ const piTui = await loadPiTuiRuntime();
1182
+ const terminal = new piTui.ProcessTerminal;
1183
+ const tui = new piTui.TUI(terminal);
1184
+ const root = new piTui.Container;
1185
+ const runView = new RigRunComponent(piTui.truncateToWidth);
1186
+ let detached = false;
1187
+ let steered = input.steered === true;
1188
+ let latest = await readOperatorSnapshot(context, input.runId);
1189
+ let timelineCursor = latest.timelineCursor;
1190
+ runView.update(latest);
1191
+ if (steered)
1192
+ runView.addLocalEvent("initial message queued to worker Pi.");
1193
+ const stop = () => {
1194
+ detached = true;
1195
+ tui.stop();
1196
+ };
1197
+ const inputView = new RigInputComponent(piTui.Input, piTui.matchesKey, piTui.truncateToWidth, async (line) => {
1198
+ if (line === "/detach" || line === "/quit" || line === "/q") {
1199
+ runView.addLocalEvent("detached from run.");
1200
+ stop();
1201
+ return;
1202
+ }
1203
+ if (line === "/stop") {
1204
+ await stopRunViaServer(context, input.runId);
1205
+ runView.addLocalEvent("stop requested.");
1206
+ stop();
1207
+ return;
1208
+ }
1209
+ await steerRunViaServer(context, input.runId, line);
1210
+ steered = true;
1211
+ runView.addLocalEvent(`queued to worker Pi: ${line.slice(0, 160)}`);
1212
+ tui.requestRender();
1213
+ }, stop);
1214
+ root.addChild(runView);
1215
+ root.addChild(inputView);
1216
+ tui.addChild(root);
1217
+ tui.setFocus(inputView.input);
1218
+ tui.start();
1219
+ tui.requestRender(true);
1220
+ const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 1000));
1221
+ try {
1222
+ while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(latest.run))) {
1223
+ await Bun.sleep(pollMs);
1224
+ latest = await readOperatorSnapshot(context, input.runId, { timelineCursor });
1225
+ timelineCursor = latest.timelineCursor;
1226
+ runView.update(latest);
1227
+ inputView.setStatus(`Remote worker ${runStatusFromPayload(latest.run)}. Input is forwarded to worker Pi; /stop /detach are local controls.`);
1228
+ tui.requestRender();
1229
+ }
1230
+ } finally {
1231
+ if (!detached)
1232
+ tui.stop();
1233
+ }
1234
+ return { ...latest, timelineCursor, steered, detached, rendered: renderOperatorSnapshot(latest) };
1235
+ }
1017
1236
  async function attachRunOperatorView(context, input) {
1018
1237
  let steered = false;
1019
1238
  if (input.message?.trim()) {
1020
1239
  await steerRunViaServer(context, input.runId, input.message.trim());
1021
1240
  steered = true;
1022
1241
  }
1242
+ if (input.follow && !input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
1243
+ return attachRunPiTuiFrontend(context, {
1244
+ runId: input.runId,
1245
+ pollMs: input.pollMs,
1246
+ steered
1247
+ });
1248
+ }
1023
1249
  const surface = createOperatorSurface({ interactive: input.interactive !== false });
1024
1250
  let snapshot = await readOperatorSnapshot(context, input.runId);
1025
1251
  if (context.outputMode === "text") {
@@ -1137,92 +1363,6 @@ function formatSubmittedRun(input) {
1137
1363
  `);
1138
1364
  }
1139
1365
 
1140
- // packages/cli/src/commands/_pi-session.ts
1141
- import { spawn } from "child_process";
1142
- function buildPiRigSessionEnv(input) {
1143
- return {
1144
- RIG_PROJECT_ROOT: input.projectRoot,
1145
- PROJECT_RIG_ROOT: input.projectRoot,
1146
- RIG_RUN_ID: input.runId,
1147
- RIG_SERVER_RUN_ID: input.runId,
1148
- RIG_RUNTIME_ADAPTER: "pi",
1149
- RIG_SERVER_URL: input.serverUrl,
1150
- RIG_SERVER_BASE_URL: input.serverUrl,
1151
- RIG_STEERING_POLL_MS: process.env.RIG_STEERING_POLL_MS?.trim() || "1000",
1152
- RIG_PI_OPERATOR_SESSION: "1",
1153
- ...input.taskId ? { RIG_TASK_ID: input.taskId } : {},
1154
- ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {}
1155
- };
1156
- }
1157
- function shellBinary(name) {
1158
- const explicit = process.env.RIG_PI_BINARY?.trim();
1159
- if (explicit)
1160
- return explicit;
1161
- return Bun.which(name) || name;
1162
- }
1163
- function buildPiRigSessionCommand(input) {
1164
- const configuredExtension = input.extensionSource ?? process.env.RIG_PI_RIG_EXTENSION_SOURCE?.trim();
1165
- const extensionSource = configuredExtension && configuredExtension.length > 0 ? configuredExtension : resolvePiRigPackageSource(input.projectRoot);
1166
- const initialCommand = `/rig attach ${input.runId}`;
1167
- return [
1168
- shellBinary("pi"),
1169
- "--no-extensions",
1170
- "--extension",
1171
- extensionSource,
1172
- initialCommand
1173
- ];
1174
- }
1175
- async function launchPiRigSession(context, input) {
1176
- if (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY) {
1177
- return { launched: false, exitCode: null, command: [] };
1178
- }
1179
- if (process.env.RIG_DISABLE_PI_LAUNCH === "1") {
1180
- return { launched: false, exitCode: null, command: [] };
1181
- }
1182
- const server = await ensureServerForCli(context.projectRoot);
1183
- const command = buildPiRigSessionCommand({ ...input, projectRoot: context.projectRoot });
1184
- const env = {
1185
- ...process.env,
1186
- ...buildPiRigSessionEnv({
1187
- projectRoot: context.projectRoot,
1188
- runId: input.runId,
1189
- taskId: input.taskId,
1190
- serverUrl: server.baseUrl,
1191
- authToken: server.authToken
1192
- })
1193
- };
1194
- process.stdout.write(`Launching Pi for Rig run ${input.runId}\u2026
1195
- `);
1196
- process.stdout.write(`Pi command: ${formatCommand(command)}
1197
- `);
1198
- const launchedAt = Date.now();
1199
- const child = spawn(command[0], command.slice(1), {
1200
- cwd: context.projectRoot,
1201
- env,
1202
- stdio: "inherit"
1203
- });
1204
- const launchError = await new Promise((resolve4) => {
1205
- child.once("error", (error) => {
1206
- resolve4({ error: error.message });
1207
- });
1208
- child.once("close", (code) => resolve4({ code }));
1209
- });
1210
- if ("error" in launchError) {
1211
- process.stderr.write(`Failed to launch Pi; falling back to Rig attach view: ${launchError.error}
1212
- `);
1213
- return { launched: false, exitCode: null, command, error: launchError.error };
1214
- }
1215
- const exitCode = launchError.code;
1216
- const elapsedMs = Date.now() - launchedAt;
1217
- if (typeof exitCode === "number" && exitCode !== 0 && elapsedMs < 5000) {
1218
- const error = `Pi exited during startup with code ${exitCode}.`;
1219
- process.stderr.write(`${error} Falling back to Rig attach view.
1220
- `);
1221
- return { launched: false, exitCode, command, error };
1222
- }
1223
- return { launched: true, exitCode, command };
1224
- }
1225
-
1226
1366
  // packages/cli/src/commands/task.ts
1227
1367
  import { buildPluginHostContext } from "@rig/runtime/control-plane/plugin-host-context";
1228
1368
  import { loadConfig } from "@rig/core/load-config";
@@ -1614,20 +1754,7 @@ async function executeTask(context, args, options) {
1614
1754
  let attachDetails = null;
1615
1755
  if (!detachResult.value && context.outputMode === "text") {
1616
1756
  console.log(formatSubmittedRun({ runId: submitted.runId, task: selectedTask ? summarizeTask(selectedTask) : null }));
1617
- if (runtimeAdapter === "pi") {
1618
- const piSession = await launchPiRigSession(context, {
1619
- runId: submitted.runId,
1620
- taskId: selectedTaskId,
1621
- title: titleResult.value ?? readTaskString(selectedTask ?? {}, "title"),
1622
- runtimeAdapter
1623
- });
1624
- attachDetails = { mode: "pi", ...piSession };
1625
- if (!piSession.launched) {
1626
- attachDetails = await attachRunOperatorView(context, { runId: submitted.runId, follow: true });
1627
- }
1628
- } else {
1629
- attachDetails = await attachRunOperatorView(context, { runId: submitted.runId, follow: true });
1630
- }
1757
+ attachDetails = await attachRunOperatorView(context, { runId: submitted.runId, follow: true });
1631
1758
  } else if (context.outputMode === "text") {
1632
1759
  console.log(formatSubmittedRun({ runId: submitted.runId, task: selectedTask ? summarizeTask(selectedTask) : null }));
1633
1760
  }