@h-rig/cli 0.0.6-alpha.2 → 0.0.6-alpha.20

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.
@@ -1,9 +1,9 @@
1
1
  // @bun
2
2
  // packages/cli/src/commands/task.ts
3
3
  import { readFileSync as readFileSync4 } from "fs";
4
- import { spawnSync as spawnSync2 } from "child_process";
5
- import { createInterface as createInterface3 } from "readline/promises";
4
+ import { spawnSync } from "child_process";
6
5
  import { resolve as resolve4 } from "path";
6
+ import { cancel as cancel2, confirm, isCancel as isCancel2 } from "@clack/prompts";
7
7
 
8
8
  // packages/cli/src/runner.ts
9
9
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
@@ -13,6 +13,9 @@ import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
13
13
  import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
14
14
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
15
15
  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
+ }
16
19
  function takeFlag(args, flag) {
17
20
  const rest = [];
18
21
  let value = false;
@@ -188,11 +191,10 @@ function resolveSelectedConnection(projectRoot, options = {}) {
188
191
  }
189
192
 
190
193
  // packages/cli/src/commands/_server-client.ts
191
- import { spawnSync } from "child_process";
192
194
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
193
195
  import { resolve as resolve2 } from "path";
194
196
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
195
- var cachedGitHubBearerToken;
197
+ var scopedGitHubBearerTokens = new Map;
196
198
  function cleanToken(value) {
197
199
  const trimmed = value?.trim();
198
200
  return trimmed ? trimmed : null;
@@ -209,25 +211,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
209
211
  }
210
212
  }
211
213
  function readGitHubBearerTokenForRemote(projectRoot) {
212
- if (cachedGitHubBearerToken !== undefined)
213
- return cachedGitHubBearerToken;
214
+ const scopedKey = resolve2(projectRoot);
215
+ if (scopedGitHubBearerTokens.has(scopedKey))
216
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
214
217
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
215
- if (privateSession) {
216
- cachedGitHubBearerToken = privateSession;
217
- return cachedGitHubBearerToken;
218
- }
219
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
220
- if (envToken) {
221
- cachedGitHubBearerToken = envToken;
222
- return cachedGitHubBearerToken;
223
- }
224
- const result = spawnSync("gh", ["auth", "token"], {
225
- encoding: "utf8",
226
- timeout: 5000,
227
- stdio: ["ignore", "pipe", "ignore"]
228
- });
229
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
230
- return cachedGitHubBearerToken;
218
+ if (privateSession)
219
+ return privateSession;
220
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
231
221
  }
