@h-rig/server 0.0.6-alpha.3 → 0.0.6-alpha.31

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.
@@ -4,8 +4,8 @@ var __require = import.meta.require;
4
4
  // packages/server/src/server-helpers/http-router.ts
5
5
  import { randomUUID } from "crypto";
6
6
  import { spawnSync as spawnSync2 } from "child_process";
7
- import { basename, dirname as dirname6, isAbsolute as isAbsolute2, resolve as resolve11 } from "path";
8
- import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync7 } from "fs";
7
+ import { basename, dirname as dirname9, isAbsolute as isAbsolute3, resolve as resolve13 } from "path";
8
+ import { copyFileSync as copyFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
9
9
 
10
10
  // packages/server/src/server.ts
11
11
  import {
@@ -174,14 +174,29 @@ function summarizeUsefulRunError(projectRoot, runId, fallback) {
174
174
  const nonGeneric = errorLines.at(-1);
175
175
  return nonGeneric ?? (typeof fallback === "string" ? fallback : null);
176
176
  }
177
+ function readRunPiSessionMetadata(projectRoot, runId) {
178
+ const run = readAuthorityRun(projectRoot, runId);
179
+ const metadata = run?.piSessionPrivate;
180
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
181
+ return null;
182
+ const record = metadata;
183
+ const publicMetadata = record.public;
184
+ const daemonConnection = record.daemonConnection;
185
+ if (!publicMetadata || typeof publicMetadata !== "object" || Array.isArray(publicMetadata))
186
+ return null;
187
+ if (!daemonConnection || typeof daemonConnection !== "object" || Array.isArray(daemonConnection))
188
+ return null;
189
+ return metadata;
190
+ }
177
191
  function readRunDetails(projectRoot, runId) {
178
192
  const run = readAuthorityRun(projectRoot, runId);
179
193
  if (!run) {
180
194
  return null;
181
195
  }
182
196
  const usefulErrorText = isGenericRunFailure(run.errorText) ? summarizeUsefulRunError(projectRoot, runId, run.errorText) : null;
197
+ const { piSessionPrivate: _piSessionPrivate, ...publicRun } = run;
183
198
  return {
184
- run: usefulErrorText ? { ...run, errorText: usefulErrorText } : run,
199
+ run: usefulErrorText ? { ...publicRun, errorText: usefulErrorText } : publicRun,
185
200
  timeline: readJsonlFile(resolve(resolveAuthorityRunDir(projectRoot, runId), "timeline.jsonl")),
186
201
  approvals: readApprovals(projectRoot, { runId }),
187
202
  userInputs: readUserInputs(projectRoot, { runId })
@@ -224,6 +239,18 @@ function readJsonlFileTail(path, options) {
224
239
  const completeLines = start > 0 ? lines.slice(1) : lines;
225
240
  return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
226
241
  }
242
+ async function readRunTimelinePage(projectRoot, runId, options = {}) {
243
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
244
+ const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
245
+ const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
246
+ const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
247
+ const endExclusive = Math.min(entries.length, startInclusive + limit);
248
+ return {
249
+ entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
250
+ nextCursor: String(endExclusive),
251
+ hasMore: endExclusive < entries.length
252
+ };
253
+ }
227
254
  var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
228
255
  async function readRunLogsPage(projectRoot, runId, options = {}) {
229
256
  const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
@@ -470,7 +497,6 @@ async function refreshTaskProjection(projectRoot, input) {
470
497
  }
471
498
 
472
499
  // packages/server/src/server-helpers/issue-analysis.ts
473
- import { execFile } from "child_process";
474
500
  import { createHash } from "crypto";
475
501
  function stableIssueHash(issue) {
476
502
  const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
@@ -604,16 +630,33 @@ function parseIssueAnalysisResult(raw) {
604
630
  return result;
605
631
  }
606
632
  function createDefaultPiIssueAnalysisCommandRunner() {
607
- return (command, args, options) => new Promise((resolve6) => {
608
- execFile(command, [...args], {
609
- timeout: options.timeoutMs,
610
- maxBuffer: 10 * 1024 * 1024,
611
- env: options.env ? { ...process.env, ...options.env } : process.env
612
- }, (error, stdout, stderr) => {
613
- const exitCode = typeof error?.code === "number" ? error.code : error ? 1 : 0;
614
- resolve6({ exitCode, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
633
+ return async (command, args, options) => {
634
+ const env = options.env ? { ...process.env, ...options.env } : process.env;
635
+ const proc = Bun.spawn([command, ...args], {
636
+ stdout: "pipe",
637
+ stderr: "pipe",
638
+ env
615
639
  });
616
- });
640
+ let timedOut = false;
641
+ const timer = setTimeout(() => {
642
+ timedOut = true;
643
+ proc.kill();
644
+ }, options.timeoutMs);
645
+ try {
646
+ const [stdout, stderr, exitCode] = await Promise.all([
647
+ new Response(proc.stdout).text(),
648
+ new Response(proc.stderr).text(),
649
+ proc.exited
650
+ ]);
651
+ return {
652
+ exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
653
+ stdout,
654
+ stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
655
+ };
656
+ } finally {
657
+ clearTimeout(timer);
658
+ }
659
+ };
617
660
  }
618
661
  function createPiIssueAnalyzer(input = {}) {
619
662
  const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
@@ -772,6 +815,11 @@ import {
772
815
  buildTaskRunLifecycleComment,
773
816
  updateConfiguredTaskSourceTask
774
817
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
818
+ import {
819
+ closeIssueAfterMergedPr,
820
+ commitRunChanges,
821
+ runPrAutomation
822
+ } from "@rig/runtime/control-plane/native/pr-automation";
775
823
 
776
824
  // packages/server/src/scheduler.ts
777
825
  import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
@@ -787,8 +835,8 @@ import {
787
835
 
788
836
  // packages/server/src/server-helpers/github-auth-store.ts
789
837
  import { randomBytes } from "crypto";
790
- import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
791
- import { resolve as resolve7 } from "path";
838
+ import { chmodSync, copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
839
+ import { dirname as dirname3, resolve as resolve7 } from "path";
792
840
  function cleanString(value) {
793
841
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
794
842
  }
@@ -818,6 +866,26 @@ function parseApiSessions(value) {
818
866
  }];
819
867
  });
820
868
  }
869
+ function parsePendingDevice(value) {
870
+ if (!value || typeof value !== "object")
871
+ return null;
872
+ const record = value;
873
+ const pollId = cleanString(record.pollId);
874
+ const deviceCode = cleanString(record.deviceCode);
875
+ const expiresAt = cleanString(record.expiresAt);
876
+ const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
877
+ if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
878
+ return null;
879
+ return { pollId, deviceCode, expiresAt, intervalSeconds };
880
+ }
881
+ function parsePendingDevices(value) {
882
+ if (!Array.isArray(value))
883
+ return [];
884
+ return value.flatMap((entry) => {
885
+ const pending = parsePendingDevice(entry);
886
+ return pending ? [pending] : [];
887
+ });
888
+ }
821
889
  function readStoredAuth(stateFile) {
822
890
  if (!existsSync4(stateFile))
823
891
  return {};
@@ -831,6 +899,7 @@ function readStoredAuth(stateFile) {
831
899
  selectedRepo: cleanString(parsed.selectedRepo),
832
900
  tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
833
901
  pendingDevice: parsePendingDevice(parsed.pendingDevice),
902
+ pendingDevices: parsePendingDevices(parsed.pendingDevices),
834
903
  apiSessions: parseApiSessions(parsed.apiSessions),
835
904
  updatedAt: cleanString(parsed.updatedAt) ?? undefined
836
905
  };
@@ -838,34 +907,36 @@ function readStoredAuth(stateFile) {
838
907
  return {};
839
908
  }
840
909
  }
841
- function parsePendingDevice(value) {
842
- if (!value || typeof value !== "object")
843
- return null;
844
- const record = value;
845
- const pollId = cleanString(record.pollId);
846
- const deviceCode = cleanString(record.deviceCode);
847
- const expiresAt = cleanString(record.expiresAt);
848
- const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
849
- if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
850
- return null;
851
- return { pollId, deviceCode, expiresAt, intervalSeconds };
852
- }
853
910
  function newApiSessionToken() {
854
911
  return `rig_${randomBytes(32).toString("base64url")}`;
855
912
  }
856
913
  function writeStoredAuth(stateFile, payload) {
857
- mkdirSync3(resolve7(stateFile, ".."), { recursive: true });
914
+ mkdirSync3(dirname3(stateFile), { recursive: true });
858
915
  writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
859
916
  `, { encoding: "utf8", mode: 384 });
860
917
  try {
861
918
  chmodSync(stateFile, 384);
862
919
  } catch {}
863
920
  }
921
+ function localProjectAuthStateFile(projectRoot) {
922
+ return resolve7(projectRoot, ".rig", "state", "github-auth.json");
923
+ }
864
924
  function resolveGitHubAuthStateFile(projectRoot) {
865
925
  return resolve7(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
866
926
  }
867
- function createGitHubAuthStore(projectRoot) {
868
- const stateFile = resolveGitHubAuthStateFile(projectRoot);
927
+ function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
928
+ const targetFile = localProjectAuthStateFile(projectRoot);
929
+ mkdirSync3(dirname3(targetFile), { recursive: true });
930
+ if (existsSync4(stateFile)) {
931
+ copyFileSync(stateFile, targetFile);
932
+ try {
933
+ chmodSync(targetFile, 384);
934
+ } catch {}
935
+ return;
936
+ }
937
+ writeStoredAuth(targetFile, {});
938
+ }
939
+ function createGitHubAuthStoreFromStateFile(stateFile) {
869
940
  return {
870
941
  stateFile,
871
942
  status(options) {
@@ -895,6 +966,7 @@ function createGitHubAuthStore(projectRoot) {
895
966
  scopes: input.scopes ?? [],
896
967
  selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
897
968
  pendingDevice: null,
969
+ pendingDevices: [],
898
970
  apiSessions: previous.apiSessions ?? [],
899
971
  updatedAt: new Date().toISOString()
900
972
  });
@@ -923,15 +995,24 @@ function createGitHubAuthStore(projectRoot) {
923
995
  const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
924
996
  return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
925
997
  },
926
- copyToProjectRoot(projectRoot2) {
927
- const targetFile = resolveGitHubAuthStateFile(projectRoot2);
998
+ copyToProjectRoot(projectRoot) {
999
+ const targetFile = resolveGitHubAuthStateFile(projectRoot);
928
1000
  writeStoredAuth(targetFile, readStoredAuth(stateFile));
929
1001
  },
1002
+ copyToLocalProjectRoot(projectRoot) {
1003
+ copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
1004
+ },
930
1005
  savePendingDevice(input) {
931
1006
  const previous = readStoredAuth(stateFile);
1007
+ const pendingDevices = [
1008
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
1009
+ ...previous.pendingDevices ?? [],
1010
+ input
1011
+ ].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
932
1012
  writeStoredAuth(stateFile, {
933
1013
  ...previous,
934
- pendingDevice: input,
1014
+ pendingDevice: null,
1015
+ pendingDevices,
935
1016
  updatedAt: new Date().toISOString()
936
1017
  });
937
1018
  },
@@ -944,23 +1025,32 @@ function createGitHubAuthStore(projectRoot) {
944
1025
  });
945
1026
  },
946
1027
  readPendingDevice(pollId) {
947
- const pending = readStoredAuth(stateFile).pendingDevice ?? null;
948
- if (!pending || pending.pollId !== pollId)
1028
+ const previous = readStoredAuth(stateFile);
1029
+ const pending = [
1030
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
1031
+ ...previous.pendingDevices ?? []
1032
+ ].find((entry) => entry.pollId === pollId) ?? null;
1033
+ if (!pending)
949
1034
  return null;
950
1035
  if (Date.parse(pending.expiresAt) <= Date.now())
951
1036
  return null;
952
1037
  return pending;
953
1038
  },
954
- clearPendingDevice() {
1039
+ clearPendingDevice(pollId) {
955
1040
  const previous = readStoredAuth(stateFile);
1041
+ const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
956
1042
  writeStoredAuth(stateFile, {
957
1043
  ...previous,
958
1044
  pendingDevice: null,
1045
+ pendingDevices: remaining,
959
1046
  updatedAt: new Date().toISOString()
960
1047
  });
961
1048
  }
962
1049
  };
963
1050
  }
1051
+ function createGitHubAuthStore(projectRoot) {
1052
+ return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
1053
+ }
964
1054
 
965
1055
  // packages/server/src/server-helpers/github-projects.ts
966
1056
  function asRecord(value) {
@@ -969,6 +1059,9 @@ function asRecord(value) {
969
1059
  function asString(value) {
970
1060
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
971
1061
  }
1062
+ function asNumber(value) {
1063
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
1064
+ }
972
1065
  async function defaultGraphQLFetch(query, variables, token) {
973
1066
  const response = await fetch("https://api.github.com/graphql", {
974
1067
  method: "POST",
@@ -985,6 +1078,32 @@ async function defaultGraphQLFetch(query, variables, token) {
985
1078
  }
986
1079
  return json.data;
987
1080
  }
1081
+ function projectNodesFrom(data) {
1082
+ const root = asRecord(data);
1083
+ const owner = asRecord(root?.organization) ?? asRecord(root?.user);
1084
+ const projects = asRecord(owner?.projectsV2);
1085
+ const nodes = projects?.nodes;
1086
+ return Array.isArray(nodes) ? nodes : [];
1087
+ }
1088
+ async function listGitHubProjects(input) {
1089
+ const query = `
1090
+ query RigListProjects($owner: String!, $first: Int!) {
1091
+ organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
1092
+ user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
1093
+ }
1094
+ `;
1095
+ const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
1096
+ const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
1097
+ return projectNodesFrom(data).flatMap((node) => {
1098
+ const record = asRecord(node);
1099
+ const id = asString(record?.id);
1100
+ const number = asNumber(record?.number);
1101
+ const title = asString(record?.title);
1102
+ if (!id || number === undefined || !title)
1103
+ return [];
1104
+ return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
1105
+ });
1106
+ }
988
1107
  async function resolveProjectStatusField(input) {
989
1108
  const query = `
990
1109
  query RigProjectStatusField($projectId: ID!) {
@@ -1079,6 +1198,7 @@ var DEFAULT_PROJECT_STATUSES = {
1079
1198
  running: "In Progress",
1080
1199
  prOpen: "In Review",
1081
1200
  ciFixing: "In Review",
1201
+ merging: "Merging",
1082
1202
  done: "Done",
1083
1203
  needsAttention: "Needs Attention"
1084
1204
  };
@@ -1092,6 +1212,8 @@ function lifecycleStatusForTaskStatus(status) {
1092
1212
  return "prOpen";
1093
1213
  if (normalized === "ci_fixing" || normalized === "fixing")
1094
1214
  return "ciFixing";
1215
+ if (normalized === "merging" || normalized === "merge")
1216
+ return "merging";
1095
1217
  if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
1096
1218
  return "needsAttention";
1097
1219
  if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
@@ -1167,7 +1289,9 @@ var TERMINAL_RUN_STATUSES2 = new Set([
1167
1289
  "needs-attention",
1168
1290
  "stopped"
1169
1291
  ]);
1170
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
1292
+ var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
1293
+ var EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running", "needs_attention"]);
1294
+ var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
1171
1295
 
1172
1296
  // packages/server/src/server-helpers/ws-router.ts
1173
1297
  import {
@@ -1328,7 +1452,7 @@ if (false) {}
1328
1452
  // packages/server/src/server-helpers/http-router.ts
1329
1453
  import {
1330
1454
  listAuthorityRuns as listAuthorityRuns7,
1331
- readAuthorityRun as readAuthorityRun8,
1455
+ readAuthorityRun as readAuthorityRun9,
1332
1456
  resolveAuthorityPaths,
1333
1457
  writeJsonFile as writeJsonFile4
1334
1458
  } from "@rig/runtime/control-plane/authority-files";
@@ -1348,10 +1472,64 @@ import {
1348
1472
  RemoteWsClient as RemoteWsClient2
1349
1473
  } from "@rig/runtime/control-plane/remote";
1350
1474
 
1475
+ // packages/server/src/server-helpers/pi-session-proxy.ts
1476
+ import { readAuthorityRun as readAuthorityRun7 } from "@rig/runtime/control-plane/authority-files";
1477
+ function resolveRunPiSessionProxy(projectRoot, runId) {
1478
+ const run = readAuthorityRun7(projectRoot, runId);
1479
+ if (!run)
1480
+ return null;
1481
+ const privateMetadata = readRunPiSessionMetadata(projectRoot, runId);
1482
+ if (!privateMetadata)
1483
+ return { pending: true, runId, status: typeof run.status === "string" ? run.status : undefined };
1484
+ const connection = privateMetadata.daemonConnection;
1485
+ if (connection.mode !== "http")
1486
+ throw new Error("Only loopback HTTP Rig Pi session daemon connections are supported");
1487
+ const token = tokenFromRef(connection.tokenRef);
1488
+ if (!token)
1489
+ throw new Error("Rig Pi session daemon token is unavailable");
1490
+ return {
1491
+ runId,
1492
+ sessionId: privateMetadata.public.sessionId,
1493
+ metadata: privateMetadata.public,
1494
+ privateMetadata,
1495
+ baseUrl: connection.baseUrl.replace(/\/+$/, ""),
1496
+ token
1497
+ };
1498
+ }
1499
+ async function proxyRunPiHttp(projectRoot, runId, input) {
1500
+ const resolved = resolveRunPiSessionProxy(projectRoot, runId);
1501
+ if (resolved === null)
1502
+ return { status: 404, payload: { ok: false, error: "Run not found" } };
1503
+ if ("pending" in resolved)
1504
+ return { status: 409, payload: { ready: false, runId, status: resolved.status, retryAfterMs: 500 } };
1505
+ const response = await fetch(`${resolved.baseUrl}${input.daemonPath}`, {
1506
+ method: input.method,
1507
+ headers: {
1508
+ authorization: `Bearer ${resolved.token}`,
1509
+ ...input.body === undefined ? {} : { "content-type": "application/json" }
1510
+ },
1511
+ body: input.body === undefined ? undefined : JSON.stringify(input.body)
1512
+ });
1513
+ const text = await response.text();
1514
+ const payload = text.trim() ? JSON.parse(text) : null;
1515
+ return { status: response.status, payload };
1516
+ }
1517
+ function buildRunPiDaemonWebSocketUrl(resolved) {
1518
+ const url = new URL(`${resolved.baseUrl}/sessions/${encodeURIComponent(resolved.sessionId)}/events`);
1519
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1520
+ url.searchParams.set("token", resolved.token);
1521
+ return url.toString();
1522
+ }
1523
+ function tokenFromRef(ref) {
1524
+ if (ref.startsWith("inline:"))
1525
+ return ref.slice("inline:".length) || null;
1526
+ return null;
1527
+ }
1528
+
1351
1529
  // packages/server/src/server-helpers/run-steering.ts
1352
- import { dirname as dirname3, resolve as resolve8 } from "path";
1530
+ import { dirname as dirname4, resolve as resolve8 } from "path";
1353
1531
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "fs";
1354
- import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun7, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
1532
+ import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun8, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
1355
1533
  var steeringSequence = 0;
1356
1534
  function runSteeringPath(projectRoot, runId) {
1357
1535
  return resolve8(resolveAuthorityRunDir4(projectRoot, runId), "steering.jsonl");
@@ -1420,7 +1598,7 @@ function markQueuedRunSteeringMessagesDelivered(projectRoot, runId, ids) {
1420
1598
  return delivered;
1421
1599
  }
1422
1600
  function queueRunSteeringMessage(projectRoot, runId, input) {
1423
- const run = readAuthorityRun7(projectRoot, runId);
1601
+ const run = readAuthorityRun8(projectRoot, runId);
1424
1602
  if (!run)
1425
1603
  throw new Error(`Run not found: ${runId}`);
1426
1604
  const text = input.message.trim();
@@ -1436,7 +1614,7 @@ function queueRunSteeringMessage(projectRoot, runId, input) {
1436
1614
  delivered: false
1437
1615
  };
1438
1616
  const path = runSteeringPath(projectRoot, runId);
1439
- mkdirSync4(dirname3(path), { recursive: true });
1617
+ mkdirSync4(dirname4(path), { recursive: true });
1440
1618
  appendJsonlRecord2(path, entry);
1441
1619
  appendRunTimelineEntry(projectRoot, runId, {
1442
1620
  id: entry.id,
@@ -1473,24 +1651,205 @@ import {
1473
1651
  updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
1474
1652
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
1475
1653
 
1654
+ // packages/server/src/server-helpers/github-api-session-index.ts
1655
+ import { chmodSync as chmodSync3, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
1656
+ import { dirname as dirname6, resolve as resolve10 } from "path";
1657
+
1658
+ // packages/server/src/server-helpers/github-user-namespace.ts
1659
+ import { chmodSync as chmodSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
1660
+ import { dirname as dirname5, isAbsolute, relative, resolve as resolve9 } from "path";
1661
+ function cleanString3(value) {
1662
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
1663
+ }
1664
+ function sanitizePathSegment(value) {
1665
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
1666
+ }
1667
+ function deriveGitHubUserNamespaceKey(identity) {
1668
+ const userId = cleanString3(identity.userId);
1669
+ if (userId) {
1670
+ const safeId = sanitizePathSegment(userId);
1671
+ if (safeId)
1672
+ return `ghu-${safeId}`;
1673
+ }
1674
+ const login = cleanString3(identity.login);
1675
+ if (login) {
1676
+ const safeLogin = sanitizePathSegment(login);
1677
+ if (safeLogin)
1678
+ return `ghu-login-${safeLogin}`;
1679
+ }
1680
+ throw new Error("GitHub user namespace requires a user id or login");
1681
+ }
1682
+ function resolveRemoteUserNamespacesRoot(projectRoot) {
1683
+ const explicitRoot = cleanString3(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
1684
+ if (explicitRoot)
1685
+ return resolve9(explicitRoot);
1686
+ const stateDir2 = cleanString3(process.env.RIG_STATE_DIR);
1687
+ if (stateDir2)
1688
+ return resolve9(dirname5(resolve9(stateDir2)), "users");
1689
+ return resolve9(projectRoot, ".rig", "users");
1690
+ }
1691
+ function resolveRemoteUserNamespace(projectRoot, identity) {
1692
+ const key = deriveGitHubUserNamespaceKey(identity);
1693
+ const root = resolve9(resolveRemoteUserNamespacesRoot(projectRoot), key);
1694
+ const stateDir2 = resolve9(root, ".rig", "state");
1695
+ return {
1696
+ key,
1697
+ userId: cleanString3(identity.userId),
1698
+ login: cleanString3(identity.login),
1699
+ root,
1700
+ stateDir: stateDir2,
1701
+ authStateFile: resolve9(stateDir2, "github-auth.json"),
1702
+ metadataFile: resolve9(stateDir2, "user-namespace.json"),
1703
+ checkoutBaseDir: resolve9(root, "remote-checkouts"),
1704
+ snapshotBaseDir: resolve9(root, "remote-snapshots")
1705
+ };
1706
+ }
1707
+ function serializeRemoteUserNamespace(namespace) {
1708
+ return {
1709
+ key: namespace.key,
1710
+ userId: namespace.userId,
1711
+ login: namespace.login,
1712
+ root: namespace.root,
1713
+ checkoutBaseDir: namespace.checkoutBaseDir,
1714
+ snapshotBaseDir: namespace.snapshotBaseDir
1715
+ };
1716
+ }
1717
+ function isPathInsideNamespace(namespaceRoot, candidatePath) {
1718
+ const root = resolve9(namespaceRoot);
1719
+ const candidate = resolve9(candidatePath);
1720
+ const rel = relative(root, candidate);
1721
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1722
+ }
1723
+ function writeRemoteUserNamespaceMetadata(namespace) {
1724
+ mkdirSync5(namespace.stateDir, { recursive: true });
1725
+ const previous = (() => {
1726
+ if (!existsSync6(namespace.metadataFile))
1727
+ return null;
1728
+ try {
1729
+ const parsed = JSON.parse(readFileSync4(namespace.metadataFile, "utf8"));
1730
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
1731
+ } catch {
1732
+ return null;
1733
+ }
1734
+ })();
1735
+ const now = new Date().toISOString();
1736
+ writeFileSync5(namespace.metadataFile, `${JSON.stringify({
1737
+ key: namespace.key,
1738
+ userId: namespace.userId,
1739
+ login: namespace.login,
1740
+ root: namespace.root,
1741
+ checkoutBaseDir: namespace.checkoutBaseDir,
1742
+ snapshotBaseDir: namespace.snapshotBaseDir,
1743
+ createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
1744
+ updatedAt: now
1745
+ }, null, 2)}
1746
+ `, { encoding: "utf8", mode: 384 });
1747
+ try {
1748
+ chmodSync2(namespace.metadataFile, 384);
1749
+ } catch {}
1750
+ }
1751
+
1752
+ // packages/server/src/server-helpers/github-api-session-index.ts
1753
+ function cleanString4(value) {
1754
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
1755
+ }
1756
+ function resolveGitHubApiSessionIndexFile(projectRoot) {
1757
+ return resolve10(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
1758
+ }
1759
+ function parseEntry(value) {
1760
+ if (!value || typeof value !== "object" || Array.isArray(value))
1761
+ return null;
1762
+ const record = value;
1763
+ const token = cleanString4(record.token);
1764
+ const namespaceKey = cleanString4(record.namespaceKey);
1765
+ const namespaceRoot = cleanString4(record.namespaceRoot);
1766
+ const authStateFile = cleanString4(record.authStateFile);
1767
+ const checkoutBaseDir = cleanString4(record.checkoutBaseDir);
1768
+ const snapshotBaseDir = cleanString4(record.snapshotBaseDir);
1769
+ const createdAt = cleanString4(record.createdAt);
1770
+ if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
1771
+ return null;
1772
+ return {
1773
+ token,
1774
+ namespaceKey,
1775
+ namespaceRoot,
1776
+ authStateFile,
1777
+ checkoutBaseDir,
1778
+ snapshotBaseDir,
1779
+ createdAt,
1780
+ login: cleanString4(record.login),
1781
+ userId: cleanString4(record.userId),
1782
+ selectedRepo: cleanString4(record.selectedRepo)
1783
+ };
1784
+ }
1785
+ function readIndex(indexFile) {
1786
+ if (!existsSync7(indexFile))
1787
+ return [];
1788
+ try {
1789
+ const parsed = JSON.parse(readFileSync5(indexFile, "utf8"));
1790
+ return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
1791
+ const parsedEntry = parseEntry(entry);
1792
+ return parsedEntry ? [parsedEntry] : [];
1793
+ }) : [];
1794
+ } catch {
1795
+ return [];
1796
+ }
1797
+ }
1798
+ function writeIndex(indexFile, sessions) {
1799
+ mkdirSync6(dirname6(indexFile), { recursive: true });
1800
+ writeFileSync6(indexFile, `${JSON.stringify({ sessions }, null, 2)}
1801
+ `, { encoding: "utf8", mode: 384 });
1802
+ try {
1803
+ chmodSync3(indexFile, 384);
1804
+ } catch {}
1805
+ }
1806
+ function registerGitHubApiSession(input) {
1807
+ const cleanToken = cleanString4(input.token);
1808
+ if (!cleanToken)
1809
+ throw new Error("GitHub API session token is required");
1810
+ const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
1811
+ const createdAt = new Date().toISOString();
1812
+ const entry = {
1813
+ token: cleanToken,
1814
+ login: input.namespace.login,
1815
+ userId: input.namespace.userId,
1816
+ namespaceKey: input.namespace.key,
1817
+ namespaceRoot: input.namespace.root,
1818
+ authStateFile: input.namespace.authStateFile,
1819
+ checkoutBaseDir: input.namespace.checkoutBaseDir,
1820
+ snapshotBaseDir: input.namespace.snapshotBaseDir,
1821
+ selectedRepo: cleanString4(input.selectedRepo),
1822
+ createdAt
1823
+ };
1824
+ const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
1825
+ writeIndex(indexFile, [...previous.slice(-199), entry]);
1826
+ return entry;
1827
+ }
1828
+ function readGitHubApiSession(input) {
1829
+ const cleanToken = cleanString4(input.token);
1830
+ if (!cleanToken)
1831
+ return null;
1832
+ return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
1833
+ }
1834
+
1476
1835
  // packages/server/src/server-helpers/project-registry.ts
1477
1836
  import { createHash as createHash2 } from "crypto";
1478
1837
  import { spawnSync } from "child_process";
1479
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync5 } from "fs";
1480
- import { dirname as dirname4, resolve as resolve9 } from "path";
1838
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync7 } from "fs";
1839
+ import { dirname as dirname7, resolve as resolve11 } from "path";
1481
1840
  function normalizeRepoSlug(value) {
1482
1841
  const trimmed = value.trim();
1483
1842
  return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
1484
1843
  }
1485
1844
  function registryPath(projectRoot) {
1486
- return resolve9(projectRoot, ".rig", "state", "projects.json");
1845
+ return resolve11(projectRoot, ".rig", "state", "projects.json");
1487
1846
  }
1488
1847
  function readRegistry(projectRoot) {
1489
1848
  const path = registryPath(projectRoot);
1490
- if (!existsSync6(path))
1849
+ if (!existsSync8(path))
1491
1850
  return {};
1492
1851
  try {
1493
- const payload = JSON.parse(readFileSync4(path, "utf8"));
1852
+ const payload = JSON.parse(readFileSync6(path, "utf8"));
1494
1853
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
1495
1854
  return {};
1496
1855
  const projects = payload.projects;
@@ -1501,14 +1860,14 @@ function readRegistry(projectRoot) {
1501
1860
  }
1502
1861
  function writeRegistry(projectRoot, projects) {
1503
1862
  const path = registryPath(projectRoot);
1504
- mkdirSync5(dirname4(path), { recursive: true });
1505
- writeFileSync5(path, `${JSON.stringify({ projects }, null, 2)}
1863
+ mkdirSync7(dirname7(path), { recursive: true });
1864
+ writeFileSync7(path, `${JSON.stringify({ projects }, null, 2)}
1506
1865
  `, "utf8");
1507
1866
  }
1508
1867
  function resolveConfigPath(projectRoot) {
1509
1868
  for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
1510
- const path = resolve9(projectRoot, name);
1511
- if (existsSync6(path))
1869
+ const path = resolve11(projectRoot, name);
1870
+ if (existsSync8(path))
1512
1871
  return path;
1513
1872
  }
1514
1873
  return null;
@@ -1517,7 +1876,7 @@ function hashFile(path) {
1517
1876
  if (!path)
1518
1877
  return null;
1519
1878
  try {
1520
- return createHash2("sha256").update(readFileSync4(path)).digest("hex");
1879
+ return createHash2("sha256").update(readFileSync6(path)).digest("hex");
1521
1880
  } catch {
1522
1881
  return null;
1523
1882
  }
@@ -1533,11 +1892,11 @@ function readDefaultBranch(projectRoot) {
1533
1892
  return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
1534
1893
  }
1535
1894
  function buildRunSummary(projectRoot) {
1536
- const runsDir = resolve9(projectRoot, ".rig", "runs");
1895
+ const runsDir = resolve11(projectRoot, ".rig", "runs");
1537
1896
  try {
1538
1897
  const runs = readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
1539
1898
  try {
1540
- const run = JSON.parse(readFileSync4(resolve9(runsDir, entry.name, "run.json"), "utf8"));
1899
+ const run = JSON.parse(readFileSync6(resolve11(runsDir, entry.name, "run.json"), "utf8"));
1541
1900
  return [{ runId: typeof run.runId === "string" ? run.runId : entry.name, status: typeof run.status === "string" ? run.status : "unknown", updatedAt: typeof run.updatedAt === "string" ? run.updatedAt : "" }];
1542
1901
  } catch {
1543
1902
  return [];
@@ -1589,10 +1948,14 @@ function upsertProjectRecord(projectRoot, input) {
1589
1948
  function linkProjectCheckout(projectRoot, repoSlug, checkout) {
1590
1949
  return upsertProjectRecord(projectRoot, { repoSlug, checkout });
1591
1950
  }
1951
+ function projectRegistryContainsCheckout(projectRoot, checkoutPath) {
1952
+ const target = resolve11(checkoutPath);
1953
+ return Object.values(readRegistry(projectRoot)).some((project) => project.checkouts.some((checkout) => checkout.path ? resolve11(checkout.path) === target : false));
1954
+ }
1592
1955
 
1593
1956
  // packages/server/src/server-helpers/remote-checkout.ts
1594
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
1595
- import { dirname as dirname5, isAbsolute, relative, resolve as resolve10 } from "path";
1957
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
1958
+ import { dirname as dirname8, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve12 } from "path";
1596
1959
  function safeSlugSegments(repoSlug) {
1597
1960
  const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
1598
1961
  if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
@@ -1609,7 +1972,7 @@ function safeCheckoutKey(value) {
1609
1972
  }
1610
1973
  function repoSlugPath(baseDir, repoSlug, checkoutKey) {
1611
1974
  const key = safeCheckoutKey(checkoutKey);
1612
- return resolve10(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
1975
+ return resolve12(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
1613
1976
  }
1614
1977
  function sanitizeSnapshotId(value, fallback) {
1615
1978
  const raw = (value ?? fallback).trim();
@@ -1617,7 +1980,7 @@ function sanitizeSnapshotId(value, fallback) {
1617
1980
  return safe || fallback;
1618
1981
  }
1619
1982
  function assertWithinRoot(root, relativePath) {
1620
- if (!relativePath || isAbsolute(relativePath) || relativePath.includes("\x00")) {
1983
+ if (!relativePath || isAbsolute2(relativePath) || relativePath.includes("\x00")) {
1621
1984
  throw new Error(`Invalid snapshot file path: ${relativePath}`);
1622
1985
  }
1623
1986
  const normalizedRelative = relativePath.replace(/\\/g, "/");
@@ -1625,9 +1988,9 @@ function assertWithinRoot(root, relativePath) {
1625
1988
  if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
1626
1989
  throw new Error(`Unsafe snapshot file path: ${relativePath}`);
1627
1990
  }
1628
- const target = resolve10(root, ...segments);
1629
- const rel = relative(root, target);
1630
- if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute(rel)) {
1991
+ const target = resolve12(root, ...segments);
1992
+ const rel = relative2(root, target);
1993
+ if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute2(rel)) {
1631
1994
  throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
1632
1995
  }
1633
1996
  return target;
@@ -1645,17 +2008,17 @@ function parseSnapshotArchiveContentBase64(contentBase64) {
1645
2008
  function extractUploadedSnapshotArchive(input) {
1646
2009
  const archive = decodeSnapshotArchive(input.archive);
1647
2010
  const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
1648
- const checkoutPath = resolve10(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
1649
- mkdirSync6(checkoutPath, { recursive: true });
2011
+ const checkoutPath = resolve12(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
2012
+ mkdirSync8(checkoutPath, { recursive: true });
1650
2013
  for (const file of archive.files) {
1651
2014
  if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
1652
2015
  throw new Error("Invalid snapshot archive file entry");
1653
2016
  }
1654
2017
  const target = assertWithinRoot(checkoutPath, file.path);
1655
- mkdirSync6(dirname5(target), { recursive: true });
1656
- writeFileSync6(target, Buffer.from(file.contentBase64, "base64"));
2018
+ mkdirSync8(dirname8(target), { recursive: true });
2019
+ writeFileSync8(target, Buffer.from(file.contentBase64, "base64"));
1657
2020
  }
1658
- writeFileSync6(resolve10(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
2021
+ writeFileSync8(resolve12(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
1659
2022
  repoSlug: input.repoSlug,
1660
2023
  snapshotId,
1661
2024
  fileCount: archive.files.length,
@@ -1690,7 +2053,7 @@ function gitCredentialConfig(token) {
1690
2053
  };
1691
2054
  }
1692
2055
  async function prepareRemoteCheckout(input) {
1693
- const exists = input.exists ?? existsSync7;
2056
+ const exists = input.exists ?? existsSync9;
1694
2057
  const strategy = input.strategy;
1695
2058
  if (strategy.kind === "uploaded-snapshot") {
1696
2059
  return extractUploadedSnapshotArchive({
@@ -1702,7 +2065,7 @@ async function prepareRemoteCheckout(input) {
1702
2065
  });
1703
2066
  }
1704
2067
  if (strategy.kind === "existing-path") {
1705
- const checkoutPath2 = resolve10(strategy.path);
2068
+ const checkoutPath2 = resolve12(strategy.path);
1706
2069
  if (!exists(checkoutPath2)) {
1707
2070
  throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
1708
2071
  }
@@ -1738,9 +2101,9 @@ function buildServerControlStatus() {
1738
2101
  };
1739
2102
  }
1740
2103
  function buildProjectConfigStatus(root) {
1741
- const hasConfigTs = existsSync8(resolve11(root, "rig.config.ts"));
1742
- const hasConfigJson = existsSync8(resolve11(root, "rig.config.json"));
1743
- const hasLegacyTaskConfig = existsSync8(resolve11(root, ".rig", "task-config.json"));
2104
+ const hasConfigTs = existsSync10(resolve13(root, "rig.config.ts"));
2105
+ const hasConfigJson = existsSync10(resolve13(root, "rig.config.json"));
2106
+ const hasLegacyTaskConfig = existsSync10(resolve13(root, ".rig", "task-config.json"));
1744
2107
  let kind = "missing";
1745
2108
  if (hasConfigTs)
1746
2109
  kind = "rig-config-ts";
@@ -1757,6 +2120,75 @@ function buildProjectConfigStatus(root) {
1757
2120
  suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
1758
2121
  };
1759
2122
  }
2123
+ var RIG_GITHUB_LIFECYCLE_LABELS = [
2124
+ "ready",
2125
+ "blocked",
2126
+ "in-progress",
2127
+ "under-review",
2128
+ "failed",
2129
+ "cancelled",
2130
+ "rig:running",
2131
+ "rig:pr-open",
2132
+ "rig:ci-fixing",
2133
+ "rig:merging",
2134
+ "rig:done",
2135
+ "rig:needs-attention"
2136
+ ];
2137
+ function githubProjectsEnabled(config) {
2138
+ if (!config || typeof config !== "object" || Array.isArray(config))
2139
+ return false;
2140
+ const root = config;
2141
+ const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
2142
+ const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
2143
+ return projects?.enabled === true;
2144
+ }
2145
+ function githubIssueSourceRepo(config) {
2146
+ if (!config || typeof config !== "object" || Array.isArray(config))
2147
+ return null;
2148
+ const root = config;
2149
+ const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
2150
+ const owner = normalizeString(taskSource?.owner);
2151
+ const repo = normalizeString(taskSource?.repo);
2152
+ if (taskSource?.kind === "github-issues" && owner && repo)
2153
+ return { owner, repo };
2154
+ const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
2155
+ const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
2156
+ const match = slug?.match(/^([^/]+)\/([^/]+)$/);
2157
+ return match ? { owner: match[1], repo: match[2] } : null;
2158
+ }
2159
+ async function ensureGitHubLifecycleLabels(projectRoot, config) {
2160
+ const repo = githubIssueSourceRepo(config);
2161
+ if (!repo)
2162
+ return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
2163
+ const token = createGitHubAuthStore(projectRoot).readToken();
2164
+ if (!token)
2165
+ return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
2166
+ const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
2167
+ headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
2168
+ });
2169
+ const existingJson = await existingResponse.json().catch(() => []);
2170
+ const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
2171
+ const created = [];
2172
+ const alreadyPresent = [];
2173
+ const failed = [];
2174
+ for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
2175
+ if (existing.has(label)) {
2176
+ alreadyPresent.push(label);
2177
+ continue;
2178
+ }
2179
+ const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
2180
+ method: "POST",
2181
+ headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
2182
+ body: JSON.stringify({ name: label, color: label.startsWith("rig:") ? "6f42c1" : "ededed", description: label.startsWith("rig:") ? "Task status managed by Rig" : "Task lifecycle status managed by Rig" })
2183
+ });
2184
+ if (response.ok || response.status === 422) {
2185
+ (response.status === 422 ? alreadyPresent : created).push(label);
2186
+ } else {
2187
+ failed.push({ label, error: await response.text().catch(() => response.statusText) });
2188
+ }
2189
+ }
2190
+ return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
2191
+ }
1760
2192
  function normalizeCommit(value) {
1761
2193
  const raw = normalizeString(value);
1762
2194
  return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
@@ -1776,24 +2208,24 @@ function repoParts(repoSlug) {
1776
2208
  return { owner, repo, slug: `${owner}/${repo}` };
1777
2209
  }
1778
2210
  function repairDir(checkoutPath) {
1779
- const dir = resolve11(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
1780
- mkdirSync7(dir, { recursive: true });
2211
+ const dir = resolve13(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
2212
+ mkdirSync9(dir, { recursive: true });
1781
2213
  return dir;
1782
2214
  }
1783
2215
  function backupCheckoutFile(checkoutPath, relativePath) {
1784
- const source = resolve11(checkoutPath, relativePath);
1785
- const backupPath = resolve11(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
1786
- mkdirSync7(dirname6(backupPath), { recursive: true });
1787
- copyFileSync(source, backupPath);
2216
+ const source = resolve13(checkoutPath, relativePath);
2217
+ const backupPath = resolve13(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
2218
+ mkdirSync9(dirname9(backupPath), { recursive: true });
2219
+ copyFileSync2(source, backupPath);
1788
2220
  return backupPath;
1789
2221
  }
1790
2222
  function parsePackageJsonLosslessly(checkoutPath) {
1791
- const packagePath = resolve11(checkoutPath, "package.json");
1792
- if (!existsSync8(packagePath)) {
2223
+ const packagePath = resolve13(checkoutPath, "package.json");
2224
+ if (!existsSync10(packagePath)) {
1793
2225
  return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
1794
2226
  }
1795
2227
  try {
1796
- const parsed = JSON.parse(readFileSync5(packagePath, "utf8"));
2228
+ const parsed = JSON.parse(readFileSync7(packagePath, "utf8"));
1797
2229
  return {
1798
2230
  existed: true,
1799
2231
  packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
@@ -1807,9 +2239,9 @@ function parsePackageJsonLosslessly(checkoutPath) {
1807
2239
  }
1808
2240
  }
1809
2241
  function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
1810
- const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync8(resolve11(checkoutPath, name)));
1811
- const packagePath = resolve11(checkoutPath, "package.json");
1812
- if (!hasConfig && !existsSync8(packagePath)) {
2242
+ const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync10(resolve13(checkoutPath, name)));
2243
+ const packagePath = resolve13(checkoutPath, "package.json");
2244
+ if (!hasConfig && !existsSync10(packagePath)) {
1813
2245
  return { skipped: true, reason: "package.json and rig.config missing" };
1814
2246
  }
1815
2247
  const parsed = parsePackageJsonLosslessly(checkoutPath);
@@ -1828,7 +2260,7 @@ function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
1828
2260
  }
1829
2261
  const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
1830
2262
  if (changed) {
1831
- writeFileSync7(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
2263
+ writeFileSync9(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
1832
2264
  `, "utf8");
1833
2265
  }
1834
2266
  return {
@@ -1854,11 +2286,11 @@ function configLooksStructurallyUsable(source) {
1854
2286
  return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
1855
2287
  }
1856
2288
  function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
1857
- const configPath = resolve11(checkoutPath, "rig.config.ts");
1858
- const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync8(resolve11(checkoutPath, name)));
2289
+ const configPath = resolve13(checkoutPath, "rig.config.ts");
2290
+ const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync10(resolve13(checkoutPath, name)));
1859
2291
  if (existingConfigName) {
1860
- const existingPath = resolve11(checkoutPath, existingConfigName);
1861
- const source = readFileSync5(existingPath, "utf8");
2292
+ const existingPath = resolve13(checkoutPath, existingConfigName);
2293
+ const source = readFileSync7(existingPath, "utf8");
1862
2294
  if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
1863
2295
  return { path: existingPath, changed: false, reason: "config structurally complete" };
1864
2296
  }
@@ -1872,7 +2304,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
1872
2304
  }
1873
2305
  }
1874
2306
  const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
1875
- writeFileSync7(configPath, generatedRigConfigSource(repoSlug), "utf8");
2307
+ writeFileSync9(configPath, generatedRigConfigSource(repoSlug), "utf8");
1876
2308
  return {
1877
2309
  path: configPath,
1878
2310
  changed: true,
@@ -1881,7 +2313,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
1881
2313
  };
1882
2314
  }
1883
2315
  function validateRemoteCheckoutRigConfig(checkoutPath) {
1884
- const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync8(resolve11(checkoutPath, name)));
2316
+ const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync10(resolve13(checkoutPath, name)));
1885
2317
  if (!configFile)
1886
2318
  return { ok: false, error: "missing rig config" };
1887
2319
  if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
@@ -1903,7 +2335,7 @@ function validateRemoteCheckoutRigConfig(checkoutPath) {
1903
2335
  return { ok: true, configFile };
1904
2336
  }
1905
2337
  function installRemoteCheckoutPackages(checkoutPath) {
1906
- if (!existsSync8(resolve11(checkoutPath, "package.json"))) {
2338
+ if (!existsSync10(resolve13(checkoutPath, "package.json"))) {
1907
2339
  return { skipped: true, reason: "package.json missing" };
1908
2340
  }
1909
2341
  if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
@@ -1916,8 +2348,8 @@ function installRemoteCheckoutPackages(checkoutPath) {
1916
2348
  return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
1917
2349
  }
1918
2350
  function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
1919
- const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync8(resolve11(checkoutPath, name)));
1920
- const hasPackage = existsSync8(resolve11(checkoutPath, "package.json"));
2351
+ const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync10(resolve13(checkoutPath, name)));
2352
+ const hasPackage = existsSync10(resolve13(checkoutPath, "package.json"));
1921
2353
  if (!hasConfig && !hasPackage) {
1922
2354
  return {
1923
2355
  packageJson: { skipped: true, reason: "package.json and rig.config missing" },
@@ -2015,26 +2447,26 @@ function buildRemoteRunLogEntry(body, identifiers) {
2015
2447
  }
2016
2448
  function readGitHeadCommit(projectRoot) {
2017
2449
  try {
2018
- let gitDir = resolve11(projectRoot, ".git");
2450
+ let gitDir = resolve13(projectRoot, ".git");
2019
2451
  try {
2020
- const dotGit = readFileSync5(gitDir, "utf8").trim();
2452
+ const dotGit = readFileSync7(gitDir, "utf8").trim();
2021
2453
  const gitDirPrefix = "gitdir:";
2022
2454
  if (dotGit.startsWith(gitDirPrefix)) {
2023
- gitDir = resolve11(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
2455
+ gitDir = resolve13(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
2024
2456
  }
2025
2457
  } catch {}
2026
- const head = readFileSync5(resolve11(gitDir, "HEAD"), "utf8").trim();
2458
+ const head = readFileSync7(resolve13(gitDir, "HEAD"), "utf8").trim();
2027
2459
  const refPrefix = "ref:";
2028
2460
  if (!head.startsWith(refPrefix)) {
2029
2461
  return normalizeCommit(head);
2030
2462
  }
2031
2463
  const ref = head.slice(refPrefix.length).trim();
2032
- const refPath = resolve11(gitDir, ref);
2033
- if (existsSync8(refPath)) {
2034
- return normalizeCommit(readFileSync5(refPath, "utf8").trim());
2464
+ const refPath = resolve13(gitDir, ref);
2465
+ if (existsSync10(refPath)) {
2466
+ return normalizeCommit(readFileSync7(refPath, "utf8").trim());
2035
2467
  }
2036
- const commonDir = normalizeString(readFileSync5(resolve11(gitDir, "commondir"), "utf8"));
2037
- return commonDir ? normalizeCommit(readFileSync5(resolve11(gitDir, commonDir, ref), "utf8").trim()) : null;
2468
+ const commonDir = normalizeString(readFileSync7(resolve13(gitDir, "commondir"), "utf8"));
2469
+ return commonDir ? normalizeCommit(readFileSync7(resolve13(gitDir, commonDir, ref), "utf8").trim()) : null;
2038
2470
  } catch {
2039
2471
  return null;
2040
2472
  }
@@ -2054,7 +2486,8 @@ function isAuthorizedInspectorStreamRequest(req, authToken) {
2054
2486
  }
2055
2487
  function buildDeploymentStatus(projectRoot) {
2056
2488
  const envCommit = normalizeCommit(process.env.RIG_COMMIT_SHA ?? process.env.GITHUB_SHA ?? process.env.VERCEL_GIT_COMMIT_SHA ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.COMMIT_SHA);
2057
- const gitCommit = envCommit ?? readGitHeadCommit(projectRoot);
2489
+ const deploymentRoot = normalizeString(process.env.RIG_HOST_PROJECT_ROOT) ?? projectRoot;
2490
+ const gitCommit = envCommit ?? readGitHeadCommit(deploymentRoot) ?? readGitHeadCommit(projectRoot);
2058
2491
  return {
2059
2492
  currentCommit: gitCommit,
2060
2493
  commitSource: envCommit ? "env" : gitCommit ? "git" : null,
@@ -2072,9 +2505,9 @@ function configuredRepoFromTaskSource(taskSource) {
2072
2505
  const repo = normalizeString(taskSource?.repo);
2073
2506
  return owner && repo ? `${owner}/${repo}` : null;
2074
2507
  }
2075
- async function buildTaskSourceStatus(state, config) {
2508
+ async function buildTaskSourceStatus(state, config, requestAuth) {
2076
2509
  const diagnostics = state.snapshotService.getTaskSourceErrors();
2077
- const selectedRepo = createGitHubAuthStore(state.projectRoot).status({
2510
+ const selectedRepo = requestScopedAuthStore(state.projectRoot, requestAuth).status({
2078
2511
  oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
2079
2512
  }).selectedRepo;
2080
2513
  try {
@@ -2127,37 +2560,146 @@ function isLoopbackRequest(req) {
2127
2560
  }
2128
2561
  }
2129
2562
  function isPublicRigAuthBootstrapRoute(pathname) {
2130
- return pathname === "/" || pathname === "/health" || pathname === "/api/health" || pathname === "/api/github/auth/status" || pathname === "/api/github/auth/token" || pathname === "/api/github/auth/device/start" || pathname === "/api/github/auth/device/poll";
2563
+ return pathname === "/" || pathname === "/install" || pathname === "/health" || pathname === "/api/health" || pathname === "/api/github/auth/status" || pathname === "/api/github/auth/token" || pathname === "/api/github/auth/device/start" || pathname === "/api/github/auth/device/poll";
2564
+ }
2565
+ function buildRigInstallScript() {
2566
+ return `#!/usr/bin/env bash
2567
+ set -euo pipefail
2568
+
2569
+ say() {
2570
+ printf 'rig-install: %s
2571
+ ' "$*"
2572
+ }
2573
+
2574
+ if ! command -v bun >/dev/null 2>&1; then
2575
+ say "Bun not found; installing Bun first"
2576
+ curl -fsSL https://bun.sh/install | bash
2577
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
2578
+ export PATH="$BUN_INSTALL/bin:$PATH"
2579
+ fi
2580
+
2581
+ if ! command -v bun >/dev/null 2>&1; then
2582
+ printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
2583
+ ' >&2
2584
+ exit 1
2585
+ fi
2586
+
2587
+ say "Installing @h-rig/cli@latest"
2588
+ bun add -g --force @h-rig/cli@latest
2589
+
2590
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
2591
+ BUN_RIG="$BUN_INSTALL/bin/rig"
2592
+ if [ ! -x "$BUN_RIG" ]; then
2593
+ printf 'rig-install: expected Bun global rig at %s but it was not executable.
2594
+ ' "$BUN_RIG" >&2
2595
+ exit 1
2596
+ fi
2597
+
2598
+ USER_BIN="$HOME/.local/bin"
2599
+ mkdir -p "$USER_BIN"
2600
+ cat > "$USER_BIN/rig" <<'RIG_SHIM'
2601
+ #!/usr/bin/env bash
2602
+ set -euo pipefail
2603
+ exec "\${BUN_INSTALL:-$HOME/.bun}/bin/rig" "$@"
2604
+ RIG_SHIM
2605
+ chmod +x "$USER_BIN/rig"
2606
+
2607
+ export PATH="$USER_BIN:$BUN_INSTALL/bin:$PATH"
2608
+ if command -v hash >/dev/null 2>&1; then hash -r; fi
2609
+
2610
+ if ! command -v rig >/dev/null 2>&1; then
2611
+ printf 'rig-install: rig installed, but rig is not on PATH. Add %s and %s/bin to PATH and retry.
2612
+ ' "$USER_BIN" "$BUN_INSTALL" >&2
2613
+ exit 1
2614
+ fi
2615
+
2616
+ say "Verifying rig"
2617
+ "$BUN_RIG" --help >/dev/null
2618
+ rig --help >/dev/null
2619
+ say "Done. Run: rig --help"
2620
+ `;
2131
2621
  }
2132
2622
  function normalizePrMode(value) {
2133
2623
  const mode = normalizeString(value);
2134
2624
  return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
2135
2625
  }
2626
+ function requestAuthResult(input) {
2627
+ return {
2628
+ authorized: input.authorized,
2629
+ actor: input.actor ?? null,
2630
+ reason: input.reason,
2631
+ login: input.login ?? null,
2632
+ userId: input.userId ?? null,
2633
+ userNamespace: input.userNamespace ?? null,
2634
+ authStateFile: input.authStateFile ?? null
2635
+ };
2636
+ }
2637
+ function namespaceFromSessionIndex(entry) {
2638
+ const stateDir2 = dirname9(entry.authStateFile);
2639
+ return {
2640
+ key: entry.namespaceKey,
2641
+ userId: entry.userId,
2642
+ login: entry.login,
2643
+ root: entry.namespaceRoot,
2644
+ stateDir: stateDir2,
2645
+ authStateFile: entry.authStateFile,
2646
+ metadataFile: resolve13(stateDir2, "user-namespace.json"),
2647
+ checkoutBaseDir: entry.checkoutBaseDir,
2648
+ snapshotBaseDir: entry.snapshotBaseDir
2649
+ };
2650
+ }
2136
2651
  function authorizeRigHttpRequest(input) {
2137
2652
  if (input.legacyAuthorized) {
2138
- return { authorized: true, actor: "rig-local-server", reason: "server-token" };
2653
+ return requestAuthResult({ authorized: true, actor: "rig-local-server", reason: "server-token" });
2139
2654
  }
2140
2655
  const bearer = bearerTokenFromRequest(input.req);
2141
2656
  const store = createGitHubAuthStore(input.projectRoot);
2142
2657
  const storedToken = store.readToken();
2143
2658
  const session = bearer ? store.readApiSession(bearer) : null;
2144
2659
  if (session) {
2145
- return { authorized: true, actor: session.login ?? "github-operator", reason: "github-session" };
2660
+ return requestAuthResult({
2661
+ authorized: true,
2662
+ actor: session.login ?? "github-operator",
2663
+ reason: "github-session",
2664
+ login: session.login,
2665
+ userId: session.userId,
2666
+ authStateFile: store.stateFile
2667
+ });
2146
2668
  }
2147
2669
  if (bearer && storedToken && bearer === storedToken) {
2148
2670
  const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
2149
- return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
2671
+ return requestAuthResult({
2672
+ authorized: true,
2673
+ actor: status.login ?? "github-operator",
2674
+ reason: "github-token",
2675
+ login: status.login,
2676
+ userId: status.userId,
2677
+ authStateFile: store.stateFile
2678
+ });
2679
+ }
2680
+ const indexedSession = readGitHubApiSession({ projectRoot: input.projectRoot, token: bearer });
2681
+ if (indexedSession) {
2682
+ const userNamespace = namespaceFromSessionIndex(indexedSession);
2683
+ return requestAuthResult({
2684
+ authorized: true,
2685
+ actor: indexedSession.login ?? "github-operator",
2686
+ reason: "github-user-session",
2687
+ login: indexedSession.login,
2688
+ userId: indexedSession.userId,
2689
+ userNamespace,
2690
+ authStateFile: indexedSession.authStateFile
2691
+ });
2150
2692
  }
2151
2693
  if (isPublicRigAuthBootstrapRoute(input.pathname)) {
2152
- return { authorized: true, actor: null, reason: "public-bootstrap" };
2694
+ return requestAuthResult({ authorized: true, actor: null, reason: "public-bootstrap" });
2153
2695
  }
2154
2696
  if (!input.serverAuthToken && !storedToken) {
2155
2697
  if (isLoopbackRequest(input.req)) {
2156
- return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
2698
+ return requestAuthResult({ authorized: true, actor: null, reason: "loopback-dev-no-auth" });
2157
2699
  }
2158
- return { authorized: false, actor: null, reason: "auth-required" };
2700
+ return requestAuthResult({ authorized: false, actor: null, reason: "auth-required" });
2159
2701
  }
2160
- return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
2702
+ return requestAuthResult({ authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" });
2161
2703
  }
2162
2704
  async function fetchGitHubUserInfo(token) {
2163
2705
  const response = await fetch("https://api.github.com/user", {
@@ -2177,6 +2719,67 @@ async function fetchGitHubUserInfo(token) {
2177
2719
  scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
2178
2720
  };
2179
2721
  }
2722
+ function shouldWriteRootAuthCompat(projectRoot) {
2723
+ if (process.env.RIG_REMOTE_USER_NAMESPACE_ROOT?.trim())
2724
+ return false;
2725
+ const stateDir2 = normalizeString(process.env.RIG_STATE_DIR);
2726
+ if (!stateDir2)
2727
+ return true;
2728
+ return resolve13(stateDir2) === resolve13(projectRoot, ".rig", "state");
2729
+ }
2730
+ function requestScopedRegistryRoot(stateProjectRoot, requestAuth) {
2731
+ return requestAuth.userNamespace?.root ?? stateProjectRoot;
2732
+ }
2733
+ function requestScopedAuthStore(stateProjectRoot, requestAuth) {
2734
+ return requestAuth.authStateFile ? createGitHubAuthStoreFromStateFile(requestAuth.authStateFile) : createGitHubAuthStore(stateProjectRoot);
2735
+ }
2736
+ function userNamespaceResponse(namespace) {
2737
+ return namespace ? serializeRemoteUserNamespace(namespace) : undefined;
2738
+ }
2739
+ function resolveNamespacedBaseDir(input) {
2740
+ if (input.explicitBaseDir)
2741
+ return input.explicitBaseDir;
2742
+ const envBase = normalizeString(process.env[input.envName]);
2743
+ if (input.userNamespace) {
2744
+ return envBase ? resolve13(envBase, input.userNamespace.key) : input.userNamespace[input.legacySubdir === "remote-checkouts" ? "checkoutBaseDir" : "snapshotBaseDir"];
2745
+ }
2746
+ return envBase ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve13(normalizeString(process.env.RIG_STATE_DIR), input.legacySubdir) : resolve13(input.legacyProjectRoot, ".rig", input.legacySubdir));
2747
+ }
2748
+ function explicitCheckoutKey(body, checkoutInput, requestAuth) {
2749
+ return normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? (requestAuth.userNamespace ? undefined : "default");
2750
+ }
2751
+ function saveGitHubTokenForRemoteUser(input) {
2752
+ const namespace = resolveRemoteUserNamespace(input.projectRoot, { userId: input.user.userId, login: input.user.login });
2753
+ writeRemoteUserNamespaceMetadata(namespace);
2754
+ const store = createGitHubAuthStoreFromStateFile(namespace.authStateFile);
2755
+ store.saveToken({
2756
+ token: input.token,
2757
+ tokenSource: input.tokenSource,
2758
+ login: input.user.login,
2759
+ userId: input.user.userId,
2760
+ scopes: input.user.scopes,
2761
+ selectedRepo: input.selectedRepo
2762
+ });
2763
+ const apiSession = store.createApiSession();
2764
+ registerGitHubApiSession({ projectRoot: input.projectRoot, token: apiSession.token, namespace, selectedRepo: input.selectedRepo });
2765
+ const requestedRoot = normalizeString(input.requestedProjectRoot);
2766
+ if (requestedRoot && isAbsolute3(requestedRoot) && existsSync10(resolve13(requestedRoot))) {
2767
+ copyGitHubAuthStateToLocalProjectRoot(namespace.authStateFile, resolve13(requestedRoot));
2768
+ }
2769
+ if (shouldWriteRootAuthCompat(input.projectRoot)) {
2770
+ const rootStore = createGitHubAuthStore(input.projectRoot);
2771
+ rootStore.saveToken({
2772
+ token: input.token,
2773
+ tokenSource: input.tokenSource,
2774
+ login: input.user.login,
2775
+ userId: input.user.userId,
2776
+ scopes: input.user.scopes,
2777
+ selectedRepo: input.selectedRepo
2778
+ });
2779
+ rootStore.createApiSession();
2780
+ }
2781
+ return { store, namespace, apiSessionToken: apiSession.token };
2782
+ }
2180
2783
  async function postGitHubForm(endpoint, body) {
2181
2784
  const response = await fetch(endpoint, {
2182
2785
  method: "POST",
@@ -2194,11 +2797,11 @@ function resolveRequestedProjectRoot(currentRoot, rawRoot) {
2194
2797
  const requestedRoot = normalizeString(rawRoot);
2195
2798
  if (!requestedRoot)
2196
2799
  return currentRoot;
2197
- if (!isAbsolute2(requestedRoot)) {
2800
+ if (!isAbsolute3(requestedRoot)) {
2198
2801
  throw new Error("projectRoot must be an absolute path on the Rig server host");
2199
2802
  }
2200
- const normalizedRoot = resolve11(requestedRoot);
2201
- if (!existsSync8(normalizedRoot)) {
2803
+ const normalizedRoot = resolve13(requestedRoot);
2804
+ if (!existsSync10(normalizedRoot)) {
2202
2805
  throw new Error("projectRoot does not exist on the Rig server host");
2203
2806
  }
2204
2807
  return normalizedRoot;
@@ -2474,7 +3077,7 @@ function redactSecretFields(value) {
2474
3077
  return redacted;
2475
3078
  }
2476
3079
  function validateRemoteLease(deps, state, input) {
2477
- const run = readAuthorityRun8(state.projectRoot, input.runId);
3080
+ const run = readAuthorityRun9(state.projectRoot, input.runId);
2478
3081
  if (!run) {
2479
3082
  return { ok: false, response: deps.jsonResponse({ ok: false, error: "Remote run not found" }, 404) };
2480
3083
  }
@@ -2494,6 +3097,43 @@ function createRigServerFetch(state, deps) {
2494
3097
  return deps.withServerPathEnv(state.projectRoot, async () => {
2495
3098
  const browserOrigin = deps.resolveAllowedBrowserOrigin(req);
2496
3099
  const finalizeResponse = (response) => deps.withCorsHeaders(response, req, browserOrigin);
3100
+ const earlyUrl = new URL(req.url);
3101
+ const piEventsWsMatch = earlyUrl.pathname.match(/^\/api\/runs\/([^/]+)\/pi\/events$/);
3102
+ if (piEventsWsMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
3103
+ const queryToken = earlyUrl.searchParams.get("token");
3104
+ const authHeaders = new Headers(req.headers);
3105
+ if (queryToken && !authHeaders.has("authorization")) {
3106
+ authHeaders.set("authorization", `Bearer ${queryToken}`);
3107
+ }
3108
+ const legacyAuthorized = Boolean(state.authToken && queryToken === state.authToken);
3109
+ const requestAuth = authorizeRigHttpRequest({
3110
+ req: new Request(req.url, { method: req.method, headers: authHeaders }),
3111
+ pathname: earlyUrl.pathname,
3112
+ projectRoot: state.projectRoot,
3113
+ serverAuthToken: state.authToken,
3114
+ legacyAuthorized
3115
+ });
3116
+ if (!requestAuth.authorized) {
3117
+ return deps.jsonResponse({ ok: false, error: "Unauthorized WebSocket connection", reason: requestAuth.reason }, 401);
3118
+ }
3119
+ const runId = decodeURIComponent(piEventsWsMatch[1]);
3120
+ const resolved = resolveRunPiSessionProxy(state.projectRoot, runId);
3121
+ if (resolved === null)
3122
+ return deps.jsonResponse({ ok: false, error: "Run not found" }, 404);
3123
+ if ("pending" in resolved)
3124
+ return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500 }, 202);
3125
+ if (!server)
3126
+ return deps.jsonResponse({ ok: false, error: "WebSocket upgrade unavailable" }, 400);
3127
+ const upgraded = server.upgrade(req, {
3128
+ data: {
3129
+ kind: "pi-session-proxy",
3130
+ connectedAt: new Date().toISOString(),
3131
+ upstreamUrl: buildRunPiDaemonWebSocketUrl(resolved),
3132
+ runId
3133
+ }
3134
+ });
3135
+ return upgraded ? new Response(null) : deps.jsonResponse({ ok: false, error: "WebSocket upgrade failed" }, 400);
3136
+ }
2497
3137
  const upgradeResponse = handleWebSocketUpgrade({
2498
3138
  req,
2499
3139
  server,
@@ -2531,6 +3171,13 @@ function createRigServerFetch(state, deps) {
2531
3171
  notifications: state.targets.length
2532
3172
  });
2533
3173
  }
3174
+ if (url.pathname === "/install" && req.method === "GET") {
3175
+ return new Response(buildRigInstallScript(), {
3176
+ headers: {
3177
+ "Content-Type": "text/x-shellscript; charset=utf-8"
3178
+ }
3179
+ });
3180
+ }
2534
3181
  const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
2535
3182
  const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
2536
3183
  const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
@@ -2743,16 +3390,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2743
3390
  if (!source) {
2744
3391
  return deps.badRequest("No task source is configured");
2745
3392
  }
3393
+ if (!source.updateTask && !(update.status && source.updateStatus)) {
3394
+ return deps.badRequest("Configured task source does not support updates");
3395
+ }
2746
3396
  const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
2747
3397
  return;
2748
3398
  }) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
2749
- if (source.updateTask) {
2750
- await source.updateTask(id, update);
2751
- } else if (update.status && source.updateStatus) {
2752
- await source.updateStatus(id, update.status);
2753
- } else {
2754
- return deps.badRequest("Configured task source does not support updates");
2755
- }
2756
3399
  const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
2757
3400
  const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
2758
3401
  taskId: id,
@@ -2761,6 +3404,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2761
3404
  token: createGitHubAuthStore(state.projectRoot).readToken(),
2762
3405
  config: ctx?.config
2763
3406
  }).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
3407
+ if (update.status && githubProjectsEnabled(ctx?.config) && projectSync.synced === false) {
3408
+ return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
3409
+ }
3410
+ try {
3411
+ if (source.updateTask) {
3412
+ await source.updateTask(id, update);
3413
+ } else if (update.status && source.updateStatus) {
3414
+ await source.updateStatus(id, update.status);
3415
+ }
3416
+ } catch (error) {
3417
+ let rollback = null;
3418
+ const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
3419
+ if (update.status && previousStatus && githubProjectsEnabled(ctx?.config) && projectSync.synced !== false) {
3420
+ rollback = await syncGitHubProjectStatusForTaskUpdate({
3421
+ taskId: id,
3422
+ status: previousStatus,
3423
+ issueNodeId,
3424
+ token: createGitHubAuthStore(state.projectRoot).readToken(),
3425
+ config: ctx?.config
3426
+ }).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
3427
+ }
3428
+ return deps.jsonResponse({
3429
+ ok: false,
3430
+ id,
3431
+ projectSync,
3432
+ rollback,
3433
+ error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
3434
+ }, 502);
3435
+ }
2764
3436
  deps.snapshotService.invalidate("github-issue-updated");
2765
3437
  await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
2766
3438
  return;
@@ -2769,25 +3441,40 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2769
3441
  return deps.jsonResponse({ ok: true, id, projectSync });
2770
3442
  }
2771
3443
  if (url.pathname === "/api/workspace/task-labels") {
3444
+ const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
3445
+ if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
3446
+ return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
3447
+ }
2772
3448
  return deps.jsonResponse({
2773
3449
  ok: true,
2774
3450
  ready: true,
2775
3451
  labelsReady: true,
2776
- labels: [
2777
- "ready",
2778
- "blocked",
2779
- "in-progress",
2780
- "under-review",
2781
- "failed",
2782
- "cancelled",
2783
- "rig:running",
2784
- "rig:pr-open",
2785
- "rig:ci-fixing",
2786
- "rig:done",
2787
- "rig:needs-attention"
2788
- ],
2789
- note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
3452
+ labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
3453
+ note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
3454
+ });
3455
+ }
3456
+ if (url.pathname === "/api/github/projects" && req.method === "GET") {
3457
+ const owner = normalizeString(url.searchParams.get("owner"));
3458
+ if (!owner)
3459
+ return deps.badRequest("owner is required");
3460
+ const token = createGitHubAuthStore(state.projectRoot).readToken();
3461
+ if (!token)
3462
+ return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
3463
+ const projects = await listGitHubProjects({ owner, token }).catch((error) => {
3464
+ throw new Error(error instanceof Error ? error.message : String(error));
2790
3465
  });
3466
+ return deps.jsonResponse({ ok: true, projects });
3467
+ }
3468
+ const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
3469
+ if (projectStatusMatch && req.method === "GET") {
3470
+ const projectId = decodeURIComponent(projectStatusMatch[1]);
3471
+ const token = createGitHubAuthStore(state.projectRoot).readToken();
3472
+ if (!token)
3473
+ return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
3474
+ const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
3475
+ throw new Error(error instanceof Error ? error.message : String(error));
3476
+ });
3477
+ return deps.jsonResponse({ ok: true, field });
2791
3478
  }
2792
3479
  if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
2793
3480
  const body = await deps.readJsonBody(req);
@@ -2852,7 +3539,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2852
3539
  }
2853
3540
  if (url.pathname === "/api/server/status") {
2854
3541
  const config = buildProjectConfigStatus(state.projectRoot);
2855
- const taskSource = await buildTaskSourceStatus(state, config);
3542
+ const taskSource = await buildTaskSourceStatus(state, config, requestAuth);
2856
3543
  return deps.jsonResponse({
2857
3544
  ok: true,
2858
3545
  projectRoot: state.projectRoot,
@@ -2876,8 +3563,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2876
3563
  path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
2877
3564
  ref: normalizeString(rawCheckout?.ref) ?? undefined
2878
3565
  } : undefined;
2879
- const record = upsertProjectRecord(state.projectRoot, { repoSlug, checkout });
2880
- return deps.jsonResponse({ ok: true, project: record });
3566
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
3567
+ const record = upsertProjectRecord(registryRoot, { repoSlug, checkout });
3568
+ return deps.jsonResponse({ ok: true, project: record, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
2881
3569
  }
2882
3570
  const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
2883
3571
  if (snapshotUploadMatch && req.method === "POST") {
@@ -2890,8 +3578,15 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2890
3578
  if (!archiveContentBase64) {
2891
3579
  return deps.badRequest("archiveContentBase64 is required");
2892
3580
  }
2893
- const baseDir = normalizeString(body.baseDir) ?? normalizeString(process.env.RIG_REMOTE_SNAPSHOT_BASE_DIR) ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve11(normalizeString(process.env.RIG_STATE_DIR), "remote-snapshots") : resolve11(state.projectRoot, ".rig", "remote-snapshots"));
2894
- const checkoutKey = normalizeString(body.checkoutKey) ?? "default";
3581
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
3582
+ const baseDir = resolveNamespacedBaseDir({
3583
+ explicitBaseDir: normalizeString(body.baseDir),
3584
+ envName: "RIG_REMOTE_SNAPSHOT_BASE_DIR",
3585
+ userNamespace: requestAuth.userNamespace,
3586
+ legacyProjectRoot: state.projectRoot,
3587
+ legacySubdir: "remote-snapshots"
3588
+ });
3589
+ const checkoutKey = explicitCheckoutKey(body, body, requestAuth);
2895
3590
  try {
2896
3591
  const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
2897
3592
  const checkout = extractUploadedSnapshotArchive({
@@ -2904,14 +3599,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2904
3599
  const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
2905
3600
  const packageInstall = installRemoteCheckoutPackages(checkout.path);
2906
3601
  const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
2907
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
3602
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
2908
3603
  kind: "uploaded-snapshot",
2909
3604
  path: checkout.path,
2910
3605
  ref: checkout.snapshotId
2911
3606
  });
2912
3607
  deps.snapshotService.invalidate("uploaded-snapshot-checkout");
2913
3608
  deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
2914
- return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
3609
+ return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
2915
3610
  } catch (error) {
2916
3611
  return deps.jsonResponse({
2917
3612
  ok: false,
@@ -2931,10 +3626,17 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2931
3626
  if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
2932
3627
  return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
2933
3628
  }
2934
- const baseDir = normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir) ?? normalizeString(process.env.RIG_REMOTE_CHECKOUT_BASE_DIR) ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve11(normalizeString(process.env.RIG_STATE_DIR), "remote-checkouts") : resolve11(state.projectRoot, ".rig", "remote-checkouts"));
2935
- const checkoutKey = normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? "default";
3629
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
3630
+ const baseDir = resolveNamespacedBaseDir({
3631
+ explicitBaseDir: normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir),
3632
+ envName: "RIG_REMOTE_CHECKOUT_BASE_DIR",
3633
+ userNamespace: requestAuth.userNamespace,
3634
+ legacyProjectRoot: state.projectRoot,
3635
+ legacySubdir: "remote-checkouts"
3636
+ });
3637
+ const checkoutKey = explicitCheckoutKey(body, checkoutInput, requestAuth);
2936
3638
  const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
2937
- const credentialToken = createGitHubAuthStore(state.projectRoot).readToken();
3639
+ const credentialToken = requestScopedAuthStore(state.projectRoot, requestAuth).readToken();
2938
3640
  try {
2939
3641
  const checkout = await prepareRemoteCheckout({
2940
3642
  command: runRemoteCheckoutCommand,
@@ -2943,14 +3645,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2943
3645
  const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
2944
3646
  const packageInstall = installRemoteCheckoutPackages(checkout.path);
2945
3647
  const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
2946
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
3648
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
2947
3649
  kind: checkout.kind,
2948
3650
  path: checkout.path,
2949
3651
  ref: checkout.ref ?? checkout.snapshotId ?? undefined
2950
3652
  });
2951
3653
  deps.snapshotService.invalidate("remote-checkout-prepared");
2952
3654
  deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
2953
- return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
3655
+ return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
2954
3656
  } catch (error) {
2955
3657
  return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
2956
3658
  }
@@ -2967,16 +3669,18 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2967
3669
  if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
2968
3670
  return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
2969
3671
  }
2970
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
3672
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
3673
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
2971
3674
  kind,
2972
3675
  path: normalizeString(body.path) ?? state.projectRoot,
2973
3676
  ref: normalizeString(body.ref) ?? undefined
2974
3677
  });
2975
- return deps.jsonResponse({ ok: true, project });
3678
+ return deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
2976
3679
  }
2977
3680
  if (req.method === "GET") {
2978
- const project = getProjectRecord(state.projectRoot, repoSlug);
2979
- return project ? deps.jsonResponse({ ok: true, project }) : deps.notFound();
3681
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
3682
+ const project = getProjectRecord(registryRoot, repoSlug);
3683
+ return project ? deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) }) : deps.notFound();
2980
3684
  }
2981
3685
  }
2982
3686
  if (url.pathname === "/api/server/project-root" && req.method === "POST") {
@@ -2985,13 +3689,26 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2985
3689
  if (!requestedRoot) {
2986
3690
  return deps.badRequest("projectRoot is required");
2987
3691
  }
2988
- if (!isAbsolute2(requestedRoot)) {
3692
+ if (!isAbsolute3(requestedRoot)) {
2989
3693
  return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
2990
3694
  }
2991
- const normalizedRoot = resolve11(requestedRoot);
2992
- const exists = existsSync8(normalizedRoot);
2993
- if (exists) {
2994
- createGitHubAuthStore(state.projectRoot).copyToProjectRoot(normalizedRoot);
3695
+ const normalizedRoot = resolve13(requestedRoot);
3696
+ const exists = existsSync10(normalizedRoot);
3697
+ if (exists && requestAuth.userNamespace) {
3698
+ const allowedByNamespace = isPathInsideNamespace(requestAuth.userNamespace.root, normalizedRoot);
3699
+ const allowedByRegistry = projectRegistryContainsCheckout(requestAuth.userNamespace.root, normalizedRoot);
3700
+ if (!allowedByNamespace && !allowedByRegistry) {
3701
+ return deps.jsonResponse({
3702
+ ok: false,
3703
+ error: "Requested project root is outside the authenticated GitHub user namespace.",
3704
+ projectRoot: state.projectRoot,
3705
+ requestedProjectRoot: normalizedRoot,
3706
+ userNamespace: userNamespaceResponse(requestAuth.userNamespace)
3707
+ }, 403);
3708
+ }
3709
+ copyGitHubAuthStateToLocalProjectRoot(requestAuth.userNamespace.authStateFile, normalizedRoot);
3710
+ } else if (exists) {
3711
+ createGitHubAuthStore(state.projectRoot).copyToLocalProjectRoot(normalizedRoot);
2995
3712
  }
2996
3713
  const control = buildServerControlStatus();
2997
3714
  const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
@@ -3006,7 +3723,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3006
3723
  message: "Requested project root does not exist on the Rig server host."
3007
3724
  }, 404);
3008
3725
  }
3009
- if (!existsSync8(resolve11(normalizedRoot, "rig.config.ts")) && !existsSync8(resolve11(normalizedRoot, "rig.config.json"))) {
3726
+ if (!existsSync10(resolve13(normalizedRoot, "rig.config.ts")) && !existsSync10(resolve13(normalizedRoot, "rig.config.json"))) {
3010
3727
  return deps.jsonResponse({
3011
3728
  ok: false,
3012
3729
  projectRoot: state.projectRoot,
@@ -3041,6 +3758,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3041
3758
  exists,
3042
3759
  control,
3043
3760
  requiresRestart: false,
3761
+ userNamespace: userNamespaceResponse(requestAuth.userNamespace),
3044
3762
  message: "Project-root switch accepted. Rig server restart has been scheduled."
3045
3763
  }, 202);
3046
3764
  }
@@ -3055,11 +3773,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3055
3773
  }, 409);
3056
3774
  }
3057
3775
  if (url.pathname === "/api/github/auth/status") {
3058
- const store = createGitHubAuthStore(state.projectRoot);
3059
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
3776
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
3777
+ return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
3060
3778
  }
3061
3779
  if (url.pathname === "/api/github/repo/permissions") {
3062
- const store = createGitHubAuthStore(state.projectRoot);
3780
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
3063
3781
  const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
3064
3782
  if (!auth.signedIn) {
3065
3783
  return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
@@ -3087,24 +3805,20 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3087
3805
  }
3088
3806
  try {
3089
3807
  const user = await fetchGitHubUserInfo(token);
3090
- const storeRoots = [
3091
- state.projectRoot,
3092
- ...requestedProjectRoot && isAbsolute2(requestedProjectRoot) && existsSync8(resolve11(requestedProjectRoot)) ? [resolve11(requestedProjectRoot)] : []
3093
- ].filter((root, index, roots) => roots.indexOf(root) === index);
3094
- const stores = storeRoots.map((root) => createGitHubAuthStore(root));
3095
- for (const store2 of stores) {
3096
- store2.saveToken({
3097
- token,
3098
- tokenSource: "manual-token",
3099
- login: user.login,
3100
- userId: user.userId,
3101
- scopes: user.scopes,
3102
- selectedRepo
3103
- });
3104
- }
3105
- const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
3106
- const apiSession = store.createApiSession();
3107
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
3808
+ const saved = saveGitHubTokenForRemoteUser({
3809
+ projectRoot: state.projectRoot,
3810
+ token,
3811
+ tokenSource: "manual-token",
3812
+ user,
3813
+ selectedRepo,
3814
+ requestedProjectRoot
3815
+ });
3816
+ return deps.jsonResponse({
3817
+ ok: true,
3818
+ ...saved.store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }),
3819
+ apiSessionToken: saved.apiSessionToken,
3820
+ userNamespace: userNamespaceResponse(saved.namespace)
3821
+ });
3108
3822
  } catch (error) {
3109
3823
  const message = error instanceof Error ? error.message : String(error);
3110
3824
  return deps.jsonResponse({ ok: false, error: message }, 400);
@@ -3171,9 +3885,21 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3171
3885
  }
3172
3886
  const token = result.payload.access_token;
3173
3887
  const user = await fetchGitHubUserInfo(token);
3174
- store.saveToken({ token, tokenSource: "oauth-device", login: user.login, userId: user.userId, scopes: user.scopes });
3175
- const apiSession = store.createApiSession();
3176
- return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }), apiSessionToken: apiSession.token });
3888
+ const saved = saveGitHubTokenForRemoteUser({
3889
+ projectRoot: state.projectRoot,
3890
+ token,
3891
+ tokenSource: "oauth-device",
3892
+ user,
3893
+ selectedRepo: null
3894
+ });
3895
+ store.clearPendingDevice(pollId);
3896
+ return deps.jsonResponse({
3897
+ ok: true,
3898
+ status: "signed-in",
3899
+ ...saved.store.status({ oauthConfigured: true }),
3900
+ apiSessionToken: saved.apiSessionToken,
3901
+ userNamespace: userNamespaceResponse(saved.namespace)
3902
+ });
3177
3903
  }
3178
3904
  if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
3179
3905
  const body = await deps.readJsonBody(req);
@@ -3182,7 +3908,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3182
3908
  if (!owner || !repo) {
3183
3909
  return deps.badRequest("owner and repo are required");
3184
3910
  }
3185
- const store = createGitHubAuthStore(state.projectRoot);
3911
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
3186
3912
  const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
3187
3913
  const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
3188
3914
  return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
@@ -3203,7 +3929,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3203
3929
  return deps.badRequest(error instanceof Error ? error.message : String(error));
3204
3930
  }
3205
3931
  const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
3206
- const configPath = resolve11(targetRoot, "rig.config.ts");
3932
+ const configPath = resolve13(targetRoot, "rig.config.ts");
3207
3933
  const source = buildGitHubProjectConfigSource({
3208
3934
  projectName: rawProjectName,
3209
3935
  owner,
@@ -3215,8 +3941,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3215
3941
  ok: true,
3216
3942
  projectRoot: targetRoot,
3217
3943
  configPath,
3218
- exists: existsSync8(configPath),
3219
- requiresOverwrite: existsSync8(configPath),
3944
+ exists: existsSync10(configPath),
3945
+ requiresOverwrite: existsSync10(configPath),
3220
3946
  source,
3221
3947
  owner,
3222
3948
  repo,
@@ -3252,8 +3978,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3252
3978
  assignee,
3253
3979
  githubUserId: authStatus.userId ?? authStatus.login
3254
3980
  });
3255
- const configPath = resolve11(targetRoot, "rig.config.ts");
3256
- if (existsSync8(configPath) && !overwrite) {
3981
+ const configPath = resolve13(targetRoot, "rig.config.ts");
3982
+ if (existsSync10(configPath) && !overwrite) {
3257
3983
  return deps.jsonResponse({
3258
3984
  ok: false,
3259
3985
  error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
@@ -3269,11 +3995,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3269
3995
  return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
3270
3996
  }
3271
3997
  let backupPath = null;
3272
- if (existsSync8(configPath)) {
3998
+ if (existsSync10(configPath)) {
3273
3999
  backupPath = backupConfigPath(configPath);
3274
- copyFileSync(configPath, backupPath);
4000
+ copyFileSync2(configPath, backupPath);
3275
4001
  }
3276
- writeFileSync7(configPath, source, "utf8");
4002
+ writeFileSync9(configPath, source, "utf8");
3277
4003
  const selectedRepo = `${owner}/${repo}`;
3278
4004
  store.saveSelectedRepo(selectedRepo);
3279
4005
  const targetStore = createGitHubAuthStore(targetRoot);
@@ -3545,11 +4271,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3545
4271
  const runId = normalizeString(body.runId);
3546
4272
  const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
3547
4273
  const promptOverride = normalizeString(body.promptOverride);
4274
+ const restart = body.restart === true;
3548
4275
  if (!runId) {
3549
4276
  return deps.badRequest("runId is required");
3550
4277
  }
3551
4278
  try {
3552
- await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
4279
+ await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
3553
4280
  deps.broadcastSnapshotInvalidation(state);
3554
4281
  return deps.jsonResponse({ ok: true, runId, createdAt });
3555
4282
  } catch (error) {
@@ -3558,7 +4285,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3558
4285
  }
3559
4286
  if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
3560
4287
  const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
3561
- const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) => resolve11(root, "packages", "pi-rig")).find((candidate) => existsSync8(resolve11(candidate, "package.json"))) ?? "npm:@rig/pi-rig";
4288
+ const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) => resolve13(root, "packages", "pi-rig")).find((candidate) => existsSync10(resolve13(candidate, "package.json"))) ?? "npm:@h-rig/pi-rig";
3562
4289
  if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
3563
4290
  return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
3564
4291
  }
@@ -3874,9 +4601,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3874
4601
  } catch {
3875
4602
  return deps.badRequest("Invalid artifact path");
3876
4603
  }
3877
- mkdirSync7(dirname6(artifactPath), { recursive: true });
4604
+ mkdirSync9(dirname9(artifactPath), { recursive: true });
3878
4605
  const bytes = Buffer.from(contentBase64, "base64");
3879
- writeFileSync7(artifactPath, bytes);
4606
+ writeFileSync9(artifactPath, bytes);
3880
4607
  writeJsonFile4(`${artifactPath}.json`, {
3881
4608
  workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
3882
4609
  runId,
@@ -3913,13 +4640,75 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3913
4640
  }
3914
4641
  const run = leaseValidation.run;
3915
4642
  const completedAt = new Date().toISOString();
4643
+ const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
4644
+ if (run.taskId && workspaceDir) {
4645
+ patchRunRecord(state.projectRoot, runId, {
4646
+ status: "reviewing",
4647
+ completedAt: null,
4648
+ hostId,
4649
+ endpointId: leaseId,
4650
+ worktreePath: workspaceDir,
4651
+ serverCloseout: {
4652
+ status: "pending",
4653
+ phase: "queued",
4654
+ requestedAt: completedAt,
4655
+ updatedAt: completedAt,
4656
+ runtimeWorkspace: workspaceDir,
4657
+ branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
4658
+ taskId: run.taskId,
4659
+ source: "remote-complete"
4660
+ }
4661
+ });
4662
+ deps.appendRunLogEntryAndBroadcast(state, runId, {
4663
+ id: `log:${runId}:remote-server-closeout-requested`,
4664
+ title: "Server-owned closeout requested",
4665
+ detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
4666
+ tone: "info",
4667
+ status: "reviewing",
4668
+ createdAt: completedAt,
4669
+ payload: { workspaceDir, hostId, leaseId }
4670
+ }, "remote-server-closeout-requested");
4671
+ deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
4672
+ const detail = error instanceof Error ? error.message : String(error);
4673
+ patchRunRecord(state.projectRoot, runId, {
4674
+ status: "failed",
4675
+ completedAt: new Date().toISOString(),
4676
+ errorText: detail,
4677
+ serverCloseout: {
4678
+ status: "failed",
4679
+ phase: "failed",
4680
+ updatedAt: new Date().toISOString(),
4681
+ error: detail
4682
+ }
4683
+ });
4684
+ deps.appendRunLogEntryAndBroadcast(state, runId, {
4685
+ id: `log:${runId}:remote-server-closeout-failed`,
4686
+ title: "Server-owned closeout failed",
4687
+ detail,
4688
+ tone: "error",
4689
+ status: "failed",
4690
+ createdAt: new Date().toISOString()
4691
+ }, "remote-server-closeout-failed");
4692
+ }).finally(() => {
4693
+ deps.reconcileScheduler(state, "remote-server-closeout-terminal");
4694
+ });
4695
+ deps.broadcastSnapshotInvalidation(state);
4696
+ return deps.jsonResponse({
4697
+ ok: true,
4698
+ workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
4699
+ hostId,
4700
+ runId,
4701
+ leaseId,
4702
+ closeout: "server-owned",
4703
+ acceptedAt: new Date().toISOString()
4704
+ });
4705
+ }
3916
4706
  patchRunRecord(state.projectRoot, runId, {
3917
4707
  status: "completed",
3918
4708
  completedAt,
3919
4709
  hostId,
3920
4710
  endpointId: leaseId
3921
4711
  });
3922
- await updateRemoteRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "completed", completedAt, hostId, endpointId: leaseId }, "closed", "Remote Rig task run completed and closed this task.");
3923
4712
  await deps.enqueueRunLinearEvent(state.projectRoot, {
3924
4713
  type: "run.completed",
3925
4714
  runId,
@@ -4038,12 +4827,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
4038
4827
  try {
4039
4828
  const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
4040
4829
  const runRoot = deps.normalizeRelativePath(runsRoot, runId);
4041
- const artifactsRoot = resolve11(runRoot, "remote-artifacts");
4830
+ const artifactsRoot = resolve13(runRoot, "remote-artifacts");
4042
4831
  artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
4043
4832
  } catch {
4044
4833
  return deps.badRequest("Invalid artifact path");
4045
4834
  }
4046
- if (!existsSync8(artifactPath)) {
4835
+ if (!existsSync10(artifactPath)) {
4047
4836
  return deps.notFound();
4048
4837
  }
4049
4838
  return new Response(Bun.file(artifactPath));
@@ -4056,6 +4845,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
4056
4845
  const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
4057
4846
  return deps.jsonResponse(page);
4058
4847
  }
4848
+ const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
4849
+ if (runTimelineMatch) {
4850
+ const runId = decodeURIComponent(runTimelineMatch[1]);
4851
+ const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
4852
+ const cursor = normalizeString(url.searchParams.get("cursor"));
4853
+ const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
4854
+ return deps.jsonResponse(page);
4855
+ }
4059
4856
  const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
4060
4857
  if (runSteerMatch && req.method === "POST") {
4061
4858
  const runId = decodeURIComponent(runSteerMatch[1]);
@@ -4075,6 +4872,49 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
4075
4872
  return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 404);
4076
4873
  }
4077
4874
  }
4875
+ const runPiMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/pi(?:\/(.*))?$/);
4876
+ if (runPiMatch) {
4877
+ const runId = decodeURIComponent(runPiMatch[1]);
4878
+ const action = runPiMatch[2] || "";
4879
+ const resolved = resolveRunPiSessionProxy(state.projectRoot, runId);
4880
+ if (resolved === null)
4881
+ return deps.notFound();
4882
+ if ("pending" in resolved) {
4883
+ if (action === "" && req.method === "GET") {
4884
+ return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500 }, 202);
4885
+ }
4886
+ return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500, error: "Pi session is not ready" }, 409);
4887
+ }
4888
+ if (action === "" && req.method === "GET")
4889
+ return deps.jsonResponse({ ready: true, metadata: resolved.metadata });
4890
+ const body = req.method === "GET" ? undefined : await deps.readJsonBody(req);
4891
+ const sessionPath = `/sessions/${encodeURIComponent(resolved.sessionId)}`;
4892
+ const daemonPath = (() => {
4893
+ if (action === "messages" && req.method === "GET")
4894
+ return `${sessionPath}/messages`;
4895
+ if (action === "status" && req.method === "GET")
4896
+ return `${sessionPath}/status`;
4897
+ if (action === "commands" && req.method === "GET")
4898
+ return `${sessionPath}/commands`;
4899
+ if (action === "prompt" && req.method === "POST")
4900
+ return `${sessionPath}/prompt`;
4901
+ if (action === "shell" && req.method === "POST")
4902
+ return `${sessionPath}/shell`;
4903
+ if (action === "commands/run" && req.method === "POST")
4904
+ return `${sessionPath}/commands/run`;
4905
+ if (action === "commands/respond" && req.method === "POST")
4906
+ return `${sessionPath}/commands/respond`;
4907
+ if (action === "extension-ui/respond" && req.method === "POST")
4908
+ return `${sessionPath}/extension-ui/respond`;
4909
+ if (action === "abort" && req.method === "POST")
4910
+ return `${sessionPath}/abort`;
4911
+ return null;
4912
+ })();
4913
+ if (!daemonPath)
4914
+ return deps.notFound();
4915
+ const proxied = await proxyRunPiHttp(state.projectRoot, runId, { method: req.method, daemonPath, body });
4916
+ return deps.jsonResponse(proxied.payload, proxied.status);
4917
+ }
4078
4918
  const runSteeringAckMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steering\/ack$/);
4079
4919
  if (runSteeringAckMatch && req.method === "POST") {
4080
4920
  const runId = decodeURIComponent(runSteeringAckMatch[1]);