@h-rig/cli 0.0.6-alpha.88 → 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 (70) hide show
  1. package/dist/bin/rig.js +1159 -292
  2. package/dist/src/app/board.js +462 -48
  3. package/dist/src/app-opentui/adapters/doctor.js +458 -46
  4. package/dist/src/app-opentui/adapters/family.js +670 -147
  5. package/dist/src/app-opentui/adapters/fleet.js +477 -46
  6. package/dist/src/app-opentui/adapters/inbox.js +477 -46
  7. package/dist/src/app-opentui/adapters/init.js +497 -74
  8. package/dist/src/app-opentui/adapters/inspect.js +477 -46
  9. package/dist/src/app-opentui/adapters/pi-attach.js +484 -53
  10. package/dist/src/app-opentui/adapters/run-detail.js +477 -46
  11. package/dist/src/app-opentui/adapters/server.d.ts +26 -0
  12. package/dist/src/app-opentui/adapters/server.js +676 -59
  13. package/dist/src/app-opentui/adapters/tasks.js +539 -81
  14. package/dist/src/app-opentui/autocomplete.js +4 -2
  15. package/dist/src/app-opentui/bootstrap.js +1155 -288
  16. package/dist/src/app-opentui/command-palette.js +37 -10
  17. package/dist/src/app-opentui/index.js +566 -89
  18. package/dist/src/app-opentui/intent.js +33 -8
  19. package/dist/src/app-opentui/keymap.js +43 -29
  20. package/dist/src/app-opentui/pi-host-child.js +465 -53
  21. package/dist/src/app-opentui/react/App.js +104 -44
  22. package/dist/src/app-opentui/react/ChromeHost.js +16 -4
  23. package/dist/src/app-opentui/react/launch.js +555 -88
  24. package/dist/src/app-opentui/react/nav.js +1 -1
  25. package/dist/src/app-opentui/registry.js +1062 -237
  26. package/dist/src/app-opentui/remote-link.d.ts +10 -0
  27. package/dist/src/app-opentui/remote-link.js +47 -0
  28. package/dist/src/app-opentui/runtime.js +566 -89
  29. package/dist/src/app-opentui/scenes/doctor.js +1 -1
  30. package/dist/src/app-opentui/scenes/error.js +50 -4
  31. package/dist/src/app-opentui/scenes/family.js +60 -6
  32. package/dist/src/app-opentui/scenes/fleet.js +65 -13
  33. package/dist/src/app-opentui/scenes/help.js +4 -2
  34. package/dist/src/app-opentui/scenes/init.js +12 -12
  35. package/dist/src/app-opentui/scenes/main.js +7 -7
  36. package/dist/src/app-opentui/scenes/server.js +83 -11
  37. package/dist/src/app-opentui/scenes/tasks.js +79 -16
  38. package/dist/src/app-opentui/state.js +25 -5
  39. package/dist/src/app-opentui/surface-catalog.js +4 -2
  40. package/dist/src/app-opentui/types.d.ts +1 -1
  41. package/dist/src/commands/_cli-format.d.ts +10 -1
  42. package/dist/src/commands/_cli-format.js +5 -2
  43. package/dist/src/commands/_connection-state.d.ts +11 -1
  44. package/dist/src/commands/_connection-state.js +50 -5
  45. package/dist/src/commands/_doctor-checks.js +458 -46
  46. package/dist/src/commands/_help-catalog.js +4 -2
  47. package/dist/src/commands/_json-output.js +4 -0
  48. package/dist/src/commands/_operator-view.js +465 -53
  49. package/dist/src/commands/_pi-frontend.js +465 -53
  50. package/dist/src/commands/_preflight.js +509 -72
  51. package/dist/src/commands/_server-client.d.ts +33 -0
  52. package/dist/src/commands/_server-client.js +477 -46
  53. package/dist/src/commands/_server-events.js +446 -41
  54. package/dist/src/commands/_snapshot-upload.js +460 -48
  55. package/dist/src/commands/connect.js +620 -15
  56. package/dist/src/commands/doctor.js +458 -46
  57. package/dist/src/commands/github.js +462 -50
  58. package/dist/src/commands/inbox.js +458 -46
  59. package/dist/src/commands/init.js +497 -74
  60. package/dist/src/commands/inspect.js +458 -46
  61. package/dist/src/commands/run.js +465 -53
  62. package/dist/src/commands/server.js +647 -163
  63. package/dist/src/commands/setup.js +463 -51
  64. package/dist/src/commands/stats.js +462 -48
  65. package/dist/src/commands/task-run-driver.js +464 -52
  66. package/dist/src/commands/task.js +520 -81
  67. package/dist/src/commands.js +670 -147
  68. package/dist/src/index.js +674 -147
  69. package/dist/src/launcher.js +4 -0
  70. 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,12 +3128,12 @@ 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
  }
