@h-rig/cli 0.0.6-alpha.13 → 0.0.6-alpha.14

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.
@@ -100,11 +100,10 @@ function resolveSelectedConnection(projectRoot, options = {}) {
100
100
  }
101
101
 
102
102
  // packages/cli/src/commands/_server-client.ts
103
- import { spawnSync } from "child_process";
104
103
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
105
104
  import { resolve as resolve2 } from "path";
106
105
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
107
- var cachedGitHubBearerToken;
106
+ var scopedGitHubBearerTokens = new Map;
108
107
  function cleanToken(value) {
109
108
  const trimmed = value?.trim();
110
109
  return trimmed ? trimmed : null;
@@ -121,25 +120,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
121
120
  }
122
121
  }
123
122
  function readGitHubBearerTokenForRemote(projectRoot) {
124
- if (cachedGitHubBearerToken !== undefined)
125
- return cachedGitHubBearerToken;
123
+ const scopedKey = resolve2(projectRoot);
124
+ if (scopedGitHubBearerTokens.has(scopedKey))
125
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
126
126
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
127
- if (privateSession) {
128
- cachedGitHubBearerToken = privateSession;
129
- return cachedGitHubBearerToken;
130
- }
131
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
132
- if (envToken) {
133
- cachedGitHubBearerToken = envToken;
134
- return cachedGitHubBearerToken;
135
- }
136
- const result = spawnSync("gh", ["auth", "token"], {
137
- encoding: "utf8",
138
- timeout: 5000,
139
- stdio: ["ignore", "pipe", "ignore"]
140
- });
141
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
142
- return cachedGitHubBearerToken;
127
+ if (privateSession)
128
+ return privateSession;
129
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
143
130
  }
