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

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,257 @@ async function refreshTaskProjection(projectRoot, input) {
469
469
  });
470
470
  }
471
471
 
472
+ // packages/server/src/server-helpers/issue-analysis.ts
473
+ import { execFile } from "child_process";
474
+ import { createHash } from "crypto";
475
+ function stableIssueHash(issue) {
476
+ const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
477
+ const body = typeof issue.body === "string" ? issue.body : "";
478
+ const title = typeof issue.title === "string" ? issue.title : "";
479
+ return createHash("sha256").update(JSON.stringify({ id: issue.id, title, body, labels, deps: issue.deps, status: issue.status })).digest("hex");
480
+ }
481
+ function renderIssueAnalysisPrompt(input) {
482
+ const issue = input.issue;
483
+ const neighbors = input.neighbors ?? [];
484
+ return [
485
+ "You are Rig issue analysis running inside Pi.",
486
+ "Return JSON only with optional metadataPatch, labelsToAdd, labelsToRemove, and generatedIssues.",
487
+ "Preserve all human-authored issue body content. Only propose edits for Rig-owned metadata/status sections, labels, and generated issues.",
488
+ "Generated issues must be concrete, minimal follow-up tasks and will be labeled rig:generated by Rig.",
489
+ "",
490
+ "Issue:",
491
+ JSON.stringify({
492
+ id: issue.id,
493
+ title: issue.title,
494
+ body: issue.body,
495
+ labels: issue.labels,
496
+ deps: issue.deps,
497
+ status: issue.status
498
+ }, null, 2),
499
+ "",
500
+ "Neighbor tasks:",
501
+ JSON.stringify(neighbors.map((task) => ({ id: task.id, title: task.title, status: task.status, deps: task.deps })), null, 2)
502
+ ].join(`
503
+ `);
504
+ }
505
+ function isRecord(value) {
506
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
507
+ }
508
+ function stringArray(value) {
509
+ if (!Array.isArray(value))
510
+ return;
511
+ return value.map(String).filter((entry) => entry.trim().length > 0);
512
+ }
513
+ function generatedIssues(value) {
514
+ if (!Array.isArray(value))
515
+ return;
516
+ return value.flatMap((entry) => {
517
+ if (!isRecord(entry) || typeof entry.title !== "string")
518
+ return [];
519
+ return [{
520
+ title: entry.title,
521
+ body: typeof entry.body === "string" ? entry.body : "",
522
+ labels: stringArray(entry.labels) ?? [],
523
+ ...Array.isArray(entry.dependsOn) ? { dependsOn: entry.dependsOn.map(String) } : {}
524
+ }];
525
+ });
526
+ }
527
+ function findJsonLikeText(value) {
528
+ if (typeof value === "string") {
529
+ const trimmed = value.trim();
530
+ if (trimmed.startsWith("{") || trimmed.startsWith("```"))
531
+ return trimmed;
532
+ return null;
533
+ }
534
+ if (Array.isArray(value)) {
535
+ for (const entry of value) {
536
+ const found = findJsonLikeText(entry);
537
+ if (found)
538
+ return found;
539
+ }
540
+ return null;
541
+ }
542
+ if (!isRecord(value))
543
+ return null;
544
+ for (const key of ["text", "content", "message", "output_text", "response", "stdout"]) {
545
+ const found = findJsonLikeText(value[key]);
546
+ if (found)
547
+ return found;
548
+ }
549
+ for (const entry of Object.values(value)) {
550
+ const found = findJsonLikeText(entry);
551
+ if (found)
552
+ return found;
553
+ }
554
+ return null;
555
+ }
556
+ function candidateAnalysisObject(value) {
557
+ if (!isRecord(value))
558
+ return null;
559
+ if (isRecord(value.result))
560
+ return candidateAnalysisObject(value.result) ?? value.result;
561
+ if (isRecord(value.analysis))
562
+ return candidateAnalysisObject(value.analysis) ?? value.analysis;
563
+ if (isRecord(value.metadataPatch) || Array.isArray(value.labelsToAdd) || Array.isArray(value.labelsToRemove) || Array.isArray(value.generatedIssues)) {
564
+ return value;
565
+ }
566
+ const nested = findJsonLikeText(value);
567
+ if (nested && nested !== JSON.stringify(value)) {
568
+ try {
569
+ const parsedNested = JSON.parse(nested.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim() ?? nested);
570
+ return candidateAnalysisObject(parsedNested);
571
+ } catch {
572
+ return null;
573
+ }
574
+ }
575
+ return null;
576
+ }
577
+ function parseIssueAnalysisResult(raw) {
578
+ let parsed = raw;
579
+ if (typeof raw === "string") {
580
+ const trimmed = raw.trim();
581
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim();
582
+ try {
583
+ parsed = JSON.parse(fenced ?? trimmed);
584
+ } catch {
585
+ const lastJsonLine = trimmed.split(/\r?\n/).reverse().find((line) => line.trim().startsWith("{"));
586
+ parsed = lastJsonLine ? JSON.parse(lastJsonLine) : {};
587
+ }
588
+ }
589
+ const candidate = candidateAnalysisObject(parsed);
590
+ if (!candidate)
591
+ return {};
592
+ const result = {};
593
+ if (isRecord(candidate.metadataPatch))
594
+ result.metadataPatch = candidate.metadataPatch;
595
+ const add = stringArray(candidate.labelsToAdd);
596
+ if (add?.length)
597
+ result.labelsToAdd = add;
598
+ const remove = stringArray(candidate.labelsToRemove);
599
+ if (remove?.length)
600
+ result.labelsToRemove = remove;
601
+ const generated = generatedIssues(candidate.generatedIssues);
602
+ if (generated?.length)
603
+ result.generatedIssues = generated;
604
+ return result;
605
+ }
606
+ function createDefaultPiIssueAnalysisCommandRunner() {
607
+ return (command, args, options) => new Promise((resolve6) => {
608
+ execFile(command, [...args], {
609
+ timeout: options.timeoutMs,
610
+ maxBuffer: 10 * 1024 * 1024,
611
+ env: options.env ? { ...process.env, ...options.env } : process.env
612
+ }, (error, stdout, stderr) => {
613
+ const exitCode = typeof error?.code === "number" ? error.code : error ? 1 : 0;
614
+ resolve6({ exitCode, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
615
+ });
616
+ });
617
+ }
618
+ function createPiIssueAnalyzer(input = {}) {
619
+ const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
620
+ const timeoutMs = Math.max(1000, Math.trunc(input.timeoutMs ?? Number(process.env.RIG_ISSUE_ANALYSIS_TIMEOUT_MS ?? 120000)));
621
+ const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
622
+ return async ({ prompt }) => {
623
+ const args = ["--print", "--mode", "json", "--no-session"];
624
+ const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
625
+ const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
626
+ if (provider)
627
+ args.push("--provider", provider);
628
+ if (model)
629
+ args.push("--model", model);
630
+ args.push(prompt);
631
+ const result = await runCommand(piBinary, args, { timeoutMs, ...input.env ? { env: input.env } : {} });
632
+ if (result.exitCode !== 0) {
633
+ throw new Error(`Pi issue analysis failed (exit ${result.exitCode}): ${result.stderr ?? result.stdout}`);
634
+ }
635
+ return parseIssueAnalysisResult(result.stdout);
636
+ };
637
+ }
638
+ function defaultStatusComment(input) {
639
+ const changes = [
640
+ input.result.metadataPatch ? "metadata" : null,
641
+ input.result.labelsToAdd?.length ? `labels added: ${input.result.labelsToAdd.join(", ")}` : null,
642
+ input.result.labelsToRemove?.length ? `labels removed: ${input.result.labelsToRemove.join(", ")}` : null,
643
+ input.result.generatedIssues?.length ? `generated issues: ${input.result.generatedIssues.length}` : null
644
+ ].filter((entry) => Boolean(entry));
645
+ if (changes.length === 0)
646
+ return null;
647
+ return [
648
+ "<!-- rig:status-comment -->",
649
+ "### Rig issue analysis",
650
+ "",
651
+ `Analyzed issue ${input.issue.id}${input.reason ? ` (${input.reason})` : ""}.`,
652
+ "",
653
+ ...changes.map((change) => `- ${change}`)
654
+ ].join(`
655
+ `);
656
+ }
657
+ function uniqueLabels(labels, required = []) {
658
+ return [...new Set([...labels ?? [], ...required].map((label) => label.trim()).filter(Boolean))];
659
+ }
660
+ function createIssueAnalysisWriteBack(input) {
661
+ return async ({ issue, result, reason }) => {
662
+ if (result.metadataPatch && Object.keys(result.metadataPatch).length > 0) {
663
+ if (!input.target.updateTask)
664
+ throw new Error("Issue analysis writeback requires updateTask for metadata patches.");
665
+ await input.target.updateTask(issue.id, { metadata: result.metadataPatch });
666
+ }
667
+ if (result.labelsToAdd?.length) {
668
+ if (!input.target.addLabels)
669
+ throw new Error("Issue analysis writeback requires addLabels for labelsToAdd.");
670
+ await input.target.addLabels(issue.id, uniqueLabels(result.labelsToAdd));
671
+ }
672
+ if (result.labelsToRemove?.length) {
673
+ if (!input.target.removeLabels)
674
+ throw new Error("Issue analysis writeback requires removeLabels for labelsToRemove.");
675
+ await input.target.removeLabels(issue.id, uniqueLabels(result.labelsToRemove));
676
+ }
677
+ const comment = (input.buildStatusComment ?? defaultStatusComment)({ issue, result, reason });
678
+ if (comment?.trim()) {
679
+ if (!input.target.updateTask)
680
+ throw new Error("Issue analysis writeback requires updateTask for sticky status comments.");
681
+ await input.target.updateTask(issue.id, { comment });
682
+ }
683
+ for (const generated of result.generatedIssues ?? []) {
684
+ if (!input.target.createIssue)
685
+ throw new Error("Issue analysis writeback requires createIssue for generated issues.");
686
+ await input.target.createIssue({
687
+ title: generated.title,
688
+ body: generated.dependsOn?.length ? `${generated.body.trimEnd()}
689
+
690
+ depends-on: ${generated.dependsOn.map((dep) => dep.startsWith("#") ? dep : `#${dep}`).join(", ")}
691
+ ` : generated.body,
692
+ labels: uniqueLabels(generated.labels, ["rig:generated"])
693
+ });
694
+ }
695
+ };
696
+ }
697
+ function createIssueAnalysisService(input) {
698
+ const analyzedHashes = new Map;
699
+ return {
700
+ async analyze(issues, options = {}) {
701
+ const results = [];
702
+ const neighbors = options.neighbors ?? issues;
703
+ for (const issue of issues) {
704
+ const hash = stableIssueHash(issue);
705
+ if (analyzedHashes.get(issue.id) === hash)
706
+ continue;
707
+ const prompt = renderIssueAnalysisPrompt({ issue, neighbors: neighbors.filter((candidate) => candidate.id !== issue.id) });
708
+ const result = await input.analyzer({ issue, neighbors, prompt });
709
+ analyzedHashes.set(issue.id, hash);
710
+ if (result.metadataPatch || result.labelsToAdd?.length || result.labelsToRemove?.length || result.generatedIssues?.length) {
711
+ await input.writeBack?.({ issue, result, reason: options.reason });
712
+ }
713
+ results.push({ issue, result });
714
+ }
715
+ return results;
716
+ },
717
+ clearCache() {
718
+ analyzedHashes.clear();
719
+ }
720
+ };
721
+ }
722
+
472
723
  // packages/server/src/server-helpers/terminal-runtime.ts
473
724
  import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
474
725
 
@@ -535,6 +786,7 @@ import {
535
786
  } from "@rig/runtime/control-plane/authority-files";
536
787
 
537
788
  // packages/server/src/server-helpers/github-auth-store.ts
789
+ import { randomBytes } from "crypto";
538
790
  import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
539
791
  import { resolve as resolve7 } from "path";
540
792
  function cleanString(value) {
@@ -548,6 +800,24 @@ function cleanScopes(value) {
548
800
  return clean ? [clean] : [];
549
801
  });
550
802
  }
803
+ function parseApiSessions(value) {
804
+ if (!Array.isArray(value))
805
+ return [];
806
+ return value.flatMap((entry) => {
807
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
808
+ return [];
809
+ const record = entry;
810
+ const token = cleanString(record.token);
811
+ if (!token)
812
+ return [];
813
+ return [{
814
+ token,
815
+ login: cleanString(record.login),
816
+ userId: cleanString(record.userId),
817
+ createdAt: cleanString(record.createdAt) ?? undefined
818
+ }];
819
+ });
820
+ }
551
821
  function readStoredAuth(stateFile) {
552
822
  if (!existsSync4(stateFile))
553
823
  return {};
@@ -561,6 +831,7 @@ function readStoredAuth(stateFile) {
561
831
  selectedRepo: cleanString(parsed.selectedRepo),
562
832
  tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
563
833
  pendingDevice: parsePendingDevice(parsed.pendingDevice),
834
+ apiSessions: parseApiSessions(parsed.apiSessions),
564
835
  updatedAt: cleanString(parsed.updatedAt) ?? undefined
565
836
  };
566
837
  } catch {
@@ -579,6 +850,9 @@ function parsePendingDevice(value) {
579
850
  return null;
580
851
  return { pollId, deviceCode, expiresAt, intervalSeconds };
581
852
  }
853
+ function newApiSessionToken() {
854
+ return `rig_${randomBytes(32).toString("base64url")}`;
855
+ }
582
856
  function writeStoredAuth(stateFile, payload) {
583
857
  mkdirSync3(resolve7(stateFile, ".."), { recursive: true });
584
858
  writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
@@ -621,8 +895,37 @@ function createGitHubAuthStore(projectRoot) {
621
895
  scopes: input.scopes ?? [],
622
896
  selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
623
897
  pendingDevice: null,
898
+ apiSessions: previous.apiSessions ?? [],
899
+ updatedAt: new Date().toISOString()
900
+ });
901
+ },
902
+ createApiSession() {
903
+ const previous = readStoredAuth(stateFile);
904
+ const token = newApiSessionToken();
905
+ const session = {
906
+ token,
907
+ login: cleanString(previous.login),
908
+ userId: cleanString(previous.userId),
909
+ createdAt: new Date().toISOString()
910
+ };
911
+ writeStoredAuth(stateFile, {
912
+ ...previous,
913
+ apiSessions: [...(previous.apiSessions ?? []).slice(-9), session],
624
914
  updatedAt: new Date().toISOString()
625
915
  });
916
+ return { token, login: session.login ?? null, userId: session.userId ?? null };
917
+ },
918
+ readApiSession(token) {
919
+ const clean = cleanString(token);
920
+ if (!clean)
921
+ return null;
922
+ const previous = readStoredAuth(stateFile);
923
+ const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
924
+ return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
925
+ },
926
+ copyToProjectRoot(projectRoot2) {
927
+ const targetFile = resolveGitHubAuthStateFile(projectRoot2);
928
+ writeStoredAuth(targetFile, readStoredAuth(stateFile));
626
929
  },
627
930
  savePendingDevice(input) {
628
931
  const previous = readStoredAuth(stateFile);
@@ -1171,7 +1474,7 @@ import {
1171
1474
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
1172
1475
 
1173
1476
  // packages/server/src/server-helpers/project-registry.ts
1174
- import { createHash } from "crypto";
1477
+ import { createHash as createHash2 } from "crypto";
1175
1478
  import { spawnSync } from "child_process";
1176
1479
  import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync5 } from "fs";
1177
1480
  import { dirname as dirname4, resolve as resolve9 } from "path";
@@ -1214,7 +1517,7 @@ function hashFile(path) {
1214
1517
  if (!path)
1215
1518
  return null;
1216
1519
  try {
1217
- return createHash("sha256").update(readFileSync4(path)).digest("hex");
1520
+ return createHash2("sha256").update(readFileSync4(path)).digest("hex");
1218
1521
  } catch {
1219
1522
  return null;
1220
1523
  }
@@ -1461,10 +1764,10 @@ function normalizeCommit(value) {
1461
1764
  function asPlainRecord(value) {
1462
1765
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1463
1766
  }
1464
- var RIG_CONFIG_PACKAGE_VERSION = "0.0.6-alpha.1";
1767
+ var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
1465
1768
  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}`
1769
+ "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
1770
+ "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
1468
1771
  };
1469
1772
  function repoParts(repoSlug) {
1470
1773
  const [owner, repo] = repoSlug.split("/");
@@ -1818,13 +2121,13 @@ function bearerTokenFromRequest(req) {
1818
2121
  function isLoopbackRequest(req) {
1819
2122
  try {
1820
2123
  const hostname = new URL(req.url).hostname.toLowerCase();
1821
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
2124
+ return hostname === "localhost" || hostname === "rig.local" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1822
2125
  } catch {
1823
2126
  return false;
1824
2127
  }
1825
2128
  }
1826
2129
  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";
2130
+ return pathname === "/" || pathname === "/health" || pathname === "/api/health" || pathname === "/api/github/auth/status" || pathname === "/api/github/auth/token" || pathname === "/api/github/auth/device/start" || pathname === "/api/github/auth/device/poll";
1828
2131
  }
1829
2132
  function normalizePrMode(value) {
1830
2133
  const mode = normalizeString(value);
@@ -1837,6 +2140,10 @@ function authorizeRigHttpRequest(input) {
1837
2140
  const bearer = bearerTokenFromRequest(input.req);
1838
2141
  const store = createGitHubAuthStore(input.projectRoot);
1839
2142
  const storedToken = store.readToken();
2143
+ const session = bearer ? store.readApiSession(bearer) : null;
2144
+ if (session) {
2145
+ return { authorized: true, actor: session.login ?? "github-operator", reason: "github-session" };
2146
+ }
1840
2147
  if (bearer && storedToken && bearer === storedToken) {
1841
2148
  const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
1842
2149
  return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
@@ -1844,8 +2151,11 @@ function authorizeRigHttpRequest(input) {
1844
2151
  if (isPublicRigAuthBootstrapRoute(input.pathname)) {
1845
2152
  return { authorized: true, actor: null, reason: "public-bootstrap" };
1846
2153
  }
1847
- if (!input.serverAuthToken && !storedToken && isLoopbackRequest(input.req)) {
1848
- return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
2154
+ if (!input.serverAuthToken && !storedToken) {
2155
+ if (isLoopbackRequest(input.req)) {
2156
+ return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
2157
+ }
2158
+ return { authorized: false, actor: null, reason: "auth-required" };
1849
2159
  }
1850
2160
  return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
1851
2161
  }
@@ -2076,7 +2386,7 @@ function selectNextWorkspaceTask(projectRoot, tasks) {
2076
2386
  if (runnable.length === 0)
2077
2387
  return null;
2078
2388
  const queue = readQueueState(projectRoot);
2079
- const queueRank = new Map(queue.map((entry, index) => [entry.taskId, { score: entry.score, position: index }]));
2389
+ const queueRank = new Map(queue.map((entry, index) => [String(entry.taskId), { score: entry.score, position: index }]));
2080
2390
  return runnable.toSorted((left, right) => {
2081
2391
  const leftId = taskIdOf(left) ?? "";
2082
2392
  const rightId = taskIdOf(right) ?? "";
@@ -2116,6 +2426,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
2116
2426
  }
2117
2427
  return filtered;
2118
2428
  }
2429
+ function issueAnalysisTargetFor(source) {
2430
+ if (!source)
2431
+ return null;
2432
+ const candidate = source;
2433
+ if (typeof candidate.updateTask !== "function")
2434
+ return null;
2435
+ return {
2436
+ ...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
2437
+ updateTask: candidate.updateTask.bind(candidate),
2438
+ ...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
2439
+ ...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
2440
+ ...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
2441
+ };
2442
+ }
2443
+ function uniqueStringList(value) {
2444
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
2445
+ return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
2446
+ }
2447
+ function taskRecordId(task) {
2448
+ return String(task.id ?? "");
2449
+ }
2119
2450
  function redactRemoteEndpoint(endpoint) {
2120
2451
  const { token, ...rest } = endpoint;
2121
2452
  return {
@@ -2202,7 +2533,7 @@ function createRigServerFetch(state, deps) {
2202
2533
  }
2203
2534
  const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
2204
2535
  const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
2205
- const legacyAuthorizedHttpRequest = isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken);
2536
+ const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
2206
2537
  const requestAuth = authorizeRigHttpRequest({
2207
2538
  req,
2208
2539
  pathname: url.pathname,
@@ -2458,6 +2789,67 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2458
2789
  note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
2459
2790
  });
2460
2791
  }
2792
+ if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
2793
+ const body = await deps.readJsonBody(req);
2794
+ const ids = uniqueStringList(body.ids ?? body.id);
2795
+ const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
2796
+ if (ids.length === 0 && !analyzeAll) {
2797
+ return deps.badRequest("ids is required unless all=true");
2798
+ }
2799
+ const ctx = await getCachedPluginHostContext(state.projectRoot);
2800
+ const [source] = ctx?.taskSourceRegistry.list() ?? [];
2801
+ const target = issueAnalysisTargetFor(source);
2802
+ if (!source || !target) {
2803
+ return deps.badRequest("Configured task source does not support issue-analysis writeback");
2804
+ }
2805
+ const allTasks = [...await source.list()];
2806
+ const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
2807
+ const cached = allTasks.find((task) => taskRecordId(task) === id);
2808
+ if (cached)
2809
+ return cached;
2810
+ return typeof source.get === "function" ? await source.get(id) : undefined;
2811
+ }))).filter((task) => Boolean(task));
2812
+ if (issues.length === 0) {
2813
+ return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
2814
+ }
2815
+ const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
2816
+ const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
2817
+ const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
2818
+ const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
2819
+ const service = createIssueAnalysisService({
2820
+ analyzer: createPiIssueAnalyzer({
2821
+ ...model ? { model } : {},
2822
+ env: { RIG_PROJECT_ROOT: state.projectRoot }
2823
+ }),
2824
+ writeBack: createIssueAnalysisWriteBack({ target })
2825
+ });
2826
+ const reason = normalizeString(body.reason) ?? "http-issue-analysis";
2827
+ let results;
2828
+ try {
2829
+ results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
2830
+ } catch (error) {
2831
+ return deps.jsonResponse({
2832
+ ok: false,
2833
+ error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
2834
+ reason,
2835
+ ids: issues.map((issue) => issue.id)
2836
+ }, 502);
2837
+ }
2838
+ deps.snapshotService.invalidate("issue-analysis-http-run");
2839
+ await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
2840
+ return;
2841
+ });
2842
+ deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
2843
+ return deps.jsonResponse({
2844
+ ok: true,
2845
+ reason,
2846
+ analyzed: results.map((entry) => ({
2847
+ id: entry.issue.id,
2848
+ title: entry.issue.title ?? null,
2849
+ result: entry.result
2850
+ }))
2851
+ });
2852
+ }
2461
2853
  if (url.pathname === "/api/server/status") {
2462
2854
  const config = buildProjectConfigStatus(state.projectRoot);
2463
2855
  const taskSource = await buildTaskSourceStatus(state, config);
@@ -2598,6 +2990,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2598
2990
  }
2599
2991
  const normalizedRoot = resolve11(requestedRoot);
2600
2992
  const exists = existsSync8(normalizedRoot);
2993
+ if (exists) {
2994
+ createGitHubAuthStore(state.projectRoot).copyToProjectRoot(normalizedRoot);
2995
+ }
2601
2996
  const control = buildServerControlStatus();
2602
2997
  const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
2603
2998
  if (!exists) {
@@ -2686,21 +3081,30 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2686
3081
  const body = await deps.readJsonBody(req);
2687
3082
  const token = normalizeString(body.token);
2688
3083
  const selectedRepo = normalizeString(body.selectedRepo);
3084
+ const requestedProjectRoot = normalizeString(body.projectRoot);
2689
3085
  if (!token) {
2690
3086
  return deps.badRequest("token is required");
2691
3087
  }
2692
3088
  try {
2693
3089
  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()) }) });
3090
+ const storeRoots = [
3091
+ state.projectRoot,
3092
+ ...requestedProjectRoot && isAbsolute2(requestedProjectRoot) && existsSync8(resolve11(requestedProjectRoot)) ? [resolve11(requestedProjectRoot)] : []
3093
+ ].filter((root, index, roots) => roots.indexOf(root) === index);
3094
+ const stores = storeRoots.map((root) => createGitHubAuthStore(root));
3095
+ for (const store2 of stores) {
3096
+ store2.saveToken({
3097
+ token,
3098
+ tokenSource: "manual-token",
3099
+ login: user.login,
3100
+ userId: user.userId,
3101
+ scopes: user.scopes,
3102
+ selectedRepo
3103
+ });
3104
+ }
3105
+ const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
3106
+ const apiSession = store.createApiSession();
3107
+ return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
2704
3108
  } catch (error) {
2705
3109
  const message = error instanceof Error ? error.message : String(error);
2706
3110
  return deps.jsonResponse({ ok: false, error: message }, 400);
@@ -2768,7 +3172,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2768
3172
  const token = result.payload.access_token;
2769
3173
  const user = await fetchGitHubUserInfo(token);
2770
3174
  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 }) });
3175
+ const apiSession = store.createApiSession();
3176
+ return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }), apiSessionToken: apiSession.token });
2772
3177
  }
2773
3178
  if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
2774
3179
  const body = await deps.readJsonBody(req);
@@ -151,7 +151,10 @@ function createPiIssueAnalyzer(input = {}) {
151
151
  const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
152
152
  return async ({ prompt }) => {
153
153
  const args = ["--print", "--mode", "json", "--no-session"];
154
- const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || "openai-codex/gpt-5.5";
154
+ const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
155
+ const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
156
+ if (provider)
157
+ args.push("--provider", provider);
155
158
  if (model)
156
159
  args.push("--model", model);
157
160
  args.push(prompt);