@h-rig/server 0.0.6-alpha.2 → 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.
package/dist/src/index.js CHANGED
@@ -2970,7 +2970,10 @@ function createPiIssueAnalyzer(input = {}) {
2970
2970
  const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
2971
2971
  return async ({ prompt }) => {
2972
2972
  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";
2973
+ const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
2974
+ const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
2975
+ if (provider)
2976
+ args.push("--provider", provider);
2974
2977
  if (model)
2975
2978
  args.push("--model", model);
2976
2979
  args.push(prompt);
@@ -4552,7 +4555,11 @@ async function startLocalRun(state, runId, options) {
4552
4555
  RIG_SERVER_INTERNAL_EXEC: "1",
4553
4556
  ...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
4554
4557
  ...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
4555
- ...bridgeGitHubToken ? { RIG_GITHUB_TOKEN: bridgeGitHubToken } : {}
4558
+ ...bridgeGitHubToken ? {
4559
+ RIG_GITHUB_TOKEN: bridgeGitHubToken,
4560
+ GITHUB_TOKEN: bridgeGitHubToken,
4561
+ GH_TOKEN: bridgeGitHubToken
4562
+ } : {}
4556
4563
  },
4557
4564
  stdio: ["ignore", "pipe", "pipe"]
4558
4565
  });
@@ -6128,6 +6135,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
6128
6135
  }
6129
6136
  return filtered;
6130
6137
  }
6138
+ function issueAnalysisTargetFor(source) {
6139
+ if (!source)
6140
+ return null;
6141
+ const candidate = source;
6142
+ if (typeof candidate.updateTask !== "function")
6143
+ return null;
6144
+ return {
6145
+ ...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
6146
+ updateTask: candidate.updateTask.bind(candidate),
6147
+ ...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
6148
+ ...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
6149
+ ...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
6150
+ };
6151
+ }
6152
+ function uniqueStringList(value) {
6153
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
6154
+ return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
6155
+ }
6156
+ function taskRecordId(task) {
6157
+ return String(task.id ?? "");
6158
+ }
6131
6159
  function redactRemoteEndpoint(endpoint) {
6132
6160
  const { token, ...rest } = endpoint;
6133
6161
  return {
@@ -6470,6 +6498,67 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6470
6498
  note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
6471
6499
  });
6472
6500
  }
