@h-rig/cli 0.0.6-alpha.4 → 0.0.6-alpha.41
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 +3905 -1193
- package/dist/src/commands/_cli-format.js +369 -0
- package/dist/src/commands/_connection-state.js +1 -3
- package/dist/src/commands/_doctor-checks.js +13 -27
- package/dist/src/commands/_help-catalog.js +445 -0
- package/dist/src/commands/_operator-surface.js +220 -0
- package/dist/src/commands/_operator-view.js +916 -56
- package/dist/src/commands/_parsers.js +0 -2
- package/dist/src/commands/_pi-frontend.js +880 -0
- package/dist/src/commands/_pi-install.js +4 -3
- package/dist/src/commands/_pi-remote-session.js +665 -0
- package/dist/src/commands/_pi-worker-bridge-extension.js +676 -0
- package/dist/src/commands/_policy.js +0 -2
- package/dist/src/commands/_preflight.js +32 -109
- package/dist/src/commands/_run-driver-helpers.js +2 -2
- package/dist/src/commands/_server-client.js +152 -31
- package/dist/src/commands/_snapshot-upload.js +8 -23
- package/dist/src/commands/_task-picker.js +44 -16
- package/dist/src/commands/agent.js +8 -9
- package/dist/src/commands/browser.js +4 -6
- package/dist/src/commands/connect.js +132 -25
- package/dist/src/commands/dist.js +4 -6
- package/dist/src/commands/doctor.js +13 -27
- package/dist/src/commands/github.js +10 -25
- package/dist/src/commands/inbox.js +351 -31
- package/dist/src/commands/init.js +298 -71
- package/dist/src/commands/inspect.js +237 -23
- package/dist/src/commands/inspector.js +2 -4
- package/dist/src/commands/pi.js +168 -0
- package/dist/src/commands/plugin.js +81 -22
- package/dist/src/commands/profile-and-review.js +8 -10
- package/dist/src/commands/queue.js +2 -3
- package/dist/src/commands/remote.js +18 -20
- package/dist/src/commands/repo-git-harness.js +6 -8
- package/dist/src/commands/run.js +1214 -123
- package/dist/src/commands/server.js +222 -33
- package/dist/src/commands/setup.js +17 -37
- package/dist/src/commands/task-report-bug.js +5 -7
- package/dist/src/commands/task-run-driver.js +630 -71
- package/dist/src/commands/task.js +1653 -252
- package/dist/src/commands/test.js +3 -5
- package/dist/src/commands/workspace.js +4 -6
- package/dist/src/commands.js +3884 -1166
- package/dist/src/index.js +3898 -1189
- package/dist/src/launcher.js +5 -3
- package/dist/src/report-bug.js +3 -3
- package/dist/src/runner.js +5 -19
- package/package.json +6 -4
package/dist/src/commands/run.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
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
|
|
8
6
|
import { EventBus } from "@rig/runtime/control-plane/runtime/events";
|
|
9
7
|
import { CliError } from "@rig/runtime/control-plane/errors";
|
|
10
8
|
import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
|
|
11
|
-
import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
|
|
12
|
-
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
13
9
|
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
14
10
|
import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
|
|
15
11
|
function takeFlag(args, flag) {
|
|
@@ -54,9 +50,7 @@ Usage: ${usage}`);
|
|
|
54
50
|
// packages/cli/src/commands/run.ts
|
|
55
51
|
import {
|
|
56
52
|
listAuthorityRuns,
|
|
57
|
-
readAuthorityRun
|
|
58
|
-
readJsonlFile,
|
|
59
|
-
resolveAuthorityRunDir
|
|
53
|
+
readAuthorityRun
|
|
60
54
|
} from "@rig/runtime/control-plane/authority-files";
|
|
61
55
|
import {
|
|
62
56
|
cleanupRunState,
|
|
@@ -70,7 +64,7 @@ import {
|
|
|
70
64
|
startRun,
|
|
71
65
|
defaultStartRunOptions
|
|
72
66
|
} from "@rig/runtime/control-plane/native/run-ops";
|
|
73
|
-
import { loadRuntimeContextFromEnv
|
|
67
|
+
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
74
68
|
|
|
75
69
|
// packages/cli/src/commands/_parsers.ts
|
|
76
70
|
function parsePositiveInt(value, option, fallback) {
|
|
@@ -85,7 +79,6 @@ function parsePositiveInt(value, option, fallback) {
|
|
|
85
79
|
}
|
|
86
80
|
|
|
87
81
|
// packages/cli/src/commands/_server-client.ts
|
|
88
|
-
import { spawnSync } from "child_process";
|
|
89
82
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
90
83
|
import { resolve as resolve2 } from "path";
|
|
91
84
|
import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
|
|
@@ -167,13 +160,13 @@ function resolveSelectedConnection(projectRoot, options = {}) {
|
|
|
167
160
|
const global = readGlobalConnections(options);
|
|
168
161
|
const connection = global.connections[repo.selected];
|
|
169
162
|
if (!connection) {
|
|
170
|
-
throw new CliError2(`Selected Rig
|
|
163
|
+
throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
|
|
171
164
|
}
|
|
172
165
|
return { alias: repo.selected, connection };
|
|
173
166
|
}
|
|
174
167
|
|
|
175
168
|
// packages/cli/src/commands/_server-client.ts
|
|
176
|
-
var
|
|
169
|
+
var scopedGitHubBearerTokens = new Map;
|
|
177
170
|
function cleanToken(value) {
|
|
178
171
|
const trimmed = value?.trim();
|
|
179
172
|
return trimmed ? trimmed : null;
|
|
@@ -190,25 +183,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
|
|
|
190
183
|
}
|
|
191
184
|
}
|
|
192
185
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
193
|
-
|
|
194
|
-
|
|
186
|
+
const scopedKey = resolve2(projectRoot);
|
|
187
|
+
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
188
|
+
return scopedGitHubBearerTokens.get(scopedKey) ?? null;
|
|
195
189
|
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
196
|
-
if (privateSession)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
|
|
201
|
-
if (envToken) {
|
|
202
|
-
cachedGitHubBearerToken = envToken;
|
|
203
|
-
return cachedGitHubBearerToken;
|
|
204
|
-
}
|
|
205
|
-
const result = spawnSync("gh", ["auth", "token"], {
|
|
206
|
-
encoding: "utf8",
|
|
207
|
-
timeout: 5000,
|
|
208
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
209
|
-
});
|
|
210
|
-
cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
|
|
211
|
-
return cachedGitHubBearerToken;
|
|
190
|
+
if (privateSession)
|
|
191
|
+
return privateSession;
|
|
192
|
+
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
212
193
|
}
|
|
213
194
|
async function ensureServerForCli(projectRoot) {
|
|
214
195
|
try {
|
|
@@ -276,6 +257,14 @@ async function requestServerJson(context, pathname, init = {}) {
|
|
|
276
257
|
}
|
|
277
258
|
return payload;
|
|
278
259
|
}
|
|
260
|
+
async function listRunsViaServer(context, options = {}) {
|
|
261
|
+
const url = new URL("http://rig.local/api/runs");
|
|
262
|
+
if (options.limit !== undefined)
|
|
263
|
+
url.searchParams.set("limit", String(options.limit));
|
|
264
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
265
|
+
const runs = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.runs) ? payload.runs : [];
|
|
266
|
+
return runs.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
267
|
+
}
|
|
279
268
|
async function getRunDetailsViaServer(context, runId) {
|
|
280
269
|
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}`);
|
|
281
270
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
@@ -289,6 +278,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
|
|
|
289
278
|
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
290
279
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
291
280
|
}
|
|
281
|
+
async function getRunTimelineViaServer(context, runId, options = {}) {
|
|
282
|
+
const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
|
|
283
|
+
if (options.limit !== undefined)
|
|
284
|
+
url.searchParams.set("limit", String(options.limit));
|
|
285
|
+
if (options.cursor)
|
|
286
|
+
url.searchParams.set("cursor", options.cursor);
|
|
287
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
288
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
289
|
+
}
|
|
292
290
|
async function stopRunViaServer(context, runId) {
|
|
293
291
|
const payload = await requestServerJson(context, "/api/runs/stop", {
|
|
294
292
|
method: "POST",
|
|
@@ -305,10 +303,69 @@ async function steerRunViaServer(context, runId, message) {
|
|
|
305
303
|
});
|
|
306
304
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
307
305
|
}
|
|
306
|
+
async function getRunPiSessionViaServer(context, runId) {
|
|
307
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi`);
|
|
308
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
309
|
+
}
|
|
310
|
+
async function getRunPiMessagesViaServer(context, runId) {
|
|
311
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/messages`);
|
|
312
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { messages: [] };
|
|
313
|
+
}
|
|
314
|
+
async function getRunPiStatusViaServer(context, runId) {
|
|
315
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/status`);
|
|
316
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
317
|
+
}
|
|
318
|
+
async function getRunPiCommandsViaServer(context, runId) {
|
|
319
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
|
|
320
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
|
|
321
|
+
}
|
|
322
|
+
async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
|
|
323
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
|
|
324
|
+
method: "POST",
|
|
325
|
+
headers: { "content-type": "application/json" },
|
|
326
|
+
body: JSON.stringify({ text, streamingBehavior })
|
|
327
|
+
});
|
|
328
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
329
|
+
}
|
|
330
|
+
async function sendRunPiShellViaServer(context, runId, text) {
|
|
331
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/shell`, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: { "content-type": "application/json" },
|
|
334
|
+
body: JSON.stringify({ text })
|
|
335
|
+
});
|
|
336
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
337
|
+
}
|
|
338
|
+
async function runRunPiCommandViaServer(context, runId, text) {
|
|
339
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands/run`, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
headers: { "content-type": "application/json" },
|
|
342
|
+
body: JSON.stringify({ text })
|
|
343
|
+
});
|
|
344
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { type: "done" };
|
|
345
|
+
}
|
|
346
|
+
async function respondRunPiExtensionUiViaServer(context, runId, requestId, valueOrCancel) {
|
|
347
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/extension-ui/respond`, {
|
|
348
|
+
method: "POST",
|
|
349
|
+
headers: { "content-type": "application/json" },
|
|
350
|
+
body: JSON.stringify({ requestId, ...valueOrCancel })
|
|
351
|
+
});
|
|
352
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
353
|
+
}
|
|
354
|
+
async function abortRunPiViaServer(context, runId) {
|
|
355
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/abort`, { method: "POST" });
|
|
356
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { aborted: true };
|
|
357
|
+
}
|
|
358
|
+
async function buildRunPiEventsWebSocketUrl(context, runId) {
|
|
359
|
+
const server = await ensureServerForCli(context.projectRoot);
|
|
360
|
+
const url = new URL(`${server.baseUrl.replace(/\/+$/, "")}/api/runs/${encodeURIComponent(runId)}/pi/events`);
|
|
361
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
362
|
+
if (server.authToken)
|
|
363
|
+
url.searchParams.set("token", server.authToken);
|
|
364
|
+
return url.toString();
|
|
365
|
+
}
|
|
308
366
|
|
|
309
|
-
// packages/cli/src/commands/_operator-
|
|
367
|
+
// packages/cli/src/commands/_operator-surface.ts
|
|
310
368
|
import { createInterface } from "readline";
|
|
311
|
-
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
312
369
|
var CANONICAL_STAGES = [
|
|
313
370
|
"Connect",
|
|
314
371
|
"GitHub/task sync",
|
|
@@ -323,18 +380,808 @@ var CANONICAL_STAGES = [
|
|
|
323
380
|
"Merge",
|
|
324
381
|
"Complete"
|
|
325
382
|
];
|
|
383
|
+
function logDetail(log) {
|
|
384
|
+
return typeof log.detail === "string" ? log.detail.trim() : "";
|
|
385
|
+
}
|
|
386
|
+
function parseProviderProtocolLog(title, detail) {
|
|
387
|
+
if (title.trim().toLowerCase() !== "agent output")
|
|
388
|
+
return null;
|
|
389
|
+
if (!detail.startsWith("{") || !detail.endsWith("}"))
|
|
390
|
+
return null;
|
|
391
|
+
try {
|
|
392
|
+
const record = JSON.parse(detail);
|
|
393
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
394
|
+
return null;
|
|
395
|
+
const type = record.type;
|
|
396
|
+
return typeof type === "string" && [
|
|
397
|
+
"assistant",
|
|
398
|
+
"message_start",
|
|
399
|
+
"message_update",
|
|
400
|
+
"message_end",
|
|
401
|
+
"stream_event",
|
|
402
|
+
"tool_result",
|
|
403
|
+
"tool_execution_start",
|
|
404
|
+
"tool_execution_update",
|
|
405
|
+
"tool_execution_end",
|
|
406
|
+
"turn_start",
|
|
407
|
+
"turn_end"
|
|
408
|
+
].includes(type) ? record : null;
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function renderProviderProtocolLog(record) {
|
|
414
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
415
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
|
|
416
|
+
const toolName = String(record.toolName ?? record.name ?? "tool");
|
|
417
|
+
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";
|
|
418
|
+
return `[Pi tool] ${toolName} ${status}`;
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
function entryId(entry, fallback) {
|
|
423
|
+
return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
|
|
424
|
+
}
|
|
326
425
|
function renderOperatorSnapshot(snapshot) {
|
|
327
426
|
const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
|
|
328
427
|
const runId = String(run.runId ?? run.id ?? "run");
|
|
329
428
|
const status = String(run.status ?? "unknown");
|
|
330
429
|
const logs = snapshot.logs ?? [];
|
|
430
|
+
const latestByStage = new Map;
|
|
431
|
+
for (const log of logs) {
|
|
432
|
+
const title = String(log.title ?? "").toLowerCase();
|
|
433
|
+
const stageName = String(log.stage ?? "").toLowerCase();
|
|
434
|
+
const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
|
|
435
|
+
if (stage)
|
|
436
|
+
latestByStage.set(stage, log);
|
|
437
|
+
}
|
|
331
438
|
const stageLines = CANONICAL_STAGES.flatMap((stage) => {
|
|
332
|
-
const match =
|
|
333
|
-
return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
|
|
439
|
+
const match = latestByStage.get(stage);
|
|
440
|
+
return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
|
|
334
441
|
});
|
|
335
442
|
return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
|
|
336
443
|
`);
|
|
337
444
|
}
|
|
445
|
+
function createPiRunStreamRenderer(output = process.stdout) {
|
|
446
|
+
let lastSnapshot = "";
|
|
447
|
+
const assistantTextById = new Map;
|
|
448
|
+
const seenTimeline = new Set;
|
|
449
|
+
const seenLogs = new Set;
|
|
450
|
+
const writeLine = (line) => output.write(`${line}
|
|
451
|
+
`);
|
|
452
|
+
return {
|
|
453
|
+
renderSnapshot(snapshot) {
|
|
454
|
+
const rendered = renderOperatorSnapshot(snapshot);
|
|
455
|
+
if (rendered && rendered !== lastSnapshot) {
|
|
456
|
+
writeLine(rendered);
|
|
457
|
+
lastSnapshot = rendered;
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
renderTimeline(entries) {
|
|
461
|
+
for (const [index, entry] of entries.entries()) {
|
|
462
|
+
const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
|
|
463
|
+
if (entry.type === "assistant_message" && typeof entry.text === "string") {
|
|
464
|
+
const text = entry.text;
|
|
465
|
+
const previousText = assistantTextById.get(id) ?? "";
|
|
466
|
+
if (!previousText && text.trim()) {
|
|
467
|
+
writeLine("[Pi assistant]");
|
|
468
|
+
}
|
|
469
|
+
if (text.startsWith(previousText)) {
|
|
470
|
+
const delta = text.slice(previousText.length);
|
|
471
|
+
if (delta)
|
|
472
|
+
output.write(delta);
|
|
473
|
+
} else if (text.trim() && text !== previousText) {
|
|
474
|
+
if (previousText)
|
|
475
|
+
writeLine(`
|
|
476
|
+
[Pi assistant]`);
|
|
477
|
+
output.write(text);
|
|
478
|
+
}
|
|
479
|
+
assistantTextById.set(id, text);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (seenTimeline.has(id))
|
|
483
|
+
continue;
|
|
484
|
+
seenTimeline.add(id);
|
|
485
|
+
if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
|
|
486
|
+
writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (entry.type === "timeline_warning") {
|
|
490
|
+
writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (entry.type === "action") {
|
|
494
|
+
const text = String(entry.detail ?? entry.message ?? entry.title ?? "").trim();
|
|
495
|
+
if (text)
|
|
496
|
+
writeLine(`[Rig action] ${text}`);
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (entry.type === "user_message") {
|
|
500
|
+
const text = String(entry.text ?? entry.message ?? entry.detail ?? "").trim();
|
|
501
|
+
if (text)
|
|
502
|
+
writeLine(`[Operator] ${text}`);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const fallback = String(entry.detail ?? entry.message ?? entry.text ?? entry.title ?? "").trim();
|
|
506
|
+
if (fallback)
|
|
507
|
+
writeLine(`[${String(entry.type ?? "timeline")}] ${fallback}`);
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
renderLogs(entries) {
|
|
511
|
+
for (const [index, entry] of entries.entries()) {
|
|
512
|
+
const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
|
|
513
|
+
if (seenLogs.has(id))
|
|
514
|
+
continue;
|
|
515
|
+
seenLogs.add(id);
|
|
516
|
+
const title = String(entry.title ?? "");
|
|
517
|
+
if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
|
|
518
|
+
continue;
|
|
519
|
+
const detail = logDetail(entry);
|
|
520
|
+
if (!detail)
|
|
521
|
+
continue;
|
|
522
|
+
const protocolRecord = parseProviderProtocolLog(title, detail);
|
|
523
|
+
if (protocolRecord) {
|
|
524
|
+
const protocolLine = renderProviderProtocolLog(protocolRecord);
|
|
525
|
+
if (protocolLine)
|
|
526
|
+
writeLine(protocolLine);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
writeLine(`[${title || "Rig log"}] ${detail}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function createOperatorSurface(options = {}) {
|
|
535
|
+
const input = options.input ?? process.stdin;
|
|
536
|
+
const output = options.output ?? process.stdout;
|
|
537
|
+
const errorOutput = options.errorOutput ?? process.stderr;
|
|
538
|
+
const renderer = createPiRunStreamRenderer(output);
|
|
539
|
+
const writeLine = (line) => output.write(`${line}
|
|
540
|
+
`);
|
|
541
|
+
return {
|
|
542
|
+
mode: "pi-compatible-text",
|
|
543
|
+
...renderer,
|
|
544
|
+
info: writeLine,
|
|
545
|
+
error: (message) => errorOutput.write(`${message}
|
|
546
|
+
`),
|
|
547
|
+
attachCommandInput(handler) {
|
|
548
|
+
if (options.interactive === false || !input.isTTY)
|
|
549
|
+
return null;
|
|
550
|
+
const rl = createInterface({ input, output: process.stdout, terminal: false });
|
|
551
|
+
rl.on("line", (line) => {
|
|
552
|
+
Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
553
|
+
});
|
|
554
|
+
return { close: () => rl.close() };
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// packages/cli/src/commands/_pi-frontend.ts
|
|
560
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
561
|
+
import { tmpdir } from "os";
|
|
562
|
+
import { join } from "path";
|
|
563
|
+
import {
|
|
564
|
+
createAgentSessionFromServices,
|
|
565
|
+
createAgentSessionServices,
|
|
566
|
+
main as runPiMain
|
|
567
|
+
} from "@earendil-works/pi-coding-agent";
|
|
568
|
+
|
|
569
|
+
// packages/cli/src/commands/_pi-remote-session.ts
|
|
570
|
+
import { AgentSession as PiAgentSession } from "@earendil-works/pi-coding-agent";
|
|
571
|
+
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
572
|
+
function defaultTransport() {
|
|
573
|
+
return {
|
|
574
|
+
getSession: getRunPiSessionViaServer,
|
|
575
|
+
getMessages: getRunPiMessagesViaServer,
|
|
576
|
+
getStatus: getRunPiStatusViaServer,
|
|
577
|
+
getCommands: getRunPiCommandsViaServer,
|
|
578
|
+
sendPrompt: sendRunPiPromptViaServer,
|
|
579
|
+
sendShell: sendRunPiShellViaServer,
|
|
580
|
+
runCommand: runRunPiCommandViaServer,
|
|
581
|
+
abort: abortRunPiViaServer,
|
|
582
|
+
buildEventsWebSocketUrl: buildRunPiEventsWebSocketUrl
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function recordOf(value) {
|
|
586
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
587
|
+
}
|
|
588
|
+
function emptyRemoteStatus() {
|
|
589
|
+
return {
|
|
590
|
+
isStreaming: false,
|
|
591
|
+
isCompacting: false,
|
|
592
|
+
isBashRunning: false,
|
|
593
|
+
pendingMessageCount: 0,
|
|
594
|
+
steeringMessages: [],
|
|
595
|
+
followUpMessages: [],
|
|
596
|
+
model: null,
|
|
597
|
+
thinkingLevel: null,
|
|
598
|
+
sessionName: null,
|
|
599
|
+
cwd: null,
|
|
600
|
+
stats: null,
|
|
601
|
+
contextUsage: null
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function resolveAttachReadyTimeoutMs() {
|
|
605
|
+
const raw = Number.parseInt(process.env.RIG_PI_ATTACH_TIMEOUT_MS ?? "", 10);
|
|
606
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60000;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
class RigRemoteSessionController {
|
|
610
|
+
context;
|
|
611
|
+
runId;
|
|
612
|
+
status = emptyRemoteStatus();
|
|
613
|
+
transport;
|
|
614
|
+
hooks = {};
|
|
615
|
+
session = null;
|
|
616
|
+
socket = null;
|
|
617
|
+
closed = false;
|
|
618
|
+
pendingShells = [];
|
|
619
|
+
pendingCompactions = [];
|
|
620
|
+
constructor(input) {
|
|
621
|
+
this.context = input.context;
|
|
622
|
+
this.runId = input.runId;
|
|
623
|
+
this.transport = input.transport ?? defaultTransport();
|
|
624
|
+
}
|
|
625
|
+
ingestEnvelope(envelopeValue) {
|
|
626
|
+
this.applyEnvelope(envelopeValue);
|
|
627
|
+
}
|
|
628
|
+
bindSession(session) {
|
|
629
|
+
this.session = session;
|
|
630
|
+
}
|
|
631
|
+
setUiHooks(hooks) {
|
|
632
|
+
this.hooks = hooks;
|
|
633
|
+
}
|
|
634
|
+
async connect() {
|
|
635
|
+
const ready = await this.waitForReady();
|
|
636
|
+
if (!ready || this.closed)
|
|
637
|
+
return;
|
|
638
|
+
let catchupDone = false;
|
|
639
|
+
const buffered = [];
|
|
640
|
+
const wsUrl = await this.transport.buildEventsWebSocketUrl(this.context, this.runId);
|
|
641
|
+
const socket = new WebSocket(wsUrl);
|
|
642
|
+
this.socket = socket;
|
|
643
|
+
socket.onopen = () => {
|
|
644
|
+
this.hooks.onConnectionChange?.(true);
|
|
645
|
+
this.hooks.onStatusText?.("worker session live");
|
|
646
|
+
};
|
|
647
|
+
socket.onmessage = (message) => {
|
|
648
|
+
try {
|
|
649
|
+
const payload = typeof message.data === "string" ? JSON.parse(message.data) : JSON.parse(Buffer.from(message.data).toString("utf8"));
|
|
650
|
+
if (!catchupDone)
|
|
651
|
+
buffered.push(payload);
|
|
652
|
+
else
|
|
653
|
+
this.applyEnvelope(payload);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
this.hooks.onError?.(`Unparseable worker Pi event: ${error instanceof Error ? error.message : String(error)}`);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
socket.onerror = () => socket.close();
|
|
659
|
+
socket.onclose = () => {
|
|
660
|
+
this.hooks.onConnectionChange?.(false);
|
|
661
|
+
if (!this.closed)
|
|
662
|
+
this.hooks.onStatusText?.("worker Pi WebSocket disconnected");
|
|
663
|
+
};
|
|
664
|
+
try {
|
|
665
|
+
const [messagesPayload, statusPayload, commandsPayload] = await Promise.all([
|
|
666
|
+
this.transport.getMessages(this.context, this.runId),
|
|
667
|
+
this.transport.getStatus(this.context, this.runId),
|
|
668
|
+
this.transport.getCommands(this.context, this.runId)
|
|
669
|
+
]);
|
|
670
|
+
const messages = Array.isArray(messagesPayload.messages) ? messagesPayload.messages : [];
|
|
671
|
+
this.applyStatusPayload(statusPayload);
|
|
672
|
+
this.session?.replaceRemoteMessages(messages);
|
|
673
|
+
const commands = Array.isArray(commandsPayload.commands) ? commandsPayload.commands : [];
|
|
674
|
+
this.hooks.onCatchUp?.(messages, commands);
|
|
675
|
+
catchupDone = true;
|
|
676
|
+
for (const payload of buffered.splice(0))
|
|
677
|
+
this.applyEnvelope(payload);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
catchupDone = true;
|
|
680
|
+
this.hooks.onError?.(`Worker Pi catch-up failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
close() {
|
|
684
|
+
this.closed = true;
|
|
685
|
+
this.socket?.close();
|
|
686
|
+
for (const shell of this.pendingShells.splice(0))
|
|
687
|
+
shell.reject(new Error("Remote session closed."));
|
|
688
|
+
for (const compaction of this.pendingCompactions.splice(0))
|
|
689
|
+
compaction.reject(new Error("Remote session closed."));
|
|
690
|
+
}
|
|
691
|
+
async sendPrompt(text, streamingBehavior) {
|
|
692
|
+
await this.transport.sendPrompt(this.context, this.runId, text, streamingBehavior);
|
|
693
|
+
}
|
|
694
|
+
async sendCommand(text) {
|
|
695
|
+
const result = await this.transport.runCommand(this.context, this.runId, text);
|
|
696
|
+
return typeof result.message === "string" ? result.message : "worker command accepted";
|
|
697
|
+
}
|
|
698
|
+
async sendShell(text) {
|
|
699
|
+
await this.transport.sendShell(this.context, this.runId, text);
|
|
700
|
+
}
|
|
701
|
+
async abort() {
|
|
702
|
+
await this.transport.abort(this.context, this.runId);
|
|
703
|
+
}
|
|
704
|
+
registerPendingShell(shell) {
|
|
705
|
+
this.pendingShells.push(shell);
|
|
706
|
+
}
|
|
707
|
+
failPendingShell(shell, error) {
|
|
708
|
+
const index = this.pendingShells.indexOf(shell);
|
|
709
|
+
if (index !== -1)
|
|
710
|
+
this.pendingShells.splice(index, 1);
|
|
711
|
+
shell.reject(error);
|
|
712
|
+
}
|
|
713
|
+
registerPendingCompaction(pending) {
|
|
714
|
+
this.pendingCompactions.push(pending);
|
|
715
|
+
}
|
|
716
|
+
async waitForReady() {
|
|
717
|
+
const startedAt = Date.now();
|
|
718
|
+
const deadline = startedAt + resolveAttachReadyTimeoutMs();
|
|
719
|
+
let consecutiveFailures = 0;
|
|
720
|
+
while (!this.closed) {
|
|
721
|
+
let requestFailed = false;
|
|
722
|
+
const session = await this.transport.getSession(this.context, this.runId).catch((error) => {
|
|
723
|
+
requestFailed = true;
|
|
724
|
+
return { ready: false, status: error instanceof Error ? error.message : String(error), retryAfterMs: 1000 };
|
|
725
|
+
});
|
|
726
|
+
if (session.ready !== false)
|
|
727
|
+
return true;
|
|
728
|
+
consecutiveFailures = requestFailed ? consecutiveFailures + 1 : 0;
|
|
729
|
+
const status = String(session.status ?? "starting");
|
|
730
|
+
if (TERMINAL_RUN_STATUSES.has(status.toLowerCase())) {
|
|
731
|
+
this.hooks.onError?.(`Run ended before the worker Pi daemon became ready: ${status}. Inspect with \`rig run show ${this.runId}\`.`);
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
this.hooks.onStatusText?.(consecutiveFailures >= 5 ? "Rig server unreachable \xB7 retrying \xB7 /detach to exit" : `waiting for worker Pi daemon \xB7 ${status} \xB7 /detach to exit`);
|
|
735
|
+
if (Date.now() >= deadline) {
|
|
736
|
+
this.hooks.onError?.(`Worker Pi daemon did not become ready (last status: ${status}). Set RIG_PI_ATTACH_TIMEOUT_MS to wait longer.`);
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
await Bun.sleep(typeof session.retryAfterMs === "number" ? session.retryAfterMs : 750);
|
|
740
|
+
}
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
applyEnvelope(envelopeValue) {
|
|
744
|
+
const envelope = recordOf(envelopeValue);
|
|
745
|
+
if (!envelope)
|
|
746
|
+
return;
|
|
747
|
+
const type = String(envelope.type ?? "");
|
|
748
|
+
if (type === "status.update") {
|
|
749
|
+
this.applyStatusPayload(envelope);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (type === "activity.update") {
|
|
753
|
+
const activity = recordOf(envelope.activity);
|
|
754
|
+
this.hooks.onActivity?.(String(activity?.label ?? ""), activity?.detail ? String(activity.detail) : undefined);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (type === "extension_ui_request") {
|
|
758
|
+
const request = recordOf(envelope.request);
|
|
759
|
+
if (request)
|
|
760
|
+
this.hooks.onExtensionUiRequest?.(request);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (type === "pi.ui_event") {
|
|
764
|
+
this.applyShellUiEvent(envelope.event);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (type === "pi.event") {
|
|
768
|
+
this.session?.handleRemoteSessionEvent(envelope.event);
|
|
769
|
+
this.settlePendingCompaction(envelope.event);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (type === "error") {
|
|
773
|
+
this.hooks.onError?.(String(envelope.message ?? envelope.detail ?? "unknown worker error"));
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
applyStatusPayload(payload) {
|
|
777
|
+
const status = recordOf(payload.status) ?? payload;
|
|
778
|
+
const next = this.status;
|
|
779
|
+
next.isStreaming = status.isStreaming === true;
|
|
780
|
+
next.isCompacting = status.isCompacting === true;
|
|
781
|
+
next.isBashRunning = status.isBashRunning === true;
|
|
782
|
+
next.pendingMessageCount = typeof status.pendingMessageCount === "number" ? status.pendingMessageCount : 0;
|
|
783
|
+
next.steeringMessages = Array.isArray(status.steeringMessages) ? status.steeringMessages.map(String) : [];
|
|
784
|
+
next.followUpMessages = Array.isArray(status.followUpMessages) ? status.followUpMessages.map(String) : [];
|
|
785
|
+
next.model = recordOf(status.model) ?? next.model;
|
|
786
|
+
next.thinkingLevel = typeof status.thinkingLevel === "string" ? status.thinkingLevel : next.thinkingLevel;
|
|
787
|
+
next.sessionName = typeof status.sessionName === "string" ? status.sessionName : next.sessionName;
|
|
788
|
+
next.cwd = typeof status.cwd === "string" ? status.cwd : next.cwd;
|
|
789
|
+
next.stats = recordOf(status.stats) ?? next.stats;
|
|
790
|
+
next.contextUsage = recordOf(status.contextUsage) ?? next.contextUsage;
|
|
791
|
+
}
|
|
792
|
+
applyShellUiEvent(value) {
|
|
793
|
+
const event = recordOf(value);
|
|
794
|
+
if (!event)
|
|
795
|
+
return;
|
|
796
|
+
const type = String(event.type ?? "");
|
|
797
|
+
const pending = this.pendingShells[0];
|
|
798
|
+
if (type === "shell.chunk" && pending) {
|
|
799
|
+
pending.sawChunk = true;
|
|
800
|
+
pending.onData(Buffer.from(String(event.chunk ?? "")));
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (type === "shell.end" && pending) {
|
|
804
|
+
const output = String(event.output ?? "");
|
|
805
|
+
if (output && !pending.sawChunk)
|
|
806
|
+
pending.onData(Buffer.from(output));
|
|
807
|
+
const index = this.pendingShells.indexOf(pending);
|
|
808
|
+
if (index !== -1)
|
|
809
|
+
this.pendingShells.splice(index, 1);
|
|
810
|
+
pending.resolve({ exitCode: typeof event.exitCode === "number" ? event.exitCode : event.isError === true ? 1 : 0 });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
settlePendingCompaction(eventValue) {
|
|
814
|
+
const event = recordOf(eventValue);
|
|
815
|
+
if (!event)
|
|
816
|
+
return;
|
|
817
|
+
const type = String(event.type ?? "");
|
|
818
|
+
if (type !== "compaction_end" || this.pendingCompactions.length === 0)
|
|
819
|
+
return;
|
|
820
|
+
const pending = this.pendingCompactions.shift();
|
|
821
|
+
const result = recordOf(event.result);
|
|
822
|
+
if (result)
|
|
823
|
+
pending.resolve(result);
|
|
824
|
+
else if (event.aborted === true)
|
|
825
|
+
pending.reject(new Error("Compaction aborted on the worker."));
|
|
826
|
+
else
|
|
827
|
+
pending.resolve({});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
class RigRemoteAgentSession extends PiAgentSession {
|
|
832
|
+
remote;
|
|
833
|
+
constructor(config, remote) {
|
|
834
|
+
super(config);
|
|
835
|
+
this.remote = remote;
|
|
836
|
+
remote.bindSession(this);
|
|
837
|
+
}
|
|
838
|
+
handleRemoteSessionEvent(eventValue) {
|
|
839
|
+
const event = recordOf(eventValue);
|
|
840
|
+
if (!event)
|
|
841
|
+
return;
|
|
842
|
+
const type = String(event.type ?? "");
|
|
843
|
+
if ((type === "message_end" || type === "turn_end") && recordOf(event.message)) {
|
|
844
|
+
this.agent.state.messages = [...this.agent.state.messages, event.message];
|
|
845
|
+
}
|
|
846
|
+
this._emit(eventValue);
|
|
847
|
+
}
|
|
848
|
+
replaceRemoteMessages(messages) {
|
|
849
|
+
this.agent.state.messages = messages;
|
|
850
|
+
}
|
|
851
|
+
async prompt(text, options) {
|
|
852
|
+
const trimmed = text.trim();
|
|
853
|
+
if (!trimmed)
|
|
854
|
+
return;
|
|
855
|
+
if (trimmed.startsWith("/")) {
|
|
856
|
+
if (await this._tryExecuteExtensionCommand(trimmed))
|
|
857
|
+
return;
|
|
858
|
+
await this.remote.sendCommand(trimmed);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (trimmed.startsWith("!")) {
|
|
862
|
+
await this.remote.sendShell(trimmed);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const behavior = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
|
|
866
|
+
options?.preflightResult?.(true);
|
|
867
|
+
await this.remote.sendPrompt(trimmed, behavior);
|
|
868
|
+
}
|
|
869
|
+
async steer(text) {
|
|
870
|
+
await this.remote.sendPrompt(text, "steer");
|
|
871
|
+
}
|
|
872
|
+
async followUp(text) {
|
|
873
|
+
await this.remote.sendPrompt(text, "followUp");
|
|
874
|
+
}
|
|
875
|
+
async abort() {
|
|
876
|
+
await this.remote.abort();
|
|
877
|
+
}
|
|
878
|
+
async compact(customInstructions) {
|
|
879
|
+
const pending = new Promise((resolve3, reject) => {
|
|
880
|
+
this.remote.registerPendingCompaction({ resolve: resolve3, reject });
|
|
881
|
+
});
|
|
882
|
+
await this.remote.sendCommand(`/compact${customInstructions ? ` ${customInstructions}` : ""}`);
|
|
883
|
+
return pending;
|
|
884
|
+
}
|
|
885
|
+
clearQueue() {
|
|
886
|
+
const cleared = {
|
|
887
|
+
steering: [...this.remote.status.steeringMessages],
|
|
888
|
+
followUp: [...this.remote.status.followUpMessages]
|
|
889
|
+
};
|
|
890
|
+
this.remote.status.steeringMessages = [];
|
|
891
|
+
this.remote.status.followUpMessages = [];
|
|
892
|
+
this.remote.status.pendingMessageCount = 0;
|
|
893
|
+
this.remote.sendCommand("/queue-clear").catch(() => {});
|
|
894
|
+
return cleared;
|
|
895
|
+
}
|
|
896
|
+
get isStreaming() {
|
|
897
|
+
return this.remote.status.isStreaming;
|
|
898
|
+
}
|
|
899
|
+
get isCompacting() {
|
|
900
|
+
return this.remote.status.isCompacting;
|
|
901
|
+
}
|
|
902
|
+
get isBashRunning() {
|
|
903
|
+
return this.remote.status.isBashRunning;
|
|
904
|
+
}
|
|
905
|
+
get pendingMessageCount() {
|
|
906
|
+
return this.remote.status.pendingMessageCount;
|
|
907
|
+
}
|
|
908
|
+
getSteeringMessages() {
|
|
909
|
+
return this.remote.status.steeringMessages;
|
|
910
|
+
}
|
|
911
|
+
getFollowUpMessages() {
|
|
912
|
+
return this.remote.status.followUpMessages;
|
|
913
|
+
}
|
|
914
|
+
get model() {
|
|
915
|
+
return this.remote.status.model ?? super.model;
|
|
916
|
+
}
|
|
917
|
+
async setModel(model) {
|
|
918
|
+
await this.remote.sendCommand(`/model ${model.provider}/${model.id}`);
|
|
919
|
+
this.remote.status.model = model;
|
|
920
|
+
}
|
|
921
|
+
get thinkingLevel() {
|
|
922
|
+
return this.remote.status.thinkingLevel ?? super.thinkingLevel;
|
|
923
|
+
}
|
|
924
|
+
setThinkingLevel(level) {
|
|
925
|
+
this.remote.status.thinkingLevel = level;
|
|
926
|
+
this.remote.sendCommand(`/thinking ${level}`).catch(() => {});
|
|
927
|
+
}
|
|
928
|
+
get sessionName() {
|
|
929
|
+
return this.remote.status.sessionName ?? super.sessionName;
|
|
930
|
+
}
|
|
931
|
+
setSessionName(name) {
|
|
932
|
+
this.remote.status.sessionName = name;
|
|
933
|
+
this.remote.sendCommand(`/name ${name}`).catch(() => {});
|
|
934
|
+
}
|
|
935
|
+
getSessionStats() {
|
|
936
|
+
return this.remote.status.stats ?? super.getSessionStats();
|
|
937
|
+
}
|
|
938
|
+
getContextUsage() {
|
|
939
|
+
return this.remote.status.contextUsage ?? super.getContextUsage();
|
|
940
|
+
}
|
|
941
|
+
dispose() {
|
|
942
|
+
this.remote.close();
|
|
943
|
+
super.dispose();
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function createRemoteBashOperations(controller, excludeFromContext) {
|
|
947
|
+
return {
|
|
948
|
+
exec(command, _cwd, execOptions) {
|
|
949
|
+
return new Promise((resolve3, reject) => {
|
|
950
|
+
const pending = { onData: execOptions.onData, resolve: resolve3, reject, sawChunk: false };
|
|
951
|
+
const cleanup = () => {
|
|
952
|
+
execOptions.signal?.removeEventListener("abort", onAbort);
|
|
953
|
+
if (timer)
|
|
954
|
+
clearTimeout(timer);
|
|
955
|
+
};
|
|
956
|
+
const onAbort = () => {
|
|
957
|
+
cleanup();
|
|
958
|
+
controller.failPendingShell(pending, new Error("Remote worker shell command aborted locally."));
|
|
959
|
+
};
|
|
960
|
+
const timeoutMs = typeof execOptions.timeout === "number" && execOptions.timeout > 0 ? execOptions.timeout : 0;
|
|
961
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
962
|
+
cleanup();
|
|
963
|
+
controller.failPendingShell(pending, new Error(`Remote worker shell command timed out after ${timeoutMs}ms.`));
|
|
964
|
+
}, timeoutMs) : null;
|
|
965
|
+
const wrappedResolve = pending.resolve;
|
|
966
|
+
const wrappedReject = pending.reject;
|
|
967
|
+
pending.resolve = (result) => {
|
|
968
|
+
cleanup();
|
|
969
|
+
wrappedResolve(result);
|
|
970
|
+
};
|
|
971
|
+
pending.reject = (error) => {
|
|
972
|
+
cleanup();
|
|
973
|
+
wrappedReject(error);
|
|
974
|
+
};
|
|
975
|
+
execOptions.signal?.addEventListener("abort", onAbort, { once: true });
|
|
976
|
+
controller.registerPendingShell(pending);
|
|
977
|
+
controller.sendShell(`${excludeFromContext ? "!!" : "!"}${command}`).catch((error) => {
|
|
978
|
+
cleanup();
|
|
979
|
+
controller.failPendingShell(pending, error instanceof Error ? error : new Error(String(error)));
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// packages/cli/src/commands/_pi-worker-bridge-extension.ts
|
|
987
|
+
function recordOf2(value) {
|
|
988
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
989
|
+
}
|
|
990
|
+
function asText(value) {
|
|
991
|
+
if (typeof value === "string")
|
|
992
|
+
return value;
|
|
993
|
+
if (value === null || value === undefined)
|
|
994
|
+
return "";
|
|
995
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
996
|
+
return String(value);
|
|
997
|
+
try {
|
|
998
|
+
return JSON.stringify(value);
|
|
999
|
+
} catch {
|
|
1000
|
+
return String(value);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
async function answerExtensionUiRequest(options, ctx, request) {
|
|
1004
|
+
const requestId = String(request.requestId ?? request.id ?? `ui-${Date.now()}`);
|
|
1005
|
+
const method = String(request.method ?? request.type ?? "input");
|
|
1006
|
+
const prompt = asText(request.prompt ?? request.message ?? request.title ?? method);
|
|
1007
|
+
const rawOptions = Array.isArray(request.options) ? request.options : Array.isArray(request.items) ? request.items : [];
|
|
1008
|
+
const choices = rawOptions.map((option) => {
|
|
1009
|
+
const record = recordOf2(option);
|
|
1010
|
+
return record ? asText(record.label ?? record.value ?? record.name ?? option) : asText(option);
|
|
1011
|
+
}).filter(Boolean);
|
|
1012
|
+
try {
|
|
1013
|
+
if (method === "confirm") {
|
|
1014
|
+
const confirmed = await ctx.ui.confirm("Worker request", prompt);
|
|
1015
|
+
await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, { value: confirmed, confirmed });
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (choices.length > 0) {
|
|
1019
|
+
const selected = await ctx.ui.select(prompt, choices);
|
|
1020
|
+
await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, selected === undefined ? { cancelled: true } : { value: selected });
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const value = await ctx.ui.input("Worker request", prompt);
|
|
1024
|
+
await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, value === undefined ? { cancelled: true } : { value });
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
ctx.ui.notify(`Failed to answer worker UI request: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
var LOCALLY_OWNED_COMMANDS = new Set(["detach", "quit", "q", "stop", "settings", "model", "thinking", "compact", "session", "name", "reload"]);
|
|
1030
|
+
function registerDaemonCommands(pi, options, ctx, commands, registered) {
|
|
1031
|
+
for (const command of commands) {
|
|
1032
|
+
const record = recordOf2(command);
|
|
1033
|
+
const name = typeof record?.name === "string" ? record.name : "";
|
|
1034
|
+
const source = typeof record?.source === "string" ? record.source : "worker";
|
|
1035
|
+
if (!name || source === "builtin" || registered.has(name) || LOCALLY_OWNED_COMMANDS.has(name))
|
|
1036
|
+
continue;
|
|
1037
|
+
registered.add(name);
|
|
1038
|
+
const description = typeof record?.description === "string" ? record.description : undefined;
|
|
1039
|
+
try {
|
|
1040
|
+
pi.registerCommand(name, {
|
|
1041
|
+
description: `[worker ${source}] ${description ?? ""}`.trim(),
|
|
1042
|
+
handler: async (args) => {
|
|
1043
|
+
try {
|
|
1044
|
+
const message = await options.controller.sendCommand(`/${name}${args ? ` ${args}` : ""}`);
|
|
1045
|
+
ctx.ui.notify(message, "info");
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
} catch {}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function createRigWorkerPiBridgeExtension(options) {
|
|
1055
|
+
return (pi) => {
|
|
1056
|
+
const registeredDaemonCommands = new Set;
|
|
1057
|
+
pi.registerCommand("detach", {
|
|
1058
|
+
description: "Detach from this run; the worker keeps going",
|
|
1059
|
+
handler: async (_args, ctx) => {
|
|
1060
|
+
ctx.ui.notify("Detached locally; the worker Pi daemon continues.", "info");
|
|
1061
|
+
ctx.shutdown();
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
pi.registerCommand("stop", {
|
|
1065
|
+
description: "Stop the worker Pi run and detach",
|
|
1066
|
+
handler: async (_args, ctx) => {
|
|
1067
|
+
await options.controller.abort();
|
|
1068
|
+
ctx.ui.notify("Stop requested for the worker Pi daemon.", "info");
|
|
1069
|
+
ctx.shutdown();
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
pi.on("user_bash", (event) => ({
|
|
1073
|
+
operations: createRemoteBashOperations(options.controller, event.excludeFromContext === true)
|
|
1074
|
+
}));
|
|
1075
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1076
|
+
ctx.ui.setTitle("Rig \xB7 worker session");
|
|
1077
|
+
ctx.ui.setStatus("rig-worker-pi", "connecting to worker session");
|
|
1078
|
+
ctx.ui.notify(`Attached to Rig run ${options.runId}. Native Pi, remote brain \u2014 /detach exits, /stop cancels the run.`, "info");
|
|
1079
|
+
if (options.initialMessageSent)
|
|
1080
|
+
ctx.ui.notify("Initial message sent to the worker Pi daemon.", "info");
|
|
1081
|
+
const nativeUi = ctx.ui;
|
|
1082
|
+
options.controller.setUiHooks({
|
|
1083
|
+
onStatusText: (text) => ctx.ui.setStatus("rig-worker-pi", text),
|
|
1084
|
+
onActivity: (label, detail) => ctx.ui.setStatus("rig-worker-pi", [label, detail].filter(Boolean).join(" \u2014 ")),
|
|
1085
|
+
onConnectionChange: (connected) => ctx.ui.setStatus("rig-worker-pi", connected ? "worker session live" : "worker session disconnected"),
|
|
1086
|
+
onError: (message) => ctx.ui.notify(message, "error"),
|
|
1087
|
+
onExtensionUiRequest: (request) => {
|
|
1088
|
+
answerExtensionUiRequest(options, ctx, request);
|
|
1089
|
+
},
|
|
1090
|
+
onCatchUp: (messages, commands) => {
|
|
1091
|
+
if (nativeUi.appendSessionMessages)
|
|
1092
|
+
nativeUi.appendSessionMessages(messages);
|
|
1093
|
+
const cwd = options.controller.status.cwd;
|
|
1094
|
+
if (nativeUi.setDisplayCwd && cwd)
|
|
1095
|
+
nativeUi.setDisplayCwd(cwd);
|
|
1096
|
+
registerDaemonCommands(pi, options, ctx, commands, registeredDaemonCommands);
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
options.controller.connect().catch((error) => {
|
|
1100
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
pi.on("session_shutdown", () => {
|
|
1104
|
+
options.controller.close();
|
|
1105
|
+
});
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// packages/cli/src/commands/_pi-frontend.ts
|
|
1110
|
+
function setTemporaryEnv(updates) {
|
|
1111
|
+
const previous = new Map;
|
|
1112
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
1113
|
+
previous.set(key, process.env[key]);
|
|
1114
|
+
process.env[key] = value;
|
|
1115
|
+
}
|
|
1116
|
+
return () => {
|
|
1117
|
+
for (const [key, value] of previous) {
|
|
1118
|
+
if (value === undefined)
|
|
1119
|
+
delete process.env[key];
|
|
1120
|
+
else
|
|
1121
|
+
process.env[key] = value;
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
async function attachRunBundledPiFrontend(context, input) {
|
|
1126
|
+
const tempSessionDir = mkdtempSync(join(tmpdir(), "rig-pi-frontend-sessions-"));
|
|
1127
|
+
const restoreEnv = setTemporaryEnv({
|
|
1128
|
+
PI_CODING_AGENT_SESSION_DIR: tempSessionDir,
|
|
1129
|
+
PI_SKIP_VERSION_CHECK: "1"
|
|
1130
|
+
});
|
|
1131
|
+
const controller = new RigRemoteSessionController({ context, runId: input.runId });
|
|
1132
|
+
const createRemoteRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
|
|
1133
|
+
const services = await createAgentSessionServices({
|
|
1134
|
+
cwd,
|
|
1135
|
+
agentDir,
|
|
1136
|
+
resourceLoaderOptions: {
|
|
1137
|
+
extensionFactories: [
|
|
1138
|
+
createRigWorkerPiBridgeExtension({
|
|
1139
|
+
context,
|
|
1140
|
+
controller,
|
|
1141
|
+
runId: input.runId,
|
|
1142
|
+
initialMessageSent: input.steered === true
|
|
1143
|
+
})
|
|
1144
|
+
],
|
|
1145
|
+
noContextFiles: true
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
const created = await createAgentSessionFromServices({
|
|
1149
|
+
services,
|
|
1150
|
+
sessionManager,
|
|
1151
|
+
sessionStartEvent,
|
|
1152
|
+
noTools: "all",
|
|
1153
|
+
sessionFactory: (config) => new RigRemoteAgentSession(config, controller)
|
|
1154
|
+
});
|
|
1155
|
+
return { ...created, services, diagnostics: services.diagnostics };
|
|
1156
|
+
};
|
|
1157
|
+
let detached = false;
|
|
1158
|
+
try {
|
|
1159
|
+
await runPiMain([], {
|
|
1160
|
+
createRuntimeOverride: () => createRemoteRuntime
|
|
1161
|
+
});
|
|
1162
|
+
detached = true;
|
|
1163
|
+
} finally {
|
|
1164
|
+
restoreEnv();
|
|
1165
|
+
controller.close();
|
|
1166
|
+
rmSync(tempSessionDir, { recursive: true, force: true });
|
|
1167
|
+
}
|
|
1168
|
+
let run = { runId: input.runId, status: "unknown" };
|
|
1169
|
+
try {
|
|
1170
|
+
run = await getRunDetailsViaServer(context, input.runId);
|
|
1171
|
+
} catch {}
|
|
1172
|
+
return {
|
|
1173
|
+
run,
|
|
1174
|
+
logs: [],
|
|
1175
|
+
timeline: [],
|
|
1176
|
+
timelineCursor: null,
|
|
1177
|
+
steered: input.steered === true,
|
|
1178
|
+
detached,
|
|
1179
|
+
rendered: "native bundled Pi frontend with remote worker session runtime"
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// packages/cli/src/commands/_operator-view.ts
|
|
1184
|
+
var TERMINAL_RUN_STATUSES2 = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
338
1185
|
function runStatusFromPayload(payload) {
|
|
339
1186
|
const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
340
1187
|
return String(run.status ?? "unknown").toLowerCase();
|
|
@@ -356,56 +1203,268 @@ async function applyOperatorCommand(context, input, deps = {}) {
|
|
|
356
1203
|
await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
|
|
357
1204
|
return { action: "continue", message: "Steering message queued." };
|
|
358
1205
|
}
|
|
359
|
-
async function readOperatorSnapshot(context, runId) {
|
|
1206
|
+
async function readOperatorSnapshot(context, runId, options = {}) {
|
|
360
1207
|
const run = await getRunDetailsViaServer(context, runId);
|
|
361
1208
|
const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
|
|
362
|
-
const
|
|
363
|
-
|
|
1209
|
+
const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
|
|
1210
|
+
entries: [{
|
|
1211
|
+
id: `timeline-unavailable:${runId}`,
|
|
1212
|
+
type: "timeline_warning",
|
|
1213
|
+
detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
|
|
1214
|
+
createdAt: new Date().toISOString()
|
|
1215
|
+
}],
|
|
1216
|
+
nextCursor: options.timelineCursor ?? null
|
|
1217
|
+
}));
|
|
1218
|
+
const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
|
|
1219
|
+
const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1220
|
+
const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
|
|
1221
|
+
return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
|
|
364
1222
|
}
|
|
365
1223
|
async function attachRunOperatorView(context, input) {
|
|
366
1224
|
let steered = false;
|
|
367
1225
|
if (input.message?.trim()) {
|
|
368
|
-
await steerRunViaServer(context, input.runId, input.message.trim());
|
|
1226
|
+
await sendRunPiPromptViaServer(context, input.runId, input.message.trim(), "steer").catch(() => steerRunViaServer(context, input.runId, input.message.trim()));
|
|
369
1227
|
steered = true;
|
|
370
1228
|
}
|
|
1229
|
+
if (input.follow && !input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
1230
|
+
return attachRunBundledPiFrontend(context, {
|
|
1231
|
+
runId: input.runId,
|
|
1232
|
+
steered
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
const surface = createOperatorSurface({ interactive: input.interactive !== false });
|
|
371
1236
|
let snapshot = await readOperatorSnapshot(context, input.runId);
|
|
372
1237
|
if (context.outputMode === "text") {
|
|
373
|
-
|
|
1238
|
+
surface.renderSnapshot(snapshot);
|
|
1239
|
+
surface.renderTimeline(snapshot.timeline);
|
|
1240
|
+
surface.renderLogs(snapshot.logs);
|
|
374
1241
|
if (steered)
|
|
375
|
-
|
|
1242
|
+
surface.info("Message submitted to worker Pi.");
|
|
376
1243
|
}
|
|
377
1244
|
let detached = false;
|
|
378
|
-
let
|
|
1245
|
+
let commandInput = null;
|
|
379
1246
|
if (input.follow && !input.once && context.outputMode === "text") {
|
|
380
1247
|
if (input.interactive !== false && process.stdin.isTTY) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
}).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
1248
|
+
surface.info("Controls: /user <message>, /stop, /detach");
|
|
1249
|
+
commandInput = surface.attachCommandInput(async (line) => {
|
|
1250
|
+
const result = await applyOperatorCommand(context, { runId: input.runId, line });
|
|
1251
|
+
if (result.message)
|
|
1252
|
+
surface.info(result.message);
|
|
1253
|
+
if (result.action === "detach" || result.action === "stopped") {
|
|
1254
|
+
detached = true;
|
|
1255
|
+
commandInput?.close();
|
|
1256
|
+
}
|
|
392
1257
|
});
|
|
393
1258
|
}
|
|
394
|
-
let lastRendered = snapshot.rendered;
|
|
395
1259
|
const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
|
|
396
|
-
|
|
1260
|
+
let timelineCursor = snapshot.timelineCursor;
|
|
1261
|
+
while (!detached && !TERMINAL_RUN_STATUSES2.has(runStatusFromPayload(snapshot.run))) {
|
|
397
1262
|
await Bun.sleep(pollMs);
|
|
398
|
-
snapshot = await readOperatorSnapshot(context, input.runId);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
1263
|
+
snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
|
|
1264
|
+
timelineCursor = snapshot.timelineCursor;
|
|
1265
|
+
surface.renderSnapshot(snapshot);
|
|
1266
|
+
surface.renderTimeline(snapshot.timeline);
|
|
1267
|
+
surface.renderLogs(snapshot.logs);
|
|
403
1268
|
}
|
|
404
|
-
|
|
1269
|
+
commandInput?.close();
|
|
405
1270
|
}
|
|
406
1271
|
return { ...snapshot, steered, detached };
|
|
407
1272
|
}
|
|
408
1273
|
|
|
1274
|
+
// packages/cli/src/commands/_cli-format.ts
|
|
1275
|
+
import { log, note } from "@clack/prompts";
|
|
1276
|
+
import pc from "picocolors";
|
|
1277
|
+
function stringField(record, key, fallback = "") {
|
|
1278
|
+
const value = record[key];
|
|
1279
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
1280
|
+
}
|
|
1281
|
+
function rawObject(record) {
|
|
1282
|
+
const raw = record.raw;
|
|
1283
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
1284
|
+
}
|
|
1285
|
+
function truncate(value, width) {
|
|
1286
|
+
if (value.length <= width)
|
|
1287
|
+
return value;
|
|
1288
|
+
if (width <= 1)
|
|
1289
|
+
return "\u2026";
|
|
1290
|
+
return `${value.slice(0, width - 1)}\u2026`;
|
|
1291
|
+
}
|
|
1292
|
+
function pad(value, width) {
|
|
1293
|
+
return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
|
|
1294
|
+
}
|
|
1295
|
+
function statusColor(status) {
|
|
1296
|
+
const normalized = status.toLowerCase();
|
|
1297
|
+
if (["completed", "merged", "closed", "done", "accepted", "pass", "selected", "approved"].includes(normalized))
|
|
1298
|
+
return pc.green;
|
|
1299
|
+
if (["failed", "needs_attention", "needs-attention", "blocked", "error", "rejected"].includes(normalized))
|
|
1300
|
+
return pc.red;
|
|
1301
|
+
if (["running", "reviewing", "validating", "in_progress", "in-progress", "remote"].includes(normalized))
|
|
1302
|
+
return pc.cyan;
|
|
1303
|
+
if (["ready", "open", "queued", "created", "preparing", "local", "pending"].includes(normalized))
|
|
1304
|
+
return pc.yellow;
|
|
1305
|
+
return pc.dim;
|
|
1306
|
+
}
|
|
1307
|
+
function compactDate(value) {
|
|
1308
|
+
if (!value.trim())
|
|
1309
|
+
return "";
|
|
1310
|
+
const parsed = Date.parse(value);
|
|
1311
|
+
if (!Number.isFinite(parsed))
|
|
1312
|
+
return value;
|
|
1313
|
+
return new Date(parsed).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
|
|
1314
|
+
}
|
|
1315
|
+
function firstString(record, keys, fallback = "") {
|
|
1316
|
+
for (const key of keys) {
|
|
1317
|
+
const value = stringField(record, key);
|
|
1318
|
+
if (value)
|
|
1319
|
+
return value;
|
|
1320
|
+
}
|
|
1321
|
+
return fallback;
|
|
1322
|
+
}
|
|
1323
|
+
function runIdOf(run) {
|
|
1324
|
+
return firstString(run, ["runId", "id"], "(unknown-run)");
|
|
1325
|
+
}
|
|
1326
|
+
function taskIdOf(run) {
|
|
1327
|
+
return firstString(run, ["taskId", "task", "task_id"]);
|
|
1328
|
+
}
|
|
1329
|
+
function runTitleOf(run) {
|
|
1330
|
+
return firstString(run, ["title", "summary", "name"], taskIdOf(run) || "(untitled)");
|
|
1331
|
+
}
|
|
1332
|
+
function shouldUseClackOutput() {
|
|
1333
|
+
return Boolean(process.stdout.isTTY) && process.env.RIG_CLI_PLAIN_HELP !== "1";
|
|
1334
|
+
}
|
|
1335
|
+
function printFormattedOutput(message, options = {}) {
|
|
1336
|
+
if (!shouldUseClackOutput()) {
|
|
1337
|
+
console.log(message);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (options.title)
|
|
1341
|
+
note(message, options.title);
|
|
1342
|
+
else
|
|
1343
|
+
log.message(message);
|
|
1344
|
+
}
|
|
1345
|
+
function formatStatusPill(status) {
|
|
1346
|
+
const label = status || "unknown";
|
|
1347
|
+
return statusColor(label)(`\u25CF ${label}`);
|
|
1348
|
+
}
|
|
1349
|
+
function formatSection(title, subtitle) {
|
|
1350
|
+
return `${pc.bold(pc.cyan("\u25C6"))} ${pc.bold(title)}${subtitle ? pc.dim(` \u2014 ${subtitle}`) : ""}`;
|
|
1351
|
+
}
|
|
1352
|
+
function formatSuccessCard(title, rows = []) {
|
|
1353
|
+
const body = rows.filter(([, value]) => value !== undefined && value !== null && String(value).length > 0).map(([key, value]) => `${pc.dim("\u2502")} ${pc.dim(key.padEnd(12))} ${value}`);
|
|
1354
|
+
return [formatSection(title), ...body].join(`
|
|
1355
|
+
`);
|
|
1356
|
+
}
|
|
1357
|
+
function formatNextSteps(steps) {
|
|
1358
|
+
if (steps.length === 0)
|
|
1359
|
+
return [];
|
|
1360
|
+
return [pc.bold("Next"), ...steps.map((step) => `${pc.dim("\u203A")} ${step}`)];
|
|
1361
|
+
}
|
|
1362
|
+
function formatRunList(runs, options = {}) {
|
|
1363
|
+
if (runs.length === 0) {
|
|
1364
|
+
return [
|
|
1365
|
+
formatSection("Runs", "none recorded"),
|
|
1366
|
+
options.source === "server" ? pc.dim("No runs recorded on the selected Rig server.") : pc.dim("No runs recorded in .rig/runs."),
|
|
1367
|
+
"",
|
|
1368
|
+
...formatNextSteps(["Start one: `rig task run --next`", "Check server: `rig server status`"])
|
|
1369
|
+
].join(`
|
|
1370
|
+
`);
|
|
1371
|
+
}
|
|
1372
|
+
const rows = runs.map((run) => {
|
|
1373
|
+
const runId = stringField(run, "runId", stringField(run, "id", "(unknown-run)"));
|
|
1374
|
+
const status = stringField(run, "status", "unknown");
|
|
1375
|
+
const taskId = stringField(run, "taskId", "");
|
|
1376
|
+
const title = stringField(run, "title", taskId || "(untitled)");
|
|
1377
|
+
const runtime = stringField(run, "runtimeAdapter", "");
|
|
1378
|
+
return { runId, status, title, runtime };
|
|
1379
|
+
});
|
|
1380
|
+
const idWidth = Math.min(36, Math.max(6, ...rows.map((row) => row.runId.length)));
|
|
1381
|
+
const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
|
|
1382
|
+
const header = `${pc.bold(pad("RUN", idWidth))} ${pc.bold(pad("STATUS", statusWidth))} ${pc.bold("TITLE")}`;
|
|
1383
|
+
const body = rows.map((row) => [
|
|
1384
|
+
pc.bold(pad(truncate(row.runId, idWidth), idWidth)),
|
|
1385
|
+
statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
|
|
1386
|
+
`${row.title}${row.runtime ? pc.dim(` ${row.runtime}`) : ""}`
|
|
1387
|
+
].join(" "));
|
|
1388
|
+
return [formatSection("Runs", options.source === "server" ? "selected server" : "local state"), header, ...body, "", ...formatNextSteps(["Follow live: `rig run attach <run-id> --follow`", "Details: `rig run show <run-id>`"])].join(`
|
|
1389
|
+
`);
|
|
1390
|
+
}
|
|
1391
|
+
function formatRunCard(run, options = {}) {
|
|
1392
|
+
const raw = rawObject(run);
|
|
1393
|
+
const merged = { ...raw, ...run };
|
|
1394
|
+
const runId = runIdOf(merged);
|
|
1395
|
+
const status = firstString(merged, ["status"], "unknown");
|
|
1396
|
+
const taskId = taskIdOf(merged);
|
|
1397
|
+
const title = runTitleOf(merged);
|
|
1398
|
+
const runtime = firstString(merged, ["runtimeAdapter", "runtime", "adapter"]);
|
|
1399
|
+
const mode = firstString(merged, ["runtimeMode", "mode"]);
|
|
1400
|
+
const interaction = firstString(merged, ["interactionMode"]);
|
|
1401
|
+
const created = compactDate(firstString(merged, ["createdAt"]));
|
|
1402
|
+
const started = compactDate(firstString(merged, ["startedAt"]));
|
|
1403
|
+
const updated = compactDate(firstString(merged, ["updatedAt"]));
|
|
1404
|
+
const completed = compactDate(firstString(merged, ["completedAt", "finishedAt"]));
|
|
1405
|
+
const worktree = firstString(merged, ["worktreePath", "cwd", "projectRoot"]);
|
|
1406
|
+
const piSession = merged.piSession && typeof merged.piSession === "object" && !Array.isArray(merged.piSession) ? firstString(merged.piSession, ["sessionId", "id"]) : "";
|
|
1407
|
+
const timeline = Array.isArray(merged.timeline) ? merged.timeline.length : null;
|
|
1408
|
+
const approvals = Array.isArray(merged.approvals) ? merged.approvals.length : null;
|
|
1409
|
+
const inputs = Array.isArray(merged.userInputs) ? merged.userInputs.length : null;
|
|
1410
|
+
const rows = [
|
|
1411
|
+
["run", pc.bold(runId)],
|
|
1412
|
+
["status", formatStatusPill(status)],
|
|
1413
|
+
["task", taskId],
|
|
1414
|
+
["title", title],
|
|
1415
|
+
["runtime", [runtime, mode, interaction].filter(Boolean).join(" \xB7 ")],
|
|
1416
|
+
["created", created],
|
|
1417
|
+
["started", started],
|
|
1418
|
+
["updated", updated],
|
|
1419
|
+
["completed", completed],
|
|
1420
|
+
["worktree", worktree],
|
|
1421
|
+
["pi", piSession],
|
|
1422
|
+
["timeline", timeline],
|
|
1423
|
+
["approvals", approvals],
|
|
1424
|
+
["inputs", inputs]
|
|
1425
|
+
];
|
|
1426
|
+
return [
|
|
1427
|
+
formatSuccessCard(options.title ?? "Run details", rows),
|
|
1428
|
+
"",
|
|
1429
|
+
...formatNextSteps([`Follow live: \`rig run attach ${runId} --follow\``, `Raw payload: \`rig run show ${runId} --raw\``])
|
|
1430
|
+
].join(`
|
|
1431
|
+
`);
|
|
1432
|
+
}
|
|
1433
|
+
function formatRunStatus(summary, options = {}) {
|
|
1434
|
+
const activeRuns = summary.activeRuns ?? [];
|
|
1435
|
+
const recentRuns = summary.recentRuns ?? [];
|
|
1436
|
+
const lines = [formatSection("Run status", options.source === "server" ? "selected server" : "local state")];
|
|
1437
|
+
lines.push("", pc.bold(`Active runs (${activeRuns.length})`));
|
|
1438
|
+
if (activeRuns.length === 0) {
|
|
1439
|
+
lines.push(pc.dim("No active runs."));
|
|
1440
|
+
} else {
|
|
1441
|
+
for (const run of activeRuns) {
|
|
1442
|
+
lines.push(formatRunSummaryLine(run));
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
lines.push("", pc.bold(`Recent runs (${recentRuns.length})`));
|
|
1446
|
+
if (recentRuns.length === 0) {
|
|
1447
|
+
lines.push(pc.dim("No recent terminal runs."));
|
|
1448
|
+
} else {
|
|
1449
|
+
for (const run of recentRuns.slice(0, 10)) {
|
|
1450
|
+
lines.push(formatRunSummaryLine(run));
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
lines.push("", ...formatNextSteps(["Start work: `rig task run --next`", "Attach: `rig run attach <run-id> --follow`", "Details: `rig run show <run-id>`"]));
|
|
1454
|
+
return lines.join(`
|
|
1455
|
+
`);
|
|
1456
|
+
}
|
|
1457
|
+
function formatRunSummaryLine(run) {
|
|
1458
|
+
const record = run;
|
|
1459
|
+
const runId = runIdOf(record);
|
|
1460
|
+
const status = firstString(record, ["status"], "unknown");
|
|
1461
|
+
const taskId = taskIdOf(record);
|
|
1462
|
+
const title = runTitleOf(record);
|
|
1463
|
+
const runtime = firstString(record, ["runtimeAdapter", "runtime", "adapter"]);
|
|
1464
|
+
const descriptor = [taskId, title].filter(Boolean).join(" \xB7 ");
|
|
1465
|
+
return `${pc.dim("\u2502")} ${pc.bold(runId)} ${formatStatusPill(status)} ${descriptor}${runtime ? pc.dim(` ${runtime}`) : ""}`;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
409
1468
|
// packages/cli/src/commands/run.ts
|
|
410
1469
|
function normalizeRemoteRunDetails(payload) {
|
|
411
1470
|
const run = payload.run;
|
|
@@ -418,6 +1477,25 @@ function normalizeRemoteRunDetails(payload) {
|
|
|
418
1477
|
...Array.isArray(payload.userInputs) ? { userInputs: payload.userInputs } : {}
|
|
419
1478
|
};
|
|
420
1479
|
}
|
|
1480
|
+
var REMOTE_TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged"]);
|
|
1481
|
+
function isRemoteConnectionSelected(projectRoot) {
|
|
1482
|
+
return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
|
|
1483
|
+
}
|
|
1484
|
+
async function listRunsForSelectedConnection(context, options = {}) {
|
|
1485
|
+
if (isRemoteConnectionSelected(context.projectRoot)) {
|
|
1486
|
+
return { runs: await listRunsViaServer(context, options), source: "server" };
|
|
1487
|
+
}
|
|
1488
|
+
return { runs: listAuthorityRuns(context.projectRoot), source: "local" };
|
|
1489
|
+
}
|
|
1490
|
+
function runStringField(run, key, fallback = "") {
|
|
1491
|
+
const value = run[key];
|
|
1492
|
+
return typeof value === "string" && value.trim() ? value : fallback;
|
|
1493
|
+
}
|
|
1494
|
+
function buildServerRunStatus(runs) {
|
|
1495
|
+
const activeRuns = runs.filter((run) => !REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
|
|
1496
|
+
const recentRuns = runs.filter((run) => REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
|
|
1497
|
+
return { activeRuns, recentRuns, runs };
|
|
1498
|
+
}
|
|
421
1499
|
function shouldPromptForEpicSelection(context, command, promptEpic, noEpicPrompt) {
|
|
422
1500
|
if (noEpicPrompt) {
|
|
423
1501
|
return false;
|
|
@@ -479,21 +1557,15 @@ async function promptForEpicSelection(projectRoot, command) {
|
|
|
479
1557
|
}
|
|
480
1558
|
async function executeRun(context, args) {
|
|
481
1559
|
const [command = "status", ...rest] = args;
|
|
482
|
-
const runtimeContext =
|
|
1560
|
+
const runtimeContext = loadRuntimeContextFromEnv() ?? undefined;
|
|
483
1561
|
switch (command) {
|
|
484
1562
|
case "list": {
|
|
485
|
-
requireNoExtraArgs(rest, "
|
|
486
|
-
const runs =
|
|
1563
|
+
requireNoExtraArgs(rest, "rig run list");
|
|
1564
|
+
const { runs, source } = await listRunsForSelectedConnection(context, { limit: 100 });
|
|
487
1565
|
if (context.outputMode === "text") {
|
|
488
|
-
|
|
489
|
-
console.log("No runs recorded in .rig/runs.");
|
|
490
|
-
} else {
|
|
491
|
-
for (const run of runs) {
|
|
492
|
-
console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.title}`);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
1566
|
+
printFormattedOutput(formatRunList(runs, { source }));
|
|
495
1567
|
}
|
|
496
|
-
return { ok: true, group: "run", command, details: { runs } };
|
|
1568
|
+
return { ok: true, group: "run", command, details: { runs, source } };
|
|
497
1569
|
}
|
|
498
1570
|
case "delete": {
|
|
499
1571
|
let pending = rest;
|
|
@@ -501,7 +1573,7 @@ async function executeRun(context, args) {
|
|
|
501
1573
|
pending = run.rest;
|
|
502
1574
|
const purgeArtifacts = takeFlag(pending, "--purge-artifacts");
|
|
503
1575
|
pending = purgeArtifacts.rest;
|
|
504
|
-
requireNoExtraArgs(pending, "
|
|
1576
|
+
requireNoExtraArgs(pending, "rig run delete --run <id> [--purge-artifacts]");
|
|
505
1577
|
if (!run.value) {
|
|
506
1578
|
throw new CliError2("run delete requires --run <id>.");
|
|
507
1579
|
}
|
|
@@ -531,7 +1603,7 @@ async function executeRun(context, args) {
|
|
|
531
1603
|
pending = keepRuntimes.rest;
|
|
532
1604
|
const keepQueue = takeFlag(pending, "--keep-queue");
|
|
533
1605
|
pending = keepQueue.rest;
|
|
534
|
-
requireNoExtraArgs(pending, "
|
|
1606
|
+
requireNoExtraArgs(pending, "rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
|
|
535
1607
|
if (!all.value) {
|
|
536
1608
|
throw new CliError2("run cleanup currently requires --all.");
|
|
537
1609
|
}
|
|
@@ -550,20 +1622,25 @@ async function executeRun(context, args) {
|
|
|
550
1622
|
}
|
|
551
1623
|
case "show": {
|
|
552
1624
|
let pending = rest;
|
|
1625
|
+
const rawResult = takeFlag(pending, "--raw");
|
|
1626
|
+
pending = rawResult.rest;
|
|
553
1627
|
const run = takeOption(pending, "--run");
|
|
554
1628
|
pending = run.rest;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1629
|
+
const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
|
|
1630
|
+
const extra = positionalRunId ? pending.slice(1) : pending;
|
|
1631
|
+
requireNoExtraArgs(extra, "rig run show <id>|--run <id> [--raw]");
|
|
1632
|
+
const runId = run.value ?? positionalRunId;
|
|
1633
|
+
if (!runId) {
|
|
1634
|
+
throw new CliError2("run show requires a run id.");
|
|
558
1635
|
}
|
|
559
|
-
const record = readAuthorityRun(context.projectRoot,
|
|
1636
|
+
const record = readAuthorityRun(context.projectRoot, runId) ?? normalizeRemoteRunDetails(await getRunDetailsViaServer(context, runId).catch(() => ({})));
|
|
560
1637
|
if (!record) {
|
|
561
|
-
throw new CliError2(`Run not found: ${
|
|
1638
|
+
throw new CliError2(`Run not found: ${runId}`, 2);
|
|
562
1639
|
}
|
|
563
1640
|
if (context.outputMode === "text") {
|
|
564
|
-
|
|
1641
|
+
printFormattedOutput(rawResult.value ? JSON.stringify(record, null, 2) : formatRunCard(record));
|
|
565
1642
|
}
|
|
566
|
-
return { ok: true, group: "run", command, details: record };
|
|
1643
|
+
return { ok: true, group: "run", command, details: { ...record, rawOutput: rawResult.value } };
|
|
567
1644
|
}
|
|
568
1645
|
case "timeline": {
|
|
569
1646
|
let pending = rest;
|
|
@@ -571,38 +1648,28 @@ async function executeRun(context, args) {
|
|
|
571
1648
|
pending = run.rest;
|
|
572
1649
|
const follow = takeFlag(pending, "--follow");
|
|
573
1650
|
pending = follow.rest;
|
|
574
|
-
requireNoExtraArgs(pending, "
|
|
1651
|
+
requireNoExtraArgs(pending, "rig run timeline --run <id> [--follow]");
|
|
575
1652
|
if (!run.value) {
|
|
576
1653
|
throw new CliError2("run timeline requires --run <id>.");
|
|
577
1654
|
}
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
return events2;
|
|
587
|
-
};
|
|
588
|
-
const events = printEvents();
|
|
1655
|
+
const renderer = createPiRunStreamRenderer();
|
|
1656
|
+
let cursor = null;
|
|
1657
|
+
const page = await getRunTimelineViaServer(context, run.value, { limit: 500 });
|
|
1658
|
+
const events = Array.isArray(page.entries) ? page.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1659
|
+
cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
|
|
1660
|
+
if (context.outputMode === "text") {
|
|
1661
|
+
renderer.renderTimeline(events);
|
|
1662
|
+
}
|
|
589
1663
|
if (follow.value && context.outputMode === "text") {
|
|
590
|
-
let lastLength = existsSync3(timelinePath) ? readFileSync3(timelinePath, "utf8").length : 0;
|
|
591
1664
|
while (true) {
|
|
592
1665
|
await Bun.sleep(1000);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
continue;
|
|
598
|
-
const delta = next.slice(lastLength);
|
|
599
|
-
lastLength = next.length;
|
|
600
|
-
for (const line of delta.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) {
|
|
601
|
-
console.log(line);
|
|
602
|
-
}
|
|
1666
|
+
const nextPage = await getRunTimelineViaServer(context, run.value, { limit: 500, ...cursor ? { cursor } : {} });
|
|
1667
|
+
const nextEvents = Array.isArray(nextPage.entries) ? nextPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1668
|
+
cursor = typeof nextPage.nextCursor === "string" ? nextPage.nextCursor : cursor;
|
|
1669
|
+
renderer.renderTimeline(nextEvents);
|
|
603
1670
|
}
|
|
604
1671
|
}
|
|
605
|
-
return { ok: true, group: "run", command, details: { runId: run.value, events } };
|
|
1672
|
+
return { ok: true, group: "run", command, details: { runId: run.value, events, cursor } };
|
|
606
1673
|
}
|
|
607
1674
|
case "attach": {
|
|
608
1675
|
let pending = rest;
|
|
@@ -618,41 +1685,38 @@ async function executeRun(context, args) {
|
|
|
618
1685
|
pending = pollMs.rest;
|
|
619
1686
|
const positionalRunId = pending.length > 0 ? pending[0] : undefined;
|
|
620
1687
|
const extra = positionalRunId ? pending.slice(1) : pending;
|
|
621
|
-
requireNoExtraArgs(extra, "
|
|
1688
|
+
requireNoExtraArgs(extra, "rig run attach <run-id>|--run <run-id> [--message <text>] [--once|--follow] [--poll-ms <ms>]");
|
|
622
1689
|
const runId = runOption.value ?? positionalRunId;
|
|
623
1690
|
if (!runId) {
|
|
624
1691
|
throw new CliError2("run attach requires a run id.", 2);
|
|
625
1692
|
}
|
|
1693
|
+
let steered = false;
|
|
1694
|
+
if (messageOption.value?.trim()) {
|
|
1695
|
+
await steerRunViaServer(context, runId, messageOption.value.trim());
|
|
1696
|
+
steered = true;
|
|
1697
|
+
}
|
|
626
1698
|
const attached = await attachRunOperatorView(context, {
|
|
627
1699
|
runId,
|
|
628
|
-
message:
|
|
1700
|
+
message: null,
|
|
629
1701
|
once: once.value,
|
|
630
1702
|
follow: follow.value,
|
|
631
1703
|
pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)
|
|
632
1704
|
});
|
|
633
|
-
return { ok: true, group: "run", command, details: attached };
|
|
1705
|
+
return { ok: true, group: "run", command, details: { ...attached, steered: attached.steered || steered } };
|
|
634
1706
|
}
|
|
635
1707
|
case "status": {
|
|
636
|
-
requireNoExtraArgs(rest, "
|
|
1708
|
+
requireNoExtraArgs(rest, "rig run status");
|
|
637
1709
|
if (context.dryRun) {
|
|
638
1710
|
if (context.outputMode === "text") {
|
|
639
1711
|
console.log("[dry-run] rig run status");
|
|
640
1712
|
}
|
|
641
1713
|
return { ok: true, group: "run", command };
|
|
642
1714
|
}
|
|
643
|
-
const summary = runStatus(context.projectRoot, runtimeContext);
|
|
1715
|
+
const summary = isRemoteConnectionSelected(context.projectRoot) ? buildServerRunStatus(await listRunsViaServer(context, { limit: 100 })) : runStatus(context.projectRoot, runtimeContext);
|
|
1716
|
+
const activeRuns = Array.isArray(summary.activeRuns) ? summary.activeRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
|
|
1717
|
+
const recentRuns = Array.isArray(summary.recentRuns) ? summary.recentRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
|
|
644
1718
|
if (context.outputMode === "text") {
|
|
645
|
-
|
|
646
|
-
for (const run of summary.activeRuns) {
|
|
647
|
-
console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.taskId ?? run.title}`);
|
|
648
|
-
}
|
|
649
|
-
if (summary.recentRuns.length > 0) {
|
|
650
|
-
console.log("");
|
|
651
|
-
console.log("Recent runs:");
|
|
652
|
-
for (const run of summary.recentRuns) {
|
|
653
|
-
console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.taskId ?? run.title}`);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
1719
|
+
printFormattedOutput(formatRunStatus({ activeRuns, recentRuns, runs: Array.isArray(summary.runs) ? summary.runs : [...activeRuns, ...recentRuns] }, { source: isRemoteConnectionSelected(context.projectRoot) ? "server" : "local" }));
|
|
656
1720
|
}
|
|
657
1721
|
return { ok: true, group: "run", command, details: summary };
|
|
658
1722
|
}
|
|
@@ -676,7 +1740,7 @@ async function executeRun(context, args) {
|
|
|
676
1740
|
pending = pollResult.rest;
|
|
677
1741
|
const noServerResult = takeFlag(pending, "--no-server");
|
|
678
1742
|
pending = noServerResult.rest;
|
|
679
|
-
requireNoExtraArgs(pending, "
|
|
1743
|
+
requireNoExtraArgs(pending, "rig run start [--epic <id>] [--prompt-epic|--no-epic-prompt] [--ws-port <n>] [--server-host <host>] [--server-port <n>] [--poll-ms <n>] [--no-server]");
|
|
680
1744
|
if (promptEpicResult.value && noEpicPromptResult.value) {
|
|
681
1745
|
throw new CliError2("Cannot use --prompt-epic and --no-epic-prompt together.");
|
|
682
1746
|
}
|
|
@@ -724,7 +1788,7 @@ async function executeRun(context, args) {
|
|
|
724
1788
|
};
|
|
725
1789
|
}
|
|
726
1790
|
case "resume": {
|
|
727
|
-
requireNoExtraArgs(rest, "
|
|
1791
|
+
requireNoExtraArgs(rest, "rig run resume");
|
|
728
1792
|
if (context.dryRun) {
|
|
729
1793
|
if (context.outputMode === "text") {
|
|
730
1794
|
console.log("[dry-run] rig run resume");
|
|
@@ -738,7 +1802,7 @@ async function executeRun(context, args) {
|
|
|
738
1802
|
return { ok: true, group: "run", command, details: resumed };
|
|
739
1803
|
}
|
|
740
1804
|
case "restart": {
|
|
741
|
-
requireNoExtraArgs(rest, "
|
|
1805
|
+
requireNoExtraArgs(rest, "rig run restart");
|
|
742
1806
|
if (context.dryRun) {
|
|
743
1807
|
if (context.outputMode === "text") {
|
|
744
1808
|
console.log("[dry-run] rig run restart");
|
|
@@ -751,11 +1815,38 @@ async function executeRun(context, args) {
|
|
|
751
1815
|
}
|
|
752
1816
|
return { ok: true, group: "run", command, details: restarted };
|
|
753
1817
|
}
|
|
1818
|
+
case "steer": {
|
|
1819
|
+
const runOption = takeOption(rest, "--run");
|
|
1820
|
+
const messageOption = takeOption(runOption.rest, "--message");
|
|
1821
|
+
const shortMessageOption = takeOption(messageOption.rest, "-m");
|
|
1822
|
+
const positionalRunId = shortMessageOption.rest.length > 0 ? shortMessageOption.rest[0] : undefined;
|
|
1823
|
+
const extra = positionalRunId ? shortMessageOption.rest.slice(1) : shortMessageOption.rest;
|
|
1824
|
+
requireNoExtraArgs(extra, "rig run steer [<run-id>|--run <id>] --message <text>");
|
|
1825
|
+
const runId = runOption.value ?? positionalRunId;
|
|
1826
|
+
const message = messageOption.value ?? shortMessageOption.value;
|
|
1827
|
+
if (!runId) {
|
|
1828
|
+
throw new CliError2("run steer requires a run id (positional or --run <id>).", 2);
|
|
1829
|
+
}
|
|
1830
|
+
if (!message?.trim()) {
|
|
1831
|
+
throw new CliError2("run steer requires --message <text>.", 2);
|
|
1832
|
+
}
|
|
1833
|
+
if (context.dryRun) {
|
|
1834
|
+
if (context.outputMode === "text") {
|
|
1835
|
+
console.log(`[dry-run] rig run steer ${runId} --message ${JSON.stringify(message)}`);
|
|
1836
|
+
}
|
|
1837
|
+
return { ok: true, group: "run", command, details: { runId, dryRun: true } };
|
|
1838
|
+
}
|
|
1839
|
+
await steerRunViaServer(context, runId, message.trim());
|
|
1840
|
+
if (context.outputMode === "text") {
|
|
1841
|
+
console.log(`Steering message queued for ${runId}.`);
|
|
1842
|
+
}
|
|
1843
|
+
return { ok: true, group: "run", command, details: { runId, queued: true } };
|
|
1844
|
+
}
|
|
754
1845
|
case "stop": {
|
|
755
1846
|
const runOption = takeOption(rest, "--run");
|
|
756
1847
|
const positionalRunId = runOption.rest.length > 0 ? runOption.rest[0] : undefined;
|
|
757
1848
|
const extra = positionalRunId ? runOption.rest.slice(1) : runOption.rest;
|
|
758
|
-
requireNoExtraArgs(extra, "
|
|
1849
|
+
requireNoExtraArgs(extra, "rig run stop [<run-id>|--run <id>]");
|
|
759
1850
|
const runId = runOption.value ?? positionalRunId;
|
|
760
1851
|
if (context.dryRun) {
|
|
761
1852
|
return {
|