@h-rig/server 0.0.6-alpha.1 → 0.0.6-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.js +331 -77
- package/dist/src/server-helpers/github-auth-store.js +52 -0
- package/dist/src/server-helpers/http-router.js +492 -24
- package/dist/src/server-helpers/issue-analysis.js +30 -11
- package/dist/src/server-helpers/run-mutations.js +188 -85
- 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 +4 -4
- package/dist/src/server.js +332 -77
- package/package.json +4 -4
|
@@ -469,6 +469,273 @@ async function refreshTaskProjection(projectRoot, input) {
|
|
|
469
469
|
});
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
+
// packages/server/src/server-helpers/issue-analysis.ts
|
|
473
|
+
import { createHash } from "crypto";
|
|
474
|
+
function stableIssueHash(issue) {
|
|
475
|
+
const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
|
|
476
|
+
const body = typeof issue.body === "string" ? issue.body : "";
|
|
477
|
+
const title = typeof issue.title === "string" ? issue.title : "";
|
|
478
|
+
return createHash("sha256").update(JSON.stringify({ id: issue.id, title, body, labels, deps: issue.deps, status: issue.status })).digest("hex");
|
|
479
|
+
}
|
|
480
|
+
function renderIssueAnalysisPrompt(input) {
|
|
481
|
+
const issue = input.issue;
|
|
482
|
+
const neighbors = input.neighbors ?? [];
|
|
483
|
+
return [
|
|
484
|
+
"You are Rig issue analysis running inside Pi.",
|
|
485
|
+
"Return JSON only with optional metadataPatch, labelsToAdd, labelsToRemove, and generatedIssues.",
|
|
486
|
+
"Preserve all human-authored issue body content. Only propose edits for Rig-owned metadata/status sections, labels, and generated issues.",
|
|
487
|
+
"Generated issues must be concrete, minimal follow-up tasks and will be labeled rig:generated by Rig.",
|
|
488
|
+
"",
|
|
489
|
+
"Issue:",
|
|
490
|
+
JSON.stringify({
|
|
491
|
+
id: issue.id,
|
|
492
|
+
title: issue.title,
|
|
493
|
+
body: issue.body,
|
|
494
|
+
labels: issue.labels,
|
|
495
|
+
deps: issue.deps,
|
|
496
|
+
status: issue.status
|
|
497
|
+
}, null, 2),
|
|
498
|
+
"",
|
|
499
|
+
"Neighbor tasks:",
|
|
500
|
+
JSON.stringify(neighbors.map((task) => ({ id: task.id, title: task.title, status: task.status, deps: task.deps })), null, 2)
|
|
501
|
+
].join(`
|
|
502
|
+
`);
|
|
503
|
+
}
|
|
504
|
+
function isRecord(value) {
|
|
505
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
506
|
+
}
|
|
507
|
+
function stringArray(value) {
|
|
508
|
+
if (!Array.isArray(value))
|
|
509
|
+
return;
|
|
510
|
+
return value.map(String).filter((entry) => entry.trim().length > 0);
|
|
511
|
+
}
|
|
512
|
+
function generatedIssues(value) {
|
|
513
|
+
if (!Array.isArray(value))
|
|
514
|
+
return;
|
|
515
|
+
return value.flatMap((entry) => {
|
|
516
|
+
if (!isRecord(entry) || typeof entry.title !== "string")
|
|
517
|
+
return [];
|
|
518
|
+
return [{
|
|
519
|
+
title: entry.title,
|
|
520
|
+
body: typeof entry.body === "string" ? entry.body : "",
|
|
521
|
+
labels: stringArray(entry.labels) ?? [],
|
|
522
|
+
...Array.isArray(entry.dependsOn) ? { dependsOn: entry.dependsOn.map(String) } : {}
|
|
523
|
+
}];
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
function findJsonLikeText(value) {
|
|
527
|
+
if (typeof value === "string") {
|
|
528
|
+
const trimmed = value.trim();
|
|
529
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("```"))
|
|
530
|
+
return trimmed;
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
if (Array.isArray(value)) {
|
|
534
|
+
for (const entry of value) {
|
|
535
|
+
const found = findJsonLikeText(entry);
|
|
536
|
+
if (found)
|
|
537
|
+
return found;
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
if (!isRecord(value))
|
|
542
|
+
return null;
|
|
543
|
+
for (const key of ["text", "content", "message", "output_text", "response", "stdout"]) {
|
|
544
|
+
const found = findJsonLikeText(value[key]);
|
|
545
|
+
if (found)
|
|
546
|
+
return found;
|
|
547
|
+
}
|
|
548
|
+
for (const entry of Object.values(value)) {
|
|
549
|
+
const found = findJsonLikeText(entry);
|
|
550
|
+
if (found)
|
|
551
|
+
return found;
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
function candidateAnalysisObject(value) {
|
|
556
|
+
if (!isRecord(value))
|
|
557
|
+
return null;
|
|
558
|
+
if (isRecord(value.result))
|
|
559
|
+
return candidateAnalysisObject(value.result) ?? value.result;
|
|
560
|
+
if (isRecord(value.analysis))
|
|
561
|
+
return candidateAnalysisObject(value.analysis) ?? value.analysis;
|
|
562
|
+
if (isRecord(value.metadataPatch) || Array.isArray(value.labelsToAdd) || Array.isArray(value.labelsToRemove) || Array.isArray(value.generatedIssues)) {
|
|
563
|
+
return value;
|
|
564
|
+
}
|
|
565
|
+
const nested = findJsonLikeText(value);
|
|
566
|
+
if (nested && nested !== JSON.stringify(value)) {
|
|
567
|
+
try {
|
|
568
|
+
const parsedNested = JSON.parse(nested.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim() ?? nested);
|
|
569
|
+
return candidateAnalysisObject(parsedNested);
|
|
570
|
+
} catch {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
function parseIssueAnalysisResult(raw) {
|
|
577
|
+
let parsed = raw;
|
|
578
|
+
if (typeof raw === "string") {
|
|
579
|
+
const trimmed = raw.trim();
|
|
580
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim();
|
|
581
|
+
try {
|
|
582
|
+
parsed = JSON.parse(fenced ?? trimmed);
|
|
583
|
+
} catch {
|
|
584
|
+
const lastJsonLine = trimmed.split(/\r?\n/).reverse().find((line) => line.trim().startsWith("{"));
|
|
585
|
+
parsed = lastJsonLine ? JSON.parse(lastJsonLine) : {};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const candidate = candidateAnalysisObject(parsed);
|
|
589
|
+
if (!candidate)
|
|
590
|
+
return {};
|
|
591
|
+
const result = {};
|
|
592
|
+
if (isRecord(candidate.metadataPatch))
|
|
593
|
+
result.metadataPatch = candidate.metadataPatch;
|
|
594
|
+
const add = stringArray(candidate.labelsToAdd);
|
|
595
|
+
if (add?.length)
|
|
596
|
+
result.labelsToAdd = add;
|
|
597
|
+
const remove = stringArray(candidate.labelsToRemove);
|
|
598
|
+
if (remove?.length)
|
|
599
|
+
result.labelsToRemove = remove;
|
|
600
|
+
const generated = generatedIssues(candidate.generatedIssues);
|
|
601
|
+
if (generated?.length)
|
|
602
|
+
result.generatedIssues = generated;
|
|
603
|
+
return result;
|
|
604
|
+
}
|
|
605
|
+
function createDefaultPiIssueAnalysisCommandRunner() {
|
|
606
|
+
return async (command, args, options) => {
|
|
607
|
+
const env = options.env ? { ...process.env, ...options.env } : process.env;
|
|
608
|
+
const proc = Bun.spawn([command, ...args], {
|
|
609
|
+
stdout: "pipe",
|
|
610
|
+
stderr: "pipe",
|
|
611
|
+
env
|
|
612
|
+
});
|
|
613
|
+
let timedOut = false;
|
|
614
|
+
const timer = setTimeout(() => {
|
|
615
|
+
timedOut = true;
|
|
616
|
+
proc.kill();
|
|
617
|
+
}, options.timeoutMs);
|
|
618
|
+
try {
|
|
619
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
620
|
+
new Response(proc.stdout).text(),
|
|
621
|
+
new Response(proc.stderr).text(),
|
|
622
|
+
proc.exited
|
|
623
|
+
]);
|
|
624
|
+
return {
|
|
625
|
+
exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
|
|
626
|
+
stdout,
|
|
627
|
+
stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
|
|
628
|
+
};
|
|
629
|
+
} finally {
|
|
630
|
+
clearTimeout(timer);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function createPiIssueAnalyzer(input = {}) {
|
|
635
|
+
const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
|
|
636
|
+
const timeoutMs = Math.max(1000, Math.trunc(input.timeoutMs ?? Number(process.env.RIG_ISSUE_ANALYSIS_TIMEOUT_MS ?? 120000)));
|
|
637
|
+
const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
|
|
638
|
+
return async ({ prompt }) => {
|
|
639
|
+
const args = ["--print", "--mode", "json", "--no-session"];
|
|
640
|
+
const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
|
|
641
|
+
const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
|
|
642
|
+
if (provider)
|
|
643
|
+
args.push("--provider", provider);
|
|
644
|
+
if (model)
|
|
645
|
+
args.push("--model", model);
|
|
646
|
+
args.push(prompt);
|
|
647
|
+
const result = await runCommand(piBinary, args, { timeoutMs, ...input.env ? { env: input.env } : {} });
|
|
648
|
+
if (result.exitCode !== 0) {
|
|
649
|
+
throw new Error(`Pi issue analysis failed (exit ${result.exitCode}): ${result.stderr ?? result.stdout}`);
|
|
650
|
+
}
|
|
651
|
+
return parseIssueAnalysisResult(result.stdout);
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function defaultStatusComment(input) {
|
|
655
|
+
const changes = [
|
|
656
|
+
input.result.metadataPatch ? "metadata" : null,
|
|
657
|
+
input.result.labelsToAdd?.length ? `labels added: ${input.result.labelsToAdd.join(", ")}` : null,
|
|
658
|
+
input.result.labelsToRemove?.length ? `labels removed: ${input.result.labelsToRemove.join(", ")}` : null,
|
|
659
|
+
input.result.generatedIssues?.length ? `generated issues: ${input.result.generatedIssues.length}` : null
|
|
660
|
+
].filter((entry) => Boolean(entry));
|
|
661
|
+
if (changes.length === 0)
|
|
662
|
+
return null;
|
|
663
|
+
return [
|
|
664
|
+
"<!-- rig:status-comment -->",
|
|
665
|
+
"### Rig issue analysis",
|
|
666
|
+
"",
|
|
667
|
+
`Analyzed issue ${input.issue.id}${input.reason ? ` (${input.reason})` : ""}.`,
|
|
668
|
+
"",
|
|
669
|
+
...changes.map((change) => `- ${change}`)
|
|
670
|
+
].join(`
|
|
671
|
+
`);
|
|
672
|
+
}
|
|
673
|
+
function uniqueLabels(labels, required = []) {
|
|
674
|
+
return [...new Set([...labels ?? [], ...required].map((label) => label.trim()).filter(Boolean))];
|
|
675
|
+
}
|
|
676
|
+
function createIssueAnalysisWriteBack(input) {
|
|
677
|
+
return async ({ issue, result, reason }) => {
|
|
678
|
+
if (result.metadataPatch && Object.keys(result.metadataPatch).length > 0) {
|
|
679
|
+
if (!input.target.updateTask)
|
|
680
|
+
throw new Error("Issue analysis writeback requires updateTask for metadata patches.");
|
|
681
|
+
await input.target.updateTask(issue.id, { metadata: result.metadataPatch });
|
|
682
|
+
}
|
|
683
|
+
if (result.labelsToAdd?.length) {
|
|
684
|
+
if (!input.target.addLabels)
|
|
685
|
+
throw new Error("Issue analysis writeback requires addLabels for labelsToAdd.");
|
|
686
|
+
await input.target.addLabels(issue.id, uniqueLabels(result.labelsToAdd));
|
|
687
|
+
}
|
|
688
|
+
if (result.labelsToRemove?.length) {
|
|
689
|
+
if (!input.target.removeLabels)
|
|
690
|
+
throw new Error("Issue analysis writeback requires removeLabels for labelsToRemove.");
|
|
691
|
+
await input.target.removeLabels(issue.id, uniqueLabels(result.labelsToRemove));
|
|
692
|
+
}
|
|
693
|
+
const comment = (input.buildStatusComment ?? defaultStatusComment)({ issue, result, reason });
|
|
694
|
+
if (comment?.trim()) {
|
|
695
|
+
if (!input.target.updateTask)
|
|
696
|
+
throw new Error("Issue analysis writeback requires updateTask for sticky status comments.");
|
|
697
|
+
await input.target.updateTask(issue.id, { comment });
|
|
698
|
+
}
|
|
699
|
+
for (const generated of result.generatedIssues ?? []) {
|
|
700
|
+
if (!input.target.createIssue)
|
|
701
|
+
throw new Error("Issue analysis writeback requires createIssue for generated issues.");
|
|
702
|
+
await input.target.createIssue({
|
|
703
|
+
title: generated.title,
|
|
704
|
+
body: generated.dependsOn?.length ? `${generated.body.trimEnd()}
|
|
705
|
+
|
|
706
|
+
depends-on: ${generated.dependsOn.map((dep) => dep.startsWith("#") ? dep : `#${dep}`).join(", ")}
|
|
707
|
+
` : generated.body,
|
|
708
|
+
labels: uniqueLabels(generated.labels, ["rig:generated"])
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
function createIssueAnalysisService(input) {
|
|
714
|
+
const analyzedHashes = new Map;
|
|
715
|
+
return {
|
|
716
|
+
async analyze(issues, options = {}) {
|
|
717
|
+
const results = [];
|
|
718
|
+
const neighbors = options.neighbors ?? issues;
|
|
719
|
+
for (const issue of issues) {
|
|
720
|
+
const hash = stableIssueHash(issue);
|
|
721
|
+
if (analyzedHashes.get(issue.id) === hash)
|
|
722
|
+
continue;
|
|
723
|
+
const prompt = renderIssueAnalysisPrompt({ issue, neighbors: neighbors.filter((candidate) => candidate.id !== issue.id) });
|
|
724
|
+
const result = await input.analyzer({ issue, neighbors, prompt });
|
|
725
|
+
analyzedHashes.set(issue.id, hash);
|
|
726
|
+
if (result.metadataPatch || result.labelsToAdd?.length || result.labelsToRemove?.length || result.generatedIssues?.length) {
|
|
727
|
+
await input.writeBack?.({ issue, result, reason: options.reason });
|
|
728
|
+
}
|
|
729
|
+
results.push({ issue, result });
|
|
730
|
+
}
|
|
731
|
+
return results;
|
|
732
|
+
},
|
|
733
|
+
clearCache() {
|
|
734
|
+
analyzedHashes.clear();
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
472
739
|
// packages/server/src/server-helpers/terminal-runtime.ts
|
|
473
740
|
import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
|
|
474
741
|
|
|
@@ -535,6 +802,7 @@ import {
|
|
|
535
802
|
} from "@rig/runtime/control-plane/authority-files";
|
|
536
803
|
|
|
537
804
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
805
|
+
import { randomBytes } from "crypto";
|
|
538
806
|
import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
539
807
|
import { resolve as resolve7 } from "path";
|
|
540
808
|
function cleanString(value) {
|
|
@@ -548,6 +816,24 @@ function cleanScopes(value) {
|
|
|
548
816
|
return clean ? [clean] : [];
|
|
549
817
|
});
|
|
550
818
|
}
|
|
819
|
+
function parseApiSessions(value) {
|
|
820
|
+
if (!Array.isArray(value))
|
|
821
|
+
return [];
|
|
822
|
+
return value.flatMap((entry) => {
|
|
823
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
824
|
+
return [];
|
|
825
|
+
const record = entry;
|
|
826
|
+
const token = cleanString(record.token);
|
|
827
|
+
if (!token)
|
|
828
|
+
return [];
|
|
829
|
+
return [{
|
|
830
|
+
token,
|
|
831
|
+
login: cleanString(record.login),
|
|
832
|
+
userId: cleanString(record.userId),
|
|
833
|
+
createdAt: cleanString(record.createdAt) ?? undefined
|
|
834
|
+
}];
|
|
835
|
+
});
|
|
836
|
+
}
|
|
551
837
|
function readStoredAuth(stateFile) {
|
|
552
838
|
if (!existsSync4(stateFile))
|
|
553
839
|
return {};
|
|
@@ -561,6 +847,7 @@ function readStoredAuth(stateFile) {
|
|
|
561
847
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
562
848
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
563
849
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
850
|
+
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
564
851
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
565
852
|
};
|
|
566
853
|
} catch {
|
|
@@ -579,6 +866,9 @@ function parsePendingDevice(value) {
|
|
|
579
866
|
return null;
|
|
580
867
|
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
581
868
|
}
|
|
869
|
+
function newApiSessionToken() {
|
|
870
|
+
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
871
|
+
}
|
|
582
872
|
function writeStoredAuth(stateFile, payload) {
|
|
583
873
|
mkdirSync3(resolve7(stateFile, ".."), { recursive: true });
|
|
584
874
|
writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
@@ -621,8 +911,37 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
621
911
|
scopes: input.scopes ?? [],
|
|
622
912
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
623
913
|
pendingDevice: null,
|
|
914
|
+
apiSessions: previous.apiSessions ?? [],
|
|
915
|
+
updatedAt: new Date().toISOString()
|
|
916
|
+
});
|
|
917
|
+
},
|
|
918
|
+
createApiSession() {
|
|
919
|
+
const previous = readStoredAuth(stateFile);
|
|
920
|
+
const token = newApiSessionToken();
|
|
921
|
+
const session = {
|
|
922
|
+
token,
|
|
923
|
+
login: cleanString(previous.login),
|
|
924
|
+
userId: cleanString(previous.userId),
|
|
925
|
+
createdAt: new Date().toISOString()
|
|
926
|
+
};
|
|
927
|
+
writeStoredAuth(stateFile, {
|
|
928
|
+
...previous,
|
|
929
|
+
apiSessions: [...(previous.apiSessions ?? []).slice(-9), session],
|
|
624
930
|
updatedAt: new Date().toISOString()
|
|
625
931
|
});
|
|
932
|
+
return { token, login: session.login ?? null, userId: session.userId ?? null };
|
|
933
|
+
},
|
|
934
|
+
readApiSession(token) {
|
|
935
|
+
const clean = cleanString(token);
|
|
936
|
+
if (!clean)
|
|
937
|
+
return null;
|
|
938
|
+
const previous = readStoredAuth(stateFile);
|
|
939
|
+
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
940
|
+
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
941
|
+
},
|
|
942
|
+
copyToProjectRoot(projectRoot2) {
|
|
943
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot2);
|
|
944
|
+
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
626
945
|
},
|
|
627
946
|
savePendingDevice(input) {
|
|
628
947
|
const previous = readStoredAuth(stateFile);
|
|
@@ -864,7 +1183,7 @@ var TERMINAL_RUN_STATUSES2 = new Set([
|
|
|
864
1183
|
"needs-attention",
|
|
865
1184
|
"stopped"
|
|
866
1185
|
]);
|
|
867
|
-
var
|
|
1186
|
+
var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
868
1187
|
|
|
869
1188
|
// packages/server/src/server-helpers/ws-router.ts
|
|
870
1189
|
import {
|
|
@@ -1171,7 +1490,7 @@ import {
|
|
|
1171
1490
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
1172
1491
|
|
|
1173
1492
|
// packages/server/src/server-helpers/project-registry.ts
|
|
1174
|
-
import { createHash } from "crypto";
|
|
1493
|
+
import { createHash as createHash2 } from "crypto";
|
|
1175
1494
|
import { spawnSync } from "child_process";
|
|
1176
1495
|
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync5 } from "fs";
|
|
1177
1496
|
import { dirname as dirname4, resolve as resolve9 } from "path";
|
|
@@ -1214,7 +1533,7 @@ function hashFile(path) {
|
|
|
1214
1533
|
if (!path)
|
|
1215
1534
|
return null;
|
|
1216
1535
|
try {
|
|
1217
|
-
return
|
|
1536
|
+
return createHash2("sha256").update(readFileSync4(path)).digest("hex");
|
|
1218
1537
|
} catch {
|
|
1219
1538
|
return null;
|
|
1220
1539
|
}
|
|
@@ -1461,10 +1780,10 @@ function normalizeCommit(value) {
|
|
|
1461
1780
|
function asPlainRecord(value) {
|
|
1462
1781
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1463
1782
|
}
|
|
1464
|
-
var
|
|
1783
|
+
var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
|
|
1465
1784
|
var RIG_CONFIG_DEV_DEPENDENCIES = {
|
|
1466
|
-
"@rig/core": `npm:@h-rig/core@${
|
|
1467
|
-
"@rig/standard-plugin": `npm:@h-rig/standard-plugin@${
|
|
1785
|
+
"@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
|
|
1786
|
+
"@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
|
|
1468
1787
|
};
|
|
1469
1788
|
function repoParts(repoSlug) {
|
|
1470
1789
|
const [owner, repo] = repoSlug.split("/");
|
|
@@ -1818,13 +2137,52 @@ function bearerTokenFromRequest(req) {
|
|
|
1818
2137
|
function isLoopbackRequest(req) {
|
|
1819
2138
|
try {
|
|
1820
2139
|
const hostname = new URL(req.url).hostname.toLowerCase();
|
|
1821
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
2140
|
+
return hostname === "localhost" || hostname === "rig.local" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1822
2141
|
} catch {
|
|
1823
2142
|
return false;
|
|
1824
2143
|
}
|
|
1825
2144
|
}
|
|
1826
2145
|
function isPublicRigAuthBootstrapRoute(pathname) {
|
|
1827
|
-
return pathname === "/" || pathname === "/
|
|
2146
|
+
return pathname === "/" || pathname === "/install" || pathname === "/health" || pathname === "/api/health" || pathname === "/api/github/auth/status" || pathname === "/api/github/auth/token" || pathname === "/api/github/auth/device/start" || pathname === "/api/github/auth/device/poll";
|
|
2147
|
+
}
|
|
2148
|
+
function buildRigInstallScript() {
|
|
2149
|
+
return `#!/usr/bin/env bash
|
|
2150
|
+
set -euo pipefail
|
|
2151
|
+
|
|
2152
|
+
say() {
|
|
2153
|
+
printf 'rig-install: %s
|
|
2154
|
+
' "$*"
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
2158
|
+
say "Bun not found; installing Bun first"
|
|
2159
|
+
curl -fsSL https://bun.sh/install | bash
|
|
2160
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
2161
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
2162
|
+
fi
|
|
2163
|
+
|
|
2164
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
2165
|
+
printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
|
|
2166
|
+
' >&2
|
|
2167
|
+
exit 1
|
|
2168
|
+
fi
|
|
2169
|
+
|
|
2170
|
+
say "Installing @h-rig/cli@latest"
|
|
2171
|
+
bun add -g @h-rig/cli@latest
|
|
2172
|
+
|
|
2173
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
2174
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
2175
|
+
|
|
2176
|
+
if ! command -v rig >/dev/null 2>&1; then
|
|
2177
|
+
printf 'rig-install: rig installed, but rig is not on PATH. Add %s/bin to PATH and retry.
|
|
2178
|
+
' "$BUN_INSTALL" >&2
|
|
2179
|
+
exit 1
|
|
2180
|
+
fi
|
|
2181
|
+
|
|
2182
|
+
say "Verifying rig"
|
|
2183
|
+
rig --help >/dev/null
|
|
2184
|
+
say "Done. Run: rig --help"
|
|
2185
|
+
`;
|
|
1828
2186
|
}
|
|
1829
2187
|
function normalizePrMode(value) {
|
|
1830
2188
|
const mode = normalizeString(value);
|
|
@@ -1837,6 +2195,10 @@ function authorizeRigHttpRequest(input) {
|
|
|
1837
2195
|
const bearer = bearerTokenFromRequest(input.req);
|
|
1838
2196
|
const store = createGitHubAuthStore(input.projectRoot);
|
|
1839
2197
|
const storedToken = store.readToken();
|
|
2198
|
+
const session = bearer ? store.readApiSession(bearer) : null;
|
|
2199
|
+
if (session) {
|
|
2200
|
+
return { authorized: true, actor: session.login ?? "github-operator", reason: "github-session" };
|
|
2201
|
+
}
|
|
1840
2202
|
if (bearer && storedToken && bearer === storedToken) {
|
|
1841
2203
|
const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
1842
2204
|
return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
|
|
@@ -1844,8 +2206,11 @@ function authorizeRigHttpRequest(input) {
|
|
|
1844
2206
|
if (isPublicRigAuthBootstrapRoute(input.pathname)) {
|
|
1845
2207
|
return { authorized: true, actor: null, reason: "public-bootstrap" };
|
|
1846
2208
|
}
|
|
1847
|
-
if (!input.serverAuthToken && !storedToken
|
|
1848
|
-
|
|
2209
|
+
if (!input.serverAuthToken && !storedToken) {
|
|
2210
|
+
if (isLoopbackRequest(input.req)) {
|
|
2211
|
+
return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
|
|
2212
|
+
}
|
|
2213
|
+
return { authorized: false, actor: null, reason: "auth-required" };
|
|
1849
2214
|
}
|
|
1850
2215
|
return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
|
|
1851
2216
|
}
|
|
@@ -2076,7 +2441,7 @@ function selectNextWorkspaceTask(projectRoot, tasks) {
|
|
|
2076
2441
|
if (runnable.length === 0)
|
|
2077
2442
|
return null;
|
|
2078
2443
|
const queue = readQueueState(projectRoot);
|
|
2079
|
-
const queueRank = new Map(queue.map((entry, index) => [entry.taskId, { score: entry.score, position: index }]));
|
|
2444
|
+
const queueRank = new Map(queue.map((entry, index) => [String(entry.taskId), { score: entry.score, position: index }]));
|
|
2080
2445
|
return runnable.toSorted((left, right) => {
|
|
2081
2446
|
const leftId = taskIdOf(left) ?? "";
|
|
2082
2447
|
const rightId = taskIdOf(right) ?? "";
|
|
@@ -2116,6 +2481,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
|
|
|
2116
2481
|
}
|
|
2117
2482
|
return filtered;
|
|
2118
2483
|
}
|
|
2484
|
+
function issueAnalysisTargetFor(source) {
|
|
2485
|
+
if (!source)
|
|
2486
|
+
return null;
|
|
2487
|
+
const candidate = source;
|
|
2488
|
+
if (typeof candidate.updateTask !== "function")
|
|
2489
|
+
return null;
|
|
2490
|
+
return {
|
|
2491
|
+
...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
|
|
2492
|
+
updateTask: candidate.updateTask.bind(candidate),
|
|
2493
|
+
...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
|
|
2494
|
+
...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
|
|
2495
|
+
...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
function uniqueStringList(value) {
|
|
2499
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
2500
|
+
return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
|
|
2501
|
+
}
|
|
2502
|
+
function taskRecordId(task) {
|
|
2503
|
+
return String(task.id ?? "");
|
|
2504
|
+
}
|
|
2119
2505
|
function redactRemoteEndpoint(endpoint) {
|
|
2120
2506
|
const { token, ...rest } = endpoint;
|
|
2121
2507
|
return {
|
|
@@ -2200,9 +2586,16 @@ function createRigServerFetch(state, deps) {
|
|
|
2200
2586
|
notifications: state.targets.length
|
|
2201
2587
|
});
|
|
2202
2588
|
}
|
|
2589
|
+
if (url.pathname === "/install" && req.method === "GET") {
|
|
2590
|
+
return new Response(buildRigInstallScript(), {
|
|
2591
|
+
headers: {
|
|
2592
|
+
"Content-Type": "text/x-shellscript; charset=utf-8"
|
|
2593
|
+
}
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2203
2596
|
const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
|
|
2204
2597
|
const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
|
|
2205
|
-
const legacyAuthorizedHttpRequest = isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken);
|
|
2598
|
+
const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
|
|
2206
2599
|
const requestAuth = authorizeRigHttpRequest({
|
|
2207
2600
|
req,
|
|
2208
2601
|
pathname: url.pathname,
|
|
@@ -2458,6 +2851,67 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2458
2851
|
note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
|
|
2459
2852
|
});
|
|
2460
2853
|
}
|
|
2854
|
+
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
2855
|
+
const body = await deps.readJsonBody(req);
|
|
2856
|
+
const ids = uniqueStringList(body.ids ?? body.id);
|
|
2857
|
+
const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
|
|
2858
|
+
if (ids.length === 0 && !analyzeAll) {
|
|
2859
|
+
return deps.badRequest("ids is required unless all=true");
|
|
2860
|
+
}
|
|
2861
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot);
|
|
2862
|
+
const [source] = ctx?.taskSourceRegistry.list() ?? [];
|
|
2863
|
+
const target = issueAnalysisTargetFor(source);
|
|
2864
|
+
if (!source || !target) {
|
|
2865
|
+
return deps.badRequest("Configured task source does not support issue-analysis writeback");
|
|
2866
|
+
}
|
|
2867
|
+
const allTasks = [...await source.list()];
|
|
2868
|
+
const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
|
|
2869
|
+
const cached = allTasks.find((task) => taskRecordId(task) === id);
|
|
2870
|
+
if (cached)
|
|
2871
|
+
return cached;
|
|
2872
|
+
return typeof source.get === "function" ? await source.get(id) : undefined;
|
|
2873
|
+
}))).filter((task) => Boolean(task));
|
|
2874
|
+
if (issues.length === 0) {
|
|
2875
|
+
return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
|
|
2876
|
+
}
|
|
2877
|
+
const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
|
|
2878
|
+
const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
|
|
2879
|
+
const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
|
|
2880
|
+
const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
|
|
2881
|
+
const service = createIssueAnalysisService({
|
|
2882
|
+
analyzer: createPiIssueAnalyzer({
|
|
2883
|
+
...model ? { model } : {},
|
|
2884
|
+
env: { RIG_PROJECT_ROOT: state.projectRoot }
|
|
2885
|
+
}),
|
|
2886
|
+
writeBack: createIssueAnalysisWriteBack({ target })
|
|
2887
|
+
});
|
|
2888
|
+
const reason = normalizeString(body.reason) ?? "http-issue-analysis";
|
|
2889
|
+
let results;
|
|
2890
|
+
try {
|
|
2891
|
+
results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
|
|
2892
|
+
} catch (error) {
|
|
2893
|
+
return deps.jsonResponse({
|
|
2894
|
+
ok: false,
|
|
2895
|
+
error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2896
|
+
reason,
|
|
2897
|
+
ids: issues.map((issue) => issue.id)
|
|
2898
|
+
}, 502);
|
|
2899
|
+
}
|
|
2900
|
+
deps.snapshotService.invalidate("issue-analysis-http-run");
|
|
2901
|
+
await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
|
|
2902
|
+
return;
|
|
2903
|
+
});
|
|
2904
|
+
deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
|
|
2905
|
+
return deps.jsonResponse({
|
|
2906
|
+
ok: true,
|
|
2907
|
+
reason,
|
|
2908
|
+
analyzed: results.map((entry) => ({
|
|
2909
|
+
id: entry.issue.id,
|
|
2910
|
+
title: entry.issue.title ?? null,
|
|
2911
|
+
result: entry.result
|
|
2912
|
+
}))
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2461
2915
|
if (url.pathname === "/api/server/status") {
|
|
2462
2916
|
const config = buildProjectConfigStatus(state.projectRoot);
|
|
2463
2917
|
const taskSource = await buildTaskSourceStatus(state, config);
|
|
@@ -2598,6 +3052,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2598
3052
|
}
|
|
2599
3053
|
const normalizedRoot = resolve11(requestedRoot);
|
|
2600
3054
|
const exists = existsSync8(normalizedRoot);
|
|
3055
|
+
if (exists) {
|
|
3056
|
+
createGitHubAuthStore(state.projectRoot).copyToProjectRoot(normalizedRoot);
|
|
3057
|
+
}
|
|
2601
3058
|
const control = buildServerControlStatus();
|
|
2602
3059
|
const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
|
|
2603
3060
|
if (!exists) {
|
|
@@ -2686,21 +3143,30 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2686
3143
|
const body = await deps.readJsonBody(req);
|
|
2687
3144
|
const token = normalizeString(body.token);
|
|
2688
3145
|
const selectedRepo = normalizeString(body.selectedRepo);
|
|
3146
|
+
const requestedProjectRoot = normalizeString(body.projectRoot);
|
|
2689
3147
|
if (!token) {
|
|
2690
3148
|
return deps.badRequest("token is required");
|
|
2691
3149
|
}
|
|
2692
3150
|
try {
|
|
2693
3151
|
const user = await fetchGitHubUserInfo(token);
|
|
2694
|
-
const
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
3152
|
+
const storeRoots = [
|
|
3153
|
+
state.projectRoot,
|
|
3154
|
+
...requestedProjectRoot && isAbsolute2(requestedProjectRoot) && existsSync8(resolve11(requestedProjectRoot)) ? [resolve11(requestedProjectRoot)] : []
|
|
3155
|
+
].filter((root, index, roots) => roots.indexOf(root) === index);
|
|
3156
|
+
const stores = storeRoots.map((root) => createGitHubAuthStore(root));
|
|
3157
|
+
for (const store2 of stores) {
|
|
3158
|
+
store2.saveToken({
|
|
3159
|
+
token,
|
|
3160
|
+
tokenSource: "manual-token",
|
|
3161
|
+
login: user.login,
|
|
3162
|
+
userId: user.userId,
|
|
3163
|
+
scopes: user.scopes,
|
|
3164
|
+
selectedRepo
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
3167
|
+
const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
|
|
3168
|
+
const apiSession = store.createApiSession();
|
|
3169
|
+
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
|
|
2704
3170
|
} catch (error) {
|
|
2705
3171
|
const message = error instanceof Error ? error.message : String(error);
|
|
2706
3172
|
return deps.jsonResponse({ ok: false, error: message }, 400);
|
|
@@ -2768,7 +3234,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2768
3234
|
const token = result.payload.access_token;
|
|
2769
3235
|
const user = await fetchGitHubUserInfo(token);
|
|
2770
3236
|
store.saveToken({ token, tokenSource: "oauth-device", login: user.login, userId: user.userId, scopes: user.scopes });
|
|
2771
|
-
|
|
3237
|
+
const apiSession = store.createApiSession();
|
|
3238
|
+
return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }), apiSessionToken: apiSession.token });
|
|
2772
3239
|
}
|
|
2773
3240
|
if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
|
|
2774
3241
|
const body = await deps.readJsonBody(req);
|
|
@@ -3140,11 +3607,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3140
3607
|
const runId = normalizeString(body.runId);
|
|
3141
3608
|
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
3142
3609
|
const promptOverride = normalizeString(body.promptOverride);
|
|
3610
|
+
const restart = body.restart === true;
|
|
3143
3611
|
if (!runId) {
|
|
3144
3612
|
return deps.badRequest("runId is required");
|
|
3145
3613
|
}
|
|
3146
3614
|
try {
|
|
3147
|
-
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
|
|
3615
|
+
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
|
|
3148
3616
|
deps.broadcastSnapshotInvalidation(state);
|
|
3149
3617
|
return deps.jsonResponse({ ok: true, runId, createdAt });
|
|
3150
3618
|
} catch (error) {
|