@h-rig/server 0.0.6-alpha.1 → 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.
package/dist/src/index.js CHANGED
@@ -2293,7 +2293,7 @@ async function buildRigSnapshotPayload(projectRoot, readers) {
2293
2293
  const taskSummaries = (await readers.readWorkspaceTasks(projectRoot)).map((task) => ({
2294
2294
  ...toTaskSummary(workspace.id, {
2295
2295
  ...task,
2296
- status: queuedTaskIds.has(task.id) && (task.status === "open" || task.status === "ready" || task.status === "draft") ? "queued" : task.status
2296
+ status: queuedTaskIds.has(task.id) && (task.status === "open" || task.status === "ready" || task.status === "draft" || task.status === "failed" || task.sourceStatus === "failed") ? "queued" : task.status
2297
2297
  }),
2298
2298
  graphId: graph.id
2299
2299
  }));
@@ -2819,7 +2819,6 @@ function createGitHubTaskReconciler(input) {
2819
2819
  }
2820
2820
 
2821
2821
  // packages/server/src/server-helpers/issue-analysis.ts
2822
- import { execFile } from "child_process";
2823
2822
  import { createHash } from "crypto";
2824
2823
  function stableIssueHash(issue) {
2825
2824
  const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
@@ -2953,16 +2952,33 @@ function parseIssueAnalysisResult(raw) {
2953
2952
  return result;
2954
2953
  }
2955
2954
  function createDefaultPiIssueAnalysisCommandRunner() {
2956
- return (command, args, options) => new Promise((resolve11) => {
2957
- execFile(command, [...args], {
2958
- timeout: options.timeoutMs,
2959
- maxBuffer: 10 * 1024 * 1024,
2960
- env: options.env ? { ...process.env, ...options.env } : process.env
2961
- }, (error, stdout, stderr) => {
2962
- const exitCode = typeof error?.code === "number" ? error.code : error ? 1 : 0;
2963
- resolve11({ exitCode, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
2955
+ return async (command, args, options) => {
2956
+ const env = options.env ? { ...process.env, ...options.env } : process.env;
2957
+ const proc = Bun.spawn([command, ...args], {
2958
+ stdout: "pipe",
2959
+ stderr: "pipe",
2960
+ env
2964
2961
  });
2965
- });
2962
+ let timedOut = false;
2963
+ const timer = setTimeout(() => {
2964
+ timedOut = true;
2965
+ proc.kill();
2966
+ }, options.timeoutMs);
2967
+ try {
2968
+ const [stdout, stderr, exitCode] = await Promise.all([
2969
+ new Response(proc.stdout).text(),
2970
+ new Response(proc.stderr).text(),
2971
+ proc.exited
2972
+ ]);
2973
+ return {
2974
+ exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
2975
+ stdout,
2976
+ stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
2977
+ };
2978
+ } finally {
2979
+ clearTimeout(timer);
2980
+ }
2981
+ };
2966
2982
  }
2967
2983
  function createPiIssueAnalyzer(input = {}) {
2968
2984
  const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
@@ -2970,7 +2986,10 @@ function createPiIssueAnalyzer(input = {}) {
2970
2986
  const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
2971
2987
  return async ({ prompt }) => {
2972
2988
  const args = ["--print", "--mode", "json", "--no-session"];
2973
- const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || "openai-codex/gpt-5.5";
2989
+ const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
2990
+ const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
2991
+ if (provider)
2992
+ args.push("--provider", provider);
2974
2993
  if (model)
2975
2994
  args.push("--model", model);
2976
2995
  args.push(prompt);
@@ -3811,6 +3830,7 @@ function summarizeRunValidationFailure(projectRoot, run) {
3811
3830
  }
3812
3831
 
3813
3832
  // packages/server/src/server-helpers/github-auth-store.ts
3833
+ import { randomBytes } from "crypto";
3814
3834
  import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
3815
3835
  import { resolve as resolve13 } from "path";
3816
3836
  function cleanString(value) {
@@ -3824,6 +3844,24 @@ function cleanScopes(value) {
3824
3844
  return clean ? [clean] : [];
3825
3845
  });
3826
3846
  }
3847
+ function parseApiSessions(value) {
3848
+ if (!Array.isArray(value))
3849
+ return [];
3850
+ return value.flatMap((entry) => {
3851
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
3852
+ return [];
3853
+ const record = entry;
3854
+ const token = cleanString(record.token);
3855
+ if (!token)
3856
+ return [];
3857
+ return [{
3858
+ token,
3859
+ login: cleanString(record.login),
3860
+ userId: cleanString(record.userId),
3861
+ createdAt: cleanString(record.createdAt) ?? undefined
3862
+ }];
3863
+ });
3864
+ }
3827
3865
  function readStoredAuth(stateFile) {
3828
3866
  if (!existsSync6(stateFile))
3829
3867
  return {};
@@ -3837,6 +3875,7 @@ function readStoredAuth(stateFile) {
3837
3875
  selectedRepo: cleanString(parsed.selectedRepo),
3838
3876
  tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
3839
3877
  pendingDevice: parsePendingDevice(parsed.pendingDevice),
3878
+ apiSessions: parseApiSessions(parsed.apiSessions),
3840
3879
  updatedAt: cleanString(parsed.updatedAt) ?? undefined
3841
3880
  };
3842
3881
  } catch {
@@ -3855,6 +3894,9 @@ function parsePendingDevice(value) {
3855
3894
  return null;
3856
3895
  return { pollId, deviceCode, expiresAt, intervalSeconds };
3857
3896
  }
3897
+ function newApiSessionToken() {
3898
+ return `rig_${randomBytes(32).toString("base64url")}`;
3899
+ }
3858
3900
  function writeStoredAuth(stateFile, payload) {
3859
3901
  mkdirSync6(resolve13(stateFile, ".."), { recursive: true });
3860
3902
  writeFileSync5(stateFile, `${JSON.stringify(payload, null, 2)}
@@ -3897,9 +3939,38 @@ function createGitHubAuthStore(projectRoot) {
3897
3939
  scopes: input.scopes ?? [],
3898
3940
  selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
3899
3941
  pendingDevice: null,
3942
+ apiSessions: previous.apiSessions ?? [],
3900
3943
  updatedAt: new Date().toISOString()
3901
3944
  });
3902
3945
  },
3946
+ createApiSession() {
3947
+ const previous = readStoredAuth(stateFile);
3948
+ const token = newApiSessionToken();
3949
+ const session = {
3950
+ token,
3951
+ login: cleanString(previous.login),
3952
+ userId: cleanString(previous.userId),
3953
+ createdAt: new Date().toISOString()
3954
+ };
3955
+ writeStoredAuth(stateFile, {
3956
+ ...previous,
3957
+ apiSessions: [...(previous.apiSessions ?? []).slice(-9), session],
3958
+ updatedAt: new Date().toISOString()
3959
+ });
3960
+ return { token, login: session.login ?? null, userId: session.userId ?? null };
3961
+ },
3962
+ readApiSession(token) {
3963
+ const clean = cleanString(token);
3964
+ if (!clean)
3965
+ return null;
3966
+ const previous = readStoredAuth(stateFile);
3967
+ const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
3968
+ return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
3969
+ },
3970
+ copyToProjectRoot(projectRoot2) {
3971
+ const targetFile = resolveGitHubAuthStateFile(projectRoot2);
3972
+ writeStoredAuth(targetFile, readStoredAuth(stateFile));
3973
+ },
3903
3974
  savePendingDevice(input) {
3904
3975
  const previous = readStoredAuth(stateFile);
3905
3976
  writeStoredAuth(stateFile, {
@@ -4197,15 +4268,36 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
4197
4268
  if (!run.taskId)
4198
4269
  return;
4199
4270
  const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
4200
- await syncGitHubProjectStatusForTaskUpdate({
4201
- taskId: run.taskId,
4202
- status,
4203
- issueNodeId,
4204
- token: createGitHubAuthStore(projectRoot).readToken(),
4205
- config
4206
- }).catch(() => {
4207
- return;
4208
- });
4271
+ try {
4272
+ const result = await syncGitHubProjectStatusForTaskUpdate({
4273
+ taskId: run.taskId,
4274
+ status,
4275
+ issueNodeId,
4276
+ token: createGitHubAuthStore(projectRoot).readToken(),
4277
+ config
4278
+ });
4279
+ if (!result.synced && result.reason !== "project-sync-disabled") {
4280
+ appendRunLogEntry(projectRoot, run.runId, {
4281
+ id: `log:${run.runId}:github-project-sync:${status}`,
4282
+ title: "GitHub Project sync skipped",
4283
+ detail: `Project status sync for ${run.taskId} could not run: ${result.reason}.`,
4284
+ tone: "warn",
4285
+ status: "running",
4286
+ createdAt: new Date().toISOString(),
4287
+ payload: { reason: result.reason, issueNodeId }
4288
+ });
4289
+ }
4290
+ } catch (error) {
4291
+ appendRunLogEntry(projectRoot, run.runId, {
4292
+ id: `log:${run.runId}:github-project-sync-error:${status}`,
4293
+ title: "GitHub Project sync failed",
4294
+ detail: error instanceof Error ? error.message : String(error),
4295
+ tone: "error",
4296
+ status: "running",
4297
+ createdAt: new Date().toISOString(),
4298
+ payload: { issueNodeId }
4299
+ });
4300
+ }
4209
4301
  }
4210
4302
  async function autoAssignRunIssue(projectRoot, run) {
4211
4303
  if (!run.taskId)
@@ -4307,11 +4399,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
4307
4399
  return;
4308
4400
  throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
4309
4401
  }
4402
+ async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
4403
+ const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
4404
+ if (fromReader)
4405
+ return fromReader;
4406
+ const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
4407
+ if (projected)
4408
+ return projected;
4409
+ if (readTasks !== readWorkspaceTasks) {
4410
+ return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
4411
+ }
4412
+ return null;
4413
+ }
4310
4414
  async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
4311
4415
  if ("taskId" in input && input.taskId) {
4312
4416
  assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
4313
4417
  }
4314
- const sourceTask = "taskId" in input && input.taskId ? (await readTasks(projectRoot)).find((task) => task.id === input.taskId) ?? null : null;
4418
+ const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
4315
4419
  const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
4316
4420
  const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
4317
4421
  const runRecord = {
@@ -4383,6 +4487,7 @@ async function startLocalRun(state, runId, options) {
4383
4487
  throw new Error(`Run not found: ${runId}`);
4384
4488
  }
4385
4489
  const startedAt = new Date().toISOString();
4490
+ const resumeMode = options?.resume === true;
4386
4491
  state.runProcesses.set(runId, {
4387
4492
  runId,
4388
4493
  child: null,
@@ -4399,9 +4504,9 @@ async function startLocalRun(state, runId, options) {
4399
4504
  summary: run.title
4400
4505
  });
4401
4506
  appendRunLogEntry(state.projectRoot, runId, {
4402
- id: `log:${runId}:prepare`,
4403
- title: "Rig task run starting",
4404
- detail: run.taskId ?? run.title,
4507
+ id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
4508
+ title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
4509
+ detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
4405
4510
  tone: "info",
4406
4511
  status: "preparing",
4407
4512
  createdAt: startedAt
@@ -4479,7 +4584,15 @@ async function startLocalRun(state, runId, options) {
4479
4584
  RIG_SERVER_INTERNAL_EXEC: "1",
4480
4585
  ...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
4481
4586
  ...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
4482
- ...bridgeGitHubToken ? { RIG_GITHUB_TOKEN: bridgeGitHubToken } : {}
4587
+ ...bridgeGitHubToken ? {
4588
+ RIG_GITHUB_TOKEN: bridgeGitHubToken,
4589
+ GITHUB_TOKEN: bridgeGitHubToken,
4590
+ GH_TOKEN: bridgeGitHubToken
4591
+ } : {},
4592
+ ...resumeMode ? {
4593
+ RIG_RUN_RESUME: "1",
4594
+ RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
4595
+ } : {}
4483
4596
  },
4484
4597
  stdio: ["ignore", "pipe", "pipe"]
4485
4598
  });
@@ -4651,7 +4764,7 @@ async function resumeRunRecord(state, input) {
4651
4764
  if (run.status === "completed") {
4652
4765
  throw new Error("Completed runs cannot be resumed.");
4653
4766
  }
4654
- await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
4767
+ await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
4655
4768
  }
4656
4769
  function appendRunMessage(projectRoot, input) {
4657
4770
  const run = readAuthorityRun4(projectRoot, input.runId);
@@ -4735,34 +4848,12 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
4735
4848
  writeQueueState(projectRoot, next);
4736
4849
  return next;
4737
4850
  }
4738
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
4739
- function reconcileOrphanedLocalRuns(state, runs, nowIso2) {
4740
- let changed = false;
4741
- for (const run of runs) {
4851
+ var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
4852
+ function collectResumableLocalRuns(state, runs) {
4853
+ return runs.filter((run) => {
4742
4854
  const status = normalizeString(run.status)?.toLowerCase() ?? "";
4743
- const serverPid = run.serverPid;
4744
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
4745
- if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
4746
- continue;
4747
- }
4748
- const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
4749
- patchRunRecord(state.projectRoot, run.runId, {
4750
- status: "failed",
4751
- completedAt: run.completedAt ?? nowIso2,
4752
- updatedAt: nowIso2,
4753
- errorText: detail
4754
- });
4755
- appendRunLogEntry(state.projectRoot, run.runId, {
4756
- id: `log:${run.runId}:stale-local-run`,
4757
- title: "Run marked stale after server restart",
4758
- detail,
4759
- tone: "error",
4760
- status: "failed",
4761
- createdAt: nowIso2
4762
- });
4763
- changed = true;
4764
- }
4765
- return changed;
4855
+ return run.mode === "local" && RESUMABLE_LOCAL_RUN_STATUSES.has(status) && !state.runProcesses.has(run.runId);
4856
+ });
4766
4857
  }
4767
4858
  async function reconcileScheduler(state, reason) {
4768
4859
  if (state.scheduler.reconciling) {
@@ -4777,7 +4868,20 @@ async function reconcileScheduler(state, reason) {
4777
4868
  const queue = readQueueState(state.projectRoot);
4778
4869
  const tasks = await state.snapshotService.getWorkspaceTasks();
4779
4870
  let runs = listAuthorityRuns4(state.projectRoot);
4780
- let changed = reconcileOrphanedLocalRuns(state, runs, new Date().toISOString());
4871
+ let changed = false;
4872
+ const resumableRuns = collectResumableLocalRuns(state, runs);
4873
+ for (const run of resumableRuns) {
4874
+ appendRunLogEntry(state.projectRoot, run.runId, {
4875
+ id: `log:${run.runId}:auto-resume:${Date.now()}`,
4876
+ title: "Run auto-resume scheduled",
4877
+ detail: `Rig server recovered nonterminal run ${run.runId} after ${reason}; resuming the same lifecycle instead of restarting it.`,
4878
+ tone: "info",
4879
+ status: "preparing",
4880
+ createdAt: new Date().toISOString()
4881
+ });
4882
+ await startLocalRun(state, run.runId, { resume: true });
4883
+ changed = true;
4884
+ }
4781
4885
  if (changed) {
4782
4886
  runs = listAuthorityRuns4(state.projectRoot);
4783
4887
  }
@@ -5393,10 +5497,10 @@ function normalizeCommit(value) {
5393
5497
  function asPlainRecord(value) {
5394
5498
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
5395
5499
  }
5396
- var RIG_CONFIG_PACKAGE_VERSION = "0.0.6-alpha.1";
5500
+ var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
5397
5501
  var RIG_CONFIG_DEV_DEPENDENCIES = {
5398
- "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_VERSION}`,
5399
- "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_VERSION}`
5502
+ "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
5503
+ "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
5400
5504
  };
5401
5505
  function repoParts(repoSlug) {
5402
5506
  const [owner, repo] = repoSlug.split("/");
@@ -5750,13 +5854,52 @@ function bearerTokenFromRequest(req) {
5750
5854
  function isLoopbackRequest(req) {
5751
5855
  try {
5752
5856
  const hostname = new URL(req.url).hostname.toLowerCase();
5753
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
5857
+ return hostname === "localhost" || hostname === "rig.local" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
5754
5858
  } catch {
5755
5859
  return false;
5756
5860
  }
5757
5861
  }
5758
5862
  function isPublicRigAuthBootstrapRoute(pathname) {
5759
- 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";
5863
+ 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";
5864
+ }
5865
+ function buildRigInstallScript() {
5866
+ return `#!/usr/bin/env bash
5867
+ set -euo pipefail
5868
+
5869
+ say() {
5870
+ printf 'rig-install: %s
5871
+ ' "$*"
5872
+ }
5873
+
5874
+ if ! command -v bun >/dev/null 2>&1; then
5875
+ say "Bun not found; installing Bun first"
5876
+ curl -fsSL https://bun.sh/install | bash
5877
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
5878
+ export PATH="$BUN_INSTALL/bin:$PATH"
5879
+ fi
5880
+
5881
+ if ! command -v bun >/dev/null 2>&1; then
5882
+ printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
5883
+ ' >&2
5884
+ exit 1
5885
+ fi
5886
+
5887
+ say "Installing @h-rig/cli@latest"
5888
+ bun add -g @h-rig/cli@latest
5889
+
5890
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
5891
+ export PATH="$BUN_INSTALL/bin:$PATH"
5892
+
5893
+ if ! command -v rig >/dev/null 2>&1; then
5894
+ printf 'rig-install: rig installed, but rig is not on PATH. Add %s/bin to PATH and retry.
5895
+ ' "$BUN_INSTALL" >&2
5896
+ exit 1
5897
+ fi
5898
+
5899
+ say "Verifying rig"
5900
+ rig --help >/dev/null
5901
+ say "Done. Run: rig --help"
5902
+ `;
5760
5903
  }
5761
5904
  function normalizePrMode(value) {
5762
5905
  const mode = normalizeString(value);
@@ -5769,6 +5912,10 @@ function authorizeRigHttpRequest(input) {
5769
5912
  const bearer = bearerTokenFromRequest(input.req);
5770
5913
  const store = createGitHubAuthStore(input.projectRoot);
5771
5914
  const storedToken = store.readToken();
5915
+ const session = bearer ? store.readApiSession(bearer) : null;
5916
+ if (session) {
5917
+ return { authorized: true, actor: session.login ?? "github-operator", reason: "github-session" };
5918
+ }
5772
5919
  if (bearer && storedToken && bearer === storedToken) {
5773
5920
  const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
5774
5921
  return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
@@ -5776,8 +5923,11 @@ function authorizeRigHttpRequest(input) {
5776
5923
  if (isPublicRigAuthBootstrapRoute(input.pathname)) {
5777
5924
  return { authorized: true, actor: null, reason: "public-bootstrap" };
5778
5925
  }
5779
- if (!input.serverAuthToken && !storedToken && isLoopbackRequest(input.req)) {
5780
- return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
5926
+ if (!input.serverAuthToken && !storedToken) {
5927
+ if (isLoopbackRequest(input.req)) {
5928
+ return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
5929
+ }
5930
+ return { authorized: false, actor: null, reason: "auth-required" };
5781
5931
  }
5782
5932
  return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
5783
5933
  }
@@ -6008,7 +6158,7 @@ function selectNextWorkspaceTask(projectRoot, tasks) {
6008
6158
  if (runnable.length === 0)
6009
6159
  return null;
6010
6160
  const queue = readQueueState(projectRoot);
6011
- const queueRank = new Map(queue.map((entry, index) => [entry.taskId, { score: entry.score, position: index }]));
6161
+ const queueRank = new Map(queue.map((entry, index) => [String(entry.taskId), { score: entry.score, position: index }]));
6012
6162
  return runnable.toSorted((left, right) => {
6013
6163
  const leftId = taskIdOf(left) ?? "";
6014
6164
  const rightId = taskIdOf(right) ?? "";
@@ -6048,6 +6198,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
6048
6198
  }
6049
6199
  return filtered;
6050
6200
  }
6201
+ function issueAnalysisTargetFor(source) {
6202
+ if (!source)
6203
+ return null;
6204
+ const candidate = source;
6205
+ if (typeof candidate.updateTask !== "function")
6206
+ return null;
6207
+ return {
6208
+ ...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
6209
+ updateTask: candidate.updateTask.bind(candidate),
6210
+ ...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
6211
+ ...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
6212
+ ...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
6213
+ };
6214
+ }
6215
+ function uniqueStringList(value) {
6216
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
6217
+ return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
6218
+ }
6219
+ function taskRecordId(task) {
6220
+ return String(task.id ?? "");
6221
+ }
6051
6222
  function redactRemoteEndpoint(endpoint) {
6052
6223
  const { token, ...rest } = endpoint;
6053
6224
  return {
@@ -6132,9 +6303,16 @@ function createRigServerFetch(state, deps) {
6132
6303
  notifications: state.targets.length
6133
6304
  });
6134
6305
  }
6306
+ if (url.pathname === "/install" && req.method === "GET") {
6307
+ return new Response(buildRigInstallScript(), {
6308
+ headers: {
6309
+ "Content-Type": "text/x-shellscript; charset=utf-8"
6310
+ }
6311
+ });
6312
+ }
6135
6313
  const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
6136
6314
  const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
6137
- const legacyAuthorizedHttpRequest = isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken);
6315
+ const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
6138
6316
  const requestAuth = authorizeRigHttpRequest({
6139
6317
  req,
6140
6318
  pathname: url.pathname,
@@ -6390,6 +6568,67 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6390
6568
  note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
6391
6569
  });
6392
6570
  }
6571
+ if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
6572
+ const body = await deps.readJsonBody(req);
6573
+ const ids = uniqueStringList(body.ids ?? body.id);
6574
+ const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
6575
+ if (ids.length === 0 && !analyzeAll) {
6576
+ return deps.badRequest("ids is required unless all=true");
6577
+ }
6578
+ const ctx = await getCachedPluginHostContext(state.projectRoot);
6579
+ const [source] = ctx?.taskSourceRegistry.list() ?? [];
6580
+ const target = issueAnalysisTargetFor(source);
6581
+ if (!source || !target) {
6582
+ return deps.badRequest("Configured task source does not support issue-analysis writeback");
6583
+ }
6584
+ const allTasks = [...await source.list()];
6585
+ const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
6586
+ const cached = allTasks.find((task) => taskRecordId(task) === id);
6587
+ if (cached)
6588
+ return cached;
6589
+ return typeof source.get === "function" ? await source.get(id) : undefined;
6590
+ }))).filter((task) => Boolean(task));
6591
+ if (issues.length === 0) {
6592
+ return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
6593
+ }
6594
+ const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
6595
+ const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
6596
+ const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
6597
+ const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
6598
+ const service = createIssueAnalysisService({
6599
+ analyzer: createPiIssueAnalyzer({
6600
+ ...model ? { model } : {},
6601
+ env: { RIG_PROJECT_ROOT: state.projectRoot }
6602
+ }),
6603
+ writeBack: createIssueAnalysisWriteBack({ target })
6604
+ });
6605
+ const reason = normalizeString(body.reason) ?? "http-issue-analysis";
6606
+ let results;
6607
+ try {
6608
+ results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
6609
+ } catch (error) {
6610
+ return deps.jsonResponse({
6611
+ ok: false,
6612
+ error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
6613
+ reason,
6614
+ ids: issues.map((issue) => issue.id)
6615
+ }, 502);
6616
+ }
6617
+ deps.snapshotService.invalidate("issue-analysis-http-run");
6618
+ await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
6619
+ return;
6620
+ });
6621
+ deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
6622
+ return deps.jsonResponse({
6623
+ ok: true,
6624
+ reason,
6625
+ analyzed: results.map((entry) => ({
6626
+ id: entry.issue.id,
6627
+ title: entry.issue.title ?? null,
6628
+ result: entry.result
6629
+ }))
6630
+ });
6631
+ }
6393
6632
  if (url.pathname === "/api/server/status") {
6394
6633
  const config = buildProjectConfigStatus(state.projectRoot);
6395
6634
  const taskSource = await buildTaskSourceStatus(state, config);
@@ -6530,6 +6769,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6530
6769
  }
6531
6770
  const normalizedRoot = resolve18(requestedRoot);
6532
6771
  const exists = existsSync11(normalizedRoot);
6772
+ if (exists) {
6773
+ createGitHubAuthStore(state.projectRoot).copyToProjectRoot(normalizedRoot);
6774
+ }
6533
6775
  const control = buildServerControlStatus();
6534
6776
  const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
6535
6777
  if (!exists) {
@@ -6618,21 +6860,30 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6618
6860
  const body = await deps.readJsonBody(req);
6619
6861
  const token = normalizeString(body.token);
6620
6862
  const selectedRepo = normalizeString(body.selectedRepo);
6863
+ const requestedProjectRoot = normalizeString(body.projectRoot);
6621
6864
  if (!token) {
6622
6865
  return deps.badRequest("token is required");
6623
6866
  }
6624
6867
  try {
6625
6868
  const user = await fetchGitHubUserInfo(token);
6626
- const store = createGitHubAuthStore(state.projectRoot);
6627
- store.saveToken({
6628
- token,
6629
- tokenSource: "manual-token",
6630
- login: user.login,
6631
- userId: user.userId,
6632
- scopes: user.scopes,
6633
- selectedRepo
6634
- });
6635
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
6869
+ const storeRoots = [
6870
+ state.projectRoot,
6871
+ ...requestedProjectRoot && isAbsolute3(requestedProjectRoot) && existsSync11(resolve18(requestedProjectRoot)) ? [resolve18(requestedProjectRoot)] : []
6872
+ ].filter((root, index, roots) => roots.indexOf(root) === index);
6873
+ const stores = storeRoots.map((root) => createGitHubAuthStore(root));
6874
+ for (const store2 of stores) {
6875
+ store2.saveToken({
6876
+ token,
6877
+ tokenSource: "manual-token",
6878
+ login: user.login,
6879
+ userId: user.userId,
6880
+ scopes: user.scopes,
6881
+ selectedRepo
6882
+ });
6883
+ }
6884
+ const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
6885
+ const apiSession = store.createApiSession();
6886
+ return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
6636
6887
  } catch (error) {
6637
6888
  const message = error instanceof Error ? error.message : String(error);
6638
6889
  return deps.jsonResponse({ ok: false, error: message }, 400);
@@ -6700,7 +6951,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6700
6951
  const token = result.payload.access_token;
6701
6952
  const user = await fetchGitHubUserInfo(token);
6702
6953
  store.saveToken({ token, tokenSource: "oauth-device", login: user.login, userId: user.userId, scopes: user.scopes });
6703
- return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }) });
6954
+ const apiSession = store.createApiSession();
6955
+ return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }), apiSessionToken: apiSession.token });
6704
6956
  }
6705
6957
  if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
6706
6958
  const body = await deps.readJsonBody(req);
@@ -7072,11 +7324,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7072
7324
  const runId = normalizeString(body.runId);
7073
7325
  const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
7074
7326
  const promptOverride = normalizeString(body.promptOverride);
7327
+ const restart = body.restart === true;
7075
7328
  if (!runId) {
7076
7329
  return deps.badRequest("runId is required");
7077
7330
  }
7078
7331
  try {
7079
- await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
7332
+ await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
7080
7333
  deps.broadcastSnapshotInvalidation(state);
7081
7334
  return deps.jsonResponse({ ok: true, runId, createdAt });
7082
7335
  } catch (error) {
@@ -12506,7 +12759,7 @@ async function readWorkspaceTasks(projectRoot) {
12506
12759
  description: normalizeString(entry.description),
12507
12760
  acceptanceCriteria: normalizeString(entry.acceptance_criteria),
12508
12761
  status: normalizedStatus ?? "unknown",
12509
- sourceStatus: normalizedStatus ? null : rawStatus,
12762
+ sourceStatus: normalizedStatus && rawStatus !== normalizedStatus ? rawStatus : normalizedStatus ? null : rawStatus,
12510
12763
  priority: typeof entry.priority === "number" ? entry.priority : typeof entry.priority === "string" ? Number(entry.priority) : null,
12511
12764
  issueType: normalizeString(entry.issue_type),
12512
12765
  role: normalizeString(config.role) ?? null,
@@ -12986,6 +13239,7 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
12986
13239
  const server = Bun.serve({
12987
13240
  hostname: options.host,
12988
13241
  port: options.port,
13242
+ idleTimeout: Math.max(10, Math.min(255, Number.parseInt(process.env.RIG_SERVER_IDLE_TIMEOUT_SECONDS || "255", 10) || 255)),
12989
13243
  fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
12990
13244
  websocket: {
12991
13245
  open(ws) {