@h-rig/cli 0.0.6-alpha.7 → 0.0.6-alpha.70
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 +4507 -1506
- package/dist/src/commands/_async-ui.js +152 -0
- 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 +30 -11
- package/dist/src/commands/_doctor-checks.js +177 -43
- package/dist/src/commands/_help-catalog.js +485 -0
- package/dist/src/commands/_json-output.js +56 -0
- package/dist/src/commands/_operator-surface.js +220 -0
- package/dist/src/commands/_operator-view.js +595 -72
- package/dist/src/commands/_parsers.js +18 -11
- package/dist/src/commands/_pi-frontend.js +411 -0
- package/dist/src/commands/_pi-install.js +4 -3
- package/dist/src/commands/_policy.js +12 -5
- package/dist/src/commands/_preflight.js +187 -127
- package/dist/src/commands/_run-driver-helpers.js +75 -22
- package/dist/src/commands/_run-replay.js +142 -0
- package/dist/src/commands/_server-client.js +343 -60
- package/dist/src/commands/_snapshot-upload.js +160 -38
- package/dist/src/commands/_spinner.js +65 -0
- package/dist/src/commands/_task-picker.js +44 -16
- package/dist/src/commands/agent.js +39 -20
- package/dist/src/commands/browser.js +28 -21
- package/dist/src/commands/connect.js +146 -33
- package/dist/src/commands/dist.js +19 -12
- package/dist/src/commands/doctor.js +304 -44
- package/dist/src/commands/github.js +301 -52
- package/dist/src/commands/inbox.js +679 -72
- package/dist/src/commands/init.js +622 -118
- package/dist/src/commands/inspect.js +515 -32
- package/dist/src/commands/inspector.js +20 -13
- package/dist/src/commands/pi.js +177 -0
- package/dist/src/commands/plugin.js +95 -27
- package/dist/src/commands/profile-and-review.js +26 -19
- package/dist/src/commands/queue.js +32 -12
- package/dist/src/commands/remote.js +43 -36
- package/dist/src/commands/repo-git-harness.js +22 -15
- package/dist/src/commands/run.js +1162 -158
- package/dist/src/commands/server.js +373 -56
- package/dist/src/commands/setup.js +316 -62
- package/dist/src/commands/stats.js +1030 -0
- package/dist/src/commands/task-report-bug.js +29 -22
- package/dist/src/commands/task-run-driver.js +862 -129
- package/dist/src/commands/task.js +1423 -311
- package/dist/src/commands/test.js +15 -8
- package/dist/src/commands/workspace.js +18 -11
- package/dist/src/commands.js +4446 -1499
- package/dist/src/index.js +4502 -1504
- package/dist/src/launcher.js +77 -13
- package/dist/src/report-bug.js +3 -3
- package/dist/src/runner.js +16 -22
- package/package.json +10 -5
package/dist/src/commands/run.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
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
|
-
import { CliError } from "@rig/runtime/control-plane/errors";
|
|
7
|
+
import { CliError as RuntimeCliError } 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
|
+
|
|
11
|
+
class CliError extends RuntimeCliError {
|
|
12
|
+
hint;
|
|
13
|
+
constructor(message, exitCode = 1, options = {}) {
|
|
14
|
+
super(message, exitCode);
|
|
15
|
+
if (options.hint?.trim()) {
|
|
16
|
+
this.hint = options.hint.trim();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
15
20
|
function takeFlag(args, flag) {
|
|
16
21
|
const rest = [];
|
|
17
22
|
let value = false;
|
|
@@ -32,7 +37,7 @@ function takeOption(args, option) {
|
|
|
32
37
|
if (current === option) {
|
|
33
38
|
const next = args[index + 1];
|
|
34
39
|
if (!next || next.startsWith("-")) {
|
|
35
|
-
throw new CliError(`Missing value for ${option}`);
|
|
40
|
+
throw new CliError(`Missing value for ${option}`, 1, { hint: `Provide a value after ${option}, e.g. \`${option} <value>\`.` });
|
|
36
41
|
}
|
|
37
42
|
value = next;
|
|
38
43
|
index += 1;
|
|
@@ -54,23 +59,19 @@ Usage: ${usage}`);
|
|
|
54
59
|
// packages/cli/src/commands/run.ts
|
|
55
60
|
import {
|
|
56
61
|
listAuthorityRuns,
|
|
57
|
-
readAuthorityRun
|
|
58
|
-
readJsonlFile,
|
|
59
|
-
resolveAuthorityRunDir
|
|
62
|
+
readAuthorityRun as readAuthorityRun2
|
|
60
63
|
} from "@rig/runtime/control-plane/authority-files";
|
|
61
64
|
import {
|
|
62
65
|
cleanupRunState,
|
|
63
66
|
deleteRunState,
|
|
64
67
|
listOpenEpics,
|
|
65
68
|
resolveDefaultEpic,
|
|
66
|
-
runResume,
|
|
67
|
-
runRestart,
|
|
68
69
|
runStatus,
|
|
69
70
|
runStop,
|
|
70
71
|
startRun,
|
|
71
72
|
defaultStartRunOptions
|
|
72
73
|
} from "@rig/runtime/control-plane/native/run-ops";
|
|
73
|
-
import { loadRuntimeContextFromEnv
|
|
74
|
+
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
74
75
|
|
|
75
76
|
// packages/cli/src/commands/_parsers.ts
|
|
76
77
|
function parsePositiveInt(value, option, fallback) {
|
|
@@ -79,13 +80,12 @@ function parsePositiveInt(value, option, fallback) {
|
|
|
79
80
|
}
|
|
80
81
|
const parsed = Number.parseInt(value, 10);
|
|
81
82
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
82
|
-
throw new
|
|
83
|
+
throw new CliError(`Invalid ${option} value: ${value}`, 1, { hint: `Pass a positive integer, e.g. \`${option} 10\`.` });
|
|
83
84
|
}
|
|
84
85
|
return parsed;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
// packages/cli/src/commands/_server-client.ts
|
|
88
|
-
import { spawnSync } from "child_process";
|
|
89
89
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
90
90
|
import { resolve as resolve2 } from "path";
|
|
91
91
|
import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
|
|
@@ -112,9 +112,14 @@ function readJsonFile(path) {
|
|
|
112
112
|
try {
|
|
113
113
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
114
114
|
} catch (error) {
|
|
115
|
-
throw new
|
|
115
|
+
throw new CliError(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1, { hint: "Fix or delete that file, then re-select a server with `rig server use <alias|local>`." });
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
function writeJsonFile(path, value) {
|
|
119
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
120
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
121
|
+
`, "utf8");
|
|
122
|
+
}
|
|
118
123
|
function normalizeConnection(value) {
|
|
119
124
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
120
125
|
return null;
|
|
@@ -155,25 +160,47 @@ function readRepoConnection(projectRoot) {
|
|
|
155
160
|
return {
|
|
156
161
|
selected,
|
|
157
162
|
project: typeof record.project === "string" ? record.project : undefined,
|
|
158
|
-
linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
|
|
163
|
+
linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
|
|
164
|
+
serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
|
|
159
165
|
};
|
|
160
166
|
}
|
|
167
|
+
function writeRepoConnection(projectRoot, state) {
|
|
168
|
+
writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
|
|
169
|
+
}
|
|
161
170
|
function resolveSelectedConnection(projectRoot, options = {}) {
|
|
162
171
|
const repo = readRepoConnection(projectRoot);
|
|
163
172
|
if (!repo)
|
|
164
173
|
return null;
|
|
165
174
|
if (repo.selected === "local")
|
|
166
|
-
return { alias: "local", connection: { kind: "local", mode: "auto" } };
|
|
175
|
+
return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
|
|
167
176
|
const global = readGlobalConnections(options);
|
|
168
177
|
const connection = global.connections[repo.selected];
|
|
169
178
|
if (!connection) {
|
|
170
|
-
throw new
|
|
179
|
+
throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
|
|
171
180
|
}
|
|
172
|
-
return { alias: repo.selected, connection };
|
|
181
|
+
return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
|
|
182
|
+
}
|
|
183
|
+
function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
|
|
184
|
+
const repo = readRepoConnection(projectRoot);
|
|
185
|
+
if (!repo)
|
|
186
|
+
return;
|
|
187
|
+
writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
|
|
188
|
+
}
|
|
189
|
+
function isRemoteConnectionSelected(projectRoot) {
|
|
190
|
+
return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
|
|
173
191
|
}
|
|
174
192
|
|
|
175
193
|
// packages/cli/src/commands/_server-client.ts
|
|
176
|
-
var
|
|
194
|
+
var scopedGitHubBearerTokens = new Map;
|
|
195
|
+
var serverPhaseListener = null;
|
|
196
|
+
function setServerPhaseListener(listener) {
|
|
197
|
+
const previous = serverPhaseListener;
|
|
198
|
+
serverPhaseListener = listener;
|
|
199
|
+
return previous;
|
|
200
|
+
}
|
|
201
|
+
function reportServerPhase(label) {
|
|
202
|
+
serverPhaseListener?.(label);
|
|
203
|
+
}
|
|
177
204
|
function cleanToken(value) {
|
|
178
205
|
const trimmed = value?.trim();
|
|
179
206
|
return trimmed ? trimmed : null;
|
|
@@ -190,49 +217,80 @@ function readPrivateRemoteSessionToken(projectRoot) {
|
|
|
190
217
|
}
|
|
191
218
|
}
|
|
192
219
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
193
|
-
|
|
194
|
-
|
|
220
|
+
const scopedKey = resolve2(projectRoot);
|
|
221
|
+
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
222
|
+
return scopedGitHubBearerTokens.get(scopedKey) ?? null;
|
|
195
223
|
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
196
|
-
if (privateSession)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
224
|
+
if (privateSession)
|
|
225
|
+
return privateSession;
|
|
226
|
+
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
227
|
+
}
|
|
228
|
+
function readStoredGitHubAuthToken(projectRoot) {
|
|
229
|
+
const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
|
|
230
|
+
if (!existsSync2(path))
|
|
231
|
+
return null;
|
|
232
|
+
try {
|
|
233
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
234
|
+
return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function readLocalConnectionFallbackToken(projectRoot) {
|
|
240
|
+
return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
|
|
212
241
|
}
|
|
213
242
|
async function ensureServerForCli(projectRoot) {
|
|
214
243
|
try {
|
|
215
244
|
const selected = resolveSelectedConnection(projectRoot);
|
|
216
245
|
if (selected?.connection.kind === "remote") {
|
|
246
|
+
reportServerPhase(`Connecting to ${selected.alias}\u2026`);
|
|
247
|
+
const authToken = readGitHubBearerTokenForRemote(projectRoot);
|
|
248
|
+
const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
|
|
217
249
|
return {
|
|
218
250
|
baseUrl: selected.connection.baseUrl,
|
|
219
|
-
authToken
|
|
220
|
-
connectionKind: "remote"
|
|
251
|
+
authToken,
|
|
252
|
+
connectionKind: "remote",
|
|
253
|
+
serverProjectRoot
|
|
221
254
|
};
|
|
222
255
|
}
|
|
256
|
+
reportServerPhase("Starting local Rig server\u2026");
|
|
223
257
|
const connection = await ensureLocalRigServerConnection(projectRoot);
|
|
224
258
|
return {
|
|
225
259
|
baseUrl: connection.baseUrl,
|
|
226
|
-
authToken: connection.authToken,
|
|
227
|
-
connectionKind: "local"
|
|
260
|
+
authToken: connection.authToken ?? readLocalConnectionFallbackToken(projectRoot),
|
|
261
|
+
connectionKind: "local",
|
|
262
|
+
serverProjectRoot: resolve2(projectRoot)
|
|
228
263
|
};
|
|
229
264
|
} catch (error) {
|
|
230
265
|
if (error instanceof Error) {
|
|
231
|
-
throw new
|
|
266
|
+
throw new CliError(error.message, 1);
|
|
232
267
|
}
|
|
233
268
|
throw error;
|
|
234
269
|
}
|
|
235
270
|
}
|
|
271
|
+
async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
|
|
272
|
+
const repo = readRepoConnection(projectRoot);
|
|
273
|
+
const slug = repo?.project?.trim();
|
|
274
|
+
if (!slug)
|
|
275
|
+
return null;
|
|
276
|
+
try {
|
|
277
|
+
const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
|
|
278
|
+
headers: mergeHeaders(undefined, authToken)
|
|
279
|
+
});
|
|
280
|
+
if (!response.ok)
|
|
281
|
+
return null;
|
|
282
|
+
const payload = await response.json();
|
|
283
|
+
const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
|
|
284
|
+
const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
|
|
285
|
+
const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
|
|
286
|
+
const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
|
|
287
|
+
if (path)
|
|
288
|
+
writeRepoServerProjectRoot(projectRoot, path);
|
|
289
|
+
return path;
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
236
294
|
function mergeHeaders(headers, authToken) {
|
|
237
295
|
const merged = new Headers(headers);
|
|
238
296
|
if (authToken) {
|
|
@@ -255,12 +313,65 @@ function diagnosticMessage(payload) {
|
|
|
255
313
|
});
|
|
256
314
|
return messages.length > 0 ? messages.join("; ") : null;
|
|
257
315
|
}
|
|
316
|
+
var serverReachabilityCache = new Map;
|
|
317
|
+
async function probeServerReachability(baseUrl, authToken) {
|
|
318
|
+
try {
|
|
319
|
+
const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
|
|
320
|
+
headers: mergeHeaders(undefined, authToken),
|
|
321
|
+
signal: AbortSignal.timeout(1500)
|
|
322
|
+
});
|
|
323
|
+
return response.ok;
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function cachedServerReachability(projectRoot, baseUrl, authToken) {
|
|
329
|
+
const key = resolve2(projectRoot);
|
|
330
|
+
const cached = serverReachabilityCache.get(key);
|
|
331
|
+
if (cached)
|
|
332
|
+
return cached;
|
|
333
|
+
const probe = probeServerReachability(baseUrl, authToken);
|
|
334
|
+
serverReachabilityCache.set(key, probe);
|
|
335
|
+
return probe;
|
|
336
|
+
}
|
|
337
|
+
function describeSelectedServer(projectRoot, server) {
|
|
338
|
+
try {
|
|
339
|
+
const selected = resolveSelectedConnection(projectRoot);
|
|
340
|
+
if (selected) {
|
|
341
|
+
return {
|
|
342
|
+
alias: selected.alias,
|
|
343
|
+
target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
} catch {}
|
|
347
|
+
return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
|
|
348
|
+
}
|
|
349
|
+
async function buildServerFailureContext(projectRoot, server) {
|
|
350
|
+
const { alias, target } = describeSelectedServer(projectRoot, server);
|
|
351
|
+
const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
|
|
352
|
+
const reachability = reachable ? "server is reachable" : "server is unreachable";
|
|
353
|
+
return {
|
|
354
|
+
contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
|
|
355
|
+
hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
|
|
356
|
+
};
|
|
357
|
+
}
|
|
258
358
|
async function requestServerJson(context, pathname, init = {}) {
|
|
259
359
|
const server = await ensureServerForCli(context.projectRoot);
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
headers
|
|
263
|
-
});
|
|
360
|
+
const headers = mergeHeaders(init.headers, server.authToken);
|
|
361
|
+
if (server.serverProjectRoot)
|
|
362
|
+
headers.set("x-rig-project-root", server.serverProjectRoot);
|
|
363
|
+
reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
|
|
364
|
+
let response;
|
|
365
|
+
try {
|
|
366
|
+
response = await fetch(`${server.baseUrl}${pathname}`, {
|
|
367
|
+
...init,
|
|
368
|
+
headers
|
|
369
|
+
});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
const failure = await buildServerFailureContext(context.projectRoot, server);
|
|
372
|
+
throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
|
|
373
|
+
${failure.contextLine}`, 1, { hint: failure.hint });
|
|
374
|
+
}
|
|
264
375
|
const text = await response.text();
|
|
265
376
|
const payload = text.trim().length > 0 ? (() => {
|
|
266
377
|
try {
|
|
@@ -272,10 +383,20 @@ async function requestServerJson(context, pathname, init = {}) {
|
|
|
272
383
|
if (!response.ok) {
|
|
273
384
|
const diagnostics = diagnosticMessage(payload);
|
|
274
385
|
const detail = diagnostics ?? (text || response.statusText);
|
|
275
|
-
|
|
386
|
+
const failure = await buildServerFailureContext(context.projectRoot, server);
|
|
387
|
+
throw new CliError(`Rig server request failed (${response.status}): ${detail}
|
|
388
|
+
${failure.contextLine}`, 1, { hint: failure.hint });
|
|
276
389
|
}
|
|
277
390
|
return payload;
|
|
278
391
|
}
|
|
392
|
+
async function listRunsViaServer(context, options = {}) {
|
|
393
|
+
const url = new URL("http://rig.local/api/runs");
|
|
394
|
+
if (options.limit !== undefined)
|
|
395
|
+
url.searchParams.set("limit", String(options.limit));
|
|
396
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
397
|
+
const runs = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.runs) ? payload.runs : [];
|
|
398
|
+
return runs.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
399
|
+
}
|
|
279
400
|
async function getRunDetailsViaServer(context, runId) {
|
|
280
401
|
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}`);
|
|
281
402
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
@@ -289,6 +410,47 @@ async function getRunLogsViaServer(context, runId, options = {}) {
|
|
|
289
410
|
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
290
411
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
291
412
|
}
|
|
413
|
+
async function getRunTimelineViaServer(context, runId, options = {}) {
|
|
414
|
+
const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
|
|
415
|
+
if (options.limit !== undefined)
|
|
416
|
+
url.searchParams.set("limit", String(options.limit));
|
|
417
|
+
if (options.cursor)
|
|
418
|
+
url.searchParams.set("cursor", options.cursor);
|
|
419
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
420
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
421
|
+
}
|
|
422
|
+
var RESUMABLE_RUN_STATUSES = new Set([
|
|
423
|
+
"created",
|
|
424
|
+
"preparing",
|
|
425
|
+
"running",
|
|
426
|
+
"validating",
|
|
427
|
+
"reviewing",
|
|
428
|
+
"stopped",
|
|
429
|
+
"failed",
|
|
430
|
+
"needs-attention",
|
|
431
|
+
"needs_attention"
|
|
432
|
+
]);
|
|
433
|
+
async function resumeRunViaServer(context, runId, options) {
|
|
434
|
+
let targetRunId = runId?.trim() || null;
|
|
435
|
+
if (!targetRunId) {
|
|
436
|
+
const candidates = (await listRunsViaServer(context)).filter((run) => RESUMABLE_RUN_STATUSES.has(String(run.status ?? ""))).sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")));
|
|
437
|
+
targetRunId = typeof candidates[0]?.runId === "string" ? candidates[0].runId : null;
|
|
438
|
+
}
|
|
439
|
+
if (!targetRunId) {
|
|
440
|
+
throw new CliError(options.restart ? "No run is available to restart." : "No resumable run is available.", 2, { hint: "List runs with `rig run list`, then pass an explicit id: `rig run resume <run-id>`." });
|
|
441
|
+
}
|
|
442
|
+
const payload = await requestServerJson(context, "/api/runs/resume", {
|
|
443
|
+
method: "POST",
|
|
444
|
+
headers: { "content-type": "application/json" },
|
|
445
|
+
body: JSON.stringify({ runId: targetRunId, createdAt: new Date().toISOString(), restart: options.restart })
|
|
446
|
+
});
|
|
447
|
+
const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
448
|
+
if (record.ok === false) {
|
|
449
|
+
const message = typeof record.error === "string" && record.error.trim() ? record.error : "run resume failed";
|
|
450
|
+
throw new CliError(`${options.restart ? "restart" : "resume"} failed for ${targetRunId}: ${message}`, 1);
|
|
451
|
+
}
|
|
452
|
+
return { ok: true, runId: targetRunId, ...record };
|
|
453
|
+
}
|
|
292
454
|
async function stopRunViaServer(context, runId) {
|
|
293
455
|
const payload = await requestServerJson(context, "/api/runs/stop", {
|
|
294
456
|
method: "POST",
|
|
@@ -305,10 +467,155 @@ async function steerRunViaServer(context, runId, message) {
|
|
|
305
467
|
});
|
|
306
468
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
307
469
|
}
|
|
470
|
+
async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
|
|
471
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
|
|
472
|
+
method: "POST",
|
|
473
|
+
headers: { "content-type": "application/json" },
|
|
474
|
+
body: JSON.stringify({ text, streamingBehavior })
|
|
475
|
+
});
|
|
476
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
477
|
+
}
|
|
308
478
|
|
|
309
|
-
// packages/cli/src/commands/
|
|
479
|
+
// packages/cli/src/commands/_run-replay.ts
|
|
480
|
+
import { existsSync as existsSync3, readdirSync } from "fs";
|
|
481
|
+
import { join, resolve as resolve3 } from "path";
|
|
482
|
+
import {
|
|
483
|
+
readAuthorityRun,
|
|
484
|
+
readJsonlFile,
|
|
485
|
+
runJournalPath
|
|
486
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
487
|
+
function asRecord(value) {
|
|
488
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
489
|
+
}
|
|
490
|
+
function text(value) {
|
|
491
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
492
|
+
}
|
|
493
|
+
function snippet(value, max = 120) {
|
|
494
|
+
const raw = text(value);
|
|
495
|
+
if (!raw)
|
|
496
|
+
return null;
|
|
497
|
+
const flat = raw.replace(/\s+/g, " ");
|
|
498
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
499
|
+
}
|
|
500
|
+
function summarizeLifecycleEntry(entry) {
|
|
501
|
+
const type = text(entry.type) ?? "event";
|
|
502
|
+
switch (type) {
|
|
503
|
+
case "status": {
|
|
504
|
+
const anchor = text(entry.sessionId);
|
|
505
|
+
return [
|
|
506
|
+
`status \u2192 ${text(entry.status) ?? "(unknown)"}`,
|
|
507
|
+
snippet(entry.detail) ? `\u2014 ${snippet(entry.detail)}` : null,
|
|
508
|
+
anchor ? `[session ${anchor}]` : null
|
|
509
|
+
].filter(Boolean).join(" ");
|
|
510
|
+
}
|
|
511
|
+
case "timeline-entry": {
|
|
512
|
+
const payload = asRecord(entry.payload) ?? {};
|
|
513
|
+
const kind = text(payload.type) ?? "entry";
|
|
514
|
+
const body = snippet(payload.title) ?? snippet(payload.text) ?? snippet(payload.detail);
|
|
515
|
+
const state = text(payload.state);
|
|
516
|
+
return [`timeline ${kind}`, body ? `\u2014 ${body}` : null, state ? `(${state})` : null].filter(Boolean).join(" ");
|
|
517
|
+
}
|
|
518
|
+
case "log-entry": {
|
|
519
|
+
const payload = asRecord(entry.payload) ?? {};
|
|
520
|
+
const title = snippet(payload.title) ?? "log";
|
|
521
|
+
const detail = snippet(payload.detail);
|
|
522
|
+
return [`log ${title}`, detail ? `\u2014 ${detail}` : null].filter(Boolean).join(" ");
|
|
523
|
+
}
|
|
524
|
+
case "record-patch": {
|
|
525
|
+
const patch = asRecord(entry.patch) ?? {};
|
|
526
|
+
const keys = Object.keys(patch);
|
|
527
|
+
const shown = keys.slice(0, 8).join(", ");
|
|
528
|
+
return `record-patch {${shown}${keys.length > 8 ? `, +${keys.length - 8} more` : ""}}`;
|
|
529
|
+
}
|
|
530
|
+
case "timeline":
|
|
531
|
+
return "timeline updated (signal)";
|
|
532
|
+
case "log":
|
|
533
|
+
return `log signal${snippet(entry.title) ? ` \u2014 ${snippet(entry.title)}` : ""}`;
|
|
534
|
+
case "completed":
|
|
535
|
+
return "run completed";
|
|
536
|
+
case "failed":
|
|
537
|
+
return `run failed${snippet(entry.error) ? ` \u2014 ${snippet(entry.error)}` : ""}`;
|
|
538
|
+
default:
|
|
539
|
+
return type;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function summarizeSessionEntry(entry) {
|
|
543
|
+
const type = text(entry.type) ?? "entry";
|
|
544
|
+
const message = asRecord(entry.message);
|
|
545
|
+
const role = text(message?.role);
|
|
546
|
+
const content = message?.content;
|
|
547
|
+
let body = null;
|
|
548
|
+
if (typeof content === "string") {
|
|
549
|
+
body = snippet(content);
|
|
550
|
+
} else if (Array.isArray(content)) {
|
|
551
|
+
body = snippet(content.map((part) => text(asRecord(part)?.text) ?? "").filter(Boolean).join(" "));
|
|
552
|
+
}
|
|
553
|
+
return [
|
|
554
|
+
`pi ${type}`,
|
|
555
|
+
role ? `(${role})` : null,
|
|
556
|
+
body ? `\u2014 ${body}` : null
|
|
557
|
+
].filter(Boolean).join(" ");
|
|
558
|
+
}
|
|
559
|
+
function sessionEntryTimestamp(entry) {
|
|
560
|
+
return text(entry.timestamp) ?? text(entry.at) ?? text(entry.createdAt) ?? null;
|
|
561
|
+
}
|
|
562
|
+
function resolveRunSessionFile(record) {
|
|
563
|
+
const piSession = record?.piSession ?? null;
|
|
564
|
+
if (!piSession)
|
|
565
|
+
return null;
|
|
566
|
+
const recorded = text(piSession.sessionFile);
|
|
567
|
+
if (recorded && existsSync3(recorded)) {
|
|
568
|
+
return recorded;
|
|
569
|
+
}
|
|
570
|
+
const cwd = text(piSession.cwd);
|
|
571
|
+
const sessionId = text(piSession.sessionId);
|
|
572
|
+
if (!cwd || !sessionId)
|
|
573
|
+
return null;
|
|
574
|
+
const sessionDir = resolve3(cwd, ".rig", "session");
|
|
575
|
+
try {
|
|
576
|
+
const match = readdirSync(sessionDir).find((name) => name.endsWith(`_${sessionId}.jsonl`));
|
|
577
|
+
return match ? join(sessionDir, match) : null;
|
|
578
|
+
} catch {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function buildRunReplay(projectRoot, runId, options = {}) {
|
|
583
|
+
const logPath = runJournalPath(projectRoot, runId);
|
|
584
|
+
const record = readAuthorityRun(projectRoot, runId);
|
|
585
|
+
const lifecycleEntries = readJsonlFile(logPath).map((entry) => asRecord(entry)).filter((entry) => entry !== null);
|
|
586
|
+
const merged = lifecycleEntries.map((entry) => ({
|
|
587
|
+
at: text(entry.at),
|
|
588
|
+
source: "run",
|
|
589
|
+
summary: summarizeLifecycleEntry(entry)
|
|
590
|
+
}));
|
|
591
|
+
let sessionFile = null;
|
|
592
|
+
let sessionEntryCount = 0;
|
|
593
|
+
if (options.withSession) {
|
|
594
|
+
sessionFile = resolveRunSessionFile(record);
|
|
595
|
+
if (sessionFile) {
|
|
596
|
+
let lastSeenAt = null;
|
|
597
|
+
const sessionLines = readJsonlFile(sessionFile).map((entry) => asRecord(entry)).filter((entry) => entry !== null).map((entry) => {
|
|
598
|
+
lastSeenAt = sessionEntryTimestamp(entry) ?? lastSeenAt;
|
|
599
|
+
return { at: lastSeenAt, source: "session", summary: summarizeSessionEntry(entry) };
|
|
600
|
+
});
|
|
601
|
+
sessionEntryCount = sessionLines.length;
|
|
602
|
+
merged.push(...sessionLines);
|
|
603
|
+
merged.sort((left, right) => (left.at ?? "").localeCompare(right.at ?? ""));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const lines = merged.map((line) => `${line.at ?? "(no timestamp) "} ${line.source === "run" ? "run " : "session"} ${line.summary}`);
|
|
607
|
+
return {
|
|
608
|
+
runId,
|
|
609
|
+
logPath,
|
|
610
|
+
sessionFile,
|
|
611
|
+
entryCount: lifecycleEntries.length,
|
|
612
|
+
sessionEntryCount,
|
|
613
|
+
lines
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// packages/cli/src/commands/_operator-surface.ts
|
|
310
618
|
import { createInterface } from "readline";
|
|
311
|
-
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
312
619
|
var CANONICAL_STAGES = [
|
|
313
620
|
"Connect",
|
|
314
621
|
"GitHub/task sync",
|
|
@@ -323,18 +630,380 @@ var CANONICAL_STAGES = [
|
|
|
323
630
|
"Merge",
|
|
324
631
|
"Complete"
|
|
325
632
|
];
|
|
633
|
+
function logDetail(log) {
|
|
634
|
+
return typeof log.detail === "string" ? log.detail.trim() : "";
|
|
635
|
+
}
|
|
636
|
+
function parseProviderProtocolLog(title, detail) {
|
|
637
|
+
if (title.trim().toLowerCase() !== "agent output")
|
|
638
|
+
return null;
|
|
639
|
+
if (!detail.startsWith("{") || !detail.endsWith("}"))
|
|
640
|
+
return null;
|
|
641
|
+
try {
|
|
642
|
+
const record = JSON.parse(detail);
|
|
643
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
644
|
+
return null;
|
|
645
|
+
const type = record.type;
|
|
646
|
+
return typeof type === "string" && [
|
|
647
|
+
"assistant",
|
|
648
|
+
"message_start",
|
|
649
|
+
"message_update",
|
|
650
|
+
"message_end",
|
|
651
|
+
"stream_event",
|
|
652
|
+
"tool_result",
|
|
653
|
+
"tool_execution_start",
|
|
654
|
+
"tool_execution_update",
|
|
655
|
+
"tool_execution_end",
|
|
656
|
+
"turn_start",
|
|
657
|
+
"turn_end"
|
|
658
|
+
].includes(type) ? record : null;
|
|
659
|
+
} catch {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function renderProviderProtocolLog(record) {
|
|
664
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
665
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
|
|
666
|
+
const toolName = String(record.toolName ?? record.name ?? "tool");
|
|
667
|
+
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";
|
|
668
|
+
return `[Pi tool] ${toolName} ${status}`;
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
function entryId(entry, fallback) {
|
|
673
|
+
return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
|
|
674
|
+
}
|
|
326
675
|
function renderOperatorSnapshot(snapshot) {
|
|
327
676
|
const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
|
|
328
677
|
const runId = String(run.runId ?? run.id ?? "run");
|
|
329
678
|
const status = String(run.status ?? "unknown");
|
|
330
679
|
const logs = snapshot.logs ?? [];
|
|
680
|
+
const latestByStage = new Map;
|
|
681
|
+
for (const log of logs) {
|
|
682
|
+
const title = String(log.title ?? "").toLowerCase();
|
|
683
|
+
const stageName = String(log.stage ?? "").toLowerCase();
|
|
684
|
+
const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
|
|
685
|
+
if (stage)
|
|
686
|
+
latestByStage.set(stage, log);
|
|
687
|
+
}
|
|
331
688
|
const stageLines = CANONICAL_STAGES.flatMap((stage) => {
|
|
332
|
-
const match =
|
|
333
|
-
return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
|
|
689
|
+
const match = latestByStage.get(stage);
|
|
690
|
+
return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
|
|
334
691
|
});
|
|
335
692
|
return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
|
|
336
693
|
`);
|
|
337
694
|
}
|
|
695
|
+
function createPiRunStreamRenderer(output = process.stdout) {
|
|
696
|
+
let lastSnapshot = "";
|
|
697
|
+
const assistantTextById = new Map;
|
|
698
|
+
const seenTimeline = new Set;
|
|
699
|
+
const seenLogs = new Set;
|
|
700
|
+
const writeLine = (line) => output.write(`${line}
|
|
701
|
+
`);
|
|
702
|
+
return {
|
|
703
|
+
renderSnapshot(snapshot) {
|
|
704
|
+
const rendered = renderOperatorSnapshot(snapshot);
|
|
705
|
+
if (rendered && rendered !== lastSnapshot) {
|
|
706
|
+
writeLine(rendered);
|
|
707
|
+
lastSnapshot = rendered;
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
renderTimeline(entries) {
|
|
711
|
+
for (const [index, entry] of entries.entries()) {
|
|
712
|
+
const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
|
|
713
|
+
if (entry.type === "assistant_message" && typeof entry.text === "string") {
|
|
714
|
+
const text2 = entry.text;
|
|
715
|
+
const previousText = assistantTextById.get(id) ?? "";
|
|
716
|
+
if (!previousText && text2.trim()) {
|
|
717
|
+
writeLine("[Pi assistant]");
|
|
718
|
+
}
|
|
719
|
+
if (text2.startsWith(previousText)) {
|
|
720
|
+
const delta = text2.slice(previousText.length);
|
|
721
|
+
if (delta)
|
|
722
|
+
output.write(delta);
|
|
723
|
+
} else if (text2.trim() && text2 !== previousText) {
|
|
724
|
+
if (previousText)
|
|
725
|
+
writeLine(`
|
|
726
|
+
[Pi assistant]`);
|
|
727
|
+
output.write(text2);
|
|
728
|
+
}
|
|
729
|
+
assistantTextById.set(id, text2);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (seenTimeline.has(id))
|
|
733
|
+
continue;
|
|
734
|
+
seenTimeline.add(id);
|
|
735
|
+
if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
|
|
736
|
+
writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
if (entry.type === "timeline_warning") {
|
|
740
|
+
writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (entry.type === "action") {
|
|
744
|
+
const text2 = String(entry.detail ?? entry.message ?? entry.title ?? "").trim();
|
|
745
|
+
if (text2)
|
|
746
|
+
writeLine(`[Rig action] ${text2}`);
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (entry.type === "user_message") {
|
|
750
|
+
const text2 = String(entry.text ?? entry.message ?? entry.detail ?? "").trim();
|
|
751
|
+
if (text2)
|
|
752
|
+
writeLine(`[Operator] ${text2}`);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
const fallback = String(entry.detail ?? entry.message ?? entry.text ?? entry.title ?? "").trim();
|
|
756
|
+
if (fallback)
|
|
757
|
+
writeLine(`[${String(entry.type ?? "timeline")}] ${fallback}`);
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
renderLogs(entries) {
|
|
761
|
+
for (const [index, entry] of entries.entries()) {
|
|
762
|
+
const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
|
|
763
|
+
if (seenLogs.has(id))
|
|
764
|
+
continue;
|
|
765
|
+
seenLogs.add(id);
|
|
766
|
+
const title = String(entry.title ?? "");
|
|
767
|
+
if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
|
|
768
|
+
continue;
|
|
769
|
+
const detail = logDetail(entry);
|
|
770
|
+
if (!detail)
|
|
771
|
+
continue;
|
|
772
|
+
const protocolRecord = parseProviderProtocolLog(title, detail);
|
|
773
|
+
if (protocolRecord) {
|
|
774
|
+
const protocolLine = renderProviderProtocolLog(protocolRecord);
|
|
775
|
+
if (protocolLine)
|
|
776
|
+
writeLine(protocolLine);
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
writeLine(`[${title || "Rig log"}] ${detail}`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
function createOperatorSurface(options = {}) {
|
|
785
|
+
const input = options.input ?? process.stdin;
|
|
786
|
+
const output = options.output ?? process.stdout;
|
|
787
|
+
const errorOutput = options.errorOutput ?? process.stderr;
|
|
788
|
+
const renderer = createPiRunStreamRenderer(output);
|
|
789
|
+
const writeLine = (line) => output.write(`${line}
|
|
790
|
+
`);
|
|
791
|
+
return {
|
|
792
|
+
mode: "pi-compatible-text",
|
|
793
|
+
...renderer,
|
|
794
|
+
info: writeLine,
|
|
795
|
+
error: (message) => errorOutput.write(`${message}
|
|
796
|
+
`),
|
|
797
|
+
attachCommandInput(handler) {
|
|
798
|
+
if (options.interactive === false || !input.isTTY)
|
|
799
|
+
return null;
|
|
800
|
+
const rl = createInterface({ input, output: process.stdout, terminal: false });
|
|
801
|
+
rl.on("line", (line) => {
|
|
802
|
+
Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
803
|
+
});
|
|
804
|
+
return { close: () => rl.close() };
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// packages/cli/src/commands/_pi-frontend.ts
|
|
810
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
811
|
+
import { tmpdir } from "os";
|
|
812
|
+
import { join as join2 } from "path";
|
|
813
|
+
import { main as runPiMain } from "@earendil-works/pi-coding-agent";
|
|
814
|
+
import createPiRigExtension from "@rig/pi-rig";
|
|
815
|
+
function setTemporaryEnv(updates) {
|
|
816
|
+
const previous = new Map;
|
|
817
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
818
|
+
previous.set(key, process.env[key]);
|
|
819
|
+
process.env[key] = value;
|
|
820
|
+
}
|
|
821
|
+
return () => {
|
|
822
|
+
for (const [key, value] of previous) {
|
|
823
|
+
if (value === undefined)
|
|
824
|
+
delete process.env[key];
|
|
825
|
+
else
|
|
826
|
+
process.env[key] = value;
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function buildOperatorPiEnv(input) {
|
|
831
|
+
return {
|
|
832
|
+
PI_CODING_AGENT_SESSION_DIR: input.sessionDir,
|
|
833
|
+
PI_SKIP_VERSION_CHECK: "1",
|
|
834
|
+
RIG_PI_OPERATOR_SESSION: "1",
|
|
835
|
+
RIG_RUN_ID: input.runId,
|
|
836
|
+
RIG_SERVER_URL: input.serverUrl,
|
|
837
|
+
...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {},
|
|
838
|
+
...input.serverProjectRoot ? { RIG_PROJECT_ROOT: input.serverProjectRoot } : {}
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
async function attachRunBundledPiFrontend(context, input) {
|
|
842
|
+
const tempSessionDir = mkdtempSync(join2(tmpdir(), "rig-pi-frontend-sessions-"));
|
|
843
|
+
const server = await ensureServerForCli(context.projectRoot);
|
|
844
|
+
const restoreEnv = setTemporaryEnv(buildOperatorPiEnv({
|
|
845
|
+
runId: input.runId,
|
|
846
|
+
serverUrl: server.baseUrl,
|
|
847
|
+
authToken: server.authToken,
|
|
848
|
+
serverProjectRoot: server.serverProjectRoot,
|
|
849
|
+
sessionDir: tempSessionDir
|
|
850
|
+
}));
|
|
851
|
+
const piRigExtensionFactory = (pi) => {
|
|
852
|
+
createPiRigExtension(pi);
|
|
853
|
+
};
|
|
854
|
+
let detached = false;
|
|
855
|
+
try {
|
|
856
|
+
await runPiMain([
|
|
857
|
+
"--no-extensions",
|
|
858
|
+
"--no-skills",
|
|
859
|
+
"--no-prompt-templates",
|
|
860
|
+
"--no-context-files"
|
|
861
|
+
], {
|
|
862
|
+
extensionFactories: [piRigExtensionFactory]
|
|
863
|
+
});
|
|
864
|
+
detached = true;
|
|
865
|
+
} finally {
|
|
866
|
+
restoreEnv();
|
|
867
|
+
rmSync(tempSessionDir, { recursive: true, force: true });
|
|
868
|
+
}
|
|
869
|
+
let run = { runId: input.runId, status: "unknown" };
|
|
870
|
+
try {
|
|
871
|
+
run = await getRunDetailsViaServer(context, input.runId);
|
|
872
|
+
} catch {}
|
|
873
|
+
return {
|
|
874
|
+
run,
|
|
875
|
+
logs: [],
|
|
876
|
+
timeline: [],
|
|
877
|
+
timelineCursor: null,
|
|
878
|
+
steered: input.steered === true,
|
|
879
|
+
detached,
|
|
880
|
+
rendered: "stock Pi operator console with the pi-rig extension"
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// packages/cli/src/commands/_async-ui.ts
|
|
885
|
+
import pc from "picocolors";
|
|
886
|
+
|
|
887
|
+
// packages/cli/src/commands/_spinner.ts
|
|
888
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
889
|
+
function createTtySpinner(input) {
|
|
890
|
+
const output = input.output ?? process.stdout;
|
|
891
|
+
const isTty = output.isTTY === true;
|
|
892
|
+
const frames = input.frames && input.frames.length > 0 ? input.frames : SPINNER_FRAMES;
|
|
893
|
+
let label = input.label;
|
|
894
|
+
let frame = 0;
|
|
895
|
+
let paused = false;
|
|
896
|
+
let stopped = false;
|
|
897
|
+
let lastPrintedLabel = "";
|
|
898
|
+
const render = () => {
|
|
899
|
+
if (stopped || paused)
|
|
900
|
+
return;
|
|
901
|
+
if (!isTty) {
|
|
902
|
+
if (label !== lastPrintedLabel) {
|
|
903
|
+
output.write(`${label}
|
|
904
|
+
`);
|
|
905
|
+
lastPrintedLabel = label;
|
|
906
|
+
}
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
frame = (frame + 1) % frames.length;
|
|
910
|
+
const glyph = frames[frame] ?? frames[0] ?? "";
|
|
911
|
+
output.write(`\r\x1B[2K${input.styleFrame ? input.styleFrame(glyph) : glyph} ${label}`);
|
|
912
|
+
};
|
|
913
|
+
const clearLine = () => {
|
|
914
|
+
if (isTty)
|
|
915
|
+
output.write("\r\x1B[2K");
|
|
916
|
+
};
|
|
917
|
+
render();
|
|
918
|
+
const timer = isTty ? setInterval(render, input.intervalMs ?? 120) : null;
|
|
919
|
+
return {
|
|
920
|
+
setLabel(next) {
|
|
921
|
+
label = next;
|
|
922
|
+
render();
|
|
923
|
+
},
|
|
924
|
+
pause() {
|
|
925
|
+
paused = true;
|
|
926
|
+
clearLine();
|
|
927
|
+
},
|
|
928
|
+
resume() {
|
|
929
|
+
if (stopped)
|
|
930
|
+
return;
|
|
931
|
+
paused = false;
|
|
932
|
+
render();
|
|
933
|
+
},
|
|
934
|
+
stop(finalLine) {
|
|
935
|
+
if (stopped)
|
|
936
|
+
return;
|
|
937
|
+
stopped = true;
|
|
938
|
+
if (timer)
|
|
939
|
+
clearInterval(timer);
|
|
940
|
+
clearLine();
|
|
941
|
+
if (finalLine)
|
|
942
|
+
output.write(`${finalLine}
|
|
943
|
+
`);
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// packages/cli/src/commands/_async-ui.ts
|
|
949
|
+
var CLACK_SPINNER_FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
|
|
950
|
+
var DONE_SYMBOL = pc.green("\u25C7");
|
|
951
|
+
var FAIL_SYMBOL = pc.red("\u25A0");
|
|
952
|
+
var activeUpdate = null;
|
|
953
|
+
async function withSpinner(label, work, options = {}) {
|
|
954
|
+
if (options.outputMode === "json") {
|
|
955
|
+
return work(() => {});
|
|
956
|
+
}
|
|
957
|
+
if (activeUpdate) {
|
|
958
|
+
const outer = activeUpdate;
|
|
959
|
+
outer(label);
|
|
960
|
+
return work(outer);
|
|
961
|
+
}
|
|
962
|
+
const output = options.output ?? process.stderr;
|
|
963
|
+
const isTty = output.isTTY === true;
|
|
964
|
+
let lastLabel = label;
|
|
965
|
+
if (!isTty) {
|
|
966
|
+
output.write(`${label}
|
|
967
|
+
`);
|
|
968
|
+
const update2 = (next) => {
|
|
969
|
+
lastLabel = next;
|
|
970
|
+
};
|
|
971
|
+
activeUpdate = update2;
|
|
972
|
+
const previousListener2 = setServerPhaseListener(update2);
|
|
973
|
+
try {
|
|
974
|
+
return await work(update2);
|
|
975
|
+
} finally {
|
|
976
|
+
activeUpdate = null;
|
|
977
|
+
setServerPhaseListener(previousListener2);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const spinner = createTtySpinner({
|
|
981
|
+
label,
|
|
982
|
+
output,
|
|
983
|
+
frames: CLACK_SPINNER_FRAMES,
|
|
984
|
+
styleFrame: (frame) => pc.magenta(frame)
|
|
985
|
+
});
|
|
986
|
+
const update = (next) => {
|
|
987
|
+
lastLabel = next;
|
|
988
|
+
spinner.setLabel(next);
|
|
989
|
+
};
|
|
990
|
+
activeUpdate = update;
|
|
991
|
+
const previousListener = setServerPhaseListener(update);
|
|
992
|
+
try {
|
|
993
|
+
const result = await work(update);
|
|
994
|
+
spinner.stop(options.doneLabel ? `${DONE_SYMBOL} ${options.doneLabel}` : undefined);
|
|
995
|
+
return result;
|
|
996
|
+
} catch (error) {
|
|
997
|
+
spinner.stop(`${FAIL_SYMBOL} ${lastLabel}`);
|
|
998
|
+
throw error;
|
|
999
|
+
} finally {
|
|
1000
|
+
activeUpdate = null;
|
|
1001
|
+
setServerPhaseListener(previousListener);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// packages/cli/src/commands/_operator-view.ts
|
|
1006
|
+
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
338
1007
|
function runStatusFromPayload(payload) {
|
|
339
1008
|
const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
340
1009
|
return String(run.status ?? "unknown").toLowerCase();
|
|
@@ -356,56 +1025,307 @@ async function applyOperatorCommand(context, input, deps = {}) {
|
|
|
356
1025
|
await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
|
|
357
1026
|
return { action: "continue", message: "Steering message queued." };
|
|
358
1027
|
}
|
|
359
|
-
async function readOperatorSnapshot(context, runId) {
|
|
1028
|
+
async function readOperatorSnapshot(context, runId, options = {}) {
|
|
360
1029
|
const run = await getRunDetailsViaServer(context, runId);
|
|
361
1030
|
const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
|
|
362
|
-
const
|
|
363
|
-
|
|
1031
|
+
const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
|
|
1032
|
+
entries: [{
|
|
1033
|
+
id: `timeline-unavailable:${runId}`,
|
|
1034
|
+
type: "timeline_warning",
|
|
1035
|
+
detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
|
|
1036
|
+
createdAt: new Date().toISOString()
|
|
1037
|
+
}],
|
|
1038
|
+
nextCursor: options.timelineCursor ?? null
|
|
1039
|
+
}));
|
|
1040
|
+
const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
|
|
1041
|
+
const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1042
|
+
const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
|
|
1043
|
+
return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
|
|
364
1044
|
}
|
|
365
1045
|
async function attachRunOperatorView(context, input) {
|
|
366
1046
|
let steered = false;
|
|
367
|
-
|
|
368
|
-
|
|
1047
|
+
const attachMessage = input.message?.trim();
|
|
1048
|
+
if (attachMessage) {
|
|
1049
|
+
await withSpinner("Queueing steering message\u2026", () => sendRunPiPromptViaServer(context, input.runId, attachMessage, "steer").catch(() => steerRunViaServer(context, input.runId, attachMessage)), { outputMode: context.outputMode });
|
|
369
1050
|
steered = true;
|
|
370
1051
|
}
|
|
371
|
-
|
|
1052
|
+
if (input.follow && !input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
1053
|
+
return attachRunBundledPiFrontend(context, {
|
|
1054
|
+
runId: input.runId,
|
|
1055
|
+
steered
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
const surface = createOperatorSurface({ interactive: input.interactive !== false });
|
|
1059
|
+
let snapshot = await withSpinner(`Connecting to run ${input.runId}\u2026`, () => readOperatorSnapshot(context, input.runId), { outputMode: context.outputMode });
|
|
372
1060
|
if (context.outputMode === "text") {
|
|
373
|
-
|
|
1061
|
+
surface.renderSnapshot(snapshot);
|
|
1062
|
+
surface.renderTimeline(snapshot.timeline);
|
|
1063
|
+
surface.renderLogs(snapshot.logs);
|
|
374
1064
|
if (steered)
|
|
375
|
-
|
|
1065
|
+
surface.info("Message submitted to worker Pi.");
|
|
376
1066
|
}
|
|
377
1067
|
let detached = false;
|
|
378
|
-
let
|
|
1068
|
+
let commandInput = null;
|
|
379
1069
|
if (input.follow && !input.once && context.outputMode === "text") {
|
|
380
1070
|
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)}`));
|
|
1071
|
+
surface.info("Controls: /user <message>, /stop, /detach");
|
|
1072
|
+
commandInput = surface.attachCommandInput(async (line) => {
|
|
1073
|
+
const result = await applyOperatorCommand(context, { runId: input.runId, line });
|
|
1074
|
+
if (result.message)
|
|
1075
|
+
surface.info(result.message);
|
|
1076
|
+
if (result.action === "detach" || result.action === "stopped") {
|
|
1077
|
+
detached = true;
|
|
1078
|
+
commandInput?.close();
|
|
1079
|
+
}
|
|
392
1080
|
});
|
|
393
1081
|
}
|
|
394
|
-
let lastRendered = snapshot.rendered;
|
|
395
1082
|
const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
|
|
1083
|
+
let timelineCursor = snapshot.timelineCursor;
|
|
396
1084
|
while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
|
|
397
1085
|
await Bun.sleep(pollMs);
|
|
398
|
-
snapshot = await readOperatorSnapshot(context, input.runId);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
1086
|
+
snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
|
|
1087
|
+
timelineCursor = snapshot.timelineCursor;
|
|
1088
|
+
surface.renderSnapshot(snapshot);
|
|
1089
|
+
surface.renderTimeline(snapshot.timeline);
|
|
1090
|
+
surface.renderLogs(snapshot.logs);
|
|
403
1091
|
}
|
|
404
|
-
|
|
1092
|
+
commandInput?.close();
|
|
405
1093
|
}
|
|
406
1094
|
return { ...snapshot, steered, detached };
|
|
407
1095
|
}
|
|
408
1096
|
|
|
1097
|
+
// packages/cli/src/commands/_cli-format.ts
|
|
1098
|
+
import { log, note } from "@clack/prompts";
|
|
1099
|
+
import pc2 from "picocolors";
|
|
1100
|
+
function stringField(record, key, fallback = "") {
|
|
1101
|
+
const value = record[key];
|
|
1102
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
1103
|
+
}
|
|
1104
|
+
function rawObject(record) {
|
|
1105
|
+
const raw = record.raw;
|
|
1106
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
1107
|
+
}
|
|
1108
|
+
function truncate(value, width) {
|
|
1109
|
+
if (value.length <= width)
|
|
1110
|
+
return value;
|
|
1111
|
+
if (width <= 1)
|
|
1112
|
+
return "\u2026";
|
|
1113
|
+
return `${value.slice(0, width - 1)}\u2026`;
|
|
1114
|
+
}
|
|
1115
|
+
function pad(value, width) {
|
|
1116
|
+
return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
|
|
1117
|
+
}
|
|
1118
|
+
function statusColor(status) {
|
|
1119
|
+
const normalized = status.toLowerCase();
|
|
1120
|
+
if (["completed", "merged", "closed", "done", "accepted", "pass", "selected", "approved"].includes(normalized))
|
|
1121
|
+
return pc2.green;
|
|
1122
|
+
if (["failed", "needs_attention", "needs-attention", "blocked", "error", "rejected"].includes(normalized))
|
|
1123
|
+
return pc2.red;
|
|
1124
|
+
if (["running", "reviewing", "validating", "in_progress", "in-progress", "remote"].includes(normalized))
|
|
1125
|
+
return pc2.cyan;
|
|
1126
|
+
if (["ready", "open", "queued", "created", "preparing", "local", "pending"].includes(normalized))
|
|
1127
|
+
return pc2.yellow;
|
|
1128
|
+
return pc2.dim;
|
|
1129
|
+
}
|
|
1130
|
+
function compactDate(value) {
|
|
1131
|
+
if (!value.trim())
|
|
1132
|
+
return "";
|
|
1133
|
+
const parsed = Date.parse(value);
|
|
1134
|
+
if (!Number.isFinite(parsed))
|
|
1135
|
+
return value;
|
|
1136
|
+
return new Date(parsed).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
|
|
1137
|
+
}
|
|
1138
|
+
function firstString(record, keys, fallback = "") {
|
|
1139
|
+
for (const key of keys) {
|
|
1140
|
+
const value = stringField(record, key);
|
|
1141
|
+
if (value)
|
|
1142
|
+
return value;
|
|
1143
|
+
}
|
|
1144
|
+
return fallback;
|
|
1145
|
+
}
|
|
1146
|
+
function runIdOf(run) {
|
|
1147
|
+
return firstString(run, ["runId", "id"], "(unknown-run)");
|
|
1148
|
+
}
|
|
1149
|
+
function taskIdOf(run) {
|
|
1150
|
+
return firstString(run, ["taskId", "task", "task_id"]);
|
|
1151
|
+
}
|
|
1152
|
+
function runTitleOf(run) {
|
|
1153
|
+
return firstString(run, ["title", "summary", "name"], taskIdOf(run) || "(untitled)");
|
|
1154
|
+
}
|
|
1155
|
+
function shouldUseClackOutput() {
|
|
1156
|
+
return Boolean(process.stdout.isTTY) && process.env.RIG_CLI_PLAIN_HELP !== "1";
|
|
1157
|
+
}
|
|
1158
|
+
function printFormattedOutput(message, options = {}) {
|
|
1159
|
+
if (!shouldUseClackOutput()) {
|
|
1160
|
+
console.log(message);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (options.title)
|
|
1164
|
+
note(message, options.title);
|
|
1165
|
+
else
|
|
1166
|
+
log.message(message);
|
|
1167
|
+
}
|
|
1168
|
+
function formatStatusPill(status) {
|
|
1169
|
+
const label = status || "unknown";
|
|
1170
|
+
return statusColor(label)(`\u25CF ${label}`);
|
|
1171
|
+
}
|
|
1172
|
+
function formatSection(title, subtitle) {
|
|
1173
|
+
return `${pc2.bold(pc2.cyan("\u25C6"))} ${pc2.bold(title)}${subtitle ? pc2.dim(` \u2014 ${subtitle}`) : ""}`;
|
|
1174
|
+
}
|
|
1175
|
+
function formatSuccessCard(title, rows = []) {
|
|
1176
|
+
const body = rows.filter(([, value]) => value !== undefined && value !== null && String(value).length > 0).map(([key, value]) => `${pc2.dim("\u2502")} ${pc2.dim(key.padEnd(12))} ${value}`);
|
|
1177
|
+
return [formatSection(title), ...body].join(`
|
|
1178
|
+
`);
|
|
1179
|
+
}
|
|
1180
|
+
function formatNextSteps(steps) {
|
|
1181
|
+
if (steps.length === 0)
|
|
1182
|
+
return [];
|
|
1183
|
+
return [pc2.bold("Next"), ...steps.map((step) => `${pc2.dim("\u203A")} ${step}`)];
|
|
1184
|
+
}
|
|
1185
|
+
function formatRunList(runs, options = {}) {
|
|
1186
|
+
if (runs.length === 0) {
|
|
1187
|
+
return [
|
|
1188
|
+
formatSection("Runs", "none recorded"),
|
|
1189
|
+
options.source === "server" ? pc2.dim("No runs recorded on the selected Rig server.") : pc2.dim("No runs recorded in .rig/runs."),
|
|
1190
|
+
"",
|
|
1191
|
+
...formatNextSteps(["Start one: `rig task run --next`", "Check server: `rig server status`"])
|
|
1192
|
+
].join(`
|
|
1193
|
+
`);
|
|
1194
|
+
}
|
|
1195
|
+
const rows = runs.map((run) => {
|
|
1196
|
+
const runId = stringField(run, "runId", stringField(run, "id", "(unknown-run)"));
|
|
1197
|
+
const status = stringField(run, "status", "unknown");
|
|
1198
|
+
const taskId = stringField(run, "taskId", "");
|
|
1199
|
+
const title = stringField(run, "title", taskId || "(untitled)");
|
|
1200
|
+
const runtime = stringField(run, "runtimeAdapter", "");
|
|
1201
|
+
return { runId, status, title, runtime };
|
|
1202
|
+
});
|
|
1203
|
+
const idWidth = Math.min(36, Math.max(6, ...rows.map((row) => row.runId.length)));
|
|
1204
|
+
const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
|
|
1205
|
+
const header = `${pc2.bold(pad("RUN", idWidth))} ${pc2.bold(pad("STATUS", statusWidth))} ${pc2.bold("TITLE")}`;
|
|
1206
|
+
const body = rows.map((row) => [
|
|
1207
|
+
pc2.bold(pad(truncate(row.runId, idWidth), idWidth)),
|
|
1208
|
+
statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
|
|
1209
|
+
`${row.title}${row.runtime ? pc2.dim(` ${row.runtime}`) : ""}`
|
|
1210
|
+
].join(" "));
|
|
1211
|
+
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(`
|
|
1212
|
+
`);
|
|
1213
|
+
}
|
|
1214
|
+
function formatRunCard(run, options = {}) {
|
|
1215
|
+
const raw = rawObject(run);
|
|
1216
|
+
const merged = { ...raw, ...run };
|
|
1217
|
+
const runId = runIdOf(merged);
|
|
1218
|
+
const status = firstString(merged, ["status"], "unknown");
|
|
1219
|
+
const taskId = taskIdOf(merged);
|
|
1220
|
+
const title = runTitleOf(merged);
|
|
1221
|
+
const runtime = firstString(merged, ["runtimeAdapter", "runtime", "adapter"]);
|
|
1222
|
+
const mode = firstString(merged, ["runtimeMode", "mode"]);
|
|
1223
|
+
const interaction = firstString(merged, ["interactionMode"]);
|
|
1224
|
+
const created = compactDate(firstString(merged, ["createdAt"]));
|
|
1225
|
+
const started = compactDate(firstString(merged, ["startedAt"]));
|
|
1226
|
+
const updated = compactDate(firstString(merged, ["updatedAt"]));
|
|
1227
|
+
const completed = compactDate(firstString(merged, ["completedAt", "finishedAt"]));
|
|
1228
|
+
const worktree = firstString(merged, ["worktreePath", "cwd", "projectRoot"]);
|
|
1229
|
+
const piSession = merged.piSession && typeof merged.piSession === "object" && !Array.isArray(merged.piSession) ? firstString(merged.piSession, ["sessionId", "id"]) : "";
|
|
1230
|
+
const timeline = Array.isArray(merged.timeline) ? merged.timeline.length : null;
|
|
1231
|
+
const approvals = Array.isArray(merged.approvals) ? merged.approvals.length : null;
|
|
1232
|
+
const inputs = Array.isArray(merged.userInputs) ? merged.userInputs.length : null;
|
|
1233
|
+
const rows = [
|
|
1234
|
+
["run", pc2.bold(runId)],
|
|
1235
|
+
["status", formatStatusPill(status)],
|
|
1236
|
+
["task", taskId],
|
|
1237
|
+
["title", title],
|
|
1238
|
+
["runtime", [runtime, mode, interaction].filter(Boolean).join(" \xB7 ")],
|
|
1239
|
+
["created", created],
|
|
1240
|
+
["started", started],
|
|
1241
|
+
["updated", updated],
|
|
1242
|
+
["completed", completed],
|
|
1243
|
+
["worktree", worktree],
|
|
1244
|
+
["pi", piSession],
|
|
1245
|
+
["timeline", timeline],
|
|
1246
|
+
["approvals", approvals],
|
|
1247
|
+
["inputs", inputs]
|
|
1248
|
+
];
|
|
1249
|
+
return [
|
|
1250
|
+
formatSuccessCard(options.title ?? "Run details", rows),
|
|
1251
|
+
"",
|
|
1252
|
+
...formatNextSteps([`Follow live: \`rig run attach ${runId} --follow\``, `Raw payload: \`rig run show ${runId} --raw\``])
|
|
1253
|
+
].join(`
|
|
1254
|
+
`);
|
|
1255
|
+
}
|
|
1256
|
+
function formatRunStatus(summary, options = {}) {
|
|
1257
|
+
const activeRuns = summary.activeRuns ?? [];
|
|
1258
|
+
const recentRuns = summary.recentRuns ?? [];
|
|
1259
|
+
const lines = [formatSection("Run status", options.source === "server" ? "selected server" : "local state")];
|
|
1260
|
+
lines.push("", pc2.bold(`Active runs (${activeRuns.length})`));
|
|
1261
|
+
if (activeRuns.length === 0) {
|
|
1262
|
+
lines.push(pc2.dim("No active runs."));
|
|
1263
|
+
} else {
|
|
1264
|
+
for (const run of activeRuns) {
|
|
1265
|
+
lines.push(formatRunSummaryLine(run));
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
lines.push("", pc2.bold(`Recent runs (${recentRuns.length})`));
|
|
1269
|
+
if (recentRuns.length === 0) {
|
|
1270
|
+
lines.push(pc2.dim("No recent terminal runs."));
|
|
1271
|
+
} else {
|
|
1272
|
+
for (const run of recentRuns.slice(0, 10)) {
|
|
1273
|
+
lines.push(formatRunSummaryLine(run));
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
lines.push("", ...formatNextSteps(["Start work: `rig task run --next`", "Attach: `rig run attach <run-id> --follow`", "Details: `rig run show <run-id>`"]));
|
|
1277
|
+
return lines.join(`
|
|
1278
|
+
`);
|
|
1279
|
+
}
|
|
1280
|
+
function formatRunSummaryLine(run) {
|
|
1281
|
+
const record = run;
|
|
1282
|
+
const runId = runIdOf(record);
|
|
1283
|
+
const status = firstString(record, ["status"], "unknown");
|
|
1284
|
+
const taskId = taskIdOf(record);
|
|
1285
|
+
const title = runTitleOf(record);
|
|
1286
|
+
const runtime = firstString(record, ["runtimeAdapter", "runtime", "adapter"]);
|
|
1287
|
+
const descriptor = [taskId, title].filter(Boolean).join(" \xB7 ");
|
|
1288
|
+
return `${pc2.dim("\u2502")} ${pc2.bold(runId)} ${formatStatusPill(status)} ${descriptor}${runtime ? pc2.dim(` ${runtime}`) : ""}`;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// packages/cli/src/commands/inbox.ts
|
|
1292
|
+
async function listInboxRecords(context, kind, filters) {
|
|
1293
|
+
const params = new URLSearchParams;
|
|
1294
|
+
if (filters.run)
|
|
1295
|
+
params.set("runId", filters.run);
|
|
1296
|
+
if (filters.task)
|
|
1297
|
+
params.set("taskId", filters.task);
|
|
1298
|
+
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
1299
|
+
const payload = await requestServerJson(context, `/api/inbox/${kind}${query}`);
|
|
1300
|
+
const records = Array.isArray(payload) ? payload : [];
|
|
1301
|
+
return filters.pendingOnly ? records.filter((entry) => (entry.status ?? "pending") !== "resolved") : records;
|
|
1302
|
+
}
|
|
1303
|
+
async function readPendingInboxCounts(context) {
|
|
1304
|
+
try {
|
|
1305
|
+
const [approvals, inputs] = await Promise.all([
|
|
1306
|
+
listInboxRecords(context, "approvals", { pendingOnly: true }),
|
|
1307
|
+
listInboxRecords(context, "inputs", { pendingOnly: true })
|
|
1308
|
+
]);
|
|
1309
|
+
return { approvals: approvals.length, inputs: inputs.length };
|
|
1310
|
+
} catch {
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
async function printPendingInboxFooter(context) {
|
|
1315
|
+
if (context.outputMode !== "text")
|
|
1316
|
+
return;
|
|
1317
|
+
const counts = await readPendingInboxCounts(context);
|
|
1318
|
+
if (!counts || counts.approvals === 0 && counts.inputs === 0)
|
|
1319
|
+
return;
|
|
1320
|
+
const parts = [];
|
|
1321
|
+
if (counts.approvals > 0)
|
|
1322
|
+
parts.push(`${counts.approvals} approval${counts.approvals === 1 ? "" : "s"}`);
|
|
1323
|
+
if (counts.inputs > 0)
|
|
1324
|
+
parts.push(`${counts.inputs} input request${counts.inputs === 1 ? "" : "s"}`);
|
|
1325
|
+
console.log(`
|
|
1326
|
+
\u26A0 ${parts.join(" and ")} pending \u2014 run \`rig inbox\` to review.`);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
409
1329
|
// packages/cli/src/commands/run.ts
|
|
410
1330
|
function normalizeRemoteRunDetails(payload) {
|
|
411
1331
|
const run = payload.run;
|
|
@@ -418,6 +1338,22 @@ function normalizeRemoteRunDetails(payload) {
|
|
|
418
1338
|
...Array.isArray(payload.userInputs) ? { userInputs: payload.userInputs } : {}
|
|
419
1339
|
};
|
|
420
1340
|
}
|
|
1341
|
+
var REMOTE_TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged"]);
|
|
1342
|
+
async function listRunsForSelectedConnection(context, options = {}) {
|
|
1343
|
+
if (isRemoteConnectionSelected(context.projectRoot)) {
|
|
1344
|
+
return { runs: await listRunsViaServer(context, options), source: "server" };
|
|
1345
|
+
}
|
|
1346
|
+
return { runs: listAuthorityRuns(context.projectRoot), source: "local" };
|
|
1347
|
+
}
|
|
1348
|
+
function runStringField(run, key, fallback = "") {
|
|
1349
|
+
const value = run[key];
|
|
1350
|
+
return typeof value === "string" && value.trim() ? value : fallback;
|
|
1351
|
+
}
|
|
1352
|
+
function buildServerRunStatus(runs) {
|
|
1353
|
+
const activeRuns = runs.filter((run) => !REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
|
|
1354
|
+
const recentRuns = runs.filter((run) => REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
|
|
1355
|
+
return { activeRuns, recentRuns, runs };
|
|
1356
|
+
}
|
|
421
1357
|
function shouldPromptForEpicSelection(context, command, promptEpic, noEpicPrompt) {
|
|
422
1358
|
if (noEpicPrompt) {
|
|
423
1359
|
return false;
|
|
@@ -438,7 +1374,7 @@ async function promptForEpicSelection(projectRoot, command) {
|
|
|
438
1374
|
options.unshift(defaultEpic);
|
|
439
1375
|
}
|
|
440
1376
|
if (options.length === 0) {
|
|
441
|
-
throw new
|
|
1377
|
+
throw new CliError("No open epic found. Pass --epic <id>.", 1, { hint: "Re-run with `rig run start --epic <id>`." });
|
|
442
1378
|
}
|
|
443
1379
|
console.log(`Select epic for run ${command}:`);
|
|
444
1380
|
options.forEach((id, index) => {
|
|
@@ -460,7 +1396,7 @@ async function promptForEpicSelection(projectRoot, command) {
|
|
|
460
1396
|
return fallback ?? options[0];
|
|
461
1397
|
}
|
|
462
1398
|
if (answer === "q" || answer === "quit") {
|
|
463
|
-
throw new
|
|
1399
|
+
throw new CliError("Run cancelled by user.");
|
|
464
1400
|
}
|
|
465
1401
|
if (/^\d+$/.test(answer)) {
|
|
466
1402
|
const index = Number.parseInt(answer, 10) - 1;
|
|
@@ -479,21 +1415,16 @@ async function promptForEpicSelection(projectRoot, command) {
|
|
|
479
1415
|
}
|
|
480
1416
|
async function executeRun(context, args) {
|
|
481
1417
|
const [command = "status", ...rest] = args;
|
|
482
|
-
const runtimeContext =
|
|
1418
|
+
const runtimeContext = loadRuntimeContextFromEnv() ?? undefined;
|
|
483
1419
|
switch (command) {
|
|
484
1420
|
case "list": {
|
|
485
|
-
requireNoExtraArgs(rest, "
|
|
486
|
-
const runs =
|
|
1421
|
+
requireNoExtraArgs(rest, "rig run list");
|
|
1422
|
+
const { runs, source } = isRemoteConnectionSelected(context.projectRoot) ? await withSpinner("Reading runs from server\u2026", () => listRunsForSelectedConnection(context, { limit: 100 }), { outputMode: context.outputMode }) : await listRunsForSelectedConnection(context, { limit: 100 });
|
|
487
1423
|
if (context.outputMode === "text") {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
} else {
|
|
491
|
-
for (const run of runs) {
|
|
492
|
-
console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.title}`);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
1424
|
+
printFormattedOutput(formatRunList(runs, { source }));
|
|
1425
|
+
await printPendingInboxFooter(context);
|
|
495
1426
|
}
|
|
496
|
-
return { ok: true, group: "run", command, details: { runs } };
|
|
1427
|
+
return { ok: true, group: "run", command, details: { runs, source } };
|
|
497
1428
|
}
|
|
498
1429
|
case "delete": {
|
|
499
1430
|
let pending = rest;
|
|
@@ -501,9 +1432,9 @@ async function executeRun(context, args) {
|
|
|
501
1432
|
pending = run.rest;
|
|
502
1433
|
const purgeArtifacts = takeFlag(pending, "--purge-artifacts");
|
|
503
1434
|
pending = purgeArtifacts.rest;
|
|
504
|
-
requireNoExtraArgs(pending, "
|
|
1435
|
+
requireNoExtraArgs(pending, "rig run delete --run <id> [--purge-artifacts]");
|
|
505
1436
|
if (!run.value) {
|
|
506
|
-
throw new
|
|
1437
|
+
throw new CliError("run delete requires --run <id>.", 1, { hint: "Run `rig run list` to find run ids, then `rig run delete --run <run-id>`." });
|
|
507
1438
|
}
|
|
508
1439
|
const result = await deleteRunState(context.projectRoot, {
|
|
509
1440
|
runId: run.value,
|
|
@@ -531,9 +1462,9 @@ async function executeRun(context, args) {
|
|
|
531
1462
|
pending = keepRuntimes.rest;
|
|
532
1463
|
const keepQueue = takeFlag(pending, "--keep-queue");
|
|
533
1464
|
pending = keepQueue.rest;
|
|
534
|
-
requireNoExtraArgs(pending, "
|
|
1465
|
+
requireNoExtraArgs(pending, "rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
|
|
535
1466
|
if (!all.value) {
|
|
536
|
-
throw new
|
|
1467
|
+
throw new CliError("run cleanup currently requires --all.", 1, { hint: "Run `rig run cleanup --all` (add --keep-artifacts/--keep-runtimes/--keep-queue to retain state)." });
|
|
537
1468
|
}
|
|
538
1469
|
const result = await cleanupRunState(context.projectRoot, {
|
|
539
1470
|
includeArtifacts: !keepArtifacts.value,
|
|
@@ -550,20 +1481,25 @@ async function executeRun(context, args) {
|
|
|
550
1481
|
}
|
|
551
1482
|
case "show": {
|
|
552
1483
|
let pending = rest;
|
|
1484
|
+
const rawResult = takeFlag(pending, "--raw");
|
|
1485
|
+
pending = rawResult.rest;
|
|
553
1486
|
const run = takeOption(pending, "--run");
|
|
554
1487
|
pending = run.rest;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1488
|
+
const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
|
|
1489
|
+
const extra = positionalRunId ? pending.slice(1) : pending;
|
|
1490
|
+
requireNoExtraArgs(extra, "rig run show <id>|--run <id> [--raw]");
|
|
1491
|
+
const runId = run.value ?? positionalRunId;
|
|
1492
|
+
if (!runId) {
|
|
1493
|
+
throw new CliError("run show requires a run id.", 1, { hint: "Run `rig run list` to find run ids, then `rig run show <run-id>`." });
|
|
558
1494
|
}
|
|
559
|
-
const record =
|
|
1495
|
+
const record = readAuthorityRun2(context.projectRoot, runId) ?? normalizeRemoteRunDetails(await withSpinner(`Reading run ${runId} from server\u2026`, () => getRunDetailsViaServer(context, runId).catch(() => ({})), { outputMode: context.outputMode }));
|
|
560
1496
|
if (!record) {
|
|
561
|
-
throw new
|
|
1497
|
+
throw new CliError(`Run not found: ${runId}`, 2, { hint: "Run `rig run list` to see recorded runs." });
|
|
562
1498
|
}
|
|
563
1499
|
if (context.outputMode === "text") {
|
|
564
|
-
|
|
1500
|
+
printFormattedOutput(rawResult.value ? JSON.stringify(record, null, 2) : formatRunCard(record));
|
|
565
1501
|
}
|
|
566
|
-
return { ok: true, group: "run", command, details: record };
|
|
1502
|
+
return { ok: true, group: "run", command, details: { ...record, rawOutput: rawResult.value } };
|
|
567
1503
|
}
|
|
568
1504
|
case "timeline": {
|
|
569
1505
|
let pending = rest;
|
|
@@ -571,38 +1507,69 @@ async function executeRun(context, args) {
|
|
|
571
1507
|
pending = run.rest;
|
|
572
1508
|
const follow = takeFlag(pending, "--follow");
|
|
573
1509
|
pending = follow.rest;
|
|
574
|
-
requireNoExtraArgs(pending, "
|
|
1510
|
+
requireNoExtraArgs(pending, "rig run timeline --run <id> [--follow]");
|
|
575
1511
|
if (!run.value) {
|
|
576
|
-
throw new
|
|
1512
|
+
throw new CliError("run timeline requires --run <id>.", 1, { hint: "Run `rig run list` to find run ids, then `rig run timeline --run <run-id>`." });
|
|
1513
|
+
}
|
|
1514
|
+
const renderer = createPiRunStreamRenderer();
|
|
1515
|
+
let cursor = null;
|
|
1516
|
+
const timelineRunId = run.value;
|
|
1517
|
+
const page = await withSpinner(`Reading timeline for ${timelineRunId}\u2026`, () => getRunTimelineViaServer(context, timelineRunId, { limit: 500 }), { outputMode: context.outputMode });
|
|
1518
|
+
const events = Array.isArray(page.entries) ? page.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1519
|
+
cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
|
|
1520
|
+
if (context.outputMode === "text") {
|
|
1521
|
+
renderer.renderTimeline(events);
|
|
577
1522
|
}
|
|
578
|
-
const timelinePath = resolve3(resolveAuthorityRunDir(context.projectRoot, run.value), "timeline.jsonl");
|
|
579
|
-
const printEvents = () => {
|
|
580
|
-
const events2 = readJsonlFile(timelinePath);
|
|
581
|
-
if (context.outputMode === "text") {
|
|
582
|
-
for (const event of events2) {
|
|
583
|
-
console.log(JSON.stringify(event));
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
return events2;
|
|
587
|
-
};
|
|
588
|
-
const events = printEvents();
|
|
589
1523
|
if (follow.value && context.outputMode === "text") {
|
|
590
|
-
let lastLength = existsSync3(timelinePath) ? readFileSync3(timelinePath, "utf8").length : 0;
|
|
591
1524
|
while (true) {
|
|
592
1525
|
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
|
-
}
|
|
1526
|
+
const nextPage = await getRunTimelineViaServer(context, run.value, { limit: 500, ...cursor ? { cursor } : {} });
|
|
1527
|
+
const nextEvents = Array.isArray(nextPage.entries) ? nextPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1528
|
+
cursor = typeof nextPage.nextCursor === "string" ? nextPage.nextCursor : cursor;
|
|
1529
|
+
renderer.renderTimeline(nextEvents);
|
|
603
1530
|
}
|
|
604
1531
|
}
|
|
605
|
-
return { ok: true, group: "run", command, details: { runId: run.value, events } };
|
|
1532
|
+
return { ok: true, group: "run", command, details: { runId: run.value, events, cursor } };
|
|
1533
|
+
}
|
|
1534
|
+
case "replay": {
|
|
1535
|
+
let pending = rest;
|
|
1536
|
+
const run = takeOption(pending, "--run");
|
|
1537
|
+
pending = run.rest;
|
|
1538
|
+
const withSession = takeFlag(pending, "--with-session");
|
|
1539
|
+
pending = withSession.rest;
|
|
1540
|
+
const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
|
|
1541
|
+
const extra = positionalRunId ? pending.slice(1) : pending;
|
|
1542
|
+
requireNoExtraArgs(extra, "rig run replay <id>|--run <id> [--with-session]");
|
|
1543
|
+
const runId = run.value ?? positionalRunId;
|
|
1544
|
+
if (!runId) {
|
|
1545
|
+
throw new CliError("run replay requires a run id.", 2, { hint: "Run `rig run list` to find run ids, then `rig run replay <run-id>`." });
|
|
1546
|
+
}
|
|
1547
|
+
const replay = buildRunReplay(context.projectRoot, runId, { withSession: withSession.value });
|
|
1548
|
+
if (replay.entryCount === 0 && replay.sessionEntryCount === 0) {
|
|
1549
|
+
throw new CliError(`No run.jsonl lifecycle log found for ${runId} (looked at ${replay.logPath}).`, 2, { hint: "Run `rig run list` to confirm the run id; older runs may predate consolidated run.jsonl logging." });
|
|
1550
|
+
}
|
|
1551
|
+
if (context.outputMode === "text") {
|
|
1552
|
+
console.log(`Replay of ${runId} (${replay.entryCount} lifecycle entries${withSession.value ? `, ${replay.sessionEntryCount} session entries` : ""}):`);
|
|
1553
|
+
for (const line of replay.lines) {
|
|
1554
|
+
console.log(line);
|
|
1555
|
+
}
|
|
1556
|
+
if (withSession.value && !replay.sessionFile) {
|
|
1557
|
+
console.log("(no Pi session file resolvable from the run record; showing run.jsonl only)");
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return {
|
|
1561
|
+
ok: true,
|
|
1562
|
+
group: "run",
|
|
1563
|
+
command,
|
|
1564
|
+
details: {
|
|
1565
|
+
runId,
|
|
1566
|
+
logPath: replay.logPath,
|
|
1567
|
+
sessionFile: replay.sessionFile,
|
|
1568
|
+
entryCount: replay.entryCount,
|
|
1569
|
+
sessionEntryCount: replay.sessionEntryCount,
|
|
1570
|
+
lines: replay.lines
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
606
1573
|
}
|
|
607
1574
|
case "attach": {
|
|
608
1575
|
let pending = rest;
|
|
@@ -618,41 +1585,40 @@ async function executeRun(context, args) {
|
|
|
618
1585
|
pending = pollMs.rest;
|
|
619
1586
|
const positionalRunId = pending.length > 0 ? pending[0] : undefined;
|
|
620
1587
|
const extra = positionalRunId ? pending.slice(1) : pending;
|
|
621
|
-
requireNoExtraArgs(extra, "
|
|
1588
|
+
requireNoExtraArgs(extra, "rig run attach <run-id>|--run <run-id> [--message <text>] [--once|--follow] [--poll-ms <ms>]");
|
|
622
1589
|
const runId = runOption.value ?? positionalRunId;
|
|
623
1590
|
if (!runId) {
|
|
624
|
-
throw new
|
|
1591
|
+
throw new CliError("run attach requires a run id.", 2, { hint: "Run `rig run list` to find run ids, then `rig run attach <run-id> --follow`." });
|
|
1592
|
+
}
|
|
1593
|
+
let steered = false;
|
|
1594
|
+
const steerMessage = messageOption.value?.trim();
|
|
1595
|
+
if (steerMessage) {
|
|
1596
|
+
await withSpinner("Queueing steering message\u2026", () => steerRunViaServer(context, runId, steerMessage), { outputMode: context.outputMode });
|
|
1597
|
+
steered = true;
|
|
625
1598
|
}
|
|
626
1599
|
const attached = await attachRunOperatorView(context, {
|
|
627
1600
|
runId,
|
|
628
|
-
message:
|
|
1601
|
+
message: null,
|
|
629
1602
|
once: once.value,
|
|
630
1603
|
follow: follow.value,
|
|
631
1604
|
pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)
|
|
632
1605
|
});
|
|
633
|
-
return { ok: true, group: "run", command, details: attached };
|
|
1606
|
+
return { ok: true, group: "run", command, details: { ...attached, steered: attached.steered || steered } };
|
|
634
1607
|
}
|
|
635
1608
|
case "status": {
|
|
636
|
-
requireNoExtraArgs(rest, "
|
|
1609
|
+
requireNoExtraArgs(rest, "rig run status");
|
|
637
1610
|
if (context.dryRun) {
|
|
638
1611
|
if (context.outputMode === "text") {
|
|
639
1612
|
console.log("[dry-run] rig run status");
|
|
640
1613
|
}
|
|
641
1614
|
return { ok: true, group: "run", command };
|
|
642
1615
|
}
|
|
643
|
-
const summary = runStatus(context.projectRoot, runtimeContext);
|
|
1616
|
+
const summary = isRemoteConnectionSelected(context.projectRoot) ? buildServerRunStatus(await withSpinner("Reading run status from server\u2026", () => listRunsViaServer(context, { limit: 100 }), { outputMode: context.outputMode })) : runStatus(context.projectRoot, runtimeContext);
|
|
1617
|
+
const activeRuns = Array.isArray(summary.activeRuns) ? summary.activeRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
|
|
1618
|
+
const recentRuns = Array.isArray(summary.recentRuns) ? summary.recentRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
|
|
644
1619
|
if (context.outputMode === "text") {
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
}
|
|
1620
|
+
printFormattedOutput(formatRunStatus({ activeRuns, recentRuns, runs: Array.isArray(summary.runs) ? summary.runs : [...activeRuns, ...recentRuns] }, { source: isRemoteConnectionSelected(context.projectRoot) ? "server" : "local" }));
|
|
1621
|
+
await printPendingInboxFooter(context);
|
|
656
1622
|
}
|
|
657
1623
|
return { ok: true, group: "run", command, details: summary };
|
|
658
1624
|
}
|
|
@@ -676,12 +1642,12 @@ async function executeRun(context, args) {
|
|
|
676
1642
|
pending = pollResult.rest;
|
|
677
1643
|
const noServerResult = takeFlag(pending, "--no-server");
|
|
678
1644
|
pending = noServerResult.rest;
|
|
679
|
-
requireNoExtraArgs(pending, "
|
|
1645
|
+
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
1646
|
if (promptEpicResult.value && noEpicPromptResult.value) {
|
|
681
|
-
throw new
|
|
1647
|
+
throw new CliError("Cannot use --prompt-epic and --no-epic-prompt together.", 1, { hint: "Pass only one of --prompt-epic or --no-epic-prompt." });
|
|
682
1648
|
}
|
|
683
1649
|
if (promptEpicResult.value && (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
684
|
-
throw new
|
|
1650
|
+
throw new CliError("--prompt-epic requires an interactive terminal (TTY) in text mode.", 1, { hint: "Pass the epic explicitly instead: `rig run start --epic <id>`." });
|
|
685
1651
|
}
|
|
686
1652
|
let resolvedEpicId = epicResult.value || undefined;
|
|
687
1653
|
if (!resolvedEpicId && shouldPromptForEpicSelection(context, command, promptEpicResult.value, noEpicPromptResult.value)) {
|
|
@@ -709,7 +1675,7 @@ async function executeRun(context, args) {
|
|
|
709
1675
|
console.log(`Runs: ${result.runIds.join(", ")}`);
|
|
710
1676
|
}
|
|
711
1677
|
if (result.exitCode !== 0) {
|
|
712
|
-
throw new
|
|
1678
|
+
throw new CliError(`run ${command} failed with exit code ${result.exitCode}.`, result.exitCode, { hint: "Inspect with `rig run status` and `rig inspect logs --task <id>`." });
|
|
713
1679
|
}
|
|
714
1680
|
return {
|
|
715
1681
|
ok: true,
|
|
@@ -724,38 +1690,76 @@ async function executeRun(context, args) {
|
|
|
724
1690
|
};
|
|
725
1691
|
}
|
|
726
1692
|
case "resume": {
|
|
727
|
-
|
|
1693
|
+
let pending = rest;
|
|
1694
|
+
const runOpt = takeOption(pending, "--run");
|
|
1695
|
+
pending = runOpt.rest;
|
|
1696
|
+
const positional = pending[0] && !pending[0].startsWith("-") ? pending.shift() : undefined;
|
|
1697
|
+
requireNoExtraArgs(pending, "rig run resume [<run-id>] [--run <id>]");
|
|
1698
|
+
const targetRunId = runOpt.value ?? positional ?? null;
|
|
728
1699
|
if (context.dryRun) {
|
|
729
1700
|
if (context.outputMode === "text") {
|
|
730
1701
|
console.log("[dry-run] rig run resume");
|
|
731
1702
|
}
|
|
732
1703
|
return { ok: true, group: "run", command };
|
|
733
1704
|
}
|
|
734
|
-
const resumed = await
|
|
1705
|
+
const resumed = await withSpinner(targetRunId ? `Resuming run ${targetRunId}\u2026` : "Resuming latest resumable run\u2026", () => resumeRunViaServer(context, targetRunId, { restart: false }), { outputMode: context.outputMode });
|
|
735
1706
|
if (context.outputMode === "text") {
|
|
736
1707
|
console.log(`Resumed run: ${resumed.runId}`);
|
|
737
1708
|
}
|
|
738
1709
|
return { ok: true, group: "run", command, details: resumed };
|
|
739
1710
|
}
|
|
740
1711
|
case "restart": {
|
|
741
|
-
|
|
1712
|
+
let pending = rest;
|
|
1713
|
+
const runOpt = takeOption(pending, "--run");
|
|
1714
|
+
pending = runOpt.rest;
|
|
1715
|
+
const positional = pending[0] && !pending[0].startsWith("-") ? pending.shift() : undefined;
|
|
1716
|
+
requireNoExtraArgs(pending, "rig run restart [<run-id>] [--run <id>]");
|
|
1717
|
+
const targetRunId = runOpt.value ?? positional ?? null;
|
|
742
1718
|
if (context.dryRun) {
|
|
743
1719
|
if (context.outputMode === "text") {
|
|
744
1720
|
console.log("[dry-run] rig run restart");
|
|
745
1721
|
}
|
|
746
1722
|
return { ok: true, group: "run", command };
|
|
747
1723
|
}
|
|
748
|
-
const restarted = await
|
|
1724
|
+
const restarted = await withSpinner(targetRunId ? `Restarting run ${targetRunId}\u2026` : "Restarting latest run\u2026", () => resumeRunViaServer(context, targetRunId, { restart: true }), { outputMode: context.outputMode });
|
|
749
1725
|
if (context.outputMode === "text") {
|
|
750
1726
|
console.log(`Restarted run: ${restarted.runId}`);
|
|
751
1727
|
}
|
|
752
1728
|
return { ok: true, group: "run", command, details: restarted };
|
|
753
1729
|
}
|
|
1730
|
+
case "steer": {
|
|
1731
|
+
const runOption = takeOption(rest, "--run");
|
|
1732
|
+
const messageOption = takeOption(runOption.rest, "--message");
|
|
1733
|
+
const shortMessageOption = takeOption(messageOption.rest, "-m");
|
|
1734
|
+
const positionalRunId = shortMessageOption.rest.length > 0 ? shortMessageOption.rest[0] : undefined;
|
|
1735
|
+
const extra = positionalRunId ? shortMessageOption.rest.slice(1) : shortMessageOption.rest;
|
|
1736
|
+
requireNoExtraArgs(extra, "rig run steer [<run-id>|--run <id>] --message <text>");
|
|
1737
|
+
const runId = runOption.value ?? positionalRunId;
|
|
1738
|
+
const message = messageOption.value ?? shortMessageOption.value;
|
|
1739
|
+
if (!runId) {
|
|
1740
|
+
throw new CliError("run steer requires a run id (positional or --run <id>).", 2, { hint: "Run `rig run list` to find run ids, then `rig run steer <run-id> --message <text>`." });
|
|
1741
|
+
}
|
|
1742
|
+
if (!message?.trim()) {
|
|
1743
|
+
throw new CliError("run steer requires --message <text>.", 2, { hint: 'Re-run as `rig run steer <run-id> --message "your steering note"`.' });
|
|
1744
|
+
}
|
|
1745
|
+
if (context.dryRun) {
|
|
1746
|
+
if (context.outputMode === "text") {
|
|
1747
|
+
console.log(`[dry-run] rig run steer ${runId} --message ${JSON.stringify(message)}`);
|
|
1748
|
+
}
|
|
1749
|
+
return { ok: true, group: "run", command, details: { runId, dryRun: true } };
|
|
1750
|
+
}
|
|
1751
|
+
const trimmedMessage = message.trim();
|
|
1752
|
+
await withSpinner("Queueing steering message\u2026", () => steerRunViaServer(context, runId, trimmedMessage), { outputMode: context.outputMode });
|
|
1753
|
+
if (context.outputMode === "text") {
|
|
1754
|
+
console.log(`Steering message queued for ${runId}.`);
|
|
1755
|
+
}
|
|
1756
|
+
return { ok: true, group: "run", command, details: { runId, queued: true } };
|
|
1757
|
+
}
|
|
754
1758
|
case "stop": {
|
|
755
1759
|
const runOption = takeOption(rest, "--run");
|
|
756
1760
|
const positionalRunId = runOption.rest.length > 0 ? runOption.rest[0] : undefined;
|
|
757
1761
|
const extra = positionalRunId ? runOption.rest.slice(1) : runOption.rest;
|
|
758
|
-
requireNoExtraArgs(extra, "
|
|
1762
|
+
requireNoExtraArgs(extra, "rig run stop [<run-id>|--run <id>]");
|
|
759
1763
|
const runId = runOption.value ?? positionalRunId;
|
|
760
1764
|
if (context.dryRun) {
|
|
761
1765
|
return {
|
|
@@ -766,14 +1770,14 @@ async function executeRun(context, args) {
|
|
|
766
1770
|
};
|
|
767
1771
|
}
|
|
768
1772
|
if (runId) {
|
|
769
|
-
const stopped = await stopRunViaServer(context, runId);
|
|
1773
|
+
const stopped = await withSpinner(`Requesting stop for ${runId}\u2026`, () => stopRunViaServer(context, runId), { outputMode: context.outputMode });
|
|
770
1774
|
if (context.outputMode === "text")
|
|
771
1775
|
console.log(`Stop requested: ${runId}`);
|
|
772
1776
|
return { ok: true, group: "run", command, details: stopped };
|
|
773
1777
|
}
|
|
774
1778
|
const result = await runStop(context.projectRoot);
|
|
775
1779
|
if (result.remaining.length > 0) {
|
|
776
|
-
throw new
|
|
1780
|
+
throw new CliError(`Failed to stop run(s): ${result.remaining.join(", ")}`, 1, { hint: "Check `rig run status`, then retry `rig run stop <run-id>` for the remaining runs." });
|
|
777
1781
|
}
|
|
778
1782
|
if (context.outputMode === "text") {
|
|
779
1783
|
console.log(`Stopped process count: ${result.stopped}`);
|
|
@@ -789,7 +1793,7 @@ async function executeRun(context, args) {
|
|
|
789
1793
|
};
|
|
790
1794
|
}
|
|
791
1795
|
default:
|
|
792
|
-
throw new
|
|
1796
|
+
throw new CliError(`Unknown run command: ${command}`, 1, { hint: "Run `rig run --help` to list available run commands." });
|
|
793
1797
|
}
|
|
794
1798
|
}
|
|
795
1799
|
export {
|