@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.
- package/dist/bin/rig.js +332 -120
- package/dist/src/commands/_operator-view.js +235 -0
- package/dist/src/commands/run.js +237 -111
- package/dist/src/commands/task-run-driver.js +92 -7
- package/dist/src/commands/task.js +236 -109
- package/dist/src/commands.js +332 -120
- package/dist/src/index.js +332 -120
- package/package.json +6 -5
- package/dist/src/commands/_pi-session.js +0 -253
|
@@ -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") {
|
package/dist/src/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
1087
|
+
message: null,
|
|
962
1088
|
once: once.value,
|
|
963
1089
|
follow: follow.value,
|
|
964
1090
|
pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)
|