@h-rig/server 0.0.6-alpha.2 → 0.0.6-alpha.20

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,
@@ -1580,6 +1580,18 @@ function readJsonlFileTail(path, options) {
1580
1580
  const completeLines = start > 0 ? lines.slice(1) : lines;
1581
1581
  return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
1582
1582
  }
1583
+ async function readRunTimelinePage(projectRoot, runId, options = {}) {
1584
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
1585
+ const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
1586
+ const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
1587
+ const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
1588
+ const endExclusive = Math.min(entries.length, startInclusive + limit);
1589
+ return {
1590
+ entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
1591
+ nextCursor: String(endExclusive),
1592
+ hasMore: endExclusive < entries.length
1593
+ };
1594
+ }
1583
1595
  var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
1584
1596
  async function readRunLogsPage(projectRoot, runId, options = {}) {
1585
1597
  const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
@@ -2819,7 +2831,6 @@ function createGitHubTaskReconciler(input) {
2819
2831
  }
2820
2832
 
2821
2833
  // packages/server/src/server-helpers/issue-analysis.ts
2822
- import { execFile } from "child_process";
2823
2834
  import { createHash } from "crypto";
2824
2835
  function stableIssueHash(issue) {
2825
2836
  const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
@@ -2953,16 +2964,33 @@ function parseIssueAnalysisResult(raw) {
2953
2964
  return result;
2954
2965
  }
2955
2966
  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 ?? "") });
2967
+ return async (command, args, options) => {
2968
+ const env = options.env ? { ...process.env, ...options.env } : process.env;
2969
+ const proc = Bun.spawn([command, ...args], {
2970
+ stdout: "pipe",
2971
+ stderr: "pipe",
2972
+ env
2964
2973
  });
2965
- });
2974
+ let timedOut = false;
2975
+ const timer = setTimeout(() => {
2976
+ timedOut = true;
2977
+ proc.kill();
2978
+ }, options.timeoutMs);
2979
+ try {
2980
+ const [stdout, stderr, exitCode] = await Promise.all([
2981
+ new Response(proc.stdout).text(),
2982
+ new Response(proc.stderr).text(),
2983
+ proc.exited
2984
+ ]);
2985
+ return {
2986
+ exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
2987
+ stdout,
2988
+ stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
2989
+ };
2990
+ } finally {
2991
+ clearTimeout(timer);
2992
+ }
2993
+ };
2966
2994
  }
