@h-rig/server 0.0.6-alpha.0 → 0.0.6-alpha.10

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.
@@ -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,6 +802,7 @@ import {
535
802
  } from "@rig/runtime/control-plane/authority-files";
536
803
 
537
804
  // packages/server/src/server-helpers/github-auth-store.ts
805
+ import { randomBytes } from "crypto";
538
806
  import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
539
807
  import { resolve as resolve7 } from "path";
540
808
  function cleanString(value) {
@@ -548,6 +816,24 @@ 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
+ }
551
837
  function readStoredAuth(stateFile) {
552
838
  if (!existsSync4(stateFile))
553
839
  return {};
@@ -561,6 +847,7 @@ function readStoredAuth(stateFile) {
561
847
  selectedRepo: cleanString(parsed.selectedRepo),
562
848
  tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
563
849
  pendingDevice: parsePendingDevice(parsed.pendingDevice),
850
+ apiSessions: parseApiSessions(parsed.apiSessions),
564
851
  updatedAt: cleanString(parsed.updatedAt) ?? undefined
565
852
  };
566
853
  } catch {
@@ -579,6 +866,9 @@ function parsePendingDevice(value) {
579
866
  return null;
580
867
  return { pollId, deviceCode, expiresAt, intervalSeconds };
581
868
  }
869
+ function newApiSessionToken() {
870
+ return `rig_${randomBytes(32).toString("base64url")}`;
871
+ }
582
872
  function writeStoredAuth(stateFile, payload) {
583
873
  mkdirSync3(resolve7(stateFile, ".."), { recursive: true });
584
874
  writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
@@ -621,8 +911,37 @@ function createGitHubAuthStore(projectRoot) {
621
911
  scopes: input.scopes ?? [],
622
912
  selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
623
913
  pendingDevice: null,
914
+ apiSessions: previous.apiSessions ?? [],
915
+ updatedAt: new Date().toISOString()
916
+ });
917
+ },
918
+ createApiSession() {
919
+ const previous = readStoredAuth(stateFile);
920
+ const token = newApiSessionToken();
921
+ const session = {
922
+ token,
923
+ login: cleanString(previous.login),
924
+ userId: cleanString(previous.userId),
925
+ createdAt: new Date().toISOString()
926
+ };
927
+ writeStoredAuth(stateFile, {
928
+ ...previous,
929
+ apiSessions: [...(previous.apiSessions ?? []).slice(-9), session],
624
930
  updatedAt: new Date().toISOString()
625
931
  });
932
+ return { token, login: session.login ?? null, userId: session.userId ?? null };
933
+ },
934
+ readApiSession(token) {
935
+ const clean = cleanString(token);
936
+ if (!clean)
937
+ return null;
938
+ const previous = readStoredAuth(stateFile);
939
+ const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
940
+ return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
941
+ },
942
+ copyToProjectRoot(projectRoot2) {
943
+ const targetFile = resolveGitHubAuthStateFile(projectRoot2);
944
+ writeStoredAuth(targetFile, readStoredAuth(stateFile));
626
945
  },
