@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/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
1
2
|
// @bun
|
|
2
3
|
var __require = import.meta.require;
|
|
3
4
|
|
|
@@ -1786,7 +1787,7 @@ async function buildRigSnapshotPayload(projectRoot, readers) {
|
|
|
1786
1787
|
const taskSummaries = (await readers.readWorkspaceTasks(projectRoot)).map((task) => ({
|
|
1787
1788
|
...toTaskSummary(workspace.id, {
|
|
1788
1789
|
...task,
|
|
1789
|
-
status: queuedTaskIds.has(task.id) && (task.status === "open" || task.status === "ready" || task.status === "draft") ? "queued" : task.status
|
|
1790
|
+
status: queuedTaskIds.has(task.id) && (task.status === "open" || task.status === "ready" || task.status === "draft" || task.status === "failed" || task.sourceStatus === "failed") ? "queued" : task.status
|
|
1790
1791
|
}),
|
|
1791
1792
|
graphId: graph.id
|
|
1792
1793
|
}));
|
|
@@ -2312,7 +2313,6 @@ function createGitHubTaskReconciler(input) {
|
|
|
2312
2313
|
}
|
|
2313
2314
|
|
|
2314
2315
|
// packages/server/src/server-helpers/issue-analysis.ts
|
|
2315
|
-
import { execFile } from "child_process";
|
|
2316
2316
|
import { createHash } from "crypto";
|
|
2317
2317
|
function stableIssueHash(issue) {
|
|
2318
2318
|
const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
|
|
@@ -2446,16 +2446,33 @@ function parseIssueAnalysisResult(raw) {
|
|
|
2446
2446
|
return result;
|
|
2447
2447
|
}
|
|
2448
2448
|
function createDefaultPiIssueAnalysisCommandRunner() {
|
|
2449
|
-
return (command, args, options) =>
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
const exitCode = typeof error?.code === "number" ? error.code : error ? 1 : 0;
|
|
2456
|
-
resolve11({ exitCode, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
|
|
2449
|
+
return async (command, args, options) => {
|
|
2450
|
+
const env = options.env ? { ...process.env, ...options.env } : process.env;
|
|
2451
|
+
const proc = Bun.spawn([command, ...args], {
|
|
2452
|
+
stdout: "pipe",
|
|
2453
|
+
stderr: "pipe",
|
|
2454
|
+
env
|
|
2457
2455
|
});
|
|
2458
|
-
|
|
2456
|
+
let timedOut = false;
|
|
2457
|
+
const timer = setTimeout(() => {
|
|
2458
|
+
timedOut = true;
|
|
2459
|
+
proc.kill();
|
|
2460
|
+
}, options.timeoutMs);
|
|
2461
|
+
try {
|
|
2462
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2463
|
+
new Response(proc.stdout).text(),
|
|
2464
|
+
new Response(proc.stderr).text(),
|
|
2465
|
+
proc.exited
|
|
2466
|
+
]);
|
|
2467
|
+
return {
|
|
2468
|
+
exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
|
|
2469
|
+
stdout,
|
|
2470
|
+
stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
|
|
2471
|
+
};
|
|
2472
|
+
} finally {
|
|
2473
|
+
clearTimeout(timer);
|
|
2474
|
+
}
|
|
2475
|
+
};
|
|
2459
2476
|
}
|
|
2460
2477
|
function createPiIssueAnalyzer(input = {}) {
|
|
2461
2478
|
const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
|
|
@@ -2463,7 +2480,10 @@ function createPiIssueAnalyzer(input = {}) {
|
|
|
2463
2480
|
const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
|
|
2464
2481
|
return async ({ prompt }) => {
|
|
2465
2482
|
const args = ["--print", "--mode", "json", "--no-session"];
|
|
2466
|
-
const
|
|
2483
|
+
const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
|
|
2484
|
+
const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
|
|
2485
|
+
if (provider)
|
|
2486
|
+
args.push("--provider", provider);
|
|
2467
2487
|
if (model)
|
|
2468
2488
|
args.push("--model", model);
|
|
2469
2489
|
args.push(prompt);
|
|
@@ -3304,6 +3324,7 @@ function summarizeRunValidationFailure(projectRoot, run) {
|
|
|
3304
3324
|
}
|
|
3305
3325
|
|
|
3306
3326
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
3327
|
+
import { randomBytes } from "crypto";
|
|
3307
3328
|
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
3308
3329
|
import { resolve as resolve13 } from "path";
|
|
3309
3330
|
function cleanString(value) {
|
|
@@ -3317,6 +3338,24 @@ function cleanScopes(value) {
|
|
|
3317
3338
|
return clean ? [clean] : [];
|
|
3318
3339
|
});
|
|
3319
3340
|
}
|
|
3341
|
+
function parseApiSessions(value) {
|
|
3342
|
+
if (!Array.isArray(value))
|
|
3343
|
+
return [];
|
|
3344
|
+
return value.flatMap((entry) => {
|
|
3345
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3346
|
+
return [];
|
|
3347
|
+
const record = entry;
|
|
3348
|
+
const token = cleanString(record.token);
|
|
3349
|
+
if (!token)
|
|
3350
|
+
return [];
|
|
3351
|
+
return [{
|
|
3352
|
+
token,
|
|
3353
|
+
login: cleanString(record.login),
|
|
3354
|
+
userId: cleanString(record.userId),
|
|
3355
|
+
createdAt: cleanString(record.createdAt) ?? undefined
|
|
3356
|
+
}];
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3320
3359
|
function readStoredAuth(stateFile) {
|
|
3321
3360
|
if (!existsSync6(stateFile))
|
|
3322
3361
|
return {};
|
|
@@ -3330,6 +3369,7 @@ function readStoredAuth(stateFile) {
|
|
|
3330
3369
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
3331
3370
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
3332
3371
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
3372
|
+
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
3333
3373
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
3334
3374
|
};
|
|
3335
3375
|
} catch {
|
|
@@ -3348,6 +3388,9 @@ function parsePendingDevice(value) {
|
|
|
3348
3388
|
return null;
|
|
3349
3389
|
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
3350
3390
|
}
|
|
3391
|
+
function newApiSessionToken() {
|
|
3392
|
+
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
3393
|
+
}
|
|
3351
3394
|
function writeStoredAuth(stateFile, payload) {
|
|
3352
3395
|
mkdirSync6(resolve13(stateFile, ".."), { recursive: true });
|
|
3353
3396
|
writeFileSync5(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
@@ -3390,9 +3433,38 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
3390
3433
|
scopes: input.scopes ?? [],
|
|
3391
3434
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
3392
3435
|
pendingDevice: null,
|
|
3436
|
+
apiSessions: previous.apiSessions ?? [],
|
|
3393
3437
|
updatedAt: new Date().toISOString()
|
|
3394
3438
|
});
|
|
3395
3439
|
},
|
|
3440
|
+
createApiSession() {
|
|
3441
|
+
const previous = readStoredAuth(stateFile);
|
|
3442
|
+
const token = newApiSessionToken();
|
|
3443
|
+
const session = {
|
|
3444
|
+
token,
|
|
3445
|
+
login: cleanString(previous.login),
|
|
3446
|
+
userId: cleanString(previous.userId),
|
|
3447
|
+
createdAt: new Date().toISOString()
|
|
3448
|
+
};
|
|
3449
|
+
writeStoredAuth(stateFile, {
|
|
3450
|
+
...previous,
|
|
3451
|
+
apiSessions: [...(previous.apiSessions ?? []).slice(-9), session],
|
|
3452
|
+
updatedAt: new Date().toISOString()
|
|
3453
|
+
});
|
|
3454
|
+
return { token, login: session.login ?? null, userId: session.userId ?? null };
|
|
3455
|
+
},
|
|
3456
|
+
readApiSession(token) {
|
|
3457
|
+
const clean = cleanString(token);
|
|
3458
|
+
if (!clean)
|
|
3459
|
+
return null;
|
|
3460
|
+
const previous = readStoredAuth(stateFile);
|
|
3461
|
+
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
3462
|
+
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
3463
|
+
},
|
|
3464
|
+
copyToProjectRoot(projectRoot2) {
|
|
3465
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot2);
|
|
3466
|
+
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
3467
|
+
},
|
|
3396
3468
|
savePendingDevice(input) {
|
|
3397
3469
|
const previous = readStoredAuth(stateFile);
|
|
3398
3470
|
writeStoredAuth(stateFile, {
|
|
@@ -3690,15 +3762,36 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
|
|
|
3690
3762
|
if (!run.taskId)
|
|
3691
3763
|
return;
|
|
3692
3764
|
const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3765
|
+
try {
|
|
3766
|
+
const result = await syncGitHubProjectStatusForTaskUpdate({
|
|
3767
|
+
taskId: run.taskId,
|
|
3768
|
+
status,
|
|
3769
|
+
issueNodeId,
|
|
3770
|
+
token: createGitHubAuthStore(projectRoot).readToken(),
|
|
3771
|
+
config
|
|
3772
|
+
});
|
|
3773
|
+
if (!result.synced && result.reason !== "project-sync-disabled") {
|
|
3774
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
3775
|
+
id: `log:${run.runId}:github-project-sync:${status}`,
|
|
3776
|
+
title: "GitHub Project sync skipped",
|
|
3777
|
+
detail: `Project status sync for ${run.taskId} could not run: ${result.reason}.`,
|
|
3778
|
+
tone: "warn",
|
|
3779
|
+
status: "running",
|
|
3780
|
+
createdAt: new Date().toISOString(),
|
|
3781
|
+
payload: { reason: result.reason, issueNodeId }
|
|
3782
|
+
});
|
|
3783
|
+
}
|
|
3784
|
+
} catch (error) {
|
|
3785
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
3786
|
+
id: `log:${run.runId}:github-project-sync-error:${status}`,
|
|
3787
|
+
title: "GitHub Project sync failed",
|
|
3788
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
3789
|
+
tone: "error",
|
|
3790
|
+
status: "running",
|
|
3791
|
+
createdAt: new Date().toISOString(),
|
|
3792
|
+
payload: { issueNodeId }
|
|
3793
|
+
});
|
|
3794
|
+
}
|
|
3702
3795
|
}
|
|
3703
3796
|
async function autoAssignRunIssue(projectRoot, run) {
|
|
3704
3797
|
if (!run.taskId)
|
|
@@ -3800,11 +3893,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
|
|
|
3800
3893
|
return;
|
|
3801
3894
|
throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
|
|
3802
3895
|
}
|
|
3896
|
+
async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
|
|
3897
|
+
const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
3898
|
+
if (fromReader)
|
|
3899
|
+
return fromReader;
|
|
3900
|
+
const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
|
|
3901
|
+
if (projected)
|
|
3902
|
+
return projected;
|
|
3903
|
+
if (readTasks !== readWorkspaceTasks) {
|
|
3904
|
+
return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
3905
|
+
}
|
|
3906
|
+
return null;
|
|
3907
|
+
}
|
|
3803
3908
|
async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
|
|
3804
3909
|
if ("taskId" in input && input.taskId) {
|
|
3805
3910
|
assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
|
|
3806
3911
|
}
|
|
3807
|
-
const sourceTask = "taskId" in input && input.taskId ?
|
|
3912
|
+
const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
|
|
3808
3913
|
const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
|
|
3809
3914
|
const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
|
|
3810
3915
|
const runRecord = {
|
|
@@ -3876,6 +3981,7 @@ async function startLocalRun(state, runId, options) {
|
|
|
3876
3981
|
throw new Error(`Run not found: ${runId}`);
|
|
3877
3982
|
}
|
|
3878
3983
|
const startedAt = new Date().toISOString();
|
|
3984
|
+
const resumeMode = options?.resume === true;
|
|
3879
3985
|
state.runProcesses.set(runId, {
|
|
3880
3986
|
runId,
|
|
3881
3987
|
child: null,
|
|
@@ -3892,9 +3998,9 @@ async function startLocalRun(state, runId, options) {
|
|
|
3892
3998
|
summary: run.title
|
|
3893
3999
|
});
|
|
3894
4000
|
appendRunLogEntry(state.projectRoot, runId, {
|
|
3895
|
-
id: `log:${runId}:prepare`,
|
|
3896
|
-
title: "Rig task run starting",
|
|
3897
|
-
detail: run.taskId ?? run.title,
|
|
4001
|
+
id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
|
|
4002
|
+
title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
|
|
4003
|
+
detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
|
|
3898
4004
|
tone: "info",
|
|
3899
4005
|
status: "preparing",
|
|
3900
4006
|
createdAt: startedAt
|
|
@@ -3972,7 +4078,15 @@ async function startLocalRun(state, runId, options) {
|
|
|
3972
4078
|
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
3973
4079
|
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
3974
4080
|
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
3975
|
-
...bridgeGitHubToken ? {
|
|
4081
|
+
...bridgeGitHubToken ? {
|
|
4082
|
+
RIG_GITHUB_TOKEN: bridgeGitHubToken,
|
|
4083
|
+
GITHUB_TOKEN: bridgeGitHubToken,
|
|
4084
|
+
GH_TOKEN: bridgeGitHubToken
|
|
4085
|
+
} : {},
|
|
4086
|
+
...resumeMode ? {
|
|
4087
|
+
RIG_RUN_RESUME: "1",
|
|
4088
|
+
RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
|
|
4089
|
+
} : {}
|
|
3976
4090
|
},
|
|
3977
4091
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3978
4092
|
});
|
|
@@ -4144,7 +4258,7 @@ async function resumeRunRecord(state, input) {
|
|
|
4144
4258
|
if (run.status === "completed") {
|
|
4145
4259
|
throw new Error("Completed runs cannot be resumed.");
|
|
4146
4260
|
}
|
|
4147
|
-
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
|
|
4261
|
+
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
|
|
4148
4262
|
}
|
|
4149
4263
|
function appendRunMessage(projectRoot, input) {
|
|
4150
4264
|
const run = readAuthorityRun4(projectRoot, input.runId);
|
|
@@ -4228,34 +4342,12 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
|
|
|
4228
4342
|
writeQueueState(projectRoot, next);
|
|
4229
4343
|
return next;
|
|
4230
4344
|
}
|
|
4231
|
-
var
|
|
4232
|
-
function
|
|
4233
|
-
|
|
4234
|
-
for (const run of runs) {
|
|
4345
|
+
var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
4346
|
+
function collectResumableLocalRuns(state, runs) {
|
|
4347
|
+
return runs.filter((run) => {
|
|
4235
4348
|
const status = normalizeString(run.status)?.toLowerCase() ?? "";
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
|
|
4239
|
-
continue;
|
|
4240
|
-
}
|
|
4241
|
-
const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
|
|
4242
|
-
patchRunRecord(state.projectRoot, run.runId, {
|
|
4243
|
-
status: "failed",
|
|
4244
|
-
completedAt: run.completedAt ?? nowIso,
|
|
4245
|
-
updatedAt: nowIso,
|
|
4246
|
-
errorText: detail
|
|
4247
|
-
});
|
|
4248
|
-
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
4249
|
-
id: `log:${run.runId}:stale-local-run`,
|
|
4250
|
-
title: "Run marked stale after server restart",
|
|
4251
|
-
detail,
|
|
4252
|
-
tone: "error",
|
|
4253
|
-
status: "failed",
|
|
4254
|
-
createdAt: nowIso
|
|
4255
|
-
});
|
|
4256
|
-
changed = true;
|
|
4257
|
-
}
|
|
4258
|
-
return changed;
|
|
4349
|
+
return run.mode === "local" && RESUMABLE_LOCAL_RUN_STATUSES.has(status) && !state.runProcesses.has(run.runId);
|
|
4350
|
+
});
|
|
4259
4351
|
}
|
|
4260
4352
|
async function reconcileScheduler(state, reason) {
|
|
4261
4353
|
if (state.scheduler.reconciling) {
|
|
@@ -4270,7 +4362,20 @@ async function reconcileScheduler(state, reason) {
|
|
|
4270
4362
|
const queue = readQueueState(state.projectRoot);
|
|
4271
4363
|
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
4272
4364
|
let runs = listAuthorityRuns4(state.projectRoot);
|
|
4273
|
-
let changed =
|
|
4365
|
+
let changed = false;
|
|
4366
|
+
const resumableRuns = collectResumableLocalRuns(state, runs);
|
|
4367
|
+
for (const run of resumableRuns) {
|
|
4368
|
+
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
4369
|
+
id: `log:${run.runId}:auto-resume:${Date.now()}`,
|
|
4370
|
+
title: "Run auto-resume scheduled",
|
|
4371
|
+
detail: `Rig server recovered nonterminal run ${run.runId} after ${reason}; resuming the same lifecycle instead of restarting it.`,
|
|
4372
|
+
tone: "info",
|
|
4373
|
+
status: "preparing",
|
|
4374
|
+
createdAt: new Date().toISOString()
|
|
4375
|
+
});
|
|
4376
|
+
await startLocalRun(state, run.runId, { resume: true });
|
|
4377
|
+
changed = true;
|
|
4378
|
+
}
|
|
4274
4379
|
if (changed) {
|
|
4275
4380
|
runs = listAuthorityRuns4(state.projectRoot);
|
|
4276
4381
|
}
|
|
@@ -4886,10 +4991,10 @@ function normalizeCommit(value) {
|
|
|
4886
4991
|
function asPlainRecord(value) {
|
|
4887
4992
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4888
4993
|
}
|
|
4889
|
-
var
|
|
4994
|
+
var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
|
|
4890
4995
|
var RIG_CONFIG_DEV_DEPENDENCIES = {
|
|
4891
|
-
"@rig/core": `npm:@h-rig/core@${
|
|
4892
|
-
"@rig/standard-plugin": `npm:@h-rig/standard-plugin@${
|
|
4996
|
+
"@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
|
|
4997
|
+
"@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
|
|
4893
4998
|
};
|
|
4894
4999
|
function repoParts(repoSlug) {
|
|
4895
5000
|
const [owner, repo] = repoSlug.split("/");
|
|
@@ -5243,13 +5348,52 @@ function bearerTokenFromRequest(req) {
|
|
|
5243
5348
|
function isLoopbackRequest(req) {
|
|
5244
5349
|
try {
|
|
5245
5350
|
const hostname = new URL(req.url).hostname.toLowerCase();
|
|
5246
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
5351
|
+
return hostname === "localhost" || hostname === "rig.local" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
5247
5352
|
} catch {
|
|
5248
5353
|
return false;
|
|
5249
5354
|
}
|
|
5250
5355
|
}
|
|
5251
5356
|
function isPublicRigAuthBootstrapRoute(pathname) {
|
|
5252
|
-
return pathname === "/" || pathname === "/
|
|
5357
|
+
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";
|
|
5358
|
+
}
|
|
5359
|
+
function buildRigInstallScript() {
|
|
5360
|
+
return `#!/usr/bin/env bash
|
|
5361
|
+
set -euo pipefail
|
|
5362
|
+
|
|
5363
|
+
say() {
|
|
5364
|
+
printf 'rig-install: %s
|
|
5365
|
+
' "$*"
|
|
5366
|
+
}
|
|
5367
|
+
|
|
5368
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
5369
|
+
say "Bun not found; installing Bun first"
|
|
5370
|
+
curl -fsSL https://bun.sh/install | bash
|
|
5371
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
5372
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
5373
|
+
fi
|
|
5374
|
+
|
|
5375
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
5376
|
+
printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
|
|
5377
|
+
' >&2
|
|
5378
|
+
exit 1
|
|
5379
|
+
fi
|
|
5380
|
+
|
|
5381
|
+
say "Installing @h-rig/cli@latest"
|
|
5382
|
+
bun add -g @h-rig/cli@latest
|
|
5383
|
+
|
|
5384
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
5385
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
5386
|
+
|
|
5387
|
+
if ! command -v rig >/dev/null 2>&1; then
|
|
5388
|
+
printf 'rig-install: rig installed, but rig is not on PATH. Add %s/bin to PATH and retry.
|
|
5389
|
+
' "$BUN_INSTALL" >&2
|
|
5390
|
+
exit 1
|
|
5391
|
+
fi
|
|
5392
|
+
|
|
5393
|
+
say "Verifying rig"
|
|
5394
|
+
rig --help >/dev/null
|
|
5395
|
+
say "Done. Run: rig --help"
|
|
5396
|
+
`;
|
|
5253
5397
|
}
|
|
5254
5398
|
function normalizePrMode(value) {
|
|
5255
5399
|
const mode = normalizeString(value);
|
|
@@ -5262,6 +5406,10 @@ function authorizeRigHttpRequest(input) {
|
|
|
5262
5406
|
const bearer = bearerTokenFromRequest(input.req);
|
|
5263
5407
|
const store = createGitHubAuthStore(input.projectRoot);
|
|
5264
5408
|
const storedToken = store.readToken();
|
|
5409
|
+
const session = bearer ? store.readApiSession(bearer) : null;
|
|
5410
|
+
if (session) {
|
|
5411
|
+
return { authorized: true, actor: session.login ?? "github-operator", reason: "github-session" };
|
|
5412
|
+
}
|
|
5265
5413
|
if (bearer && storedToken && bearer === storedToken) {
|
|
5266
5414
|
const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
5267
5415
|
return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
|
|
@@ -5269,8 +5417,11 @@ function authorizeRigHttpRequest(input) {
|
|
|
5269
5417
|
if (isPublicRigAuthBootstrapRoute(input.pathname)) {
|
|
5270
5418
|
return { authorized: true, actor: null, reason: "public-bootstrap" };
|
|
5271
5419
|
}
|
|
5272
|
-
if (!input.serverAuthToken && !storedToken
|
|
5273
|
-
|
|
5420
|
+
if (!input.serverAuthToken && !storedToken) {
|
|
5421
|
+
if (isLoopbackRequest(input.req)) {
|
|
5422
|
+
return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
|
|
5423
|
+
}
|
|
5424
|
+
return { authorized: false, actor: null, reason: "auth-required" };
|
|
5274
5425
|
}
|
|
5275
5426
|
return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
|
|
5276
5427
|
}
|
|
@@ -5501,7 +5652,7 @@ function selectNextWorkspaceTask(projectRoot, tasks) {
|
|
|
5501
5652
|
if (runnable.length === 0)
|
|
5502
5653
|
return null;
|
|
5503
5654
|
const queue = readQueueState(projectRoot);
|
|
5504
|
-
const queueRank = new Map(queue.map((entry, index) => [entry.taskId, { score: entry.score, position: index }]));
|
|
5655
|
+
const queueRank = new Map(queue.map((entry, index) => [String(entry.taskId), { score: entry.score, position: index }]));
|
|
5505
5656
|
return runnable.toSorted((left, right) => {
|
|
5506
5657
|
const leftId = taskIdOf(left) ?? "";
|
|
5507
5658
|
const rightId = taskIdOf(right) ?? "";
|
|
@@ -5541,6 +5692,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
|
|
|
5541
5692
|
}
|
|
5542
5693
|
return filtered;
|
|
5543
5694
|
}
|
|
5695
|
+
function issueAnalysisTargetFor(source) {
|
|
5696
|
+
if (!source)
|
|
5697
|
+
return null;
|
|
5698
|
+
const candidate = source;
|
|
5699
|
+
if (typeof candidate.updateTask !== "function")
|
|
5700
|
+
return null;
|
|
5701
|
+
return {
|
|
5702
|
+
...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
|
|
5703
|
+
updateTask: candidate.updateTask.bind(candidate),
|
|
5704
|
+
...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
|
|
5705
|
+
...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
|
|
5706
|
+
...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
|
|
5707
|
+
};
|
|
5708
|
+
}
|
|
5709
|
+
function uniqueStringList(value) {
|
|
5710
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
5711
|
+
return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
|
|
5712
|
+
}
|
|
5713
|
+
function taskRecordId(task) {
|
|
5714
|
+
return String(task.id ?? "");
|
|
5715
|
+
}
|
|
5544
5716
|
function redactRemoteEndpoint(endpoint) {
|
|
5545
5717
|
const { token, ...rest } = endpoint;
|
|
5546
5718
|
return {
|
|
@@ -5625,9 +5797,16 @@ function createRigServerFetch(state, deps) {
|
|
|
5625
5797
|
notifications: state.targets.length
|
|
5626
5798
|
});
|
|
5627
5799
|
}
|
|
5800
|
+
if (url.pathname === "/install" && req.method === "GET") {
|
|
5801
|
+
return new Response(buildRigInstallScript(), {
|
|
5802
|
+
headers: {
|
|
5803
|
+
"Content-Type": "text/x-shellscript; charset=utf-8"
|
|
5804
|
+
}
|
|
5805
|
+
});
|
|
5806
|
+
}
|
|
5628
5807
|
const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
|
|
5629
5808
|
const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
|
|
5630
|
-
const legacyAuthorizedHttpRequest = isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken);
|
|
5809
|
+
const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
|
|
5631
5810
|
const requestAuth = authorizeRigHttpRequest({
|
|
5632
5811
|
req,
|
|
5633
5812
|
pathname: url.pathname,
|
|
@@ -5883,6 +6062,67 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
5883
6062
|
note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
|
|
5884
6063
|
});
|
|
5885
6064
|
}
|
|
6065
|
+
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
6066
|
+
const body = await deps.readJsonBody(req);
|
|
6067
|
+
const ids = uniqueStringList(body.ids ?? body.id);
|
|
6068
|
+
const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
|
|
6069
|
+
if (ids.length === 0 && !analyzeAll) {
|
|
6070
|
+
return deps.badRequest("ids is required unless all=true");
|
|
6071
|
+
}
|
|
6072
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot);
|
|
6073
|
+
const [source] = ctx?.taskSourceRegistry.list() ?? [];
|
|
6074
|
+
const target = issueAnalysisTargetFor(source);
|
|
6075
|
+
if (!source || !target) {
|
|
6076
|
+
return deps.badRequest("Configured task source does not support issue-analysis writeback");
|
|
6077
|
+
}
|
|
6078
|
+
const allTasks = [...await source.list()];
|
|
6079
|
+
const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
|
|
6080
|
+
const cached = allTasks.find((task) => taskRecordId(task) === id);
|
|
6081
|
+
if (cached)
|
|
6082
|
+
return cached;
|
|
6083
|
+
return typeof source.get === "function" ? await source.get(id) : undefined;
|
|
6084
|
+
}))).filter((task) => Boolean(task));
|
|
6085
|
+
if (issues.length === 0) {
|
|
6086
|
+
return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
|
|
6087
|
+
}
|
|
6088
|
+
const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
|
|
6089
|
+
const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
|
|
6090
|
+
const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
|
|
6091
|
+
const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
|
|
6092
|
+
const service = createIssueAnalysisService({
|
|
6093
|
+
analyzer: createPiIssueAnalyzer({
|
|
6094
|
+
...model ? { model } : {},
|
|
6095
|
+
env: { RIG_PROJECT_ROOT: state.projectRoot }
|
|
6096
|
+
}),
|
|
6097
|
+
writeBack: createIssueAnalysisWriteBack({ target })
|
|
6098
|
+
});
|
|
6099
|
+
const reason = normalizeString(body.reason) ?? "http-issue-analysis";
|
|
6100
|
+
let results;
|
|
6101
|
+
try {
|
|
6102
|
+
results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
|
|
6103
|
+
} catch (error) {
|
|
6104
|
+
return deps.jsonResponse({
|
|
6105
|
+
ok: false,
|
|
6106
|
+
error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
6107
|
+
reason,
|
|
6108
|
+
ids: issues.map((issue) => issue.id)
|
|
6109
|
+
}, 502);
|
|
6110
|
+
}
|
|
6111
|
+
deps.snapshotService.invalidate("issue-analysis-http-run");
|
|
6112
|
+
await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
|
|
6113
|
+
return;
|
|
6114
|
+
});
|
|
6115
|
+
deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
|
|
6116
|
+
return deps.jsonResponse({
|
|
6117
|
+
ok: true,
|
|
6118
|
+
reason,
|
|
6119
|
+
analyzed: results.map((entry) => ({
|
|
6120
|
+
id: entry.issue.id,
|
|
6121
|
+
title: entry.issue.title ?? null,
|
|
6122
|
+
result: entry.result
|
|
6123
|
+
}))
|
|
6124
|
+
});
|
|
6125
|
+
}
|
|
5886
6126
|
if (url.pathname === "/api/server/status") {
|
|
5887
6127
|
const config = buildProjectConfigStatus(state.projectRoot);
|
|
5888
6128
|
const taskSource = await buildTaskSourceStatus(state, config);
|
|
@@ -6023,6 +6263,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6023
6263
|
}
|
|
6024
6264
|
const normalizedRoot = resolve18(requestedRoot);
|
|
6025
6265
|
const exists = existsSync11(normalizedRoot);
|
|
6266
|
+
if (exists) {
|
|
6267
|
+
createGitHubAuthStore(state.projectRoot).copyToProjectRoot(normalizedRoot);
|
|
6268
|
+
}
|
|
6026
6269
|
const control = buildServerControlStatus();
|
|
6027
6270
|
const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
|
|
6028
6271
|
if (!exists) {
|
|
@@ -6111,21 +6354,30 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6111
6354
|
const body = await deps.readJsonBody(req);
|
|
6112
6355
|
const token = normalizeString(body.token);
|
|
6113
6356
|
const selectedRepo = normalizeString(body.selectedRepo);
|
|
6357
|
+
const requestedProjectRoot = normalizeString(body.projectRoot);
|
|
6114
6358
|
if (!token) {
|
|
6115
6359
|
return deps.badRequest("token is required");
|
|
6116
6360
|
}
|
|
6117
6361
|
try {
|
|
6118
6362
|
const user = await fetchGitHubUserInfo(token);
|
|
6119
|
-
const
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6363
|
+
const storeRoots = [
|
|
6364
|
+
state.projectRoot,
|
|
6365
|
+
...requestedProjectRoot && isAbsolute3(requestedProjectRoot) && existsSync11(resolve18(requestedProjectRoot)) ? [resolve18(requestedProjectRoot)] : []
|
|
6366
|
+
].filter((root, index, roots) => roots.indexOf(root) === index);
|
|
6367
|
+
const stores = storeRoots.map((root) => createGitHubAuthStore(root));
|
|
6368
|
+
for (const store2 of stores) {
|
|
6369
|
+
store2.saveToken({
|
|
6370
|
+
token,
|
|
6371
|
+
tokenSource: "manual-token",
|
|
6372
|
+
login: user.login,
|
|
6373
|
+
userId: user.userId,
|
|
6374
|
+
scopes: user.scopes,
|
|
6375
|
+
selectedRepo
|
|
6376
|
+
});
|
|
6377
|
+
}
|
|
6378
|
+
const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
|
|
6379
|
+
const apiSession = store.createApiSession();
|
|
6380
|
+
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
|
|
6129
6381
|
} catch (error) {
|
|
6130
6382
|
const message = error instanceof Error ? error.message : String(error);
|
|
6131
6383
|
return deps.jsonResponse({ ok: false, error: message }, 400);
|
|
@@ -6193,7 +6445,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6193
6445
|
const token = result.payload.access_token;
|
|
6194
6446
|
const user = await fetchGitHubUserInfo(token);
|
|
6195
6447
|
store.saveToken({ token, tokenSource: "oauth-device", login: user.login, userId: user.userId, scopes: user.scopes });
|
|
6196
|
-
|
|
6448
|
+
const apiSession = store.createApiSession();
|
|
6449
|
+
return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }), apiSessionToken: apiSession.token });
|
|
6197
6450
|
}
|
|
6198
6451
|
if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
|
|
6199
6452
|
const body = await deps.readJsonBody(req);
|
|
@@ -6565,11 +6818,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6565
6818
|
const runId = normalizeString(body.runId);
|
|
6566
6819
|
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
6567
6820
|
const promptOverride = normalizeString(body.promptOverride);
|
|
6821
|
+
const restart = body.restart === true;
|
|
6568
6822
|
if (!runId) {
|
|
6569
6823
|
return deps.badRequest("runId is required");
|
|
6570
6824
|
}
|
|
6571
6825
|
try {
|
|
6572
|
-
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
|
|
6826
|
+
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
|
|
6573
6827
|
deps.broadcastSnapshotInvalidation(state);
|
|
6574
6828
|
return deps.jsonResponse({ ok: true, runId, createdAt });
|
|
6575
6829
|
} catch (error) {
|
|
@@ -11999,7 +12253,7 @@ async function readWorkspaceTasks(projectRoot) {
|
|
|
11999
12253
|
description: normalizeString(entry.description),
|
|
12000
12254
|
acceptanceCriteria: normalizeString(entry.acceptance_criteria),
|
|
12001
12255
|
status: normalizedStatus ?? "unknown",
|
|
12002
|
-
sourceStatus: normalizedStatus ? null : rawStatus,
|
|
12256
|
+
sourceStatus: normalizedStatus && rawStatus !== normalizedStatus ? rawStatus : normalizedStatus ? null : rawStatus,
|
|
12003
12257
|
priority: typeof entry.priority === "number" ? entry.priority : typeof entry.priority === "string" ? Number(entry.priority) : null,
|
|
12004
12258
|
issueType: normalizeString(entry.issue_type),
|
|
12005
12259
|
role: normalizeString(config.role) ?? null,
|
|
@@ -12479,6 +12733,7 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
|
|
|
12479
12733
|
const server = Bun.serve({
|
|
12480
12734
|
hostname: options.host,
|
|
12481
12735
|
port: options.port,
|
|
12736
|
+
idleTimeout: Math.max(10, Math.min(255, Number.parseInt(process.env.RIG_SERVER_IDLE_TIMEOUT_SECONDS || "255", 10) || 255)),
|
|
12482
12737
|
fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
|
|
12483
12738
|
websocket: {
|
|
12484
12739
|
open(ws) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-rig/server",
|
|
3
|
-
"version": "0.0.6-alpha.
|
|
3
|
+
"version": "0.0.6-alpha.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Rig package",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"rig-server": "./dist/src/server.js"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.
|
|
29
|
-
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.
|
|
30
|
-
"@rig/runtime": "npm:@h-rig/runtime@0.0.6-alpha.
|
|
28
|
+
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.10",
|
|
29
|
+
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.10",
|
|
30
|
+
"@rig/runtime": "npm:@h-rig/runtime@0.0.6-alpha.10",
|
|
31
31
|
"effect": "4.0.0-beta.78"
|
|
32
32
|
}
|
|
33
33
|
}
|