2967
2995
  function createPiIssueAnalyzer(input = {}) {
2968
2996
  const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
@@ -2970,7 +2998,10 @@ function createPiIssueAnalyzer(input = {}) {
2970
2998
  const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
2971
2999
  return async ({ prompt }) => {
2972
3000
  const args = ["--print", "--mode", "json", "--no-session"];
2973
- const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || "openai-codex/gpt-5.5";
3001
+ const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
3002
+ const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
3003
+ if (provider)
3004
+ args.push("--provider", provider);
2974
3005
  if (model)
2975
3006
  args.push("--model", model);
2976
3007
  args.push(prompt);
@@ -3218,8 +3249,7 @@ function buildRunStartPatch(startedAt) {
3218
3249
  status: "preparing",
3219
3250
  startedAt,
3220
3251
  completedAt: null,
3221
- errorText: null,
3222
- serverPid: process.pid
3252
+ errorText: null
3223
3253
  };
3224
3254
  }
3225
3255
 
@@ -3688,7 +3718,7 @@ function applyOrchestrationCommand2(state, command) {
3688
3718
  import { spawn as spawn3 } from "child_process";
3689
3719
  import { loadConfig } from "@rig/core/load-config";
3690
3720
  import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
3691
- import { dirname as dirname8, relative as relative2, resolve as resolve14 } from "path";
3721
+ import { dirname as dirname9, relative as relative2, resolve as resolve14 } from "path";
3692
3722
  import {
3693
3723
  listAuthorityRuns as listAuthorityRuns4,
3694
3724
  readAuthorityRun as readAuthorityRun4,
@@ -3701,6 +3731,11 @@ import {
3701
3731
  buildTaskRunLifecycleComment,
3702
3732
  updateConfiguredTaskSourceTask
3703
3733
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
3734
+ import {
3735
+ closeIssueAfterMergedPr,
3736
+ commitRunChanges,
3737
+ runPrAutomation
3738
+ } from "@rig/runtime/control-plane/native/pr-automation";
3704
3739
 
3705
3740
  // packages/server/src/scheduler.ts
3706
3741
  import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
@@ -3812,8 +3847,8 @@ function summarizeRunValidationFailure(projectRoot, run) {
3812
3847
 
3813
3848
  // packages/server/src/server-helpers/github-auth-store.ts
3814
3849
  import { randomBytes } from "crypto";
3815
- import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
3816
- import { resolve as resolve13 } from "path";
3850
+ import { chmodSync, copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
3851
+ import { dirname as dirname8, resolve as resolve13 } from "path";
3817
3852
  function cleanString(value) {
3818
3853
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
3819
3854
  }
@@ -3843,6 +3878,26 @@ function parseApiSessions(value) {
3843
3878
  }];
3844
3879
  });
3845
3880
  }
3881
+ function parsePendingDevice(value) {
3882
+ if (!value || typeof value !== "object")
3883
+ return null;
3884
+ const record = value;
3885
+ const pollId = cleanString(record.pollId);
3886
+ const deviceCode = cleanString(record.deviceCode);
3887
+ const expiresAt = cleanString(record.expiresAt);
3888
+ const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
3889
+ if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
3890
+ return null;
3891
+ return { pollId, deviceCode, expiresAt, intervalSeconds };
3892
+ }
3893
+ function parsePendingDevices(value) {
3894
+ if (!Array.isArray(value))
3895
+ return [];
3896
+ return value.flatMap((entry) => {
3897
+ const pending = parsePendingDevice(entry);
3898
+ return pending ? [pending] : [];
3899
+ });
3900
+ }
3846
3901
  function readStoredAuth(stateFile) {
3847
3902
  if (!existsSync6(stateFile))
3848
3903
  return {};
@@ -3856,6 +3911,7 @@ function readStoredAuth(stateFile) {
3856
3911
  selectedRepo: cleanString(parsed.selectedRepo),
3857
3912
  tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
3858
3913
  pendingDevice: parsePendingDevice(parsed.pendingDevice),
3914
+ pendingDevices: parsePendingDevices(parsed.pendingDevices),
3859
3915
  apiSessions: parseApiSessions(parsed.apiSessions),
3860
3916
  updatedAt: cleanString(parsed.updatedAt) ?? undefined
3861
3917
  };
@@ -3863,34 +3919,36 @@ function readStoredAuth(stateFile) {
3863
3919
  return {};
3864
3920
  }
3865
3921
  }
3866
- function parsePendingDevice(value) {
3867
- if (!value || typeof value !== "object")
3868
- return null;
3869
- const record = value;
3870
- const pollId = cleanString(record.pollId);
3871
- const deviceCode = cleanString(record.deviceCode);
3872
- const expiresAt = cleanString(record.expiresAt);
3873
- const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
3874
- if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
3875
- return null;
3876
- return { pollId, deviceCode, expiresAt, intervalSeconds };
3877
- }
3878
3922
  function newApiSessionToken() {
3879
3923
  return `rig_${randomBytes(32).toString("base64url")}`;
3880
3924
  }
3881
3925
  function writeStoredAuth(stateFile, payload) {
3882
- mkdirSync6(resolve13(stateFile, ".."), { recursive: true });
3926
+ mkdirSync6(dirname8(stateFile), { recursive: true });
3883
3927
  writeFileSync5(stateFile, `${JSON.stringify(payload, null, 2)}
3884
3928
  `, { encoding: "utf8", mode: 384 });
3885
3929
  try {
3886
3930
  chmodSync(stateFile, 384);
3887
3931
  } catch {}
3888
3932
  }
3933
+ function localProjectAuthStateFile(projectRoot) {
3934
+ return resolve13(projectRoot, ".rig", "state", "github-auth.json");
3935
+ }
3889
3936
  function resolveGitHubAuthStateFile(projectRoot) {
3890
3937
  return resolve13(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
3891
3938
  }
3892
- function createGitHubAuthStore(projectRoot) {
3893
- const stateFile = resolveGitHubAuthStateFile(projectRoot);
3939
+ function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
3940
+ const targetFile = localProjectAuthStateFile(projectRoot);
3941
+ mkdirSync6(dirname8(targetFile), { recursive: true });
3942
+ if (existsSync6(stateFile)) {
3943
+ copyFileSync(stateFile, targetFile);
3944
+ try {
3945
+ chmodSync(targetFile, 384);
3946
+ } catch {}
3947
+ return;
3948
+ }
3949
+ writeStoredAuth(targetFile, {});
3950
+ }
3951
+ function createGitHubAuthStoreFromStateFile(stateFile) {
3894
3952
  return {
3895
3953
  stateFile,
3896
3954
  status(options) {
@@ -3920,6 +3978,7 @@ function createGitHubAuthStore(projectRoot) {
3920
3978
  scopes: input.scopes ?? [],
3921
3979
  selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
3922
3980
  pendingDevice: null,
3981
+ pendingDevices: [],
3923
3982
  apiSessions: previous.apiSessions ?? [],
3924
3983
  updatedAt: new Date().toISOString()
3925
3984
  });
@@ -3948,15 +4007,24 @@ function createGitHubAuthStore(projectRoot) {
3948
4007
  const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
3949
4008
  return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
3950
4009
  },
3951
- copyToProjectRoot(projectRoot2) {
3952
- const targetFile = resolveGitHubAuthStateFile(projectRoot2);
4010
+ copyToProjectRoot(projectRoot) {
4011
+ const targetFile = resolveGitHubAuthStateFile(projectRoot);
3953
4012
  writeStoredAuth(targetFile, readStoredAuth(stateFile));
3954
4013
  },
4014
+ copyToLocalProjectRoot(projectRoot) {
4015
+ copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
4016
+ },
3955
4017
  savePendingDevice(input) {
3956
4018
  const previous = readStoredAuth(stateFile);
4019
+ const pendingDevices = [
4020
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
4021
+ ...previous.pendingDevices ?? [],
4022
+ input
4023
+ ].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
3957
4024
  writeStoredAuth(stateFile, {
3958
4025
  ...previous,
3959
- pendingDevice: input,
4026
+ pendingDevice: null,
4027
+ pendingDevices,
3960
4028
  updatedAt: new Date().toISOString()
3961
4029
  });
3962
4030
  },
@@ -3969,23 +4037,32 @@ function createGitHubAuthStore(projectRoot) {
3969
4037
  });
3970
4038
  },
3971
4039
  readPendingDevice(pollId) {
3972
- const pending = readStoredAuth(stateFile).pendingDevice ?? null;
3973
- if (!pending || pending.pollId !== pollId)
4040
+ const previous = readStoredAuth(stateFile);
4041
+ const pending = [
4042
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
4043
+ ...previous.pendingDevices ?? []
4044
+ ].find((entry) => entry.pollId === pollId) ?? null;
4045
+ if (!pending)
3974
4046
  return null;
3975
4047
  if (Date.parse(pending.expiresAt) <= Date.now())
3976
4048
  return null;
3977
4049
  return pending;
3978
4050
  },
3979
- clearPendingDevice() {
4051
+ clearPendingDevice(pollId) {
3980
4052
  const previous = readStoredAuth(stateFile);
4053
+ const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
3981
4054
  writeStoredAuth(stateFile, {
3982
4055
  ...previous,
3983
4056
  pendingDevice: null,
4057
+ pendingDevices: remaining,
3984
4058
  updatedAt: new Date().toISOString()
3985
4059
  });
3986
4060
  }
3987
4061
  };
3988
4062
  }
4063
+ function createGitHubAuthStore(projectRoot) {
4064
+ return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
4065
+ }
3989
4066
 
3990
4067
  // packages/server/src/server-helpers/github-projects.ts
3991
4068
  function asRecord(value) {
@@ -3994,6 +4071,9 @@ function asRecord(value) {
3994
4071
  function asString(value) {
3995
4072
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
3996
4073
  }
4074
+ function asNumber(value) {
4075
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
4076
+ }
3997
4077
  async function defaultGraphQLFetch(query, variables, token) {
3998
4078
  const response = await fetch("https://api.github.com/graphql", {
3999
4079
  method: "POST",
@@ -4010,6 +4090,32 @@ async function defaultGraphQLFetch(query, variables, token) {
4010
4090
  }
4011
4091
  return json.data;
4012
4092
  }
4093
+ function projectNodesFrom(data) {
4094
+ const root = asRecord(data);
4095
+ const owner = asRecord(root?.organization) ?? asRecord(root?.user);
4096
+ const projects = asRecord(owner?.projectsV2);
4097
+ const nodes = projects?.nodes;
4098
+ return Array.isArray(nodes) ? nodes : [];
4099
+ }
4100
+ async function listGitHubProjects(input) {
4101
+ const query = `
4102
+ query RigListProjects($owner: String!, $first: Int!) {
4103
+ organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
4104
+ user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
4105
+ }
4106
+ `;
4107
+ const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
4108
+ const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
4109
+ return projectNodesFrom(data).flatMap((node) => {
4110
+ const record = asRecord(node);
4111
+ const id = asString(record?.id);
4112
+ const number = asNumber(record?.number);
4113
+ const title = asString(record?.title);
4114
+ if (!id || number === undefined || !title)
4115
+ return [];
4116
+ return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
4117
+ });
4118
+ }
4013
4119
  async function resolveProjectStatusField(input) {
4014
4120
  const query = `
4015
4121
  query RigProjectStatusField($projectId: ID!) {
@@ -4104,6 +4210,7 @@ var DEFAULT_PROJECT_STATUSES = {
4104
4210
  running: "In Progress",
4105
4211
  prOpen: "In Review",
4106
4212
  ciFixing: "In Review",
4213
+ merging: "Merging",
4107
4214
  done: "Done",
4108
4215
  needsAttention: "Needs Attention"
4109
4216
  };
@@ -4117,6 +4224,8 @@ function lifecycleStatusForTaskStatus(status) {
4117
4224
  return "prOpen";
4118
4225
  if (normalized === "ci_fixing" || normalized === "fixing")
4119
4226
  return "ciFixing";
4227
+ if (normalized === "merging" || normalized === "merge")
4228
+ return "merging";
4120
4229
  if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
4121
4230
  return "needsAttention";
4122
4231
  if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
@@ -4245,9 +4354,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
4245
4354
  return null;
4246
4355
  return null;
4247
4356
  }
4357
+ function githubProjectsEnabled(config) {
4358
+ const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
4359
+ const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
4360
+ return projects?.enabled === true;
4361
+ }
4248
4362
  async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
4249
4363
  if (!run.taskId)
4250
- return;
4364
+ return false;
4251
4365
  const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
4252
4366
  try {
4253
4367
  const result = await syncGitHubProjectStatusForTaskUpdate({
@@ -4258,28 +4372,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
4258
4372
  config
4259
4373
  });
4260
4374
  if (!result.synced && result.reason !== "project-sync-disabled") {
4375
+ const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
4261
4376
  appendRunLogEntry(projectRoot, run.runId, {
4262
4377
  id: `log:${run.runId}:github-project-sync:${status}`,
4263
4378
  title: "GitHub Project sync skipped",
4264
- detail: `Project status sync for ${run.taskId} could not run: ${result.reason}.`,
4379
+ detail,
4265
4380
  tone: "warn",
4266
4381
  status: "running",
4267
4382
  createdAt: new Date().toISOString(),
4268
4383
  payload: { reason: result.reason, issueNodeId }
4269
4384
  });
4385
+ if (githubProjectsEnabled(config)) {
4386
+ throw new Error(detail);
4387
+ }
4388
+ return false;
4270
4389
  }
4390
+ return result.synced === true;
4271
4391
  } catch (error) {
4392
+ const detail = error instanceof Error ? error.message : String(error);
4272
4393
  appendRunLogEntry(projectRoot, run.runId, {
4273
4394
  id: `log:${run.runId}:github-project-sync-error:${status}`,
4274
4395
  title: "GitHub Project sync failed",
4275
- detail: error instanceof Error ? error.message : String(error),
4396
+ detail,
4276
4397
  tone: "error",
4277
4398
  status: "running",
4278
4399
  createdAt: new Date().toISOString(),
4279
4400
  payload: { issueNodeId }
4280
4401
  });
4402
+ if (githubProjectsEnabled(config)) {
4403
+ throw new Error(detail);
4404
+ }
4405
+ return false;
4281
4406
  }
4282
4407
  }
4408
+ function createCommandRunner(binary, extraEnv = {}) {
4409
+ return async (args, options) => {
4410
+ const child = spawn3(binary, [...args], {
4411
+ cwd: options?.cwd,
4412
+ env: { ...process.env, ...extraEnv },
4413
+ stdio: ["ignore", "pipe", "pipe"]
4414
+ });
4415
+ const stdoutChunks = [];
4416
+ const stderrChunks = [];
4417
+ child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
4418
+ child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
4419
+ const exitCode = await new Promise((resolve15) => {
4420
+ child.once("error", () => resolve15(1));
4421
+ child.once("close", (code) => resolve15(code ?? 1));
4422
+ });
4423
+ return {
4424
+ exitCode,
4425
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
4426
+ stderr: Buffer.concat(stderrChunks).toString("utf8")
4427
+ };
4428
+ };
4429
+ }
4430
+ function closeoutRecord(run) {
4431
+ const value = run.serverCloseout;
4432
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
4433
+ }
4434
+ function closeoutPhasePatch(phase, status, extra = {}) {
4435
+ const updatedAt = new Date().toISOString();
4436
+ return {
4437
+ serverCloseout: {
4438
+ phase,
4439
+ status,
4440
+ updatedAt,
4441
+ ...extra
4442
+ }
4443
+ };
4444
+ }
4445
+ function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
4446
+ appendRunLogEntryAndBroadcast(state, runId, {
4447
+ id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
4448
+ title: `Server closeout: ${phase}`,
4449
+ detail,
4450
+ tone,
4451
+ status,
4452
+ createdAt: new Date().toISOString()
4453
+ }, `server-closeout-${phase}`);
4454
+ }
4283
4455
  async function autoAssignRunIssue(projectRoot, run) {
4284
4456
  if (!run.taskId)
4285
4457
  return;
@@ -4309,7 +4481,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
4309
4481
  return;
4310
4482
  }
4311
4483
  const config = await loadRigLifecycleConfig(projectRoot);
4312
- await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
4484
+ const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
4313
4485
  if (status === "in_progress") {
4314
4486
  await autoAssignRunIssue(projectRoot, run);
4315
4487
  }
@@ -4325,24 +4497,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
4325
4497
  });
4326
4498
  return;
4327
4499
  }
4328
- const result = await updateConfiguredTaskSourceTask(projectRoot, {
4329
- taskId: run.taskId,
4330
- sourceTask: runSourceTaskIdentity(run),
4331
- update: {
4332
- status,
4333
- comment: buildTaskRunLifecycleComment({
4334
- runId: run.runId,
4500
+ const sourceTask = runSourceTaskIdentity(run);
4501
+ const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
4502
+ const rollbackProjectSync = async () => {
4503
+ if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
4504
+ return;
4505
+ await syncGitHubProjectStatusForTaskUpdate({
4506
+ taskId: run.taskId,
4507
+ status: previousStatus,
4508
+ issueNodeId: extractGitHubIssueNodeId(sourceTask),
4509
+ token: createGitHubAuthStore(projectRoot).readToken(),
4510
+ config
4511
+ }).catch((rollbackError) => {
4512
+ appendRunLogEntry(projectRoot, run.runId, {
4513
+ id: `log:${run.runId}:github-project-sync-rollback:${status}`,
4514
+ title: "GitHub Project sync rollback failed",
4515
+ detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
4516
+ tone: "error",
4517
+ status: "running",
4518
+ createdAt: new Date().toISOString()
4519
+ });
4520
+ });
4521
+ };
4522
+ let result;
4523
+ try {
4524
+ result = await updateConfiguredTaskSourceTask(projectRoot, {
4525
+ taskId: run.taskId,
4526
+ sourceTask,
4527
+ update: {
4335
4528
  status,
4336
- summary,
4337
- runtimeWorkspace: normalizeString(run.worktreePath),
4338
- logsDir: normalizeString(run.logRoot),
4339
- sessionDir: normalizeString(run.sessionPath),
4340
- errorText: options.errorText ?? normalizeString(run.errorText)
4341
- })
4342
- }
4343
- });
4529
+ comment: buildTaskRunLifecycleComment({
4530
+ runId: run.runId,
4531
+ status,
4532
+ summary,
4533
+ runtimeWorkspace: normalizeString(run.worktreePath),
4534
+ logsDir: normalizeString(run.logRoot),
4535
+ sessionDir: normalizeString(run.sessionPath),
4536
+ errorText: options.errorText ?? normalizeString(run.errorText)
4537
+ })
4538
+ }
4539
+ });
4540
+ } catch (error) {
4541
+ await rollbackProjectSync();
4542
+ throw error;
4543
+ }
4344
4544
  if (!result.updated) {
4345
4545
  if (result.source === "plugin" || result.sourceKind) {
4546
+ await rollbackProjectSync();
4346
4547
  throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
4347
4548
  }
4348
4549
  appendRunLogEntry(projectRoot, run.runId, {
@@ -4355,6 +4556,219 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
4355
4556
  });
4356
4557
  }
4357
4558
  }
4559
+ async function runServerOwnedPrCloseout(state, runId) {
4560
+ const run = readAuthorityRun4(state.projectRoot, runId);
4561
+ if (!run)
4562
+ throw new Error(`Run not found: ${runId}`);
4563
+ const closeout = closeoutRecord(run);
4564
+ if (!closeout)
4565
+ return;
4566
+ const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
4567
+ if (!taskId)
4568
+ throw new Error("Server-owned closeout requires a task id.");
4569
+ const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
4570
+ const branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
4571
+ const config = await loadRigLifecycleConfig(state.projectRoot);
4572
+ const runPrMode = normalizeString(run.prMode);
4573
+ const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
4574
+ const effectiveConfig = {
4575
+ ...config ?? {},
4576
+ pr: {
4577
+ ...config?.pr ?? {},
4578
+ mode: prMode,
4579
+ autoFixChecks: false,
4580
+ autoFixReview: false
4581
+ }
4582
+ };
4583
+ const readCurrentRun = () => readAuthorityRun4(state.projectRoot, runId) ?? run;
4584
+ const sourceTask = runSourceTaskIdentity(run);
4585
+ const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
4586
+ const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
4587
+ const closeoutPrUrl = normalizeString(closeout.prUrl);
4588
+ if (closeoutPhase === "completed" || closeoutStatus === "completed") {
4589
+ return;
4590
+ }
4591
+ if (closeoutPhase === "close-source" && closeoutPrUrl) {
4592
+ patchRunRecord(state.projectRoot, runId, {
4593
+ status: "reviewing",
4594
+ ...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
4595
+ });
4596
+ await closeIssueAfterMergedPr({
4597
+ projectRoot: state.projectRoot,
4598
+ taskId,
4599
+ runId,
4600
+ prUrl: closeoutPrUrl,
4601
+ sourceTask,
4602
+ updateTaskSource: async (projectRoot, input) => {
4603
+ await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
4604
+ return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
4605
+ }
4606
+ });
4607
+ const completedAt = new Date().toISOString();
4608
+ patchRunRecord(state.projectRoot, runId, {
4609
+ status: "completed",
4610
+ completedAt,
4611
+ errorText: null,
4612
+ ...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
4613
+ });
4614
+ appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
4615
+ emitRigEvent(state, {
4616
+ type: "rig.run.completed",
4617
+ aggregateId: runId,
4618
+ payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
4619
+ createdAt: completedAt
4620
+ });
4621
+ return;
4622
+ }
4623
+ if (prMode === "off" || prMode === "ask") {
4624
+ const completedAt = new Date().toISOString();
4625
+ patchRunRecord(state.projectRoot, runId, {
4626
+ status: "completed",
4627
+ completedAt,
4628
+ errorText: null,
4629
+ ...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
4630
+ });
4631
+ appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
4632
+ emitRigEvent(state, {
4633
+ type: "rig.run.completed",
4634
+ aggregateId: runId,
4635
+ payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
4636
+ createdAt: completedAt
4637
+ });
4638
+ return;
4639
+ }
4640
+ const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
4641
+ const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
4642
+ const gitCommand = createCommandRunner("git", githubEnv);
4643
+ const ghCommand = createCommandRunner("gh", githubEnv);
4644
+ const setCloseout = (phase, status, extra = {}) => {
4645
+ const previous = closeoutRecord(readCurrentRun()) ?? closeout;
4646
+ patchRunRecord(state.projectRoot, runId, {
4647
+ status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
4648
+ ...closeoutPhasePatch(phase, status, { ...previous, ...extra })
4649
+ });
4650
+ };
4651
+ setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
4652
+ appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
4653
+ const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
4654
+ appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
4655
+ setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
4656
+ const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
4657
+ if (push.exitCode !== 0) {
4658
+ throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
4659
+ }
4660
+ const sourceTaskForPr = {
4661
+ title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
4662
+ };
4663
+ const artifactRoot = resolve14(state.projectRoot, "artifacts", taskId);
4664
+ setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4665
+ const pr = await runPrAutomation({
4666
+ projectRoot: workspace,
4667
+ taskId,
4668
+ runId,
4669
+ branch,
4670
+ config: effectiveConfig,
4671
+ sourceTask: sourceTaskForPr,
4672
+ artifactRoot,
4673
+ command: ghCommand,
4674
+ gitCommand,
4675
+ steerPi: async (message) => {
4676
+ appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
4677
+ appendRunTimelineEntry(state.projectRoot, runId, {
4678
+ id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
4679
+ type: "user_message",
4680
+ text: message,
4681
+ createdAt: new Date().toISOString(),
4682
+ state: "completed"
4683
+ });
4684
+ },
4685
+ lifecycle: {
4686
+ onPrOpened: async ({ prUrl }) => {
4687
+ setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4688
+ appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
4689
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
4690
+ },
4691
+ onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
4692
+ onFeedback: async ({ feedback }) => {
4693
+ appendCloseoutStage(state, runId, "feedback", feedback.join(`
4694
+ `), "reviewing", "error");
4695
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
4696
+ },
4697
+ onMergeStarted: async ({ prUrl }) => {
4698
+ setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4699
+ appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
4700
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
4701
+ },
4702
+ onMerged: ({ prUrl }) => {
4703
+ setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
4704
+ appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
4705
+ }
4706
+ }
4707
+ });
4708
+ if (pr.status === "merged" && pr.prUrl) {
4709
+ setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
4710
+ await closeIssueAfterMergedPr({
4711
+ projectRoot: state.projectRoot,
4712
+ taskId,
4713
+ runId,
4714
+ prUrl: pr.prUrl,
4715
+ sourceTask,
4716
+ updateTaskSource: async (projectRoot, input) => {
4717
+ await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
4718
+ return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
4719
+ }
4720
+ });
4721
+ const completedAt = new Date().toISOString();
4722
+ patchRunRecord(state.projectRoot, runId, {
4723
+ status: "completed",
4724
+ completedAt,
4725
+ errorText: null,
4726
+ ...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
4727
+ });
4728
+ appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
4729
+ emitRigEvent(state, {
4730
+ type: "rig.run.completed",
4731
+ aggregateId: runId,
4732
+ payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
4733
+ createdAt: completedAt
4734
+ });
4735
+ return;
4736
+ }
4737
+ if (pr.status === "opened" && pr.prUrl) {
4738
+ const completedAt = new Date().toISOString();
4739
+ patchRunRecord(state.projectRoot, runId, {
4740
+ status: "completed",
4741
+ completedAt,
4742
+ errorText: null,
4743
+ ...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
4744
+ });
4745
+ appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
4746
+ emitRigEvent(state, {
4747
+ type: "rig.run.completed",
4748
+ aggregateId: runId,
4749
+ payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
4750
+ createdAt: completedAt
4751
+ });
4752
+ return;
4753
+ }
4754
+ const detail = pr.actionableFeedback.join(`
4755
+ `) || "PR automation did not merge the PR.";
4756
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
4757
+ appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
4758
+ });
4759
+ patchRunRecord(state.projectRoot, runId, {
4760
+ status: "needs_attention",
4761
+ completedAt: new Date().toISOString(),
4762
+ errorText: detail,
4763
+ ...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
4764
+ });
4765
+ appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
4766
+ emitRigEvent(state, {
4767
+ type: "rig.run.needs-attention",
4768
+ aggregateId: runId,
4769
+ payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
4770
+ });
4771
+ }
4358
4772
  var TERMINAL_RUN_STATUSES2 = new Set([
4359
4773
  "completed",
4360
4774
  "complete",
@@ -4380,11 +4794,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
4380
4794
  return;
4381
4795
  throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
4382
4796
  }
4797
+ async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
4798
+ const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
4799
+ if (fromReader)
4800
+ return fromReader;
4801
+ const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
4802
+ if (projected)
4803
+ return projected;
4804
+ if (readTasks !== readWorkspaceTasks) {
4805
+ return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
4806
+ }
4807
+ return null;
4808
+ }
4383
4809
  async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
4384
4810
  if ("taskId" in input && input.taskId) {
4385
4811
  assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
4386
4812
  }
4387
- const sourceTask = "taskId" in input && input.taskId ? (await readTasks(projectRoot)).find((task) => task.id === input.taskId) ?? null : null;
4813
+ const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
4388
4814
  const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
4389
4815
  const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
4390
4816
  const runRecord = {
@@ -4456,6 +4882,7 @@ async function startLocalRun(state, runId, options) {
4456
4882
  throw new Error(`Run not found: ${runId}`);
4457
4883
  }
4458
4884
  const startedAt = new Date().toISOString();
4885
+ const resumeMode = options?.resume === true;
4459
4886
  state.runProcesses.set(runId, {
4460
4887
  runId,
4461
4888
  child: null,
@@ -4472,9 +4899,9 @@ async function startLocalRun(state, runId, options) {
4472
4899
  summary: run.title
4473
4900
  });
4474
4901
  appendRunLogEntry(state.projectRoot, runId, {
4475
- id: `log:${runId}:prepare`,
4476
- title: "Rig task run starting",
4477
- detail: run.taskId ?? run.title,
4902
+ id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
4903
+ title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
4904
+ detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
4478
4905
  tone: "info",
4479
4906
  status: "preparing",
4480
4907
  createdAt: startedAt
@@ -4550,9 +4977,18 @@ async function startLocalRun(state, runId, options) {
4550
4977
  RIG_HOST_PROJECT_ROOT: cliProjectRoot,
4551
4978
  RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
4552
4979
  RIG_SERVER_INTERNAL_EXEC: "1",
4980
+ RIG_SERVER_OWNS_CLOSEOUT: "1",
4553
4981
  ...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
4554
4982
  ...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
4555
- ...bridgeGitHubToken ? { RIG_GITHUB_TOKEN: bridgeGitHubToken } : {}
4983
+ ...bridgeGitHubToken ? {
4984
+ RIG_GITHUB_TOKEN: bridgeGitHubToken,
4985
+ GITHUB_TOKEN: bridgeGitHubToken,
4986
+ GH_TOKEN: bridgeGitHubToken
4987
+ } : {},
4988
+ ...resumeMode ? {
4989
+ RIG_RUN_RESUME: "1",
4990
+ RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
4991
+ } : {}
4556
4992
  },
4557
4993
  stdio: ["ignore", "pipe", "pipe"]
4558
4994
  });
@@ -4638,6 +5074,38 @@ ${sourceFailure}` });
4638
5074
  agent: current.runtimeAdapter,
4639
5075
  summary: failureSummary
4640
5076
  });
5077
+ } else if (closeoutRecord(current)?.status === "pending") {
5078
+ try {
5079
+ await runServerOwnedPrCloseout(state, runId);
5080
+ } catch (closeoutError) {
5081
+ const closeoutFailure = closeoutError instanceof Error ? closeoutError.message : String(closeoutError);
5082
+ patchRunRecord(state.projectRoot, runId, {
5083
+ status: "failed",
5084
+ completedAt: new Date().toISOString(),
5085
+ errorText: closeoutFailure,
5086
+ ...closeoutPhasePatch("failed", "failed", { error: closeoutFailure })
5087
+ });
5088
+ appendRunLogEntryAndBroadcast(state, runId, {
5089
+ id: `log:${runId}:server-closeout-failed`,
5090
+ title: "Server-owned closeout failed",
5091
+ detail: closeoutFailure,
5092
+ tone: "error",
5093
+ status: "failed",
5094
+ createdAt: new Date().toISOString()
5095
+ }, "server-closeout-failed");
5096
+ if (current.taskId) {
5097
+ await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: closeoutFailure }, "failed", "Rig server-owned closeout failed.", { errorText: closeoutFailure }).catch((error) => {
5098
+ appendRunLogEntry(state.projectRoot, runId, {
5099
+ id: `log:${runId}:task-source-closeout-failed-update`,
5100
+ title: "Task source closeout failure update failed",
5101
+ detail: error instanceof Error ? error.message : String(error),
5102
+ tone: "error",
5103
+ status: "failed",
5104
+ createdAt: new Date().toISOString()
5105
+ });
5106
+ });
5107
+ }
5108
+ }
4641
5109
  }
4642
5110
  broadcastSnapshotInvalidation(state);
4643
5111
  } catch (error) {
@@ -4703,7 +5171,7 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
4703
5171
  }
4704
5172
  try {
4705
5173
  const monorepoRoot = resolveMonorepoRoot3(projectRoot);
4706
- const outerProjectRoot = dirname8(dirname8(monorepoRoot));
5174
+ const outerProjectRoot = dirname9(dirname9(monorepoRoot));
4707
5175
  if (existsSync7(resolve14(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
4708
5176
  return outerProjectRoot;
4709
5177
  }
@@ -4724,7 +5192,13 @@ async function resumeRunRecord(state, input) {
4724
5192
  if (run.status === "completed") {
4725
5193
  throw new Error("Completed runs cannot be resumed.");
4726
5194
  }
4727
- await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
5195
+ const closeout = closeoutRecord(run);
5196
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
5197
+ if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
5198
+ await runServerOwnedPrCloseout(state, input.runId);
5199
+ return;
5200
+ }
5201
+ await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
4728
5202
  }
4729
5203
  function appendRunMessage(projectRoot, input) {
4730
5204
  const run = readAuthorityRun4(projectRoot, input.runId);
@@ -4808,34 +5282,46 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
4808
5282
  writeQueueState(projectRoot, next);
4809
5283
  return next;
4810
5284
  }
4811
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
4812
- function reconcileOrphanedLocalRuns(state, runs, nowIso2) {
4813
- let changed = false;
4814
- for (const run of runs) {
4815
- const status = normalizeString(run.status)?.toLowerCase() ?? "";
4816
- const serverPid = run.serverPid;
4817
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
4818
- if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
4819
- continue;
4820
- }
4821
- const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
4822
- patchRunRecord(state.projectRoot, run.runId, {
4823
- status: "failed",
4824
- completedAt: run.completedAt ?? nowIso2,
4825
- updatedAt: nowIso2,
4826
- errorText: detail
4827
- });
4828
- appendRunLogEntry(state.projectRoot, run.runId, {
4829
- id: `log:${run.runId}:stale-local-run`,
4830
- title: "Run marked stale after server restart",
4831
- detail,
4832
- tone: "error",
4833
- status: "failed",
4834
- createdAt: nowIso2
4835
- });
4836
- changed = true;
5285
+ var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
5286
+ var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
5287
+ function processExists(pid) {
5288
+ if (!Number.isInteger(pid) || pid <= 0)
5289
+ return false;
5290
+ try {
5291
+ process.kill(pid, 0);
5292
+ return true;
5293
+ } catch {
5294
+ return false;
4837
5295
  }
4838
- return changed;
5296
+ }
5297
+ function recoverStaleLocalRun(projectRoot, run) {
5298
+ const record = run;
5299
+ if (run.mode !== "local")
5300
+ return false;
5301
+ const status = normalizeString(record.status)?.toLowerCase() ?? "";
5302
+ if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
5303
+ return false;
5304
+ const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
5305
+ const childPid = typeof record.pid === "number" ? record.pid : null;
5306
+ if (serverPid === null && childPid === null)
5307
+ return false;
5308
+ const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
5309
+ if (hasLiveRecordedProcess && serverPid === process.pid)
5310
+ return false;
5311
+ const completedAt = new Date().toISOString();
5312
+ patchRunRecord(projectRoot, run.runId, {
5313
+ status: "failed",
5314
+ completedAt,
5315
+ errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
5316
+ });
5317
+ return true;
5318
+ }
5319
+ function collectResumableServerCloseouts(state, runs) {
5320
+ return runs.filter((run) => {
5321
+ const closeout = closeoutRecord(run);
5322
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
5323
+ return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
5324
+ });
4839
5325
  }
4840
5326
  async function reconcileScheduler(state, reason) {
4841
5327
  if (state.scheduler.reconciling) {
@@ -4850,7 +5336,36 @@ async function reconcileScheduler(state, reason) {
4850
5336
  const queue = readQueueState(state.projectRoot);
4851
5337
  const tasks = await state.snapshotService.getWorkspaceTasks();
4852
5338
  let runs = listAuthorityRuns4(state.projectRoot);
4853
- let changed = reconcileOrphanedLocalRuns(state, runs, new Date().toISOString());
5339
+ let changed = false;
5340
+ for (const run of runs) {
5341
+ if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
5342
+ changed = true;
5343
+ }
5344
+ }
5345
+ if (changed) {
5346
+ runs = listAuthorityRuns4(state.projectRoot);
5347
+ }
5348
+ const resumableCloseouts = collectResumableServerCloseouts(state, runs);
5349
+ for (const run of resumableCloseouts) {
5350
+ appendRunLogEntry(state.projectRoot, run.runId, {
5351
+ id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
5352
+ title: "Server-owned closeout auto-resume scheduled",
5353
+ detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
5354
+ tone: "info",
5355
+ status: "reviewing",
5356
+ createdAt: new Date().toISOString()
5357
+ });
5358
+ await runServerOwnedPrCloseout(state, run.runId).catch((error) => {
5359
+ const detail = error instanceof Error ? error.message : String(error);
5360
+ patchRunRecord(state.projectRoot, run.runId, {
5361
+ status: "failed",
5362
+ completedAt: new Date().toISOString(),
5363
+ errorText: detail,
5364
+ ...closeoutPhasePatch("failed", "failed", { error: detail })
5365
+ });
5366
+ });
5367
+ changed = true;
5368
+ }
4854
5369
  if (changed) {
4855
5370
  runs = listAuthorityRuns4(state.projectRoot);
4856
5371
  }
@@ -4926,8 +5441,8 @@ async function reconcileScheduler(state, reason) {
4926
5441
  // packages/server/src/server-helpers/http-router.ts
4927
5442
  import { randomUUID } from "crypto";
4928
5443
  import { spawnSync as spawnSync3 } from "child_process";
4929
- import { basename, dirname as dirname12, isAbsolute as isAbsolute3, resolve as resolve18 } from "path";
4930
- import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync10 } from "fs";
5444
+ import { basename, dirname as dirname15, isAbsolute as isAbsolute4, resolve as resolve20 } from "path";
5445
+ import { copyFileSync as copyFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync12 } from "fs";
4931
5446
  import {
4932
5447
  listAuthorityRuns as listAuthorityRuns5,
4933
5448
  readAuthorityRun as readAuthorityRun6,
@@ -4951,7 +5466,7 @@ import {
4951
5466
  } from "@rig/runtime/control-plane/remote";
4952
5467
 
4953
5468
  // packages/server/src/server-helpers/run-steering.ts
4954
- import { dirname as dirname9, resolve as resolve15 } from "path";
5469
+ import { dirname as dirname10, resolve as resolve15 } from "path";
4955
5470
  import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync5 } from "fs";
4956
5471
  import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun5, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
4957
5472
  var steeringSequence = 0;
@@ -5038,7 +5553,7 @@ function queueRunSteeringMessage(projectRoot, runId, input) {
5038
5553
  delivered: false
5039
5554
  };
5040
5555
  const path = runSteeringPath(projectRoot, runId);
5041
- mkdirSync8(dirname9(path), { recursive: true });
5556
+ mkdirSync8(dirname10(path), { recursive: true });
5042
5557
  appendJsonlRecord2(path, entry);
5043
5558
  appendRunTimelineEntry(projectRoot, runId, {
5044
5559
  id: entry.id,
@@ -5075,6 +5590,187 @@ import {
5075
5590
  updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
5076
5591
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
5077
5592
 
5593
+ // packages/server/src/server-helpers/github-api-session-index.ts
5594
+ import { chmodSync as chmodSync3, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
5595
+ import { dirname as dirname12, resolve as resolve17 } from "path";
5596
+
5597
+ // packages/server/src/server-helpers/github-user-namespace.ts
5598
+ import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync8 } from "fs";
5599
+ import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve16 } from "path";
5600
+ function cleanString3(value) {
5601
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
5602
+ }
5603
+ function sanitizePathSegment(value) {
5604
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
5605
+ }
5606
+ function deriveGitHubUserNamespaceKey(identity) {
5607
+ const userId = cleanString3(identity.userId);
5608
+ if (userId) {
5609
+ const safeId = sanitizePathSegment(userId);
5610
+ if (safeId)
5611
+ return `ghu-${safeId}`;
5612
+ }
5613
+ const login = cleanString3(identity.login);
5614
+ if (login) {
5615
+ const safeLogin = sanitizePathSegment(login);
5616
+ if (safeLogin)
5617
+ return `ghu-login-${safeLogin}`;
5618
+ }
5619
+ throw new Error("GitHub user namespace requires a user id or login");
5620
+ }
5621
+ function resolveRemoteUserNamespacesRoot(projectRoot) {
5622
+ const explicitRoot = cleanString3(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
5623
+ if (explicitRoot)
5624
+ return resolve16(explicitRoot);
5625
+ const stateDir2 = cleanString3(process.env.RIG_STATE_DIR);
5626
+ if (stateDir2)
5627
+ return resolve16(dirname11(resolve16(stateDir2)), "users");
5628
+ return resolve16(projectRoot, ".rig", "users");
5629
+ }
5630
+ function resolveRemoteUserNamespace(projectRoot, identity) {
5631
+ const key = deriveGitHubUserNamespaceKey(identity);
5632
+ const root = resolve16(resolveRemoteUserNamespacesRoot(projectRoot), key);
5633
+ const stateDir2 = resolve16(root, ".rig", "state");
5634
+ return {
5635
+ key,
5636
+ userId: cleanString3(identity.userId),
5637
+ login: cleanString3(identity.login),
5638
+ root,
5639
+ stateDir: stateDir2,
5640
+ authStateFile: resolve16(stateDir2, "github-auth.json"),
5641
+ metadataFile: resolve16(stateDir2, "user-namespace.json"),
5642
+ checkoutBaseDir: resolve16(root, "remote-checkouts"),
5643
+ snapshotBaseDir: resolve16(root, "remote-snapshots")
5644
+ };
5645
+ }
5646
+ function serializeRemoteUserNamespace(namespace) {
5647
+ return {
5648
+ key: namespace.key,
5649
+ userId: namespace.userId,
5650
+ login: namespace.login,
5651
+ root: namespace.root,
5652
+ checkoutBaseDir: namespace.checkoutBaseDir,
5653
+ snapshotBaseDir: namespace.snapshotBaseDir
5654
+ };
5655
+ }
5656
+ function isPathInsideNamespace(namespaceRoot, candidatePath) {
5657
+ const root = resolve16(namespaceRoot);
5658
+ const candidate = resolve16(candidatePath);
5659
+ const rel = relative3(root, candidate);
5660
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
5661
+ }
5662
+ function writeRemoteUserNamespaceMetadata(namespace) {
5663
+ mkdirSync9(namespace.stateDir, { recursive: true });
5664
+ const previous = (() => {
5665
+ if (!existsSync9(namespace.metadataFile))
5666
+ return null;
5667
+ try {
5668
+ const parsed = JSON.parse(readFileSync6(namespace.metadataFile, "utf8"));
5669
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
5670
+ } catch {
5671
+ return null;
5672
+ }
5673
+ })();
5674
+ const now = new Date().toISOString();
5675
+ writeFileSync8(namespace.metadataFile, `${JSON.stringify({
5676
+ key: namespace.key,
5677
+ userId: namespace.userId,
5678
+ login: namespace.login,
5679
+ root: namespace.root,
5680
+ checkoutBaseDir: namespace.checkoutBaseDir,
5681
+ snapshotBaseDir: namespace.snapshotBaseDir,
5682
+ createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
5683
+ updatedAt: now
5684
+ }, null, 2)}
5685
+ `, { encoding: "utf8", mode: 384 });
5686
+ try {
5687
+ chmodSync2(namespace.metadataFile, 384);
5688
+ } catch {}
5689
+ }
5690
+
5691
+ // packages/server/src/server-helpers/github-api-session-index.ts
5692
+ function cleanString4(value) {
5693
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
5694
+ }
5695
+ function resolveGitHubApiSessionIndexFile(projectRoot) {
5696
+ return resolve17(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
5697
+ }
5698
+ function parseEntry(value) {
5699
+ if (!value || typeof value !== "object" || Array.isArray(value))
5700
+ return null;
5701
+ const record = value;
5702
+ const token = cleanString4(record.token);
5703
+ const namespaceKey = cleanString4(record.namespaceKey);
5704
+ const namespaceRoot = cleanString4(record.namespaceRoot);
5705
+ const authStateFile = cleanString4(record.authStateFile);
5706
+ const checkoutBaseDir = cleanString4(record.checkoutBaseDir);
5707
+ const snapshotBaseDir = cleanString4(record.snapshotBaseDir);
5708
+ const createdAt = cleanString4(record.createdAt);
5709
+ if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
5710
+ return null;
5711
+ return {
5712
+ token,
5713
+ namespaceKey,
5714
+ namespaceRoot,
5715
+ authStateFile,
5716
+ checkoutBaseDir,
5717
+ snapshotBaseDir,
5718
+ createdAt,
5719
+ login: cleanString4(record.login),
5720
+ userId: cleanString4(record.userId),
5721
+ selectedRepo: cleanString4(record.selectedRepo)
5722
+ };
5723
+ }
5724
+ function readIndex(indexFile) {
5725
+ if (!existsSync10(indexFile))
5726
+ return [];
5727
+ try {
5728
+ const parsed = JSON.parse(readFileSync7(indexFile, "utf8"));
5729
+ return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
5730
+ const parsedEntry = parseEntry(entry);
5731
+ return parsedEntry ? [parsedEntry] : [];
5732
+ }) : [];
5733
+ } catch {
5734
+ return [];
5735
+ }
5736
+ }
5737
+ function writeIndex(indexFile, sessions) {
5738
+ mkdirSync10(dirname12(indexFile), { recursive: true });
5739
+ writeFileSync9(indexFile, `${JSON.stringify({ sessions }, null, 2)}
5740
+ `, { encoding: "utf8", mode: 384 });
5741
+ try {
5742
+ chmodSync3(indexFile, 384);
5743
+ } catch {}
5744
+ }
5745
+ function registerGitHubApiSession(input) {
5746
+ const cleanToken = cleanString4(input.token);
5747
+ if (!cleanToken)
5748
+ throw new Error("GitHub API session token is required");
5749
+ const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
5750
+ const createdAt = new Date().toISOString();
5751
+ const entry = {
5752
+ token: cleanToken,
5753
+ login: input.namespace.login,
5754
+ userId: input.namespace.userId,
5755
+ namespaceKey: input.namespace.key,
5756
+ namespaceRoot: input.namespace.root,
5757
+ authStateFile: input.namespace.authStateFile,
5758
+ checkoutBaseDir: input.namespace.checkoutBaseDir,
5759
+ snapshotBaseDir: input.namespace.snapshotBaseDir,
5760
+ selectedRepo: cleanString4(input.selectedRepo),
5761
+ createdAt
5762
+ };
5763
+ const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
5764
+ writeIndex(indexFile, [...previous.slice(-199), entry]);
5765
+ return entry;
5766
+ }
5767
+ function readGitHubApiSession(input) {
5768
+ const cleanToken = cleanString4(input.token);
5769
+ if (!cleanToken)
5770
+ return null;
5771
+ return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
5772
+ }
5773
+
5078
5774
  // packages/server/src/server-helpers/inspector-agent-lifecycle.ts
5079
5775
  function createInspectorAgentLifecycleController(options) {
5080
5776
  const initialDelayMs = Math.max(1, options.initialDelayMs ?? 5000);
@@ -5178,21 +5874,21 @@ function inspectorAgentLifecycleSnapshot(input) {
5178
5874
  // packages/server/src/server-helpers/project-registry.ts
5179
5875
  import { createHash as createHash2 } from "crypto";
5180
5876
  import { spawnSync as spawnSync2 } from "child_process";
5181
- import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync6, readdirSync as readdirSync3, writeFileSync as writeFileSync8 } from "fs";
5182
- import { dirname as dirname10, resolve as resolve16 } from "path";
5877
+ import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync8, readdirSync as readdirSync3, writeFileSync as writeFileSync10 } from "fs";
5878
+ import { dirname as dirname13, resolve as resolve18 } from "path";
5183
5879
  function normalizeRepoSlug(value) {
5184
5880
  const trimmed = value.trim();
5185
5881
  return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
5186
5882
  }
5187
5883
  function registryPath(projectRoot) {
5188
- return resolve16(projectRoot, ".rig", "state", "projects.json");
5884
+ return resolve18(projectRoot, ".rig", "state", "projects.json");
5189
5885
  }
5190
5886
  function readRegistry(projectRoot) {
5191
5887
  const path = registryPath(projectRoot);
5192
- if (!existsSync9(path))
5888
+ if (!existsSync11(path))
5193
5889
  return {};
5194
5890
  try {
5195
- const payload = JSON.parse(readFileSync6(path, "utf8"));
5891
+ const payload = JSON.parse(readFileSync8(path, "utf8"));
5196
5892
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
5197
5893
  return {};
5198
5894
  const projects = payload.projects;
@@ -5203,14 +5899,14 @@ function readRegistry(projectRoot) {
5203
5899
  }
5204
5900
  function writeRegistry(projectRoot, projects) {
5205
5901
  const path = registryPath(projectRoot);
5206
- mkdirSync9(dirname10(path), { recursive: true });
5207
- writeFileSync8(path, `${JSON.stringify({ projects }, null, 2)}
5902
+ mkdirSync11(dirname13(path), { recursive: true });
5903
+ writeFileSync10(path, `${JSON.stringify({ projects }, null, 2)}
5208
5904
  `, "utf8");
5209
5905
  }
5210
5906
  function resolveConfigPath(projectRoot) {
5211
5907
  for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
5212
- const path = resolve16(projectRoot, name);
5213
- if (existsSync9(path))
5908
+ const path = resolve18(projectRoot, name);
5909
+ if (existsSync11(path))
5214
5910
  return path;
5215
5911
  }
5216
5912
  return null;
@@ -5219,7 +5915,7 @@ function hashFile(path) {
5219
5915
  if (!path)
5220
5916
  return null;
5221
5917
  try {
5222
- return createHash2("sha256").update(readFileSync6(path)).digest("hex");
5918
+ return createHash2("sha256").update(readFileSync8(path)).digest("hex");
5223
5919
  } catch {
5224
5920
  return null;
5225
5921
  }
@@ -5235,11 +5931,11 @@ function readDefaultBranch(projectRoot) {
5235
5931
  return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
5236
5932
  }
5237
5933
  function buildRunSummary(projectRoot) {
5238
- const runsDir = resolve16(projectRoot, ".rig", "runs");
5934
+ const runsDir = resolve18(projectRoot, ".rig", "runs");
5239
5935
  try {
5240
5936
  const runs = readdirSync3(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
5241
5937
  try {
5242
- const run = JSON.parse(readFileSync6(resolve16(runsDir, entry.name, "run.json"), "utf8"));
5938
+ const run = JSON.parse(readFileSync8(resolve18(runsDir, entry.name, "run.json"), "utf8"));
5243
5939
  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 : "" }];
5244
5940
  } catch {
5245
5941
  return [];
@@ -5291,10 +5987,14 @@ function upsertProjectRecord(projectRoot, input) {
5291
5987
  function linkProjectCheckout(projectRoot, repoSlug, checkout) {
5292
5988
  return upsertProjectRecord(projectRoot, { repoSlug, checkout });
5293
5989
  }
5990
+ function projectRegistryContainsCheckout(projectRoot, checkoutPath) {
5991
+ const target = resolve18(checkoutPath);
5992
+ return Object.values(readRegistry(projectRoot)).some((project) => project.checkouts.some((checkout) => checkout.path ? resolve18(checkout.path) === target : false));
5993
+ }
5294
5994
 
5295
5995
  // packages/server/src/server-helpers/remote-checkout.ts
5296
- import { existsSync as existsSync10, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
5297
- import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve17 } from "path";
5996
+ import { existsSync as existsSync12, mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
5997
+ import { dirname as dirname14, isAbsolute as isAbsolute3, relative as relative4, resolve as resolve19 } from "path";
5298
5998
  function safeSlugSegments(repoSlug) {
5299
5999
  const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
5300
6000
  if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
@@ -5311,7 +6011,7 @@ function safeCheckoutKey(value) {
5311
6011
  }
5312
6012
  function repoSlugPath(baseDir, repoSlug, checkoutKey) {
5313
6013
  const key = safeCheckoutKey(checkoutKey);
5314
- return resolve17(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
6014
+ return resolve19(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
5315
6015
  }
5316
6016
  function sanitizeSnapshotId(value, fallback) {
5317
6017
  const raw = (value ?? fallback).trim();
@@ -5319,7 +6019,7 @@ function sanitizeSnapshotId(value, fallback) {
5319
6019
  return safe || fallback;
5320
6020
  }
5321
6021
  function assertWithinRoot(root, relativePath) {
5322
- if (!relativePath || isAbsolute2(relativePath) || relativePath.includes("\x00")) {
6022
+ if (!relativePath || isAbsolute3(relativePath) || relativePath.includes("\x00")) {
5323
6023
  throw new Error(`Invalid snapshot file path: ${relativePath}`);
5324
6024
  }
5325
6025
  const normalizedRelative = relativePath.replace(/\\/g, "/");
@@ -5327,9 +6027,9 @@ function assertWithinRoot(root, relativePath) {
5327
6027
  if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
5328
6028
  throw new Error(`Unsafe snapshot file path: ${relativePath}`);
5329
6029
  }
5330
- const target = resolve17(root, ...segments);
5331
- const rel = relative3(root, target);
5332
- if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute2(rel)) {
6030
+ const target = resolve19(root, ...segments);
6031
+ const rel = relative4(root, target);
6032
+ if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute3(rel)) {
5333
6033
  throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
5334
6034
  }
5335
6035
  return target;
@@ -5347,17 +6047,17 @@ function parseSnapshotArchiveContentBase64(contentBase64) {
5347
6047
  function extractUploadedSnapshotArchive(input) {
5348
6048
  const archive = decodeSnapshotArchive(input.archive);
5349
6049
  const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
5350
- const checkoutPath = resolve17(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
5351
- mkdirSync10(checkoutPath, { recursive: true });
6050
+ const checkoutPath = resolve19(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
6051
+ mkdirSync12(checkoutPath, { recursive: true });
5352
6052
  for (const file of archive.files) {
5353
6053
  if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
5354
6054
  throw new Error("Invalid snapshot archive file entry");
5355
6055
  }
5356
6056
  const target = assertWithinRoot(checkoutPath, file.path);
5357
- mkdirSync10(dirname11(target), { recursive: true });
5358
- writeFileSync9(target, Buffer.from(file.contentBase64, "base64"));
6057
+ mkdirSync12(dirname14(target), { recursive: true });
6058
+ writeFileSync11(target, Buffer.from(file.contentBase64, "base64"));
5359
6059
  }
5360
- writeFileSync9(resolve17(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
6060
+ writeFileSync11(resolve19(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
5361
6061
  repoSlug: input.repoSlug,
5362
6062
  snapshotId,
5363
6063
  fileCount: archive.files.length,
@@ -5392,7 +6092,7 @@ function gitCredentialConfig(token) {
5392
6092
  };
5393
6093
  }
5394
6094
  async function prepareRemoteCheckout(input) {
5395
- const exists = input.exists ?? existsSync10;
6095
+ const exists = input.exists ?? existsSync12;
5396
6096
  const strategy = input.strategy;
5397
6097
  if (strategy.kind === "uploaded-snapshot") {
5398
6098
  return extractUploadedSnapshotArchive({
@@ -5404,7 +6104,7 @@ async function prepareRemoteCheckout(input) {
5404
6104
  });
5405
6105
  }
5406
6106
  if (strategy.kind === "existing-path") {
5407
- const checkoutPath2 = resolve17(strategy.path);
6107
+ const checkoutPath2 = resolve19(strategy.path);
5408
6108
  if (!exists(checkoutPath2)) {
5409
6109
  throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
5410
6110
  }
@@ -5440,9 +6140,9 @@ function buildServerControlStatus() {
5440
6140
  };
5441
6141
  }
5442
6142
  function buildProjectConfigStatus(root) {
5443
- const hasConfigTs = existsSync11(resolve18(root, "rig.config.ts"));
5444
- const hasConfigJson = existsSync11(resolve18(root, "rig.config.json"));
5445
- const hasLegacyTaskConfig = existsSync11(resolve18(root, ".rig", "task-config.json"));
6143
+ const hasConfigTs = existsSync13(resolve20(root, "rig.config.ts"));
6144
+ const hasConfigJson = existsSync13(resolve20(root, "rig.config.json"));
6145
+ const hasLegacyTaskConfig = existsSync13(resolve20(root, ".rig", "task-config.json"));
5446
6146
  let kind = "missing";
5447
6147
  if (hasConfigTs)
5448
6148
  kind = "rig-config-ts";
@@ -5459,6 +6159,75 @@ function buildProjectConfigStatus(root) {
5459
6159
  suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
5460
6160
  };
5461
6161
  }
6162
+ var RIG_GITHUB_LIFECYCLE_LABELS = [
6163
+ "ready",
6164
+ "blocked",
6165
+ "in-progress",
6166
+ "under-review",
6167
+ "failed",
6168
+ "cancelled",
6169
+ "rig:running",
6170
+ "rig:pr-open",
6171
+ "rig:ci-fixing",
6172
+ "rig:merging",
6173
+ "rig:done",
6174
+ "rig:needs-attention"
6175
+ ];
6176
+ function githubProjectsEnabled2(config) {
6177
+ if (!config || typeof config !== "object" || Array.isArray(config))
6178
+ return false;
6179
+ const root = config;
6180
+ const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
6181
+ const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
6182
+ return projects?.enabled === true;
6183
+ }
6184
+ function githubIssueSourceRepo(config) {
6185
+ if (!config || typeof config !== "object" || Array.isArray(config))
6186
+ return null;
6187
+ const root = config;
6188
+ const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
6189
+ const owner = normalizeString(taskSource?.owner);
6190
+ const repo = normalizeString(taskSource?.repo);
6191
+ if (taskSource?.kind === "github-issues" && owner && repo)
6192
+ return { owner, repo };
6193
+ const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
6194
+ const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
6195
+ const match = slug?.match(/^([^/]+)\/([^/]+)$/);
6196
+ return match ? { owner: match[1], repo: match[2] } : null;
6197
+ }
6198
+ async function ensureGitHubLifecycleLabels(projectRoot, config) {
6199
+ const repo = githubIssueSourceRepo(config);
6200
+ if (!repo)
6201
+ return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
6202
+ const token = createGitHubAuthStore(projectRoot).readToken();
6203
+ if (!token)
6204
+ return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
6205
+ const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
6206
+ headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
6207
+ });
6208
+ const existingJson = await existingResponse.json().catch(() => []);
6209
+ const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
6210
+ const created = [];
6211
+ const alreadyPresent = [];
6212
+ const failed = [];
6213
+ for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
6214
+ if (existing.has(label)) {
6215
+ alreadyPresent.push(label);
6216
+ continue;
6217
+ }
6218
+ const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
6219
+ method: "POST",
6220
+ headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
6221
+ 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" })
6222
+ });
6223
+ if (response.ok || response.status === 422) {
6224
+ (response.status === 422 ? alreadyPresent : created).push(label);
6225
+ } else {
6226
+ failed.push({ label, error: await response.text().catch(() => response.statusText) });
6227
+ }
6228
+ }
6229
+ return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
6230
+ }
5462
6231
  function normalizeCommit(value) {
5463
6232
  const raw = normalizeString(value);
5464
6233
  return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
@@ -5478,24 +6247,24 @@ function repoParts(repoSlug) {
5478
6247
  return { owner, repo, slug: `${owner}/${repo}` };
5479
6248
  }
5480
6249
  function repairDir(checkoutPath) {
5481
- const dir = resolve18(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
5482
- mkdirSync11(dir, { recursive: true });
6250
+ const dir = resolve20(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
6251
+ mkdirSync13(dir, { recursive: true });
5483
6252
  return dir;
5484
6253
  }
5485
6254
  function backupCheckoutFile(checkoutPath, relativePath) {
5486
- const source = resolve18(checkoutPath, relativePath);
5487
- const backupPath = resolve18(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
5488
- mkdirSync11(dirname12(backupPath), { recursive: true });
5489
- copyFileSync(source, backupPath);
6255
+ const source = resolve20(checkoutPath, relativePath);
6256
+ const backupPath = resolve20(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
6257
+ mkdirSync13(dirname15(backupPath), { recursive: true });
6258
+ copyFileSync2(source, backupPath);
5490
6259
  return backupPath;
5491
6260
  }
5492
6261
  function parsePackageJsonLosslessly(checkoutPath) {
5493
- const packagePath = resolve18(checkoutPath, "package.json");
5494
- if (!existsSync11(packagePath)) {
6262
+ const packagePath = resolve20(checkoutPath, "package.json");
6263
+ if (!existsSync13(packagePath)) {
5495
6264
  return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
5496
6265
  }
5497
6266
  try {
5498
- const parsed = JSON.parse(readFileSync7(packagePath, "utf8"));
6267
+ const parsed = JSON.parse(readFileSync9(packagePath, "utf8"));
5499
6268
  return {
5500
6269
  existed: true,
5501
6270
  packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
@@ -5509,9 +6278,9 @@ function parsePackageJsonLosslessly(checkoutPath) {
5509
6278
  }
5510
6279
  }
5511
6280
  function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
5512
- const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync11(resolve18(checkoutPath, name)));
5513
- const packagePath = resolve18(checkoutPath, "package.json");
5514
- if (!hasConfig && !existsSync11(packagePath)) {
6281
+ const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
6282
+ const packagePath = resolve20(checkoutPath, "package.json");
6283
+ if (!hasConfig && !existsSync13(packagePath)) {
5515
6284
  return { skipped: true, reason: "package.json and rig.config missing" };
5516
6285
  }
5517
6286
  const parsed = parsePackageJsonLosslessly(checkoutPath);
@@ -5530,7 +6299,7 @@ function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
5530
6299
  }
5531
6300
  const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
5532
6301
  if (changed) {
5533
- writeFileSync10(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
6302
+ writeFileSync12(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
5534
6303
  `, "utf8");
5535
6304
  }
5536
6305
  return {
@@ -5556,11 +6325,11 @@ function configLooksStructurallyUsable(source) {
5556
6325
  return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
5557
6326
  }
5558
6327
  function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
5559
- const configPath = resolve18(checkoutPath, "rig.config.ts");
5560
- const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync11(resolve18(checkoutPath, name)));
6328
+ const configPath = resolve20(checkoutPath, "rig.config.ts");
6329
+ const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
5561
6330
  if (existingConfigName) {
5562
- const existingPath = resolve18(checkoutPath, existingConfigName);
5563
- const source = readFileSync7(existingPath, "utf8");
6331
+ const existingPath = resolve20(checkoutPath, existingConfigName);
6332
+ const source = readFileSync9(existingPath, "utf8");
5564
6333
  if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
5565
6334
  return { path: existingPath, changed: false, reason: "config structurally complete" };
5566
6335
  }
@@ -5574,7 +6343,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
5574
6343
  }
5575
6344
  }
5576
6345
  const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
5577
- writeFileSync10(configPath, generatedRigConfigSource(repoSlug), "utf8");
6346
+ writeFileSync12(configPath, generatedRigConfigSource(repoSlug), "utf8");
5578
6347
  return {
5579
6348
  path: configPath,
5580
6349
  changed: true,
@@ -5583,7 +6352,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
5583
6352
  };
5584
6353
  }
5585
6354
  function validateRemoteCheckoutRigConfig(checkoutPath) {
5586
- const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync11(resolve18(checkoutPath, name)));
6355
+ const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
5587
6356
  if (!configFile)
5588
6357
  return { ok: false, error: "missing rig config" };
5589
6358
  if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
@@ -5605,7 +6374,7 @@ function validateRemoteCheckoutRigConfig(checkoutPath) {
5605
6374
  return { ok: true, configFile };
5606
6375
  }
5607
6376
  function installRemoteCheckoutPackages(checkoutPath) {
5608
- if (!existsSync11(resolve18(checkoutPath, "package.json"))) {
6377
+ if (!existsSync13(resolve20(checkoutPath, "package.json"))) {
5609
6378
  return { skipped: true, reason: "package.json missing" };
5610
6379
  }
5611
6380
  if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
@@ -5618,8 +6387,8 @@ function installRemoteCheckoutPackages(checkoutPath) {
5618
6387
  return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
5619
6388
  }
5620
6389
  function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
5621
- const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync11(resolve18(checkoutPath, name)));
5622
- const hasPackage = existsSync11(resolve18(checkoutPath, "package.json"));
6390
+ const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
6391
+ const hasPackage = existsSync13(resolve20(checkoutPath, "package.json"));
5623
6392
  if (!hasConfig && !hasPackage) {
5624
6393
  return {
5625
6394
  packageJson: { skipped: true, reason: "package.json and rig.config missing" },
@@ -5717,26 +6486,26 @@ function buildRemoteRunLogEntry(body, identifiers) {
5717
6486
  }
5718
6487
  function readGitHeadCommit(projectRoot) {
5719
6488
  try {
5720
- let gitDir = resolve18(projectRoot, ".git");
6489
+ let gitDir = resolve20(projectRoot, ".git");
5721
6490
  try {
5722
- const dotGit = readFileSync7(gitDir, "utf8").trim();
6491
+ const dotGit = readFileSync9(gitDir, "utf8").trim();
5723
6492
  const gitDirPrefix = "gitdir:";
5724
6493
  if (dotGit.startsWith(gitDirPrefix)) {
5725
- gitDir = resolve18(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
6494
+ gitDir = resolve20(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
5726
6495
  }
5727
6496
  } catch {}
5728
- const head = readFileSync7(resolve18(gitDir, "HEAD"), "utf8").trim();
6497
+ const head = readFileSync9(resolve20(gitDir, "HEAD"), "utf8").trim();
5729
6498
  const refPrefix = "ref:";
5730
6499
  if (!head.startsWith(refPrefix)) {
5731
6500
  return normalizeCommit(head);
5732
6501
  }
5733
6502
  const ref = head.slice(refPrefix.length).trim();
5734
- const refPath = resolve18(gitDir, ref);
5735
- if (existsSync11(refPath)) {
5736
- return normalizeCommit(readFileSync7(refPath, "utf8").trim());
6503
+ const refPath = resolve20(gitDir, ref);
6504
+ if (existsSync13(refPath)) {
6505
+ return normalizeCommit(readFileSync9(refPath, "utf8").trim());
5737
6506
  }
5738
- const commonDir = normalizeString(readFileSync7(resolve18(gitDir, "commondir"), "utf8"));
5739
- return commonDir ? normalizeCommit(readFileSync7(resolve18(gitDir, commonDir, ref), "utf8").trim()) : null;
6507
+ const commonDir = normalizeString(readFileSync9(resolve20(gitDir, "commondir"), "utf8"));
6508
+ return commonDir ? normalizeCommit(readFileSync9(resolve20(gitDir, commonDir, ref), "utf8").trim()) : null;
5740
6509
  } catch {
5741
6510
  return null;
5742
6511
  }
@@ -5774,9 +6543,9 @@ function configuredRepoFromTaskSource(taskSource) {
5774
6543
  const repo = normalizeString(taskSource?.repo);
5775
6544
  return owner && repo ? `${owner}/${repo}` : null;
5776
6545
  }
5777
- async function buildTaskSourceStatus(state, config) {
6546
+ async function buildTaskSourceStatus(state, config, requestAuth) {
5778
6547
  const diagnostics = state.snapshotService.getTaskSourceErrors();
5779
- const selectedRepo = createGitHubAuthStore(state.projectRoot).status({
6548
+ const selectedRepo = requestScopedAuthStore(state.projectRoot, requestAuth).status({
5780
6549
  oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
5781
6550
  }).selectedRepo;
5782
6551
  try {
@@ -5829,37 +6598,146 @@ function isLoopbackRequest(req) {
5829
6598
  }
5830
6599
  }
5831
6600
  function isPublicRigAuthBootstrapRoute(pathname) {
5832
- 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";
6601
+ 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";
6602
+ }
6603
+ function buildRigInstallScript() {
6604
+ return `#!/usr/bin/env bash
6605
+ set -euo pipefail
6606
+
6607
+ say() {
6608
+ printf 'rig-install: %s
6609
+ ' "$*"
6610
+ }
6611
+
6612
+ if ! command -v bun >/dev/null 2>&1; then
6613
+ say "Bun not found; installing Bun first"
6614
+ curl -fsSL https://bun.sh/install | bash
6615
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
6616
+ export PATH="$BUN_INSTALL/bin:$PATH"
6617
+ fi
6618
+
6619
+ if ! command -v bun >/dev/null 2>&1; then
6620
+ printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
6621
+ ' >&2
6622
+ exit 1
6623
+ fi
6624
+
6625
+ say "Installing @h-rig/cli@latest"
6626
+ bun add -g @h-rig/cli@latest
6627
+
6628
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
6629
+ BUN_RIG="$BUN_INSTALL/bin/rig"
6630
+ if [ ! -x "$BUN_RIG" ]; then
6631
+ printf 'rig-install: expected Bun global rig at %s but it was not executable.
6632
+ ' "$BUN_RIG" >&2
6633
+ exit 1
6634
+ fi
6635
+
6636
+ USER_BIN="$HOME/.local/bin"
6637
+ mkdir -p "$USER_BIN"
6638
+ cat > "$USER_BIN/rig" <<'RIG_SHIM'
6639
+ #!/usr/bin/env bash
6640
+ set -euo pipefail
6641
+ exec "\${BUN_INSTALL:-$HOME/.bun}/bin/rig" "$@"
6642
+ RIG_SHIM
6643
+ chmod +x "$USER_BIN/rig"
6644
+
6645
+ export PATH="$USER_BIN:$BUN_INSTALL/bin:$PATH"
6646
+ if command -v hash >/dev/null 2>&1; then hash -r; fi
6647
+
6648
+ if ! command -v rig >/dev/null 2>&1; then
6649
+ printf 'rig-install: rig installed, but rig is not on PATH. Add %s and %s/bin to PATH and retry.
6650
+ ' "$USER_BIN" "$BUN_INSTALL" >&2
6651
+ exit 1
6652
+ fi
6653
+
6654
+ say "Verifying rig"
6655
+ "$BUN_RIG" --help >/dev/null
6656
+ rig --help >/dev/null
6657
+ say "Done. Run: rig --help"
6658
+ `;
5833
6659
  }
5834
6660
  function normalizePrMode(value) {
5835
6661
  const mode = normalizeString(value);
5836
6662
  return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
5837
6663
  }
6664
+ function requestAuthResult(input) {
6665
+ return {
6666
+ authorized: input.authorized,
6667
+ actor: input.actor ?? null,
6668
+ reason: input.reason,
6669
+ login: input.login ?? null,
6670
+ userId: input.userId ?? null,
6671
+ userNamespace: input.userNamespace ?? null,
6672
+ authStateFile: input.authStateFile ?? null
6673
+ };
6674
+ }
6675
+ function namespaceFromSessionIndex(entry) {
6676
+ const stateDir2 = dirname15(entry.authStateFile);
6677
+ return {
6678
+ key: entry.namespaceKey,
6679
+ userId: entry.userId,
6680
+ login: entry.login,
6681
+ root: entry.namespaceRoot,
6682
+ stateDir: stateDir2,
6683
+ authStateFile: entry.authStateFile,
6684
+ metadataFile: resolve20(stateDir2, "user-namespace.json"),
6685
+ checkoutBaseDir: entry.checkoutBaseDir,
6686
+ snapshotBaseDir: entry.snapshotBaseDir
6687
+ };
6688
+ }
5838
6689
  function authorizeRigHttpRequest(input) {
5839
6690
  if (input.legacyAuthorized) {
5840
- return { authorized: true, actor: "rig-local-server", reason: "server-token" };
6691
+ return requestAuthResult({ authorized: true, actor: "rig-local-server", reason: "server-token" });
5841
6692
  }
5842
6693
  const bearer = bearerTokenFromRequest(input.req);
5843
6694
  const store = createGitHubAuthStore(input.projectRoot);
5844
6695
  const storedToken = store.readToken();
5845
6696
  const session = bearer ? store.readApiSession(bearer) : null;
5846
6697
  if (session) {
5847
- return { authorized: true, actor: session.login ?? "github-operator", reason: "github-session" };
6698
+ return requestAuthResult({
6699
+ authorized: true,
6700
+ actor: session.login ?? "github-operator",
6701
+ reason: "github-session",
6702
+ login: session.login,
6703
+ userId: session.userId,
6704
+ authStateFile: store.stateFile
6705
+ });
5848
6706
  }
5849
6707
  if (bearer && storedToken && bearer === storedToken) {
5850
6708
  const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
5851
- return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
6709
+ return requestAuthResult({
6710
+ authorized: true,
6711
+ actor: status.login ?? "github-operator",
6712
+ reason: "github-token",
6713
+ login: status.login,
6714
+ userId: status.userId,
6715
+ authStateFile: store.stateFile
6716
+ });
6717
+ }
6718
+ const indexedSession = readGitHubApiSession({ projectRoot: input.projectRoot, token: bearer });
6719
+ if (indexedSession) {
6720
+ const userNamespace = namespaceFromSessionIndex(indexedSession);
6721
+ return requestAuthResult({
6722
+ authorized: true,
6723
+ actor: indexedSession.login ?? "github-operator",
6724
+ reason: "github-user-session",
6725
+ login: indexedSession.login,
6726
+ userId: indexedSession.userId,
6727
+ userNamespace,
6728
+ authStateFile: indexedSession.authStateFile
6729
+ });
5852
6730
  }
5853
6731
  if (isPublicRigAuthBootstrapRoute(input.pathname)) {
5854
- return { authorized: true, actor: null, reason: "public-bootstrap" };
6732
+ return requestAuthResult({ authorized: true, actor: null, reason: "public-bootstrap" });
5855
6733
  }
5856
6734
  if (!input.serverAuthToken && !storedToken) {
5857
6735
  if (isLoopbackRequest(input.req)) {
5858
- return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
6736
+ return requestAuthResult({ authorized: true, actor: null, reason: "loopback-dev-no-auth" });
5859
6737
  }
5860
- return { authorized: false, actor: null, reason: "auth-required" };
6738
+ return requestAuthResult({ authorized: false, actor: null, reason: "auth-required" });
5861
6739
  }
5862
- return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
6740
+ return requestAuthResult({ authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" });
5863
6741
  }
5864
6742
  async function fetchGitHubUserInfo(token) {
5865
6743
  const response = await fetch("https://api.github.com/user", {
@@ -5879,6 +6757,67 @@ async function fetchGitHubUserInfo(token) {
5879
6757
  scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
5880
6758
  };
5881
6759
  }
6760
+ function shouldWriteRootAuthCompat(projectRoot) {
6761
+ if (process.env.RIG_REMOTE_USER_NAMESPACE_ROOT?.trim())
6762
+ return false;
6763
+ const stateDir2 = normalizeString(process.env.RIG_STATE_DIR);
6764
+ if (!stateDir2)
6765
+ return true;
6766
+ return resolve20(stateDir2) === resolve20(projectRoot, ".rig", "state");
6767
+ }
6768
+ function requestScopedRegistryRoot(stateProjectRoot, requestAuth) {
6769
+ return requestAuth.userNamespace?.root ?? stateProjectRoot;
6770
+ }
6771
+ function requestScopedAuthStore(stateProjectRoot, requestAuth) {
6772
+ return requestAuth.authStateFile ? createGitHubAuthStoreFromStateFile(requestAuth.authStateFile) : createGitHubAuthStore(stateProjectRoot);
6773
+ }
6774
+ function userNamespaceResponse(namespace) {
6775
+ return namespace ? serializeRemoteUserNamespace(namespace) : undefined;
6776
+ }
6777
+ function resolveNamespacedBaseDir(input) {
6778
+ if (input.explicitBaseDir)
6779
+ return input.explicitBaseDir;
6780
+ const envBase = normalizeString(process.env[input.envName]);
6781
+ if (input.userNamespace) {
6782
+ return envBase ? resolve20(envBase, input.userNamespace.key) : input.userNamespace[input.legacySubdir === "remote-checkouts" ? "checkoutBaseDir" : "snapshotBaseDir"];
6783
+ }
6784
+ return envBase ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve20(normalizeString(process.env.RIG_STATE_DIR), input.legacySubdir) : resolve20(input.legacyProjectRoot, ".rig", input.legacySubdir));
6785
+ }
6786
+ function explicitCheckoutKey(body, checkoutInput, requestAuth) {
6787
+ return normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? (requestAuth.userNamespace ? undefined : "default");
6788
+ }
6789
+ function saveGitHubTokenForRemoteUser(input) {
6790
+ const namespace = resolveRemoteUserNamespace(input.projectRoot, { userId: input.user.userId, login: input.user.login });
6791
+ writeRemoteUserNamespaceMetadata(namespace);
6792
+ const store = createGitHubAuthStoreFromStateFile(namespace.authStateFile);
6793
+ store.saveToken({
6794
+ token: input.token,
6795
+ tokenSource: input.tokenSource,
6796
+ login: input.user.login,
6797
+ userId: input.user.userId,
6798
+ scopes: input.user.scopes,
6799
+ selectedRepo: input.selectedRepo
6800
+ });
6801
+ const apiSession = store.createApiSession();
6802
+ registerGitHubApiSession({ projectRoot: input.projectRoot, token: apiSession.token, namespace, selectedRepo: input.selectedRepo });
6803
+ const requestedRoot = normalizeString(input.requestedProjectRoot);
6804
+ if (requestedRoot && isAbsolute4(requestedRoot) && existsSync13(resolve20(requestedRoot))) {
6805
+ copyGitHubAuthStateToLocalProjectRoot(namespace.authStateFile, resolve20(requestedRoot));
6806
+ }
6807
+ if (shouldWriteRootAuthCompat(input.projectRoot)) {
6808
+ const rootStore = createGitHubAuthStore(input.projectRoot);
6809
+ rootStore.saveToken({
6810
+ token: input.token,
6811
+ tokenSource: input.tokenSource,
6812
+ login: input.user.login,
6813
+ userId: input.user.userId,
6814
+ scopes: input.user.scopes,
6815
+ selectedRepo: input.selectedRepo
6816
+ });
6817
+ rootStore.createApiSession();
6818
+ }
6819
+ return { store, namespace, apiSessionToken: apiSession.token };
6820
+ }
5882
6821
  async function postGitHubForm(endpoint, body) {
5883
6822
  const response = await fetch(endpoint, {
5884
6823
  method: "POST",
@@ -5896,11 +6835,11 @@ function resolveRequestedProjectRoot(currentRoot, rawRoot) {
5896
6835
  const requestedRoot = normalizeString(rawRoot);
5897
6836
  if (!requestedRoot)
5898
6837
  return currentRoot;
5899
- if (!isAbsolute3(requestedRoot)) {
6838
+ if (!isAbsolute4(requestedRoot)) {
5900
6839
  throw new Error("projectRoot must be an absolute path on the Rig server host");
5901
6840
  }
5902
- const normalizedRoot = resolve18(requestedRoot);
5903
- if (!existsSync11(normalizedRoot)) {
6841
+ const normalizedRoot = resolve20(requestedRoot);
6842
+ if (!existsSync13(normalizedRoot)) {
5904
6843
  throw new Error("projectRoot does not exist on the Rig server host");
5905
6844
  }
5906
6845
  return normalizedRoot;
@@ -6128,6 +7067,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
6128
7067
  }
6129
7068
  return filtered;
6130
7069
  }
7070
+ function issueAnalysisTargetFor(source) {
7071
+ if (!source)
7072
+ return null;
7073
+ const candidate = source;
7074
+ if (typeof candidate.updateTask !== "function")
7075
+ return null;
7076
+ return {
7077
+ ...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
7078
+ updateTask: candidate.updateTask.bind(candidate),
7079
+ ...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
7080
+ ...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
7081
+ ...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
7082
+ };
7083
+ }
7084
+ function uniqueStringList(value) {
7085
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
7086
+ return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
7087
+ }
7088
+ function taskRecordId(task) {
7089
+ return String(task.id ?? "");
7090
+ }
6131
7091
  function redactRemoteEndpoint(endpoint) {
6132
7092
  const { token, ...rest } = endpoint;
6133
7093
  return {
@@ -6212,6 +7172,13 @@ function createRigServerFetch(state, deps) {
6212
7172
  notifications: state.targets.length
6213
7173
  });
6214
7174
  }
7175
+ if (url.pathname === "/install" && req.method === "GET") {
7176
+ return new Response(buildRigInstallScript(), {
7177
+ headers: {
7178
+ "Content-Type": "text/x-shellscript; charset=utf-8"
7179
+ }
7180
+ });
7181
+ }
6215
7182
  const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
6216
7183
  const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
6217
7184
  const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
@@ -6424,16 +7391,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6424
7391
  if (!source) {
6425
7392
  return deps.badRequest("No task source is configured");
6426
7393
  }
7394
+ if (!source.updateTask && !(update.status && source.updateStatus)) {
7395
+ return deps.badRequest("Configured task source does not support updates");
7396
+ }
6427
7397
  const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
6428
7398
  return;
6429
7399
  }) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
6430
- if (source.updateTask) {
6431
- await source.updateTask(id, update);
6432
- } else if (update.status && source.updateStatus) {
6433
- await source.updateStatus(id, update.status);
6434
- } else {
6435
- return deps.badRequest("Configured task source does not support updates");
6436
- }
6437
7400
  const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
6438
7401
  const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
6439
7402
  taskId: id,
@@ -6442,6 +7405,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6442
7405
  token: createGitHubAuthStore(state.projectRoot).readToken(),
6443
7406
  config: ctx?.config
6444
7407
  }).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
7408
+ if (update.status && githubProjectsEnabled2(ctx?.config) && projectSync.synced === false) {
7409
+ return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
7410
+ }
7411
+ try {
7412
+ if (source.updateTask) {
7413
+ await source.updateTask(id, update);
7414
+ } else if (update.status && source.updateStatus) {
7415
+ await source.updateStatus(id, update.status);
7416
+ }
7417
+ } catch (error) {
7418
+ let rollback = null;
7419
+ const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
7420
+ if (update.status && previousStatus && githubProjectsEnabled2(ctx?.config) && projectSync.synced !== false) {
7421
+ rollback = await syncGitHubProjectStatusForTaskUpdate({
7422
+ taskId: id,
7423
+ status: previousStatus,
7424
+ issueNodeId,
7425
+ token: createGitHubAuthStore(state.projectRoot).readToken(),
7426
+ config: ctx?.config
7427
+ }).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
7428
+ }
7429
+ return deps.jsonResponse({
7430
+ ok: false,
7431
+ id,
7432
+ projectSync,
7433
+ rollback,
7434
+ error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
7435
+ }, 502);
7436
+ }
6445
7437
  deps.snapshotService.invalidate("github-issue-updated");
6446
7438
  await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
6447
7439
  return;
@@ -6450,29 +7442,105 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6450
7442
  return deps.jsonResponse({ ok: true, id, projectSync });
6451
7443
  }
6452
7444
  if (url.pathname === "/api/workspace/task-labels") {
7445
+ const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
7446
+ if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
7447
+ return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
7448
+ }
6453
7449
  return deps.jsonResponse({
6454
7450
  ok: true,
6455
7451
  ready: true,
6456
7452
  labelsReady: true,
6457
- labels: [
6458
- "ready",
6459
- "blocked",
6460
- "in-progress",
6461
- "under-review",
6462
- "failed",
6463
- "cancelled",
6464
- "rig:running",
6465
- "rig:pr-open",
6466
- "rig:ci-fixing",
6467
- "rig:done",
6468
- "rig:needs-attention"
6469
- ],
6470
- note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
7453
+ labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
7454
+ note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
7455
+ });
7456
+ }
7457
+ if (url.pathname === "/api/github/projects" && req.method === "GET") {
7458
+ const owner = normalizeString(url.searchParams.get("owner"));
7459
+ if (!owner)
7460
+ return deps.badRequest("owner is required");
7461
+ const token = createGitHubAuthStore(state.projectRoot).readToken();
7462
+ if (!token)
7463
+ return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
7464
+ const projects = await listGitHubProjects({ owner, token }).catch((error) => {
7465
+ throw new Error(error instanceof Error ? error.message : String(error));
7466
+ });
7467
+ return deps.jsonResponse({ ok: true, projects });
7468
+ }
7469
+ const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
7470
+ if (projectStatusMatch && req.method === "GET") {
7471
+ const projectId = decodeURIComponent(projectStatusMatch[1]);
7472
+ const token = createGitHubAuthStore(state.projectRoot).readToken();
7473
+ if (!token)
7474
+ return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
7475
+ const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
7476
+ throw new Error(error instanceof Error ? error.message : String(error));
7477
+ });
7478
+ return deps.jsonResponse({ ok: true, field });
7479
+ }
7480
+ if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
7481
+ const body = await deps.readJsonBody(req);
7482
+ const ids = uniqueStringList(body.ids ?? body.id);
7483
+ const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
7484
+ if (ids.length === 0 && !analyzeAll) {
7485
+ return deps.badRequest("ids is required unless all=true");
7486
+ }
7487
+ const ctx = await getCachedPluginHostContext(state.projectRoot);
7488
+ const [source] = ctx?.taskSourceRegistry.list() ?? [];
7489
+ const target = issueAnalysisTargetFor(source);
7490
+ if (!source || !target) {
7491
+ return deps.badRequest("Configured task source does not support issue-analysis writeback");
7492
+ }
7493
+ const allTasks = [...await source.list()];
7494
+ const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
7495
+ const cached = allTasks.find((task) => taskRecordId(task) === id);
7496
+ if (cached)
7497
+ return cached;
7498
+ return typeof source.get === "function" ? await source.get(id) : undefined;
7499
+ }))).filter((task) => Boolean(task));
7500
+ if (issues.length === 0) {
7501
+ return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
7502
+ }
7503
+ const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
7504
+ const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
7505
+ const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
7506
+ const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
7507
+ const service = createIssueAnalysisService({
7508
+ analyzer: createPiIssueAnalyzer({
7509
+ ...model ? { model } : {},
7510
+ env: { RIG_PROJECT_ROOT: state.projectRoot }
7511
+ }),
7512
+ writeBack: createIssueAnalysisWriteBack({ target })
7513
+ });
7514
+ const reason = normalizeString(body.reason) ?? "http-issue-analysis";
7515
+ let results;
7516
+ try {
7517
+ results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
7518
+ } catch (error) {
7519
+ return deps.jsonResponse({
7520
+ ok: false,
7521
+ error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
7522
+ reason,
7523
+ ids: issues.map((issue) => issue.id)
7524
+ }, 502);
7525
+ }
7526
+ deps.snapshotService.invalidate("issue-analysis-http-run");
7527
+ await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
7528
+ return;
7529
+ });
7530
+ deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
7531
+ return deps.jsonResponse({
7532
+ ok: true,
7533
+ reason,
7534
+ analyzed: results.map((entry) => ({
7535
+ id: entry.issue.id,
7536
+ title: entry.issue.title ?? null,
7537
+ result: entry.result
7538
+ }))
6471
7539
  });
6472
7540
  }
6473
7541
  if (url.pathname === "/api/server/status") {
6474
7542
  const config = buildProjectConfigStatus(state.projectRoot);
6475
- const taskSource = await buildTaskSourceStatus(state, config);
7543
+ const taskSource = await buildTaskSourceStatus(state, config, requestAuth);
6476
7544
  return deps.jsonResponse({
6477
7545
  ok: true,
6478
7546
  projectRoot: state.projectRoot,
@@ -6496,8 +7564,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6496
7564
  path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
6497
7565
  ref: normalizeString(rawCheckout?.ref) ?? undefined
6498
7566
  } : undefined;
6499
- const record = upsertProjectRecord(state.projectRoot, { repoSlug, checkout });
6500
- return deps.jsonResponse({ ok: true, project: record });
7567
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7568
+ const record = upsertProjectRecord(registryRoot, { repoSlug, checkout });
7569
+ return deps.jsonResponse({ ok: true, project: record, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6501
7570
  }
6502
7571
  const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
6503
7572
  if (snapshotUploadMatch && req.method === "POST") {
@@ -6510,8 +7579,15 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6510
7579
  if (!archiveContentBase64) {
6511
7580
  return deps.badRequest("archiveContentBase64 is required");
6512
7581
  }
6513
- 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"));
6514
- const checkoutKey = normalizeString(body.checkoutKey) ?? "default";
7582
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7583
+ const baseDir = resolveNamespacedBaseDir({
7584
+ explicitBaseDir: normalizeString(body.baseDir),
7585
+ envName: "RIG_REMOTE_SNAPSHOT_BASE_DIR",
7586
+ userNamespace: requestAuth.userNamespace,
7587
+ legacyProjectRoot: state.projectRoot,
7588
+ legacySubdir: "remote-snapshots"
7589
+ });
7590
+ const checkoutKey = explicitCheckoutKey(body, body, requestAuth);
6515
7591
  try {
6516
7592
  const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
6517
7593
  const checkout = extractUploadedSnapshotArchive({
@@ -6524,14 +7600,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6524
7600
  const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
6525
7601
  const packageInstall = installRemoteCheckoutPackages(checkout.path);
6526
7602
  const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
6527
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
7603
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
6528
7604
  kind: "uploaded-snapshot",
6529
7605
  path: checkout.path,
6530
7606
  ref: checkout.snapshotId
6531
7607
  });
6532
7608
  deps.snapshotService.invalidate("uploaded-snapshot-checkout");
6533
7609
  deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
6534
- return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
7610
+ return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6535
7611
  } catch (error) {
6536
7612
  return deps.jsonResponse({
6537
7613
  ok: false,
@@ -6551,10 +7627,17 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6551
7627
  if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
6552
7628
  return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
6553
7629
  }
6554
- 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"));
6555
- const checkoutKey = normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? "default";
7630
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7631
+ const baseDir = resolveNamespacedBaseDir({
7632
+ explicitBaseDir: normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir),
7633
+ envName: "RIG_REMOTE_CHECKOUT_BASE_DIR",
7634
+ userNamespace: requestAuth.userNamespace,
7635
+ legacyProjectRoot: state.projectRoot,
7636
+ legacySubdir: "remote-checkouts"
7637
+ });
7638
+ const checkoutKey = explicitCheckoutKey(body, checkoutInput, requestAuth);
6556
7639
  const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
6557
- const credentialToken = createGitHubAuthStore(state.projectRoot).readToken();
7640
+ const credentialToken = requestScopedAuthStore(state.projectRoot, requestAuth).readToken();
6558
7641
  try {
6559
7642
  const checkout = await prepareRemoteCheckout({
6560
7643
  command: runRemoteCheckoutCommand,
@@ -6563,14 +7646,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6563
7646
  const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
6564
7647
  const packageInstall = installRemoteCheckoutPackages(checkout.path);
6565
7648
  const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
6566
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
7649
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
6567
7650
  kind: checkout.kind,
6568
7651
  path: checkout.path,
6569
7652
  ref: checkout.ref ?? checkout.snapshotId ?? undefined
6570
7653
  });
6571
7654
  deps.snapshotService.invalidate("remote-checkout-prepared");
6572
7655
  deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
6573
- return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
7656
+ return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6574
7657
  } catch (error) {
6575
7658
  return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
6576
7659
  }
@@ -6587,16 +7670,18 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6587
7670
  if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
6588
7671
  return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
6589
7672
  }
6590
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
7673
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7674
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
6591
7675
  kind,
6592
7676
  path: normalizeString(body.path) ?? state.projectRoot,
6593
7677
  ref: normalizeString(body.ref) ?? undefined
6594
7678
  });
6595
- return deps.jsonResponse({ ok: true, project });
7679
+ return deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6596
7680
  }
6597
7681
  if (req.method === "GET") {
6598
- const project = getProjectRecord(state.projectRoot, repoSlug);
6599
- return project ? deps.jsonResponse({ ok: true, project }) : deps.notFound();
7682
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7683
+ const project = getProjectRecord(registryRoot, repoSlug);
7684
+ return project ? deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) }) : deps.notFound();
6600
7685
  }
6601
7686
  }
6602
7687
  if (url.pathname === "/api/server/project-root" && req.method === "POST") {
@@ -6605,13 +7690,26 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6605
7690
  if (!requestedRoot) {
6606
7691
  return deps.badRequest("projectRoot is required");
6607
7692
  }
6608
- if (!isAbsolute3(requestedRoot)) {
7693
+ if (!isAbsolute4(requestedRoot)) {
6609
7694
  return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
6610
7695
  }
6611
- const normalizedRoot = resolve18(requestedRoot);
6612
- const exists = existsSync11(normalizedRoot);
6613
- if (exists) {
6614
- createGitHubAuthStore(state.projectRoot).copyToProjectRoot(normalizedRoot);
7696
+ const normalizedRoot = resolve20(requestedRoot);
7697
+ const exists = existsSync13(normalizedRoot);
7698
+ if (exists && requestAuth.userNamespace) {
7699
+ const allowedByNamespace = isPathInsideNamespace(requestAuth.userNamespace.root, normalizedRoot);
7700
+ const allowedByRegistry = projectRegistryContainsCheckout(requestAuth.userNamespace.root, normalizedRoot);
7701
+ if (!allowedByNamespace && !allowedByRegistry) {
7702
+ return deps.jsonResponse({
7703
+ ok: false,
7704
+ error: "Requested project root is outside the authenticated GitHub user namespace.",
7705
+ projectRoot: state.projectRoot,
7706
+ requestedProjectRoot: normalizedRoot,
7707
+ userNamespace: userNamespaceResponse(requestAuth.userNamespace)
7708
+ }, 403);
7709
+ }
7710
+ copyGitHubAuthStateToLocalProjectRoot(requestAuth.userNamespace.authStateFile, normalizedRoot);
7711
+ } else if (exists) {
7712
+ createGitHubAuthStore(state.projectRoot).copyToLocalProjectRoot(normalizedRoot);
6615
7713
  }
6616
7714
  const control = buildServerControlStatus();
6617
7715
  const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
@@ -6626,7 +7724,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6626
7724
  message: "Requested project root does not exist on the Rig server host."
6627
7725
  }, 404);
6628
7726
  }
6629
- if (!existsSync11(resolve18(normalizedRoot, "rig.config.ts")) && !existsSync11(resolve18(normalizedRoot, "rig.config.json"))) {
7727
+ if (!existsSync13(resolve20(normalizedRoot, "rig.config.ts")) && !existsSync13(resolve20(normalizedRoot, "rig.config.json"))) {
6630
7728
  return deps.jsonResponse({
6631
7729
  ok: false,
6632
7730
  projectRoot: state.projectRoot,
@@ -6661,6 +7759,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6661
7759
  exists,
6662
7760
  control,
6663
7761
  requiresRestart: false,
7762
+ userNamespace: userNamespaceResponse(requestAuth.userNamespace),
6664
7763
  message: "Project-root switch accepted. Rig server restart has been scheduled."
6665
7764
  }, 202);
6666
7765
  }
@@ -6675,11 +7774,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6675
7774
  }, 409);
6676
7775
  }
6677
7776
  if (url.pathname === "/api/github/auth/status") {
6678
- const store = createGitHubAuthStore(state.projectRoot);
6679
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
7777
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
7778
+ return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6680
7779
  }
6681
7780
  if (url.pathname === "/api/github/repo/permissions") {
6682
- const store = createGitHubAuthStore(state.projectRoot);
7781
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
6683
7782
  const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
6684
7783
  if (!auth.signedIn) {
6685
7784
  return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
@@ -6707,24 +7806,20 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6707
7806
  }
6708
7807
  try {
6709
7808
  const user = await fetchGitHubUserInfo(token);
6710
- const storeRoots = [
6711
- state.projectRoot,
6712
- ...requestedProjectRoot && isAbsolute3(requestedProjectRoot) && existsSync11(resolve18(requestedProjectRoot)) ? [resolve18(requestedProjectRoot)] : []
6713
- ].filter((root, index, roots) => roots.indexOf(root) === index);
6714
- const stores = storeRoots.map((root) => createGitHubAuthStore(root));
6715
- for (const store2 of stores) {
6716
- store2.saveToken({
6717
- token,
6718
- tokenSource: "manual-token",
6719
- login: user.login,
6720
- userId: user.userId,
6721
- scopes: user.scopes,
6722
- selectedRepo
6723
- });
6724
- }
6725
- const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
6726
- const apiSession = store.createApiSession();
6727
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
7809
+ const saved = saveGitHubTokenForRemoteUser({
7810
+ projectRoot: state.projectRoot,
7811
+ token,
7812
+ tokenSource: "manual-token",
7813
+ user,
7814
+ selectedRepo,
7815
+ requestedProjectRoot
7816
+ });
7817
+ return deps.jsonResponse({
7818
+ ok: true,
7819
+ ...saved.store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }),
7820
+ apiSessionToken: saved.apiSessionToken,
7821
+ userNamespace: userNamespaceResponse(saved.namespace)
7822
+ });
6728
7823
  } catch (error) {
6729
7824
  const message = error instanceof Error ? error.message : String(error);
6730
7825
  return deps.jsonResponse({ ok: false, error: message }, 400);
@@ -6791,9 +7886,21 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6791
7886
  }
6792
7887
  const token = result.payload.access_token;
6793
7888
  const user = await fetchGitHubUserInfo(token);
6794
- store.saveToken({ token, tokenSource: "oauth-device", login: user.login, userId: user.userId, scopes: user.scopes });
6795
- const apiSession = store.createApiSession();
6796
- return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }), apiSessionToken: apiSession.token });
7889
+ const saved = saveGitHubTokenForRemoteUser({
7890
+ projectRoot: state.projectRoot,
7891
+ token,
7892
+ tokenSource: "oauth-device",
7893
+ user,
7894
+ selectedRepo: null
7895
+ });
7896
+ store.clearPendingDevice(pollId);
7897
+ return deps.jsonResponse({
7898
+ ok: true,
7899
+ status: "signed-in",
7900
+ ...saved.store.status({ oauthConfigured: true }),
7901
+ apiSessionToken: saved.apiSessionToken,
7902
+ userNamespace: userNamespaceResponse(saved.namespace)
7903
+ });
6797
7904
  }