627
946
  savePendingDevice(input) {
628
947
  const previous = readStoredAuth(stateFile);
@@ -864,7 +1183,7 @@ var TERMINAL_RUN_STATUSES2 = new Set([
864
1183
  "needs-attention",
865
1184
  "stopped"
866
1185
  ]);
867
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
1186
+ var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
868
1187
 
869
1188
  // packages/server/src/server-helpers/ws-router.ts
870
1189
  import {
@@ -1171,7 +1490,7 @@ import {
1171
1490
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
1172
1491
 
1173
1492
  // packages/server/src/server-helpers/project-registry.ts
1174
- import { createHash } from "crypto";
1493
+ import { createHash as createHash2 } from "crypto";
1175
1494
  import { spawnSync } from "child_process";
1176
1495
  import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync5 } from "fs";
1177
1496
  import { dirname as dirname4, resolve as resolve9 } from "path";
@@ -1214,7 +1533,7 @@ function hashFile(path) {
1214
1533
  if (!path)
1215
1534
  return null;
1216
1535
  try {
1217
- return createHash("sha256").update(readFileSync4(path)).digest("hex");
1536
+ return createHash2("sha256").update(readFileSync4(path)).digest("hex");
1218
1537
  } catch {
1219
1538
  return null;
1220
1539
  }
@@ -1461,10 +1780,10 @@ function normalizeCommit(value) {
1461
1780
  function asPlainRecord(value) {
1462
1781
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1463
1782
  }
1464
- var RIG_CONFIG_PACKAGE_VERSION = "0.0.6-alpha.0";
1783
+ var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
1465
1784
  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}`
1785
+ "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
1786
+ "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
1468
1787
  };
1469
1788
  function repoParts(repoSlug) {
1470
1789
  const [owner, repo] = repoSlug.split("/");
@@ -1818,13 +2137,52 @@ function bearerTokenFromRequest(req) {
1818
2137
  function isLoopbackRequest(req) {
1819
2138
  try {
1820
2139
  const hostname = new URL(req.url).hostname.toLowerCase();
1821
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
2140
+ return hostname === "localhost" || hostname === "rig.local" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1822
2141
  } catch {
1823
2142
  return false;
1824
2143
  }
1825
2144
  }
1826
2145
  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";
2146
+ 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";
2147
+ }
2148
+ function buildRigInstallScript() {
2149
+ return `#!/usr/bin/env bash
2150
+ set -euo pipefail
2151
+
2152
+ say() {
2153
+ printf 'rig-install: %s
2154
+ ' "$*"
2155
+ }
2156
+
2157
+ if ! command -v bun >/dev/null 2>&1; then
2158
+ say "Bun not found; installing Bun first"
2159
+ curl -fsSL https://bun.sh/install | bash
2160
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
2161
+ export PATH="$BUN_INSTALL/bin:$PATH"
2162
+ fi
2163
+
2164
+ if ! command -v bun >/dev/null 2>&1; then
2165
+ printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
2166
+ ' >&2
2167
+ exit 1
2168
+ fi
2169
+
2170
+ say "Installing @h-rig/cli@latest"
2171
+ bun add -g @h-rig/cli@latest
2172
+
2173
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
2174
+ export PATH="$BUN_INSTALL/bin:$PATH"
2175
+
2176
+ if ! command -v rig >/dev/null 2>&1; then
2177
+ printf 'rig-install: rig installed, but rig is not on PATH. Add %s/bin to PATH and retry.
2178
+ ' "$BUN_INSTALL" >&2
2179
+ exit 1
2180
+ fi
2181
+
2182
+ say "Verifying rig"
2183
+ rig --help >/dev/null
2184
+ say "Done. Run: rig --help"
2185
+ `;
1828
2186
  }
1829
2187
  function normalizePrMode(value) {
1830
2188
  const mode = normalizeString(value);
@@ -1837,6 +2195,10 @@ function authorizeRigHttpRequest(input) {
1837
2195
  const bearer = bearerTokenFromRequest(input.req);
1838
2196
  const store = createGitHubAuthStore(input.projectRoot);
1839
2197
  const storedToken = store.readToken();
2198
+ const session = bearer ? store.readApiSession(bearer) : null;
2199
+ if (session) {
2200
+ return { authorized: true, actor: session.login ?? "github-operator", reason: "github-session" };
2201
+ }
1840
2202
  if (bearer && storedToken && bearer === storedToken) {
1841
2203
  const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
1842
2204
  return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
@@ -1844,8 +2206,11 @@ function authorizeRigHttpRequest(input) {
1844
2206
  if (isPublicRigAuthBootstrapRoute(input.pathname)) {
1845
2207
  return { authorized: true, actor: null, reason: "public-bootstrap" };
1846
2208
  }
1847
- if (!input.serverAuthToken && !storedToken && isLoopbackRequest(input.req)) {
1848
- return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
2209
+ if (!input.serverAuthToken && !storedToken) {
2210
+ if (isLoopbackRequest(input.req)) {
2211
+ return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
2212
+ }
2213
+ return { authorized: false, actor: null, reason: "auth-required" };
1849
2214
  }
1850
2215
  return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
1851
2216
  }
@@ -2076,7 +2441,7 @@ function selectNextWorkspaceTask(projectRoot, tasks) {
2076
2441
  if (runnable.length === 0)
2077
2442
  return null;
2078
2443
  const queue = readQueueState(projectRoot);
2079
- const queueRank = new Map(queue.map((entry, index) => [entry.taskId, { score: entry.score, position: index }]));
2444
+ const queueRank = new Map(queue.map((entry, index) => [String(entry.taskId), { score: entry.score, position: index }]));
2080
2445
  return runnable.toSorted((left, right) => {
2081
2446
  const leftId = taskIdOf(left) ?? "";
2082
2447
  const rightId = taskIdOf(right) ?? "";
@@ -2116,6 +2481,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
2116
2481
  }
2117
2482
  return filtered;
2118
2483
  }
2484
+ function issueAnalysisTargetFor(source) {
2485
+ if (!source)
2486
+ return null;
2487
+ const candidate = source;
2488
+ if (typeof candidate.updateTask !== "function")
2489
+ return null;
2490
+ return {
2491
+ ...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
2492
+ updateTask: candidate.updateTask.bind(candidate),
2493
+ ...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
2494
+ ...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
2495
+ ...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
2496
+ };
2497
+ }
2498
+ function uniqueStringList(value) {
2499
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
2500
+ return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
2501
+ }
2502
+ function taskRecordId(task) {
2503
+ return String(task.id ?? "");
2504
+ }
2119
2505
  function redactRemoteEndpoint(endpoint) {
2120
2506
  const { token, ...rest } = endpoint;
2121
2507
  return {
@@ -2200,9 +2586,16 @@ function createRigServerFetch(state, deps) {
2200
2586
  notifications: state.targets.length
2201
2587
  });
2202
2588
  }
2589
+ if (url.pathname === "/install" && req.method === "GET") {
2590
+ return new Response(buildRigInstallScript(), {
2591
+ headers: {
2592
+ "Content-Type": "text/x-shellscript; charset=utf-8"
2593
+ }
2594
+ });
2595
+ }
2203
2596
  const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
2204
2597
  const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
2205
- const legacyAuthorizedHttpRequest = isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken);
2598
+ const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
2206
2599
  const requestAuth = authorizeRigHttpRequest({
2207
2600
  req,
2208
2601
  pathname: url.pathname,
@@ -2458,6 +2851,67 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2458
2851
  note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
2459
2852
  });
2460
2853
  }
2854
+ if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
2855
+ const body = await deps.readJsonBody(req);
2856
+ const ids = uniqueStringList(body.ids ?? body.id);
2857
+ const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
2858
+ if (ids.length === 0 && !analyzeAll) {
2859
+ return deps.badRequest("ids is required unless all=true");
2860
+ }
2861
+ const ctx = await getCachedPluginHostContext(state.projectRoot);
2862
+ const [source] = ctx?.taskSourceRegistry.list() ?? [];
2863
+ const target = issueAnalysisTargetFor(source);
2864
+ if (!source || !target) {
2865
+ return deps.badRequest("Configured task source does not support issue-analysis writeback");
2866
+ }
2867
+ const allTasks = [...await source.list()];
2868
+ const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
2869
+ const cached = allTasks.find((task) => taskRecordId(task) === id);
2870
+ if (cached)
2871
+ return cached;
2872
+ return typeof source.get === "function" ? await source.get(id) : undefined;
2873
+ }))).filter((task) => Boolean(task));
2874
+ if (issues.length === 0) {
2875
+ return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
2876
+ }
2877
+ const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
2878
+ const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
2879
+ const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
2880
+ const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
2881
+ const service = createIssueAnalysisService({
2882
+ analyzer: createPiIssueAnalyzer({
2883
+ ...model ? { model } : {},
2884
+ env: { RIG_PROJECT_ROOT: state.projectRoot }
2885
+ }),
2886
+ writeBack: createIssueAnalysisWriteBack({ target })
2887
+ });
2888
+ const reason = normalizeString(body.reason) ?? "http-issue-analysis";
2889
+ let results;
2890
+ try {
2891
+ results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
2892
+ } catch (error) {
2893
+ return deps.jsonResponse({
2894
+ ok: false,
2895
+ error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
2896
+ reason,
2897
+ ids: issues.map((issue) => issue.id)
2898
+ }, 502);
2899
+ }
2900
+ deps.snapshotService.invalidate("issue-analysis-http-run");
2901
+ await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
2902
+ return;
2903
+ });
2904
+ deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
2905
+ return deps.jsonResponse({
2906
+ ok: true,
2907
+ reason,
2908
+ analyzed: results.map((entry) => ({
2909
+ id: entry.issue.id,
2910
+ title: entry.issue.title ?? null,
2911
+ result: entry.result
2912
+ }))
2913
+ });
2914
+ }
2461
2915
  if (url.pathname === "/api/server/status") {
2462
2916
  const config = buildProjectConfigStatus(state.projectRoot);
2463
2917
  const taskSource = await buildTaskSourceStatus(state, config);
@@ -2598,6 +3052,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2598
3052
  }
2599
3053
  const normalizedRoot = resolve11(requestedRoot);
2600
3054
  const exists = existsSync8(normalizedRoot);
3055
+ if (exists) {
3056
+ createGitHubAuthStore(state.projectRoot).copyToProjectRoot(normalizedRoot);
3057
+ }
2601
3058
  const control = buildServerControlStatus();
2602
3059
  const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
2603
3060
  if (!exists) {
@@ -2686,21 +3143,30 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2686
3143
  const body = await deps.readJsonBody(req);
2687
3144
  const token = normalizeString(body.token);
2688
3145
  const selectedRepo = normalizeString(body.selectedRepo);
3146
+ const requestedProjectRoot = normalizeString(body.projectRoot);
2689
3147
  if (!token) {
2690
3148
  return deps.badRequest("token is required");
2691
3149
  }
2692
3150
  try {
2693
3151
  const user = await fetchGitHubUserInfo(token);
2694
- const store = createGitHubAuthStore(state.projectRoot);
2695
- store.saveToken({
2696
- token,
2697
- tokenSource: "manual-token",
2698
- login: user.login,
2699
- userId: user.userId,
2700
- scopes: user.scopes,
2701
- selectedRepo
2702
- });
2703
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
3152
+ const storeRoots = [
3153
+ state.projectRoot,
3154
+ ...requestedProjectRoot && isAbsolute2(requestedProjectRoot) && existsSync8(resolve11(requestedProjectRoot)) ? [resolve11(requestedProjectRoot)] : []
3155
+ ].filter((root, index, roots) => roots.indexOf(root) === index);
3156
+ const stores = storeRoots.map((root) => createGitHubAuthStore(root));
3157
+ for (const store2 of stores) {
3158
+ store2.saveToken({
3159
+ token,
3160
+ tokenSource: "manual-token",
3161
+ login: user.login,
3162
+ userId: user.userId,
3163
+ scopes: user.scopes,
3164
+ selectedRepo
3165
+ });
3166
+ }
3167
+ const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
3168
+ const apiSession = store.createApiSession();
3169
+ return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
2704
3170
  } catch (error) {
2705
3171
  const message = error instanceof Error ? error.message : String(error);
2706
3172
  return deps.jsonResponse({ ok: false, error: message }, 400);
@@ -2768,7 +3234,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2768
3234
  const token = result.payload.access_token;
2769
3235
  const user = await fetchGitHubUserInfo(token);
2770
3236
  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 }) });
3237
+ const apiSession = store.createApiSession();
3238
+ return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }), apiSessionToken: apiSession.token });
2772
3239
  }
2773
3240
  if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
2774
3241
  const body = await deps.readJsonBody(req);
@@ -3140,11 +3607,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3140
3607
  const runId = normalizeString(body.runId);
3141
3608
  const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
3142
3609
  const promptOverride = normalizeString(body.promptOverride);
3610
+ const restart = body.restart === true;
3143
3611
  if (!runId) {
3144
3612
  return deps.badRequest("runId is required");
3145
3613
  }
3146
3614
  try {
3147
- await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
3615
+ await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
3148
3616
  deps.broadcastSnapshotInvalidation(state);
3149
3617
  return deps.jsonResponse({ ok: true, runId, createdAt });
3150
3618
  } catch (error) {