@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.
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env bun
1
2
  // @bun
2
3
  var __require = import.meta.require;
3
4
 
@@ -1786,7 +1787,7 @@ async function buildRigSnapshotPayload(projectRoot, readers) {
1786
1787
  const taskSummaries = (await readers.readWorkspaceTasks(projectRoot)).map((task) => ({
1787
1788
  ...toTaskSummary(workspace.id, {
1788
1789
  ...task,
1789
- status: queuedTaskIds.has(task.id) && (task.status === "open" || task.status === "ready" || task.status === "draft") ? "queued" : task.status
1790
+ status: queuedTaskIds.has(task.id) && (task.status === "open" || task.status === "ready" || task.status === "draft" || task.status === "failed" || task.sourceStatus === "failed") ? "queued" : task.status
1790
1791
  }),
1791
1792
  graphId: graph.id
1792
1793
  }));
@@ -2312,7 +2313,6 @@ function createGitHubTaskReconciler(input) {
2312
2313
  }
2313
2314
 
2314
2315
  // packages/server/src/server-helpers/issue-analysis.ts
2315
- import { execFile } from "child_process";
2316
2316
  import { createHash } from "crypto";
2317
2317
  function stableIssueHash(issue) {
2318
2318
  const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
@@ -2446,16 +2446,33 @@ function parseIssueAnalysisResult(raw) {
2446
2446
  return result;
2447
2447
  }
2448
2448
  function createDefaultPiIssueAnalysisCommandRunner() {
2449
- return (command, args, options) => new Promise((resolve11) => {
2450
- execFile(command, [...args], {
2451
- timeout: options.timeoutMs,
2452
- maxBuffer: 10 * 1024 * 1024,
2453
- env: options.env ? { ...process.env, ...options.env } : process.env
2454
- }, (error, stdout, stderr) => {
2455
- const exitCode = typeof error?.code === "number" ? error.code : error ? 1 : 0;
2456
- resolve11({ exitCode, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
2449
+ return async (command, args, options) => {
2450
+ const env = options.env ? { ...process.env, ...options.env } : process.env;
2451
+ const proc = Bun.spawn([command, ...args], {
2452
+ stdout: "pipe",
2453
+ stderr: "pipe",
2454
+ env
2457
2455
  });
2458
- });
2456
+ let timedOut = false;
2457
+ const timer = setTimeout(() => {
2458
+ timedOut = true;
2459
+ proc.kill();
2460
+ }, options.timeoutMs);
2461
+ try {
2462
+ const [stdout, stderr, exitCode] = await Promise.all([
2463
+ new Response(proc.stdout).text(),
2464
+ new Response(proc.stderr).text(),
2465
+ proc.exited
2466
+ ]);
2467
+ return {
2468
+ exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
2469
+ stdout,
2470
+ stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
2471
+ };
2472
+ } finally {
2473
+ clearTimeout(timer);
2474
+ }
2475
+ };
2459
2476
  }
2460
2477
  function createPiIssueAnalyzer(input = {}) {
2461
2478
  const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
@@ -2463,7 +2480,10 @@ function createPiIssueAnalyzer(input = {}) {
2463
2480
  const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
2464
2481
  return async ({ prompt }) => {
2465
2482
  const args = ["--print", "--mode", "json", "--no-session"];
2466
- const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || "openai-codex/gpt-5.5";
2483
+ const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
2484
+ const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
2485
+ if (provider)
2486
+ args.push("--provider", provider);
2467
2487
  if (model)
2468
2488
  args.push("--model", model);
2469
2489
  args.push(prompt);
@@ -3304,6 +3324,7 @@ function summarizeRunValidationFailure(projectRoot, run) {
3304
3324
  }
3305
3325
 
3306
3326
  // packages/server/src/server-helpers/github-auth-store.ts
3327
+ import { randomBytes } from "crypto";
3307
3328
  import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
3308
3329
  import { resolve as resolve13 } from "path";
3309
3330
  function cleanString(value) {
@@ -3317,6 +3338,24 @@ function cleanScopes(value) {
3317
3338
  return clean ? [clean] : [];
3318
3339
  });
3319
3340
  }
3341
+ function parseApiSessions(value) {
3342
+ if (!Array.isArray(value))
3343
+ return [];
3344
+ return value.flatMap((entry) => {
3345
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
3346
+ return [];
3347
+ const record = entry;
3348
+ const token = cleanString(record.token);
3349
+ if (!token)
3350
+ return [];
3351
+ return [{
3352
+ token,
3353
+ login: cleanString(record.login),
3354
+ userId: cleanString(record.userId),
3355
+ createdAt: cleanString(record.createdAt) ?? undefined
3356
+ }];
3357
+ });
3358
+ }
3320
3359
  function readStoredAuth(stateFile) {
3321
3360
  if (!existsSync6(stateFile))
3322
3361
  return {};
@@ -3330,6 +3369,7 @@ function readStoredAuth(stateFile) {
3330
3369
  selectedRepo: cleanString(parsed.selectedRepo),
3331
3370
  tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
3332
3371
  pendingDevice: parsePendingDevice(parsed.pendingDevice),
3372
+ apiSessions: parseApiSessions(parsed.apiSessions),
3333
3373
  updatedAt: cleanString(parsed.updatedAt) ?? undefined
3334
3374
  };
3335
3375
  } catch {
@@ -3348,6 +3388,9 @@ function parsePendingDevice(value) {
3348
3388
  return null;
3349
3389
  return { pollId, deviceCode, expiresAt, intervalSeconds };
3350
3390
  }
3391
+ function newApiSessionToken() {
3392
+ return `rig_${randomBytes(32).toString("base64url")}`;
3393
+ }
3351
3394
  function writeStoredAuth(stateFile, payload) {
3352
3395
  mkdirSync6(resolve13(stateFile, ".."), { recursive: true });
3353
3396
  writeFileSync5(stateFile, `${JSON.stringify(payload, null, 2)}
@@ -3390,9 +3433,38 @@ function createGitHubAuthStore(projectRoot) {
3390
3433
  scopes: input.scopes ?? [],
3391
3434
  selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
3392
3435
  pendingDevice: null,
3436
+ apiSessions: previous.apiSessions ?? [],
3393
3437
  updatedAt: new Date().toISOString()
3394
3438
  });
3395
3439
  },
3440
+ createApiSession() {
3441
+ const previous = readStoredAuth(stateFile);
3442
+ const token = newApiSessionToken();
3443
+ const session = {
3444
+ token,
3445
+ login: cleanString(previous.login),
3446
+ userId: cleanString(previous.userId),
3447
+ createdAt: new Date().toISOString()
3448
+ };
3449
+ writeStoredAuth(stateFile, {
3450
+ ...previous,
3451
+ apiSessions: [...(previous.apiSessions ?? []).slice(-9), session],
3452
+ updatedAt: new Date().toISOString()
3453
+ });
3454
+ return { token, login: session.login ?? null, userId: session.userId ?? null };
3455
+ },
3456
+ readApiSession(token) {
3457
+ const clean = cleanString(token);
3458
+ if (!clean)
3459
+ return null;
3460
+ const previous = readStoredAuth(stateFile);
3461
+ const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
3462
+ return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
3463
+ },
3464
+ copyToProjectRoot(projectRoot2) {
3465
+ const targetFile = resolveGitHubAuthStateFile(projectRoot2);
3466
+ writeStoredAuth(targetFile, readStoredAuth(stateFile));
3467
+ },
3396
3468
  savePendingDevice(input) {
3397
3469
  const previous = readStoredAuth(stateFile);
3398
3470
  writeStoredAuth(stateFile, {
@@ -3690,15 +3762,36 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
3690
3762
  if (!run.taskId)
3691
3763
  return;
3692
3764
  const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
3693
- await syncGitHubProjectStatusForTaskUpdate({
3694
- taskId: run.taskId,
3695
- status,
3696
- issueNodeId,
3697
- token: createGitHubAuthStore(projectRoot).readToken(),
3698
- config
3699
- }).catch(() => {
3700
- return;
3701
- });
3765
+ try {
3766
+ const result = await syncGitHubProjectStatusForTaskUpdate({
3767
+ taskId: run.taskId,
3768
+ status,
3769
+ issueNodeId,
3770
+ token: createGitHubAuthStore(projectRoot).readToken(),
3771
+ config
3772
+ });
3773
+ if (!result.synced && result.reason !== "project-sync-disabled") {
3774
+ appendRunLogEntry(projectRoot, run.runId, {
3775
+ id: `log:${run.runId}:github-project-sync:${status}`,
3776
+ title: "GitHub Project sync skipped",
3777
+ detail: `Project status sync for ${run.taskId} could not run: ${result.reason}.`,
3778
+ tone: "warn",
3779
+ status: "running",
3780
+ createdAt: new Date().toISOString(),
3781
+ payload: { reason: result.reason, issueNodeId }
3782
+ });
3783
+ }
3784
+ } catch (error) {
3785
+ appendRunLogEntry(projectRoot, run.runId, {
3786
+ id: `log:${run.runId}:github-project-sync-error:${status}`,
3787
+ title: "GitHub Project sync failed",
3788
+ detail: error instanceof Error ? error.message : String(error),
3789
+ tone: "error",
3790
+ status: "running",
3791
+ createdAt: new Date().toISOString(),
3792
+ payload: { issueNodeId }
3793
+ });
3794
+ }
3702
3795
  }
3703
3796
  async function autoAssignRunIssue(projectRoot, run) {
3704
3797
  if (!run.taskId)
@@ -3800,11 +3893,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
3800
3893
  return;
3801
3894
  throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
3802
3895
  }
3896
+ async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
3897
+ const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
3898
+ if (fromReader)
3899
+ return fromReader;
3900
+ const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
3901
+ if (projected)
3902
+ return projected;
3903
+ if (readTasks !== readWorkspaceTasks) {
3904
+ return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
3905
+ }
3906
+ return null;
3907
+ }
3803
3908
  async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
3804
3909
  if ("taskId" in input && input.taskId) {
3805
3910
  assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
3806
3911
  }
3807
- const sourceTask = "taskId" in input && input.taskId ? (await readTasks(projectRoot)).find((task) => task.id === input.taskId) ?? null : null;
3912
+ const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
3808
3913
  const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
3809
3914
  const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
3810
3915
  const runRecord = {
@@ -3876,6 +3981,7 @@ async function startLocalRun(state, runId, options) {
3876
3981
  throw new Error(`Run not found: ${runId}`);
3877
3982
  }
3878
3983
  const startedAt = new Date().toISOString();
3984
+ const resumeMode = options?.resume === true;
3879
3985
  state.runProcesses.set(runId, {
3880
3986
  runId,
3881
3987
  child: null,
@@ -3892,9 +3998,9 @@ async function startLocalRun(state, runId, options) {
3892
3998
  summary: run.title
3893
3999
  });
3894
4000
  appendRunLogEntry(state.projectRoot, runId, {
3895
- id: `log:${runId}:prepare`,
3896
- title: "Rig task run starting",
3897
- detail: run.taskId ?? run.title,
4001
+ id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
4002
+ title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
4003
+ detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
3898
4004
  tone: "info",
3899
4005
  status: "preparing",
3900
4006
  createdAt: startedAt
@@ -3972,7 +4078,15 @@ async function startLocalRun(state, runId, options) {
3972
4078
  RIG_SERVER_INTERNAL_EXEC: "1",
3973
4079
  ...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
3974
4080
  ...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
3975
- ...bridgeGitHubToken ? { RIG_GITHUB_TOKEN: bridgeGitHubToken } : {}
4081
+ ...bridgeGitHubToken ? {
4082
+ RIG_GITHUB_TOKEN: bridgeGitHubToken,
4083
+ GITHUB_TOKEN: bridgeGitHubToken,
4084
+ GH_TOKEN: bridgeGitHubToken
4085
+ } : {},
4086
+ ...resumeMode ? {
4087
+ RIG_RUN_RESUME: "1",
4088
+ RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
4089
+ } : {}
3976
4090
  },
3977
4091
  stdio: ["ignore", "pipe", "pipe"]
3978
4092
  });
@@ -4144,7 +4258,7 @@ async function resumeRunRecord(state, input) {
4144
4258
  if (run.status === "completed") {
4145
4259
  throw new Error("Completed runs cannot be resumed.");
4146
4260
  }
4147
- await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
4261
+ await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
4148
4262
  }
4149
4263
  function appendRunMessage(projectRoot, input) {
4150
4264
  const run = readAuthorityRun4(projectRoot, input.runId);
@@ -4228,34 +4342,12 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
4228
4342
  writeQueueState(projectRoot, next);
4229
4343
  return next;
4230
4344
  }
4231
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
4232
- function reconcileOrphanedLocalRuns(state, runs, nowIso) {
4233
- let changed = false;
4234
- for (const run of runs) {
4345
+ var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
4346
+ function collectResumableLocalRuns(state, runs) {
4347
+ return runs.filter((run) => {
4235
4348
  const status = normalizeString(run.status)?.toLowerCase() ?? "";
4236
- const serverPid = run.serverPid;
4237
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
4238
- if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
4239
- continue;
4240
- }
4241
- const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
4242
- patchRunRecord(state.projectRoot, run.runId, {
4243
- status: "failed",
4244
- completedAt: run.completedAt ?? nowIso,
4245
- updatedAt: nowIso,
4246
- errorText: detail
4247
- });
4248
- appendRunLogEntry(state.projectRoot, run.runId, {
4249
- id: `log:${run.runId}:stale-local-run`,
4250
- title: "Run marked stale after server restart",
4251
- detail,
4252
- tone: "error",
4253
- status: "failed",
4254
- createdAt: nowIso
4255
- });
4256
- changed = true;
4257
- }
4258
- return changed;
4349
+ return run.mode === "local" && RESUMABLE_LOCAL_RUN_STATUSES.has(status) && !state.runProcesses.has(run.runId);
4350
+ });
4259
4351
  }
4260
4352
  async function reconcileScheduler(state, reason) {
4261
4353
  if (state.scheduler.reconciling) {
@@ -4270,7 +4362,20 @@ async function reconcileScheduler(state, reason) {
4270
4362
  const queue = readQueueState(state.projectRoot);
4271
4363
  const tasks = await state.snapshotService.getWorkspaceTasks();
4272
4364
  let runs = listAuthorityRuns4(state.projectRoot);
4273
- let changed = reconcileOrphanedLocalRuns(state, runs, new Date().toISOString());
4365
+ let changed = false;
4366
+ const resumableRuns = collectResumableLocalRuns(state, runs);
4367
+ for (const run of resumableRuns) {
4368
+ appendRunLogEntry(state.projectRoot, run.runId, {
4369
+ id: `log:${run.runId}:auto-resume:${Date.now()}`,
4370
+ title: "Run auto-resume scheduled",
4371
+ detail: `Rig server recovered nonterminal run ${run.runId} after ${reason}; resuming the same lifecycle instead of restarting it.`,
4372
+ tone: "info",
4373
+ status: "preparing",
4374
+ createdAt: new Date().toISOString()
4375
+ });
4376
+ await startLocalRun(state, run.runId, { resume: true });
4377
+ changed = true;
4378
+ }
4274
4379
  if (changed) {
4275
4380
  runs = listAuthorityRuns4(state.projectRoot);
4276
4381
  }
@@ -4886,10 +4991,10 @@ function normalizeCommit(value) {
4886
4991
  function asPlainRecord(value) {
4887
4992
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
4888
4993
  }
4889
- var RIG_CONFIG_PACKAGE_VERSION = "0.0.6-alpha.1";
4994
+ var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
4890
4995
  var RIG_CONFIG_DEV_DEPENDENCIES = {
4891
- "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_VERSION}`,
4892
- "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_VERSION}`
4996
+ "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
4997
+ "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
4893
4998
  };
4894
4999
  function repoParts(repoSlug) {
4895
5000
  const [owner, repo] = repoSlug.split("/");
@@ -5243,13 +5348,52 @@ function bearerTokenFromRequest(req) {
5243
5348
  function isLoopbackRequest(req) {
5244
5349
  try {
5245
5350
  const hostname = new URL(req.url).hostname.toLowerCase();
5246
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
5351
+ return hostname === "localhost" || hostname === "rig.local" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
5247
5352
  } catch {
5248
5353
  return false;
5249
5354
  }
5250
5355
  }
5251
5356
  function isPublicRigAuthBootstrapRoute(pathname) {
5252
- 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";
5357
+ 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";
5358
+ }
5359
+ function buildRigInstallScript() {
5360
+ return `#!/usr/bin/env bash
5361
+ set -euo pipefail
5362
+
5363
+ say() {
5364
+ printf 'rig-install: %s
5365
+ ' "$*"
5366
+ }
5367
+
5368
+ if ! command -v bun >/dev/null 2>&1; then
5369
+ say "Bun not found; installing Bun first"
5370
+ curl -fsSL https://bun.sh/install | bash
5371
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
5372
+ export PATH="$BUN_INSTALL/bin:$PATH"
5373
+ fi
5374
+
5375
+ if ! command -v bun >/dev/null 2>&1; then
5376
+ printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
5377
+ ' >&2
5378
+ exit 1
5379
+ fi
5380
+
5381
+ say "Installing @h-rig/cli@latest"
5382
+ bun add -g @h-rig/cli@latest
5383
+
5384
+ export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
5385
+ export PATH="$BUN_INSTALL/bin:$PATH"
5386
+
5387
+ if ! command -v rig >/dev/null 2>&1; then
5388
+ printf 'rig-install: rig installed, but rig is not on PATH. Add %s/bin to PATH and retry.
5389
+ ' "$BUN_INSTALL" >&2
5390
+ exit 1
5391
+ fi
5392
+
5393
+ say "Verifying rig"
5394
+ rig --help >/dev/null
5395
+ say "Done. Run: rig --help"
5396
+ `;
5253
5397
  }
5254
5398
  function normalizePrMode(value) {
5255
5399
  const mode = normalizeString(value);
@@ -5262,6 +5406,10 @@ function authorizeRigHttpRequest(input) {
5262
5406
  const bearer = bearerTokenFromRequest(input.req);
5263
5407
  const store = createGitHubAuthStore(input.projectRoot);
5264
5408
  const storedToken = store.readToken();
5409
+ const session = bearer ? store.readApiSession(bearer) : null;
5410
+ if (session) {
5411
+ return { authorized: true, actor: session.login ?? "github-operator", reason: "github-session" };
5412
+ }
5265
5413
  if (bearer && storedToken && bearer === storedToken) {
5266
5414
  const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
5267
5415
  return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
@@ -5269,8 +5417,11 @@ function authorizeRigHttpRequest(input) {
5269
5417
  if (isPublicRigAuthBootstrapRoute(input.pathname)) {
5270
5418
  return { authorized: true, actor: null, reason: "public-bootstrap" };
5271
5419
  }
5272
- if (!input.serverAuthToken && !storedToken && isLoopbackRequest(input.req)) {
5273
- return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
5420
+ if (!input.serverAuthToken && !storedToken) {
5421
+ if (isLoopbackRequest(input.req)) {
5422
+ return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
5423
+ }
5424
+ return { authorized: false, actor: null, reason: "auth-required" };
5274
5425
  }
5275
5426
  return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
5276
5427
  }
@@ -5501,7 +5652,7 @@ function selectNextWorkspaceTask(projectRoot, tasks) {
5501
5652
  if (runnable.length === 0)
5502
5653
  return null;
5503
5654
  const queue = readQueueState(projectRoot);
5504
- const queueRank = new Map(queue.map((entry, index) => [entry.taskId, { score: entry.score, position: index }]));
5655
+ const queueRank = new Map(queue.map((entry, index) => [String(entry.taskId), { score: entry.score, position: index }]));
5505
5656
  return runnable.toSorted((left, right) => {
5506
5657
  const leftId = taskIdOf(left) ?? "";
5507
5658
  const rightId = taskIdOf(right) ?? "";
@@ -5541,6 +5692,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
5541
5692
  }
5542
5693
  return filtered;
5543
5694
  }
5695
+ function issueAnalysisTargetFor(source) {
5696
+ if (!source)
5697
+ return null;
5698
+ const candidate = source;
5699
+ if (typeof candidate.updateTask !== "function")
5700
+ return null;
5701
+ return {
5702
+ ...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
5703
+ updateTask: candidate.updateTask.bind(candidate),
5704
+ ...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
5705
+ ...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
5706
+ ...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
5707
+ };
5708
+ }
5709
+ function uniqueStringList(value) {
5710
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
5711
+ return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
5712
+ }
5713
+ function taskRecordId(task) {
5714
+ return String(task.id ?? "");
5715
+ }
5544
5716
  function redactRemoteEndpoint(endpoint) {
5545
5717
  const { token, ...rest } = endpoint;
5546
5718
  return {
@@ -5625,9 +5797,16 @@ function createRigServerFetch(state, deps) {
5625
5797
  notifications: state.targets.length
5626
5798
  });
5627
5799
  }
5800
+ if (url.pathname === "/install" && req.method === "GET") {
5801
+ return new Response(buildRigInstallScript(), {
5802
+ headers: {
5803
+ "Content-Type": "text/x-shellscript; charset=utf-8"
5804
+ }
5805
+ });
5806
+ }
5628
5807
  const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
5629
5808
  const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
5630
- const legacyAuthorizedHttpRequest = isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken);
5809
+ const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
5631
5810
  const requestAuth = authorizeRigHttpRequest({
5632
5811
  req,
5633
5812
  pathname: url.pathname,
@@ -5883,6 +6062,67 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
5883
6062
  note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
5884
6063
  });
5885
6064
  }
6065
+ if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
6066
+ const body = await deps.readJsonBody(req);
6067
+ const ids = uniqueStringList(body.ids ?? body.id);
6068
+ const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
6069
+ if (ids.length === 0 && !analyzeAll) {
6070
+ return deps.badRequest("ids is required unless all=true");
6071
+ }
6072
+ const ctx = await getCachedPluginHostContext(state.projectRoot);
6073
+ const [source] = ctx?.taskSourceRegistry.list() ?? [];
6074
+ const target = issueAnalysisTargetFor(source);
6075
+ if (!source || !target) {
6076
+ return deps.badRequest("Configured task source does not support issue-analysis writeback");
6077
+ }
6078
+ const allTasks = [...await source.list()];
6079
+ const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
6080
+ const cached = allTasks.find((task) => taskRecordId(task) === id);
6081
+ if (cached)
6082
+ return cached;
6083
+ return typeof source.get === "function" ? await source.get(id) : undefined;
6084
+ }))).filter((task) => Boolean(task));
6085
+ if (issues.length === 0) {
6086
+ return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
6087
+ }
6088
+ const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
6089
+ const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
6090
+ const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
6091
+ const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
6092
+ const service = createIssueAnalysisService({
6093
+ analyzer: createPiIssueAnalyzer({
6094
+ ...model ? { model } : {},
6095
+ env: { RIG_PROJECT_ROOT: state.projectRoot }
6096
+ }),
6097
+ writeBack: createIssueAnalysisWriteBack({ target })
6098
+ });
6099
+ const reason = normalizeString(body.reason) ?? "http-issue-analysis";
6100
+ let results;
6101
+ try {
6102
+ results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
6103
+ } catch (error) {
6104
+ return deps.jsonResponse({
6105
+ ok: false,
6106
+ error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
6107
+ reason,
6108
+ ids: issues.map((issue) => issue.id)
6109
+ }, 502);
6110
+ }
6111
+ deps.snapshotService.invalidate("issue-analysis-http-run");
6112
+ await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
6113
+ return;
6114
+ });
6115
+ deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
6116
+ return deps.jsonResponse({
6117
+ ok: true,
6118
+ reason,
6119
+ analyzed: results.map((entry) => ({
6120
+ id: entry.issue.id,
6121
+ title: entry.issue.title ?? null,
6122
+ result: entry.result
6123
+ }))
6124
+ });
6125
+ }
5886
6126
  if (url.pathname === "/api/server/status") {
5887
6127
  const config = buildProjectConfigStatus(state.projectRoot);
5888
6128
  const taskSource = await buildTaskSourceStatus(state, config);
@@ -6023,6 +6263,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6023
6263
  }
6024
6264
  const normalizedRoot = resolve18(requestedRoot);
6025
6265
  const exists = existsSync11(normalizedRoot);
6266
+ if (exists) {
6267
+ createGitHubAuthStore(state.projectRoot).copyToProjectRoot(normalizedRoot);
6268
+ }
6026
6269
  const control = buildServerControlStatus();
6027
6270
  const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
6028
6271
  if (!exists) {
@@ -6111,21 +6354,30 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6111
6354
  const body = await deps.readJsonBody(req);
6112
6355
  const token = normalizeString(body.token);
6113
6356
  const selectedRepo = normalizeString(body.selectedRepo);
6357
+ const requestedProjectRoot = normalizeString(body.projectRoot);
6114
6358
  if (!token) {
6115
6359
  return deps.badRequest("token is required");
6116
6360
  }
6117
6361
  try {
6118
6362
  const user = await fetchGitHubUserInfo(token);
6119
- const store = createGitHubAuthStore(state.projectRoot);
6120
- store.saveToken({
6121
- token,
6122
- tokenSource: "manual-token",
6123
- login: user.login,
6124
- userId: user.userId,
6125
- scopes: user.scopes,
6126
- selectedRepo
6127
- });
6128
- return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
6363
+ const storeRoots = [
6364
+ state.projectRoot,
6365
+ ...requestedProjectRoot && isAbsolute3(requestedProjectRoot) && existsSync11(resolve18(requestedProjectRoot)) ? [resolve18(requestedProjectRoot)] : []
6366
+ ].filter((root, index, roots) => roots.indexOf(root) === index);
6367
+ const stores = storeRoots.map((root) => createGitHubAuthStore(root));
6368
+ for (const store2 of stores) {
6369
+ store2.saveToken({
6370
+ token,
6371
+ tokenSource: "manual-token",
6372
+ login: user.login,
6373
+ userId: user.userId,
6374
+ scopes: user.scopes,
6375
+ selectedRepo
6376
+ });
6377
+ }
6378
+ const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
6379
+ const apiSession = store.createApiSession();
6380
+ return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
6129
6381
  } catch (error) {
6130
6382
  const message = error instanceof Error ? error.message : String(error);
6131
6383
  return deps.jsonResponse({ ok: false, error: message }, 400);
@@ -6193,7 +6445,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6193
6445
  const token = result.payload.access_token;
6194
6446
  const user = await fetchGitHubUserInfo(token);
6195
6447
  store.saveToken({ token, tokenSource: "oauth-device", login: user.login, userId: user.userId, scopes: user.scopes });
6196
- return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }) });
6448
+ const apiSession = store.createApiSession();
6449
+ return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }), apiSessionToken: apiSession.token });
6197
6450
  }
6198
6451
  if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
6199
6452
  const body = await deps.readJsonBody(req);
@@ -6565,11 +6818,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6565
6818
  const runId = normalizeString(body.runId);
6566
6819
  const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
6567
6820
  const promptOverride = normalizeString(body.promptOverride);
6821
+ const restart = body.restart === true;
6568
6822
  if (!runId) {
6569
6823
  return deps.badRequest("runId is required");
6570
6824
  }
6571
6825
  try {
6572
- await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
6826
+ await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
6573
6827
  deps.broadcastSnapshotInvalidation(state);
6574
6828
  return deps.jsonResponse({ ok: true, runId, createdAt });
6575
6829
  } catch (error) {
@@ -11999,7 +12253,7 @@ async function readWorkspaceTasks(projectRoot) {
11999
12253
  description: normalizeString(entry.description),
12000
12254
  acceptanceCriteria: normalizeString(entry.acceptance_criteria),
12001
12255
  status: normalizedStatus ?? "unknown",
12002
- sourceStatus: normalizedStatus ? null : rawStatus,
12256
+ sourceStatus: normalizedStatus && rawStatus !== normalizedStatus ? rawStatus : normalizedStatus ? null : rawStatus,
12003
12257
  priority: typeof entry.priority === "number" ? entry.priority : typeof entry.priority === "string" ? Number(entry.priority) : null,
12004
12258
  issueType: normalizeString(entry.issue_type),
12005
12259
  role: normalizeString(config.role) ?? null,
@@ -12479,6 +12733,7 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
12479
12733
  const server = Bun.serve({
12480
12734
  hostname: options.host,
12481
12735
  port: options.port,
12736
+ idleTimeout: Math.max(10, Math.min(255, Number.parseInt(process.env.RIG_SERVER_IDLE_TIMEOUT_SECONDS || "255", 10) || 255)),
12482
12737
  fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
12483
12738
  websocket: {
12484
12739
  open(ws) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@h-rig/server",
3
- "version": "0.0.6-alpha.1",
3
+ "version": "0.0.6-alpha.10",
4
4
  "type": "module",
5
5
  "description": "Rig package",
6
6
  "license": "UNLICENSED",
@@ -25,9 +25,9 @@
25
25
  "rig-server": "./dist/src/server.js"
26
26
  },
27
27
  "dependencies": {
28
- "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.1",
29
- "@rig/core": "npm:@h-rig/core@0.0.6-alpha.1",
30
- "@rig/runtime": "npm:@h-rig/runtime@0.0.6-alpha.1",
28
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.10",
29
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.10",
30
+ "@rig/runtime": "npm:@h-rig/runtime@0.0.6-alpha.10",
31
31
  "effect": "4.0.0-beta.78"
32
32
  }
33
33
  }