@h-rig/cli 0.0.6-alpha.87 → 0.0.6-alpha.89

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 (78) hide show
  1. package/dist/bin/rig.js +1380 -865
  2. package/dist/src/app/board.js +462 -48
  3. package/dist/src/app-opentui/adapters/common.d.ts +3 -0
  4. package/dist/src/app-opentui/adapters/common.js +4 -0
  5. package/dist/src/app-opentui/adapters/doctor.js +458 -46
  6. package/dist/src/app-opentui/adapters/family.js +701 -151
  7. package/dist/src/app-opentui/adapters/fleet.js +477 -46
  8. package/dist/src/app-opentui/adapters/inbox.js +477 -46
  9. package/dist/src/app-opentui/adapters/init.js +497 -74
  10. package/dist/src/app-opentui/adapters/inspect.js +477 -46
  11. package/dist/src/app-opentui/adapters/pi-attach.d.ts +7 -0
  12. package/dist/src/app-opentui/adapters/pi-attach.js +1004 -519
  13. package/dist/src/app-opentui/adapters/run-detail.js +477 -46
  14. package/dist/src/app-opentui/adapters/server.d.ts +26 -0
  15. package/dist/src/app-opentui/adapters/server.js +676 -59
  16. package/dist/src/app-opentui/adapters/tasks.js +621 -549
  17. package/dist/src/app-opentui/autocomplete.js +4 -2
  18. package/dist/src/app-opentui/bootstrap.js +1376 -861
  19. package/dist/src/app-opentui/command-palette.js +37 -10
  20. package/dist/src/app-opentui/index.js +632 -528
  21. package/dist/src/app-opentui/intent.js +33 -8
  22. package/dist/src/app-opentui/keymap.js +43 -414
  23. package/dist/src/app-opentui/pi-host-child.js +496 -57
  24. package/dist/src/app-opentui/pi-pty-host.d.ts +14 -64
  25. package/dist/src/app-opentui/pi-pty-host.js +3 -397
  26. package/dist/src/app-opentui/react/App.js +144 -469
  27. package/dist/src/app-opentui/react/ChromeHost.js +44 -415
  28. package/dist/src/app-opentui/react/launch.js +659 -552
  29. package/dist/src/app-opentui/react/nav.js +1 -1
  30. package/dist/src/app-opentui/registry.js +1181 -742
  31. package/dist/src/app-opentui/remote-link.d.ts +10 -0
  32. package/dist/src/app-opentui/remote-link.js +47 -0
  33. package/dist/src/app-opentui/render/terminal-handoff.d.ts +16 -0
  34. package/dist/src/app-opentui/render/terminal-handoff.js +14 -0
  35. package/dist/src/app-opentui/runtime.js +632 -528
  36. package/dist/src/app-opentui/scenes/doctor.js +1 -1
  37. package/dist/src/app-opentui/scenes/error.js +50 -4
  38. package/dist/src/app-opentui/scenes/family.js +60 -6
  39. package/dist/src/app-opentui/scenes/fleet.js +65 -13
  40. package/dist/src/app-opentui/scenes/help.js +4 -2
  41. package/dist/src/app-opentui/scenes/init.js +12 -12
  42. package/dist/src/app-opentui/scenes/main.js +7 -7
  43. package/dist/src/app-opentui/scenes/server.js +83 -11
  44. package/dist/src/app-opentui/scenes/tasks.js +79 -16
  45. package/dist/src/app-opentui/state.js +25 -5
  46. package/dist/src/app-opentui/surface-catalog.js +4 -2
  47. package/dist/src/app-opentui/types.d.ts +1 -1
  48. package/dist/src/commands/_cli-format.d.ts +10 -1
  49. package/dist/src/commands/_cli-format.js +5 -2
  50. package/dist/src/commands/_connection-state.d.ts +11 -1
  51. package/dist/src/commands/_connection-state.js +50 -5
  52. package/dist/src/commands/_doctor-checks.js +458 -46
  53. package/dist/src/commands/_help-catalog.js +4 -2
  54. package/dist/src/commands/_json-output.js +4 -0
  55. package/dist/src/commands/_operator-view.js +496 -57
  56. package/dist/src/commands/_pi-frontend.d.ts +25 -0
  57. package/dist/src/commands/_pi-frontend.js +497 -57
  58. package/dist/src/commands/_preflight.js +509 -72
  59. package/dist/src/commands/_server-client.d.ts +33 -0
  60. package/dist/src/commands/_server-client.js +477 -46
  61. package/dist/src/commands/_server-events.js +446 -41
  62. package/dist/src/commands/_snapshot-upload.js +460 -48
  63. package/dist/src/commands/connect.js +620 -15
  64. package/dist/src/commands/doctor.js +458 -46
  65. package/dist/src/commands/github.js +462 -50
  66. package/dist/src/commands/inbox.js +458 -46
  67. package/dist/src/commands/init.js +497 -74
  68. package/dist/src/commands/inspect.js +458 -46
  69. package/dist/src/commands/run.js +496 -57
  70. package/dist/src/commands/server.js +647 -163
  71. package/dist/src/commands/setup.js +463 -51
  72. package/dist/src/commands/stats.js +462 -48
  73. package/dist/src/commands/task-run-driver.js +464 -52
  74. package/dist/src/commands/task.js +551 -85
  75. package/dist/src/commands.js +701 -151
  76. package/dist/src/index.js +705 -151
  77. package/dist/src/launcher.js +4 -0
  78. package/package.json +8 -8
@@ -864,12 +864,28 @@ function readRepoConnection(projectRoot) {
864
864
  selected,
865
865
  project: typeof record.project === "string" ? record.project : undefined,
866
866
  linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
867
- serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
867
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined,
868
+ serverProjectRootAlias: typeof record.serverProjectRootAlias === "string" && record.serverProjectRootAlias.trim() ? record.serverProjectRootAlias.trim() : undefined,
869
+ serverProjectRootBaseUrl: typeof record.serverProjectRootBaseUrl === "string" && record.serverProjectRootBaseUrl.trim() ? record.serverProjectRootBaseUrl.trim().replace(/\/+$/, "") : undefined
868
870
  };
869
871
  }
870
872
  function writeRepoConnection(projectRoot, state) {
871
873
  writeJsonFile2(resolveRepoConnectionPath(projectRoot), state);
872
874
  }
875
+ function rootAllowedForSelection(repo, connection) {
876
+ const root = repo.serverProjectRoot?.trim();
877
+ if (!root)
878
+ return;
879
+ if (connection.kind === "remote") {
880
+ if (repo.serverProjectRootAlias !== repo.selected)
881
+ return;
882
+ if (repo.serverProjectRootBaseUrl !== connection.baseUrl)
883
+ return;
884
+ } else if (repo.serverProjectRootAlias && repo.serverProjectRootAlias !== repo.selected) {
885
+ return;
886
+ }
887
+ return root;
888
+ }
873
889
  function resolveSelectedConnection(projectRoot, options = {}) {
874
890
  const repo = readRepoConnection(projectRoot);
875
891
  if (!repo)
@@ -881,13 +897,41 @@ function resolveSelectedConnection(projectRoot, options = {}) {
881
897
  if (!connection) {
882
898
  throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
883
899
  }
884
- return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
900
+ return { alias: repo.selected, connection, serverProjectRoot: rootAllowedForSelection(repo, connection) };
901
+ }
902
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot, metadata = {}) {
903
+ const repo = readRepoConnection(projectRoot);
904
+ if (!repo)
905
+ return;
906
+ let inferred = metadata;
907
+ if (!inferred.alias || !inferred.baseUrl) {
908
+ try {
909
+ const selected = resolveSelectedConnection(projectRoot);
910
+ if (selected?.connection.kind === "remote") {
911
+ inferred = {
912
+ alias: inferred.alias ?? selected.alias,
913
+ baseUrl: inferred.baseUrl ?? selected.connection.baseUrl
914
+ };
915
+ }
916
+ } catch {}
917
+ }
918
+ writeRepoConnection(projectRoot, {
919
+ ...repo,
920
+ ...metadata.project ? { project: metadata.project } : {},
921
+ serverProjectRoot,
922
+ ...inferred.alias ? { serverProjectRootAlias: inferred.alias } : {},
923
+ ...inferred.baseUrl ? { serverProjectRootBaseUrl: inferred.baseUrl.replace(/\/+$/, "") } : {}
924
+ });
885
925
  }
886
- function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
926
+ function clearRepoServerProjectRoot(projectRoot) {
887
927
  const repo = readRepoConnection(projectRoot);
888
928
  if (!repo)
889
929
  return;
890
- writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
930
+ writeRepoConnection(projectRoot, {
931
+ selected: repo.selected,
932
+ ...repo.project ? { project: repo.project } : {},
933
+ ...repo.linkedAt ? { linkedAt: repo.linkedAt } : {}
934
+ });
891
935
  }
