@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.
@@ -1,11 +1,12 @@
1
+ #!/usr/bin/env bun
1
2
  // @bun
2
3
  var __require = import.meta.require;
3
4
 
4
5
  // packages/server/src/server.ts
5
6
  import { spawn as spawn5 } from "child_process";
6
- import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync6 } from "fs";
7
+ import { existsSync as existsSync19, readdirSync as readdirSync5, readFileSync as readFileSync14, statSync as statSync6 } from "fs";
7
8
  import { open } from "fs/promises";
8
- import { dirname as dirname17, resolve as resolve22 } from "path";
9
+ import { dirname as dirname20, resolve as resolve24 } from "path";
9
10
  import {
10
11
  listAuthorityArtifactRoots,
11
12
  listAuthorityRuns as listAuthorityRuns7,
@@ -1073,6 +1074,18 @@ function readJsonlFileTail(path, options) {
1073
1074
  const completeLines = start > 0 ? lines.slice(1) : lines;
1074
1075
  return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
1075
1076
  }
1077
+ async function readRunTimelinePage(projectRoot, runId, options = {}) {
1078
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
1079
+ const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
1080
+ const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
1081
+ const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
1082
+ const endExclusive = Math.min(entries.length, startInclusive + limit);
1083
+ return {
1084
+ entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
1085
+ nextCursor: String(endExclusive),
1086
+ hasMore: endExclusive < entries.length
1087
+ };
1088
+ }
1076
1089
  var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
1077
1090
  async function readRunLogsPage(projectRoot, runId, options = {}) {
1078
1091
  const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
@@ -2312,7 +2325,6 @@ function createGitHubTaskReconciler(input) {
2312
2325
  }
2313
2326
 
2314
2327
  // packages/server/src/server-helpers/issue-analysis.ts
2315
- import { execFile } from "child_process";
2316
2328
  import { createHash } from "crypto";
2317
2329
  function stableIssueHash(issue) {
2318
2330
  const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
@@ -2446,16 +2458,33 @@ function parseIssueAnalysisResult(raw) {
2446
2458
  return result;
2447
2459
  }
2448
2460
  function createDefaultPiIssueAnalysisCommandRunner() {
2449
- return (command, args, options) => new Promise((resolve11) => {
2450
- execFile(command, [...args], {
2451
- timeout: options.timeoutMs,
2452
- maxBuffer: 10 * 1024 * 1024,
2453
- env: options.env ? { ...process.env, ...options.env } : process.env
2454
- }, (error, stdout, stderr) => {
2455
- const exitCode = typeof error?.code === "number" ? error.code : error ? 1 : 0;
2456
- resolve11({ exitCode, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
2461
+ return async (command, args, options) => {
2462
+ const env = options.env ? { ...process.env, ...options.env } : process.env;
2463
+ const proc = Bun.spawn([command, ...args], {
2464
+ stdout: "pipe",
2465
+ stderr: "pipe",
2466
+ env
2457
2467
  });
2458
- });
2468
+ let timedOut = false;
2469
+ const timer = setTimeout(() => {
2470
+ timedOut = true;
2471
+ proc.kill();
2472
+ }, options.timeoutMs);
2473
+ try {
2474
+ const [stdout, stderr, exitCode] = await Promise.all([
2475
+ new Response(proc.stdout).text(),
2476
+ new Response(proc.stderr).text(),
2477
+ proc.exited
2478
+ ]);
2479
+ return {
2480
+ exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
2481
+ stdout,
2482
+ stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
2483
+ };
2484
+ } finally {
2485
+ clearTimeout(timer);
2486
+ }
2487
+ };
2459
2488
  }
2460
2489
  function createPiIssueAnalyzer(input = {}) {
2461
2490
  const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
@@ -2463,7 +2492,10 @@ function createPiIssueAnalyzer(input = {}) {
2463
2492
  const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
2464
2493
  return async ({ prompt }) => {
2465
2494
  const args = ["--print", "--mode", "json", "--no-session"];
2466
- const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || "openai-codex/gpt-5.5";
2495
+ const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
2496
+ const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
2497
+ if (provider)
2498
+ args.push("--provider", provider);
2467
2499
  if (model)
2468
2500
  args.push("--model", model);
2469
2501
  args.push(prompt);
@@ -2711,8 +2743,7 @@ function buildRunStartPatch(startedAt) {
2711
2743
  status: "preparing",
2712
2744
  startedAt,
2713
2745
  completedAt: null,
2714
- errorText: null,
2715
- serverPid: process.pid
2746
+ errorText: null
2716
2747
  };
2717
2748
  }
2718
2749
 
@@ -3181,7 +3212,7 @@ function applyOrchestrationCommand(state, command) {
3181
3212
  import { spawn as spawn3 } from "child_process";
3182
3213
  import { loadConfig } from "@rig/core/load-config";
3183
3214
  import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
3184
- import { dirname as dirname8, relative as relative2, resolve as resolve14 } from "path";
3215
+ import { dirname as dirname9, relative as relative2, resolve as resolve14 } from "path";
3185
3216
  import {
3186
3217
  listAuthorityRuns as listAuthorityRuns4,
3187
3218
  readAuthorityRun as readAuthorityRun4,
@@ -3194,6 +3225,11 @@ import {
3194
3225
  buildTaskRunLifecycleComment,
3195
3226
  updateConfiguredTaskSourceTask
3196
3227
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
3228
+ import {
3229
+ closeIssueAfterMergedPr,
3230
+ commitRunChanges,
3231
+ runPrAutomation
3232
+ } from "@rig/runtime/control-plane/native/pr-automation";
3197
3233
 
3198
3234
  // packages/server/src/scheduler.ts
3199
3235
  import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
@@ -3305,8 +3341,8 @@ function summarizeRunValidationFailure(projectRoot, run) {
3305
3341
 
3306
3342
  // packages/server/src/server-helpers/github-auth-store.ts
3307
3343
  import { randomBytes } from "crypto";
3308
- import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
3309
- import { resolve as resolve13 } from "path";
3344
+ import { chmodSync, copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
3345
+ import { dirname as dirname8, resolve as resolve13 } from "path";
3310
3346
  function cleanString(value) {
3311
3347
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
3312
3348
  }
@@ -3336,6 +3372,26 @@ function parseApiSessions(value) {
3336
3372
  }];
3337
3373
  });
3338
3374
  }
3375
+ function parsePendingDevice(value) {
3376
+ if (!value || typeof value !== "object")
3377
+ return null;
3378
+ const record = value;
3379
+ const pollId = cleanString(record.pollId);
3380
+ const deviceCode = cleanString(record.deviceCode);
3381
+ const expiresAt = cleanString(record.expiresAt);
3382
+ const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
3383
+ if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
3384
+ return null;
3385
+ return { pollId, deviceCode, expiresAt, intervalSeconds };
3386
+ }
3387
+ function parsePendingDevices(value) {
3388
+ if (!Array.isArray(value))
3389
+ return [];
3390
+ return value.flatMap((entry) => {
3391
+ const pending = parsePendingDevice(entry);
3392
+ return pending ? [pending] : [];
3393
+ });
3394
+ }
3339
3395
  function readStoredAuth(stateFile) {
3340
3396
  if (!existsSync6(stateFile))
3341
3397
  return {};
@@ -3349,6 +3405,7 @@ function readStoredAuth(stateFile) {
3349
3405
  selectedRepo: cleanString(parsed.selectedRepo),
3350
3406
  tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
3351
3407
  pendingDevice: parsePendingDevice(parsed.pendingDevice),
3408
+ pendingDevices: parsePendingDevices(parsed.pendingDevices),
3352
3409
  apiSessions: parseApiSessions(parsed.apiSessions),
3353
3410
  updatedAt: cleanString(parsed.updatedAt) ?? undefined
3354
3411
  };
@@ -3356,34 +3413,36 @@ function readStoredAuth(stateFile) {
3356
3413
  return {};
3357
3414
  }
3358
3415
  }
3359
- function parsePendingDevice(value) {
3360
- if (!value || typeof value !== "object")
3361
- return null;
3362
- const record = value;
3363
- const pollId = cleanString(record.pollId);
3364
- const deviceCode = cleanString(record.deviceCode);
3365
- const expiresAt = cleanString(record.expiresAt);
3366
- const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
3367
- if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
3368
- return null;
3369
- return { pollId, deviceCode, expiresAt, intervalSeconds };
3370
- }
3371
3416
  function newApiSessionToken() {
3372
3417
  return `rig_${randomBytes(32).toString("base64url")}`;
3373
3418
  }
3374
3419
  function writeStoredAuth(stateFile, payload) {
3375
- mkdirSync6(resolve13(stateFile, ".."), { recursive: true });
3420
+ mkdirSync6(dirname8(stateFile), { recursive: true });
3376
3421
  writeFileSync5(stateFile, `${JSON.stringify(payload, null, 2)}
3377
3422
  `, { encoding: "utf8", mode: 384 });
3378
3423
  try {
3379
3424
  chmodSync(stateFile, 384);
3380
3425
  } catch {}
3381
3426
  }
3427
+ function localProjectAuthStateFile(projectRoot) {
3428
+ return resolve13(projectRoot, ".rig", "state", "github-auth.json");
3429
+ }
3382
3430
  function resolveGitHubAuthStateFile(projectRoot) {
3383
3431
  return resolve13(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
3384
3432
  }
3385
- function createGitHubAuthStore(projectRoot) {
3386
- const stateFile = resolveGitHubAuthStateFile(projectRoot);
3433
+ function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
3434
+ const targetFile = localProjectAuthStateFile(projectRoot);
3435
+ mkdirSync6(dirname8(targetFile), { recursive: true });
3436
+ if (existsSync6(stateFile)) {
3437
+ copyFileSync(stateFile, targetFile);
3438
+ try {
3439
+ chmodSync(targetFile, 384);
3440
+ } catch {}
3441
+ return;
3442
+ }
3443
+ writeStoredAuth(targetFile, {});
3444
+ }
3445
+ function createGitHubAuthStoreFromStateFile(stateFile) {
3387
3446
  return {
3388
3447
  stateFile,
3389
3448
  status(options) {
@@ -3413,6 +3472,7 @@ function createGitHubAuthStore(projectRoot) {
3413
3472
  scopes: input.scopes ?? [],
3414
3473
  selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
3415
3474
  pendingDevice: null,
3475
+ pendingDevices: [],
3416
3476
  apiSessions: previous.apiSessions ?? [],
3417
3477
  updatedAt: new Date().toISOString()
3418
3478
  });
@@ -3441,15 +3501,24 @@ function createGitHubAuthStore(projectRoot) {
3441
3501
  const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
3442
3502
  return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
3443
3503
  },
3444
- copyToProjectRoot(projectRoot2) {
3445
- const targetFile = resolveGitHubAuthStateFile(projectRoot2);
3504
+ copyToProjectRoot(projectRoot) {
3505
+ const targetFile = resolveGitHubAuthStateFile(projectRoot);
3446
3506
  writeStoredAuth(targetFile, readStoredAuth(stateFile));
3447
3507
  },
3508
+ copyToLocalProjectRoot(projectRoot) {
3509
+ copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
3510
+ },
3448
3511
  savePendingDevice(input) {
3449
3512
  const previous = readStoredAuth(stateFile);
3513
+ const pendingDevices = [
3514
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
3515
+ ...previous.pendingDevices ?? [],
3516
+ input
3517
+ ].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
3450
3518
  writeStoredAuth(stateFile, {
3451
3519
  ...previous,
3452
- pendingDevice: input,
3520
+ pendingDevice: null,
3521
+ pendingDevices,
3453
3522
  updatedAt: new Date().toISOString()
3454
3523
  });
3455
3524
  },
@@ -3462,23 +3531,32 @@ function createGitHubAuthStore(projectRoot) {
3462
3531
  });
3463
3532
  },
3464
3533
  readPendingDevice(pollId) {
3465
- const pending = readStoredAuth(stateFile).pendingDevice ?? null;
3466
- if (!pending || pending.pollId !== pollId)
3534
+ const previous = readStoredAuth(stateFile);
3535
+ const pending = [
3536
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
3537
+ ...previous.pendingDevices ?? []
3538
+ ].find((entry) => entry.pollId === pollId) ?? null;
3539
+ if (!pending)
3467
3540
  return null;
3468
3541
  if (Date.parse(pending.expiresAt) <= Date.now())
3469
3542
  return null;
3470
3543
  return pending;
3471
3544
  },
3472
- clearPendingDevice() {
3545
+ clearPendingDevice(pollId) {
3473
3546
  const previous = readStoredAuth(stateFile);
3547
+ const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
3474
3548
  writeStoredAuth(stateFile, {
3475
3549
  ...previous,
3476
3550
  pendingDevice: null,
3551
+ pendingDevices: remaining,
3477
3552
  updatedAt: new Date().toISOString()
3478
3553
  });
3479
3554
  }
3480
3555
  };
3481
3556
  }
3557
+ function createGitHubAuthStore(projectRoot) {
3558
+ return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
3559
+ }
3482
3560
 
3483
3561
  // packages/server/src/server-helpers/github-projects.ts
3484
3562
  function asRecord(value) {
@@ -3487,6 +3565,9 @@ function asRecord(value) {
3487
3565
  function asString(value) {
3488
3566
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
3489
3567
  }
3568
+ function asNumber(value) {
3569
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
3570
+ }
3490
3571
  async function defaultGraphQLFetch(query, variables, token) {
3491
3572
  const response = await fetch("https://api.github.com/graphql", {
3492
3573
  method: "POST",
@@ -3503,6 +3584,32 @@ async function defaultGraphQLFetch(query, variables, token) {
3503
3584
  }
3504
3585
  return json.data;
3505
3586
  }
3587
+ function projectNodesFrom(data) {
3588
+ const root = asRecord(data);
3589
+ const owner = asRecord(root?.organization) ?? asRecord(root?.user);
3590
+ const projects = asRecord(owner?.projectsV2);
3591
+ const nodes = projects?.nodes;
3592
+ return Array.isArray(nodes) ? nodes : [];
3593
+ }
3594
+ async function listGitHubProjects(input) {
3595
+ const query = `
3596
+ query RigListProjects($owner: String!, $first: Int!) {
3597
+ organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
3598
+ user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
3599
+ }
3600
+ `;
3601
+ const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
3602
+ const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
3603
+ return projectNodesFrom(data).flatMap((node) => {
3604
+ const record = asRecord(node);
3605
+ const id = asString(record?.id);
3606
+ const number = asNumber(record?.number);
3607
+ const title = asString(record?.title);
3608
+ if (!id || number === undefined || !title)
3609
+ return [];
3610
+ return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
3611
+ });
3612
+ }
3506
3613
  async function resolveProjectStatusField(input) {
3507
3614
  const query = `
3508
3615
  query RigProjectStatusField($projectId: ID!) {
@@ -3597,6 +3704,7 @@ var DEFAULT_PROJECT_STATUSES = {
3597
3704
  running: "In Progress",
3598
3705
  prOpen: "In Review",
3599
3706
  ciFixing: "In Review",
3707
+ merging: "Merging",
3600
3708
  done: "Done",
3601
3709
  needsAttention: "Needs Attention"
3602
3710
  };
@@ -3610,6 +3718,8 @@ function lifecycleStatusForTaskStatus(status) {
3610
3718
  return "prOpen";
3611
3719
  if (normalized === "ci_fixing" || normalized === "fixing")
3612
3720
  return "ciFixing";
3721
+ if (normalized === "merging" || normalized === "merge")
3722
+ return "merging";
3613
3723
  if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
3614
3724
  return "needsAttention";
3615
3725
  if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
@@ -3738,9 +3848,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
3738
3848
  return null;
3739
3849
  return null;
3740
3850
  }
3851
+ function githubProjectsEnabled(config) {
3852
+ const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
3853
+ const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
3854
+ return projects?.enabled === true;
3855
+ }
3741
3856
  async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
3742
3857
  if (!run.taskId)
3743
- return;
3858
+ return false;
3744
3859
  const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
3745
3860
  try {
3746
3861
  const result = await syncGitHubProjectStatusForTaskUpdate({
@@ -3751,28 +3866,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
3751
3866
  config
3752
3867
  });
3753
3868
  if (!result.synced && result.reason !== "project-sync-disabled") {
3869
+ const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
3754
3870
  appendRunLogEntry(projectRoot, run.runId, {
3755
3871
  id: `log:${run.runId}:github-project-sync:${status}`,
3756
3872
  title: "GitHub Project sync skipped",
3757
- detail: `Project status sync for ${run.taskId} could not run: ${result.reason}.`,
3873
+ detail,
3758
3874
  tone: "warn",
3759
3875
  status: "running",
3760
3876
  createdAt: new Date().toISOString(),
3761
3877
  payload: { reason: result.reason, issueNodeId }
3762
3878
  });
3879
+ if (githubProjectsEnabled(config)) {
3880
+ throw new Error(detail);
3881
+ }
3882
+ return false;
3763
3883
  }
3884
+ return result.synced === true;
3764
3885
  } catch (error) {
3886
+ const detail = error instanceof Error ? error.message : String(error);
3765
3887
  appendRunLogEntry(projectRoot, run.runId, {
3766
3888
  id: `log:${run.runId}:github-project-sync-error:${status}`,
3767
3889
  title: "GitHub Project sync failed",
3768
- detail: error instanceof Error ? error.message : String(error),
3890
+ detail,
3769
3891
  tone: "error",
3770
3892
  status: "running",
3771
3893
  createdAt: new Date().toISOString(),
3772
3894
  payload: { issueNodeId }
3773
3895
  });
3896
+ if (githubProjectsEnabled(config)) {
3897
+ throw new Error(detail);
3898
+ }
3899
+ return false;
3774
3900
  }
3775
3901
  }
3902
+ function createCommandRunner(binary, extraEnv = {}) {
3903
+ return async (args, options) => {
3904
+ const child = spawn3(binary, [...args], {
3905
+ cwd: options?.cwd,
3906
+ env: { ...process.env, ...extraEnv },
3907
+ stdio: ["ignore", "pipe", "pipe"]
3908
+ });
3909
+ const stdoutChunks = [];
3910
+ const stderrChunks = [];
3911
+ child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
3912
+ child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
3913
+ const exitCode = await new Promise((resolve15) => {
3914
+ child.once("error", () => resolve15(1));
3915
+ child.once("close", (code) => resolve15(code ?? 1));
3916
+ });
3917
+ return {
3918
+ exitCode,
3919
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
3920
+ stderr: Buffer.concat(stderrChunks).toString("utf8")
3921
+ };
3922
+ };
3923
+ }
3924
+ function closeoutRecord(run) {
3925
+ const value = run.serverCloseout;
3926
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
3927
+ }
3928
+ function closeoutPhasePatch(phase, status, extra = {}) {
3929
+ const updatedAt = new Date().toISOString();
3930
+ return {
3931
+ serverCloseout: {
3932
+ phase,
3933
+ status,
3934
+ updatedAt,
3935
+ ...extra
3936
+ }
3937
+ };
3938
+ }
3939
+ function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
3940
+ appendRunLogEntryAndBroadcast(state, runId, {
3941
+ id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
3942
+ title: `Server closeout: ${phase}`,
3943
+ detail,
3944
+ tone,
3945
+ status,
3946
+ createdAt: new Date().toISOString()
3947
+ }, `server-closeout-${phase}`);
3948
+ }
3776
3949
  async function autoAssignRunIssue(projectRoot, run) {
3777
3950
  if (!run.taskId)
3778
3951
  return;
@@ -3802,7 +3975,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
3802
3975
  return;
3803
3976
  }
3804
3977
  const config = await loadRigLifecycleConfig(projectRoot);
3805
- await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
3978
+ const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
3806
3979
  if (status === "in_progress") {
3807
3980
  await autoAssignRunIssue(projectRoot, run);
3808
3981
  }
@@ -3818,24 +3991,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
3818
3991
  });
3819
3992
  return;
3820
3993
  }
3821
- const result = await updateConfiguredTaskSourceTask(projectRoot, {
3822
- taskId: run.taskId,
3823
- sourceTask: runSourceTaskIdentity(run),
3824
- update: {
3825
- status,
3826
- comment: buildTaskRunLifecycleComment({
3827
- runId: run.runId,
3994
+ const sourceTask = runSourceTaskIdentity(run);
3995
+ const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
3996
+ const rollbackProjectSync = async () => {
3997
+ if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
3998
+ return;
3999
+ await syncGitHubProjectStatusForTaskUpdate({
4000
+ taskId: run.taskId,
4001
+ status: previousStatus,
4002
+ issueNodeId: extractGitHubIssueNodeId(sourceTask),
4003
+ token: createGitHubAuthStore(projectRoot).readToken(),
4004
+ config
4005
+ }).catch((rollbackError) => {
4006
+ appendRunLogEntry(projectRoot, run.runId, {
4007
+ id: `log:${run.runId}:github-project-sync-rollback:${status}`,
4008
+ title: "GitHub Project sync rollback failed",
4009
+ detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
4010
+ tone: "error",
4011
+ status: "running",
4012
+ createdAt: new Date().toISOString()
4013
+ });
4014
+ });
4015
+ };
4016
+ let result;
4017
+ try {
4018
+ result = await updateConfiguredTaskSourceTask(projectRoot, {
4019
+ taskId: run.taskId,
4020
+ sourceTask,
4021
+ update: {
3828
4022
  status,
3829
- summary,
3830
- runtimeWorkspace: normalizeString(run.worktreePath),
3831
- logsDir: normalizeString(run.logRoot),
3832
- sessionDir: normalizeString(run.sessionPath),
3833
- errorText: options.errorText ?? normalizeString(run.errorText)
3834
- })
3835
- }
3836
- });
4023
+ comment: buildTaskRunLifecycleComment({
4024
+ runId: run.runId,
4025
+ status,
4026
+ summary,
4027
+ runtimeWorkspace: normalizeString(run.worktreePath),
4028
+ logsDir: normalizeString(run.logRoot),
4029
+ sessionDir: normalizeString(run.sessionPath),
4030
+ errorText: options.errorText ?? normalizeString(run.errorText)
4031
+ })
4032
+ }
4033
+ });
4034
+ } catch (error) {
4035
+ await rollbackProjectSync();
4036
+ throw error;
4037
+ }
3837
4038
  if (!result.updated) {
3838
4039
  if (result.source === "plugin" || result.sourceKind) {
4040
+ await rollbackProjectSync();
3839
4041
  throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
3840
4042
  }
3841
4043
  appendRunLogEntry(projectRoot, run.runId, {
@@ -3848,6 +4050,219 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
3848
4050
  });
3849
4051
  }
3850
4052
  }
4053
+ async function runServerOwnedPrCloseout(state, runId) {
4054
+ const run = readAuthorityRun4(state.projectRoot, runId);
4055
+ if (!run)
4056
+ throw new Error(`Run not found: ${runId}`);
4057
+ const closeout = closeoutRecord(run);
4058
+ if (!closeout)
4059
+ return;
4060
+ const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
4061
+ if (!taskId)
4062
+ throw new Error("Server-owned closeout requires a task id.");
4063
+ const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
4064
+ const branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
4065
+ const config = await loadRigLifecycleConfig(state.projectRoot);
4066
+ const runPrMode = normalizeString(run.prMode);
4067
+ const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
4068
+ const effectiveConfig = {
4069
+ ...config ?? {},
4070
+ pr: {
4071
+ ...config?.pr ?? {},
4072
+ mode: prMode,
4073
+ autoFixChecks: false,
4074
+ autoFixReview: false
4075
+ }
4076
+ };
4077
+ const readCurrentRun = () => readAuthorityRun4(state.projectRoot, runId) ?? run;
4078
+ const sourceTask = runSourceTaskIdentity(run);
4079
+ const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
4080
+ const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
4081
+ const closeoutPrUrl = normalizeString(closeout.prUrl);
4082
+ if (closeoutPhase === "completed" || closeoutStatus === "completed") {
4083
+ return;
4084
+ }
4085
+ if (closeoutPhase === "close-source" && closeoutPrUrl) {
4086
+ patchRunRecord(state.projectRoot, runId, {
4087
+ status: "reviewing",
4088
+ ...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
4089
+ });
4090
+ await closeIssueAfterMergedPr({
4091
+ projectRoot: state.projectRoot,
4092
+ taskId,
4093
+ runId,
4094
+ prUrl: closeoutPrUrl,
4095
+ sourceTask,
4096
+ updateTaskSource: async (projectRoot, input) => {
4097
+ await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
4098
+ return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
4099
+ }
4100
+ });
4101
+ const completedAt = new Date().toISOString();
4102
+ patchRunRecord(state.projectRoot, runId, {
4103
+ status: "completed",
4104
+ completedAt,
4105
+ errorText: null,
4106
+ ...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
4107
+ });
4108
+ appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
4109
+ emitRigEvent(state, {
4110
+ type: "rig.run.completed",
4111
+ aggregateId: runId,
4112
+ payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
4113
+ createdAt: completedAt
4114
+ });
4115
+ return;
4116
+ }
4117
+ if (prMode === "off" || prMode === "ask") {
4118
+ const completedAt = new Date().toISOString();
4119
+ patchRunRecord(state.projectRoot, runId, {
4120
+ status: "completed",
4121
+ completedAt,
4122
+ errorText: null,
4123
+ ...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
4124
+ });
4125
+ appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
4126
+ emitRigEvent(state, {
4127
+ type: "rig.run.completed",
4128
+ aggregateId: runId,
4129
+ payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
4130
+ createdAt: completedAt
4131
+ });
4132
+ return;
4133
+ }
4134
+ const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
4135
+ const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
4136
+ const gitCommand = createCommandRunner("git", githubEnv);
4137
+ const ghCommand = createCommandRunner("gh", githubEnv);
4138
+ const setCloseout = (phase, status, extra = {}) => {
4139
+ const previous = closeoutRecord(readCurrentRun()) ?? closeout;
4140
+ patchRunRecord(state.projectRoot, runId, {
4141
+ status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
4142
+ ...closeoutPhasePatch(phase, status, { ...previous, ...extra })
4143
+ });
4144
+ };
4145
+ setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
4146
+ appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
4147
+ const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
4148
+ appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
4149
+ setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
4150
+ const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
4151
+ if (push.exitCode !== 0) {
4152
+ throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
4153
+ }
4154
+ const sourceTaskForPr = {
4155
+ title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
4156
+ };
4157
+ const artifactRoot = resolve14(state.projectRoot, "artifacts", taskId);
4158
+ setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4159
+ const pr = await runPrAutomation({
4160
+ projectRoot: workspace,
4161
+ taskId,
4162
+ runId,
4163
+ branch,
4164
+ config: effectiveConfig,
4165
+ sourceTask: sourceTaskForPr,
4166
+ artifactRoot,
4167
+ command: ghCommand,
4168
+ gitCommand,
4169
+ steerPi: async (message) => {
4170
+ appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
4171
+ appendRunTimelineEntry(state.projectRoot, runId, {
4172
+ id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
4173
+ type: "user_message",
4174
+ text: message,
4175
+ createdAt: new Date().toISOString(),
4176
+ state: "completed"
4177
+ });
4178
+ },
4179
+ lifecycle: {
4180
+ onPrOpened: async ({ prUrl }) => {
4181
+ setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4182
+ appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
4183
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
4184
+ },
4185
+ onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
4186
+ onFeedback: async ({ feedback }) => {
4187
+ appendCloseoutStage(state, runId, "feedback", feedback.join(`
4188
+ `), "reviewing", "error");
4189
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
4190
+ },
4191
+ onMergeStarted: async ({ prUrl }) => {
4192
+ setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4193
+ appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
4194
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
4195
+ },
4196
+ onMerged: ({ prUrl }) => {
4197
+ setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
4198
+ appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
4199
+ }
4200
+ }
4201
+ });
4202
+ if (pr.status === "merged" && pr.prUrl) {
4203
+ setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
4204
+ await closeIssueAfterMergedPr({
4205
+ projectRoot: state.projectRoot,
4206
+ taskId,
4207
+ runId,
4208
+ prUrl: pr.prUrl,
4209
+ sourceTask,
4210
+ updateTaskSource: async (projectRoot, input) => {
4211
+ await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
4212
+ return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
4213
+ }
4214
+ });
4215
+ const completedAt = new Date().toISOString();
4216
+ patchRunRecord(state.projectRoot, runId, {
4217
+ status: "completed",
4218
+ completedAt,
4219
+ errorText: null,
4220
+ ...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
4221
+ });
4222
+ appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
4223
+ emitRigEvent(state, {
4224
+ type: "rig.run.completed",
4225
+ aggregateId: runId,
4226
+ payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
4227
+ createdAt: completedAt
4228
+ });
4229
+ return;
4230
+ }
4231
+ if (pr.status === "opened" && pr.prUrl) {
4232
+ const completedAt = new Date().toISOString();
4233
+ patchRunRecord(state.projectRoot, runId, {
4234
+ status: "completed",
4235
+ completedAt,
4236
+ errorText: null,
4237
+ ...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
4238
+ });
4239
+ appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
4240
+ emitRigEvent(state, {
4241
+ type: "rig.run.completed",
4242
+ aggregateId: runId,
4243
+ payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
4244
+ createdAt: completedAt
4245
+ });
4246
+ return;
4247
+ }
4248
+ const detail = pr.actionableFeedback.join(`
4249
+ `) || "PR automation did not merge the PR.";
4250
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
4251
+ appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
4252
+ });
4253
+ patchRunRecord(state.projectRoot, runId, {
4254
+ status: "needs_attention",
4255
+ completedAt: new Date().toISOString(),
4256
+ errorText: detail,
4257
+ ...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
4258
+ });
4259
+ appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
4260
+ emitRigEvent(state, {
4261
+ type: "rig.run.needs-attention",
4262
+ aggregateId: runId,
4263
+ payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
4264
+ });
4265
+ }
3851
4266
  var TERMINAL_RUN_STATUSES2 = new Set([
3852
4267
  "completed",
3853
4268
  "complete",
@@ -3873,11 +4288,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
3873
4288
  return;
3874
4289
  throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
3875
4290
  }
4291
+ async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
4292
+ const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
4293
+ if (fromReader)
4294
+ return fromReader;
4295
+ const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
4296
+ if (projected)
4297
+ return projected;
4298
+ if (readTasks !== readWorkspaceTasks) {
4299
+ return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
4300
+ }
4301
+ return null;
4302
+ }
3876
4303
  async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
3877
4304
  if ("taskId" in input && input.taskId) {
3878
4305
  assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
3879
4306
  }
3880
- const sourceTask = "taskId" in input && input.taskId ? (await readTasks(projectRoot)).find((task) => task.id === input.taskId) ?? null : null;
4307
+ const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
3881
4308
  const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
3882
4309
  const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
3883
4310
  const runRecord = {
@@ -3949,6 +4376,7 @@ async function startLocalRun(state, runId, options) {
3949
4376
  throw new Error(`Run not found: ${runId}`);
3950
4377
  }
3951
4378
  const startedAt = new Date().toISOString();
4379
+ const resumeMode = options?.resume === true;
3952
4380
  state.runProcesses.set(runId, {
3953
4381
  runId,
3954
4382
  child: null,
@@ -3965,9 +4393,9 @@ async function startLocalRun(state, runId, options) {
3965
4393
  summary: run.title
3966
4394
  });
3967
4395
  appendRunLogEntry(state.projectRoot, runId, {
3968
- id: `log:${runId}:prepare`,
3969
- title: "Rig task run starting",
3970
- detail: run.taskId ?? run.title,
4396
+ id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
4397
+ title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
4398
+ detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
3971
4399
  tone: "info",
3972
4400
  status: "preparing",
3973
4401
  createdAt: startedAt
@@ -4043,9 +4471,18 @@ async function startLocalRun(state, runId, options) {
4043
4471
  RIG_HOST_PROJECT_ROOT: cliProjectRoot,
4044
4472
  RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
4045
4473
  RIG_SERVER_INTERNAL_EXEC: "1",
4474
+ RIG_SERVER_OWNS_CLOSEOUT: "1",
4046
4475
  ...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
4047
4476
  ...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
4048
- ...bridgeGitHubToken ? { RIG_GITHUB_TOKEN: bridgeGitHubToken } : {}
4477
+ ...bridgeGitHubToken ? {
4478
+ RIG_GITHUB_TOKEN: bridgeGitHubToken,
4479
+ GITHUB_TOKEN: bridgeGitHubToken,
4480
+ GH_TOKEN: bridgeGitHubToken
4481
+ } : {},
4482
+ ...resumeMode ? {
4483
+ RIG_RUN_RESUME: "1",
4484
+ RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
4485
+ } : {}
4049
4486
  },
4050
4487
  stdio: ["ignore", "pipe", "pipe"]
4051
4488
  });
@@ -4131,6 +4568,38 @@ ${sourceFailure}` });
4131
4568
  agent: current.runtimeAdapter,
4132
4569
  summary: failureSummary
4133
4570
  });
4571
+ } else if (closeoutRecord(current)?.status === "pending") {
4572
+ try {
4573
+ await runServerOwnedPrCloseout(state, runId);
4574
+ } catch (closeoutError) {
4575
+ const closeoutFailure = closeoutError instanceof Error ? closeoutError.message : String(closeoutError);
4576
+ patchRunRecord(state.projectRoot, runId, {
4577
+ status: "failed",
4578
+ completedAt: new Date().toISOString(),
4579
+ errorText: closeoutFailure,
4580
+ ...closeoutPhasePatch("failed", "failed", { error: closeoutFailure })
4581
+ });
4582
+ appendRunLogEntryAndBroadcast(state, runId, {
4583
+ id: `log:${runId}:server-closeout-failed`,
4584
+ title: "Server-owned closeout failed",
4585
+ detail: closeoutFailure,
4586
+ tone: "error",
4587
+ status: "failed",
4588
+ createdAt: new Date().toISOString()
4589
+ }, "server-closeout-failed");
4590
+ if (current.taskId) {
4591
+ await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: closeoutFailure }, "failed", "Rig server-owned closeout failed.", { errorText: closeoutFailure }).catch((error) => {
4592
+ appendRunLogEntry(state.projectRoot, runId, {
4593
+ id: `log:${runId}:task-source-closeout-failed-update`,
4594
+ title: "Task source closeout failure update failed",
4595
+ detail: error instanceof Error ? error.message : String(error),
4596
+ tone: "error",
4597
+ status: "failed",
4598
+ createdAt: new Date().toISOString()
4599
+ });
4600
+ });
4601
+ }
4602
+ }
4134
4603
  }
4135
4604
  broadcastSnapshotInvalidation(state);
4136
4605
  } catch (error) {
@@ -4196,7 +4665,7 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
4196
4665
  }
4197
4666
  try {
4198
4667
  const monorepoRoot = resolveMonorepoRoot3(projectRoot);
4199
- const outerProjectRoot = dirname8(dirname8(monorepoRoot));
4668
+ const outerProjectRoot = dirname9(dirname9(monorepoRoot));
4200
4669
  if (existsSync7(resolve14(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
4201
4670
  return outerProjectRoot;
4202
4671
  }
@@ -4217,7 +4686,13 @@ async function resumeRunRecord(state, input) {
4217
4686
  if (run.status === "completed") {
4218
4687
  throw new Error("Completed runs cannot be resumed.");
4219
4688
  }
4220
- await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
4689
+ const closeout = closeoutRecord(run);
4690
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
4691
+ if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
4692
+ await runServerOwnedPrCloseout(state, input.runId);
4693
+ return;
4694
+ }
4695
+ await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
4221
4696
  }
4222
4697
  function appendRunMessage(projectRoot, input) {
4223
4698
  const run = readAuthorityRun4(projectRoot, input.runId);
@@ -4301,34 +4776,46 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
4301
4776
  writeQueueState(projectRoot, next);
4302
4777
  return next;
4303
4778
  }
4304
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
4305
- function reconcileOrphanedLocalRuns(state, runs, nowIso) {
4306
- let changed = false;
4307
- for (const run of runs) {
4308
- const status = normalizeString(run.status)?.toLowerCase() ?? "";
4309
- const serverPid = run.serverPid;
4310
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
4311
- if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
4312
- continue;
4313
- }
4314
- const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
4315
- patchRunRecord(state.projectRoot, run.runId, {
4316
- status: "failed",
4317
- completedAt: run.completedAt ?? nowIso,
4318
- updatedAt: nowIso,
4319
- errorText: detail
4320
- });
4321
- appendRunLogEntry(state.projectRoot, run.runId, {
4322
- id: `log:${run.runId}:stale-local-run`,
4323
- title: "Run marked stale after server restart",
4324
- detail,
4325
- tone: "error",
4326
- status: "failed",
4327
- createdAt: nowIso
4328
- });
4329
- changed = true;
4779
+ var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
4780
+ var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
4781
+ function processExists(pid) {
4782
+ if (!Number.isInteger(pid) || pid <= 0)
4783
+ return false;
4784
+ try {
4785
+ process.kill(pid, 0);
4786
+ return true;
4787
+ } catch {
4788
+ return false;
4330
4789
  }
4331
- return changed;
4790
+ }
4791
+ function recoverStaleLocalRun(projectRoot, run) {
4792
+ const record = run;
4793
+ if (run.mode !== "local")
4794
+ return false;
4795
+ const status = normalizeString(record.status)?.toLowerCase() ?? "";
4796
+ if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
4797
+ return false;
4798
+ const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
4799
+ const childPid = typeof record.pid === "number" ? record.pid : null;
4800
+ if (serverPid === null && childPid === null)
4801
+ return false;
4802
+ const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
4803
+ if (hasLiveRecordedProcess && serverPid === process.pid)
4804
+ return false;
4805
+ const completedAt = new Date().toISOString();
4806
+ patchRunRecord(projectRoot, run.runId, {
4807
+ status: "failed",
4808
+ completedAt,
4809
+ errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
4810
+ });
4811
+ return true;
4812
+ }
4813
+ function collectResumableServerCloseouts(state, runs) {
4814
+ return runs.filter((run) => {
4815
+ const closeout = closeoutRecord(run);
4816
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
4817
+ return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
4818
+ });
4332
4819
  }
4333
4820
  async function reconcileScheduler(state, reason) {
4334
4821
  if (state.scheduler.reconciling) {
@@ -4343,7 +4830,36 @@ async function reconcileScheduler(state, reason) {
4343
4830
  const queue = readQueueState(state.projectRoot);
4344
4831
  const tasks = await state.snapshotService.getWorkspaceTasks();
4345
4832
  let runs = listAuthorityRuns4(state.projectRoot);
4346
- let changed = reconcileOrphanedLocalRuns(state, runs, new Date().toISOString());
4833
+ let changed = false;
4834
+ for (const run of runs) {
4835
+ if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
4836
+ changed = true;
4837
+ }
4838
+ }
4839
+ if (changed) {
4840
+ runs = listAuthorityRuns4(state.projectRoot);
4841
+ }
4842
+ const resumableCloseouts = collectResumableServerCloseouts(state, runs);
4843
+ for (const run of resumableCloseouts) {
4844
+ appendRunLogEntry(state.projectRoot, run.runId, {
4845
+ id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
4846
+ title: "Server-owned closeout auto-resume scheduled",
4847
+ detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
4848
+ tone: "info",
4849
+ status: "reviewing",
4850
+ createdAt: new Date().toISOString()
4851
+ });
4852
+ await runServerOwnedPrCloseout(state, run.runId).catch((error) => {
4853
+ const detail = error instanceof Error ? error.message : String(error);
4854
+ patchRunRecord(state.projectRoot, run.runId, {
4855
+ status: "failed",
4856
+ completedAt: new Date().toISOString(),
4857
+ errorText: detail,
4858
+ ...closeoutPhasePatch("failed", "failed", { error: detail })
4859
+ });
4860
+ });
4861
+ changed = true;
4862
+ }
4347
4863
  if (changed) {
4348
4864
  runs = listAuthorityRuns4(state.projectRoot);
4349
4865
  }
@@ -4419,8 +4935,8 @@ async function reconcileScheduler(state, reason) {
4419
4935
  // packages/server/src/server-helpers/http-router.ts
4420
4936
  import { randomUUID } from "crypto";
4421
4937
  import { spawnSync as spawnSync3 } from "child_process";
4422
- import { basename, dirname as dirname12, isAbsolute as isAbsolute3, resolve as resolve18 } from "path";
4423
- import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync10 } from "fs";
4938
+ import { basename, dirname as dirname15, isAbsolute as isAbsolute4, resolve as resolve20 } from "path";
4939
+ import { copyFileSync as copyFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync12 } from "fs";
4424
4940
  import {
4425
4941
  listAuthorityRuns as listAuthorityRuns5,
4426
4942
  readAuthorityRun as readAuthorityRun6,
@@ -4444,7 +4960,7 @@ import {
4444
4960
  } from "@rig/runtime/control-plane/remote";
4445
4961
 
4446
4962
  // packages/server/src/server-helpers/run-steering.ts
4447
- import { dirname as dirname9, resolve as resolve15 } from "path";
4963
+ import { dirname as dirname10, resolve as resolve15 } from "path";
4448
4964
  import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync5 } from "fs";
4449
4965
  import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun5, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
4450
4966
  var steeringSequence = 0;
@@ -4531,7 +5047,7 @@ function queueRunSteeringMessage(projectRoot, runId, input) {
4531
5047
  delivered: false
4532
5048
  };
4533
5049
  const path = runSteeringPath(projectRoot, runId);
4534
- mkdirSync8(dirname9(path), { recursive: true });
5050
+ mkdirSync8(dirname10(path), { recursive: true });
4535
5051
  appendJsonlRecord2(path, entry);
4536
5052
  appendRunTimelineEntry(projectRoot, runId, {
4537
5053
  id: entry.id,
@@ -4568,6 +5084,187 @@ import {
4568
5084
  updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
4569
5085
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
4570
5086
 
5087
+ // packages/server/src/server-helpers/github-api-session-index.ts
5088
+ import { chmodSync as chmodSync3, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
5089
+ import { dirname as dirname12, resolve as resolve17 } from "path";
5090
+
5091
+ // packages/server/src/server-helpers/github-user-namespace.ts
5092
+ import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync8 } from "fs";
5093
+ import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve16 } from "path";
5094
+ function cleanString3(value) {
5095
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
5096
+ }
5097
+ function sanitizePathSegment(value) {
5098
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
5099
+ }
5100
+ function deriveGitHubUserNamespaceKey(identity) {
5101
+ const userId = cleanString3(identity.userId);
5102
+ if (userId) {
5103
+ const safeId = sanitizePathSegment(userId);
5104
+ if (safeId)
5105
+ return `ghu-${safeId}`;
5106
+ }
5107
+ const login = cleanString3(identity.login);
5108
+ if (login) {
5109
+ const safeLogin = sanitizePathSegment(login);
5110
+ if (safeLogin)
5111
+ return `ghu-login-${safeLogin}`;
5112
+ }
5113
+ throw new Error("GitHub user namespace requires a user id or login");
5114
+ }
5115
+ function resolveRemoteUserNamespacesRoot(projectRoot) {
5116
+ const explicitRoot = cleanString3(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
5117
+ if (explicitRoot)
5118
+ return resolve16(explicitRoot);
5119
+ const stateDir2 = cleanString3(process.env.RIG_STATE_DIR);
5120
+ if (stateDir2)
5121
+ return resolve16(dirname11(resolve16(stateDir2)), "users");
5122
+ return resolve16(projectRoot, ".rig", "users");
5123
+ }
5124
+ function resolveRemoteUserNamespace(projectRoot, identity) {
5125
+ const key = deriveGitHubUserNamespaceKey(identity);
5126
+ const root = resolve16(resolveRemoteUserNamespacesRoot(projectRoot), key);
5127
+ const stateDir2 = resolve16(root, ".rig", "state");
5128
+ return {
5129
+ key,
5130
+ userId: cleanString3(identity.userId),
5131
+ login: cleanString3(identity.login),
5132
+ root,
5133
+ stateDir: stateDir2,
5134
+ authStateFile: resolve16(stateDir2, "github-auth.json"),
5135
+ metadataFile: resolve16(stateDir2, "user-namespace.json"),
5136
+ checkoutBaseDir: resolve16(root, "remote-checkouts"),
5137
+ snapshotBaseDir: resolve16(root, "remote-snapshots")
5138
+ };
5139
+ }
5140
+ function serializeRemoteUserNamespace(namespace) {
5141
+ return {
5142
+ key: namespace.key,
5143
+ userId: namespace.userId,
5144
+ login: namespace.login,
5145
+ root: namespace.root,
5146
+ checkoutBaseDir: namespace.checkoutBaseDir,
5147
+ snapshotBaseDir: namespace.snapshotBaseDir
5148
+ };
5149
+ }
5150
+ function isPathInsideNamespace(namespaceRoot, candidatePath) {
5151
+ const root = resolve16(namespaceRoot);
5152
+ const candidate = resolve16(candidatePath);
5153
+ const rel = relative3(root, candidate);
5154
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
5155
+ }
5156
+ function writeRemoteUserNamespaceMetadata(namespace) {
5157
+ mkdirSync9(namespace.stateDir, { recursive: true });
5158
+ const previous = (() => {
5159
+ if (!existsSync9(namespace.metadataFile))
5160
+ return null;
5161
+ try {
5162
+ const parsed = JSON.parse(readFileSync6(namespace.metadataFile, "utf8"));
5163
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
5164
+ } catch {
5165
+ return null;
5166
+ }
5167
+ })();
5168
+ const now = new Date().toISOString();
5169
+ writeFileSync8(namespace.metadataFile, `${JSON.stringify({
5170
+ key: namespace.key,
5171
+ userId: namespace.userId,
5172
+ login: namespace.login,
5173
+ root: namespace.root,
5174
+ checkoutBaseDir: namespace.checkoutBaseDir,
5175
+ snapshotBaseDir: namespace.snapshotBaseDir,
5176
+ createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
5177
+ updatedAt: now
5178
+ }, null, 2)}
5179
+ `, { encoding: "utf8", mode: 384 });
5180
+ try {
5181
+ chmodSync2(namespace.metadataFile, 384);
5182
+ } catch {}
5183
+ }
5184
+
5185
+ // packages/server/src/server-helpers/github-api-session-index.ts
5186
+ function cleanString4(value) {
5187
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
5188
+ }
5189
+ function resolveGitHubApiSessionIndexFile(projectRoot) {
5190
+ return resolve17(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
5191
+ }
5192
+ function parseEntry(value) {
5193
+ if (!value || typeof value !== "object" || Array.isArray(value))
5194
+ return null;
5195
+ const record = value;
5196
+ const token = cleanString4(record.token);
5197
+ const namespaceKey = cleanString4(record.namespaceKey);
5198
+ const namespaceRoot = cleanString4(record.namespaceRoot);
5199
+ const authStateFile = cleanString4(record.authStateFile);
5200
+ const checkoutBaseDir = cleanString4(record.checkoutBaseDir);
5201
+ const snapshotBaseDir = cleanString4(record.snapshotBaseDir);
5202
+ const createdAt = cleanString4(record.createdAt);
5203
+ if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
5204
+ return null;
5205
+ return {
5206
+ token,
5207
+ namespaceKey,
5208
+ namespaceRoot,
5209
+ authStateFile,
5210
+ checkoutBaseDir,
5211
+ snapshotBaseDir,
5212
+ createdAt,
5213
+ login: cleanString4(record.login),
5214
+ userId: cleanString4(record.userId),
5215
+ selectedRepo: cleanString4(record.selectedRepo)
5216
+ };
5217
+ }
5218
+ function readIndex(indexFile) {
5219
+ if (!existsSync10(indexFile))
5220
+ return [];
5221
+ try {
5222
+ const parsed = JSON.parse(readFileSync7(indexFile, "utf8"));
5223
+ return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
5224
+ const parsedEntry = parseEntry(entry);
5225
+ return parsedEntry ? [parsedEntry] : [];
5226
+ }) : [];
5227
+ } catch {
5228
+ return [];
5229
+ }
5230
+ }
5231
+ function writeIndex(indexFile, sessions) {
5232
+ mkdirSync10(dirname12(indexFile), { recursive: true });
5233
+ writeFileSync9(indexFile, `${JSON.stringify({ sessions }, null, 2)}
5234
+ `, { encoding: "utf8", mode: 384 });
5235
+ try {
5236
+ chmodSync3(indexFile, 384);
5237
+ } catch {}
5238
+ }
5239
+ function registerGitHubApiSession(input) {
5240
+ const cleanToken = cleanString4(input.token);
5241
+ if (!cleanToken)
5242
+ throw new Error("GitHub API session token is required");
5243
+ const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
5244
+ const createdAt = new Date().toISOString();
5245
+ const entry = {
5246
+ token: cleanToken,
5247
+ login: input.namespace.login,
5248
+ userId: input.namespace.userId,
5249
+ namespaceKey: input.namespace.key,
5250
+ namespaceRoot: input.namespace.root,
5251
+ authStateFile: input.namespace.authStateFile,
5252
+ checkoutBaseDir: input.namespace.checkoutBaseDir,
5253
+ snapshotBaseDir: input.namespace.snapshotBaseDir,
5254
+ selectedRepo: cleanString4(input.selectedRepo),
5255
+ createdAt
5256
+ };
5257
+ const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
5258
+ writeIndex(indexFile, [...previous.slice(-199), entry]);
5259
+ return entry;
5260
+ }
5261
+ function readGitHubApiSession(input) {
5262
+ const cleanToken = cleanString4(input.token);
5263
+ if (!cleanToken)
5264
+ return null;
5265
+ return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
5266
+ }
5267
+
4571
5268
  // packages/server/src/server-helpers/inspector-agent-lifecycle.ts
4572
5269
  function createInspectorAgentLifecycleController(options) {
4573
5270
  const initialDelayMs = Math.max(1, options.initialDelayMs ?? 5000);
@@ -4671,21 +5368,21 @@ function inspectorAgentLifecycleSnapshot(input) {
4671
5368
  // packages/server/src/server-helpers/project-registry.ts
4672
5369
  import { createHash as createHash2 } from "crypto";
4673
5370
  import { spawnSync as spawnSync2 } from "child_process";
4674
- import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync6, readdirSync as readdirSync3, writeFileSync as writeFileSync8 } from "fs";
4675
- import { dirname as dirname10, resolve as resolve16 } from "path";
5371
+ import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync8, readdirSync as readdirSync3, writeFileSync as writeFileSync10 } from "fs";
5372
+ import { dirname as dirname13, resolve as resolve18 } from "path";
4676
5373
  function normalizeRepoSlug(value) {
4677
5374
  const trimmed = value.trim();
4678
5375
  return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
4679
5376
  }
4680
5377
  function registryPath(projectRoot) {
4681
- return resolve16(projectRoot, ".rig", "state", "projects.json");
5378
+ return resolve18(projectRoot, ".rig", "state", "projects.json");
4682
5379
  }
4683
5380
  function readRegistry(projectRoot) {
4684
5381
  const path = registryPath(projectRoot);
4685
- if (!existsSync9(path))
5382
+ if (!existsSync11(path))
4686
5383
  return {};
4687
5384
  try {
4688
- const payload = JSON.parse(readFileSync6(path, "utf8"));
5385
+ const payload = JSON.parse(readFileSync8(path, "utf8"));
4689
5386
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
4690
5387
  return {};
4691
5388
  const projects = payload.projects;
@@ -4696,14 +5393,14 @@ function readRegistry(projectRoot) {
4696
5393
  }
4697
5394
  function writeRegistry(projectRoot, projects) {
4698
5395
  const path = registryPath(projectRoot);
4699
- mkdirSync9(dirname10(path), { recursive: true });
4700
- writeFileSync8(path, `${JSON.stringify({ projects }, null, 2)}
5396
+ mkdirSync11(dirname13(path), { recursive: true });
5397
+ writeFileSync10(path, `${JSON.stringify({ projects }, null, 2)}
4701
5398
  `, "utf8");
4702
5399
  }
4703
5400
  function resolveConfigPath(projectRoot) {
4704
5401
  for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
4705
- const path = resolve16(projectRoot, name);
4706
- if (existsSync9(path))
5402
+ const path = resolve18(projectRoot, name);
5403
+ if (existsSync11(path))
4707
5404
  return path;
4708
5405
  }
4709
5406
  return null;
@@ -4712,7 +5409,7 @@ function hashFile(path) {
4712
5409
  if (!path)
4713
5410
  return null;
4714
5411
  try {
4715
- return createHash2("sha256").update(readFileSync6(path)).digest("hex");
5412
+ return createHash2("sha256").update(readFileSync8(path)).digest("hex");
4716
5413
  } catch {
4717
5414
  return null;
4718
5415
  }
@@ -4728,11 +5425,11 @@ function readDefaultBranch(projectRoot) {
4728
5425
  return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
4729
5426
  }
4730
5427
  function buildRunSummary(projectRoot) {
4731
- const runsDir = resolve16(projectRoot, ".rig", "runs");
5428
+ const runsDir = resolve18(projectRoot, ".rig", "runs");
4732
5429
  try {
4733
5430
  const runs = readdirSync3(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
4734
5431
  try {
4735
- const run = JSON.parse(readFileSync6(resolve16(runsDir, entry.name, "run.json"), "utf8"));
5432
+ const run = JSON.parse(readFileSync8(resolve18(runsDir, entry.name, "run.json"), "utf8"));
4736
5433
  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 : "" }];
4737
5434
  } catch {
4738
5435
  return [];
@@ -4784,10 +5481,14 @@ function upsertProjectRecord(projectRoot, input) {
4784
5481
  function linkProjectCheckout(projectRoot, repoSlug, checkout) {
4785
5482
  return upsertProjectRecord(projectRoot, { repoSlug, checkout });
4786
5483
  }
5484
+ function projectRegistryContainsCheckout(projectRoot, checkoutPath) {
5485
+ const target = resolve18(checkoutPath);
5486
+ return Object.values(readRegistry(projectRoot)).some((project) => project.checkouts.some((checkout) => checkout.path ? resolve18(checkout.path) === target : false));
5487
+ }
4787
5488
 
4788
5489
  // packages/server/src/server-helpers/remote-checkout.ts
4789
- import { existsSync as existsSync10, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
4790
- import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve17 } from "path";
5490
+ import { existsSync as existsSync12, mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
5491
+ import { dirname as dirname14, isAbsolute as isAbsolute3, relative as relative4, resolve as resolve19 } from "path";
4791
5492
  function safeSlugSegments(repoSlug) {
4792
5493
  const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
4793
5494
  if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
@@ -4804,7 +5505,7 @@ function safeCheckoutKey(value) {
4804
5505
  }
4805
5506
  function repoSlugPath(baseDir, repoSlug, checkoutKey) {
4806
5507
  const key = safeCheckoutKey(checkoutKey);
4807
- return resolve17(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
5508
+ return resolve19(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
4808
5509
  }
4809
5510
  function sanitizeSnapshotId(value, fallback) {
4810
5511
  const raw = (value ?? fallback).trim();
@@ -4812,7 +5513,7 @@ function sanitizeSnapshotId(value, fallback) {
4812
5513
  return safe || fallback;
4813
5514
  }
4814
5515
  function assertWithinRoot(root, relativePath) {
4815
- if (!relativePath || isAbsolute2(relativePath) || relativePath.includes("\x00")) {
5516
+ if (!relativePath || isAbsolute3(relativePath) || relativePath.includes("\x00")) {
4816
5517
  throw new Error(`Invalid snapshot file path: ${relativePath}`);
4817
5518
  }
4818
5519
  const normalizedRelative = relativePath.replace(/\\/g, "/");
@@ -4820,9 +5521,9 @@ function assertWithinRoot(root, relativePath) {
4820
5521
  if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
4821
5522
  throw new Error(`Unsafe snapshot file path: ${relativePath}`);
4822
5523
  }
4823
- const target = resolve17(root, ...segments);
4824
- const rel = relative3(root, target);
4825
- if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute2(rel)) {
5524
+ const target = resolve19(root, ...segments);
5525
+ const rel = relative4(root, target);
5526
+ if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute3(rel)) {
4826
5527
  throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
4827
5528
  }
4828
5529
  return target;
@@ -4840,17 +5541,17 @@ function parseSnapshotArchiveContentBase64(contentBase64) {
4840
5541
  function extractUploadedSnapshotArchive(input) {
4841
5542
  const archive = decodeSnapshotArchive(input.archive);
4842
5543
  const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
4843
- const checkoutPath = resolve17(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
4844
- mkdirSync10(checkoutPath, { recursive: true });
5544
+ const checkoutPath = resolve19(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
5545
+ mkdirSync12(checkoutPath, { recursive: true });
4845
5546
  for (const file of archive.files) {
4846
5547
  if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
4847
5548
  throw new Error("Invalid snapshot archive file entry");
4848
5549
  }
4849
5550
  const target = assertWithinRoot(checkoutPath, file.path);
4850
- mkdirSync10(dirname11(target), { recursive: true });
4851
- writeFileSync9(target, Buffer.from(file.contentBase64, "base64"));
5551
+ mkdirSync12(dirname14(target), { recursive: true });
5552
+ writeFileSync11(target, Buffer.from(file.contentBase64, "base64"));
4852
5553
  }
4853
- writeFileSync9(resolve17(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
5554
+ writeFileSync11(resolve19(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
4854
5555
  repoSlug: input.repoSlug,
4855
5556
  snapshotId,
4856
5557
  fileCount: archive.files.length,
@@ -4885,7 +5586,7 @@ function gitCredentialConfig(token) {
4885
5586
  };
4886
5587
  }
4887
5588
  async function prepareRemoteCheckout(input) {
4888
- const exists = input.exists ?? existsSync10;
5589
+ const exists = input.exists ?? existsSync12;
4889
5590
  const strategy = input.strategy;
4890
5591
  if (strategy.kind === "uploaded-snapshot") {
4891
5592
  return extractUploadedSnapshotArchive({
@@ -4897,7 +5598,7 @@ async function prepareRemoteCheckout(input) {
4897
5598
  });
4898
5599
  }
4899
5600
  if (strategy.kind === "existing-path") {
4900
- const checkoutPath2 = resolve17(strategy.path);
5601
+ const checkoutPath2 = resolve19(strategy.path);
4901
5602
  if (!exists(checkoutPath2)) {
4902
5603
  throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
4903
5604
  }
@@ -4933,9 +5634,9 @@ function buildServerControlStatus() {
4933
5634
  };
4934
5635
  }
4935
5636
  function buildProjectConfigStatus(root) {
4936
- const hasConfigTs = existsSync11(resolve18(root, "rig.config.ts"));
4937
- const hasConfigJson = existsSync11(resolve18(root, "rig.config.json"));
4938
- const hasLegacyTaskConfig = existsSync11(resolve18(root, ".rig", "task-config.json"));
5637
+ const hasConfigTs = existsSync13(resolve20(root, "rig.config.ts"));
5638
+ const hasConfigJson = existsSync13(resolve20(root, "rig.config.json"));
5639
+ const hasLegacyTaskConfig = existsSync13(resolve20(root, ".rig", "task-config.json"));
4939
5640
  let kind = "missing";
4940
5641
  if (hasConfigTs)
4941
5642
  kind = "rig-config-ts";
@@ -4952,6 +5653,75 @@ function buildProjectConfigStatus(root) {
4952
5653
  suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
4953
5654
  };
4954
5655
  }
5656
+ var RIG_GITHUB_LIFECYCLE_LABELS = [
5657
+ "ready",
5658
+ "blocked",
5659
+ "in-progress",
5660
+ "under-review",
5661
+ "failed",
5662
+ "cancelled",
5663
+ "rig:running",
5664
+ "rig:pr-open",
5665
+ "rig:ci-fixing",
5666
+ "rig:merging",
5667
+ "rig:done",
5668
+ "rig:needs-attention"
5669
+ ];
5670
+ function githubProjectsEnabled2(config) {
5671
+ if (!config || typeof config !== "object" || Array.isArray(config))
5672
+ return false;
5673
+ const root = config;
5674
+ const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
5675
+ const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
5676
+ return projects?.enabled === true;
5677
+ }
5678
+ function githubIssueSourceRepo(config) {
5679
+ if (!config || typeof config !== "object" || Array.isArray(config))
5680
+ return null;
5681
+ const root = config;
5682
+ const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
5683
+ const owner = normalizeString(taskSource?.owner);
5684
+ const repo = normalizeString(taskSource?.repo);
5685
+ if (taskSource?.kind === "github-issues" && owner && repo)
5686
+ return { owner, repo };
5687
+ const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
5688
+ const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
5689
+ const match = slug?.match(/^([^/]+)\/([^/]+)$/);
5690
+ return match ? { owner: match[1], repo: match[2] } : null;
5691
+ }
5692
+ async function ensureGitHubLifecycleLabels(projectRoot, config) {
5693
+ const repo = githubIssueSourceRepo(config);
5694
+ if (!repo)
5695
+ return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
5696
+ const token = createGitHubAuthStore(projectRoot).readToken();
5697
+ if (!token)
5698
+ return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
5699
+ const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
5700
+ headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
5701
+ });
5702
+ const existingJson = await existingResponse.json().catch(() => []);
5703
+ const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
5704
+ const created = [];
5705
+ const alreadyPresent = [];
5706
+ const failed = [];
5707
+ for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
5708
+ if (existing.has(label)) {
5709
+ alreadyPresent.push(label);
5710
+ continue;
5711
+ }
5712
+ const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
5713
+ method: "POST",
5714
+ headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
5715
+ 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" })
5716
+ });
5717
+ if (response.ok || response.status === 422) {
5718
+ (response.status === 422 ? alreadyPresent : created).push(label);
5719
+ } else {
5720
+ failed.push({ label, error: await response.text().catch(() => response.statusText) });
5721
+ }
5722
+ }
5723
+ return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
5724
+ }
4955
5725
  function normalizeCommit(value) {
4956
5726
  const raw = normalizeString(value);
4957
5727
  return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
@@ -4971,24 +5741,24 @@ function repoParts(repoSlug) {
4971
5741
  return { owner, repo, slug: `${owner}/${repo}` };
4972
5742
  }
4973
5743
  function repairDir(checkoutPath) {
4974
- const dir = resolve18(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
4975
- mkdirSync11(dir, { recursive: true });
5744
+ const dir = resolve20(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
5745
+ mkdirSync13(dir, { recursive: true });
4976
5746
  return dir;
4977
5747
  }
4978
5748
  function backupCheckoutFile(checkoutPath, relativePath) {
4979
- const source = resolve18(checkoutPath, relativePath);
4980
- const backupPath = resolve18(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
4981
- mkdirSync11(dirname12(backupPath), { recursive: true });
4982
- copyFileSync(source, backupPath);
5749
+ const source = resolve20(checkoutPath, relativePath);
5750
+ const backupPath = resolve20(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
5751
+ mkdirSync13(dirname15(backupPath), { recursive: true });
5752
+ copyFileSync2(source, backupPath);
4983
5753
  return backupPath;
4984
5754
  }
4985
5755
  function parsePackageJsonLosslessly(checkoutPath) {
4986
- const packagePath = resolve18(checkoutPath, "package.json");
4987
- if (!existsSync11(packagePath)) {
5756
+ const packagePath = resolve20(checkoutPath, "package.json");
5757
+ if (!existsSync13(packagePath)) {
4988
5758
  return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
4989
5759
  }
4990
5760
  try {
4991
- const parsed = JSON.parse(readFileSync7(packagePath, "utf8"));
5761
+ const parsed = JSON.parse(readFileSync9(packagePath, "utf8"));
4992
5762
  return {
4993
5763
  existed: true,
4994
5764
  packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
@@ -5002,9 +5772,9 @@ function parsePackageJsonLosslessly(checkoutPath) {
5002
5772
  }
5003
5773
  }
5004
5774
  function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
5005
- const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync11(resolve18(checkoutPath, name)));
5006
- const packagePath = resolve18(checkoutPath, "package.json");
5007
- if (!hasConfig && !existsSync11(packagePath)) {
5775
+ const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
5776
+ const packagePath = resolve20(checkoutPath, "package.json");
5777
+ if (!hasConfig && !existsSync13(packagePath)) {
5008
5778
  return { skipped: true, reason: "package.json and rig.config missing" };
5009
5779
  }
5010
5780
  const parsed = parsePackageJsonLosslessly(checkoutPath);
@@ -5023,7 +5793,7 @@ function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
5023
5793
  }
5024
5794
  const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
5025
5795
  if (changed) {
5026
- writeFileSync10(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
5796
+ writeFileSync12(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
5027
5797
  `, "utf8");
5028
5798
  }
5029
5799
  return {
@@ -5049,11 +5819,11 @@ function configLooksStructurallyUsable(source) {
5049
5819
  return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
5050
5820
  }
5051
5821
  function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
5052
- const configPath = resolve18(checkoutPath, "rig.config.ts");
5053
- const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync11(resolve18(checkoutPath, name)));
5822
+ const configPath = resolve20(checkoutPath, "rig.config.ts");
5823
+ const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
5054
5824
  if (existingConfigName) {
5055
- const existingPath = resolve18(checkoutPath, existingConfigName);
5056
- const source = readFileSync7(existingPath, "utf8");
5825
+ const existingPath = resolve20(checkoutPath, existingConfigName);
5826
+ const source = readFileSync9(existingPath, "utf8");
5057
5827
  if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
5058
5828
  return { path: existingPath, changed: false, reason: "config structurally complete" };
5059
5829
  }
@@ -5067,7 +5837,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
5067
5837
  }
5068
5838
  }
5069
5839
  const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
5070
- writeFileSync10(configPath, generatedRigConfigSource(repoSlug), "utf8");
5840
+ writeFileSync12(configPath, generatedRigConfigSource(repoSlug), "utf8");
5071
5841
  return {
5072
5842
  path: configPath,
5073
5843
  changed: true,
@@ -5076,7 +5846,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
5076
5846
  };
5077
5847
  }
5078
5848
  function validateRemoteCheckoutRigConfig(checkoutPath) {
5079
- const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync11(resolve18(checkoutPath, name)));
5849
+ const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
5080
5850
  if (!configFile)
5081
5851
  return { ok: false, error: "missing rig config" };
5082
5852
  if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
@@ -5098,7 +5868,7 @@ function validateRemoteCheckoutRigConfig(checkoutPath) {
5098
5868
  return { ok: true, configFile };
5099
5869
  }
5100
5870
  function installRemoteCheckoutPackages(checkoutPath) {
5101
- if (!existsSync11(resolve18(checkoutPath, "package.json"))) {
5871
+ if (!existsSync13(resolve20(checkoutPath, "package.json"))) {
5102
5872
  return { skipped: true, reason: "package.json missing" };
5103
5873
  }
5104
5874
  if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
@@ -5111,8 +5881,8 @@ function installRemoteCheckoutPackages(checkoutPath) {
5111
5881
  return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
5112
5882
  }
5113
5883
  function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
5114
- const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync11(resolve18(checkoutPath, name)));
5115
- const hasPackage = existsSync11(resolve18(checkoutPath, "package.json"));
5884
+ const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
5885
+ const hasPackage = existsSync13(resolve20(checkoutPath, "package.json"));
5116
5886
  if (!hasConfig && !hasPackage) {
5117
5887
  return {
5118
5888
  packageJson: { skipped: true, reason: "package.json and rig.config missing" },
@@ -5210,26 +5980,26 @@ function buildRemoteRunLogEntry(body, identifiers) {
5210
5980
  }
5211
5981
  function readGitHeadCommit(projectRoot) {
5212
5982
  try {
5213
- let gitDir = resolve18(projectRoot, ".git");
5983
+ let gitDir = resolve20(projectRoot, ".git");
5214
5984
  try {
5215
- const dotGit = readFileSync7(gitDir, "utf8").trim();
5985
+ const dotGit = readFileSync9(gitDir, "utf8").trim();
5216
5986
  const gitDirPrefix = "gitdir:";
5217
5987
  if (dotGit.startsWith(gitDirPrefix)) {
5218
- gitDir = resolve18(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
5988
+ gitDir = resolve20(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
5219
5989
  }
5220
5990
  } catch {}
5221
- const head = readFileSync7(resolve18(gitDir, "HEAD"), "utf8").trim();
5991
+ const head = readFileSync9(resolve20(gitDir, "HEAD"), "utf8").trim();
5222
5992
  const refPrefix = "ref:";
5223
5993
  if (!head.startsWith(refPrefix)) {
5224
5994
  return normalizeCommit(head);
5225
5995
  }
5226
5996
  const ref = head.slice(refPrefix.length).trim();
5227
- const refPath = resolve18(gitDir, ref);
5228
- if (existsSync11(refPath)) {
5229
- return normalizeCommit(readFileSync7(refPath, "utf8").trim());
5997
+ const refPath = resolve20(gitDir, ref);
5998
+ if (existsSync13(refPath)) {
5999
+ return normalizeCommit(readFileSync9(refPath, "utf8").trim());
5230
6000
  }
5231
- const commonDir = normalizeString(readFileSync7(resolve18(gitDir, "commondir"), "utf8"));
5232
- return commonDir ? normalizeCommit(readFileSync7(resolve18(gitDir, commonDir, ref), "utf8").trim()) : null;
6001
+ const commonDir = normalizeString(readFileSync9(resolve20(gitDir, "commondir"), "utf8"));
6002
+ return commonDir ? normalizeCommit(readFileSync9(resolve20(gitDir, commonDir, ref), "utf8").trim()) : null;
5233
6003
  } catch {
5234
6004
  return null;
5235
6005
  }
@@ -5267,9 +6037,9 @@ function configuredRepoFromTaskSource(taskSource) {
5267
6037
  const repo = normalizeString(taskSource?.repo);
5268
6038
  return owner && repo ? `${owner}/${repo}` : null;
5269
6039
  }
5270
- async function buildTaskSourceStatus(state, config) {
6040
+ async function buildTaskSourceStatus(state, config, requestAuth) {
5271
6041
  const diagnostics = state.snapshotService.getTaskSourceErrors();
5272
- const selectedRepo = createGitHubAuthStore(state.projectRoot).status({
6042
+ const selectedRepo = requestScopedAuthStore(state.projectRoot, requestAuth).status({
5273
6043
  oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
5274
6044
  }).selectedRepo;
5275
6045
  try {
@@ -5322,37 +6092,146 @@ function isLoopbackRequest(req) {
5322
6092
  }
5323
6093
  }
5324
6094
  function isPublicRigAuthBootstrapRoute(pathname) {
5325
- 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";
6095
+ 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";
6096
+ }
6097
+ function buildRigInstallScript() {
6098
+ return `#!/usr/bin/env bash
6099
+ set -euo pipefail
6100
+
6101
+ say() {
6102
+ printf 'rig-install: %s
6103
+ ' "$*"
6104
+ }
6105
+
6106
+ if ! command -v bun >/dev/null 2>&1; then
6107
+ say "Bun not found; installing Bun first"
6108
+ curl -fsSL https://bun.sh/install | bash
6109
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
6110
+ export PATH="$BUN_INSTALL/bin:$PATH"
6111
+ fi
6112
+
6113
+ if ! command -v bun >/dev/null 2>&1; then
6114
+ printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
6115
+ ' >&2
6116
+ exit 1
6117
+ fi
6118
+
6119
+ say "Installing @h-rig/cli@latest"
6120
+ bun add -g @h-rig/cli@latest
6121
+
6122
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
6123
+ BUN_RIG="$BUN_INSTALL/bin/rig"
6124
+ if [ ! -x "$BUN_RIG" ]; then
6125
+ printf 'rig-install: expected Bun global rig at %s but it was not executable.
6126
+ ' "$BUN_RIG" >&2
6127
+ exit 1
6128
+ fi
6129
+
6130
+ USER_BIN="$HOME/.local/bin"
6131
+ mkdir -p "$USER_BIN"
6132
+ cat > "$USER_BIN/rig" <<'RIG_SHIM'
6133
+ #!/usr/bin/env bash
6134
+ set -euo pipefail
6135
+ exec "\${BUN_INSTALL:-$HOME/.bun}/bin/rig" "$@"
6136
+ RIG_SHIM
6137
+ chmod +x "$USER_BIN/rig"
6138
+
6139
+ export PATH="$USER_BIN:$BUN_INSTALL/bin:$PATH"
6140
+ if command -v hash >/dev/null 2>&1; then hash -r; fi
6141
+
6142
+ if ! command -v rig >/dev/null 2>&1; then
6143
+ printf 'rig-install: rig installed, but rig is not on PATH. Add %s and %s/bin to PATH and retry.
6144
+ ' "$USER_BIN" "$BUN_INSTALL" >&2
6145
+ exit 1
6146
+ fi
6147
+
6148
+ say "Verifying rig"
6149
+ "$BUN_RIG" --help >/dev/null
6150
+ rig --help >/dev/null
6151
+ say "Done. Run: rig --help"
6152
+ `;
5326
6153
  }
5327
6154
  function normalizePrMode(value) {
5328
6155
  const mode = normalizeString(value);
5329
6156
  return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
5330
6157
  }
6158
+ function requestAuthResult(input) {
6159
+ return {
6160
+ authorized: input.authorized,
6161
+ actor: input.actor ?? null,
6162
+ reason: input.reason,
6163
+ login: input.login ?? null,
6164
+ userId: input.userId ?? null,
6165
+ userNamespace: input.userNamespace ?? null,
6166
+ authStateFile: input.authStateFile ?? null
6167
+ };
6168
+ }
6169
+ function namespaceFromSessionIndex(entry) {
6170
+ const stateDir2 = dirname15(entry.authStateFile);
6171
+ return {
6172
+ key: entry.namespaceKey,
6173
+ userId: entry.userId,
6174
+ login: entry.login,
6175
+ root: entry.namespaceRoot,
6176
+ stateDir: stateDir2,
6177
+ authStateFile: entry.authStateFile,
6178
+ metadataFile: resolve20(stateDir2, "user-namespace.json"),
6179
+ checkoutBaseDir: entry.checkoutBaseDir,
6180
+ snapshotBaseDir: entry.snapshotBaseDir
6181
+ };
6182
+ }
5331
6183
  function authorizeRigHttpRequest(input) {
5332
6184
  if (input.legacyAuthorized) {
5333
- return { authorized: true, actor: "rig-local-server", reason: "server-token" };
6185
+ return requestAuthResult({ authorized: true, actor: "rig-local-server", reason: "server-token" });
5334
6186
  }
5335
6187
  const bearer = bearerTokenFromRequest(input.req);
5336
6188
  const store = createGitHubAuthStore(input.projectRoot);
5337
6189
  const storedToken = store.readToken();
5338
6190
  const session = bearer ? store.readApiSession(bearer) : null;
5339
6191
  if (session) {
5340
- return { authorized: true, actor: session.login ?? "github-operator", reason: "github-session" };
6192
+ return requestAuthResult({
6193
+ authorized: true,
6194
+ actor: session.login ?? "github-operator",
6195
+ reason: "github-session",
6196
+ login: session.login,
6197
+ userId: session.userId,
6198
+ authStateFile: store.stateFile
6199
+ });
5341
6200
  }
5342
6201
  if (bearer && storedToken && bearer === storedToken) {
5343
6202
  const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
5344
- return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
6203
+ return requestAuthResult({
6204
+ authorized: true,
6205
+ actor: status.login ?? "github-operator",
6206
+ reason: "github-token",
6207
+ login: status.login,
6208
+ userId: status.userId,
6209
+ authStateFile: store.stateFile
6210
+ });
6211
+ }
6212
+ const indexedSession = readGitHubApiSession({ projectRoot: input.projectRoot, token: bearer });
6213
+ if (indexedSession) {
6214
+ const userNamespace = namespaceFromSessionIndex(indexedSession);
6215
+ return requestAuthResult({
6216
+ authorized: true,
6217
+ actor: indexedSession.login ?? "github-operator",
6218
+ reason: "github-user-session",
6219
+ login: indexedSession.login,
6220
+ userId: indexedSession.userId,
6221
+ userNamespace,
6222
+ authStateFile: indexedSession.authStateFile
6223
+ });
5345
6224
  }
5346
6225
  if (isPublicRigAuthBootstrapRoute(input.pathname)) {
5347
- return { authorized: true, actor: null, reason: "public-bootstrap" };
6226
+ return requestAuthResult({ authorized: true, actor: null, reason: "public-bootstrap" });
5348
6227
  }
5349
6228
  if (!input.serverAuthToken && !storedToken) {
5350
6229
  if (isLoopbackRequest(input.req)) {
5351
- return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
6230
+ return requestAuthResult({ authorized: true, actor: null, reason: "loopback-dev-no-auth" });
5352
6231
  }
5353
- return { authorized: false, actor: null, reason: "auth-required" };
6232
+ return requestAuthResult({ authorized: false, actor: null, reason: "auth-required" });
5354
6233
  }
5355
- return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
6234
+ return requestAuthResult({ authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" });
5356
6235
  }
5357
6236
  async function fetchGitHubUserInfo(token) {
5358
6237
  const response = await fetch("https://api.github.com/user", {
@@ -5372,6 +6251,67 @@ async function fetchGitHubUserInfo(token) {
5372
6251
  scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
5373
6252
  };
5374
6253
  }
6254
+ function shouldWriteRootAuthCompat(projectRoot) {
6255
+ if (process.env.RIG_REMOTE_USER_NAMESPACE_ROOT?.trim())
6256
+ return false;
6257
+ const stateDir2 = normalizeString(process.env.RIG_STATE_DIR);
6258
+ if (!stateDir2)
6259
+ return true;
6260
+ return resolve20(stateDir2) === resolve20(projectRoot, ".rig", "state");
6261
+ }
6262
+ function requestScopedRegistryRoot(stateProjectRoot, requestAuth) {
6263
+ return requestAuth.userNamespace?.root ?? stateProjectRoot;
6264
+ }
6265
+ function requestScopedAuthStore(stateProjectRoot, requestAuth) {
6266
+ return requestAuth.authStateFile ? createGitHubAuthStoreFromStateFile(requestAuth.authStateFile) : createGitHubAuthStore(stateProjectRoot);
6267
+ }
6268
+ function userNamespaceResponse(namespace) {
6269
+ return namespace ? serializeRemoteUserNamespace(namespace) : undefined;
6270
+ }
6271
+ function resolveNamespacedBaseDir(input) {
6272
+ if (input.explicitBaseDir)
6273
+ return input.explicitBaseDir;
6274
+ const envBase = normalizeString(process.env[input.envName]);
6275
+ if (input.userNamespace) {
6276
+ return envBase ? resolve20(envBase, input.userNamespace.key) : input.userNamespace[input.legacySubdir === "remote-checkouts" ? "checkoutBaseDir" : "snapshotBaseDir"];
6277
+ }
6278
+ return envBase ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve20(normalizeString(process.env.RIG_STATE_DIR), input.legacySubdir) : resolve20(input.legacyProjectRoot, ".rig", input.legacySubdir));
6279
+ }
6280
+ function explicitCheckoutKey(body, checkoutInput, requestAuth) {
6281
+ return normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? (requestAuth.userNamespace ? undefined : "default");
6282
+ }
6283
+ function saveGitHubTokenForRemoteUser(input) {
6284
+ const namespace = resolveRemoteUserNamespace(input.projectRoot, { userId: input.user.userId, login: input.user.login });
6285
+ writeRemoteUserNamespaceMetadata(namespace);
6286
+ const store = createGitHubAuthStoreFromStateFile(namespace.authStateFile);
6287
+ store.saveToken({
6288
+ token: input.token,
6289
+ tokenSource: input.tokenSource,
6290
+ login: input.user.login,
6291
+ userId: input.user.userId,
6292
+ scopes: input.user.scopes,
6293
+ selectedRepo: input.selectedRepo
6294
+ });
6295
+ const apiSession = store.createApiSession();
6296
+ registerGitHubApiSession({ projectRoot: input.projectRoot, token: apiSession.token, namespace, selectedRepo: input.selectedRepo });
6297
+ const requestedRoot = normalizeString(input.requestedProjectRoot);
6298
+ if (requestedRoot && isAbsolute4(requestedRoot) && existsSync13(resolve20(requestedRoot))) {
6299
+ copyGitHubAuthStateToLocalProjectRoot(namespace.authStateFile, resolve20(requestedRoot));
6300
+ }
6301
+ if (shouldWriteRootAuthCompat(input.projectRoot)) {
6302
+ const rootStore = createGitHubAuthStore(input.projectRoot);
6303
+ rootStore.saveToken({
6304
+ token: input.token,
6305
+ tokenSource: input.tokenSource,
6306
+ login: input.user.login,
6307
+ userId: input.user.userId,
6308
+ scopes: input.user.scopes,
6309
+ selectedRepo: input.selectedRepo
6310
+ });
6311
+ rootStore.createApiSession();
6312
+ }
6313
+ return { store, namespace, apiSessionToken: apiSession.token };
6314
+ }
5375
6315
  async function postGitHubForm(endpoint, body) {
5376
6316
  const response = await fetch(endpoint, {
5377
6317
  method: "POST",
@@ -5389,11 +6329,11 @@ function resolveRequestedProjectRoot(currentRoot, rawRoot) {
5389
6329
  const requestedRoot = normalizeString(rawRoot);
5390
6330
  if (!requestedRoot)
5391
6331
  return currentRoot;
5392
- if (!isAbsolute3(requestedRoot)) {
6332
+ if (!isAbsolute4(requestedRoot)) {
5393
6333
  throw new Error("projectRoot must be an absolute path on the Rig server host");
5394
6334
  }
5395
- const normalizedRoot = resolve18(requestedRoot);
5396
- if (!existsSync11(normalizedRoot)) {
6335
+ const normalizedRoot = resolve20(requestedRoot);
6336
+ if (!existsSync13(normalizedRoot)) {
5397
6337
  throw new Error("projectRoot does not exist on the Rig server host");
5398
6338
  }
5399
6339
  return normalizedRoot;
@@ -5621,6 +6561,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
5621
6561
  }
5622
6562
  return filtered;
5623
6563
  }
6564
+ function issueAnalysisTargetFor(source) {
6565
+ if (!source)
6566
+ return null;
6567
+ const candidate = source;
6568
+ if (typeof candidate.updateTask !== "function")
6569
+ return null;
6570
+ return {
6571
+ ...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
6572
+ updateTask: candidate.updateTask.bind(candidate),
6573
+ ...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
6574
+ ...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
6575
+ ...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
6576
+ };
6577
+ }
6578
+ function uniqueStringList(value) {
6579
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
6580
+ return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
6581
+ }
6582
+ function taskRecordId(task) {
6583
+ return String(task.id ?? "");
6584
+ }
5624
6585
  function redactRemoteEndpoint(endpoint) {
5625
6586
  const { token, ...rest } = endpoint;
5626
6587
  return {
@@ -5705,6 +6666,13 @@ function createRigServerFetch(state, deps) {
5705
6666
  notifications: state.targets.length
5706
6667
  });
5707
6668
  }
6669
+ if (url.pathname === "/install" && req.method === "GET") {
6670
+ return new Response(buildRigInstallScript(), {
6671
+ headers: {
6672
+ "Content-Type": "text/x-shellscript; charset=utf-8"
6673
+ }
6674
+ });
6675
+ }
5708
6676
  const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
5709
6677
  const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
5710
6678
  const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
@@ -5917,16 +6885,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
5917
6885
  if (!source) {
5918
6886
  return deps.badRequest("No task source is configured");
5919
6887
  }
6888
+ if (!source.updateTask && !(update.status && source.updateStatus)) {
6889
+ return deps.badRequest("Configured task source does not support updates");
6890
+ }
5920
6891
  const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
5921
6892
  return;
5922
6893
  }) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
5923
- if (source.updateTask) {
5924
- await source.updateTask(id, update);
5925
- } else if (update.status && source.updateStatus) {
5926
- await source.updateStatus(id, update.status);
5927
- } else {
5928
- return deps.badRequest("Configured task source does not support updates");
5929
- }
5930
6894
  const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
5931
6895
  const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
5932
6896
  taskId: id,
@@ -5935,6 +6899,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
5935
6899
  token: createGitHubAuthStore(state.projectRoot).readToken(),
5936
6900
  config: ctx?.config
5937
6901
  }).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
6902
+ if (update.status && githubProjectsEnabled2(ctx?.config) && projectSync.synced === false) {
6903
+ return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
6904
+ }
6905
+ try {
6906
+ if (source.updateTask) {
6907
+ await source.updateTask(id, update);
6908
+ } else if (update.status && source.updateStatus) {
6909
+ await source.updateStatus(id, update.status);
6910
+ }
6911
+ } catch (error) {
6912
+ let rollback = null;
6913
+ const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
6914
+ if (update.status && previousStatus && githubProjectsEnabled2(ctx?.config) && projectSync.synced !== false) {
6915
+ rollback = await syncGitHubProjectStatusForTaskUpdate({
6916
+ taskId: id,
6917
+ status: previousStatus,
6918
+ issueNodeId,
6919
+ token: createGitHubAuthStore(state.projectRoot).readToken(),
6920
+ config: ctx?.config
6921
+ }).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
6922
+ }
6923
+ return deps.jsonResponse({
6924
+ ok: false,
6925
+ id,
6926
+ projectSync,
6927
+ rollback,
6928
+ error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
6929
+ }, 502);
6930
+ }
5938
6931
  deps.snapshotService.invalidate("github-issue-updated");
5939
6932
  await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
5940
6933
  return;
@@ -5943,29 +6936,105 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
5943
6936
  return deps.jsonResponse({ ok: true, id, projectSync });
5944
6937
  }
5945
6938
  if (url.pathname === "/api/workspace/task-labels") {
6939
+ const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
6940
+ if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
6941
+ return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
6942
+ }
5946
6943
  return deps.jsonResponse({
5947
6944
  ok: true,
5948
6945
  ready: true,
5949
6946
  labelsReady: true,
5950
- labels: [
5951
- "ready",
5952
- "blocked",
5953
- "in-progress",
5954
- "under-review",
5955
- "failed",
5956
- "cancelled",
5957
- "rig:running",
5958
- "rig:pr-open",
5959
- "rig:ci-fixing",
5960
- "rig:done",
5961
- "rig:needs-attention"
5962
- ],
5963
- note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
6947
+ labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
6948
+ note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
6949
+ });
6950
+ }
6951
+ if (url.pathname === "/api/github/projects" && req.method === "GET") {
6952
+ const owner = normalizeString(url.searchParams.get("owner"));
6953
+ if (!owner)
6954
+ return deps.badRequest("owner is required");
6955
+ const token = createGitHubAuthStore(state.projectRoot).readToken();
6956
+ if (!token)
6957
+ return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
6958
+ const projects = await listGitHubProjects({ owner, token }).catch((error) => {
6959
+ throw new Error(error instanceof Error ? error.message : String(error));
6960
+ });
6961
+ return deps.jsonResponse({ ok: true, projects });
6962
+ }
6963
+ const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
6964
+ if (projectStatusMatch && req.method === "GET") {
6965
+ const projectId = decodeURIComponent(projectStatusMatch[1]);
6966
+ const token = createGitHubAuthStore(state.projectRoot).readToken();
6967
+ if (!token)
6968
+ return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
6969
+ const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
6970
+ throw new Error(error instanceof Error ? error.message : String(error));
6971
+ });
6972
+ return deps.jsonResponse({ ok: true, field });
6973
+ }
6974
+ if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
6975
+ const body = await deps.readJsonBody(req);
6976
+ const ids = uniqueStringList(body.ids ?? body.id);
6977
+ const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
6978
+ if (ids.length === 0 && !analyzeAll) {
6979
+ return deps.badRequest("ids is required unless all=true");
6980
+ }
6981
+ const ctx = await getCachedPluginHostContext(state.projectRoot);
6982
+ const [source] = ctx?.taskSourceRegistry.list() ?? [];
6983
+ const target = issueAnalysisTargetFor(source);
6984
+ if (!source || !target) {
6985
+ return deps.badRequest("Configured task source does not support issue-analysis writeback");
6986
+ }
6987
+ const allTasks = [...await source.list()];
6988
+ const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
6989
+ const cached = allTasks.find((task) => taskRecordId(task) === id);
6990
+ if (cached)
6991
+ return cached;
6992
+ return typeof source.get === "function" ? await source.get(id) : undefined;
6993
+ }))).filter((task) => Boolean(task));
6994
+ if (issues.length === 0) {
6995
+ return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
6996
+ }
6997
+ const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
6998
+ const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
6999
+ const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
7000
+ const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
7001
+ const service = createIssueAnalysisService({
7002
+ analyzer: createPiIssueAnalyzer({
7003
+ ...model ? { model } : {},
7004
+ env: { RIG_PROJECT_ROOT: state.projectRoot }
7005
+ }),
7006
+ writeBack: createIssueAnalysisWriteBack({ target })
7007
+ });
7008
+ const reason = normalizeString(body.reason) ?? "http-issue-analysis";
7009
+ let results;
7010
+ try {
7011
+ results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
7012
+ } catch (error) {
7013
+ return deps.jsonResponse({
7014
+ ok: false,
7015
+ error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
7016
+ reason,
7017
+ ids: issues.map((issue) => issue.id)
7018
+ }, 502);
7019
+ }
7020
+ deps.snapshotService.invalidate("issue-analysis-http-run");
7021
+ await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
7022
+ return;
7023
+ });
7024
+ deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
7025
+ return deps.jsonResponse({
7026
+ ok: true,
7027
+ reason,
7028
+ analyzed: results.map((entry) => ({
7029
+ id: entry.issue.id,
7030
+ title: entry.issue.title ?? null,
7031
+ result: entry.result
7032
+ }))
5964
7033
  });
5965
7034
  }
5966
7035
  if (url.pathname === "/api/server/status") {
5967
7036
  const config = buildProjectConfigStatus(state.projectRoot);
5968
- const taskSource = await buildTaskSourceStatus(state, config);
7037
+ const taskSource = await buildTaskSourceStatus(state, config, requestAuth);
5969
7038
  return deps.jsonResponse({
5970
7039
  ok: true,
5971
7040
  projectRoot: state.projectRoot,
@@ -5989,8 +7058,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
5989
7058
  path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
5990
7059
  ref: normalizeString(rawCheckout?.ref) ?? undefined
5991
7060
  } : undefined;
5992
- const record = upsertProjectRecord(state.projectRoot, { repoSlug, checkout });
5993
- return deps.jsonResponse({ ok: true, project: record });
7061
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7062
+ const record = upsertProjectRecord(registryRoot, { repoSlug, checkout });
7063
+ return deps.jsonResponse({ ok: true, project: record, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
5994
7064
  }
5995
7065
  const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
5996
7066
  if (snapshotUploadMatch && req.method === "POST") {
@@ -6003,8 +7073,15 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6003
7073
  if (!archiveContentBase64) {
6004
7074
  return deps.badRequest("archiveContentBase64 is required");
6005
7075
  }
6006
- 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"));
6007
- const checkoutKey = normalizeString(body.checkoutKey) ?? "default";
7076
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7077
+ const baseDir = resolveNamespacedBaseDir({
7078
+ explicitBaseDir: normalizeString(body.baseDir),
7079
+ envName: "RIG_REMOTE_SNAPSHOT_BASE_DIR",
7080
+ userNamespace: requestAuth.userNamespace,
7081
+ legacyProjectRoot: state.projectRoot,
7082
+ legacySubdir: "remote-snapshots"
7083
+ });
7084
+ const checkoutKey = explicitCheckoutKey(body, body, requestAuth);
6008
7085
  try {
6009
7086
  const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
6010
7087
  const checkout = extractUploadedSnapshotArchive({
@@ -6017,14 +7094,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6017
7094
  const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
6018
7095
  const packageInstall = installRemoteCheckoutPackages(checkout.path);
6019
7096
  const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
6020
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
7097
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
6021
7098
  kind: "uploaded-snapshot",
6022
7099
  path: checkout.path,
6023
7100
  ref: checkout.snapshotId
6024
7101
  });
6025
7102
  deps.snapshotService.invalidate("uploaded-snapshot-checkout");
6026
7103
  deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
6027
- return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
7104
+ return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6028
7105
  } catch (error) {
6029
7106
  return deps.jsonResponse({
6030
7107
  ok: false,
@@ -6044,10 +7121,17 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6044
7121
  if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
6045
7122
  return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
6046
7123
  }
6047
- 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"));
6048
- const checkoutKey = normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? "default";
7124
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7125
+ const baseDir = resolveNamespacedBaseDir({
7126
+ explicitBaseDir: normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir),
7127
+ envName: "RIG_REMOTE_CHECKOUT_BASE_DIR",
7128
+ userNamespace: requestAuth.userNamespace,
7129
+ legacyProjectRoot: state.projectRoot,
7130
+ legacySubdir: "remote-checkouts"
7131
+ });
7132
+ const checkoutKey = explicitCheckoutKey(body, checkoutInput, requestAuth);
6049
7133
  const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
6050
- const credentialToken = createGitHubAuthStore(state.projectRoot).readToken();
7134
+ const credentialToken = requestScopedAuthStore(state.projectRoot, requestAuth).readToken();
6051
7135
  try {
6052
7136
  const checkout = await prepareRemoteCheckout({
6053
7137
  command: runRemoteCheckoutCommand,
@@ -6056,14 +7140,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6056
7140
  const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
6057
7141
  const packageInstall = installRemoteCheckoutPackages(checkout.path);
6058
7142
  const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
6059
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
7143
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
6060
7144
  kind: checkout.kind,
6061
7145
  path: checkout.path,
6062
7146
  ref: checkout.ref ?? checkout.snapshotId ?? undefined
6063
7147
  });
6064
7148
  deps.snapshotService.invalidate("remote-checkout-prepared");
6065
7149
  deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
6066
- return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
7150
+ return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6067
7151
  } catch (error) {
6068
7152
  return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
6069
7153
  }
@@ -6080,16 +7164,18 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6080
7164
  if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
6081
7165
  return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
6082
7166
  }
6083
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
7167
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7168
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
6084
7169
  kind,
6085
7170
  path: normalizeString(body.path) ?? state.projectRoot,
6086
7171
  ref: normalizeString(body.ref) ?? undefined
6087
7172
  });
6088
- return deps.jsonResponse({ ok: true, project });
7173
+ return deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6089
7174
  }
6090
7175
  if (req.method === "GET") {
6091
- const project = getProjectRecord(state.projectRoot, repoSlug);
6092
- return project ? deps.jsonResponse({ ok: true, project }) : deps.notFound();
7176
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7177
+ const project = getProjectRecord(registryRoot, repoSlug);
7178
+ return project ? deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) }) : deps.notFound();
6093
7179
  }
6094
7180
  }
6095
7181
  if (url.pathname === "/api/server/project-root" && req.method === "POST") {
@@ -6098,13 +7184,26 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6098
7184
  if (!requestedRoot) {
6099
7185
  return deps.badRequest("projectRoot is required");
6100
7186
  }
6101
- if (!isAbsolute3(requestedRoot)) {
7187
+ if (!isAbsolute4(requestedRoot)) {
6102
7188
  return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
6103
7189
  }
6104
- const normalizedRoot = resolve18(requestedRoot);
6105
- const exists = existsSync11(normalizedRoot);
6106
- if (exists) {
6107
- createGitHubAuthStore(state.projectRoot).copyToProjectRoot(normalizedRoot);
7190
+ const normalizedRoot = resolve20(requestedRoot);
7191
+ const exists = existsSync13(normalizedRoot);
7192
+ if (exists && requestAuth.userNamespace) {
7193
+ const allowedByNamespace = isPathInsideNamespace(requestAuth.userNamespace.root, normalizedRoot);
7194
+ const allowedByRegistry = projectRegistryContainsCheckout(requestAuth.userNamespace.root, normalizedRoot);
7195
+ if (!allowedByNamespace && !allowedByRegistry) {
7196
+ return deps.jsonResponse({
7197
+ ok: false,
7198
+ error: "Requested project root is outside the authenticated GitHub user namespace.",
7199
+ projectRoot: state.projectRoot,
7200
+ requestedProjectRoot: normalizedRoot,
7201
+ userNamespace: userNamespaceResponse(requestAuth.userNamespace)
7202
+ }, 403);
7203
+ }
7204
+ copyGitHubAuthStateToLocalProjectRoot(requestAuth.userNamespace.authStateFile, normalizedRoot);
7205
+ } else if (exists) {
7206
+ createGitHubAuthStore(state.projectRoot).copyToLocalProjectRoot(normalizedRoot);
6108
7207
  }
6109
7208
  const control = buildServerControlStatus();
6110
7209
  const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
@@ -6119,7 +7218,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6119
7218
  message: "Requested project root does not exist on the Rig server host."
6120
7219
  }, 404);
6121
7220
  }
6122
- if (!existsSync11(resolve18(normalizedRoot, "rig.config.ts")) && !existsSync11(resolve18(normalizedRoot, "rig.config.json"))) {
7221
+ if (!existsSync13(resolve20(normalizedRoot, "rig.config.ts")) && !existsSync13(resolve20(normalizedRoot, "rig.config.json"))) {
6123
7222
  return deps.jsonResponse({
6124
7223
  ok: false,
6125
7224
  projectRoot: state.projectRoot,
@@ -6154,6 +7253,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6154
7253
  exists,
6155
7254
  control,
6156
7255
  requiresRestart: false,
7256
+ userNamespace: userNamespaceResponse(requestAuth.userNamespace),
6157
7257
  message: "Project-root switch accepted. Rig server restart has been scheduled."
6158
7258
  }, 202);
6159
7259
  }
@@ -6168,11 +7268,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6168
7268
  }, 409);
6169
7269
  }
6170
7270
  if (url.pathname === "/api/github/auth/status") {
6171
- const store = createGitHubAuthStore(state.projectRoot);
6172
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
7271
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
7272
+ return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6173
7273
  }
6174
7274
  if (url.pathname === "/api/github/repo/permissions") {
6175
- const store = createGitHubAuthStore(state.projectRoot);
7275
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
6176
7276
  const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
6177
7277
  if (!auth.signedIn) {
6178
7278
  return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
@@ -6200,24 +7300,20 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6200
7300
  }
6201
7301
  try {
6202
7302
  const user = await fetchGitHubUserInfo(token);
6203
- const storeRoots = [
6204
- state.projectRoot,
6205
- ...requestedProjectRoot && isAbsolute3(requestedProjectRoot) && existsSync11(resolve18(requestedProjectRoot)) ? [resolve18(requestedProjectRoot)] : []
6206
- ].filter((root, index, roots) => roots.indexOf(root) === index);
6207
- const stores = storeRoots.map((root) => createGitHubAuthStore(root));
6208
- for (const store2 of stores) {
6209
- store2.saveToken({
6210
- token,
6211
- tokenSource: "manual-token",
6212
- login: user.login,
6213
- userId: user.userId,
6214
- scopes: user.scopes,
6215
- selectedRepo
6216
- });
6217
- }
6218
- const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
6219
- const apiSession = store.createApiSession();
6220
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
7303
+ const saved = saveGitHubTokenForRemoteUser({
7304
+ projectRoot: state.projectRoot,
7305
+ token,
7306
+ tokenSource: "manual-token",
7307
+ user,
7308
+ selectedRepo,
7309
+ requestedProjectRoot
7310
+ });
7311
+ return deps.jsonResponse({
7312
+ ok: true,
7313
+ ...saved.store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }),
7314
+ apiSessionToken: saved.apiSessionToken,
7315
+ userNamespace: userNamespaceResponse(saved.namespace)
7316
+ });
6221
7317
  } catch (error) {
6222
7318
  const message = error instanceof Error ? error.message : String(error);
6223
7319
  return deps.jsonResponse({ ok: false, error: message }, 400);
@@ -6284,9 +7380,21 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6284
7380
  }
6285
7381
  const token = result.payload.access_token;
6286
7382
  const user = await fetchGitHubUserInfo(token);
6287
- store.saveToken({ token, tokenSource: "oauth-device", login: user.login, userId: user.userId, scopes: user.scopes });
6288
- const apiSession = store.createApiSession();
6289
- return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }), apiSessionToken: apiSession.token });
7383
+ const saved = saveGitHubTokenForRemoteUser({
7384
+ projectRoot: state.projectRoot,
7385
+ token,
7386
+ tokenSource: "oauth-device",
7387
+ user,
7388
+ selectedRepo: null
7389
+ });
7390
+ store.clearPendingDevice(pollId);
7391
+ return deps.jsonResponse({
7392
+ ok: true,
7393
+ status: "signed-in",
7394
+ ...saved.store.status({ oauthConfigured: true }),
7395
+ apiSessionToken: saved.apiSessionToken,
7396
+ userNamespace: userNamespaceResponse(saved.namespace)
7397
+ });
6290
7398
  }
6291
7399
  if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
6292
7400
  const body = await deps.readJsonBody(req);
@@ -6295,7 +7403,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6295
7403
  if (!owner || !repo) {
6296
7404
  return deps.badRequest("owner and repo are required");
6297
7405
  }
6298
- const store = createGitHubAuthStore(state.projectRoot);
7406
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
6299
7407
  const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
6300
7408
  const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
6301
7409
  return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
@@ -6316,7 +7424,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6316
7424
  return deps.badRequest(error instanceof Error ? error.message : String(error));
6317
7425
  }
6318
7426
  const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
6319
- const configPath = resolve18(targetRoot, "rig.config.ts");
7427
+ const configPath = resolve20(targetRoot, "rig.config.ts");
6320
7428
  const source = buildGitHubProjectConfigSource({
6321
7429
  projectName: rawProjectName,
6322
7430
  owner,
@@ -6328,8 +7436,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6328
7436
  ok: true,
6329
7437
  projectRoot: targetRoot,
6330
7438
  configPath,
6331
- exists: existsSync11(configPath),
6332
- requiresOverwrite: existsSync11(configPath),
7439
+ exists: existsSync13(configPath),
7440
+ requiresOverwrite: existsSync13(configPath),
6333
7441
  source,
6334
7442
  owner,
6335
7443
  repo,
@@ -6365,8 +7473,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6365
7473
  assignee,
6366
7474
  githubUserId: authStatus.userId ?? authStatus.login
6367
7475
  });
6368
- const configPath = resolve18(targetRoot, "rig.config.ts");
6369
- if (existsSync11(configPath) && !overwrite) {
7476
+ const configPath = resolve20(targetRoot, "rig.config.ts");
7477
+ if (existsSync13(configPath) && !overwrite) {
6370
7478
  return deps.jsonResponse({
6371
7479
  ok: false,
6372
7480
  error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
@@ -6382,11 +7490,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6382
7490
  return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
6383
7491
  }
6384
7492
  let backupPath = null;
6385
- if (existsSync11(configPath)) {
7493
+ if (existsSync13(configPath)) {
6386
7494
  backupPath = backupConfigPath(configPath);
6387
- copyFileSync(configPath, backupPath);
7495
+ copyFileSync2(configPath, backupPath);
6388
7496
  }
6389
- writeFileSync10(configPath, source, "utf8");
7497
+ writeFileSync12(configPath, source, "utf8");
6390
7498
  const selectedRepo = `${owner}/${repo}`;
6391
7499
  store.saveSelectedRepo(selectedRepo);
6392
7500
  const targetStore = createGitHubAuthStore(targetRoot);
@@ -6658,11 +7766,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6658
7766
  const runId = normalizeString(body.runId);
6659
7767
  const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
6660
7768
  const promptOverride = normalizeString(body.promptOverride);
7769
+ const restart = body.restart === true;
6661
7770
  if (!runId) {
6662
7771
  return deps.badRequest("runId is required");
6663
7772
  }
6664
7773
  try {
6665
- await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
7774
+ await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
6666
7775
  deps.broadcastSnapshotInvalidation(state);
6667
7776
  return deps.jsonResponse({ ok: true, runId, createdAt });
6668
7777
  } catch (error) {
@@ -6671,7 +7780,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6671
7780
  }
6672
7781
  if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
6673
7782
  const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
6674
- 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";
7783
+ 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";
6675
7784
  if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
6676
7785
  return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
6677
7786
  }
@@ -6987,9 +8096,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6987
8096
  } catch {
6988
8097
  return deps.badRequest("Invalid artifact path");
6989
8098
  }
6990
- mkdirSync11(dirname12(artifactPath), { recursive: true });
8099
+ mkdirSync13(dirname15(artifactPath), { recursive: true });
6991
8100
  const bytes = Buffer.from(contentBase64, "base64");
6992
- writeFileSync10(artifactPath, bytes);
8101
+ writeFileSync12(artifactPath, bytes);
6993
8102
  writeJsonFile4(`${artifactPath}.json`, {
6994
8103
  workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
6995
8104
  runId,
@@ -7026,13 +8135,75 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7026
8135
  }
7027
8136
  const run = leaseValidation.run;
7028
8137
  const completedAt = new Date().toISOString();
8138
+ const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
8139
+ if (run.taskId && workspaceDir) {
8140
+ patchRunRecord(state.projectRoot, runId, {
8141
+ status: "reviewing",
8142
+ completedAt: null,
8143
+ hostId,
8144
+ endpointId: leaseId,
8145
+ worktreePath: workspaceDir,
8146
+ serverCloseout: {
8147
+ status: "pending",
8148
+ phase: "queued",
8149
+ requestedAt: completedAt,
8150
+ updatedAt: completedAt,
8151
+ runtimeWorkspace: workspaceDir,
8152
+ branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
8153
+ taskId: run.taskId,
8154
+ source: "remote-complete"
8155
+ }
8156
+ });
8157
+ deps.appendRunLogEntryAndBroadcast(state, runId, {
8158
+ id: `log:${runId}:remote-server-closeout-requested`,
8159
+ title: "Server-owned closeout requested",
8160
+ detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
8161
+ tone: "info",
8162
+ status: "reviewing",
8163
+ createdAt: completedAt,
8164
+ payload: { workspaceDir, hostId, leaseId }
8165
+ }, "remote-server-closeout-requested");
8166
+ deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
8167
+ const detail = error instanceof Error ? error.message : String(error);
8168
+ patchRunRecord(state.projectRoot, runId, {
8169
+ status: "failed",
8170
+ completedAt: new Date().toISOString(),
8171
+ errorText: detail,
8172
+ serverCloseout: {
8173
+ status: "failed",
8174
+ phase: "failed",
8175
+ updatedAt: new Date().toISOString(),
8176
+ error: detail
8177
+ }
8178
+ });
8179
+ deps.appendRunLogEntryAndBroadcast(state, runId, {
8180
+ id: `log:${runId}:remote-server-closeout-failed`,
8181
+ title: "Server-owned closeout failed",
8182
+ detail,
8183
+ tone: "error",
8184
+ status: "failed",
8185
+ createdAt: new Date().toISOString()
8186
+ }, "remote-server-closeout-failed");
8187
+ }).finally(() => {
8188
+ deps.reconcileScheduler(state, "remote-server-closeout-terminal");
8189
+ });
8190
+ deps.broadcastSnapshotInvalidation(state);
8191
+ return deps.jsonResponse({
8192
+ ok: true,
8193
+ workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
8194
+ hostId,
8195
+ runId,
8196
+ leaseId,
8197
+ closeout: "server-owned",
8198
+ acceptedAt: new Date().toISOString()
8199
+ });
8200
+ }
7029
8201
  patchRunRecord(state.projectRoot, runId, {
7030
8202
  status: "completed",
7031
8203
  completedAt,
7032
8204
  hostId,
7033
8205
  endpointId: leaseId
7034
8206
  });
7035
- await updateRemoteRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "completed", completedAt, hostId, endpointId: leaseId }, "closed", "Remote Rig task run completed and closed this task.");
7036
8207
  await deps.enqueueRunLinearEvent(state.projectRoot, {
7037
8208
  type: "run.completed",
7038
8209
  runId,
@@ -7151,12 +8322,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7151
8322
  try {
7152
8323
  const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
7153
8324
  const runRoot = deps.normalizeRelativePath(runsRoot, runId);
7154
- const artifactsRoot = resolve18(runRoot, "remote-artifacts");
8325
+ const artifactsRoot = resolve20(runRoot, "remote-artifacts");
7155
8326
  artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
7156
8327
  } catch {
7157
8328
  return deps.badRequest("Invalid artifact path");
7158
8329
  }
7159
- if (!existsSync11(artifactPath)) {
8330
+ if (!existsSync13(artifactPath)) {
7160
8331
  return deps.notFound();
7161
8332
  }
7162
8333
  return new Response(Bun.file(artifactPath));
@@ -7169,6 +8340,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7169
8340
  const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
7170
8341
  return deps.jsonResponse(page);
7171
8342
  }
8343
+ const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
8344
+ if (runTimelineMatch) {
8345
+ const runId = decodeURIComponent(runTimelineMatch[1]);
8346
+ const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
8347
+ const cursor = normalizeString(url.searchParams.get("cursor"));
8348
+ const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
8349
+ return deps.jsonResponse(page);
8350
+ }
7172
8351
  const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
7173
8352
  if (runSteerMatch && req.method === "POST") {
7174
8353
  const runId = decodeURIComponent(runSteerMatch[1]);
@@ -8049,8 +9228,8 @@ async function routeWebSocketRequest(state, deps, request) {
8049
9228
  }
8050
9229
 
8051
9230
  // packages/server/src/server-helpers/inspector-jobs.ts
8052
- import { existsSync as existsSync15, mkdirSync as mkdirSync14, readFileSync as readFileSync10, writeFileSync as writeFileSync13 } from "fs";
8053
- import { dirname as dirname15, resolve as resolve21 } from "path";
9231
+ import { existsSync as existsSync17, mkdirSync as mkdirSync16, readFileSync as readFileSync12, writeFileSync as writeFileSync15 } from "fs";
9232
+ import { dirname as dirname18, resolve as resolve23 } from "path";
8054
9233
  import { readJsonFile as readJsonFile3 } from "@rig/runtime/control-plane/authority-files";
8055
9234
  import { resolveMonorepoRoot as resolveMonorepoRoot5 } from "@rig/runtime/control-plane/native/utils";
8056
9235
  import { normalizeTaskLifecycleStatus as normalizeTaskLifecycleStatus2 } from "@rig/runtime/control-plane/state-sync/types";
@@ -8158,8 +9337,8 @@ import { randomUUID as randomUUID3 } from "crypto";
8158
9337
 
8159
9338
  // packages/server/src/inspector/mission.ts
8160
9339
  import { randomUUID as randomUUID2 } from "crypto";
8161
- import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync8, readdirSync as readdirSync4, renameSync, writeFileSync as writeFileSync11 } from "fs";
8162
- import { dirname as dirname13, join, resolve as resolve19 } from "path";
9340
+ import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync14, readFileSync as readFileSync10, readdirSync as readdirSync4, renameSync, writeFileSync as writeFileSync13 } from "fs";
9341
+ import { dirname as dirname16, join, resolve as resolve21 } from "path";
8163
9342
  function isJsonValue(value) {
8164
9343
  if (value === null)
8165
9344
  return true;
@@ -8199,7 +9378,7 @@ function isRecord2(value) {
8199
9378
  }
8200
9379
  function readJsonRecord(path) {
8201
9380
  try {
8202
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
9381
+ const parsed = JSON.parse(readFileSync10(path, "utf8"));
8203
9382
  if (!isRecord2(parsed)) {
8204
9383
  return { ok: false, error: `Mission file ${path} does not contain an object` };
8205
9384
  }
@@ -8279,14 +9458,14 @@ function missionActionDetails(mission) {
8279
9458
  };
8280
9459
  }
8281
9460
  function writeJsonFile5(path, value) {
8282
- mkdirSync12(dirname13(path), { recursive: true });
9461
+ mkdirSync14(dirname16(path), { recursive: true });
8283
9462
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
8284
- writeFileSync11(tempPath, `${JSON.stringify(value, null, 2)}
9463
+ writeFileSync13(tempPath, `${JSON.stringify(value, null, 2)}
8285
9464
  `, "utf8");
8286
9465
  renameSync(tempPath, path);
8287
9466
  }
8288
9467
  function resolveInspectorMissionPaths(projectRoot) {
8289
- const inspectorDir = resolve19(resolveRigServerPaths(projectRoot).stateDir, "inspector");
9468
+ const inspectorDir = resolve21(resolveRigServerPaths(projectRoot).stateDir, "inspector");
8290
9469
  return {
8291
9470
  inspectorDir,
8292
9471
  missionsDir: join(inspectorDir, "missions"),
@@ -8295,8 +9474,8 @@ function resolveInspectorMissionPaths(projectRoot) {
8295
9474
  }
8296
9475
  function createInspectorMissionController(options) {
8297
9476
  const paths = resolveInspectorMissionPaths(options.projectRoot);
8298
- mkdirSync12(paths.missionsDir, { recursive: true });
8299
- mkdirSync12(paths.journalsDir, { recursive: true });
9477
+ mkdirSync14(paths.missionsDir, { recursive: true });
9478
+ mkdirSync14(paths.journalsDir, { recursive: true });
8300
9479
  const now = options.now ?? (() => new Date().toISOString());
8301
9480
  const nextId = options.idGenerator ?? (() => `mission:${randomUUID2()}`);
8302
9481
  function missionPath(missionId) {
@@ -8306,15 +9485,15 @@ function createInspectorMissionController(options) {
8306
9485
  return join(paths.journalsDir, `${missionId}.jsonl`);
8307
9486
  }
8308
9487
  function appendMissionJournal(entry) {
8309
- mkdirSync12(paths.journalsDir, { recursive: true });
9488
+ mkdirSync14(paths.journalsDir, { recursive: true });
8310
9489
  appendFileSync(journalPath(entry.missionId), `${JSON.stringify(entry)}
8311
9490
  `, "utf8");
8312
9491
  }
8313
9492
  function listMissionJournal(missionId) {
8314
9493
  const path = journalPath(missionId);
8315
- if (!existsSync12(path))
9494
+ if (!existsSync14(path))
8316
9495
  return [];
8317
- return readFileSync8(path, "utf8").split(`
9496
+ return readFileSync10(path, "utf8").split(`
8318
9497
  `).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line)).filter(isRecord2).map((entry) => ({
8319
9498
  id: typeof entry.id === "string" ? entry.id : `journal:${randomUUID2()}`,
8320
9499
  missionId,
@@ -8330,7 +9509,7 @@ function createInspectorMissionController(options) {
8330
9509
  }
8331
9510
  function readMissionOnly(missionId) {
8332
9511
  const path = missionPath(missionId);
8333
- if (!existsSync12(path)) {
9512
+ if (!existsSync14(path)) {
8334
9513
  return { ok: false, error: `Mission ${missionId} was not found` };
8335
9514
  }
8336
9515
  const read = readJsonRecord(path);
@@ -8381,7 +9560,7 @@ function createInspectorMissionController(options) {
8381
9560
  const source = cloneJsonRecord(input.sourceTask);
8382
9561
  const missionId = nextId();
8383
9562
  const path = missionPath(missionId);
8384
- if (existsSync12(path)) {
9563
+ if (existsSync14(path)) {
8385
9564
  const existing = readMissionOnly(missionId);
8386
9565
  if (!existing.ok)
8387
9566
  return existing;
@@ -9981,8 +11160,8 @@ function createCodexInspectorTransport(options) {
9981
11160
  const sendRequest = async (method, params) => {
9982
11161
  const id = nextRequestId;
9983
11162
  nextRequestId += 1;
9984
- const response = new Promise((resolve20, reject) => {
9985
- pendingResponses.set(id, { resolve: resolve20, reject });
11163
+ const response = new Promise((resolve22, reject) => {
11164
+ pendingResponses.set(id, { resolve: resolve22, reject });
9986
11165
  });
9987
11166
  response.catch(() => {});
9988
11167
  try {
@@ -10292,9 +11471,9 @@ function createCodexInspectorTransport(options) {
10292
11471
  }
10293
11472
  lastAssistantMessage = null;
10294
11473
  lastError = null;
10295
- const turnResult = new Promise((resolve20, reject) => {
11474
+ const turnResult = new Promise((resolve22, reject) => {
10296
11475
  currentTurn = {
10297
- resolve: resolve20,
11476
+ resolve: resolve22,
10298
11477
  reject,
10299
11478
  events: []
10300
11479
  };
@@ -10354,13 +11533,13 @@ function createCodexInspectorTransport(options) {
10354
11533
  };
10355
11534
  }
10356
11535
  function writeChildLine(child, line) {
10357
- return new Promise((resolve20, reject) => {
11536
+ return new Promise((resolve22, reject) => {
10358
11537
  child.stdin.write(line, (error) => {
10359
11538
  if (error) {
10360
11539
  reject(error);
10361
11540
  return;
10362
11541
  }
10363
- resolve20();
11542
+ resolve22();
10364
11543
  });
10365
11544
  });
10366
11545
  }
@@ -10373,10 +11552,10 @@ function terminateChild(child) {
10373
11552
  } catch {}
10374
11553
  }
10375
11554
  async function waitForChildSpawn(child) {
10376
- await new Promise((resolve20, reject) => {
11555
+ await new Promise((resolve22, reject) => {
10377
11556
  const onSpawn = () => {
10378
11557
  cleanup();
10379
- resolve20();
11558
+ resolve22();
10380
11559
  };
10381
11560
  const onError = (error) => {
10382
11561
  cleanup();
@@ -10888,8 +12067,8 @@ function createGlobalInspectorService(options) {
10888
12067
 
10889
12068
  // packages/server/src/inspector/upstream-sync.ts
10890
12069
  import { spawnSync as spawnSync4 } from "child_process";
10891
- import { existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync12 } from "fs";
10892
- import { dirname as dirname14, resolve as resolve20 } from "path";
12070
+ import { existsSync as existsSync15, mkdirSync as mkdirSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync14 } from "fs";
12071
+ import { dirname as dirname17, resolve as resolve22 } from "path";
10893
12072
  import { resolveMonorepoRoot as resolveMonorepoRoot4 } from "@rig/runtime/control-plane/native/utils";
10894
12073
  var UPSTREAM_VALIDATION_DESCRIPTIONS = {
10895
12074
  "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.",
@@ -11027,34 +12206,34 @@ function defaultGitRunner(repoRoot, args) {
11027
12206
  }
11028
12207
  function upstreamStatePath(projectRoot, override) {
11029
12208
  if (override) {
11030
- return resolve20(override);
12209
+ return resolve22(override);
11031
12210
  }
11032
- return resolve20(resolveRigServerPaths(projectRoot).stateDir, "inspector", "upstream-sync.json");
12211
+ return resolve22(resolveRigServerPaths(projectRoot).stateDir, "inspector", "upstream-sync.json");
11033
12212
  }
11034
12213
  function readUpstreamState(projectRoot, statePath) {
11035
12214
  const path = upstreamStatePath(projectRoot, statePath);
11036
- if (!existsSync13(path)) {
12215
+ if (!existsSync15(path)) {
11037
12216
  return null;
11038
12217
  }
11039
12218
  try {
11040
- return JSON.parse(readFileSync9(path, "utf-8"));
12219
+ return JSON.parse(readFileSync11(path, "utf-8"));
11041
12220
  } catch {
11042
12221
  return null;
11043
12222
  }
11044
12223
  }
11045
12224
  function writeUpstreamState(projectRoot, state, statePath) {
11046
12225
  const path = upstreamStatePath(projectRoot, statePath);
11047
- mkdirSync13(dirname14(path), { recursive: true });
11048
- writeFileSync12(path, `${JSON.stringify(state, null, 2)}
12226
+ mkdirSync15(dirname17(path), { recursive: true });
12227
+ writeFileSync14(path, `${JSON.stringify(state, null, 2)}
11049
12228
  `, "utf8");
11050
12229
  }
11051
12230
  function readImportedRevision(projectRoot, upstreamsDocPath) {
11052
12231
  const monorepoRoot = resolveMonorepoRoot4(projectRoot);
11053
- const docPath = upstreamsDocPath ? resolve20(upstreamsDocPath) : resolve20(monorepoRoot, "docs", "UPSTREAMS.md");
11054
- if (!existsSync13(docPath)) {
12232
+ const docPath = upstreamsDocPath ? resolve22(upstreamsDocPath) : resolve22(monorepoRoot, "docs", "UPSTREAMS.md");
12233
+ if (!existsSync15(docPath)) {
11055
12234
  throw new Error(`UPSTREAMS.md not found at ${docPath}`);
11056
12235
  }
11057
- const docContent = readFileSync9(docPath, "utf-8");
12236
+ const docContent = readFileSync11(docPath, "utf-8");
11058
12237
  const revision = parseImportedUpstreamRevision(docContent, "upstream") ?? parseImportedUpstreamRevision(docContent, "humoongate");
11059
12238
  if (!revision) {
11060
12239
  throw new Error(`Failed to parse upstream imported revision from ${docPath}`);
@@ -11076,7 +12255,7 @@ function resolveRemoteBranch(repoRoot, remote, gitRunner) {
11076
12255
  return null;
11077
12256
  }
11078
12257
  function isGitCheckout(path, gitRunner) {
11079
- if (!existsSync13(resolve20(path, ".git"))) {
12258
+ if (!existsSync15(resolve22(path, ".git"))) {
11080
12259
  return false;
11081
12260
  }
11082
12261
  const result = gitRunner(path, ["rev-parse", "--is-inside-work-tree"]);
@@ -11085,12 +12264,12 @@ function isGitCheckout(path, gitRunner) {
11085
12264
  function resolveUpstreamCheckout(projectRoot, explicitCheckout, gitRunner) {
11086
12265
  const monorepoRoot = resolveMonorepoRoot4(projectRoot);
11087
12266
  const candidates = [
11088
- explicitCheckout ? resolve20(explicitCheckout) : "",
11089
- process.env.UPSTREAM_CHECKOUT?.trim() ? resolve20(process.env.UPSTREAM_CHECKOUT.trim()) : "",
11090
- process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ? resolve20(process.env.HUMOONGATE_UPSTREAM_CHECKOUT.trim()) : "",
11091
- resolve20(projectRoot, "..", "humoongate"),
11092
- resolve20(monorepoRoot, "..", "humoongate"),
11093
- resolve20(monorepoRoot, "humoongate")
12267
+ explicitCheckout ? resolve22(explicitCheckout) : "",
12268
+ process.env.UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.UPSTREAM_CHECKOUT.trim()) : "",
12269
+ process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.HUMOONGATE_UPSTREAM_CHECKOUT.trim()) : "",
12270
+ resolve22(projectRoot, "..", "humoongate"),
12271
+ resolve22(monorepoRoot, "..", "humoongate"),
12272
+ resolve22(monorepoRoot, "humoongate")
11094
12273
  ].filter(Boolean);
11095
12274
  for (const candidate of candidates) {
11096
12275
  if (isGitCheckout(candidate, gitRunner)) {
@@ -11326,10 +12505,10 @@ async function runUpstreamSyncScan(options) {
11326
12505
  }
11327
12506
 
11328
12507
  // packages/server/src/server-helpers/task-config.ts
11329
- import { existsSync as existsSync14 } from "fs";
12508
+ import { existsSync as existsSync16 } from "fs";
11330
12509
  async function readTaskConfig(projectRoot) {
11331
12510
  const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
11332
- if (!existsSync14(taskConfigPath)) {
12511
+ if (!existsSync16(taskConfigPath)) {
11333
12512
  return {};
11334
12513
  }
11335
12514
  try {
@@ -11365,11 +12544,11 @@ function resolveFollowupSourceCommit(input) {
11365
12544
  }
11366
12545
  async function createInspectorFollowupTask(projectRoot, input) {
11367
12546
  const monorepoRoot = resolveMonorepoRoot5(projectRoot);
11368
- const issuesPath = resolve21(monorepoRoot, ".beads", "issues.jsonl");
11369
- const taskStatePath = resolve21(monorepoRoot, ".beads", "task-state.json");
11370
- const taskConfigPath = resolve21(monorepoRoot, ".rig", "task-config.json");
11371
- mkdirSync14(dirname15(issuesPath), { recursive: true });
11372
- mkdirSync14(dirname15(taskConfigPath), { recursive: true });
12547
+ const issuesPath = resolve23(monorepoRoot, ".beads", "issues.jsonl");
12548
+ const taskStatePath = resolve23(monorepoRoot, ".beads", "task-state.json");
12549
+ const taskConfigPath = resolve23(monorepoRoot, ".rig", "task-config.json");
12550
+ mkdirSync16(dirname18(issuesPath), { recursive: true });
12551
+ mkdirSync16(dirname18(taskConfigPath), { recursive: true });
11373
12552
  const summary = normalizeString(input.summary) ?? "Inspector follow-up";
11374
12553
  const description = normalizeString(input.description) ?? normalizeString(input.details?.description) ?? `Created by the global inspector: ${summary}`;
11375
12554
  const acceptanceCriteria = normalizeString(input.acceptanceCriteria) ?? "Investigate the detected drift and port the relevant changes into Rig.";
@@ -11388,7 +12567,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
11388
12567
  const sourceKey = normalizeString(input.sourceKey) ?? normalizeString(input.details?.sourceKey);
11389
12568
  const createdAt = normalizeString(input.createdAt) ?? new Date().toISOString();
11390
12569
  const status = normalizeTaskLifecycleStatus2(normalizeString(input.status) ?? "open") ?? "open";
11391
- const existingIssueLines = existsSync15(issuesPath) ? readFileSync10(issuesPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
12570
+ const existingIssueLines = existsSync17(issuesPath) ? readFileSync12(issuesPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
11392
12571
  const existingIssues = existingIssueLines.map((line) => {
11393
12572
  try {
11394
12573
  return JSON.parse(line);
@@ -11397,7 +12576,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
11397
12576
  }
11398
12577
  }).filter((value) => value !== null);
11399
12578
  const existingIds = new Set(existingIssues.map((issue) => typeof issue.id === "string" ? issue.id : null).filter((value) => value !== null));
11400
- const rawTaskState = existsSync15(taskStatePath) ? readJsonFile3(taskStatePath, {}) : {};
12579
+ const rawTaskState = existsSync17(taskStatePath) ? readJsonFile3(taskStatePath, {}) : {};
11401
12580
  const tasks = rawTaskState.tasks && typeof rawTaskState.tasks === "object" && !Array.isArray(rawTaskState.tasks) ? rawTaskState.tasks : {};
11402
12581
  const existingTaskIdFromSourceKey = sourceKey == null ? null : Object.entries(tasks).find(([, metadata]) => {
11403
12582
  if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
@@ -11423,7 +12602,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
11423
12602
  updated_at: createdAt,
11424
12603
  labels: mergedLabels
11425
12604
  };
11426
- writeFileSync13(issuesPath, existingIssueLines.length > 0 ? `${existingIssueLines.join(`
12605
+ writeFileSync15(issuesPath, existingIssueLines.length > 0 ? `${existingIssueLines.join(`
11427
12606
  `)}
11428
12607
  ${JSON.stringify(issueRecord)}
11429
12608
  ` : `${JSON.stringify(issueRecord)}
@@ -11441,7 +12620,7 @@ ${JSON.stringify(issueRecord)}
11441
12620
  labels: mergedLabels
11442
12621
  };
11443
12622
  });
11444
- writeFileSync13(issuesPath, `${updatedIssues.map((issue) => JSON.stringify(issue)).join(`
12623
+ writeFileSync15(issuesPath, `${updatedIssues.map((issue) => JSON.stringify(issue)).join(`
11445
12624
  `)}
11446
12625
  `, "utf8");
11447
12626
  }
@@ -11464,14 +12643,14 @@ ${JSON.stringify(issueRecord)}
11464
12643
  }
11465
12644
  };
11466
12645
  }
11467
- writeFileSync13(taskConfigPath, `${JSON.stringify(taskConfig, null, 2)}
12646
+ writeFileSync15(taskConfigPath, `${JSON.stringify(taskConfig, null, 2)}
11468
12647
  `, "utf8");
11469
12648
  tasks[taskId] = {
11470
12649
  status,
11471
12650
  sourceCommit: resolveFollowupSourceCommit(input),
11472
12651
  ...sourceKey ? { sourceKey } : {}
11473
12652
  };
11474
- writeFileSync13(taskStatePath, `${JSON.stringify({
12653
+ writeFileSync15(taskStatePath, `${JSON.stringify({
11475
12654
  schemaVersion: 1,
11476
12655
  baseTrackerCommit: typeof rawTaskState.baseTrackerCommit === "string" ? rawTaskState.baseTrackerCommit : null,
11477
12656
  tasks
@@ -11779,12 +12958,12 @@ function isAuthorizedLinearWebhookRequest(req) {
11779
12958
  }
11780
12959
 
11781
12960
  // packages/server/src/server-helpers/notifications.ts
11782
- import { existsSync as existsSync16, mkdirSync as mkdirSync15, readFileSync as readFileSync11 } from "fs";
11783
- import { dirname as dirname16 } from "path";
12961
+ import { existsSync as existsSync18, mkdirSync as mkdirSync17, readFileSync as readFileSync13 } from "fs";
12962
+ import { dirname as dirname19 } from "path";
11784
12963
  async function loadNotificationConfig(path) {
11785
- if (!existsSync16(path)) {
12964
+ if (!existsSync18(path)) {
11786
12965
  const defaultConfig = { targets: [] };
11787
- mkdirSync15(dirname16(path), { recursive: true });
12966
+ mkdirSync17(dirname19(path), { recursive: true });
11788
12967
  await Bun.write(path, `${JSON.stringify(defaultConfig, null, 2)}
11789
12968
  `);
11790
12969
  return defaultConfig;
@@ -11799,10 +12978,10 @@ async function loadNotificationConfig(path) {
11799
12978
  }
11800
12979
  }
11801
12980
  function readRecentEvents(file, limit) {
11802
- if (!existsSync16(file)) {
12981
+ if (!existsSync18(file)) {
11803
12982
  return [];
11804
12983
  }
11805
- const lines = readFileSync11(file, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-limit);
12984
+ const lines = readFileSync13(file, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-limit);
11806
12985
  const events = [];
11807
12986
  for (const line of lines) {
11808
12987
  try {
@@ -11897,11 +13076,11 @@ function extractObjectLiteralBlock(source, property) {
11897
13076
  }
11898
13077
  function readFallbackIssueAnalysisConfig(projectRoot) {
11899
13078
  for (const fileName of ["rig.config.ts", "rig.config.json"]) {
11900
- const path = resolve22(projectRoot, fileName);
11901
- if (!existsSync17(path))
13079
+ const path = resolve24(projectRoot, fileName);
13080
+ if (!existsSync19(path))
11902
13081
  continue;
11903
13082
  try {
11904
- const source = readFileSync12(path, "utf8");
13083
+ const source = readFileSync14(path, "utf8");
11905
13084
  if (fileName.endsWith(".json"))
11906
13085
  return JSON.parse(source);
11907
13086
  const issueBlock = extractObjectLiteralBlock(source, "issueAnalysis");
@@ -12034,8 +13213,8 @@ async function createIssueAnalysisRunnerForServerState(state, input) {
12034
13213
  async function withServerPathEnv(projectRoot, fn) {
12035
13214
  const waitForTurn = serverPathEnvQueue;
12036
13215
  let releaseTurn;
12037
- serverPathEnvQueue = new Promise((resolve23) => {
12038
- releaseTurn = resolve23;
13216
+ serverPathEnvQueue = new Promise((resolve25) => {
13217
+ releaseTurn = resolve25;
12039
13218
  });
12040
13219
  await waitForTurn;
12041
13220
  const paths = resolveServerAuthorityPaths(projectRoot);
@@ -12071,9 +13250,9 @@ async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
12071
13250
  return withServerPathEnv(projectRoot, fn);
12072
13251
  }
12073
13252
  async function readWorkspaceTasks(projectRoot) {
12074
- const issuesPath = resolve22(resolveMonorepoRoot6(projectRoot), ".beads", "issues.jsonl");
13253
+ const issuesPath = resolve24(resolveMonorepoRoot6(projectRoot), ".beads", "issues.jsonl");
12075
13254
  const taskConfig = await readTaskConfig(projectRoot);
12076
- if (!existsSync17(issuesPath)) {
13255
+ if (!existsSync19(issuesPath)) {
12077
13256
  return [];
12078
13257
  }
12079
13258
  const latestById = new Map;
@@ -12147,11 +13326,11 @@ function resolveTaskArtifactDirsFromRuns(projectRoot, taskId, knownRuns) {
12147
13326
  continue;
12148
13327
  add(run.artifactRoot);
12149
13328
  if (run.worktreePath) {
12150
- add(resolve22(run.worktreePath, "artifacts", taskId));
13329
+ add(resolve24(run.worktreePath, "artifacts", taskId));
12151
13330
  }
12152
13331
  }
12153
13332
  for (const artifactsRoot of listAuthorityArtifactRoots(projectRoot)) {
12154
- add(resolve22(artifactsRoot, taskId));
13333
+ add(resolve24(artifactsRoot, taskId));
12155
13334
  }
12156
13335
  return candidates;
12157
13336
  }
@@ -12165,7 +13344,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
12165
13344
  }
12166
13345
  }
12167
13346
  return taskIds.flatMap((currentTaskId) => {
12168
- const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) => existsSync17(path));
13347
+ const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) => existsSync19(path));
12169
13348
  if (!currentRoot) {
12170
13349
  return [];
12171
13350
  }
@@ -12177,7 +13356,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
12177
13356
  taskId: currentTaskId,
12178
13357
  kind: "file",
12179
13358
  label: fileName,
12180
- path: resolve22(currentRoot, fileName),
13359
+ path: resolve24(currentRoot, fileName),
12181
13360
  url: null,
12182
13361
  metadata: {
12183
13362
  fileName
@@ -12220,11 +13399,11 @@ function buildInspectorStreamPayload(state, sequence) {
12220
13399
  }
12221
13400
  function listRemoteRunArtifacts(projectRoot, runId) {
12222
13401
  const root = remoteArtifactsRoot(projectRoot, runId);
12223
- if (!existsSync17(root)) {
13402
+ if (!existsSync19(root)) {
12224
13403
  return [];
12225
13404
  }
12226
13405
  return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isFile()).filter((entry) => !entry.name.endsWith(".json")).map((entry) => {
12227
- const artifactPath = resolve22(root, entry.name);
13406
+ const artifactPath = resolve24(root, entry.name);
12228
13407
  const stat = statSync6(artifactPath);
12229
13408
  const meta = readJsonFile4(`${artifactPath}.json`, null);
12230
13409
  return {
@@ -12385,6 +13564,7 @@ function buildHttpRouterDeps(state) {
12385
13564
  startLocalRun,
12386
13565
  stopRunRecord,
12387
13566
  resumeRunRecord,
13567
+ runServerOwnedPrCloseout,
12388
13568
  claimRemoteRun,
12389
13569
  listRemoteRunArtifacts,
12390
13570
  broadcastSnapshotInvalidation,
@@ -12466,8 +13646,8 @@ function fileStats(path) {
12466
13646
  }
12467
13647
  }
12468
13648
  function runFileCursor(projectRoot, run) {
12469
- const runDir = dirname17(runLogsPath(projectRoot, run.runId));
12470
- const runJson = fileStats(resolve22(runDir, "run.json"));
13649
+ const runDir = dirname20(runLogsPath(projectRoot, run.runId));
13650
+ const runJson = fileStats(resolve24(runDir, "run.json"));
12471
13651
  const timeline = fileStats(runTimelinePath(projectRoot, run.runId));
12472
13652
  const logs = fileStats(runLogsPath(projectRoot, run.runId));
12473
13653
  return {
@@ -12517,10 +13697,10 @@ function startRunFileWatcher(state, pollMs) {
12517
13697
  }, Math.max(250, Math.min(pollMs, 1000)));
12518
13698
  }
12519
13699
  function startPoller(state, pollMs) {
12520
- let offset = existsSync17(state.eventsFile) ? statSync6(state.eventsFile).size : 0;
13700
+ let offset = existsSync19(state.eventsFile) ? statSync6(state.eventsFile).size : 0;
12521
13701
  return setInterval(async () => {
12522
13702
  try {
12523
- if (!existsSync17(state.eventsFile)) {
13703
+ if (!existsSync19(state.eventsFile)) {
12524
13704
  return;
12525
13705
  }
12526
13706
  const file = await open(state.eventsFile, "r");
@@ -12572,6 +13752,7 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
12572
13752
  const server = Bun.serve({
12573
13753
  hostname: options.host,
12574
13754
  port: options.port,
13755
+ idleTimeout: Math.max(10, Math.min(255, Number.parseInt(process.env.RIG_SERVER_IDLE_TIMEOUT_SECONDS || "255", 10) || 255)),
12575
13756
  fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
12576
13757
  websocket: {
12577
13758
  open(ws) {
@@ -12647,7 +13828,7 @@ function resolveProjectRoot() {
12647
13828
  return resolveRigProjectRoot({
12648
13829
  envProjectRoot: process.env.PROJECT_RIG_ROOT ?? null,
12649
13830
  cwd: process.cwd(),
12650
- fallbackRoot: resolve22(import.meta.dir, "../..")
13831
+ fallbackRoot: resolve24(import.meta.dir, "../..")
12651
13832
  });
12652
13833
  }
12653
13834
  var __testOnly = {