144
131
  async function ensureServerForCli(projectRoot) {
145
132
  try {
@@ -0,0 +1,157 @@
1
+ // @bun
2
+ // packages/cli/src/commands/_operator-surface.ts
3
+ import { createInterface } from "readline";
4
+ import { createInterface as createPromptInterface } from "readline/promises";
5
+ var CANONICAL_STAGES = [
6
+ "Connect",
7
+ "GitHub/task sync",
8
+ "Prepare workspace",
9
+ "Launch Pi",
10
+ "Plan",
11
+ "Implement",
12
+ "Validate",
13
+ "Commit",
14
+ "Open PR",
15
+ "Review/CI",
16
+ "Merge",
17
+ "Complete"
18
+ ];
19
+ function logDetail(log) {
20
+ return typeof log.detail === "string" ? log.detail.trim() : "";
21
+ }
22
+ function entryId(entry, fallback) {
23
+ return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
24
+ }
25
+ function renderOperatorSnapshot(snapshot) {
26
+ const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
27
+ const runId = String(run.runId ?? run.id ?? "run");
28
+ const status = String(run.status ?? "unknown");
29
+ const logs = snapshot.logs ?? [];
30
+ const latestByStage = new Map;
31
+ for (const log of logs) {
32
+ const title = String(log.title ?? "").toLowerCase();
33
+ const stageName = String(log.stage ?? "").toLowerCase();
34
+ const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
35
+ if (stage)
36
+ latestByStage.set(stage, log);
37
+ }
38
+ const stageLines = CANONICAL_STAGES.flatMap((stage) => {
39
+ const match = latestByStage.get(stage);
40
+ return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
41
+ });
42
+ return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
43
+ `);
44
+ }
45
+ function createPiRunStreamRenderer(output = process.stdout) {
46
+ let lastSnapshot = "";
47
+ const assistantTextById = new Map;
48
+ const seenTimeline = new Set;
49
+ const seenLogs = new Set;
50
+ const writeLine = (line) => output.write(`${line}
51
+ `);
52
+ return {
53
+ renderSnapshot(snapshot) {
54
+ const rendered = renderOperatorSnapshot(snapshot);
55
+ if (rendered && rendered !== lastSnapshot) {
56
+ writeLine(rendered);
57
+ lastSnapshot = rendered;
58
+ }
59
+ },
60
+ renderTimeline(entries) {
61
+ for (const [index, entry] of entries.entries()) {
62
+ const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
63
+ if (entry.type === "assistant_message" && typeof entry.text === "string") {
64
+ const text = entry.text;
65
+ const previousText = assistantTextById.get(id) ?? "";
66
+ if (text.startsWith(previousText)) {
67
+ const delta = text.slice(previousText.length);
68
+ if (delta)
69
+ output.write(delta);
70
+ } else if (text.trim() && text !== previousText) {
71
+ writeLine(`
72
+ [Pi assistant]`);
73
+ output.write(text);
74
+ }
75
+ assistantTextById.set(id, text);
76
+ continue;
77
+ }
78
+ if (seenTimeline.has(id))
79
+ continue;
80
+ seenTimeline.add(id);
81
+ if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
82
+ writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
83
+ continue;
84
+ }
85
+ if (entry.type === "timeline_warning") {
86
+ writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
87
+ }
88
+ }
89
+ },
90
+ renderLogs(entries) {
91
+ for (const [index, entry] of entries.entries()) {
92
+ const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
93
+ if (seenLogs.has(id))
94
+ continue;
95
+ seenLogs.add(id);
96
+ const title = String(entry.title ?? "");
97
+ if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
98
+ continue;
99
+ const detail = logDetail(entry);
100
+ if (!detail)
101
+ continue;
102
+ writeLine(`[${title || "Rig log"}] ${detail}`);
103
+ }
104
+ }
105
+ };
106
+ }
107
+ function createOperatorSurface(options = {}) {
108
+ const input = options.input ?? process.stdin;
109
+ const output = options.output ?? process.stdout;
110
+ const errorOutput = options.errorOutput ?? process.stderr;
111
+ const renderer = createPiRunStreamRenderer(output);
112
+ const writeLine = (line) => output.write(`${line}
113
+ `);
114
+ return {
115
+ mode: "pi-compatible-text",
116
+ ...renderer,
117
+ info: writeLine,
118
+ error: (message) => errorOutput.write(`${message}
119
+ `),
120
+ attachCommandInput(handler) {
121
+ if (options.interactive === false || !input.isTTY)
122
+ return null;
123
+ const rl = createInterface({ input, output: process.stdout, terminal: false });
124
+ rl.on("line", (line) => {
125
+ Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
126
+ });
127
+ return { close: () => rl.close() };
128
+ }
129
+ };
130
+ }
131
+ function taskId(task) {
132
+ return typeof task.id === "string" && task.id.trim() ? task.id : "<unknown>";
133
+ }
134
+ function taskTitle(task) {
135
+ return typeof task.title === "string" && task.title.trim() ? task.title : "Untitled task";
136
+ }
137
+ function taskStatus(task) {
138
+ return typeof task.status === "string" && task.status.trim() ? task.status : "unknown";
139
+ }
140
+ function renderTaskPickerRows(tasks) {
141
+ return tasks.map((task, index) => `${index + 1}. ${taskId(task)} \xB7 ${taskStatus(task)} \xB7 ${taskTitle(task)}`);
142
+ }
143
+ async function promptForTaskSelection(question) {
144
+ const rl = createPromptInterface({ input: process.stdin, output: process.stdout });
145
+ try {
146
+ return await rl.question(question);
147
+ } finally {
148
+ rl.close();
149
+ }
150
+ }
151
+ export {
152
+ renderTaskPickerRows,
153
+ renderOperatorSnapshot,
154
+ promptForTaskSelection,
155
+ createPiRunStreamRenderer,
156
+ createOperatorSurface
157
+ };
@@ -1,9 +1,5 @@
1
1
  // @bun
2
- // packages/cli/src/commands/_operator-view.ts
3
- import { createInterface } from "readline";
4
-
5
2
  // packages/cli/src/commands/_server-client.ts
6
- import { spawnSync } from "child_process";
7
3
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
8
4
  import { resolve as resolve2 } from "path";
9
5
 
@@ -102,7 +98,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
102
98
  }
103
99
 
104
100
  // packages/cli/src/commands/_server-client.ts
105
- var cachedGitHubBearerToken;
101
+ var scopedGitHubBearerTokens = new Map;
106
102
  function cleanToken(value) {
107
103
  const trimmed = value?.trim();
108
104
  return trimmed ? trimmed : null;
@@ -119,25 +115,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
119
115
  }
120
116
  }
121
117
  function readGitHubBearerTokenForRemote(projectRoot) {
122
- if (cachedGitHubBearerToken !== undefined)
123
- return cachedGitHubBearerToken;
118
+ const scopedKey = resolve2(projectRoot);
119
+ if (scopedGitHubBearerTokens.has(scopedKey))
120
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
124
121
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
125
- if (privateSession) {
126
- cachedGitHubBearerToken = privateSession;
127
- return cachedGitHubBearerToken;
128
- }
129
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
130
- if (envToken) {
131
- cachedGitHubBearerToken = envToken;
132
- return cachedGitHubBearerToken;
133
- }
134
- const result = spawnSync("gh", ["auth", "token"], {
135
- encoding: "utf8",
136
- timeout: 5000,
137
- stdio: ["ignore", "pipe", "ignore"]
138
- });
139
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
140
- return cachedGitHubBearerToken;
122
+ if (privateSession)
123
+ return privateSession;
124
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
141
125
  }
142
126
  async function ensureServerForCli(projectRoot) {
143
127
  try {
@@ -218,6 +202,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
218
202
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
219
203
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
220
204
  }
205
+ async function getRunTimelineViaServer(context, runId, options = {}) {
206
+ const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
207
+ if (options.limit !== undefined)
208
+ url.searchParams.set("limit", String(options.limit));
209
+ if (options.cursor)
210
+ url.searchParams.set("cursor", options.cursor);
211
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
212
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
213
+ }
221
214
  async function stopRunViaServer(context, runId) {
222
215
  const payload = await requestServerJson(context, "/api/runs/stop", {
223
216
  method: "POST",
@@ -235,8 +228,8 @@ async function steerRunViaServer(context, runId, message) {
235
228
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
236
229
  }
237
230
 
238
- // packages/cli/src/commands/_operator-view.ts
239
- var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
231
+ // packages/cli/src/commands/_operator-surface.ts
232
+ import { createInterface } from "readline";
240
233
  var CANONICAL_STAGES = [
241
234
  "Connect",
242
235
  "GitHub/task sync",
@@ -251,18 +244,121 @@ var CANONICAL_STAGES = [
251
244
  "Merge",
252
245
  "Complete"
253
246
  ];
247
+ function logDetail(log) {
248
+ return typeof log.detail === "string" ? log.detail.trim() : "";
249
+ }
250
+ function entryId(entry, fallback) {
251
+ return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
252
+ }
254
253
  function renderOperatorSnapshot(snapshot) {
255
254
  const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
256
255
  const runId = String(run.runId ?? run.id ?? "run");
257
256
  const status = String(run.status ?? "unknown");
258
257
  const logs = snapshot.logs ?? [];
258
+ const latestByStage = new Map;
259
+ for (const log of logs) {
260
+ const title = String(log.title ?? "").toLowerCase();
261
+ const stageName = String(log.stage ?? "").toLowerCase();
262
+ const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
263
+ if (stage)
264
+ latestByStage.set(stage, log);
265
+ }
259
266
  const stageLines = CANONICAL_STAGES.flatMap((stage) => {
260
- const match = logs.find((log) => String(log.title ?? "").toLowerCase() === stage.toLowerCase() || String(log.stage ?? "").toLowerCase() === stage.toLowerCase());
261
- return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
267
+ const match = latestByStage.get(stage);
268
+ return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
262
269
  });
263
270
  return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
264
271
  `);
265
272
  }
273
+ function createPiRunStreamRenderer(output = process.stdout) {
274
+ let lastSnapshot = "";
275
+ const assistantTextById = new Map;
276
+ const seenTimeline = new Set;
277
+ const seenLogs = new Set;
278
+ const writeLine = (line) => output.write(`${line}
279
+ `);
280
+ return {
281
+ renderSnapshot(snapshot) {
282
+ const rendered = renderOperatorSnapshot(snapshot);
283
+ if (rendered && rendered !== lastSnapshot) {
284
+ writeLine(rendered);
285
+ lastSnapshot = rendered;
286
+ }
287
+ },
288
+ renderTimeline(entries) {
289
+ for (const [index, entry] of entries.entries()) {
290
+ const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
291
+ if (entry.type === "assistant_message" && typeof entry.text === "string") {
292
+ const text = entry.text;
293
+ const previousText = assistantTextById.get(id) ?? "";
294
+ if (text.startsWith(previousText)) {
295
+ const delta = text.slice(previousText.length);
296
+ if (delta)
297
+ output.write(delta);
298
+ } else if (text.trim() && text !== previousText) {
299
+ writeLine(`
300
+ [Pi assistant]`);
301
+ output.write(text);
302
+ }
303
+ assistantTextById.set(id, text);
304
+ continue;
305
+ }
306
+ if (seenTimeline.has(id))
307
+ continue;
308
+ seenTimeline.add(id);
309
+ if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
310
+ writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
311
+ continue;
312
+ }
313
+ if (entry.type === "timeline_warning") {
314
+ writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
315
+ }
316
+ }
317
+ },
318
+ renderLogs(entries) {
319
+ for (const [index, entry] of entries.entries()) {
320
+ const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
321
+ if (seenLogs.has(id))
322
+ continue;
323
+ seenLogs.add(id);
324
+ const title = String(entry.title ?? "");
325
+ if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
326
+ continue;
327
+ const detail = logDetail(entry);
328
+ if (!detail)
329
+ continue;
330
+ writeLine(`[${title || "Rig log"}] ${detail}`);
331
+ }
332
+ }
333
+ };
334
+ }
335
+ function createOperatorSurface(options = {}) {
336
+ const input = options.input ?? process.stdin;
337
+ const output = options.output ?? process.stdout;
338
+ const errorOutput = options.errorOutput ?? process.stderr;
339
+ const renderer = createPiRunStreamRenderer(output);
340
+ const writeLine = (line) => output.write(`${line}
341
+ `);
342
+ return {
343
+ mode: "pi-compatible-text",
344
+ ...renderer,
345
+ info: writeLine,
346
+ error: (message) => errorOutput.write(`${message}
347
+ `),
348
+ attachCommandInput(handler) {
349
+ if (options.interactive === false || !input.isTTY)
350
+ return null;
351
+ const rl = createInterface({ input, output: process.stdout, terminal: false });
352
+ rl.on("line", (line) => {
353
+ Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
354
+ });
355
+ return { close: () => rl.close() };
356
+ }
357
+ };
358
+ }
359
+
360
+ // packages/cli/src/commands/_operator-view.ts
361
+ var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
266
362
  function runStatusFromPayload(payload) {
267
363
  const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
268
364
  return String(run.status ?? "unknown").toLowerCase();
@@ -284,11 +380,22 @@ async function applyOperatorCommand(context, input, deps = {}) {
284
380
  await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
285
381
  return { action: "continue", message: "Steering message queued." };
286
382
  }
287
- async function readOperatorSnapshot(context, runId) {
383
+ async function readOperatorSnapshot(context, runId, options = {}) {
288
384
  const run = await getRunDetailsViaServer(context, runId);
289
385
  const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
290
- const entries = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
291
- return { run, logs: entries, rendered: renderOperatorSnapshot({ run, logs: entries }) };
386
+ const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
387
+ entries: [{
388
+ id: `timeline-unavailable:${runId}`,
389
+ type: "timeline_warning",
390
+ detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
391
+ createdAt: new Date().toISOString()
392
+ }],
393
+ nextCursor: options.timelineCursor ?? null
394
+ }));
395
+ const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
396
+ const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
397
+ const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
398
+ return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
292
399
  }