6501
+ if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
6502
+ const body = await deps.readJsonBody(req);
6503
+ const ids = uniqueStringList(body.ids ?? body.id);
6504
+ const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
6505
+ if (ids.length === 0 && !analyzeAll) {
6506
+ return deps.badRequest("ids is required unless all=true");
6507
+ }
6508
+ const ctx = await getCachedPluginHostContext(state.projectRoot);
6509
+ const [source] = ctx?.taskSourceRegistry.list() ?? [];
6510
+ const target = issueAnalysisTargetFor(source);
6511
+ if (!source || !target) {
6512
+ return deps.badRequest("Configured task source does not support issue-analysis writeback");
6513
+ }
6514
+ const allTasks = [...await source.list()];
6515
+ const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
6516
+ const cached = allTasks.find((task) => taskRecordId(task) === id);
6517
+ if (cached)
6518
+ return cached;
6519
+ return typeof source.get === "function" ? await source.get(id) : undefined;
6520
+ }))).filter((task) => Boolean(task));
6521
+ if (issues.length === 0) {
6522
+ return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
6523
+ }
6524
+ const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
6525
+ const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
6526
+ const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
6527
+ const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
6528
+ const service = createIssueAnalysisService({
6529
+ analyzer: createPiIssueAnalyzer({
6530
+ ...model ? { model } : {},
6531
+ env: { RIG_PROJECT_ROOT: state.projectRoot }
6532
+ }),
6533
+ writeBack: createIssueAnalysisWriteBack({ target })
6534
+ });
6535
+ const reason = normalizeString(body.reason) ?? "http-issue-analysis";
6536
+ let results;
6537
+ try {
6538
+ results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
6539
+ } catch (error) {
6540
+ return deps.jsonResponse({
6541
+ ok: false,
6542
+ error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
6543
+ reason,
6544
+ ids: issues.map((issue) => issue.id)
6545
+ }, 502);
6546
+ }
6547
+ deps.snapshotService.invalidate("issue-analysis-http-run");
6548
+ await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
6549
+ return;
6550
+ });
6551
+ deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
6552
+ return deps.jsonResponse({
6553
+ ok: true,
6554
+ reason,
6555
+ analyzed: results.map((entry) => ({
6556
+ id: entry.issue.id,
6557
+ title: entry.issue.title ?? null,
6558
+ result: entry.result
6559
+ }))
6560
+ });
6561
+ }
6473
6562
  if (url.pathname === "/api/server/status") {
6474
6563
  const config = buildProjectConfigStatus(state.projectRoot);
6475
6564
  const taskSource = await buildTaskSourceStatus(state, config);
@@ -13079,6 +13168,7 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
13079
13168
  const server = Bun.serve({
13080
13169
  hostname: options.host,
13081
13170
  port: options.port,
13171
+ idleTimeout: Math.max(10, Math.min(255, Number.parseInt(process.env.RIG_SERVER_IDLE_TIMEOUT_SECONDS || "255", 10) || 255)),
13082
13172
  fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
13083
13173
  websocket: {
13084
13174
  open(ws) {
@@ -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
 
@@ -1223,7 +1474,7 @@ import {
1223
1474
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
1224
1475
 
1225
1476
  // packages/server/src/server-helpers/project-registry.ts
1226
- import { createHash } from "crypto";
1477
+ import { createHash as createHash2 } from "crypto";
1227
1478
  import { spawnSync } from "child_process";
1228
1479
  import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync5 } from "fs";
1229
1480
  import { dirname as dirname4, resolve as resolve9 } from "path";
@@ -1266,7 +1517,7 @@ function hashFile(path) {
1266
1517
  if (!path)
1267
1518
  return null;
1268
1519
  try {
1269
- return createHash("sha256").update(readFileSync4(path)).digest("hex");
1520
+ return createHash2("sha256").update(readFileSync4(path)).digest("hex");
1270
1521
  } catch {
1271
1522
  return null;
1272
1523
  }
@@ -2175,6 +2426,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
2175
2426
  }
2176
2427
  return filtered;
2177
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
+ }
2178
2450
  function redactRemoteEndpoint(endpoint) {
2179
2451
  const { token, ...rest } = endpoint;
2180
2452
  return {
@@ -2517,6 +2789,67 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
2517
2789
  note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
2518
2790
  });
2519
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
+ }
2520
2853
  if (url.pathname === "/api/server/status") {
2521
2854
  const config = buildProjectConfigStatus(state.projectRoot);
2522
2855
  const taskSource = await buildTaskSourceStatus(state, config);
@@ -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);
@@ -1475,7 +1475,11 @@ async function startLocalRun(state, runId, options) {
1475
1475
  RIG_SERVER_INTERNAL_EXEC: "1",
1476
1476
  ...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
1477
1477
  ...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
1478
- ...bridgeGitHubToken ? { RIG_GITHUB_TOKEN: bridgeGitHubToken } : {}
1478
+ ...bridgeGitHubToken ? {
1479
+ RIG_GITHUB_TOKEN: bridgeGitHubToken,
1480
+ GITHUB_TOKEN: bridgeGitHubToken,
1481
+ GH_TOKEN: bridgeGitHubToken
1482
+ } : {}
1479
1483
  },
1480
1484
  stdio: ["ignore", "pipe", "pipe"]
1481
1485
  });
@@ -2463,7 +2463,10 @@ function createPiIssueAnalyzer(input = {}) {
2463
2463
  const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
2464
2464
  return async ({ prompt }) => {
2465
2465
  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";
2466
+ const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
2467
+ const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
2468
+ if (provider)
2469
+ args.push("--provider", provider);
2467
2470
  if (model)
2468
2471
  args.push("--model", model);
2469
2472
  args.push(prompt);
@@ -4045,7 +4048,11 @@ async function startLocalRun(state, runId, options) {
4045
4048
  RIG_SERVER_INTERNAL_EXEC: "1",
4046
4049
  ...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
4047
4050
  ...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
4048
- ...bridgeGitHubToken ? { RIG_GITHUB_TOKEN: bridgeGitHubToken } : {}
4051
+ ...bridgeGitHubToken ? {
4052
+ RIG_GITHUB_TOKEN: bridgeGitHubToken,
4053
+ GITHUB_TOKEN: bridgeGitHubToken,
4054
+ GH_TOKEN: bridgeGitHubToken
4055
+ } : {}
4049
4056
  },
4050
4057
  stdio: ["ignore", "pipe", "pipe"]
4051
4058
  });
@@ -5621,6 +5628,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
5621
5628
  }
5622
5629
  return filtered;
5623
5630
  }
