@h-rig/cli 0.0.6-alpha.63 → 0.0.6-alpha.65

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.
Files changed (46) hide show
  1. package/dist/bin/rig.js +1349 -1599
  2. package/dist/src/commands/_connection-state.js +14 -5
  3. package/dist/src/commands/_doctor-checks.js +71 -11
  4. package/dist/src/commands/_help-catalog.js +99 -60
  5. package/dist/src/commands/_json-output.js +56 -0
  6. package/dist/src/commands/_operator-view.js +97 -784
  7. package/dist/src/commands/_parsers.js +18 -9
  8. package/dist/src/commands/_pi-frontend.js +96 -788
  9. package/dist/src/commands/_policy.js +12 -3
  10. package/dist/src/commands/_preflight.js +73 -13
  11. package/dist/src/commands/_run-driver-helpers.js +31 -5
  12. package/dist/src/commands/_run-replay.js +2 -2
  13. package/dist/src/commands/_server-client.js +76 -16
  14. package/dist/src/commands/_snapshot-upload.js +70 -10
  15. package/dist/src/commands/agent.js +21 -11
  16. package/dist/src/commands/browser.js +24 -15
  17. package/dist/src/commands/connect.js +22 -17
  18. package/dist/src/commands/dist.js +15 -6
  19. package/dist/src/commands/doctor.js +70 -10
  20. package/dist/src/commands/github.js +79 -19
  21. package/dist/src/commands/inbox.js +215 -131
  22. package/dist/src/commands/init.js +202 -35
  23. package/dist/src/commands/inspect.js +77 -17
  24. package/dist/src/commands/inspector.js +18 -9
  25. package/dist/src/commands/pi.js +18 -9
  26. package/dist/src/commands/plugin.js +19 -10
  27. package/dist/src/commands/profile-and-review.js +22 -13
  28. package/dist/src/commands/queue.js +19 -9
  29. package/dist/src/commands/remote.js +25 -16
  30. package/dist/src/commands/repo-git-harness.js +18 -9
  31. package/dist/src/commands/run.js +158 -805
  32. package/dist/src/commands/server.js +85 -25
  33. package/dist/src/commands/setup.js +73 -13
  34. package/dist/src/commands/stats.js +681 -0
  35. package/dist/src/commands/task-report-bug.js +24 -15
  36. package/dist/src/commands/task-run-driver.js +121 -49
  37. package/dist/src/commands/task.js +254 -893
  38. package/dist/src/commands/test.js +12 -3
  39. package/dist/src/commands/workspace.js +14 -5
  40. package/dist/src/commands.js +1339 -1650
  41. package/dist/src/index.js +1349 -1599
  42. package/dist/src/launcher.js +72 -10
  43. package/dist/src/runner.js +14 -5
  44. package/package.json +10 -7
  45. package/dist/src/commands/_pi-remote-session.js +0 -771
  46. package/dist/src/commands/_pi-worker-bridge-extension.js +0 -834
@@ -5,10 +5,19 @@ import { resolve } from "path";
5
5
 
6
6
  // packages/cli/src/runner.ts
7
7
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
8
- import { CliError } from "@rig/runtime/control-plane/errors";
8
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
9
9
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
10
10
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
11
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
11
+
12
+ class CliError extends RuntimeCliError {
13
+ hint;
14
+ constructor(message, exitCode = 1, options = {}) {
15
+ super(message, exitCode);
16
+ if (options.hint?.trim()) {
17
+ this.hint = options.hint.trim();
18
+ }
19
+ }
20
+ }
12
21
  function formatCommand(parts) {
13
22
  return parts.map((part) => /[^a-zA-Z0-9_./:-]/.test(part) ? JSON.stringify(part) : part).join(" ");
14
23
  }
@@ -67,7 +76,7 @@ async function enforceNativeCommandPolicy(context, args, options) {
67
76
  appendControlledBashAudit(context, mode, command, args, matchedRuleIds, action === "block" ? "blocked" : action === "warn" ? "warn" : undefined);
68
77
  }