@@ -3115,12 +3561,13 @@ var init__help_catalog = __esm(() => {
3115
3561
  {
3116
3562
  name: "server",
3117
3563
  summary: "Choose, inspect, and start the Rig server that owns tasks and runs.",
3118
- usage: ["rig server <status|list|add|use|start> [options]"],
3564
+ usage: ["rig server <status|list|add|use|repair-link|start> [options]"],
3119
3565
  commands: [
3120
- { 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 },
3121
3567
  { command: "use local", description: "Switch this repo to the local Rig server.", primary: true },
3122
3568
  { command: "add <alias> <url>", description: "Save a remote Rig server URL.", primary: true },
3123
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 },
3124
3571
  { command: "list", description: "List saved local/remote server aliases.", primary: true },
3125
3572
  { command: "start [--host <host>] [--port <n>]", description: "Start a local rig-server process." }
3126
3573
  ],
@@ -3128,6 +3575,7 @@ var init__help_catalog = __esm(() => {
3128
3575
  "rig server status",
3129
3576
  "rig server add prod https://where.rig-does.work",
3130
3577
  "rig server use prod",
3578
+ "rig server repair-link --repo owner/repo",
3131
3579
  "rig server use local",
3132
3580
  "rig server start --port 3773"
3133
3581
  ],
@@ -7456,7 +7904,7 @@ import {
7456
7904
  chmodSync,
7457
7905
  copyFileSync as copyFileSync2,
7458
7906
  existsSync as existsSync8,
7459
- mkdirSync as mkdirSync6,
7907
+ mkdirSync as mkdirSync7,
7460
7908
  readdirSync,
7461
7909
  readlinkSync,
7462
7910
  rmSync as rmSync3,
@@ -7573,7 +8021,7 @@ async function executeDist(context, args) {
7573
8021
  requireNoExtraArgs(pending, "rig dist install [--scope user|system] [--path <dir>]");
7574
8022
  const scope = parseInstallScope(scopeResult.value);
7575
8023
  const installDir = resolveInstallDir(scope, pathResult.value);
7576
- mkdirSync6(installDir, { recursive: true });
8024
+ mkdirSync7(installDir, { recursive: true });
7577
8025
  let source = await findLatestDistBinary(context.projectRoot);
7578
8026
  let buildDir = null;
7579
8027
  if (!source) {
@@ -7628,7 +8076,7 @@ async function executeDist(context, args) {
7628
8076
  const fp = await computeRuntimeImageFingerprint(context.projectRoot);
7629
8077
  const currentId = computeRuntimeImageId(fp);
7630
8078
  const imagesDir = resolve13(resolveControlPlaneMonorepoRuntimeDir(context.projectRoot), "images");
7631
- mkdirSync6(imagesDir, { recursive: true });
8079
+ mkdirSync7(imagesDir, { recursive: true });
7632
8080
  let pruned = 0;
7633
8081
  for (const entry of readdirSync(imagesDir, { withFileTypes: true })) {
7634
8082
  if (entry.isDirectory() && entry.name !== currentId) {
@@ -7640,9 +8088,9 @@ async function executeDist(context, args) {
7640
8088
  console.log(`Pruned ${pruned} stale image(s).`);
7641
8089
  }
7642
8090
  const imageDir = resolve13(imagesDir, currentId);
7643
- mkdirSync6(resolve13(imageDir, "bin/hooks"), { recursive: true });
7644
- mkdirSync6(resolve13(imageDir, "bin/plugins"), { recursive: true });
7645
- 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 });
7646
8094
  const hookNames = [
7647
8095
  "scope-guard",
7648
8096
  "import-guard",
@@ -7667,8 +8115,8 @@ async function executeDist(context, args) {
7667
8115
  const binPluginsDir = resolve13(resolveControlPlaneHostBinDir(context.projectRoot), "plugins");
7668
8116
  const validatorsRoot = resolve13(hostProjectRoot, "packages/runtime/src/control-plane/validators");
7669
8117
  const binValidatorsDir = resolve13(resolveControlPlaneHostBinDir(context.projectRoot), "validators");
7670
- mkdirSync6(binPluginsDir, { recursive: true });
7671
- mkdirSync6(binValidatorsDir, { recursive: true });
8118
+ mkdirSync7(binPluginsDir, { recursive: true });
8119
+ mkdirSync7(binValidatorsDir, { recursive: true });
7672
8120
  if (existsSync8(pluginsDir)) {
7673
8121
  for (const entry of readdirSync(pluginsDir, { withFileTypes: true })) {
7674
8122
  const m = entry.name.match(/^(.+)\.plugin\.(ts|js|mjs|cjs)$/);
@@ -7775,7 +8223,7 @@ init_inbox();
7775
8223
  init_runner();
7776
8224
  init__connection_state();
7777
8225
  init__server_client();
7778
- 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";
7779
8227
  import { spawnSync } from "child_process";
7780
8228
  import { basename as basename2, resolve as resolve17 } from "path";
7781
8229
  import { buildRigInitConfigSource } from "@rig/core";
@@ -7961,7 +8409,7 @@ async function buildPiSetupChecks(input = {}) {
7961
8409
  // packages/cli/src/commands/_snapshot-upload.ts
7962
8410
  init__server_client();
7963
8411
  import { mkdir, readdir, readFile, writeFile } from "fs/promises";
7964
- import { dirname as dirname3, resolve as resolve15, relative, sep } from "path";
8412
+ import { dirname as dirname4, resolve as resolve15, relative, sep } from "path";
7965
8413
  var SNAPSHOT_ARCHIVE_VERSION = 1;
7966
8414
  var SNAPSHOT_ARCHIVE_CONTENT_TYPE = "application/vnd.rig.snapshot+json";
7967
8415
  var DEFAULT_EXCLUDED_DIRECTORIES = new Set([
@@ -8310,14 +8758,14 @@ function parseRepoSlug(value) {
8310
8758
  }
8311
8759
  function ensureRigPrivateDirs(projectRoot) {
8312
8760
  const rigDir = resolve17(projectRoot, ".rig");
8313
- mkdirSync7(resolve17(rigDir, "state"), { recursive: true });
8314
- mkdirSync7(resolve17(rigDir, "logs"), { recursive: true });
8315
- mkdirSync7(resolve17(rigDir, "runs"), { recursive: true });
8316
- mkdirSync7(resolve17(rigDir, "tmp"), { recursive: true });
8317
- 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 });
8318
8766
  const taskConfigPath = resolve17(rigDir, "task-config.json");
8319
8767
  if (!existsSync11(taskConfigPath))
8320
- writeFileSync5(taskConfigPath, `{}
8768
+ writeFileSync6(taskConfigPath, `{}
8321
8769
  `, "utf-8");
8322
8770
  }
8323
8771
  function ensureGitignoreEntries(projectRoot) {
@@ -8345,7 +8793,7 @@ function ensureRigConfigPackageDependencies(projectRoot) {
8345
8793
  ...existsSync11(path) ? existing : { name: "rig-project", private: true },
8346
8794
  devDependencies
8347
8795
  };
8348
- writeFileSync5(path, `${JSON.stringify(next, null, 2)}
8796
+ writeFileSync6(path, `${JSON.stringify(next, null, 2)}
8349
8797
  `, "utf8");
8350
8798
  }
8351
8799
  function applyGitHubProjectConfig(source, options) {
@@ -8371,9 +8819,9 @@ function checkoutForInit(projectRoot, serverKind, strategy) {
8371
8819
  const selected = strategy ?? { kind: "managed-clone" };
8372
8820
  switch (selected.kind) {
8373
8821
  case "managed-clone":
8374
- return { kind: "managed-clone", path: projectRoot };
8822
+ return { kind: "managed-clone" };
8375
8823
  case "current-ref":
8376
- return { kind: "current-ref", path: projectRoot, ...selected.ref ? { ref: selected.ref } : {} };
8824
+ return { kind: "current-ref", ...selected.ref ? { ref: selected.ref } : {} };
8377
8825
  case "uploaded-snapshot":
8378
8826
  return { kind: "uploaded-snapshot", path: projectRoot, source: "local-working-tree" };
8379
8827
  case "existing-path":
@@ -8643,7 +9091,7 @@ function remoteGitHubAuthMetadata(payload) {
8643
9091
  };
8644
9092
  }
8645
9093
  function writeRemoteGitHubAuthState(projectRoot, input) {
8646
- writeFileSync5(resolve17(projectRoot, ".rig", "state", "github-auth.json"), `${JSON.stringify({
9094
+ writeFileSync6(resolve17(projectRoot, ".rig", "state", "github-auth.json"), `${JSON.stringify({
8647
9095
  authenticated: true,
8648
9096
  source: input.source,
8649
9097
  storedOnServer: true,
@@ -8691,7 +9139,7 @@ function runLocalFilesInit(context, options) {
8691
9139
  console.log("rig.config already exists; leaving it unchanged. Pass --repair to rewrite it.");
8692
9140
  } else {
8693
9141
  const projectName = basename2(projectRoot) || "rig-project";
8694
- writeFileSync5(configTsPath, buildRigInitConfigSource({
9142
+ writeFileSync6(configTsPath, buildRigInitConfigSource({
8695
9143
  projectName,
8696
9144
  taskSource: { kind: "files", path: "tasks" },
8697
9145
  useStandardPlugin: true
@@ -8700,8 +9148,8 @@ function runLocalFilesInit(context, options) {
8700
9148
  ensureRigConfigPackageDependencies(projectRoot);
8701
9149
  const tasksDir = resolve17(projectRoot, "tasks");
8702
9150
  if (!existsSync11(tasksDir)) {
8703
- mkdirSync7(tasksDir, { recursive: true });
8704
- 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)}
8705
9153
  `, "utf-8");
8706
9154
  }
8707
9155
  if (context.outputMode !== "json") {
@@ -8764,7 +9212,7 @@ function runDemoInit(context, options) {
8764
9212
  console.log("rig.config already exists; leaving it unchanged. Pass --repair to rewrite it for the demo.");
8765
9213
  }
8766
9214
  } else {
8767
- writeFileSync5(configTsPath, buildRigInitConfigSource({
9215
+ writeFileSync6(configTsPath, buildRigInitConfigSource({
8768
9216
  projectName: basename2(projectRoot) || "rig-demo",
8769
9217
  taskSource: { kind: "files", path: DEMO_TASKS_RELATIVE_DIR },
8770
9218
  useStandardPlugin: true
@@ -8773,14 +9221,14 @@ function runDemoInit(context, options) {
8773
9221
  }
8774
9222
  ensureRigConfigPackageDependencies(projectRoot);
8775
9223
  const demoTasksDir = resolve17(projectRoot, DEMO_TASKS_RELATIVE_DIR);
8776
- mkdirSync7(demoTasksDir, { recursive: true });
9224
+ mkdirSync8(demoTasksDir, { recursive: true });
8777
9225
  const taskIds = [];
8778
9226
  for (const task of DEMO_TASKS) {
8779
9227
  const id = String(task.id);
8780
9228
  taskIds.push(id);
8781
9229
  const taskPath = resolve17(demoTasksDir, `${id}.json`);
8782
9230
  if (!existsSync11(taskPath)) {
8783
- writeFileSync5(taskPath, `${JSON.stringify(task, null, 2)}
9231
+ writeFileSync6(taskPath, `${JSON.stringify(task, null, 2)}
8784
9232
  `, "utf-8");
8785
9233
  }
8786
9234
  }
@@ -8808,21 +9256,32 @@ function runDemoInit(context, options) {
8808
9256
  }
8809
9257
  async function runControlPlaneInit(context, options) {
8810
9258
  const projectRoot = context.projectRoot;
8811
- 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");
8812
9270
  if (!detectedSlug) {
8813
9271
  const authMethod2 = options.githubAuthMethod ?? (options.githubToken ? "token" : "skip");
8814
- if ((options.server ?? "local") === "local" && authMethod2 === "skip") {
9272
+ if (serverKind === "local" && authMethod2 === "skip") {
8815
9273
  return runLocalFilesInit(context, options);
8816
9274
  }
8817
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);
8818
9276
  }
8819
9277
  const repo = parseRepoSlug(detectedSlug);
8820
- const serverKind = options.server ?? "local";
8821
- 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;
8822
9281
  if (serverKind === "remote") {
8823
- if (!options.remoteUrl)
8824
- throw new CliError("Missing --remote-url for --server remote.", 1, { hint: "Re-run as `rig init --server remote --remote-url https://your-rig-server`." });
8825
- 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 });
8826
9285
  }
8827
9286
  writeRepoConnection(projectRoot, {
8828
9287
  selected: connectionAlias,
@@ -8845,11 +9304,11 @@ async function runControlPlaneInit(context, options) {
8845
9304
  taskSource: { kind: "github-issues", owner: repo.owner, repo: repo.repo },
8846
9305
  useStandardPlugin: true
8847
9306
  }), options);
8848
- writeFileSync5(configTsPath, source, "utf-8");
9307
+ writeFileSync6(configTsPath, source, "utf-8");
8849
9308
  }
8850
9309
  ensureRigConfigPackageDependencies(projectRoot);
8851
9310
  }
8852
- 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)}
8853
9312
  `, "utf8");
8854
9313
  const checkout = checkoutForInit(projectRoot, serverKind, options.remoteCheckout);
8855
9314
  let uploadedSnapshot = null;
@@ -8864,7 +9323,7 @@ async function runControlPlaneInit(context, options) {
8864
9323
  let githubAuth = null;
8865
9324
  let deviceAuth = null;
8866
9325
  const authMethod = options.githubAuthMethod ?? (options.githubToken ? "token" : "skip");
8867
- 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;
8868
9327
  if (remoteGhTokenWarning && !options.yes) {
8869
9328
  throw new CliError(`${remoteGhTokenWarning} Re-run with --yes to confirm this explicit token transfer.`, 1);
8870
9329
  }
@@ -9180,8 +9639,8 @@ init__server_client();
9180
9639
  init__connection_state();
9181
9640
  init__async_ui();
9182
9641
  import { spawnSync as spawnSync2 } from "child_process";
9183
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync6 } from "fs";
9184
- 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";
9185
9644
  function printPayload(context, payload, fallback) {
9186
9645
  if (context.outputMode === "json")
9187
9646
  console.log(JSON.stringify(payload, null, 2));
@@ -9220,8 +9679,8 @@ function persistRemoteAuthSession(context, source, result, fallbackToken) {
9220
9679
  return;
9221
9680
  const repo = readRepoConnection(context.projectRoot);
9222
9681
  const path = resolve18(context.projectRoot, ".rig", "state", "github-auth.json");
9223
- mkdirSync8(dirname4(path), { recursive: true });
9224
- writeFileSync6(path, `${JSON.stringify({
9682
+ mkdirSync9(dirname5(path), { recursive: true });
9683
+ writeFileSync7(path, `${JSON.stringify({
9225
9684
  authenticated: true,
9226
9685
  source,
9227
9686
  storedOnServer: true,
@@ -11001,6 +11460,7 @@ init_drone_ui();
11001
11460
  init_runner();
11002
11461
  init__connection_state();
11003
11462
  init__cli_format();
11463
+ init__server_client();
11004
11464
  function usageName(options) {
11005
11465
  return `rig ${options.group}`;
11006
11466
  }
@@ -11020,13 +11480,46 @@ function parseConnection(alias, value, options) {
11020
11480
  }
11021
11481
  return { kind: "remote", baseUrl: parsed.toString().replace(/\/+$/, "") };
11022
11482
  }
11023
- function printJsonOrText(context, payload, text2) {
11024
- if (context.outputMode === "json") {
11025
- console.log(JSON.stringify(payload, null, 2));
11026
- } else {
11483
+ function printJsonOrText(context, _payload, text2) {
11484
+ if (context.outputMode !== "json") {
11027
11485
  console.log(text2);
11028
11486
  }
11029
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
+ }
11030
11523
  async function promptForConnectionAlias(context) {
11031
11524
  const state = readGlobalConnections();
11032
11525
  const repo = readRepoConnection(context.projectRoot);
@@ -11080,37 +11573,67 @@ async function executeConnectionCommand(context, args, options) {
11080
11573
  }
11081
11574
  if (!alias)
11082
11575
  throw new CliError(`Missing alias. Usage: ${usageName(options)} use <alias|local>`, 1);
11576
+ let selectedConnection = { kind: "local", mode: "auto" };
11083
11577
  if (alias !== "local") {
11084
11578
  const state = readGlobalConnections();
11085
- if (!state.connections[alias])
11579
+ const connection = state.connections[alias];
11580
+ if (!connection)
11086
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;
11087
11583
  }
11088
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);
11089
11586
  const repoState = {
11090
11587
  selected: alias,
11091
11588
  ...previousRepo?.project ? { project: previousRepo.project } : {},
11092
11589
  linkedAt: new Date().toISOString(),
11093
- ...previousRepo?.serverProjectRoot && previousRepo.selected === alias ? { serverProjectRoot: previousRepo.serverProjectRoot } : {}
11590
+ ...preserveRemoteRoot ? {
11591
+ serverProjectRoot: previousRepo.serverProjectRoot,
11592
+ serverProjectRootAlias: previousRepo.serverProjectRootAlias,
11593
+ serverProjectRootBaseUrl: previousRepo.serverProjectRootBaseUrl
11594
+ } : {}
11094
11595
  };
11095
11596
  writeRepoConnection(context.projectRoot, repoState);
11096
- 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", [
11097
11606
  ["selected", alias],
11098
11607
  ["scope", "this repo"],
11099
- ["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"]
11100
11610
  ]));
11101
- 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 };
11102
11618
  }
11103
11619
  case "status": {
11104
11620
  requireNoExtraArgs(rest, `${usageName(options)} status`);
11105
11621
  const repo = readRepoConnection(context.projectRoot);
11106
11622
  const global = readGlobalConnections();
11107
- const details = { selected: repo?.selected ?? "local", repo, connections: global.connections };
11108
- 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));
11109
11632
  return { ok: true, group: options.group, command: "status", details };
11110
11633
  }
11111
11634
  default:
11112
11635
  throw new CliError(`Unknown ${options.group} command: ${String(command)}
11113
- Usage: ${usageName(options)} <list|add|use|status>`, 1);
11636
+ Usage: ${usageName(options)} <list|add|use|status|repair-link>`, 1);
11114
11637
  }
11115
11638
  }
11116
11639
 
@@ -11118,7 +11641,7 @@ Usage: ${usageName(options)} <list|add|use|status>`, 1);
11118
11641
  init__server_client();
11119
11642
  async function executeServer(context, args, options) {
11120
11643
  const [command = "status", ...rest] = args;
11121
- if (["status", "list", "add", "use"].includes(command)) {
11644
+ if (["status", "list", "add", "use", "repair-link"].includes(command)) {
11122
11645
  return executeConnectionCommand(context, [command, ...rest], { group: "server", interactiveUse: true });
11123
11646
  }
11124
11647
  switch (command) {
@@ -11326,7 +11849,7 @@ init_task();
11326
11849
 
11327
11850
  // packages/cli/src/commands/task-run-driver.ts
11328
11851
  init_runner();
11329
- 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";
11330
11853
  import { resolve as resolve23 } from "path";
11331
11854
  import { spawn as spawn2, spawnSync as spawnSync4 } from "child_process";
11332
11855
  import { createInterface as createLineInterface } from "readline";
@@ -11431,7 +11954,7 @@ function copyUntrackedDirtyFiles(sourceRoot, targetRoot) {
11431
11954
  try {
11432
11955
  if (!statSync2(sourcePath).isFile())
11433
11956
  continue;
11434
- mkdirSync10(resolve23(targetPath, ".."), { recursive: true });
11957
+ mkdirSync11(resolve23(targetPath, ".."), { recursive: true });
11435
11958
  copyFileSync3(sourcePath, targetPath);
11436
11959
  copied += 1;
11437
11960
  } catch {}
@@ -12323,8 +12846,8 @@ async function executeRigOwnedTaskRun(context, input) {
12323
12846
  artifactPath: planningClassification.planningRequired ? planningArtifactPath : null,
12324
12847
  classifiedAt: new Date().toISOString()
12325
12848
  };
12326
- mkdirSync10(resolve23(context.projectRoot, ".rig", "runs", input.runId), { recursive: true });
12327
- 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)}
12328
12851
  `, "utf8");
12329
12852
  patchAuthorityRun(context.projectRoot, input.runId, { planning: persistedPlanning });
12330
12853
  prompt = `${prompt}
@@ -12471,8 +12994,8 @@ ${planningClassification.planningRequired ? `Before implementing, write a concis
12471
12994
  const dirty = applyDirtyBaselineSnapshot({ sourceRoot: context.projectRoot, targetRoot: latestRuntimeWorkspace });
12472
12995
  const readyFile = childEnv.RIG_DIRTY_BASELINE_READY_FILE;
12473
12996
  if (readyFile) {
12474
- mkdirSync10(resolve23(readyFile, ".."), { recursive: true });
12475
- 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)}
12476
12999
  `, "utf8");
12477
13000
  }
12478
13001
  appendRunLog(context.projectRoot, input.runId, {
@@ -13392,7 +13915,7 @@ async function executeTest(context, args) {
13392
13915
  init_runner();
13393
13916
  init__parsers();
13394
13917
  init__paths();
13395
- 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";
13396
13919
  import { resolve as resolve24 } from "path";
13397
13920
  import { createPluginHost } from "@rig/core";
13398
13921
  import {
@@ -13448,12 +13971,12 @@ function runSetupInit(projectRoot) {
13448
13971
  const stateDir = resolveControlPlaneHostStateDir(projectRoot);
13449
13972
  const logsDir = resolveControlPlaneHostLogsDir(projectRoot);
13450
13973
  const artifactsDir = resolveControlPlaneArtifactsDir(projectRoot);
13451
- mkdirSync11(stateDir, { recursive: true });
13452
- mkdirSync11(logsDir, { recursive: true });
13453
- mkdirSync11(artifactsDir, { recursive: true });
13974
+ mkdirSync12(stateDir, { recursive: true });
13975
+ mkdirSync12(logsDir, { recursive: true });
13976
+ mkdirSync12(artifactsDir, { recursive: true });
13454
13977
  const failuresPath = resolve24(stateDir, "failed_approaches.md");
13455
13978
  if (!existsSync16(failuresPath)) {
13456
- writeFileSync9(failuresPath, `# Failed Approaches
13979
+ writeFileSync10(failuresPath, `# Failed Approaches
13457
13980
 
13458
13981
  `, "utf-8");
13459
13982
  }