892
936
  function isRemoteConnectionSelected(projectRoot) {
893
937
  return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
@@ -897,8 +941,8 @@ var init__connection_state = __esm(() => {
897
941
  });
898
942
 
899
943
  // packages/cli/src/commands/_server-client.ts
900
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
901
- import { resolve as resolve10 } from "path";
944
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
945
+ import { dirname as dirname3, isAbsolute, resolve as resolve10 } from "path";
902
946
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
903
947
  function setServerPhaseListener(listener) {
904
948
  const previous = serverPhaseListener;
@@ -944,11 +988,10 @@ function readStoredGitHubAuthToken(projectRoot) {
944
988
  const parsed = readRemoteAuthState(projectRoot);
945
989
  return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
946
990
  }
947
- function inferRemoteServerProjectRootFromAuthState(projectRoot) {
948
- const repo = readRepoConnection(projectRoot);
949
- const slug = repo?.project?.trim();
950
- if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
991
+ function inferRemoteServerProjectRootCandidateFromAuthState(projectRoot, repoSlug) {
992
+ if (!/^[^/]+\/[^/]+$/.test(repoSlug))
951
993
  return null;
994
+ const repo = readRepoConnection(projectRoot);
952
995
  const auth = readRemoteAuthState(projectRoot);
953
996
  const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
954
997
  const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
@@ -957,25 +1000,419 @@ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
957
1000
  const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
958
1001
  if (!checkoutBaseDir)
959
1002
  return null;
960
- const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
961
- writeRepoServerProjectRoot(projectRoot, inferred);
962
- return inferred;
1003
+ return `${checkoutBaseDir.replace(/\/+$/, "")}/${repoSlug}`;
963
1004
  }
964
1005
  function readLocalConnectionFallbackToken(projectRoot) {
965
1006
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
966
1007
  }
1008
+ function normalizeRepoSlug(value) {
1009
+ const slug = value?.trim();
1010
+ return slug && /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
1011
+ }
1012
+ function readProjectLinkSlug(projectRoot) {
1013
+ const path = resolve10(projectRoot, ".rig", "state", "project-link.json");
1014
+ if (!existsSync6(path))
1015
+ return null;
1016
+ try {
1017
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
1018
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
1019
+ return null;
1020
+ return normalizeRepoSlug(typeof parsed.repoSlug === "string" ? String(parsed.repoSlug) : null);
1021
+ } catch {
1022
+ return null;
1023
+ }
1024
+ }
1025
+ function writeProjectLinkConnection(projectRoot, repoSlug, alias) {
1026
+ const path = resolve10(projectRoot, ".rig", "state", "project-link.json");
1027
+ mkdirSync6(dirname3(path), { recursive: true });
1028
+ writeFileSync5(path, `${JSON.stringify({ repoSlug, connection: alias, linkedAt: new Date().toISOString() }, null, 2)}
1029
+ `, "utf8");
1030
+ }
1031
+ function remoteLinkRepairCommand(repoSlug) {
1032
+ return repoSlug ? `rig server repair-link --repo ${repoSlug}` : "rig server repair-link --repo owner/repo";
1033
+ }
1034
+ function formatRemoteProjectLinkHint(resolution) {
1035
+ const repair = remoteLinkRepairCommand(resolution.repoSlug);
1036
+ if (resolution.status === "auth_required") {
1037
+ return `Authenticate the selected remote${resolution.alias ? ` (${resolution.alias})` : ""}, then run \`${repair}\`. Use \`rig github auth import-gh\` or \`rig init --repair --server remote --github-auth device${resolution.repoSlug ? ` --repo ${resolution.repoSlug}` : ""}\`.`;
1038
+ }
1039
+ if (resolution.status === "missing_project") {
1040
+ return `Record the repo slug, then run \`${repair}\` (or \`rig init --repo owner/repo --server remote --github-auth device\`).`;
1041
+ }
1042
+ if (resolution.status === "not_remote")
1043
+ return "Select a remote server with `rig server use <alias>` before repairing a remote project link.";
1044
+ return `Run \`${repair}\` to backfill or prepare a server-host checkout for the selected remote${resolution.alias ? ` (${resolution.alias})` : ""}.`;
1045
+ }
1046
+ function remoteProjectLinkFailure(input) {
1047
+ const partial = {
1048
+ status: input.status,
1049
+ alias: input.alias,
1050
+ baseUrl: input.baseUrl,
1051
+ repoSlug: input.repoSlug
1052
+ };
1053
+ return {
1054
+ ok: false,
1055
+ ...input,
1056
+ hint: formatRemoteProjectLinkHint(partial),
1057
+ next: remoteLinkRepairCommand(input.repoSlug)
1058
+ };
1059
+ }
1060
+ function remoteProjectLinkSuccess(input) {
1061
+ return {
1062
+ ok: true,
1063
+ status: input.status,
1064
+ alias: input.alias,
1065
+ baseUrl: input.baseUrl,
1066
+ repoSlug: input.repoSlug,
1067
+ serverProjectRoot: input.serverProjectRoot,
1068
+ source: input.source,
1069
+ prepared: input.prepared ?? input.status === "prepared",
1070
+ validated: input.validated ?? false,
1071
+ message: input.message ?? `Remote project link ready for ${input.repoSlug}.`,
1072
+ hint: "Remote-scoped task/run/status requests will send x-rig-project-root for this server-host checkout.",
1073
+ next: "rig task list"
1074
+ };
1075
+ }
1076
+ function localCheckoutPathCandidate(projectRoot, candidate) {
1077
+ try {
1078
+ const local = resolve10(projectRoot);
1079
+ const resolved = resolve10(candidate);
1080
+ return resolved === local || resolved.startsWith(`${local}/`);
1081
+ } catch {
1082
+ return false;
1083
+ }
1084
+ }
1085
+ async function requestRemoteProjectLinkJson(baseUrl, pathname, authToken, init = {}) {
1086
+ const requestUrl = new URL(`${baseUrl}${pathname}`);
1087
+ if (authToken && queryAuthFallbackEnabled())
1088
+ requestUrl.searchParams.set("rt", authToken);
1089
+ const response = await fetch(requestUrl, {
1090
+ ...init,
1091
+ headers: mergeHeaders(init.headers, authToken)
1092
+ });
1093
+ const text = await response.text();
1094
+ const payload = text.trim().length > 0 ? (() => {
1095
+ try {
1096
+ return JSON.parse(text);
1097
+ } catch {
1098
+ return null;
1099
+ }
1100
+ })() : null;
1101
+ return { ok: response.ok, status: response.status, payload, text };
1102
+ }
1103
+ function payloadError(payload, fallback) {
1104
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
1105
+ const record = payload;
1106
+ const value = record.error ?? record.message ?? record.reason;
1107
+ if (typeof value === "string" && value.trim())
1108
+ return value.trim();
1109
+ }
1110
+ return fallback;
1111
+ }
1112
+ function checkoutPathsFromProjectPayload(payload) {
1113
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
1114
+ return [];
1115
+ const project = payload.project;
1116
+ if (!project || typeof project !== "object" || Array.isArray(project))
1117
+ return [];
1118
+ const checkouts = project.checkouts;
1119
+ if (!Array.isArray(checkouts))
1120
+ return [];
1121
+ return [...checkouts].reverse().flatMap((entry) => {
1122
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
1123
+ return [];
1124
+ const path = entry.path;
1125
+ return typeof path === "string" && path.trim() ? [path.trim()] : [];
1126
+ });
1127
+ }
1128
+ function checkoutPathFromPreparePayload(payload) {
1129
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
1130
+ return null;
1131
+ const checkout = payload.checkout;
1132
+ if (!checkout || typeof checkout !== "object" || Array.isArray(checkout))
1133
+ return null;
1134
+ const path = checkout.path;
1135
+ return typeof path === "string" && path.trim() ? path.trim() : null;
1136
+ }
1137
+ async function validateAndPersistRemoteRoot(input) {
1138
+ const candidate = input.candidate.trim();
1139
+ if (!candidate || !isAbsolute(candidate)) {
1140
+ return remoteProjectLinkFailure({
1141
+ status: "invalid_root",
1142
+ alias: input.alias,
1143
+ baseUrl: input.baseUrl,
1144
+ repoSlug: input.repoSlug,
1145
+ message: `Remote project root candidate is not an absolute server-host path: ${candidate || "(empty)"}.`
1146
+ });
1147
+ }
1148
+ if (localCheckoutPathCandidate(input.projectRoot, candidate)) {
1149
+ return remoteProjectLinkFailure({
1150
+ status: "invalid_root",
1151
+ alias: input.alias,
1152
+ baseUrl: input.baseUrl,
1153
+ repoSlug: input.repoSlug,
1154
+ message: `Refusing to use local checkout path ${candidate} as a remote server project root.`
1155
+ });
1156
+ }
1157
+ let response;
1158
+ try {
1159
+ response = await requestRemoteProjectLinkJson(input.baseUrl, "/api/server/project-root", input.authToken, {
1160
+ method: "POST",
1161
+ headers: { "content-type": "application/json" },
1162
+ body: JSON.stringify({ projectRoot: candidate })
1163
+ });
1164
+ } catch (error) {
1165
+ return remoteProjectLinkFailure({
1166
+ status: "error",
1167
+ alias: input.alias,
1168
+ baseUrl: input.baseUrl,
1169
+ repoSlug: input.repoSlug,
1170
+ message: `Could not validate remote project root ${candidate}: ${error instanceof Error ? error.message : String(error)}`
1171
+ });
1172
+ }
1173
+ if (response.status === 401 || response.status === 403) {
1174
+ return remoteProjectLinkFailure({
1175
+ status: "auth_required",
1176
+ alias: input.alias,
1177
+ baseUrl: input.baseUrl,
1178
+ repoSlug: input.repoSlug,
1179
+ statusCode: response.status,
1180
+ message: `Remote server ${input.alias} requires authentication before validating project root ${candidate}.`
1181
+ });
1182
+ }
1183
+ if (!response.ok) {
1184
+ return remoteProjectLinkFailure({
1185
+ status: "invalid_root",
1186
+ alias: input.alias,
1187
+ baseUrl: input.baseUrl,
1188
+ repoSlug: input.repoSlug,
1189
+ statusCode: response.status,
1190
+ message: `Remote server rejected project root ${candidate} (${response.status}): ${payloadError(response.payload, response.text || "project-root validation failed")}`
1191
+ });
1192
+ }
1193
+ const record = response.payload && typeof response.payload === "object" && !Array.isArray(response.payload) ? response.payload : {};
1194
+ if (record.ok !== true) {
1195
+ return remoteProjectLinkFailure({
1196
+ status: "invalid_root",
1197
+ alias: input.alias,
1198
+ baseUrl: input.baseUrl,
1199
+ repoSlug: input.repoSlug,
1200
+ message: `Remote server did not accept project root ${candidate}: ${payloadError(response.payload, "project-root validation failed")}`
1201
+ });
1202
+ }
1203
+ const accepted = typeof record.projectRoot === "string" && record.projectRoot.trim() ? record.projectRoot.trim() : typeof record.requestedProjectRoot === "string" && record.requestedProjectRoot.trim() ? record.requestedProjectRoot.trim() : candidate;
1204
+ writeRepoServerProjectRoot(input.projectRoot, accepted, { alias: input.alias, baseUrl: input.baseUrl, project: input.repoSlug });
1205
+ writeProjectLinkConnection(input.projectRoot, input.repoSlug, input.alias);
1206
+ const status = input.status ?? (input.prepared ? "prepared" : "backfilled");
1207
+ return remoteProjectLinkSuccess({
1208
+ status,
1209
+ alias: input.alias,
1210
+ baseUrl: input.baseUrl,
1211
+ repoSlug: input.repoSlug,
1212
+ serverProjectRoot: accepted,
1213
+ source: input.source,
1214
+ prepared: input.prepared,
1215
+ validated: true,
1216
+ message: status === "ready" ? `Remote project link already points to ${accepted} and was validated on ${input.alias}.` : input.prepared ? `Prepared and linked remote checkout ${accepted} for ${input.repoSlug}.` : `Backfilled remote checkout ${accepted} for ${input.repoSlug}.`
1217
+ });
1218
+ }
1219
+ async function ensureRemoteProjectRootLink(projectRoot, options = {}) {
1220
+ let selected;
1221
+ try {
1222
+ selected = resolveSelectedConnection(projectRoot);
1223
+ } catch (error) {
1224
+ return remoteProjectLinkFailure({ status: "error", message: error instanceof Error ? error.message : String(error) });
1225
+ }
1226
+ if (!selected || selected.connection.kind !== "remote") {
1227
+ return remoteProjectLinkFailure({ status: "not_remote", message: "No remote Rig server is selected for this repo." });
1228
+ }
1229
+ const repo = readRepoConnection(projectRoot);
1230
+ const explicitRepoRequested = options.repoSlug !== undefined;
1231
+ const explicitRepoSlug = explicitRepoRequested ? normalizeRepoSlug(options.repoSlug) : null;
1232
+ const repoProjectSlug = normalizeRepoSlug(repo?.project);
1233
+ const repoSlug = explicitRepoSlug ?? repoProjectSlug ?? readProjectLinkSlug(projectRoot);
1234
+ const alias = selected.alias;
1235
+ const baseUrl = selected.connection.baseUrl;
1236
+ const authToken = options.authToken === undefined ? readGitHubBearerTokenForRemote(projectRoot) : options.authToken;
1237
+ const mode = options.mode ?? "backfill-only";
1238
+ if (explicitRepoRequested && !explicitRepoSlug) {
1239
+ return remoteProjectLinkFailure({
1240
+ status: "missing_project",
1241
+ alias,
1242
+ baseUrl,
1243
+ message: `Invalid --repo value ${JSON.stringify(options.repoSlug)}. Expected owner/repo.`
1244
+ });
1245
+ }
1246
+ if (!repoSlug) {
1247
+ return remoteProjectLinkFailure({
1248
+ status: "missing_project",
1249
+ alias,
1250
+ baseUrl,
1251
+ message: `Remote server ${alias} is selected, but this checkout has no GitHub repo slug recorded.`
1252
+ });
1253
+ }
1254
+ const skippedCandidates = [];
1255
+ const storedRoot = selected.serverProjectRoot?.trim() || null;
1256
+ const storedRootMatchesResolvedRepo = repoProjectSlug === repoSlug;
1257
+ if (storedRoot && storedRootMatchesResolvedRepo) {
1258
+ if (localCheckoutPathCandidate(projectRoot, storedRoot)) {
1259
+ clearRepoServerProjectRoot(projectRoot);
1260
+ skippedCandidates.push(storedRoot);
1261
+ } else {
1262
+ const storedResult = await validateAndPersistRemoteRoot({
1263
+ projectRoot,
1264
+ alias,
1265
+ baseUrl,
1266
+ authToken,
1267
+ repoSlug,
1268
+ candidate: storedRoot,
1269
+ source: "stored",
1270
+ status: "ready"
1271
+ });
1272
+ if (storedResult.ok || storedResult.status === "auth_required" || storedResult.status === "error")
1273
+ return storedResult;
1274
+ clearRepoServerProjectRoot(projectRoot);
1275
+ skippedCandidates.push(storedRoot);
1276
+ }
1277
+ } else if (storedRoot) {
1278
+ skippedCandidates.push(storedRoot);
1279
+ }
1280
+ const authCandidate = inferRemoteServerProjectRootCandidateFromAuthState(projectRoot, repoSlug);
1281
+ if (authCandidate) {
1282
+ const authResult = await validateAndPersistRemoteRoot({ projectRoot, alias, baseUrl, authToken, repoSlug, candidate: authCandidate, source: "auth-state" });
1283
+ if (authResult.ok || authResult.status === "auth_required")
1284
+ return authResult;
1285
+ }
1286
+ let registryResponse;
1287
+ try {
1288
+ registryResponse = await requestRemoteProjectLinkJson(baseUrl, `/api/projects/${encodeURIComponent(repoSlug)}`, authToken);
1289
+ } catch (error) {
1290
+ return remoteProjectLinkFailure({
1291
+ status: "error",
1292
+ alias,
1293
+ baseUrl,
1294
+ repoSlug,
1295
+ message: `Could not read remote project registry for ${repoSlug} on ${alias}: ${error instanceof Error ? error.message : String(error)}`
1296
+ });
1297
+ }
1298
+ if (registryResponse.status === 401 || registryResponse.status === 403) {
1299
+ return remoteProjectLinkFailure({
1300
+ status: "auth_required",
1301
+ alias,
1302
+ baseUrl,
1303
+ repoSlug,
1304
+ statusCode: registryResponse.status,
1305
+ message: `Remote server ${alias} requires authentication before reading the ${repoSlug} project registry.`
1306
+ });
1307
+ }
1308
+ if (registryResponse.ok) {
1309
+ const candidates = checkoutPathsFromProjectPayload(registryResponse.payload);
1310
+ for (const candidate of candidates) {
1311
+ const result = await validateAndPersistRemoteRoot({ projectRoot, alias, baseUrl, authToken, repoSlug, candidate, source: "registry" });
1312
+ if (result.ok || result.status === "auth_required")
1313
+ return result;
1314
+ skippedCandidates.push(candidate);
1315
+ }
1316
+ if (mode === "backfill-only") {
1317
+ return remoteProjectLinkFailure({
1318
+ status: candidates.length > 0 ? "invalid_root" : "no_server_checkout",
1319
+ alias,
1320
+ baseUrl,
1321
+ repoSlug,
1322
+ message: candidates.length > 0 ? `Remote registry has checkout candidates for ${repoSlug}, but none validated for ${alias}.` : `Remote registry has ${repoSlug}, but no server checkout path is linked yet.`,
1323
+ skippedCandidates
1324
+ });
1325
+ }
1326
+ } else if (registryResponse.status === 404) {
1327
+ if (mode === "backfill-only") {
1328
+ return remoteProjectLinkFailure({
1329
+ status: "project_not_registered",
1330
+ alias,
1331
+ baseUrl,
1332
+ repoSlug,
1333
+ statusCode: registryResponse.status,
1334
+ message: `Remote server ${alias} has no registered project record for ${repoSlug}.`
1335
+ });
1336
+ }
1337
+ } else {
1338
+ return remoteProjectLinkFailure({
1339
+ status: "error",
1340
+ alias,
1341
+ baseUrl,
1342
+ repoSlug,
1343
+ statusCode: registryResponse.status,
1344
+ message: `Could not backfill ${repoSlug} from remote registry (${registryResponse.status}): ${payloadError(registryResponse.payload, registryResponse.text || "registry lookup failed")}`,
1345
+ skippedCandidates
1346
+ });
1347
+ }
1348
+ let prepareResponse;
1349
+ try {
1350
+ prepareResponse = await requestRemoteProjectLinkJson(baseUrl, `/api/projects/${encodeURIComponent(repoSlug)}/prepare-checkout`, authToken, {
1351
+ method: "POST",
1352
+ headers: { "content-type": "application/json" },
1353
+ body: JSON.stringify({ checkout: { kind: "managed-clone" }, repoUrl: `https://github.com/${repoSlug}.git` })
1354
+ });
1355
+ } catch (error) {
1356
+ return remoteProjectLinkFailure({
1357
+ status: "error",
1358
+ alias,
1359
+ baseUrl,
1360
+ repoSlug,
1361
+ message: `Could not prepare remote checkout for ${repoSlug} on ${alias}: ${error instanceof Error ? error.message : String(error)}`
1362
+ });
1363
+ }
1364
+ if (prepareResponse.status === 401 || prepareResponse.status === 403) {
1365
+ return remoteProjectLinkFailure({
1366
+ status: "auth_required",
1367
+ alias,
1368
+ baseUrl,
1369
+ repoSlug,
1370
+ statusCode: prepareResponse.status,
1371
+ message: `Remote server ${alias} requires authentication before preparing ${repoSlug}.`
1372
+ });
1373
+ }
1374
+ if (!prepareResponse.ok) {
1375
+ return remoteProjectLinkFailure({
1376
+ status: "error",
1377
+ alias,
1378
+ baseUrl,
1379
+ repoSlug,
1380
+ statusCode: prepareResponse.status,
1381
+ message: `Remote checkout prepare failed for ${repoSlug} (${prepareResponse.status}): ${payloadError(prepareResponse.payload, prepareResponse.text || "prepare-checkout failed")}`,
1382
+ skippedCandidates
1383
+ });
1384
+ }
1385
+ const preparedPath = checkoutPathFromPreparePayload(prepareResponse.payload);
1386
+ if (!preparedPath) {
1387
+ return remoteProjectLinkFailure({
1388
+ status: "invalid_root",
1389
+ alias,
1390
+ baseUrl,
1391
+ repoSlug,
1392
+ message: `Remote checkout prepare for ${repoSlug} did not return a checkout.path.`,
1393
+ skippedCandidates
1394
+ });
1395
+ }
1396
+ return validateAndPersistRemoteRoot({ projectRoot, alias, baseUrl, authToken, repoSlug, candidate: preparedPath, source: "prepare", prepared: true });
1397
+ }
1398
+ async function repairRemoteProjectRootLink(context, options = {}) {
1399
+ const resolution = await ensureRemoteProjectRootLink(context.projectRoot, { mode: options.mode ?? "prepare-if-missing", repoSlug: options.repoSlug });
1400
+ if (!resolution.ok)
1401
+ throw new CliError(resolution.message, 1, { hint: resolution.hint });
1402
+ return resolution;
1403
+ }
967
1404
  async function ensureServerForCli(projectRoot) {
968
1405
  try {
969
1406
  const selected = resolveSelectedConnection(projectRoot);
970
1407
  if (selected?.connection.kind === "remote") {
971
1408
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
972
1409
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
973
- const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
1410
+ const link = await ensureRemoteProjectRootLink(projectRoot, { mode: "backfill-only", authToken });
974
1411
  return {
975
1412
  baseUrl: selected.connection.baseUrl,
976
1413
  authToken,
977
1414
  connectionKind: "remote",
978
- serverProjectRoot
1415
+ serverProjectRoot: link.ok ? link.serverProjectRoot ?? null : null
979
1416
  };
980
1417
  }
981
1418
  reportServerPhase("Starting local Rig server\u2026");
@@ -993,32 +1430,6 @@ async function ensureServerForCli(projectRoot) {
993
1430
  throw error;
994
1431
  }
995
1432
  }
996
- async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
997
- const repo = readRepoConnection(projectRoot);
998
- const slug = repo?.project?.trim();
999
- if (!slug)
1000
- return null;
1001
- try {
1002
- const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
1003
- if (authToken && queryAuthFallbackEnabled())
1004
- url.searchParams.set("rt", authToken);
1005
- const response = await fetch(url, {
1006
- headers: mergeHeaders(undefined, authToken)
1007
- });
1008
- if (!response.ok)
1009
- return null;
1010
- const payload = await response.json();
1011
- const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
1012
- const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
1013
- const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
1014
- const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
1015
- if (path)
1016
- writeRepoServerProjectRoot(projectRoot, path);
1017
- return path;
1018
- } catch {
1019
- return null;
1020
- }
1021
- }
1022
1433
  function appendTaskFilterParams(url, filters) {
1023
1434
  if (filters.assignee)
1024
1435
  url.searchParams.set("assignee", filters.assignee);
@@ -1119,13 +1530,20 @@ function canUseRemoteWithoutProjectRoot(pathname) {
1119
1530
  async function requestServerJson(context, pathname, init = {}) {
1120
1531
  const server = await ensureServerForCli(context.projectRoot);
1121
1532
  const requestUrl = new URL(`${server.baseUrl}${pathname}`);
1122
- if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
1123
- const repo = readRepoConnection(context.projectRoot);
1124
- throw new CliError(`Remote server is selected for ${repo?.project ?? "this repo"}, but this checkout has no server-host project root link.`, 1, { hint: "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to repair the remote project link before task/run commands." });
1533
+ let scopedServerProjectRoot = server.serverProjectRoot;
1534
+ if (server.connectionKind === "remote" && !scopedServerProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
1535
+ const link = await ensureRemoteProjectRootLink(context.projectRoot, { mode: "backfill-only", authToken: server.authToken });
1536
+ if (link.ok && link.serverProjectRoot) {
1537
+ scopedServerProjectRoot = link.serverProjectRoot;
1538
+ } else {
1539
+ const repo = readRepoConnection(context.projectRoot);
1540
+ const target = link.alias && link.baseUrl ? `${link.alias} (${link.baseUrl})` : server.baseUrl;
1541
+ throw new CliError(`Remote server ${target} is selected for ${link.repoSlug ?? repo?.project ?? "this repo"}, but this checkout has no server-host project root link. ${link.message}`, 1, { hint: link.hint });
1542
+ }
1125
1543
  }
1126
1544
  const headers = mergeHeaders(init.headers, server.authToken);
1127
- if (server.serverProjectRoot)
1128
- headers.set("x-rig-project-root", server.serverProjectRoot);
1545
+ if (scopedServerProjectRoot)
1546
+ headers.set("x-rig-project-root", scopedServerProjectRoot);
1129
1547
  if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
1130
1548
  requestUrl.searchParams.set("rt", server.authToken);
1131
1549
  }
@@ -1491,6 +1909,14 @@ function permissionAllowsPr(payload) {
1491
1909
  function isNotFoundError(error) {
1492
1910
  return /\b(404|not found)\b/i.test(message(error));
1493
1911
  }
1912
+ function isLoopbackBaseUrl(baseUrl) {
1913
+ try {
1914
+ const host = new URL(baseUrl).hostname.toLowerCase();
1915
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
1916
+ } catch {
1917
+ return false;
1918
+ }
1919
+ }
1494
1920
  function projectCheckoutReady(payload) {
1495
1921
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
1496
1922
  return null;
@@ -1545,25 +1971,37 @@ async function runFastTaskRunPreflight(context, options = {}) {
1545
1971
  }
1546
1972
  const repo = readRepoConnection(context.projectRoot);
1547
1973
  const remoteWithoutProjectLink = selectedServer?.connectionKind === "remote" && !repo?.project;
1548
- checks.push(repo ? preflightCheck("project-link", "project linked to selected server", repo.project ? "pass" : remoteWithoutProjectLink ? "fail" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : " (missing project link)"}`, remoteWithoutProjectLink ? "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to restore the remote project link." : "Run `rig init --yes --repo owner/repo` to record the GitHub repo slug.") : preflightCheck("project-link", "project linked to selected server", legacyServerCompatibility ? "warn" : "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig server use <alias|local>`."));
1974
+ let remoteRootBlocked = false;
1975
+ checks.push(repo ? preflightCheck("project-link", "project linked to selected server", repo.project ? "pass" : remoteWithoutProjectLink ? "fail" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : " (missing project link)"}`, remoteWithoutProjectLink ? "Run `rig server repair-link --repo owner/repo` or `rig init --repo owner/repo --server remote` to restore the remote project link." : "Run `rig init --yes --repo owner/repo` to record the GitHub repo slug.") : preflightCheck("project-link", "project linked to selected server", legacyServerCompatibility ? "warn" : "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig server use <alias|local>`."));
1976
+ if (selectedServer?.connectionKind === "remote" && repo?.project && !selectedServer.serverProjectRoot) {
1977
+ const link = await ensureRemoteProjectRootLink(context.projectRoot, { mode: "backfill-only", authToken: selectedServer.authToken });
1978
+ const loopbackDevRemote = !selectedServer.authToken && isLoopbackBaseUrl(selectedServer.baseUrl);
1979
+ checks.push(preflightCheck("remote-project-root-link", "remote server checkout/root linked", link.ok ? "pass" : loopbackDevRemote ? "warn" : "fail", link.ok ? `${link.repoSlug} -> ${link.serverProjectRoot}` : link.message, link.ok ? undefined : loopbackDevRemote ? "Loopback development remotes may continue without x-rig-project-root; production remotes must run `rig server repair-link`." : link.hint));
1980
+ remoteRootBlocked = !link.ok && !loopbackDevRemote;
1981
+ }
1549
1982
  try {
1550
1983
  const auth = await request("/api/github/auth/status");
1551
1984
  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>`."));
1552
1985
  } catch (error) {
1553
1986
  checks.push(preflightCheck("github-auth", "GitHub auth valid", legacyServerCompatibility ? "warn" : "fail", message(error), "Fix GitHub auth on the selected Rig server."));
1554
1987
  }
1555
- try {
1556
- const projection = await request("/api/workspace/task-projection");
1557
- checks.push(preflightCheck("task-projection", "task projection ready", "pass", JSON.stringify(projection).slice(0, 120)));
1558
- } catch (error) {
1559
- checks.push(preflightCheck("task-projection", "task projection ready", "warn", message(error), "Refresh task projection with `rig task list` before launching."));
1560
- }
1561
- try {
1562
- const permissions = await request("/api/github/repo/permissions");
1563
- const allowed = permissionAllowsPr(permissions);
1564
- checks.push(allowed === false ? preflightCheck("github-repo-permissions", "GitHub PR permissions", "fail", JSON.stringify(permissions).slice(0, 120), "Grant the selected GitHub token permission to push branches and open PRs.") : preflightCheck("github-repo-permissions", "GitHub PR permissions", allowed === true ? "pass" : "warn", JSON.stringify(permissions).slice(0, 120), "Confirm the selected token can push branches and open PRs."));
1565
- } catch (error) {
1566
- checks.push(preflightCheck("github-repo-permissions", "GitHub PR permissions", "warn", message(error), "Ensure the selected token can push branches and open PRs."));
1988
+ if (!remoteRootBlocked) {
1989
+ try {
1990
+ const projection = await request("/api/workspace/task-projection");
1991
+ checks.push(preflightCheck("task-projection", "task projection ready", "pass", JSON.stringify(projection).slice(0, 120)));
1992
+ } catch (error) {
1993
+ checks.push(preflightCheck("task-projection", "task projection ready", "warn", message(error), "Refresh task projection with `rig task list` before launching."));
1994
+ }
1995
+ try {
1996
+ const permissions = await request("/api/github/repo/permissions");
1997
+ const allowed = permissionAllowsPr(permissions);
1998
+ checks.push(allowed === false ? preflightCheck("github-repo-permissions", "GitHub PR permissions", "fail", JSON.stringify(permissions).slice(0, 120), "Grant the selected GitHub token permission to push branches and open PRs.") : preflightCheck("github-repo-permissions", "GitHub PR permissions", allowed === true ? "pass" : "warn", JSON.stringify(permissions).slice(0, 120), "Confirm the selected token can push branches and open PRs."));
1999
+ } catch (error) {
2000
+ checks.push(preflightCheck("github-repo-permissions", "GitHub PR permissions", "warn", message(error), "Ensure the selected token can push branches and open PRs."));
2001
+ }
2002
+ } else {
2003
+ checks.push(preflightCheck("task-projection", "task projection ready", "warn", "skipped until remote project-root link is repaired", "Run `rig server repair-link`, then retry."));
2004
+ checks.push(preflightCheck("github-repo-permissions", "GitHub PR permissions", "warn", "skipped until remote project-root link is repaired", "Run `rig server repair-link`, then retry."));
1567
2005
  }
1568
2006
  if (repo?.project) {
1569
2007
  try {
@@ -1575,19 +2013,24 @@ async function runFastTaskRunPreflight(context, options = {}) {
1575
2013
  }
1576
2014
  }
1577
2015
  if (taskId) {
1578
- try {
1579
- const tasks = await request(`/api/workspace/tasks?limit=200&refresh=1`);
1580
- const found = Array.isArray(tasks) && tasks.some((task) => taskMatchesId(task, taskId));
1581
- 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."));
1582
- } catch (error) {
1583
- checks.push(preflightCheck("issue", "task/issue accessible", legacyServerCompatibility ? "warn" : "fail", message(error), "Fix the task source before launching a run."));
1584
- }
1585
- try {
1586
- const runs = await request("/api/runs?limit=200");
1587
- const duplicate = activeDuplicateRun(runs, taskId);
1588
- checks.push(duplicate ? preflightCheck("duplicate-active-run", "one active run per task", "fail", String(duplicate.runId ?? taskId), "Attach to or stop the existing run before starting another one for this task.") : preflightCheck("duplicate-active-run", "one active run per task", "pass", taskId));
1589
- } catch (error) {
1590
- checks.push(preflightCheck("duplicate-active-run", "one active run per task", "warn", message(error), "Could not list active runs; the server will still reject conflicting runs if configured."));
2016
+ if (!remoteRootBlocked) {
2017
+ try {
2018
+ const tasks = await request(`/api/workspace/tasks?limit=200&refresh=1`);
2019
+ const found = Array.isArray(tasks) && tasks.some((task) => taskMatchesId(task, taskId));
2020
+ 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."));
2021
+ } catch (error) {
2022
+ checks.push(preflightCheck("issue", "task/issue accessible", legacyServerCompatibility ? "warn" : "fail", message(error), "Fix the task source before launching a run."));
2023
+ }
2024
+ try {
2025
+ const runs = await request("/api/runs?limit=200");
2026
+ const duplicate = activeDuplicateRun(runs, taskId);
2027
+ checks.push(duplicate ? preflightCheck("duplicate-active-run", "one active run per task", "fail", String(duplicate.runId ?? taskId), "Attach to or stop the existing run before starting another one for this task.") : preflightCheck("duplicate-active-run", "one active run per task", "pass", taskId));
2028
+ } catch (error) {
2029
+ checks.push(preflightCheck("duplicate-active-run", "one active run per task", "warn", message(error), "Could not list active runs; the server will still reject conflicting runs if configured."));
2030
+ }
2031
+ } else {
2032
+ checks.push(preflightCheck("issue", "task/issue accessible", "fail", "skipped until remote project-root link is repaired", "Run `rig server repair-link`, then retry."));
2033
+ checks.push(preflightCheck("duplicate-active-run", "one active run per task", "warn", "skipped until remote project-root link is repaired", "Run `rig server repair-link`, then retry."));
1591
2034
  }
1592
2035
  }
1593
2036
  if ((options.runtimeAdapter ?? "pi") === "pi") {
@@ -2071,16 +2514,19 @@ function formatConnectionList(connections) {
2071
2514
  return [formatSection("Rig servers", `${rows.length} available`), `${pc3.bold(pad("ALIAS", aliasWidth))} ${pc3.bold("KIND")} ${pc3.bold("TARGET")}`, ...lines, "", ...formatNextSteps(["Select one: `rig server use <alias|local>`"])].join(`
2072
2515
  `);
2073
2516
  }
2074
- function formatConnectionStatus(selected, connections) {
2517
+ function formatConnectionStatus(selected, connections, repo, remoteProjectLink) {
2075
2518
  const connection = selected === "local" ? { kind: "local", mode: "auto" } : connections[selected];
2076
2519
  const target = !connection ? "not configured" : connection.kind === "remote" ? connection.baseUrl : "local";
2520
+ const rootLabel = remoteProjectLink ? remoteProjectLink.ok ? `ready \xB7 ${remoteProjectLink.serverProjectRoot ?? repo?.serverProjectRoot ?? "linked"}` : `${remoteProjectLink.status ?? "needs repair"} \xB7 ${remoteProjectLink.hint ?? remoteProjectLink.message ?? "run rig server repair-link"}` : repo?.serverProjectRoot ? repo.serverProjectRoot : "not required";
2077
2521
  return [
2078
2522
  formatSection("Rig server", "selected for this repo"),
2079
2523
  `${themeFaint("\u2502")} ${themeDim("selected ")} ${pc3.bold(selected)}`,
2080
2524
  `${themeFaint("\u2502")} ${themeDim("kind ")} ${formatStatusPill(connection?.kind ?? "unknown")}`,
2081
2525
  `${themeFaint("\u2502")} ${themeDim("target ")} ${target ?? "not configured"}`,
2526
+ ...repo?.project ? [`${themeFaint("\u2502")} ${themeDim("project ")} ${repo.project}`] : [],
2527
+ ...connection?.kind === "remote" ? [`${themeFaint("\u2502")} ${themeDim("root ")} ${rootLabel}`] : [],
2082
2528
  "",
2083
- ...formatNextSteps(["Change: `rig server use <alias|local>`", "List saved servers: `rig server list`"])
2529
+ ...formatNextSteps(remoteProjectLink && !remoteProjectLink.ok ? ["Repair remote link: `rig server repair-link`", "Authenticate first if needed: `rig github auth import-gh`"] : ["Change: `rig server use <alias|local>`", "List saved servers: `rig server list`"])
2084
2530
  ].join(`
2085
2531
  `);
2086
2532
  }
@@ -2579,7 +3025,7 @@ var init__operator_surface = __esm(() => {
2579
3025
  });
2580
3026
 
2581
3027
  // packages/cli/src/commands/_pi-frontend.ts
2582
- import { existsSync as existsSync14, mkdirSync as mkdirSync9, mkdtempSync, readFileSync as readFileSync10, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
3028
+ import { existsSync as existsSync14, mkdirSync as mkdirSync10, mkdtempSync, readFileSync as readFileSync10, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
2583
3029
  import { homedir as homedir6, tmpdir } from "os";
2584
3030
  import { join as join3 } from "path";
2585
3031
  import { main as runPiMain } from "@earendil-works/pi-coding-agent";
@@ -2631,7 +3077,7 @@ function statusFromRunDetails(run) {
2631
3077
  async function prepareOperatorConsole(context, runId, tempSessionDir) {
2632
3078
  const server = await ensureServerForCli(context.projectRoot);
2633
3079
  const localCwd = join3(homedir6(), ".rig", "drones", runId.slice(0, 8));
2634
- mkdirSync9(localCwd, { recursive: true });
3080
+ mkdirSync10(localCwd, { recursive: true });
2635
3081
  trustDroneCwd(localCwd);
2636
3082
  installRigPiTheme();
2637
3083
  let sessionFileArg = [];
@@ -2653,7 +3099,7 @@ async function prepareOperatorConsole(context, runId, tempSessionDir) {
2653
3099
  return line;
2654
3100
  }).join(`
2655
3101
  `);
2656
- writeFileSync7(localSessionPath, content);
3102
+ writeFileSync8(localSessionPath, content);
2657
3103
  sessionFileArg = ["--session", localSessionPath];
2658
3104
  }
2659
3105
  } catch {}
@@ -2669,12 +3115,12 @@ async function prepareOperatorConsole(context, runId, tempSessionDir) {
2669
3115
  function trustDroneCwd(localCwd) {
2670
3116
  try {
2671
3117
  const agentDir = join3(homedir6(), ".pi", "agent");
2672
- mkdirSync9(agentDir, { recursive: true });
3118
+ mkdirSync10(agentDir, { recursive: true });
2673
3119
  const trustPath = join3(agentDir, "trust.json");
2674
3120
  const store = existsSync14(trustPath) ? JSON.parse(readFileSync10(trustPath, "utf8")) : {};
2675
3121
  if (store[localCwd] !== true) {
2676
3122
  store[localCwd] = true;
2677
- writeFileSync7(trustPath, `${JSON.stringify(store, null, "\t")}
3123
+ writeFileSync8(trustPath, `${JSON.stringify(store, null, "\t")}
2678
3124
  `);
2679
3125
  }
2680
3126
  } catch {}
@@ -2682,15 +3128,38 @@ function trustDroneCwd(localCwd) {
2682
3128
  function installRigPiTheme() {
2683
3129
  try {
2684
3130
  const themesDir = join3(homedir6(), ".pi", "agent", "themes");
2685
- mkdirSync9(themesDir, { recursive: true });
3131
+ mkdirSync10(themesDir, { recursive: true });
2686
3132
  const themePath = join3(themesDir, "rig.json");
2687
3133
  const next = `${JSON.stringify(RIG_PI_THEME, null, "\t")}
2688
3134
  `;
2689
3135
  if (!existsSync14(themePath) || readFileSync10(themePath, "utf8") !== next) {
2690
- writeFileSync7(themePath, next);
3136
+ writeFileSync8(themePath, next);
2691
3137
  }
2692
3138
  } catch {}
2693
3139
  }
3140
+ async function runWithProcessExitGuard(body) {
3141
+ const realExit = process.exit;
3142
+ let exitCode = 0;
3143
+ let signalQuit = () => {};
3144
+ const quit = new Promise((resolve22) => {
3145
+ signalQuit = resolve22;
3146
+ });
3147
+ const guardedExit = (code) => {
3148
+ exitCode = typeof code === "number" ? code : 0;
3149
+ signalQuit();
3150
+ return;
3151
+ };
3152
+ process.exit = guardedExit;
3153
+ try {
3154
+ await Promise.race([Promise.resolve().then(body), quit]);
3155
+ } finally {
3156
+ process.exit = realExit;
3157
+ }
3158
+ return exitCode;
3159
+ }
3160
+ function runPiMainReturningOnQuit(args, options) {
3161
+ return runWithProcessExitGuard(() => runPiMain(args, options));
3162
+ }
2694
3163
  async function attachRunBundledPiFrontend(context, input) {
2695
3164
  const tempSessionDir = mkdtempSync(join3(tmpdir(), "rig-pi-frontend-sessions-"));
2696
3165
  const { server, sessionFileArg } = await withSpinner(`Opening Pi console for run ${input.runId}\u2026`, () => prepareOperatorConsole(context, input.runId, tempSessionDir), { outputMode: context.outputMode });
@@ -2706,16 +3175,20 @@ async function attachRunBundledPiFrontend(context, input) {
2706
3175
  };
2707
3176
  let detached = false;
2708
3177
  try {
2709
- await runPiMain([
3178
+ const piArgs = [
2710
3179
  "--offline",
2711
3180
  "--no-extensions",
2712
3181
  "--no-skills",
2713
3182
  "--no-prompt-templates",
2714
3183
  "--no-context-files",
2715
3184
  ...sessionFileArg
2716
- ], {
2717
- extensionFactories: [piRigExtensionFactory]
2718
- });
3185
+ ];
3186
+ const piOptions = { extensionFactories: [piRigExtensionFactory] };
3187
+ if (input.returnOnQuit) {
3188
+ await runPiMainReturningOnQuit(piArgs, piOptions);
3189
+ } else {
3190
+ await runPiMain(piArgs, piOptions);
3191
+ }
2719
3192
  detached = true;
2720
3193
  } finally {
2721
3194
  restoreEnv();
@@ -3088,12 +3561,13 @@ var init__help_catalog = __esm(() => {
3088
3561
  {
3089
3562
  name: "server",
3090
3563
  summary: "Choose, inspect, and start the Rig server that owns tasks and runs.",
3091
- usage: ["rig server <status|list|add|use|start> [options]"],
3564
+ usage: ["rig server <status|list|add|use|repair-link|start> [options]"],
3092
3565
  commands: [
3093
- { command: "status", description: "Show the selected server for this repo.", primary: true },
3566
+ { command: "status", description: "Show the selected server and remote project-root link for this repo.", primary: true },
3094
3567
  { command: "use local", description: "Switch this repo to the local Rig server.", primary: true },
3095
3568
  { command: "add <alias> <url>", description: "Save a remote Rig server URL.", primary: true },
3096
3569
  { command: "use <alias>", description: "Select a saved remote server alias.", primary: true },
3570
+ { command: "repair-link [--prepare|--backfill-only] [--repo owner/repo]", description: "Backfill or prepare the selected remote's server-host checkout link.", primary: true },
3097
3571
  { command: "list", description: "List saved local/remote server aliases.", primary: true },
3098
3572
  { command: "start [--host <host>] [--port <n>]", description: "Start a local rig-server process." }
3099
3573
  ],
@@ -3101,6 +3575,7 @@ var init__help_catalog = __esm(() => {
3101
3575
  "rig server status",
3102
3576
  "rig server add prod https://where.rig-does.work",
3103
3577
  "rig server use prod",
3578
+ "rig server repair-link --repo owner/repo",
3104
3579
  "rig server use local",
3105
3580
  "rig server start --port 3773"
3106
3581
  ],
@@ -7429,7 +7904,7 @@ import {
7429
7904
  chmodSync,
7430
7905
  copyFileSync as copyFileSync2,
7431
7906
  existsSync as existsSync8,
7432
- mkdirSync as mkdirSync6,
7907
+ mkdirSync as mkdirSync7,
7433
7908
  readdirSync,
7434
7909
  readlinkSync,
7435
7910
  rmSync as rmSync3,
@@ -7546,7 +8021,7 @@ async function executeDist(context, args) {
7546
8021
  requireNoExtraArgs(pending, "rig dist install [--scope user|system] [--path <dir>]");
7547
8022
  const scope = parseInstallScope(scopeResult.value);
7548
8023
  const installDir = resolveInstallDir(scope, pathResult.value);
7549
- mkdirSync6(installDir, { recursive: true });
8024
+ mkdirSync7(installDir, { recursive: true });
7550
8025
  let source = await findLatestDistBinary(context.projectRoot);
7551
8026
  let buildDir = null;
7552
8027
  if (!source) {
@@ -7601,7 +8076,7 @@ async function executeDist(context, args) {
7601
8076
  const fp = await computeRuntimeImageFingerprint(context.projectRoot);
7602
8077
  const currentId = computeRuntimeImageId(fp);
7603
8078
  const imagesDir = resolve13(resolveControlPlaneMonorepoRuntimeDir(context.projectRoot), "images");
7604
- mkdirSync6(imagesDir, { recursive: true });
8079
+ mkdirSync7(imagesDir, { recursive: true });
7605
8080
  let pruned = 0;
7606
8081
  for (const entry of readdirSync(imagesDir, { withFileTypes: true })) {
7607
8082
  if (entry.isDirectory() && entry.name !== currentId) {
@@ -7613,9 +8088,9 @@ async function executeDist(context, args) {
7613
8088
  console.log(`Pruned ${pruned} stale image(s).`);
7614
8089
  }
7615
8090
  const imageDir = resolve13(imagesDir, currentId);
7616
- mkdirSync6(resolve13(imageDir, "bin/hooks"), { recursive: true });
7617
- mkdirSync6(resolve13(imageDir, "bin/plugins"), { recursive: true });
7618
- mkdirSync6(resolve13(imageDir, "bin/validators"), { recursive: true });
8091
+ mkdirSync7(resolve13(imageDir, "bin/hooks"), { recursive: true });
8092
+ mkdirSync7(resolve13(imageDir, "bin/plugins"), { recursive: true });
8093
+ mkdirSync7(resolve13(imageDir, "bin/validators"), { recursive: true });
7619
8094
  const hookNames = [
7620
8095
  "scope-guard",
7621
8096
  "import-guard",
@@ -7640,8 +8115,8 @@ async function executeDist(context, args) {
7640
8115
  const binPluginsDir = resolve13(resolveControlPlaneHostBinDir(context.projectRoot), "plugins");
7641
8116
  const validatorsRoot = resolve13(hostProjectRoot, "packages/runtime/src/control-plane/validators");
7642
8117
  const binValidatorsDir = resolve13(resolveControlPlaneHostBinDir(context.projectRoot), "validators");
7643
- mkdirSync6(binPluginsDir, { recursive: true });
7644
- mkdirSync6(binValidatorsDir, { recursive: true });
8118
+ mkdirSync7(binPluginsDir, { recursive: true });
8119
+ mkdirSync7(binValidatorsDir, { recursive: true });
7645
8120
  if (existsSync8(pluginsDir)) {
7646
8121
  for (const entry of readdirSync(pluginsDir, { withFileTypes: true })) {
7647
8122
  const m = entry.name.match(/^(.+)\.plugin\.(ts|js|mjs|cjs)$/);
@@ -7748,7 +8223,7 @@ init_inbox();
7748
8223
  init_runner();
7749
8224
  init__connection_state();
7750
8225
  init__server_client();
7751
- import { appendFileSync as appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
8226
+ import { appendFileSync as appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
7752
8227
  import { spawnSync } from "child_process";
7753
8228
  import { basename as basename2, resolve as resolve17 } from "path";
7754
8229
  import { buildRigInitConfigSource } from "@rig/core";
@@ -7934,7 +8409,7 @@ async function buildPiSetupChecks(input = {}) {
7934
8409
  // packages/cli/src/commands/_snapshot-upload.ts
7935
8410
  init__server_client();
7936
8411
  import { mkdir, readdir, readFile, writeFile } from "fs/promises";
7937
- import { dirname as dirname3, resolve as resolve15, relative, sep } from "path";
8412
+ import { dirname as dirname4, resolve as resolve15, relative, sep } from "path";
7938
8413
  var SNAPSHOT_ARCHIVE_VERSION = 1;
7939
8414
  var SNAPSHOT_ARCHIVE_CONTENT_TYPE = "application/vnd.rig.snapshot+json";
7940
8415
  var DEFAULT_EXCLUDED_DIRECTORIES = new Set([
@@ -8283,14 +8758,14 @@ function parseRepoSlug(value) {
8283
8758
  }
8284
8759
  function ensureRigPrivateDirs(projectRoot) {
8285
8760
  const rigDir = resolve17(projectRoot, ".rig");
8286
- mkdirSync7(resolve17(rigDir, "state"), { recursive: true });
8287
- mkdirSync7(resolve17(rigDir, "logs"), { recursive: true });
8288
- mkdirSync7(resolve17(rigDir, "runs"), { recursive: true });
8289
- mkdirSync7(resolve17(rigDir, "tmp"), { recursive: true });
8290
- mkdirSync7(resolve17(projectRoot, "artifacts"), { recursive: true });
8761
+ mkdirSync8(resolve17(rigDir, "state"), { recursive: true });
8762
+ mkdirSync8(resolve17(rigDir, "logs"), { recursive: true });
8763
+ mkdirSync8(resolve17(rigDir, "runs"), { recursive: true });
8764
+ mkdirSync8(resolve17(rigDir, "tmp"), { recursive: true });
8765
+ mkdirSync8(resolve17(projectRoot, "artifacts"), { recursive: true });
8291
8766
  const taskConfigPath = resolve17(rigDir, "task-config.json");
8292
8767
  if (!existsSync11(taskConfigPath))
8293
- writeFileSync5(taskConfigPath, `{}
8768
+ writeFileSync6(taskConfigPath, `{}
8294
8769
  `, "utf-8");
8295
8770
  }
8296
8771
  function ensureGitignoreEntries(projectRoot) {
@@ -8318,7 +8793,7 @@ function ensureRigConfigPackageDependencies(projectRoot) {
8318
8793
  ...existsSync11(path) ? existing : { name: "rig-project", private: true },
8319
8794
  devDependencies
8320
8795
  };
8321
- writeFileSync5(path, `${JSON.stringify(next, null, 2)}
8796
+ writeFileSync6(path, `${JSON.stringify(next, null, 2)}
8322
8797
  `, "utf8");
8323
8798
  }
8324
8799
  function applyGitHubProjectConfig(source, options) {
@@ -8344,9 +8819,9 @@ function checkoutForInit(projectRoot, serverKind, strategy) {
8344
8819
  const selected = strategy ?? { kind: "managed-clone" };
8345
8820
  switch (selected.kind) {
8346
8821
  case "managed-clone":
8347
- return { kind: "managed-clone", path: projectRoot };
8822
+ return { kind: "managed-clone" };
8348
8823
  case "current-ref":
8349
- return { kind: "current-ref", path: projectRoot, ...selected.ref ? { ref: selected.ref } : {} };
8824
+ return { kind: "current-ref", ...selected.ref ? { ref: selected.ref } : {} };
8350
8825
  case "uploaded-snapshot":
8351
8826
  return { kind: "uploaded-snapshot", path: projectRoot, source: "local-working-tree" };
8352
8827
  case "existing-path":
@@ -8616,7 +9091,7 @@ function remoteGitHubAuthMetadata(payload) {
8616
9091
  };
8617
9092
  }
8618
9093
  function writeRemoteGitHubAuthState(projectRoot, input) {
8619
- writeFileSync5(resolve17(projectRoot, ".rig", "state", "github-auth.json"), `${JSON.stringify({
9094
+ writeFileSync6(resolve17(projectRoot, ".rig", "state", "github-auth.json"), `${JSON.stringify({
8620
9095
  authenticated: true,
8621
9096
  source: input.source,
8622
9097
  storedOnServer: true,
@@ -8664,7 +9139,7 @@ function runLocalFilesInit(context, options) {
8664
9139
  console.log("rig.config already exists; leaving it unchanged. Pass --repair to rewrite it.");
8665
9140
  } else {
8666
9141
  const projectName = basename2(projectRoot) || "rig-project";
8667
- writeFileSync5(configTsPath, buildRigInitConfigSource({
9142
+ writeFileSync6(configTsPath, buildRigInitConfigSource({
8668
9143
  projectName,
8669
9144
  taskSource: { kind: "files", path: "tasks" },
8670
9145
  useStandardPlugin: true
@@ -8673,8 +9148,8 @@ function runLocalFilesInit(context, options) {
8673
9148
  ensureRigConfigPackageDependencies(projectRoot);
8674
9149
  const tasksDir = resolve17(projectRoot, "tasks");
8675
9150
  if (!existsSync11(tasksDir)) {
8676
- mkdirSync7(tasksDir, { recursive: true });
8677
- writeFileSync5(resolve17(tasksDir, "T-1.json"), `${JSON.stringify({ id: "T-1", title: "My first Rig task", body: "Describe the change you want an agent to make." }, null, 2)}
9151
+ mkdirSync8(tasksDir, { recursive: true });
9152
+ writeFileSync6(resolve17(tasksDir, "T-1.json"), `${JSON.stringify({ id: "T-1", title: "My first Rig task", body: "Describe the change you want an agent to make." }, null, 2)}
8678
9153
  `, "utf-8");
8679
9154
  }
8680
9155
  if (context.outputMode !== "json") {
@@ -8737,7 +9212,7 @@ function runDemoInit(context, options) {
8737
9212
  console.log("rig.config already exists; leaving it unchanged. Pass --repair to rewrite it for the demo.");
8738
9213
  }
8739
9214
  } else {
8740
- writeFileSync5(configTsPath, buildRigInitConfigSource({
9215
+ writeFileSync6(configTsPath, buildRigInitConfigSource({
8741
9216
  projectName: basename2(projectRoot) || "rig-demo",
8742
9217
  taskSource: { kind: "files", path: DEMO_TASKS_RELATIVE_DIR },
8743
9218
  useStandardPlugin: true
@@ -8746,14 +9221,14 @@ function runDemoInit(context, options) {
8746
9221
  }
8747
9222
  ensureRigConfigPackageDependencies(projectRoot);
8748
9223
  const demoTasksDir = resolve17(projectRoot, DEMO_TASKS_RELATIVE_DIR);
8749
- mkdirSync7(demoTasksDir, { recursive: true });
9224
+ mkdirSync8(demoTasksDir, { recursive: true });
8750
9225
  const taskIds = [];
8751
9226
  for (const task of DEMO_TASKS) {
8752
9227
  const id = String(task.id);
8753
9228
  taskIds.push(id);
8754
9229
  const taskPath = resolve17(demoTasksDir, `${id}.json`);
8755
9230
  if (!existsSync11(taskPath)) {
8756
- writeFileSync5(taskPath, `${JSON.stringify(task, null, 2)}
9231
+ writeFileSync6(taskPath, `${JSON.stringify(task, null, 2)}
8757
9232
  `, "utf-8");
8758
9233
  }
8759
9234
  }
@@ -8781,21 +9256,32 @@ function runDemoInit(context, options) {
8781
9256
  }
8782
9257
  async function runControlPlaneInit(context, options) {
8783
9258
  const projectRoot = context.projectRoot;
8784
- const detectedSlug = options.repoSlug ?? detectOriginRepoSlug(projectRoot);
9259
+ const existingRepoConnection = readRepoConnection(projectRoot);
9260
+ const selectedConnection = (() => {
9261
+ try {
9262
+ return resolveSelectedConnection(projectRoot);
9263
+ } catch {
9264
+ return null;
9265
+ }
9266
+ })();
9267
+ const selectedRemote = selectedConnection?.connection.kind === "remote" ? { alias: selectedConnection.alias, connection: selectedConnection.connection } : null;
9268
+ const detectedSlug = options.repoSlug ?? existingRepoConnection?.project ?? detectOriginRepoSlug(projectRoot);
9269
+ const serverKind = options.server ?? (selectedRemote ? "remote" : "local");
8785
9270
  if (!detectedSlug) {
8786
9271
  const authMethod2 = options.githubAuthMethod ?? (options.githubToken ? "token" : "skip");
8787
- if ((options.server ?? "local") === "local" && authMethod2 === "skip") {
9272
+ if (serverKind === "local" && authMethod2 === "skip") {
8788
9273
  return runLocalFilesInit(context, options);
8789
9274
  }
8790
9275
  throw new CliError("Could not detect GitHub repo slug from origin. Pass --repo owner/repo \u2014 or run `rig init --yes --server local --github-auth skip` for a local files-source project without GitHub.", 1);
8791
9276
  }
8792
9277
  const repo = parseRepoSlug(detectedSlug);
8793
- const serverKind = options.server ?? "local";
8794
- const connectionAlias = options.connectionAlias ?? (serverKind === "local" ? "local" : "remote");
9278
+ const connectionAlias = options.connectionAlias ?? (serverKind === "local" ? "local" : selectedRemote?.alias ?? "remote");
9279
+ const selectedRemoteUrl = selectedRemote && (!options.connectionAlias || options.connectionAlias === selectedRemote.alias) ? selectedRemote.connection.baseUrl : undefined;
9280
+ const remoteUrl = serverKind === "remote" ? (options.remoteUrl ?? selectedRemoteUrl)?.replace(/\/+$/, "") : undefined;
8795
9281
  if (serverKind === "remote") {
8796
- if (!options.remoteUrl)
8797
- throw new CliError("Missing --remote-url for --server remote.", 1, { hint: "Re-run as `rig init --server remote --remote-url https://your-rig-server`." });
8798
- upsertGlobalConnection(connectionAlias, { kind: "remote", baseUrl: options.remoteUrl });
9282
+ if (!remoteUrl)
9283
+ throw new CliError("Missing --remote-url for --server remote.", 1, { hint: "Re-run as `rig init --server remote --remote-url https://your-rig-server`, or first select a saved remote with `rig server use <alias>`." });
9284
+ upsertGlobalConnection(connectionAlias, { kind: "remote", baseUrl: remoteUrl });
8799
9285
  }
8800
9286
  writeRepoConnection(projectRoot, {
8801
9287
  selected: connectionAlias,
@@ -8818,11 +9304,11 @@ async function runControlPlaneInit(context, options) {
8818
9304
  taskSource: { kind: "github-issues", owner: repo.owner, repo: repo.repo },
8819
9305
  useStandardPlugin: true
8820
9306
  }), options);
8821
- writeFileSync5(configTsPath, source, "utf-8");
9307
+ writeFileSync6(configTsPath, source, "utf-8");
8822
9308
  }
8823
9309
  ensureRigConfigPackageDependencies(projectRoot);
8824
9310
  }
8825
- writeFileSync5(resolve17(projectRoot, ".rig", "state", "project-link.json"), `${JSON.stringify({ repoSlug: repo.slug, connection: connectionAlias, linkedAt: new Date().toISOString() }, null, 2)}
9311
+ writeFileSync6(resolve17(projectRoot, ".rig", "state", "project-link.json"), `${JSON.stringify({ repoSlug: repo.slug, connection: connectionAlias, linkedAt: new Date().toISOString() }, null, 2)}
8826
9312
  `, "utf8");
8827
9313
  const checkout = checkoutForInit(projectRoot, serverKind, options.remoteCheckout);
8828
9314
  let uploadedSnapshot = null;
@@ -8837,7 +9323,7 @@ async function runControlPlaneInit(context, options) {
8837
9323
  let githubAuth = null;
8838
9324
  let deviceAuth = null;
8839
9325
  const authMethod = options.githubAuthMethod ?? (options.githubToken ? "token" : "skip");
8840
- const remoteGhTokenWarning = serverKind === "remote" && authMethod === "gh" ? `This sends a GitHub token from this machine to ${options.remoteUrl ?? "the remote Rig server"}.` : null;
9326
+ const remoteGhTokenWarning = serverKind === "remote" && authMethod === "gh" ? `This sends a GitHub token from this machine to ${remoteUrl ?? "the remote Rig server"}.` : null;
8841
9327
  if (remoteGhTokenWarning && !options.yes) {
8842
9328
  throw new CliError(`${remoteGhTokenWarning} Re-run with --yes to confirm this explicit token transfer.`, 1);
8843
9329
  }
@@ -9153,8 +9639,8 @@ init__server_client();
9153
9639
  init__connection_state();
9154
9640
  init__async_ui();
9155
9641
  import { spawnSync as spawnSync2 } from "child_process";
9156
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync6 } from "fs";
9157
- import { dirname as dirname4, resolve as resolve18 } from "path";
9642
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "fs";
9643
+ import { dirname as dirname5, resolve as resolve18 } from "path";
9158
9644
  function printPayload(context, payload, fallback) {
9159
9645
  if (context.outputMode === "json")
9160
9646
  console.log(JSON.stringify(payload, null, 2));
@@ -9193,8 +9679,8 @@ function persistRemoteAuthSession(context, source, result, fallbackToken) {
9193
9679
  return;
9194
9680
  const repo = readRepoConnection(context.projectRoot);
9195
9681
  const path = resolve18(context.projectRoot, ".rig", "state", "github-auth.json");
9196
- mkdirSync8(dirname4(path), { recursive: true });
9197
- writeFileSync6(path, `${JSON.stringify({
9682
+ mkdirSync9(dirname5(path), { recursive: true });
9683
+ writeFileSync7(path, `${JSON.stringify({
9198
9684
  authenticated: true,
9199
9685
  source,
9200
9686
  storedOnServer: true,
@@ -10974,6 +11460,7 @@ init_drone_ui();
10974
11460
  init_runner();
10975
11461
  init__connection_state();
10976
11462
  init__cli_format();
11463
+ init__server_client();
10977
11464
  function usageName(options) {
10978
11465
  return `rig ${options.group}`;
10979
11466
  }
@@ -10993,13 +11480,46 @@ function parseConnection(alias, value, options) {
10993
11480
  }
10994
11481
  return { kind: "remote", baseUrl: parsed.toString().replace(/\/+$/, "") };
10995
11482
  }
10996
- function printJsonOrText(context, payload, text2) {
10997
- if (context.outputMode === "json") {
10998
- console.log(JSON.stringify(payload, null, 2));
10999
- } else {
11483
+ function printJsonOrText(context, _payload, text2) {
11484
+ if (context.outputMode !== "json") {
11000
11485
  console.log(text2);
11001
11486
  }
11002
11487
  }
11488
+ function formatRemoteProjectLinkText(result) {
11489
+ return formatSuccessCard(result.ok ? "Remote project link ready" : "Remote project link needs repair", [
11490
+ ["selected", result.alias ?? "remote"],
11491
+ ["target", result.baseUrl ?? "unknown"],
11492
+ ["repo", result.repoSlug ?? "owner/repo"],
11493
+ ["status", result.status],
11494
+ ...result.serverProjectRoot ? [["server root", result.serverProjectRoot]] : [],
11495
+ ["next", result.ok ? result.next ?? "rig task list" : result.hint]
11496
+ ]);
11497
+ }
11498
+ function parseRepairLinkArgs(rest, usage) {
11499
+ let repoSlug;
11500
+ let mode = "prepare-if-missing";
11501
+ const pending = [...rest];
11502
+ while (pending.length > 0) {
11503
+ const token = pending.shift();
11504
+ if (token === "--repo") {
11505
+ const value = pending.shift()?.trim();
11506
+ if (!value)
11507
+ throw new CliError(`Missing --repo value. Usage: ${usage}`, 1);
11508
+ const normalized = normalizeRepoSlug(value);
11509
+ if (!normalized)
11510
+ throw new CliError(`Invalid --repo value: ${value}`, 1, { hint: "Use the canonical owner/repo slug, e.g. `--repo humanity-org/humanwork`." });
11511
+ repoSlug = normalized;
11512
+ } else if (token === "--backfill-only") {
11513
+ mode = "backfill-only";
11514
+ } else if (token === "--prepare") {
11515
+ mode = "prepare-if-missing";
11516
+ } else {
11517
+ throw new CliError(`Unexpected argument: ${String(token)}
11518
+ Usage: ${usage}`, 1);
11519
+ }
11520
+ }
11521
+ return { repoSlug, mode };
11522
+ }
11003
11523
  async function promptForConnectionAlias(context) {
11004
11524
  const state = readGlobalConnections();
11005
11525
  const repo = readRepoConnection(context.projectRoot);
@@ -11053,37 +11573,67 @@ async function executeConnectionCommand(context, args, options) {
11053
11573
  }
11054
11574
  if (!alias)
11055
11575
  throw new CliError(`Missing alias. Usage: ${usageName(options)} use <alias|local>`, 1);
11576
+ let selectedConnection = { kind: "local", mode: "auto" };
11056
11577
  if (alias !== "local") {
11057
11578
  const state = readGlobalConnections();
11058
- if (!state.connections[alias])
11579
+ const connection = state.connections[alias];
11580
+ if (!connection)
11059
11581
  throw new CliError(`Unknown Rig server: ${alias}`, 1, { hint: "Run `rig server list` to see saved aliases, or save one with `rig server add <alias> <url>`." });
11582
+ selectedConnection = connection;
11060
11583
  }
11061
11584
  const previousRepo = readRepoConnection(context.projectRoot);
11585
+ const preserveRemoteRoot = Boolean(previousRepo?.serverProjectRoot && previousRepo.selected === alias && selectedConnection.kind === "remote" && previousRepo.serverProjectRootAlias === alias && previousRepo.serverProjectRootBaseUrl === selectedConnection.baseUrl);
11062
11586
  const repoState = {
11063
11587
  selected: alias,
11064
11588
  ...previousRepo?.project ? { project: previousRepo.project } : {},
11065
11589
  linkedAt: new Date().toISOString(),
11066
- ...previousRepo?.serverProjectRoot && previousRepo.selected === alias ? { serverProjectRoot: previousRepo.serverProjectRoot } : {}
11590
+ ...preserveRemoteRoot ? {
11591
+ serverProjectRoot: previousRepo.serverProjectRoot,
11592
+ serverProjectRootAlias: previousRepo.serverProjectRootAlias,
11593
+ serverProjectRootBaseUrl: previousRepo.serverProjectRootBaseUrl
11594
+ } : {}
11067
11595
  };
11068
11596
  writeRepoConnection(context.projectRoot, repoState);
11069
- printJsonOrText(context, repoState, formatSuccessCard("Rig server selected", [
11597
+ const remoteProjectLink = alias !== "local" ? await ensureRemoteProjectRootLink(context.projectRoot, { mode: "backfill-only" }).catch((error) => ({
11598
+ ok: false,
11599
+ status: "error",
11600
+ message: error instanceof Error ? error.message : String(error),
11601
+ hint: "Run `rig server repair-link` after authenticating the selected remote."
11602
+ })) : null;
11603
+ const latestRepo = readRepoConnection(context.projectRoot);
11604
+ const details = { selected: latestRepo?.selected ?? alias, repo: latestRepo, remoteProjectLink };
11605
+ printJsonOrText(context, details, formatSuccessCard("Rig server selected", [
11070
11606
  ["selected", alias],
11071
11607
  ["scope", "this repo"],
11072
- ["next", "rig task list"]
11608
+ ...remoteProjectLink ? [["remote link", remoteProjectLink.ok ? `ready (${remoteProjectLink.serverProjectRoot ?? "linked"})` : `${remoteProjectLink.status}: ${remoteProjectLink.hint}`]] : [],
11609
+ ["next", remoteProjectLink && !remoteProjectLink.ok ? "rig server repair-link" : "rig task list"]
11073
11610
  ]));
11074
- return { ok: true, group: options.group, command: "use", details: repoState };
11611
+ return { ok: true, group: options.group, command: "use", details };
11612
+ }
11613
+ case "repair-link": {
11614
+ const parsed = parseRepairLinkArgs(rest, `${usageName(options)} repair-link [--prepare|--backfill-only] [--repo owner/repo]`);
11615
+ const result = await repairRemoteProjectRootLink(context, parsed);
11616
+ printJsonOrText(context, result, formatRemoteProjectLinkText(result));
11617
+ return { ok: true, group: options.group, command: "repair-link", details: result };
11075
11618
  }
11076
11619
  case "status": {
11077
11620
  requireNoExtraArgs(rest, `${usageName(options)} status`);
11078
11621
  const repo = readRepoConnection(context.projectRoot);
11079
11622
  const global = readGlobalConnections();
11080
- const details = { selected: repo?.selected ?? "local", repo, connections: global.connections };
11081
- printJsonOrText(context, details, formatConnectionStatus(details.selected, global.connections));
11623
+ const remoteProjectLink = repo?.selected && repo.selected !== "local" ? await ensureRemoteProjectRootLink(context.projectRoot, { mode: "backfill-only" }).catch((error) => ({
11624
+ ok: false,
11625
+ status: "error",
11626
+ message: error instanceof Error ? error.message : String(error),
11627
+ hint: "Run `rig server repair-link` after authenticating the selected remote."
11628
+ })) : null;
11629
+ const latestRepo = readRepoConnection(context.projectRoot) ?? repo;
11630
+ const details = { selected: latestRepo?.selected ?? "local", repo: latestRepo, connections: global.connections, remoteProjectLink };
11631
+ printJsonOrText(context, details, formatConnectionStatus(details.selected, global.connections, latestRepo ?? null, remoteProjectLink));
11082
11632
  return { ok: true, group: options.group, command: "status", details };
11083
11633
  }
11084
11634
  default:
11085
11635
  throw new CliError(`Unknown ${options.group} command: ${String(command)}
11086
- Usage: ${usageName(options)} <list|add|use|status>`, 1);
11636
+ Usage: ${usageName(options)} <list|add|use|status|repair-link>`, 1);
11087
11637
  }
11088
11638
  }
11089
11639
 
@@ -11091,7 +11641,7 @@ Usage: ${usageName(options)} <list|add|use|status>`, 1);
11091
11641
  init__server_client();
11092
11642
  async function executeServer(context, args, options) {
11093
11643
  const [command = "status", ...rest] = args;
11094
- if (["status", "list", "add", "use"].includes(command)) {
11644
+ if (["status", "list", "add", "use", "repair-link"].includes(command)) {
11095
11645
  return executeConnectionCommand(context, [command, ...rest], { group: "server", interactiveUse: true });
11096
11646
  }
11097
11647
  switch (command) {
@@ -11299,7 +11849,7 @@ init_task();
11299
11849
 
11300
11850
  // packages/cli/src/commands/task-run-driver.ts
11301
11851
  init_runner();
11302
- import { copyFileSync as copyFileSync3, existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync12, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
11852
+ import { copyFileSync as copyFileSync3, existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync12, statSync as statSync2, writeFileSync as writeFileSync9 } from "fs";
11303
11853
  import { resolve as resolve23 } from "path";
11304
11854
  import { spawn as spawn2, spawnSync as spawnSync4 } from "child_process";
11305
11855
  import { createInterface as createLineInterface } from "readline";
@@ -11404,7 +11954,7 @@ function copyUntrackedDirtyFiles(sourceRoot, targetRoot) {
11404
11954
  try {
11405
11955
  if (!statSync2(sourcePath).isFile())
11406
11956
  continue;
11407
- mkdirSync10(resolve23(targetPath, ".."), { recursive: true });
11957
+ mkdirSync11(resolve23(targetPath, ".."), { recursive: true });
11408
11958
  copyFileSync3(sourcePath, targetPath);
11409
11959
  copied += 1;
11410
11960
  } catch {}
@@ -12296,8 +12846,8 @@ async function executeRigOwnedTaskRun(context, input) {
12296
12846
  artifactPath: planningClassification.planningRequired ? planningArtifactPath : null,
12297
12847
  classifiedAt: new Date().toISOString()
12298
12848
  };
12299
- mkdirSync10(resolve23(context.projectRoot, ".rig", "runs", input.runId), { recursive: true });
12300
- writeFileSync8(resolve23(context.projectRoot, ".rig", "runs", input.runId, "planning-classification.json"), `${JSON.stringify(persistedPlanning, null, 2)}
12849
+ mkdirSync11(resolve23(context.projectRoot, ".rig", "runs", input.runId), { recursive: true });
12850
+ writeFileSync9(resolve23(context.projectRoot, ".rig", "runs", input.runId, "planning-classification.json"), `${JSON.stringify(persistedPlanning, null, 2)}
12301
12851
  `, "utf8");
12302
12852
  patchAuthorityRun(context.projectRoot, input.runId, { planning: persistedPlanning });
12303
12853
  prompt = `${prompt}
@@ -12444,8 +12994,8 @@ ${planningClassification.planningRequired ? `Before implementing, write a concis
12444
12994
  const dirty = applyDirtyBaselineSnapshot({ sourceRoot: context.projectRoot, targetRoot: latestRuntimeWorkspace });
12445
12995
  const readyFile = childEnv.RIG_DIRTY_BASELINE_READY_FILE;
12446
12996
  if (readyFile) {
12447
- mkdirSync10(resolve23(readyFile, ".."), { recursive: true });
12448
- writeFileSync8(readyFile, `${JSON.stringify({ ...dirty, workspaceDir: latestRuntimeWorkspace, appliedAt: new Date().toISOString() }, null, 2)}
12997
+ mkdirSync11(resolve23(readyFile, ".."), { recursive: true });
12998
+ writeFileSync9(readyFile, `${JSON.stringify({ ...dirty, workspaceDir: latestRuntimeWorkspace, appliedAt: new Date().toISOString() }, null, 2)}
12449
12999
  `, "utf8");
12450
13000
  }
12451
13001
  appendRunLog(context.projectRoot, input.runId, {
@@ -13365,7 +13915,7 @@ async function executeTest(context, args) {
13365
13915
  init_runner();
13366
13916
  init__parsers();
13367
13917
  init__paths();
13368
- import { existsSync as existsSync16, mkdirSync as mkdirSync11, readdirSync as readdirSync3, writeFileSync as writeFileSync9 } from "fs";
13918
+ import { existsSync as existsSync16, mkdirSync as mkdirSync12, readdirSync as readdirSync3, writeFileSync as writeFileSync10 } from "fs";
13369
13919
  import { resolve as resolve24 } from "path";
13370
13920
  import { createPluginHost } from "@rig/core";
13371
13921
  import {
@@ -13421,12 +13971,12 @@ function runSetupInit(projectRoot) {
13421
13971
  const stateDir = resolveControlPlaneHostStateDir(projectRoot);
13422
13972
  const logsDir = resolveControlPlaneHostLogsDir(projectRoot);
13423
13973
  const artifactsDir = resolveControlPlaneArtifactsDir(projectRoot);
13424
- mkdirSync11(stateDir, { recursive: true });
13425
- mkdirSync11(logsDir, { recursive: true });
13426
- mkdirSync11(artifactsDir, { recursive: true });
13974
+ mkdirSync12(stateDir, { recursive: true });
13975
+ mkdirSync12(logsDir, { recursive: true });
13976
+ mkdirSync12(artifactsDir, { recursive: true });
13427
13977
  const failuresPath = resolve24(stateDir, "failed_approaches.md");
13428
13978
  if (!existsSync16(failuresPath)) {
13429
- writeFileSync9(failuresPath, `# Failed Approaches
13979
+ writeFileSync10(failuresPath, `# Failed Approaches
13430
13980
 
13431
13981
  `, "utf-8");
13432
13982
  }