@h-rig/server 0.0.6-alpha.1 → 0.0.6-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,8 @@ var __require = import.meta.require;
4
4
  // packages/server/src/server-helpers/http-router.ts
5
5
  import { randomUUID } from "crypto";
6
6
  import { spawnSync as spawnSync2 } from "child_process";
7
- import { basename, dirname as dirname6, isAbsolute as isAbsolute2, resolve as resolve11 } from "path";
8
- import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync7 } from "fs";
7
+ import { basename, dirname as dirname9, isAbsolute as isAbsolute3, resolve as resolve13 } from "path";
8
+ import { copyFileSync as copyFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
9
9
 
10
10
  // packages/server/src/server.ts
11
11
  import {
@@ -469,6 +469,273 @@ async function refreshTaskProjection(projectRoot, input) {
469
469
  });
470
470
  }
471
471
 
472
+ // packages/server/src/server-helpers/issue-analysis.ts
473
+ import { createHash } from "crypto";
474
+ function stableIssueHash(issue) {
475
+ const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
476
+ const body = typeof issue.body === "string" ? issue.body : "";
477
+ const title = typeof issue.title === "string" ? issue.title : "";
478
+ return createHash("sha256").update(JSON.stringify({ id: issue.id, title, body, labels, deps: issue.deps, status: issue.status })).digest("hex");
479
+ }
480
+ function renderIssueAnalysisPrompt(input) {
481
+ const issue = input.issue;
482
+ const neighbors = input.neighbors ?? [];
483
+ return [
484
+ "You are Rig issue analysis running inside Pi.",
485
+ "Return JSON only with optional metadataPatch, labelsToAdd, labelsToRemove, and generatedIssues.",
486
+ "Preserve all human-authored issue body content. Only propose edits for Rig-owned metadata/status sections, labels, and generated issues.",
487
+ "Generated issues must be concrete, minimal follow-up tasks and will be labeled rig:generated by Rig.",
488
+ "",
489
+ "Issue:",
490
+ JSON.stringify({
491
+ id: issue.id,
492
+ title: issue.title,
493
+ body: issue.body,
494
+ labels: issue.labels,
495
+ deps: issue.deps,
496
+ status: issue.status
497
+ }, null, 2),
498
+ "",
499
+ "Neighbor tasks:",
500
+ JSON.stringify(neighbors.map((task) => ({ id: task.id, title: task.title, status: task.status, deps: task.deps })), null, 2)
501
+ ].join(`
502
+ `);
503
+ }
504
+ function isRecord(value) {
505
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
506
+ }
507
+ function stringArray(value) {
508
+ if (!Array.isArray(value))
509
+ return;
510
+ return value.map(String).filter((entry) => entry.trim().length > 0);
511
+ }
512
+ function generatedIssues(value) {
513
+ if (!Array.isArray(value))
514
+ return;
515
+ return value.flatMap((entry) => {
516
+ if (!isRecord(entry) || typeof entry.title !== "string")
517
+ return [];
518
+ return [{
519
+ title: entry.title,
520
+ body: typeof entry.body === "string" ? entry.body : "",
521
+ labels: stringArray(entry.labels) ?? [],
522
+ ...Array.isArray(entry.dependsOn) ? { dependsOn: entry.dependsOn.map(String) } : {}
523
+ }];
524
+ });
525
+ }
526
+ function findJsonLikeText(value) {
527
+ if (typeof value === "string") {
528
+ const trimmed = value.trim();
529
+ if (trimmed.startsWith("{") || trimmed.startsWith("```"))
530
+ return trimmed;
531
+ return null;
532
+ }
533
+ if (Array.isArray(value)) {
534
+ for (const entry of value) {
535
+ const found = findJsonLikeText(entry);
536
+ if (found)
537
+ return found;
538
+ }
539
+ return null;
540
+ }
541
+ if (!isRecord(value))
542
+ return null;
543
+ for (const key of ["text", "content", "message", "output_text", "response", "stdout"]) {
544
+ const found = findJsonLikeText(value[key]);
545
+ if (found)
546
+ return found;
547
+ }
548
+ for (const entry of Object.values(value)) {
549
+ const found = findJsonLikeText(entry);
550
+ if (found)
551
+ return found;
552
+ }
553
+ return null;
554
+ }
555
+ function candidateAnalysisObject(value) {
556
+ if (!isRecord(value))
557
+ return null;
558
+ if (isRecord(value.result))
559
+ return candidateAnalysisObject(value.result) ?? value.result;
560
+ if (isRecord(value.analysis))
561
+ return candidateAnalysisObject(value.analysis) ?? value.analysis;
562
+ if (isRecord(value.metadataPatch) || Array.isArray(value.labelsToAdd) || Array.isArray(value.labelsToRemove) || Array.isArray(value.generatedIssues)) {
563
+ return value;
564
+ }
565
+ const nested = findJsonLikeText(value);
566
+ if (nested && nested !== JSON.stringify(value)) {
567
+ try {
568
+ const parsedNested = JSON.parse(nested.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim() ?? nested);
569
+ return candidateAnalysisObject(parsedNested);
570
+ } catch {
571
+ return null;
572
+ }
573
+ }
574
+ return null;
575
+ }
576
+ function parseIssueAnalysisResult(raw) {
577
+ let parsed = raw;
578
+ if (typeof raw === "string") {
579
+ const trimmed = raw.trim();
580
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim();
581
+ try {
582
+ parsed = JSON.parse(fenced ?? trimmed);
583
+ } catch {
584
+ const lastJsonLine = trimmed.split(/\r?\n/).reverse().find((line) => line.trim().startsWith("{"));
585
+ parsed = lastJsonLine ? JSON.parse(lastJsonLine) : {};
586
+ }
587
+ }
588
+ const candidate = candidateAnalysisObject(parsed);
589
+ if (!candidate)
590
+ return {};
591
+ const result = {};
592
+ if (isRecord(candidate.metadataPatch))
593
+ result.metadataPatch = candidate.metadataPatch;
594
+ const add = stringArray(candidate.labelsToAdd);
595
+ if (add?.length)
596
+ result.labelsToAdd = add;
597
+ const remove = stringArray(candidate.labelsToRemove);
598
+ if (remove?.length)
599
+ result.labelsToRemove = remove;
600
+ const generated = generatedIssues(candidate.generatedIssues);
601
+ if (generated?.length)
602
+ result.generatedIssues = generated;
603
+ return result;
604
+ }
605
+ function createDefaultPiIssueAnalysisCommandRunner() {
606
+ return async (command, args, options) => {
607
+ const env = options.env ? { ...process.env, ...options.env } : process.env;
608
+ const proc = Bun.spawn([command, ...args], {
609
+ stdout: "pipe",
610
+ stderr: "pipe",
611
+ env
612
+ });
613
+ let timedOut = false;
614
+ const timer = setTimeout(() => {
615
+ timedOut = true;
616
+ proc.kill();
617
+ }, options.timeoutMs);
618
+ try {
619
+ const [stdout, stderr, exitCode] = await Promise.all([
620
+ new Response(proc.stdout).text(),
621
+ new Response(proc.stderr).text(),
622
+ proc.exited
623
+ ]);
624
+ return {
625
+ exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
626
+ stdout,
627
+ stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
628
+ };
629
+ } finally {
630
+ clearTimeout(timer);
631
+ }
632
+ };
633
+ }
634
+ function createPiIssueAnalyzer(input = {}) {
635
+ const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
636
+ const timeoutMs = Math.max(1000, Math.trunc(input.timeoutMs ?? Number(process.env.RIG_ISSUE_ANALYSIS_TIMEOUT_MS ?? 120000)));
637
+ const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
638
+ return async ({ prompt }) => {
639
+ const args = ["--print", "--mode", "json", "--no-session"];
640
+ const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
641
+ const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
642
+ if (provider)
643
+ args.push("--provider", provider);
644
+ if (model)
645
+ args.push("--model", model);
646
+ args.push(prompt);
647
+ const result = await runCommand(piBinary, args, { timeoutMs, ...input.env ? { env: input.env } : {} });
648
+ if (result.exitCode !== 0) {
649
+ throw new Error(`Pi issue analysis failed (exit ${result.exitCode}): ${result.stderr ?? result.stdout}`);
650
+ }
651
+ return parseIssueAnalysisResult(result.stdout);
652
+ };
653
+ }
654
+ function defaultStatusComment(input) {
655
+ const changes = [
656
+ input.result.metadataPatch ? "metadata" : null,
657
+ input.result.labelsToAdd?.length ? `labels added: ${input.result.labelsToAdd.join(", ")}` : null,
658
+ input.result.labelsToRemove?.length ? `labels removed: ${input.result.labelsToRemove.join(", ")}` : null,
659
+ input.result.generatedIssues?.length ? `generated issues: ${input.result.generatedIssues.length}` : null
660
+ ].filter((entry) => Boolean(entry));
661
+ if (changes.length === 0)
662
+ return null;
663
+ return [
664
+ "<!-- rig:status-comment -->",
665
+ "### Rig issue analysis",
666
+ "",
667
+ `Analyzed issue ${input.issue.id}${input.reason ? ` (${input.reason})` : ""}.`,
668
+ "",
669
+ ...changes.map((change) => `- ${change}`)
670
+ ].join(`
671
+ `);
672
+ }
673
+ function uniqueLabels(labels, required = []) {
674
+ return [...new Set([...labels ?? [], ...required].map((label) => label.trim()).filter(Boolean))];
675
+ }
676
+ function createIssueAnalysisWriteBack(input) {
677
+ return async ({ issue, result, reason }) => {
678
+ if (result.metadataPatch && Object.keys(result.metadataPatch).length > 0) {
679
+ if (!input.target.updateTask)
680
+ throw new Error("Issue analysis writeback requires updateTask for metadata patches.");
681
+ await input.target.updateTask(issue.id, { metadata: result.metadataPatch });
682
+ }
683
+ if (result.labelsToAdd?.length) {
684
+ if (!input.target.addLabels)
685
+ throw new Error("Issue analysis writeback requires addLabels for labelsToAdd.");
686
+ await input.target.addLabels(issue.id, uniqueLabels(result.labelsToAdd));
687
+ }
688
+ if (result.labelsToRemove?.length) {
689
+ if (!input.target.removeLabels)
690
+ throw new Error("Issue analysis writeback requires removeLabels for labelsToRemove.");
691
+ await input.target.removeLabels(issue.id, uniqueLabels(result.labelsToRemove));
692
+ }
693
+ const comment = (input.buildStatusComment ?? defaultStatusComment)({ issue, result, reason });
694
+ if (comment?.trim()) {
695
+ if (!input.target.updateTask)
696
+ throw new Error("Issue analysis writeback requires updateTask for sticky status comments.");
697
+ await input.target.updateTask(issue.id, { comment });
698
+ }
699
+ for (const generated of result.generatedIssues ?? []) {
700
+ if (!input.target.createIssue)
701
+ throw new Error("Issue analysis writeback requires createIssue for generated issues.");
702
+ await input.target.createIssue({
703
+ title: generated.title,
704
+ body: generated.dependsOn?.length ? `${generated.body.trimEnd()}
705
+
706
+ depends-on: ${generated.dependsOn.map((dep) => dep.startsWith("#") ? dep : `#${dep}`).join(", ")}
707
+ ` : generated.body,
708
+ labels: uniqueLabels(generated.labels, ["rig:generated"])
709
+ });
710
+ }
711
+ };
712
+ }
713
+ function createIssueAnalysisService(input) {
714
+ const analyzedHashes = new Map;
715
+ return {
716
+ async analyze(issues, options = {}) {
717
+ const results = [];
718
+ const neighbors = options.neighbors ?? issues;
719
+ for (const issue of issues) {
720
+ const hash = stableIssueHash(issue);
721
+ if (analyzedHashes.get(issue.id) === hash)
722
+ continue;
723
+ const prompt = renderIssueAnalysisPrompt({ issue, neighbors: neighbors.filter((candidate) => candidate.id !== issue.id) });
724
+ const result = await input.analyzer({ issue, neighbors, prompt });
725
+ analyzedHashes.set(issue.id, hash);
726
+ if (result.metadataPatch || result.labelsToAdd?.length || result.labelsToRemove?.length || result.generatedIssues?.length) {
727
+ await input.writeBack?.({ issue, result, reason: options.reason });
728
+ }
729
+ results.push({ issue, result });
730
+ }
731
+ return results;
732
+ },
733
+ clearCache() {
734
+ analyzedHashes.clear();
735
+ }
736
+ };
737
+ }
738
+
472
739
  // packages/server/src/server-helpers/terminal-runtime.ts
473
740
  import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
474
741
 
@@ -535,8 +802,9 @@ import {
535
802
  } from "@rig/runtime/control-plane/authority-files";
536
803
 
537
804
  // packages/server/src/server-helpers/github-auth-store.ts
538
- import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
539
- import { resolve as resolve7 } from "path";
805
+ import { randomBytes } from "crypto";
806
+ import { chmodSync, copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
807
+ import { dirname as dirname3, resolve as resolve7 } from "path";
540
808
  function cleanString(value) {
541
809
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
542
810
  }
@@ -548,6 +816,44 @@ function cleanScopes(value) {
548
816
  return clean ? [clean] : [];
549
817
  });
550
818
  }
819
+ function parseApiSessions(value) {
820
+ if (!Array.isArray(value))
821
+ return [];
822
+ return value.flatMap((entry) => {
823
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
824
+ return [];
825
+ const record = entry;
826
+ const token = cleanString(record.token);
827
+ if (!token)
828
+ return [];
829
+ return [{
830
+ token,
831
+ login: cleanString(record.login),
832
+ userId: cleanString(record.userId),
833
+ createdAt: cleanString(record.createdAt) ?? undefined
834
+ }];
835
+ });
836
+ }
837
+ function parsePendingDevice(value) {
838
+ if (!value || typeof value !== "object")
839
+ return null;
840
+ const record = value;
841
+ const pollId = cleanString(record.pollId);
842
+ const deviceCode = cleanString(record.deviceCode);
843
+ const expiresAt = cleanString(record.expiresAt);
844
+ const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
845
+ if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
846
+ return null;
847
+ return { pollId, deviceCode, expiresAt, intervalSeconds };
848
+ }
849
+ function parsePendingDevices(value) {
850
+ if (!Array.isArray(value))
851
+ return [];
852
+ return value.flatMap((entry) => {
853
+ const pending = parsePendingDevice(entry);
854
+ return pending ? [pending] : [];
855
+ });
856
+ }
551
857
  function readStoredAuth(stateFile) {
552
858
  if (!existsSync4(stateFile))
553
859
  return {};
@@ -561,37 +867,44 @@ function readStoredAuth(stateFile) {
561
867
  selectedRepo: cleanString(parsed.selectedRepo),
562
868
  tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
563
869
  pendingDevice: parsePendingDevice(parsed.pendingDevice),
870
+ pendingDevices: parsePendingDevices(parsed.pendingDevices),
871
+ apiSessions: parseApiSessions(parsed.apiSessions),
564
872
  updatedAt: cleanString(parsed.updatedAt) ?? undefined
565
873
  };
566
874
  } catch {
567
875
  return {};
568
876
  }
569
877
  }
570
- function parsePendingDevice(value) {
571
- if (!value || typeof value !== "object")
572
- return null;
573
- const record = value;
574
- const pollId = cleanString(record.pollId);
575
- const deviceCode = cleanString(record.deviceCode);
576
- const expiresAt = cleanString(record.expiresAt);
577
- const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
578
- if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
579
- return null;
580
- return { pollId, deviceCode, expiresAt, intervalSeconds };
878
+ function newApiSessionToken() {
879
+ return `rig_${randomBytes(32).toString("base64url")}`;
581
880
  }
582
881
  function writeStoredAuth(stateFile, payload) {
583
- mkdirSync3(resolve7(stateFile, ".."), { recursive: true });
882
+ mkdirSync3(dirname3(stateFile), { recursive: true });
584
883
  writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
585
884
  `, { encoding: "utf8", mode: 384 });
586
885
  try {
587
886
  chmodSync(stateFile, 384);
588
887
  } catch {}
589
888
  }
889
+ function localProjectAuthStateFile(projectRoot) {
890
+ return resolve7(projectRoot, ".rig", "state", "github-auth.json");
891
+ }
590
892
  function resolveGitHubAuthStateFile(projectRoot) {
591
893
  return resolve7(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
592
894
  }
593
- function createGitHubAuthStore(projectRoot) {
594
- const stateFile = resolveGitHubAuthStateFile(projectRoot);
895
+ function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
896
+ const targetFile = localProjectAuthStateFile(projectRoot);
897
+ mkdirSync3(dirname3(targetFile), { recursive: true });
898
+ if (existsSync4(stateFile)) {
899
+ copyFileSync(stateFile, targetFile);
900
+ try {
901
+ chmodSync(targetFile, 384);
902
+ } catch {}
903
+ return;
904
+ }
905
+ writeStoredAuth(targetFile, {});
906
+ }
907
+ function createGitHubAuthStoreFromStateFile(stateFile) {
595
908
  return {
596
909
  stateFile,
597
910
  status(options) {
@@ -621,14 +934,53 @@ function createGitHubAuthStore(projectRoot) {
621
934
  scopes: input.scopes ?? [],
622
935
  selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
623
936
  pendingDevice: null,
937
+ pendingDevices: [],
938
+ apiSessions: previous.apiSessions ?? [],
939
+ updatedAt: new Date().toISOString()
940
+ });
941
+ },
942
+ createApiSession() {
943
+ const previous = readStoredAuth(stateFile);
944
+ const token = newApiSessionToken();
945
+ const session = {
946
+ token,
947
+ login: cleanString(previous.login),
948
+ userId: cleanString(previous.userId),
949
+ createdAt: new Date().toISOString()
950
+ };
951
+ writeStoredAuth(stateFile, {
952
+ ...previous,
953
+ apiSessions: [...(previous.apiSessions ?? []).slice(-9), session],
624
954
  updatedAt: new Date().toISOString()
625
955
  });
956
+ return { token, login: session.login ?? null, userId: session.userId ?? null };
957
+ },
958
+ readApiSession(token) {
959
+ const clean = cleanString(token);
960
+ if (!clean)
961
+ return null;
962
+ const previous = readStoredAuth(stateFile);
963
+ const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
964
+ return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
965
+ },
966
+ copyToProjectRoot(projectRoot) {
967
+ const targetFile = resolveGitHubAuthStateFile(projectRoot);
968
+ writeStoredAuth(targetFile, readStoredAuth(stateFile));
969
+ },
970
+ copyToLocalProjectRoot(projectRoot) {
971
+ copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
626
972
  },
627
973
  savePendingDevice(input) {
628
974
  const previous = readStoredAuth(stateFile);
975
+ const pendingDevices = [
976
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
977
+ ...previous.pendingDevices ?? [],
978
+ input
979
+ ].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
629
980
  writeStoredAuth(stateFile, {
630
981
  ...previous,
631
- pendingDevice: input,
982
+ pendingDevice: null,
983
+ pendingDevices,
632
984
  updatedAt: new Date().toISOString()
633
985
  });
634
986
  },
@@ -641,23 +993,32 @@ function createGitHubAuthStore(projectRoot) {
641
993
  });
642
994
  },
643
995
  readPendingDevice(pollId) {
644
- const pending = readStoredAuth(stateFile).pendingDevice ?? null;
645
- if (!pending || pending.pollId !== pollId)
996
+ const previous = readStoredAuth(stateFile);
997
+ const pending = [
998
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
999
+ ...previous.pendingDevices ?? []
1000
+ ].find((entry) => entry.pollId === pollId) ?? null;
1001
+ if (!pending)
646
1002
  return null;
647
1003
  if (Date.parse(pending.expiresAt) <= Date.now())
648
1004
  return null;
649
1005
  return pending;
650
1006
  },
651
- clearPendingDevice() {
1007
+ clearPendingDevice(pollId) {
652
1008
  const previous = readStoredAuth(stateFile);
1009
+ const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
653
1010
  writeStoredAuth(stateFile, {
654
1011
  ...previous,
655
1012
  pendingDevice: null,
1013
+ pendingDevices: remaining,
656
1014
  updatedAt: new Date().toISOString()
657
1015
  });
658
1016
  }
659
1017
  };
660
1018
  }
1019
+ function createGitHubAuthStore(projectRoot) {
1020
+ return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
1021
+ }
661
1022
 
662
1023
  // packages/server/src/server-helpers/github-projects.ts
663
1024
  function asRecord(value) {
@@ -864,7 +1225,7 @@ var TERMINAL_RUN_STATUSES2 = new Set([
864
1225
  "needs-attention",
865
1226
  "stopped"
866
1227
  ]);
867
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
1228
+ var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
868
1229
 
869
1230
  // packages/server/src/server-helpers/ws-router.ts
870
1231
  import {
@@ -1046,7 +1407,7 @@ import {
1046
1407
  } from "@rig/runtime/control-plane/remote";
1047
1408
 
1048
1409
  // packages/server/src/server-helpers/run-steering.ts
1049
- import { dirname as dirname3, resolve as resolve8 } from "path";
1410
+ import { dirname as dirname4, resolve as resolve8 } from "path";
1050
1411
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "fs";
1051
1412
  import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun7, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
1052
1413
  var steeringSequence = 0;
@@ -1133,7 +1494,7 @@ function queueRunSteeringMessage(projectRoot, runId, input) {
1133
1494
  delivered: false
1134
1495
  };
1135
1496
  const path = runSteeringPath(projectRoot, runId);
1136
- mkdirSync4(dirname3(path), { recursive: true });
1497
+ mkdirSync4(dirname4(path), { recursive: true });
1137
1498
  appendJsonlRecord2(path, entry);
1138
1499
  appendRunTimelineEntry(projectRoot, runId, {
1139
1500
  id: entry.id,
@@ -1170,24 +1531,205 @@ import {
1170
1531
  updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
1171
1532
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
1172
1533
 
1534
+ // packages/server/src/server-helpers/github-api-session-index.ts
1535
+ import { chmodSync as chmodSync3, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
1536
+ import { dirname as dirname6, resolve as resolve10 } from "path";
1537
+
1538
+ // packages/server/src/server-helpers/github-user-namespace.ts
1539
+ import { chmodSync as chmodSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
1540
+ import { dirname as dirname5, isAbsolute, relative, resolve as resolve9 } from "path";
1541
+ function cleanString3(value) {
1542
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
1543
+ }
1544
+ function sanitizePathSegment(value) {
1545
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
1546
+ }
1547
+ function deriveGitHubUserNamespaceKey(identity) {
1548
+ const userId = cleanString3(identity.userId);
1549
+ if (userId) {
1550
+ const safeId = sanitizePathSegment(userId);
1551
+ if (safeId)
1552
+ return `ghu-${safeId}`;
1553
+ }
1554
+ const login = cleanString3(identity.login);
1555
+ if (login) {
1556
+ const safeLogin = sanitizePathSegment(login);
1557
+ if (safeLogin)
1558
+ return `ghu-login-${safeLogin}`;
1559
+ }
1560
+ throw new Error("GitHub user namespace requires a user id or login");
1561
+ }
1562
+ function resolveRemoteUserNamespacesRoot(projectRoot) {
1563
+ const explicitRoot = cleanString3(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
1564
+ if (explicitRoot)
1565
+ return resolve9(explicitRoot);
1566
+ const stateDir2 = cleanString3(process.env.RIG_STATE_DIR);
1567
+ if (stateDir2)
1568
+ return resolve9(dirname5(resolve9(stateDir2)), "users");
1569
+ return resolve9(projectRoot, ".rig", "users");
1570
+ }
1571
+ function resolveRemoteUserNamespace(projectRoot, identity) {
1572
+ const key = deriveGitHubUserNamespaceKey(identity);
1573
+ const root = resolve9(resolveRemoteUserNamespacesRoot(projectRoot), key);
1574
+ const stateDir2 = resolve9(root, ".rig", "state");
1575
+ return {
1576
+ key,
1577
+ userId: cleanString3(identity.userId),
1578
+ login: cleanString3(identity.login),
1579
+ root,
1580
+ stateDir: stateDir2,
1581
+ authStateFile: resolve9(stateDir2, "github-auth.json"),
1582
+ metadataFile: resolve9(stateDir2, "user-namespace.json"),
1583
+ checkoutBaseDir: resolve9(root, "remote-checkouts"),
1584
+ snapshotBaseDir: resolve9(root, "remote-snapshots")
1585
+ };
1586
+ }
1587
+ function serializeRemoteUserNamespace(namespace) {
1588
+ return {
1589
+ key: namespace.key,
1590
+ userId: namespace.userId,
1591
+ login: namespace.login,
1592
+ root: namespace.root,
1593
+ checkoutBaseDir: namespace.checkoutBaseDir,
1594
+ snapshotBaseDir: namespace.snapshotBaseDir
1595
+ };
1596
+ }
1597
+ function isPathInsideNamespace(namespaceRoot, candidatePath) {
1598
+ const root = resolve9(namespaceRoot);
1599
+ const candidate = resolve9(candidatePath);
1600
+ const rel = relative(root, candidate);
1601
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1602
+ }
1603
+ function writeRemoteUserNamespaceMetadata(namespace) {
1604
+ mkdirSync5(namespace.stateDir, { recursive: true });
1605
+ const previous = (() => {
1606
+ if (!existsSync6(namespace.metadataFile))
1607
+ return null;
1608
+ try {
1609
+ const parsed = JSON.parse(readFileSync4(namespace.metadataFile, "utf8"));
1610
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
1611
+ } catch {
1612
+ return null;
1613
+ }
1614
+ })();
1615
+ const now = new Date().toISOString();
1616
+ writeFileSync5(namespace.metadataFile, `${JSON.stringify({
1617
+ key: namespace.key,
1618
+ userId: namespace.userId,
1619
+ login: namespace.login,
1620
+ root: namespace.root,
1621
+ checkoutBaseDir: namespace.checkoutBaseDir,
1622
+ snapshotBaseDir: namespace.snapshotBaseDir,
1623
+ createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
1624
+ updatedAt: now
1625
+ }, null, 2)}
1626
+ `, { encoding: "utf8", mode: 384 });
1627
+ try {
1628
+ chmodSync2(namespace.metadataFile, 384);
1629
+ } catch {}
1630
+ }
1631
+
1632
+ // packages/server/src/server-helpers/github-api-session-index.ts
1633
+ function cleanString4(value) {
1634
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
1635
+ }
1636
+ function resolveGitHubApiSessionIndexFile(projectRoot) {
1637
+ return resolve10(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
1638
+ }
1639
+ function parseEntry(value) {
1640
+ if (!value || typeof value !== "object" || Array.isArray(value))
1641
+ return null;
1642
+ const record = value;
1643
+ const token = cleanString4(record.token);
1644
+ const namespaceKey = cleanString4(record.namespaceKey);
1645
+ const namespaceRoot = cleanString4(record.namespaceRoot);
1646
+ const authStateFile = cleanString4(record.authStateFile);
1647
+ const checkoutBaseDir = cleanString4(record.checkoutBaseDir);
1648
+ const snapshotBaseDir = cleanString4(record.snapshotBaseDir);
1649
+ const createdAt = cleanString4(record.createdAt);
1650
+ if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
1651
+ return null;
1652
+ return {
1653
+ token,
1654
+ namespaceKey,
1655
+ namespaceRoot,
1656
+ authStateFile,
1657
+ checkoutBaseDir,
1658
+ snapshotBaseDir,
1659
+ createdAt,
1660
+ login: cleanString4(record.login),
1661
+ userId: cleanString4(record.userId),
1662
+ selectedRepo: cleanString4(record.selectedRepo)
1663
+ };
1664
+ }
1665
+ function readIndex(indexFile) {
1666
+ if (!existsSync7(indexFile))
1667
+ return [];
1668
+ try {
1669
+ const parsed = JSON.parse(readFileSync5(indexFile, "utf8"));
1670
+ return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
1671
+ const parsedEntry = parseEntry(entry);
1672
+ return parsedEntry ? [parsedEntry] : [];
1673
+ }) : [];
1674
+ } catch {
1675
+ return [];
1676
+ }
1677
+ }
1678
+ function writeIndex(indexFile, sessions) {
1679
+ mkdirSync6(dirname6(indexFile), { recursive: true });
1680
+ writeFileSync6(indexFile, `${JSON.stringify({ sessions }, null, 2)}
1681
+ `, { encoding: "utf8", mode: 384 });
1682
+ try {
1683
+ chmodSync3(indexFile, 384);
1684
+ } catch {}
1685
+ }
1686
+ function registerGitHubApiSession(input) {
1687
+ const cleanToken = cleanString4(input.token);
1688
+ if (!cleanToken)
1689
+ throw new Error("GitHub API session token is required");
1690
+ const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
1691
+ const createdAt = new Date().toISOString();
1692
+ const entry = {
1693
+ token: cleanToken,
1694
+ login: input.namespace.login,
1695
+ userId: input.namespace.userId,
1696
+ namespaceKey: input.namespace.key,
1697
+ namespaceRoot: input.namespace.root,
1698
+ authStateFile: input.namespace.authStateFile,
1699
+ checkoutBaseDir: input.namespace.checkoutBaseDir,
1700
+ snapshotBaseDir: input.namespace.snapshotBaseDir,
1701
+ selectedRepo: cleanString4(input.selectedRepo),
1702
+ createdAt
1703
+ };
1704
+ const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
1705
+ writeIndex(indexFile, [...previous.slice(-199), entry]);
1706
+ return entry;
1707
+ }
1708
+ function readGitHubApiSession(input) {
1709
+ const cleanToken = cleanString4(input.token);
1710
+ if (!cleanToken)
1711
+ return null;
1712
+ return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
1713
+ }
1714
+
1173
1715
  // packages/server/src/server-helpers/project-registry.ts
1174
- import { createHash } from "crypto";
1716
+ import { createHash as createHash2 } from "crypto";
1175
1717
  import { spawnSync } from "child_process";
1176
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync5 } from "fs";
1177
- import { dirname as dirname4, resolve as resolve9 } from "path";
1718
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync7 } from "fs";
1719
+ import { dirname as dirname7, resolve as resolve11 } from "path";
1178
1720
  function normalizeRepoSlug(value) {
1179
1721
  const trimmed = value.trim();
1180
1722
  return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
1181
1723
  }
1182
1724
  function registryPath(projectRoot) {
1183
- return resolve9(projectRoot, ".rig", "state", "projects.json");
1725
+ return resolve11(projectRoot, ".rig", "state", "projects.json");
1184
1726
  }
1185
1727
  function readRegistry(projectRoot) {
1186
1728
  const path = registryPath(projectRoot);
1187
- if (!existsSync6(path))
1729
+ if (!existsSync8(path))
1188
1730
  return {};
1189
1731
  try {
1190
- const payload = JSON.parse(readFileSync4(path, "utf8"));
1732
+ const payload = JSON.parse(readFileSync6(path, "utf8"));
1191
1733
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
1192
1734
  return {};
1193
1735
  const projects = payload.projects;
@@ -1198,14 +1740,14 @@ function readRegistry(projectRoot) {
1198
1740
  }
1199
1741
  function writeRegistry(projectRoot, projects) {
1200
1742
  const path = registryPath(projectRoot);
1201
- mkdirSync5(dirname4(path), { recursive: true });
1202
- writeFileSync5(path, `${JSON.stringify({ projects }, null, 2)}
1743
+ mkdirSync7(dirname7(path), { recursive: true });
1744
+ writeFileSync7(path, `${JSON.stringify({ projects }, null, 2)}
1203
1745
  `, "utf8");
1204
1746
  }
1205
1747
  function resolveConfigPath(projectRoot) {
1206
1748
  for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
1207
- const path = resolve9(projectRoot, name);
1208
- if (existsSync6(path))
1749
+ const path = resolve11(projectRoot, name);
1750
+ if (existsSync8(path))
1209
1751
  return path;
1210
1752
  }
1211
1753
  return null;
@@ -1214,7 +1756,7 @@ function hashFile(path) {
1214
1756
  if (!path)
1215
1757
  return null;
1216
1758
  try {
1217
- return createHash("sha256").update(readFileSync4(path)).digest("hex");
1759
+ return createHash2("sha256").update(readFileSync6(path)).digest("hex");
1218
1760
  } catch {
1219
1761
  return null;
1220
1762
  }
@@ -1230,11 +1772,11 @@ function readDefaultBranch(projectRoot) {
1230
1772
  return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
1231
1773
  }
1232
1774
  function buildRunSummary(projectRoot) {
1233
- const runsDir = resolve9(projectRoot, ".rig", "runs");
1775
+ const runsDir = resolve11(projectRoot, ".rig", "runs");
1234
1776
  try {
1235
1777
  const runs = readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
1236
1778
  try {
1237
- const run = JSON.parse(readFileSync4(resolve9(runsDir, entry.name, "run.json"), "utf8"));
1779
+ const run = JSON.parse(readFileSync6(resolve11(runsDir, entry.name, "run.json"), "utf8"));
1238
1780
  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 : "" }];
1239
1781
  } catch {
1240
1782
  return [];
@@ -1286,10 +1828,14 @@ function upsertProjectRecord(projectRoot, input) {
1286
1828
  function linkProjectCheckout(projectRoot, repoSlug, checkout) {
1287
1829
  return upsertProjectRecord(projectRoot, { repoSlug, checkout });
1288
1830
  }
1831
+ function projectRegistryContainsCheckout(projectRoot, checkoutPath) {
1832
+ const target = resolve11(checkoutPath);
1833
+ return Object.values(readRegistry(projectRoot)).some((project) => project.checkouts.some((checkout) => checkout.path ? resolve11(checkout.path) === target : false));
1834
+ }
1289
1835
 
1290
1836
  // packages/server/src/server-helpers/remote-checkout.ts
1291
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
1292
- import { dirname as dirname5, isAbsolute, relative, resolve as resolve10 } from "path";
1837
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
1838
+ import { dirname as dirname8, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve12 } from "path";
1293
1839
  function safeSlugSegments(repoSlug) {
1294
1840
  const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
1295
1841
  if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
@@ -1306,7 +1852,7 @@ function safeCheckoutKey(value) {
1306
1852
  }
1307
1853
  function repoSlugPath(baseDir, repoSlug, checkoutKey) {
1308
1854
  const key = safeCheckoutKey(checkoutKey);
1309
- return resolve10(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
1855
+ return resolve12(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
1310
1856
  }
1311
1857
  function sanitizeSnapshotId(value, fallback) {
1312
1858
  const raw = (value ?? fallback).trim();
@@ -1314,7 +1860,7 @@ function sanitizeSnapshotId(value, fallback) {
1314
1860
  return safe || fallback;
1315
1861
  }
1316
1862
  function assertWithinRoot(root, relativePath) {
1317
- if (!relativePath || isAbsolute(relativePath) || relativePath.includes("\x00")) {
1863
+ if (!relativePath || isAbsolute2(relativePath) || relativePath.includes("\x00")) {
1318
1864
  throw new Error(`Invalid snapshot file path: ${relativePath}`);
1319
1865
  }
1320
1866
  const normalizedRelative = relativePath.replace(/\\/g, "/");
@@ -1322,9 +1868,9 @@ function assertWithinRoot(root, relativePath) {
1322
1868
  if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
1323
1869
  throw new Error(`Unsafe snapshot file path: ${relativePath}`);
1324
1870
  }
1325
- const target = resolve10(root, ...segments);
1326
- const rel = relative(root, target);
1327
- if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute(rel)) {
1871
+ const target = resolve12(root, ...segments);
1872
+ const rel = relative2(root, target);
1873
+ if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute2(rel)) {
1328
1874
  throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
1329
1875
  }
1330
1876
  return target;
@@ -1342,17 +1888,17 @@ function parseSnapshotArchiveContentBase64(contentBase64) {
1342
1888
  function extractUploadedSnapshotArchive(input) {
1343
1889
  const archive = decodeSnapshotArchive(input.archive);
1344
1890
  const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
1345
- const checkoutPath = resolve10(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
1346
- mkdirSync6(checkoutPath, { recursive: true });
1891
+ const checkoutPath = resolve12(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
1892
+ mkdirSync8(checkoutPath, { recursive: true });
1347
1893
  for (const file of archive.files) {
1348
1894
  if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
1349
1895
  throw new Error("Invalid snapshot archive file entry");
1350
1896
  }
1351
1897
  const target = assertWithinRoot(checkoutPath, file.path);
1352
- mkdirSync6(dirname5(target), { recursive: true });
1353
- writeFileSync6(target, Buffer.from(file.contentBase64, "base64"));
1898
+ mkdirSync8(dirname8(target), { recursive: true });
1899
+ writeFileSync8(target, Buffer.from(file.contentBase64, "base64"));
1354
1900
  }
1355
- writeFileSync6(resolve10(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
1901
+ writeFileSync8(resolve12(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
1356
1902
  repoSlug: input.repoSlug,
1357
1903
  snapshotId,
1358
1904
  fileCount: archive.files.length,
@@ -1387,7 +1933,7 @@ function gitCredentialConfig(token) {
1387
1933
  };
1388
1934
  }
1389
1935
  async function prepareRemoteCheckout(input) {
1390
- const exists = input.exists ?? existsSync7;
1936
+ const exists = input.exists ?? existsSync9;
1391
1937
  const strategy = input.strategy;
1392
1938
  if (strategy.kind === "uploaded-snapshot") {
1393
1939
  return extractUploadedSnapshotArchive({
@@ -1399,7 +1945,7 @@ async function prepareRemoteCheckout(input) {
1399
1945
  });
1400
1946
  }
1401
1947
  if (strategy.kind === "existing-path") {
1402
- const checkoutPath2 = resolve10(strategy.path);
1948
+ const checkoutPath2 = resolve12(strategy.path);
1403
1949
  if (!exists(checkoutPath2)) {
1404
1950
  throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
1405
1951
  }
@@ -1435,9 +1981,9 @@ function buildServerControlStatus() {
1435
1981
  };
1436
1982
  }
1437
1983
  function buildProjectConfigStatus(root) {
1438
- const hasConfigTs = existsSync8(resolve11(root, "rig.config.ts"));
1439
- const hasConfigJson = existsSync8(resolve11(root, "rig.config.json"));
1440
- const hasLegacyTaskConfig = existsSync8(resolve11(root, ".rig", "task-config.json"));
1984
+ const hasConfigTs = existsSync10(resolve13(root, "rig.config.ts"));
1985
+ const hasConfigJson = existsSync10(resolve13(root, "rig.config.json"));
1986
+ const hasLegacyTaskConfig = existsSync10(resolve13(root, ".rig", "task-config.json"));
1441
1987
  let kind = "missing";
1442
1988
  if (hasConfigTs)
1443
1989
  kind = "rig-config-ts";
@@ -1461,10 +2007,10 @@ function normalizeCommit(value) {
1461
2007
  function asPlainRecord(value) {
1462
2008
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1463
2009
  }
1464
- var RIG_CONFIG_PACKAGE_VERSION = "0.0.6-alpha.1";
2010
+ var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
1465
2011
  var RIG_CONFIG_DEV_DEPENDENCIES = {
1466
- "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_VERSION}`,
1467
- "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_VERSION}`
2012
+ "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
2013
+ "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
1468
2014
  };
1469
2015
  function repoParts(repoSlug) {
1470
2016
  const [owner, repo] = repoSlug.split("/");
@@ -1473,24 +2019,24 @@ function repoParts(repoSlug) {
1473
2019
  return { owner, repo, slug: `${owner}/${repo}` };
1474
2020
  }
1475
2021
  function repairDir(checkoutPath) {
1476
- const dir = resolve11(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
1477
- mkdirSync7(dir, { recursive: true });
2022
+ const dir = resolve13(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
2023
+ mkdirSync9(dir, { recursive: true });
1478
2024
  return dir;
1479
2025
  }
1480
2026
  function backupCheckoutFile(checkoutPath, relativePath) {
1481
- const source = resolve11(checkoutPath, relativePath);
1482
- const backupPath = resolve11(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
1483
- mkdirSync7(dirname6(backupPath), { recursive: true });
1484
- copyFileSync(source, backupPath);
2027
+ const source = resolve13(checkoutPath, relativePath);
2028
+ const backupPath = resolve13(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
2029
+ mkdirSync9(dirname9(backupPath), { recursive: true });
2030
+ copyFileSync2(source, backupPath);
1485
2031
  return backupPath;
1486
2032
  }
1487
2033
  function parsePackageJsonLosslessly(checkoutPath) {
1488
- const packagePath = resolve11(checkoutPath, "package.json");
1489
- if (!existsSync8(packagePath)) {
2034
+ const packagePath = resolve13(checkoutPath, "package.json");
2035
+ if (!existsSync10(packagePath)) {
1490
2036
  return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
1491
2037
  }
1492
2038
  try {
1493
- const parsed = JSON.parse(readFileSync5(packagePath, "utf8"));
2039
+ const parsed = JSON.parse(readFileSync7(packagePath, "utf8"));
1494
2040
  return {
1495
2041
  existed: true,
1496
2042
  packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
@@ -1504,9 +2050,9 @@ function parsePackageJsonLosslessly(checkoutPath) {
1504
2050
  }
1505
2051
  }
1506
2052
  function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
1507
- const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync8(resolve11(checkoutPath, name)));
1508
- const packagePath = resolve11(checkoutPath, "package.json");
1509
- if (!hasConfig && !existsSync8(packagePath)) {
2053
+ const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync10(resolve13(checkoutPath, name)));
2054
+ const packagePath = resolve13(checkoutPath, "package.json");
2055
+ if (!hasConfig && !existsSync10(packagePath)) {
1510
2056
  return { skipped: true, reason: "package.json and rig.config missing" };
1511
2057
  }
1512
2058
  const parsed = parsePackageJsonLosslessly(checkoutPath);
@@ -1525,7 +2071,7 @@ function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
1525
2071
  }
1526
2072
  const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
1527
2073
  if (changed) {
1528
- writeFileSync7(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
2074
+ writeFileSync9(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
1529
2075
  `, "utf8");
1530
2076
  }
1531
2077
  return {
@@ -1551,11 +2097,11 @@ function configLooksStructurallyUsable(source) {
1551
2097
  return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
1552
2098
  }
1553
2099
  function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
1554
- const configPath = resolve11(checkoutPath, "rig.config.ts");
1555
- const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync8(resolve11(checkoutPath, name)));
2100
+ const configPath = resolve13(checkoutPath, "rig.config.ts");
2101
+ const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync10(resolve13(checkoutPath, name)));
1556
2102
  if (existingConfigName) {
1557
- const existingPath = resolve11(checkoutPath, existingConfigName);
1558
- const source = readFileSync5(existingPath, "utf8");
2103
+ const existingPath = resolve13(checkoutPath, existingConfigName);
2104
+ const source = readFileSync7(existingPath, "utf8");
1559
2105
  if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
1560
2106
  return { path: existingPath, changed: false, reason: "config structurally complete" };
1561
2107
  }
@@ -1569,7 +2115,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
1569
2115
  }
1570
2116
  }
1571
2117
  const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
1572
- writeFileSync7(configPath, generatedRigConfigSource(repoSlug), "utf8");
2118
+ writeFileSync9(configPath, generatedRigConfigSource(repoSlug), "utf8");
1573
2119
  return {
1574
2120
  path: configPath,
1575
2121
  changed: true,
@@ -1578,7 +2124,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
1578
2124
  };
1579
2125
  }
1580
2126
  function validateRemoteCheckoutRigConfig(checkoutPath) {
1581
- const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync8(resolve11(checkoutPath, name)));
2127
+ const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync10(resolve13(checkoutPath, name)));
1582
2128
  if (!configFile)
1583
2129
  return { ok: false, error: "missing rig config" };
1584
2130
  if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
@@ -1600,7 +2146,7 @@ function validateRemoteCheckoutRigConfig(checkoutPath) {
1600
2146
  return { ok: true, configFile };
1601
2147
  }
1602
2148
  function installRemoteCheckoutPackages(checkoutPath) {
1603
- if (!existsSync8(resolve11(checkoutPath, "package.json"))) {
2149
+ if (!existsSync10(resolve13(checkoutPath, "package.json"))) {
1604
2150
  return { skipped: true, reason: "package.json missing" };
1605
2151
  }
1606
2152
  if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
@@ -1613,8 +2159,8 @@ function installRemoteCheckoutPackages(checkoutPath) {
1613
2159
  return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
1614
2160
  }
1615
2161
  function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
1616
- const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync8(resolve11(checkoutPath, name)));
1617
- const hasPackage = existsSync8(resolve11(checkoutPath, "package.json"));
2162
+ const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync10(resolve13(checkoutPath, name)));
2163
+ const hasPackage = existsSync10(resolve13(checkoutPath, "package.json"));
1618
2164
  if (!hasConfig && !hasPackage) {
1619
2165
  return {
1620
2166
  packageJson: { skipped: true, reason: "package.json and rig.config missing" },
@@ -1712,26 +2258,26 @@ function buildRemoteRunLogEntry(body, identifiers) {
1712
2258
  }
1713
2259
  function readGitHeadCommit(projectRoot) {
1714
2260
  try {
1715
- let gitDir = resolve11(projectRoot, ".git");
2261
+ let gitDir = resolve13(projectRoot, ".git");
1716
2262
  try {
1717
- const dotGit = readFileSync5(gitDir, "utf8").trim();
2263
+ const dotGit = readFileSync7(gitDir, "utf8").trim();
1718
2264
  const gitDirPrefix = "gitdir:";
1719
2265
  if (dotGit.startsWith(gitDirPrefix)) {
1720
- gitDir = resolve11(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
2266
+ gitDir = resolve13(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
1721
2267
  }
1722
2268
  } catch {}
1723
- const head = readFileSync5(resolve11(gitDir, "HEAD"), "utf8").trim();
2269
+ const head = readFileSync7(resolve13(gitDir, "HEAD"), "utf8").trim();
1724
2270
  const refPrefix = "ref:";
1725
2271
  if (!head.startsWith(refPrefix)) {
1726
2272
  return normalizeCommit(head);
1727
2273
  }
1728
2274
  const ref = head.slice(refPrefix.length).trim();
1729
- const refPath = resolve11(gitDir, ref);
1730
- if (existsSync8(refPath)) {
1731
- return normalizeCommit(readFileSync5(refPath, "utf8").trim());
2275
+ const refPath = resolve13(gitDir, ref);
2276
+ if (existsSync10(refPath)) {
2277
+ return normalizeCommit(readFileSync7(refPath, "utf8").trim());
1732
2278
  }
1733
- const commonDir = normalizeString(readFileSync5(resolve11(gitDir, "commondir"), "utf8"));
1734
- return commonDir ? normalizeCommit(readFileSync5(resolve11(gitDir, commonDir, ref), "utf8").trim()) : null;
2279
+ const commonDir = normalizeString(readFileSync7(resolve13(gitDir, "commondir"), "utf8"));
2280
+ return commonDir ? normalizeCommit(readFileSync7(resolve13(gitDir, commonDir, ref), "utf8").trim()) : null;
1735
2281
  } catch {
1736
2282
  return null;
1737
2283
  }
@@ -1769,9 +2315,9 @@ function configuredRepoFromTaskSource(taskSource) {
1769
2315
  const repo = normalizeString(taskSource?.repo);
1770
2316
  return owner && repo ? `${owner}/${repo}` : null;
1771
2317
  }
1772
- async function buildTaskSourceStatus(state, config) {
2318
+ async function buildTaskSourceStatus(state, config, requestAuth) {
1773
2319
  const diagnostics = state.snapshotService.getTaskSourceErrors();
1774
- const selectedRepo = createGitHubAuthStore(state.projectRoot).status({
2320
+ const selectedRepo = requestScopedAuthStore(state.projectRoot, requestAuth).status({
1775
2321
  oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
1776
2322
  }).selectedRepo;
1777
2323
  try {
@@ -1818,36 +2364,134 @@ function bearerTokenFromRequest(req) {
1818
2364
  function isLoopbackRequest(req) {
1819
2365
  try {
1820
2366
  const hostname = new URL(req.url).hostname.toLowerCase();
1821
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
2367
+ return hostname === "localhost" || hostname === "rig.local" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1822
2368
  } catch {
1823
2369
  return false;
1824
2370
  }
1825
2371
  }
1826
2372
  function isPublicRigAuthBootstrapRoute(pathname) {
1827
- return pathname === "/" || pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/github/auth/status" || pathname === "/api/github/auth/token" || pathname === "/api/github/auth/device/start" || pathname === "/api/github/auth/device/poll";
2373
+ 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";
2374
+ }
2375
+ function buildRigInstallScript() {
2376
+ return `#!/usr/bin/env bash
2377
+ set -euo pipefail
2378
+
2379
+ say() {
2380
+ printf 'rig-install: %s
2381
+ ' "$*"
2382
+ }
2383
+
2384
+ if ! command -v bun >/dev/null 2>&1; then
2385
+ say "Bun not found; installing Bun first"
2386
+ curl -fsSL https://bun.sh/install | bash
2387
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
2388
+ export PATH="$BUN_INSTALL/bin:$PATH"
2389
+ fi
2390
+
2391
+ if ! command -v bun >/dev/null 2>&1; then
2392
+ printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
2393
+ ' >&2
2394
+ exit 1
2395
+ fi
2396
+
2397
+ say "Installing @h-rig/cli@latest"
2398
+ bun add -g @h-rig/cli@latest
2399
+
2400
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
2401
+ export PATH="$BUN_INSTALL/bin:$PATH"
2402
+
2403
+ if ! command -v rig >/dev/null 2>&1; then
2404
+ printf 'rig-install: rig installed, but rig is not on PATH. Add %s/bin to PATH and retry.
2405
+ ' "$BUN_INSTALL" >&2
2406
+ exit 1
2407
+ fi
2408
+
2409
+ say "Verifying rig"
2410
+ rig --help >/dev/null
2411
+ say "Done. Run: rig --help"
2412
+ `;
1828
2413
  }
1829
2414
  function normalizePrMode(value) {
1830
2415
  const mode = normalizeString(value);
1831
2416
  return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
1832
2417
  }
2418
+ function requestAuthResult(input) {
2419
+ return {
2420
+ authorized: input.authorized,
2421
+ actor: input.actor ?? null,
2422
+ reason: input.reason,
2423
+ login: input.login ?? null,
2424
+ userId: input.userId ?? null,
2425
+ userNamespace: input.userNamespace ?? null,
2426
+ authStateFile: input.authStateFile ?? null
2427
+ };
2428
+ }
2429
+ function namespaceFromSessionIndex(entry) {
2430
+ const stateDir2 = dirname9(entry.authStateFile);
2431
+ return {
2432
+ key: entry.namespaceKey,
2433
+ userId: entry.userId,
2434
+ login: entry.login,
2435
+ root: entry.namespaceRoot,
2436
+ stateDir: stateDir2,
2437
+ authStateFile: entry.authStateFile,
2438
+ metadataFile: resolve13(stateDir2, "user-namespace.json"),
2439
+ checkoutBaseDir: entry.checkoutBaseDir,
2440
+ snapshotBaseDir: entry.snapshotBaseDir
2441
+ };
2442
+ }
1833
2443
  function authorizeRigHttpRequest(input) {
1834
2444
  if (input.legacyAuthorized) {
1835
- return { authorized: true, actor: "rig-local-server", reason: "server-token" };
2445
+ return requestAuthResult({ authorized: true, actor: "rig-local-server", reason: "server-token" });
1836
2446
  }
1837
2447
  const bearer = bearerTokenFromRequest(input.req);
1838
2448
  const store = createGitHubAuthStore(input.projectRoot);
1839
2449
  const storedToken = store.readToken();
2450
+ const session = bearer ? store.readApiSession(bearer) : null;
2451
+ if (session) {
2452
+ return requestAuthResult({
2453
+ authorized: true,
2454
+ actor: session.login ?? "github-operator",
2455
+ reason: "github-session",
2456
+ login: session.login,
2457
+ userId: session.userId,
2458
+ authStateFile: store.stateFile
2459
+ });
2460
+ }
1840
2461
  if (bearer && storedToken && bearer === storedToken) {
1841
2462
  const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
1842
- return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
2463
+ return requestAuthResult({
2464
+ authorized: true,
2465
+ actor: status.login ?? "github-operator",
2466
+ reason: "github-token",
2467
+ login: status.login,
2468
+ userId: status.userId,
2469
+ authStateFile: store.stateFile
2470
+ });
2471
+ }
2472
+ const indexedSession = readGitHubApiSession({ projectRoot: input.projectRoot, token: bearer });
2473
+ if (indexedSession) {
2474
+ const userNamespace = namespaceFromSessionIndex(indexedSession);
2475
+ return requestAuthResult({
2476
+ authorized: true,
2477
+ actor: indexedSession.login ?? "github-operator",
2478
+ reason: "github-user-session",
2479
+ login: indexedSession.login,
2480
+ userId: indexedSession.userId,
2481
+ userNamespace,
2482
+ authStateFile: indexedSession.authStateFile
2483
+ });
1843
2484
  }
1844
2485
  if (isPublicRigAuthBootstrapRoute(input.pathname)) {
1845
- return { authorized: true, actor: null, reason: "public-bootstrap" };
2486
+ return requestAuthResult({ authorized: true, actor: null, reason: "public-bootstrap" });
1846
2487
  }
1847
- if (!input.serverAuthToken && !storedToken && isLoopbackRequest(input.req)) {
1848
- return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
2488
+ if (!input.serverAuthToken && !storedToken) {
2489
+ if (isLoopbackRequest(input.req)) {
2490
+ return requestAuthResult({ authorized: true, actor: null, reason: "loopback-dev-no-auth" });
2491
+ }
2492
+ return requestAuthResult({ authorized: false, actor: null, reason: "auth-required" });
1849
2493
  }
1850
- return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
2494
+ return requestAuthResult({ authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" });
1851
2495
  }
1852
2496
  async function fetchGitHubUserInfo(token) {
1853
2497
  const response = await fetch("https://api.github.com/user", {
@@ -1867,6 +2511,67 @@ async function fetchGitHubUserInfo(token) {
1867
2511
  scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
1868
2512
  };
1869
2513
  }
2514
+ function shouldWriteRootAuthCompat(projectRoot) {
2515
+ if (process.env.RIG_REMOTE_USER_NAMESPACE_ROOT?.trim())
2516
+ return false;
2517
+ const stateDir2 = normalizeString(process.env.RIG_STATE_DIR);
2518
+ if (!stateDir2)
2519
+ return true;
2520
+ return resolve13(stateDir2) === resolve13(projectRoot, ".rig", "state");
2521
+ }
2522
+ function requestScopedRegistryRoot(stateProjectRoot, requestAuth) {
2523
+ return requestAuth.userNamespace?.root ?? stateProjectRoot;
2524
+ }
2525
+ function requestScopedAuthStore(stateProjectRoot, requestAuth) {
2526
+ return requestAuth.authStateFile ? createGitHubAuthStoreFromStateFile(requestAuth.authStateFile) : createGitHubAuthStore(stateProjectRoot);
2527
+ }
2528
+ function userNamespaceResponse(namespace) {
2529
+ return namespace ? serializeRemoteUserNamespace(namespace) : undefined;
2530
+ }
2531
+ function resolveNamespacedBaseDir(input) {
2532
+ if (input.explicitBaseDir)
2533
+ return input.explicitBaseDir;
2534
+ const envBase = normalizeString(process.env[input.envName]);
2535
+ if (input.userNamespace) {
2536
+ return envBase ? resolve13(envBase, input.userNamespace.key) : input.userNamespace[input.legacySubdir === "remote-checkouts" ? "checkoutBaseDir" : "snapshotBaseDir"];
2537
+ }
2538
+ return envBase ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve13(normalizeString(process.env.RIG_STATE_DIR), input.legacySubdir) : resolve13(input.legacyProjectRoot, ".rig", input.legacySubdir));
2539
+ }
2540
+ function explicitCheckoutKey(body, checkoutInput, requestAuth) {
2541
+ return normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? (requestAuth.userNamespace ? undefined : "default");
2542
+ }
2543
+ function saveGitHubTokenForRemoteUser(input) {
2544
+ const namespace = resolveRemoteUserNamespace(input.projectRoot, { userId: input.user.userId, login: input.user.login });
2545
+ writeRemoteUserNamespaceMetadata(namespace);
2546
+ const store = createGitHubAuthStoreFromStateFile(namespace.authStateFile);
2547
+ store.saveToken({
2548
+ token: input.token,
2549
+ tokenSource: input.tokenSource,
2550
+ login: input.user.login,
2551
+ userId: input.user.userId,
2552
+ scopes: input.user.scopes,
2553
+ selectedRepo: input.selectedRepo
2554
+ });
2555
+ const apiSession = store.createApiSession();
2556
+ registerGitHubApiSession({ projectRoot: input.projectRoot, token: apiSession.token, namespace, selectedRepo: input.selectedRepo });
2557
+ const requestedRoot = normalizeString(input.requestedProjectRoot);
2558
+ if (requestedRoot && isAbsolute3(requestedRoot) && existsSync10(resolve13(requestedRoot))) {
2559
+ copyGitHubAuthStateToLocalProjectRoot(namespace.authStateFile, resolve13(requestedRoot));
2560
+ }
2561
+ if (shouldWriteRootAuthCompat(input.projectRoot)) {
2562
+ const rootStore = createGitHubAuthStore(input.projectRoot);
2563
+ rootStore.saveToken({
2564
+ token: input.token,
2565
+ tokenSource: input.tokenSource,
2566
+ login: input.user.login,
2567
+ userId: input.user.userId,
2568
+ scopes: input.user.scopes,
2569
+ selectedRepo: input.selectedRepo
2570
+ });
2571
+ rootStore.createApiSession();
2572
+ }
2573
+ return { store, namespace, apiSessionToken: apiSession.token };
2574
+ }
1870
2575
  async function postGitHubForm(endpoint, body) {
1871
2576
  const response = await fetch(endpoint, {
1872
2577
  method: "POST",
@@ -1884,11 +2589,11 @@ function resolveRequestedProjectRoot(currentRoot, rawRoot) {
1884
2589
  const requestedRoot = normalizeString(rawRoot);
1885
2590
  if (!requestedRoot)
1886
2591
  return currentRoot;
1887
- if (!isAbsolute2(requestedRoot)) {
2592
+ if (!isAbsolute3(requestedRoot)) {
1888
2593
  throw new Error("projectRoot must be an absolute path on the Rig server host");
1889
2594
  }
1890
- const normalizedRoot = resolve11(requestedRoot);
1891
- if (!existsSync8(normalizedRoot)) {
2595
+ const normalizedRoot = resolve13(requestedRoot);
2596
+ if (!existsSync10(normalizedRoot)) {
1892
2597
  throw new Error("projectRoot does not exist on the Rig server host");
1893
2598
  }
1894
2599
  return normalizedRoot;
@@ -2076,7 +2781,7 @@ function selectNextWorkspaceTask(projectRoot, tasks) {
2076
2781
  if (runnable.length === 0)
2077
2782
  return null;
2078
2783
  const queue = readQueueState(projectRoot);
2079
- const queueRank = new Map(queue.map((entry, index) => [entry.taskId, { score: entry.score, position: index }]));
2784
+ const queueRank = new Map(queue.map((entry, index) => [String(entry.taskId), { score: entry.score, position: index }]));
2080
2785
  return runnable.toSorted((left, right) => {
2081
2786
  const leftId = taskIdOf(left) ?? "";
2082
2787
  const rightId = taskIdOf(right) ?? "";
@@ -2116,6 +2821,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
2116
2821
  }
2117
2822
  return filtered;
2118
2823
  }
2824
+ function issueAnalysisTargetFor(source) {
2825
+ if (!source)
2826
+ return null;
2827
+ const candidate = source;
2828
+ if (typeof candidate.updateTask !== "function")
2829
+ return null;
2830
+ return {
2831
+ ...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
2832
+ updateTask: candidate.updateTask.bind(candidate),
2833
+ ...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
2834
+ ...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
2835
+ ...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
2836
+ };
2837
+ }
2838
+ function uniqueStringList(value) {
2839
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
2840
+ return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
2841
+ }
2842
+ function taskRecordId(task) {
2843
+ return String(task.id ?? "");
2844
+ }
2119
2845
  function redactRemoteEndpoint(endpoint) {
2120
2846
  const { token, ...rest } = endpoint;
2121
2847
  return {
@@ -2200,9 +2926,16 @@ function createRigServerFetch(state, deps) {
2200
2926
  notifications: state.targets.length
2201
2927
  });
2202
2928
  }
2929
+ if (url.pathname === "/install" && req.method === "GET") {
2930
+ return new Response(buildRigInstallScript(), {
2931
+ headers: {
2932
+ "Content-Type": "text/x-shellscript; charset=utf-8"
2933
+ }
2934
+ });
2935
+ }
2203
2936
  const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
2204
2937
  const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
2205
- const legacyAuthorizedHttpRequest = isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken);
2938
+ const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
2206
2939
  const requestAuth = authorizeRigHttpRequest({
2207
2940
  req,
2208
2941
  pathname: url.pathname,
@@ -2458,9 +3191,70 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2458
3191
  note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
2459
3192
  });
2460
3193
  }
3194
+ if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
3195
+ const body = await deps.readJsonBody(req);
3196
+ const ids = uniqueStringList(body.ids ?? body.id);
3197
+ const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
3198
+ if (ids.length === 0 && !analyzeAll) {
3199
+ return deps.badRequest("ids is required unless all=true");
3200
+ }
3201
+ const ctx = await getCachedPluginHostContext(state.projectRoot);
3202
+ const [source] = ctx?.taskSourceRegistry.list() ?? [];
3203
+ const target = issueAnalysisTargetFor(source);
3204
+ if (!source || !target) {
3205
+ return deps.badRequest("Configured task source does not support issue-analysis writeback");
3206
+ }
3207
+ const allTasks = [...await source.list()];
3208
+ const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
3209
+ const cached = allTasks.find((task) => taskRecordId(task) === id);
3210
+ if (cached)
3211
+ return cached;
3212
+ return typeof source.get === "function" ? await source.get(id) : undefined;
3213
+ }))).filter((task) => Boolean(task));
3214
+ if (issues.length === 0) {
3215
+ return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
3216
+ }
3217
+ const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
3218
+ const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
3219
+ const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
3220
+ const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
3221
+ const service = createIssueAnalysisService({
3222
+ analyzer: createPiIssueAnalyzer({
3223
+ ...model ? { model } : {},
3224
+ env: { RIG_PROJECT_ROOT: state.projectRoot }
3225
+ }),
3226
+ writeBack: createIssueAnalysisWriteBack({ target })
3227
+ });
3228
+ const reason = normalizeString(body.reason) ?? "http-issue-analysis";
3229
+ let results;
3230
+ try {
3231
+ results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
3232
+ } catch (error) {
3233
+ return deps.jsonResponse({
3234
+ ok: false,
3235
+ error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
3236
+ reason,
3237
+ ids: issues.map((issue) => issue.id)
3238
+ }, 502);
3239
+ }
3240
+ deps.snapshotService.invalidate("issue-analysis-http-run");
3241
+ await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
3242
+ return;
3243
+ });
3244
+ deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
3245
+ return deps.jsonResponse({
3246
+ ok: true,
3247
+ reason,
3248
+ analyzed: results.map((entry) => ({
3249
+ id: entry.issue.id,
3250
+ title: entry.issue.title ?? null,
3251
+ result: entry.result
3252
+ }))
3253
+ });
3254
+ }
2461
3255
  if (url.pathname === "/api/server/status") {
2462
3256
  const config = buildProjectConfigStatus(state.projectRoot);
2463
- const taskSource = await buildTaskSourceStatus(state, config);
3257
+ const taskSource = await buildTaskSourceStatus(state, config, requestAuth);
2464
3258
  return deps.jsonResponse({
2465
3259
  ok: true,
2466
3260
  projectRoot: state.projectRoot,
@@ -2484,8 +3278,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2484
3278
  path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
2485
3279
  ref: normalizeString(rawCheckout?.ref) ?? undefined
2486
3280
  } : undefined;
2487
- const record = upsertProjectRecord(state.projectRoot, { repoSlug, checkout });
2488
- return deps.jsonResponse({ ok: true, project: record });
3281
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
3282
+ const record = upsertProjectRecord(registryRoot, { repoSlug, checkout });
3283
+ return deps.jsonResponse({ ok: true, project: record, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
2489
3284
  }
2490
3285
  const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
2491
3286
  if (snapshotUploadMatch && req.method === "POST") {
@@ -2498,8 +3293,15 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2498
3293
  if (!archiveContentBase64) {
2499
3294
  return deps.badRequest("archiveContentBase64 is required");
2500
3295
  }
2501
- const baseDir = normalizeString(body.baseDir) ?? normalizeString(process.env.RIG_REMOTE_SNAPSHOT_BASE_DIR) ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve11(normalizeString(process.env.RIG_STATE_DIR), "remote-snapshots") : resolve11(state.projectRoot, ".rig", "remote-snapshots"));
2502
- const checkoutKey = normalizeString(body.checkoutKey) ?? "default";
3296
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
3297
+ const baseDir = resolveNamespacedBaseDir({
3298
+ explicitBaseDir: normalizeString(body.baseDir),
3299
+ envName: "RIG_REMOTE_SNAPSHOT_BASE_DIR",
3300
+ userNamespace: requestAuth.userNamespace,
3301
+ legacyProjectRoot: state.projectRoot,
3302
+ legacySubdir: "remote-snapshots"
3303
+ });
3304
+ const checkoutKey = explicitCheckoutKey(body, body, requestAuth);
2503
3305
  try {
2504
3306
  const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
2505
3307
  const checkout = extractUploadedSnapshotArchive({
@@ -2512,14 +3314,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2512
3314
  const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
2513
3315
  const packageInstall = installRemoteCheckoutPackages(checkout.path);
2514
3316
  const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
2515
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
3317
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
2516
3318
  kind: "uploaded-snapshot",
2517
3319
  path: checkout.path,
2518
3320
  ref: checkout.snapshotId
2519
3321
  });
2520
3322
  deps.snapshotService.invalidate("uploaded-snapshot-checkout");
2521
3323
  deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
2522
- return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
3324
+ return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
2523
3325
  } catch (error) {
2524
3326
  return deps.jsonResponse({
2525
3327
  ok: false,
@@ -2539,10 +3341,17 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2539
3341
  if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
2540
3342
  return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
2541
3343
  }
2542
- const baseDir = normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir) ?? normalizeString(process.env.RIG_REMOTE_CHECKOUT_BASE_DIR) ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve11(normalizeString(process.env.RIG_STATE_DIR), "remote-checkouts") : resolve11(state.projectRoot, ".rig", "remote-checkouts"));
2543
- const checkoutKey = normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? "default";
3344
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
3345
+ const baseDir = resolveNamespacedBaseDir({
3346
+ explicitBaseDir: normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir),
3347
+ envName: "RIG_REMOTE_CHECKOUT_BASE_DIR",
3348
+ userNamespace: requestAuth.userNamespace,
3349
+ legacyProjectRoot: state.projectRoot,
3350
+ legacySubdir: "remote-checkouts"
3351
+ });
3352
+ const checkoutKey = explicitCheckoutKey(body, checkoutInput, requestAuth);
2544
3353
  const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
2545
- const credentialToken = createGitHubAuthStore(state.projectRoot).readToken();
3354
+ const credentialToken = requestScopedAuthStore(state.projectRoot, requestAuth).readToken();
2546
3355
  try {
2547
3356
  const checkout = await prepareRemoteCheckout({
2548
3357
  command: runRemoteCheckoutCommand,
@@ -2551,14 +3360,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2551
3360
  const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
2552
3361
  const packageInstall = installRemoteCheckoutPackages(checkout.path);
2553
3362
  const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
2554
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
3363
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
2555
3364
  kind: checkout.kind,
2556
3365
  path: checkout.path,
2557
3366
  ref: checkout.ref ?? checkout.snapshotId ?? undefined
2558
3367
  });
2559
3368
  deps.snapshotService.invalidate("remote-checkout-prepared");
2560
3369
  deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
2561
- return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
3370
+ return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
2562
3371
  } catch (error) {
2563
3372
  return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
2564
3373
  }
@@ -2575,16 +3384,18 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2575
3384
  if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
2576
3385
  return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
2577
3386
  }
2578
- const project = linkProjectCheckout(state.projectRoot, repoSlug, {
3387
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
3388
+ const project = linkProjectCheckout(registryRoot, repoSlug, {
2579
3389
  kind,
2580
3390
  path: normalizeString(body.path) ?? state.projectRoot,
2581
3391
  ref: normalizeString(body.ref) ?? undefined
2582
3392
  });
2583
- return deps.jsonResponse({ ok: true, project });
3393
+ return deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
2584
3394
  }
2585
3395
  if (req.method === "GET") {
2586
- const project = getProjectRecord(state.projectRoot, repoSlug);
2587
- return project ? deps.jsonResponse({ ok: true, project }) : deps.notFound();
3396
+ const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
3397
+ const project = getProjectRecord(registryRoot, repoSlug);
3398
+ return project ? deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) }) : deps.notFound();
2588
3399
  }
2589
3400
  }
2590
3401
  if (url.pathname === "/api/server/project-root" && req.method === "POST") {
@@ -2593,11 +3404,27 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2593
3404
  if (!requestedRoot) {
2594
3405
  return deps.badRequest("projectRoot is required");
2595
3406
  }
2596
- if (!isAbsolute2(requestedRoot)) {
3407
+ if (!isAbsolute3(requestedRoot)) {
2597
3408
  return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
2598
3409
  }
2599
- const normalizedRoot = resolve11(requestedRoot);
2600
- const exists = existsSync8(normalizedRoot);
3410
+ const normalizedRoot = resolve13(requestedRoot);
3411
+ const exists = existsSync10(normalizedRoot);
3412
+ if (exists && requestAuth.userNamespace) {
3413
+ const allowedByNamespace = isPathInsideNamespace(requestAuth.userNamespace.root, normalizedRoot);
3414
+ const allowedByRegistry = projectRegistryContainsCheckout(requestAuth.userNamespace.root, normalizedRoot);
3415
+ if (!allowedByNamespace && !allowedByRegistry) {
3416
+ return deps.jsonResponse({
3417
+ ok: false,
3418
+ error: "Requested project root is outside the authenticated GitHub user namespace.",
3419
+ projectRoot: state.projectRoot,
3420
+ requestedProjectRoot: normalizedRoot,
3421
+ userNamespace: userNamespaceResponse(requestAuth.userNamespace)
3422
+ }, 403);
3423
+ }
3424
+ copyGitHubAuthStateToLocalProjectRoot(requestAuth.userNamespace.authStateFile, normalizedRoot);
3425
+ } else if (exists) {
3426
+ createGitHubAuthStore(state.projectRoot).copyToLocalProjectRoot(normalizedRoot);
3427
+ }
2601
3428
  const control = buildServerControlStatus();
2602
3429
  const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
2603
3430
  if (!exists) {
@@ -2611,7 +3438,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2611
3438
  message: "Requested project root does not exist on the Rig server host."
2612
3439
  }, 404);
2613
3440
  }
2614
- if (!existsSync8(resolve11(normalizedRoot, "rig.config.ts")) && !existsSync8(resolve11(normalizedRoot, "rig.config.json"))) {
3441
+ if (!existsSync10(resolve13(normalizedRoot, "rig.config.ts")) && !existsSync10(resolve13(normalizedRoot, "rig.config.json"))) {
2615
3442
  return deps.jsonResponse({
2616
3443
  ok: false,
2617
3444
  projectRoot: state.projectRoot,
@@ -2646,6 +3473,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2646
3473
  exists,
2647
3474
  control,
2648
3475
  requiresRestart: false,
3476
+ userNamespace: userNamespaceResponse(requestAuth.userNamespace),
2649
3477
  message: "Project-root switch accepted. Rig server restart has been scheduled."
2650
3478
  }, 202);
2651
3479
  }
@@ -2660,11 +3488,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2660
3488
  }, 409);
2661
3489
  }
2662
3490
  if (url.pathname === "/api/github/auth/status") {
2663
- const store = createGitHubAuthStore(state.projectRoot);
2664
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
3491
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
3492
+ return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
2665
3493
  }
2666
3494
  if (url.pathname === "/api/github/repo/permissions") {
2667
- const store = createGitHubAuthStore(state.projectRoot);
3495
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
2668
3496
  const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
2669
3497
  if (!auth.signedIn) {
2670
3498
  return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
@@ -2686,21 +3514,26 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2686
3514
  const body = await deps.readJsonBody(req);
2687
3515
  const token = normalizeString(body.token);
2688
3516
  const selectedRepo = normalizeString(body.selectedRepo);
3517
+ const requestedProjectRoot = normalizeString(body.projectRoot);
2689
3518
  if (!token) {
2690
3519
  return deps.badRequest("token is required");
2691
3520
  }
2692
3521
  try {
2693
3522
  const user = await fetchGitHubUserInfo(token);
2694
- const store = createGitHubAuthStore(state.projectRoot);
2695
- store.saveToken({
3523
+ const saved = saveGitHubTokenForRemoteUser({
3524
+ projectRoot: state.projectRoot,
2696
3525
  token,
2697
3526
  tokenSource: "manual-token",
2698
- login: user.login,
2699
- userId: user.userId,
2700
- scopes: user.scopes,
2701
- selectedRepo
3527
+ user,
3528
+ selectedRepo,
3529
+ requestedProjectRoot
3530
+ });
3531
+ return deps.jsonResponse({
3532
+ ok: true,
3533
+ ...saved.store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }),
3534
+ apiSessionToken: saved.apiSessionToken,
3535
+ userNamespace: userNamespaceResponse(saved.namespace)
2702
3536
  });
2703
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
2704
3537
  } catch (error) {
2705
3538
  const message = error instanceof Error ? error.message : String(error);
2706
3539
  return deps.jsonResponse({ ok: false, error: message }, 400);
@@ -2767,8 +3600,21 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2767
3600
  }
2768
3601
  const token = result.payload.access_token;
2769
3602
  const user = await fetchGitHubUserInfo(token);
2770
- store.saveToken({ token, tokenSource: "oauth-device", login: user.login, userId: user.userId, scopes: user.scopes });
2771
- return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }) });
3603
+ const saved = saveGitHubTokenForRemoteUser({
3604
+ projectRoot: state.projectRoot,
3605
+ token,
3606
+ tokenSource: "oauth-device",
3607
+ user,
3608
+ selectedRepo: null
3609
+ });
3610
+ store.clearPendingDevice(pollId);
3611
+ return deps.jsonResponse({
3612
+ ok: true,
3613
+ status: "signed-in",
3614
+ ...saved.store.status({ oauthConfigured: true }),
3615
+ apiSessionToken: saved.apiSessionToken,
3616
+ userNamespace: userNamespaceResponse(saved.namespace)
3617
+ });
2772
3618
  }
2773
3619
  if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
2774
3620
  const body = await deps.readJsonBody(req);
@@ -2777,7 +3623,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2777
3623
  if (!owner || !repo) {
2778
3624
  return deps.badRequest("owner and repo are required");
2779
3625
  }
2780
- const store = createGitHubAuthStore(state.projectRoot);
3626
+ const store = requestScopedAuthStore(state.projectRoot, requestAuth);
2781
3627
  const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
2782
3628
  const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
2783
3629
  return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
@@ -2798,7 +3644,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2798
3644
  return deps.badRequest(error instanceof Error ? error.message : String(error));
2799
3645
  }
2800
3646
  const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
2801
- const configPath = resolve11(targetRoot, "rig.config.ts");
3647
+ const configPath = resolve13(targetRoot, "rig.config.ts");
2802
3648
  const source = buildGitHubProjectConfigSource({
2803
3649
  projectName: rawProjectName,
2804
3650
  owner,
@@ -2810,8 +3656,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2810
3656
  ok: true,
2811
3657
  projectRoot: targetRoot,
2812
3658
  configPath,
2813
- exists: existsSync8(configPath),
2814
- requiresOverwrite: existsSync8(configPath),
3659
+ exists: existsSync10(configPath),
3660
+ requiresOverwrite: existsSync10(configPath),
2815
3661
  source,
2816
3662
  owner,
2817
3663
  repo,
@@ -2847,8 +3693,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2847
3693
  assignee,
2848
3694
  githubUserId: authStatus.userId ?? authStatus.login
2849
3695
  });
2850
- const configPath = resolve11(targetRoot, "rig.config.ts");
2851
- if (existsSync8(configPath) && !overwrite) {
3696
+ const configPath = resolve13(targetRoot, "rig.config.ts");
3697
+ if (existsSync10(configPath) && !overwrite) {
2852
3698
  return deps.jsonResponse({
2853
3699
  ok: false,
2854
3700
  error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
@@ -2864,11 +3710,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2864
3710
  return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
2865
3711
  }
2866
3712
  let backupPath = null;
2867
- if (existsSync8(configPath)) {
3713
+ if (existsSync10(configPath)) {
2868
3714
  backupPath = backupConfigPath(configPath);
2869
- copyFileSync(configPath, backupPath);
3715
+ copyFileSync2(configPath, backupPath);
2870
3716
  }
2871
- writeFileSync7(configPath, source, "utf8");
3717
+ writeFileSync9(configPath, source, "utf8");
2872
3718
  const selectedRepo = `${owner}/${repo}`;
2873
3719
  store.saveSelectedRepo(selectedRepo);
2874
3720
  const targetStore = createGitHubAuthStore(targetRoot);
@@ -3140,11 +3986,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3140
3986
  const runId = normalizeString(body.runId);
3141
3987
  const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
3142
3988
  const promptOverride = normalizeString(body.promptOverride);
3989
+ const restart = body.restart === true;
3143
3990
  if (!runId) {
3144
3991
  return deps.badRequest("runId is required");
3145
3992
  }
3146
3993
  try {
3147
- await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
3994
+ await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
3148
3995
  deps.broadcastSnapshotInvalidation(state);
3149
3996
  return deps.jsonResponse({ ok: true, runId, createdAt });
3150
3997
  } catch (error) {
@@ -3153,7 +4000,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3153
4000
  }
3154
4001
  if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
3155
4002
  const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
3156
- const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) => resolve11(root, "packages", "pi-rig")).find((candidate) => existsSync8(resolve11(candidate, "package.json"))) ?? "npm:@rig/pi-rig";
4003
+ const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) => resolve13(root, "packages", "pi-rig")).find((candidate) => existsSync10(resolve13(candidate, "package.json"))) ?? "npm:@rig/pi-rig";
3157
4004
  if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
3158
4005
  return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
3159
4006
  }
@@ -3469,9 +4316,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3469
4316
  } catch {
3470
4317
  return deps.badRequest("Invalid artifact path");
3471
4318
  }
3472
- mkdirSync7(dirname6(artifactPath), { recursive: true });
4319
+ mkdirSync9(dirname9(artifactPath), { recursive: true });
3473
4320
  const bytes = Buffer.from(contentBase64, "base64");
3474
- writeFileSync7(artifactPath, bytes);
4321
+ writeFileSync9(artifactPath, bytes);
3475
4322
  writeJsonFile4(`${artifactPath}.json`, {
3476
4323
  workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
3477
4324
  runId,
@@ -3633,12 +4480,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3633
4480
  try {
3634
4481
  const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
3635
4482
  const runRoot = deps.normalizeRelativePath(runsRoot, runId);
3636
- const artifactsRoot = resolve11(runRoot, "remote-artifacts");
4483
+ const artifactsRoot = resolve13(runRoot, "remote-artifacts");
3637
4484
  artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
3638
4485
  } catch {
3639
4486
  return deps.badRequest("Invalid artifact path");
3640
4487
  }
3641
- if (!existsSync8(artifactPath)) {
4488
+ if (!existsSync10(artifactPath)) {
3642
4489
  return deps.notFound();
3643
4490
  }
3644
4491
  return new Response(Bun.file(artifactPath));