69
78
  if (action === "block") {
70
- throw new CliError2(`Policy blocked command: ${command}`, 126);
79
+ throw new CliError(`Policy blocked command: ${command}`, 126, { hint: "Review rig/policy/policy.json, or re-run with `--policy-mode observe` to log instead of block." });
71
80
  }
72
81
  }
73
82
  export {
@@ -1,10 +1,19 @@
1
1
  // @bun
2
2
  // packages/cli/src/runner.ts
3
3
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
4
- import { CliError } from "@rig/runtime/control-plane/errors";
4
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
5
5
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
6
6
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
7
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
7
+
8
+ class CliError extends RuntimeCliError {
9
+ hint;
10
+ constructor(message, exitCode = 1, options = {}) {
11
+ super(message, exitCode);
12
+ if (options.hint?.trim()) {
13
+ this.hint = options.hint.trim();
14
+ }
15
+ }
16
+ }
8
17
 
9
18
  // packages/cli/src/commands/_preflight.ts
10
19
  import { ensureProjectMainFreshBeforeRun } from "@rig/runtime/control-plane/project-main-pre-run-sync";
@@ -31,7 +40,7 @@ function readJsonFile(path) {
31
40
  try {
32
41
  return JSON.parse(readFileSync(path, "utf8"));
33
42
  } catch (error) {
34
- throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
43
+ 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>`." });
35
44
  }
36
45
  }
37
46
  function writeJsonFile(path, value) {
@@ -95,7 +104,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
95
104
  const global = readGlobalConnections(options);
96
105
  const connection = global.connections[repo.selected];
97
106
  if (!connection) {
98
- throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
107
+ throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
99
108
  }
100
109
  return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
101
110
  }
@@ -171,7 +180,7 @@ async function ensureServerForCli(projectRoot) {
171
180
  };
172
181
  } catch (error) {
173
182
  if (error instanceof Error) {
174
- throw new CliError2(error.message, 1);
183
+ throw new CliError(error.message, 1);
175
184
  }
176
185
  throw error;
177
186
  }
@@ -221,15 +230,64 @@ function diagnosticMessage(payload) {
221
230
  });
222
231
  return messages.length > 0 ? messages.join("; ") : null;
223
232
  }
233
+ var serverReachabilityCache = new Map;
234
+ async function probeServerReachability(baseUrl, authToken) {
235
+ try {
236
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
237
+ headers: mergeHeaders(undefined, authToken),
238
+ signal: AbortSignal.timeout(1500)
239
+ });
240
+ return response.ok;
241
+ } catch {
242
+ return false;
243
+ }
244
+ }
245
+ function cachedServerReachability(projectRoot, baseUrl, authToken) {
246
+ const key = resolve2(projectRoot);
247
+ const cached = serverReachabilityCache.get(key);
248
+ if (cached)
249
+ return cached;
250
+ const probe = probeServerReachability(baseUrl, authToken);
251
+ serverReachabilityCache.set(key, probe);
252
+ return probe;
253
+ }
254
+ function describeSelectedServer(projectRoot, server) {
255
+ try {
256
+ const selected = resolveSelectedConnection(projectRoot);
257
+ if (selected) {
258
+ return {
259
+ alias: selected.alias,
260
+ target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
261
+ };
262
+ }
263
+ } catch {}
264
+ return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
265
+ }
266
+ async function buildServerFailureContext(projectRoot, server) {
267
+ const { alias, target } = describeSelectedServer(projectRoot, server);
268
+ const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
269
+ const reachability = reachable ? "server is reachable" : "server is unreachable";
270
+ return {
271
+ contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
272
+ hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
273
+ };
274
+ }
224
275
  async function requestServerJson(context, pathname, init = {}) {
225
276
  const server = await ensureServerForCli(context.projectRoot);
226
277
  const headers = mergeHeaders(init.headers, server.authToken);
227
278
  if (server.serverProjectRoot)
228
279
  headers.set("x-rig-project-root", server.serverProjectRoot);
229
- const response = await fetch(`${server.baseUrl}${pathname}`, {
230
- ...init,
231
- headers
232
- });
280
+ let response;
281
+ try {
282
+ response = await fetch(`${server.baseUrl}${pathname}`, {
283
+ ...init,
284
+ headers
285
+ });
286
+ } catch (error) {
287
+ const failure = await buildServerFailureContext(context.projectRoot, server);
288
+ throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
289
+ ${failure.contextLine}`, 1, { hint: failure.hint });
290
+ }
233
291
  const text = await response.text();
234
292
  const payload = text.trim().length > 0 ? (() => {
235
293
  try {
@@ -241,7 +299,9 @@ async function requestServerJson(context, pathname, init = {}) {
241
299
  if (!response.ok) {
242
300
  const diagnostics = diagnosticMessage(payload);
243
301
  const detail = diagnostics ?? (text || response.statusText);
244
- throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
302
+ const failure = await buildServerFailureContext(context.projectRoot, server);
303
+ throw new CliError(`Rig server request failed (${response.status}): ${detail}
304
+ ${failure.contextLine}`, 1, { hint: failure.hint });
245
305
  }
246
306
  return payload;
247
307
  }
@@ -411,9 +471,9 @@ async function runFastTaskRunPreflight(context, options = {}) {
411
471
  if (failures.length > 0) {
412
472
  const summary = failures.map((check) => `${check.label}${check.detail ? `: ${check.detail}` : ""}`).join("; ");
413
473
  if (failures.some((check) => check.id === "duplicate-active-run") && taskId) {
414
- throw new CliError2(`Task ${taskId} already has an active Rig run. ${summary}`, 1);
474
+ throw new CliError(`Task ${taskId} already has an active Rig run. ${summary}`, 1, { hint: `Attach to it with \`rig run attach <run-id> --follow\`, or stop it first with \`rig run stop <run-id>\`.` });
415
475
  }
416
- throw new CliError2(`Task run preflight failed: ${summary}`, 1);
476
+ throw new CliError(`Task run preflight failed: ${summary}`, 1, { hint: "Run `rig doctor` to diagnose, then retry `rig task run`." });
417
477
  }
418
478
  return { ok: true, checks };
419
479
  }
@@ -430,7 +490,7 @@ async function runProjectMainSyncPreflight(context, options) {
430
490
  runBootstrap: async () => {
431
491
  const bootstrap = await context.runCommand(["bun", "run", "bootstrap"]);
432
492
  if (bootstrap.exitCode !== 0) {
433
- throw new CliError2(bootstrap.stderr || bootstrap.stdout || "bun run bootstrap failed during project pre-run sync", bootstrap.exitCode || 1);
493
+ throw new CliError(bootstrap.stderr || bootstrap.stdout || "bun run bootstrap failed during project pre-run sync", bootstrap.exitCode || 1);
434
494
  }
435
495
  }
436
496
  });
@@ -5,19 +5,29 @@ import { resolve as resolve3 } from "path";
5
5
 
6
6
  // packages/cli/src/runner.ts
7
7
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
8
- import { CliError } from "@rig/runtime/control-plane/errors";
8
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
9
9
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
10
10
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
11
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
11
+
12
+ class CliError extends RuntimeCliError {
13
+ hint;
14
+ constructor(message, exitCode = 1, options = {}) {
15
+ super(message, exitCode);
16
+ if (options.hint?.trim()) {
17
+ this.hint = options.hint.trim();
18
+ }
19
+ }
20
+ }
12
21
 
13
22
  // packages/cli/src/commands/_run-driver-helpers.ts
14
23
  import {
15
- appendRunLifecycleEvent,
24
+ appendRunJournalEvent,
16
25
  appendRunLogEntry,
17
26
  appendRunTimelineEntry,
18
27
  patchAuthorityRunRecord,
19
28
  readAuthorityRun as readAuthorityRun2,
20
29
  resolveAuthorityRunDir,
30
+ setAuthorityRunStatus,
21
31
  writeJsonFile
22
32
  } from "@rig/runtime/control-plane/authority-files";
23
33
  import { taskLookup } from "@rig/runtime/control-plane/native/task-ops";
@@ -67,7 +77,17 @@ function readLatestBeadRecord(projectRoot, taskId) {
67
77
  function patchAuthorityRun(projectRoot, runId, patch) {
68
78
  const next = patchAuthorityRunRecord(projectRoot, runId, patch);
69
79
  if (!next) {
70
- throw new CliError2(`Run not found: ${runId}`, 2);
80
+ throw new CliError(`Run not found: ${runId}`, 2, { hint: "Run `rig run list` to see recorded runs." });
81
+ }
82
+ return next;
83
+ }
84
+ function setRunStatusOrFail(projectRoot, runId, to, options = {}) {
85
+ const next = setAuthorityRunStatus(projectRoot, runId, to, {
86
+ ...options,
87
+ actor: { kind: "agent" }
88
+ });
89
+ if (!next) {
90
+ throw new CliError(`Run not found: ${runId}`, 2, { hint: "Run `rig run list` to see recorded runs." });
71
91
  }
72
92
  return next;
73
93
  }
@@ -172,7 +192,12 @@ function emitServerRunEvent(event) {
172
192
  console.log(`__RIG_RUN_EVENT__${JSON.stringify({ ...event, at: new Date().toISOString() })}`);
173
193
  if (runEventSinkProjectRoot) {
174
194
  try {
175
- appendRunLifecycleEvent(runEventSinkProjectRoot, event.runId, { ...event });
195
+ if (event.type === "status" || event.type === "completed" || event.type === "failed") {
196
+ appendRunJournalEvent(runEventSinkProjectRoot, event.runId, {
197
+ type: "timeline-entry",
198
+ payload: { kind: "driver-event", ...event }
199
+ });
200
+ }
176
201
  } catch {}
177
202
  ringServerRunDoorbell(event.runId);
178
203
  }
@@ -306,6 +331,7 @@ function renderSourceScopeValidation(task, validation) {
306
331
  export {
307
332
  touchAuthorityRun,
308
333
  startRunAction,
334
+ setRunStatusOrFail,
309
335
  patchAuthorityRun,
310
336
  emitServerRunEvent,
311
337
  configureRunEventSink,
@@ -5,7 +5,7 @@ import { join, resolve } from "path";
5
5
  import {
6
6
  readAuthorityRun,
7
7
  readJsonlFile,
8
- runLifecycleLogPath
8
+ runJournalPath
9
9
  } from "@rig/runtime/control-plane/authority-files";
10
10
  function asRecord(value) {
11
11
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -103,7 +103,7 @@ function resolveRunSessionFile(record) {
103
103
  }
104
104
  }
105
105
  function buildRunReplay(projectRoot, runId, options = {}) {
106
- const logPath = runLifecycleLogPath(projectRoot, runId);
106
+ const logPath = runJournalPath(projectRoot, runId);
107
107
  const record = readAuthorityRun(projectRoot, runId);
108
108
  const lifecycleEntries = readJsonlFile(logPath).map((entry) => asRecord(entry)).filter((entry) => entry !== null);
109
109
  const merged = lifecycleEntries.map((entry) => ({
@@ -5,10 +5,19 @@ import { resolve as resolve2 } from "path";
5
5
 
6
6
  // packages/cli/src/runner.ts
7
7
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
8
- import { CliError } from "@rig/runtime/control-plane/errors";
8
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
9
9
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
10
10
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
11
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
11
+
12
+ class CliError extends RuntimeCliError {
13
+ hint;
14
+ constructor(message, exitCode = 1, options = {}) {
15
+ super(message, exitCode);
16
+ if (options.hint?.trim()) {
17
+ this.hint = options.hint.trim();
18
+ }
19
+ }
20
+ }
12
21
 
13
22
  // packages/cli/src/commands/_server-client.ts
14
23
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
@@ -35,7 +44,7 @@ function readJsonFile(path) {
35
44
  try {
36
45
  return JSON.parse(readFileSync(path, "utf8"));
37
46
  } catch (error) {
38
- throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
47
+ 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>`." });
39
48
  }
40
49
  }
41
50
  function writeJsonFile(path, value) {
@@ -99,7 +108,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
99
108
  const global = readGlobalConnections(options);
100
109
  const connection = global.connections[repo.selected];
101
110
  if (!connection) {
102
- throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
111
+ throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
103
112
  }
104
113
  return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
105
114
  }
@@ -176,7 +185,7 @@ async function ensureServerForCli(projectRoot) {
176
185
  };
177
186
  } catch (error) {
178
187
  if (error instanceof Error) {
179
- throw new CliError2(error.message, 1);
188
+ throw new CliError(error.message, 1);
180
189
  }
181
190
  throw error;
182
191
  }
@@ -236,15 +245,64 @@ function diagnosticMessage(payload) {
236
245
  });
237
246
  return messages.length > 0 ? messages.join("; ") : null;
238
247
  }
248
+ var serverReachabilityCache = new Map;
249
+ async function probeServerReachability(baseUrl, authToken) {
250
+ try {
251
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
252
+ headers: mergeHeaders(undefined, authToken),
253
+ signal: AbortSignal.timeout(1500)
254
+ });
255
+ return response.ok;
256
+ } catch {
257
+ return false;
258
+ }
259
+ }
260
+ function cachedServerReachability(projectRoot, baseUrl, authToken) {
261
+ const key = resolve2(projectRoot);
262
+ const cached = serverReachabilityCache.get(key);
263
+ if (cached)
264
+ return cached;
265
+ const probe = probeServerReachability(baseUrl, authToken);
266
+ serverReachabilityCache.set(key, probe);
267
+ return probe;
268
+ }
269
+ function describeSelectedServer(projectRoot, server) {
270
+ try {
271
+ const selected = resolveSelectedConnection(projectRoot);
272
+ if (selected) {
273
+ return {
274
+ alias: selected.alias,
275
+ target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
276
+ };
277
+ }
278
+ } catch {}
279
+ return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
280
+ }
281
+ async function buildServerFailureContext(projectRoot, server) {
282
+ const { alias, target } = describeSelectedServer(projectRoot, server);
283
+ const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
284
+ const reachability = reachable ? "server is reachable" : "server is unreachable";
285
+ return {
286
+ contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
287
+ hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
288
+ };
289
+ }
239
290
  async function requestServerJson(context, pathname, init = {}) {
240
291
  const server = await ensureServerForCli(context.projectRoot);
241
292
  const headers = mergeHeaders(init.headers, server.authToken);
242
293
  if (server.serverProjectRoot)
243
294
  headers.set("x-rig-project-root", server.serverProjectRoot);
244
- const response = await fetch(`${server.baseUrl}${pathname}`, {
245
- ...init,
246
- headers
247
- });
295
+ let response;
296
+ try {
297
+ response = await fetch(`${server.baseUrl}${pathname}`, {
298
+ ...init,
299
+ headers
300
+ });
301
+ } catch (error) {
302
+ const failure = await buildServerFailureContext(context.projectRoot, server);
303
+ throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
304
+ ${failure.contextLine}`, 1, { hint: failure.hint });
305
+ }
248
306
  const text = await response.text();
249
307
  const payload = text.trim().length > 0 ? (() => {
250
308
  try {
@@ -256,7 +314,9 @@ async function requestServerJson(context, pathname, init = {}) {
256
314
  if (!response.ok) {
257
315
  const diagnostics = diagnosticMessage(payload);
258
316
  const detail = diagnostics ?? (text || response.statusText);
259
- throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
317
+ const failure = await buildServerFailureContext(context.projectRoot, server);
318
+ throw new CliError(`Rig server request failed (${response.status}): ${detail}
319
+ ${failure.contextLine}`, 1, { hint: failure.hint });
260
320
  }
261
321
  return payload;
262
322
  }
@@ -265,7 +325,7 @@ async function listWorkspaceTasksViaServer(context, filters = {}) {
265
325
  appendTaskFilterParams(url, filters);
266
326
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
267
327
  if (!Array.isArray(payload)) {
268
- throw new CliError2("Rig server returned an invalid task list payload.", 1);
328
+ throw new CliError("Rig server returned an invalid task list payload.", 1, { hint: "Check the selected server with `rig server status`; mixed CLI/server versions can mismatch \u2014 try `rig doctor`." });
269
329
  }
270
330
  return payload.flatMap((entry) => entry && typeof entry === "object" && !Array.isArray(entry) ? [entry] : []);
271
331
  }
@@ -281,7 +341,7 @@ async function selectNextWorkspaceTaskViaServer(context, filters = {}) {
281
341
  appendTaskFilterParams(url, filters);
282
342
  const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
283
343
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
284
- throw new CliError2("Rig server returned an invalid next-task payload.", 1);
344
+ throw new CliError("Rig server returned an invalid next-task payload.", 1, { hint: "Check the selected server with `rig server status`; try `rig task list` to see the raw task set." });
285
345
  }
286
346
  const record = payload;
287
347
  const rawTask = record.task;
@@ -348,14 +408,14 @@ async function switchServerProjectRootViaServer(context, projectRoot, options =
348
408
  }
349
409
  }
350
410
  if (!switched) {
351
- throw new CliError2(`Rig server did not accept project-root switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no response")}).`, 1);
411
+ throw new CliError(`Rig server did not accept project-root switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no response")}).`, 1);
352
412
  }
353
413
  const record = switched && typeof switched === "object" && !Array.isArray(switched) ? switched : {};
354
414
  if (record.ok === true) {
355
415
  writeRepoServerProjectRoot(context.projectRoot, projectRoot);
356
416
  return { ok: true, switched: record };
357
417
  }
358
- throw new CliError2(`Rig server rejected project-root scope ${projectRoot}: ${String(record.message ?? record.error ?? "unknown error")}`, 1);
418
+ throw new CliError(`Rig server rejected project-root scope ${projectRoot}: ${String(record.message ?? record.error ?? "unknown error")}`, 1);
359
419
  }
360
420
  async function listRunsViaServer(context, options = {}) {
361
421
  const url = new URL("http://rig.local/api/runs");
@@ -514,11 +574,11 @@ async function submitTaskRunViaServer(context, input) {
514
574
  })
515
575
  });
516
576
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
517
- throw new CliError2("Rig server returned an invalid run submission payload.", 1);
577
+ throw new CliError("Rig server returned an invalid run submission payload.", 1, { hint: "Check `rig server status` and retry; `rig run list` shows whether the run was created anyway." });
518
578
  }
519
579
  const runId = payload.runId;
520
580
  if (typeof runId !== "string" || runId.trim().length === 0) {
521
- throw new CliError2("Rig server returned no runId for the submitted run.", 1);
581
+ throw new CliError("Rig server returned no runId for the submitted run.", 1, { hint: "Check `rig run list` \u2014 the run may still have been created; otherwise retry the submission." });
522
582
  }
523
583
  return { runId };
524
584
  }
@@ -9,10 +9,19 @@ import { resolve as resolve2 } from "path";
9
9
 
10
10
  // packages/cli/src/runner.ts
11
11
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
12
- import { CliError } from "@rig/runtime/control-plane/errors";
12
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
13
13
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
14
14
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
15
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
15
+
16
+ class CliError extends RuntimeCliError {
17
+ hint;
18
+ constructor(message, exitCode = 1, options = {}) {
19
+ super(message, exitCode);
20
+ if (options.hint?.trim()) {
21
+ this.hint = options.hint.trim();
22
+ }
23
+ }
24
+ }
16
25
 
17
26
  // packages/cli/src/commands/_server-client.ts
18
27
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
@@ -39,7 +48,7 @@ function readJsonFile(path) {
39
48
  try {
40
49
  return JSON.parse(readFileSync(path, "utf8"));
41
50
  } catch (error) {
42
- throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
51
+ 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>`." });
43
52
  }
44
53
  }
45
54
  function writeJsonFile(path, value) {
@@ -103,7 +112,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
103
112
  const global = readGlobalConnections(options);
104
113
  const connection = global.connections[repo.selected];
105
114
  if (!connection) {
106
- throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
115
+ throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
107
116
  }
108
117
  return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
109
118
  }
@@ -176,7 +185,7 @@ async function ensureServerForCli(projectRoot) {
176
185
  };
177
186
  } catch (error) {
178
187
  if (error instanceof Error) {
179
- throw new CliError2(error.message, 1);
188
+ throw new CliError(error.message, 1);
180
189
  }
181
190
  throw error;
182
191
  }
@@ -226,15 +235,64 @@ function diagnosticMessage(payload) {
226
235
  });
227
236
  return messages.length > 0 ? messages.join("; ") : null;
228
237
  }
238
+ var serverReachabilityCache = new Map;
239
+ async function probeServerReachability(baseUrl, authToken) {
240
+ try {
241
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
242
+ headers: mergeHeaders(undefined, authToken),
243
+ signal: AbortSignal.timeout(1500)
244
+ });
245
+ return response.ok;
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+ function cachedServerReachability(projectRoot, baseUrl, authToken) {
251
+ const key = resolve2(projectRoot);
252
+ const cached = serverReachabilityCache.get(key);
253
+ if (cached)
254
+ return cached;
255
+ const probe = probeServerReachability(baseUrl, authToken);
256
+ serverReachabilityCache.set(key, probe);
257
+ return probe;
258
+ }
259
+ function describeSelectedServer(projectRoot, server) {
260
+ try {
261
+ const selected = resolveSelectedConnection(projectRoot);
262
+ if (selected) {
263
+ return {
264
+ alias: selected.alias,
265
+ target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
266
+ };
267
+ }
268
+ } catch {}
269
+ return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
270
+ }
271
+ async function buildServerFailureContext(projectRoot, server) {
272
+ const { alias, target } = describeSelectedServer(projectRoot, server);
273
+ const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
274
+ const reachability = reachable ? "server is reachable" : "server is unreachable";
275
+ return {
276
+ contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
277
+ hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
278
+ };
279
+ }
229
280
  async function requestServerJson(context, pathname, init = {}) {
230
281
  const server = await ensureServerForCli(context.projectRoot);
231
282
  const headers = mergeHeaders(init.headers, server.authToken);
232
283
  if (server.serverProjectRoot)
233
284
  headers.set("x-rig-project-root", server.serverProjectRoot);
234
- const response = await fetch(`${server.baseUrl}${pathname}`, {
235
- ...init,
236
- headers
237
- });
285
+ let response;
286
+ try {
287
+ response = await fetch(`${server.baseUrl}${pathname}`, {
288
+ ...init,
289
+ headers
290
+ });
291
+ } catch (error) {
292
+ const failure = await buildServerFailureContext(context.projectRoot, server);
293
+ throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
294
+ ${failure.contextLine}`, 1, { hint: failure.hint });
295
+ }
238
296
  const text = await response.text();
239
297
  const payload = text.trim().length > 0 ? (() => {
240
298
  try {
@@ -246,7 +304,9 @@ async function requestServerJson(context, pathname, init = {}) {
246
304
  if (!response.ok) {
247
305
  const diagnostics = diagnosticMessage(payload);
248
306
  const detail = diagnostics ?? (text || response.statusText);
249
- throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
307
+ const failure = await buildServerFailureContext(context.projectRoot, server);
308
+ throw new CliError(`Rig server request failed (${response.status}): ${detail}
309
+ ${failure.contextLine}`, 1, { hint: failure.hint });
250
310
  }
251
311
  return payload;
252
312
  }