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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,
@@ -1023,14 +1024,29 @@ function summarizeUsefulRunError(projectRoot, runId, fallback) {
1023
1024
  const nonGeneric = errorLines.at(-1);
1024
1025
  return nonGeneric ?? (typeof fallback === "string" ? fallback : null);
1025
1026
  }
1027
+ function readRunPiSessionMetadata(projectRoot, runId) {
1028
+ const run = readAuthorityRun(projectRoot, runId);
1029
+ const metadata = run?.piSessionPrivate;
1030
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
1031
+ return null;
1032
+ const record = metadata;
1033
+ const publicMetadata = record.public;
1034
+ const daemonConnection = record.daemonConnection;
1035
+ if (!publicMetadata || typeof publicMetadata !== "object" || Array.isArray(publicMetadata))
1036
+ return null;
1037
+ if (!daemonConnection || typeof daemonConnection !== "object" || Array.isArray(daemonConnection))
1038
+ return null;
1039
+ return metadata;
1040
+ }
1026
1041
  function readRunDetails(projectRoot, runId) {
1027
1042
  const run = readAuthorityRun(projectRoot, runId);
1028
1043
  if (!run) {
1029
1044
  return null;
1030
1045
  }
1031
1046
  const usefulErrorText = isGenericRunFailure(run.errorText) ? summarizeUsefulRunError(projectRoot, runId, run.errorText) : null;
1047
+ const { piSessionPrivate: _piSessionPrivate, ...publicRun } = run;
1032
1048
  return {
1033
- run: usefulErrorText ? { ...run, errorText: usefulErrorText } : run,
1049
+ run: usefulErrorText ? { ...publicRun, errorText: usefulErrorText } : publicRun,
1034
1050
  timeline: readJsonlFile(resolve4(resolveAuthorityRunDir(projectRoot, runId), "timeline.jsonl")),
1035
1051
  approvals: readApprovals(projectRoot, { runId }),
1036
1052
  userInputs: readUserInputs(projectRoot, { runId })
@@ -1073,6 +1089,18 @@ function readJsonlFileTail(path, options) {
1073
1089
  const completeLines = start > 0 ? lines.slice(1) : lines;
1074
1090
  return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
1075
1091
  }
1092
+ async function readRunTimelinePage(projectRoot, runId, options = {}) {
1093
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
1094
+ const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
1095
+ const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
1096
+ const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
1097
+ const endExclusive = Math.min(entries.length, startInclusive + limit);
1098
+ return {
1099
+ entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
1100
+ nextCursor: String(endExclusive),
1101
+ hasMore: endExclusive < entries.length
1102
+ };
1103
+ }
1076
1104
  var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
1077
1105
  async function readRunLogsPage(projectRoot, runId, options = {}) {
1078
1106
  const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
@@ -2312,7 +2340,6 @@ function createGitHubTaskReconciler(input) {
2312
2340
  }
2313
2341
 
2314
2342
  // packages/server/src/server-helpers/issue-analysis.ts
2315
- import { execFile } from "child_process";
2316
2343
  import { createHash } from "crypto";
2317
2344
  function stableIssueHash(issue) {
2318
2345
  const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
@@ -2446,16 +2473,33 @@ function parseIssueAnalysisResult(raw) {
2446
2473
  return result;
2447
2474
  }
2448
2475
  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 ?? "") });
2476
+ return async (command, args, options) => {
2477
+ const env = options.env ? { ...process.env, ...options.env } : process.env;
2478
+ const proc = Bun.spawn([command, ...args], {
2479
+ stdout: "pipe",
2480
+ stderr: "pipe",
2481
+ env
2457
2482
  });
2458
- });
2483
+ let timedOut = false;
2484
+ const timer = setTimeout(() => {
2485
+ timedOut = true;
2486
+ proc.kill();
2487
+ }, options.timeoutMs);
2488
+ try {
2489
+ const [stdout, stderr, exitCode] = await Promise.all([
2490
+ new Response(proc.stdout).text(),
2491
+ new Response(proc.stderr).text(),
2492
+ proc.exited
2493
+ ]);
2494
+ return {
2495
+ exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
2496
+ stdout,
2497
+ stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
2498
+ };
2499
+ } finally {
2500
+ clearTimeout(timer);
2501
+ }
2502
+ };
2459
2503
  }
