@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.
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // packages/cli/src/commands/_server-client.ts
3
5
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
4
6
  import { resolve as resolve2 } from "path";
@@ -406,6 +408,50 @@ function createOperatorSurface(options = {}) {
406
408
 
407
409
  // packages/cli/src/commands/_operator-view.ts
408
410
  var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
411
+ var CANONICAL_STAGES2 = [
412
+ "Connect",
413
+ "GitHub/task sync",
414
+ "Prepare workspace",
415
+ "Launch Pi",
416
+ "Plan",
417
+ "Implement",
418
+ "Validate",
419
+ "Commit",
420
+ "Open PR",
421
+ "Review/CI",
422
+ "Merge",
423
+ "Complete"
424
+ ];
425
+ var GREEN = "\x1B[32m";
426
+ var BLUE = "\x1B[34m";
427
+ var MAGENTA = "\x1B[35m";
428
+ var YELLOW = "\x1B[33m";
429
+ var RED = "\x1B[31m";
430
+ var DIM = "\x1B[2m";
431
+ var BOLD = "\x1B[1m";
432
+ var RESET = "\x1B[0m";
433
+ async function loadPiTuiRuntime() {
434
+ try {
435
+ return await import("@earendil-works/pi-tui");
436
+ } catch {
437
+ const base = new URL("../../../pi/packages/tui/src/", import.meta.url);
438
+ const [tui, input, terminal, keys, utils] = await Promise.all([
439
+ import(new URL("tui.ts", base).href),
440
+ import(new URL("components/input.ts", base).href),
441
+ import(new URL("terminal.ts", base).href),
442
+ import(new URL("keys.ts", base).href),
443
+ import(new URL("utils.ts", base).href)
444
+ ]);
445
+ return {
446
+ Container: tui.Container,
447
+ TUI: tui.TUI,
448
+ Input: input.Input,
449
+ ProcessTerminal: terminal.ProcessTerminal,
450
+ matchesKey: keys.matchesKey,
451
+ truncateToWidth: utils.truncateToWidth
452
+ };
453
+ }
454
+ }
409
455
  function runStatusFromPayload(payload) {
410
456
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
411
457
  return String(run.status ?? "unknown").toLowerCase();
@@ -444,12 +490,201 @@ async function readOperatorSnapshot(context, runId, options = {}) {
444
490
  const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
445
491
  return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
446
492
  }
493
+ function unwrapRun(runPayload) {
494
+ return runPayload.run && typeof runPayload.run === "object" && !Array.isArray(runPayload.run) ? runPayload.run : runPayload;
495
+ }
496
+ function logDetail2(log) {
497
+ return typeof log.detail === "string" ? log.detail.trim() : "";
498
+ }
499
+ function logTitle(log) {
500
+ return typeof log.title === "string" ? log.title.trim() : "";
501
+ }
502
+ function renderAssistantTextFromTimeline(entries) {
503
+ const assistant = entries.filter((entry) => entry.type === "assistant_message" && typeof entry.text === "string").at(-1);
504
+ const text = typeof assistant?.text === "string" ? assistant.text.trimEnd() : "";
505
+ if (!text)
506
+ return [];
507
+ return [`${BLUE}${BOLD}Remote Pi assistant${RESET}`, ...text.split(/\r?\n/).slice(-18)];
508
+ }
509
+ function renderToolLines(entries) {
510
+ 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());
511
+ }
512
+ function renderStageLines(logs) {
513
+ const latestByStage = new Map;
514
+ for (const log of logs) {
515
+ const title = logTitle(log).toLowerCase();
516
+ const stageName = String(log.stage ?? "").toLowerCase();
517
+ const stage = CANONICAL_STAGES2.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
518
+ if (stage)
519
+ latestByStage.set(stage, log);
520
+ }
521
+ return CANONICAL_STAGES2.map((stage) => {
522
+ const log = latestByStage.get(stage);
523
+ const status = String(log?.status ?? "pending");
524
+ const detail = log ? logDetail2(log) : "";
525
+ const color = status === "completed" ? GREEN : status === "failed" || status === "rejected" ? RED : status === "pending" ? DIM : YELLOW;
526
+ const mark = status === "completed" ? "\u2713" : status === "pending" ? "\xB7" : status === "failed" ? "\u2717" : "\u25B6";
527
+ return `${color}${mark} ${stage}${RESET}${detail ? ` ${DIM}\u2014 ${detail.slice(0, 140)}${RESET}` : ""}`;
528
+ });
529
+ }
530
+ function renderEventLines(logs) {
531
+ return logs.filter((log) => !CANONICAL_STAGES2.some((stage) => stage.toLowerCase() === logTitle(log).toLowerCase())).slice(-12).flatMap((log) => {
532
+ const title = logTitle(log) || "Rig event";
533
+ const detail = logDetail2(log);
534
+ if (!detail)
535
+ return [];
536
+ const tone = String(log.tone ?? "");
537
+ const color = tone === "error" ? RED : tone === "tool" ? MAGENTA : DIM;
538
+ return [`${color}[${title}]${RESET} ${detail.slice(0, 220)}`];
539
+ });
540
+ }
541
+
542
+ class RigRunComponent {
543
+ truncateToWidth;
544
+ snapshot = { run: {}, logs: [], timeline: [] };
545
+ localEvents = [];
546
+ constructor(truncateToWidth) {
547
+ this.truncateToWidth = truncateToWidth;
548
+ }
549
+ update(snapshot) {
550
+ this.snapshot = snapshot;
551
+ }
552
+ addLocalEvent(message) {
553
+ this.localEvents.push(`${new Date().toLocaleTimeString()} ${message}`);
554
+ this.localEvents = this.localEvents.slice(-8);
555
+ }
556
+ invalidate() {}
557
+ render(width) {
558
+ const run = unwrapRun(this.snapshot.run);
559
+ const runId = String(run.runId ?? run.id ?? "run");
560
+ const status = String(run.status ?? "unknown");
561
+ 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";
562
+ const lines = [
563
+ `${BOLD}Rig Pi frontend${RESET} ${DIM}(local Pi TUI \u2192 Rig server \u2192 worker Pi backend)${RESET}`,
564
+ `${BOLD}${runId}${RESET} \xB7 ${status} \xB7 ${DIM}${worker}${RESET}`,
565
+ "",
566
+ `${BOLD}Rig flow${RESET}`,
567
+ ...renderStageLines(this.snapshot.logs ?? []),
568
+ "",
569
+ ...renderAssistantTextFromTimeline(this.snapshot.timeline ?? []),
570
+ ...renderToolLines(this.snapshot.timeline ?? []),
571
+ "",
572
+ `${BOLD}Rig / Pi events${RESET}`,
573
+ ...renderEventLines(this.snapshot.logs ?? []),
574
+ ...this.localEvents.map((event) => `${GREEN}[frontend]${RESET} ${event}`),
575
+ ""
576
+ ];
577
+ return lines.slice(-42).map((line) => this.truncateToWidth(line, Math.max(10, width)));
578
+ }
579
+ }
580
+
581
+ class RigInputComponent {
582
+ matchesKey;
583
+ truncateToWidth;
584
+ input;
585
+ status = "Type text, /skill:..., or remote Pi slash commands. Local: /stop /detach.";
586
+ constructor(InputCtor, matchesKey, truncateToWidth, onSubmit, onEscape) {
587
+ this.matchesKey = matchesKey;
588
+ this.truncateToWidth = truncateToWidth;
589
+ this.input = new InputCtor;
590
+ this.input.onSubmit = (value) => {
591
+ const text = value.trim();
592
+ this.input.setValue("");
593
+ if (text)
594
+ Promise.resolve(onSubmit(text));
595
+ };
596
+ this.input.onEscape = onEscape;
597
+ }
598
+ handleInput(data) {
599
+ if (this.matchesKey(data, "ctrl+d")) {
600
+ this.input.onEscape?.();
601
+ return;
602
+ }
603
+ this.input.handleInput?.(data);
604
+ }
605
+ setStatus(status) {
606
+ this.status = status;
607
+ }
608
+ invalidate() {
609
+ this.input.invalidate();
610
+ }
611
+ render(width) {
612
+ return [
613
+ `${DIM}${this.truncateToWidth(this.status, Math.max(10, width))}${RESET}`,
614
+ `${GREEN}${BOLD}You \u2192 worker Pi:${RESET}`,
615
+ ...this.input.render(width)
616
+ ];
617
+ }
618
+ }
619
+ async function attachRunPiTuiFrontend(context, input) {
620
+ const piTui = await loadPiTuiRuntime();
621
+ const terminal = new piTui.ProcessTerminal;
622
+ const tui = new piTui.TUI(terminal);
623
+ const root = new piTui.Container;
624
+ const runView = new RigRunComponent(piTui.truncateToWidth);
625
+ let detached = false;
626
+ let steered = input.steered === true;
627
+ let latest = await readOperatorSnapshot(context, input.runId);
628
+ let timelineCursor = latest.timelineCursor;
629
+ runView.update(latest);
630
+ if (steered)
631
+ runView.addLocalEvent("initial message queued to worker Pi.");
632
+ const stop = () => {
633
+ detached = true;
634
+ tui.stop();
635
+ };
636
+ const inputView = new RigInputComponent(piTui.Input, piTui.matchesKey, piTui.truncateToWidth, async (line) => {
637
+ if (line === "/detach" || line === "/quit" || line === "/q") {
638
+ runView.addLocalEvent("detached from run.");
639
+ stop();
640
+ return;
641
+ }
642
+ if (line === "/stop") {
643
+ await stopRunViaServer(context, input.runId);
644
+ runView.addLocalEvent("stop requested.");
645
+ stop();
646
+ return;
647
+ }
648
+ await steerRunViaServer(context, input.runId, line);
649
+ steered = true;
650
+ runView.addLocalEvent(`queued to worker Pi: ${line.slice(0, 160)}`);
651
+ tui.requestRender();
652
+ }, stop);
653
+ root.addChild(runView);
654
+ root.addChild(inputView);
655
+ tui.addChild(root);
656
+ tui.setFocus(inputView.input);
657
+ tui.start();
658
+ tui.requestRender(true);
659
+ const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 1000));
660
+ try {
661
+ while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(latest.run))) {
662
+ await Bun.sleep(pollMs);
663
+ latest = await readOperatorSnapshot(context, input.runId, { timelineCursor });
664
+ timelineCursor = latest.timelineCursor;
665
+ runView.update(latest);
666
+ inputView.setStatus(`Remote worker ${runStatusFromPayload(latest.run)}. Input is forwarded to worker Pi; /stop /detach are local controls.`);
667
+ tui.requestRender();
668
+ }
669
+ } finally {
670
+ if (!detached)
671
+ tui.stop();
672
+ }
673
+ return { ...latest, timelineCursor, steered, detached, rendered: renderOperatorSnapshot(latest) };
674
+ }
447
675
  async function attachRunOperatorView(context, input) {
448
676
  let steered = false;
449
677
  if (input.message?.trim()) {
450
678
  await steerRunViaServer(context, input.runId, input.message.trim());
451
679
  steered = true;
452
680
  }
681
+ if (input.follow && !input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
682
+ return attachRunPiTuiFrontend(context, {
683
+ runId: input.runId,
684
+ pollMs: input.pollMs,
685
+ steered
686
+ });
687
+ }
453
688
  const surface = createOperatorSurface({ interactive: input.interactive !== false });
454
689
  let snapshot = await readOperatorSnapshot(context, input.runId);
455
690
  if (context.outputMode === "text") {
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // packages/cli/src/commands/run.ts
3
5
  import { createInterface as createInterface2 } from "readline/promises";
4
6
 
@@ -10,9 +12,6 @@ import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
10
12
  import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
11
13
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
12
14
  import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
13
- function formatCommand(parts) {
14
- return parts.map((part) => /[^a-zA-Z0-9_./:-]/.test(part) ? JSON.stringify(part) : part).join(" ");
15
- }
16
15
  function takeFlag(args, flag) {
17
16
  const rest = [];
18
17
  let value = false;
@@ -487,6 +486,50 @@ function createOperatorSurface(options = {}) {
487
486
 
488
487
  // packages/cli/src/commands/_operator-view.ts
489
488
  var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
489
+ var CANONICAL_STAGES2 = [
490
+ "Connect",
491
+ "GitHub/task sync",
492
+ "Prepare workspace",
493
+ "Launch Pi",
494
+ "Plan",
495
+ "Implement",
496
+ "Validate",
497
+ "Commit",
498
+ "Open PR",
499
+ "Review/CI",
500
+ "Merge",
501
+ "Complete"
502
+ ];
503
+ var GREEN = "\x1B[32m";
504
+ var BLUE = "\x1B[34m";
505
+ var MAGENTA = "\x1B[35m";
506
+ var YELLOW = "\x1B[33m";
507
+ var RED = "\x1B[31m";
508
+ var DIM = "\x1B[2m";
509
+ var BOLD = "\x1B[1m";
510
+ var RESET = "\x1B[0m";
511
+ async function loadPiTuiRuntime() {
512
+ try {
513
+ return await import("@earendil-works/pi-tui");
514
+ } catch {
515
+ const base = new URL("../../../pi/packages/tui/src/", import.meta.url);
516
+ const [tui, input, terminal, keys, utils] = await Promise.all([
517
+ import(new URL("tui.ts", base).href),
518
+ import(new URL("components/input.ts", base).href),
519
+ import(new URL("terminal.ts", base).href),
520
+ import(new URL("keys.ts", base).href),
521
+ import(new URL("utils.ts", base).href)
522
+ ]);
523
+ return {
524
+ Container: tui.Container,
525
+ TUI: tui.TUI,
526
+ Input: input.Input,
527
+ ProcessTerminal: terminal.ProcessTerminal,
528
+ matchesKey: keys.matchesKey,
529
+ truncateToWidth: utils.truncateToWidth
530
+ };
531
+ }
532
+ }
490
533
  function runStatusFromPayload(payload) {
491
534
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
492
535
  return String(run.status ?? "unknown").toLowerCase();
@@ -525,12 +568,201 @@ async function readOperatorSnapshot(context, runId, options = {}) {
525
568
  const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
526
569
  return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
527
570
  }
571
+ function unwrapRun(runPayload) {
572
+ return runPayload.run && typeof runPayload.run === "object" && !Array.isArray(runPayload.run) ? runPayload.run : runPayload;
573
+ }
574
+ function logDetail2(log) {
575
+ return typeof log.detail === "string" ? log.detail.trim() : "";
576
+ }
577
+ function logTitle(log) {
578
+ return typeof log.title === "string" ? log.title.trim() : "";
579
+ }
580
+ function renderAssistantTextFromTimeline(entries) {
581
+ const assistant = entries.filter((entry) => entry.type === "assistant_message" && typeof entry.text === "string").at(-1);
582
+ const text = typeof assistant?.text === "string" ? assistant.text.trimEnd() : "";
583
+ if (!text)
584
+ return [];
585
+ return [`${BLUE}${BOLD}Remote Pi assistant${RESET}`, ...text.split(/\r?\n/).slice(-18)];
586
+ }
587
+ function renderToolLines(entries) {
588
+ 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());
589
+ }
590
+ function renderStageLines(logs) {
591
+ const latestByStage = new Map;
592
+ for (const log of logs) {
593
+ const title = logTitle(log).toLowerCase();
594
+ const stageName = String(log.stage ?? "").toLowerCase();
595
+ const stage = CANONICAL_STAGES2.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
596
+ if (stage)
597
+ latestByStage.set(stage, log);
598
+ }
599
+ return CANONICAL_STAGES2.map((stage) => {
600
+ const log = latestByStage.get(stage);
601
+ const status = String(log?.status ?? "pending");
602
+ const detail = log ? logDetail2(log) : "";
603
+ const color = status === "completed" ? GREEN : status === "failed" || status === "rejected" ? RED : status === "pending" ? DIM : YELLOW;
604
+ const mark = status === "completed" ? "\u2713" : status === "pending" ? "\xB7" : status === "failed" ? "\u2717" : "\u25B6";
605
+ return `${color}${mark} ${stage}${RESET}${detail ? ` ${DIM}\u2014 ${detail.slice(0, 140)}${RESET}` : ""}`;
606
+ });
607
+ }
608
+ function renderEventLines(logs) {
609
+ return logs.filter((log) => !CANONICAL_STAGES2.some((stage) => stage.toLowerCase() === logTitle(log).toLowerCase())).slice(-12).flatMap((log) => {
610
+ const title = logTitle(log) || "Rig event";
611
+ const detail = logDetail2(log);
612
+ if (!detail)
613
+ return [];
614
+ const tone = String(log.tone ?? "");
615
+ const color = tone === "error" ? RED : tone === "tool" ? MAGENTA : DIM;
616
+ return [`${color}[${title}]${RESET} ${detail.slice(0, 220)}`];
617
+ });
618
+ }
619
+
620
+ class RigRunComponent {
621
+ truncateToWidth;
622
+ snapshot = { run: {}, logs: [], timeline: [] };
623
+ localEvents = [];
624
+ constructor(truncateToWidth) {
625
+ this.truncateToWidth = truncateToWidth;
626
+ }
627
+ update(snapshot) {
628
+ this.snapshot = snapshot;
629
+ }
630
+ addLocalEvent(message) {
631
+ this.localEvents.push(`${new Date().toLocaleTimeString()} ${message}`);
632
+ this.localEvents = this.localEvents.slice(-8);
633
+ }
634
+ invalidate() {}
635
+ render(width) {
636
+ const run = unwrapRun(this.snapshot.run);
637
+ const runId = String(run.runId ?? run.id ?? "run");
638
+ const status = String(run.status ?? "unknown");
639
+ 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";
640
+ const lines = [
641
+ `${BOLD}Rig Pi frontend${RESET} ${DIM}(local Pi TUI \u2192 Rig server \u2192 worker Pi backend)${RESET}`,
642
+ `${BOLD}${runId}${RESET} \xB7 ${status} \xB7 ${DIM}${worker}${RESET}`,
643
+ "",
644
+ `${BOLD}Rig flow${RESET}`,
645
+ ...renderStageLines(this.snapshot.logs ?? []),
646
+ "",
647
+ ...renderAssistantTextFromTimeline(this.snapshot.timeline ?? []),
648
+ ...renderToolLines(this.snapshot.timeline ?? []),
649
+ "",
650
+ `${BOLD}Rig / Pi events${RESET}`,
651
+ ...renderEventLines(this.snapshot.logs ?? []),
652
+ ...this.localEvents.map((event) => `${GREEN}[frontend]${RESET} ${event}`),
653
+ ""
654
+ ];
655
+ return lines.slice(-42).map((line) => this.truncateToWidth(line, Math.max(10, width)));
656
+ }
657
+ }
658
+
659
+ class RigInputComponent {
660
+ matchesKey;
661
+ truncateToWidth;
662
+ input;
663
+ status = "Type text, /skill:..., or remote Pi slash commands. Local: /stop /detach.";
664
+ constructor(InputCtor, matchesKey, truncateToWidth, onSubmit, onEscape) {
665
+ this.matchesKey = matchesKey;
666
+ this.truncateToWidth = truncateToWidth;
667
+ this.input = new InputCtor;
668
+ this.input.onSubmit = (value) => {
669
+ const text = value.trim();
670
+ this.input.setValue("");
671
+ if (text)
672
+ Promise.resolve(onSubmit(text));
673
+ };
674
+ this.input.onEscape = onEscape;
675
+ }
676
+ handleInput(data) {
677
+ if (this.matchesKey(data, "ctrl+d")) {
678
+ this.input.onEscape?.();
679
+ return;
680
+ }
681
+ this.input.handleInput?.(data);
682
+ }
683
+ setStatus(status) {
684
+ this.status = status;
685
+ }
686
+ invalidate() {
687
+ this.input.invalidate();
688
+ }
689
+ render(width) {
690
+ return [
691
+ `${DIM}${this.truncateToWidth(this.status, Math.max(10, width))}${RESET}`,
692
+ `${GREEN}${BOLD}You \u2192 worker Pi:${RESET}`,
693
+ ...this.input.render(width)
694
+ ];
695
+ }
696
+ }
697
+ async function attachRunPiTuiFrontend(context, input) {
698
+ const piTui = await loadPiTuiRuntime();
699
+ const terminal = new piTui.ProcessTerminal;
700
+ const tui = new piTui.TUI(terminal);
701
+ const root = new piTui.Container;
702
+ const runView = new RigRunComponent(piTui.truncateToWidth);
703
+ let detached = false;
704
+ let steered = input.steered === true;
705
+ let latest = await readOperatorSnapshot(context, input.runId);
706
+ let timelineCursor = latest.timelineCursor;
707
+ runView.update(latest);
708
+ if (steered)
709
+ runView.addLocalEvent("initial message queued to worker Pi.");
710
+ const stop = () => {
711
+ detached = true;
712
+ tui.stop();
713
+ };
714
+ const inputView = new RigInputComponent(piTui.Input, piTui.matchesKey, piTui.truncateToWidth, async (line) => {
715
+ if (line === "/detach" || line === "/quit" || line === "/q") {
716
+ runView.addLocalEvent("detached from run.");
717
+ stop();
718
+ return;
719
+ }
720
+ if (line === "/stop") {
721
+ await stopRunViaServer(context, input.runId);
722
+ runView.addLocalEvent("stop requested.");
723
+ stop();
724
+ return;
725
+ }
726
+ await steerRunViaServer(context, input.runId, line);
727
+ steered = true;
728
+ runView.addLocalEvent(`queued to worker Pi: ${line.slice(0, 160)}`);
729
+ tui.requestRender();
730
+ }, stop);
731
+ root.addChild(runView);
732
+ root.addChild(inputView);
733
+ tui.addChild(root);
734
+ tui.setFocus(inputView.input);
735
+ tui.start();
736
+ tui.requestRender(true);
737
+ const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 1000));
738
+ try {
739
+ while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(latest.run))) {
740
+ await Bun.sleep(pollMs);
741
+ latest = await readOperatorSnapshot(context, input.runId, { timelineCursor });
742
+ timelineCursor = latest.timelineCursor;
743
+ runView.update(latest);
744
+ inputView.setStatus(`Remote worker ${runStatusFromPayload(latest.run)}. Input is forwarded to worker Pi; /stop /detach are local controls.`);
745
+ tui.requestRender();
746
+ }
747
+ } finally {
748
+ if (!detached)
749
+ tui.stop();
750
+ }
751
+ return { ...latest, timelineCursor, steered, detached, rendered: renderOperatorSnapshot(latest) };
752
+ }
528
753
  async function attachRunOperatorView(context, input) {
529
754
  let steered = false;
530
755
  if (input.message?.trim()) {
531
756
  await steerRunViaServer(context, input.runId, input.message.trim());
532
757
  steered = true;
533
758
  }
759
+ if (input.follow && !input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
760
+ return attachRunPiTuiFrontend(context, {
761
+ runId: input.runId,
762
+ pollMs: input.pollMs,
763
+ steered
764
+ });
765
+ }
534
766
  const surface = createOperatorSurface({ interactive: input.interactive !== false });
535
767
  let snapshot = await readOperatorSnapshot(context, input.runId);
536
768
  if (context.outputMode === "text") {
@@ -622,105 +854,6 @@ function formatRunList(runs, options = {}) {
622
854
  `);
623
855
  }
624
856
 
625
- // packages/cli/src/commands/_pi-session.ts
626
- import { spawn } from "child_process";
627
-
628
- // packages/cli/src/commands/_pi-install.ts
629
- import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
630
- import { resolve as resolve3 } from "path";
631
- var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
632
- function resolvePiRigPackageSource(projectRoot, exists = existsSync3) {
633
- const localPackage = resolve3(projectRoot, "packages", "pi-rig");
634
- if (exists(resolve3(localPackage, "package.json")))
635
- return localPackage;
636
- return `npm:${PI_RIG_PACKAGE_NAME}`;
637
- }
638
-
639
- // packages/cli/src/commands/_pi-session.ts
640
- function buildPiRigSessionEnv(input) {
641
- return {
642
- RIG_PROJECT_ROOT: input.projectRoot,
643
- PROJECT_RIG_ROOT: input.projectRoot,
644
- RIG_RUN_ID: input.runId,
645
- RIG_SERVER_RUN_ID: input.runId,
646
- RIG_RUNTIME_ADAPTER: "pi",
647
- RIG_SERVER_URL: input.serverUrl,
648
- RIG_SERVER_BASE_URL: input.serverUrl,
649
- RIG_STEERING_POLL_MS: process.env.RIG_STEERING_POLL_MS?.trim() || "1000",
650
- RIG_PI_OPERATOR_SESSION: "1",
651
- ...input.taskId ? { RIG_TASK_ID: input.taskId } : {},
652
- ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {}
653
- };
654
- }
655
- function shellBinary(name) {
656
- const explicit = process.env.RIG_PI_BINARY?.trim();
657
- if (explicit)
658
- return explicit;
659
- return Bun.which(name) || name;
660
- }
661
- function buildPiRigSessionCommand(input) {
662
- const configuredExtension = input.extensionSource ?? process.env.RIG_PI_RIG_EXTENSION_SOURCE?.trim();
663
- const extensionSource = configuredExtension && configuredExtension.length > 0 ? configuredExtension : resolvePiRigPackageSource(input.projectRoot);
664
- const initialCommand = `/rig attach ${input.runId}`;
665
- return [
666
- shellBinary("pi"),
667
- "--no-extensions",
668
- "--extension",
669
- extensionSource,
670
- initialCommand
671
- ];
672
- }
673
- async function launchPiRigSession(context, input) {
674
- if (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY) {
675
- return { launched: false, exitCode: null, command: [] };
676
- }
677
- if (process.env.RIG_DISABLE_PI_LAUNCH === "1") {
678
- return { launched: false, exitCode: null, command: [] };
679
- }
680
- const server = await ensureServerForCli(context.projectRoot);
681
- const command = buildPiRigSessionCommand({ ...input, projectRoot: context.projectRoot });
682
- const env = {
683
- ...process.env,
684
- ...buildPiRigSessionEnv({
685
- projectRoot: context.projectRoot,
686
- runId: input.runId,
687
- taskId: input.taskId,
688
- serverUrl: server.baseUrl,
689
- authToken: server.authToken
690
- })
691
- };
692
- process.stdout.write(`Launching Pi for Rig run ${input.runId}\u2026
693
- `);
694
- process.stdout.write(`Pi command: ${formatCommand(command)}
695
- `);
696
- const launchedAt = Date.now();
697
- const child = spawn(command[0], command.slice(1), {
698
- cwd: context.projectRoot,
699
- env,
700
- stdio: "inherit"
701
- });
702
- const launchError = await new Promise((resolve4) => {
703
- child.once("error", (error) => {
704
- resolve4({ error: error.message });
705
- });
706
- child.once("close", (code) => resolve4({ code }));
707
- });
708
- if ("error" in launchError) {
709
- process.stderr.write(`Failed to launch Pi; falling back to Rig attach view: ${launchError.error}
710
- `);
711
- return { launched: false, exitCode: null, command, error: launchError.error };
712
- }
713
- const exitCode = launchError.code;
714
- const elapsedMs = Date.now() - launchedAt;
715
- if (typeof exitCode === "number" && exitCode !== 0 && elapsedMs < 5000) {
716
- const error = `Pi exited during startup with code ${exitCode}.`;
717
- process.stderr.write(`${error} Falling back to Rig attach view.
718
- `);
719
- return { launched: false, exitCode, command, error };
720
- }
721
- return { launched: true, exitCode, command };
722
- }
723
-
724
857
  // packages/cli/src/commands/run.ts
725
858
  function normalizeRemoteRunDetails(payload) {
726
859
  const run = payload.run;
@@ -945,20 +1078,13 @@ async function executeRun(context, args) {
945
1078
  throw new CliError2("run attach requires a run id.", 2);
946
1079
  }
947
1080
  let steered = false;
948
- const shouldTryPiAttach = context.outputMode === "text" && follow.value && !once.value && Boolean(process.stdin.isTTY && process.stdout.isTTY) && process.env.RIG_DISABLE_PI_LAUNCH !== "1";
949
- if (shouldTryPiAttach && messageOption.value?.trim()) {
1081
+ if (messageOption.value?.trim()) {
950
1082
  await steerRunViaServer(context, runId, messageOption.value.trim());
951
1083
  steered = true;
952
1084
  }
953
- if (shouldTryPiAttach) {
954
- const piSession = await launchPiRigSession(context, { runId });
955
- if (piSession.launched) {
956
- return { ok: true, group: "run", command, details: { runId, steered, mode: "pi", ...piSession } };
957
- }
958
- }
959
1085
  const attached = await attachRunOperatorView(context, {
960
1086
  runId,
961
- message: shouldTryPiAttach ? null : messageOption.value ?? null,
1087
+ message: null,
962
1088
  once: once.value,
963
1089
  follow: follow.value,
964
1090
  pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)