@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
|
|
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 ? {
|
|
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
|
|
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
|
|
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 ? {
|
|
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
|
});
|
package/dist/src/server.js
CHANGED
|
@@ -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
|
|
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 ? {
|
|
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.
|
|
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.
|
|
29
|
-
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.
|
|
30
|
-
"@rig/runtime": "npm:@h-rig/runtime@0.0.6-alpha.
|
|
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
|
}
|