2460
2504
  function createPiIssueAnalyzer(input = {}) {
2461
2505
  const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
@@ -2709,13 +2753,18 @@ function patchRunRecord(projectRoot, runId, patch) {
2709
2753
  writeJsonFile2(resolve11(resolveAuthorityRunDir2(projectRoot, runId), "run.json"), next);
2710
2754
  return next;
2711
2755
  }
2756
+ function patchRunPiSessionMetadata(projectRoot, runId, metadata) {
2757
+ return patchRunRecord(projectRoot, runId, {
2758
+ piSession: metadata?.public ?? null,
2759
+ piSessionPrivate: metadata
2760
+ });
2761
+ }
2712
2762
  function buildRunStartPatch(startedAt) {
2713
2763
  return {
2714
2764
  status: "preparing",
2715
2765
  startedAt,
2716
2766
  completedAt: null,
2717
- errorText: null,
2718
- serverPid: process.pid
2767
+ errorText: null
2719
2768
  };
2720
2769
  }
2721
2770
 
@@ -3184,7 +3233,7 @@ function applyOrchestrationCommand(state, command) {
3184
3233
  import { spawn as spawn3 } from "child_process";
3185
3234
  import { loadConfig } from "@rig/core/load-config";
3186
3235
  import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
3187
- import { dirname as dirname8, relative as relative2, resolve as resolve14 } from "path";
3236
+ import { dirname as dirname9, relative as relative2, resolve as resolve14 } from "path";
3188
3237
  import {
3189
3238
  listAuthorityRuns as listAuthorityRuns4,
3190
3239
  readAuthorityRun as readAuthorityRun4,
@@ -3197,6 +3246,11 @@ import {
3197
3246
  buildTaskRunLifecycleComment,
3198
3247
  updateConfiguredTaskSourceTask
3199
3248
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
3249
+ import {
3250
+ closeIssueAfterMergedPr,
3251
+ commitRunChanges,
3252
+ runPrAutomation
3253
+ } from "@rig/runtime/control-plane/native/pr-automation";
3200
3254
 
3201
3255
  // packages/server/src/scheduler.ts
3202
3256
  import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
@@ -3308,8 +3362,8 @@ function summarizeRunValidationFailure(projectRoot, run) {
3308
3362
 
3309
3363
  // packages/server/src/server-helpers/github-auth-store.ts
3310
3364
  import { randomBytes } from "crypto";
3311
- import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
3312
- import { resolve as resolve13 } from "path";
3365
+ import { chmodSync, copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
3366
+ import { dirname as dirname8, resolve as resolve13 } from "path";
3313
3367
  function cleanString(value) {
3314
3368
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
3315
3369
  }
@@ -3339,6 +3393,26 @@ function parseApiSessions(value) {
3339
3393
  }];
3340
3394
  });
3341
3395
  }
3396
+ function parsePendingDevice(value) {
3397
+ if (!value || typeof value !== "object")
3398
+ return null;
3399
+ const record = value;
3400
+ const pollId = cleanString(record.pollId);
3401
+ const deviceCode = cleanString(record.deviceCode);
3402
+ const expiresAt = cleanString(record.expiresAt);
3403
+ const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
3404
+ if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
3405
+ return null;
3406
+ return { pollId, deviceCode, expiresAt, intervalSeconds };
3407
+ }
3408
+ function parsePendingDevices(value) {
3409
+ if (!Array.isArray(value))
3410
+ return [];
3411
+ return value.flatMap((entry) => {
3412
+ const pending = parsePendingDevice(entry);
3413
+ return pending ? [pending] : [];
3414
+ });
3415
+ }
3342
3416
  function readStoredAuth(stateFile) {
3343
3417
  if (!existsSync6(stateFile))
3344
3418
  return {};
@@ -3352,6 +3426,7 @@ function readStoredAuth(stateFile) {
3352
3426
  selectedRepo: cleanString(parsed.selectedRepo),
3353
3427
  tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
3354
3428
  pendingDevice: parsePendingDevice(parsed.pendingDevice),
3429
+ pendingDevices: parsePendingDevices(parsed.pendingDevices),
3355
3430
  apiSessions: parseApiSessions(parsed.apiSessions),
3356
3431
  updatedAt: cleanString(parsed.updatedAt) ?? undefined
3357
3432
  };
@@ -3359,34 +3434,36 @@ function readStoredAuth(stateFile) {
3359
3434
  return {};
3360
3435
  }
3361
3436
  }
3362
- function parsePendingDevice(value) {
3363
- if (!value || typeof value !== "object")
3364
- return null;
3365
- const record = value;
3366
- const pollId = cleanString(record.pollId);
3367
- const deviceCode = cleanString(record.deviceCode);
3368
- const expiresAt = cleanString(record.expiresAt);
3369
- const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
3370
- if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
3371
- return null;
3372
- return { pollId, deviceCode, expiresAt, intervalSeconds };
3373
- }
3374
3437
  function newApiSessionToken() {
3375
3438
  return `rig_${randomBytes(32).toString("base64url")}`;
3376
3439
  }
3377
3440
  function writeStoredAuth(stateFile, payload) {
3378
- mkdirSync6(resolve13(stateFile, ".."), { recursive: true });
3441
+ mkdirSync6(dirname8(stateFile), { recursive: true });
3379
3442
  writeFileSync5(stateFile, `${JSON.stringify(payload, null, 2)}
3380
3443
  `, { encoding: "utf8", mode: 384 });
3381
3444
  try {
3382
3445
  chmodSync(stateFile, 384);
3383
3446
  } catch {}
3384
3447
  }
3448
+ function localProjectAuthStateFile(projectRoot) {
3449
+ return resolve13(projectRoot, ".rig", "state", "github-auth.json");
3450
+ }
3385
3451
  function resolveGitHubAuthStateFile(projectRoot) {
3386
3452
  return resolve13(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
3387
3453
  }
3388
- function createGitHubAuthStore(projectRoot) {
3389
- const stateFile = resolveGitHubAuthStateFile(projectRoot);
3454
+ function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
3455
+ const targetFile = localProjectAuthStateFile(projectRoot);
3456
+ mkdirSync6(dirname8(targetFile), { recursive: true });
3457
+ if (existsSync6(stateFile)) {
3458
+ copyFileSync(stateFile, targetFile);
3459
+ try {
3460
+ chmodSync(targetFile, 384);
3461
+ } catch {}
3462
+ return;
3463
+ }
3464
+ writeStoredAuth(targetFile, {});
3465
+ }
3466
+ function createGitHubAuthStoreFromStateFile(stateFile) {
3390
3467
  return {
3391
3468
  stateFile,
3392
3469
  status(options) {
@@ -3416,6 +3493,7 @@ function createGitHubAuthStore(projectRoot) {
3416
3493
  scopes: input.scopes ?? [],
3417
3494
  selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
3418
3495
  pendingDevice: null,
3496
+ pendingDevices: [],
3419
3497
  apiSessions: previous.apiSessions ?? [],
3420
3498
  updatedAt: new Date().toISOString()
3421
3499
  });
@@ -3444,15 +3522,24 @@ function createGitHubAuthStore(projectRoot) {
3444
3522
  const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
3445
3523
  return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
3446
3524
  },
3447
- copyToProjectRoot(projectRoot2) {
3448
- const targetFile = resolveGitHubAuthStateFile(projectRoot2);
3525
+ copyToProjectRoot(projectRoot) {
3526
+ const targetFile = resolveGitHubAuthStateFile(projectRoot);
3449
3527
  writeStoredAuth(targetFile, readStoredAuth(stateFile));
3450
3528
  },
3529
+ copyToLocalProjectRoot(projectRoot) {
3530
+ copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
3531
+ },
3451
3532
  savePendingDevice(input) {
3452
3533
  const previous = readStoredAuth(stateFile);
3534
+ const pendingDevices = [
3535
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
3536
+ ...previous.pendingDevices ?? [],
3537
+ input
3538
+ ].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
3453
3539
  writeStoredAuth(stateFile, {
3454
3540
  ...previous,
3455
- pendingDevice: input,
3541
+ pendingDevice: null,
3542
+ pendingDevices,
3456
3543
  updatedAt: new Date().toISOString()
3457
3544
  });
3458
3545
  },
@@ -3465,23 +3552,32 @@ function createGitHubAuthStore(projectRoot) {
3465
3552
  });
3466
3553
  },
3467
3554
  readPendingDevice(pollId) {
3468
- const pending = readStoredAuth(stateFile).pendingDevice ?? null;
3469
- if (!pending || pending.pollId !== pollId)
3555
+ const previous = readStoredAuth(stateFile);
3556
+ const pending = [
3557
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
3558
+ ...previous.pendingDevices ?? []
3559
+ ].find((entry) => entry.pollId === pollId) ?? null;
3560
+ if (!pending)
3470
3561
  return null;
3471
3562
  if (Date.parse(pending.expiresAt) <= Date.now())
3472
3563
  return null;
3473
3564
  return pending;
3474
3565
  },
3475
- clearPendingDevice() {
3566
+ clearPendingDevice(pollId) {
3476
3567
  const previous = readStoredAuth(stateFile);
3568
+ const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
3477
3569
  writeStoredAuth(stateFile, {
3478
3570
  ...previous,
3479
3571
  pendingDevice: null,
3572
+ pendingDevices: remaining,
3480
3573
  updatedAt: new Date().toISOString()
3481
3574
  });
3482
3575
  }
3483
3576
  };
3484
3577
  }
3578
+ function createGitHubAuthStore(projectRoot) {
3579
+ return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
3580
+ }
3485
3581
 
3486
3582
  // packages/server/src/server-helpers/github-projects.ts
3487
3583
  function asRecord(value) {
@@ -3490,6 +3586,9 @@ function asRecord(value) {
3490
3586
  function asString(value) {
3491
3587
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
3492
3588
  }
3589
+ function asNumber(value) {
3590
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
3591
+ }
3493
3592
  async function defaultGraphQLFetch(query, variables, token) {
3494
3593
  const response = await fetch("https://api.github.com/graphql", {
3495
3594
  method: "POST",
@@ -3506,6 +3605,32 @@ async function defaultGraphQLFetch(query, variables, token) {
3506
3605
  }
3507
3606
  return json.data;
3508
3607
  }
3608
+ function projectNodesFrom(data) {
3609
+ const root = asRecord(data);
3610
+ const owner = asRecord(root?.organization) ?? asRecord(root?.user);
3611
+ const projects = asRecord(owner?.projectsV2);
3612
+ const nodes = projects?.nodes;
3613
+ return Array.isArray(nodes) ? nodes : [];
3614
+ }
3615
+ async function listGitHubProjects(input) {
3616
+ const query = `
3617
+ query RigListProjects($owner: String!, $first: Int!) {
3618
+ organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
3619
+ user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
3620
+ }
3621
+ `;
3622
+ const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
3623
+ const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
3624
+ return projectNodesFrom(data).flatMap((node) => {
3625
+ const record = asRecord(node);
3626
+ const id = asString(record?.id);
3627
+ const number = asNumber(record?.number);
3628
+ const title = asString(record?.title);
3629
+ if (!id || number === undefined || !title)
3630
+ return [];
3631
+ return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
3632
+ });
3633
+ }
3509
3634
  async function resolveProjectStatusField(input) {
3510
3635
  const query = `
3511
3636
  query RigProjectStatusField($projectId: ID!) {
@@ -3600,6 +3725,7 @@ var DEFAULT_PROJECT_STATUSES = {
3600
3725
  running: "In Progress",
3601
3726
  prOpen: "In Review",
3602
3727
  ciFixing: "In Review",
3728
+ merging: "Merging",
3603
3729
  done: "Done",
3604
3730
  needsAttention: "Needs Attention"
3605
3731
  };
@@ -3613,6 +3739,8 @@ function lifecycleStatusForTaskStatus(status) {
3613
3739
  return "prOpen";
3614
3740
  if (normalized === "ci_fixing" || normalized === "fixing")
3615
3741
  return "ciFixing";
3742
+ if (normalized === "merging" || normalized === "merge")
3743
+ return "merging";
3616
3744
  if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
3617
3745
  return "needsAttention";
3618
3746
  if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
@@ -3741,9 +3869,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
3741
3869
  return null;
3742
3870
  return null;
3743
3871
  }
3872
+ function githubProjectsEnabled(config) {
3873
+ const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
3874
+ const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
3875
+ return projects?.enabled === true;
3876
+ }
3744
3877
  async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
3745
3878
  if (!run.taskId)
3746
- return;
3879
+ return false;
3747
3880
  const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
3748
3881
  try {
3749
3882
  const result = await syncGitHubProjectStatusForTaskUpdate({
@@ -3754,28 +3887,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
3754
3887
  config
3755
3888
  });
3756
3889
  if (!result.synced && result.reason !== "project-sync-disabled") {
3890
+ const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
3757
3891
  appendRunLogEntry(projectRoot, run.runId, {
3758
3892
  id: `log:${run.runId}:github-project-sync:${status}`,
3759
3893
  title: "GitHub Project sync skipped",
3760
- detail: `Project status sync for ${run.taskId} could not run: ${result.reason}.`,
3894
+ detail,
3761
3895
  tone: "warn",
3762
3896
  status: "running",
3763
3897
  createdAt: new Date().toISOString(),
3764
3898
  payload: { reason: result.reason, issueNodeId }
3765
3899
  });
3900
+ if (githubProjectsEnabled(config)) {
3901
+ throw new Error(detail);
3902
+ }
3903
+ return false;
3766
3904
  }
3905
+ return result.synced === true;
3767
3906
  } catch (error) {
3907
+ const detail = error instanceof Error ? error.message : String(error);
3768
3908
  appendRunLogEntry(projectRoot, run.runId, {
3769
3909
  id: `log:${run.runId}:github-project-sync-error:${status}`,
3770
3910
  title: "GitHub Project sync failed",
3771
- detail: error instanceof Error ? error.message : String(error),
3911
+ detail,
3772
3912
  tone: "error",
3773
3913
  status: "running",
3774
3914
  createdAt: new Date().toISOString(),
3775
3915
  payload: { issueNodeId }
3776
3916
  });
3917
+ if (githubProjectsEnabled(config)) {
3918
+ throw new Error(detail);
3919
+ }
3920
+ return false;
3777
3921
  }
3778
3922
  }
3923
+ function createCommandRunner(binary, extraEnv = {}) {
3924
+ return async (args, options) => {
3925
+ const child = spawn3(binary, [...args], {
3926
+ cwd: options?.cwd,
3927
+ env: { ...process.env, ...extraEnv },
3928
+ stdio: ["ignore", "pipe", "pipe"]
3929
+ });
3930
+ const stdoutChunks = [];
3931
+ const stderrChunks = [];
3932
+ child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
3933
+ child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
3934
+ const exitCode = await new Promise((resolve15) => {
3935
+ child.once("error", () => resolve15(1));
3936
+ child.once("close", (code) => resolve15(code ?? 1));
3937
+ });
3938
+ return {
3939
+ exitCode,
3940
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
3941
+ stderr: Buffer.concat(stderrChunks).toString("utf8")
3942
+ };
3943
+ };
3944
+ }
3945
+ function closeoutRecord(run) {
3946
+ const value = run.serverCloseout;
3947
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
3948
+ }
3949
+ function closeoutPhasePatch(phase, status, extra = {}) {
3950
+ const updatedAt = new Date().toISOString();
3951
+ return {
3952
+ serverCloseout: {
3953
+ ...extra,
3954
+ phase,
3955
+ status,
3956
+ updatedAt
3957
+ }
3958
+ };
3959
+ }
3960
+ function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
3961
+ appendRunLogEntryAndBroadcast(state, runId, {
3962
+ id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
3963
+ title: `Server closeout: ${phase}`,
3964
+ detail,
3965
+ tone,
3966
+ status,
3967
+ createdAt: new Date().toISOString()
3968
+ }, `server-closeout-${phase}`);
3969
+ }
3779
3970
  async function autoAssignRunIssue(projectRoot, run) {
3780
3971
  if (!run.taskId)
3781
3972
  return;
@@ -3805,7 +3996,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
3805
3996
  return;
3806
3997
  }
3807
3998
  const config = await loadRigLifecycleConfig(projectRoot);
3808
- await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
3999
+ const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
3809
4000
  if (status === "in_progress") {
3810
4001
  await autoAssignRunIssue(projectRoot, run);
3811
4002
  }
@@ -3821,24 +4012,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
3821
4012
  });
3822
4013
  return;
3823
4014
  }
3824
- const result = await updateConfiguredTaskSourceTask(projectRoot, {
3825
- taskId: run.taskId,
3826
- sourceTask: runSourceTaskIdentity(run),
3827
- update: {
3828
- status,
3829
- comment: buildTaskRunLifecycleComment({
3830
- runId: run.runId,
4015
+ const sourceTask = runSourceTaskIdentity(run);
4016
+ const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
4017
+ const rollbackProjectSync = async () => {
4018
+ if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
4019
+ return;
4020
+ await syncGitHubProjectStatusForTaskUpdate({
4021
+ taskId: run.taskId,
4022
+ status: previousStatus,
4023
+ issueNodeId: extractGitHubIssueNodeId(sourceTask),
4024
+ token: createGitHubAuthStore(projectRoot).readToken(),
4025
+ config
4026
+ }).catch((rollbackError) => {
4027
+ appendRunLogEntry(projectRoot, run.runId, {
4028
+ id: `log:${run.runId}:github-project-sync-rollback:${status}`,
4029
+ title: "GitHub Project sync rollback failed",
4030
+ detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
4031
+ tone: "error",
4032
+ status: "running",
4033
+ createdAt: new Date().toISOString()
4034
+ });
4035
+ });
4036
+ };
4037
+ let result;
4038
+ try {
4039
+ result = await updateConfiguredTaskSourceTask(projectRoot, {
4040
+ taskId: run.taskId,
4041
+ sourceTask,
4042
+ update: {
3831
4043
  status,
3832
- summary,
3833
- runtimeWorkspace: normalizeString(run.worktreePath),
3834
- logsDir: normalizeString(run.logRoot),
3835
- sessionDir: normalizeString(run.sessionPath),
3836
- errorText: options.errorText ?? normalizeString(run.errorText)
3837
- })
3838
- }
3839
- });
4044
+ comment: buildTaskRunLifecycleComment({
4045
+ runId: run.runId,
4046
+ status,
4047
+ summary,
4048
+ runtimeWorkspace: normalizeString(run.worktreePath),
4049
+ logsDir: normalizeString(run.logRoot),
4050
+ sessionDir: normalizeString(run.sessionPath),
4051
+ errorText: options.errorText ?? normalizeString(run.errorText)
4052
+ })
4053
+ }
4054
+ });
4055
+ } catch (error) {
4056
+ await rollbackProjectSync();
4057
+ throw error;
4058
+ }
3840
4059
  if (!result.updated) {
3841
4060
  if (result.source === "plugin" || result.sourceKind) {
4061
+ await rollbackProjectSync();
3842
4062
  throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
3843
4063
  }
3844
4064
  appendRunLogEntry(projectRoot, run.runId, {
@@ -3851,6 +4071,277 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
3851
4071
  });
3852
4072
  }
3853
4073
  }
4074
+ async function markServerOwnedCloseoutFailed(state, runId, error) {
4075
+ const detail = error instanceof Error ? error.message : String(error);
4076
+ const current = readAuthorityRun4(state.projectRoot, runId);
4077
+ patchRunRecord(state.projectRoot, runId, {
4078
+ status: "failed",
4079
+ completedAt: new Date().toISOString(),
4080
+ errorText: detail,
4081
+ ...closeoutPhasePatch("failed", "failed", { error: detail })
4082
+ });
4083
+ appendRunLogEntryAndBroadcast(state, runId, {
4084
+ id: `log:${runId}:server-closeout-failed`,
4085
+ title: "Server-owned closeout failed",
4086
+ detail,
4087
+ tone: "error",
4088
+ status: "failed",
4089
+ createdAt: new Date().toISOString()
4090
+ }, "server-closeout-failed");
4091
+ if (current?.taskId) {
4092
+ await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: detail }, "failed", "Rig server-owned closeout failed.", { errorText: detail }).catch((sourceError) => {
4093
+ appendRunLogEntry(state.projectRoot, runId, {
4094
+ id: `log:${runId}:task-source-closeout-failed-update`,
4095
+ title: "Task source closeout failure update failed",
4096
+ detail: sourceError instanceof Error ? sourceError.message : String(sourceError),
4097
+ tone: "error",
4098
+ status: "failed",
4099
+ createdAt: new Date().toISOString()
4100
+ });
4101
+ });
4102
+ }
4103
+ }
4104
+ function scheduleServerOwnedPrCloseout(state, runId, reason) {
4105
+ const startedAt = new Date().toISOString();
4106
+ state.runProcesses.set(runId, {
4107
+ runId,
4108
+ child: null,
4109
+ startedAt,
4110
+ stopped: false
4111
+ });
4112
+ queueMicrotask(() => {
4113
+ withServerAuthorityEnvIfNeeded(state.projectRoot, async () => {
4114
+ try {
4115
+ await runServerOwnedPrCloseout(state, runId);
4116
+ } catch (error) {
4117
+ await markServerOwnedCloseoutFailed(state, runId, error);
4118
+ } finally {
4119
+ state.runProcesses.delete(runId);
4120
+ broadcastSnapshotInvalidation(state, `server-closeout-${reason}-terminal`);
4121
+ await reconcileScheduler(state, `server-closeout-${reason}-terminal`);
4122
+ }
4123
+ });
4124
+ });
4125
+ }
4126
+ async function runServerOwnedPrCloseout(state, runId) {
4127
+ const run = readAuthorityRun4(state.projectRoot, runId);
4128
+ if (!run)
4129
+ throw new Error(`Run not found: ${runId}`);
4130
+ const closeout = closeoutRecord(run);
4131
+ if (!closeout)
4132
+ return;
4133
+ const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
4134
+ if (!taskId)
4135
+ throw new Error("Server-owned closeout requires a task id.");
4136
+ const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
4137
+ let branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
4138
+ const config = await loadRigLifecycleConfig(state.projectRoot);
4139
+ const runPrMode = normalizeString(run.prMode);
4140
+ const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
4141
+ const effectiveConfig = {
4142
+ ...config ?? {},
4143
+ pr: {
4144
+ ...config?.pr ?? {},
4145
+ mode: prMode,
4146
+ autoFixChecks: false,
4147
+ autoFixReview: false
4148
+ }
4149
+ };
4150
+ const readCurrentRun = () => readAuthorityRun4(state.projectRoot, runId) ?? run;
4151
+ const sourceTask = runSourceTaskIdentity(run);
4152
+ const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
4153
+ const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
4154
+ const closeoutPrUrl = normalizeString(closeout.prUrl);
4155
+ if (closeoutPhase === "completed" || closeoutStatus === "completed") {
4156
+ return;
4157
+ }
4158
+ if (closeoutPhase === "close-source" && closeoutPrUrl) {
4159
+ patchRunRecord(state.projectRoot, runId, {
4160
+ status: "reviewing",
4161
+ ...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
4162
+ });
4163
+ await closeIssueAfterMergedPr({
4164
+ projectRoot: state.projectRoot,
4165
+ taskId,
4166
+ runId,
4167
+ prUrl: closeoutPrUrl,
4168
+ sourceTask,
4169
+ updateTaskSource: async (projectRoot, input) => {
4170
+ await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
4171
+ return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
4172
+ }
4173
+ });
4174
+ const completedAt = new Date().toISOString();
4175
+ patchRunRecord(state.projectRoot, runId, {
4176
+ status: "completed",
4177
+ completedAt,
4178
+ errorText: null,
4179
+ ...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
4180
+ });
4181
+ appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
4182
+ emitRigEvent(state, {
4183
+ type: "rig.run.completed",
4184
+ aggregateId: runId,
4185
+ payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
4186
+ createdAt: completedAt
4187
+ });
4188
+ return;
4189
+ }
4190
+ if (prMode === "off" || prMode === "ask") {
4191
+ const completedAt = new Date().toISOString();
4192
+ patchRunRecord(state.projectRoot, runId, {
4193
+ status: "completed",
4194
+ completedAt,
4195
+ errorText: null,
4196
+ ...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
4197
+ });
4198
+ appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
4199
+ emitRigEvent(state, {
4200
+ type: "rig.run.completed",
4201
+ aggregateId: runId,
4202
+ payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
4203
+ createdAt: completedAt
4204
+ });
4205
+ return;
4206
+ }
4207
+ const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
4208
+ const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
4209
+ const gitCommand = createCommandRunner("git", githubEnv);
4210
+ const ghCommand = createCommandRunner("gh", githubEnv);
4211
+ const workspaceBranch = await gitCommand(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: workspace });
4212
+ const currentWorkspaceBranch = workspaceBranch.exitCode === 0 ? normalizeString(workspaceBranch.stdout) : null;
4213
+ if (currentWorkspaceBranch && currentWorkspaceBranch !== "HEAD" && currentWorkspaceBranch !== branch) {
4214
+ appendCloseoutStage(state, runId, "branch", `Using runtime workspace branch ${currentWorkspaceBranch} instead of recorded branch ${branch}.`, "reviewing", "info");
4215
+ branch = currentWorkspaceBranch;
4216
+ }
4217
+ const setCloseout = (phase, status, extra = {}) => {
4218
+ const previous = closeoutRecord(readCurrentRun()) ?? closeout;
4219
+ patchRunRecord(state.projectRoot, runId, {
4220
+ status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
4221
+ ...closeoutPhasePatch(phase, status, { ...previous, ...extra })
4222
+ });
4223
+ };
4224
+ setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
4225
+ appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
4226
+ const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
4227
+ appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
4228
+ setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
4229
+ const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
4230
+ if (push.exitCode !== 0) {
4231
+ throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
4232
+ }
4233
+ const sourceTaskForPr = {
4234
+ title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
4235
+ };
4236
+ const artifactRoot = resolve14(state.projectRoot, "artifacts", taskId);
4237
+ setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4238
+ const pr = await runPrAutomation({
4239
+ projectRoot: workspace,
4240
+ taskId,
4241
+ runId,
4242
+ branch,
4243
+ config: effectiveConfig,
4244
+ sourceTask: sourceTaskForPr,
4245
+ artifactRoot,
4246
+ command: ghCommand,
4247
+ gitCommand,
4248
+ steerPi: async (message) => {
4249
+ appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
4250
+ appendRunTimelineEntry(state.projectRoot, runId, {
4251
+ id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
4252
+ type: "user_message",
4253
+ text: message,
4254
+ createdAt: new Date().toISOString(),
4255
+ state: "completed"
4256
+ });
4257
+ },
4258
+ lifecycle: {
4259
+ onPrOpened: async ({ prUrl }) => {
4260
+ setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4261
+ appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
4262
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
4263
+ },
4264
+ onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
4265
+ onFeedback: async ({ feedback }) => {
4266
+ appendCloseoutStage(state, runId, "feedback", feedback.join(`
4267
+ `), "reviewing", "error");
4268
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
4269
+ },
4270
+ onMergeStarted: async ({ prUrl }) => {
4271
+ setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4272
+ appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
4273
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
4274
+ },
4275
+ onMerged: ({ prUrl }) => {
4276
+ setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
4277
+ appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
4278
+ }
4279
+ }
4280
+ });
4281
+ if (pr.status === "merged" && pr.prUrl) {
4282
+ setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
4283
+ await closeIssueAfterMergedPr({
4284
+ projectRoot: state.projectRoot,
4285
+ taskId,
4286
+ runId,
4287
+ prUrl: pr.prUrl,
4288
+ sourceTask,
4289
+ updateTaskSource: async (projectRoot, input) => {
4290
+ await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
4291
+ return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
4292
+ }
4293
+ });
4294
+ const completedAt = new Date().toISOString();
4295
+ patchRunRecord(state.projectRoot, runId, {
4296
+ status: "completed",
4297
+ completedAt,
4298
+ errorText: null,
4299
+ ...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
4300
+ });
4301
+ appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
4302
+ emitRigEvent(state, {
4303
+ type: "rig.run.completed",
4304
+ aggregateId: runId,
4305
+ payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
4306
+ createdAt: completedAt
4307
+ });
4308
+ return;
4309
+ }
4310
+ if (pr.status === "opened" && pr.prUrl) {
4311
+ const completedAt = new Date().toISOString();
4312
+ patchRunRecord(state.projectRoot, runId, {
4313
+ status: "completed",
4314
+ completedAt,
4315
+ errorText: null,
4316
+ ...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
4317
+ });
4318
+ appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
4319
+ emitRigEvent(state, {
4320
+ type: "rig.run.completed",
4321
+ aggregateId: runId,
4322
+ payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
4323
+ createdAt: completedAt
4324
+ });
4325
+ return;
4326
+ }
4327
+ const detail = pr.actionableFeedback.join(`
4328
+ `) || "PR automation did not merge the PR.";
4329
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
4330
+ appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
4331
+ });
4332
+ patchRunRecord(state.projectRoot, runId, {
4333
+ status: "needs_attention",
4334
+ completedAt: new Date().toISOString(),
4335
+ errorText: detail,
4336
+ ...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
4337
+ });
4338
+ appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
4339
+ emitRigEvent(state, {
4340
+ type: "rig.run.needs-attention",
4341
+ aggregateId: runId,
4342
+ payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
4343
+ });
4344
+ }
3854
4345
  var TERMINAL_RUN_STATUSES2 = new Set([
3855
4346
  "completed",
3856
4347
  "complete",
@@ -3876,11 +4367,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
3876
4367
  return;
3877
4368
  throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
3878
4369
  }
4370
+ async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
4371
+ const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
4372
+ if (fromReader)
4373
+ return fromReader;
4374
+ const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
4375
+ if (projected)
4376
+ return projected;
4377
+ if (readTasks !== readWorkspaceTasks) {
4378
+ return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
4379
+ }
4380
+ return null;
4381
+ }
3879
4382
  async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
3880
4383
  if ("taskId" in input && input.taskId) {
3881
4384
  assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
3882
4385
  }
3883
- const sourceTask = "taskId" in input && input.taskId ? (await readTasks(projectRoot)).find((task) => task.id === input.taskId) ?? null : null;
4386
+ const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
3884
4387
  const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
3885
4388
  const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
3886
4389
  const runRecord = {
@@ -3952,6 +4455,7 @@ async function startLocalRun(state, runId, options) {
3952
4455
  throw new Error(`Run not found: ${runId}`);
3953
4456
  }
3954
4457
  const startedAt = new Date().toISOString();
4458
+ const resumeMode = options?.resume === true;
3955
4459
  state.runProcesses.set(runId, {
3956
4460
  runId,
3957
4461
  child: null,
@@ -3968,9 +4472,9 @@ async function startLocalRun(state, runId, options) {
3968
4472
  summary: run.title
3969
4473
  });
3970
4474
  appendRunLogEntry(state.projectRoot, runId, {
3971
- id: `log:${runId}:prepare`,
3972
- title: "Rig task run starting",
3973
- detail: run.taskId ?? run.title,
4475
+ id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
4476
+ title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
4477
+ detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
3974
4478
  tone: "info",
3975
4479
  status: "preparing",
3976
4480
  createdAt: startedAt
@@ -4046,12 +4550,17 @@ async function startLocalRun(state, runId, options) {
4046
4550
  RIG_HOST_PROJECT_ROOT: cliProjectRoot,
4047
4551
  RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
4048
4552
  RIG_SERVER_INTERNAL_EXEC: "1",
4553
+ RIG_SERVER_OWNS_CLOSEOUT: "1",
4049
4554
  ...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
4050
4555
  ...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
4051
4556
  ...bridgeGitHubToken ? {
4052
4557
  RIG_GITHUB_TOKEN: bridgeGitHubToken,
4053
4558
  GITHUB_TOKEN: bridgeGitHubToken,
4054
4559
  GH_TOKEN: bridgeGitHubToken
4560
+ } : {},
4561
+ ...resumeMode ? {
4562
+ RIG_RUN_RESUME: "1",
4563
+ RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
4055
4564
  } : {}
4056
4565
  },
4057
4566
  stdio: ["ignore", "pipe", "pipe"]
@@ -4075,6 +4584,25 @@ async function startLocalRun(state, runId, options) {
4075
4584
  broadcastSnapshotInvalidation(state);
4076
4585
  continue;
4077
4586
  }
4587
+ if (line.startsWith("__RIG_WRAPPER_EVENT__")) {
4588
+ try {
4589
+ const wrapperEvent = JSON.parse(line.slice("__RIG_WRAPPER_EVENT__".length));
4590
+ const eventType = normalizeString(wrapperEvent.type);
4591
+ const payload = wrapperEvent.payload && typeof wrapperEvent.payload === "object" && !Array.isArray(wrapperEvent.payload) ? wrapperEvent.payload : {};
4592
+ if (eventType === "pi.session.ready" && payload.privateMetadata && typeof payload.privateMetadata === "object" && !Array.isArray(payload.privateMetadata)) {
4593
+ patchRunPiSessionMetadata(state.projectRoot, runId, payload.privateMetadata);
4594
+ }
4595
+ appendRunTimelineEntry(state.projectRoot, runId, {
4596
+ id: `timeline:${runId}:${Date.now()}:wrapper:${eventType ?? "event"}`,
4597
+ type: "wrapper-event",
4598
+ eventType,
4599
+ payload,
4600
+ createdAt: normalizeString(wrapperEvent.at) ?? new Date().toISOString()
4601
+ });
4602
+ } catch {}
4603
+ broadcastSnapshotInvalidation(state, "wrapper-event");
4604
+ continue;
4605
+ }
4078
4606
  appendRunLogEntryAndBroadcast(state, runId, {
4079
4607
  id: `log:${runId}:${Date.now()}`,
4080
4608
  title,
@@ -4103,7 +4631,13 @@ async function startLocalRun(state, runId, options) {
4103
4631
  if (!current) {
4104
4632
  return;
4105
4633
  }
4106
- if (exit.code !== 0 && current.status !== "completed" && current.status !== "stopped") {
4634
+ if (closeoutRecord(current)?.status === "pending") {
4635
+ try {
4636
+ await runServerOwnedPrCloseout(state, runId);
4637
+ } catch (closeoutError) {
4638
+ await markServerOwnedCloseoutFailed(state, runId, closeoutError);
4639
+ }
4640
+ } else if (exit.code !== 0 && current.status !== "completed" && current.status !== "stopped") {
4107
4641
  const completedAt = current.completedAt ?? new Date().toISOString();
4108
4642
  const failureSummary = normalizeString(current.errorText) ?? summarizeRunValidationFailure(state.projectRoot, current) ?? `Rig task-run exited with code ${String(exit.code ?? "unknown")}`;
4109
4643
  if (current.status !== "failed") {
@@ -4203,7 +4737,7 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
4203
4737
  }
4204
4738
  try {
4205
4739
  const monorepoRoot = resolveMonorepoRoot3(projectRoot);
4206
- const outerProjectRoot = dirname8(dirname8(monorepoRoot));
4740
+ const outerProjectRoot = dirname9(dirname9(monorepoRoot));
4207
4741
  if (existsSync7(resolve14(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
4208
4742
  return outerProjectRoot;
4209
4743
  }
@@ -4224,7 +4758,19 @@ async function resumeRunRecord(state, input) {
4224
4758
  if (run.status === "completed") {
4225
4759
  throw new Error("Completed runs cannot be resumed.");
4226
4760
  }
4227
- await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
4761
+ const closeout = closeoutRecord(run);
4762
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
4763
+ if (EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
4764
+ patchRunRecord(state.projectRoot, input.runId, {
4765
+ status: "reviewing",
4766
+ completedAt: null,
4767
+ errorText: null,
4768
+ ...closeoutPhasePatch("queued", "pending", { ...closeout, resumedAt: input.createdAt })
4769
+ });
4770
+ scheduleServerOwnedPrCloseout(state, input.runId, "explicit-resume");
4771
+ return;
4772
+ }
4773
+ await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
4228
4774
  }
4229
4775
  function appendRunMessage(projectRoot, input) {
4230
4776
  const run = readAuthorityRun4(projectRoot, input.runId);
@@ -4308,34 +4854,63 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
4308
4854
  writeQueueState(projectRoot, next);
4309
4855
  return next;
4310
4856
  }
4311
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
4312
- function reconcileOrphanedLocalRuns(state, runs, nowIso) {
4313
- let changed = false;
4314
- for (const run of runs) {
4315
- const status = normalizeString(run.status)?.toLowerCase() ?? "";
4316
- const serverPid = run.serverPid;
4317
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
4318
- if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
4319
- continue;
4320
- }
4321
- const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
4322
- patchRunRecord(state.projectRoot, run.runId, {
4323
- status: "failed",
4324
- completedAt: run.completedAt ?? nowIso,
4325
- updatedAt: nowIso,
4326
- errorText: detail
4327
- });
4328
- appendRunLogEntry(state.projectRoot, run.runId, {
4329
- id: `log:${run.runId}:stale-local-run`,
4330
- title: "Run marked stale after server restart",
4331
- detail,
4332
- tone: "error",
4333
- status: "failed",
4334
- createdAt: nowIso
4857
+ var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
4858
+ var EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running", "needs_attention"]);
4859
+ var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
4860
+ function processExists(pid) {
4861
+ if (!Number.isInteger(pid) || pid <= 0)
4862
+ return false;
4863
+ try {
4864
+ process.kill(pid, 0);
4865
+ return true;
4866
+ } catch {
4867
+ return false;
4868
+ }
4869
+ }
4870
+ function recoverStaleLocalRun(projectRoot, run) {
4871
+ const record = run;
4872
+ if (run.mode !== "local")
4873
+ return false;
4874
+ const closeout = closeoutRecord(record);
4875
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
4876
+ const status = normalizeString(record.status)?.toLowerCase() ?? "";
4877
+ if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus))
4878
+ return false;
4879
+ if (closeoutStatus === "needs_attention") {
4880
+ if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
4881
+ return false;
4882
+ const completedAt2 = record.completedAt ?? new Date().toISOString();
4883
+ patchRunRecord(projectRoot, run.runId, {
4884
+ status: "needs_attention",
4885
+ completedAt: completedAt2,
4886
+ errorText: normalizeString(record.errorText) ?? (Array.isArray(closeout?.feedback) ? closeout.feedback.map(String).join(`
4887
+ `) : null)
4335
4888
  });
4336
- changed = true;
4889
+ return true;
4337
4890
  }
4338
- return changed;
4891
+ if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
4892
+ return false;
4893
+ const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
4894
+ const childPid = typeof record.pid === "number" ? record.pid : null;
4895
+ if (serverPid === null && childPid === null)
4896
+ return false;
4897
+ const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
4898
+ if (hasLiveRecordedProcess && serverPid === process.pid)
4899
+ return false;
4900
+ const completedAt = new Date().toISOString();
4901
+ patchRunRecord(projectRoot, run.runId, {
4902
+ status: "failed",
4903
+ completedAt,
4904
+ errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
4905
+ });
4906
+ return true;
4907
+ }
4908
+ function collectResumableServerCloseouts(state, runs) {
4909
+ return runs.filter((run) => {
4910
+ const closeout = closeoutRecord(run);
4911
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
4912
+ return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
4913
+ });
4339
4914
  }
4340
4915
  async function reconcileScheduler(state, reason) {
4341
4916
  if (state.scheduler.reconciling) {
@@ -4350,7 +4925,28 @@ async function reconcileScheduler(state, reason) {
4350
4925
  const queue = readQueueState(state.projectRoot);
4351
4926
  const tasks = await state.snapshotService.getWorkspaceTasks();
4352
4927
  let runs = listAuthorityRuns4(state.projectRoot);
4353
- let changed = reconcileOrphanedLocalRuns(state, runs, new Date().toISOString());
4928
+ let changed = false;
4929
+ for (const run of runs) {
4930
+ if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
4931
+ changed = true;
4932
+ }
4933
+ }
4934
+ if (changed) {
4935
+ runs = listAuthorityRuns4(state.projectRoot);
4936
+ }
4937
+ const resumableCloseouts = collectResumableServerCloseouts(state, runs);
4938
+ for (const run of resumableCloseouts) {
4939
+ appendRunLogEntry(state.projectRoot, run.runId, {
4940
+ id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
4941
+ title: "Server-owned closeout auto-resume scheduled",
4942
+ detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
4943
+ tone: "info",
4944
+ status: "reviewing",
4945
+ createdAt: new Date().toISOString()
4946
+ });
4947
+ scheduleServerOwnedPrCloseout(state, run.runId, "auto-resume");
4948
+ changed = true;
4949
+ }
4354
4950
  if (changed) {
4355
4951
  runs = listAuthorityRuns4(state.projectRoot);
4356
4952
  }
@@ -4426,11 +5022,11 @@ async function reconcileScheduler(state, reason) {
4426
5022
  // packages/server/src/server-helpers/http-router.ts
4427
5023
  import { randomUUID } from "crypto";
4428
5024
  import { spawnSync as spawnSync3 } from "child_process";
4429
- import { basename, dirname as dirname12, isAbsolute as isAbsolute3, resolve as resolve18 } from "path";
4430
- import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync10 } from "fs";
5025
+ import { basename, dirname as dirname15, isAbsolute as isAbsolute4, resolve as resolve20 } from "path";
5026
+ import { copyFileSync as copyFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync12 } from "fs";
4431
5027
  import {
4432
5028
  listAuthorityRuns as listAuthorityRuns5,
4433
- readAuthorityRun as readAuthorityRun6,
5029
+ readAuthorityRun as readAuthorityRun7,
4434
5030
  resolveAuthorityPaths,
4435
5031
  writeJsonFile as writeJsonFile4
4436
5032
  } from "@rig/runtime/control-plane/authority-files";
@@ -4450,10 +5046,64 @@ import {
4450
5046
  RemoteWsClient
4451
5047
  } from "@rig/runtime/control-plane/remote";
4452
5048
 
5049
+ // packages/server/src/server-helpers/pi-session-proxy.ts
5050
+ import { readAuthorityRun as readAuthorityRun5 } from "@rig/runtime/control-plane/authority-files";
5051
+ function resolveRunPiSessionProxy(projectRoot, runId) {
5052
+ const run = readAuthorityRun5(projectRoot, runId);
5053
+ if (!run)
5054
+ return null;
5055
+ const privateMetadata = readRunPiSessionMetadata(projectRoot, runId);
5056
+ if (!privateMetadata)
5057
+ return { pending: true, runId, status: typeof run.status === "string" ? run.status : undefined };
5058
+ const connection = privateMetadata.daemonConnection;
5059
+ if (connection.mode !== "http")
5060
+ throw new Error("Only loopback HTTP Rig Pi session daemon connections are supported");
5061
+ const token = tokenFromRef(connection.tokenRef);
5062
+ if (!token)
5063
+ throw new Error("Rig Pi session daemon token is unavailable");
5064
+ return {
5065
+ runId,
5066
+ sessionId: privateMetadata.public.sessionId,
5067
+ metadata: privateMetadata.public,
5068
+ privateMetadata,
5069
+ baseUrl: connection.baseUrl.replace(/\/+$/, ""),
5070
+ token
5071
+ };
5072
+ }
5073
+ async function proxyRunPiHttp(projectRoot, runId, input) {
5074
+ const resolved = resolveRunPiSessionProxy(projectRoot, runId);
5075
+ if (resolved === null)
5076
+ return { status: 404, payload: { ok: false, error: "Run not found" } };
5077
+ if ("pending" in resolved)
5078
+ return { status: 409, payload: { ready: false, runId, status: resolved.status, retryAfterMs: 500 } };
5079
+ const response = await fetch(`${resolved.baseUrl}${input.daemonPath}`, {
5080
+ method: input.method,
5081
+ headers: {
5082
+ authorization: `Bearer ${resolved.token}`,
5083
+ ...input.body === undefined ? {} : { "content-type": "application/json" }
5084
+ },
5085
+ body: input.body === undefined ? undefined : JSON.stringify(input.body)
5086
+ });
5087
+ const text = await response.text();
5088
+ const payload = text.trim() ? JSON.parse(text) : null;
5089
+ return { status: response.status, payload };
5090
+ }
5091
+ function buildRunPiDaemonWebSocketUrl(resolved) {
5092
+ const url = new URL(`${resolved.baseUrl}/sessions/${encodeURIComponent(resolved.sessionId)}/events`);
5093
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
5094
+ url.searchParams.set("token", resolved.token);
5095
+ return url.toString();
5096
+ }
5097
+ function tokenFromRef(ref) {
5098
+ if (ref.startsWith("inline:"))
5099
+ return ref.slice("inline:".length) || null;
5100
+ return null;
5101
+ }
5102
+
4453
5103
  // packages/server/src/server-helpers/run-steering.ts
4454
- import { dirname as dirname9, resolve as resolve15 } from "path";
5104
+ import { dirname as dirname10, resolve as resolve15 } from "path";
4455
5105
  import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync5 } from "fs";
4456
- import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun5, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
5106
+ import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun6, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
4457
5107
  var steeringSequence = 0;
4458
5108
  function runSteeringPath(projectRoot, runId) {
4459
5109
  return resolve15(resolveAuthorityRunDir4(projectRoot, runId), "steering.jsonl");
@@ -4522,7 +5172,7 @@ function markQueuedRunSteeringMessagesDelivered(projectRoot, runId, ids) {
4522
5172
  return delivered;
4523
5173
  }
4524
5174
  function queueRunSteeringMessage(projectRoot, runId, input) {
4525
- const run = readAuthorityRun5(projectRoot, runId);
5175
+ const run = readAuthorityRun6(projectRoot, runId);
4526
5176
  if (!run)
4527
5177
  throw new Error(`Run not found: ${runId}`);
4528
5178
  const text = input.message.trim();
@@ -4538,7 +5188,7 @@ function queueRunSteeringMessage(projectRoot, runId, input) {
4538
5188
  delivered: false
4539
5189
  };
4540
5190
  const path = runSteeringPath(projectRoot, runId);
4541
- mkdirSync8(dirname9(path), { recursive: true });
5191
+ mkdirSync8(dirname10(path), { recursive: true });
4542
5192
  appendJsonlRecord2(path, entry);
4543
5193
  appendRunTimelineEntry(projectRoot, runId, {
4544
5194
  id: entry.id,
@@ -4575,6 +5225,187 @@ import {
4575
5225
  updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
4576
5226
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
4577
5227
 
5228
+ // packages/server/src/server-helpers/github-api-session-index.ts
5229
+ import { chmodSync as chmodSync3, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
5230
+ import { dirname as dirname12, resolve as resolve17 } from "path";
5231
+
5232
+ // packages/server/src/server-helpers/github-user-namespace.ts
5233
+ import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync8 } from "fs";
5234
+ import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve16 } from "path";
5235
+ function cleanString3(value) {
5236
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
5237
+ }
5238
+ function sanitizePathSegment(value) {
5239
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
5240
+ }
5241
+ function deriveGitHubUserNamespaceKey(identity) {
5242
+ const userId = cleanString3(identity.userId);
5243
+ if (userId) {
5244
+ const safeId = sanitizePathSegment(userId);
5245
+ if (safeId)
5246
+ return `ghu-${safeId}`;
5247
+ }
5248
+ const login = cleanString3(identity.login);
5249
+ if (login) {
5250
+ const safeLogin = sanitizePathSegment(login);
5251
+ if (safeLogin)
5252
+ return `ghu-login-${safeLogin}`;
5253
+ }
5254
+ throw new Error("GitHub user namespace requires a user id or login");
5255
+ }
5256
+ function resolveRemoteUserNamespacesRoot(projectRoot) {
5257
+ const explicitRoot = cleanString3(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
5258
+ if (explicitRoot)
5259
+ return resolve16(explicitRoot);
5260
+ const stateDir2 = cleanString3(process.env.RIG_STATE_DIR);
5261
+ if (stateDir2)
5262
+ return resolve16(dirname11(resolve16(stateDir2)), "users");
5263
+ return resolve16(projectRoot, ".rig", "users");
5264
+ }
5265
+ function resolveRemoteUserNamespace(projectRoot, identity) {
5266
+ const key = deriveGitHubUserNamespaceKey(identity);
5267
+ const root = resolve16(resolveRemoteUserNamespacesRoot(projectRoot), key);
5268
+ const stateDir2 = resolve16(root, ".rig", "state");
5269
+ return {
5270
+ key,
5271
+ userId: cleanString3(identity.userId),
5272
+ login: cleanString3(identity.login),
5273
+ root,
5274
+ stateDir: stateDir2,
5275
+ authStateFile: resolve16(stateDir2, "github-auth.json"),
5276
+ metadataFile: resolve16(stateDir2, "user-namespace.json"),
5277
+ checkoutBaseDir: resolve16(root, "remote-checkouts"),
5278
+ snapshotBaseDir: resolve16(root, "remote-snapshots")
5279
+ };
5280
+ }
5281
+ function serializeRemoteUserNamespace(namespace) {
5282
+ return {
5283
+ key: namespace.key,
5284
+ userId: namespace.userId,
5285
+ login: namespace.login,
5286
+ root: namespace.root,
5287
+ checkoutBaseDir: namespace.checkoutBaseDir,
5288
+ snapshotBaseDir: namespace.snapshotBaseDir
5289
+ };
5290
+ }
5291
+ function isPathInsideNamespace(namespaceRoot, candidatePath) {
5292
+ const root = resolve16(namespaceRoot);
5293
+ const candidate = resolve16(candidatePath);
5294
+ const rel = relative3(root, candidate);
5295
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
5296
+ }
5297
+ function writeRemoteUserNamespaceMetadata(namespace) {
5298
+ mkdirSync9(namespace.stateDir, { recursive: true });
5299
+ const previous = (() => {
5300
+ if (!existsSync9(namespace.metadataFile))
5301
+ return null;
5302
+ try {
5303
+ const parsed = JSON.parse(readFileSync6(namespace.metadataFile, "utf8"));
5304
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
5305
+ } catch {
5306
+ return null;
5307
+ }
5308
+ })();
5309
+ const now = new Date().toISOString();
5310
+ writeFileSync8(namespace.metadataFile, `${JSON.stringify({
5311
+ key: namespace.key,
5312
+ userId: namespace.userId,
5313
+ login: namespace.login,
5314
+ root: namespace.root,
5315
+ checkoutBaseDir: namespace.checkoutBaseDir,
5316
+ snapshotBaseDir: namespace.snapshotBaseDir,
5317
+ createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
5318
+ updatedAt: now
5319
+ }, null, 2)}
5320
+ `, { encoding: "utf8", mode: 384 });
5321
+ try {
5322
+ chmodSync2(namespace.metadataFile, 384);
5323
+ } catch {}
5324
+ }
5325
+
5326
+ // packages/server/src/server-helpers/github-api-session-index.ts
5327
+ function cleanString4(value) {
5328
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
5329
+ }
5330
+ function resolveGitHubApiSessionIndexFile(projectRoot) {
5331
+ return resolve17(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
5332
+ }
5333
+ function parseEntry(value) {
5334
+ if (!value || typeof value !== "object" || Array.isArray(value))
5335
+ return null;
5336
+ const record = value;
5337
+ const token = cleanString4(record.token);
5338
+ const namespaceKey = cleanString4(record.namespaceKey);
5339
+ const namespaceRoot = cleanString4(record.namespaceRoot);
5340
+ const authStateFile = cleanString4(record.authStateFile);
5341
+ const checkoutBaseDir = cleanString4(record.checkoutBaseDir);
5342
+ const snapshotBaseDir = cleanString4(record.snapshotBaseDir);
5343
+ const createdAt = cleanString4(record.createdAt);
5344
+ if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
5345
+ return null;
5346
+ return {
5347
+ token,
5348
+ namespaceKey,
5349
+ namespaceRoot,
5350
+ authStateFile,
5351
+ checkoutBaseDir,
5352
+ snapshotBaseDir,
5353
+ createdAt,
5354
+ login: cleanString4(record.login),
5355
+ userId: cleanString4(record.userId),
5356
+ selectedRepo: cleanString4(record.selectedRepo)
5357
+ };
5358
+ }
5359
+ function readIndex(indexFile) {
5360
+ if (!existsSync10(indexFile))
5361
+ return [];
5362
+ try {
5363
+ const parsed = JSON.parse(readFileSync7(indexFile, "utf8"));
5364
+ return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
5365
+ const parsedEntry = parseEntry(entry);
5366
+ return parsedEntry ? [parsedEntry] : [];
5367
+ }) : [];
5368
+ } catch {
5369
+ return [];
5370
+ }
5371
+ }
5372
+ function writeIndex(indexFile, sessions) {
5373
+ mkdirSync10(dirname12(indexFile), { recursive: true });
5374
+ writeFileSync9(indexFile, `${JSON.stringify({ sessions }, null, 2)}
5375
+ `, { encoding: "utf8", mode: 384 });
5376
+ try {
5377
+ chmodSync3(indexFile, 384);
5378
+ } catch {}
5379
+ }
5380
+ function registerGitHubApiSession(input) {
5381
+ const cleanToken = cleanString4(input.token);
5382
+ if (!cleanToken)
5383
+ throw new Error("GitHub API session token is required");
5384
+ const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
5385
+ const createdAt = new Date().toISOString();
5386
+ const entry = {
5387
+ token: cleanToken,
5388
+ login: input.namespace.login,
5389
+ userId: input.namespace.userId,
5390
+ namespaceKey: input.namespace.key,
5391
+ namespaceRoot: input.namespace.root,
5392
+ authStateFile: input.namespace.authStateFile,
5393
+ checkoutBaseDir: input.namespace.checkoutBaseDir,
5394
+ snapshotBaseDir: input.namespace.snapshotBaseDir,
5395
+ selectedRepo: cleanString4(input.selectedRepo),
5396
+ createdAt
5397
+ };
5398
+ const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
5399
+ writeIndex(indexFile, [...previous.slice(-199), entry]);
5400
+ return entry;
5401
+ }
5402
+ function readGitHubApiSession(input) {
5403
+ const cleanToken = cleanString4(input.token);
5404
+ if (!cleanToken)
5405
+ return null;
5406
+ return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
5407
+ }
5408
+
4578
5409
  // packages/server/src/server-helpers/inspector-agent-lifecycle.ts
4579
5410
  function createInspectorAgentLifecycleController(options) {
4580
5411
  const initialDelayMs = Math.max(1, options.initialDelayMs ?? 5000);
@@ -4678,21 +5509,21 @@ function inspectorAgentLifecycleSnapshot(input) {
4678
5509
  // packages/server/src/server-helpers/project-registry.ts
4679
5510
  import { createHash as createHash2 } from "crypto";
4680
5511
  import { spawnSync as spawnSync2 } from "child_process";
4681
- import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync6, readdirSync as readdirSync3, writeFileSync as writeFileSync8 } from "fs";
4682
- import { dirname as dirname10, resolve as resolve16 } from "path";
5512
+ import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync8, readdirSync as readdirSync3, writeFileSync as writeFileSync10 } from "fs";
5513
+ import { dirname as dirname13, resolve as resolve18 } from "path";
4683
5514
  function normalizeRepoSlug(value) {
4684
5515
  const trimmed = value.trim();
4685
5516
  return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
4686
5517
  }
4687
5518
  function registryPath(projectRoot) {
4688
- return resolve16(projectRoot, ".rig", "state", "projects.json");
5519
+ return resolve18(projectRoot, ".rig", "state", "projects.json");
4689
5520
  }
4690
5521
  function readRegistry(projectRoot) {
4691
5522
  const path = registryPath(projectRoot);
4692
- if (!existsSync9(path))
5523
+ if (!existsSync11(path))
4693
5524
  return {};
4694
5525
  try {
4695
- const payload = JSON.parse(readFileSync6(path, "utf8"));
5526
+ const payload = JSON.parse(readFileSync8(path, "utf8"));
4696
5527
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
4697
5528
  return {};
4698
5529
  const projects = payload.projects;
@@ -4703,14 +5534,14 @@ function readRegistry(projectRoot) {
4703
5534
  }
4704
5535
  function writeRegistry(projectRoot, projects) {
4705
5536
  const path = registryPath(projectRoot);
4706
- mkdirSync9(dirname10(path), { recursive: true });
4707
- writeFileSync8(path, `${JSON.stringify({ projects }, null, 2)}
5537
+ mkdirSync11(dirname13(path), { recursive: true });
5538
+ writeFileSync10(path, `${JSON.stringify({ projects }, null, 2)}
4708
5539
  `, "utf8");
4709
5540
  }
4710
5541
  function resolveConfigPath(projectRoot) {
4711
5542
  for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
4712
- const path = resolve16(projectRoot, name);
4713
- if (existsSync9(path))
5543
+ const path = resolve18(projectRoot, name);
5544
+ if (existsSync11(path))
4714
5545
  return path;
4715
5546
  }
4716
5547
  return null;
@@ -4719,7 +5550,7 @@ function hashFile(path) {
4719
5550
  if (!path)
4720
5551
  return null;
4721
5552
  try {
4722
- return createHash2("sha256").update(readFileSync6(path)).digest("hex");
5553
+ return createHash2("sha256").update(readFileSync8(path)).digest("hex");
4723
5554
  } catch {
4724
5555
  return null;
4725
5556
  }
@@ -4735,11 +5566,11 @@ function readDefaultBranch(projectRoot) {
4735
5566
  return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
4736
5567
  }
4737
5568
  function buildRunSummary(projectRoot) {
4738
- const runsDir = resolve16(projectRoot, ".rig", "runs");
5569
+ const runsDir = resolve18(projectRoot, ".rig", "runs");
4739
5570
  try {
4740
5571
  const runs = readdirSync3(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
4741
5572
  try {
4742
- const run = JSON.parse(readFileSync6(resolve16(runsDir, entry.name, "run.json"), "utf8"));
5573
+ const run = JSON.parse(readFileSync8(resolve18(runsDir, entry.name, "run.json"), "utf8"));
4743
5574
  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 : "" }];
4744
5575
  } catch {
4745
5576
  return [];
@@ -4791,10 +5622,14 @@ function upsertProjectRecord(projectRoot, input) {
4791
5622
  function linkProjectCheckout(projectRoot, repoSlug, checkout) {
4792
5623
  return upsertProjectRecord(projectRoot, { repoSlug, checkout });
4793
5624
  }
5625
+ function projectRegistryContainsCheckout(projectRoot, checkoutPath) {
5626
+ const target = resolve18(checkoutPath);
5627
+ return Object.values(readRegistry(projectRoot)).some((project) => project.checkouts.some((checkout) => checkout.path ? resolve18(checkout.path) === target : false));
5628
+ }
4794
5629
 
4795
5630
  // packages/server/src/server-helpers/remote-checkout.ts
4796
- import { existsSync as existsSync10, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
4797
- import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve17 } from "path";
5631
+ import { existsSync as existsSync12, mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
5632
+ import { dirname as dirname14, isAbsolute as isAbsolute3, relative as relative4, resolve as resolve19 } from "path";
4798
5633
  function safeSlugSegments(repoSlug) {
4799
5634
  const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
4800
5635
  if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
@@ -4811,7 +5646,7 @@ function safeCheckoutKey(value) {
4811
5646
  }
4812
5647
  function repoSlugPath(baseDir, repoSlug, checkoutKey) {
4813
5648
  const key = safeCheckoutKey(checkoutKey);
4814
- return resolve17(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
5649
+ return resolve19(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
4815
5650
  }
4816
5651
  function sanitizeSnapshotId(value, fallback) {
4817
5652
  const raw = (value ?? fallback).trim();
@@ -4819,7 +5654,7 @@ function sanitizeSnapshotId(value, fallback) {
4819
5654
  return safe || fallback;
4820
5655
  }
4821
5656
  function assertWithinRoot(root, relativePath) {
4822
- if (!relativePath || isAbsolute2(relativePath) || relativePath.includes("\x00")) {
5657
+ if (!relativePath || isAbsolute3(relativePath) || relativePath.includes("\x00")) {
4823
5658
  throw new Error(`Invalid snapshot file path: ${relativePath}`);
4824
5659
  }
4825
5660
  const normalizedRelative = relativePath.replace(/\\/g, "/");
@@ -4827,9 +5662,9 @@ function assertWithinRoot(root, relativePath) {
4827
5662
  if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
4828
5663
  throw new Error(`Unsafe snapshot file path: ${relativePath}`);
4829
5664
  }
4830
- const target = resolve17(root, ...segments);
4831
- const rel = relative3(root, target);
4832
- if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute2(rel)) {
5665
+ const target = resolve19(root, ...segments);
5666
+ const rel = relative4(root, target);
5667
+ if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute3(rel)) {
4833
5668
  throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
4834
5669
  }
4835
5670
  return target;
@@ -4847,17 +5682,17 @@ function parseSnapshotArchiveContentBase64(contentBase64) {
4847
5682
  function extractUploadedSnapshotArchive(input) {
4848
5683
  const archive = decodeSnapshotArchive(input.archive);
4849
5684
  const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
4850
- const checkoutPath = resolve17(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
4851
- mkdirSync10(checkoutPath, { recursive: true });
5685
+ const checkoutPath = resolve19(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
5686
+ mkdirSync12(checkoutPath, { recursive: true });
4852
5687
  for (const file of archive.files) {
4853
5688
  if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
4854
5689
  throw new Error("Invalid snapshot archive file entry");
4855
5690
  }
4856
5691
  const target = assertWithinRoot(checkoutPath, file.path);
4857
- mkdirSync10(dirname11(target), { recursive: true });
4858
- writeFileSync9(target, Buffer.from(file.contentBase64, "base64"));
5692
+ mkdirSync12(dirname14(target), { recursive: true });
5693
+ writeFileSync11(target, Buffer.from(file.contentBase64, "base64"));
4859
5694
  }
4860
- writeFileSync9(resolve17(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
5695
+ writeFileSync11(resolve19(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
4861
5696
  repoSlug: input.repoSlug,
4862
5697
  snapshotId,
4863
5698
  fileCount: archive.files.length,
@@ -4892,7 +5727,7 @@ function gitCredentialConfig(token) {
4892
5727
  };
4893
5728
  }
4894
5729
  async function prepareRemoteCheckout(input) {
4895
- const exists = input.exists ?? existsSync10;
5730
+ const exists = input.exists ?? existsSync12;
4896
5731
  const strategy = input.strategy;
4897
5732
  if (strategy.kind === "uploaded-snapshot") {
4898
5733
  return extractUploadedSnapshotArchive({
@@ -4904,7 +5739,7 @@ async function prepareRemoteCheckout(input) {
4904
5739
  });
4905
5740
  }
4906
5741
  if (strategy.kind === "existing-path") {
4907
- const checkoutPath2 = resolve17(strategy.path);
5742
+ const checkoutPath2 = resolve19(strategy.path);
4908
5743
  if (!exists(checkoutPath2)) {
4909
5744
  throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
4910
5745
  }
@@ -4940,9 +5775,9 @@ function buildServerControlStatus() {
4940
5775
  };
4941
5776
  }
4942
5777
  function buildProjectConfigStatus(root) {
4943
- const hasConfigTs = existsSync11(resolve18(root, "rig.config.ts"));
4944
- const hasConfigJson = existsSync11(resolve18(root, "rig.config.json"));
4945
- const hasLegacyTaskConfig = existsSync11(resolve18(root, ".rig", "task-config.json"));
5778
+ const hasConfigTs = existsSync13(resolve20(root, "rig.config.ts"));
5779
+ const hasConfigJson = existsSync13(resolve20(root, "rig.config.json"));
5780
+ const hasLegacyTaskConfig = existsSync13(resolve20(root, ".rig", "task-config.json"));
4946
5781
  let kind = "missing";
4947
5782
  if (hasConfigTs)
4948
5783
  kind = "rig-config-ts";
@@ -4959,6 +5794,75 @@ function buildProjectConfigStatus(root) {
4959
5794
  suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
4960
5795
  };
4961
5796
  }
5797
+ var RIG_GITHUB_LIFECYCLE_LABELS = [
5798
+ "ready",
5799
+ "blocked",
5800
+ "in-progress",
5801
+ "under-review",
5802
+ "failed",
5803
+ "cancelled",
5804
+ "rig:running",
5805
+ "rig:pr-open",
5806
+ "rig:ci-fixing",
5807
+ "rig:merging",
5808
+ "rig:done",
5809
+ "rig:needs-attention"
5810
+ ];
5811
+ function githubProjectsEnabled2(config) {
5812
+ if (!config || typeof config !== "object" || Array.isArray(config))
5813
+ return false;
5814
+ const root = config;
5815
+ const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
5816
+ const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
5817
+ return projects?.enabled === true;
5818
+ }
5819
+ function githubIssueSourceRepo(config) {
5820
+ if (!config || typeof config !== "object" || Array.isArray(config))
5821
+ return null;
5822
+ const root = config;
5823
+ const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
5824
+ const owner = normalizeString(taskSource?.owner);
5825
+ const repo = normalizeString(taskSource?.repo);
5826
+ if (taskSource?.kind === "github-issues" && owner && repo)
5827
+ return { owner, repo };
5828
+ const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
5829
+ const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
5830
+ const match = slug?.match(/^([^/]+)\/([^/]+)$/);
5831
+ return match ? { owner: match[1], repo: match[2] } : null;
5832
+ }
5833
+ async function ensureGitHubLifecycleLabels(projectRoot, config) {
5834
+ const repo = githubIssueSourceRepo(config);
5835
+ if (!repo)
5836
+ return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
5837
+ const token = createGitHubAuthStore(projectRoot).readToken();
5838
+ if (!token)
5839
+ return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
5840
+ const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
5841
+ headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
5842
+ });
5843
+ const existingJson = await existingResponse.json().catch(() => []);
5844
+ const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
5845
+ const created = [];
5846
+ const alreadyPresent = [];
5847
+ const failed = [];
5848
+ for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
5849
+ if (existing.has(label)) {
5850
+ alreadyPresent.push(label);
5851
+ continue;
5852
+ }
5853
+ const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
5854
+ method: "POST",
5855
+ headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
5856
+ 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" })
5857
+ });
5858
+ if (response.ok || response.status === 422) {
5859
+ (response.status === 422 ? alreadyPresent : created).push(label);
5860
+ } else {
5861
+ failed.push({ label, error: await response.text().catch(() => response.statusText) });
5862
+ }
5863
+ }
5864
+ return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
5865
+ }
4962
5866
  function normalizeCommit(value) {
4963
5867
  const raw = normalizeString(value);
4964
5868
  return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
@@ -4978,24 +5882,24 @@ function repoParts(repoSlug) {
4978
5882
  return { owner, repo, slug: `${owner}/${repo}` };
4979
5883
  }
4980
5884
  function repairDir(checkoutPath) {
4981
- const dir = resolve18(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
4982
- mkdirSync11(dir, { recursive: true });
5885
+ const dir = resolve20(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
5886
+ mkdirSync13(dir, { recursive: true });
4983
5887
  return dir;
4984
5888
  }
4985
5889
  function backupCheckoutFile(checkoutPath, relativePath) {
4986
- const source = resolve18(checkoutPath, relativePath);
4987
- const backupPath = resolve18(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
4988
- mkdirSync11(dirname12(backupPath), { recursive: true });
4989
- copyFileSync(source, backupPath);
5890
+ const source = resolve20(checkoutPath, relativePath);
5891
+ const backupPath = resolve20(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
5892
+ mkdirSync13(dirname15(backupPath), { recursive: true });
5893
+ copyFileSync2(source, backupPath);
4990
5894
  return backupPath;
4991
5895
  }
4992
5896
  function parsePackageJsonLosslessly(checkoutPath) {
4993
- const packagePath = resolve18(checkoutPath, "package.json");
4994
- if (!existsSync11(packagePath)) {
5897
+ const packagePath = resolve20(checkoutPath, "package.json");
5898
+ if (!existsSync13(packagePath)) {
4995
5899
  return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
4996
5900
  }
4997
5901
  try {
4998
- const parsed = JSON.parse(readFileSync7(packagePath, "utf8"));
5902
+ const parsed = JSON.parse(readFileSync9(packagePath, "utf8"));
4999
5903
  return {
5000
5904
  existed: true,
5001
5905
  packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
@@ -5009,9 +5913,9 @@ function parsePackageJsonLosslessly(checkoutPath) {
5009
5913
  }
5010
5914
  }
5011
5915
  function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
5012
- const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync11(resolve18(checkoutPath, name)));
5013
- const packagePath = resolve18(checkoutPath, "package.json");
5014
- if (!hasConfig && !existsSync11(packagePath)) {
5916
+ const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
5917
+ const packagePath = resolve20(checkoutPath, "package.json");
5918
+ if (!hasConfig && !existsSync13(packagePath)) {
5015
5919
  return { skipped: true, reason: "package.json and rig.config missing" };
5016
5920
  }
5017
5921
  const parsed = parsePackageJsonLosslessly(checkoutPath);
@@ -5030,7 +5934,7 @@ function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
5030
5934
  }
5031
5935
  const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
5032
5936
  if (changed) {
5033
- writeFileSync10(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
5937
+ writeFileSync12(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
5034
5938
  `, "utf8");
5035
5939
  }
5036
5940
  return {
@@ -5056,11 +5960,11 @@ function configLooksStructurallyUsable(source) {
5056
5960
  return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
5057
5961
  }
5058
5962
  function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
5059
- const configPath = resolve18(checkoutPath, "rig.config.ts");
5060
- const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync11(resolve18(checkoutPath, name)));
5963
+ const configPath = resolve20(checkoutPath, "rig.config.ts");
5964
+ const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
5061
5965
  if (existingConfigName) {
5062
- const existingPath = resolve18(checkoutPath, existingConfigName);
5063
- const source = readFileSync7(existingPath, "utf8");
5966
+ const existingPath = resolve20(checkoutPath, existingConfigName);
5967
+ const source = readFileSync9(existingPath, "utf8");
5064
5968
  if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
5065
5969
  return { path: existingPath, changed: false, reason: "config structurally complete" };
5066
5970
  }
@@ -5074,7 +5978,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
5074
5978
  }
5075
5979
  }
5076
5980
  const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
5077
- writeFileSync10(configPath, generatedRigConfigSource(repoSlug), "utf8");
5981
+ writeFileSync12(configPath, generatedRigConfigSource(repoSlug), "utf8");
5078
5982
  return {
5079
5983
  path: configPath,
5080
5984
  changed: true,
@@ -5083,7 +5987,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
5083
5987
  };
5084
5988
  }
5085
5989
  function validateRemoteCheckoutRigConfig(checkoutPath) {
5086
- const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync11(resolve18(checkoutPath, name)));
5990
+ const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
5087
5991
  if (!configFile)
5088
5992
  return { ok: false, error: "missing rig config" };
5089
5993
  if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
@@ -5105,7 +6009,7 @@ function validateRemoteCheckoutRigConfig(checkoutPath) {
5105
6009
  return { ok: true, configFile };
5106
6010
  }
5107
6011
  function installRemoteCheckoutPackages(checkoutPath) {
5108
- if (!existsSync11(resolve18(checkoutPath, "package.json"))) {
6012
+ if (!existsSync13(resolve20(checkoutPath, "package.json"))) {
5109
6013
  return { skipped: true, reason: "package.json missing" };
5110
6014
  }
5111
6015
  if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
@@ -5118,8 +6022,8 @@ function installRemoteCheckoutPackages(checkoutPath) {
5118
6022
  return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
5119
6023
  }
5120
6024
  function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
5121
- const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync11(resolve18(checkoutPath, name)));
5122
- const hasPackage = existsSync11(resolve18(checkoutPath, "package.json"));
6025
+ const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
6026
+ const hasPackage = existsSync13(resolve20(checkoutPath, "package.json"));
5123
6027
  if (!hasConfig && !hasPackage) {
5124
6028
  return {
5125
6029
  packageJson: { skipped: true, reason: "package.json and rig.config missing" },
@@ -5217,26 +6121,26 @@ function buildRemoteRunLogEntry(body, identifiers) {
5217
6121
  }
5218
6122
  function readGitHeadCommit(projectRoot) {
5219
6123
  try {
5220
- let gitDir = resolve18(projectRoot, ".git");
6124
+ let gitDir = resolve20(projectRoot, ".git");
5221
6125
  try {
5222
- const dotGit = readFileSync7(gitDir, "utf8").trim();
6126
+ const dotGit = readFileSync9(gitDir, "utf8").trim();
5223
6127
  const gitDirPrefix = "gitdir:";
5224
6128
  if (dotGit.startsWith(gitDirPrefix)) {
5225
- gitDir = resolve18(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
6129
+ gitDir = resolve20(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
5226
6130
  }
5227
6131
  } catch {}
5228
- const head = readFileSync7(resolve18(gitDir, "HEAD"), "utf8").trim();
6132
+ const head = readFileSync9(resolve20(gitDir, "HEAD"), "utf8").trim();
5229
6133
  const refPrefix = "ref:";
5230
6134
  if (!head.startsWith(refPrefix)) {
5231
6135
  return normalizeCommit(head);
5232
6136
  }
5233
6137
  const ref = head.slice(refPrefix.length).trim();
5234
- const refPath = resolve18(gitDir, ref);
5235
- if (existsSync11(refPath)) {
5236
- return normalizeCommit(readFileSync7(refPath, "utf8").trim());
6138
+ const refPath = resolve20(gitDir, ref);
6139
+ if (existsSync13(refPath)) {
6140
+ return normalizeCommit(readFileSync9(refPath, "utf8").trim());
5237
6141
  }
5238
- const commonDir = normalizeString(readFileSync7(resolve18(gitDir, "commondir"), "utf8"));
5239
- return commonDir ? normalizeCommit(readFileSync7(resolve18(gitDir, commonDir, ref), "utf8").trim()) : null;
6142
+ const commonDir = normalizeString(readFileSync9(resolve20(gitDir, "commondir"), "utf8"));
6143
+ return commonDir ? normalizeCommit(readFileSync9(resolve20(gitDir, commonDir, ref), "utf8").trim()) : null;
5240
6144
  } catch {
5241
6145
  return null;
5242
6146
  }
@@ -5256,7 +6160,8 @@ function isAuthorizedInspectorStreamRequest(req, authToken) {
5256
6160
  }
5257
6161
  function buildDeploymentStatus(projectRoot) {
5258
6162
  const envCommit = normalizeCommit(process.env.RIG_COMMIT_SHA ?? process.env.GITHUB_SHA ?? process.env.VERCEL_GIT_COMMIT_SHA ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.COMMIT_SHA);
5259
- const gitCommit = envCommit ?? readGitHeadCommit(projectRoot);
6163
+ const deploymentRoot = normalizeString(process.env.RIG_HOST_PROJECT_ROOT) ?? projectRoot;
6164
+ const gitCommit = envCommit ?? readGitHeadCommit(deploymentRoot) ?? readGitHeadCommit(projectRoot);
5260
6165
  return {
5261
6166
  currentCommit: gitCommit,
5262
6167
  commitSource: envCommit ? "env" : gitCommit ? "git" : null,
@@ -5274,9 +6179,9 @@ function configuredRepoFromTaskSource(taskSource) {
5274
6179
  const repo = normalizeString(taskSource?.repo);
5275
6180
  return owner && repo ? `${owner}/${repo}` : null;
5276
6181
  }
5277
- async function buildTaskSourceStatus(state, config) {
6182
+ async function buildTaskSourceStatus(state, config, requestAuth) {
5278
6183
  const diagnostics = state.snapshotService.getTaskSourceErrors();
5279
- const selectedRepo = createGitHubAuthStore(state.projectRoot).status({
6184
+ const selectedRepo = requestScopedAuthStore(state.projectRoot, requestAuth).status({
5280
6185
  oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
5281
6186
  }).selectedRepo;
5282
6187
  try {
@@ -5329,37 +6234,146 @@ function isLoopbackRequest(req) {
5329
6234
  }
5330
6235
  }
5331
6236
  function isPublicRigAuthBootstrapRoute(pathname) {
5332
- 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";
6237
+ 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";
6238
+ }
6239
+ function buildRigInstallScript() {
6240
+ return `#!/usr/bin/env bash
6241
+ set -euo pipefail
6242
+
6243
+ say() {
6244
+ printf 'rig-install: %s
6245
+ ' "$*"
6246
+ }
6247
+
6248
+ if ! command -v bun >/dev/null 2>&1; then
6249
+ say "Bun not found; installing Bun first"
6250
+ curl -fsSL https://bun.sh/install | bash
6251
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
6252
+ export PATH="$BUN_INSTALL/bin:$PATH"
6253
+ fi
6254
+
6255
+ if ! command -v bun >/dev/null 2>&1; then
6256
+ printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
6257
+ ' >&2
6258
+ exit 1
6259
+ fi
6260
+
6261
+ say "Installing @h-rig/cli@latest"
6262
+ bun add -g --force @h-rig/cli@latest
6263
+
6264
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
6265
+ BUN_RIG="$BUN_INSTALL/bin/rig"
6266
+ if [ ! -x "$BUN_RIG" ]; then
6267
+ printf 'rig-install: expected Bun global rig at %s but it was not executable.
6268
+ ' "$BUN_RIG" >&2
6269
+ exit 1
6270
+ fi
6271
+
6272
+ USER_BIN="$HOME/.local/bin"
6273
+ mkdir -p "$USER_BIN"
6274
+ cat > "$USER_BIN/rig" <<'RIG_SHIM'
6275
+ #!/usr/bin/env bash
6276
+ set -euo pipefail
6277
+ exec "\${BUN_INSTALL:-$HOME/.bun}/bin/rig" "$@"
6278
+ RIG_SHIM
6279
+ chmod +x "$USER_BIN/rig"
6280
+
6281
+ export PATH="$USER_BIN:$BUN_INSTALL/bin:$PATH"
6282
+ if command -v hash >/dev/null 2>&1; then hash -r; fi
6283
+
6284
+ if ! command -v rig >/dev/null 2>&1; then
6285
+ printf 'rig-install: rig installed, but rig is not on PATH. Add %s and %s/bin to PATH and retry.
6286
+ ' "$USER_BIN" "$BUN_INSTALL" >&2
6287
+ exit 1
6288
+ fi
6289
+
6290
+ say "Verifying rig"
6291
+ "$BUN_RIG" --help >/dev/null
6292
+ rig --help >/dev/null
6293
+ say "Done. Run: rig --help"
6294
+ `;
5333
6295
  }
5334
6296
  function normalizePrMode(value) {
5335
6297
  const mode = normalizeString(value);
5336
6298
  return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
5337
6299
  }
6300
+ function requestAuthResult(input) {
6301
+ return {
6302
+ authorized: input.authorized,
6303
+ actor: input.actor ?? null,
6304
+ reason: input.reason,
6305
+ login: input.login ?? null,
6306
+ userId: input.userId ?? null,
6307
+ userNamespace: input.userNamespace ?? null,
6308
+ authStateFile: input.authStateFile ?? null
6309
+ };
6310
+ }
6311
+ function namespaceFromSessionIndex(entry) {
6312
+ const stateDir2 = dirname15(entry.authStateFile);
6313
+ return {
6314
+ key: entry.namespaceKey,
6315
+ userId: entry.userId,
6316
+ login: entry.login,
6317
+ root: entry.namespaceRoot,
6318
+ stateDir: stateDir2,
6319
+ authStateFile: entry.authStateFile,
6320
+ metadataFile: resolve20(stateDir2, "user-namespace.json"),
6321
+ checkoutBaseDir: entry.checkoutBaseDir,
6322
+ snapshotBaseDir: entry.snapshotBaseDir
6323
+ };
6324
+ }
5338
6325
  function authorizeRigHttpRequest(input) {
5339
6326
  if (input.legacyAuthorized) {
5340
- return { authorized: true, actor: "rig-local-server", reason: "server-token" };
6327
+ return requestAuthResult({ authorized: true, actor: "rig-local-server", reason: "server-token" });
5341
6328
  }
5342
6329
  const bearer = bearerTokenFromRequest(input.req);
5343
6330
  const store = createGitHubAuthStore(input.projectRoot);
5344
6331
  const storedToken = store.readToken();
5345
6332
  const session = bearer ? store.readApiSession(bearer) : null;
5346
6333
  if (session) {
5347
- return { authorized: true, actor: session.login ?? "github-operator", reason: "github-session" };
6334
+ return requestAuthResult({
6335
+ authorized: true,
6336
+ actor: session.login ?? "github-operator",
6337
+ reason: "github-session",
6338
+ login: session.login,
6339
+ userId: session.userId,
6340
+ authStateFile: store.stateFile
6341
+ });
5348
6342
  }
5349
6343
  if (bearer && storedToken && bearer === storedToken) {
5350
6344
  const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
5351
- return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
6345
+ return requestAuthResult({
6346
+ authorized: true,
6347
+ actor: status.login ?? "github-operator",
6348
+ reason: "github-token",
6349
+ login: status.login,
6350
+ userId: status.userId,
6351
+ authStateFile: store.stateFile
6352
+ });
6353
+ }
6354
+ const indexedSession = readGitHubApiSession({ projectRoot: input.projectRoot, token: bearer });
6355
+ if (indexedSession) {
6356
+ const userNamespace = namespaceFromSessionIndex(indexedSession);
6357
+ return requestAuthResult({
6358
+ authorized: true,
6359
+ actor: indexedSession.login ?? "github-operator",
6360
+ reason: "github-user-session",
6361
+ login: indexedSession.login,
6362
+ userId: indexedSession.userId,
6363
+ userNamespace,
6364
+ authStateFile: indexedSession.authStateFile
6365
+ });
5352
6366
  }
5353
6367
  if (isPublicRigAuthBootstrapRoute(input.pathname)) {
5354
- return { authorized: true, actor: null, reason: "public-bootstrap" };
6368
+ return requestAuthResult({ authorized: true, actor: null, reason: "public-bootstrap" });
5355
6369
  }
5356
6370
  if (!input.serverAuthToken && !storedToken) {
5357
6371
  if (isLoopbackRequest(input.req)) {
5358
- return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
6372
+ return requestAuthResult({ authorized: true, actor: null, reason: "loopback-dev-no-auth" });
5359
6373
  }
5360
- return { authorized: false, actor: null, reason: "auth-required" };
6374
+ return requestAuthResult({ authorized: false, actor: null, reason: "auth-required" });
5361
6375
  }
5362
- return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
6376
+ return requestAuthResult({ authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" });
5363
6377
  }
5364
6378
  async function fetchGitHubUserInfo(token) {
5365
6379
  const response = await fetch("https://api.github.com/user", {
@@ -5379,6 +6393,67 @@ async function fetchGitHubUserInfo(token) {
5379
6393
  scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
5380
6394
  };
5381
6395
  }
6396
+ function shouldWriteRootAuthCompat(projectRoot) {
6397
+ if (process.env.RIG_REMOTE_USER_NAMESPACE_ROOT?.trim())
6398
+ return false;
6399
+ const stateDir2 = normalizeString(process.env.RIG_STATE_DIR);
6400
+ if (!stateDir2)
6401
+ return true;
6402
+ return resolve20(stateDir2) === resolve20(projectRoot, ".rig", "state");
6403
+ }
6404
+ function requestScopedRegistryRoot(stateProjectRoot, requestAuth) {
6405
+ return requestAuth.userNamespace?.root ?? stateProjectRoot;
6406
+ }
6407
+ function requestScopedAuthStore(stateProjectRoot, requestAuth) {
6408
+ return requestAuth.authStateFile ? createGitHubAuthStoreFromStateFile(requestAuth.authStateFile) : createGitHubAuthStore(stateProjectRoot);
6409
+ }
6410
+ function userNamespaceResponse(namespace) {
6411
+ return namespace ? serializeRemoteUserNamespace(namespace) : undefined;
6412
+ }
6413
+ function resolveNamespacedBaseDir(input) {
6414
+ if (input.explicitBaseDir)
6415
+ return input.explicitBaseDir;
6416
+ const envBase = normalizeString(process.env[input.envName]);
6417
+ if (input.userNamespace) {
6418
+ return envBase ? resolve20(envBase, input.userNamespace.key) : input.userNamespace[input.legacySubdir === "remote-checkouts" ? "checkoutBaseDir" : "snapshotBaseDir"];
6419
+ }
6420
+ return envBase ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve20(normalizeString(process.env.RIG_STATE_DIR), input.legacySubdir) : resolve20(input.legacyProjectRoot, ".rig", input.legacySubdir));
6421
+ }
6422
+ function explicitCheckoutKey(body, checkoutInput, requestAuth) {
6423
+ return normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? (requestAuth.userNamespace ? undefined : "default");
6424
+ }
6425
+ function saveGitHubTokenForRemoteUser(input) {
6426
+ const namespace = resolveRemoteUserNamespace(input.projectRoot, { userId: input.user.userId, login: input.user.login });
6427
+ writeRemoteUserNamespaceMetadata(namespace);
6428
+ const store = createGitHubAuthStoreFromStateFile(namespace.authStateFile);
6429
+ store.saveToken({
6430
+ token: input.token,
6431
+ tokenSource: input.tokenSource,
6432
+ login: input.user.login,
6433
+ userId: input.user.userId,
6434
+ scopes: input.user.scopes,
6435
+ selectedRepo: input.selectedRepo
6436
+ });
6437
+ const apiSession = store.createApiSession();
6438
+ registerGitHubApiSession({ projectRoot: input.projectRoot, token: apiSession.token, namespace, selectedRepo: input.selectedRepo });
6439
+ const requestedRoot = normalizeString(input.requestedProjectRoot);
6440
+ if (requestedRoot && isAbsolute4(requestedRoot) && existsSync13(resolve20(requestedRoot))) {
6441
+ copyGitHubAuthStateToLocalProjectRoot(namespace.authStateFile, resolve20(requestedRoot));
6442
+ }
6443
+ if (shouldWriteRootAuthCompat(input.projectRoot)) {
6444
+ const rootStore = createGitHubAuthStore(input.projectRoot);
6445
+ rootStore.saveToken({
6446
+ token: input.token,
6447
+ tokenSource: input.tokenSource,
6448
+ login: input.user.login,
6449
+ userId: input.user.userId,
6450
+ scopes: input.user.scopes,
6451
+ selectedRepo: input.selectedRepo
6452
+ });
6453
+ rootStore.createApiSession();
6454
+ }
6455
+ return { store, namespace, apiSessionToken: apiSession.token };
6456
+ }
5382
6457
  async function postGitHubForm(endpoint, body) {
5383
6458
  const response = await fetch(endpoint, {
5384
6459
  method: "POST",
@@ -5396,11 +6471,11 @@ function resolveRequestedProjectRoot(currentRoot, rawRoot) {
5396
6471
  const requestedRoot = normalizeString(rawRoot);
5397
6472
  if (!requestedRoot)
5398
6473
  return currentRoot;
5399
- if (!isAbsolute3(requestedRoot)) {
6474
+ if (!isAbsolute4(requestedRoot)) {
5400
6475
  throw new Error("projectRoot must be an absolute path on the Rig server host");
5401
6476
  }
5402
- const normalizedRoot = resolve18(requestedRoot);
5403
- if (!existsSync11(normalizedRoot)) {
6477
+ const normalizedRoot = resolve20(requestedRoot);
6478
+ if (!existsSync13(normalizedRoot)) {
5404
6479
  throw new Error("projectRoot does not exist on the Rig server host");
5405
6480
  }
5406
6481
  return normalizedRoot;
@@ -5676,7 +6751,7 @@ function redactSecretFields(value) {
5676
6751
  return redacted;
5677
6752
  }
5678
6753
  function validateRemoteLease(deps, state, input) {
5679
- const run = readAuthorityRun6(state.projectRoot, input.runId);
6754
+ const run = readAuthorityRun7(state.projectRoot, input.runId);
5680
6755
  if (!run) {
5681
6756
  return { ok: false, response: deps.jsonResponse({ ok: false, error: "Remote run not found" }, 404) };
5682
6757
  }
@@ -5696,6 +6771,43 @@ function createRigServerFetch(state, deps) {
5696
6771
  return deps.withServerPathEnv(state.projectRoot, async () => {
5697
6772
  const browserOrigin = deps.resolveAllowedBrowserOrigin(req);
5698
6773
  const finalizeResponse = (response) => deps.withCorsHeaders(response, req, browserOrigin);
6774
+ const earlyUrl = new URL(req.url);
6775
+ const piEventsWsMatch = earlyUrl.pathname.match(/^\/api\/runs\/([^/]+)\/pi\/events$/);
6776
+ if (piEventsWsMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
6777
+ const queryToken = earlyUrl.searchParams.get("token");
6778
+ const authHeaders = new Headers(req.headers);
6779
+ if (queryToken && !authHeaders.has("authorization")) {
6780
+ authHeaders.set("authorization", `Bearer ${queryToken}`);
6781
+ }
6782
+ const legacyAuthorized = Boolean(state.authToken && queryToken === state.authToken);
6783
+ const requestAuth = authorizeRigHttpRequest({
6784
+ req: new Request(req.url, { method: req.method, headers: authHeaders }),
6785
+ pathname: earlyUrl.pathname,
6786
+ projectRoot: state.projectRoot,
6787
+ serverAuthToken: state.authToken,
6788
+ legacyAuthorized
6789
+ });
6790
+ if (!requestAuth.authorized) {
6791
+ return deps.jsonResponse({ ok: false, error: "Unauthorized WebSocket connection", reason: requestAuth.reason }, 401);
6792
+ }
6793
+ const runId = decodeURIComponent(piEventsWsMatch[1]);
6794
+ const resolved = resolveRunPiSessionProxy(state.projectRoot, runId);
6795
+ if (resolved === null)
6796
+ return deps.jsonResponse({ ok: false, error: "Run not found" }, 404);
6797
+ if ("pending" in resolved)
6798
+ return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500 }, 202);
6799
+ if (!server)
6800
+ return deps.jsonResponse({ ok: false, error: "WebSocket upgrade unavailable" }, 400);
6801
+ const upgraded = server.upgrade(req, {
6802
+ data: {
6803
+ kind: "pi-session-proxy",
6804
+ connectedAt: new Date().toISOString(),
6805
+ upstreamUrl: buildRunPiDaemonWebSocketUrl(resolved),
6806
+ runId
6807
+ }
6808
+ });
6809
+ return upgraded ? new Response(null) : deps.jsonResponse({ ok: false, error: "WebSocket upgrade failed" }, 400);
6810
+ }
5699
6811
  const upgradeResponse = handleWebSocketUpgrade({
5700
6812
  req,
5701
6813
  server,
@@ -5733,6 +6845,13 @@ function createRigServerFetch(state, deps) {
5733
6845
  notifications: state.targets.length
5734
6846
  });
5735
6847
  }
6848
+ if (url.pathname === "/install" && req.method === "GET") {
6849
+ return new Response(buildRigInstallScript(), {
6850
+ headers: {
6851
+ "Content-Type": "text/x-shellscript; charset=utf-8"
6852
+ }
6853
+ });
6854
+ }
5736
6855
  const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
5737
6856
  const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
5738
6857
  const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
@@ -5945,16 +7064,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
5945
7064
  if (!source) {
5946
7065
  return deps.badRequest("No task source is configured");
5947
7066
  }
7067
+ if (!source.updateTask && !(update.status && source.updateStatus)) {
7068
+ return deps.badRequest("Configured task source does not support updates");
7069
+ }
5948
7070
  const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
5949
7071
  return;
5950
7072
  }) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
5951
- if (source.updateTask) {
5952
- await source.updateTask(id, update);
5953
- } else if (update.status && source.updateStatus) {
5954
- await source.updateStatus(id, update.status);
5955
- } else {
5956
- return deps.badRequest("Configured task source does not support updates");
5957
- }
5958
7073
  const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
5959
7074
  const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
5960
7075
  taskId: id,
@@ -5963,6 +7078,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
5963
7078
  token: createGitHubAuthStore(state.projectRoot).readToken(),
5964
7079
  config: ctx?.config
5965
7080
  }).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
7081
+ if (update.status && githubProjectsEnabled2(ctx?.config) && projectSync.synced === false) {
7082
+ return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
7083
+ }
7084
+ try {
7085
+ if (source.updateTask) {
7086
+ await source.updateTask(id, update);
7087
+ } else if (update.status && source.updateStatus) {
7088
+ await source.updateStatus(id, update.status);
7089
+ }
7090
+ } catch (error) {
7091
+ let rollback = null;
7092
+ const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
7093
+ if (update.status && previousStatus && githubProjectsEnabled2(ctx?.config) && projectSync.synced !== false) {
7094
+ rollback = await syncGitHubProjectStatusForTaskUpdate({
7095
+ taskId: id,
7096
+ status: previousStatus,
7097
+ issueNodeId,
7098
+ token: createGitHubAuthStore(state.projectRoot).readToken(),
7099
+ config: ctx?.config
7100
+ }).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
7101
+ }
7102
+ return deps.jsonResponse({
7103
+ ok: false,
7104
+ id,
7105
+ projectSync,
7106
+ rollback,
7107
+ error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
7108
+ }, 502);
7109
+ }
5966
7110
  deps.snapshotService.invalidate("github-issue-updated");
5967
7111
  await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
5968
7112
  return;
@@ -5971,25 +7115,40 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
5971
7115
  return deps.jsonResponse({ ok: true, id, projectSync });
5972
7116
  }
5973
7117
  if (url.pathname === "/api/workspace/task-labels") {
7118
+ const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
7119
+ if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
7120
+ return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
7121
+ }
5974
7122
  return deps.jsonResponse({
5975
7123
  ok: true,
5976
7124
  ready: true,
5977
7125
  labelsReady: true,
5978
- labels: [
5979
- "ready",
5980
- "blocked",
5981
- "in-progress",
5982
- "under-review",
5983
- "failed",
5984
- "cancelled",
5985
- "rig:running",
5986
- "rig:pr-open",
5987
- "rig:ci-fixing",
5988
- "rig:done",
5989
- "rig:needs-attention"
5990
- ],
5991
- note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
7126
+ labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
7127
+ note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
7128
+ });
7129
+ }
7130
+ if (url.pathname === "/api/github/projects" && req.method === "GET") {
7131
+ const owner = normalizeString(url.searchParams.get("owner"));
7132
+ if (!owner)
7133
+ return deps.badRequest("owner is required");
7134
+ const token = createGitHubAuthStore(state.projectRoot).readToken();
7135
+ if (!token)
7136
+ return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
7137
+ const projects = await listGitHubProjects({ owner, token }).catch((error) => {
7138
+ throw new Error(error instanceof Error ? error.message : String(error));
7139
+ });
7140
+ return deps.jsonResponse({ ok: true, projects });
7141
+ }
7142
+ const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
7143
+ if (projectStatusMatch && req.method === "GET") {
7144
+ const projectId = decodeURIComponent(projectStatusMatch[1]);
7145
+ const token = createGitHubAuthStore(state.projectRoot).readToken();
7146
+ if (!token)
7147
+ return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
7148
+ const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
7149
+ throw new Error(error instanceof Error ? error.message : String(error));
5992
7150
  });
7151
+ return deps.jsonResponse({ ok: true, field });
5993
7152
  }
5994
7153
  if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
5995
7154
  const body = await deps.readJsonBody(req);
@@ -6054,7 +7213,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6054
7213
  }
6055
7214
  if (url.pathname === "/api/server/status") {
6056
7215
  const config = buildProjectConfigStatus(state.projectRoot);
6057
- const taskSource = await buildTaskSourceStatus(state, config);
7216
+ const taskSource = await buildTaskSourceStatus(state, config, requestAuth);
6058
7217
  return deps.jsonResponse({
6059
7218
  ok: true,
6060
7219
  projectRoot: state.projectRoot,
@@ -6078,8 +7237,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6078
7237
  path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
6079
7238
  ref: normalizeString(rawCheckout?.ref) ?? undefined
6080
7239
  } : undefined;
6081
- const record = upsertProjectRecord(state.projectRoot, { repoSlug, checkout });
6082
- return deps.jsonResponse({ ok: true, project: record });
7240
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7241
+ const record = upsertProjectRecord(registryRoot, { repoSlug, checkout });
7242
+ return deps.jsonResponse({ ok: true, project: record, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6083
7243
  }
6084
7244
  const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
6085
7245
  if (snapshotUploadMatch && req.method === "POST") {
@@ -6092,8 +7252,15 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6092
7252
  if (!archiveContentBase64) {
6093
7253
  return deps.badRequest("archiveContentBase64 is required");
6094
7254
  }
6095
- 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"));
6096
- const checkoutKey = normalizeString(body.checkoutKey) ?? "default";
7255
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7256
+ const baseDir = resolveNamespacedBaseDir({
7257
+ explicitBaseDir: normalizeString(body.baseDir),
7258
+ envName: "RIG_REMOTE_SNAPSHOT_BASE_DIR",
7259
+ userNamespace: requestAuth.userNamespace,
7260
+ legacyProjectRoot: state.projectRoot,
7261
+ legacySubdir: "remote-snapshots"
7262
+ });
7263
+ const checkoutKey = explicitCheckoutKey(body, body, requestAuth);
6097
7264
  try {
6098
7265
  const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
6099
7266
  const checkout = extractUploadedSnapshotArchive({
@@ -6106,14 +7273,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6106
7273
  const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
6107
7274
  const packageInstall = installRemoteCheckoutPackages(checkout.path);
6108
7275
  const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
6109
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
7276
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
6110
7277
  kind: "uploaded-snapshot",
6111
7278
  path: checkout.path,
6112
7279
  ref: checkout.snapshotId
6113
7280
  });
6114
7281
  deps.snapshotService.invalidate("uploaded-snapshot-checkout");
6115
7282
  deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
6116
- return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
7283
+ return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6117
7284
  } catch (error) {
6118
7285
  return deps.jsonResponse({
6119
7286
  ok: false,
@@ -6133,10 +7300,17 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6133
7300
  if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
6134
7301
  return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
6135
7302
  }
6136
- 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"));
6137
- const checkoutKey = normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? "default";
7303
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7304
+ const baseDir = resolveNamespacedBaseDir({
7305
+ explicitBaseDir: normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir),
7306
+ envName: "RIG_REMOTE_CHECKOUT_BASE_DIR",
7307
+ userNamespace: requestAuth.userNamespace,
7308
+ legacyProjectRoot: state.projectRoot,
7309
+ legacySubdir: "remote-checkouts"
7310
+ });
7311
+ const checkoutKey = explicitCheckoutKey(body, checkoutInput, requestAuth);
6138
7312
  const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
6139
- const credentialToken = createGitHubAuthStore(state.projectRoot).readToken();
7313
+ const credentialToken = requestScopedAuthStore(state.projectRoot, requestAuth).readToken();
6140
7314
  try {
6141
7315
  const checkout = await prepareRemoteCheckout({
6142
7316
  command: runRemoteCheckoutCommand,
@@ -6145,14 +7319,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6145
7319
  const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
6146
7320
  const packageInstall = installRemoteCheckoutPackages(checkout.path);
6147
7321
  const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
6148
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
7322
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
6149
7323
  kind: checkout.kind,
6150
7324
  path: checkout.path,
6151
7325
  ref: checkout.ref ?? checkout.snapshotId ?? undefined
6152
7326
  });
6153
7327
  deps.snapshotService.invalidate("remote-checkout-prepared");
6154
7328
  deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
6155
- return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
7329
+ return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6156
7330
  } catch (error) {
6157
7331
  return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
6158
7332
  }
@@ -6169,16 +7343,18 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6169
7343
  if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
6170
7344
  return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
6171
7345
  }
6172
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
7346
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7347
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
6173
7348
  kind,
6174
7349
  path: normalizeString(body.path) ?? state.projectRoot,
6175
7350
  ref: normalizeString(body.ref) ?? undefined
6176
7351
  });
6177
- return deps.jsonResponse({ ok: true, project });
7352
+ return deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6178
7353
  }
6179
7354
  if (req.method === "GET") {
6180
- const project = getProjectRecord(state.projectRoot, repoSlug);
6181
- return project ? deps.jsonResponse({ ok: true, project }) : deps.notFound();
7355
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
7356
+ const project = getProjectRecord(registryRoot, repoSlug);
7357
+ return project ? deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) }) : deps.notFound();
6182
7358
  }
6183
7359
  }
6184
7360
  if (url.pathname === "/api/server/project-root" && req.method === "POST") {
@@ -6187,13 +7363,26 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6187
7363
  if (!requestedRoot) {
6188
7364
  return deps.badRequest("projectRoot is required");
6189
7365
  }
6190
- if (!isAbsolute3(requestedRoot)) {
7366
+ if (!isAbsolute4(requestedRoot)) {
6191
7367
  return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
6192
7368
  }
6193
- const normalizedRoot = resolve18(requestedRoot);
6194
- const exists = existsSync11(normalizedRoot);
6195
- if (exists) {
6196
- createGitHubAuthStore(state.projectRoot).copyToProjectRoot(normalizedRoot);
7369
+ const normalizedRoot = resolve20(requestedRoot);
7370
+ const exists = existsSync13(normalizedRoot);
7371
+ if (exists && requestAuth.userNamespace) {
7372
+ const allowedByNamespace = isPathInsideNamespace(requestAuth.userNamespace.root, normalizedRoot);
7373
+ const allowedByRegistry = projectRegistryContainsCheckout(requestAuth.userNamespace.root, normalizedRoot);
7374
+ if (!allowedByNamespace && !allowedByRegistry) {
7375
+ return deps.jsonResponse({
7376
+ ok: false,
7377
+ error: "Requested project root is outside the authenticated GitHub user namespace.",
7378
+ projectRoot: state.projectRoot,
7379
+ requestedProjectRoot: normalizedRoot,
7380
+ userNamespace: userNamespaceResponse(requestAuth.userNamespace)
7381
+ }, 403);
7382
+ }
7383
+ copyGitHubAuthStateToLocalProjectRoot(requestAuth.userNamespace.authStateFile, normalizedRoot);
7384
+ } else if (exists) {
7385
+ createGitHubAuthStore(state.projectRoot).copyToLocalProjectRoot(normalizedRoot);
6197
7386
  }
6198
7387
  const control = buildServerControlStatus();
6199
7388
  const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
@@ -6208,7 +7397,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6208
7397
  message: "Requested project root does not exist on the Rig server host."
6209
7398
  }, 404);
6210
7399
  }
6211
- if (!existsSync11(resolve18(normalizedRoot, "rig.config.ts")) && !existsSync11(resolve18(normalizedRoot, "rig.config.json"))) {
7400
+ if (!existsSync13(resolve20(normalizedRoot, "rig.config.ts")) && !existsSync13(resolve20(normalizedRoot, "rig.config.json"))) {
6212
7401
  return deps.jsonResponse({
6213
7402
  ok: false,
6214
7403
  projectRoot: state.projectRoot,
@@ -6243,6 +7432,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6243
7432
  exists,
6244
7433
  control,
6245
7434
  requiresRestart: false,
7435
+ userNamespace: userNamespaceResponse(requestAuth.userNamespace),
6246
7436
  message: "Project-root switch accepted. Rig server restart has been scheduled."
6247
7437
  }, 202);
6248
7438
  }
@@ -6257,11 +7447,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6257
7447
  }, 409);
6258
7448
  }
6259
7449
  if (url.pathname === "/api/github/auth/status") {
6260
- const store = createGitHubAuthStore(state.projectRoot);
6261
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
7450
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
7451
+ return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
6262
7452
  }
6263
7453
  if (url.pathname === "/api/github/repo/permissions") {
6264
- const store = createGitHubAuthStore(state.projectRoot);
7454
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
6265
7455
  const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
6266
7456
  if (!auth.signedIn) {
6267
7457
  return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
@@ -6289,24 +7479,20 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6289
7479
  }
6290
7480
  try {
6291
7481
  const user = await fetchGitHubUserInfo(token);
6292
- const storeRoots = [
6293
- state.projectRoot,
6294
- ...requestedProjectRoot && isAbsolute3(requestedProjectRoot) && existsSync11(resolve18(requestedProjectRoot)) ? [resolve18(requestedProjectRoot)] : []
6295
- ].filter((root, index, roots) => roots.indexOf(root) === index);
6296
- const stores = storeRoots.map((root) => createGitHubAuthStore(root));
6297
- for (const store2 of stores) {
6298
- store2.saveToken({
6299
- token,
6300
- tokenSource: "manual-token",
6301
- login: user.login,
6302
- userId: user.userId,
6303
- scopes: user.scopes,
6304
- selectedRepo
6305
- });
6306
- }
6307
- const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
6308
- const apiSession = store.createApiSession();
6309
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
7482
+ const saved = saveGitHubTokenForRemoteUser({
7483
+ projectRoot: state.projectRoot,
7484
+ token,
7485
+ tokenSource: "manual-token",
7486
+ user,
7487
+ selectedRepo,
7488
+ requestedProjectRoot
7489
+ });
7490
+ return deps.jsonResponse({
7491
+ ok: true,
7492
+ ...saved.store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }),
7493
+ apiSessionToken: saved.apiSessionToken,
7494
+ userNamespace: userNamespaceResponse(saved.namespace)
7495
+ });
6310
7496
  } catch (error) {
6311
7497
  const message = error instanceof Error ? error.message : String(error);
6312
7498
  return deps.jsonResponse({ ok: false, error: message }, 400);
@@ -6373,9 +7559,21 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6373
7559
  }
6374
7560
  const token = result.payload.access_token;
6375
7561
  const user = await fetchGitHubUserInfo(token);
6376
- store.saveToken({ token, tokenSource: "oauth-device", login: user.login, userId: user.userId, scopes: user.scopes });
6377
- const apiSession = store.createApiSession();
6378
- return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }), apiSessionToken: apiSession.token });
7562
+ const saved = saveGitHubTokenForRemoteUser({
7563
+ projectRoot: state.projectRoot,
7564
+ token,
7565
+ tokenSource: "oauth-device",
7566
+ user,
7567
+ selectedRepo: null
7568
+ });
7569
+ store.clearPendingDevice(pollId);
7570
+ return deps.jsonResponse({
7571
+ ok: true,
7572
+ status: "signed-in",
7573
+ ...saved.store.status({ oauthConfigured: true }),
7574
+ apiSessionToken: saved.apiSessionToken,
7575
+ userNamespace: userNamespaceResponse(saved.namespace)
7576
+ });
6379
7577
  }
6380
7578
  if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
6381
7579
  const body = await deps.readJsonBody(req);
@@ -6384,7 +7582,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6384
7582
  if (!owner || !repo) {
6385
7583
  return deps.badRequest("owner and repo are required");
6386
7584
  }
6387
- const store = createGitHubAuthStore(state.projectRoot);
7585
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
6388
7586
  const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
6389
7587
  const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
6390
7588
  return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
@@ -6405,7 +7603,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6405
7603
  return deps.badRequest(error instanceof Error ? error.message : String(error));
6406
7604
  }
6407
7605
  const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
6408
- const configPath = resolve18(targetRoot, "rig.config.ts");
7606
+ const configPath = resolve20(targetRoot, "rig.config.ts");
6409
7607
  const source = buildGitHubProjectConfigSource({
6410
7608
  projectName: rawProjectName,
6411
7609
  owner,
@@ -6417,8 +7615,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6417
7615
  ok: true,
6418
7616
  projectRoot: targetRoot,
6419
7617
  configPath,
6420
- exists: existsSync11(configPath),
6421
- requiresOverwrite: existsSync11(configPath),
7618
+ exists: existsSync13(configPath),
7619
+ requiresOverwrite: existsSync13(configPath),
6422
7620
  source,
6423
7621
  owner,
6424
7622
  repo,
@@ -6454,8 +7652,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6454
7652
  assignee,
6455
7653
  githubUserId: authStatus.userId ?? authStatus.login
6456
7654
  });
6457
- const configPath = resolve18(targetRoot, "rig.config.ts");
6458
- if (existsSync11(configPath) && !overwrite) {
7655
+ const configPath = resolve20(targetRoot, "rig.config.ts");
7656
+ if (existsSync13(configPath) && !overwrite) {
6459
7657
  return deps.jsonResponse({
6460
7658
  ok: false,
6461
7659
  error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
@@ -6471,11 +7669,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6471
7669
  return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
6472
7670
  }
6473
7671
  let backupPath = null;
6474
- if (existsSync11(configPath)) {
7672
+ if (existsSync13(configPath)) {
6475
7673
  backupPath = backupConfigPath(configPath);
6476
- copyFileSync(configPath, backupPath);
7674
+ copyFileSync2(configPath, backupPath);
6477
7675
  }
6478
- writeFileSync10(configPath, source, "utf8");
7676
+ writeFileSync12(configPath, source, "utf8");
6479
7677
  const selectedRepo = `${owner}/${repo}`;
6480
7678
  store.saveSelectedRepo(selectedRepo);
6481
7679
  const targetStore = createGitHubAuthStore(targetRoot);
@@ -6747,11 +7945,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6747
7945
  const runId = normalizeString(body.runId);
6748
7946
  const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
6749
7947
  const promptOverride = normalizeString(body.promptOverride);
7948
+ const restart = body.restart === true;
6750
7949
  if (!runId) {
6751
7950
  return deps.badRequest("runId is required");
6752
7951
  }
6753
7952
  try {
6754
- await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
7953
+ await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
6755
7954
  deps.broadcastSnapshotInvalidation(state);
6756
7955
  return deps.jsonResponse({ ok: true, runId, createdAt });
6757
7956
  } catch (error) {
@@ -6760,7 +7959,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6760
7959
  }
6761
7960
  if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
6762
7961
  const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
6763
- 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";
7962
+ 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";
6764
7963
  if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
6765
7964
  return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
6766
7965
  }
@@ -7076,9 +8275,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7076
8275
  } catch {
7077
8276
  return deps.badRequest("Invalid artifact path");
7078
8277
  }
7079
- mkdirSync11(dirname12(artifactPath), { recursive: true });
8278
+ mkdirSync13(dirname15(artifactPath), { recursive: true });
7080
8279
  const bytes = Buffer.from(contentBase64, "base64");
7081
- writeFileSync10(artifactPath, bytes);
8280
+ writeFileSync12(artifactPath, bytes);
7082
8281
  writeJsonFile4(`${artifactPath}.json`, {
7083
8282
  workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
7084
8283
  runId,
@@ -7115,13 +8314,75 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7115
8314
  }
7116
8315
  const run = leaseValidation.run;
7117
8316
  const completedAt = new Date().toISOString();
8317
+ const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
8318
+ if (run.taskId && workspaceDir) {
8319
+ patchRunRecord(state.projectRoot, runId, {
8320
+ status: "reviewing",
8321
+ completedAt: null,
8322
+ hostId,
8323
+ endpointId: leaseId,
8324
+ worktreePath: workspaceDir,
8325
+ serverCloseout: {
8326
+ status: "pending",
8327
+ phase: "queued",
8328
+ requestedAt: completedAt,
8329
+ updatedAt: completedAt,
8330
+ runtimeWorkspace: workspaceDir,
8331
+ branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
8332
+ taskId: run.taskId,
8333
+ source: "remote-complete"
8334
+ }
8335
+ });
8336
+ deps.appendRunLogEntryAndBroadcast(state, runId, {
8337
+ id: `log:${runId}:remote-server-closeout-requested`,
8338
+ title: "Server-owned closeout requested",
8339
+ detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
8340
+ tone: "info",
8341
+ status: "reviewing",
8342
+ createdAt: completedAt,
8343
+ payload: { workspaceDir, hostId, leaseId }
8344
+ }, "remote-server-closeout-requested");
8345
+ deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
8346
+ const detail = error instanceof Error ? error.message : String(error);
8347
+ patchRunRecord(state.projectRoot, runId, {
8348
+ status: "failed",
8349
+ completedAt: new Date().toISOString(),
8350
+ errorText: detail,
8351
+ serverCloseout: {
8352
+ status: "failed",
8353
+ phase: "failed",
8354
+ updatedAt: new Date().toISOString(),
8355
+ error: detail
8356
+ }
8357
+ });
8358
+ deps.appendRunLogEntryAndBroadcast(state, runId, {
8359
+ id: `log:${runId}:remote-server-closeout-failed`,
8360
+ title: "Server-owned closeout failed",
8361
+ detail,
8362
+ tone: "error",
8363
+ status: "failed",
8364
+ createdAt: new Date().toISOString()
8365
+ }, "remote-server-closeout-failed");
8366
+ }).finally(() => {
8367
+ deps.reconcileScheduler(state, "remote-server-closeout-terminal");
8368
+ });
8369
+ deps.broadcastSnapshotInvalidation(state);
8370
+ return deps.jsonResponse({
8371
+ ok: true,
8372
+ workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
8373
+ hostId,
8374
+ runId,
8375
+ leaseId,
8376
+ closeout: "server-owned",
8377
+ acceptedAt: new Date().toISOString()
8378
+ });
8379
+ }
7118
8380
  patchRunRecord(state.projectRoot, runId, {
7119
8381
  status: "completed",
7120
8382
  completedAt,
7121
8383
  hostId,
7122
8384
  endpointId: leaseId
7123
8385
  });
7124
- await updateRemoteRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "completed", completedAt, hostId, endpointId: leaseId }, "closed", "Remote Rig task run completed and closed this task.");
7125
8386
  await deps.enqueueRunLinearEvent(state.projectRoot, {
7126
8387
  type: "run.completed",
7127
8388
  runId,
@@ -7240,12 +8501,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7240
8501
  try {
7241
8502
  const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
7242
8503
  const runRoot = deps.normalizeRelativePath(runsRoot, runId);
7243
- const artifactsRoot = resolve18(runRoot, "remote-artifacts");
8504
+ const artifactsRoot = resolve20(runRoot, "remote-artifacts");
7244
8505
  artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
7245
8506
  } catch {
7246
8507
  return deps.badRequest("Invalid artifact path");
7247
8508
  }
7248
- if (!existsSync11(artifactPath)) {
8509
+ if (!existsSync13(artifactPath)) {
7249
8510
  return deps.notFound();
7250
8511
  }
7251
8512
  return new Response(Bun.file(artifactPath));
@@ -7258,6 +8519,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7258
8519
  const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
7259
8520
  return deps.jsonResponse(page);
7260
8521
  }
8522
+ const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
8523
+ if (runTimelineMatch) {
8524
+ const runId = decodeURIComponent(runTimelineMatch[1]);
8525
+ const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
8526
+ const cursor = normalizeString(url.searchParams.get("cursor"));
8527
+ const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
8528
+ return deps.jsonResponse(page);
8529
+ }
7261
8530
  const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
7262
8531
  if (runSteerMatch && req.method === "POST") {
7263
8532
  const runId = decodeURIComponent(runSteerMatch[1]);
@@ -7277,6 +8546,49 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7277
8546
  return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 404);
7278
8547
  }
7279
8548
  }
8549
+ const runPiMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/pi(?:\/(.*))?$/);
8550
+ if (runPiMatch) {
8551
+ const runId = decodeURIComponent(runPiMatch[1]);
8552
+ const action = runPiMatch[2] || "";
8553
+ const resolved = resolveRunPiSessionProxy(state.projectRoot, runId);
8554
+ if (resolved === null)
8555
+ return deps.notFound();
8556
+ if ("pending" in resolved) {
8557
+ if (action === "" && req.method === "GET") {
8558
+ return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500 }, 202);
8559
+ }
8560
+ return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500, error: "Pi session is not ready" }, 409);
8561
+ }
8562
+ if (action === "" && req.method === "GET")
8563
+ return deps.jsonResponse({ ready: true, metadata: resolved.metadata });
8564
+ const body = req.method === "GET" ? undefined : await deps.readJsonBody(req);
8565
+ const sessionPath = `/sessions/${encodeURIComponent(resolved.sessionId)}`;
8566
+ const daemonPath = (() => {
8567
+ if (action === "messages" && req.method === "GET")
8568
+ return `${sessionPath}/messages`;
8569
+ if (action === "status" && req.method === "GET")
8570
+ return `${sessionPath}/status`;
8571
+ if (action === "commands" && req.method === "GET")
8572
+ return `${sessionPath}/commands`;
8573
+ if (action === "prompt" && req.method === "POST")
8574
+ return `${sessionPath}/prompt`;
8575
+ if (action === "shell" && req.method === "POST")
8576
+ return `${sessionPath}/shell`;
8577
+ if (action === "commands/run" && req.method === "POST")
8578
+ return `${sessionPath}/commands/run`;
8579
+ if (action === "commands/respond" && req.method === "POST")
8580
+ return `${sessionPath}/commands/respond`;
8581
+ if (action === "extension-ui/respond" && req.method === "POST")
8582
+ return `${sessionPath}/extension-ui/respond`;
8583
+ if (action === "abort" && req.method === "POST")
8584
+ return `${sessionPath}/abort`;
8585
+ return null;
8586
+ })();
8587
+ if (!daemonPath)
8588
+ return deps.notFound();
8589
+ const proxied = await proxyRunPiHttp(state.projectRoot, runId, { method: req.method, daemonPath, body });
8590
+ return deps.jsonResponse(proxied.payload, proxied.status);
8591
+ }
7280
8592
  const runSteeringAckMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steering\/ack$/);
7281
8593
  if (runSteeringAckMatch && req.method === "POST") {
7282
8594
  const runId = decodeURIComponent(runSteeringAckMatch[1]);
@@ -7399,7 +8711,7 @@ import {
7399
8711
  RemoteWsClient as RemoteWsClient2
7400
8712
  } from "@rig/runtime/control-plane/remote";
7401
8713
  import { deleteRunState } from "@rig/runtime/control-plane/native/run-ops";
7402
- import { readAuthorityRun as readAuthorityRun7 } from "@rig/runtime/control-plane/authority-files";
8714
+ import { readAuthorityRun as readAuthorityRun8 } from "@rig/runtime/control-plane/authority-files";
7403
8715
  function redactRemoteEndpoint2(endpoint) {
7404
8716
  const { token, ...rest } = endpoint;
7405
8717
  return {
@@ -7613,7 +8925,7 @@ async function routeWebSocketRequest(state, deps, request) {
7613
8925
  if (!runId || !messageId || text === null) {
7614
8926
  throw new Error("runId, messageId, and text are required");
7615
8927
  }
7616
- const run = readAuthorityRun7(state.projectRoot, runId);
8928
+ const run = readAuthorityRun8(state.projectRoot, runId);
7617
8929
  if (!run) {
7618
8930
  throw new Error(`Run not found: ${runId}`);
7619
8931
  }
@@ -8138,8 +9450,8 @@ async function routeWebSocketRequest(state, deps, request) {
8138
9450
  }
8139
9451
 
8140
9452
  // packages/server/src/server-helpers/inspector-jobs.ts
8141
- import { existsSync as existsSync15, mkdirSync as mkdirSync14, readFileSync as readFileSync10, writeFileSync as writeFileSync13 } from "fs";
8142
- import { dirname as dirname15, resolve as resolve21 } from "path";
9453
+ import { existsSync as existsSync17, mkdirSync as mkdirSync16, readFileSync as readFileSync12, writeFileSync as writeFileSync15 } from "fs";
9454
+ import { dirname as dirname18, resolve as resolve23 } from "path";
8143
9455
  import { readJsonFile as readJsonFile3 } from "@rig/runtime/control-plane/authority-files";
8144
9456
  import { resolveMonorepoRoot as resolveMonorepoRoot5 } from "@rig/runtime/control-plane/native/utils";
8145
9457
  import { normalizeTaskLifecycleStatus as normalizeTaskLifecycleStatus2 } from "@rig/runtime/control-plane/state-sync/types";
@@ -8247,8 +9559,8 @@ import { randomUUID as randomUUID3 } from "crypto";
8247
9559
 
8248
9560
  // packages/server/src/inspector/mission.ts
8249
9561
  import { randomUUID as randomUUID2 } from "crypto";
8250
- import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync8, readdirSync as readdirSync4, renameSync, writeFileSync as writeFileSync11 } from "fs";
8251
- import { dirname as dirname13, join, resolve as resolve19 } from "path";
9562
+ import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync14, readFileSync as readFileSync10, readdirSync as readdirSync4, renameSync, writeFileSync as writeFileSync13 } from "fs";
9563
+ import { dirname as dirname16, join, resolve as resolve21 } from "path";
8252
9564
  function isJsonValue(value) {
8253
9565
  if (value === null)
8254
9566
  return true;
@@ -8288,7 +9600,7 @@ function isRecord2(value) {
8288
9600
  }
8289
9601
  function readJsonRecord(path) {
8290
9602
  try {
8291
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
9603
+ const parsed = JSON.parse(readFileSync10(path, "utf8"));
8292
9604
  if (!isRecord2(parsed)) {
8293
9605
  return { ok: false, error: `Mission file ${path} does not contain an object` };
8294
9606
  }
@@ -8368,14 +9680,14 @@ function missionActionDetails(mission) {
8368
9680
  };
8369
9681
  }
8370
9682
  function writeJsonFile5(path, value) {
8371
- mkdirSync12(dirname13(path), { recursive: true });
9683
+ mkdirSync14(dirname16(path), { recursive: true });
8372
9684
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
8373
- writeFileSync11(tempPath, `${JSON.stringify(value, null, 2)}
9685
+ writeFileSync13(tempPath, `${JSON.stringify(value, null, 2)}
8374
9686
  `, "utf8");
8375
9687
  renameSync(tempPath, path);
8376
9688
  }
8377
9689
  function resolveInspectorMissionPaths(projectRoot) {
8378
- const inspectorDir = resolve19(resolveRigServerPaths(projectRoot).stateDir, "inspector");
9690
+ const inspectorDir = resolve21(resolveRigServerPaths(projectRoot).stateDir, "inspector");
8379
9691
  return {
8380
9692
  inspectorDir,
8381
9693
  missionsDir: join(inspectorDir, "missions"),
@@ -8384,8 +9696,8 @@ function resolveInspectorMissionPaths(projectRoot) {
8384
9696
  }
8385
9697
  function createInspectorMissionController(options) {
8386
9698
  const paths = resolveInspectorMissionPaths(options.projectRoot);
8387
- mkdirSync12(paths.missionsDir, { recursive: true });
8388
- mkdirSync12(paths.journalsDir, { recursive: true });
9699
+ mkdirSync14(paths.missionsDir, { recursive: true });
9700
+ mkdirSync14(paths.journalsDir, { recursive: true });
8389
9701
  const now = options.now ?? (() => new Date().toISOString());
8390
9702
  const nextId = options.idGenerator ?? (() => `mission:${randomUUID2()}`);
8391
9703
  function missionPath(missionId) {
@@ -8395,15 +9707,15 @@ function createInspectorMissionController(options) {
8395
9707
  return join(paths.journalsDir, `${missionId}.jsonl`);
8396
9708
  }
8397
9709
  function appendMissionJournal(entry) {
8398
- mkdirSync12(paths.journalsDir, { recursive: true });
9710
+ mkdirSync14(paths.journalsDir, { recursive: true });
8399
9711
  appendFileSync(journalPath(entry.missionId), `${JSON.stringify(entry)}
8400
9712
  `, "utf8");
8401
9713
  }
8402
9714
  function listMissionJournal(missionId) {
8403
9715
  const path = journalPath(missionId);
8404
- if (!existsSync12(path))
9716
+ if (!existsSync14(path))
8405
9717
  return [];
8406
- return readFileSync8(path, "utf8").split(`
9718
+ return readFileSync10(path, "utf8").split(`
8407
9719
  `).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line)).filter(isRecord2).map((entry) => ({
8408
9720
  id: typeof entry.id === "string" ? entry.id : `journal:${randomUUID2()}`,
8409
9721
  missionId,
@@ -8419,7 +9731,7 @@ function createInspectorMissionController(options) {
8419
9731
  }
8420
9732
  function readMissionOnly(missionId) {
8421
9733
  const path = missionPath(missionId);
8422
- if (!existsSync12(path)) {
9734
+ if (!existsSync14(path)) {
8423
9735
  return { ok: false, error: `Mission ${missionId} was not found` };
8424
9736
  }
8425
9737
  const read = readJsonRecord(path);
@@ -8470,7 +9782,7 @@ function createInspectorMissionController(options) {
8470
9782
  const source = cloneJsonRecord(input.sourceTask);
8471
9783
  const missionId = nextId();
8472
9784
  const path = missionPath(missionId);
8473
- if (existsSync12(path)) {
9785
+ if (existsSync14(path)) {
8474
9786
  const existing = readMissionOnly(missionId);
8475
9787
  if (!existing.ok)
8476
9788
  return existing;
@@ -10070,8 +11382,8 @@ function createCodexInspectorTransport(options) {
10070
11382
  const sendRequest = async (method, params) => {
10071
11383
  const id = nextRequestId;
10072
11384
  nextRequestId += 1;
10073
- const response = new Promise((resolve20, reject) => {
10074
- pendingResponses.set(id, { resolve: resolve20, reject });
11385
+ const response = new Promise((resolve22, reject) => {
11386
+ pendingResponses.set(id, { resolve: resolve22, reject });
10075
11387
  });
10076
11388
  response.catch(() => {});
10077
11389
  try {
@@ -10381,9 +11693,9 @@ function createCodexInspectorTransport(options) {
10381
11693
  }
10382
11694
  lastAssistantMessage = null;
10383
11695
  lastError = null;
10384
- const turnResult = new Promise((resolve20, reject) => {
11696
+ const turnResult = new Promise((resolve22, reject) => {
10385
11697
  currentTurn = {
10386
- resolve: resolve20,
11698
+ resolve: resolve22,
10387
11699
  reject,
10388
11700
  events: []
10389
11701
  };
@@ -10443,13 +11755,13 @@ function createCodexInspectorTransport(options) {
10443
11755
  };
10444
11756
  }
10445
11757
  function writeChildLine(child, line) {
10446
- return new Promise((resolve20, reject) => {
11758
+ return new Promise((resolve22, reject) => {
10447
11759
  child.stdin.write(line, (error) => {
10448
11760
  if (error) {
10449
11761
  reject(error);
10450
11762
  return;
10451
11763
  }
10452
- resolve20();
11764
+ resolve22();
10453
11765
  });
10454
11766
  });
10455
11767
  }
@@ -10462,10 +11774,10 @@ function terminateChild(child) {
10462
11774
  } catch {}
10463
11775
  }
10464
11776
  async function waitForChildSpawn(child) {
10465
- await new Promise((resolve20, reject) => {
11777
+ await new Promise((resolve22, reject) => {
10466
11778
  const onSpawn = () => {
10467
11779
  cleanup();
10468
- resolve20();
11780
+ resolve22();
10469
11781
  };
10470
11782
  const onError = (error) => {
10471
11783
  cleanup();
@@ -10574,7 +11886,7 @@ import {
10574
11886
  } from "@rig/runtime/control-plane/native/run-ops";
10575
11887
  import {
10576
11888
  listAuthorityRuns as listAuthorityRuns6,
10577
- readAuthorityRun as readAuthorityRun8
11889
+ readAuthorityRun as readAuthorityRun9
10578
11890
  } from "@rig/runtime/control-plane/authority-files";
10579
11891
  function providerFromRuntimeAdapter(runtimeAdapter) {
10580
11892
  if (!runtimeAdapter) {
@@ -10654,7 +11966,7 @@ function discoverInspectorRuns(options) {
10654
11966
  discovered.set(surface.runId, existing);
10655
11967
  };
10656
11968
  for (const authorityEntry of listAuthorityRuns6(options.projectRoot)) {
10657
- const run = readAuthorityRun8(options.projectRoot, authorityEntry.runId);
11969
+ const run = readAuthorityRun9(options.projectRoot, authorityEntry.runId);
10658
11970
  if (!run) {
10659
11971
  continue;
10660
11972
  }
@@ -10977,8 +12289,8 @@ function createGlobalInspectorService(options) {
10977
12289
 
10978
12290
  // packages/server/src/inspector/upstream-sync.ts
10979
12291
  import { spawnSync as spawnSync4 } from "child_process";
10980
- import { existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync12 } from "fs";
10981
- import { dirname as dirname14, resolve as resolve20 } from "path";
12292
+ import { existsSync as existsSync15, mkdirSync as mkdirSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync14 } from "fs";
12293
+ import { dirname as dirname17, resolve as resolve22 } from "path";
10982
12294
  import { resolveMonorepoRoot as resolveMonorepoRoot4 } from "@rig/runtime/control-plane/native/utils";
10983
12295
  var UPSTREAM_VALIDATION_DESCRIPTIONS = {
10984
12296
  "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.",
@@ -11116,34 +12428,34 @@ function defaultGitRunner(repoRoot, args) {
11116
12428
  }
11117
12429
  function upstreamStatePath(projectRoot, override) {
11118
12430
  if (override) {
11119
- return resolve20(override);
12431
+ return resolve22(override);
11120
12432
  }
11121
- return resolve20(resolveRigServerPaths(projectRoot).stateDir, "inspector", "upstream-sync.json");
12433
+ return resolve22(resolveRigServerPaths(projectRoot).stateDir, "inspector", "upstream-sync.json");
11122
12434
  }
11123
12435
  function readUpstreamState(projectRoot, statePath) {
11124
12436
  const path = upstreamStatePath(projectRoot, statePath);
11125
- if (!existsSync13(path)) {
12437
+ if (!existsSync15(path)) {
11126
12438
  return null;
11127
12439
  }
11128
12440
  try {
11129
- return JSON.parse(readFileSync9(path, "utf-8"));
12441
+ return JSON.parse(readFileSync11(path, "utf-8"));
11130
12442
  } catch {
11131
12443
  return null;
11132
12444
  }
11133
12445
  }
11134
12446
  function writeUpstreamState(projectRoot, state, statePath) {
11135
12447
  const path = upstreamStatePath(projectRoot, statePath);
11136
- mkdirSync13(dirname14(path), { recursive: true });
11137
- writeFileSync12(path, `${JSON.stringify(state, null, 2)}
12448
+ mkdirSync15(dirname17(path), { recursive: true });
12449
+ writeFileSync14(path, `${JSON.stringify(state, null, 2)}
11138
12450
  `, "utf8");
11139
12451
  }
11140
12452
  function readImportedRevision(projectRoot, upstreamsDocPath) {
11141
12453
  const monorepoRoot = resolveMonorepoRoot4(projectRoot);
11142
- const docPath = upstreamsDocPath ? resolve20(upstreamsDocPath) : resolve20(monorepoRoot, "docs", "UPSTREAMS.md");
11143
- if (!existsSync13(docPath)) {
12454
+ const docPath = upstreamsDocPath ? resolve22(upstreamsDocPath) : resolve22(monorepoRoot, "docs", "UPSTREAMS.md");
12455
+ if (!existsSync15(docPath)) {
11144
12456
  throw new Error(`UPSTREAMS.md not found at ${docPath}`);
11145
12457
  }
11146
- const docContent = readFileSync9(docPath, "utf-8");
12458
+ const docContent = readFileSync11(docPath, "utf-8");
11147
12459
  const revision = parseImportedUpstreamRevision(docContent, "upstream") ?? parseImportedUpstreamRevision(docContent, "humoongate");
11148
12460
  if (!revision) {
11149
12461
  throw new Error(`Failed to parse upstream imported revision from ${docPath}`);
@@ -11165,7 +12477,7 @@ function resolveRemoteBranch(repoRoot, remote, gitRunner) {
11165
12477
  return null;
11166
12478
  }
11167
12479
  function isGitCheckout(path, gitRunner) {
11168
- if (!existsSync13(resolve20(path, ".git"))) {
12480
+ if (!existsSync15(resolve22(path, ".git"))) {
11169
12481
  return false;
11170
12482
  }
11171
12483
  const result = gitRunner(path, ["rev-parse", "--is-inside-work-tree"]);
@@ -11174,12 +12486,12 @@ function isGitCheckout(path, gitRunner) {
11174
12486
  function resolveUpstreamCheckout(projectRoot, explicitCheckout, gitRunner) {
11175
12487
  const monorepoRoot = resolveMonorepoRoot4(projectRoot);
11176
12488
  const candidates = [
11177
- explicitCheckout ? resolve20(explicitCheckout) : "",
11178
- process.env.UPSTREAM_CHECKOUT?.trim() ? resolve20(process.env.UPSTREAM_CHECKOUT.trim()) : "",
11179
- process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ? resolve20(process.env.HUMOONGATE_UPSTREAM_CHECKOUT.trim()) : "",
11180
- resolve20(projectRoot, "..", "humoongate"),
11181
- resolve20(monorepoRoot, "..", "humoongate"),
11182
- resolve20(monorepoRoot, "humoongate")
12489
+ explicitCheckout ? resolve22(explicitCheckout) : "",
12490
+ process.env.UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.UPSTREAM_CHECKOUT.trim()) : "",
12491
+ process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.HUMOONGATE_UPSTREAM_CHECKOUT.trim()) : "",
12492
+ resolve22(projectRoot, "..", "humoongate"),
12493
+ resolve22(monorepoRoot, "..", "humoongate"),
12494
+ resolve22(monorepoRoot, "humoongate")
11183
12495
  ].filter(Boolean);
11184
12496
  for (const candidate of candidates) {
11185
12497
  if (isGitCheckout(candidate, gitRunner)) {
@@ -11415,10 +12727,10 @@ async function runUpstreamSyncScan(options) {
11415
12727
  }
11416
12728
 
11417
12729
  // packages/server/src/server-helpers/task-config.ts
11418
- import { existsSync as existsSync14 } from "fs";
12730
+ import { existsSync as existsSync16 } from "fs";
11419
12731
  async function readTaskConfig(projectRoot) {
11420
12732
  const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
11421
- if (!existsSync14(taskConfigPath)) {
12733
+ if (!existsSync16(taskConfigPath)) {
11422
12734
  return {};
11423
12735
  }
11424
12736
  try {
@@ -11454,11 +12766,11 @@ function resolveFollowupSourceCommit(input) {
11454
12766
  }
11455
12767
  async function createInspectorFollowupTask(projectRoot, input) {
11456
12768
  const monorepoRoot = resolveMonorepoRoot5(projectRoot);
11457
- const issuesPath = resolve21(monorepoRoot, ".beads", "issues.jsonl");
11458
- const taskStatePath = resolve21(monorepoRoot, ".beads", "task-state.json");
11459
- const taskConfigPath = resolve21(monorepoRoot, ".rig", "task-config.json");
11460
- mkdirSync14(dirname15(issuesPath), { recursive: true });
11461
- mkdirSync14(dirname15(taskConfigPath), { recursive: true });
12769
+ const issuesPath = resolve23(monorepoRoot, ".beads", "issues.jsonl");
12770
+ const taskStatePath = resolve23(monorepoRoot, ".beads", "task-state.json");
12771
+ const taskConfigPath = resolve23(monorepoRoot, ".rig", "task-config.json");
12772
+ mkdirSync16(dirname18(issuesPath), { recursive: true });
12773
+ mkdirSync16(dirname18(taskConfigPath), { recursive: true });
11462
12774
  const summary = normalizeString(input.summary) ?? "Inspector follow-up";
11463
12775
  const description = normalizeString(input.description) ?? normalizeString(input.details?.description) ?? `Created by the global inspector: ${summary}`;
11464
12776
  const acceptanceCriteria = normalizeString(input.acceptanceCriteria) ?? "Investigate the detected drift and port the relevant changes into Rig.";
@@ -11477,7 +12789,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
11477
12789
  const sourceKey = normalizeString(input.sourceKey) ?? normalizeString(input.details?.sourceKey);
11478
12790
  const createdAt = normalizeString(input.createdAt) ?? new Date().toISOString();
11479
12791
  const status = normalizeTaskLifecycleStatus2(normalizeString(input.status) ?? "open") ?? "open";
11480
- const existingIssueLines = existsSync15(issuesPath) ? readFileSync10(issuesPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
12792
+ const existingIssueLines = existsSync17(issuesPath) ? readFileSync12(issuesPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
11481
12793
  const existingIssues = existingIssueLines.map((line) => {
11482
12794
  try {
11483
12795
  return JSON.parse(line);
@@ -11486,7 +12798,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
11486
12798
  }
11487
12799
  }).filter((value) => value !== null);
11488
12800
  const existingIds = new Set(existingIssues.map((issue) => typeof issue.id === "string" ? issue.id : null).filter((value) => value !== null));
11489
- const rawTaskState = existsSync15(taskStatePath) ? readJsonFile3(taskStatePath, {}) : {};
12801
+ const rawTaskState = existsSync17(taskStatePath) ? readJsonFile3(taskStatePath, {}) : {};
11490
12802
  const tasks = rawTaskState.tasks && typeof rawTaskState.tasks === "object" && !Array.isArray(rawTaskState.tasks) ? rawTaskState.tasks : {};
11491
12803
  const existingTaskIdFromSourceKey = sourceKey == null ? null : Object.entries(tasks).find(([, metadata]) => {
11492
12804
  if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
@@ -11512,7 +12824,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
11512
12824
  updated_at: createdAt,
11513
12825
  labels: mergedLabels
11514
12826
  };
11515
- writeFileSync13(issuesPath, existingIssueLines.length > 0 ? `${existingIssueLines.join(`
12827
+ writeFileSync15(issuesPath, existingIssueLines.length > 0 ? `${existingIssueLines.join(`
11516
12828
  `)}
11517
12829
  ${JSON.stringify(issueRecord)}
11518
12830
  ` : `${JSON.stringify(issueRecord)}
@@ -11530,7 +12842,7 @@ ${JSON.stringify(issueRecord)}
11530
12842
  labels: mergedLabels
11531
12843
  };
11532
12844
  });
11533
- writeFileSync13(issuesPath, `${updatedIssues.map((issue) => JSON.stringify(issue)).join(`
12845
+ writeFileSync15(issuesPath, `${updatedIssues.map((issue) => JSON.stringify(issue)).join(`
11534
12846
  `)}
11535
12847
  `, "utf8");
11536
12848
  }
@@ -11553,14 +12865,14 @@ ${JSON.stringify(issueRecord)}
11553
12865
  }
11554
12866
  };
11555
12867
  }
11556
- writeFileSync13(taskConfigPath, `${JSON.stringify(taskConfig, null, 2)}
12868
+ writeFileSync15(taskConfigPath, `${JSON.stringify(taskConfig, null, 2)}
11557
12869
  `, "utf8");
11558
12870
  tasks[taskId] = {
11559
12871
  status,
11560
12872
  sourceCommit: resolveFollowupSourceCommit(input),
11561
12873
  ...sourceKey ? { sourceKey } : {}
11562
12874
  };
11563
- writeFileSync13(taskStatePath, `${JSON.stringify({
12875
+ writeFileSync15(taskStatePath, `${JSON.stringify({
11564
12876
  schemaVersion: 1,
11565
12877
  baseTrackerCommit: typeof rawTaskState.baseTrackerCommit === "string" ? rawTaskState.baseTrackerCommit : null,
11566
12878
  tasks
@@ -11587,21 +12899,10 @@ async function runInspectorLocalReview(projectRoot, input) {
11587
12899
  details: null
11588
12900
  };
11589
12901
  }
11590
- const [{ RuntimeEventBus }, { PluginManager }, { verifyTask }] = await Promise.all([
11591
- import("@rig/runtime/control-plane/runtime/events"),
11592
- import("@rig/runtime/control-plane/runtime/plugins"),
11593
- import("@rig/runtime/control-plane/native/verifier")
11594
- ]);
11595
- const eventBus = new RuntimeEventBus({ projectRoot, runId: `inspector-review:${taskId}` });
11596
- const plugins = await PluginManager.load({
11597
- projectRoot,
11598
- runId: `inspector-review:${taskId}`,
11599
- eventBus
11600
- });
12902
+ const { verifyTask } = await import("@rig/runtime/control-plane/native/verifier");
11601
12903
  const outcome = await verifyTask({
11602
12904
  projectRoot,
11603
12905
  taskId,
11604
- plugins,
11605
12906
  skipAiReview: true
11606
12907
  });
11607
12908
  return {
@@ -11868,12 +13169,12 @@ function isAuthorizedLinearWebhookRequest(req) {
11868
13169
  }
11869
13170
 
11870
13171
  // packages/server/src/server-helpers/notifications.ts
11871
- import { existsSync as existsSync16, mkdirSync as mkdirSync15, readFileSync as readFileSync11 } from "fs";
11872
- import { dirname as dirname16 } from "path";
13172
+ import { existsSync as existsSync18, mkdirSync as mkdirSync17, readFileSync as readFileSync13 } from "fs";
13173
+ import { dirname as dirname19 } from "path";
11873
13174
  async function loadNotificationConfig(path) {
11874
- if (!existsSync16(path)) {
13175
+ if (!existsSync18(path)) {
11875
13176
  const defaultConfig = { targets: [] };
11876
- mkdirSync15(dirname16(path), { recursive: true });
13177
+ mkdirSync17(dirname19(path), { recursive: true });
11877
13178
  await Bun.write(path, `${JSON.stringify(defaultConfig, null, 2)}
11878
13179
  `);
11879
13180
  return defaultConfig;
@@ -11888,10 +13189,10 @@ async function loadNotificationConfig(path) {
11888
13189
  }
11889
13190
  }
11890
13191
  function readRecentEvents(file, limit) {
11891
- if (!existsSync16(file)) {
13192
+ if (!existsSync18(file)) {
11892
13193
  return [];
11893
13194
  }
11894
- const lines = readFileSync11(file, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-limit);
13195
+ const lines = readFileSync13(file, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-limit);
11895
13196
  const events = [];
11896
13197
  for (const line of lines) {
11897
13198
  try {
@@ -11986,11 +13287,11 @@ function extractObjectLiteralBlock(source, property) {
11986
13287
  }
11987
13288
  function readFallbackIssueAnalysisConfig(projectRoot) {
11988
13289
  for (const fileName of ["rig.config.ts", "rig.config.json"]) {
11989
- const path = resolve22(projectRoot, fileName);
11990
- if (!existsSync17(path))
13290
+ const path = resolve24(projectRoot, fileName);
13291
+ if (!existsSync19(path))
11991
13292
  continue;
11992
13293
  try {
11993
- const source = readFileSync12(path, "utf8");
13294
+ const source = readFileSync14(path, "utf8");
11994
13295
  if (fileName.endsWith(".json"))
11995
13296
  return JSON.parse(source);
11996
13297
  const issueBlock = extractObjectLiteralBlock(source, "issueAnalysis");
@@ -12123,8 +13424,8 @@ async function createIssueAnalysisRunnerForServerState(state, input) {
12123
13424
  async function withServerPathEnv(projectRoot, fn) {
12124
13425
  const waitForTurn = serverPathEnvQueue;
12125
13426
  let releaseTurn;
12126
- serverPathEnvQueue = new Promise((resolve23) => {
12127
- releaseTurn = resolve23;
13427
+ serverPathEnvQueue = new Promise((resolve25) => {
13428
+ releaseTurn = resolve25;
12128
13429
  });
12129
13430
  await waitForTurn;
12130
13431
  const paths = resolveServerAuthorityPaths(projectRoot);
@@ -12160,9 +13461,9 @@ async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
12160
13461
  return withServerPathEnv(projectRoot, fn);
12161
13462
  }
12162
13463
  async function readWorkspaceTasks(projectRoot) {
12163
- const issuesPath = resolve22(resolveMonorepoRoot6(projectRoot), ".beads", "issues.jsonl");
13464
+ const issuesPath = resolve24(resolveMonorepoRoot6(projectRoot), ".beads", "issues.jsonl");
12164
13465
  const taskConfig = await readTaskConfig(projectRoot);
12165
- if (!existsSync17(issuesPath)) {
13466
+ if (!existsSync19(issuesPath)) {
12166
13467
  return [];
12167
13468
  }
12168
13469
  const latestById = new Map;
@@ -12236,11 +13537,11 @@ function resolveTaskArtifactDirsFromRuns(projectRoot, taskId, knownRuns) {
12236
13537
  continue;
12237
13538
  add(run.artifactRoot);
12238
13539
  if (run.worktreePath) {
12239
- add(resolve22(run.worktreePath, "artifacts", taskId));
13540
+ add(resolve24(run.worktreePath, "artifacts", taskId));
12240
13541
  }
12241
13542
  }
12242
13543
  for (const artifactsRoot of listAuthorityArtifactRoots(projectRoot)) {
12243
- add(resolve22(artifactsRoot, taskId));
13544
+ add(resolve24(artifactsRoot, taskId));
12244
13545
  }
12245
13546
  return candidates;
12246
13547
  }
@@ -12254,7 +13555,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
12254
13555
  }
12255
13556
  }
12256
13557
  return taskIds.flatMap((currentTaskId) => {
12257
- const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) => existsSync17(path));
13558
+ const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) => existsSync19(path));
12258
13559
  if (!currentRoot) {
12259
13560
  return [];
12260
13561
  }
@@ -12266,7 +13567,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
12266
13567
  taskId: currentTaskId,
12267
13568
  kind: "file",
12268
13569
  label: fileName,
12269
- path: resolve22(currentRoot, fileName),
13570
+ path: resolve24(currentRoot, fileName),
12270
13571
  url: null,
12271
13572
  metadata: {
12272
13573
  fileName
@@ -12309,11 +13610,11 @@ function buildInspectorStreamPayload(state, sequence) {
12309
13610
  }
12310
13611
  function listRemoteRunArtifacts(projectRoot, runId) {
12311
13612
  const root = remoteArtifactsRoot(projectRoot, runId);
12312
- if (!existsSync17(root)) {
13613
+ if (!existsSync19(root)) {
12313
13614
  return [];
12314
13615
  }
12315
13616
  return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isFile()).filter((entry) => !entry.name.endsWith(".json")).map((entry) => {
12316
- const artifactPath = resolve22(root, entry.name);
13617
+ const artifactPath = resolve24(root, entry.name);
12317
13618
  const stat = statSync6(artifactPath);
12318
13619
  const meta = readJsonFile4(`${artifactPath}.json`, null);
12319
13620
  return {
@@ -12474,6 +13775,7 @@ function buildHttpRouterDeps(state) {
12474
13775
  startLocalRun,
12475
13776
  stopRunRecord,
12476
13777
  resumeRunRecord,
13778
+ runServerOwnedPrCloseout,
12477
13779
  claimRemoteRun,
12478
13780
  listRemoteRunArtifacts,
12479
13781
  broadcastSnapshotInvalidation,
@@ -12555,8 +13857,8 @@ function fileStats(path) {
12555
13857
  }
12556
13858
  }
12557
13859
  function runFileCursor(projectRoot, run) {
12558
- const runDir = dirname17(runLogsPath(projectRoot, run.runId));
12559
- const runJson = fileStats(resolve22(runDir, "run.json"));
13860
+ const runDir = dirname20(runLogsPath(projectRoot, run.runId));
13861
+ const runJson = fileStats(resolve24(runDir, "run.json"));
12560
13862
  const timeline = fileStats(runTimelinePath(projectRoot, run.runId));
12561
13863
  const logs = fileStats(runLogsPath(projectRoot, run.runId));
12562
13864
  return {
@@ -12606,10 +13908,10 @@ function startRunFileWatcher(state, pollMs) {
12606
13908
  }, Math.max(250, Math.min(pollMs, 1000)));
12607
13909
  }
12608
13910
  function startPoller(state, pollMs) {
12609
- let offset = existsSync17(state.eventsFile) ? statSync6(state.eventsFile).size : 0;
13911
+ let offset = existsSync19(state.eventsFile) ? statSync6(state.eventsFile).size : 0;
12610
13912
  return setInterval(async () => {
12611
13913
  try {
12612
- if (!existsSync17(state.eventsFile)) {
13914
+ if (!existsSync19(state.eventsFile)) {
12613
13915
  return;
12614
13916
  }
12615
13917
  const file = await open(state.eventsFile, "r");
@@ -12653,6 +13955,26 @@ function startPoller(state, pollMs) {
12653
13955
  }
12654
13956
  }, pollMs);
12655
13957
  }
13958
+ function attachPiSessionProxySocket(ws) {
13959
+ if (ws.data.kind !== "pi-session-proxy")
13960
+ return;
13961
+ const upstream = new WebSocket(ws.data.upstreamUrl);
13962
+ ws.__rigPiUpstream = upstream;
13963
+ upstream.addEventListener("message", (event) => {
13964
+ if (ws.readyState === 1)
13965
+ ws.send(typeof event.data === "string" ? event.data : event.data);
13966
+ });
13967
+ upstream.addEventListener("close", () => {
13968
+ try {
13969
+ ws.close();
13970
+ } catch {}
13971
+ });
13972
+ upstream.addEventListener("error", () => {
13973
+ try {
13974
+ ws.close(1011, "Upstream Pi session stream failed");
13975
+ } catch {}
13976
+ });
13977
+ }
12656
13978
  async function createRigServer(options, projectRoot = resolveProjectRoot()) {
12657
13979
  const state = await createRigServerState(projectRoot, options.eventType, options.authToken, {
12658
13980
  upstreamSyncMs: options.upstreamSyncMs,
@@ -12665,10 +13987,20 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
12665
13987
  fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
12666
13988
  websocket: {
12667
13989
  open(ws) {
13990
+ if (ws.data.kind === "pi-session-proxy") {
13991
+ attachPiSessionProxySocket(ws);
13992
+ return;
13993
+ }
12668
13994
  state.sockets.add(ws);
12669
13995
  sendWebSocketResponse(ws, buildServerWelcomePush(state.projectRoot));
12670
13996
  },
12671
13997
  async message(ws, raw) {
13998
+ if (ws.data.kind === "pi-session-proxy") {
13999
+ const upstream = ws.__rigPiUpstream;
14000
+ if (upstream?.readyState === WebSocket.OPEN)
14001
+ upstream.send(raw);
14002
+ return;
14003
+ }
12672
14004
  const request = parseWebSocketRequestEnvelope(typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8"));
12673
14005
  if (!request) {
12674
14006
  sendWebSocketResponse(ws, {
@@ -12691,6 +14023,10 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
12691
14023
  }
12692
14024
  },
12693
14025
  close(ws) {
14026
+ if (ws.data.kind === "pi-session-proxy") {
14027
+ ws.__rigPiUpstream?.close();
14028
+ return;
14029
+ }
12694
14030
  state.sockets.delete(ws);
12695
14031
  }
12696
14032
  }
@@ -12737,7 +14073,7 @@ function resolveProjectRoot() {
12737
14073
  return resolveRigProjectRoot({
12738
14074
  envProjectRoot: process.env.PROJECT_RIG_ROOT ?? null,
12739
14075
  cwd: process.cwd(),
12740
- fallbackRoot: resolve22(import.meta.dir, "../..")
14076
+ fallbackRoot: resolve24(import.meta.dir, "../..")
12741
14077
  });
12742
14078
  }
12743
14079
  var __testOnly = {