293
400
  async function attachRunOperatorView(context, input) {
294
401
  let steered = false;
@@ -296,45 +403,47 @@ async function attachRunOperatorView(context, input) {
296
403
  await steerRunViaServer(context, input.runId, input.message.trim());
297
404
  steered = true;
298
405
  }
406
+ const surface = createOperatorSurface({ interactive: input.interactive !== false });
299
407
  let snapshot = await readOperatorSnapshot(context, input.runId);
300
408
  if (context.outputMode === "text") {
301
- console.log(snapshot.rendered);
409
+ surface.renderSnapshot(snapshot);
410
+ surface.renderTimeline(snapshot.timeline);
411
+ surface.renderLogs(snapshot.logs);
302
412
  if (steered)
303
- console.log("Steering message queued.");
413
+ surface.info("Steering message queued.");
304
414
  }
305
415
  let detached = false;
306
- let rl = null;
416
+ let commandInput = null;
307
417
  if (input.follow && !input.once && context.outputMode === "text") {
308
418
  if (input.interactive !== false && process.stdin.isTTY) {
309
- console.log("Controls: /user <message>, /stop, /detach");
310
- rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
311
- rl.on("line", (line) => {
312
- applyOperatorCommand(context, { runId: input.runId, line }).then((result) => {
313
- if (result.message)
314
- console.log(result.message);
315
- if (result.action === "detach" || result.action === "stopped") {
316
- detached = true;
317
- rl?.close();
318
- }
319
- }).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
419
+ surface.info("Controls: /user <message>, /stop, /detach");
420
+ commandInput = surface.attachCommandInput(async (line) => {
421
+ const result = await applyOperatorCommand(context, { runId: input.runId, line });
422
+ if (result.message)
423
+ surface.info(result.message);
424
+ if (result.action === "detach" || result.action === "stopped") {
425
+ detached = true;
426
+ commandInput?.close();
427
+ }
320
428
  });
321
429
  }
322
- let lastRendered = snapshot.rendered;
323
430
  const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
431
+ let timelineCursor = snapshot.timelineCursor;
324
432
  while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
325
433
  await Bun.sleep(pollMs);
326
- snapshot = await readOperatorSnapshot(context, input.runId);
327
- if (snapshot.rendered !== lastRendered) {
328
- console.log(snapshot.rendered);
329
- lastRendered = snapshot.rendered;
330
- }
434
+ snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
435
+ timelineCursor = snapshot.timelineCursor;
436
+ surface.renderSnapshot(snapshot);
437
+ surface.renderTimeline(snapshot.timeline);
438
+ surface.renderLogs(snapshot.logs);
331
439
  }
332
- rl?.close();
440
+ commandInput?.close();
333
441
  }
334
442
  return { ...snapshot, steered, detached };
335
443
  }
336
444
  export {
337
445
  renderOperatorSnapshot,
446
+ createPiRunStreamRenderer,
338
447
  attachRunOperatorView,
339
448
  applyOperatorCommand
340
449
  };
@@ -94,11 +94,10 @@ function resolveSelectedConnection(projectRoot, options = {}) {
94
94
  }
95
95
 
96
96
  // packages/cli/src/commands/_server-client.ts
97
- import { spawnSync } from "child_process";
98
97
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
99
98
  import { resolve as resolve2 } from "path";
100
99
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
101
- var cachedGitHubBearerToken;
100
+ var scopedGitHubBearerTokens = new Map;
102
101
  function cleanToken(value) {
103
102
  const trimmed = value?.trim();
104
103
  return trimmed ? trimmed : null;
@@ -115,25 +114,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
115
114
  }
116
115
  }