6798
7905
  if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
6799
7906
  const body = await deps.readJsonBody(req);
@@ -6802,7 +7909,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6802
7909
  if (!owner || !repo) {
6803
7910
  return deps.badRequest("owner and repo are required");
6804
7911
  }
6805
- const store = createGitHubAuthStore(state.projectRoot);
7912
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
6806
7913
  const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
6807
7914
  const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
6808
7915
  return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
@@ -6823,7 +7930,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6823
7930
  return deps.badRequest(error instanceof Error ? error.message : String(error));
6824
7931
  }
6825
7932
  const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
6826
- const configPath = resolve18(targetRoot, "rig.config.ts");
7933
+ const configPath = resolve20(targetRoot, "rig.config.ts");
6827
7934
  const source = buildGitHubProjectConfigSource({
6828
7935
  projectName: rawProjectName,
6829
7936
  owner,
@@ -6835,8 +7942,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6835
7942
  ok: true,
6836
7943
  projectRoot: targetRoot,
6837
7944
  configPath,
6838
- exists: existsSync11(configPath),
6839
- requiresOverwrite: existsSync11(configPath),
7945
+ exists: existsSync13(configPath),
7946
+ requiresOverwrite: existsSync13(configPath),
6840
7947
  source,
6841
7948
  owner,
6842
7949
  repo,
@@ -6872,8 +7979,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6872
7979
  assignee,
6873
7980
  githubUserId: authStatus.userId ?? authStatus.login
6874
7981
  });