5631
+ function issueAnalysisTargetFor(source) {
5632
+ if (!source)
5633
+ return null;
5634
+ const candidate = source;
5635
+ if (typeof candidate.updateTask !== "function")
5636
+ return null;
5637
+ return {
5638
+ ...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
5639
+ updateTask: candidate.updateTask.bind(candidate),
5640
+ ...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
5641
+ ...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
5642
+ ...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
5643
+ };
5644
+ }
5645
+ function uniqueStringList(value) {
5646
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
5647
+ return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
5648
+ }
5649
+ function taskRecordId(task) {
5650
+ return String(task.id ?? "");
5651
+ }
5624
5652
  function redactRemoteEndpoint(endpoint) {
5625
5653
  const { token, ...rest } = endpoint;
5626
5654
  return {
@@ -5963,6 +5991,67 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
5963
5991
  note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
5964
5992
  });
5965
5993
  }
5994
+ if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
5995
+ const body = await deps.readJsonBody(req);
5996
+ const ids = uniqueStringList(body.ids ?? body.id);
5997
+ const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
5998
+ if (ids.length === 0 && !analyzeAll) {
5999
+ return deps.badRequest("ids is required unless all=true");
6000
+ }
6001
+ const ctx = await getCachedPluginHostContext(state.projectRoot);
6002
+ const [source] = ctx?.taskSourceRegistry.list() ?? [];
6003
+ const target = issueAnalysisTargetFor(source);
6004
+ if (!source || !target) {
6005
+ return deps.badRequest("Configured task source does not support issue-analysis writeback");
6006
+ }
6007
+ const allTasks = [...await source.list()];
6008
+ const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
6009
+ const cached = allTasks.find((task) => taskRecordId(task) === id);
6010
+ if (cached)
6011
+ return cached;
6012
+ return typeof source.get === "function" ? await source.get(id) : undefined;
6013
+ }))).filter((task) => Boolean(task));
6014
+ if (issues.length === 0) {
6015
+ return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
6016
+ }
6017
+ const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
6018
+ const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
6019
+ const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
6020
+ const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
6021
+ const service = createIssueAnalysisService({
6022
+ analyzer: createPiIssueAnalyzer({
6023
+ ...model ? { model } : {},
6024
+ env: { RIG_PROJECT_ROOT: state.projectRoot }
6025
+ }),
6026
+ writeBack: createIssueAnalysisWriteBack({ target })
6027
+ });
6028
+ const reason = normalizeString(body.reason) ?? "http-issue-analysis";
6029
+ let results;
6030
+ try {
6031
+ results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
6032
+ } catch (error) {
6033
+ return deps.jsonResponse({
6034
+ ok: false,
6035
+ error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
6036
+ reason,
6037
+ ids: issues.map((issue) => issue.id)
6038
+ }, 502);
6039
+ }
6040
+ deps.snapshotService.invalidate("issue-analysis-http-run");
6041
+ await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
6042
+ return;
6043
+ });
6044
+ deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
6045
+ return deps.jsonResponse({
6046
+ ok: true,
6047
+ reason,
6048
+ analyzed: results.map((entry) => ({
6049
+ id: entry.issue.id,
6050
+ title: entry.issue.title ?? null,
6051
+ result: entry.result
6052
+ }))
6053
+ });
6054
+ }
5966
6055
  if (url.pathname === "/api/server/status") {
5967
6056
  const config = buildProjectConfigStatus(state.projectRoot);
5968
6057
  const taskSource = await buildTaskSourceStatus(state, config);
@@ -12572,6 +12661,7 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
12572
12661
  const server = Bun.serve({
12573
12662
  hostname: options.host,
12574
12663
  port: options.port,
12664
+ idleTimeout: Math.max(10, Math.min(255, Number.parseInt(process.env.RIG_SERVER_IDLE_TIMEOUT_SECONDS || "255", 10) || 255)),
12575
12665
  fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
12576
12666
  websocket: {
12577
12667
  open(ws) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@h-rig/server",
3
- "version": "0.0.6-alpha.2",
3
+ "version": "0.0.6-alpha.3",
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.2",
29
- "@rig/core": "npm:@h-rig/core@0.0.6-alpha.2",
30
- "@rig/runtime": "npm:@h-rig/runtime@0.0.6-alpha.2",
28
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.3",
29
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.3",
30
+ "@rig/runtime": "npm:@h-rig/runtime@0.0.6-alpha.3",
31
31
  "effect": "4.0.0-beta.78"
32
32
  }
33
33
  }