@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.
- package/dist/bin/rig.js +1288 -315
- package/dist/src/commands/_authority-runs.js +1 -0
- package/dist/src/commands/_cli-format.js +106 -0
- package/dist/src/commands/_doctor-checks.js +10 -22
- package/dist/src/commands/_operator-surface.js +204 -0
- package/dist/src/commands/_operator-view.js +207 -51
- package/dist/src/commands/_pi-install.js +4 -3
- package/dist/src/commands/_pi-session.js +253 -0
- package/dist/src/commands/_preflight.js +33 -28
- package/dist/src/commands/_server-client.js +80 -27
- package/dist/src/commands/_snapshot-upload.js +7 -20
- package/dist/src/commands/_task-picker.js +44 -16
- package/dist/src/commands/agent.js +2 -0
- package/dist/src/commands/doctor.js +10 -22
- package/dist/src/commands/github.js +9 -22
- package/dist/src/commands/init.js +295 -66
- package/dist/src/commands/queue.js +1 -0
- package/dist/src/commands/run.js +456 -95
- package/dist/src/commands/server.js +9 -22
- package/dist/src/commands/setup.js +10 -22
- package/dist/src/commands/task-run-driver.js +539 -64
- package/dist/src/commands/task.js +502 -130
- package/dist/src/commands.js +1276 -306
- package/dist/src/index.js +1288 -315
- package/dist/src/launcher.js +5 -3
- package/dist/src/runner.js +3 -2
- package/package.json +5 -4
package/dist/src/commands/run.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/cli/src/commands/run.ts
|
|
3
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
4
|
-
import { resolve as resolve3 } from "path";
|
|
5
3
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
6
4
|
|
|
7
5
|
// packages/cli/src/runner.ts
|
|
@@ -12,6 +10,9 @@ import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
|
|
|
12
10
|
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
13
11
|
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
14
12
|
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
|
+
}
|
|
15
16
|
function takeFlag(args, flag) {
|
|
16
17
|
const rest = [];
|
|
17
18
|
let value = false;
|
|
@@ -54,9 +55,7 @@ Usage: ${usage}`);
|
|
|
54
55
|
// packages/cli/src/commands/run.ts
|
|
55
56
|
import {
|
|
56
57
|
listAuthorityRuns,
|
|
57
|
-
readAuthorityRun
|
|
58
|
-
readJsonlFile,
|
|
59
|
-
resolveAuthorityRunDir
|
|
58
|
+
readAuthorityRun
|
|
60
59
|
} from "@rig/runtime/control-plane/authority-files";
|
|
61
60
|
import {
|
|
62
61
|
cleanupRunState,
|
|
@@ -64,6 +63,7 @@ import {
|
|
|
64
63
|
listOpenEpics,
|
|
65
64
|
resolveDefaultEpic,
|
|
66
65
|
runResume,
|
|
66
|
+
runRestart,
|
|
67
67
|
runStatus,
|
|
68
68
|
runStop,
|
|
69
69
|
startRun,
|
|
@@ -84,7 +84,6 @@ function parsePositiveInt(value, option, fallback) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// packages/cli/src/commands/_server-client.ts
|
|
87
|
-
import { spawnSync } from "child_process";
|
|
88
87
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
89
88
|
import { resolve as resolve2 } from "path";
|
|
90
89
|
import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
|
|
@@ -172,7 +171,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
|
|
|
172
171
|
}
|
|
173
172
|
|
|
174
173
|
// packages/cli/src/commands/_server-client.ts
|
|
175
|
-
var
|
|
174
|
+
var scopedGitHubBearerTokens = new Map;
|
|
176
175
|
function cleanToken(value) {
|
|
177
176
|
const trimmed = value?.trim();
|
|
178
177
|
return trimmed ? trimmed : null;
|
|
@@ -189,25 +188,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
|
|
|
189
188
|
}
|
|
190
189
|
}
|
|
191
190
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
const scopedKey = resolve2(projectRoot);
|
|
192
|
+
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
193
|
+
return scopedGitHubBearerTokens.get(scopedKey) ?? null;
|
|
194
194
|
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
195
|
-
if (privateSession)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
|
|
200
|
-
if (envToken) {
|
|
201
|
-
cachedGitHubBearerToken = envToken;
|
|
202
|
-
return cachedGitHubBearerToken;
|
|
203
|
-
}
|
|
204
|
-
const result = spawnSync("gh", ["auth", "token"], {
|
|
205
|
-
encoding: "utf8",
|
|
206
|
-
timeout: 5000,
|
|
207
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
208
|
-
});
|
|
209
|
-
cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
|
|
210
|
-
return cachedGitHubBearerToken;
|
|
195
|
+
if (privateSession)
|
|
196
|
+
return privateSession;
|
|
197
|
+
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
211
198
|
}
|
|
212
199
|
async function ensureServerForCli(projectRoot) {
|
|
213
200
|
try {
|
|
@@ -275,6 +262,14 @@ async function requestServerJson(context, pathname, init = {}) {
|
|
|
275
262
|
}
|
|
276
263
|
return payload;
|
|
277
264
|
}
|
|
265
|
+
async function listRunsViaServer(context, options = {}) {
|
|
266
|
+
const url = new URL("http://rig.local/api/runs");
|
|
267
|
+
if (options.limit !== undefined)
|
|
268
|
+
url.searchParams.set("limit", String(options.limit));
|
|
269
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
270
|
+
const runs = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.runs) ? payload.runs : [];
|
|
271
|
+
return runs.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
272
|
+
}
|
|
278
273
|
async function getRunDetailsViaServer(context, runId) {
|
|
279
274
|
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}`);
|
|
280
275
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
@@ -288,6 +283,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
|
|
|
288
283
|
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
289
284
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
290
285
|
}
|
|
286
|
+
async function getRunTimelineViaServer(context, runId, options = {}) {
|
|
287
|
+
const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
|
|
288
|
+
if (options.limit !== undefined)
|
|
289
|
+
url.searchParams.set("limit", String(options.limit));
|
|
290
|
+
if (options.cursor)
|
|
291
|
+
url.searchParams.set("cursor", options.cursor);
|
|
292
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
293
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
294
|
+
}
|
|
291
295
|
async function stopRunViaServer(context, runId) {
|
|
292
296
|
const payload = await requestServerJson(context, "/api/runs/stop", {
|
|
293
297
|
method: "POST",
|
|
@@ -305,9 +309,8 @@ async function steerRunViaServer(context, runId, message) {
|
|
|
305
309
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
306
310
|
}
|
|
307
311
|
|
|
308
|
-
// packages/cli/src/commands/_operator-
|
|
312
|
+
// packages/cli/src/commands/_operator-surface.ts
|
|
309
313
|
import { createInterface } from "readline";
|
|
310
|
-
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
311
314
|
var CANONICAL_STAGES = [
|
|
312
315
|
"Connect",
|
|
313
316
|
"GitHub/task sync",
|
|
@@ -322,18 +325,168 @@ var CANONICAL_STAGES = [
|
|
|
322
325
|
"Merge",
|
|
323
326
|
"Complete"
|
|
324
327
|
];
|
|
328
|
+
function logDetail(log) {
|
|
329
|
+
return typeof log.detail === "string" ? log.detail.trim() : "";
|
|
330
|
+
}
|
|
331
|
+
function parseProviderProtocolLog(title, detail) {
|
|
332
|
+
if (title.trim().toLowerCase() !== "agent output")
|
|
333
|
+
return null;
|
|
334
|
+
if (!detail.startsWith("{") || !detail.endsWith("}"))
|
|
335
|
+
return null;
|
|
336
|
+
try {
|
|
337
|
+
const record = JSON.parse(detail);
|
|
338
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
339
|
+
return null;
|
|
340
|
+
const type = record.type;
|
|
341
|
+
return typeof type === "string" && [
|
|
342
|
+
"assistant",
|
|
343
|
+
"message_start",
|
|
344
|
+
"message_update",
|
|
345
|
+
"message_end",
|
|
346
|
+
"stream_event",
|
|
347
|
+
"tool_result",
|
|
348
|
+
"tool_execution_start",
|
|
349
|
+
"tool_execution_update",
|
|
350
|
+
"tool_execution_end",
|
|
351
|
+
"turn_start",
|
|
352
|
+
"turn_end"
|
|
353
|
+
].includes(type) ? record : null;
|
|
354
|
+
} catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function renderProviderProtocolLog(record) {
|
|
359
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
360
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
|
|
361
|
+
const toolName = String(record.toolName ?? record.name ?? "tool");
|
|
362
|
+
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";
|
|
363
|
+
return `[Pi tool] ${toolName} ${status}`;
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
function entryId(entry, fallback) {
|
|
368
|
+
return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
|
|
369
|
+
}
|
|
325
370
|
function renderOperatorSnapshot(snapshot) {
|
|
326
371
|
const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
|
|
327
372
|
const runId = String(run.runId ?? run.id ?? "run");
|
|
328
373
|
const status = String(run.status ?? "unknown");
|
|
329
374
|
const logs = snapshot.logs ?? [];
|
|
375
|
+
const latestByStage = new Map;
|
|
376
|
+
for (const log of logs) {
|
|
377
|
+
const title = String(log.title ?? "").toLowerCase();
|
|
378
|
+
const stageName = String(log.stage ?? "").toLowerCase();
|
|
379
|
+
const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
|
|
380
|
+
if (stage)
|
|
381
|
+
latestByStage.set(stage, log);
|
|
382
|
+
}
|
|
330
383
|
const stageLines = CANONICAL_STAGES.flatMap((stage) => {
|
|
331
|
-
const match =
|
|
332
|
-
return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
|
|
384
|
+
const match = latestByStage.get(stage);
|
|
385
|
+
return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
|
|
333
386
|
});
|
|
334
387
|
return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
|
|
335
388
|
`);
|
|
336
389
|
}
|
|
390
|
+
function createPiRunStreamRenderer(output = process.stdout) {
|
|
391
|
+
let lastSnapshot = "";
|
|
392
|
+
const assistantTextById = new Map;
|
|
393
|
+
const seenTimeline = new Set;
|
|
394
|
+
const seenLogs = new Set;
|
|
395
|
+
const writeLine = (line) => output.write(`${line}
|
|
396
|
+
`);
|
|
397
|
+
return {
|
|
398
|
+
renderSnapshot(snapshot) {
|
|
399
|
+
const rendered = renderOperatorSnapshot(snapshot);
|
|
400
|
+
if (rendered && rendered !== lastSnapshot) {
|
|
401
|
+
writeLine(rendered);
|
|
402
|
+
lastSnapshot = rendered;
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
renderTimeline(entries) {
|
|
406
|
+
for (const [index, entry] of entries.entries()) {
|
|
407
|
+
const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
|
|
408
|
+
if (entry.type === "assistant_message" && typeof entry.text === "string") {
|
|
409
|
+
const text = entry.text;
|
|
410
|
+
const previousText = assistantTextById.get(id) ?? "";
|
|
411
|
+
if (!previousText && text.trim()) {
|
|
412
|
+
writeLine("[Pi assistant]");
|
|
413
|
+
}
|
|
414
|
+
if (text.startsWith(previousText)) {
|
|
415
|
+
const delta = text.slice(previousText.length);
|
|
416
|
+
if (delta)
|
|
417
|
+
output.write(delta);
|
|
418
|
+
} else if (text.trim() && text !== previousText) {
|
|
419
|
+
if (previousText)
|
|
420
|
+
writeLine(`
|
|
421
|
+
[Pi assistant]`);
|
|
422
|
+
output.write(text);
|
|
423
|
+
}
|
|
424
|
+
assistantTextById.set(id, text);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (seenTimeline.has(id))
|
|
428
|
+
continue;
|
|
429
|
+
seenTimeline.add(id);
|
|
430
|
+
if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
|
|
431
|
+
writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (entry.type === "timeline_warning") {
|
|
435
|
+
writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
renderLogs(entries) {
|
|
440
|
+
for (const [index, entry] of entries.entries()) {
|
|
441
|
+
const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
|
|
442
|
+
if (seenLogs.has(id))
|
|
443
|
+
continue;
|
|
444
|
+
seenLogs.add(id);
|
|
445
|
+
const title = String(entry.title ?? "");
|
|
446
|
+
if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
|
|
447
|
+
continue;
|
|
448
|
+
const detail = logDetail(entry);
|
|
449
|
+
if (!detail)
|
|
450
|
+
continue;
|
|
451
|
+
const protocolRecord = parseProviderProtocolLog(title, detail);
|
|
452
|
+
if (protocolRecord) {
|
|
453
|
+
const protocolLine = renderProviderProtocolLog(protocolRecord);
|
|
454
|
+
if (protocolLine)
|
|
455
|
+
writeLine(protocolLine);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
writeLine(`[${title || "Rig log"}] ${detail}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function createOperatorSurface(options = {}) {
|
|
464
|
+
const input = options.input ?? process.stdin;
|
|
465
|
+
const output = options.output ?? process.stdout;
|
|
466
|
+
const errorOutput = options.errorOutput ?? process.stderr;
|
|
467
|
+
const renderer = createPiRunStreamRenderer(output);
|
|
468
|
+
const writeLine = (line) => output.write(`${line}
|
|
469
|
+
`);
|
|
470
|
+
return {
|
|
471
|
+
mode: "pi-compatible-text",
|
|
472
|
+
...renderer,
|
|
473
|
+
info: writeLine,
|
|
474
|
+
error: (message) => errorOutput.write(`${message}
|
|
475
|
+
`),
|
|
476
|
+
attachCommandInput(handler) {
|
|
477
|
+
if (options.interactive === false || !input.isTTY)
|
|
478
|
+
return null;
|
|
479
|
+
const rl = createInterface({ input, output: process.stdout, terminal: false });
|
|
480
|
+
rl.on("line", (line) => {
|
|
481
|
+
Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
482
|
+
});
|
|
483
|
+
return { close: () => rl.close() };
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// packages/cli/src/commands/_operator-view.ts
|
|
489
|
+
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
337
490
|
function runStatusFromPayload(payload) {
|
|
338
491
|
const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
339
492
|
return String(run.status ?? "unknown").toLowerCase();
|
|
@@ -355,11 +508,22 @@ async function applyOperatorCommand(context, input, deps = {}) {
|
|
|
355
508
|
await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
|
|
356
509
|
return { action: "continue", message: "Steering message queued." };
|
|
357
510
|
}
|
|
358
|
-
async function readOperatorSnapshot(context, runId) {
|
|
511
|
+
async function readOperatorSnapshot(context, runId, options = {}) {
|
|
359
512
|
const run = await getRunDetailsViaServer(context, runId);
|
|
360
513
|
const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
|
|
361
|
-
const
|
|
362
|
-
|
|
514
|
+
const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
|
|
515
|
+
entries: [{
|
|
516
|
+
id: `timeline-unavailable:${runId}`,
|
|
517
|
+
type: "timeline_warning",
|
|
518
|
+
detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
|
|
519
|
+
createdAt: new Date().toISOString()
|
|
520
|
+
}],
|
|
521
|
+
nextCursor: options.timelineCursor ?? null
|
|
522
|
+
}));
|
|
523
|
+
const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
|
|
524
|
+
const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
525
|
+
const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
|
|
526
|
+
return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
|
|
363
527
|
}
|
|
364
528
|
async function attachRunOperatorView(context, input) {
|
|
365
529
|
let steered = false;
|
|
@@ -367,45 +531,230 @@ async function attachRunOperatorView(context, input) {
|
|
|
367
531
|
await steerRunViaServer(context, input.runId, input.message.trim());
|
|
368
532
|
steered = true;
|
|
369
533
|
}
|
|
534
|
+
const surface = createOperatorSurface({ interactive: input.interactive !== false });
|
|
370
535
|
let snapshot = await readOperatorSnapshot(context, input.runId);
|
|
371
536
|
if (context.outputMode === "text") {
|
|
372
|
-
|
|
537
|
+
surface.renderSnapshot(snapshot);
|
|
538
|
+
surface.renderTimeline(snapshot.timeline);
|
|
539
|
+
surface.renderLogs(snapshot.logs);
|
|
373
540
|
if (steered)
|
|
374
|
-
|
|
541
|
+
surface.info("Steering message queued.");
|
|
375
542
|
}
|
|
376
543
|
let detached = false;
|
|
377
|
-
let
|
|
544
|
+
let commandInput = null;
|
|
378
545
|
if (input.follow && !input.once && context.outputMode === "text") {
|
|
379
546
|
if (input.interactive !== false && process.stdin.isTTY) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
}).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
547
|
+
surface.info("Controls: /user <message>, /stop, /detach");
|
|
548
|
+
commandInput = surface.attachCommandInput(async (line) => {
|
|
549
|
+
const result = await applyOperatorCommand(context, { runId: input.runId, line });
|
|
550
|
+
if (result.message)
|
|
551
|
+
surface.info(result.message);
|
|
552
|
+
if (result.action === "detach" || result.action === "stopped") {
|
|
553
|
+
detached = true;
|
|
554
|
+
commandInput?.close();
|
|
555
|
+
}
|
|
391
556
|
});
|
|
392
557
|
}
|
|
393
|
-
let lastRendered = snapshot.rendered;
|
|
394
558
|
const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
|
|
559
|
+
let timelineCursor = snapshot.timelineCursor;
|
|
395
560
|
while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
|
|
396
561
|
await Bun.sleep(pollMs);
|
|
397
|
-
snapshot = await readOperatorSnapshot(context, input.runId);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
562
|
+
snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
|
|
563
|
+
timelineCursor = snapshot.timelineCursor;
|
|
564
|
+
surface.renderSnapshot(snapshot);
|
|
565
|
+
surface.renderTimeline(snapshot.timeline);
|
|
566
|
+
surface.renderLogs(snapshot.logs);
|
|
402
567
|
}
|
|
403
|
-
|
|
568
|
+
commandInput?.close();
|
|
404
569
|
}
|
|
405
570
|
return { ...snapshot, steered, detached };
|
|
406
571
|
}
|
|
407
572
|
|
|
573
|
+
// packages/cli/src/commands/_cli-format.ts
|
|
574
|
+
import pc from "picocolors";
|
|
575
|
+
function stringField(record, key, fallback = "") {
|
|
576
|
+
const value = record[key];
|
|
577
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
578
|
+
}
|
|
579
|
+
function truncate(value, width) {
|
|
580
|
+
if (value.length <= width)
|
|
581
|
+
return value;
|
|
582
|
+
if (width <= 1)
|
|
583
|
+
return "\u2026";
|
|
584
|
+
return `${value.slice(0, width - 1)}\u2026`;
|
|
585
|
+
}
|
|
586
|
+
function pad(value, width) {
|
|
587
|
+
return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
|
|
588
|
+
}
|
|
589
|
+
function statusColor(status) {
|
|
590
|
+
const normalized = status.toLowerCase();
|
|
591
|
+
if (["completed", "merged", "closed", "done", "accepted"].includes(normalized))
|
|
592
|
+
return pc.green;
|
|
593
|
+
if (["failed", "needs_attention", "needs-attention", "blocked"].includes(normalized))
|
|
594
|
+
return pc.red;
|
|
595
|
+
if (["running", "reviewing", "validating", "in_progress", "in-progress"].includes(normalized))
|
|
596
|
+
return pc.cyan;
|
|
597
|
+
if (["ready", "open", "queued", "created", "preparing"].includes(normalized))
|
|
598
|
+
return pc.yellow;
|
|
599
|
+
return pc.dim;
|
|
600
|
+
}
|
|
601
|
+
function formatRunList(runs, options = {}) {
|
|
602
|
+
if (runs.length === 0) {
|
|
603
|
+
return pc.dim(options.source === "server" ? "No runs recorded on the selected Rig server." : "No runs recorded in .rig/runs.");
|
|
604
|
+
}
|
|
605
|
+
const rows = runs.map((run) => {
|
|
606
|
+
const runId = stringField(run, "runId", stringField(run, "id", "(unknown-run)"));
|
|
607
|
+
const status = stringField(run, "status", "unknown");
|
|
608
|
+
const taskId = stringField(run, "taskId", "");
|
|
609
|
+
const title = stringField(run, "title", taskId || "(untitled)");
|
|
610
|
+
const runtime = stringField(run, "runtimeAdapter", "");
|
|
611
|
+
return { runId, status, title, runtime };
|
|
612
|
+
});
|
|
613
|
+
const idWidth = Math.min(36, Math.max(6, ...rows.map((row) => row.runId.length)));
|
|
614
|
+
const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
|
|
615
|
+
const header = `${pc.bold(pad("RUN", idWidth))} ${pc.bold(pad("STATUS", statusWidth))} ${pc.bold("TITLE")}`;
|
|
616
|
+
const body = rows.map((row) => [
|
|
617
|
+
pc.bold(pad(truncate(row.runId, idWidth), idWidth)),
|
|
618
|
+
statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
|
|
619
|
+
`${row.title}${row.runtime ? pc.dim(` ${row.runtime}`) : ""}`
|
|
620
|
+
].join(" "));
|
|
621
|
+
return [pc.bold(options.source === "server" ? "Rig runs (server)" : "Rig runs"), header, ...body].join(`
|
|
622
|
+
`);
|
|
623
|
+
}
|
|
624
|
+
|
|
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
|
+
|
|
408
724
|
// packages/cli/src/commands/run.ts
|
|
725
|
+
function normalizeRemoteRunDetails(payload) {
|
|
726
|
+
const run = payload.run;
|
|
727
|
+
if (!run || typeof run !== "object" || Array.isArray(run))
|
|
728
|
+
return null;
|
|
729
|
+
return {
|
|
730
|
+
...run,
|
|
731
|
+
...Array.isArray(payload.timeline) ? { timeline: payload.timeline } : {},
|
|
732
|
+
...Array.isArray(payload.approvals) ? { approvals: payload.approvals } : {},
|
|
733
|
+
...Array.isArray(payload.userInputs) ? { userInputs: payload.userInputs } : {}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
var REMOTE_TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged"]);
|
|
737
|
+
function isRemoteConnectionSelected(projectRoot) {
|
|
738
|
+
return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
|
|
739
|
+
}
|
|
740
|
+
async function listRunsForSelectedConnection(context, options = {}) {
|
|
741
|
+
if (isRemoteConnectionSelected(context.projectRoot)) {
|
|
742
|
+
return { runs: await listRunsViaServer(context, options), source: "server" };
|
|
743
|
+
}
|
|
744
|
+
return { runs: listAuthorityRuns(context.projectRoot), source: "local" };
|
|
745
|
+
}
|
|
746
|
+
function runStringField(run, key, fallback = "") {
|
|
747
|
+
const value = run[key];
|
|
748
|
+
return typeof value === "string" && value.trim() ? value : fallback;
|
|
749
|
+
}
|
|
750
|
+
function runDisplayTitle(run) {
|
|
751
|
+
return runStringField(run, "title", runStringField(run, "taskId", "(untitled)"));
|
|
752
|
+
}
|
|
753
|
+
function buildServerRunStatus(runs) {
|
|
754
|
+
const activeRuns = runs.filter((run) => !REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
|
|
755
|
+
const recentRuns = runs.filter((run) => REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
|
|
756
|
+
return { activeRuns, recentRuns, runs };
|
|
757
|
+
}
|
|
409
758
|
function shouldPromptForEpicSelection(context, command, promptEpic, noEpicPrompt) {
|
|
410
759
|
if (noEpicPrompt) {
|
|
411
760
|
return false;
|
|
@@ -471,17 +820,11 @@ async function executeRun(context, args) {
|
|
|
471
820
|
switch (command) {
|
|
472
821
|
case "list": {
|
|
473
822
|
requireNoExtraArgs(rest, "bun run rig run list");
|
|
474
|
-
const runs =
|
|
823
|
+
const { runs, source } = await listRunsForSelectedConnection(context, { limit: 100 });
|
|
475
824
|
if (context.outputMode === "text") {
|
|
476
|
-
|
|
477
|
-
console.log("No runs recorded in .rig/runs.");
|
|
478
|
-
} else {
|
|
479
|
-
for (const run of runs) {
|
|
480
|
-
console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.title}`);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
825
|
+
console.log(formatRunList(runs, { source }));
|
|
483
826
|
}
|
|
484
|
-
return { ok: true, group: "run", command, details: { runs } };
|
|
827
|
+
return { ok: true, group: "run", command, details: { runs, source } };
|
|
485
828
|
}
|
|
486
829
|
case "delete": {
|
|
487
830
|
let pending = rest;
|
|
@@ -544,7 +887,7 @@ async function executeRun(context, args) {
|
|
|
544
887
|
if (!run.value) {
|
|
545
888
|
throw new CliError2("run show requires --run <id>.");
|
|
546
889
|
}
|
|
547
|
-
const record = readAuthorityRun(context.projectRoot, run.value);
|
|
890
|
+
const record = readAuthorityRun(context.projectRoot, run.value) ?? normalizeRemoteRunDetails(await getRunDetailsViaServer(context, run.value).catch(() => ({})));
|
|
548
891
|
if (!record) {
|
|
549
892
|
throw new CliError2(`Run not found: ${run.value}`, 2);
|
|
550
893
|
}
|
|
@@ -563,34 +906,24 @@ async function executeRun(context, args) {
|
|
|
563
906
|
if (!run.value) {
|
|
564
907
|
throw new CliError2("run timeline requires --run <id>.");
|
|
565
908
|
}
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
return events2;
|
|
575
|
-
};
|
|
576
|
-
const events = printEvents();
|
|
909
|
+
const renderer = createPiRunStreamRenderer();
|
|
910
|
+
let cursor = null;
|
|
911
|
+
const page = await getRunTimelineViaServer(context, run.value, { limit: 500 });
|
|
912
|
+
const events = Array.isArray(page.entries) ? page.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
913
|
+
cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
|
|
914
|
+
if (context.outputMode === "text") {
|
|
915
|
+
renderer.renderTimeline(events);
|
|
916
|
+
}
|
|
577
917
|
if (follow.value && context.outputMode === "text") {
|
|
578
|
-
let lastLength = existsSync3(timelinePath) ? readFileSync3(timelinePath, "utf8").length : 0;
|
|
579
918
|
while (true) {
|
|
580
919
|
await Bun.sleep(1000);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
continue;
|
|
586
|
-
const delta = next.slice(lastLength);
|
|
587
|
-
lastLength = next.length;
|
|
588
|
-
for (const line of delta.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) {
|
|
589
|
-
console.log(line);
|
|
590
|
-
}
|
|
920
|
+
const nextPage = await getRunTimelineViaServer(context, run.value, { limit: 500, ...cursor ? { cursor } : {} });
|
|
921
|
+
const nextEvents = Array.isArray(nextPage.entries) ? nextPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
922
|
+
cursor = typeof nextPage.nextCursor === "string" ? nextPage.nextCursor : cursor;
|
|
923
|
+
renderer.renderTimeline(nextEvents);
|
|
591
924
|
}
|
|
592
925
|
}
|
|
593
|
-
return { ok: true, group: "run", command, details: { runId: run.value, events } };
|
|
926
|
+
return { ok: true, group: "run", command, details: { runId: run.value, events, cursor } };
|
|
594
927
|
}
|
|
595
928
|
case "attach": {
|
|
596
929
|
let pending = rest;
|
|
@@ -611,14 +944,26 @@ async function executeRun(context, args) {
|
|
|
611
944
|
if (!runId) {
|
|
612
945
|
throw new CliError2("run attach requires a run id.", 2);
|
|
613
946
|
}
|
|
947
|
+
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()) {
|
|
950
|
+
await steerRunViaServer(context, runId, messageOption.value.trim());
|
|
951
|
+
steered = true;
|
|
952
|
+
}
|
|
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
|
+
}
|
|
614
959
|
const attached = await attachRunOperatorView(context, {
|
|
615
960
|
runId,
|
|
616
|
-
message: messageOption.value ?? null,
|
|
961
|
+
message: shouldTryPiAttach ? null : messageOption.value ?? null,
|
|
617
962
|
once: once.value,
|
|
618
963
|
follow: follow.value,
|
|
619
964
|
pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)
|
|
620
965
|
});
|
|
621
|
-
return { ok: true, group: "run", command, details: attached };
|
|
966
|
+
return { ok: true, group: "run", command, details: { ...attached, steered: attached.steered || steered } };
|
|
622
967
|
}
|
|
623
968
|
case "status": {
|
|
624
969
|
requireNoExtraArgs(rest, "bun run rig run status");
|
|
@@ -628,17 +973,19 @@ async function executeRun(context, args) {
|
|
|
628
973
|
}
|
|
629
974
|
return { ok: true, group: "run", command };
|
|
630
975
|
}
|
|
631
|
-
const summary = runStatus(context.projectRoot, runtimeContext);
|
|
976
|
+
const summary = isRemoteConnectionSelected(context.projectRoot) ? buildServerRunStatus(await listRunsViaServer(context, { limit: 100 })) : runStatus(context.projectRoot, runtimeContext);
|
|
977
|
+
const activeRuns = Array.isArray(summary.activeRuns) ? summary.activeRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
|
|
978
|
+
const recentRuns = Array.isArray(summary.recentRuns) ? summary.recentRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
|
|
632
979
|
if (context.outputMode === "text") {
|
|
633
|
-
console.log(`Active runs: ${
|
|
634
|
-
for (const run of
|
|
635
|
-
console.log(`- ${run
|
|
980
|
+
console.log(`Active runs: ${activeRuns.length}`);
|
|
981
|
+
for (const run of activeRuns) {
|
|
982
|
+
console.log(`- ${runStringField(run, "runId", "(unknown-run)")} \xB7 ${runStringField(run, "status", "unknown")} \xB7 ${runStringField(run, "taskId", runDisplayTitle(run))}`);
|
|
636
983
|
}
|
|
637
|
-
if (
|
|
984
|
+
if (recentRuns.length > 0) {
|
|
638
985
|
console.log("");
|
|
639
986
|
console.log("Recent runs:");
|
|
640
|
-
for (const run of
|
|
641
|
-
console.log(`- ${run
|
|
987
|
+
for (const run of recentRuns) {
|
|
988
|
+
console.log(`- ${runStringField(run, "runId", "(unknown-run)")} \xB7 ${runStringField(run, "status", "unknown")} \xB7 ${runStringField(run, "taskId", runDisplayTitle(run))}`);
|
|
642
989
|
}
|
|
643
990
|
}
|
|
644
991
|
}
|
|
@@ -725,6 +1072,20 @@ async function executeRun(context, args) {
|
|
|
725
1072
|
}
|
|
726
1073
|
return { ok: true, group: "run", command, details: resumed };
|
|
727
1074
|
}
|
|
1075
|
+
case "restart": {
|
|
1076
|
+
requireNoExtraArgs(rest, "bun run rig run restart");
|
|
1077
|
+
if (context.dryRun) {
|
|
1078
|
+
if (context.outputMode === "text") {
|
|
1079
|
+
console.log("[dry-run] rig run restart");
|
|
1080
|
+
}
|
|
1081
|
+
return { ok: true, group: "run", command };
|
|
1082
|
+
}
|
|
1083
|
+
const restarted = await runRestart(context.projectRoot, runtimeContext);
|
|
1084
|
+
if (context.outputMode === "text") {
|
|
1085
|
+
console.log(`Restarted run: ${restarted.runId}`);
|
|
1086
|
+
}
|
|
1087
|
+
return { ok: true, group: "run", command, details: restarted };
|
|
1088
|
+
}
|
|
728
1089
|
case "stop": {
|
|
729
1090
|
const runOption = takeOption(rest, "--run");
|
|
730
1091
|
const positionalRunId = runOption.rest.length > 0 ? runOption.rest[0] : undefined;
|