6875
- const configPath = resolve18(targetRoot, "rig.config.ts");
6876
- if (existsSync11(configPath) && !overwrite) {
7982
+ const configPath = resolve20(targetRoot, "rig.config.ts");
7983
+ if (existsSync13(configPath) && !overwrite) {
6877
7984
  return deps.jsonResponse({
6878
7985
  ok: false,
6879
7986
  error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
@@ -6889,11 +7996,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6889
7996
  return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
6890
7997
  }
6891
7998
  let backupPath = null;
6892
- if (existsSync11(configPath)) {
7999
+ if (existsSync13(configPath)) {
6893
8000
  backupPath = backupConfigPath(configPath);
6894
- copyFileSync(configPath, backupPath);
8001
+ copyFileSync2(configPath, backupPath);
6895
8002
  }
6896
- writeFileSync10(configPath, source, "utf8");
8003
+ writeFileSync12(configPath, source, "utf8");
6897
8004
  const selectedRepo = `${owner}/${repo}`;
6898
8005
  store.saveSelectedRepo(selectedRepo);
6899
8006
  const targetStore = createGitHubAuthStore(targetRoot);
@@ -7165,11 +8272,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7165
8272
  const runId = normalizeString(body.runId);
7166
8273
  const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
7167
8274
  const promptOverride = normalizeString(body.promptOverride);
8275
+ const restart = body.restart === true;
7168
8276
  if (!runId) {
7169
8277
  return deps.badRequest("runId is required");
7170
8278
  }
7171
8279
  try {
7172
- await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
8280
+ await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
7173
8281
  deps.broadcastSnapshotInvalidation(state);
7174
8282
  return deps.jsonResponse({ ok: true, runId, createdAt });
7175
8283
  } catch (error) {
@@ -7178,7 +8286,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7178
8286
  }
7179
8287
  if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
7180
8288
  const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
7181
- 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";
8289
+ 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";
7182
8290
  if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
7183
8291
  return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
7184
8292
  }
@@ -7494,9 +8602,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7494
8602
  } catch {
7495
8603
  return deps.badRequest("Invalid artifact path");
7496
8604
  }
7497
- mkdirSync11(dirname12(artifactPath), { recursive: true });
8605
+ mkdirSync13(dirname15(artifactPath), { recursive: true });
7498
8606
  const bytes = Buffer.from(contentBase64, "base64");
7499
- writeFileSync10(artifactPath, bytes);
8607
+ writeFileSync12(artifactPath, bytes);
7500
8608
  writeJsonFile4(`${artifactPath}.json`, {
7501
8609
  workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
7502
8610
  runId,
@@ -7533,13 +8641,75 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7533
8641
  }
7534
8642
  const run = leaseValidation.run;
7535
8643
  const completedAt = new Date().toISOString();
8644
+ const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
8645
+ if (run.taskId && workspaceDir) {
8646
+ patchRunRecord(state.projectRoot, runId, {
8647
+ status: "reviewing",
8648
+ completedAt: null,
8649
+ hostId,
8650
+ endpointId: leaseId,
8651
+ worktreePath: workspaceDir,
8652
+ serverCloseout: {
8653
+ status: "pending",
8654
+ phase: "queued",
8655
+ requestedAt: completedAt,
8656
+ updatedAt: completedAt,
8657
+ runtimeWorkspace: workspaceDir,
8658
+ branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
8659
+ taskId: run.taskId,
8660
+ source: "remote-complete"
8661
+ }
8662
+ });
8663
+ deps.appendRunLogEntryAndBroadcast(state, runId, {
8664
+ id: `log:${runId}:remote-server-closeout-requested`,
8665
+ title: "Server-owned closeout requested",
8666
+ detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
8667
+ tone: "info",
8668
+ status: "reviewing",
8669
+ createdAt: completedAt,
8670
+ payload: { workspaceDir, hostId, leaseId }
8671
+ }, "remote-server-closeout-requested");
8672
+ deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
8673
+ const detail = error instanceof Error ? error.message : String(error);
8674
+ patchRunRecord(state.projectRoot, runId, {
8675
+ status: "failed",
8676
+ completedAt: new Date().toISOString(),
8677
+ errorText: detail,
8678
+ serverCloseout: {
8679
+ status: "failed",
8680
+ phase: "failed",
8681
+ updatedAt: new Date().toISOString(),
8682
+ error: detail
8683
+ }
8684
+ });
8685
+ deps.appendRunLogEntryAndBroadcast(state, runId, {
8686
+ id: `log:${runId}:remote-server-closeout-failed`,
8687
+ title: "Server-owned closeout failed",
8688
+ detail,
8689
+ tone: "error",
8690
+ status: "failed",
8691
+ createdAt: new Date().toISOString()
8692
+ }, "remote-server-closeout-failed");
8693
+ }).finally(() => {
8694
+ deps.reconcileScheduler(state, "remote-server-closeout-terminal");
8695
+ });
8696
+ deps.broadcastSnapshotInvalidation(state);
8697
+ return deps.jsonResponse({
8698
+ ok: true,
8699
+ workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
8700
+ hostId,
8701
+ runId,
8702
+ leaseId,
8703
+ closeout: "server-owned",
8704
+ acceptedAt: new Date().toISOString()
8705
+ });
8706
+ }
7536
8707
  patchRunRecord(state.projectRoot, runId, {
7537
8708
  status: "completed",
7538
8709
  completedAt,
7539
8710
  hostId,
7540
8711
  endpointId: leaseId
7541
8712
  });
7542
- await updateRemoteRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "completed", completedAt, hostId, endpointId: leaseId }, "closed", "Remote Rig task run completed and closed this task.");
7543
8713
  await deps.enqueueRunLinearEvent(state.projectRoot, {
7544
8714
  type: "run.completed",
7545
8715
  runId,
@@ -7658,12 +8828,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7658
8828
  try {
7659
8829
  const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
7660
8830
  const runRoot = deps.normalizeRelativePath(runsRoot, runId);
7661
- const artifactsRoot = resolve18(runRoot, "remote-artifacts");
8831
+ const artifactsRoot = resolve20(runRoot, "remote-artifacts");
7662
8832
  artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
7663
8833
  } catch {
7664
8834
  return deps.badRequest("Invalid artifact path");
7665
8835
  }
7666
- if (!existsSync11(artifactPath)) {
8836
+ if (!existsSync13(artifactPath)) {
7667
8837
  return deps.notFound();
7668
8838
  }
7669
8839
  return new Response(Bun.file(artifactPath));
@@ -7676,6 +8846,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7676
8846
  const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
7677
8847
  return deps.jsonResponse(page);
7678
8848
  }
8849
+ const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
8850
+ if (runTimelineMatch) {
8851
+ const runId = decodeURIComponent(runTimelineMatch[1]);
8852
+ const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
8853
+ const cursor = normalizeString(url.searchParams.get("cursor"));
8854
+ const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
8855
+ return deps.jsonResponse(page);
8856
+ }
7679
8857
  const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
7680
8858
  if (runSteerMatch && req.method === "POST") {
7681
8859
  const runId = decodeURIComponent(runSteerMatch[1]);
@@ -8556,8 +9734,8 @@ async function routeWebSocketRequest(state, deps, request) {
8556
9734
  }
8557
9735
 
8558
9736
  // packages/server/src/server-helpers/inspector-jobs.ts
8559
- import { existsSync as existsSync15, mkdirSync as mkdirSync14, readFileSync as readFileSync10, writeFileSync as writeFileSync13 } from "fs";
8560
- import { dirname as dirname15, resolve as resolve21 } from "path";
9737
+ import { existsSync as existsSync17, mkdirSync as mkdirSync16, readFileSync as readFileSync12, writeFileSync as writeFileSync15 } from "fs";
9738
+ import { dirname as dirname18, resolve as resolve23 } from "path";
8561
9739
  import { readJsonFile as readJsonFile3 } from "@rig/runtime/control-plane/authority-files";
8562
9740
  import { resolveMonorepoRoot as resolveMonorepoRoot5 } from "@rig/runtime/control-plane/native/utils";
8563
9741
  import { normalizeTaskLifecycleStatus as normalizeTaskLifecycleStatus2 } from "@rig/runtime/control-plane/state-sync/types";
@@ -8665,8 +9843,8 @@ import { randomUUID as randomUUID3 } from "crypto";
8665
9843
 
8666
9844
  // packages/server/src/inspector/mission.ts
8667
9845
  import { randomUUID as randomUUID2 } from "crypto";
8668
- import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync8, readdirSync as readdirSync4, renameSync, writeFileSync as writeFileSync11 } from "fs";
8669
- import { dirname as dirname13, join, resolve as resolve19 } from "path";
9846
+ import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync14, readFileSync as readFileSync10, readdirSync as readdirSync4, renameSync, writeFileSync as writeFileSync13 } from "fs";
9847
+ import { dirname as dirname16, join, resolve as resolve21 } from "path";
8670
9848
  function isJsonValue(value) {
8671
9849
  if (value === null)
8672
9850
  return true;
@@ -8706,7 +9884,7 @@ function isRecord2(value) {
8706
9884
  }
8707
9885
  function readJsonRecord(path) {
8708
9886
  try {
8709
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
9887
+ const parsed = JSON.parse(readFileSync10(path, "utf8"));
8710
9888
  if (!isRecord2(parsed)) {
8711
9889
  return { ok: false, error: `Mission file ${path} does not contain an object` };
8712
9890
  }
@@ -8786,14 +9964,14 @@ function missionActionDetails(mission) {
8786
9964
  };
8787
9965
  }
8788
9966
  function writeJsonFile5(path, value) {
8789
- mkdirSync12(dirname13(path), { recursive: true });
9967
+ mkdirSync14(dirname16(path), { recursive: true });
8790
9968
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
8791
- writeFileSync11(tempPath, `${JSON.stringify(value, null, 2)}
9969
+ writeFileSync13(tempPath, `${JSON.stringify(value, null, 2)}
8792
9970
  `, "utf8");
8793
9971
  renameSync(tempPath, path);
8794
9972
  }
8795
9973
  function resolveInspectorMissionPaths(projectRoot) {
8796
- const inspectorDir = resolve19(resolveRigServerPaths(projectRoot).stateDir, "inspector");
9974
+ const inspectorDir = resolve21(resolveRigServerPaths(projectRoot).stateDir, "inspector");
8797
9975
  return {
8798
9976
  inspectorDir,
8799
9977
  missionsDir: join(inspectorDir, "missions"),
@@ -8802,8 +9980,8 @@ function resolveInspectorMissionPaths(projectRoot) {
8802
9980
  }
8803
9981
  function createInspectorMissionController(options) {
8804
9982
  const paths = resolveInspectorMissionPaths(options.projectRoot);
8805
- mkdirSync12(paths.missionsDir, { recursive: true });
8806
- mkdirSync12(paths.journalsDir, { recursive: true });
9983
+ mkdirSync14(paths.missionsDir, { recursive: true });
9984
+ mkdirSync14(paths.journalsDir, { recursive: true });
8807
9985
  const now = options.now ?? (() => new Date().toISOString());
8808
9986
  const nextId = options.idGenerator ?? (() => `mission:${randomUUID2()}`);
8809
9987
  function missionPath(missionId) {
@@ -8813,15 +9991,15 @@ function createInspectorMissionController(options) {
8813
9991
  return join(paths.journalsDir, `${missionId}.jsonl`);
8814
9992
  }
8815
9993
  function appendMissionJournal(entry) {
8816
- mkdirSync12(paths.journalsDir, { recursive: true });
9994
+ mkdirSync14(paths.journalsDir, { recursive: true });
8817
9995
  appendFileSync(journalPath(entry.missionId), `${JSON.stringify(entry)}
8818
9996
  `, "utf8");
8819
9997
  }
8820
9998
  function listMissionJournal(missionId) {
8821
9999
  const path = journalPath(missionId);
8822
- if (!existsSync12(path))
10000
+ if (!existsSync14(path))
8823
10001
  return [];
8824
- return readFileSync8(path, "utf8").split(`
10002
+ return readFileSync10(path, "utf8").split(`
8825
10003
  `).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line)).filter(isRecord2).map((entry) => ({
8826
10004
  id: typeof entry.id === "string" ? entry.id : `journal:${randomUUID2()}`,
8827
10005
  missionId,
@@ -8837,7 +10015,7 @@ function createInspectorMissionController(options) {
8837
10015
  }
8838
10016
  function readMissionOnly(missionId) {
8839
10017
  const path = missionPath(missionId);
8840
- if (!existsSync12(path)) {
10018
+ if (!existsSync14(path)) {
8841
10019
  return { ok: false, error: `Mission ${missionId} was not found` };
8842
10020
  }
8843
10021
  const read = readJsonRecord(path);
@@ -8888,7 +10066,7 @@ function createInspectorMissionController(options) {
8888
10066
  const source = cloneJsonRecord(input.sourceTask);
8889
10067
  const missionId = nextId();
8890
10068
  const path = missionPath(missionId);
8891
- if (existsSync12(path)) {
10069
+ if (existsSync14(path)) {
8892
10070
  const existing = readMissionOnly(missionId);
8893
10071
  if (!existing.ok)
8894
10072
  return existing;
@@ -10488,8 +11666,8 @@ function createCodexInspectorTransport(options) {
10488
11666
  const sendRequest = async (method, params) => {
10489
11667
  const id = nextRequestId;
10490
11668
  nextRequestId += 1;
10491
- const response = new Promise((resolve20, reject) => {
10492
- pendingResponses.set(id, { resolve: resolve20, reject });
11669
+ const response = new Promise((resolve22, reject) => {
11670
+ pendingResponses.set(id, { resolve: resolve22, reject });
10493
11671
  });
10494
11672
  response.catch(() => {});
10495
11673
  try {
@@ -10799,9 +11977,9 @@ function createCodexInspectorTransport(options) {
10799
11977
  }
10800
11978
  lastAssistantMessage = null;
10801
11979
  lastError = null;
10802
- const turnResult = new Promise((resolve20, reject) => {
11980
+ const turnResult = new Promise((resolve22, reject) => {
10803
11981
  currentTurn = {
10804
- resolve: resolve20,
11982
+ resolve: resolve22,
10805
11983
  reject,
10806
11984
  events: []
10807
11985
  };
@@ -10861,13 +12039,13 @@ function createCodexInspectorTransport(options) {
10861
12039
  };
10862
12040
  }
10863
12041
  function writeChildLine(child, line) {
10864
- return new Promise((resolve20, reject) => {
12042
+ return new Promise((resolve22, reject) => {
10865
12043
  child.stdin.write(line, (error) => {
10866
12044
  if (error) {
10867
12045
  reject(error);
10868
12046
  return;
10869
12047
  }
10870
- resolve20();
12048
+ resolve22();
10871
12049
  });
10872
12050
  });
10873
12051
  }
@@ -10880,10 +12058,10 @@ function terminateChild(child) {
10880
12058
  } catch {}
10881
12059
  }
10882
12060
  async function waitForChildSpawn(child) {
10883
- await new Promise((resolve20, reject) => {
12061
+ await new Promise((resolve22, reject) => {
10884
12062
  const onSpawn = () => {
10885
12063
  cleanup();
10886
- resolve20();
12064
+ resolve22();
10887
12065
  };
10888
12066
  const onError = (error) => {
10889
12067
  cleanup();
@@ -11395,8 +12573,8 @@ function createGlobalInspectorService(options) {
11395
12573
 
11396
12574
  // packages/server/src/inspector/upstream-sync.ts
11397
12575
  import { spawnSync as spawnSync4 } from "child_process";
11398
- import { existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync12 } from "fs";
11399
- import { dirname as dirname14, resolve as resolve20 } from "path";
12576
+ import { existsSync as existsSync15, mkdirSync as mkdirSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync14 } from "fs";
12577
+ import { dirname as dirname17, resolve as resolve22 } from "path";
11400
12578
  import { resolveMonorepoRoot as resolveMonorepoRoot4 } from "@rig/runtime/control-plane/native/utils";
11401
12579
  var UPSTREAM_VALIDATION_DESCRIPTIONS = {
11402
12580
  "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.",
@@ -11534,34 +12712,34 @@ function defaultGitRunner(repoRoot, args) {
11534
12712
  }
11535
12713
  function upstreamStatePath(projectRoot, override) {
11536
12714
  if (override) {
11537
- return resolve20(override);
12715
+ return resolve22(override);
11538
12716
  }
11539
- return resolve20(resolveRigServerPaths(projectRoot).stateDir, "inspector", "upstream-sync.json");
12717
+ return resolve22(resolveRigServerPaths(projectRoot).stateDir, "inspector", "upstream-sync.json");
11540
12718
  }
11541
12719
  function readUpstreamState(projectRoot, statePath) {
11542
12720
  const path = upstreamStatePath(projectRoot, statePath);
11543
- if (!existsSync13(path)) {
12721
+ if (!existsSync15(path)) {
11544
12722
  return null;
11545
12723
  }
11546
12724
  try {
11547
- return JSON.parse(readFileSync9(path, "utf-8"));
12725
+ return JSON.parse(readFileSync11(path, "utf-8"));
11548
12726
  } catch {
11549
12727
  return null;
11550
12728
  }
11551
12729
  }
11552
12730
  function writeUpstreamState(projectRoot, state, statePath) {
11553
12731
  const path = upstreamStatePath(projectRoot, statePath);
11554
- mkdirSync13(dirname14(path), { recursive: true });
11555
- writeFileSync12(path, `${JSON.stringify(state, null, 2)}
12732
+ mkdirSync15(dirname17(path), { recursive: true });
12733
+ writeFileSync14(path, `${JSON.stringify(state, null, 2)}
11556
12734
  `, "utf8");
11557
12735
  }
11558
12736
  function readImportedRevision(projectRoot, upstreamsDocPath) {
11559
12737
  const monorepoRoot = resolveMonorepoRoot4(projectRoot);
11560
- const docPath = upstreamsDocPath ? resolve20(upstreamsDocPath) : resolve20(monorepoRoot, "docs", "UPSTREAMS.md");
11561
- if (!existsSync13(docPath)) {
12738
+ const docPath = upstreamsDocPath ? resolve22(upstreamsDocPath) : resolve22(monorepoRoot, "docs", "UPSTREAMS.md");
12739
+ if (!existsSync15(docPath)) {
11562
12740
  throw new Error(`UPSTREAMS.md not found at ${docPath}`);
11563
12741
  }
11564
- const docContent = readFileSync9(docPath, "utf-8");
12742
+ const docContent = readFileSync11(docPath, "utf-8");
11565
12743
  const revision = parseImportedUpstreamRevision(docContent, "upstream") ?? parseImportedUpstreamRevision(docContent, "humoongate");
11566
12744
  if (!revision) {
11567
12745
  throw new Error(`Failed to parse upstream imported revision from ${docPath}`);
@@ -11583,7 +12761,7 @@ function resolveRemoteBranch(repoRoot, remote, gitRunner) {
11583
12761
  return null;
11584
12762
  }
11585
12763
  function isGitCheckout(path, gitRunner) {
11586
- if (!existsSync13(resolve20(path, ".git"))) {
12764
+ if (!existsSync15(resolve22(path, ".git"))) {
11587
12765
  return false;
11588
12766
  }
11589
12767
  const result = gitRunner(path, ["rev-parse", "--is-inside-work-tree"]);
@@ -11592,12 +12770,12 @@ function isGitCheckout(path, gitRunner) {
11592
12770
  function resolveUpstreamCheckout(projectRoot, explicitCheckout, gitRunner) {
11593
12771
  const monorepoRoot = resolveMonorepoRoot4(projectRoot);
11594
12772
  const candidates = [
11595
- explicitCheckout ? resolve20(explicitCheckout) : "",
11596
- process.env.UPSTREAM_CHECKOUT?.trim() ? resolve20(process.env.UPSTREAM_CHECKOUT.trim()) : "",
11597
- process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ? resolve20(process.env.HUMOONGATE_UPSTREAM_CHECKOUT.trim()) : "",
11598
- resolve20(projectRoot, "..", "humoongate"),
11599
- resolve20(monorepoRoot, "..", "humoongate"),
11600
- resolve20(monorepoRoot, "humoongate")
12773
+ explicitCheckout ? resolve22(explicitCheckout) : "",
12774
+ process.env.UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.UPSTREAM_CHECKOUT.trim()) : "",
12775
+ process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.HUMOONGATE_UPSTREAM_CHECKOUT.trim()) : "",
12776
+ resolve22(projectRoot, "..", "humoongate"),
12777
+ resolve22(monorepoRoot, "..", "humoongate"),
12778
+ resolve22(monorepoRoot, "humoongate")
11601
12779
  ].filter(Boolean);
11602
12780
  for (const candidate of candidates) {
11603
12781
  if (isGitCheckout(candidate, gitRunner)) {
@@ -11833,10 +13011,10 @@ async function runUpstreamSyncScan(options) {
11833
13011
  }
11834
13012
 
11835
13013
  // packages/server/src/server-helpers/task-config.ts
11836
- import { existsSync as existsSync14 } from "fs";
13014
+ import { existsSync as existsSync16 } from "fs";
11837
13015
  async function readTaskConfig(projectRoot) {
11838
13016
  const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
11839
- if (!existsSync14(taskConfigPath)) {
13017
+ if (!existsSync16(taskConfigPath)) {
11840
13018
  return {};
11841
13019
  }
11842
13020
  try {
@@ -11872,11 +13050,11 @@ function resolveFollowupSourceCommit(input) {
11872
13050
  }
11873
13051
  async function createInspectorFollowupTask(projectRoot, input) {
11874
13052
  const monorepoRoot = resolveMonorepoRoot5(projectRoot);
11875
- const issuesPath = resolve21(monorepoRoot, ".beads", "issues.jsonl");
11876
- const taskStatePath = resolve21(monorepoRoot, ".beads", "task-state.json");
11877
- const taskConfigPath = resolve21(monorepoRoot, ".rig", "task-config.json");
11878
- mkdirSync14(dirname15(issuesPath), { recursive: true });
11879
- mkdirSync14(dirname15(taskConfigPath), { recursive: true });
13053
+ const issuesPath = resolve23(monorepoRoot, ".beads", "issues.jsonl");
13054
+ const taskStatePath = resolve23(monorepoRoot, ".beads", "task-state.json");
13055
+ const taskConfigPath = resolve23(monorepoRoot, ".rig", "task-config.json");
13056
+ mkdirSync16(dirname18(issuesPath), { recursive: true });
13057
+ mkdirSync16(dirname18(taskConfigPath), { recursive: true });
11880
13058
  const summary = normalizeString(input.summary) ?? "Inspector follow-up";
11881
13059
  const description = normalizeString(input.description) ?? normalizeString(input.details?.description) ?? `Created by the global inspector: ${summary}`;
11882
13060
  const acceptanceCriteria = normalizeString(input.acceptanceCriteria) ?? "Investigate the detected drift and port the relevant changes into Rig.";
@@ -11895,7 +13073,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
11895
13073
  const sourceKey = normalizeString(input.sourceKey) ?? normalizeString(input.details?.sourceKey);
11896
13074
  const createdAt = normalizeString(input.createdAt) ?? new Date().toISOString();
11897
13075
  const status = normalizeTaskLifecycleStatus2(normalizeString(input.status) ?? "open") ?? "open";
11898
- const existingIssueLines = existsSync15(issuesPath) ? readFileSync10(issuesPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
13076
+ const existingIssueLines = existsSync17(issuesPath) ? readFileSync12(issuesPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
11899
13077
  const existingIssues = existingIssueLines.map((line) => {
11900
13078
  try {
11901
13079
  return JSON.parse(line);
@@ -11904,7 +13082,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
11904
13082
  }
11905
13083
  }).filter((value) => value !== null);
11906
13084
  const existingIds = new Set(existingIssues.map((issue) => typeof issue.id === "string" ? issue.id : null).filter((value) => value !== null));
11907
- const rawTaskState = existsSync15(taskStatePath) ? readJsonFile3(taskStatePath, {}) : {};
13085
+ const rawTaskState = existsSync17(taskStatePath) ? readJsonFile3(taskStatePath, {}) : {};
11908
13086
  const tasks = rawTaskState.tasks && typeof rawTaskState.tasks === "object" && !Array.isArray(rawTaskState.tasks) ? rawTaskState.tasks : {};
11909
13087
  const existingTaskIdFromSourceKey = sourceKey == null ? null : Object.entries(tasks).find(([, metadata]) => {
11910
13088
  if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
@@ -11930,7 +13108,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
11930
13108
  updated_at: createdAt,
11931
13109
  labels: mergedLabels
11932
13110
  };
11933
- writeFileSync13(issuesPath, existingIssueLines.length > 0 ? `${existingIssueLines.join(`
13111
+ writeFileSync15(issuesPath, existingIssueLines.length > 0 ? `${existingIssueLines.join(`
11934
13112
  `)}
11935
13113
  ${JSON.stringify(issueRecord)}
11936
13114
  ` : `${JSON.stringify(issueRecord)}
@@ -11948,7 +13126,7 @@ ${JSON.stringify(issueRecord)}
11948
13126
  labels: mergedLabels
11949
13127
  };
11950
13128
  });
11951
- writeFileSync13(issuesPath, `${updatedIssues.map((issue) => JSON.stringify(issue)).join(`
13129
+ writeFileSync15(issuesPath, `${updatedIssues.map((issue) => JSON.stringify(issue)).join(`
11952
13130
  `)}
11953
13131
  `, "utf8");
11954
13132
  }
@@ -11971,14 +13149,14 @@ ${JSON.stringify(issueRecord)}
11971
13149
  }
11972
13150
  };
11973
13151
  }
11974
- writeFileSync13(taskConfigPath, `${JSON.stringify(taskConfig, null, 2)}
13152
+ writeFileSync15(taskConfigPath, `${JSON.stringify(taskConfig, null, 2)}
11975
13153
  `, "utf8");
11976
13154
  tasks[taskId] = {
11977
13155
  status,
11978
13156
  sourceCommit: resolveFollowupSourceCommit(input),
11979
13157
  ...sourceKey ? { sourceKey } : {}
11980
13158
  };
11981
- writeFileSync13(taskStatePath, `${JSON.stringify({
13159
+ writeFileSync15(taskStatePath, `${JSON.stringify({
11982
13160
  schemaVersion: 1,
11983
13161
  baseTrackerCommit: typeof rawTaskState.baseTrackerCommit === "string" ? rawTaskState.baseTrackerCommit : null,
11984
13162
  tasks
@@ -12286,12 +13464,12 @@ function isAuthorizedLinearWebhookRequest(req) {
12286
13464
  }
12287
13465
 
12288
13466
  // packages/server/src/server-helpers/notifications.ts
12289
- import { existsSync as existsSync16, mkdirSync as mkdirSync15, readFileSync as readFileSync11 } from "fs";
12290
- import { dirname as dirname16 } from "path";
13467
+ import { existsSync as existsSync18, mkdirSync as mkdirSync17, readFileSync as readFileSync13 } from "fs";
13468
+ import { dirname as dirname19 } from "path";
12291
13469
  async function loadNotificationConfig(path) {
12292
- if (!existsSync16(path)) {
13470
+ if (!existsSync18(path)) {
12293
13471
  const defaultConfig = { targets: [] };
12294
- mkdirSync15(dirname16(path), { recursive: true });
13472
+ mkdirSync17(dirname19(path), { recursive: true });
12295
13473
  await Bun.write(path, `${JSON.stringify(defaultConfig, null, 2)}
12296
13474
  `);
12297
13475
  return defaultConfig;
@@ -12306,10 +13484,10 @@ async function loadNotificationConfig(path) {
12306
13484
  }
12307
13485
  }
12308
13486
  function readRecentEvents(file, limit) {
12309
- if (!existsSync16(file)) {
13487
+ if (!existsSync18(file)) {
12310
13488
  return [];
12311
13489
  }
12312
- const lines = readFileSync11(file, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-limit);
13490
+ const lines = readFileSync13(file, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-limit);
12313
13491
  const events = [];
12314
13492
  for (const line of lines) {
12315
13493
  try {
@@ -12404,11 +13582,11 @@ function extractObjectLiteralBlock(source, property) {
12404
13582
  }
12405
13583
  function readFallbackIssueAnalysisConfig(projectRoot) {
12406
13584
  for (const fileName of ["rig.config.ts", "rig.config.json"]) {
12407
- const path = resolve22(projectRoot, fileName);
12408
- if (!existsSync17(path))
13585
+ const path = resolve24(projectRoot, fileName);
13586
+ if (!existsSync19(path))
12409
13587
  continue;
12410
13588
  try {
12411
- const source = readFileSync12(path, "utf8");
13589
+ const source = readFileSync14(path, "utf8");
12412
13590
  if (fileName.endsWith(".json"))
12413
13591
  return JSON.parse(source);
12414
13592
  const issueBlock = extractObjectLiteralBlock(source, "issueAnalysis");
@@ -12541,8 +13719,8 @@ async function createIssueAnalysisRunnerForServerState(state, input) {
12541
13719
  async function withServerPathEnv(projectRoot, fn) {
12542
13720
  const waitForTurn = serverPathEnvQueue;
12543
13721
  let releaseTurn;
12544
- serverPathEnvQueue = new Promise((resolve23) => {
12545
- releaseTurn = resolve23;
13722
+ serverPathEnvQueue = new Promise((resolve25) => {
13723
+ releaseTurn = resolve25;
12546
13724
  });
12547
13725
  await waitForTurn;
12548
13726
  const paths = resolveServerAuthorityPaths(projectRoot);
@@ -12578,9 +13756,9 @@ async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
12578
13756
  return withServerPathEnv(projectRoot, fn);
12579
13757
  }
12580
13758
  async function readWorkspaceTasks(projectRoot) {
12581
- const issuesPath = resolve22(resolveMonorepoRoot6(projectRoot), ".beads", "issues.jsonl");
13759
+ const issuesPath = resolve24(resolveMonorepoRoot6(projectRoot), ".beads", "issues.jsonl");
12582
13760
  const taskConfig = await readTaskConfig(projectRoot);
12583
- if (!existsSync17(issuesPath)) {
13761
+ if (!existsSync19(issuesPath)) {
12584
13762
  return [];
12585
13763
  }
12586
13764
  const latestById = new Map;
@@ -12654,11 +13832,11 @@ function resolveTaskArtifactDirsFromRuns(projectRoot, taskId, knownRuns) {
12654
13832
  continue;
12655
13833
  add(run.artifactRoot);
12656
13834
  if (run.worktreePath) {
12657
- add(resolve22(run.worktreePath, "artifacts", taskId));
13835
+ add(resolve24(run.worktreePath, "artifacts", taskId));
12658
13836
  }
12659
13837
  }
12660
13838
  for (const artifactsRoot of listAuthorityArtifactRoots(projectRoot)) {
12661
- add(resolve22(artifactsRoot, taskId));
13839
+ add(resolve24(artifactsRoot, taskId));
12662
13840
  }
12663
13841
  return candidates;
12664
13842
  }
@@ -12672,7 +13850,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
12672
13850
  }
12673
13851
  }
12674
13852
  return taskIds.flatMap((currentTaskId) => {
12675
- const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) => existsSync17(path));
13853
+ const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) => existsSync19(path));
12676
13854
  if (!currentRoot) {
12677
13855
  return [];
12678
13856
  }
@@ -12684,7 +13862,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
12684
13862
  taskId: currentTaskId,
12685
13863
  kind: "file",
12686
13864
  label: fileName,
12687
- path: resolve22(currentRoot, fileName),
13865
+ path: resolve24(currentRoot, fileName),
12688
13866
  url: null,
12689
13867
  metadata: {
12690
13868
  fileName
@@ -12727,11 +13905,11 @@ function buildInspectorStreamPayload(state, sequence) {
12727
13905
  }
12728
13906
  function listRemoteRunArtifacts(projectRoot, runId) {
12729
13907
  const root = remoteArtifactsRoot(projectRoot, runId);
12730
- if (!existsSync17(root)) {
13908
+ if (!existsSync19(root)) {
12731
13909
  return [];
12732
13910
  }
12733
13911
  return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isFile()).filter((entry) => !entry.name.endsWith(".json")).map((entry) => {
12734
- const artifactPath = resolve22(root, entry.name);
13912
+ const artifactPath = resolve24(root, entry.name);
12735
13913
  const stat = statSync6(artifactPath);
12736
13914
  const meta = readJsonFile4(`${artifactPath}.json`, null);
12737
13915
  return {
@@ -12892,6 +14070,7 @@ function buildHttpRouterDeps(state) {
12892
14070
  startLocalRun,
12893
14071
  stopRunRecord,
12894
14072
  resumeRunRecord,
14073
+ runServerOwnedPrCloseout,
12895
14074
  claimRemoteRun,
12896
14075
  listRemoteRunArtifacts,
12897
14076
  broadcastSnapshotInvalidation,
@@ -12973,8 +14152,8 @@ function fileStats(path) {
12973
14152
  }
12974
14153
  }
12975
14154
  function runFileCursor(projectRoot, run) {
12976
- const runDir = dirname17(runLogsPath(projectRoot, run.runId));
12977
- const runJson = fileStats(resolve22(runDir, "run.json"));
14155
+ const runDir = dirname20(runLogsPath(projectRoot, run.runId));
14156
+ const runJson = fileStats(resolve24(runDir, "run.json"));
12978
14157
  const timeline = fileStats(runTimelinePath(projectRoot, run.runId));
12979
14158
  const logs = fileStats(runLogsPath(projectRoot, run.runId));
12980
14159
  return {
@@ -13024,10 +14203,10 @@ function startRunFileWatcher(state, pollMs) {
13024
14203
  }, Math.max(250, Math.min(pollMs, 1000)));
13025
14204
  }
13026
14205
  function startPoller(state, pollMs) {
13027
- let offset = existsSync17(state.eventsFile) ? statSync6(state.eventsFile).size : 0;
14206
+ let offset = existsSync19(state.eventsFile) ? statSync6(state.eventsFile).size : 0;
13028
14207
  return setInterval(async () => {
13029
14208
  try {
13030
- if (!existsSync17(state.eventsFile)) {
14209
+ if (!existsSync19(state.eventsFile)) {
13031
14210
  return;
13032
14211
  }
13033
14212
  const file = await open(state.eventsFile, "r");
@@ -13079,6 +14258,7 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
13079
14258
  const server = Bun.serve({
13080
14259
  hostname: options.host,
13081
14260
  port: options.port,
14261
+ idleTimeout: Math.max(10, Math.min(255, Number.parseInt(process.env.RIG_SERVER_IDLE_TIMEOUT_SECONDS || "255", 10) || 255)),
13082
14262
  fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
13083
14263
  websocket: {
13084
14264
  open(ws) {
@@ -13154,7 +14334,7 @@ function resolveProjectRoot() {
13154
14334
  return resolveRigProjectRoot({
13155
14335
  envProjectRoot: process.env.PROJECT_RIG_ROOT ?? null,
13156
14336
  cwd: process.cwd(),
13157
- fallbackRoot: resolve22(import.meta.dir, "../..")
14337
+ fallbackRoot: resolve24(import.meta.dir, "../..")
13158
14338
  });
13159
14339
  }
13160
14340
  var __testOnly = {
@@ -13215,6 +14395,7 @@ export {
13215
14395
  resolveRigServerPaths,
13216
14396
  resolveRigProjectRoot,
13217
14397
  resolvePublishedRigServerStatePath,
14398
+ resolveProjectStatusField,
13218
14399
  resolveProjectRoot,
13219
14400
  registerRemoteHost,
13220
14401
  readWorkspaceTasks,
@@ -13223,6 +14404,7 @@ export {
13223
14404
  parseRigServerArgs,
13224
14405
  parseArgs,
13225
14406
  main,
14407
+ listGitHubProjects,
13226
14408
  heartbeatRemoteHost,
13227
14409
  handleWebSocketUpgrade,
13228
14410
  encodeWebSocketPayload,