@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.
package/dist/src/index.js CHANGED
@@ -663,9 +663,9 @@ function createRemoteOrchestrationSummary(input) {
663
663
  }
664
664
  // packages/server/src/server.ts
665
665
  import { spawn as spawn5 } from "child_process";
666
- import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync6 } from "fs";
666
+ import { existsSync as existsSync19, readdirSync as readdirSync5, readFileSync as readFileSync14, statSync as statSync6 } from "fs";
667
667
  import { open } from "fs/promises";
668
- import { dirname as dirname17, resolve as resolve22 } from "path";
668
+ import { dirname as dirname20, resolve as resolve24 } from "path";
669
669
  import {
670
670
  listAuthorityArtifactRoots,
671
671
  listAuthorityRuns as listAuthorityRuns7,
@@ -1530,14 +1530,29 @@ function summarizeUsefulRunError(projectRoot, runId, fallback) {
1530
1530
  const nonGeneric = errorLines.at(-1);
1531
1531
  return nonGeneric ?? (typeof fallback === "string" ? fallback : null);
1532
1532
  }
1533
+ function readRunPiSessionMetadata(projectRoot, runId) {
1534
+ const run = readAuthorityRun(projectRoot, runId);
1535
+ const metadata = run?.piSessionPrivate;
1536
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
1537
+ return null;
1538
+ const record = metadata;
1539
+ const publicMetadata = record.public;
1540
+ const daemonConnection = record.daemonConnection;
1541
+ if (!publicMetadata || typeof publicMetadata !== "object" || Array.isArray(publicMetadata))
1542
+ return null;
1543
+ if (!daemonConnection || typeof daemonConnection !== "object" || Array.isArray(daemonConnection))
1544
+ return null;
1545
+ return metadata;
1546
+ }
1533
1547
  function readRunDetails(projectRoot, runId) {
1534
1548
  const run = readAuthorityRun(projectRoot, runId);
1535
1549
  if (!run) {
1536
1550
  return null;
1537
1551
  }
1538
1552
  const usefulErrorText = isGenericRunFailure(run.errorText) ? summarizeUsefulRunError(projectRoot, runId, run.errorText) : null;
1553
+ const { piSessionPrivate: _piSessionPrivate, ...publicRun } = run;
1539
1554
  return {
1540
- run: usefulErrorText ? { ...run, errorText: usefulErrorText } : run,
1555
+ run: usefulErrorText ? { ...publicRun, errorText: usefulErrorText } : publicRun,
1541
1556
  timeline: readJsonlFile(resolve4(resolveAuthorityRunDir(projectRoot, runId), "timeline.jsonl")),
1542
1557
  approvals: readApprovals(projectRoot, { runId }),
1543
1558
  userInputs: readUserInputs(projectRoot, { runId })
@@ -1580,6 +1595,18 @@ function readJsonlFileTail(path, options) {
1580
1595
  const completeLines = start > 0 ? lines.slice(1) : lines;
1581
1596
  return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
1582
1597
  }
1598
+ async function readRunTimelinePage(projectRoot, runId, options = {}) {
1599
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
1600
+ const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
1601
+ const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
1602
+ const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
1603
+ const endExclusive = Math.min(entries.length, startInclusive + limit);
1604
+ return {
1605
+ entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
1606
+ nextCursor: String(endExclusive),
1607
+ hasMore: endExclusive < entries.length
1608
+ };
1609
+ }
1583
1610
  var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
1584
1611
  async function readRunLogsPage(projectRoot, runId, options = {}) {
1585
1612
  const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
@@ -2819,7 +2846,6 @@ function createGitHubTaskReconciler(input) {
2819
2846
  }
2820
2847
 
2821
2848
  // packages/server/src/server-helpers/issue-analysis.ts
2822
- import { execFile } from "child_process";
2823
2849
  import { createHash } from "crypto";
2824
2850
  function stableIssueHash(issue) {
2825
2851
  const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
@@ -2953,16 +2979,33 @@ function parseIssueAnalysisResult(raw) {
2953
2979
  return result;
2954
2980
  }
2955
2981
  function createDefaultPiIssueAnalysisCommandRunner() {
2956
- return (command, args, options) => new Promise((resolve11) => {
2957
- execFile(command, [...args], {
2958
- timeout: options.timeoutMs,
2959
- maxBuffer: 10 * 1024 * 1024,
2960
- env: options.env ? { ...process.env, ...options.env } : process.env
2961
- }, (error, stdout, stderr) => {
2962
- const exitCode = typeof error?.code === "number" ? error.code : error ? 1 : 0;
2963
- resolve11({ exitCode, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
2982
+ return async (command, args, options) => {
2983
+ const env = options.env ? { ...process.env, ...options.env } : process.env;
2984
+ const proc = Bun.spawn([command, ...args], {
2985
+ stdout: "pipe",
2986
+ stderr: "pipe",
2987
+ env
2964
2988
  });
2965
- });
2989
+ let timedOut = false;
2990
+ const timer = setTimeout(() => {
2991
+ timedOut = true;
2992
+ proc.kill();
2993
+ }, options.timeoutMs);
2994
+ try {
2995
+ const [stdout, stderr, exitCode] = await Promise.all([
2996
+ new Response(proc.stdout).text(),
2997
+ new Response(proc.stderr).text(),
2998
+ proc.exited
2999
+ ]);
3000
+ return {
3001
+ exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
3002
+ stdout,
3003
+ stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
3004
+ };
3005
+ } finally {
3006
+ clearTimeout(timer);
3007
+ }
3008
+ };
2966
3009
  }
2967
3010
  function createPiIssueAnalyzer(input = {}) {
2968
3011
  const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
@@ -3216,13 +3259,18 @@ function patchRunRecord(projectRoot, runId, patch) {
3216
3259
  writeJsonFile2(resolve11(resolveAuthorityRunDir2(projectRoot, runId), "run.json"), next);
3217
3260
  return next;
3218
3261
  }
3262
+ function patchRunPiSessionMetadata(projectRoot, runId, metadata) {
3263
+ return patchRunRecord(projectRoot, runId, {
3264
+ piSession: metadata?.public ?? null,
3265
+ piSessionPrivate: metadata
3266
+ });
3267
+ }
3219
3268
  function buildRunStartPatch(startedAt) {
3220
3269
  return {
3221
3270
  status: "preparing",
3222
3271
  startedAt,
3223
3272
  completedAt: null,
3224
- errorText: null,
3225
- serverPid: process.pid
3273
+ errorText: null
3226
3274
  };
3227
3275
  }
3228
3276
 
@@ -3691,7 +3739,7 @@ function applyOrchestrationCommand2(state, command) {
3691
3739
  import { spawn as spawn3 } from "child_process";
3692
3740
  import { loadConfig } from "@rig/core/load-config";
3693
3741
  import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
3694
- import { dirname as dirname8, relative as relative2, resolve as resolve14 } from "path";
3742
+ import { dirname as dirname9, relative as relative2, resolve as resolve14 } from "path";
3695
3743
  import {
3696
3744
  listAuthorityRuns as listAuthorityRuns4,
3697
3745
  readAuthorityRun as readAuthorityRun4,
@@ -3704,6 +3752,11 @@ import {
3704
3752
  buildTaskRunLifecycleComment,
3705
3753
  updateConfiguredTaskSourceTask
3706
3754
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
3755
+ import {
3756
+ closeIssueAfterMergedPr,
3757
+ commitRunChanges,
3758
+ runPrAutomation
3759
+ } from "@rig/runtime/control-plane/native/pr-automation";
3707
3760
 
3708
3761
  // packages/server/src/scheduler.ts
3709
3762
  import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
@@ -3815,8 +3868,8 @@ function summarizeRunValidationFailure(projectRoot, run) {
3815
3868
 
3816
3869
  // packages/server/src/server-helpers/github-auth-store.ts
3817
3870
  import { randomBytes } from "crypto";
3818
- import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
3819
- import { resolve as resolve13 } from "path";
3871
+ import { chmodSync, copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
3872
+ import { dirname as dirname8, resolve as resolve13 } from "path";
3820
3873
  function cleanString(value) {
3821
3874
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
3822
3875
  }
@@ -3846,6 +3899,26 @@ function parseApiSessions(value) {
3846
3899
  }];
3847
3900
  });
3848
3901
  }
3902
+ function parsePendingDevice(value) {
3903
+ if (!value || typeof value !== "object")
3904
+ return null;
3905
+ const record = value;
3906
+ const pollId = cleanString(record.pollId);
3907
+ const deviceCode = cleanString(record.deviceCode);
3908
+ const expiresAt = cleanString(record.expiresAt);
3909
+ const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
3910
+ if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
3911
+ return null;
3912
+ return { pollId, deviceCode, expiresAt, intervalSeconds };
3913
+ }
3914
+ function parsePendingDevices(value) {
3915
+ if (!Array.isArray(value))
3916
+ return [];
3917
+ return value.flatMap((entry) => {
3918
+ const pending = parsePendingDevice(entry);
3919
+ return pending ? [pending] : [];
3920
+ });
3921
+ }
3849
3922
  function readStoredAuth(stateFile) {
3850
3923
  if (!existsSync6(stateFile))
3851
3924
  return {};
@@ -3859,6 +3932,7 @@ function readStoredAuth(stateFile) {
3859
3932
  selectedRepo: cleanString(parsed.selectedRepo),
3860
3933
  tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
3861
3934
  pendingDevice: parsePendingDevice(parsed.pendingDevice),
3935
+ pendingDevices: parsePendingDevices(parsed.pendingDevices),
3862
3936
  apiSessions: parseApiSessions(parsed.apiSessions),
3863
3937
  updatedAt: cleanString(parsed.updatedAt) ?? undefined
3864
3938
  };
@@ -3866,34 +3940,36 @@ function readStoredAuth(stateFile) {
3866
3940
  return {};
3867
3941
  }
3868
3942
  }
3869
- function parsePendingDevice(value) {
3870
- if (!value || typeof value !== "object")
3871
- return null;
3872
- const record = value;
3873
- const pollId = cleanString(record.pollId);
3874
- const deviceCode = cleanString(record.deviceCode);
3875
- const expiresAt = cleanString(record.expiresAt);
3876
- const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
3877
- if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
3878
- return null;
3879
- return { pollId, deviceCode, expiresAt, intervalSeconds };
3880
- }
3881
3943
  function newApiSessionToken() {
3882
3944
  return `rig_${randomBytes(32).toString("base64url")}`;
3883
3945
  }
3884
3946
  function writeStoredAuth(stateFile, payload) {
3885
- mkdirSync6(resolve13(stateFile, ".."), { recursive: true });
3947
+ mkdirSync6(dirname8(stateFile), { recursive: true });
3886
3948
  writeFileSync5(stateFile, `${JSON.stringify(payload, null, 2)}
3887
3949
  `, { encoding: "utf8", mode: 384 });
3888
3950
  try {
3889
3951
  chmodSync(stateFile, 384);
3890
3952
  } catch {}
3891
3953
  }
3954
+ function localProjectAuthStateFile(projectRoot) {
3955
+ return resolve13(projectRoot, ".rig", "state", "github-auth.json");
3956
+ }
3892
3957
  function resolveGitHubAuthStateFile(projectRoot) {
3893
3958
  return resolve13(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
3894
3959
  }
3895
- function createGitHubAuthStore(projectRoot) {
3896
- const stateFile = resolveGitHubAuthStateFile(projectRoot);
3960
+ function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
3961
+ const targetFile = localProjectAuthStateFile(projectRoot);
3962
+ mkdirSync6(dirname8(targetFile), { recursive: true });
3963
+ if (existsSync6(stateFile)) {
3964
+ copyFileSync(stateFile, targetFile);
3965
+ try {
3966
+ chmodSync(targetFile, 384);
3967
+ } catch {}
3968
+ return;
3969
+ }
3970
+ writeStoredAuth(targetFile, {});
3971
+ }
3972
+ function createGitHubAuthStoreFromStateFile(stateFile) {
3897
3973
  return {
3898
3974
  stateFile,
3899
3975
  status(options) {
@@ -3923,6 +3999,7 @@ function createGitHubAuthStore(projectRoot) {
3923
3999
  scopes: input.scopes ?? [],
3924
4000
  selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
3925
4001
  pendingDevice: null,
4002
+ pendingDevices: [],
3926
4003
  apiSessions: previous.apiSessions ?? [],
3927
4004
  updatedAt: new Date().toISOString()
3928
4005
  });
@@ -3951,15 +4028,24 @@ function createGitHubAuthStore(projectRoot) {
3951
4028
  const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
3952
4029
  return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
3953
4030
  },
3954
- copyToProjectRoot(projectRoot2) {
3955
- const targetFile = resolveGitHubAuthStateFile(projectRoot2);
4031
+ copyToProjectRoot(projectRoot) {
4032
+ const targetFile = resolveGitHubAuthStateFile(projectRoot);
3956
4033
  writeStoredAuth(targetFile, readStoredAuth(stateFile));
3957
4034
  },
4035
+ copyToLocalProjectRoot(projectRoot) {
4036
+ copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
4037
+ },
3958
4038
  savePendingDevice(input) {
3959
4039
  const previous = readStoredAuth(stateFile);
4040
+ const pendingDevices = [
4041
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
4042
+ ...previous.pendingDevices ?? [],
4043
+ input
4044
+ ].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
3960
4045
  writeStoredAuth(stateFile, {
3961
4046
  ...previous,
3962
- pendingDevice: input,
4047
+ pendingDevice: null,
4048
+ pendingDevices,
3963
4049
  updatedAt: new Date().toISOString()
3964
4050
  });
3965
4051
  },
@@ -3972,23 +4058,32 @@ function createGitHubAuthStore(projectRoot) {
3972
4058
  });
3973
4059
  },
3974
4060
  readPendingDevice(pollId) {
3975
- const pending = readStoredAuth(stateFile).pendingDevice ?? null;
3976
- if (!pending || pending.pollId !== pollId)
4061
+ const previous = readStoredAuth(stateFile);
4062
+ const pending = [
4063
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
4064
+ ...previous.pendingDevices ?? []
4065
+ ].find((entry) => entry.pollId === pollId) ?? null;
4066
+ if (!pending)
3977
4067
  return null;
3978
4068
  if (Date.parse(pending.expiresAt) <= Date.now())
3979
4069
  return null;
3980
4070
  return pending;
3981
4071
  },
3982
- clearPendingDevice() {
4072
+ clearPendingDevice(pollId) {
3983
4073
  const previous = readStoredAuth(stateFile);
4074
+ const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
3984
4075
  writeStoredAuth(stateFile, {
3985
4076
  ...previous,
3986
4077
  pendingDevice: null,
4078
+ pendingDevices: remaining,
3987
4079
  updatedAt: new Date().toISOString()
3988
4080
  });
3989
4081
  }
3990
4082
  };
3991
4083
  }
4084
+ function createGitHubAuthStore(projectRoot) {
4085
+ return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
4086
+ }
3992
4087
 
3993
4088
  // packages/server/src/server-helpers/github-projects.ts
3994
4089
  function asRecord(value) {
@@ -3997,6 +4092,9 @@ function asRecord(value) {
3997
4092
  function asString(value) {
3998
4093
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
3999
4094
  }
4095
+ function asNumber(value) {
4096
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
4097
+ }
4000
4098
  async function defaultGraphQLFetch(query, variables, token) {
4001
4099
  const response = await fetch("https://api.github.com/graphql", {
4002
4100
  method: "POST",
@@ -4013,6 +4111,32 @@ async function defaultGraphQLFetch(query, variables, token) {
4013
4111
  }
4014
4112
  return json.data;
4015
4113
  }
4114
+ function projectNodesFrom(data) {
4115
+ const root = asRecord(data);
4116
+ const owner = asRecord(root?.organization) ?? asRecord(root?.user);
4117
+ const projects = asRecord(owner?.projectsV2);
4118
+ const nodes = projects?.nodes;
4119
+ return Array.isArray(nodes) ? nodes : [];
4120
+ }
4121
+ async function listGitHubProjects(input) {
4122
+ const query = `
4123
+ query RigListProjects($owner: String!, $first: Int!) {
4124
+ organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
4125
+ user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
4126
+ }
4127
+ `;
4128
+ const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
4129
+ const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
4130
+ return projectNodesFrom(data).flatMap((node) => {
4131
+ const record = asRecord(node);
4132
+ const id = asString(record?.id);
4133
+ const number = asNumber(record?.number);
4134
+ const title = asString(record?.title);
4135
+ if (!id || number === undefined || !title)
4136
+ return [];
4137
+ return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
4138
+ });
4139
+ }
4016
4140
  async function resolveProjectStatusField(input) {
4017
4141
  const query = `
4018
4142
  query RigProjectStatusField($projectId: ID!) {
@@ -4107,6 +4231,7 @@ var DEFAULT_PROJECT_STATUSES = {
4107
4231
  running: "In Progress",
4108
4232
  prOpen: "In Review",
4109
4233
  ciFixing: "In Review",
4234
+ merging: "Merging",
4110
4235
  done: "Done",
4111
4236
  needsAttention: "Needs Attention"
4112
4237
  };
@@ -4120,6 +4245,8 @@ function lifecycleStatusForTaskStatus(status) {
4120
4245
  return "prOpen";
4121
4246
  if (normalized === "ci_fixing" || normalized === "fixing")
4122
4247
  return "ciFixing";
4248
+ if (normalized === "merging" || normalized === "merge")
4249
+ return "merging";
4123
4250
  if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
4124
4251
  return "needsAttention";
4125
4252
  if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
@@ -4248,9 +4375,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
4248
4375
  return null;
4249
4376
  return null;
4250
4377
  }
4378
+ function githubProjectsEnabled(config) {
4379
+ const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
4380
+ const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
4381
+ return projects?.enabled === true;
4382
+ }
4251
4383
  async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
4252
4384
  if (!run.taskId)
4253
- return;
4385
+ return false;
4254
4386
  const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
4255
4387
  try {
4256
4388
  const result = await syncGitHubProjectStatusForTaskUpdate({
@@ -4261,28 +4393,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
4261
4393
  config
4262
4394
  });
4263
4395
  if (!result.synced && result.reason !== "project-sync-disabled") {
4396
+ const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
4264
4397
  appendRunLogEntry(projectRoot, run.runId, {
4265
4398
  id: `log:${run.runId}:github-project-sync:${status}`,
4266
4399
  title: "GitHub Project sync skipped",
4267
- detail: `Project status sync for ${run.taskId} could not run: ${result.reason}.`,
4400
+ detail,
4268
4401
  tone: "warn",
4269
4402
  status: "running",
4270
4403
  createdAt: new Date().toISOString(),
4271
4404
  payload: { reason: result.reason, issueNodeId }
4272
4405
  });
4406
+ if (githubProjectsEnabled(config)) {
4407
+ throw new Error(detail);
4408
+ }
4409
+ return false;
4273
4410
  }
4411
+ return result.synced === true;
4274
4412
  } catch (error) {
4413
+ const detail = error instanceof Error ? error.message : String(error);
4275
4414
  appendRunLogEntry(projectRoot, run.runId, {
4276
4415
  id: `log:${run.runId}:github-project-sync-error:${status}`,
4277
4416
  title: "GitHub Project sync failed",
4278
- detail: error instanceof Error ? error.message : String(error),
4417
+ detail,
4279
4418
  tone: "error",
4280
4419
  status: "running",
4281
4420
  createdAt: new Date().toISOString(),
4282
4421
  payload: { issueNodeId }
4283
4422
  });
4423
+ if (githubProjectsEnabled(config)) {
4424
+ throw new Error(detail);
4425
+ }
4426
+ return false;
4284
4427
  }
4285
4428
  }
4429
+ function createCommandRunner(binary, extraEnv = {}) {
4430
+ return async (args, options) => {
4431
+ const child = spawn3(binary, [...args], {
4432
+ cwd: options?.cwd,
4433
+ env: { ...process.env, ...extraEnv },
4434
+ stdio: ["ignore", "pipe", "pipe"]
4435
+ });
4436
+ const stdoutChunks = [];
4437
+ const stderrChunks = [];
4438
+ child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
4439
+ child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
4440
+ const exitCode = await new Promise((resolve15) => {
4441
+ child.once("error", () => resolve15(1));
4442
+ child.once("close", (code) => resolve15(code ?? 1));
4443
+ });
4444
+ return {
4445
+ exitCode,
4446
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
4447
+ stderr: Buffer.concat(stderrChunks).toString("utf8")
4448
+ };
4449
+ };
4450
+ }
4451
+ function closeoutRecord(run) {
4452
+ const value = run.serverCloseout;
4453
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
4454
+ }
4455
+ function closeoutPhasePatch(phase, status, extra = {}) {
4456
+ const updatedAt = new Date().toISOString();
4457
+ return {
4458
+ serverCloseout: {
4459
+ ...extra,
4460
+ phase,
4461
+ status,
4462
+ updatedAt
4463
+ }
4464
+ };
4465
+ }
4466
+ function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
4467
+ appendRunLogEntryAndBroadcast(state, runId, {
4468
+ id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
4469
+ title: `Server closeout: ${phase}`,
4470
+ detail,
4471
+ tone,
4472
+ status,
4473
+ createdAt: new Date().toISOString()
4474
+ }, `server-closeout-${phase}`);
4475
+ }
4286
4476
  async function autoAssignRunIssue(projectRoot, run) {
4287
4477
  if (!run.taskId)
4288
4478
  return;
@@ -4312,7 +4502,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
4312
4502
  return;
4313
4503
  }
4314
4504
  const config = await loadRigLifecycleConfig(projectRoot);
4315
- await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
4505
+ const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
4316
4506
  if (status === "in_progress") {
4317
4507
  await autoAssignRunIssue(projectRoot, run);
4318
4508
  }
@@ -4328,24 +4518,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
4328
4518
  });
4329
4519
  return;
4330
4520
  }
4331
- const result = await updateConfiguredTaskSourceTask(projectRoot, {
4332
- taskId: run.taskId,
4333
- sourceTask: runSourceTaskIdentity(run),
4334
- update: {
4335
- status,
4336
- comment: buildTaskRunLifecycleComment({
4337
- runId: run.runId,
4521
+ const sourceTask = runSourceTaskIdentity(run);
4522
+ const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
4523
+ const rollbackProjectSync = async () => {
4524
+ if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
4525
+ return;
4526
+ await syncGitHubProjectStatusForTaskUpdate({
4527
+ taskId: run.taskId,
4528
+ status: previousStatus,
4529
+ issueNodeId: extractGitHubIssueNodeId(sourceTask),
4530
+ token: createGitHubAuthStore(projectRoot).readToken(),
4531
+ config
4532
+ }).catch((rollbackError) => {
4533
+ appendRunLogEntry(projectRoot, run.runId, {
4534
+ id: `log:${run.runId}:github-project-sync-rollback:${status}`,
4535
+ title: "GitHub Project sync rollback failed",
4536
+ detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
4537
+ tone: "error",
4538
+ status: "running",
4539
+ createdAt: new Date().toISOString()
4540
+ });
4541
+ });
4542
+ };
4543
+ let result;
4544
+ try {
4545
+ result = await updateConfiguredTaskSourceTask(projectRoot, {
4546
+ taskId: run.taskId,
4547
+ sourceTask,
4548
+ update: {
4338
4549
  status,
4339
- summary,
4340
- runtimeWorkspace: normalizeString(run.worktreePath),
4341
- logsDir: normalizeString(run.logRoot),
4342
- sessionDir: normalizeString(run.sessionPath),
4343
- errorText: options.errorText ?? normalizeString(run.errorText)
4344
- })
4345
- }
4346
- });
4550
+ comment: buildTaskRunLifecycleComment({
4551
+ runId: run.runId,
4552
+ status,
4553
+ summary,
4554
+ runtimeWorkspace: normalizeString(run.worktreePath),
4555
+ logsDir: normalizeString(run.logRoot),
4556
+ sessionDir: normalizeString(run.sessionPath),
4557
+ errorText: options.errorText ?? normalizeString(run.errorText)
4558
+ })
4559
+ }
4560
+ });
4561
+ } catch (error) {
4562
+ await rollbackProjectSync();
4563
+ throw error;
4564
+ }
4347
4565
  if (!result.updated) {
4348
4566
  if (result.source === "plugin" || result.sourceKind) {
4567
+ await rollbackProjectSync();
4349
4568
  throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
4350
4569
  }
4351
4570
  appendRunLogEntry(projectRoot, run.runId, {
@@ -4358,6 +4577,277 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
4358
4577
  });
4359
4578
  }
4360
4579
  }
4580
+ async function markServerOwnedCloseoutFailed(state, runId, error) {
4581
+ const detail = error instanceof Error ? error.message : String(error);
4582
+ const current = readAuthorityRun4(state.projectRoot, runId);
4583
+ patchRunRecord(state.projectRoot, runId, {
4584
+ status: "failed",
4585
+ completedAt: new Date().toISOString(),
4586
+ errorText: detail,
4587
+ ...closeoutPhasePatch("failed", "failed", { error: detail })
4588
+ });
4589
+ appendRunLogEntryAndBroadcast(state, runId, {
4590
+ id: `log:${runId}:server-closeout-failed`,
4591
+ title: "Server-owned closeout failed",
4592
+ detail,
4593
+ tone: "error",
4594
+ status: "failed",
4595
+ createdAt: new Date().toISOString()
4596
+ }, "server-closeout-failed");
4597
+ if (current?.taskId) {
4598
+ await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: detail }, "failed", "Rig server-owned closeout failed.", { errorText: detail }).catch((sourceError) => {
4599
+ appendRunLogEntry(state.projectRoot, runId, {
4600
+ id: `log:${runId}:task-source-closeout-failed-update`,
4601
+ title: "Task source closeout failure update failed",
4602
+ detail: sourceError instanceof Error ? sourceError.message : String(sourceError),
4603
+ tone: "error",
4604
+ status: "failed",
4605
+ createdAt: new Date().toISOString()
4606
+ });
4607
+ });
4608
+ }
4609
+ }
4610
+ function scheduleServerOwnedPrCloseout(state, runId, reason) {
4611
+ const startedAt = new Date().toISOString();
4612
+ state.runProcesses.set(runId, {
4613
+ runId,
4614
+ child: null,
4615
+ startedAt,
4616
+ stopped: false
4617
+ });
4618
+ queueMicrotask(() => {
4619
+ withServerAuthorityEnvIfNeeded(state.projectRoot, async () => {
4620
+ try {
4621
+ await runServerOwnedPrCloseout(state, runId);
4622
+ } catch (error) {
4623
+ await markServerOwnedCloseoutFailed(state, runId, error);
4624
+ } finally {
4625
+ state.runProcesses.delete(runId);
4626
+ broadcastSnapshotInvalidation(state, `server-closeout-${reason}-terminal`);
4627
+ await reconcileScheduler(state, `server-closeout-${reason}-terminal`);
4628
+ }
4629
+ });
4630
+ });
4631
+ }
4632
+ async function runServerOwnedPrCloseout(state, runId) {
4633
+ const run = readAuthorityRun4(state.projectRoot, runId);
4634
+ if (!run)
4635
+ throw new Error(`Run not found: ${runId}`);
4636
+ const closeout = closeoutRecord(run);
4637
+ if (!closeout)
4638
+ return;
4639
+ const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
4640
+ if (!taskId)
4641
+ throw new Error("Server-owned closeout requires a task id.");
4642
+ const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
4643
+ let branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
4644
+ const config = await loadRigLifecycleConfig(state.projectRoot);
4645
+ const runPrMode = normalizeString(run.prMode);
4646
+ const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
4647
+ const effectiveConfig = {
4648
+ ...config ?? {},
4649
+ pr: {
4650
+ ...config?.pr ?? {},
4651
+ mode: prMode,
4652
+ autoFixChecks: false,
4653
+ autoFixReview: false
4654
+ }
4655
+ };
4656
+ const readCurrentRun = () => readAuthorityRun4(state.projectRoot, runId) ?? run;
4657
+ const sourceTask = runSourceTaskIdentity(run);
4658
+ const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
4659
+ const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
4660
+ const closeoutPrUrl = normalizeString(closeout.prUrl);
4661
+ if (closeoutPhase === "completed" || closeoutStatus === "completed") {
4662
+ return;
4663
+ }
4664
+ if (closeoutPhase === "close-source" && closeoutPrUrl) {
4665
+ patchRunRecord(state.projectRoot, runId, {
4666
+ status: "reviewing",
4667
+ ...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
4668
+ });
4669
+ await closeIssueAfterMergedPr({
4670
+ projectRoot: state.projectRoot,
4671
+ taskId,
4672
+ runId,
4673
+ prUrl: closeoutPrUrl,
4674
+ sourceTask,
4675
+ updateTaskSource: async (projectRoot, input) => {
4676
+ await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
4677
+ return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
4678
+ }
4679
+ });
4680
+ const completedAt = new Date().toISOString();
4681
+ patchRunRecord(state.projectRoot, runId, {
4682
+ status: "completed",
4683
+ completedAt,
4684
+ errorText: null,
4685
+ ...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
4686
+ });
4687
+ appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
4688
+ emitRigEvent(state, {
4689
+ type: "rig.run.completed",
4690
+ aggregateId: runId,
4691
+ payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
4692
+ createdAt: completedAt
4693
+ });
4694
+ return;
4695
+ }
4696
+ if (prMode === "off" || prMode === "ask") {
4697
+ const completedAt = new Date().toISOString();
4698
+ patchRunRecord(state.projectRoot, runId, {
4699
+ status: "completed",
4700
+ completedAt,
4701
+ errorText: null,
4702
+ ...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
4703
+ });
4704
+ appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
4705
+ emitRigEvent(state, {
4706
+ type: "rig.run.completed",
4707
+ aggregateId: runId,
4708
+ payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
4709
+ createdAt: completedAt
4710
+ });
4711
+ return;
4712
+ }
4713
+ const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
4714
+ const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
4715
+ const gitCommand = createCommandRunner("git", githubEnv);
4716
+ const ghCommand = createCommandRunner("gh", githubEnv);
4717
+ const workspaceBranch = await gitCommand(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: workspace });
4718
+ const currentWorkspaceBranch = workspaceBranch.exitCode === 0 ? normalizeString(workspaceBranch.stdout) : null;
4719
+ if (currentWorkspaceBranch && currentWorkspaceBranch !== "HEAD" && currentWorkspaceBranch !== branch) {
4720
+ appendCloseoutStage(state, runId, "branch", `Using runtime workspace branch ${currentWorkspaceBranch} instead of recorded branch ${branch}.`, "reviewing", "info");
4721
+ branch = currentWorkspaceBranch;
4722
+ }
4723
+ const setCloseout = (phase, status, extra = {}) => {
4724
+ const previous = closeoutRecord(readCurrentRun()) ?? closeout;
4725
+ patchRunRecord(state.projectRoot, runId, {
4726
+ status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
4727
+ ...closeoutPhasePatch(phase, status, { ...previous, ...extra })
4728
+ });
4729
+ };
4730
+ setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
4731
+ appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
4732
+ const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
4733
+ appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
4734
+ setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
4735
+ const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
4736
+ if (push.exitCode !== 0) {
4737
+ throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
4738
+ }
4739
+ const sourceTaskForPr = {
4740
+ title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
4741
+ };
4742
+ const artifactRoot = resolve14(state.projectRoot, "artifacts", taskId);
4743
+ setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4744
+ const pr = await runPrAutomation({
4745
+ projectRoot: workspace,
4746
+ taskId,
4747
+ runId,
4748
+ branch,
4749
+ config: effectiveConfig,
4750
+ sourceTask: sourceTaskForPr,
4751
+ artifactRoot,
4752
+ command: ghCommand,
4753
+ gitCommand,
4754
+ steerPi: async (message) => {
4755
+ appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
4756
+ appendRunTimelineEntry(state.projectRoot, runId, {
4757
+ id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
4758
+ type: "user_message",
4759
+ text: message,
4760
+ createdAt: new Date().toISOString(),
4761
+ state: "completed"
4762
+ });
4763
+ },
4764
+ lifecycle: {
4765
+ onPrOpened: async ({ prUrl }) => {
4766
+ setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4767
+ appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
4768
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
4769
+ },
4770
+ onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
4771
+ onFeedback: async ({ feedback }) => {
4772
+ appendCloseoutStage(state, runId, "feedback", feedback.join(`
4773
+ `), "reviewing", "error");
4774
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
4775
+ },
4776
+ onMergeStarted: async ({ prUrl }) => {
4777
+ setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4778
+ appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
4779
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
4780
+ },
4781
+ onMerged: ({ prUrl }) => {
4782
+ setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
4783
+ appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
4784
+ }
4785
+ }
4786
+ });
4787
+ if (pr.status === "merged" && pr.prUrl) {
4788
+ setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
4789
+ await closeIssueAfterMergedPr({
4790
+ projectRoot: state.projectRoot,
4791
+ taskId,
4792
+ runId,
4793
+ prUrl: pr.prUrl,
4794
+ sourceTask,
4795
+ updateTaskSource: async (projectRoot, input) => {
4796
+ await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
4797
+ return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
4798
+ }
4799
+ });
4800
+ const completedAt = new Date().toISOString();
4801
+ patchRunRecord(state.projectRoot, runId, {
4802
+ status: "completed",
4803
+ completedAt,
4804
+ errorText: null,
4805
+ ...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
4806
+ });
4807
+ appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
4808
+ emitRigEvent(state, {
4809
+ type: "rig.run.completed",
4810
+ aggregateId: runId,
4811
+ payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
4812
+ createdAt: completedAt
4813
+ });
4814
+ return;
4815
+ }
4816
+ if (pr.status === "opened" && pr.prUrl) {
4817
+ const completedAt = new Date().toISOString();
4818
+ patchRunRecord(state.projectRoot, runId, {
4819
+ status: "completed",
4820
+ completedAt,
4821
+ errorText: null,
4822
+ ...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
4823
+ });
4824
+ appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
4825
+ emitRigEvent(state, {
4826
+ type: "rig.run.completed",
4827
+ aggregateId: runId,
4828
+ payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
4829
+ createdAt: completedAt
4830
+ });
4831
+ return;
4832
+ }
4833
+ const detail = pr.actionableFeedback.join(`
4834
+ `) || "PR automation did not merge the PR.";
4835
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
4836
+ appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
4837
+ });
4838
+ patchRunRecord(state.projectRoot, runId, {
4839
+ status: "needs_attention",
4840
+ completedAt: new Date().toISOString(),
4841
+ errorText: detail,
4842
+ ...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
4843
+ });
4844
+ appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
4845
+ emitRigEvent(state, {
4846
+ type: "rig.run.needs-attention",
4847
+ aggregateId: runId,
4848
+ payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
4849
+ });
4850
+ }
4361
4851
  var TERMINAL_RUN_STATUSES2 = new Set([
4362
4852
  "completed",
4363
4853
  "complete",
@@ -4383,11 +4873,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
4383
4873
  return;
4384
4874
  throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
4385
4875
  }
4876
+ async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
4877
+ const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
4878
+ if (fromReader)
4879
+ return fromReader;
4880
+ const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
4881
+ if (projected)
4882
+ return projected;
4883
+ if (readTasks !== readWorkspaceTasks) {
4884
+ return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
4885
+ }
4886
+ return null;
4887
+ }
4386
4888
  async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
4387
4889
  if ("taskId" in input && input.taskId) {
4388
4890
  assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
4389
4891
  }
4390
- const sourceTask = "taskId" in input && input.taskId ? (await readTasks(projectRoot)).find((task) => task.id === input.taskId) ?? null : null;
4892
+ const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
4391
4893
  const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
4392
4894
  const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
4393
4895
  const runRecord = {
@@ -4459,6 +4961,7 @@ async function startLocalRun(state, runId, options) {
4459
4961
  throw new Error(`Run not found: ${runId}`);
4460
4962
  }
4461
4963
  const startedAt = new Date().toISOString();
4964
+ const resumeMode = options?.resume === true;
4462
4965
  state.runProcesses.set(runId, {
4463
4966
  runId,
4464
4967
  child: null,
@@ -4475,9 +4978,9 @@ async function startLocalRun(state, runId, options) {
4475
4978
  summary: run.title
4476
4979
  });
4477
4980
  appendRunLogEntry(state.projectRoot, runId, {
4478
- id: `log:${runId}:prepare`,
4479
- title: "Rig task run starting",
4480
- detail: run.taskId ?? run.title,
4981
+ id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
4982
+ title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
4983
+ detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
4481
4984
  tone: "info",
4482
4985
  status: "preparing",
4483
4986
  createdAt: startedAt
@@ -4553,12 +5056,17 @@ async function startLocalRun(state, runId, options) {
4553
5056
  RIG_HOST_PROJECT_ROOT: cliProjectRoot,
4554
5057
  RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
4555
5058
  RIG_SERVER_INTERNAL_EXEC: "1",
5059
+ RIG_SERVER_OWNS_CLOSEOUT: "1",
4556
5060
  ...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
4557
5061
  ...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
4558
5062
  ...bridgeGitHubToken ? {
4559
5063
  RIG_GITHUB_TOKEN: bridgeGitHubToken,
4560
5064
  GITHUB_TOKEN: bridgeGitHubToken,
4561
5065
  GH_TOKEN: bridgeGitHubToken
5066
+ } : {},
5067
+ ...resumeMode ? {
5068
+ RIG_RUN_RESUME: "1",
5069
+ RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
4562
5070
  } : {}
4563
5071
  },
4564
5072
  stdio: ["ignore", "pipe", "pipe"]
@@ -4582,6 +5090,25 @@ async function startLocalRun(state, runId, options) {
4582
5090
  broadcastSnapshotInvalidation(state);
4583
5091
  continue;
4584
5092
  }
5093
+ if (line.startsWith("__RIG_WRAPPER_EVENT__")) {
5094
+ try {
5095
+ const wrapperEvent = JSON.parse(line.slice("__RIG_WRAPPER_EVENT__".length));
5096
+ const eventType = normalizeString(wrapperEvent.type);
5097
+ const payload = wrapperEvent.payload && typeof wrapperEvent.payload === "object" && !Array.isArray(wrapperEvent.payload) ? wrapperEvent.payload : {};
5098
+ if (eventType === "pi.session.ready" && payload.privateMetadata && typeof payload.privateMetadata === "object" && !Array.isArray(payload.privateMetadata)) {
5099
+ patchRunPiSessionMetadata(state.projectRoot, runId, payload.privateMetadata);
5100
+ }
5101
+ appendRunTimelineEntry(state.projectRoot, runId, {
5102
+ id: `timeline:${runId}:${Date.now()}:wrapper:${eventType ?? "event"}`,
5103
+ type: "wrapper-event",
5104
+ eventType,
5105
+ payload,
5106
+ createdAt: normalizeString(wrapperEvent.at) ?? new Date().toISOString()
5107
+ });
5108
+ } catch {}
5109
+ broadcastSnapshotInvalidation(state, "wrapper-event");
5110
+ continue;
5111
+ }
4585
5112
  appendRunLogEntryAndBroadcast(state, runId, {
4586
5113
  id: `log:${runId}:${Date.now()}`,
4587
5114
  title,
@@ -4610,7 +5137,13 @@ async function startLocalRun(state, runId, options) {
4610
5137
  if (!current) {
4611
5138
  return;
4612
5139
  }
4613
- if (exit.code !== 0 && current.status !== "completed" && current.status !== "stopped") {
5140
+ if (closeoutRecord(current)?.status === "pending") {
5141
+ try {
5142
+ await runServerOwnedPrCloseout(state, runId);
5143
+ } catch (closeoutError) {
5144
+ await markServerOwnedCloseoutFailed(state, runId, closeoutError);
5145
+ }
5146
+ } else if (exit.code !== 0 && current.status !== "completed" && current.status !== "stopped") {
4614
5147
  const completedAt = current.completedAt ?? new Date().toISOString();
4615
5148
  const failureSummary = normalizeString(current.errorText) ?? summarizeRunValidationFailure(state.projectRoot, current) ?? `Rig task-run exited with code ${String(exit.code ?? "unknown")}`;
4616
5149
  if (current.status !== "failed") {
@@ -4710,7 +5243,7 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
4710
5243
  }
4711
5244
  try {
4712
5245
  const monorepoRoot = resolveMonorepoRoot3(projectRoot);
4713
- const outerProjectRoot = dirname8(dirname8(monorepoRoot));
5246
+ const outerProjectRoot = dirname9(dirname9(monorepoRoot));
4714
5247
  if (existsSync7(resolve14(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
4715
5248
  return outerProjectRoot;
4716
5249
  }
@@ -4731,7 +5264,19 @@ async function resumeRunRecord(state, input) {
4731
5264
  if (run.status === "completed") {
4732
5265
  throw new Error("Completed runs cannot be resumed.");
4733
5266
  }
4734
- await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
5267
+ const closeout = closeoutRecord(run);
5268
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
5269
+ if (EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
5270
+ patchRunRecord(state.projectRoot, input.runId, {
5271
+ status: "reviewing",
5272
+ completedAt: null,
5273
+ errorText: null,
5274
+ ...closeoutPhasePatch("queued", "pending", { ...closeout, resumedAt: input.createdAt })
5275
+ });
5276
+ scheduleServerOwnedPrCloseout(state, input.runId, "explicit-resume");
5277
+ return;
5278
+ }
5279
+ await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
4735
5280
  }
4736
5281
  function appendRunMessage(projectRoot, input) {
4737
5282
  const run = readAuthorityRun4(projectRoot, input.runId);
@@ -4815,34 +5360,63 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
4815
5360
  writeQueueState(projectRoot, next);
4816
5361
  return next;
4817
5362
  }
4818
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
4819
- function reconcileOrphanedLocalRuns(state, runs, nowIso2) {
4820
- let changed = false;
4821
- for (const run of runs) {
4822
- const status = normalizeString(run.status)?.toLowerCase() ?? "";
4823
- const serverPid = run.serverPid;
4824
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
4825
- if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
4826
- continue;
4827
- }
4828
- const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
4829
- patchRunRecord(state.projectRoot, run.runId, {
4830
- status: "failed",
4831
- completedAt: run.completedAt ?? nowIso2,
4832
- updatedAt: nowIso2,
4833
- errorText: detail
4834
- });
4835
- appendRunLogEntry(state.projectRoot, run.runId, {
4836
- id: `log:${run.runId}:stale-local-run`,
4837
- title: "Run marked stale after server restart",
4838
- detail,
4839
- tone: "error",
4840
- status: "failed",
4841
- createdAt: nowIso2
5363
+ var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
5364
+ var EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running", "needs_attention"]);
5365
+ var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
5366
+ function processExists(pid) {
5367
+ if (!Number.isInteger(pid) || pid <= 0)
5368
+ return false;
5369
+ try {
5370
+ process.kill(pid, 0);
5371
+ return true;
5372
+ } catch {
5373
+ return false;
5374
+ }
5375
+ }
5376
+ function recoverStaleLocalRun(projectRoot, run) {
5377
+ const record = run;
5378
+ if (run.mode !== "local")
5379
+ return false;
5380
+ const closeout = closeoutRecord(record);
5381
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
5382
+ const status = normalizeString(record.status)?.toLowerCase() ?? "";
5383
+ if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus))
5384
+ return false;
5385
+ if (closeoutStatus === "needs_attention") {
5386
+ if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
5387
+ return false;
5388
+ const completedAt2 = record.completedAt ?? new Date().toISOString();
5389
+ patchRunRecord(projectRoot, run.runId, {
5390
+ status: "needs_attention",
5391
+ completedAt: completedAt2,
5392
+ errorText: normalizeString(record.errorText) ?? (Array.isArray(closeout?.feedback) ? closeout.feedback.map(String).join(`
5393
+ `) : null)
4842
5394
  });
4843
- changed = true;
5395
+ return true;
4844
5396
  }
4845
- return changed;
5397
+ if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
5398
+ return false;
5399
+ const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
5400
+ const childPid = typeof record.pid === "number" ? record.pid : null;
5401
+ if (serverPid === null && childPid === null)
5402
+ return false;
5403
+ const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
5404
+ if (hasLiveRecordedProcess && serverPid === process.pid)
5405
+ return false;
5406
+ const completedAt = new Date().toISOString();
5407
+ patchRunRecord(projectRoot, run.runId, {
5408
+ status: "failed",
5409
+ completedAt,
5410
+ errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
5411
+ });
5412
+ return true;
5413
+ }
5414
+ function collectResumableServerCloseouts(state, runs) {
5415
+ return runs.filter((run) => {
5416
+ const closeout = closeoutRecord(run);
5417
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
5418
+ return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
5419
+ });
4846
5420
  }
4847
5421
  async function reconcileScheduler(state, reason) {
4848
5422
  if (state.scheduler.reconciling) {
@@ -4857,7 +5431,28 @@ async function reconcileScheduler(state, reason) {
4857
5431
  const queue = readQueueState(state.projectRoot);
4858
5432
  const tasks = await state.snapshotService.getWorkspaceTasks();
4859
5433
  let runs = listAuthorityRuns4(state.projectRoot);
4860
- let changed = reconcileOrphanedLocalRuns(state, runs, new Date().toISOString());
5434
+ let changed = false;
5435
+ for (const run of runs) {
5436
+ if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
5437
+ changed = true;
5438
+ }
5439
+ }
5440
+ if (changed) {
5441
+ runs = listAuthorityRuns4(state.projectRoot);
5442
+ }
5443
+ const resumableCloseouts = collectResumableServerCloseouts(state, runs);
5444
+ for (const run of resumableCloseouts) {
5445
+ appendRunLogEntry(state.projectRoot, run.runId, {
5446
+ id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
5447
+ title: "Server-owned closeout auto-resume scheduled",
5448
+ detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
5449
+ tone: "info",
5450
+ status: "reviewing",
5451
+ createdAt: new Date().toISOString()
5452
+ });
5453
+ scheduleServerOwnedPrCloseout(state, run.runId, "auto-resume");
5454
+ changed = true;
5455
+ }
4861
5456
  if (changed) {
4862
5457
  runs = listAuthorityRuns4(state.projectRoot);
4863
5458
  }
@@ -4933,11 +5528,11 @@ async function reconcileScheduler(state, reason) {
4933
5528
  // packages/server/src/server-helpers/http-router.ts
4934
5529
  import { randomUUID } from "crypto";
4935
5530
  import { spawnSync as spawnSync3 } from "child_process";
4936
- import { basename, dirname as dirname12, isAbsolute as isAbsolute3, resolve as resolve18 } from "path";
4937
- import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync10 } from "fs";
5531
+ import { basename, dirname as dirname15, isAbsolute as isAbsolute4, resolve as resolve20 } from "path";
5532
+ import { copyFileSync as copyFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync12 } from "fs";
4938
5533
  import {
4939
5534
  listAuthorityRuns as listAuthorityRuns5,
4940
- readAuthorityRun as readAuthorityRun6,
5535
+ readAuthorityRun as readAuthorityRun7,
4941
5536
  resolveAuthorityPaths,
4942
5537
  writeJsonFile as writeJsonFile4
4943
5538
  } from "@rig/runtime/control-plane/authority-files";
@@ -4957,10 +5552,64 @@ import {
4957
5552
  RemoteWsClient
4958
5553
  } from "@rig/runtime/control-plane/remote";
4959
5554
 
5555
+ // packages/server/src/server-helpers/pi-session-proxy.ts
5556
+ import { readAuthorityRun as readAuthorityRun5 } from "@rig/runtime/control-plane/authority-files";
5557
+ function resolveRunPiSessionProxy(projectRoot, runId) {
5558
+ const run = readAuthorityRun5(projectRoot, runId);
5559
+ if (!run)
5560
+ return null;
5561
+ const privateMetadata = readRunPiSessionMetadata(projectRoot, runId);
5562
+ if (!privateMetadata)
5563
+ return { pending: true, runId, status: typeof run.status === "string" ? run.status : undefined };
5564
+ const connection = privateMetadata.daemonConnection;
5565
+ if (connection.mode !== "http")
5566
+ throw new Error("Only loopback HTTP Rig Pi session daemon connections are supported");
5567
+ const token = tokenFromRef(connection.tokenRef);
5568
+ if (!token)
5569
+ throw new Error("Rig Pi session daemon token is unavailable");
5570
+ return {
5571
+ runId,
5572
+ sessionId: privateMetadata.public.sessionId,
5573
+ metadata: privateMetadata.public,
5574
+ privateMetadata,
5575
+ baseUrl: connection.baseUrl.replace(/\/+$/, ""),
5576
+ token
5577
+ };
5578
+ }
5579
+ async function proxyRunPiHttp(projectRoot, runId, input) {
5580
+ const resolved = resolveRunPiSessionProxy(projectRoot, runId);
5581
+ if (resolved === null)
5582
+ return { status: 404, payload: { ok: false, error: "Run not found" } };
5583
+ if ("pending" in resolved)
5584
+ return { status: 409, payload: { ready: false, runId, status: resolved.status, retryAfterMs: 500 } };
5585
+ const response = await fetch(`${resolved.baseUrl}${input.daemonPath}`, {
5586
+ method: input.method,
5587
+ headers: {
5588
+ authorization: `Bearer ${resolved.token}`,
5589
+ ...input.body === undefined ? {} : { "content-type": "application/json" }
5590
+ },
5591
+ body: input.body === undefined ? undefined : JSON.stringify(input.body)
5592
+ });
5593
+ const text = await response.text();
5594
+ const payload = text.trim() ? JSON.parse(text) : null;
5595
+ return { status: response.status, payload };
5596
+ }
5597
+ function buildRunPiDaemonWebSocketUrl(resolved) {
5598
+ const url = new URL(`${resolved.baseUrl}/sessions/${encodeURIComponent(resolved.sessionId)}/events`);
5599
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
5600
+ url.searchParams.set("token", resolved.token);
5601
+ return url.toString();
5602
+ }
5603
+ function tokenFromRef(ref) {
5604
+ if (ref.startsWith("inline:"))
5605
+ return ref.slice("inline:".length) || null;
5606
+ return null;
5607
+ }
5608
+
4960
5609
  // packages/server/src/server-helpers/run-steering.ts
4961
- import { dirname as dirname9, resolve as resolve15 } from "path";
5610
+ import { dirname as dirname10, resolve as resolve15 } from "path";
4962
5611
  import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync5 } from "fs";
4963
- import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun5, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
5612
+ import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun6, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
4964
5613
  var steeringSequence = 0;
4965
5614
  function runSteeringPath(projectRoot, runId) {
4966
5615
  return resolve15(resolveAuthorityRunDir4(projectRoot, runId), "steering.jsonl");
@@ -5029,7 +5678,7 @@ function markQueuedRunSteeringMessagesDelivered(projectRoot, runId, ids) {
5029
5678
  return delivered;
5030
5679
  }
5031
5680
  function queueRunSteeringMessage(projectRoot, runId, input) {
5032
- const run = readAuthorityRun5(projectRoot, runId);
5681
+ const run = readAuthorityRun6(projectRoot, runId);
5033
5682
  if (!run)
5034
5683
  throw new Error(`Run not found: ${runId}`);
5035
5684
  const text = input.message.trim();
@@ -5045,7 +5694,7 @@ function queueRunSteeringMessage(projectRoot, runId, input) {
5045
5694
  delivered: false
5046
5695
  };
5047
5696
  const path = runSteeringPath(projectRoot, runId);
5048
- mkdirSync8(dirname9(path), { recursive: true });
5697
+ mkdirSync8(dirname10(path), { recursive: true });
5049
5698
  appendJsonlRecord2(path, entry);
5050
5699
  appendRunTimelineEntry(projectRoot, runId, {
5051
5700
  id: entry.id,
@@ -5082,6 +5731,187 @@ import {
5082
5731
  updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
5083
5732
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
5084
5733
 
5734
+ // packages/server/src/server-helpers/github-api-session-index.ts
5735
+ import { chmodSync as chmodSync3, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
5736
+ import { dirname as dirname12, resolve as resolve17 } from "path";
5737
+
5738
+ // packages/server/src/server-helpers/github-user-namespace.ts
5739
+ import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync8 } from "fs";
5740
+ import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve16 } from "path";
5741
+ function cleanString3(value) {
5742
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
5743
+ }
5744
+ function sanitizePathSegment(value) {
5745
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
5746
+ }
5747
+ function deriveGitHubUserNamespaceKey(identity) {
5748
+ const userId = cleanString3(identity.userId);
5749
+ if (userId) {
5750
+ const safeId = sanitizePathSegment(userId);
5751
+ if (safeId)
5752
+ return `ghu-${safeId}`;
5753
+ }
5754
+ const login = cleanString3(identity.login);
5755
+ if (login) {
5756
+ const safeLogin = sanitizePathSegment(login);
5757
+ if (safeLogin)
5758
+ return `ghu-login-${safeLogin}`;
5759
+ }
5760
+ throw new Error("GitHub user namespace requires a user id or login");
5761
+ }
5762
+ function resolveRemoteUserNamespacesRoot(projectRoot) {
5763
+ const explicitRoot = cleanString3(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
5764
+ if (explicitRoot)
5765
+ return resolve16(explicitRoot);
5766
+ const stateDir2 = cleanString3(process.env.RIG_STATE_DIR);
5767
+ if (stateDir2)
5768
+ return resolve16(dirname11(resolve16(stateDir2)), "users");
5769
+ return resolve16(projectRoot, ".rig", "users");
5770
+ }
5771
+ function resolveRemoteUserNamespace(projectRoot, identity) {
5772
+ const key = deriveGitHubUserNamespaceKey(identity);
5773
+ const root = resolve16(resolveRemoteUserNamespacesRoot(projectRoot), key);
5774
+ const stateDir2 = resolve16(root, ".rig", "state");
5775
+ return {
5776
+ key,
5777
+ userId: cleanString3(identity.userId),
5778
+ login: cleanString3(identity.login),
5779
+ root,
5780
+ stateDir: stateDir2,
5781
+ authStateFile: resolve16(stateDir2, "github-auth.json"),
5782
+ metadataFile: resolve16(stateDir2, "user-namespace.json"),
5783
+ checkoutBaseDir: resolve16(root, "remote-checkouts"),
5784
+ snapshotBaseDir: resolve16(root, "remote-snapshots")
5785
+ };
5786
+ }
5787
+ function serializeRemoteUserNamespace(namespace) {
5788
+ return {
5789
+ key: namespace.key,
5790
+ userId: namespace.userId,
5791
+ login: namespace.login,
5792
+ root: namespace.root,
5793
+ checkoutBaseDir: namespace.checkoutBaseDir,
5794
+ snapshotBaseDir: namespace.snapshotBaseDir
5795
+ };
5796
+ }
5797
+ function isPathInsideNamespace(namespaceRoot, candidatePath) {
5798
+ const root = resolve16(namespaceRoot);
5799
+ const candidate = resolve16(candidatePath);
5800
+ const rel = relative3(root, candidate);
5801
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
5802
+ }
5803
+ function writeRemoteUserNamespaceMetadata(namespace) {
5804
+ mkdirSync9(namespace.stateDir, { recursive: true });
5805
+ const previous = (() => {
5806
+ if (!existsSync9(namespace.metadataFile))
5807
+ return null;
5808
+ try {
5809
+ const parsed = JSON.parse(readFileSync6(namespace.metadataFile, "utf8"));
5810
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
5811
+ } catch {
5812
+ return null;
5813
+ }
5814
+ })();
5815
+ const now = new Date().toISOString();
5816
+ writeFileSync8(namespace.metadataFile, `${JSON.stringify({
5817
+ key: namespace.key,
5818
+ userId: namespace.userId,
5819
+ login: namespace.login,
5820
+ root: namespace.root,
5821
+ checkoutBaseDir: namespace.checkoutBaseDir,
5822
+ snapshotBaseDir: namespace.snapshotBaseDir,
5823
+ createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
5824
+ updatedAt: now
5825
+ }, null, 2)}
5826
+ `, { encoding: "utf8", mode: 384 });
5827
+ try {
5828
+ chmodSync2(namespace.metadataFile, 384);
5829
+ } catch {}
5830
+ }
5831
+
5832
+ // packages/server/src/server-helpers/github-api-session-index.ts
5833
+ function cleanString4(value) {
5834
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
5835
+ }
5836
+ function resolveGitHubApiSessionIndexFile(projectRoot) {
5837
+ return resolve17(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
5838
+ }
5839
+ function parseEntry(value) {
5840
+ if (!value || typeof value !== "object" || Array.isArray(value))
5841
+ return null;
5842
+ const record = value;
5843
+ const token = cleanString4(record.token);
5844
+ const namespaceKey = cleanString4(record.namespaceKey);
5845
+ const namespaceRoot = cleanString4(record.namespaceRoot);
5846
+ const authStateFile = cleanString4(record.authStateFile);
5847
+ const checkoutBaseDir = cleanString4(record.checkoutBaseDir);
5848
+ const snapshotBaseDir = cleanString4(record.snapshotBaseDir);
5849
+ const createdAt = cleanString4(record.createdAt);
5850
+ if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
5851
+ return null;
5852
+ return {
5853
+ token,
5854
+ namespaceKey,
5855
+ namespaceRoot,
5856
+ authStateFile,
5857
+ checkoutBaseDir,
5858
+ snapshotBaseDir,
5859
+ createdAt,
5860
+ login: cleanString4(record.login),
5861
+ userId: cleanString4(record.userId),
5862
+ selectedRepo: cleanString4(record.selectedRepo)
5863
+ };
5864
+ }
5865
+ function readIndex(indexFile) {
5866
+ if (!existsSync10(indexFile))
5867
+ return [];
5868
+ try {
5869
+ const parsed = JSON.parse(readFileSync7(indexFile, "utf8"));
5870
+ return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
5871
+ const parsedEntry = parseEntry(entry);
5872
+ return parsedEntry ? [parsedEntry] : [];
5873
+ }) : [];
5874
+ } catch {
5875
+ return [];
5876
+ }
5877
+ }
5878
+ function writeIndex(indexFile, sessions) {
5879
+ mkdirSync10(dirname12(indexFile), { recursive: true });
5880
+ writeFileSync9(indexFile, `${JSON.stringify({ sessions }, null, 2)}
5881
+ `, { encoding: "utf8", mode: 384 });
5882
+ try {
5883
+ chmodSync3(indexFile, 384);
5884
+ } catch {}
5885
+ }
5886
+ function registerGitHubApiSession(input) {
5887
+ const cleanToken = cleanString4(input.token);
5888
+ if (!cleanToken)
5889
+ throw new Error("GitHub API session token is required");
5890
+ const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
5891
+ const createdAt = new Date().toISOString();
5892
+ const entry = {
5893
+ token: cleanToken,
5894
+ login: input.namespace.login,
5895
+ userId: input.namespace.userId,
5896
+ namespaceKey: input.namespace.key,
5897
+ namespaceRoot: input.namespace.root,
5898
+ authStateFile: input.namespace.authStateFile,
5899
+ checkoutBaseDir: input.namespace.checkoutBaseDir,
5900
+ snapshotBaseDir: input.namespace.snapshotBaseDir,
5901
+ selectedRepo: cleanString4(input.selectedRepo),
5902
+ createdAt
5903
+ };
5904
+ const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
5905
+ writeIndex(indexFile, [...previous.slice(-199), entry]);
5906
+ return entry;
5907
+ }
5908
+ function readGitHubApiSession(input) {
5909
+ const cleanToken = cleanString4(input.token);
5910
+ if (!cleanToken)
5911
+ return null;
5912
+ return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
5913
+ }
5914
+
5085
5915
  // packages/server/src/server-helpers/inspector-agent-lifecycle.ts
5086
5916
  function createInspectorAgentLifecycleController(options) {
5087
5917
  const initialDelayMs = Math.max(1, options.initialDelayMs ?? 5000);
@@ -5185,21 +6015,21 @@ function inspectorAgentLifecycleSnapshot(input) {
5185
6015
  // packages/server/src/server-helpers/project-registry.ts
5186
6016
  import { createHash as createHash2 } from "crypto";
5187
6017
  import { spawnSync as spawnSync2 } from "child_process";
5188
- import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync6, readdirSync as readdirSync3, writeFileSync as writeFileSync8 } from "fs";
5189
- import { dirname as dirname10, resolve as resolve16 } from "path";
6018
+ import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync8, readdirSync as readdirSync3, writeFileSync as writeFileSync10 } from "fs";
6019
+ import { dirname as dirname13, resolve as resolve18 } from "path";
5190
6020
  function normalizeRepoSlug(value) {
5191
6021
  const trimmed = value.trim();
5192
6022
  return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
5193
6023
  }
5194
6024
  function registryPath(projectRoot) {
5195
- return resolve16(projectRoot, ".rig", "state", "projects.json");
6025
+ return resolve18(projectRoot, ".rig", "state", "projects.json");
5196
6026
  }
5197
6027
  function readRegistry(projectRoot) {
5198
6028
  const path = registryPath(projectRoot);
5199
- if (!existsSync9(path))
6029
+ if (!existsSync11(path))
5200
6030
  return {};
5201
6031
  try {
5202
- const payload = JSON.parse(readFileSync6(path, "utf8"));
6032
+ const payload = JSON.parse(readFileSync8(path, "utf8"));
5203
6033
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
5204
6034
  return {};
5205
6035
  const projects = payload.projects;
@@ -5210,14 +6040,14 @@ function readRegistry(projectRoot) {
5210
6040
  }
5211
6041
  function writeRegistry(projectRoot, projects) {
5212
6042
  const path = registryPath(projectRoot);
5213
- mkdirSync9(dirname10(path), { recursive: true });
5214
- writeFileSync8(path, `${JSON.stringify({ projects }, null, 2)}
6043
+ mkdirSync11(dirname13(path), { recursive: true });
6044
+ writeFileSync10(path, `${JSON.stringify({ projects }, null, 2)}
5215
6045
  `, "utf8");
5216
6046
  }
5217
6047
  function resolveConfigPath(projectRoot) {
5218
6048
  for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
5219
- const path = resolve16(projectRoot, name);
5220
- if (existsSync9(path))
6049
+ const path = resolve18(projectRoot, name);
6050
+ if (existsSync11(path))
5221
6051
  return path;
5222
6052
  }
5223
6053
  return null;
@@ -5226,7 +6056,7 @@ function hashFile(path) {
5226
6056
  if (!path)
5227
6057
  return null;
5228
6058
  try {
5229
- return createHash2("sha256").update(readFileSync6(path)).digest("hex");
6059
+ return createHash2("sha256").update(readFileSync8(path)).digest("hex");
5230
6060
  } catch {
5231
6061
  return null;
5232
6062
  }
@@ -5242,11 +6072,11 @@ function readDefaultBranch(projectRoot) {
5242
6072
  return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
5243
6073
  }
5244
6074
  function buildRunSummary(projectRoot) {
5245
- const runsDir = resolve16(projectRoot, ".rig", "runs");
6075
+ const runsDir = resolve18(projectRoot, ".rig", "runs");
5246
6076
  try {
5247
6077
  const runs = readdirSync3(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
5248
6078
  try {
5249
- const run = JSON.parse(readFileSync6(resolve16(runsDir, entry.name, "run.json"), "utf8"));
6079
+ const run = JSON.parse(readFileSync8(resolve18(runsDir, entry.name, "run.json"), "utf8"));
5250
6080
  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 : "" }];
5251
6081
  } catch {
5252
6082
  return [];
@@ -5298,10 +6128,14 @@ function upsertProjectRecord(projectRoot, input) {
5298
6128
  function linkProjectCheckout(projectRoot, repoSlug, checkout) {
5299
6129
  return upsertProjectRecord(projectRoot, { repoSlug, checkout });
5300
6130
  }
6131
+ function projectRegistryContainsCheckout(projectRoot, checkoutPath) {
6132
+ const target = resolve18(checkoutPath);
6133
+ return Object.values(readRegistry(projectRoot)).some((project) => project.checkouts.some((checkout) => checkout.path ? resolve18(checkout.path) === target : false));
6134
+ }
5301
6135
 
5302
6136
  // packages/server/src/server-helpers/remote-checkout.ts
5303
- import { existsSync as existsSync10, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
5304
- import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve17 } from "path";
6137
+ import { existsSync as existsSync12, mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
6138
+ import { dirname as dirname14, isAbsolute as isAbsolute3, relative as relative4, resolve as resolve19 } from "path";
5305
6139
  function safeSlugSegments(repoSlug) {
5306
6140
  const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
5307
6141
  if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
@@ -5318,7 +6152,7 @@ function safeCheckoutKey(value) {
5318
6152
  }
5319
6153
  function repoSlugPath(baseDir, repoSlug, checkoutKey) {
5320
6154
  const key = safeCheckoutKey(checkoutKey);
5321
- return resolve17(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
6155
+ return resolve19(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
5322
6156
  }
5323
6157
  function sanitizeSnapshotId(value, fallback) {
5324
6158
  const raw = (value ?? fallback).trim();
@@ -5326,7 +6160,7 @@ function sanitizeSnapshotId(value, fallback) {
5326
6160
  return safe || fallback;
5327
6161
  }
5328
6162
  function assertWithinRoot(root, relativePath) {
5329
- if (!relativePath || isAbsolute2(relativePath) || relativePath.includes("\x00")) {
6163
+ if (!relativePath || isAbsolute3(relativePath) || relativePath.includes("\x00")) {
5330
6164
  throw new Error(`Invalid snapshot file path: ${relativePath}`);
5331
6165
  }
5332
6166
  const normalizedRelative = relativePath.replace(/\\/g, "/");
@@ -5334,9 +6168,9 @@ function assertWithinRoot(root, relativePath) {
5334
6168
  if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
5335
6169
  throw new Error(`Unsafe snapshot file path: ${relativePath}`);
5336
6170
  }
5337
- const target = resolve17(root, ...segments);
5338
- const rel = relative3(root, target);
5339
- if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute2(rel)) {
6171
+ const target = resolve19(root, ...segments);
6172
+ const rel = relative4(root, target);
6173
+ if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute3(rel)) {
5340
6174
  throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
5341
6175
  }
5342
6176
  return target;
@@ -5354,17 +6188,17 @@ function parseSnapshotArchiveContentBase64(contentBase64) {
5354
6188
  function extractUploadedSnapshotArchive(input) {
5355
6189
  const archive = decodeSnapshotArchive(input.archive);
5356
6190
  const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
5357
- const checkoutPath = resolve17(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
5358
- mkdirSync10(checkoutPath, { recursive: true });
6191
+ const checkoutPath = resolve19(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
6192
+ mkdirSync12(checkoutPath, { recursive: true });
5359
6193
  for (const file of archive.files) {
5360
6194
  if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
5361
6195
  throw new Error("Invalid snapshot archive file entry");
5362
6196
  }
5363
6197
  const target = assertWithinRoot(checkoutPath, file.path);
5364
- mkdirSync10(dirname11(target), { recursive: true });
5365
- writeFileSync9(target, Buffer.from(file.contentBase64, "base64"));
6198
+ mkdirSync12(dirname14(target), { recursive: true });
6199
+ writeFileSync11(target, Buffer.from(file.contentBase64, "base64"));
5366
6200
  }
5367
- writeFileSync9(resolve17(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
6201
+ writeFileSync11(resolve19(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
5368
6202
  repoSlug: input.repoSlug,
5369
6203
  snapshotId,
5370
6204
  fileCount: archive.files.length,
@@ -5399,7 +6233,7 @@ function gitCredentialConfig(token) {
5399
6233
  };
5400
6234
  }
5401
6235
  async function prepareRemoteCheckout(input) {
5402
- const exists = input.exists ?? existsSync10;
6236
+ const exists = input.exists ?? existsSync12;
5403
6237
  const strategy = input.strategy;
5404
6238
  if (strategy.kind === "uploaded-snapshot") {
5405
6239
  return extractUploadedSnapshotArchive({
@@ -5411,7 +6245,7 @@ async function prepareRemoteCheckout(input) {
5411
6245
  });
5412
6246
  }
5413
6247
  if (strategy.kind === "existing-path") {
5414
- const checkoutPath2 = resolve17(strategy.path);
6248
+ const checkoutPath2 = resolve19(strategy.path);
5415
6249
  if (!exists(checkoutPath2)) {
5416
6250
  throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
5417
6251
  }
@@ -5447,9 +6281,9 @@ function buildServerControlStatus() {
5447
6281
  };
5448
6282
  }
5449
6283
  function buildProjectConfigStatus(root) {
5450
- const hasConfigTs = existsSync11(resolve18(root, "rig.config.ts"));
5451
- const hasConfigJson = existsSync11(resolve18(root, "rig.config.json"));
5452
- const hasLegacyTaskConfig = existsSync11(resolve18(root, ".rig", "task-config.json"));
6284
+ const hasConfigTs = existsSync13(resolve20(root, "rig.config.ts"));
6285
+ const hasConfigJson = existsSync13(resolve20(root, "rig.config.json"));
6286
+ const hasLegacyTaskConfig = existsSync13(resolve20(root, ".rig", "task-config.json"));
5453
6287
  let kind = "missing";
5454
6288
  if (hasConfigTs)
5455
6289
  kind = "rig-config-ts";
@@ -5466,6 +6300,75 @@ function buildProjectConfigStatus(root) {
5466
6300
  suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
5467
6301
  };
5468
6302
  }
6303
+ var RIG_GITHUB_LIFECYCLE_LABELS = [
6304
+ "ready",
6305
+ "blocked",
6306
+ "in-progress",
6307
+ "under-review",
6308
+ "failed",
6309
+ "cancelled",
6310
+ "rig:running",
6311
+ "rig:pr-open",
6312
+ "rig:ci-fixing",
6313
+ "rig:merging",
6314
+ "rig:done",
6315
+ "rig:needs-attention"
6316
+ ];
6317
+ function githubProjectsEnabled2(config) {
6318
+ if (!config || typeof config !== "object" || Array.isArray(config))
6319
+ return false;
6320
+ const root = config;
6321
+ const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
6322
+ const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
6323
+ return projects?.enabled === true;
6324
+ }
6325
+ function githubIssueSourceRepo(config) {
6326
+ if (!config || typeof config !== "object" || Array.isArray(config))
6327
+ return null;
6328
+ const root = config;
6329
+ const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
6330
+ const owner = normalizeString(taskSource?.owner);
6331
+ const repo = normalizeString(taskSource?.repo);
6332
+ if (taskSource?.kind === "github-issues" && owner && repo)
6333
+ return { owner, repo };
6334
+ const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
6335
+ const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
6336
+ const match = slug?.match(/^([^/]+)\/([^/]+)$/);
6337
+ return match ? { owner: match[1], repo: match[2] } : null;
6338
+ }
6339
+ async function ensureGitHubLifecycleLabels(projectRoot, config) {
6340
+ const repo = githubIssueSourceRepo(config);
6341
+ if (!repo)
6342
+ return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
6343
+ const token = createGitHubAuthStore(projectRoot).readToken();
6344
+ if (!token)
6345
+ return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
6346
+ const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
6347
+ headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
6348
+ });
6349
+ const existingJson = await existingResponse.json().catch(() => []);
6350
+ const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
6351
+ const created = [];
6352
+ const alreadyPresent = [];
6353
+ const failed = [];
6354
+ for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
6355
+ if (existing.has(label)) {
6356
+ alreadyPresent.push(label);
6357
+ continue;
6358
+ }
6359
+ const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
6360
+ method: "POST",
6361
+ headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
6362
+ 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" })
6363
+ });
6364
+ if (response.ok || response.status === 422) {
6365
+ (response.status === 422 ? alreadyPresent : created).push(label);
6366
+ } else {
6367
+ failed.push({ label, error: await response.text().catch(() => response.statusText) });
6368
+ }
6369
+ }
6370
+ return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
6371
+ }
5469
6372
  function normalizeCommit(value) {
5470
6373
  const raw = normalizeString(value);
5471
6374
  return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
@@ -5485,24 +6388,24 @@ function repoParts(repoSlug) {
5485
6388
  return { owner, repo, slug: `${owner}/${repo}` };
5486
6389
  }
5487
6390
  function repairDir(checkoutPath) {
5488
- const dir = resolve18(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
5489
- mkdirSync11(dir, { recursive: true });
6391
+ const dir = resolve20(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
6392
+ mkdirSync13(dir, { recursive: true });
5490
6393
  return dir;
5491
6394
  }
5492
6395
  function backupCheckoutFile(checkoutPath, relativePath) {
5493
- const source = resolve18(checkoutPath, relativePath);
5494
- const backupPath = resolve18(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
5495
- mkdirSync11(dirname12(backupPath), { recursive: true });
5496
- copyFileSync(source, backupPath);
6396
+ const source = resolve20(checkoutPath, relativePath);
6397
+ const backupPath = resolve20(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
6398
+ mkdirSync13(dirname15(backupPath), { recursive: true });
6399
+ copyFileSync2(source, backupPath);
5497
6400
  return backupPath;
5498
6401
  }
5499
6402
  function parsePackageJsonLosslessly(checkoutPath) {
5500
- const packagePath = resolve18(checkoutPath, "package.json");
5501
- if (!existsSync11(packagePath)) {
6403
+ const packagePath = resolve20(checkoutPath, "package.json");
6404
+ if (!existsSync13(packagePath)) {
5502
6405
  return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
5503
6406
  }
5504
6407
  try {
5505
- const parsed = JSON.parse(readFileSync7(packagePath, "utf8"));
6408
+ const parsed = JSON.parse(readFileSync9(packagePath, "utf8"));
5506
6409
  return {
5507
6410
  existed: true,
5508
6411
  packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
@@ -5516,9 +6419,9 @@ function parsePackageJsonLosslessly(checkoutPath) {
5516
6419
  }
5517
6420
  }
5518
6421
  function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
5519
- const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync11(resolve18(checkoutPath, name)));
5520
- const packagePath = resolve18(checkoutPath, "package.json");
5521
- if (!hasConfig && !existsSync11(packagePath)) {
6422
+ const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
6423
+ const packagePath = resolve20(checkoutPath, "package.json");
6424
+ if (!hasConfig && !existsSync13(packagePath)) {
5522
6425
  return { skipped: true, reason: "package.json and rig.config missing" };
5523
6426
  }
5524
6427
  const parsed = parsePackageJsonLosslessly(checkoutPath);
@@ -5537,7 +6440,7 @@ function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
5537
6440
  }
5538
6441
  const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
5539
6442
  if (changed) {
5540
- writeFileSync10(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
6443
+ writeFileSync12(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
5541
6444
  `, "utf8");
5542
6445
  }
5543
6446
  return {
@@ -5563,11 +6466,11 @@ function configLooksStructurallyUsable(source) {
5563
6466
  return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
5564
6467
  }
5565
6468
  function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
5566
- const configPath = resolve18(checkoutPath, "rig.config.ts");
5567
- const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync11(resolve18(checkoutPath, name)));
6469
+ const configPath = resolve20(checkoutPath, "rig.config.ts");
6470
+ const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
5568
6471
  if (existingConfigName) {
5569
- const existingPath = resolve18(checkoutPath, existingConfigName);
5570
- const source = readFileSync7(existingPath, "utf8");
6472
+ const existingPath = resolve20(checkoutPath, existingConfigName);
6473
+ const source = readFileSync9(existingPath, "utf8");
5571
6474
  if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
5572
6475
  return { path: existingPath, changed: false, reason: "config structurally complete" };
5573
6476
  }
@@ -5581,7 +6484,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
5581
6484
  }
5582
6485
  }
5583
6486
  const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
5584
- writeFileSync10(configPath, generatedRigConfigSource(repoSlug), "utf8");
6487
+ writeFileSync12(configPath, generatedRigConfigSource(repoSlug), "utf8");
5585
6488
  return {
5586
6489
  path: configPath,
5587
6490
  changed: true,
@@ -5590,7 +6493,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
5590
6493
  };
5591
6494
  }
5592
6495
  function validateRemoteCheckoutRigConfig(checkoutPath) {
5593
- const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync11(resolve18(checkoutPath, name)));
6496
+ const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
5594
6497
  if (!configFile)
5595
6498
  return { ok: false, error: "missing rig config" };
5596
6499
  if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
@@ -5612,7 +6515,7 @@ function validateRemoteCheckoutRigConfig(checkoutPath) {
5612
6515
  return { ok: true, configFile };
5613
6516
  }
5614
6517
  function installRemoteCheckoutPackages(checkoutPath) {
5615
- if (!existsSync11(resolve18(checkoutPath, "package.json"))) {
6518
+ if (!existsSync13(resolve20(checkoutPath, "package.json"))) {
5616
6519
  return { skipped: true, reason: "package.json missing" };
5617
6520
  }
5618
6521
  if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
@@ -5625,8 +6528,8 @@ function installRemoteCheckoutPackages(checkoutPath) {
5625
6528
  return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
5626
6529
  }
5627
6530
  function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
5628
- const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync11(resolve18(checkoutPath, name)));
5629
- const hasPackage = existsSync11(resolve18(checkoutPath, "package.json"));
6531
+ const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
6532
+ const hasPackage = existsSync13(resolve20(checkoutPath, "package.json"));
5630
6533
  if (!hasConfig && !hasPackage) {
5631
6534
  return {
5632
6535
  packageJson: { skipped: true, reason: "package.json and rig.config missing" },
@@ -5724,26 +6627,26 @@ function buildRemoteRunLogEntry(body, identifiers) {
5724
6627
  }
5725
6628
  function readGitHeadCommit(projectRoot) {
5726
6629
  try {
5727
- let gitDir = resolve18(projectRoot, ".git");
6630
+ let gitDir = resolve20(projectRoot, ".git");
5728
6631
  try {
5729
- const dotGit = readFileSync7(gitDir, "utf8").trim();
6632
+ const dotGit = readFileSync9(gitDir, "utf8").trim();
5730
6633
  const gitDirPrefix = "gitdir:";
5731
6634
  if (dotGit.startsWith(gitDirPrefix)) {
5732
- gitDir = resolve18(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
6635
+ gitDir = resolve20(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
5733
6636
  }
5734
6637
  } catch {}
5735
- const head = readFileSync7(resolve18(gitDir, "HEAD"), "utf8").trim();
6638
+ const head = readFileSync9(resolve20(gitDir, "HEAD"), "utf8").trim();
5736
6639
  const refPrefix = "ref:";
5737
6640
  if (!head.startsWith(refPrefix)) {
5738
6641
  return normalizeCommit(head);
5739
6642
  }
5740
6643
  const ref = head.slice(refPrefix.length).trim();
5741
- const refPath = resolve18(gitDir, ref);
5742
- if (existsSync11(refPath)) {
5743
- return normalizeCommit(readFileSync7(refPath, "utf8").trim());
6644
+ const refPath = resolve20(gitDir, ref);
6645
+ if (existsSync13(refPath)) {
6646
+ return normalizeCommit(readFileSync9(refPath, "utf8").trim());
5744
6647
  }
5745
- const commonDir = normalizeString(readFileSync7(resolve18(gitDir, "commondir"), "utf8"));
5746
- return commonDir ? normalizeCommit(readFileSync7(resolve18(gitDir, commonDir, ref), "utf8").trim()) : null;
6648
+ const commonDir = normalizeString(readFileSync9(resolve20(gitDir, "commondir"), "utf8"));
6649
+ return commonDir ? normalizeCommit(readFileSync9(resolve20(gitDir, commonDir, ref), "utf8").trim()) : null;
5747
6650
  } catch {
5748
6651
  return null;
5749
6652
  }
@@ -5763,7 +6666,8 @@ function isAuthorizedInspectorStreamRequest(req, authToken) {
5763
6666
  }
5764
6667
  function buildDeploymentStatus(projectRoot) {
5765
6668
  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);
5766
- const gitCommit = envCommit ?? readGitHeadCommit(projectRoot);
6669
+ const deploymentRoot = normalizeString(process.env.RIG_HOST_PROJECT_ROOT) ?? projectRoot;
6670
+ const gitCommit = envCommit ?? readGitHeadCommit(deploymentRoot) ?? readGitHeadCommit(projectRoot);
5767
6671
  return {
5768
6672
  currentCommit: gitCommit,
5769
6673
  commitSource: envCommit ? "env" : gitCommit ? "git" : null,
@@ -5781,9 +6685,9 @@ function configuredRepoFromTaskSource(taskSource) {
5781
6685
  const repo = normalizeString(taskSource?.repo);
5782
6686
  return owner && repo ? `${owner}/${repo}` : null;
5783
6687
  }
5784
- async function buildTaskSourceStatus(state, config) {
6688
+ async function buildTaskSourceStatus(state, config, requestAuth) {
5785
6689
  const diagnostics = state.snapshotService.getTaskSourceErrors();
5786
- const selectedRepo = createGitHubAuthStore(state.projectRoot).status({
6690
+ const selectedRepo = requestScopedAuthStore(state.projectRoot, requestAuth).status({
5787
6691
  oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
5788
6692
  }).selectedRepo;
5789
6693
  try {
@@ -5836,37 +6740,146 @@ function isLoopbackRequest(req) {
5836
6740
  }
5837
6741
  }
5838
6742
  function isPublicRigAuthBootstrapRoute(pathname) {
5839
- 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";
6743
+ 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";
6744
+ }
6745
+ function buildRigInstallScript() {
6746
+ return `#!/usr/bin/env bash
6747
+ set -euo pipefail
6748
+
6749
+ say() {
6750
+ printf 'rig-install: %s
6751
+ ' "$*"
6752
+ }
6753
+
6754
+ if ! command -v bun >/dev/null 2>&1; then
6755
+ say "Bun not found; installing Bun first"
6756
+ curl -fsSL https://bun.sh/install | bash
6757
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
6758
+ export PATH="$BUN_INSTALL/bin:$PATH"
6759
+ fi
6760
+
6761
+ if ! command -v bun >/dev/null 2>&1; then
6762
+ printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
6763
+ ' >&2
6764
+ exit 1
6765
+ fi
6766
+
6767
+ say "Installing @h-rig/cli@latest"
6768
+ bun add -g --force @h-rig/cli@latest
6769
+
6770
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
6771
+ BUN_RIG="$BUN_INSTALL/bin/rig"
6772
+ if [ ! -x "$BUN_RIG" ]; then
6773
+ printf 'rig-install: expected Bun global rig at %s but it was not executable.
6774
+ ' "$BUN_RIG" >&2
6775
+ exit 1
6776
+ fi
6777
+
6778
+ USER_BIN="$HOME/.local/bin"
6779
+ mkdir -p "$USER_BIN"
6780
+ cat > "$USER_BIN/rig" <<'RIG_SHIM'
6781
+ #!/usr/bin/env bash
6782
+ set -euo pipefail
6783
+ exec "\${BUN_INSTALL:-$HOME/.bun}/bin/rig" "$@"
6784
+ RIG_SHIM
6785
+ chmod +x "$USER_BIN/rig"
6786
+
6787
+ export PATH="$USER_BIN:$BUN_INSTALL/bin:$PATH"
6788
+ if command -v hash >/dev/null 2>&1; then hash -r; fi
6789
+
6790
+ if ! command -v rig >/dev/null 2>&1; then
6791
+ printf 'rig-install: rig installed, but rig is not on PATH. Add %s and %s/bin to PATH and retry.
6792
+ ' "$USER_BIN" "$BUN_INSTALL" >&2
6793
+ exit 1
6794
+ fi
6795
+
6796
+ say "Verifying rig"
6797
+ "$BUN_RIG" --help >/dev/null
6798
+ rig --help >/dev/null
6799
+ say "Done. Run: rig --help"
6800
+ `;
5840
6801
  }
5841
6802
  function normalizePrMode(value) {
5842
6803
  const mode = normalizeString(value);
5843
6804
  return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
5844
6805
  }
6806
+ function requestAuthResult(input) {
6807
+ return {
6808
+ authorized: input.authorized,
6809
+ actor: input.actor ?? null,
6810
+ reason: input.reason,
6811
+ login: input.login ?? null,
6812
+ userId: input.userId ?? null,
6813
+ userNamespace: input.userNamespace ?? null,
6814
+ authStateFile: input.authStateFile ?? null
6815
+ };
6816
+ }
6817
+ function namespaceFromSessionIndex(entry) {
6818
+ const stateDir2 = dirname15(entry.authStateFile);
6819
+ return {
6820
+ key: entry.namespaceKey,
6821
+ userId: entry.userId,
6822
+ login: entry.login,
6823
+ root: entry.namespaceRoot,
6824
+ stateDir: stateDir2,
6825
+ authStateFile: entry.authStateFile,
6826
+ metadataFile: resolve20(stateDir2, "user-namespace.json"),
6827
+ checkoutBaseDir: entry.checkoutBaseDir,
6828
+ snapshotBaseDir: entry.snapshotBaseDir
6829
+ };
6830
+ }
5845
6831
  function authorizeRigHttpRequest(input) {
5846
6832
  if (input.legacyAuthorized) {
5847
- return { authorized: true, actor: "rig-local-server", reason: "server-token" };
6833
+ return requestAuthResult({ authorized: true, actor: "rig-local-server", reason: "server-token" });
5848
6834
  }
5849
6835
  const bearer = bearerTokenFromRequest(input.req);
5850
6836
  const store = createGitHubAuthStore(input.projectRoot);
5851
6837
  const storedToken = store.readToken();
5852
6838
  const session = bearer ? store.readApiSession(bearer) : null;
5853
6839
  if (session) {
5854
- return { authorized: true, actor: session.login ?? "github-operator", reason: "github-session" };
6840
+ return requestAuthResult({
6841
+ authorized: true,
6842
+ actor: session.login ?? "github-operator",
6843
+ reason: "github-session",
6844
+ login: session.login,
6845
+ userId: session.userId,
6846
+ authStateFile: store.stateFile
6847
+ });
5855
6848
  }
5856
6849
  if (bearer && storedToken && bearer === storedToken) {
5857
6850
  const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
5858
- return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
6851
+ return requestAuthResult({
6852
+ authorized: true,
6853
+ actor: status.login ?? "github-operator",
6854
+ reason: "github-token",
6855
+ login: status.login,
6856
+ userId: status.userId,
6857
+ authStateFile: store.stateFile
6858
+ });
6859
+ }
6860
+ const indexedSession = readGitHubApiSession({ projectRoot: input.projectRoot, token: bearer });
6861
+ if (indexedSession) {
6862
+ const userNamespace = namespaceFromSessionIndex(indexedSession);
6863
+ return requestAuthResult({
6864
+ authorized: true,
6865
+ actor: indexedSession.login ?? "github-operator",
6866
+ reason: "github-user-session",
6867
+ login: indexedSession.login,
6868
+ userId: indexedSession.userId,
6869
+ userNamespace,
6870
+ authStateFile: indexedSession.authStateFile
6871
+ });
5859
6872
  }
5860
6873
  if (isPublicRigAuthBootstrapRoute(input.pathname)) {
5861
- return { authorized: true, actor: null, reason: "public-bootstrap" };
6874
+ return requestAuthResult({ authorized: true, actor: null, reason: "public-bootstrap" });
5862
6875
  }
5863
6876
  if (!input.serverAuthToken && !storedToken) {
5864
6877
  if (isLoopbackRequest(input.req)) {
5865
- return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
6878
+ return requestAuthResult({ authorized: true, actor: null, reason: "loopback-dev-no-auth" });
5866
6879
  }
5867
- return { authorized: false, actor: null, reason: "auth-required" };
6880
+ return requestAuthResult({ authorized: false, actor: null, reason: "auth-required" });
5868
6881
  }
5869
- return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
6882
+ return requestAuthResult({ authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" });
5870
6883
  }
5871
6884
  async function fetchGitHubUserInfo(token) {
5872
6885
  const response = await fetch("https://api.github.com/user", {
@@ -5886,6 +6899,67 @@ async function fetchGitHubUserInfo(token) {
5886
6899
  scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
5887
6900
  };
5888
6901
  }
6902
+ function shouldWriteRootAuthCompat(projectRoot) {
6903
+ if (process.env.RIG_REMOTE_USER_NAMESPACE_ROOT?.trim())
6904
+ return false;
6905
+ const stateDir2 = normalizeString(process.env.RIG_STATE_DIR);
6906
+ if (!stateDir2)
6907
+ return true;
6908
+ return resolve20(stateDir2) === resolve20(projectRoot, ".rig", "state");
6909
+ }
6910
+ function requestScopedRegistryRoot(stateProjectRoot, requestAuth) {
6911
+ return requestAuth.userNamespace?.root ?? stateProjectRoot;
6912
+ }
6913
+ function requestScopedAuthStore(stateProjectRoot, requestAuth) {
6914
+ return requestAuth.authStateFile ? createGitHubAuthStoreFromStateFile(requestAuth.authStateFile) : createGitHubAuthStore(stateProjectRoot);
6915
+ }
6916
+ function userNamespaceResponse(namespace) {
6917
+ return namespace ? serializeRemoteUserNamespace(namespace) : undefined;
6918
+ }
6919
+ function resolveNamespacedBaseDir(input) {
6920
+ if (input.explicitBaseDir)
6921
+ return input.explicitBaseDir;
6922
+ const envBase = normalizeString(process.env[input.envName]);
6923
+ if (input.userNamespace) {
6924
+ return envBase ? resolve20(envBase, input.userNamespace.key) : input.userNamespace[input.legacySubdir === "remote-checkouts" ? "checkoutBaseDir" : "snapshotBaseDir"];
6925
+ }
6926
+ return envBase ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve20(normalizeString(process.env.RIG_STATE_DIR), input.legacySubdir) : resolve20(input.legacyProjectRoot, ".rig", input.legacySubdir));
6927
+ }
6928
+ function explicitCheckoutKey(body, checkoutInput, requestAuth) {
6929
+ return normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? (requestAuth.userNamespace ? undefined : "default");
6930
+ }
6931
+ function saveGitHubTokenForRemoteUser(input) {
6932
+ const namespace = resolveRemoteUserNamespace(input.projectRoot, { userId: input.user.userId, login: input.user.login });
6933
+ writeRemoteUserNamespaceMetadata(namespace);
6934
+ const store = createGitHubAuthStoreFromStateFile(namespace.authStateFile);
6935
+ store.saveToken({
6936
+ token: input.token,
6937
+ tokenSource: input.tokenSource,
6938
+ login: input.user.login,
6939
+ userId: input.user.userId,
6940
+ scopes: input.user.scopes,
6941
+ selectedRepo: input.selectedRepo
6942
+ });
6943
+ const apiSession = store.createApiSession();
6944
+ registerGitHubApiSession({ projectRoot: input.projectRoot, token: apiSession.token, namespace, selectedRepo: input.selectedRepo });
6945
+ const requestedRoot = normalizeString(input.requestedProjectRoot);
6946
+ if (requestedRoot && isAbsolute4(requestedRoot) && existsSync13(resolve20(requestedRoot))) {
6947
+ copyGitHubAuthStateToLocalProjectRoot(namespace.authStateFile, resolve20(requestedRoot));
6948
+ }
6949
+ if (shouldWriteRootAuthCompat(input.projectRoot)) {
6950
+ const rootStore = createGitHubAuthStore(input.projectRoot);
6951
+ rootStore.saveToken({
6952
+ token: input.token,
6953
+ tokenSource: input.tokenSource,
6954
+ login: input.user.login,
6955
+ userId: input.user.userId,
6956
+ scopes: input.user.scopes,
6957
+ selectedRepo: input.selectedRepo
6958
+ });
6959
+ rootStore.createApiSession();
6960
+ }
6961
+ return { store, namespace, apiSessionToken: apiSession.token };
6962
+ }
5889
6963
  async function postGitHubForm(endpoint, body) {
5890
6964
  const response = await fetch(endpoint, {
5891
6965
  method: "POST",
@@ -5903,11 +6977,11 @@ function resolveRequestedProjectRoot(currentRoot, rawRoot) {
5903
6977
  const requestedRoot = normalizeString(rawRoot);
5904
6978
  if (!requestedRoot)
5905
6979
  return currentRoot;
5906
- if (!isAbsolute3(requestedRoot)) {
6980
+ if (!isAbsolute4(requestedRoot)) {
5907
6981
  throw new Error("projectRoot must be an absolute path on the Rig server host");
5908
6982
  }
5909
- const normalizedRoot = resolve18(requestedRoot);
5910
- if (!existsSync11(normalizedRoot)) {
6983
+ const normalizedRoot = resolve20(requestedRoot);
6984
+ if (!existsSync13(normalizedRoot)) {
5911
6985
  throw new Error("projectRoot does not exist on the Rig server host");
5912
6986
  }
5913
6987
  return normalizedRoot;
@@ -6183,7 +7257,7 @@ function redactSecretFields(value) {
6183
7257
  return redacted;
6184
7258
  }
6185
7259
  function validateRemoteLease(deps, state, input) {
6186
- const run = readAuthorityRun6(state.projectRoot, input.runId);
7260
+ const run = readAuthorityRun7(state.projectRoot, input.runId);
6187
7261
  if (!run) {
6188
7262
  return { ok: false, response: deps.jsonResponse({ ok: false, error: "Remote run not found" }, 404) };
6189
7263
  }
@@ -6203,6 +7277,43 @@ function createRigServerFetch(state, deps) {
6203
7277
  return deps.withServerPathEnv(state.projectRoot, async () => {
6204
7278
  const browserOrigin = deps.resolveAllowedBrowserOrigin(req);
6205
7279
  const finalizeResponse = (response) => deps.withCorsHeaders(response, req, browserOrigin);
7280
+ const earlyUrl = new URL(req.url);
7281
+ const piEventsWsMatch = earlyUrl.pathname.match(/^\/api\/runs\/([^/]+)\/pi\/events$/);
7282
+ if (piEventsWsMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
7283
+ const queryToken = earlyUrl.searchParams.get("token");
7284
+ const authHeaders = new Headers(req.headers);
7285
+ if (queryToken && !authHeaders.has("authorization")) {
7286
+ authHeaders.set("authorization", `Bearer ${queryToken}`);
7287
+ }
7288
+ const legacyAuthorized = Boolean(state.authToken && queryToken === state.authToken);
7289
+ const requestAuth = authorizeRigHttpRequest({
7290
+ req: new Request(req.url, { method: req.method, headers: authHeaders }),
7291
+ pathname: earlyUrl.pathname,
7292
+ projectRoot: state.projectRoot,
7293
+ serverAuthToken: state.authToken,
7294
+ legacyAuthorized
7295
+ });
7296
+ if (!requestAuth.authorized) {
7297
+ return deps.jsonResponse({ ok: false, error: "Unauthorized WebSocket connection", reason: requestAuth.reason }, 401);
7298
+ }
7299
+ const runId = decodeURIComponent(piEventsWsMatch[1]);
7300
+ const resolved = resolveRunPiSessionProxy(state.projectRoot, runId);
7301
+ if (resolved === null)
7302
+ return deps.jsonResponse({ ok: false, error: "Run not found" }, 404);
7303
+ if ("pending" in resolved)
7304
+ return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500 }, 202);
7305
+ if (!server)
7306
+ return deps.jsonResponse({ ok: false, error: "WebSocket upgrade unavailable" }, 400);
7307
+ const upgraded = server.upgrade(req, {
7308
+ data: {
7309
+ kind: "pi-session-proxy",
7310
+ connectedAt: new Date().toISOString(),
7311
+ upstreamUrl: buildRunPiDaemonWebSocketUrl(resolved),
7312
+ runId
7313
+ }
7314
+ });
7315
+ return upgraded ? new Response(null) : deps.jsonResponse({ ok: false, error: "WebSocket upgrade failed" }, 400);
7316
+ }
6206
7317
  const upgradeResponse = handleWebSocketUpgrade({
6207
7318
  req,
6208
7319
  server,
@@ -6240,6 +7351,13 @@ function createRigServerFetch(state, deps) {
6240
7351
  notifications: state.targets.length
6241
7352
  });
6242
7353
  }
7354
+ if (url.pathname === "/install" && req.method === "GET") {
7355
+ return new Response(buildRigInstallScript(), {
7356
+ headers: {
7357
+ "Content-Type": "text/x-shellscript; charset=utf-8"
7358
+ }
7359
+ });
7360
+ }
6243
7361
  const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
6244
7362
  const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
6245
7363
  const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
@@ -6452,16 +7570,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6452
7570
  if (!source) {
6453
7571
  return deps.badRequest("No task source is configured");
6454
7572
  }
7573
+ if (!source.updateTask && !(update.status && source.updateStatus)) {
7574
+ return deps.badRequest("Configured task source does not support updates");
7575
+ }
6455
7576
  const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
6456
7577
  return;
6457
7578
  }) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
6458
- if (source.updateTask) {
6459
- await source.updateTask(id, update);
6460
- } else if (update.status && source.updateStatus) {
6461
- await source.updateStatus(id, update.status);
6462
- } else {
6463
- return deps.badRequest("Configured task source does not support updates");
6464
- }
6465
7579
  const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
6466
7580
  const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
6467
7581
  taskId: id,
@@ -6470,6 +7584,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6470
7584
  token: createGitHubAuthStore(state.projectRoot).readToken(),
6471
7585
  config: ctx?.config
6472
7586
  }).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
7587
+ if (update.status && githubProjectsEnabled2(ctx?.config) && projectSync.synced === false) {
7588
+ return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
7589
+ }
7590
+ try {
7591
+ if (source.updateTask) {
7592
+ await source.updateTask(id, update);
7593
+ } else if (update.status && source.updateStatus) {
7594
+ await source.updateStatus(id, update.status);
7595
+ }
7596
+ } catch (error) {
7597
+ let rollback = null;
7598
+ const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
7599
+ if (update.status && previousStatus && githubProjectsEnabled2(ctx?.config) && projectSync.synced !== false) {
7600
+ rollback = await syncGitHubProjectStatusForTaskUpdate({
7601
+ taskId: id,
7602
+ status: previousStatus,
7603
+ issueNodeId,
7604
+ token: createGitHubAuthStore(state.projectRoot).readToken(),
7605
+ config: ctx?.config
7606
+ }).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
7607
+ }
7608
+ return deps.jsonResponse({
7609
+ ok: false,
7610
+ id,
7611
+ projectSync,
7612
+ rollback,
7613
+ error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
7614
+ }, 502);
7615
+ }
6473
7616
  deps.snapshotService.invalidate("github-issue-updated");
6474
7617
  await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
6475
7618
  return;
@@ -6478,25 +7621,40 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6478
7621
  return deps.jsonResponse({ ok: true, id, projectSync });
6479
7622
  }
6480
7623
  if (url.pathname === "/api/workspace/task-labels") {
7624
+ const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
7625
+ if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
7626
+ return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
7627
+ }
6481
7628
  return deps.jsonResponse({
6482
7629
  ok: true,
6483
7630
  ready: true,
6484
7631
  labelsReady: true,
6485
- labels: [
6486
- "ready",
6487
- "blocked",
6488
- "in-progress",
6489
- "under-review",
6490
- "failed",
6491
- "cancelled",
6492
- "rig:running",
6493
- "rig:pr-open",
6494
- "rig:ci-fixing",
6495
- "rig:done",
6496
- "rig:needs-attention"
6497
- ],
6498
- note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
7632
+ labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
7633
+ note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
7634
+ });
7635
+ }
7636
+ if (url.pathname === "/api/github/projects" && req.method === "GET") {
7637
+ const owner = normalizeString(url.searchParams.get("owner"));
7638
+ if (!owner)
7639
+ return deps.badRequest("owner is required");
7640
+ const token = createGitHubAuthStore(state.projectRoot).readToken();
7641
+ if (!token)
7642
+ return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
7643
+ const projects = await listGitHubProjects({ owner, token }).catch((error) => {
7644
+ throw new Error(error instanceof Error ? error.message : String(error));
7645
+ });
7646
+ return deps.jsonResponse({ ok: true, projects });
7647
+ }
7648
+ const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
7649
+ if (projectStatusMatch && req.method === "GET") {
7650
+ const projectId = decodeURIComponent(projectStatusMatch[1]);
7651
+ const token = createGitHubAuthStore(state.projectRoot).readToken();
7652
+ if (!token)
7653
+ return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
7654
+ const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
7655
+ throw new Error(error instanceof Error ? error.message : String(error));
6499
7656
  });
7657
+ return deps.jsonResponse({ ok: true, field });
6500
7658
  }
6501
7659
  if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
6502
7660
  const body = await deps.readJsonBody(req);
@@ -6561,7 +7719,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6561
7719
  }
6562
7720
  if (url.pathname === "/api/server/status") {
6563
7721
  const config = buildProjectConfigStatus(state.projectRoot);
6564
- const taskSource = await buildTaskSourceStatus(state, config);
7722
+ const taskSource = await buildTaskSourceStatus(state, config, requestAuth);
6565
7723
  return deps.jsonResponse({
6566
7724
  ok: true,
6567
7725
  projectRoot: state.projectRoot,
@@ -6585,8 +7743,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6585
7743
  path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
6586
7744
  ref: normalizeString(rawCheckout?.ref) ?? undefined
6587
7745
  } : undefined;
6588
- const record = upsertProjectRecord(state.projectRoot, { repoSlug, checkout });
6589
- return deps.jsonResponse({ ok: true, project: record });
7746
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7747
+ const record = upsertProjectRecord(registryRoot, { repoSlug, checkout });
7748
+ return deps.jsonResponse({ ok: true, project: record, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6590
7749
  }
6591
7750
  const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
6592
7751
  if (snapshotUploadMatch && req.method === "POST") {
@@ -6599,8 +7758,15 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6599
7758
  if (!archiveContentBase64) {
6600
7759
  return deps.badRequest("archiveContentBase64 is required");
6601
7760
  }
6602
- const baseDir = normalizeString(body.baseDir) ?? normalizeString(process.env.RIG_REMOTE_SNAPSHOT_BASE_DIR) ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve18(normalizeString(process.env.RIG_STATE_DIR), "remote-snapshots") : resolve18(state.projectRoot, ".rig", "remote-snapshots"));
6603
- const checkoutKey = normalizeString(body.checkoutKey) ?? "default";
7761
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7762
+ const baseDir = resolveNamespacedBaseDir({
7763
+ explicitBaseDir: normalizeString(body.baseDir),
7764
+ envName: "RIG_REMOTE_SNAPSHOT_BASE_DIR",
7765
+ userNamespace: requestAuth.userNamespace,
7766
+ legacyProjectRoot: state.projectRoot,
7767
+ legacySubdir: "remote-snapshots"
7768
+ });
7769
+ const checkoutKey = explicitCheckoutKey(body, body, requestAuth);
6604
7770
  try {
6605
7771
  const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
6606
7772
  const checkout = extractUploadedSnapshotArchive({
@@ -6613,14 +7779,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6613
7779
  const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
6614
7780
  const packageInstall = installRemoteCheckoutPackages(checkout.path);
6615
7781
  const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
6616
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
7782
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
6617
7783
  kind: "uploaded-snapshot",
6618
7784
  path: checkout.path,
6619
7785
  ref: checkout.snapshotId
6620
7786
  });
6621
7787
  deps.snapshotService.invalidate("uploaded-snapshot-checkout");
6622
7788
  deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
6623
- return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
7789
+ return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6624
7790
  } catch (error) {
6625
7791
  return deps.jsonResponse({
6626
7792
  ok: false,
@@ -6640,10 +7806,17 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6640
7806
  if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
6641
7807
  return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
6642
7808
  }
6643
- const baseDir = normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir) ?? normalizeString(process.env.RIG_REMOTE_CHECKOUT_BASE_DIR) ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve18(normalizeString(process.env.RIG_STATE_DIR), "remote-checkouts") : resolve18(state.projectRoot, ".rig", "remote-checkouts"));
6644
- const checkoutKey = normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? "default";
7809
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7810
+ const baseDir = resolveNamespacedBaseDir({
7811
+ explicitBaseDir: normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir),
7812
+ envName: "RIG_REMOTE_CHECKOUT_BASE_DIR",
7813
+ userNamespace: requestAuth.userNamespace,
7814
+ legacyProjectRoot: state.projectRoot,
7815
+ legacySubdir: "remote-checkouts"
7816
+ });
7817
+ const checkoutKey = explicitCheckoutKey(body, checkoutInput, requestAuth);
6645
7818
  const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
6646
- const credentialToken = createGitHubAuthStore(state.projectRoot).readToken();
7819
+ const credentialToken = requestScopedAuthStore(state.projectRoot, requestAuth).readToken();
6647
7820
  try {
6648
7821
  const checkout = await prepareRemoteCheckout({
6649
7822
  command: runRemoteCheckoutCommand,
@@ -6652,14 +7825,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6652
7825
  const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
6653
7826
  const packageInstall = installRemoteCheckoutPackages(checkout.path);
6654
7827
  const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
6655
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
7828
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
6656
7829
  kind: checkout.kind,
6657
7830
  path: checkout.path,
6658
7831
  ref: checkout.ref ?? checkout.snapshotId ?? undefined
6659
7832
  });
6660
7833
  deps.snapshotService.invalidate("remote-checkout-prepared");
6661
7834
  deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
6662
- return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
7835
+ return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6663
7836
  } catch (error) {
6664
7837
  return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
6665
7838
  }
@@ -6676,16 +7849,18 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6676
7849
  if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
6677
7850
  return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
6678
7851
  }
6679
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
7852
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7853
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
6680
7854
  kind,
6681
7855
  path: normalizeString(body.path) ?? state.projectRoot,
6682
7856
  ref: normalizeString(body.ref) ?? undefined
6683
7857
  });
6684
- return deps.jsonResponse({ ok: true, project });
7858
+ return deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6685
7859
  }
6686
7860
  if (req.method === "GET") {
6687
- const project = getProjectRecord(state.projectRoot, repoSlug);
6688
- return project ? deps.jsonResponse({ ok: true, project }) : deps.notFound();
7861
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7862
+ const project = getProjectRecord(registryRoot, repoSlug);
7863
+ return project ? deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) }) : deps.notFound();
6689
7864
  }
6690
7865
  }
6691
7866
  if (url.pathname === "/api/server/project-root" && req.method === "POST") {
@@ -6694,13 +7869,26 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6694
7869
  if (!requestedRoot) {
6695
7870
  return deps.badRequest("projectRoot is required");
6696
7871
  }
6697
- if (!isAbsolute3(requestedRoot)) {
7872
+ if (!isAbsolute4(requestedRoot)) {
6698
7873
  return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
6699
7874
  }
6700
- const normalizedRoot = resolve18(requestedRoot);
6701
- const exists = existsSync11(normalizedRoot);
6702
- if (exists) {
6703
- createGitHubAuthStore(state.projectRoot).copyToProjectRoot(normalizedRoot);
7875
+ const normalizedRoot = resolve20(requestedRoot);
7876
+ const exists = existsSync13(normalizedRoot);
7877
+ if (exists && requestAuth.userNamespace) {
7878
+ const allowedByNamespace = isPathInsideNamespace(requestAuth.userNamespace.root, normalizedRoot);
7879
+ const allowedByRegistry = projectRegistryContainsCheckout(requestAuth.userNamespace.root, normalizedRoot);
7880
+ if (!allowedByNamespace && !allowedByRegistry) {
7881
+ return deps.jsonResponse({
7882
+ ok: false,
7883
+ error: "Requested project root is outside the authenticated GitHub user namespace.",
7884
+ projectRoot: state.projectRoot,
7885
+ requestedProjectRoot: normalizedRoot,
7886
+ userNamespace: userNamespaceResponse(requestAuth.userNamespace)
7887
+ }, 403);
7888
+ }
7889
+ copyGitHubAuthStateToLocalProjectRoot(requestAuth.userNamespace.authStateFile, normalizedRoot);
7890
+ } else if (exists) {
7891
+ createGitHubAuthStore(state.projectRoot).copyToLocalProjectRoot(normalizedRoot);
6704
7892
  }
6705
7893
  const control = buildServerControlStatus();
6706
7894
  const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
@@ -6715,7 +7903,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6715
7903
  message: "Requested project root does not exist on the Rig server host."
6716
7904
  }, 404);
6717
7905
  }
6718
- if (!existsSync11(resolve18(normalizedRoot, "rig.config.ts")) && !existsSync11(resolve18(normalizedRoot, "rig.config.json"))) {
7906
+ if (!existsSync13(resolve20(normalizedRoot, "rig.config.ts")) && !existsSync13(resolve20(normalizedRoot, "rig.config.json"))) {
6719
7907
  return deps.jsonResponse({
6720
7908
  ok: false,
6721
7909
  projectRoot: state.projectRoot,
@@ -6750,6 +7938,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6750
7938
  exists,
6751
7939
  control,
6752
7940
  requiresRestart: false,
7941
+ userNamespace: userNamespaceResponse(requestAuth.userNamespace),
6753
7942
  message: "Project-root switch accepted. Rig server restart has been scheduled."
6754
7943
  }, 202);
6755
7944
  }
@@ -6764,11 +7953,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6764
7953
  }, 409);
6765
7954
  }
6766
7955
  if (url.pathname === "/api/github/auth/status") {
6767
- const store = createGitHubAuthStore(state.projectRoot);
6768
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
7956
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
7957
+ return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6769
7958
  }
6770
7959
  if (url.pathname === "/api/github/repo/permissions") {
6771
- const store = createGitHubAuthStore(state.projectRoot);
7960
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
6772
7961
  const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
6773
7962
  if (!auth.signedIn) {
6774
7963
  return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
@@ -6796,24 +7985,20 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6796
7985
  }
6797
7986
  try {
6798
7987
  const user = await fetchGitHubUserInfo(token);
6799
- const storeRoots = [
6800
- state.projectRoot,
6801
- ...requestedProjectRoot && isAbsolute3(requestedProjectRoot) && existsSync11(resolve18(requestedProjectRoot)) ? [resolve18(requestedProjectRoot)] : []
6802
- ].filter((root, index, roots) => roots.indexOf(root) === index);
6803
- const stores = storeRoots.map((root) => createGitHubAuthStore(root));
6804
- for (const store2 of stores) {
6805
- store2.saveToken({
6806
- token,
6807
- tokenSource: "manual-token",
6808
- login: user.login,
6809
- userId: user.userId,
6810
- scopes: user.scopes,
6811
- selectedRepo
6812
- });
6813
- }
6814
- const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
6815
- const apiSession = store.createApiSession();
6816
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
7988
+ const saved = saveGitHubTokenForRemoteUser({
7989
+ projectRoot: state.projectRoot,
7990
+ token,
7991
+ tokenSource: "manual-token",
7992
+ user,
7993
+ selectedRepo,
7994
+ requestedProjectRoot
7995
+ });
7996
+ return deps.jsonResponse({
7997
+ ok: true,
7998
+ ...saved.store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }),
7999
+ apiSessionToken: saved.apiSessionToken,
8000
+ userNamespace: userNamespaceResponse(saved.namespace)
8001
+ });
6817
8002
  } catch (error) {
6818
8003
  const message = error instanceof Error ? error.message : String(error);
6819
8004
  return deps.jsonResponse({ ok: false, error: message }, 400);
@@ -6880,9 +8065,21 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6880
8065
  }
6881
8066
  const token = result.payload.access_token;
6882
8067
  const user = await fetchGitHubUserInfo(token);
6883
- store.saveToken({ token, tokenSource: "oauth-device", login: user.login, userId: user.userId, scopes: user.scopes });
6884
- const apiSession = store.createApiSession();
6885
- return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }), apiSessionToken: apiSession.token });
8068
+ const saved = saveGitHubTokenForRemoteUser({
8069
+ projectRoot: state.projectRoot,
8070
+ token,
8071
+ tokenSource: "oauth-device",
8072
+ user,
8073
+ selectedRepo: null
8074
+ });
8075
+ store.clearPendingDevice(pollId);
8076
+ return deps.jsonResponse({
8077
+ ok: true,
8078
+ status: "signed-in",
8079
+ ...saved.store.status({ oauthConfigured: true }),
8080
+ apiSessionToken: saved.apiSessionToken,
8081
+ userNamespace: userNamespaceResponse(saved.namespace)
8082
+ });
6886
8083
  }
6887
8084
  if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
6888
8085
  const body = await deps.readJsonBody(req);
@@ -6891,7 +8088,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6891
8088
  if (!owner || !repo) {
6892
8089
  return deps.badRequest("owner and repo are required");
6893
8090
  }
6894
- const store = createGitHubAuthStore(state.projectRoot);
8091
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
6895
8092
  const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
6896
8093
  const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
6897
8094
  return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
@@ -6912,7 +8109,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6912
8109
  return deps.badRequest(error instanceof Error ? error.message : String(error));
6913
8110
  }
6914
8111
  const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
6915
- const configPath = resolve18(targetRoot, "rig.config.ts");
8112
+ const configPath = resolve20(targetRoot, "rig.config.ts");
6916
8113
  const source = buildGitHubProjectConfigSource({
6917
8114
  projectName: rawProjectName,
6918
8115
  owner,
@@ -6924,8 +8121,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6924
8121
  ok: true,
6925
8122
  projectRoot: targetRoot,
6926
8123
  configPath,
6927
- exists: existsSync11(configPath),
6928
- requiresOverwrite: existsSync11(configPath),
8124
+ exists: existsSync13(configPath),
8125
+ requiresOverwrite: existsSync13(configPath),
6929
8126
  source,
6930
8127
  owner,
6931
8128
  repo,
@@ -6961,8 +8158,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6961
8158
  assignee,
6962
8159
  githubUserId: authStatus.userId ?? authStatus.login
6963
8160
  });
6964
- const configPath = resolve18(targetRoot, "rig.config.ts");
6965
- if (existsSync11(configPath) && !overwrite) {
8161
+ const configPath = resolve20(targetRoot, "rig.config.ts");
8162
+ if (existsSync13(configPath) && !overwrite) {
6966
8163
  return deps.jsonResponse({
6967
8164
  ok: false,
6968
8165
  error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
@@ -6978,11 +8175,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6978
8175
  return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
6979
8176
  }
6980
8177
  let backupPath = null;
6981
- if (existsSync11(configPath)) {
8178
+ if (existsSync13(configPath)) {
6982
8179
  backupPath = backupConfigPath(configPath);
6983
- copyFileSync(configPath, backupPath);
8180
+ copyFileSync2(configPath, backupPath);
6984
8181
  }
6985
- writeFileSync10(configPath, source, "utf8");
8182
+ writeFileSync12(configPath, source, "utf8");
6986
8183
  const selectedRepo = `${owner}/${repo}`;
6987
8184
  store.saveSelectedRepo(selectedRepo);
6988
8185
  const targetStore = createGitHubAuthStore(targetRoot);
@@ -7254,11 +8451,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7254
8451
  const runId = normalizeString(body.runId);
7255
8452
  const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
7256
8453
  const promptOverride = normalizeString(body.promptOverride);
8454
+ const restart = body.restart === true;
7257
8455
  if (!runId) {
7258
8456
  return deps.badRequest("runId is required");
7259
8457
  }
7260
8458
  try {
7261
- await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
8459
+ await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
7262
8460
  deps.broadcastSnapshotInvalidation(state);
7263
8461
  return deps.jsonResponse({ ok: true, runId, createdAt });
7264
8462
  } catch (error) {
@@ -7267,7 +8465,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7267
8465
  }
7268
8466
  if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
7269
8467
  const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
7270
- const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) => resolve18(root, "packages", "pi-rig")).find((candidate) => existsSync11(resolve18(candidate, "package.json"))) ?? "npm:@rig/pi-rig";
8468
+ const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) => resolve20(root, "packages", "pi-rig")).find((candidate) => existsSync13(resolve20(candidate, "package.json"))) ?? "npm:@h-rig/pi-rig";
7271
8469
  if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
7272
8470
  return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
7273
8471
  }
@@ -7583,9 +8781,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7583
8781
  } catch {
7584
8782
  return deps.badRequest("Invalid artifact path");
7585
8783
  }
7586
- mkdirSync11(dirname12(artifactPath), { recursive: true });
8784
+ mkdirSync13(dirname15(artifactPath), { recursive: true });
7587
8785
  const bytes = Buffer.from(contentBase64, "base64");
7588
- writeFileSync10(artifactPath, bytes);
8786
+ writeFileSync12(artifactPath, bytes);
7589
8787
  writeJsonFile4(`${artifactPath}.json`, {
7590
8788
  workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
7591
8789
  runId,
@@ -7622,13 +8820,75 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7622
8820
  }
7623
8821
  const run = leaseValidation.run;
7624
8822
  const completedAt = new Date().toISOString();
8823
+ const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
8824
+ if (run.taskId && workspaceDir) {
8825
+ patchRunRecord(state.projectRoot, runId, {
8826
+ status: "reviewing",
8827
+ completedAt: null,
8828
+ hostId,
8829
+ endpointId: leaseId,
8830
+ worktreePath: workspaceDir,
8831
+ serverCloseout: {
8832
+ status: "pending",
8833
+ phase: "queued",
8834
+ requestedAt: completedAt,
8835
+ updatedAt: completedAt,
8836
+ runtimeWorkspace: workspaceDir,
8837
+ branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
8838
+ taskId: run.taskId,
8839
+ source: "remote-complete"
8840
+ }
8841
+ });
8842
+ deps.appendRunLogEntryAndBroadcast(state, runId, {
8843
+ id: `log:${runId}:remote-server-closeout-requested`,
8844
+ title: "Server-owned closeout requested",
8845
+ detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
8846
+ tone: "info",
8847
+ status: "reviewing",
8848
+ createdAt: completedAt,
8849
+ payload: { workspaceDir, hostId, leaseId }
8850
+ }, "remote-server-closeout-requested");
8851
+ deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
8852
+ const detail = error instanceof Error ? error.message : String(error);
8853
+ patchRunRecord(state.projectRoot, runId, {
8854
+ status: "failed",
8855
+ completedAt: new Date().toISOString(),
8856
+ errorText: detail,
8857
+ serverCloseout: {
8858
+ status: "failed",
8859
+ phase: "failed",
8860
+ updatedAt: new Date().toISOString(),
8861
+ error: detail
8862
+ }
8863
+ });
8864
+ deps.appendRunLogEntryAndBroadcast(state, runId, {
8865
+ id: `log:${runId}:remote-server-closeout-failed`,
8866
+ title: "Server-owned closeout failed",
8867
+ detail,
8868
+ tone: "error",
8869
+ status: "failed",
8870
+ createdAt: new Date().toISOString()
8871
+ }, "remote-server-closeout-failed");
8872
+ }).finally(() => {
8873
+ deps.reconcileScheduler(state, "remote-server-closeout-terminal");
8874
+ });
8875
+ deps.broadcastSnapshotInvalidation(state);
8876
+ return deps.jsonResponse({
8877
+ ok: true,
8878
+ workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
8879
+ hostId,
8880
+ runId,
8881
+ leaseId,
8882
+ closeout: "server-owned",
8883
+ acceptedAt: new Date().toISOString()
8884
+ });
8885
+ }
7625
8886
  patchRunRecord(state.projectRoot, runId, {
7626
8887
  status: "completed",
7627
8888
  completedAt,
7628
8889
  hostId,
7629
8890
  endpointId: leaseId
7630
8891
  });
7631
- await updateRemoteRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "completed", completedAt, hostId, endpointId: leaseId }, "closed", "Remote Rig task run completed and closed this task.");
7632
8892
  await deps.enqueueRunLinearEvent(state.projectRoot, {
7633
8893
  type: "run.completed",
7634
8894
  runId,
@@ -7747,12 +9007,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7747
9007
  try {
7748
9008
  const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
7749
9009
  const runRoot = deps.normalizeRelativePath(runsRoot, runId);
7750
- const artifactsRoot = resolve18(runRoot, "remote-artifacts");
9010
+ const artifactsRoot = resolve20(runRoot, "remote-artifacts");
7751
9011
  artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
7752
9012
  } catch {
7753
9013
  return deps.badRequest("Invalid artifact path");
7754
9014
  }
7755
- if (!existsSync11(artifactPath)) {
9015
+ if (!existsSync13(artifactPath)) {
7756
9016
  return deps.notFound();
7757
9017
  }
7758
9018
  return new Response(Bun.file(artifactPath));
@@ -7765,6 +9025,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7765
9025
  const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
7766
9026
  return deps.jsonResponse(page);
7767
9027
  }
9028
+ const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
9029
+ if (runTimelineMatch) {
9030
+ const runId = decodeURIComponent(runTimelineMatch[1]);
9031
+ const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
9032
+ const cursor = normalizeString(url.searchParams.get("cursor"));
9033
+ const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
9034
+ return deps.jsonResponse(page);
9035
+ }
7768
9036
  const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
7769
9037
  if (runSteerMatch && req.method === "POST") {
7770
9038
  const runId = decodeURIComponent(runSteerMatch[1]);
@@ -7784,6 +9052,49 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7784
9052
  return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 404);
7785
9053
  }
7786
9054
  }
9055
+ const runPiMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/pi(?:\/(.*))?$/);
9056
+ if (runPiMatch) {
9057
+ const runId = decodeURIComponent(runPiMatch[1]);
9058
+ const action = runPiMatch[2] || "";
9059
+ const resolved = resolveRunPiSessionProxy(state.projectRoot, runId);
9060
+ if (resolved === null)
9061
+ return deps.notFound();
9062
+ if ("pending" in resolved) {
9063
+ if (action === "" && req.method === "GET") {
9064
+ return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500 }, 202);
9065
+ }
9066
+ return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500, error: "Pi session is not ready" }, 409);
9067
+ }
9068
+ if (action === "" && req.method === "GET")
9069
+ return deps.jsonResponse({ ready: true, metadata: resolved.metadata });
9070
+ const body = req.method === "GET" ? undefined : await deps.readJsonBody(req);
9071
+ const sessionPath = `/sessions/${encodeURIComponent(resolved.sessionId)}`;
9072
+ const daemonPath = (() => {
9073
+ if (action === "messages" && req.method === "GET")
9074
+ return `${sessionPath}/messages`;
9075
+ if (action === "status" && req.method === "GET")
9076
+ return `${sessionPath}/status`;
9077
+ if (action === "commands" && req.method === "GET")
9078
+ return `${sessionPath}/commands`;
9079
+ if (action === "prompt" && req.method === "POST")
9080
+ return `${sessionPath}/prompt`;
9081
+ if (action === "shell" && req.method === "POST")
9082
+ return `${sessionPath}/shell`;
9083
+ if (action === "commands/run" && req.method === "POST")
9084
+ return `${sessionPath}/commands/run`;
9085
+ if (action === "commands/respond" && req.method === "POST")
9086
+ return `${sessionPath}/commands/respond`;
9087
+ if (action === "extension-ui/respond" && req.method === "POST")
9088
+ return `${sessionPath}/extension-ui/respond`;
9089
+ if (action === "abort" && req.method === "POST")
9090
+ return `${sessionPath}/abort`;
9091
+ return null;
9092
+ })();
9093
+ if (!daemonPath)
9094
+ return deps.notFound();
9095
+ const proxied = await proxyRunPiHttp(state.projectRoot, runId, { method: req.method, daemonPath, body });
9096
+ return deps.jsonResponse(proxied.payload, proxied.status);
9097
+ }
7787
9098
  const runSteeringAckMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steering\/ack$/);
7788
9099
  if (runSteeringAckMatch && req.method === "POST") {
7789
9100
  const runId = decodeURIComponent(runSteeringAckMatch[1]);
@@ -7906,7 +9217,7 @@ import {
7906
9217
  RemoteWsClient as RemoteWsClient2
7907
9218
  } from "@rig/runtime/control-plane/remote";
7908
9219
  import { deleteRunState } from "@rig/runtime/control-plane/native/run-ops";
7909
- import { readAuthorityRun as readAuthorityRun7 } from "@rig/runtime/control-plane/authority-files";
9220
+ import { readAuthorityRun as readAuthorityRun8 } from "@rig/runtime/control-plane/authority-files";
7910
9221
  function redactRemoteEndpoint2(endpoint) {
7911
9222
  const { token, ...rest } = endpoint;
7912
9223
  return {
@@ -8120,7 +9431,7 @@ async function routeWebSocketRequest(state, deps, request) {
8120
9431
  if (!runId || !messageId || text === null) {
8121
9432
  throw new Error("runId, messageId, and text are required");
8122
9433
  }
8123
- const run = readAuthorityRun7(state.projectRoot, runId);
9434
+ const run = readAuthorityRun8(state.projectRoot, runId);
8124
9435
  if (!run) {
8125
9436
  throw new Error(`Run not found: ${runId}`);
8126
9437
  }
@@ -8645,8 +9956,8 @@ async function routeWebSocketRequest(state, deps, request) {
8645
9956
  }
8646
9957
 
8647
9958
  // packages/server/src/server-helpers/inspector-jobs.ts
8648
- import { existsSync as existsSync15, mkdirSync as mkdirSync14, readFileSync as readFileSync10, writeFileSync as writeFileSync13 } from "fs";
8649
- import { dirname as dirname15, resolve as resolve21 } from "path";
9959
+ import { existsSync as existsSync17, mkdirSync as mkdirSync16, readFileSync as readFileSync12, writeFileSync as writeFileSync15 } from "fs";
9960
+ import { dirname as dirname18, resolve as resolve23 } from "path";
8650
9961
  import { readJsonFile as readJsonFile3 } from "@rig/runtime/control-plane/authority-files";
8651
9962
  import { resolveMonorepoRoot as resolveMonorepoRoot5 } from "@rig/runtime/control-plane/native/utils";
8652
9963
  import { normalizeTaskLifecycleStatus as normalizeTaskLifecycleStatus2 } from "@rig/runtime/control-plane/state-sync/types";
@@ -8754,8 +10065,8 @@ import { randomUUID as randomUUID3 } from "crypto";
8754
10065
 
8755
10066
  // packages/server/src/inspector/mission.ts
8756
10067
  import { randomUUID as randomUUID2 } from "crypto";
8757
- import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync8, readdirSync as readdirSync4, renameSync, writeFileSync as writeFileSync11 } from "fs";
8758
- import { dirname as dirname13, join, resolve as resolve19 } from "path";
10068
+ import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync14, readFileSync as readFileSync10, readdirSync as readdirSync4, renameSync, writeFileSync as writeFileSync13 } from "fs";
10069
+ import { dirname as dirname16, join, resolve as resolve21 } from "path";
8759
10070
  function isJsonValue(value) {
8760
10071
  if (value === null)
8761
10072
  return true;
@@ -8795,7 +10106,7 @@ function isRecord2(value) {
8795
10106
  }
8796
10107
  function readJsonRecord(path) {
8797
10108
  try {
8798
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
10109
+ const parsed = JSON.parse(readFileSync10(path, "utf8"));
8799
10110
  if (!isRecord2(parsed)) {
8800
10111
  return { ok: false, error: `Mission file ${path} does not contain an object` };
8801
10112
  }
@@ -8875,14 +10186,14 @@ function missionActionDetails(mission) {
8875
10186
  };
8876
10187
  }
8877
10188
  function writeJsonFile5(path, value) {
8878
- mkdirSync12(dirname13(path), { recursive: true });
10189
+ mkdirSync14(dirname16(path), { recursive: true });
8879
10190
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
8880
- writeFileSync11(tempPath, `${JSON.stringify(value, null, 2)}
10191
+ writeFileSync13(tempPath, `${JSON.stringify(value, null, 2)}
8881
10192
  `, "utf8");
8882
10193
  renameSync(tempPath, path);
8883
10194
  }
8884
10195
  function resolveInspectorMissionPaths(projectRoot) {
8885
- const inspectorDir = resolve19(resolveRigServerPaths(projectRoot).stateDir, "inspector");
10196
+ const inspectorDir = resolve21(resolveRigServerPaths(projectRoot).stateDir, "inspector");
8886
10197
  return {
8887
10198
  inspectorDir,
8888
10199
  missionsDir: join(inspectorDir, "missions"),
@@ -8891,8 +10202,8 @@ function resolveInspectorMissionPaths(projectRoot) {
8891
10202
  }
8892
10203
  function createInspectorMissionController(options) {
8893
10204
  const paths = resolveInspectorMissionPaths(options.projectRoot);
8894
- mkdirSync12(paths.missionsDir, { recursive: true });
8895
- mkdirSync12(paths.journalsDir, { recursive: true });
10205
+ mkdirSync14(paths.missionsDir, { recursive: true });
10206
+ mkdirSync14(paths.journalsDir, { recursive: true });
8896
10207
  const now = options.now ?? (() => new Date().toISOString());
8897
10208
  const nextId = options.idGenerator ?? (() => `mission:${randomUUID2()}`);
8898
10209
  function missionPath(missionId) {
@@ -8902,15 +10213,15 @@ function createInspectorMissionController(options) {
8902
10213
  return join(paths.journalsDir, `${missionId}.jsonl`);
8903
10214
  }
8904
10215
  function appendMissionJournal(entry) {
8905
- mkdirSync12(paths.journalsDir, { recursive: true });
10216
+ mkdirSync14(paths.journalsDir, { recursive: true });
8906
10217
  appendFileSync(journalPath(entry.missionId), `${JSON.stringify(entry)}
8907
10218
  `, "utf8");
8908
10219
  }
8909
10220
  function listMissionJournal(missionId) {
8910
10221
  const path = journalPath(missionId);
8911
- if (!existsSync12(path))
10222
+ if (!existsSync14(path))
8912
10223
  return [];
8913
- return readFileSync8(path, "utf8").split(`
10224
+ return readFileSync10(path, "utf8").split(`
8914
10225
  `).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line)).filter(isRecord2).map((entry) => ({
8915
10226
  id: typeof entry.id === "string" ? entry.id : `journal:${randomUUID2()}`,
8916
10227
  missionId,
@@ -8926,7 +10237,7 @@ function createInspectorMissionController(options) {
8926
10237
  }
8927
10238
  function readMissionOnly(missionId) {
8928
10239
  const path = missionPath(missionId);
8929
- if (!existsSync12(path)) {
10240
+ if (!existsSync14(path)) {
8930
10241
  return { ok: false, error: `Mission ${missionId} was not found` };
8931
10242
  }
8932
10243
  const read = readJsonRecord(path);
@@ -8977,7 +10288,7 @@ function createInspectorMissionController(options) {
8977
10288
  const source = cloneJsonRecord(input.sourceTask);
8978
10289
  const missionId = nextId();
8979
10290
  const path = missionPath(missionId);
8980
- if (existsSync12(path)) {
10291
+ if (existsSync14(path)) {
8981
10292
  const existing = readMissionOnly(missionId);
8982
10293
  if (!existing.ok)
8983
10294
  return existing;
@@ -10577,8 +11888,8 @@ function createCodexInspectorTransport(options) {
10577
11888
  const sendRequest = async (method, params) => {
10578
11889
  const id = nextRequestId;
10579
11890
  nextRequestId += 1;
10580
- const response = new Promise((resolve20, reject) => {
10581
- pendingResponses.set(id, { resolve: resolve20, reject });
11891
+ const response = new Promise((resolve22, reject) => {
11892
+ pendingResponses.set(id, { resolve: resolve22, reject });
10582
11893
  });
10583
11894
  response.catch(() => {});
10584
11895
  try {
@@ -10888,9 +12199,9 @@ function createCodexInspectorTransport(options) {
10888
12199
  }
10889
12200
  lastAssistantMessage = null;
10890
12201
  lastError = null;
10891
- const turnResult = new Promise((resolve20, reject) => {
12202
+ const turnResult = new Promise((resolve22, reject) => {
10892
12203
  currentTurn = {
10893
- resolve: resolve20,
12204
+ resolve: resolve22,
10894
12205
  reject,
10895
12206
  events: []
10896
12207
  };
@@ -10950,13 +12261,13 @@ function createCodexInspectorTransport(options) {
10950
12261
  };
10951
12262
  }
10952
12263
  function writeChildLine(child, line) {
10953
- return new Promise((resolve20, reject) => {
12264
+ return new Promise((resolve22, reject) => {
10954
12265
  child.stdin.write(line, (error) => {
10955
12266
  if (error) {
10956
12267
  reject(error);
10957
12268
  return;
10958
12269
  }
10959
- resolve20();
12270
+ resolve22();
10960
12271
  });
10961
12272
  });
10962
12273
  }
@@ -10969,10 +12280,10 @@ function terminateChild(child) {
10969
12280
  } catch {}
10970
12281
  }
10971
12282
  async function waitForChildSpawn(child) {
10972
- await new Promise((resolve20, reject) => {
12283
+ await new Promise((resolve22, reject) => {
10973
12284
  const onSpawn = () => {
10974
12285
  cleanup();
10975
- resolve20();
12286
+ resolve22();
10976
12287
  };
10977
12288
  const onError = (error) => {
10978
12289
  cleanup();
@@ -11081,7 +12392,7 @@ import {
11081
12392
  } from "@rig/runtime/control-plane/native/run-ops";
11082
12393
  import {
11083
12394
  listAuthorityRuns as listAuthorityRuns6,
11084
- readAuthorityRun as readAuthorityRun8
12395
+ readAuthorityRun as readAuthorityRun9
11085
12396
  } from "@rig/runtime/control-plane/authority-files";
11086
12397
  function providerFromRuntimeAdapter(runtimeAdapter) {
11087
12398
  if (!runtimeAdapter) {
@@ -11161,7 +12472,7 @@ function discoverInspectorRuns(options) {
11161
12472
  discovered.set(surface.runId, existing);
11162
12473
  };
11163
12474
  for (const authorityEntry of listAuthorityRuns6(options.projectRoot)) {
11164
- const run = readAuthorityRun8(options.projectRoot, authorityEntry.runId);
12475
+ const run = readAuthorityRun9(options.projectRoot, authorityEntry.runId);
11165
12476
  if (!run) {
11166
12477
  continue;
11167
12478
  }
@@ -11484,8 +12795,8 @@ function createGlobalInspectorService(options) {
11484
12795
 
11485
12796
  // packages/server/src/inspector/upstream-sync.ts
11486
12797
  import { spawnSync as spawnSync4 } from "child_process";
11487
- import { existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync12 } from "fs";
11488
- import { dirname as dirname14, resolve as resolve20 } from "path";
12798
+ import { existsSync as existsSync15, mkdirSync as mkdirSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync14 } from "fs";
12799
+ import { dirname as dirname17, resolve as resolve22 } from "path";
11489
12800
  import { resolveMonorepoRoot as resolveMonorepoRoot4 } from "@rig/runtime/control-plane/native/utils";
11490
12801
  var UPSTREAM_VALIDATION_DESCRIPTIONS = {
11491
12802
  "integration:hg-auth-backport": "Preserves the upstream auth hardening cluster: nonce-backed node-client login, signature-aware JWT verification, shared-token onboarding semantics, and regression coverage.",
@@ -11623,34 +12934,34 @@ function defaultGitRunner(repoRoot, args) {
11623
12934
  }
11624
12935
  function upstreamStatePath(projectRoot, override) {
11625
12936
  if (override) {
11626
- return resolve20(override);
12937
+ return resolve22(override);
11627
12938
  }
11628
- return resolve20(resolveRigServerPaths(projectRoot).stateDir, "inspector", "upstream-sync.json");
12939
+ return resolve22(resolveRigServerPaths(projectRoot).stateDir, "inspector", "upstream-sync.json");
11629
12940
  }
11630
12941
  function readUpstreamState(projectRoot, statePath) {
11631
12942
  const path = upstreamStatePath(projectRoot, statePath);
11632
- if (!existsSync13(path)) {
12943
+ if (!existsSync15(path)) {
11633
12944
  return null;
11634
12945
  }
11635
12946
  try {
11636
- return JSON.parse(readFileSync9(path, "utf-8"));
12947
+ return JSON.parse(readFileSync11(path, "utf-8"));
11637
12948
  } catch {
11638
12949
  return null;
11639
12950
  }
11640
12951
  }
11641
12952
  function writeUpstreamState(projectRoot, state, statePath) {
11642
12953
  const path = upstreamStatePath(projectRoot, statePath);
11643
- mkdirSync13(dirname14(path), { recursive: true });
11644
- writeFileSync12(path, `${JSON.stringify(state, null, 2)}
12954
+ mkdirSync15(dirname17(path), { recursive: true });
12955
+ writeFileSync14(path, `${JSON.stringify(state, null, 2)}
11645
12956
  `, "utf8");
11646
12957
  }
11647
12958
  function readImportedRevision(projectRoot, upstreamsDocPath) {
11648
12959
  const monorepoRoot = resolveMonorepoRoot4(projectRoot);
11649
- const docPath = upstreamsDocPath ? resolve20(upstreamsDocPath) : resolve20(monorepoRoot, "docs", "UPSTREAMS.md");
11650
- if (!existsSync13(docPath)) {
12960
+ const docPath = upstreamsDocPath ? resolve22(upstreamsDocPath) : resolve22(monorepoRoot, "docs", "UPSTREAMS.md");
12961
+ if (!existsSync15(docPath)) {
11651
12962
  throw new Error(`UPSTREAMS.md not found at ${docPath}`);
11652
12963
  }
11653
- const docContent = readFileSync9(docPath, "utf-8");
12964
+ const docContent = readFileSync11(docPath, "utf-8");
11654
12965
  const revision = parseImportedUpstreamRevision(docContent, "upstream") ?? parseImportedUpstreamRevision(docContent, "humoongate");
11655
12966
  if (!revision) {
11656
12967
  throw new Error(`Failed to parse upstream imported revision from ${docPath}`);
@@ -11672,7 +12983,7 @@ function resolveRemoteBranch(repoRoot, remote, gitRunner) {
11672
12983
  return null;
11673
12984
  }
11674
12985
  function isGitCheckout(path, gitRunner) {
11675
- if (!existsSync13(resolve20(path, ".git"))) {
12986
+ if (!existsSync15(resolve22(path, ".git"))) {
11676
12987
  return false;
11677
12988
  }
11678
12989
  const result = gitRunner(path, ["rev-parse", "--is-inside-work-tree"]);
@@ -11681,12 +12992,12 @@ function isGitCheckout(path, gitRunner) {
11681
12992
  function resolveUpstreamCheckout(projectRoot, explicitCheckout, gitRunner) {
11682
12993
  const monorepoRoot = resolveMonorepoRoot4(projectRoot);
11683
12994
  const candidates = [
11684
- explicitCheckout ? resolve20(explicitCheckout) : "",
11685
- process.env.UPSTREAM_CHECKOUT?.trim() ? resolve20(process.env.UPSTREAM_CHECKOUT.trim()) : "",
11686
- process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ? resolve20(process.env.HUMOONGATE_UPSTREAM_CHECKOUT.trim()) : "",
11687
- resolve20(projectRoot, "..", "humoongate"),
11688
- resolve20(monorepoRoot, "..", "humoongate"),
11689
- resolve20(monorepoRoot, "humoongate")
12995
+ explicitCheckout ? resolve22(explicitCheckout) : "",
12996
+ process.env.UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.UPSTREAM_CHECKOUT.trim()) : "",
12997
+ process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.HUMOONGATE_UPSTREAM_CHECKOUT.trim()) : "",
12998
+ resolve22(projectRoot, "..", "humoongate"),
12999
+ resolve22(monorepoRoot, "..", "humoongate"),
13000
+ resolve22(monorepoRoot, "humoongate")
11690
13001
  ].filter(Boolean);
11691
13002
  for (const candidate of candidates) {
11692
13003
  if (isGitCheckout(candidate, gitRunner)) {
@@ -11922,10 +13233,10 @@ async function runUpstreamSyncScan(options) {
11922
13233
  }
11923
13234
 
11924
13235
  // packages/server/src/server-helpers/task-config.ts
11925
- import { existsSync as existsSync14 } from "fs";
13236
+ import { existsSync as existsSync16 } from "fs";
11926
13237
  async function readTaskConfig(projectRoot) {
11927
13238
  const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
11928
- if (!existsSync14(taskConfigPath)) {
13239
+ if (!existsSync16(taskConfigPath)) {
11929
13240
  return {};
11930
13241
  }
11931
13242
  try {
@@ -11961,11 +13272,11 @@ function resolveFollowupSourceCommit(input) {
11961
13272
  }
11962
13273
  async function createInspectorFollowupTask(projectRoot, input) {
11963
13274
  const monorepoRoot = resolveMonorepoRoot5(projectRoot);
11964
- const issuesPath = resolve21(monorepoRoot, ".beads", "issues.jsonl");
11965
- const taskStatePath = resolve21(monorepoRoot, ".beads", "task-state.json");
11966
- const taskConfigPath = resolve21(monorepoRoot, ".rig", "task-config.json");
11967
- mkdirSync14(dirname15(issuesPath), { recursive: true });
11968
- mkdirSync14(dirname15(taskConfigPath), { recursive: true });
13275
+ const issuesPath = resolve23(monorepoRoot, ".beads", "issues.jsonl");
13276
+ const taskStatePath = resolve23(monorepoRoot, ".beads", "task-state.json");
13277
+ const taskConfigPath = resolve23(monorepoRoot, ".rig", "task-config.json");
13278
+ mkdirSync16(dirname18(issuesPath), { recursive: true });
13279
+ mkdirSync16(dirname18(taskConfigPath), { recursive: true });
11969
13280
  const summary = normalizeString(input.summary) ?? "Inspector follow-up";
11970
13281
  const description = normalizeString(input.description) ?? normalizeString(input.details?.description) ?? `Created by the global inspector: ${summary}`;
11971
13282
  const acceptanceCriteria = normalizeString(input.acceptanceCriteria) ?? "Investigate the detected drift and port the relevant changes into Rig.";
@@ -11984,7 +13295,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
11984
13295
  const sourceKey = normalizeString(input.sourceKey) ?? normalizeString(input.details?.sourceKey);
11985
13296
  const createdAt = normalizeString(input.createdAt) ?? new Date().toISOString();
11986
13297
  const status = normalizeTaskLifecycleStatus2(normalizeString(input.status) ?? "open") ?? "open";
11987
- const existingIssueLines = existsSync15(issuesPath) ? readFileSync10(issuesPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
13298
+ const existingIssueLines = existsSync17(issuesPath) ? readFileSync12(issuesPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
11988
13299
  const existingIssues = existingIssueLines.map((line) => {
11989
13300
  try {
11990
13301
  return JSON.parse(line);
@@ -11993,7 +13304,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
11993
13304
  }
11994
13305
  }).filter((value) => value !== null);
11995
13306
  const existingIds = new Set(existingIssues.map((issue) => typeof issue.id === "string" ? issue.id : null).filter((value) => value !== null));
11996
- const rawTaskState = existsSync15(taskStatePath) ? readJsonFile3(taskStatePath, {}) : {};
13307
+ const rawTaskState = existsSync17(taskStatePath) ? readJsonFile3(taskStatePath, {}) : {};
11997
13308
  const tasks = rawTaskState.tasks && typeof rawTaskState.tasks === "object" && !Array.isArray(rawTaskState.tasks) ? rawTaskState.tasks : {};
11998
13309
  const existingTaskIdFromSourceKey = sourceKey == null ? null : Object.entries(tasks).find(([, metadata]) => {
11999
13310
  if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
@@ -12019,7 +13330,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
12019
13330
  updated_at: createdAt,
12020
13331
  labels: mergedLabels
12021
13332
  };
12022
- writeFileSync13(issuesPath, existingIssueLines.length > 0 ? `${existingIssueLines.join(`
13333
+ writeFileSync15(issuesPath, existingIssueLines.length > 0 ? `${existingIssueLines.join(`
12023
13334
  `)}
12024
13335
  ${JSON.stringify(issueRecord)}
12025
13336
  ` : `${JSON.stringify(issueRecord)}
@@ -12037,7 +13348,7 @@ ${JSON.stringify(issueRecord)}
12037
13348
  labels: mergedLabels
12038
13349
  };
12039
13350
  });
12040
- writeFileSync13(issuesPath, `${updatedIssues.map((issue) => JSON.stringify(issue)).join(`
13351
+ writeFileSync15(issuesPath, `${updatedIssues.map((issue) => JSON.stringify(issue)).join(`
12041
13352
  `)}
12042
13353
  `, "utf8");
12043
13354
  }
@@ -12060,14 +13371,14 @@ ${JSON.stringify(issueRecord)}
12060
13371
  }
12061
13372
  };
12062
13373
  }
12063
- writeFileSync13(taskConfigPath, `${JSON.stringify(taskConfig, null, 2)}
13374
+ writeFileSync15(taskConfigPath, `${JSON.stringify(taskConfig, null, 2)}
12064
13375
  `, "utf8");
12065
13376
  tasks[taskId] = {
12066
13377
  status,
12067
13378
  sourceCommit: resolveFollowupSourceCommit(input),
12068
13379
  ...sourceKey ? { sourceKey } : {}
12069
13380
  };
12070
- writeFileSync13(taskStatePath, `${JSON.stringify({
13381
+ writeFileSync15(taskStatePath, `${JSON.stringify({
12071
13382
  schemaVersion: 1,
12072
13383
  baseTrackerCommit: typeof rawTaskState.baseTrackerCommit === "string" ? rawTaskState.baseTrackerCommit : null,
12073
13384
  tasks
@@ -12094,21 +13405,10 @@ async function runInspectorLocalReview(projectRoot, input) {
12094
13405
  details: null
12095
13406
  };
12096
13407
  }
12097
- const [{ RuntimeEventBus }, { PluginManager }, { verifyTask }] = await Promise.all([
12098
- import("@rig/runtime/control-plane/runtime/events"),
12099
- import("@rig/runtime/control-plane/runtime/plugins"),
12100
- import("@rig/runtime/control-plane/native/verifier")
12101
- ]);
12102
- const eventBus = new RuntimeEventBus({ projectRoot, runId: `inspector-review:${taskId}` });
12103
- const plugins = await PluginManager.load({
12104
- projectRoot,
12105
- runId: `inspector-review:${taskId}`,
12106
- eventBus
12107
- });
13408
+ const { verifyTask } = await import("@rig/runtime/control-plane/native/verifier");
12108
13409
  const outcome = await verifyTask({
12109
13410
  projectRoot,
12110
13411
  taskId,
12111
- plugins,
12112
13412
  skipAiReview: true
12113
13413
  });
12114
13414
  return {
@@ -12375,12 +13675,12 @@ function isAuthorizedLinearWebhookRequest(req) {
12375
13675
  }
12376
13676
 
12377
13677
  // packages/server/src/server-helpers/notifications.ts
12378
- import { existsSync as existsSync16, mkdirSync as mkdirSync15, readFileSync as readFileSync11 } from "fs";
12379
- import { dirname as dirname16 } from "path";
13678
+ import { existsSync as existsSync18, mkdirSync as mkdirSync17, readFileSync as readFileSync13 } from "fs";
13679
+ import { dirname as dirname19 } from "path";
12380
13680
  async function loadNotificationConfig(path) {
12381
- if (!existsSync16(path)) {
13681
+ if (!existsSync18(path)) {
12382
13682
  const defaultConfig = { targets: [] };
12383
- mkdirSync15(dirname16(path), { recursive: true });
13683
+ mkdirSync17(dirname19(path), { recursive: true });
12384
13684
  await Bun.write(path, `${JSON.stringify(defaultConfig, null, 2)}
12385
13685
  `);
12386
13686
  return defaultConfig;
@@ -12395,10 +13695,10 @@ async function loadNotificationConfig(path) {
12395
13695
  }
12396
13696
  }
12397
13697
  function readRecentEvents(file, limit) {
12398
- if (!existsSync16(file)) {
13698
+ if (!existsSync18(file)) {
12399
13699
  return [];
12400
13700
  }
12401
- const lines = readFileSync11(file, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-limit);
13701
+ const lines = readFileSync13(file, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-limit);
12402
13702
  const events = [];
12403
13703
  for (const line of lines) {
12404
13704
  try {
@@ -12493,11 +13793,11 @@ function extractObjectLiteralBlock(source, property) {
12493
13793
  }
12494
13794
  function readFallbackIssueAnalysisConfig(projectRoot) {
12495
13795
  for (const fileName of ["rig.config.ts", "rig.config.json"]) {
12496
- const path = resolve22(projectRoot, fileName);
12497
- if (!existsSync17(path))
13796
+ const path = resolve24(projectRoot, fileName);
13797
+ if (!existsSync19(path))
12498
13798
  continue;
12499
13799
  try {
12500
- const source = readFileSync12(path, "utf8");
13800
+ const source = readFileSync14(path, "utf8");
12501
13801
  if (fileName.endsWith(".json"))
12502
13802
  return JSON.parse(source);
12503
13803
  const issueBlock = extractObjectLiteralBlock(source, "issueAnalysis");
@@ -12630,8 +13930,8 @@ async function createIssueAnalysisRunnerForServerState(state, input) {
12630
13930
  async function withServerPathEnv(projectRoot, fn) {
12631
13931
  const waitForTurn = serverPathEnvQueue;
12632
13932
  let releaseTurn;
12633
- serverPathEnvQueue = new Promise((resolve23) => {
12634
- releaseTurn = resolve23;
13933
+ serverPathEnvQueue = new Promise((resolve25) => {
13934
+ releaseTurn = resolve25;
12635
13935
  });
12636
13936
  await waitForTurn;
12637
13937
  const paths = resolveServerAuthorityPaths(projectRoot);
@@ -12667,9 +13967,9 @@ async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
12667
13967
  return withServerPathEnv(projectRoot, fn);
12668
13968
  }
12669
13969
  async function readWorkspaceTasks(projectRoot) {
12670
- const issuesPath = resolve22(resolveMonorepoRoot6(projectRoot), ".beads", "issues.jsonl");
13970
+ const issuesPath = resolve24(resolveMonorepoRoot6(projectRoot), ".beads", "issues.jsonl");
12671
13971
  const taskConfig = await readTaskConfig(projectRoot);
12672
- if (!existsSync17(issuesPath)) {
13972
+ if (!existsSync19(issuesPath)) {
12673
13973
  return [];
12674
13974
  }
12675
13975
  const latestById = new Map;
@@ -12743,11 +14043,11 @@ function resolveTaskArtifactDirsFromRuns(projectRoot, taskId, knownRuns) {
12743
14043
  continue;
12744
14044
  add(run.artifactRoot);
12745
14045
  if (run.worktreePath) {
12746
- add(resolve22(run.worktreePath, "artifacts", taskId));
14046
+ add(resolve24(run.worktreePath, "artifacts", taskId));
12747
14047
  }
12748
14048
  }
12749
14049
  for (const artifactsRoot of listAuthorityArtifactRoots(projectRoot)) {
12750
- add(resolve22(artifactsRoot, taskId));
14050
+ add(resolve24(artifactsRoot, taskId));
12751
14051
  }
12752
14052
  return candidates;
12753
14053
  }
@@ -12761,7 +14061,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
12761
14061
  }
12762
14062
  }
12763
14063
  return taskIds.flatMap((currentTaskId) => {
12764
- const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) => existsSync17(path));
14064
+ const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) => existsSync19(path));
12765
14065
  if (!currentRoot) {
12766
14066
  return [];
12767
14067
  }
@@ -12773,7 +14073,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
12773
14073
  taskId: currentTaskId,
12774
14074
  kind: "file",
12775
14075
  label: fileName,
12776
- path: resolve22(currentRoot, fileName),
14076
+ path: resolve24(currentRoot, fileName),
12777
14077
  url: null,
12778
14078
  metadata: {
12779
14079
  fileName
@@ -12816,11 +14116,11 @@ function buildInspectorStreamPayload(state, sequence) {
12816
14116
  }
12817
14117
  function listRemoteRunArtifacts(projectRoot, runId) {
12818
14118
  const root = remoteArtifactsRoot(projectRoot, runId);
12819
- if (!existsSync17(root)) {
14119
+ if (!existsSync19(root)) {
12820
14120
  return [];
12821
14121
  }
12822
14122
  return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isFile()).filter((entry) => !entry.name.endsWith(".json")).map((entry) => {
12823
- const artifactPath = resolve22(root, entry.name);
14123
+ const artifactPath = resolve24(root, entry.name);
12824
14124
  const stat = statSync6(artifactPath);
12825
14125
  const meta = readJsonFile4(`${artifactPath}.json`, null);
12826
14126
  return {
@@ -12981,6 +14281,7 @@ function buildHttpRouterDeps(state) {
12981
14281
  startLocalRun,
12982
14282
  stopRunRecord,
12983
14283
  resumeRunRecord,
14284
+ runServerOwnedPrCloseout,
12984
14285
  claimRemoteRun,
12985
14286
  listRemoteRunArtifacts,
12986
14287
  broadcastSnapshotInvalidation,
@@ -13062,8 +14363,8 @@ function fileStats(path) {
13062
14363
  }
13063
14364
  }
13064
14365
  function runFileCursor(projectRoot, run) {
13065
- const runDir = dirname17(runLogsPath(projectRoot, run.runId));
13066
- const runJson = fileStats(resolve22(runDir, "run.json"));
14366
+ const runDir = dirname20(runLogsPath(projectRoot, run.runId));
14367
+ const runJson = fileStats(resolve24(runDir, "run.json"));
13067
14368
  const timeline = fileStats(runTimelinePath(projectRoot, run.runId));
13068
14369
  const logs = fileStats(runLogsPath(projectRoot, run.runId));
13069
14370
  return {
@@ -13113,10 +14414,10 @@ function startRunFileWatcher(state, pollMs) {
13113
14414
  }, Math.max(250, Math.min(pollMs, 1000)));
13114
14415
  }
13115
14416
  function startPoller(state, pollMs) {
13116
- let offset = existsSync17(state.eventsFile) ? statSync6(state.eventsFile).size : 0;
14417
+ let offset = existsSync19(state.eventsFile) ? statSync6(state.eventsFile).size : 0;
13117
14418
  return setInterval(async () => {
13118
14419
  try {
13119
- if (!existsSync17(state.eventsFile)) {
14420
+ if (!existsSync19(state.eventsFile)) {
13120
14421
  return;
13121
14422
  }
13122
14423
  const file = await open(state.eventsFile, "r");
@@ -13160,6 +14461,26 @@ function startPoller(state, pollMs) {
13160
14461
  }
13161
14462
  }, pollMs);
13162
14463
  }
14464
+ function attachPiSessionProxySocket(ws) {
14465
+ if (ws.data.kind !== "pi-session-proxy")
14466
+ return;
14467
+ const upstream = new WebSocket(ws.data.upstreamUrl);
14468
+ ws.__rigPiUpstream = upstream;
14469
+ upstream.addEventListener("message", (event) => {
14470
+ if (ws.readyState === 1)
14471
+ ws.send(typeof event.data === "string" ? event.data : event.data);
14472
+ });
14473
+ upstream.addEventListener("close", () => {
14474
+ try {
14475
+ ws.close();
14476
+ } catch {}
14477
+ });
14478
+ upstream.addEventListener("error", () => {
14479
+ try {
14480
+ ws.close(1011, "Upstream Pi session stream failed");
14481
+ } catch {}
14482
+ });
14483
+ }
13163
14484
  async function createRigServer(options, projectRoot = resolveProjectRoot()) {
13164
14485
  const state = await createRigServerState(projectRoot, options.eventType, options.authToken, {
13165
14486
  upstreamSyncMs: options.upstreamSyncMs,
@@ -13172,10 +14493,20 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
13172
14493
  fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
13173
14494
  websocket: {
13174
14495
  open(ws) {
14496
+ if (ws.data.kind === "pi-session-proxy") {
14497
+ attachPiSessionProxySocket(ws);
14498
+ return;
14499
+ }
13175
14500
  state.sockets.add(ws);
13176
14501
  sendWebSocketResponse(ws, buildServerWelcomePush(state.projectRoot));
13177
14502
  },
13178
14503
  async message(ws, raw) {
14504
+ if (ws.data.kind === "pi-session-proxy") {
14505
+ const upstream = ws.__rigPiUpstream;
14506
+ if (upstream?.readyState === WebSocket.OPEN)
14507
+ upstream.send(raw);
14508
+ return;
14509
+ }
13179
14510
  const request = parseWebSocketRequestEnvelope(typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8"));
13180
14511
  if (!request) {
13181
14512
  sendWebSocketResponse(ws, {
@@ -13198,6 +14529,10 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
13198
14529
  }
13199
14530
  },
13200
14531
  close(ws) {
14532
+ if (ws.data.kind === "pi-session-proxy") {
14533
+ ws.__rigPiUpstream?.close();
14534
+ return;
14535
+ }
13201
14536
  state.sockets.delete(ws);
13202
14537
  }
13203
14538
  }
@@ -13244,7 +14579,7 @@ function resolveProjectRoot() {
13244
14579
  return resolveRigProjectRoot({
13245
14580
  envProjectRoot: process.env.PROJECT_RIG_ROOT ?? null,
13246
14581
  cwd: process.cwd(),
13247
- fallbackRoot: resolve22(import.meta.dir, "../..")
14582
+ fallbackRoot: resolve24(import.meta.dir, "../..")
13248
14583
  });
13249
14584
  }
13250
14585
  var __testOnly = {
@@ -13305,6 +14640,7 @@ export {
13305
14640
  resolveRigServerPaths,
13306
14641
  resolveRigProjectRoot,
13307
14642
  resolvePublishedRigServerStatePath,
14643
+ resolveProjectStatusField,
13308
14644
  resolveProjectRoot,
13309
14645
  registerRemoteHost,
13310
14646
  readWorkspaceTasks,
@@ -13313,6 +14649,7 @@ export {
13313
14649
  parseRigServerArgs,
13314
14650
  parseArgs,
13315
14651
  main,
14652
+ listGitHubProjects,
13316
14653
  heartbeatRemoteHost,
13317
14654
  handleWebSocketUpgrade,
13318
14655
  encodeWebSocketPayload,