@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
package/dist/src/index.js
CHANGED
|
@@ -2293,7 +2293,7 @@ async function buildRigSnapshotPayload(projectRoot, readers) {
|
|
|
2293
2293
|
const taskSummaries = (await readers.readWorkspaceTasks(projectRoot)).map((task) => ({
|
|
2294
2294
|
...toTaskSummary(workspace.id, {
|
|
2295
2295
|
...task,
|
|
2296
|
-
status: queuedTaskIds.has(task.id) && (task.status === "open" || task.status === "ready" || task.status === "draft") ? "queued" : task.status
|
|
2296
|
+
status: queuedTaskIds.has(task.id) && (task.status === "open" || task.status === "ready" || task.status === "draft" || task.status === "failed" || task.sourceStatus === "failed") ? "queued" : task.status
|
|
2297
2297
|
}),
|
|
2298
2298
|
graphId: graph.id
|
|
2299
2299
|
}));
|
|
@@ -2819,7 +2819,6 @@ function createGitHubTaskReconciler(input) {
|
|
|
2819
2819
|
}
|
|
2820
2820
|
|
|
2821
2821
|
// packages/server/src/server-helpers/issue-analysis.ts
|
|
2822
|
-
import { execFile } from "child_process";
|
|
2823
2822
|
import { createHash } from "crypto";
|
|
2824
2823
|
function stableIssueHash(issue) {
|
|
2825
2824
|
const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
|
|
@@ -2953,16 +2952,33 @@ function parseIssueAnalysisResult(raw) {
|
|
|
2953
2952
|
return result;
|
|
2954
2953
|
}
|
|
2955
2954
|
function createDefaultPiIssueAnalysisCommandRunner() {
|
|
2956
|
-
return (command, args, options) =>
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
const exitCode = typeof error?.code === "number" ? error.code : error ? 1 : 0;
|
|
2963
|
-
resolve11({ exitCode, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
|
|
2955
|
+
return async (command, args, options) => {
|
|
2956
|
+
const env = options.env ? { ...process.env, ...options.env } : process.env;
|
|
2957
|
+
const proc = Bun.spawn([command, ...args], {
|
|
2958
|
+
stdout: "pipe",
|
|
2959
|
+
stderr: "pipe",
|
|
2960
|
+
env
|
|
2964
2961
|
});
|
|
2965
|
-
|
|
2962
|
+
let timedOut = false;
|
|
2963
|
+
const timer = setTimeout(() => {
|
|
2964
|
+
timedOut = true;
|
|
2965
|
+
proc.kill();
|
|
2966
|
+
}, options.timeoutMs);
|
|
2967
|
+
try {
|
|
2968
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2969
|
+
new Response(proc.stdout).text(),
|
|
2970
|
+
new Response(proc.stderr).text(),
|
|
2971
|
+
proc.exited
|
|
2972
|
+
]);
|
|
2973
|
+
return {
|
|
2974
|
+
exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
|
|
2975
|
+
stdout,
|
|
2976
|
+
stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
|
|
2977
|
+
};
|
|
2978
|
+
} finally {
|
|
2979
|
+
clearTimeout(timer);
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2966
2982
|
}
|
|
2967
2983
|
function createPiIssueAnalyzer(input = {}) {
|
|
2968
2984
|
const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
|
|
@@ -2970,7 +2986,10 @@ function createPiIssueAnalyzer(input = {}) {
|
|
|
2970
2986
|
const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
|
|
2971
2987
|
return async ({ prompt }) => {
|
|
2972
2988
|
const args = ["--print", "--mode", "json", "--no-session"];
|
|
2973
|
-
const
|
|
2989
|
+
const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
|
|
2990
|
+
const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
|
|
2991
|
+
if (provider)
|
|
2992
|
+
args.push("--provider", provider);
|
|
2974
2993
|
if (model)
|
|
2975
2994
|
args.push("--model", model);
|
|
2976
2995
|
args.push(prompt);
|
|
@@ -3811,6 +3830,7 @@ function summarizeRunValidationFailure(projectRoot, run) {
|
|
|
3811
3830
|
}
|
|
3812
3831
|
|
|
3813
3832
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
3833
|
+
import { randomBytes } from "crypto";
|
|
3814
3834
|
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
3815
3835
|
import { resolve as resolve13 } from "path";
|
|
3816
3836
|
function cleanString(value) {
|
|
@@ -3824,6 +3844,24 @@ function cleanScopes(value) {
|
|
|
3824
3844
|
return clean ? [clean] : [];
|
|
3825
3845
|
});
|
|
3826
3846
|
}
|
|
3847
|
+
function parseApiSessions(value) {
|
|
3848
|
+
if (!Array.isArray(value))
|
|
3849
|
+
return [];
|
|
3850
|
+
return value.flatMap((entry) => {
|
|
3851
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3852
|
+
return [];
|
|
3853
|
+
const record = entry;
|
|
3854
|
+
const token = cleanString(record.token);
|
|
3855
|
+
if (!token)
|
|
3856
|
+
return [];
|
|
3857
|
+
return [{
|
|
3858
|
+
token,
|
|
3859
|
+
login: cleanString(record.login),
|
|
3860
|
+
userId: cleanString(record.userId),
|
|
3861
|
+
createdAt: cleanString(record.createdAt) ?? undefined
|
|
3862
|
+
}];
|
|
3863
|
+
});
|
|
3864
|
+
}
|
|
3827
3865
|
function readStoredAuth(stateFile) {
|
|
3828
3866
|
if (!existsSync6(stateFile))
|
|
3829
3867
|
return {};
|
|
@@ -3837,6 +3875,7 @@ function readStoredAuth(stateFile) {
|
|
|
3837
3875
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
3838
3876
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
3839
3877
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
3878
|
+
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
3840
3879
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
3841
3880
|
};
|
|
3842
3881
|
} catch {
|
|
@@ -3855,6 +3894,9 @@ function parsePendingDevice(value) {
|
|
|
3855
3894
|
return null;
|
|
3856
3895
|
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
3857
3896
|
}
|
|
3897
|
+
function newApiSessionToken() {
|
|
3898
|
+
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
3899
|
+
}
|
|
3858
3900
|
function writeStoredAuth(stateFile, payload) {
|
|
3859
3901
|
mkdirSync6(resolve13(stateFile, ".."), { recursive: true });
|
|
3860
3902
|
writeFileSync5(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
@@ -3897,9 +3939,38 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
3897
3939
|
scopes: input.scopes ?? [],
|
|
3898
3940
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
3899
3941
|
pendingDevice: null,
|
|
3942
|
+
apiSessions: previous.apiSessions ?? [],
|
|
3900
3943
|
updatedAt: new Date().toISOString()
|
|
3901
3944
|
});
|
|
3902
3945
|
},
|
|
3946
|
+
createApiSession() {
|
|
3947
|
+
const previous = readStoredAuth(stateFile);
|
|
3948
|
+
const token = newApiSessionToken();
|
|
3949
|
+
const session = {
|
|
3950
|
+
token,
|
|
3951
|
+
login: cleanString(previous.login),
|
|
3952
|
+
userId: cleanString(previous.userId),
|
|
3953
|
+
createdAt: new Date().toISOString()
|
|
3954
|
+
};
|
|
3955
|
+
writeStoredAuth(stateFile, {
|
|
3956
|
+
...previous,
|
|
3957
|
+
apiSessions: [...(previous.apiSessions ?? []).slice(-9), session],
|
|
3958
|
+
updatedAt: new Date().toISOString()
|
|
3959
|
+
});
|
|
3960
|
+
return { token, login: session.login ?? null, userId: session.userId ?? null };
|
|
3961
|
+
},
|
|
3962
|
+
readApiSession(token) {
|
|
3963
|
+
const clean = cleanString(token);
|
|
3964
|
+
if (!clean)
|
|
3965
|
+
return null;
|
|
3966
|
+
const previous = readStoredAuth(stateFile);
|
|
3967
|
+
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
3968
|
+
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
3969
|
+
},
|
|
3970
|
+
copyToProjectRoot(projectRoot2) {
|
|
3971
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot2);
|
|
3972
|
+
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
3973
|
+
},
|
|
3903
3974
|
savePendingDevice(input) {
|
|
3904
3975
|
const previous = readStoredAuth(stateFile);
|
|
3905
3976
|
writeStoredAuth(stateFile, {
|
|
@@ -4197,15 +4268,36 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
|
|
|
4197
4268
|
if (!run.taskId)
|
|
4198
4269
|
return;
|
|
4199
4270
|
const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4271
|
+
try {
|
|
4272
|
+
const result = await syncGitHubProjectStatusForTaskUpdate({
|
|
4273
|
+
taskId: run.taskId,
|
|
4274
|
+
status,
|
|
4275
|
+
issueNodeId,
|
|
4276
|
+
token: createGitHubAuthStore(projectRoot).readToken(),
|
|
4277
|
+
config
|
|
4278
|
+
});
|
|
4279
|
+
if (!result.synced && result.reason !== "project-sync-disabled") {
|
|
4280
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
4281
|
+
id: `log:${run.runId}:github-project-sync:${status}`,
|
|
4282
|
+
title: "GitHub Project sync skipped",
|
|
4283
|
+
detail: `Project status sync for ${run.taskId} could not run: ${result.reason}.`,
|
|
4284
|
+
tone: "warn",
|
|
4285
|
+
status: "running",
|
|
4286
|
+
createdAt: new Date().toISOString(),
|
|
4287
|
+
payload: { reason: result.reason, issueNodeId }
|
|
4288
|
+
});
|
|
4289
|
+
}
|
|
4290
|
+
} catch (error) {
|
|
4291
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
4292
|
+
id: `log:${run.runId}:github-project-sync-error:${status}`,
|
|
4293
|
+
title: "GitHub Project sync failed",
|
|
4294
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
4295
|
+
tone: "error",
|
|
4296
|
+
status: "running",
|
|
4297
|
+
createdAt: new Date().toISOString(),
|
|
4298
|
+
payload: { issueNodeId }
|
|
4299
|
+
});
|
|
4300
|
+
}
|
|
4209
4301
|
}
|
|
4210
4302
|
async function autoAssignRunIssue(projectRoot, run) {
|
|
4211
4303
|
if (!run.taskId)
|
|
@@ -4307,11 +4399,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
|
|
|
4307
4399
|
return;
|
|
4308
4400
|
throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
|
|
4309
4401
|
}
|
|
4402
|
+
async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
|
|
4403
|
+
const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
4404
|
+
if (fromReader)
|
|
4405
|
+
return fromReader;
|
|
4406
|
+
const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
|
|
4407
|
+
if (projected)
|
|
4408
|
+
return projected;
|
|
4409
|
+
if (readTasks !== readWorkspaceTasks) {
|
|
4410
|
+
return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
4411
|
+
}
|
|
4412
|
+
return null;
|
|
4413
|
+
}
|
|
4310
4414
|
async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
|
|
4311
4415
|
if ("taskId" in input && input.taskId) {
|
|
4312
4416
|
assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
|
|
4313
4417
|
}
|
|
4314
|
-
const sourceTask = "taskId" in input && input.taskId ?
|
|
4418
|
+
const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
|
|
4315
4419
|
const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
|
|
4316
4420
|
const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
|
|
4317
4421
|
const runRecord = {
|
|
@@ -4383,6 +4487,7 @@ async function startLocalRun(state, runId, options) {
|
|
|
4383
4487
|
throw new Error(`Run not found: ${runId}`);
|
|
4384
4488
|
}
|
|
4385
4489
|
const startedAt = new Date().toISOString();
|
|
4490
|
+
const resumeMode = options?.resume === true;
|
|
4386
4491
|
state.runProcesses.set(runId, {
|
|
4387
4492
|
runId,
|
|
4388
4493
|
child: null,
|
|
@@ -4399,9 +4504,9 @@ async function startLocalRun(state, runId, options) {
|
|
|
4399
4504
|
summary: run.title
|
|
4400
4505
|
});
|
|
4401
4506
|
appendRunLogEntry(state.projectRoot, runId, {
|
|
4402
|
-
id: `log:${runId}:prepare`,
|
|
4403
|
-
title: "Rig task run starting",
|
|
4404
|
-
detail: run.taskId ?? run.title,
|
|
4507
|
+
id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
|
|
4508
|
+
title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
|
|
4509
|
+
detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
|
|
4405
4510
|
tone: "info",
|
|
4406
4511
|
status: "preparing",
|
|
4407
4512
|
createdAt: startedAt
|
|
@@ -4479,7 +4584,15 @@ async function startLocalRun(state, runId, options) {
|
|
|
4479
4584
|
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
4480
4585
|
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
4481
4586
|
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
4482
|
-
...bridgeGitHubToken ? {
|
|
4587
|
+
...bridgeGitHubToken ? {
|
|
4588
|
+
RIG_GITHUB_TOKEN: bridgeGitHubToken,
|
|
4589
|
+
GITHUB_TOKEN: bridgeGitHubToken,
|
|
4590
|
+
GH_TOKEN: bridgeGitHubToken
|
|
4591
|
+
} : {},
|
|
4592
|
+
...resumeMode ? {
|
|
4593
|
+
RIG_RUN_RESUME: "1",
|
|
4594
|
+
RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
|
|
4595
|
+
} : {}
|
|
4483
4596
|
},
|
|
4484
4597
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4485
4598
|
});
|
|
@@ -4651,7 +4764,7 @@ async function resumeRunRecord(state, input) {
|
|
|
4651
4764
|
if (run.status === "completed") {
|
|
4652
4765
|
throw new Error("Completed runs cannot be resumed.");
|
|
4653
4766
|
}
|
|
4654
|
-
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
|
|
4767
|
+
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
|
|
4655
4768
|
}
|
|
4656
4769
|
function appendRunMessage(projectRoot, input) {
|
|
4657
4770
|
const run = readAuthorityRun4(projectRoot, input.runId);
|
|
@@ -4735,34 +4848,12 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
|
|
|
4735
4848
|
writeQueueState(projectRoot, next);
|
|
4736
4849
|
return next;
|
|
4737
4850
|
}
|
|
4738
|
-
var
|
|
4739
|
-
function
|
|
4740
|
-
|
|
4741
|
-
for (const run of runs) {
|
|
4851
|
+
var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
4852
|
+
function collectResumableLocalRuns(state, runs) {
|
|
4853
|
+
return runs.filter((run) => {
|
|
4742
4854
|
const status = normalizeString(run.status)?.toLowerCase() ?? "";
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
|
|
4746
|
-
continue;
|
|
4747
|
-
}
|
|
4748
|
-
const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
|
|
4749
|
-
patchRunRecord(state.projectRoot, run.runId, {
|
|
4750
|
-
status: "failed",
|
|
4751
|
-
completedAt: run.completedAt ?? nowIso2,
|
|
4752
|
-
updatedAt: nowIso2,
|
|
4753
|
-
errorText: detail
|
|
4754
|
-
});
|
|
4755
|
-
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
4756
|
-
id: `log:${run.runId}:stale-local-run`,
|
|
4757
|
-
title: "Run marked stale after server restart",
|
|
4758
|
-
detail,
|
|
4759
|
-
tone: "error",
|
|
4760
|
-
status: "failed",
|
|
4761
|
-
createdAt: nowIso2
|
|
4762
|
-
});
|
|
4763
|
-
changed = true;
|
|
4764
|
-
}
|
|
4765
|
-
return changed;
|
|
4855
|
+
return run.mode === "local" && RESUMABLE_LOCAL_RUN_STATUSES.has(status) && !state.runProcesses.has(run.runId);
|
|
4856
|
+
});
|
|
4766
4857
|
}
|
|
4767
4858
|
async function reconcileScheduler(state, reason) {
|
|
4768
4859
|
if (state.scheduler.reconciling) {
|
|
@@ -4777,7 +4868,20 @@ async function reconcileScheduler(state, reason) {
|
|
|
4777
4868
|
const queue = readQueueState(state.projectRoot);
|
|
4778
4869
|
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
4779
4870
|
let runs = listAuthorityRuns4(state.projectRoot);
|
|
4780
|
-
let changed =
|
|
4871
|
+
let changed = false;
|
|
4872
|
+
const resumableRuns = collectResumableLocalRuns(state, runs);
|
|
4873
|
+
for (const run of resumableRuns) {
|
|
4874
|
+
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
4875
|
+
id: `log:${run.runId}:auto-resume:${Date.now()}`,
|
|
4876
|
+
title: "Run auto-resume scheduled",
|
|
4877
|
+
detail: `Rig server recovered nonterminal run ${run.runId} after ${reason}; resuming the same lifecycle instead of restarting it.`,
|
|
4878
|
+
tone: "info",
|
|
4879
|
+
status: "preparing",
|
|
4880
|
+
createdAt: new Date().toISOString()
|
|
4881
|
+
});
|
|
4882
|
+
await startLocalRun(state, run.runId, { resume: true });
|
|
4883
|
+
changed = true;
|
|
4884
|
+
}
|
|
4781
4885
|
if (changed) {
|
|
4782
4886
|
runs = listAuthorityRuns4(state.projectRoot);
|
|
4783
4887
|
}
|
|
@@ -5393,10 +5497,10 @@ function normalizeCommit(value) {
|
|
|
5393
5497
|
function asPlainRecord(value) {
|
|
5394
5498
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
5395
5499
|
}
|
|
5396
|
-
var
|
|
5500
|
+
var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
|
|
5397
5501
|
var RIG_CONFIG_DEV_DEPENDENCIES = {
|
|
5398
|
-
"@rig/core": `npm:@h-rig/core@${
|
|
5399
|
-
"@rig/standard-plugin": `npm:@h-rig/standard-plugin@${
|
|
5502
|
+
"@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
|
|
5503
|
+
"@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
|
|
5400
5504
|
};
|
|
5401
5505
|
function repoParts(repoSlug) {
|
|
5402
5506
|
const [owner, repo] = repoSlug.split("/");
|
|
@@ -5750,13 +5854,52 @@ function bearerTokenFromRequest(req) {
|
|
|
5750
5854
|
function isLoopbackRequest(req) {
|
|
5751
5855
|
try {
|
|
5752
5856
|
const hostname = new URL(req.url).hostname.toLowerCase();
|
|
5753
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
5857
|
+
return hostname === "localhost" || hostname === "rig.local" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
5754
5858
|
} catch {
|
|
5755
5859
|
return false;
|
|
5756
5860
|
}
|
|
5757
5861
|
}
|
|
5758
5862
|
function isPublicRigAuthBootstrapRoute(pathname) {
|
|
5759
|
-
return pathname === "/" || pathname === "/
|
|
5863
|
+
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";
|
|
5864
|
+
}
|
|
5865
|
+
function buildRigInstallScript() {
|
|
5866
|
+
return `#!/usr/bin/env bash
|
|
5867
|
+
set -euo pipefail
|
|
5868
|
+
|
|
5869
|
+
say() {
|
|
5870
|
+
printf 'rig-install: %s
|
|
5871
|
+
' "$*"
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5874
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
5875
|
+
say "Bun not found; installing Bun first"
|
|
5876
|
+
curl -fsSL https://bun.sh/install | bash
|
|
5877
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
5878
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
5879
|
+
fi
|
|
5880
|
+
|
|
5881
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
5882
|
+
printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
|
|
5883
|
+
' >&2
|
|
5884
|
+
exit 1
|
|
5885
|
+
fi
|
|
5886
|
+
|
|
5887
|
+
say "Installing @h-rig/cli@latest"
|
|
5888
|
+
bun add -g @h-rig/cli@latest
|
|
5889
|
+
|
|
5890
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
5891
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
5892
|
+
|
|
5893
|
+
if ! command -v rig >/dev/null 2>&1; then
|
|
5894
|
+
printf 'rig-install: rig installed, but rig is not on PATH. Add %s/bin to PATH and retry.
|
|
5895
|
+
' "$BUN_INSTALL" >&2
|
|
5896
|
+
exit 1
|
|
5897
|
+
fi
|
|
5898
|
+
|
|
5899
|
+
say "Verifying rig"
|
|
5900
|
+
rig --help >/dev/null
|
|
5901
|
+
say "Done. Run: rig --help"
|
|
5902
|
+
`;
|
|
5760
5903
|
}
|
|
5761
5904
|
function normalizePrMode(value) {
|
|
5762
5905
|
const mode = normalizeString(value);
|
|
@@ -5769,6 +5912,10 @@ function authorizeRigHttpRequest(input) {
|
|
|
5769
5912
|
const bearer = bearerTokenFromRequest(input.req);
|
|
5770
5913
|
const store = createGitHubAuthStore(input.projectRoot);
|
|
5771
5914
|
const storedToken = store.readToken();
|
|
5915
|
+
const session = bearer ? store.readApiSession(bearer) : null;
|
|
5916
|
+
if (session) {
|
|
5917
|
+
return { authorized: true, actor: session.login ?? "github-operator", reason: "github-session" };
|
|
5918
|
+
}
|
|
5772
5919
|
if (bearer && storedToken && bearer === storedToken) {
|
|
5773
5920
|
const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
5774
5921
|
return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
|
|
@@ -5776,8 +5923,11 @@ function authorizeRigHttpRequest(input) {
|
|
|
5776
5923
|
if (isPublicRigAuthBootstrapRoute(input.pathname)) {
|
|
5777
5924
|
return { authorized: true, actor: null, reason: "public-bootstrap" };
|
|
5778
5925
|
}
|
|
5779
|
-
if (!input.serverAuthToken && !storedToken
|
|
5780
|
-
|
|
5926
|
+
if (!input.serverAuthToken && !storedToken) {
|
|
5927
|
+
if (isLoopbackRequest(input.req)) {
|
|
5928
|
+
return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
|
|
5929
|
+
}
|
|
5930
|
+
return { authorized: false, actor: null, reason: "auth-required" };
|
|
5781
5931
|
}
|
|
5782
5932
|
return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
|
|
5783
5933
|
}
|
|
@@ -6008,7 +6158,7 @@ function selectNextWorkspaceTask(projectRoot, tasks) {
|
|
|
6008
6158
|
if (runnable.length === 0)
|
|
6009
6159
|
return null;
|
|
6010
6160
|
const queue = readQueueState(projectRoot);
|
|
6011
|
-
const queueRank = new Map(queue.map((entry, index) => [entry.taskId, { score: entry.score, position: index }]));
|
|
6161
|
+
const queueRank = new Map(queue.map((entry, index) => [String(entry.taskId), { score: entry.score, position: index }]));
|
|
6012
6162
|
return runnable.toSorted((left, right) => {
|
|
6013
6163
|
const leftId = taskIdOf(left) ?? "";
|
|
6014
6164
|
const rightId = taskIdOf(right) ?? "";
|
|
@@ -6048,6 +6198,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
|
|
|
6048
6198
|
}
|
|
6049
6199
|
return filtered;
|
|
6050
6200
|
}
|
|
6201
|
+
function issueAnalysisTargetFor(source) {
|
|
6202
|
+
if (!source)
|
|
6203
|
+
return null;
|
|
6204
|
+
const candidate = source;
|
|
6205
|
+
if (typeof candidate.updateTask !== "function")
|
|
6206
|
+
return null;
|
|
6207
|
+
return {
|
|
6208
|
+
...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
|
|
6209
|
+
updateTask: candidate.updateTask.bind(candidate),
|
|
6210
|
+
...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
|
|
6211
|
+
...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
|
|
6212
|
+
...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
|
|
6213
|
+
};
|
|
6214
|
+
}
|
|
6215
|
+
function uniqueStringList(value) {
|
|
6216
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
6217
|
+
return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
|
|
6218
|
+
}
|
|
6219
|
+
function taskRecordId(task) {
|
|
6220
|
+
return String(task.id ?? "");
|
|
6221
|
+
}
|
|
6051
6222
|
function redactRemoteEndpoint(endpoint) {
|
|
6052
6223
|
const { token, ...rest } = endpoint;
|
|
6053
6224
|
return {
|
|
@@ -6132,9 +6303,16 @@ function createRigServerFetch(state, deps) {
|
|
|
6132
6303
|
notifications: state.targets.length
|
|
6133
6304
|
});
|
|
6134
6305
|
}
|
|
6306
|
+
if (url.pathname === "/install" && req.method === "GET") {
|
|
6307
|
+
return new Response(buildRigInstallScript(), {
|
|
6308
|
+
headers: {
|
|
6309
|
+
"Content-Type": "text/x-shellscript; charset=utf-8"
|
|
6310
|
+
}
|
|
6311
|
+
});
|
|
6312
|
+
}
|
|
6135
6313
|
const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
|
|
6136
6314
|
const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
|
|
6137
|
-
const legacyAuthorizedHttpRequest = isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken);
|
|
6315
|
+
const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
|
|
6138
6316
|
const requestAuth = authorizeRigHttpRequest({
|
|
6139
6317
|
req,
|
|
6140
6318
|
pathname: url.pathname,
|
|
@@ -6390,6 +6568,67 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6390
6568
|
note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
|
|
6391
6569
|
});
|
|
6392
6570
|
}
|
|
6571
|
+
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
6572
|
+
const body = await deps.readJsonBody(req);
|
|
6573
|
+
const ids = uniqueStringList(body.ids ?? body.id);
|
|
6574
|
+
const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
|
|
6575
|
+
if (ids.length === 0 && !analyzeAll) {
|
|
6576
|
+
return deps.badRequest("ids is required unless all=true");
|
|
6577
|
+
}
|
|
6578
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot);
|
|
6579
|
+
const [source] = ctx?.taskSourceRegistry.list() ?? [];
|
|
6580
|
+
const target = issueAnalysisTargetFor(source);
|
|
6581
|
+
if (!source || !target) {
|
|
6582
|
+
return deps.badRequest("Configured task source does not support issue-analysis writeback");
|
|
6583
|
+
}
|
|
6584
|
+
const allTasks = [...await source.list()];
|
|
6585
|
+
const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
|
|
6586
|
+
const cached = allTasks.find((task) => taskRecordId(task) === id);
|
|
6587
|
+
if (cached)
|
|
6588
|
+
return cached;
|
|
6589
|
+
return typeof source.get === "function" ? await source.get(id) : undefined;
|
|
6590
|
+
}))).filter((task) => Boolean(task));
|
|
6591
|
+
if (issues.length === 0) {
|
|
6592
|
+
return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
|
|
6593
|
+
}
|
|
6594
|
+
const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
|
|
6595
|
+
const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
|
|
6596
|
+
const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
|
|
6597
|
+
const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
|
|
6598
|
+
const service = createIssueAnalysisService({
|
|
6599
|
+
analyzer: createPiIssueAnalyzer({
|
|
6600
|
+
...model ? { model } : {},
|
|
6601
|
+
env: { RIG_PROJECT_ROOT: state.projectRoot }
|
|
6602
|
+
}),
|
|
6603
|
+
writeBack: createIssueAnalysisWriteBack({ target })
|
|
6604
|
+
});
|
|
6605
|
+
const reason = normalizeString(body.reason) ?? "http-issue-analysis";
|
|
6606
|
+
let results;
|
|
6607
|
+
try {
|
|
6608
|
+
results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
|
|
6609
|
+
} catch (error) {
|
|
6610
|
+
return deps.jsonResponse({
|
|
6611
|
+
ok: false,
|
|
6612
|
+
error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
6613
|
+
reason,
|
|
6614
|
+
ids: issues.map((issue) => issue.id)
|
|
6615
|
+
}, 502);
|
|
6616
|
+
}
|
|
6617
|
+
deps.snapshotService.invalidate("issue-analysis-http-run");
|
|
6618
|
+
await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
|
|
6619
|
+
return;
|
|
6620
|
+
});
|
|
6621
|
+
deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
|
|
6622
|
+
return deps.jsonResponse({
|
|
6623
|
+
ok: true,
|
|
6624
|
+
reason,
|
|
6625
|
+
analyzed: results.map((entry) => ({
|
|
6626
|
+
id: entry.issue.id,
|
|
6627
|
+
title: entry.issue.title ?? null,
|
|
6628
|
+
result: entry.result
|
|
6629
|
+
}))
|
|
6630
|
+
});
|
|
6631
|
+
}
|
|
6393
6632
|
if (url.pathname === "/api/server/status") {
|
|
6394
6633
|
const config = buildProjectConfigStatus(state.projectRoot);
|
|
6395
6634
|
const taskSource = await buildTaskSourceStatus(state, config);
|
|
@@ -6530,6 +6769,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6530
6769
|
}
|
|
6531
6770
|
const normalizedRoot = resolve18(requestedRoot);
|
|
6532
6771
|
const exists = existsSync11(normalizedRoot);
|
|
6772
|
+
if (exists) {
|
|
6773
|
+
createGitHubAuthStore(state.projectRoot).copyToProjectRoot(normalizedRoot);
|
|
6774
|
+
}
|
|
6533
6775
|
const control = buildServerControlStatus();
|
|
6534
6776
|
const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
|
|
6535
6777
|
if (!exists) {
|
|
@@ -6618,21 +6860,30 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6618
6860
|
const body = await deps.readJsonBody(req);
|
|
6619
6861
|
const token = normalizeString(body.token);
|
|
6620
6862
|
const selectedRepo = normalizeString(body.selectedRepo);
|
|
6863
|
+
const requestedProjectRoot = normalizeString(body.projectRoot);
|
|
6621
6864
|
if (!token) {
|
|
6622
6865
|
return deps.badRequest("token is required");
|
|
6623
6866
|
}
|
|
6624
6867
|
try {
|
|
6625
6868
|
const user = await fetchGitHubUserInfo(token);
|
|
6626
|
-
const
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6869
|
+
const storeRoots = [
|
|
6870
|
+
state.projectRoot,
|
|
6871
|
+
...requestedProjectRoot && isAbsolute3(requestedProjectRoot) && existsSync11(resolve18(requestedProjectRoot)) ? [resolve18(requestedProjectRoot)] : []
|
|
6872
|
+
].filter((root, index, roots) => roots.indexOf(root) === index);
|
|
6873
|
+
const stores = storeRoots.map((root) => createGitHubAuthStore(root));
|
|
6874
|
+
for (const store2 of stores) {
|
|
6875
|
+
store2.saveToken({
|
|
6876
|
+
token,
|
|
6877
|
+
tokenSource: "manual-token",
|
|
6878
|
+
login: user.login,
|
|
6879
|
+
userId: user.userId,
|
|
6880
|
+
scopes: user.scopes,
|
|
6881
|
+
selectedRepo
|
|
6882
|
+
});
|
|
6883
|
+
}
|
|
6884
|
+
const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
|
|
6885
|
+
const apiSession = store.createApiSession();
|
|
6886
|
+
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
|
|
6636
6887
|
} catch (error) {
|
|
6637
6888
|
const message = error instanceof Error ? error.message : String(error);
|
|
6638
6889
|
return deps.jsonResponse({ ok: false, error: message }, 400);
|
|
@@ -6700,7 +6951,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6700
6951
|
const token = result.payload.access_token;
|
|
6701
6952
|
const user = await fetchGitHubUserInfo(token);
|
|
6702
6953
|
store.saveToken({ token, tokenSource: "oauth-device", login: user.login, userId: user.userId, scopes: user.scopes });
|
|
6703
|
-
|
|
6954
|
+
const apiSession = store.createApiSession();
|
|
6955
|
+
return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }), apiSessionToken: apiSession.token });
|
|
6704
6956
|
}
|
|
6705
6957
|
if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
|
|
6706
6958
|
const body = await deps.readJsonBody(req);
|
|
@@ -7072,11 +7324,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7072
7324
|
const runId = normalizeString(body.runId);
|
|
7073
7325
|
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
7074
7326
|
const promptOverride = normalizeString(body.promptOverride);
|
|
7327
|
+
const restart = body.restart === true;
|
|
7075
7328
|
if (!runId) {
|
|
7076
7329
|
return deps.badRequest("runId is required");
|
|
7077
7330
|
}
|
|
7078
7331
|
try {
|
|
7079
|
-
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
|
|
7332
|
+
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
|
|
7080
7333
|
deps.broadcastSnapshotInvalidation(state);
|
|
7081
7334
|
return deps.jsonResponse({ ok: true, runId, createdAt });
|
|
7082
7335
|
} catch (error) {
|
|
@@ -12506,7 +12759,7 @@ async function readWorkspaceTasks(projectRoot) {
|
|
|
12506
12759
|
description: normalizeString(entry.description),
|
|
12507
12760
|
acceptanceCriteria: normalizeString(entry.acceptance_criteria),
|
|
12508
12761
|
status: normalizedStatus ?? "unknown",
|
|
12509
|
-
sourceStatus: normalizedStatus ? null : rawStatus,
|
|
12762
|
+
sourceStatus: normalizedStatus && rawStatus !== normalizedStatus ? rawStatus : normalizedStatus ? null : rawStatus,
|
|
12510
12763
|
priority: typeof entry.priority === "number" ? entry.priority : typeof entry.priority === "string" ? Number(entry.priority) : null,
|
|
12511
12764
|
issueType: normalizeString(entry.issue_type),
|
|
12512
12765
|
role: normalizeString(config.role) ?? null,
|
|
@@ -12986,6 +13239,7 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
|
|
|
12986
13239
|
const server = Bun.serve({
|
|
12987
13240
|
hostname: options.host,
|
|
12988
13241
|
port: options.port,
|
|
13242
|
+
idleTimeout: Math.max(10, Math.min(255, Number.parseInt(process.env.RIG_SERVER_IDLE_TIMEOUT_SECONDS || "255", 10) || 255)),
|
|
12989
13243
|
fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
|
|
12990
13244
|
websocket: {
|
|
12991
13245
|
open(ws) {
|