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