117
116
  function readGitHubBearerTokenForRemote(projectRoot) {
118
- if (cachedGitHubBearerToken !== undefined)
119
- return cachedGitHubBearerToken;
117
+ const scopedKey = resolve2(projectRoot);
118
+ if (scopedGitHubBearerTokens.has(scopedKey))
119
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
120
120
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
121
- if (privateSession) {
122
- cachedGitHubBearerToken = privateSession;
123
- return cachedGitHubBearerToken;
124
- }
125
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
126
- if (envToken) {
127
- cachedGitHubBearerToken = envToken;
128
- return cachedGitHubBearerToken;
129
- }
130
- const result = spawnSync("gh", ["auth", "token"], {
131
- encoding: "utf8",
132
- timeout: 5000,
133
- stdio: ["ignore", "pipe", "ignore"]
134
- });
135
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
136
- return cachedGitHubBearerToken;
121
+ if (privateSession)
122
+ return privateSession;
123
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
137
124
  }
138
125
  async function ensureServerForCli(projectRoot) {
139
126
  try {
@@ -329,6 +316,9 @@ function permissionAllowsPr(payload) {
329
316
  }
330
317
  return null;
331
318
  }
319
+ function isNotFoundError(error) {
320
+ return /\b(404|not found)\b/i.test(message(error));
321
+ }
332
322
  function projectCheckoutReady(payload) {
333
323
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
334
324
  return null;
@@ -361,19 +351,33 @@ async function runFastTaskRunPreflight(context, options = {}) {
361
351
  const checks = [];
362
352
  const request = options.requestJson ?? ((pathname, init) => requestServerJson(context, pathname, init));
363
353
  const taskId = options.taskId?.trim() || null;
354
+ const requiresCurrentRunApi = Boolean(taskId);
355
+ const selectedServer = options.requestJson ? null : await ensureServerForCli(context.projectRoot).catch(() => null);
356
+ const allowLocalLegacyTaskRunCompatibility = selectedServer?.connectionKind === "local";
357
+ let legacyServerCompatibility = false;
364
358
  try {
365
359
  await request("/api/server/status");
366
360
  checks.push(preflightCheck("server", "Rig server reachable", "pass"));
367
361
  } catch (error) {
368
- checks.push(preflightCheck("server", "Rig server reachable", "fail", message(error), "Start or select a reachable Rig server."));
362
+ if (isNotFoundError(error)) {
363
+ try {
364
+ await request("/health");
365
+ legacyServerCompatibility = !requiresCurrentRunApi || allowLocalLegacyTaskRunCompatibility;
366
+ checks.push(requiresCurrentRunApi && !allowLocalLegacyTaskRunCompatibility ? preflightCheck("server", "Rig server reachable", "fail", "legacy /health endpoint only; current task-run APIs are required", "Upgrade/select the Rig server before launching a task run.") : preflightCheck("server", "Rig server reachable", "pass", allowLocalLegacyTaskRunCompatibility ? "local legacy /health endpoint; submit endpoint will be authoritative" : "legacy /health endpoint"));
367
+ } catch (healthError) {
368
+ checks.push(preflightCheck("server", "Rig server reachable", "fail", message(healthError), "Start or select a reachable Rig server."));
369
+ }
370
+ } else {
371
+ checks.push(preflightCheck("server", "Rig server reachable", "fail", message(error), "Start or select a reachable Rig server."));
372
+ }
369
373
  }
370
374
  const repo = readRepoConnection(context.projectRoot);
371
- checks.push(repo ? preflightCheck("project-link", "project linked to Rig connection", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to record the GitHub repo slug.") : preflightCheck("project-link", "project linked to Rig connection", "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig connect use <alias|local>`."));
375
+ checks.push(repo ? preflightCheck("project-link", "project linked to Rig connection", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to record the GitHub repo slug.") : preflightCheck("project-link", "project linked to Rig connection", legacyServerCompatibility ? "warn" : "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig connect use <alias|local>`."));
372
376
  try {
373
377
  const auth = await request("/api/github/auth/status");
374
- checks.push(isAuthenticated(auth) ? preflightCheck("github-auth", "GitHub auth valid", "pass") : preflightCheck("github-auth", "GitHub auth valid", "fail", "not authenticated", "Run `rig github auth import-gh` or `rig github auth token --token <token>`."));
378
+ checks.push(isAuthenticated(auth) ? preflightCheck("github-auth", "GitHub auth valid", "pass") : preflightCheck("github-auth", "GitHub auth valid", legacyServerCompatibility ? "warn" : "fail", "not authenticated", "Run `rig github auth import-gh` or `rig github auth token --token <token>`."));
375
379
  } catch (error) {
376
- checks.push(preflightCheck("github-auth", "GitHub auth valid", "fail", message(error), "Fix GitHub auth on the selected Rig server."));
380
+ checks.push(preflightCheck("github-auth", "GitHub auth valid", legacyServerCompatibility ? "warn" : "fail", message(error), "Fix GitHub auth on the selected Rig server."));
377
381
  }
378
382
  try {
379
383
  const projection = await request("/api/workspace/task-projection");
@@ -401,9 +405,9 @@ async function runFastTaskRunPreflight(context, options = {}) {
401
405
  try {
402
406
  const tasks = await request(`/api/workspace/tasks?limit=200&refresh=1`);
403
407
  const found = Array.isArray(tasks) && tasks.some((task) => taskMatchesId(task, taskId));
404
- checks.push(found ? preflightCheck("issue", "task/issue accessible", "pass", taskId) : preflightCheck("issue", "task/issue accessible", "fail", taskId, "Confirm the issue exists and matches the configured task filters."));
408
+ checks.push(found ? preflightCheck("issue", "task/issue accessible", "pass", taskId) : preflightCheck("issue", "task/issue accessible", legacyServerCompatibility ? "warn" : "fail", taskId, "Confirm the issue exists and matches the configured task filters."));
405
409
  } catch (error) {
406
- checks.push(preflightCheck("issue", "task/issue accessible", "fail", message(error), "Fix the task source before launching a run."));
410
+ checks.push(preflightCheck("issue", "task/issue accessible", legacyServerCompatibility ? "warn" : "fail", message(error), "Fix the task source before launching a run."));
407
411
  }
408
412
  try {
409
413
  const runs = await request("/api/runs?limit=200");