@h-rig/cli 0.0.6-alpha.6 → 0.0.6-alpha.61
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/README.md +1 -1
- package/dist/bin/rig.js +5033 -1895
- package/dist/src/commands/_authority-runs.js +2 -3
- package/dist/src/commands/_cli-format.js +369 -0
- package/dist/src/commands/_connection-state.js +12 -6
- package/dist/src/commands/_doctor-checks.js +79 -34
- package/dist/src/commands/_help-catalog.js +446 -0
- package/dist/src/commands/_operator-surface.js +220 -0
- package/dist/src/commands/_operator-view.js +1124 -64
- package/dist/src/commands/_parsers.js +0 -2
- package/dist/src/commands/_pi-frontend.js +1080 -0
- package/dist/src/commands/_pi-install.js +4 -3
- package/dist/src/commands/_pi-remote-session.js +771 -0
- package/dist/src/commands/_pi-worker-bridge-extension.js +834 -0
- package/dist/src/commands/_policy.js +0 -2
- package/dist/src/commands/_preflight.js +98 -116
- package/dist/src/commands/_run-driver-helpers.js +46 -19
- package/dist/src/commands/_run-replay.js +142 -0
- package/dist/src/commands/_server-client.js +225 -48
- package/dist/src/commands/_snapshot-upload.js +74 -30
- package/dist/src/commands/_spinner.js +63 -0
- package/dist/src/commands/_task-picker.js +44 -16
- package/dist/src/commands/agent.js +10 -12
- package/dist/src/commands/browser.js +4 -6
- package/dist/src/commands/connect.js +134 -26
- package/dist/src/commands/dist.js +4 -6
- package/dist/src/commands/doctor.js +79 -34
- package/dist/src/commands/github.js +76 -32
- package/dist/src/commands/inbox.js +410 -31
- package/dist/src/commands/init.js +398 -90
- package/dist/src/commands/inspect.js +296 -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 +1600 -131
- package/dist/src/commands/server.js +281 -42
- package/dist/src/commands/setup.js +84 -45
- package/dist/src/commands/task-report-bug.js +5 -7
- package/dist/src/commands/task-run-driver.js +736 -93
- package/dist/src/commands/task.js +1863 -262
- package/dist/src/commands/test.js +3 -5
- package/dist/src/commands/workspace.js +4 -6
- package/dist/src/commands.js +5675 -2531
- package/dist/src/index.js +5654 -2519
- 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 +7 -5
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 as readAuthorityRun2
|
|
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";
|
|
@@ -115,6 +108,11 @@ function readJsonFile(path) {
|
|
|
115
108
|
throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
116
109
|
}
|
|
117
110
|
}
|
|
111
|
+
function writeJsonFile(path, value) {
|
|
112
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
113
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
114
|
+
`, "utf8");
|
|
115
|
+
}
|
|
118
116
|
function normalizeConnection(value) {
|
|
119
117
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
120
118
|
return null;
|
|
@@ -155,25 +153,35 @@ function readRepoConnection(projectRoot) {
|
|
|
155
153
|
return {
|
|
156
154
|
selected,
|
|
157
155
|
project: typeof record.project === "string" ? record.project : undefined,
|
|
158
|
-
linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
|
|
156
|
+
linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
|
|
157
|
+
serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
|
|
159
158
|
};
|
|
160
159
|
}
|
|
160
|
+
function writeRepoConnection(projectRoot, state) {
|
|
161
|
+
writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
|
|
162
|
+
}
|
|
161
163
|
function resolveSelectedConnection(projectRoot, options = {}) {
|
|
162
164
|
const repo = readRepoConnection(projectRoot);
|
|
163
165
|
if (!repo)
|
|
164
166
|
return null;
|
|
165
167
|
if (repo.selected === "local")
|
|
166
|
-
return { alias: "local", connection: { kind: "local", mode: "auto" } };
|
|
168
|
+
return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
|
|
167
169
|
const global = readGlobalConnections(options);
|
|
168
170
|
const connection = global.connections[repo.selected];
|
|
169
171
|
if (!connection) {
|
|
170
|
-
throw new CliError2(`Selected Rig
|
|
172
|
+
throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
|
|
171
173
|
}
|
|
172
|
-
return { alias: repo.selected, connection };
|
|
174
|
+
return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
|
|
175
|
+
}
|
|
176
|
+
function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
|
|
177
|
+
const repo = readRepoConnection(projectRoot);
|
|
178
|
+
if (!repo)
|
|
179
|
+
return;
|
|
180
|
+
writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
|
|
173
181
|
}
|
|
174
182
|
|
|
175
183
|
// packages/cli/src/commands/_server-client.ts
|
|
176
|
-
var
|
|
184
|
+
var scopedGitHubBearerTokens = new Map;
|
|
177
185
|
function cleanToken(value) {
|
|
178
186
|
const trimmed = value?.trim();
|
|
179
187
|
return trimmed ? trimmed : null;
|
|
@@ -190,41 +198,47 @@ function readPrivateRemoteSessionToken(projectRoot) {
|
|
|
190
198
|
}
|
|
191
199
|
}
|
|
192
200
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
193
|
-
|
|
194
|
-
|
|
201
|
+
const scopedKey = resolve2(projectRoot);
|
|
202
|
+
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
203
|
+
return scopedGitHubBearerTokens.get(scopedKey) ?? null;
|
|
195
204
|
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
196
|
-
if (privateSession)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
205
|
+
if (privateSession)
|
|
206
|
+
return privateSession;
|
|
207
|
+
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
208
|
+
}
|
|
209
|
+
function readStoredGitHubAuthToken(projectRoot) {
|
|
210
|
+
const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
|
|
211
|
+
if (!existsSync2(path))
|
|
212
|
+
return null;
|
|
213
|
+
try {
|
|
214
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
215
|
+
return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function readLocalConnectionFallbackToken(projectRoot) {
|
|
221
|
+
return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
|
|
212
222
|
}
|
|
213
223
|
async function ensureServerForCli(projectRoot) {
|
|
214
224
|
try {
|
|
215
225
|
const selected = resolveSelectedConnection(projectRoot);
|
|
216
226
|
if (selected?.connection.kind === "remote") {
|
|
227
|
+
const authToken = readGitHubBearerTokenForRemote(projectRoot);
|
|
228
|
+
const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
|
|
217
229
|
return {
|
|
218
230
|
baseUrl: selected.connection.baseUrl,
|
|
219
|
-
authToken
|
|
220
|
-
connectionKind: "remote"
|
|
231
|
+
authToken,
|
|
232
|
+
connectionKind: "remote",
|
|
233
|
+
serverProjectRoot
|
|
221
234
|
};
|
|
222
235
|
}
|
|
223
236
|
const connection = await ensureLocalRigServerConnection(projectRoot);
|
|
224
237
|
return {
|
|
225
238
|
baseUrl: connection.baseUrl,
|
|
226
|
-
authToken: connection.authToken,
|
|
227
|
-
connectionKind: "local"
|
|
239
|
+
authToken: connection.authToken ?? readLocalConnectionFallbackToken(projectRoot),
|
|
240
|
+
connectionKind: "local",
|
|
241
|
+
serverProjectRoot: resolve2(projectRoot)
|
|
228
242
|
};
|
|
229
243
|
} catch (error) {
|
|
230
244
|
if (error instanceof Error) {
|
|
@@ -233,6 +247,29 @@ async function ensureServerForCli(projectRoot) {
|
|
|
233
247
|
throw error;
|
|
234
248
|
}
|
|
235
249
|
}
|
|
250
|
+
async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
|
|
251
|
+
const repo = readRepoConnection(projectRoot);
|
|
252
|
+
const slug = repo?.project?.trim();
|
|
253
|
+
if (!slug)
|
|
254
|
+
return null;
|
|
255
|
+
try {
|
|
256
|
+
const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
|
|
257
|
+
headers: mergeHeaders(undefined, authToken)
|
|
258
|
+
});
|
|
259
|
+
if (!response.ok)
|
|
260
|
+
return null;
|
|
261
|
+
const payload = await response.json();
|
|
262
|
+
const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
|
|
263
|
+
const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
|
|
264
|
+
const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
|
|
265
|
+
const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
|
|
266
|
+
if (path)
|
|
267
|
+
writeRepoServerProjectRoot(projectRoot, path);
|
|
268
|
+
return path;
|
|
269
|
+
} catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
236
273
|
function mergeHeaders(headers, authToken) {
|
|
237
274
|
const merged = new Headers(headers);
|
|
238
275
|
if (authToken) {
|
|
@@ -257,9 +294,12 @@ function diagnosticMessage(payload) {
|
|
|
257
294
|
}
|
|
258
295
|
async function requestServerJson(context, pathname, init = {}) {
|
|
259
296
|
const server = await ensureServerForCli(context.projectRoot);
|
|
297
|
+
const headers = mergeHeaders(init.headers, server.authToken);
|
|
298
|
+
if (server.serverProjectRoot)
|
|
299
|
+
headers.set("x-rig-project-root", server.serverProjectRoot);
|
|
260
300
|
const response = await fetch(`${server.baseUrl}${pathname}`, {
|
|
261
301
|
...init,
|
|
262
|
-
headers
|
|
302
|
+
headers
|
|
263
303
|
});
|
|
264
304
|
const text = await response.text();
|
|
265
305
|
const payload = text.trim().length > 0 ? (() => {
|
|
@@ -276,6 +316,14 @@ async function requestServerJson(context, pathname, init = {}) {
|
|
|
276
316
|
}
|
|
277
317
|
return payload;
|
|
278
318
|
}
|
|
319
|
+
async function listRunsViaServer(context, options = {}) {
|
|
320
|
+
const url = new URL("http://rig.local/api/runs");
|
|
321
|
+
if (options.limit !== undefined)
|
|
322
|
+
url.searchParams.set("limit", String(options.limit));
|
|
323
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
324
|
+
const runs = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.runs) ? payload.runs : [];
|
|
325
|
+
return runs.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
326
|
+
}
|
|
279
327
|
async function getRunDetailsViaServer(context, runId) {
|
|
280
328
|
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}`);
|
|
281
329
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
@@ -289,6 +337,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
|
|
|
289
337
|
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
290
338
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
291
339
|
}
|
|
340
|
+
async function getRunTimelineViaServer(context, runId, options = {}) {
|
|
341
|
+
const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
|
|
342
|
+
if (options.limit !== undefined)
|
|
343
|
+
url.searchParams.set("limit", String(options.limit));
|
|
344
|
+
if (options.cursor)
|
|
345
|
+
url.searchParams.set("cursor", options.cursor);
|
|
346
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
347
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
348
|
+
}
|
|
292
349
|
async function stopRunViaServer(context, runId) {
|
|
293
350
|
const payload = await requestServerJson(context, "/api/runs/stop", {
|
|
294
351
|
method: "POST",
|
|
@@ -305,10 +362,213 @@ async function steerRunViaServer(context, runId, message) {
|
|
|
305
362
|
});
|
|
306
363
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
307
364
|
}
|
|
365
|
+
async function getRunPiSessionViaServer(context, runId) {
|
|
366
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi`);
|
|
367
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
368
|
+
}
|
|
369
|
+
async function getRunPiMessagesViaServer(context, runId) {
|
|
370
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/messages`);
|
|
371
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { messages: [] };
|
|
372
|
+
}
|
|
373
|
+
async function getRunPiStatusViaServer(context, runId) {
|
|
374
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/status`);
|
|
375
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
376
|
+
}
|
|
377
|
+
async function getRunPiCommandsViaServer(context, runId) {
|
|
378
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
|
|
379
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
|
|
380
|
+
}
|
|
381
|
+
async function getRunPiCapabilitiesViaServer(context, runId) {
|
|
382
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/capabilities`);
|
|
383
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { capabilities: null };
|
|
384
|
+
}
|
|
385
|
+
async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
|
|
386
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
|
|
387
|
+
method: "POST",
|
|
388
|
+
headers: { "content-type": "application/json" },
|
|
389
|
+
body: JSON.stringify({ text, streamingBehavior })
|
|
390
|
+
});
|
|
391
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
392
|
+
}
|
|
393
|
+
async function sendRunPiShellViaServer(context, runId, text) {
|
|
394
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/shell`, {
|
|
395
|
+
method: "POST",
|
|
396
|
+
headers: { "content-type": "application/json" },
|
|
397
|
+
body: JSON.stringify({ text })
|
|
398
|
+
});
|
|
399
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
400
|
+
}
|
|
401
|
+
async function runRunPiCommandViaServer(context, runId, text) {
|
|
402
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands/run`, {
|
|
403
|
+
method: "POST",
|
|
404
|
+
headers: { "content-type": "application/json" },
|
|
405
|
+
body: JSON.stringify({ text })
|
|
406
|
+
});
|
|
407
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { type: "done" };
|
|
408
|
+
}
|
|
409
|
+
async function respondRunPiExtensionUiViaServer(context, runId, requestId, valueOrCancel) {
|
|
410
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/extension-ui/respond`, {
|
|
411
|
+
method: "POST",
|
|
412
|
+
headers: { "content-type": "application/json" },
|
|
413
|
+
body: JSON.stringify({ requestId, ...valueOrCancel })
|
|
414
|
+
});
|
|
415
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
416
|
+
}
|
|
417
|
+
async function abortRunPiViaServer(context, runId) {
|
|
418
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/abort`, { method: "POST" });
|
|
419
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { aborted: true };
|
|
420
|
+
}
|
|
421
|
+
async function buildRunPiEventsWebSocketUrl(context, runId) {
|
|
422
|
+
const server = await ensureServerForCli(context.projectRoot);
|
|
423
|
+
const url = new URL(`${server.baseUrl.replace(/\/+$/, "")}/api/runs/${encodeURIComponent(runId)}/pi/events`);
|
|
424
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
425
|
+
if (server.authToken)
|
|
426
|
+
url.searchParams.set("token", server.authToken);
|
|
427
|
+
if (server.serverProjectRoot)
|
|
428
|
+
url.searchParams.set("rigProjectRoot", server.serverProjectRoot);
|
|
429
|
+
return url.toString();
|
|
430
|
+
}
|
|
308
431
|
|
|
309
|
-
// packages/cli/src/commands/
|
|
432
|
+
// packages/cli/src/commands/_run-replay.ts
|
|
433
|
+
import { existsSync as existsSync3, readdirSync } from "fs";
|
|
434
|
+
import { join, resolve as resolve3 } from "path";
|
|
435
|
+
import {
|
|
436
|
+
readAuthorityRun,
|
|
437
|
+
readJsonlFile,
|
|
438
|
+
runLifecycleLogPath
|
|
439
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
440
|
+
function asRecord(value) {
|
|
441
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
442
|
+
}
|
|
443
|
+
function text(value) {
|
|
444
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
445
|
+
}
|
|
446
|
+
function snippet(value, max = 120) {
|
|
447
|
+
const raw = text(value);
|
|
448
|
+
if (!raw)
|
|
449
|
+
return null;
|
|
450
|
+
const flat = raw.replace(/\s+/g, " ");
|
|
451
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
452
|
+
}
|
|
453
|
+
function summarizeLifecycleEntry(entry) {
|
|
454
|
+
const type = text(entry.type) ?? "event";
|
|
455
|
+
switch (type) {
|
|
456
|
+
case "status": {
|
|
457
|
+
const anchor = text(entry.sessionId);
|
|
458
|
+
return [
|
|
459
|
+
`status \u2192 ${text(entry.status) ?? "(unknown)"}`,
|
|
460
|
+
snippet(entry.detail) ? `\u2014 ${snippet(entry.detail)}` : null,
|
|
461
|
+
anchor ? `[session ${anchor}]` : null
|
|
462
|
+
].filter(Boolean).join(" ");
|
|
463
|
+
}
|
|
464
|
+
case "timeline-entry": {
|
|
465
|
+
const payload = asRecord(entry.payload) ?? {};
|
|
466
|
+
const kind = text(payload.type) ?? "entry";
|
|
467
|
+
const body = snippet(payload.title) ?? snippet(payload.text) ?? snippet(payload.detail);
|
|
468
|
+
const state = text(payload.state);
|
|
469
|
+
return [`timeline ${kind}`, body ? `\u2014 ${body}` : null, state ? `(${state})` : null].filter(Boolean).join(" ");
|
|
470
|
+
}
|
|
471
|
+
case "log-entry": {
|
|
472
|
+
const payload = asRecord(entry.payload) ?? {};
|
|
473
|
+
const title = snippet(payload.title) ?? "log";
|
|
474
|
+
const detail = snippet(payload.detail);
|
|
475
|
+
return [`log ${title}`, detail ? `\u2014 ${detail}` : null].filter(Boolean).join(" ");
|
|
476
|
+
}
|
|
477
|
+
case "record-patch": {
|
|
478
|
+
const patch = asRecord(entry.patch) ?? {};
|
|
479
|
+
const keys = Object.keys(patch);
|
|
480
|
+
const shown = keys.slice(0, 8).join(", ");
|
|
481
|
+
return `record-patch {${shown}${keys.length > 8 ? `, +${keys.length - 8} more` : ""}}`;
|
|
482
|
+
}
|
|
483
|
+
case "timeline":
|
|
484
|
+
return "timeline updated (signal)";
|
|
485
|
+
case "log":
|
|
486
|
+
return `log signal${snippet(entry.title) ? ` \u2014 ${snippet(entry.title)}` : ""}`;
|
|
487
|
+
case "completed":
|
|
488
|
+
return "run completed";
|
|
489
|
+
case "failed":
|
|
490
|
+
return `run failed${snippet(entry.error) ? ` \u2014 ${snippet(entry.error)}` : ""}`;
|
|
491
|
+
default:
|
|
492
|
+
return type;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function summarizeSessionEntry(entry) {
|
|
496
|
+
const type = text(entry.type) ?? "entry";
|
|
497
|
+
const message = asRecord(entry.message);
|
|
498
|
+
const role = text(message?.role);
|
|
499
|
+
const content = message?.content;
|
|
500
|
+
let body = null;
|
|
501
|
+
if (typeof content === "string") {
|
|
502
|
+
body = snippet(content);
|
|
503
|
+
} else if (Array.isArray(content)) {
|
|
504
|
+
body = snippet(content.map((part) => text(asRecord(part)?.text) ?? "").filter(Boolean).join(" "));
|
|
505
|
+
}
|
|
506
|
+
return [
|
|
507
|
+
`pi ${type}`,
|
|
508
|
+
role ? `(${role})` : null,
|
|
509
|
+
body ? `\u2014 ${body}` : null
|
|
510
|
+
].filter(Boolean).join(" ");
|
|
511
|
+
}
|
|
512
|
+
function sessionEntryTimestamp(entry) {
|
|
513
|
+
return text(entry.timestamp) ?? text(entry.at) ?? text(entry.createdAt) ?? null;
|
|
514
|
+
}
|
|
515
|
+
function resolveRunSessionFile(record) {
|
|
516
|
+
const piSession = record?.piSession ?? null;
|
|
517
|
+
if (!piSession)
|
|
518
|
+
return null;
|
|
519
|
+
const recorded = text(piSession.sessionFile);
|
|
520
|
+
if (recorded && existsSync3(recorded)) {
|
|
521
|
+
return recorded;
|
|
522
|
+
}
|
|
523
|
+
const cwd = text(piSession.cwd);
|
|
524
|
+
const sessionId = text(piSession.sessionId);
|
|
525
|
+
if (!cwd || !sessionId)
|
|
526
|
+
return null;
|
|
527
|
+
const sessionDir = resolve3(cwd, ".rig", "session");
|
|
528
|
+
try {
|
|
529
|
+
const match = readdirSync(sessionDir).find((name) => name.endsWith(`_${sessionId}.jsonl`));
|
|
530
|
+
return match ? join(sessionDir, match) : null;
|
|
531
|
+
} catch {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function buildRunReplay(projectRoot, runId, options = {}) {
|
|
536
|
+
const logPath = runLifecycleLogPath(projectRoot, runId);
|
|
537
|
+
const record = readAuthorityRun(projectRoot, runId);
|
|
538
|
+
const lifecycleEntries = readJsonlFile(logPath).map((entry) => asRecord(entry)).filter((entry) => entry !== null);
|
|
539
|
+
const merged = lifecycleEntries.map((entry) => ({
|
|
540
|
+
at: text(entry.at),
|
|
541
|
+
source: "run",
|
|
542
|
+
summary: summarizeLifecycleEntry(entry)
|
|
543
|
+
}));
|
|
544
|
+
let sessionFile = null;
|
|
545
|
+
let sessionEntryCount = 0;
|
|
546
|
+
if (options.withSession) {
|
|
547
|
+
sessionFile = resolveRunSessionFile(record);
|
|
548
|
+
if (sessionFile) {
|
|
549
|
+
let lastSeenAt = null;
|
|
550
|
+
const sessionLines = readJsonlFile(sessionFile).map((entry) => asRecord(entry)).filter((entry) => entry !== null).map((entry) => {
|
|
551
|
+
lastSeenAt = sessionEntryTimestamp(entry) ?? lastSeenAt;
|
|
552
|
+
return { at: lastSeenAt, source: "session", summary: summarizeSessionEntry(entry) };
|
|
553
|
+
});
|
|
554
|
+
sessionEntryCount = sessionLines.length;
|
|
555
|
+
merged.push(...sessionLines);
|
|
556
|
+
merged.sort((left, right) => (left.at ?? "").localeCompare(right.at ?? ""));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const lines = merged.map((line) => `${line.at ?? "(no timestamp) "} ${line.source === "run" ? "run " : "session"} ${line.summary}`);
|
|
560
|
+
return {
|
|
561
|
+
runId,
|
|
562
|
+
logPath,
|
|
563
|
+
sessionFile,
|
|
564
|
+
entryCount: lifecycleEntries.length,
|
|
565
|
+
sessionEntryCount,
|
|
566
|
+
lines
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// packages/cli/src/commands/_operator-surface.ts
|
|
310
571
|
import { createInterface } from "readline";
|
|
311
|
-
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
312
572
|
var CANONICAL_STAGES = [
|
|
313
573
|
"Connect",
|
|
314
574
|
"GitHub/task sync",
|
|
@@ -323,18 +583,943 @@ var CANONICAL_STAGES = [
|
|
|
323
583
|
"Merge",
|
|
324
584
|
"Complete"
|
|
325
585
|
];
|
|
586
|
+
function logDetail(log) {
|
|
587
|
+
return typeof log.detail === "string" ? log.detail.trim() : "";
|
|
588
|
+
}
|
|
589
|
+
function parseProviderProtocolLog(title, detail) {
|
|
590
|
+
if (title.trim().toLowerCase() !== "agent output")
|
|
591
|
+
return null;
|
|
592
|
+
if (!detail.startsWith("{") || !detail.endsWith("}"))
|
|
593
|
+
return null;
|
|
594
|
+
try {
|
|
595
|
+
const record = JSON.parse(detail);
|
|
596
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
597
|
+
return null;
|
|
598
|
+
const type = record.type;
|
|
599
|
+
return typeof type === "string" && [
|
|
600
|
+
"assistant",
|
|
601
|
+
"message_start",
|
|
602
|
+
"message_update",
|
|
603
|
+
"message_end",
|
|
604
|
+
"stream_event",
|
|
605
|
+
"tool_result",
|
|
606
|
+
"tool_execution_start",
|
|
607
|
+
"tool_execution_update",
|
|
608
|
+
"tool_execution_end",
|
|
609
|
+
"turn_start",
|
|
610
|
+
"turn_end"
|
|
611
|
+
].includes(type) ? record : null;
|
|
612
|
+
} catch {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function renderProviderProtocolLog(record) {
|
|
617
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
618
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
|
|
619
|
+
const toolName = String(record.toolName ?? record.name ?? "tool");
|
|
620
|
+
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";
|
|
621
|
+
return `[Pi tool] ${toolName} ${status}`;
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
function entryId(entry, fallback) {
|
|
626
|
+
return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
|
|
627
|
+
}
|
|
326
628
|
function renderOperatorSnapshot(snapshot) {
|
|
327
629
|
const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
|
|
328
630
|
const runId = String(run.runId ?? run.id ?? "run");
|
|
329
631
|
const status = String(run.status ?? "unknown");
|
|
330
632
|
const logs = snapshot.logs ?? [];
|
|
633
|
+
const latestByStage = new Map;
|
|
634
|
+
for (const log of logs) {
|
|
635
|
+
const title = String(log.title ?? "").toLowerCase();
|
|
636
|
+
const stageName = String(log.stage ?? "").toLowerCase();
|
|
637
|
+
const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
|
|
638
|
+
if (stage)
|
|
639
|
+
latestByStage.set(stage, log);
|
|
640
|
+
}
|
|
331
641
|
const stageLines = CANONICAL_STAGES.flatMap((stage) => {
|
|
332
|
-
const match =
|
|
333
|
-
return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
|
|
642
|
+
const match = latestByStage.get(stage);
|
|
643
|
+
return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
|
|
334
644
|
});
|
|
335
645
|
return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
|
|
336
646
|
`);
|
|
337
647
|
}
|
|
648
|
+
function createPiRunStreamRenderer(output = process.stdout) {
|
|
649
|
+
let lastSnapshot = "";
|
|
650
|
+
const assistantTextById = new Map;
|
|
651
|
+
const seenTimeline = new Set;
|
|
652
|
+
const seenLogs = new Set;
|
|
653
|
+
const writeLine = (line) => output.write(`${line}
|
|
654
|
+
`);
|
|
655
|
+
return {
|
|
656
|
+
renderSnapshot(snapshot) {
|
|
657
|
+
const rendered = renderOperatorSnapshot(snapshot);
|
|
658
|
+
if (rendered && rendered !== lastSnapshot) {
|
|
659
|
+
writeLine(rendered);
|
|
660
|
+
lastSnapshot = rendered;
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
renderTimeline(entries) {
|
|
664
|
+
for (const [index, entry] of entries.entries()) {
|
|
665
|
+
const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
|
|
666
|
+
if (entry.type === "assistant_message" && typeof entry.text === "string") {
|
|
667
|
+
const text2 = entry.text;
|
|
668
|
+
const previousText = assistantTextById.get(id) ?? "";
|
|
669
|
+
if (!previousText && text2.trim()) {
|
|
670
|
+
writeLine("[Pi assistant]");
|
|
671
|
+
}
|
|
672
|
+
if (text2.startsWith(previousText)) {
|
|
673
|
+
const delta = text2.slice(previousText.length);
|
|
674
|
+
if (delta)
|
|
675
|
+
output.write(delta);
|
|
676
|
+
} else if (text2.trim() && text2 !== previousText) {
|
|
677
|
+
if (previousText)
|
|
678
|
+
writeLine(`
|
|
679
|
+
[Pi assistant]`);
|
|
680
|
+
output.write(text2);
|
|
681
|
+
}
|
|
682
|
+
assistantTextById.set(id, text2);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (seenTimeline.has(id))
|
|
686
|
+
continue;
|
|
687
|
+
seenTimeline.add(id);
|
|
688
|
+
if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
|
|
689
|
+
writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (entry.type === "timeline_warning") {
|
|
693
|
+
writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (entry.type === "action") {
|
|
697
|
+
const text2 = String(entry.detail ?? entry.message ?? entry.title ?? "").trim();
|
|
698
|
+
if (text2)
|
|
699
|
+
writeLine(`[Rig action] ${text2}`);
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (entry.type === "user_message") {
|
|
703
|
+
const text2 = String(entry.text ?? entry.message ?? entry.detail ?? "").trim();
|
|
704
|
+
if (text2)
|
|
705
|
+
writeLine(`[Operator] ${text2}`);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
const fallback = String(entry.detail ?? entry.message ?? entry.text ?? entry.title ?? "").trim();
|
|
709
|
+
if (fallback)
|
|
710
|
+
writeLine(`[${String(entry.type ?? "timeline")}] ${fallback}`);
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
renderLogs(entries) {
|
|
714
|
+
for (const [index, entry] of entries.entries()) {
|
|
715
|
+
const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
|
|
716
|
+
if (seenLogs.has(id))
|
|
717
|
+
continue;
|
|
718
|
+
seenLogs.add(id);
|
|
719
|
+
const title = String(entry.title ?? "");
|
|
720
|
+
if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
|
|
721
|
+
continue;
|
|
722
|
+
const detail = logDetail(entry);
|
|
723
|
+
if (!detail)
|
|
724
|
+
continue;
|
|
725
|
+
const protocolRecord = parseProviderProtocolLog(title, detail);
|
|
726
|
+
if (protocolRecord) {
|
|
727
|
+
const protocolLine = renderProviderProtocolLog(protocolRecord);
|
|
728
|
+
if (protocolLine)
|
|
729
|
+
writeLine(protocolLine);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
writeLine(`[${title || "Rig log"}] ${detail}`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function createOperatorSurface(options = {}) {
|
|
738
|
+
const input = options.input ?? process.stdin;
|
|
739
|
+
const output = options.output ?? process.stdout;
|
|
740
|
+
const errorOutput = options.errorOutput ?? process.stderr;
|
|
741
|
+
const renderer = createPiRunStreamRenderer(output);
|
|
742
|
+
const writeLine = (line) => output.write(`${line}
|
|
743
|
+
`);
|
|
744
|
+
return {
|
|
745
|
+
mode: "pi-compatible-text",
|
|
746
|
+
...renderer,
|
|
747
|
+
info: writeLine,
|
|
748
|
+
error: (message) => errorOutput.write(`${message}
|
|
749
|
+
`),
|
|
750
|
+
attachCommandInput(handler) {
|
|
751
|
+
if (options.interactive === false || !input.isTTY)
|
|
752
|
+
return null;
|
|
753
|
+
const rl = createInterface({ input, output: process.stdout, terminal: false });
|
|
754
|
+
rl.on("line", (line) => {
|
|
755
|
+
Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
756
|
+
});
|
|
757
|
+
return { close: () => rl.close() };
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// packages/cli/src/commands/_pi-frontend.ts
|
|
763
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
764
|
+
import { tmpdir } from "os";
|
|
765
|
+
import { join as join2 } from "path";
|
|
766
|
+
import {
|
|
767
|
+
createAgentSessionFromServices,
|
|
768
|
+
createAgentSessionServices,
|
|
769
|
+
main as runPiMain
|
|
770
|
+
} from "@earendil-works/pi-coding-agent";
|
|
771
|
+
|
|
772
|
+
// packages/cli/src/commands/_pi-remote-session.ts
|
|
773
|
+
import { AgentSession as PiAgentSession, expandPromptTemplate } from "@earendil-works/pi-coding-agent";
|
|
774
|
+
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
775
|
+
function defaultTransport() {
|
|
776
|
+
return {
|
|
777
|
+
getSession: getRunPiSessionViaServer,
|
|
778
|
+
getMessages: getRunPiMessagesViaServer,
|
|
779
|
+
getStatus: getRunPiStatusViaServer,
|
|
780
|
+
getCommands: getRunPiCommandsViaServer,
|
|
781
|
+
getCapabilities: getRunPiCapabilitiesViaServer,
|
|
782
|
+
sendPrompt: sendRunPiPromptViaServer,
|
|
783
|
+
sendShell: sendRunPiShellViaServer,
|
|
784
|
+
runCommand: runRunPiCommandViaServer,
|
|
785
|
+
abort: abortRunPiViaServer,
|
|
786
|
+
buildEventsWebSocketUrl: buildRunPiEventsWebSocketUrl
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function recordOf(value) {
|
|
790
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
791
|
+
}
|
|
792
|
+
function emptyRemoteStatus() {
|
|
793
|
+
return {
|
|
794
|
+
isStreaming: false,
|
|
795
|
+
isCompacting: false,
|
|
796
|
+
isBashRunning: false,
|
|
797
|
+
pendingMessageCount: 0,
|
|
798
|
+
steeringMessages: [],
|
|
799
|
+
followUpMessages: [],
|
|
800
|
+
model: null,
|
|
801
|
+
thinkingLevel: null,
|
|
802
|
+
sessionName: null,
|
|
803
|
+
cwd: null,
|
|
804
|
+
stats: null,
|
|
805
|
+
contextUsage: null
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function resolveAttachReadyTimeoutMs() {
|
|
809
|
+
const raw = Number.parseInt(process.env.RIG_PI_ATTACH_TIMEOUT_MS ?? "", 10);
|
|
810
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60000;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
class RigRemoteSessionController {
|
|
814
|
+
context;
|
|
815
|
+
runId;
|
|
816
|
+
status = emptyRemoteStatus();
|
|
817
|
+
transport;
|
|
818
|
+
hooks = {};
|
|
819
|
+
session = null;
|
|
820
|
+
socket = null;
|
|
821
|
+
closed = false;
|
|
822
|
+
pendingShells = [];
|
|
823
|
+
pendingCompactions = [];
|
|
824
|
+
constructor(input) {
|
|
825
|
+
this.context = input.context;
|
|
826
|
+
this.runId = input.runId;
|
|
827
|
+
this.transport = input.transport ?? defaultTransport();
|
|
828
|
+
}
|
|
829
|
+
ingestEnvelope(envelopeValue) {
|
|
830
|
+
this.applyEnvelope(envelopeValue);
|
|
831
|
+
}
|
|
832
|
+
bindSession(session) {
|
|
833
|
+
this.session = session;
|
|
834
|
+
}
|
|
835
|
+
setUiHooks(hooks) {
|
|
836
|
+
this.hooks = hooks;
|
|
837
|
+
}
|
|
838
|
+
async connect() {
|
|
839
|
+
const ready = await this.waitForReady();
|
|
840
|
+
if (!ready || this.closed)
|
|
841
|
+
return;
|
|
842
|
+
let catchupDone = false;
|
|
843
|
+
const buffered = [];
|
|
844
|
+
const wsUrl = await this.transport.buildEventsWebSocketUrl(this.context, this.runId);
|
|
845
|
+
const socket = new WebSocket(wsUrl);
|
|
846
|
+
this.socket = socket;
|
|
847
|
+
socket.onopen = () => {
|
|
848
|
+
this.hooks.onConnectionChange?.(true);
|
|
849
|
+
this.hooks.onStatusText?.("worker session live");
|
|
850
|
+
};
|
|
851
|
+
socket.onmessage = (message) => {
|
|
852
|
+
try {
|
|
853
|
+
const payload = typeof message.data === "string" ? JSON.parse(message.data) : JSON.parse(Buffer.from(message.data).toString("utf8"));
|
|
854
|
+
if (!catchupDone)
|
|
855
|
+
buffered.push(payload);
|
|
856
|
+
else
|
|
857
|
+
this.applyEnvelope(payload);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
this.hooks.onError?.(`Unparseable worker Pi event: ${error instanceof Error ? error.message : String(error)}`);
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
socket.onerror = () => socket.close();
|
|
863
|
+
socket.onclose = () => {
|
|
864
|
+
this.hooks.onConnectionChange?.(false);
|
|
865
|
+
if (!this.closed)
|
|
866
|
+
this.hooks.onStatusText?.("worker Pi WebSocket disconnected");
|
|
867
|
+
};
|
|
868
|
+
try {
|
|
869
|
+
const [messagesPayload, statusPayload, commandsPayload, capabilitiesPayload] = await Promise.all([
|
|
870
|
+
this.transport.getMessages(this.context, this.runId),
|
|
871
|
+
this.transport.getStatus(this.context, this.runId),
|
|
872
|
+
this.transport.getCommands(this.context, this.runId),
|
|
873
|
+
this.transport.getCapabilities(this.context, this.runId).catch(() => ({ capabilities: null }))
|
|
874
|
+
]);
|
|
875
|
+
const messages = Array.isArray(messagesPayload.messages) ? messagesPayload.messages : [];
|
|
876
|
+
this.applyStatusPayload(statusPayload);
|
|
877
|
+
this.session?.replaceRemoteMessages(messages);
|
|
878
|
+
const commands = Array.isArray(commandsPayload.commands) ? commandsPayload.commands : [];
|
|
879
|
+
const capabilities = capabilitiesPayload.capabilities && typeof capabilitiesPayload.capabilities === "object" && !Array.isArray(capabilitiesPayload.capabilities) ? capabilitiesPayload.capabilities : null;
|
|
880
|
+
this.hooks.onCatchUp?.(messages, commands, capabilities);
|
|
881
|
+
catchupDone = true;
|
|
882
|
+
for (const payload of buffered.splice(0))
|
|
883
|
+
this.applyEnvelope(payload);
|
|
884
|
+
} catch (error) {
|
|
885
|
+
catchupDone = true;
|
|
886
|
+
this.hooks.onError?.(`Worker Pi catch-up failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
close() {
|
|
890
|
+
this.closed = true;
|
|
891
|
+
this.socket?.close();
|
|
892
|
+
for (const shell of this.pendingShells.splice(0))
|
|
893
|
+
shell.reject(new Error("Remote session closed."));
|
|
894
|
+
for (const compaction of this.pendingCompactions.splice(0))
|
|
895
|
+
compaction.reject(new Error("Remote session closed."));
|
|
896
|
+
}
|
|
897
|
+
async sendPrompt(text2, streamingBehavior) {
|
|
898
|
+
await this.transport.sendPrompt(this.context, this.runId, text2, streamingBehavior);
|
|
899
|
+
}
|
|
900
|
+
async sendCommand(text2) {
|
|
901
|
+
const result = await this.transport.runCommand(this.context, this.runId, text2);
|
|
902
|
+
return typeof result.message === "string" ? result.message : "worker command accepted";
|
|
903
|
+
}
|
|
904
|
+
async sendShell(text2) {
|
|
905
|
+
await this.transport.sendShell(this.context, this.runId, text2);
|
|
906
|
+
}
|
|
907
|
+
async abort() {
|
|
908
|
+
await this.transport.abort(this.context, this.runId);
|
|
909
|
+
}
|
|
910
|
+
registerPendingShell(shell) {
|
|
911
|
+
this.pendingShells.push(shell);
|
|
912
|
+
}
|
|
913
|
+
failPendingShell(shell, error) {
|
|
914
|
+
const index = this.pendingShells.indexOf(shell);
|
|
915
|
+
if (index !== -1)
|
|
916
|
+
this.pendingShells.splice(index, 1);
|
|
917
|
+
shell.reject(error);
|
|
918
|
+
}
|
|
919
|
+
registerPendingCompaction(pending) {
|
|
920
|
+
this.pendingCompactions.push(pending);
|
|
921
|
+
}
|
|
922
|
+
async waitForReady() {
|
|
923
|
+
const startedAt = Date.now();
|
|
924
|
+
const deadline = startedAt + resolveAttachReadyTimeoutMs();
|
|
925
|
+
let consecutiveFailures = 0;
|
|
926
|
+
while (!this.closed) {
|
|
927
|
+
let requestFailed = false;
|
|
928
|
+
const session = await this.transport.getSession(this.context, this.runId).catch((error) => {
|
|
929
|
+
requestFailed = true;
|
|
930
|
+
return { ready: false, status: error instanceof Error ? error.message : String(error), retryAfterMs: 1000 };
|
|
931
|
+
});
|
|
932
|
+
if (session.ready !== false)
|
|
933
|
+
return true;
|
|
934
|
+
consecutiveFailures = requestFailed ? consecutiveFailures + 1 : 0;
|
|
935
|
+
const status = String(session.status ?? "starting");
|
|
936
|
+
if (TERMINAL_RUN_STATUSES.has(status.toLowerCase())) {
|
|
937
|
+
this.hooks.onError?.(`Run ended before the worker Pi daemon became ready: ${status}. Inspect with \`rig run show ${this.runId}\`.`);
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
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`);
|
|
941
|
+
if (Date.now() >= deadline) {
|
|
942
|
+
this.hooks.onError?.(`Worker Pi daemon did not become ready (last status: ${status}). Set RIG_PI_ATTACH_TIMEOUT_MS to wait longer.`);
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
await Bun.sleep(typeof session.retryAfterMs === "number" ? session.retryAfterMs : 750);
|
|
946
|
+
}
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
applyEnvelope(envelopeValue) {
|
|
950
|
+
const envelope = recordOf(envelopeValue);
|
|
951
|
+
if (!envelope)
|
|
952
|
+
return;
|
|
953
|
+
const type = String(envelope.type ?? "");
|
|
954
|
+
if (type === "status.update") {
|
|
955
|
+
this.applyStatusPayload(envelope);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (type === "activity.update") {
|
|
959
|
+
const activity = recordOf(envelope.activity);
|
|
960
|
+
this.hooks.onActivity?.(String(activity?.label ?? ""), activity?.detail ? String(activity.detail) : undefined);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (type === "extension_ui_request") {
|
|
964
|
+
const request = recordOf(envelope.request);
|
|
965
|
+
if (request)
|
|
966
|
+
this.hooks.onExtensionUiRequest?.(request);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (type === "pi.ui_event") {
|
|
970
|
+
this.applyShellUiEvent(envelope.event);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if (type === "pi.event") {
|
|
974
|
+
this.session?.handleRemoteSessionEvent(envelope.event);
|
|
975
|
+
this.settlePendingCompaction(envelope.event);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (type === "error") {
|
|
979
|
+
this.hooks.onError?.(String(envelope.message ?? envelope.detail ?? "unknown worker error"));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
applyStatusPayload(payload) {
|
|
983
|
+
const status = recordOf(payload.status) ?? payload;
|
|
984
|
+
const next = this.status;
|
|
985
|
+
next.isStreaming = status.isStreaming === true;
|
|
986
|
+
next.isCompacting = status.isCompacting === true;
|
|
987
|
+
next.isBashRunning = status.isBashRunning === true;
|
|
988
|
+
next.pendingMessageCount = typeof status.pendingMessageCount === "number" ? status.pendingMessageCount : 0;
|
|
989
|
+
next.steeringMessages = Array.isArray(status.steeringMessages) ? status.steeringMessages.map(String) : [];
|
|
990
|
+
next.followUpMessages = Array.isArray(status.followUpMessages) ? status.followUpMessages.map(String) : [];
|
|
991
|
+
next.model = recordOf(status.model) ?? next.model;
|
|
992
|
+
next.thinkingLevel = typeof status.thinkingLevel === "string" ? status.thinkingLevel : next.thinkingLevel;
|
|
993
|
+
next.sessionName = typeof status.sessionName === "string" ? status.sessionName : next.sessionName;
|
|
994
|
+
next.cwd = typeof status.cwd === "string" ? status.cwd : next.cwd;
|
|
995
|
+
next.stats = recordOf(status.stats) ?? next.stats;
|
|
996
|
+
next.contextUsage = recordOf(status.contextUsage) ?? next.contextUsage;
|
|
997
|
+
}
|
|
998
|
+
applyShellUiEvent(value) {
|
|
999
|
+
const event = recordOf(value);
|
|
1000
|
+
if (!event)
|
|
1001
|
+
return;
|
|
1002
|
+
const type = String(event.type ?? "");
|
|
1003
|
+
const pending = this.pendingShells[0];
|
|
1004
|
+
if (type === "shell.chunk" && pending) {
|
|
1005
|
+
pending.sawChunk = true;
|
|
1006
|
+
pending.onData(Buffer.from(String(event.chunk ?? "")));
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (type === "shell.end" && pending) {
|
|
1010
|
+
const output = String(event.output ?? "");
|
|
1011
|
+
if (output && !pending.sawChunk)
|
|
1012
|
+
pending.onData(Buffer.from(output));
|
|
1013
|
+
const index = this.pendingShells.indexOf(pending);
|
|
1014
|
+
if (index !== -1)
|
|
1015
|
+
this.pendingShells.splice(index, 1);
|
|
1016
|
+
pending.resolve({ exitCode: typeof event.exitCode === "number" ? event.exitCode : event.isError === true ? 1 : 0 });
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
settlePendingCompaction(eventValue) {
|
|
1020
|
+
const event = recordOf(eventValue);
|
|
1021
|
+
if (!event)
|
|
1022
|
+
return;
|
|
1023
|
+
const type = String(event.type ?? "");
|
|
1024
|
+
if (type !== "compaction_end" || this.pendingCompactions.length === 0)
|
|
1025
|
+
return;
|
|
1026
|
+
const pending = this.pendingCompactions.shift();
|
|
1027
|
+
const result = recordOf(event.result);
|
|
1028
|
+
if (result)
|
|
1029
|
+
pending.resolve(result);
|
|
1030
|
+
else if (event.aborted === true)
|
|
1031
|
+
pending.reject(new Error("Compaction aborted on the worker."));
|
|
1032
|
+
else
|
|
1033
|
+
pending.resolve({});
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
class RigRemoteAgentSession extends PiAgentSession {
|
|
1038
|
+
remote;
|
|
1039
|
+
constructor(config, remote) {
|
|
1040
|
+
super(config);
|
|
1041
|
+
this.remote = remote;
|
|
1042
|
+
remote.bindSession(this);
|
|
1043
|
+
}
|
|
1044
|
+
handleRemoteSessionEvent(eventValue) {
|
|
1045
|
+
const event = recordOf(eventValue);
|
|
1046
|
+
if (!event)
|
|
1047
|
+
return;
|
|
1048
|
+
const type = String(event.type ?? "");
|
|
1049
|
+
if ((type === "message_end" || type === "turn_end") && recordOf(event.message)) {
|
|
1050
|
+
this.agent.state.messages = [...this.agent.state.messages, event.message];
|
|
1051
|
+
}
|
|
1052
|
+
this._emit(eventValue);
|
|
1053
|
+
}
|
|
1054
|
+
replaceRemoteMessages(messages) {
|
|
1055
|
+
this.agent.state.messages = messages;
|
|
1056
|
+
}
|
|
1057
|
+
async expandLocalInput(text2) {
|
|
1058
|
+
let current = text2;
|
|
1059
|
+
const runner = this.extensionRunner;
|
|
1060
|
+
if (runner.hasHandlers("input")) {
|
|
1061
|
+
const result = await runner.emitInput(current, undefined, "interactive");
|
|
1062
|
+
if (result.action === "handled")
|
|
1063
|
+
return null;
|
|
1064
|
+
if (result.action === "transform")
|
|
1065
|
+
current = result.text;
|
|
1066
|
+
}
|
|
1067
|
+
current = this._expandSkillCommand(current);
|
|
1068
|
+
current = expandPromptTemplate(current, [...this.promptTemplates]);
|
|
1069
|
+
return current;
|
|
1070
|
+
}
|
|
1071
|
+
async prompt(text2, options) {
|
|
1072
|
+
const trimmed = text2.trim();
|
|
1073
|
+
if (!trimmed)
|
|
1074
|
+
return;
|
|
1075
|
+
if (trimmed.startsWith("/")) {
|
|
1076
|
+
if (await this._tryExecuteExtensionCommand(trimmed))
|
|
1077
|
+
return;
|
|
1078
|
+
const expanded2 = await this.expandLocalInput(trimmed);
|
|
1079
|
+
if (expanded2 === null)
|
|
1080
|
+
return;
|
|
1081
|
+
if (expanded2 !== trimmed) {
|
|
1082
|
+
const behavior2 = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
|
|
1083
|
+
options?.preflightResult?.(true);
|
|
1084
|
+
await this.remote.sendPrompt(expanded2, behavior2);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
await this.remote.sendCommand(trimmed);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (trimmed.startsWith("!")) {
|
|
1091
|
+
await this.remote.sendShell(trimmed);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
const expanded = await this.expandLocalInput(trimmed);
|
|
1095
|
+
if (expanded === null)
|
|
1096
|
+
return;
|
|
1097
|
+
const behavior = options?.streamingBehavior ?? (this.isStreaming || this.isCompacting ? "steer" : undefined);
|
|
1098
|
+
options?.preflightResult?.(true);
|
|
1099
|
+
await this.remote.sendPrompt(expanded, behavior);
|
|
1100
|
+
}
|
|
1101
|
+
async steer(text2) {
|
|
1102
|
+
const trimmed = text2.trim();
|
|
1103
|
+
if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
|
|
1104
|
+
return;
|
|
1105
|
+
const expanded = await this.expandLocalInput(trimmed);
|
|
1106
|
+
if (expanded === null)
|
|
1107
|
+
return;
|
|
1108
|
+
await this.remote.sendPrompt(expanded, "steer");
|
|
1109
|
+
}
|
|
1110
|
+
async followUp(text2) {
|
|
1111
|
+
const trimmed = text2.trim();
|
|
1112
|
+
if (trimmed.startsWith("/") && await this._tryExecuteExtensionCommand(trimmed))
|
|
1113
|
+
return;
|
|
1114
|
+
const expanded = await this.expandLocalInput(trimmed);
|
|
1115
|
+
if (expanded === null)
|
|
1116
|
+
return;
|
|
1117
|
+
await this.remote.sendPrompt(expanded, "followUp");
|
|
1118
|
+
}
|
|
1119
|
+
async abort() {
|
|
1120
|
+
await this.remote.abort();
|
|
1121
|
+
}
|
|
1122
|
+
async compact(customInstructions) {
|
|
1123
|
+
const pending = new Promise((resolve4, reject) => {
|
|
1124
|
+
this.remote.registerPendingCompaction({ resolve: resolve4, reject });
|
|
1125
|
+
});
|
|
1126
|
+
await this.remote.sendCommand(`/compact${customInstructions ? ` ${customInstructions}` : ""}`);
|
|
1127
|
+
return pending;
|
|
1128
|
+
}
|
|
1129
|
+
clearQueue() {
|
|
1130
|
+
const cleared = {
|
|
1131
|
+
steering: [...this.remote.status.steeringMessages],
|
|
1132
|
+
followUp: [...this.remote.status.followUpMessages]
|
|
1133
|
+
};
|
|
1134
|
+
this.remote.status.steeringMessages = [];
|
|
1135
|
+
this.remote.status.followUpMessages = [];
|
|
1136
|
+
this.remote.status.pendingMessageCount = 0;
|
|
1137
|
+
this.remote.sendCommand("/queue-clear").catch(() => {});
|
|
1138
|
+
return cleared;
|
|
1139
|
+
}
|
|
1140
|
+
get isStreaming() {
|
|
1141
|
+
return this.remote.status.isStreaming;
|
|
1142
|
+
}
|
|
1143
|
+
get isCompacting() {
|
|
1144
|
+
return this.remote.status.isCompacting;
|
|
1145
|
+
}
|
|
1146
|
+
get isBashRunning() {
|
|
1147
|
+
return this.remote.status.isBashRunning;
|
|
1148
|
+
}
|
|
1149
|
+
get pendingMessageCount() {
|
|
1150
|
+
return this.remote.status.pendingMessageCount;
|
|
1151
|
+
}
|
|
1152
|
+
getSteeringMessages() {
|
|
1153
|
+
return this.remote.status.steeringMessages;
|
|
1154
|
+
}
|
|
1155
|
+
getFollowUpMessages() {
|
|
1156
|
+
return this.remote.status.followUpMessages;
|
|
1157
|
+
}
|
|
1158
|
+
get model() {
|
|
1159
|
+
return this.remote.status.model ?? super.model;
|
|
1160
|
+
}
|
|
1161
|
+
async setModel(model) {
|
|
1162
|
+
await this.remote.sendCommand(`/model ${model.provider}/${model.id}`);
|
|
1163
|
+
this.remote.status.model = model;
|
|
1164
|
+
}
|
|
1165
|
+
get thinkingLevel() {
|
|
1166
|
+
return this.remote.status.thinkingLevel ?? super.thinkingLevel;
|
|
1167
|
+
}
|
|
1168
|
+
setThinkingLevel(level) {
|
|
1169
|
+
this.remote.status.thinkingLevel = level;
|
|
1170
|
+
this.remote.sendCommand(`/thinking ${level}`).catch(() => {});
|
|
1171
|
+
}
|
|
1172
|
+
get sessionName() {
|
|
1173
|
+
return this.remote.status.sessionName ?? super.sessionName;
|
|
1174
|
+
}
|
|
1175
|
+
setSessionName(name) {
|
|
1176
|
+
this.remote.status.sessionName = name;
|
|
1177
|
+
this.remote.sendCommand(`/name ${name}`).catch(() => {});
|
|
1178
|
+
}
|
|
1179
|
+
getSessionStats() {
|
|
1180
|
+
return this.remote.status.stats ?? super.getSessionStats();
|
|
1181
|
+
}
|
|
1182
|
+
getContextUsage() {
|
|
1183
|
+
return this.remote.status.contextUsage ?? super.getContextUsage();
|
|
1184
|
+
}
|
|
1185
|
+
dispose() {
|
|
1186
|
+
this.remote.close();
|
|
1187
|
+
super.dispose();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function createRemoteBashOperations(controller, excludeFromContext) {
|
|
1191
|
+
return {
|
|
1192
|
+
exec(command, _cwd, execOptions) {
|
|
1193
|
+
return new Promise((resolve4, reject) => {
|
|
1194
|
+
const pending = { onData: execOptions.onData, resolve: resolve4, reject, sawChunk: false };
|
|
1195
|
+
const cleanup = () => {
|
|
1196
|
+
execOptions.signal?.removeEventListener("abort", onAbort);
|
|
1197
|
+
if (timer)
|
|
1198
|
+
clearTimeout(timer);
|
|
1199
|
+
};
|
|
1200
|
+
const onAbort = () => {
|
|
1201
|
+
cleanup();
|
|
1202
|
+
controller.failPendingShell(pending, new Error("Remote worker shell command aborted locally."));
|
|
1203
|
+
};
|
|
1204
|
+
const timeoutMs = typeof execOptions.timeout === "number" && execOptions.timeout > 0 ? execOptions.timeout : 0;
|
|
1205
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
1206
|
+
cleanup();
|
|
1207
|
+
controller.failPendingShell(pending, new Error(`Remote worker shell command timed out after ${timeoutMs}ms.`));
|
|
1208
|
+
}, timeoutMs) : null;
|
|
1209
|
+
const wrappedResolve = pending.resolve;
|
|
1210
|
+
const wrappedReject = pending.reject;
|
|
1211
|
+
pending.resolve = (result) => {
|
|
1212
|
+
cleanup();
|
|
1213
|
+
wrappedResolve(result);
|
|
1214
|
+
};
|
|
1215
|
+
pending.reject = (error) => {
|
|
1216
|
+
cleanup();
|
|
1217
|
+
wrappedReject(error);
|
|
1218
|
+
};
|
|
1219
|
+
execOptions.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1220
|
+
controller.registerPendingShell(pending);
|
|
1221
|
+
controller.sendShell(`${excludeFromContext ? "!!" : "!"}${command}`).catch((error) => {
|
|
1222
|
+
cleanup();
|
|
1223
|
+
controller.failPendingShell(pending, error instanceof Error ? error : new Error(String(error)));
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// packages/cli/src/commands/_spinner.ts
|
|
1231
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1232
|
+
|
|
1233
|
+
// packages/cli/src/commands/_pi-worker-bridge-extension.ts
|
|
1234
|
+
function recordOf2(value) {
|
|
1235
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1236
|
+
}
|
|
1237
|
+
function asText(value) {
|
|
1238
|
+
if (typeof value === "string")
|
|
1239
|
+
return value;
|
|
1240
|
+
if (value === null || value === undefined)
|
|
1241
|
+
return "";
|
|
1242
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
1243
|
+
return String(value);
|
|
1244
|
+
try {
|
|
1245
|
+
return JSON.stringify(value);
|
|
1246
|
+
} catch {
|
|
1247
|
+
return String(value);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
function names(value, key = "name") {
|
|
1251
|
+
if (!Array.isArray(value))
|
|
1252
|
+
return [];
|
|
1253
|
+
return value.flatMap((entry) => {
|
|
1254
|
+
if (typeof entry === "string")
|
|
1255
|
+
return [entry];
|
|
1256
|
+
const record = recordOf2(entry);
|
|
1257
|
+
const name = record?.[key];
|
|
1258
|
+
return typeof name === "string" ? [name] : [];
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
function renderWorkerCapabilities(capabilities) {
|
|
1262
|
+
const lines = ["Worker session capabilities (in effect for this run)"];
|
|
1263
|
+
const tools = Array.isArray(capabilities.tools) ? capabilities.tools : [];
|
|
1264
|
+
const activeTools = tools.flatMap((tool) => {
|
|
1265
|
+
const record = recordOf2(tool);
|
|
1266
|
+
return record && record.active === true && typeof record.name === "string" ? [record.name] : [];
|
|
1267
|
+
});
|
|
1268
|
+
const inactiveCount = tools.length - activeTools.length;
|
|
1269
|
+
lines.push(` tools ${activeTools.join(", ") || "(none)"}${inactiveCount > 0 ? ` (+${inactiveCount} inactive)` : ""}`);
|
|
1270
|
+
const extensions = Array.isArray(capabilities.extensions) ? capabilities.extensions : [];
|
|
1271
|
+
const extensionLabels = extensions.flatMap((entry) => {
|
|
1272
|
+
const record = recordOf2(entry);
|
|
1273
|
+
if (!record)
|
|
1274
|
+
return [];
|
|
1275
|
+
const path = typeof record.path === "string" ? record.path : "";
|
|
1276
|
+
const short = path.split("/").filter(Boolean).slice(-2).join("/") || path || "extension";
|
|
1277
|
+
return [short];
|
|
1278
|
+
});
|
|
1279
|
+
lines.push(` extensions ${extensionLabels.join(", ") || "(none)"}`);
|
|
1280
|
+
const hookEvents = names(capabilities.hookEvents);
|
|
1281
|
+
const hookList = Array.isArray(capabilities.hookEvents) ? capabilities.hookEvents.map(asText).filter(Boolean) : hookEvents;
|
|
1282
|
+
lines.push(` hooks ${hookList.join(", ") || "(none)"}`);
|
|
1283
|
+
lines.push(` skills ${names(capabilities.skills).join(", ") || "(none)"}`);
|
|
1284
|
+
lines.push(` prompts ${names(capabilities.prompts).join(", ") || "(none)"}`);
|
|
1285
|
+
const model = typeof capabilities.model === "string" ? capabilities.model : "(unknown)";
|
|
1286
|
+
const thinking = typeof capabilities.thinkingLevel === "string" ? capabilities.thinkingLevel : "";
|
|
1287
|
+
const cwd = typeof capabilities.cwd === "string" ? capabilities.cwd : "";
|
|
1288
|
+
lines.push(` model ${model}${thinking ? ` \xB7 ${thinking}` : ""}`);
|
|
1289
|
+
if (cwd)
|
|
1290
|
+
lines.push(` cwd ${cwd}`);
|
|
1291
|
+
lines.push(" (worker commands are in your palette as [worker \u2026] \xB7 /worker shows this again)");
|
|
1292
|
+
return lines;
|
|
1293
|
+
}
|
|
1294
|
+
async function answerExtensionUiRequest(options, ctx, request) {
|
|
1295
|
+
const requestId = String(request.requestId ?? request.id ?? `ui-${Date.now()}`);
|
|
1296
|
+
const method = String(request.method ?? request.type ?? "input");
|
|
1297
|
+
const prompt = asText(request.prompt ?? request.message ?? request.title ?? method);
|
|
1298
|
+
const rawOptions = Array.isArray(request.options) ? request.options : Array.isArray(request.items) ? request.items : [];
|
|
1299
|
+
const choices = rawOptions.map((option) => {
|
|
1300
|
+
const record = recordOf2(option);
|
|
1301
|
+
return record ? asText(record.label ?? record.value ?? record.name ?? option) : asText(option);
|
|
1302
|
+
}).filter(Boolean);
|
|
1303
|
+
try {
|
|
1304
|
+
if (method === "confirm") {
|
|
1305
|
+
const confirmed = await ctx.ui.confirm("Worker request", prompt);
|
|
1306
|
+
await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, { value: confirmed, confirmed });
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (choices.length > 0) {
|
|
1310
|
+
const selected = await ctx.ui.select(prompt, choices);
|
|
1311
|
+
await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, selected === undefined ? { cancelled: true } : { value: selected });
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const value = await ctx.ui.input("Worker request", prompt);
|
|
1315
|
+
await respondRunPiExtensionUiViaServer(options.context, options.runId, requestId, value === undefined ? { cancelled: true } : { value });
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
ctx.ui.notify(`Failed to answer worker UI request: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
var LOCALLY_OWNED_COMMANDS = new Set(["detach", "quit", "q", "stop", "worker", "settings", "model", "thinking", "compact", "session", "name", "reload"]);
|
|
1321
|
+
function registerDaemonCommands(pi, options, ctx, commands, registered) {
|
|
1322
|
+
for (const command of commands) {
|
|
1323
|
+
const record = recordOf2(command);
|
|
1324
|
+
const name = typeof record?.name === "string" ? record.name : "";
|
|
1325
|
+
const source = typeof record?.source === "string" ? record.source : "worker";
|
|
1326
|
+
if (!name || source === "builtin" || registered.has(name) || LOCALLY_OWNED_COMMANDS.has(name))
|
|
1327
|
+
continue;
|
|
1328
|
+
registered.add(name);
|
|
1329
|
+
const description = typeof record?.description === "string" ? record.description : undefined;
|
|
1330
|
+
try {
|
|
1331
|
+
pi.registerCommand(name, {
|
|
1332
|
+
description: `[worker ${source}] ${description ?? ""}`.trim(),
|
|
1333
|
+
handler: async (args) => {
|
|
1334
|
+
try {
|
|
1335
|
+
const message = await options.controller.sendCommand(`/${name}${args ? ` ${args}` : ""}`);
|
|
1336
|
+
ctx.ui.notify(message, "info");
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
} catch {}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
function createRigWorkerPiBridgeExtension(options) {
|
|
1346
|
+
return (pi) => {
|
|
1347
|
+
const registeredDaemonCommands = new Set;
|
|
1348
|
+
let capabilityLines = null;
|
|
1349
|
+
let statusText = "connecting to worker session";
|
|
1350
|
+
let busy = true;
|
|
1351
|
+
let connected = false;
|
|
1352
|
+
let frame = 0;
|
|
1353
|
+
let spinnerTimer = null;
|
|
1354
|
+
const renderStatus = (ctx) => {
|
|
1355
|
+
const prefix = busy ? `${SPINNER_FRAMES[frame]} ` : connected ? "\u25CF " : "\u25CB ";
|
|
1356
|
+
ctx.ui.setStatus("rig-worker-pi", `${prefix}${statusText}`);
|
|
1357
|
+
};
|
|
1358
|
+
const setStatus = (ctx, text2, isBusy) => {
|
|
1359
|
+
statusText = text2;
|
|
1360
|
+
busy = isBusy;
|
|
1361
|
+
renderStatus(ctx);
|
|
1362
|
+
};
|
|
1363
|
+
pi.registerCommand("detach", {
|
|
1364
|
+
description: "Detach from this run; the worker keeps going",
|
|
1365
|
+
handler: async (_args, ctx) => {
|
|
1366
|
+
ctx.ui.notify("Detached locally; the worker Pi daemon continues.", "info");
|
|
1367
|
+
ctx.shutdown();
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
pi.registerCommand("stop", {
|
|
1371
|
+
description: "Stop the worker Pi run and detach",
|
|
1372
|
+
handler: async (_args, ctx) => {
|
|
1373
|
+
await options.controller.abort();
|
|
1374
|
+
ctx.ui.notify("Stop requested for the worker Pi daemon.", "info");
|
|
1375
|
+
ctx.shutdown();
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
pi.registerCommand("worker", {
|
|
1379
|
+
description: "Show the worker session's real capabilities (tools, extensions, hooks, skills)",
|
|
1380
|
+
handler: async (_args, ctx) => {
|
|
1381
|
+
if (capabilityLines)
|
|
1382
|
+
ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
|
|
1383
|
+
else
|
|
1384
|
+
ctx.ui.notify("Worker capabilities are not available yet (still connecting).", "info");
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
pi.on("user_bash", (event) => ({
|
|
1388
|
+
operations: createRemoteBashOperations(options.controller, event.excludeFromContext === true)
|
|
1389
|
+
}));
|
|
1390
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1391
|
+
ctx.ui.setTitle("Rig \xB7 enriched bundled Pi");
|
|
1392
|
+
setStatus(ctx, "waiting for worker Pi daemon", true);
|
|
1393
|
+
spinnerTimer = setInterval(() => {
|
|
1394
|
+
frame = (frame + 1) % SPINNER_FRAMES.length;
|
|
1395
|
+
if (busy)
|
|
1396
|
+
renderStatus(ctx);
|
|
1397
|
+
}, 150);
|
|
1398
|
+
ctx.ui.notify(`Enriched bundled Pi \u2014 native UI + Rig layers, worker brain. Attached to run ${options.runId}. /worker shows live capabilities \xB7 /detach exits \xB7 /stop cancels.`, "info");
|
|
1399
|
+
if (options.initialMessageSent)
|
|
1400
|
+
ctx.ui.notify("Initial message sent to the worker Pi daemon.", "info");
|
|
1401
|
+
const nativeUi = ctx.ui;
|
|
1402
|
+
options.controller.setUiHooks({
|
|
1403
|
+
onStatusText: (text2) => setStatus(ctx, text2, !connected),
|
|
1404
|
+
onActivity: (label, detail) => {
|
|
1405
|
+
const active = label !== "idle" && !/complete|ready/i.test(label);
|
|
1406
|
+
setStatus(ctx, [label, detail].filter(Boolean).join(" \u2014 "), active);
|
|
1407
|
+
if (/agent running|tool:/.test(label))
|
|
1408
|
+
ctx.ui.setWidget("rig-worker-capabilities", undefined);
|
|
1409
|
+
},
|
|
1410
|
+
onConnectionChange: (nextConnected) => {
|
|
1411
|
+
connected = nextConnected;
|
|
1412
|
+
setStatus(ctx, nextConnected ? "worker session live" : "worker session disconnected", false);
|
|
1413
|
+
},
|
|
1414
|
+
onError: (message) => ctx.ui.notify(message, "error"),
|
|
1415
|
+
onExtensionUiRequest: (request) => {
|
|
1416
|
+
answerExtensionUiRequest(options, ctx, request);
|
|
1417
|
+
},
|
|
1418
|
+
onCatchUp: (messages, commands, capabilities) => {
|
|
1419
|
+
if (nativeUi.appendSessionMessages)
|
|
1420
|
+
nativeUi.appendSessionMessages(messages);
|
|
1421
|
+
const cwd = options.controller.status.cwd;
|
|
1422
|
+
if (nativeUi.setDisplayCwd && cwd)
|
|
1423
|
+
nativeUi.setDisplayCwd(cwd);
|
|
1424
|
+
registerDaemonCommands(pi, options, ctx, commands, registeredDaemonCommands);
|
|
1425
|
+
if (capabilities) {
|
|
1426
|
+
capabilityLines = renderWorkerCapabilities(capabilities);
|
|
1427
|
+
ctx.ui.setWidget("rig-worker-capabilities", capabilityLines, { placement: "aboveEditor" });
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
options.controller.connect().catch((error) => {
|
|
1432
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1433
|
+
});
|
|
1434
|
+
});
|
|
1435
|
+
pi.on("session_shutdown", () => {
|
|
1436
|
+
if (spinnerTimer)
|
|
1437
|
+
clearInterval(spinnerTimer);
|
|
1438
|
+
options.controller.close();
|
|
1439
|
+
});
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// packages/cli/src/commands/_pi-frontend.ts
|
|
1444
|
+
function setTemporaryEnv(updates) {
|
|
1445
|
+
const previous = new Map;
|
|
1446
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
1447
|
+
previous.set(key, process.env[key]);
|
|
1448
|
+
process.env[key] = value;
|
|
1449
|
+
}
|
|
1450
|
+
return () => {
|
|
1451
|
+
for (const [key, value] of previous) {
|
|
1452
|
+
if (value === undefined)
|
|
1453
|
+
delete process.env[key];
|
|
1454
|
+
else
|
|
1455
|
+
process.env[key] = value;
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
async function attachRunBundledPiFrontend(context, input) {
|
|
1460
|
+
const tempSessionDir = mkdtempSync(join2(tmpdir(), "rig-pi-frontend-sessions-"));
|
|
1461
|
+
const restoreEnv = setTemporaryEnv({
|
|
1462
|
+
PI_CODING_AGENT_SESSION_DIR: tempSessionDir,
|
|
1463
|
+
PI_SKIP_VERSION_CHECK: "1",
|
|
1464
|
+
PI_HIDDEN_COMMANDS: "import,fork,clone,tree,new,resume,trust"
|
|
1465
|
+
});
|
|
1466
|
+
const controller = new RigRemoteSessionController({ context, runId: input.runId });
|
|
1467
|
+
const createRemoteRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
|
|
1468
|
+
const services = await createAgentSessionServices({
|
|
1469
|
+
cwd,
|
|
1470
|
+
agentDir,
|
|
1471
|
+
resourceLoaderOptions: {
|
|
1472
|
+
extensionFactories: [
|
|
1473
|
+
createRigWorkerPiBridgeExtension({
|
|
1474
|
+
context,
|
|
1475
|
+
controller,
|
|
1476
|
+
runId: input.runId,
|
|
1477
|
+
initialMessageSent: input.steered === true
|
|
1478
|
+
})
|
|
1479
|
+
],
|
|
1480
|
+
noExtensions: true,
|
|
1481
|
+
noSkills: true,
|
|
1482
|
+
noPromptTemplates: true,
|
|
1483
|
+
noContextFiles: true
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
const created = await createAgentSessionFromServices({
|
|
1487
|
+
services,
|
|
1488
|
+
sessionManager,
|
|
1489
|
+
sessionStartEvent,
|
|
1490
|
+
noTools: "all",
|
|
1491
|
+
sessionFactory: (config) => new RigRemoteAgentSession(config, controller)
|
|
1492
|
+
});
|
|
1493
|
+
return { ...created, services, diagnostics: services.diagnostics };
|
|
1494
|
+
};
|
|
1495
|
+
let detached = false;
|
|
1496
|
+
try {
|
|
1497
|
+
await runPiMain([], {
|
|
1498
|
+
createRuntimeOverride: () => createRemoteRuntime
|
|
1499
|
+
});
|
|
1500
|
+
detached = true;
|
|
1501
|
+
} finally {
|
|
1502
|
+
restoreEnv();
|
|
1503
|
+
controller.close();
|
|
1504
|
+
rmSync(tempSessionDir, { recursive: true, force: true });
|
|
1505
|
+
}
|
|
1506
|
+
let run = { runId: input.runId, status: "unknown" };
|
|
1507
|
+
try {
|
|
1508
|
+
run = await getRunDetailsViaServer(context, input.runId);
|
|
1509
|
+
} catch {}
|
|
1510
|
+
return {
|
|
1511
|
+
run,
|
|
1512
|
+
logs: [],
|
|
1513
|
+
timeline: [],
|
|
1514
|
+
timelineCursor: null,
|
|
1515
|
+
steered: input.steered === true,
|
|
1516
|
+
detached,
|
|
1517
|
+
rendered: "enriched bundled Pi frontend with remote worker session runtime"
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// packages/cli/src/commands/_operator-view.ts
|
|
1522
|
+
var TERMINAL_RUN_STATUSES2 = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
338
1523
|
function runStatusFromPayload(payload) {
|
|
339
1524
|
const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
340
1525
|
return String(run.status ?? "unknown").toLowerCase();
|
|
@@ -356,56 +1541,268 @@ async function applyOperatorCommand(context, input, deps = {}) {
|
|
|
356
1541
|
await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
|
|
357
1542
|
return { action: "continue", message: "Steering message queued." };
|
|
358
1543
|
}
|
|
359
|
-
async function readOperatorSnapshot(context, runId) {
|
|
1544
|
+
async function readOperatorSnapshot(context, runId, options = {}) {
|
|
360
1545
|
const run = await getRunDetailsViaServer(context, runId);
|
|
361
1546
|
const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
|
|
362
|
-
const
|
|
363
|
-
|
|
1547
|
+
const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
|
|
1548
|
+
entries: [{
|
|
1549
|
+
id: `timeline-unavailable:${runId}`,
|
|
1550
|
+
type: "timeline_warning",
|
|
1551
|
+
detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
|
|
1552
|
+
createdAt: new Date().toISOString()
|
|
1553
|
+
}],
|
|
1554
|
+
nextCursor: options.timelineCursor ?? null
|
|
1555
|
+
}));
|
|
1556
|
+
const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
|
|
1557
|
+
const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1558
|
+
const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
|
|
1559
|
+
return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
|
|
364
1560
|
}
|
|
365
1561
|
async function attachRunOperatorView(context, input) {
|
|
366
1562
|
let steered = false;
|
|
367
1563
|
if (input.message?.trim()) {
|
|
368
|
-
await steerRunViaServer(context, input.runId, input.message.trim());
|
|
1564
|
+
await sendRunPiPromptViaServer(context, input.runId, input.message.trim(), "steer").catch(() => steerRunViaServer(context, input.runId, input.message.trim()));
|
|
369
1565
|
steered = true;
|
|
370
1566
|
}
|
|
1567
|
+
if (input.follow && !input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
1568
|
+
return attachRunBundledPiFrontend(context, {
|
|
1569
|
+
runId: input.runId,
|
|
1570
|
+
steered
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
const surface = createOperatorSurface({ interactive: input.interactive !== false });
|
|
371
1574
|
let snapshot = await readOperatorSnapshot(context, input.runId);
|
|
372
1575
|
if (context.outputMode === "text") {
|
|
373
|
-
|
|
1576
|
+
surface.renderSnapshot(snapshot);
|
|
1577
|
+
surface.renderTimeline(snapshot.timeline);
|
|
1578
|
+
surface.renderLogs(snapshot.logs);
|
|
374
1579
|
if (steered)
|
|
375
|
-
|
|
1580
|
+
surface.info("Message submitted to worker Pi.");
|
|
376
1581
|
}
|
|
377
1582
|
let detached = false;
|
|
378
|
-
let
|
|
1583
|
+
let commandInput = null;
|
|
379
1584
|
if (input.follow && !input.once && context.outputMode === "text") {
|
|
380
1585
|
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)}`));
|
|
1586
|
+
surface.info("Controls: /user <message>, /stop, /detach");
|
|
1587
|
+
commandInput = surface.attachCommandInput(async (line) => {
|
|
1588
|
+
const result = await applyOperatorCommand(context, { runId: input.runId, line });
|
|
1589
|
+
if (result.message)
|
|
1590
|
+
surface.info(result.message);
|
|
1591
|
+
if (result.action === "detach" || result.action === "stopped") {
|
|
1592
|
+
detached = true;
|
|
1593
|
+
commandInput?.close();
|
|
1594
|
+
}
|
|
392
1595
|
});
|
|
393
1596
|
}
|
|
394
|
-
let lastRendered = snapshot.rendered;
|
|
395
1597
|
const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
|
|
396
|
-
|
|
1598
|
+
let timelineCursor = snapshot.timelineCursor;
|
|
1599
|
+
while (!detached && !TERMINAL_RUN_STATUSES2.has(runStatusFromPayload(snapshot.run))) {
|
|
397
1600
|
await Bun.sleep(pollMs);
|
|
398
|
-
snapshot = await readOperatorSnapshot(context, input.runId);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
1601
|
+
snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
|
|
1602
|
+
timelineCursor = snapshot.timelineCursor;
|
|
1603
|
+
surface.renderSnapshot(snapshot);
|
|
1604
|
+
surface.renderTimeline(snapshot.timeline);
|
|
1605
|
+
surface.renderLogs(snapshot.logs);
|
|
403
1606
|
}
|
|
404
|
-
|
|
1607
|
+
commandInput?.close();
|
|
405
1608
|
}
|
|
406
1609
|
return { ...snapshot, steered, detached };
|
|
407
1610
|
}
|
|
408
1611
|
|
|
1612
|
+
// packages/cli/src/commands/_cli-format.ts
|
|
1613
|
+
import { log, note } from "@clack/prompts";
|
|
1614
|
+
import pc from "picocolors";
|
|
1615
|
+
function stringField(record, key, fallback = "") {
|
|
1616
|
+
const value = record[key];
|
|
1617
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
1618
|
+
}
|
|
1619
|
+
function rawObject(record) {
|
|
1620
|
+
const raw = record.raw;
|
|
1621
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
1622
|
+
}
|
|
1623
|
+
function truncate(value, width) {
|
|
1624
|
+
if (value.length <= width)
|
|
1625
|
+
return value;
|
|
1626
|
+
if (width <= 1)
|
|
1627
|
+
return "\u2026";
|
|
1628
|
+
return `${value.slice(0, width - 1)}\u2026`;
|
|
1629
|
+
}
|
|
1630
|
+
function pad(value, width) {
|
|
1631
|
+
return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
|
|
1632
|
+
}
|
|
1633
|
+
function statusColor(status) {
|
|
1634
|
+
const normalized = status.toLowerCase();
|
|
1635
|
+
if (["completed", "merged", "closed", "done", "accepted", "pass", "selected", "approved"].includes(normalized))
|
|
1636
|
+
return pc.green;
|
|
1637
|
+
if (["failed", "needs_attention", "needs-attention", "blocked", "error", "rejected"].includes(normalized))
|
|
1638
|
+
return pc.red;
|
|
1639
|
+
if (["running", "reviewing", "validating", "in_progress", "in-progress", "remote"].includes(normalized))
|
|
1640
|
+
return pc.cyan;
|
|
1641
|
+
if (["ready", "open", "queued", "created", "preparing", "local", "pending"].includes(normalized))
|
|
1642
|
+
return pc.yellow;
|
|
1643
|
+
return pc.dim;
|
|
1644
|
+
}
|
|
1645
|
+
function compactDate(value) {
|
|
1646
|
+
if (!value.trim())
|
|
1647
|
+
return "";
|
|
1648
|
+
const parsed = Date.parse(value);
|
|
1649
|
+
if (!Number.isFinite(parsed))
|
|
1650
|
+
return value;
|
|
1651
|
+
return new Date(parsed).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
|
|
1652
|
+
}
|
|
1653
|
+
function firstString(record, keys, fallback = "") {
|
|
1654
|
+
for (const key of keys) {
|
|
1655
|
+
const value = stringField(record, key);
|
|
1656
|
+
if (value)
|
|
1657
|
+
return value;
|
|
1658
|
+
}
|
|
1659
|
+
return fallback;
|
|
1660
|
+
}
|
|
1661
|
+
function runIdOf(run) {
|
|
1662
|
+
return firstString(run, ["runId", "id"], "(unknown-run)");
|
|
1663
|
+
}
|
|
1664
|
+
function taskIdOf(run) {
|
|
1665
|
+
return firstString(run, ["taskId", "task", "task_id"]);
|
|
1666
|
+
}
|
|
1667
|
+
function runTitleOf(run) {
|
|
1668
|
+
return firstString(run, ["title", "summary", "name"], taskIdOf(run) || "(untitled)");
|
|
1669
|
+
}
|
|
1670
|
+
function shouldUseClackOutput() {
|
|
1671
|
+
return Boolean(process.stdout.isTTY) && process.env.RIG_CLI_PLAIN_HELP !== "1";
|
|
1672
|
+
}
|
|
1673
|
+
function printFormattedOutput(message, options = {}) {
|
|
1674
|
+
if (!shouldUseClackOutput()) {
|
|
1675
|
+
console.log(message);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
if (options.title)
|
|
1679
|
+
note(message, options.title);
|
|
1680
|
+
else
|
|
1681
|
+
log.message(message);
|
|
1682
|
+
}
|
|
1683
|
+
function formatStatusPill(status) {
|
|
1684
|
+
const label = status || "unknown";
|
|
1685
|
+
return statusColor(label)(`\u25CF ${label}`);
|
|
1686
|
+
}
|
|
1687
|
+
function formatSection(title, subtitle) {
|
|
1688
|
+
return `${pc.bold(pc.cyan("\u25C6"))} ${pc.bold(title)}${subtitle ? pc.dim(` \u2014 ${subtitle}`) : ""}`;
|
|
1689
|
+
}
|
|
1690
|
+
function formatSuccessCard(title, rows = []) {
|
|
1691
|
+
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}`);
|
|
1692
|
+
return [formatSection(title), ...body].join(`
|
|
1693
|
+
`);
|
|
1694
|
+
}
|
|
1695
|
+
function formatNextSteps(steps) {
|
|
1696
|
+
if (steps.length === 0)
|
|
1697
|
+
return [];
|
|
1698
|
+
return [pc.bold("Next"), ...steps.map((step) => `${pc.dim("\u203A")} ${step}`)];
|
|
1699
|
+
}
|
|
1700
|
+
function formatRunList(runs, options = {}) {
|
|
1701
|
+
if (runs.length === 0) {
|
|
1702
|
+
return [
|
|
1703
|
+
formatSection("Runs", "none recorded"),
|
|
1704
|
+
options.source === "server" ? pc.dim("No runs recorded on the selected Rig server.") : pc.dim("No runs recorded in .rig/runs."),
|
|
1705
|
+
"",
|
|
1706
|
+
...formatNextSteps(["Start one: `rig task run --next`", "Check server: `rig server status`"])
|
|
1707
|
+
].join(`
|
|
1708
|
+
`);
|
|
1709
|
+
}
|
|
1710
|
+
const rows = runs.map((run) => {
|
|
1711
|
+
const runId = stringField(run, "runId", stringField(run, "id", "(unknown-run)"));
|
|
1712
|
+
const status = stringField(run, "status", "unknown");
|
|
1713
|
+
const taskId = stringField(run, "taskId", "");
|
|
1714
|
+
const title = stringField(run, "title", taskId || "(untitled)");
|
|
1715
|
+
const runtime = stringField(run, "runtimeAdapter", "");
|
|
1716
|
+
return { runId, status, title, runtime };
|
|
1717
|
+
});
|
|
1718
|
+
const idWidth = Math.min(36, Math.max(6, ...rows.map((row) => row.runId.length)));
|
|
1719
|
+
const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
|
|
1720
|
+
const header = `${pc.bold(pad("RUN", idWidth))} ${pc.bold(pad("STATUS", statusWidth))} ${pc.bold("TITLE")}`;
|
|
1721
|
+
const body = rows.map((row) => [
|
|
1722
|
+
pc.bold(pad(truncate(row.runId, idWidth), idWidth)),
|
|
1723
|
+
statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
|
|
1724
|
+
`${row.title}${row.runtime ? pc.dim(` ${row.runtime}`) : ""}`
|
|
1725
|
+
].join(" "));
|
|
1726
|
+
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(`
|
|
1727
|
+
`);
|
|
1728
|
+
}
|
|
1729
|
+
function formatRunCard(run, options = {}) {
|
|
1730
|
+
const raw = rawObject(run);
|
|
1731
|
+
const merged = { ...raw, ...run };
|
|
1732
|
+
const runId = runIdOf(merged);
|
|
1733
|
+
const status = firstString(merged, ["status"], "unknown");
|
|
1734
|
+
const taskId = taskIdOf(merged);
|
|
1735
|
+
const title = runTitleOf(merged);
|
|
1736
|
+
const runtime = firstString(merged, ["runtimeAdapter", "runtime", "adapter"]);
|
|
1737
|
+
const mode = firstString(merged, ["runtimeMode", "mode"]);
|
|
1738
|
+
const interaction = firstString(merged, ["interactionMode"]);
|
|
1739
|
+
const created = compactDate(firstString(merged, ["createdAt"]));
|
|
1740
|
+
const started = compactDate(firstString(merged, ["startedAt"]));
|
|
1741
|
+
const updated = compactDate(firstString(merged, ["updatedAt"]));
|
|
1742
|
+
const completed = compactDate(firstString(merged, ["completedAt", "finishedAt"]));
|
|
1743
|
+
const worktree = firstString(merged, ["worktreePath", "cwd", "projectRoot"]);
|
|
1744
|
+
const piSession = merged.piSession && typeof merged.piSession === "object" && !Array.isArray(merged.piSession) ? firstString(merged.piSession, ["sessionId", "id"]) : "";
|
|
1745
|
+
const timeline = Array.isArray(merged.timeline) ? merged.timeline.length : null;
|
|
1746
|
+
const approvals = Array.isArray(merged.approvals) ? merged.approvals.length : null;
|
|
1747
|
+
const inputs = Array.isArray(merged.userInputs) ? merged.userInputs.length : null;
|
|
1748
|
+
const rows = [
|
|
1749
|
+
["run", pc.bold(runId)],
|
|
1750
|
+
["status", formatStatusPill(status)],
|
|
1751
|
+
["task", taskId],
|
|
1752
|
+
["title", title],
|
|
1753
|
+
["runtime", [runtime, mode, interaction].filter(Boolean).join(" \xB7 ")],
|
|
1754
|
+
["created", created],
|
|
1755
|
+
["started", started],
|
|
1756
|
+
["updated", updated],
|
|
1757
|
+
["completed", completed],
|
|
1758
|
+
["worktree", worktree],
|
|
1759
|
+
["pi", piSession],
|
|
1760
|
+
["timeline", timeline],
|
|
1761
|
+
["approvals", approvals],
|
|
1762
|
+
["inputs", inputs]
|
|
1763
|
+
];
|
|
1764
|
+
return [
|
|
1765
|
+
formatSuccessCard(options.title ?? "Run details", rows),
|
|
1766
|
+
"",
|
|
1767
|
+
...formatNextSteps([`Follow live: \`rig run attach ${runId} --follow\``, `Raw payload: \`rig run show ${runId} --raw\``])
|
|
1768
|
+
].join(`
|
|
1769
|
+
`);
|
|
1770
|
+
}
|
|
1771
|
+
function formatRunStatus(summary, options = {}) {
|
|
1772
|
+
const activeRuns = summary.activeRuns ?? [];
|
|
1773
|
+
const recentRuns = summary.recentRuns ?? [];
|
|
1774
|
+
const lines = [formatSection("Run status", options.source === "server" ? "selected server" : "local state")];
|
|
1775
|
+
lines.push("", pc.bold(`Active runs (${activeRuns.length})`));
|
|
1776
|
+
if (activeRuns.length === 0) {
|
|
1777
|
+
lines.push(pc.dim("No active runs."));
|
|
1778
|
+
} else {
|
|
1779
|
+
for (const run of activeRuns) {
|
|
1780
|
+
lines.push(formatRunSummaryLine(run));
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
lines.push("", pc.bold(`Recent runs (${recentRuns.length})`));
|
|
1784
|
+
if (recentRuns.length === 0) {
|
|
1785
|
+
lines.push(pc.dim("No recent terminal runs."));
|
|
1786
|
+
} else {
|
|
1787
|
+
for (const run of recentRuns.slice(0, 10)) {
|
|
1788
|
+
lines.push(formatRunSummaryLine(run));
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
lines.push("", ...formatNextSteps(["Start work: `rig task run --next`", "Attach: `rig run attach <run-id> --follow`", "Details: `rig run show <run-id>`"]));
|
|
1792
|
+
return lines.join(`
|
|
1793
|
+
`);
|
|
1794
|
+
}
|
|
1795
|
+
function formatRunSummaryLine(run) {
|
|
1796
|
+
const record = run;
|
|
1797
|
+
const runId = runIdOf(record);
|
|
1798
|
+
const status = firstString(record, ["status"], "unknown");
|
|
1799
|
+
const taskId = taskIdOf(record);
|
|
1800
|
+
const title = runTitleOf(record);
|
|
1801
|
+
const runtime = firstString(record, ["runtimeAdapter", "runtime", "adapter"]);
|
|
1802
|
+
const descriptor = [taskId, title].filter(Boolean).join(" \xB7 ");
|
|
1803
|
+
return `${pc.dim("\u2502")} ${pc.bold(runId)} ${formatStatusPill(status)} ${descriptor}${runtime ? pc.dim(` ${runtime}`) : ""}`;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
409
1806
|
// packages/cli/src/commands/run.ts
|
|
410
1807
|
function normalizeRemoteRunDetails(payload) {
|
|
411
1808
|
const run = payload.run;
|
|
@@ -418,6 +1815,25 @@ function normalizeRemoteRunDetails(payload) {
|
|
|
418
1815
|
...Array.isArray(payload.userInputs) ? { userInputs: payload.userInputs } : {}
|
|
419
1816
|
};
|
|
420
1817
|
}
|
|
1818
|
+
var REMOTE_TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged"]);
|
|
1819
|
+
function isRemoteConnectionSelected(projectRoot) {
|
|
1820
|
+
return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
|
|
1821
|
+
}
|
|
1822
|
+
async function listRunsForSelectedConnection(context, options = {}) {
|
|
1823
|
+
if (isRemoteConnectionSelected(context.projectRoot)) {
|
|
1824
|
+
return { runs: await listRunsViaServer(context, options), source: "server" };
|
|
1825
|
+
}
|
|
1826
|
+
return { runs: listAuthorityRuns(context.projectRoot), source: "local" };
|
|
1827
|
+
}
|
|
1828
|
+
function runStringField(run, key, fallback = "") {
|
|
1829
|
+
const value = run[key];
|
|
1830
|
+
return typeof value === "string" && value.trim() ? value : fallback;
|
|
1831
|
+
}
|
|
1832
|
+
function buildServerRunStatus(runs) {
|
|
1833
|
+
const activeRuns = runs.filter((run) => !REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
|
|
1834
|
+
const recentRuns = runs.filter((run) => REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
|
|
1835
|
+
return { activeRuns, recentRuns, runs };
|
|
1836
|
+
}
|
|
421
1837
|
function shouldPromptForEpicSelection(context, command, promptEpic, noEpicPrompt) {
|
|
422
1838
|
if (noEpicPrompt) {
|
|
423
1839
|
return false;
|
|
@@ -479,21 +1895,15 @@ async function promptForEpicSelection(projectRoot, command) {
|
|
|
479
1895
|
}
|
|
480
1896
|
async function executeRun(context, args) {
|
|
481
1897
|
const [command = "status", ...rest] = args;
|
|
482
|
-
const runtimeContext =
|
|
1898
|
+
const runtimeContext = loadRuntimeContextFromEnv() ?? undefined;
|
|
483
1899
|
switch (command) {
|
|
484
1900
|
case "list": {
|
|
485
|
-
requireNoExtraArgs(rest, "
|
|
486
|
-
const runs =
|
|
1901
|
+
requireNoExtraArgs(rest, "rig run list");
|
|
1902
|
+
const { runs, source } = await listRunsForSelectedConnection(context, { limit: 100 });
|
|
487
1903
|
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
|
-
}
|
|
1904
|
+
printFormattedOutput(formatRunList(runs, { source }));
|
|
495
1905
|
}
|
|
496
|
-
return { ok: true, group: "run", command, details: { runs } };
|
|
1906
|
+
return { ok: true, group: "run", command, details: { runs, source } };
|
|
497
1907
|
}
|
|
498
1908
|
case "delete": {
|
|
499
1909
|
let pending = rest;
|
|
@@ -501,7 +1911,7 @@ async function executeRun(context, args) {
|
|
|
501
1911
|
pending = run.rest;
|
|
502
1912
|
const purgeArtifacts = takeFlag(pending, "--purge-artifacts");
|
|
503
1913
|
pending = purgeArtifacts.rest;
|
|
504
|
-
requireNoExtraArgs(pending, "
|
|
1914
|
+
requireNoExtraArgs(pending, "rig run delete --run <id> [--purge-artifacts]");
|
|
505
1915
|
if (!run.value) {
|
|
506
1916
|
throw new CliError2("run delete requires --run <id>.");
|
|
507
1917
|
}
|
|
@@ -531,7 +1941,7 @@ async function executeRun(context, args) {
|
|
|
531
1941
|
pending = keepRuntimes.rest;
|
|
532
1942
|
const keepQueue = takeFlag(pending, "--keep-queue");
|
|
533
1943
|
pending = keepQueue.rest;
|
|
534
|
-
requireNoExtraArgs(pending, "
|
|
1944
|
+
requireNoExtraArgs(pending, "rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
|
|
535
1945
|
if (!all.value) {
|
|
536
1946
|
throw new CliError2("run cleanup currently requires --all.");
|
|
537
1947
|
}
|
|
@@ -550,20 +1960,25 @@ async function executeRun(context, args) {
|
|
|
550
1960
|
}
|
|
551
1961
|
case "show": {
|
|
552
1962
|
let pending = rest;
|
|
1963
|
+
const rawResult = takeFlag(pending, "--raw");
|
|
1964
|
+
pending = rawResult.rest;
|
|
553
1965
|
const run = takeOption(pending, "--run");
|
|
554
1966
|
pending = run.rest;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1967
|
+
const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
|
|
1968
|
+
const extra = positionalRunId ? pending.slice(1) : pending;
|
|
1969
|
+
requireNoExtraArgs(extra, "rig run show <id>|--run <id> [--raw]");
|
|
1970
|
+
const runId = run.value ?? positionalRunId;
|
|
1971
|
+
if (!runId) {
|
|
1972
|
+
throw new CliError2("run show requires a run id.");
|
|
558
1973
|
}
|
|
559
|
-
const record =
|
|
1974
|
+
const record = readAuthorityRun2(context.projectRoot, runId) ?? normalizeRemoteRunDetails(await getRunDetailsViaServer(context, runId).catch(() => ({})));
|
|
560
1975
|
if (!record) {
|
|
561
|
-
throw new CliError2(`Run not found: ${
|
|
1976
|
+
throw new CliError2(`Run not found: ${runId}`, 2);
|
|
562
1977
|
}
|
|
563
1978
|
if (context.outputMode === "text") {
|
|
564
|
-
|
|
1979
|
+
printFormattedOutput(rawResult.value ? JSON.stringify(record, null, 2) : formatRunCard(record));
|
|
565
1980
|
}
|
|
566
|
-
return { ok: true, group: "run", command, details: record };
|
|
1981
|
+
return { ok: true, group: "run", command, details: { ...record, rawOutput: rawResult.value } };
|
|
567
1982
|
}
|
|
568
1983
|
case "timeline": {
|
|
569
1984
|
let pending = rest;
|
|
@@ -571,38 +1986,68 @@ async function executeRun(context, args) {
|
|
|
571
1986
|
pending = run.rest;
|
|
572
1987
|
const follow = takeFlag(pending, "--follow");
|
|
573
1988
|
pending = follow.rest;
|
|
574
|
-
requireNoExtraArgs(pending, "
|
|
1989
|
+
requireNoExtraArgs(pending, "rig run timeline --run <id> [--follow]");
|
|
575
1990
|
if (!run.value) {
|
|
576
1991
|
throw new CliError2("run timeline requires --run <id>.");
|
|
577
1992
|
}
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
return events2;
|
|
587
|
-
};
|
|
588
|
-
const events = printEvents();
|
|
1993
|
+
const renderer = createPiRunStreamRenderer();
|
|
1994
|
+
let cursor = null;
|
|
1995
|
+
const page = await getRunTimelineViaServer(context, run.value, { limit: 500 });
|
|
1996
|
+
const events = Array.isArray(page.entries) ? page.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1997
|
+
cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
|
|
1998
|
+
if (context.outputMode === "text") {
|
|
1999
|
+
renderer.renderTimeline(events);
|
|
2000
|
+
}
|
|
589
2001
|
if (follow.value && context.outputMode === "text") {
|
|
590
|
-
let lastLength = existsSync3(timelinePath) ? readFileSync3(timelinePath, "utf8").length : 0;
|
|
591
2002
|
while (true) {
|
|
592
2003
|
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
|
-
}
|
|
2004
|
+
const nextPage = await getRunTimelineViaServer(context, run.value, { limit: 500, ...cursor ? { cursor } : {} });
|
|
2005
|
+
const nextEvents = Array.isArray(nextPage.entries) ? nextPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
2006
|
+
cursor = typeof nextPage.nextCursor === "string" ? nextPage.nextCursor : cursor;
|
|
2007
|
+
renderer.renderTimeline(nextEvents);
|
|
603
2008
|
}
|
|
604
2009
|
}
|
|
605
|
-
return { ok: true, group: "run", command, details: { runId: run.value, events } };
|
|
2010
|
+
return { ok: true, group: "run", command, details: { runId: run.value, events, cursor } };
|
|
2011
|
+
}
|
|
2012
|
+
case "replay": {
|
|
2013
|
+
let pending = rest;
|
|
2014
|
+
const run = takeOption(pending, "--run");
|
|
2015
|
+
pending = run.rest;
|
|
2016
|
+
const withSession = takeFlag(pending, "--with-session");
|
|
2017
|
+
pending = withSession.rest;
|
|
2018
|
+
const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
|
|
2019
|
+
const extra = positionalRunId ? pending.slice(1) : pending;
|
|
2020
|
+
requireNoExtraArgs(extra, "rig run replay <id>|--run <id> [--with-session]");
|
|
2021
|
+
const runId = run.value ?? positionalRunId;
|
|
2022
|
+
if (!runId) {
|
|
2023
|
+
throw new CliError2("run replay requires a run id.", 2);
|
|
2024
|
+
}
|
|
2025
|
+
const replay = buildRunReplay(context.projectRoot, runId, { withSession: withSession.value });
|
|
2026
|
+
if (replay.entryCount === 0 && replay.sessionEntryCount === 0) {
|
|
2027
|
+
throw new CliError2(`No run.jsonl lifecycle log found for ${runId} (looked at ${replay.logPath}).`, 2);
|
|
2028
|
+
}
|
|
2029
|
+
if (context.outputMode === "text") {
|
|
2030
|
+
console.log(`Replay of ${runId} (${replay.entryCount} lifecycle entries${withSession.value ? `, ${replay.sessionEntryCount} session entries` : ""}):`);
|
|
2031
|
+
for (const line of replay.lines) {
|
|
2032
|
+
console.log(line);
|
|
2033
|
+
}
|
|
2034
|
+
if (withSession.value && !replay.sessionFile) {
|
|
2035
|
+
console.log("(no Pi session file resolvable from the run record; showing run.jsonl only)");
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
return {
|
|
2039
|
+
ok: true,
|
|
2040
|
+
group: "run",
|
|
2041
|
+
command,
|
|
2042
|
+
details: {
|
|
2043
|
+
runId,
|
|
2044
|
+
logPath: replay.logPath,
|
|
2045
|
+
sessionFile: replay.sessionFile,
|
|
2046
|
+
entryCount: replay.entryCount,
|
|
2047
|
+
sessionEntryCount: replay.sessionEntryCount,
|
|
2048
|
+
lines: replay.lines
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
606
2051
|
}
|
|
607
2052
|
case "attach": {
|
|
608
2053
|
let pending = rest;
|
|
@@ -618,41 +2063,38 @@ async function executeRun(context, args) {
|
|
|
618
2063
|
pending = pollMs.rest;
|
|
619
2064
|
const positionalRunId = pending.length > 0 ? pending[0] : undefined;
|
|
620
2065
|
const extra = positionalRunId ? pending.slice(1) : pending;
|
|
621
|
-
requireNoExtraArgs(extra, "
|
|
2066
|
+
requireNoExtraArgs(extra, "rig run attach <run-id>|--run <run-id> [--message <text>] [--once|--follow] [--poll-ms <ms>]");
|
|
622
2067
|
const runId = runOption.value ?? positionalRunId;
|
|
623
2068
|
if (!runId) {
|
|
624
2069
|
throw new CliError2("run attach requires a run id.", 2);
|
|
625
2070
|
}
|
|
2071
|
+
let steered = false;
|
|
2072
|
+
if (messageOption.value?.trim()) {
|
|
2073
|
+
await steerRunViaServer(context, runId, messageOption.value.trim());
|
|
2074
|
+
steered = true;
|
|
2075
|
+
}
|
|
626
2076
|
const attached = await attachRunOperatorView(context, {
|
|
627
2077
|
runId,
|
|
628
|
-
message:
|
|
2078
|
+
message: null,
|
|
629
2079
|
once: once.value,
|
|
630
2080
|
follow: follow.value,
|
|
631
2081
|
pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)
|
|
632
2082
|
});
|
|
633
|
-
return { ok: true, group: "run", command, details: attached };
|
|
2083
|
+
return { ok: true, group: "run", command, details: { ...attached, steered: attached.steered || steered } };
|
|
634
2084
|
}
|
|
635
2085
|
case "status": {
|
|
636
|
-
requireNoExtraArgs(rest, "
|
|
2086
|
+
requireNoExtraArgs(rest, "rig run status");
|
|
637
2087
|
if (context.dryRun) {
|
|
638
2088
|
if (context.outputMode === "text") {
|
|
639
2089
|
console.log("[dry-run] rig run status");
|
|
640
2090
|
}
|
|
641
2091
|
return { ok: true, group: "run", command };
|
|
642
2092
|
}
|
|
643
|
-
const summary = runStatus(context.projectRoot, runtimeContext);
|
|
2093
|
+
const summary = isRemoteConnectionSelected(context.projectRoot) ? buildServerRunStatus(await listRunsViaServer(context, { limit: 100 })) : runStatus(context.projectRoot, runtimeContext);
|
|
2094
|
+
const activeRuns = Array.isArray(summary.activeRuns) ? summary.activeRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
|
|
2095
|
+
const recentRuns = Array.isArray(summary.recentRuns) ? summary.recentRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
|
|
644
2096
|
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
|
-
}
|
|
2097
|
+
printFormattedOutput(formatRunStatus({ activeRuns, recentRuns, runs: Array.isArray(summary.runs) ? summary.runs : [...activeRuns, ...recentRuns] }, { source: isRemoteConnectionSelected(context.projectRoot) ? "server" : "local" }));
|
|
656
2098
|
}
|
|
657
2099
|
return { ok: true, group: "run", command, details: summary };
|
|
658
2100
|
}
|
|
@@ -676,7 +2118,7 @@ async function executeRun(context, args) {
|
|
|
676
2118
|
pending = pollResult.rest;
|
|
677
2119
|
const noServerResult = takeFlag(pending, "--no-server");
|
|
678
2120
|
pending = noServerResult.rest;
|
|
679
|
-
requireNoExtraArgs(pending, "
|
|
2121
|
+
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
2122
|
if (promptEpicResult.value && noEpicPromptResult.value) {
|
|
681
2123
|
throw new CliError2("Cannot use --prompt-epic and --no-epic-prompt together.");
|
|
682
2124
|
}
|
|
@@ -724,7 +2166,7 @@ async function executeRun(context, args) {
|
|
|
724
2166
|
};
|
|
725
2167
|
}
|
|
726
2168
|
case "resume": {
|
|
727
|
-
requireNoExtraArgs(rest, "
|
|
2169
|
+
requireNoExtraArgs(rest, "rig run resume");
|
|
728
2170
|
if (context.dryRun) {
|
|
729
2171
|
if (context.outputMode === "text") {
|
|
730
2172
|
console.log("[dry-run] rig run resume");
|
|
@@ -738,7 +2180,7 @@ async function executeRun(context, args) {
|
|
|
738
2180
|
return { ok: true, group: "run", command, details: resumed };
|
|
739
2181
|
}
|
|
740
2182
|
case "restart": {
|
|
741
|
-
requireNoExtraArgs(rest, "
|
|
2183
|
+
requireNoExtraArgs(rest, "rig run restart");
|
|
742
2184
|
if (context.dryRun) {
|
|
743
2185
|
if (context.outputMode === "text") {
|
|
744
2186
|
console.log("[dry-run] rig run restart");
|
|
@@ -751,11 +2193,38 @@ async function executeRun(context, args) {
|
|
|
751
2193
|
}
|
|
752
2194
|
return { ok: true, group: "run", command, details: restarted };
|
|
753
2195
|
}
|
|
2196
|
+
case "steer": {
|
|
2197
|
+
const runOption = takeOption(rest, "--run");
|
|
2198
|
+
const messageOption = takeOption(runOption.rest, "--message");
|
|
2199
|
+
const shortMessageOption = takeOption(messageOption.rest, "-m");
|
|
2200
|
+
const positionalRunId = shortMessageOption.rest.length > 0 ? shortMessageOption.rest[0] : undefined;
|
|
2201
|
+
const extra = positionalRunId ? shortMessageOption.rest.slice(1) : shortMessageOption.rest;
|
|
2202
|
+
requireNoExtraArgs(extra, "rig run steer [<run-id>|--run <id>] --message <text>");
|
|
2203
|
+
const runId = runOption.value ?? positionalRunId;
|
|
2204
|
+
const message = messageOption.value ?? shortMessageOption.value;
|
|
2205
|
+
if (!runId) {
|
|
2206
|
+
throw new CliError2("run steer requires a run id (positional or --run <id>).", 2);
|
|
2207
|
+
}
|
|
2208
|
+
if (!message?.trim()) {
|
|
2209
|
+
throw new CliError2("run steer requires --message <text>.", 2);
|
|
2210
|
+
}
|
|
2211
|
+
if (context.dryRun) {
|
|
2212
|
+
if (context.outputMode === "text") {
|
|
2213
|
+
console.log(`[dry-run] rig run steer ${runId} --message ${JSON.stringify(message)}`);
|
|
2214
|
+
}
|
|
2215
|
+
return { ok: true, group: "run", command, details: { runId, dryRun: true } };
|
|
2216
|
+
}
|
|
2217
|
+
await steerRunViaServer(context, runId, message.trim());
|
|
2218
|
+
if (context.outputMode === "text") {
|
|
2219
|
+
console.log(`Steering message queued for ${runId}.`);
|
|
2220
|
+
}
|
|
2221
|
+
return { ok: true, group: "run", command, details: { runId, queued: true } };
|
|
2222
|
+
}
|
|
754
2223
|
case "stop": {
|
|
755
2224
|
const runOption = takeOption(rest, "--run");
|
|
756
2225
|
const positionalRunId = runOption.rest.length > 0 ? runOption.rest[0] : undefined;
|
|
757
2226
|
const extra = positionalRunId ? runOption.rest.slice(1) : runOption.rest;
|
|
758
|
-
requireNoExtraArgs(extra, "
|
|
2227
|
+
requireNoExtraArgs(extra, "rig run stop [<run-id>|--run <id>]");
|
|
759
2228
|
const runId = runOption.value ?? positionalRunId;
|
|
760
2229
|
if (context.dryRun) {
|
|
761
2230
|
return {
|