232
222
  async function ensureServerForCli(projectRoot) {
233
223
  try {
@@ -347,6 +337,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
347
337
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
348
338
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
349
339
  }
340
+ async function getRunTimelineViaServer(context, runId, options = {}) {
341
+ const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
342
+ if (options.limit !== undefined)
343
+ url.searchParams.set("limit", String(options.limit));
344
+ if (options.cursor)
345
+ url.searchParams.set("cursor", options.cursor);
346
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
347
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
348
+ }
350
349
  async function stopRunViaServer(context, runId) {
351
350
  const payload = await requestServerJson(context, "/api/runs/stop", {
352
351
  method: "POST",
@@ -399,7 +398,8 @@ async function submitTaskRunViaServer(context, input) {
399
398
  import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
400
399
  import { homedir as homedir2 } from "os";
401
400
  import { resolve as resolve3 } from "path";
402
- var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
401
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
402
+ var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
403
403
  async function defaultCommandRunner(command, options = {}) {
404
404
  const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
405
405
  const [stdout, stderr, exitCode] = await Promise.all([
@@ -412,13 +412,19 @@ async function defaultCommandRunner(command, options = {}) {
412
412
  function resolvePiRigExtensionPath(homeDir) {
413
413
  return resolve3(homeDir, ".pi", "agent", "extensions", "pi-rig");
414
414
  }
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
+ }
415
421
  function resolvePiHomeDir(inputHomeDir) {
416
422
  return inputHomeDir ?? process.env.RIG_PI_HOME_DIR?.trim() ?? homedir2();
417
423
  }
418
424
  function piListContainsPiRig(output) {
419
425
  return output.split(/\r?\n/).some((line) => {
420
426
  const normalized = line.trim();
421
- return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
427
+ return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
422
428
  });
423
429
  }
424
430
  async function safeRun(runner, command, options) {
@@ -522,6 +528,9 @@ function permissionAllowsPr(payload) {
522
528
  }
523
529
  return null;
524
530
  }
531
+ function isNotFoundError(error) {
532
+ return /\b(404|not found)\b/i.test(message(error));
533
+ }
525
534
  function projectCheckoutReady(payload) {
526
535
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
527
536
  return null;
@@ -554,19 +563,33 @@ async function runFastTaskRunPreflight(context, options = {}) {
554
563
  const checks = [];
555
564
  const request = options.requestJson ?? ((pathname, init) => requestServerJson(context, pathname, init));
556
565
  const taskId = options.taskId?.trim() || null;
566
+ const requiresCurrentRunApi = Boolean(taskId);
567
+ const selectedServer = options.requestJson ? null : await ensureServerForCli(context.projectRoot).catch(() => null);
568
+ const allowLocalLegacyTaskRunCompatibility = selectedServer?.connectionKind === "local";
569
+ let legacyServerCompatibility = false;
557
570
  try {
558
571
  await request("/api/server/status");
559
572
  checks.push(preflightCheck("server", "Rig server reachable", "pass"));
560
573
  } catch (error) {
561
- checks.push(preflightCheck("server", "Rig server reachable", "fail", message(error), "Start or select a reachable Rig server."));
574
+ if (isNotFoundError(error)) {
575
+ try {
576
+ await request("/health");
577
+ legacyServerCompatibility = !requiresCurrentRunApi || allowLocalLegacyTaskRunCompatibility;
578
+ checks.push(requiresCurrentRunApi && !allowLocalLegacyTaskRunCompatibility ? preflightCheck("server", "Rig server reachable", "fail", "legacy /health endpoint only; current task-run APIs are required", "Upgrade/select the Rig server before launching a task run.") : preflightCheck("server", "Rig server reachable", "pass", allowLocalLegacyTaskRunCompatibility ? "local legacy /health endpoint; submit endpoint will be authoritative" : "legacy /health endpoint"));
579
+ } catch (healthError) {
580
+ checks.push(preflightCheck("server", "Rig server reachable", "fail", message(healthError), "Start or select a reachable Rig server."));
581
+ }
582
+ } else {
583
+ checks.push(preflightCheck("server", "Rig server reachable", "fail", message(error), "Start or select a reachable Rig server."));
584
+ }
562
585
  }
563
586
  const repo = readRepoConnection(context.projectRoot);
564
- checks.push(repo ? preflightCheck("project-link", "project linked to Rig connection", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to record the GitHub repo slug.") : preflightCheck("project-link", "project linked to Rig connection", "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig connect use <alias|local>`."));
587
+ checks.push(repo ? preflightCheck("project-link", "project linked to Rig connection", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to record the GitHub repo slug.") : preflightCheck("project-link", "project linked to Rig connection", legacyServerCompatibility ? "warn" : "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig connect use <alias|local>`."));
565
588
  try {
566
589
  const auth = await request("/api/github/auth/status");
567
- checks.push(isAuthenticated(auth) ? preflightCheck("github-auth", "GitHub auth valid", "pass") : preflightCheck("github-auth", "GitHub auth valid", "fail", "not authenticated", "Run `rig github auth import-gh` or `rig github auth token --token <token>`."));
590
+ checks.push(isAuthenticated(auth) ? preflightCheck("github-auth", "GitHub auth valid", "pass") : preflightCheck("github-auth", "GitHub auth valid", legacyServerCompatibility ? "warn" : "fail", "not authenticated", "Run `rig github auth import-gh` or `rig github auth token --token <token>`."));
568
591
  } catch (error) {
569
- checks.push(preflightCheck("github-auth", "GitHub auth valid", "fail", message(error), "Fix GitHub auth on the selected Rig server."));
592
+ checks.push(preflightCheck("github-auth", "GitHub auth valid", legacyServerCompatibility ? "warn" : "fail", message(error), "Fix GitHub auth on the selected Rig server."));
570
593
  }
571
594
  try {
572
595
  const projection = await request("/api/workspace/task-projection");
@@ -594,9 +617,9 @@ async function runFastTaskRunPreflight(context, options = {}) {
594
617
  try {
595
618
  const tasks = await request(`/api/workspace/tasks?limit=200&refresh=1`);
596
619
  const found = Array.isArray(tasks) && tasks.some((task) => taskMatchesId(task, taskId));
597
- checks.push(found ? preflightCheck("issue", "task/issue accessible", "pass", taskId) : preflightCheck("issue", "task/issue accessible", "fail", taskId, "Confirm the issue exists and matches the configured task filters."));
620
+ checks.push(found ? preflightCheck("issue", "task/issue accessible", "pass", taskId) : preflightCheck("issue", "task/issue accessible", legacyServerCompatibility ? "warn" : "fail", taskId, "Confirm the issue exists and matches the configured task filters."));
598
621
  } catch (error) {
599
- checks.push(preflightCheck("issue", "task/issue accessible", "fail", message(error), "Fix the task source before launching a run."));
622
+ checks.push(preflightCheck("issue", "task/issue accessible", legacyServerCompatibility ? "warn" : "fail", message(error), "Fix the task source before launching a run."));
600
623
  }
601
624
  try {
602
625
  const runs = await request("/api/runs?limit=200");
@@ -706,7 +729,184 @@ function withMutedConsole(mute, fn) {
706
729
  }
707
730
 
708
731
  // packages/cli/src/commands/_task-picker.ts
709
- import { createInterface } from "readline/promises";
732
+ import { cancel, isCancel, select } from "@clack/prompts";
733
+
734
+ // packages/cli/src/commands/_operator-surface.ts
735
+ import { createInterface } from "readline";
736
+ import { createInterface as createPromptInterface } from "readline/promises";
737
+ var CANONICAL_STAGES = [
738
+ "Connect",
739
+ "GitHub/task sync",
740
+ "Prepare workspace",
741
+ "Launch Pi",
742
+ "Plan",
743
+ "Implement",
744
+ "Validate",
745
+ "Commit",
746
+ "Open PR",
747
+ "Review/CI",
748
+ "Merge",
749
+ "Complete"
750
+ ];
751
+ function logDetail(log) {
752
+ return typeof log.detail === "string" ? log.detail.trim() : "";
753
+ }
754
+ function parseProviderProtocolLog(title, detail) {
755
+ if (title.trim().toLowerCase() !== "agent output")
756
+ return null;
757
+ if (!detail.startsWith("{") || !detail.endsWith("}"))
758
+ return null;
759
+ try {
760
+ const record = JSON.parse(detail);
761
+ if (!record || typeof record !== "object" || Array.isArray(record))
762
+ return null;
763
+ const type = record.type;
764
+ return typeof type === "string" && [
765
+ "assistant",
766
+ "message_start",
767
+ "message_update",
768
+ "message_end",
769
+ "stream_event",
770
+ "tool_result",
771
+ "tool_execution_start",
772
+ "tool_execution_update",
773
+ "tool_execution_end",
774
+ "turn_start",
775
+ "turn_end"
776
+ ].includes(type) ? record : null;
777
+ } catch {
778
+ return null;
779
+ }
780
+ }
781
+ function renderProviderProtocolLog(record) {
782
+ const type = typeof record.type === "string" ? record.type : "";
783
+ if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
784
+ const toolName = String(record.toolName ?? record.name ?? "tool");
785
+ const status = type === "tool_execution_start" ? "started" : type === "tool_execution_end" ? record.isError === true || record.result && typeof record.result === "object" && !Array.isArray(record.result) && record.result.isError === true ? "failed" : "completed" : "running";
786
+ return `[Pi tool] ${toolName} ${status}`;
787
+ }
788
+ return null;
789
+ }
790
+ function entryId(entry, fallback) {
791
+ return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
792
+ }
793
+ function renderOperatorSnapshot(snapshot) {
794
+ const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
795
+ const runId = String(run.runId ?? run.id ?? "run");
796
+ const status = String(run.status ?? "unknown");
797
+ const logs = snapshot.logs ?? [];
798
+ const latestByStage = new Map;
799
+ for (const log of logs) {
800
+ const title = String(log.title ?? "").toLowerCase();
801
+ const stageName = String(log.stage ?? "").toLowerCase();
802
+ const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
803
+ if (stage)
804
+ latestByStage.set(stage, log);
805
+ }
806
+ const stageLines = CANONICAL_STAGES.flatMap((stage) => {
807
+ const match = latestByStage.get(stage);
808
+ return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
809
+ });
810
+ return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
811
+ `);
812
+ }
813
+ function createPiRunStreamRenderer(output = process.stdout) {
814
+ let lastSnapshot = "";
815
+ const assistantTextById = new Map;
816
+ const seenTimeline = new Set;
817
+ const seenLogs = new Set;
818
+ const writeLine = (line) => output.write(`${line}
819
+ `);
820
+ return {
821
+ renderSnapshot(snapshot) {
822
+ const rendered = renderOperatorSnapshot(snapshot);
823
+ if (rendered && rendered !== lastSnapshot) {
824
+ writeLine(rendered);
825
+ lastSnapshot = rendered;
826
+ }
827
+ },
828
+ renderTimeline(entries) {
829
+ for (const [index, entry] of entries.entries()) {
830
+ const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
831
+ if (entry.type === "assistant_message" && typeof entry.text === "string") {
832
+ const text = entry.text;
833
+ const previousText = assistantTextById.get(id) ?? "";
834
+ if (!previousText && text.trim()) {
835
+ writeLine("[Pi assistant]");
836
+ }
837
+ if (text.startsWith(previousText)) {
838
+ const delta = text.slice(previousText.length);
839
+ if (delta)
840
+ output.write(delta);
841
+ } else if (text.trim() && text !== previousText) {
842
+ if (previousText)
843
+ writeLine(`
844
+ [Pi assistant]`);
845
+ output.write(text);
846
+ }
847
+ assistantTextById.set(id, text);
848
+ continue;
849
+ }
850
+ if (seenTimeline.has(id))
851
+ continue;
852
+ seenTimeline.add(id);
853
+ if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
854
+ writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
855
+ continue;
856
+ }
857
+ if (entry.type === "timeline_warning") {
858
+ writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
859
+ }
860
+ }
861
+ },
862
+ renderLogs(entries) {
863
+ for (const [index, entry] of entries.entries()) {
864
+ const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
865
+ if (seenLogs.has(id))
866
+ continue;
867
+ seenLogs.add(id);
868
+ const title = String(entry.title ?? "");
869
+ if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
870
+ continue;
871
+ const detail = logDetail(entry);
872
+ if (!detail)
873
+ continue;
874
+ const protocolRecord = parseProviderProtocolLog(title, detail);
875
+ if (protocolRecord) {
876
+ const protocolLine = renderProviderProtocolLog(protocolRecord);
877
+ if (protocolLine)
878
+ writeLine(protocolLine);
879
+ continue;
880
+ }
881
+ writeLine(`[${title || "Rig log"}] ${detail}`);
882
+ }
883
+ }
884
+ };
885
+ }
886
+ function createOperatorSurface(options = {}) {
887
+ const input = options.input ?? process.stdin;
888
+ const output = options.output ?? process.stdout;
889
+ const errorOutput = options.errorOutput ?? process.stderr;
890
+ const renderer = createPiRunStreamRenderer(output);
891
+ const writeLine = (line) => output.write(`${line}
892
+ `);
893
+ return {
894
+ mode: "pi-compatible-text",
895
+ ...renderer,
896
+ info: writeLine,
897
+ error: (message2) => errorOutput.write(`${message2}
898
+ `),
899
+ attachCommandInput(handler) {
900
+ if (options.interactive === false || !input.isTTY)
901
+ return null;
902
+ const rl = createInterface({ input, output: process.stdout, terminal: false });
903
+ rl.on("line", (line) => {
904
+ Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
905
+ });
906
+ return { close: () => rl.close() };
907
+ }
908
+ };
909
+ }
710
910
  function taskId(task) {
711
911
  return typeof task.id === "string" && task.id.trim() ? task.id : "<unknown>";
712
912
  }
@@ -719,6 +919,19 @@ function taskStatus(task) {
719
919
  function renderTaskPickerRows(tasks) {
720
920
  return tasks.map((task, index) => `${index + 1}. ${taskId(task)} \xB7 ${taskStatus(task)} \xB7 ${taskTitle(task)}`);
721
921
  }
922
+ async function promptForTaskSelection(question) {
923
+ const rl = createPromptInterface({ input: process.stdin, output: process.stdout });
924
+ try {
925
+ return await rl.question(question);
926
+ } finally {
927
+ rl.close();
928
+ }
929
+ }
930
+
931
+ // packages/cli/src/commands/_task-picker.ts
932
+ function taskId2(task) {
933
+ return typeof task.id === "string" && task.id.trim() ? task.id : "<unknown>";
934
+ }
722
935
  async function selectTaskWithTextPicker(tasks, io = {}) {
723
936
  if (tasks.length === 0)
724
937
  return null;
@@ -728,56 +941,41 @@ async function selectTaskWithTextPicker(tasks, io = {}) {
728
941
  if (!isTty) {
729
942
  throw new Error("task run requires an interactive terminal to pick a task; pass --task <id>, --next, or --detach with a task id.");
730
943
  }
731
- const prompt = io.prompt ?? (async (question) => {
732
- const rl = createInterface({ input: process.stdin, output: process.stdout });
733
- try {
734
- return await rl.question(question);
735
- } finally {
736
- rl.close();
944
+ if (io.prompt || io.renderer) {
945
+ const prompt = io.prompt ?? promptForTaskSelection;
946
+ const renderer = io.renderer ?? { writeLine: (line) => process.stdout.write(`${line}
947
+ `) };
948
+ renderer.writeLine("Select Rig task:");
949
+ for (const row of renderTaskPickerRows(tasks))
950
+ renderer.writeLine(` ${row}`);
951
+ const answer2 = (await prompt(`Task [1-${tasks.length}] or id: `)).trim();
952
+ if (!answer2)
953
+ return null;
954
+ if (/^\d+$/.test(answer2)) {
955
+ const index2 = Number.parseInt(answer2, 10) - 1;
956
+ return tasks[index2] ?? null;
737
957
  }
958
+ return tasks.find((task) => taskId2(task) === answer2) ?? null;
959
+ }
960
+ const options = tasks.map((task, index2) => ({
961
+ value: `${index2}`,
962
+ label: `${taskId2(task)} \xB7 ${typeof task.title === "string" && task.title.trim() ? task.title.trim() : "Untitled task"}`,
963
+ hint: typeof task.status === "string" && task.status.trim() ? task.status.trim() : undefined
964
+ }));
965
+ const answer = await select({
966
+ message: "Select Rig task",
967
+ options
738
968
  });
739
- console.log("Select Rig task:");
740
- for (const row of renderTaskPickerRows(tasks))
741
- console.log(` ${row}`);
742
- const answer = (await prompt(`Task [1-${tasks.length}] or id: `)).trim();
743
- if (!answer)
969
+ if (isCancel(answer)) {
970
+ cancel("No task selected.");
744
971
  return null;
745
- if (/^\d+$/.test(answer)) {
746
- const index = Number.parseInt(answer, 10) - 1;
747
- return tasks[index] ?? null;
748
972
  }
749
- return tasks.find((task) => taskId(task) === answer) ?? null;
973
+ const index = Number.parseInt(String(answer), 10);
974
+ return Number.isFinite(index) ? tasks[index] ?? null : null;
750
975
  }
751
976
 
752
977
  // packages/cli/src/commands/_operator-view.ts
753
- import { createInterface as createInterface2 } from "readline";
754
978
  var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
755
- var CANONICAL_STAGES = [
756
- "Connect",
757
- "GitHub/task sync",
758
- "Prepare workspace",
759
- "Launch Pi",
760
- "Plan",
761
- "Implement",
762
- "Validate",
763
- "Commit",
764
- "Open PR",
765
- "Review/CI",
766
- "Merge",
767
- "Complete"
768
- ];
769
- function renderOperatorSnapshot(snapshot) {
770
- const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
771
- const runId = String(run.runId ?? run.id ?? "run");
772
- const status = String(run.status ?? "unknown");
773
- const logs = snapshot.logs ?? [];
774
- const stageLines = CANONICAL_STAGES.flatMap((stage) => {
775
- const match = logs.find((log) => String(log.title ?? "").toLowerCase() === stage.toLowerCase() || String(log.stage ?? "").toLowerCase() === stage.toLowerCase());
776
- return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
777
- });
778
- return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
779
- `);
780
- }
781
979
  function runStatusFromPayload(payload) {
782
980
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
783
981
  return String(run.status ?? "unknown").toLowerCase();
@@ -799,11 +997,22 @@ async function applyOperatorCommand(context, input, deps = {}) {
799
997
  await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
800
998
  return { action: "continue", message: "Steering message queued." };
801
999
  }
802
- async function readOperatorSnapshot(context, runId) {
1000
+ async function readOperatorSnapshot(context, runId, options = {}) {
803
1001
  const run = await getRunDetailsViaServer(context, runId);
804
1002
  const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
805
- const entries = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
806
- return { run, logs: entries, rendered: renderOperatorSnapshot({ run, logs: entries }) };
1003
+ const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
1004
+ entries: [{
1005
+ id: `timeline-unavailable:${runId}`,
1006
+ type: "timeline_warning",
1007
+ detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
1008
+ createdAt: new Date().toISOString()
1009
+ }],
1010
+ nextCursor: options.timelineCursor ?? null
1011
+ }));
1012
+ const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
1013
+ const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1014
+ const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
1015
+ return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
807
1016
  }
808
1017
  async function attachRunOperatorView(context, input) {
809
1018
  let steered = false;
@@ -811,44 +1020,209 @@ async function attachRunOperatorView(context, input) {
811
1020
  await steerRunViaServer(context, input.runId, input.message.trim());
812
1021
  steered = true;
813
1022
  }
1023
+ const surface = createOperatorSurface({ interactive: input.interactive !== false });
814
1024
  let snapshot = await readOperatorSnapshot(context, input.runId);
815
1025
  if (context.outputMode === "text") {
816
- console.log(snapshot.rendered);
1026
+ surface.renderSnapshot(snapshot);
1027
+ surface.renderTimeline(snapshot.timeline);
1028
+ surface.renderLogs(snapshot.logs);
817
1029
  if (steered)
818
- console.log("Steering message queued.");
1030
+ surface.info("Steering message queued.");
819
1031
  }
820
1032
  let detached = false;
821
- let rl = null;
1033
+ let commandInput = null;
822
1034
  if (input.follow && !input.once && context.outputMode === "text") {
823
1035
  if (input.interactive !== false && process.stdin.isTTY) {
824
- console.log("Controls: /user <message>, /stop, /detach");
825
- rl = createInterface2({ input: process.stdin, output: process.stdout, terminal: false });
826
- rl.on("line", (line) => {
827
- applyOperatorCommand(context, { runId: input.runId, line }).then((result) => {
828
- if (result.message)
829
- console.log(result.message);
830
- if (result.action === "detach" || result.action === "stopped") {
831
- detached = true;
832
- rl?.close();
833
- }
834
- }).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
1036
+ surface.info("Controls: /user <message>, /stop, /detach");
1037
+ commandInput = surface.attachCommandInput(async (line) => {
1038
+ const result = await applyOperatorCommand(context, { runId: input.runId, line });
1039
+ if (result.message)
1040
+ surface.info(result.message);
1041
+ if (result.action === "detach" || result.action === "stopped") {
1042
+ detached = true;
1043
+ commandInput?.close();
1044
+ }
835
1045
  });
836
1046
  }
837
- let lastRendered = snapshot.rendered;
838
1047
  const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
1048
+ let timelineCursor = snapshot.timelineCursor;
839
1049
  while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
840
1050
  await Bun.sleep(pollMs);
841
- snapshot = await readOperatorSnapshot(context, input.runId);
842
- if (snapshot.rendered !== lastRendered) {
843
- console.log(snapshot.rendered);
844
- lastRendered = snapshot.rendered;
845
- }
1051
+ snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
1052
+ timelineCursor = snapshot.timelineCursor;
1053
+ surface.renderSnapshot(snapshot);
1054
+ surface.renderTimeline(snapshot.timeline);
1055
+ surface.renderLogs(snapshot.logs);
846
1056
  }
847
- rl?.close();
1057
+ commandInput?.close();
848
1058
  }
849
1059
  return { ...snapshot, steered, detached };
850
1060
  }
851
1061
 
1062
+ // packages/cli/src/commands/_cli-format.ts
1063
+ import pc from "picocolors";
1064
+ function stringField(record, key, fallback = "") {
1065
+ const value = record[key];
1066
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
1067
+ }
1068
+ function arrayField(record, key) {
1069
+ const value = record[key];
1070
+ return Array.isArray(value) ? value.flatMap((entry) => typeof entry === "string" && entry.trim() ? [entry.trim()] : []) : [];
1071
+ }
1072
+ function rawObject(record) {
1073
+ const raw = record.raw;
1074
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
1075
+ }
1076
+ function truncate(value, width) {
1077
+ if (value.length <= width)
1078
+ return value;
1079
+ if (width <= 1)
1080
+ return "\u2026";
1081
+ return `${value.slice(0, width - 1)}\u2026`;
1082
+ }
1083
+ function pad(value, width) {
1084
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
1085
+ }
1086
+ function statusColor(status) {
1087
+ const normalized = status.toLowerCase();
1088
+ if (["completed", "merged", "closed", "done", "accepted"].includes(normalized))
1089
+ return pc.green;
1090
+ if (["failed", "needs_attention", "needs-attention", "blocked"].includes(normalized))
1091
+ return pc.red;
1092
+ if (["running", "reviewing", "validating", "in_progress", "in-progress"].includes(normalized))
1093
+ return pc.cyan;
1094
+ if (["ready", "open", "queued", "created", "preparing"].includes(normalized))
1095
+ return pc.yellow;
1096
+ return pc.dim;
1097
+ }
1098
+ function formatTaskList(tasks, options = {}) {
1099
+ if (tasks.length === 0)
1100
+ return pc.dim("No matching tasks.");
1101
+ if (options.raw)
1102
+ return tasks.map((task) => JSON.stringify(task)).join(`
1103
+ `);
1104
+ const rows = tasks.map((task) => {
1105
+ const raw = rawObject(task);
1106
+ const id = stringField(task, "id", "<unknown>");
1107
+ const status = stringField(task, "status", "unknown");
1108
+ const title = stringField(task, "title", "Untitled task");
1109
+ const source = stringField(task, "source", stringField(raw, "source", ""));
1110
+ const labels = arrayField(task, "labels").length > 0 ? arrayField(task, "labels") : arrayField(raw, "labels");
1111
+ return { id, status, title, source, labels };
1112
+ });
1113
+ const idWidth = Math.min(18, Math.max(4, ...rows.map((row) => row.id.length)));
1114
+ const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
1115
+ const header = `${pc.bold(pad("TASK", idWidth))} ${pc.bold(pad("STATUS", statusWidth))} ${pc.bold("TITLE")}`;
1116
+ const body = rows.map((row) => {
1117
+ const labels = row.labels.length > 0 ? pc.dim(` ${row.labels.slice(0, 4).map((label) => `#${label}`).join(" ")}`) : "";
1118
+ const source = row.source ? pc.dim(` ${row.source}`) : "";
1119
+ return [
1120
+ pc.bold(pad(truncate(row.id, idWidth), idWidth)),
1121
+ statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
1122
+ `${row.title}${labels}${source}`
1123
+ ].join(" ");
1124
+ });
1125
+ return [pc.bold("Rig tasks"), header, ...body].join(`
1126
+ `);
1127
+ }
1128
+ function formatSubmittedRun(input) {
1129
+ const lines = [`${pc.green("Run submitted")}: ${pc.bold(input.runId)}`];
1130
+ if (input.task) {
1131
+ const id = stringField(input.task, "id", "<unknown>");
1132
+ const status = stringField(input.task, "status", "unknown");
1133
+ const title = stringField(input.task, "title", "Untitled task");
1134
+ lines.push(`${pc.dim("task")} ${pc.bold(id)} ${statusColor(status)(status)} ${title}`);
1135
+ }
1136
+ return lines.join(`
1137
+ `);
1138
+ }
1139
+
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
+
852
1226
  // packages/cli/src/commands/task.ts
853
1227
  import { buildPluginHostContext } from "@rig/runtime/control-plane/plugin-host-context";
854
1228
  import { loadConfig } from "@rig/core/load-config";
@@ -924,7 +1298,7 @@ function normalizePrMode(value) {
924
1298
  throw new CliError2("--pr must be auto, ask, or off.", 2);
925
1299
  }
926
1300
  function detectLocalDirtyState(projectRoot) {
927
- const result = spawnSync2("git", ["-C", projectRoot, "status", "--porcelain"], { encoding: "utf8", timeout: 5000 });
1301
+ const result = spawnSync("git", ["-C", projectRoot, "status", "--porcelain"], { encoding: "utf8", timeout: 5000 });
928
1302
  if (result.status !== 0)
929
1303
  return { dirty: false, modified: 0, untracked: 0, lines: [] };
930
1304
  const lines = result.stdout.split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean);
@@ -958,13 +1332,15 @@ async function resolveDirtyBaselineForTaskRun(context, explicit) {
958
1332
  if (explicit)
959
1333
  return { mode: explicit, state };
960
1334
  if (context.outputMode === "text" && process.stdin.isTTY && process.stdout.isTTY) {
961
- const rl = createInterface3({ input: process.stdin, output: process.stdout });
962
- try {
963
- const answer = (await rl.question("Include current uncommitted changes in run baseline? [y/N] ")).trim().toLowerCase();
964
- return { mode: answer === "y" || answer === "yes" ? "dirty-snapshot" : "head", state };
965
- } finally {
966
- rl.close();
1335
+ const answer = await confirm({
1336
+ message: "Include current uncommitted changes in run baseline?",
1337
+ initialValue: false
1338
+ });
1339
+ if (isCancel2(answer)) {
1340
+ cancel2("Run cancelled.");
1341
+ throw new CliError2("Run cancelled by user.", 1);
967
1342
  }
1343
+ return { mode: answer ? "dirty-snapshot" : "head", state };
968
1344
  }
969
1345
  return { mode: "head", state };
970
1346
  }
@@ -999,10 +1375,7 @@ function summarizeTask(task, options = {}) {
999
1375
  };
1000
1376
  }
1001
1377
  function printTaskSummary(task) {
1002
- const id = readTaskId(task) ?? "<unknown>";
1003
- const title = readTaskString(task, "title") ?? "Untitled task";
1004
- const status = readTaskString(task, "status") ?? "unknown";
1005
- console.log(`- ${id} \xB7 ${status} \xB7 ${title}`);
1378
+ console.log(formatTaskList([task]));
1006
1379
  }
1007
1380
  async function validatorRegistryForTaskCommands(projectRoot) {
1008
1381
  return buildPluginHostContext(projectRoot).then((ctx) => ctx?.validatorRegistry ?? undefined).catch(() => {
@@ -1020,16 +1393,8 @@ async function executeTask(context, args, options) {
1020
1393
  requireNoExtraArgs(remaining, "bun run rig task list [--raw] [--assignee <login|@me>] [--assigned-to <login|me|@me>] [--state open|closed] [--status <status>] [--limit <n>]");
1021
1394
  const tasks = await listWorkspaceTasksViaServer(context, filters);
1022
1395
  if (context.outputMode === "text") {
1023
- if (tasks.length === 0) {
1024
- console.log("No matching tasks.");
1025
- } else {
1026
- for (const task of tasks) {
1027
- if (rawResult.value)
1028
- console.log(JSON.stringify(summarizeTask(task, { raw: true })));
1029
- else
1030
- printTaskSummary(task);
1031
- }
1032
- }
1396
+ const renderedTasks = rawResult.value ? tasks.map((task) => summarizeTask(task, { raw: true })) : tasks.map((task) => summarizeTask(task));
1397
+ console.log(formatTaskList(renderedTasks, { raw: rawResult.value }));
1033
1398
  }
1034
1399
  return {
1035
1400
  ok: true,
@@ -1043,12 +1408,12 @@ async function executeTask(context, args, options) {
1043
1408
  const positional = taskOption.rest.length > 0 && taskOption.rest[0] && !taskOption.rest[0].startsWith("-") ? taskOption.rest[0] : undefined;
1044
1409
  const remaining = positional ? taskOption.rest.slice(1) : taskOption.rest;
1045
1410
  requireNoExtraArgs(remaining, "bun run rig task show <id>|--task <id>");
1046
- const taskId2 = normalizeTaskRunTaskId(taskOption.value ?? positional);
1047
- if (!taskId2)
1411
+ const taskId3 = normalizeTaskRunTaskId(taskOption.value ?? positional);
1412
+ if (!taskId3)
1048
1413
  throw new CliError2("task show requires a task id.", 2);
1049
- const task = await getWorkspaceTaskViaServer(context, taskId2);
1414
+ const task = await getWorkspaceTaskViaServer(context, taskId3);
1050
1415
  if (!task)
1051
- throw new CliError2(`Task not found: ${taskId2}`, 3);
1416
+ throw new CliError2(`Task not found: ${taskId3}`, 3);
1052
1417
  const summary = summarizeTask(task, { raw: true });
1053
1418
  if (context.outputMode === "text")
1054
1419
  console.log(JSON.stringify(summary, null, 2));
@@ -1248,16 +1613,23 @@ async function executeTask(context, args, options) {
1248
1613
  });
1249
1614
  let attachDetails = null;
1250
1615
  if (!detachResult.value && context.outputMode === "text") {
1251
- console.log(`Run submitted: ${submitted.runId}`);
1252
- if (selectedTask) {
1253
- printTaskSummary(selectedTask);
1616
+ 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 });
1254
1630
  }
1255
- attachDetails = await attachRunOperatorView(context, { runId: submitted.runId, follow: true });
1256
1631
  } else if (context.outputMode === "text") {
1257
- console.log(`Run submitted: ${submitted.runId}`);
1258
- if (selectedTask) {
1259
- printTaskSummary(selectedTask);
1260
- }
1632
+ console.log(formatSubmittedRun({ runId: submitted.runId, task: selectedTask ? summarizeTask(selectedTask) : null }));
1261
1633
  }
1262
1634
  return {
1263
1635
  ok: true,