@h-rig/server 0.0.6-alpha.2 → 0.0.6-alpha.20
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/README.md +23 -0
- package/dist/src/index.js +1513 -331
- package/dist/src/server-helpers/github-api-session-index.js +107 -0
- package/dist/src/server-helpers/github-auth-store.js +68 -24
- package/dist/src/server-helpers/github-project-status-sync.js +3 -0
- package/dist/src/server-helpers/github-user-namespace.js +102 -0
- package/dist/src/server-helpers/http-router.js +1207 -185
- package/dist/src/server-helpers/issue-analysis.js +30 -11
- package/dist/src/server-helpers/project-registry.js +5 -0
- package/dist/src/server-helpers/run-io.js +13 -0
- package/dist/src/server-helpers/run-mutations.js +599 -114
- package/dist/src/server-helpers/run-writers.js +1 -2
- package/dist/src/server-helpers/ws-router.js +7 -1
- package/dist/src/server.js +1512 -331
- package/package.json +4 -4
package/dist/src/server.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
1
2
|
// @bun
|
|
2
3
|
var __require = import.meta.require;
|
|
3
4
|
|
|
4
5
|
// packages/server/src/server.ts
|
|
5
6
|
import { spawn as spawn5 } from "child_process";
|
|
6
|
-
import { existsSync as
|
|
7
|
+
import { existsSync as existsSync19, readdirSync as readdirSync5, readFileSync as readFileSync14, statSync as statSync6 } from "fs";
|
|
7
8
|
import { open } from "fs/promises";
|
|
8
|
-
import { dirname as
|
|
9
|
+
import { dirname as dirname20, resolve as resolve24 } from "path";
|
|
9
10
|
import {
|
|
10
11
|
listAuthorityArtifactRoots,
|
|
11
12
|
listAuthorityRuns as listAuthorityRuns7,
|
|
@@ -1073,6 +1074,18 @@ function readJsonlFileTail(path, options) {
|
|
|
1073
1074
|
const completeLines = start > 0 ? lines.slice(1) : lines;
|
|
1074
1075
|
return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
|
|
1075
1076
|
}
|
|
1077
|
+
async function readRunTimelinePage(projectRoot, runId, options = {}) {
|
|
1078
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
1079
|
+
const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
|
|
1080
|
+
const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
1081
|
+
const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
|
|
1082
|
+
const endExclusive = Math.min(entries.length, startInclusive + limit);
|
|
1083
|
+
return {
|
|
1084
|
+
entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
|
|
1085
|
+
nextCursor: String(endExclusive),
|
|
1086
|
+
hasMore: endExclusive < entries.length
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1076
1089
|
var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
1077
1090
|
async function readRunLogsPage(projectRoot, runId, options = {}) {
|
|
1078
1091
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
@@ -2312,7 +2325,6 @@ function createGitHubTaskReconciler(input) {
|
|
|
2312
2325
|
}
|
|
2313
2326
|
|
|
2314
2327
|
// packages/server/src/server-helpers/issue-analysis.ts
|
|
2315
|
-
import { execFile } from "child_process";
|
|
2316
2328
|
import { createHash } from "crypto";
|
|
2317
2329
|
function stableIssueHash(issue) {
|
|
2318
2330
|
const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
|
|
@@ -2446,16 +2458,33 @@ function parseIssueAnalysisResult(raw) {
|
|
|
2446
2458
|
return result;
|
|
2447
2459
|
}
|
|
2448
2460
|
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 ?? "") });
|
|
2461
|
+
return async (command, args, options) => {
|
|
2462
|
+
const env = options.env ? { ...process.env, ...options.env } : process.env;
|
|
2463
|
+
const proc = Bun.spawn([command, ...args], {
|
|
2464
|
+
stdout: "pipe",
|
|
2465
|
+
stderr: "pipe",
|
|
2466
|
+
env
|
|
2457
2467
|
});
|
|
2458
|
-
|
|
2468
|
+
let timedOut = false;
|
|
2469
|
+
const timer = setTimeout(() => {
|
|
2470
|
+
timedOut = true;
|
|
2471
|
+
proc.kill();
|
|
2472
|
+
}, options.timeoutMs);
|
|
2473
|
+
try {
|
|
2474
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2475
|
+
new Response(proc.stdout).text(),
|
|
2476
|
+
new Response(proc.stderr).text(),
|
|
2477
|
+
proc.exited
|
|
2478
|
+
]);
|
|
2479
|
+
return {
|
|
2480
|
+
exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
|
|
2481
|
+
stdout,
|
|
2482
|
+
stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
|
|
2483
|
+
};
|
|
2484
|
+
} finally {
|
|
2485
|
+
clearTimeout(timer);
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2459
2488
|
}
|
|
2460
2489
|
function createPiIssueAnalyzer(input = {}) {
|
|
2461
2490
|
const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
|
|
@@ -2463,7 +2492,10 @@ function createPiIssueAnalyzer(input = {}) {
|
|
|
2463
2492
|
const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
|
|
2464
2493
|
return async ({ prompt }) => {
|
|
2465
2494
|
const args = ["--print", "--mode", "json", "--no-session"];
|
|
2466
|
-
const
|
|
2495
|
+
const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
|
|
2496
|
+
const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
|
|
2497
|
+
if (provider)
|
|
2498
|
+
args.push("--provider", provider);
|
|
2467
2499
|
if (model)
|
|
2468
2500
|
args.push("--model", model);
|
|
2469
2501
|
args.push(prompt);
|
|
@@ -2711,8 +2743,7 @@ function buildRunStartPatch(startedAt) {
|
|
|
2711
2743
|
status: "preparing",
|
|
2712
2744
|
startedAt,
|
|
2713
2745
|
completedAt: null,
|
|
2714
|
-
errorText: null
|
|
2715
|
-
serverPid: process.pid
|
|
2746
|
+
errorText: null
|
|
2716
2747
|
};
|
|
2717
2748
|
}
|
|
2718
2749
|
|
|
@@ -3181,7 +3212,7 @@ function applyOrchestrationCommand(state, command) {
|
|
|
3181
3212
|
import { spawn as spawn3 } from "child_process";
|
|
3182
3213
|
import { loadConfig } from "@rig/core/load-config";
|
|
3183
3214
|
import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
3184
|
-
import { dirname as
|
|
3215
|
+
import { dirname as dirname9, relative as relative2, resolve as resolve14 } from "path";
|
|
3185
3216
|
import {
|
|
3186
3217
|
listAuthorityRuns as listAuthorityRuns4,
|
|
3187
3218
|
readAuthorityRun as readAuthorityRun4,
|
|
@@ -3194,6 +3225,11 @@ import {
|
|
|
3194
3225
|
buildTaskRunLifecycleComment,
|
|
3195
3226
|
updateConfiguredTaskSourceTask
|
|
3196
3227
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
3228
|
+
import {
|
|
3229
|
+
closeIssueAfterMergedPr,
|
|
3230
|
+
commitRunChanges,
|
|
3231
|
+
runPrAutomation
|
|
3232
|
+
} from "@rig/runtime/control-plane/native/pr-automation";
|
|
3197
3233
|
|
|
3198
3234
|
// packages/server/src/scheduler.ts
|
|
3199
3235
|
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -3305,8 +3341,8 @@ function summarizeRunValidationFailure(projectRoot, run) {
|
|
|
3305
3341
|
|
|
3306
3342
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
3307
3343
|
import { randomBytes } from "crypto";
|
|
3308
|
-
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
3309
|
-
import { resolve as resolve13 } from "path";
|
|
3344
|
+
import { chmodSync, copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
3345
|
+
import { dirname as dirname8, resolve as resolve13 } from "path";
|
|
3310
3346
|
function cleanString(value) {
|
|
3311
3347
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
3312
3348
|
}
|
|
@@ -3336,6 +3372,26 @@ function parseApiSessions(value) {
|
|
|
3336
3372
|
}];
|
|
3337
3373
|
});
|
|
3338
3374
|
}
|
|
3375
|
+
function parsePendingDevice(value) {
|
|
3376
|
+
if (!value || typeof value !== "object")
|
|
3377
|
+
return null;
|
|
3378
|
+
const record = value;
|
|
3379
|
+
const pollId = cleanString(record.pollId);
|
|
3380
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
3381
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
3382
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
3383
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
3384
|
+
return null;
|
|
3385
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
3386
|
+
}
|
|
3387
|
+
function parsePendingDevices(value) {
|
|
3388
|
+
if (!Array.isArray(value))
|
|
3389
|
+
return [];
|
|
3390
|
+
return value.flatMap((entry) => {
|
|
3391
|
+
const pending = parsePendingDevice(entry);
|
|
3392
|
+
return pending ? [pending] : [];
|
|
3393
|
+
});
|
|
3394
|
+
}
|
|
3339
3395
|
function readStoredAuth(stateFile) {
|
|
3340
3396
|
if (!existsSync6(stateFile))
|
|
3341
3397
|
return {};
|
|
@@ -3349,6 +3405,7 @@ function readStoredAuth(stateFile) {
|
|
|
3349
3405
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
3350
3406
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
3351
3407
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
3408
|
+
pendingDevices: parsePendingDevices(parsed.pendingDevices),
|
|
3352
3409
|
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
3353
3410
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
3354
3411
|
};
|
|
@@ -3356,34 +3413,36 @@ function readStoredAuth(stateFile) {
|
|
|
3356
3413
|
return {};
|
|
3357
3414
|
}
|
|
3358
3415
|
}
|
|
3359
|
-
function parsePendingDevice(value) {
|
|
3360
|
-
if (!value || typeof value !== "object")
|
|
3361
|
-
return null;
|
|
3362
|
-
const record = value;
|
|
3363
|
-
const pollId = cleanString(record.pollId);
|
|
3364
|
-
const deviceCode = cleanString(record.deviceCode);
|
|
3365
|
-
const expiresAt = cleanString(record.expiresAt);
|
|
3366
|
-
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
3367
|
-
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
3368
|
-
return null;
|
|
3369
|
-
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
3370
|
-
}
|
|
3371
3416
|
function newApiSessionToken() {
|
|
3372
3417
|
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
3373
3418
|
}
|
|
3374
3419
|
function writeStoredAuth(stateFile, payload) {
|
|
3375
|
-
mkdirSync6(
|
|
3420
|
+
mkdirSync6(dirname8(stateFile), { recursive: true });
|
|
3376
3421
|
writeFileSync5(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
3377
3422
|
`, { encoding: "utf8", mode: 384 });
|
|
3378
3423
|
try {
|
|
3379
3424
|
chmodSync(stateFile, 384);
|
|
3380
3425
|
} catch {}
|
|
3381
3426
|
}
|
|
3427
|
+
function localProjectAuthStateFile(projectRoot) {
|
|
3428
|
+
return resolve13(projectRoot, ".rig", "state", "github-auth.json");
|
|
3429
|
+
}
|
|
3382
3430
|
function resolveGitHubAuthStateFile(projectRoot) {
|
|
3383
3431
|
return resolve13(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
3384
3432
|
}
|
|
3385
|
-
function
|
|
3386
|
-
const
|
|
3433
|
+
function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
|
|
3434
|
+
const targetFile = localProjectAuthStateFile(projectRoot);
|
|
3435
|
+
mkdirSync6(dirname8(targetFile), { recursive: true });
|
|
3436
|
+
if (existsSync6(stateFile)) {
|
|
3437
|
+
copyFileSync(stateFile, targetFile);
|
|
3438
|
+
try {
|
|
3439
|
+
chmodSync(targetFile, 384);
|
|
3440
|
+
} catch {}
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
writeStoredAuth(targetFile, {});
|
|
3444
|
+
}
|
|
3445
|
+
function createGitHubAuthStoreFromStateFile(stateFile) {
|
|
3387
3446
|
return {
|
|
3388
3447
|
stateFile,
|
|
3389
3448
|
status(options) {
|
|
@@ -3413,6 +3472,7 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
3413
3472
|
scopes: input.scopes ?? [],
|
|
3414
3473
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
3415
3474
|
pendingDevice: null,
|
|
3475
|
+
pendingDevices: [],
|
|
3416
3476
|
apiSessions: previous.apiSessions ?? [],
|
|
3417
3477
|
updatedAt: new Date().toISOString()
|
|
3418
3478
|
});
|
|
@@ -3441,15 +3501,24 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
3441
3501
|
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
3442
3502
|
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
3443
3503
|
},
|
|
3444
|
-
copyToProjectRoot(
|
|
3445
|
-
const targetFile = resolveGitHubAuthStateFile(
|
|
3504
|
+
copyToProjectRoot(projectRoot) {
|
|
3505
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot);
|
|
3446
3506
|
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
3447
3507
|
},
|
|
3508
|
+
copyToLocalProjectRoot(projectRoot) {
|
|
3509
|
+
copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
|
|
3510
|
+
},
|
|
3448
3511
|
savePendingDevice(input) {
|
|
3449
3512
|
const previous = readStoredAuth(stateFile);
|
|
3513
|
+
const pendingDevices = [
|
|
3514
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
3515
|
+
...previous.pendingDevices ?? [],
|
|
3516
|
+
input
|
|
3517
|
+
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
|
|
3450
3518
|
writeStoredAuth(stateFile, {
|
|
3451
3519
|
...previous,
|
|
3452
|
-
pendingDevice:
|
|
3520
|
+
pendingDevice: null,
|
|
3521
|
+
pendingDevices,
|
|
3453
3522
|
updatedAt: new Date().toISOString()
|
|
3454
3523
|
});
|
|
3455
3524
|
},
|
|
@@ -3462,23 +3531,32 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
3462
3531
|
});
|
|
3463
3532
|
},
|
|
3464
3533
|
readPendingDevice(pollId) {
|
|
3465
|
-
const
|
|
3466
|
-
|
|
3534
|
+
const previous = readStoredAuth(stateFile);
|
|
3535
|
+
const pending = [
|
|
3536
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
3537
|
+
...previous.pendingDevices ?? []
|
|
3538
|
+
].find((entry) => entry.pollId === pollId) ?? null;
|
|
3539
|
+
if (!pending)
|
|
3467
3540
|
return null;
|
|
3468
3541
|
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
3469
3542
|
return null;
|
|
3470
3543
|
return pending;
|
|
3471
3544
|
},
|
|
3472
|
-
clearPendingDevice() {
|
|
3545
|
+
clearPendingDevice(pollId) {
|
|
3473
3546
|
const previous = readStoredAuth(stateFile);
|
|
3547
|
+
const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
|
|
3474
3548
|
writeStoredAuth(stateFile, {
|
|
3475
3549
|
...previous,
|
|
3476
3550
|
pendingDevice: null,
|
|
3551
|
+
pendingDevices: remaining,
|
|
3477
3552
|
updatedAt: new Date().toISOString()
|
|
3478
3553
|
});
|
|
3479
3554
|
}
|
|
3480
3555
|
};
|
|
3481
3556
|
}
|
|
3557
|
+
function createGitHubAuthStore(projectRoot) {
|
|
3558
|
+
return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
|
|
3559
|
+
}
|
|
3482
3560
|
|
|
3483
3561
|
// packages/server/src/server-helpers/github-projects.ts
|
|
3484
3562
|
function asRecord(value) {
|
|
@@ -3487,6 +3565,9 @@ function asRecord(value) {
|
|
|
3487
3565
|
function asString(value) {
|
|
3488
3566
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
3489
3567
|
}
|
|
3568
|
+
function asNumber(value) {
|
|
3569
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
3570
|
+
}
|
|
3490
3571
|
async function defaultGraphQLFetch(query, variables, token) {
|
|
3491
3572
|
const response = await fetch("https://api.github.com/graphql", {
|
|
3492
3573
|
method: "POST",
|
|
@@ -3503,6 +3584,32 @@ async function defaultGraphQLFetch(query, variables, token) {
|
|
|
3503
3584
|
}
|
|
3504
3585
|
return json.data;
|
|
3505
3586
|
}
|
|
3587
|
+
function projectNodesFrom(data) {
|
|
3588
|
+
const root = asRecord(data);
|
|
3589
|
+
const owner = asRecord(root?.organization) ?? asRecord(root?.user);
|
|
3590
|
+
const projects = asRecord(owner?.projectsV2);
|
|
3591
|
+
const nodes = projects?.nodes;
|
|
3592
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
3593
|
+
}
|
|
3594
|
+
async function listGitHubProjects(input) {
|
|
3595
|
+
const query = `
|
|
3596
|
+
query RigListProjects($owner: String!, $first: Int!) {
|
|
3597
|
+
organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
3598
|
+
user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
3599
|
+
}
|
|
3600
|
+
`;
|
|
3601
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
3602
|
+
const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
|
|
3603
|
+
return projectNodesFrom(data).flatMap((node) => {
|
|
3604
|
+
const record = asRecord(node);
|
|
3605
|
+
const id = asString(record?.id);
|
|
3606
|
+
const number = asNumber(record?.number);
|
|
3607
|
+
const title = asString(record?.title);
|
|
3608
|
+
if (!id || number === undefined || !title)
|
|
3609
|
+
return [];
|
|
3610
|
+
return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3506
3613
|
async function resolveProjectStatusField(input) {
|
|
3507
3614
|
const query = `
|
|
3508
3615
|
query RigProjectStatusField($projectId: ID!) {
|
|
@@ -3597,6 +3704,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
3597
3704
|
running: "In Progress",
|
|
3598
3705
|
prOpen: "In Review",
|
|
3599
3706
|
ciFixing: "In Review",
|
|
3707
|
+
merging: "Merging",
|
|
3600
3708
|
done: "Done",
|
|
3601
3709
|
needsAttention: "Needs Attention"
|
|
3602
3710
|
};
|
|
@@ -3610,6 +3718,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
3610
3718
|
return "prOpen";
|
|
3611
3719
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
3612
3720
|
return "ciFixing";
|
|
3721
|
+
if (normalized === "merging" || normalized === "merge")
|
|
3722
|
+
return "merging";
|
|
3613
3723
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
3614
3724
|
return "needsAttention";
|
|
3615
3725
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -3738,9 +3848,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
|
|
|
3738
3848
|
return null;
|
|
3739
3849
|
return null;
|
|
3740
3850
|
}
|
|
3851
|
+
function githubProjectsEnabled(config) {
|
|
3852
|
+
const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
|
|
3853
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
3854
|
+
return projects?.enabled === true;
|
|
3855
|
+
}
|
|
3741
3856
|
async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
|
|
3742
3857
|
if (!run.taskId)
|
|
3743
|
-
return;
|
|
3858
|
+
return false;
|
|
3744
3859
|
const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
|
|
3745
3860
|
try {
|
|
3746
3861
|
const result = await syncGitHubProjectStatusForTaskUpdate({
|
|
@@ -3751,28 +3866,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
|
|
|
3751
3866
|
config
|
|
3752
3867
|
});
|
|
3753
3868
|
if (!result.synced && result.reason !== "project-sync-disabled") {
|
|
3869
|
+
const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
|
|
3754
3870
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
3755
3871
|
id: `log:${run.runId}:github-project-sync:${status}`,
|
|
3756
3872
|
title: "GitHub Project sync skipped",
|
|
3757
|
-
detail
|
|
3873
|
+
detail,
|
|
3758
3874
|
tone: "warn",
|
|
3759
3875
|
status: "running",
|
|
3760
3876
|
createdAt: new Date().toISOString(),
|
|
3761
3877
|
payload: { reason: result.reason, issueNodeId }
|
|
3762
3878
|
});
|
|
3879
|
+
if (githubProjectsEnabled(config)) {
|
|
3880
|
+
throw new Error(detail);
|
|
3881
|
+
}
|
|
3882
|
+
return false;
|
|
3763
3883
|
}
|
|
3884
|
+
return result.synced === true;
|
|
3764
3885
|
} catch (error) {
|
|
3886
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3765
3887
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
3766
3888
|
id: `log:${run.runId}:github-project-sync-error:${status}`,
|
|
3767
3889
|
title: "GitHub Project sync failed",
|
|
3768
|
-
detail
|
|
3890
|
+
detail,
|
|
3769
3891
|
tone: "error",
|
|
3770
3892
|
status: "running",
|
|
3771
3893
|
createdAt: new Date().toISOString(),
|
|
3772
3894
|
payload: { issueNodeId }
|
|
3773
3895
|
});
|
|
3896
|
+
if (githubProjectsEnabled(config)) {
|
|
3897
|
+
throw new Error(detail);
|
|
3898
|
+
}
|
|
3899
|
+
return false;
|
|
3774
3900
|
}
|
|
3775
3901
|
}
|
|
3902
|
+
function createCommandRunner(binary, extraEnv = {}) {
|
|
3903
|
+
return async (args, options) => {
|
|
3904
|
+
const child = spawn3(binary, [...args], {
|
|
3905
|
+
cwd: options?.cwd,
|
|
3906
|
+
env: { ...process.env, ...extraEnv },
|
|
3907
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3908
|
+
});
|
|
3909
|
+
const stdoutChunks = [];
|
|
3910
|
+
const stderrChunks = [];
|
|
3911
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
3912
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
3913
|
+
const exitCode = await new Promise((resolve15) => {
|
|
3914
|
+
child.once("error", () => resolve15(1));
|
|
3915
|
+
child.once("close", (code) => resolve15(code ?? 1));
|
|
3916
|
+
});
|
|
3917
|
+
return {
|
|
3918
|
+
exitCode,
|
|
3919
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
3920
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8")
|
|
3921
|
+
};
|
|
3922
|
+
};
|
|
3923
|
+
}
|
|
3924
|
+
function closeoutRecord(run) {
|
|
3925
|
+
const value = run.serverCloseout;
|
|
3926
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3927
|
+
}
|
|
3928
|
+
function closeoutPhasePatch(phase, status, extra = {}) {
|
|
3929
|
+
const updatedAt = new Date().toISOString();
|
|
3930
|
+
return {
|
|
3931
|
+
serverCloseout: {
|
|
3932
|
+
phase,
|
|
3933
|
+
status,
|
|
3934
|
+
updatedAt,
|
|
3935
|
+
...extra
|
|
3936
|
+
}
|
|
3937
|
+
};
|
|
3938
|
+
}
|
|
3939
|
+
function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
|
|
3940
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
3941
|
+
id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
|
|
3942
|
+
title: `Server closeout: ${phase}`,
|
|
3943
|
+
detail,
|
|
3944
|
+
tone,
|
|
3945
|
+
status,
|
|
3946
|
+
createdAt: new Date().toISOString()
|
|
3947
|
+
}, `server-closeout-${phase}`);
|
|
3948
|
+
}
|
|
3776
3949
|
async function autoAssignRunIssue(projectRoot, run) {
|
|
3777
3950
|
if (!run.taskId)
|
|
3778
3951
|
return;
|
|
@@ -3802,7 +3975,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
3802
3975
|
return;
|
|
3803
3976
|
}
|
|
3804
3977
|
const config = await loadRigLifecycleConfig(projectRoot);
|
|
3805
|
-
await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
3978
|
+
const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
3806
3979
|
if (status === "in_progress") {
|
|
3807
3980
|
await autoAssignRunIssue(projectRoot, run);
|
|
3808
3981
|
}
|
|
@@ -3818,24 +3991,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
3818
3991
|
});
|
|
3819
3992
|
return;
|
|
3820
3993
|
}
|
|
3821
|
-
const
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3994
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
3995
|
+
const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
|
|
3996
|
+
const rollbackProjectSync = async () => {
|
|
3997
|
+
if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
|
|
3998
|
+
return;
|
|
3999
|
+
await syncGitHubProjectStatusForTaskUpdate({
|
|
4000
|
+
taskId: run.taskId,
|
|
4001
|
+
status: previousStatus,
|
|
4002
|
+
issueNodeId: extractGitHubIssueNodeId(sourceTask),
|
|
4003
|
+
token: createGitHubAuthStore(projectRoot).readToken(),
|
|
4004
|
+
config
|
|
4005
|
+
}).catch((rollbackError) => {
|
|
4006
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
4007
|
+
id: `log:${run.runId}:github-project-sync-rollback:${status}`,
|
|
4008
|
+
title: "GitHub Project sync rollback failed",
|
|
4009
|
+
detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
|
|
4010
|
+
tone: "error",
|
|
4011
|
+
status: "running",
|
|
4012
|
+
createdAt: new Date().toISOString()
|
|
4013
|
+
});
|
|
4014
|
+
});
|
|
4015
|
+
};
|
|
4016
|
+
let result;
|
|
4017
|
+
try {
|
|
4018
|
+
result = await updateConfiguredTaskSourceTask(projectRoot, {
|
|
4019
|
+
taskId: run.taskId,
|
|
4020
|
+
sourceTask,
|
|
4021
|
+
update: {
|
|
3828
4022
|
status,
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
4023
|
+
comment: buildTaskRunLifecycleComment({
|
|
4024
|
+
runId: run.runId,
|
|
4025
|
+
status,
|
|
4026
|
+
summary,
|
|
4027
|
+
runtimeWorkspace: normalizeString(run.worktreePath),
|
|
4028
|
+
logsDir: normalizeString(run.logRoot),
|
|
4029
|
+
sessionDir: normalizeString(run.sessionPath),
|
|
4030
|
+
errorText: options.errorText ?? normalizeString(run.errorText)
|
|
4031
|
+
})
|
|
4032
|
+
}
|
|
4033
|
+
});
|
|
4034
|
+
} catch (error) {
|
|
4035
|
+
await rollbackProjectSync();
|
|
4036
|
+
throw error;
|
|
4037
|
+
}
|
|
3837
4038
|
if (!result.updated) {
|
|
3838
4039
|
if (result.source === "plugin" || result.sourceKind) {
|
|
4040
|
+
await rollbackProjectSync();
|
|
3839
4041
|
throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
|
|
3840
4042
|
}
|
|
3841
4043
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
@@ -3848,6 +4050,219 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
3848
4050
|
});
|
|
3849
4051
|
}
|
|
3850
4052
|
}
|
|
4053
|
+
async function runServerOwnedPrCloseout(state, runId) {
|
|
4054
|
+
const run = readAuthorityRun4(state.projectRoot, runId);
|
|
4055
|
+
if (!run)
|
|
4056
|
+
throw new Error(`Run not found: ${runId}`);
|
|
4057
|
+
const closeout = closeoutRecord(run);
|
|
4058
|
+
if (!closeout)
|
|
4059
|
+
return;
|
|
4060
|
+
const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
|
|
4061
|
+
if (!taskId)
|
|
4062
|
+
throw new Error("Server-owned closeout requires a task id.");
|
|
4063
|
+
const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
|
|
4064
|
+
const branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
|
|
4065
|
+
const config = await loadRigLifecycleConfig(state.projectRoot);
|
|
4066
|
+
const runPrMode = normalizeString(run.prMode);
|
|
4067
|
+
const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
|
|
4068
|
+
const effectiveConfig = {
|
|
4069
|
+
...config ?? {},
|
|
4070
|
+
pr: {
|
|
4071
|
+
...config?.pr ?? {},
|
|
4072
|
+
mode: prMode,
|
|
4073
|
+
autoFixChecks: false,
|
|
4074
|
+
autoFixReview: false
|
|
4075
|
+
}
|
|
4076
|
+
};
|
|
4077
|
+
const readCurrentRun = () => readAuthorityRun4(state.projectRoot, runId) ?? run;
|
|
4078
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
4079
|
+
const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
|
|
4080
|
+
const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
|
|
4081
|
+
const closeoutPrUrl = normalizeString(closeout.prUrl);
|
|
4082
|
+
if (closeoutPhase === "completed" || closeoutStatus === "completed") {
|
|
4083
|
+
return;
|
|
4084
|
+
}
|
|
4085
|
+
if (closeoutPhase === "close-source" && closeoutPrUrl) {
|
|
4086
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4087
|
+
status: "reviewing",
|
|
4088
|
+
...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
|
|
4089
|
+
});
|
|
4090
|
+
await closeIssueAfterMergedPr({
|
|
4091
|
+
projectRoot: state.projectRoot,
|
|
4092
|
+
taskId,
|
|
4093
|
+
runId,
|
|
4094
|
+
prUrl: closeoutPrUrl,
|
|
4095
|
+
sourceTask,
|
|
4096
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
4097
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
4098
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
4099
|
+
}
|
|
4100
|
+
});
|
|
4101
|
+
const completedAt = new Date().toISOString();
|
|
4102
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4103
|
+
status: "completed",
|
|
4104
|
+
completedAt,
|
|
4105
|
+
errorText: null,
|
|
4106
|
+
...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
|
|
4107
|
+
});
|
|
4108
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
|
|
4109
|
+
emitRigEvent(state, {
|
|
4110
|
+
type: "rig.run.completed",
|
|
4111
|
+
aggregateId: runId,
|
|
4112
|
+
payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
|
|
4113
|
+
createdAt: completedAt
|
|
4114
|
+
});
|
|
4115
|
+
return;
|
|
4116
|
+
}
|
|
4117
|
+
if (prMode === "off" || prMode === "ask") {
|
|
4118
|
+
const completedAt = new Date().toISOString();
|
|
4119
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4120
|
+
status: "completed",
|
|
4121
|
+
completedAt,
|
|
4122
|
+
errorText: null,
|
|
4123
|
+
...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
|
|
4124
|
+
});
|
|
4125
|
+
appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
|
|
4126
|
+
emitRigEvent(state, {
|
|
4127
|
+
type: "rig.run.completed",
|
|
4128
|
+
aggregateId: runId,
|
|
4129
|
+
payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
|
|
4130
|
+
createdAt: completedAt
|
|
4131
|
+
});
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
|
|
4135
|
+
const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
|
|
4136
|
+
const gitCommand = createCommandRunner("git", githubEnv);
|
|
4137
|
+
const ghCommand = createCommandRunner("gh", githubEnv);
|
|
4138
|
+
const setCloseout = (phase, status, extra = {}) => {
|
|
4139
|
+
const previous = closeoutRecord(readCurrentRun()) ?? closeout;
|
|
4140
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4141
|
+
status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
|
|
4142
|
+
...closeoutPhasePatch(phase, status, { ...previous, ...extra })
|
|
4143
|
+
});
|
|
4144
|
+
};
|
|
4145
|
+
setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
4146
|
+
appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
|
|
4147
|
+
const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
|
|
4148
|
+
appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
|
|
4149
|
+
setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
4150
|
+
const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
|
|
4151
|
+
if (push.exitCode !== 0) {
|
|
4152
|
+
throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
|
|
4153
|
+
}
|
|
4154
|
+
const sourceTaskForPr = {
|
|
4155
|
+
title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
|
|
4156
|
+
};
|
|
4157
|
+
const artifactRoot = resolve14(state.projectRoot, "artifacts", taskId);
|
|
4158
|
+
setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4159
|
+
const pr = await runPrAutomation({
|
|
4160
|
+
projectRoot: workspace,
|
|
4161
|
+
taskId,
|
|
4162
|
+
runId,
|
|
4163
|
+
branch,
|
|
4164
|
+
config: effectiveConfig,
|
|
4165
|
+
sourceTask: sourceTaskForPr,
|
|
4166
|
+
artifactRoot,
|
|
4167
|
+
command: ghCommand,
|
|
4168
|
+
gitCommand,
|
|
4169
|
+
steerPi: async (message) => {
|
|
4170
|
+
appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
|
|
4171
|
+
appendRunTimelineEntry(state.projectRoot, runId, {
|
|
4172
|
+
id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
|
|
4173
|
+
type: "user_message",
|
|
4174
|
+
text: message,
|
|
4175
|
+
createdAt: new Date().toISOString(),
|
|
4176
|
+
state: "completed"
|
|
4177
|
+
});
|
|
4178
|
+
},
|
|
4179
|
+
lifecycle: {
|
|
4180
|
+
onPrOpened: async ({ prUrl }) => {
|
|
4181
|
+
setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4182
|
+
appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
|
|
4183
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
|
|
4184
|
+
},
|
|
4185
|
+
onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
|
|
4186
|
+
onFeedback: async ({ feedback }) => {
|
|
4187
|
+
appendCloseoutStage(state, runId, "feedback", feedback.join(`
|
|
4188
|
+
`), "reviewing", "error");
|
|
4189
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
|
|
4190
|
+
},
|
|
4191
|
+
onMergeStarted: async ({ prUrl }) => {
|
|
4192
|
+
setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4193
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
4194
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
|
|
4195
|
+
},
|
|
4196
|
+
onMerged: ({ prUrl }) => {
|
|
4197
|
+
setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
|
|
4198
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
});
|
|
4202
|
+
if (pr.status === "merged" && pr.prUrl) {
|
|
4203
|
+
setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
|
|
4204
|
+
await closeIssueAfterMergedPr({
|
|
4205
|
+
projectRoot: state.projectRoot,
|
|
4206
|
+
taskId,
|
|
4207
|
+
runId,
|
|
4208
|
+
prUrl: pr.prUrl,
|
|
4209
|
+
sourceTask,
|
|
4210
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
4211
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
4212
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
4213
|
+
}
|
|
4214
|
+
});
|
|
4215
|
+
const completedAt = new Date().toISOString();
|
|
4216
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4217
|
+
status: "completed",
|
|
4218
|
+
completedAt,
|
|
4219
|
+
errorText: null,
|
|
4220
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
|
|
4221
|
+
});
|
|
4222
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
|
|
4223
|
+
emitRigEvent(state, {
|
|
4224
|
+
type: "rig.run.completed",
|
|
4225
|
+
aggregateId: runId,
|
|
4226
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
|
|
4227
|
+
createdAt: completedAt
|
|
4228
|
+
});
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
if (pr.status === "opened" && pr.prUrl) {
|
|
4232
|
+
const completedAt = new Date().toISOString();
|
|
4233
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4234
|
+
status: "completed",
|
|
4235
|
+
completedAt,
|
|
4236
|
+
errorText: null,
|
|
4237
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
|
|
4238
|
+
});
|
|
4239
|
+
appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
|
|
4240
|
+
emitRigEvent(state, {
|
|
4241
|
+
type: "rig.run.completed",
|
|
4242
|
+
aggregateId: runId,
|
|
4243
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
|
|
4244
|
+
createdAt: completedAt
|
|
4245
|
+
});
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
const detail = pr.actionableFeedback.join(`
|
|
4249
|
+
`) || "PR automation did not merge the PR.";
|
|
4250
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
|
|
4251
|
+
appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
|
|
4252
|
+
});
|
|
4253
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4254
|
+
status: "needs_attention",
|
|
4255
|
+
completedAt: new Date().toISOString(),
|
|
4256
|
+
errorText: detail,
|
|
4257
|
+
...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
|
|
4258
|
+
});
|
|
4259
|
+
appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
|
|
4260
|
+
emitRigEvent(state, {
|
|
4261
|
+
type: "rig.run.needs-attention",
|
|
4262
|
+
aggregateId: runId,
|
|
4263
|
+
payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
|
|
4264
|
+
});
|
|
4265
|
+
}
|
|
3851
4266
|
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
3852
4267
|
"completed",
|
|
3853
4268
|
"complete",
|
|
@@ -3873,11 +4288,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
|
|
|
3873
4288
|
return;
|
|
3874
4289
|
throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
|
|
3875
4290
|
}
|
|
4291
|
+
async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
|
|
4292
|
+
const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
4293
|
+
if (fromReader)
|
|
4294
|
+
return fromReader;
|
|
4295
|
+
const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
|
|
4296
|
+
if (projected)
|
|
4297
|
+
return projected;
|
|
4298
|
+
if (readTasks !== readWorkspaceTasks) {
|
|
4299
|
+
return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
4300
|
+
}
|
|
4301
|
+
return null;
|
|
4302
|
+
}
|
|
3876
4303
|
async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
|
|
3877
4304
|
if ("taskId" in input && input.taskId) {
|
|
3878
4305
|
assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
|
|
3879
4306
|
}
|
|
3880
|
-
const sourceTask = "taskId" in input && input.taskId ?
|
|
4307
|
+
const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
|
|
3881
4308
|
const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
|
|
3882
4309
|
const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
|
|
3883
4310
|
const runRecord = {
|
|
@@ -3949,6 +4376,7 @@ async function startLocalRun(state, runId, options) {
|
|
|
3949
4376
|
throw new Error(`Run not found: ${runId}`);
|
|
3950
4377
|
}
|
|
3951
4378
|
const startedAt = new Date().toISOString();
|
|
4379
|
+
const resumeMode = options?.resume === true;
|
|
3952
4380
|
state.runProcesses.set(runId, {
|
|
3953
4381
|
runId,
|
|
3954
4382
|
child: null,
|
|
@@ -3965,9 +4393,9 @@ async function startLocalRun(state, runId, options) {
|
|
|
3965
4393
|
summary: run.title
|
|
3966
4394
|
});
|
|
3967
4395
|
appendRunLogEntry(state.projectRoot, runId, {
|
|
3968
|
-
id: `log:${runId}:prepare`,
|
|
3969
|
-
title: "Rig task run starting",
|
|
3970
|
-
detail: run.taskId ?? run.title,
|
|
4396
|
+
id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
|
|
4397
|
+
title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
|
|
4398
|
+
detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
|
|
3971
4399
|
tone: "info",
|
|
3972
4400
|
status: "preparing",
|
|
3973
4401
|
createdAt: startedAt
|
|
@@ -4043,9 +4471,18 @@ async function startLocalRun(state, runId, options) {
|
|
|
4043
4471
|
RIG_HOST_PROJECT_ROOT: cliProjectRoot,
|
|
4044
4472
|
RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
|
|
4045
4473
|
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
4474
|
+
RIG_SERVER_OWNS_CLOSEOUT: "1",
|
|
4046
4475
|
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
4047
4476
|
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
4048
|
-
...bridgeGitHubToken ? {
|
|
4477
|
+
...bridgeGitHubToken ? {
|
|
4478
|
+
RIG_GITHUB_TOKEN: bridgeGitHubToken,
|
|
4479
|
+
GITHUB_TOKEN: bridgeGitHubToken,
|
|
4480
|
+
GH_TOKEN: bridgeGitHubToken
|
|
4481
|
+
} : {},
|
|
4482
|
+
...resumeMode ? {
|
|
4483
|
+
RIG_RUN_RESUME: "1",
|
|
4484
|
+
RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
|
|
4485
|
+
} : {}
|
|
4049
4486
|
},
|
|
4050
4487
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4051
4488
|
});
|
|
@@ -4131,6 +4568,38 @@ ${sourceFailure}` });
|
|
|
4131
4568
|
agent: current.runtimeAdapter,
|
|
4132
4569
|
summary: failureSummary
|
|
4133
4570
|
});
|
|
4571
|
+
} else if (closeoutRecord(current)?.status === "pending") {
|
|
4572
|
+
try {
|
|
4573
|
+
await runServerOwnedPrCloseout(state, runId);
|
|
4574
|
+
} catch (closeoutError) {
|
|
4575
|
+
const closeoutFailure = closeoutError instanceof Error ? closeoutError.message : String(closeoutError);
|
|
4576
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4577
|
+
status: "failed",
|
|
4578
|
+
completedAt: new Date().toISOString(),
|
|
4579
|
+
errorText: closeoutFailure,
|
|
4580
|
+
...closeoutPhasePatch("failed", "failed", { error: closeoutFailure })
|
|
4581
|
+
});
|
|
4582
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
4583
|
+
id: `log:${runId}:server-closeout-failed`,
|
|
4584
|
+
title: "Server-owned closeout failed",
|
|
4585
|
+
detail: closeoutFailure,
|
|
4586
|
+
tone: "error",
|
|
4587
|
+
status: "failed",
|
|
4588
|
+
createdAt: new Date().toISOString()
|
|
4589
|
+
}, "server-closeout-failed");
|
|
4590
|
+
if (current.taskId) {
|
|
4591
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: closeoutFailure }, "failed", "Rig server-owned closeout failed.", { errorText: closeoutFailure }).catch((error) => {
|
|
4592
|
+
appendRunLogEntry(state.projectRoot, runId, {
|
|
4593
|
+
id: `log:${runId}:task-source-closeout-failed-update`,
|
|
4594
|
+
title: "Task source closeout failure update failed",
|
|
4595
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
4596
|
+
tone: "error",
|
|
4597
|
+
status: "failed",
|
|
4598
|
+
createdAt: new Date().toISOString()
|
|
4599
|
+
});
|
|
4600
|
+
});
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4134
4603
|
}
|
|
4135
4604
|
broadcastSnapshotInvalidation(state);
|
|
4136
4605
|
} catch (error) {
|
|
@@ -4196,7 +4665,7 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
|
|
|
4196
4665
|
}
|
|
4197
4666
|
try {
|
|
4198
4667
|
const monorepoRoot = resolveMonorepoRoot3(projectRoot);
|
|
4199
|
-
const outerProjectRoot =
|
|
4668
|
+
const outerProjectRoot = dirname9(dirname9(monorepoRoot));
|
|
4200
4669
|
if (existsSync7(resolve14(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
|
|
4201
4670
|
return outerProjectRoot;
|
|
4202
4671
|
}
|
|
@@ -4217,7 +4686,13 @@ async function resumeRunRecord(state, input) {
|
|
|
4217
4686
|
if (run.status === "completed") {
|
|
4218
4687
|
throw new Error("Completed runs cannot be resumed.");
|
|
4219
4688
|
}
|
|
4220
|
-
|
|
4689
|
+
const closeout = closeoutRecord(run);
|
|
4690
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
4691
|
+
if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
|
|
4692
|
+
await runServerOwnedPrCloseout(state, input.runId);
|
|
4693
|
+
return;
|
|
4694
|
+
}
|
|
4695
|
+
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
|
|
4221
4696
|
}
|
|
4222
4697
|
function appendRunMessage(projectRoot, input) {
|
|
4223
4698
|
const run = readAuthorityRun4(projectRoot, input.runId);
|
|
@@ -4301,34 +4776,46 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
|
|
|
4301
4776
|
writeQueueState(projectRoot, next);
|
|
4302
4777
|
return next;
|
|
4303
4778
|
}
|
|
4304
|
-
var
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
|
|
4315
|
-
patchRunRecord(state.projectRoot, run.runId, {
|
|
4316
|
-
status: "failed",
|
|
4317
|
-
completedAt: run.completedAt ?? nowIso,
|
|
4318
|
-
updatedAt: nowIso,
|
|
4319
|
-
errorText: detail
|
|
4320
|
-
});
|
|
4321
|
-
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
4322
|
-
id: `log:${run.runId}:stale-local-run`,
|
|
4323
|
-
title: "Run marked stale after server restart",
|
|
4324
|
-
detail,
|
|
4325
|
-
tone: "error",
|
|
4326
|
-
status: "failed",
|
|
4327
|
-
createdAt: nowIso
|
|
4328
|
-
});
|
|
4329
|
-
changed = true;
|
|
4779
|
+
var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
|
|
4780
|
+
var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
4781
|
+
function processExists(pid) {
|
|
4782
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
4783
|
+
return false;
|
|
4784
|
+
try {
|
|
4785
|
+
process.kill(pid, 0);
|
|
4786
|
+
return true;
|
|
4787
|
+
} catch {
|
|
4788
|
+
return false;
|
|
4330
4789
|
}
|
|
4331
|
-
|
|
4790
|
+
}
|
|
4791
|
+
function recoverStaleLocalRun(projectRoot, run) {
|
|
4792
|
+
const record = run;
|
|
4793
|
+
if (run.mode !== "local")
|
|
4794
|
+
return false;
|
|
4795
|
+
const status = normalizeString(record.status)?.toLowerCase() ?? "";
|
|
4796
|
+
if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
|
|
4797
|
+
return false;
|
|
4798
|
+
const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
|
|
4799
|
+
const childPid = typeof record.pid === "number" ? record.pid : null;
|
|
4800
|
+
if (serverPid === null && childPid === null)
|
|
4801
|
+
return false;
|
|
4802
|
+
const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
|
|
4803
|
+
if (hasLiveRecordedProcess && serverPid === process.pid)
|
|
4804
|
+
return false;
|
|
4805
|
+
const completedAt = new Date().toISOString();
|
|
4806
|
+
patchRunRecord(projectRoot, run.runId, {
|
|
4807
|
+
status: "failed",
|
|
4808
|
+
completedAt,
|
|
4809
|
+
errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
|
|
4810
|
+
});
|
|
4811
|
+
return true;
|
|
4812
|
+
}
|
|
4813
|
+
function collectResumableServerCloseouts(state, runs) {
|
|
4814
|
+
return runs.filter((run) => {
|
|
4815
|
+
const closeout = closeoutRecord(run);
|
|
4816
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
4817
|
+
return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
|
|
4818
|
+
});
|
|
4332
4819
|
}
|
|
4333
4820
|
async function reconcileScheduler(state, reason) {
|
|
4334
4821
|
if (state.scheduler.reconciling) {
|
|
@@ -4343,7 +4830,36 @@ async function reconcileScheduler(state, reason) {
|
|
|
4343
4830
|
const queue = readQueueState(state.projectRoot);
|
|
4344
4831
|
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
4345
4832
|
let runs = listAuthorityRuns4(state.projectRoot);
|
|
4346
|
-
let changed =
|
|
4833
|
+
let changed = false;
|
|
4834
|
+
for (const run of runs) {
|
|
4835
|
+
if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
|
|
4836
|
+
changed = true;
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
if (changed) {
|
|
4840
|
+
runs = listAuthorityRuns4(state.projectRoot);
|
|
4841
|
+
}
|
|
4842
|
+
const resumableCloseouts = collectResumableServerCloseouts(state, runs);
|
|
4843
|
+
for (const run of resumableCloseouts) {
|
|
4844
|
+
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
4845
|
+
id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
|
|
4846
|
+
title: "Server-owned closeout auto-resume scheduled",
|
|
4847
|
+
detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
|
|
4848
|
+
tone: "info",
|
|
4849
|
+
status: "reviewing",
|
|
4850
|
+
createdAt: new Date().toISOString()
|
|
4851
|
+
});
|
|
4852
|
+
await runServerOwnedPrCloseout(state, run.runId).catch((error) => {
|
|
4853
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4854
|
+
patchRunRecord(state.projectRoot, run.runId, {
|
|
4855
|
+
status: "failed",
|
|
4856
|
+
completedAt: new Date().toISOString(),
|
|
4857
|
+
errorText: detail,
|
|
4858
|
+
...closeoutPhasePatch("failed", "failed", { error: detail })
|
|
4859
|
+
});
|
|
4860
|
+
});
|
|
4861
|
+
changed = true;
|
|
4862
|
+
}
|
|
4347
4863
|
if (changed) {
|
|
4348
4864
|
runs = listAuthorityRuns4(state.projectRoot);
|
|
4349
4865
|
}
|
|
@@ -4419,8 +4935,8 @@ async function reconcileScheduler(state, reason) {
|
|
|
4419
4935
|
// packages/server/src/server-helpers/http-router.ts
|
|
4420
4936
|
import { randomUUID } from "crypto";
|
|
4421
4937
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
4422
|
-
import { basename, dirname as
|
|
4423
|
-
import { copyFileSync, existsSync as
|
|
4938
|
+
import { basename, dirname as dirname15, isAbsolute as isAbsolute4, resolve as resolve20 } from "path";
|
|
4939
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync12 } from "fs";
|
|
4424
4940
|
import {
|
|
4425
4941
|
listAuthorityRuns as listAuthorityRuns5,
|
|
4426
4942
|
readAuthorityRun as readAuthorityRun6,
|
|
@@ -4444,7 +4960,7 @@ import {
|
|
|
4444
4960
|
} from "@rig/runtime/control-plane/remote";
|
|
4445
4961
|
|
|
4446
4962
|
// packages/server/src/server-helpers/run-steering.ts
|
|
4447
|
-
import { dirname as
|
|
4963
|
+
import { dirname as dirname10, resolve as resolve15 } from "path";
|
|
4448
4964
|
import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync5 } from "fs";
|
|
4449
4965
|
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun5, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
|
|
4450
4966
|
var steeringSequence = 0;
|
|
@@ -4531,7 +5047,7 @@ function queueRunSteeringMessage(projectRoot, runId, input) {
|
|
|
4531
5047
|
delivered: false
|
|
4532
5048
|
};
|
|
4533
5049
|
const path = runSteeringPath(projectRoot, runId);
|
|
4534
|
-
mkdirSync8(
|
|
5050
|
+
mkdirSync8(dirname10(path), { recursive: true });
|
|
4535
5051
|
appendJsonlRecord2(path, entry);
|
|
4536
5052
|
appendRunTimelineEntry(projectRoot, runId, {
|
|
4537
5053
|
id: entry.id,
|
|
@@ -4568,6 +5084,187 @@ import {
|
|
|
4568
5084
|
updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
|
|
4569
5085
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
4570
5086
|
|
|
5087
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
5088
|
+
import { chmodSync as chmodSync3, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
5089
|
+
import { dirname as dirname12, resolve as resolve17 } from "path";
|
|
5090
|
+
|
|
5091
|
+
// packages/server/src/server-helpers/github-user-namespace.ts
|
|
5092
|
+
import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
5093
|
+
import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve16 } from "path";
|
|
5094
|
+
function cleanString3(value) {
|
|
5095
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
5096
|
+
}
|
|
5097
|
+
function sanitizePathSegment(value) {
|
|
5098
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
|
|
5099
|
+
}
|
|
5100
|
+
function deriveGitHubUserNamespaceKey(identity) {
|
|
5101
|
+
const userId = cleanString3(identity.userId);
|
|
5102
|
+
if (userId) {
|
|
5103
|
+
const safeId = sanitizePathSegment(userId);
|
|
5104
|
+
if (safeId)
|
|
5105
|
+
return `ghu-${safeId}`;
|
|
5106
|
+
}
|
|
5107
|
+
const login = cleanString3(identity.login);
|
|
5108
|
+
if (login) {
|
|
5109
|
+
const safeLogin = sanitizePathSegment(login);
|
|
5110
|
+
if (safeLogin)
|
|
5111
|
+
return `ghu-login-${safeLogin}`;
|
|
5112
|
+
}
|
|
5113
|
+
throw new Error("GitHub user namespace requires a user id or login");
|
|
5114
|
+
}
|
|
5115
|
+
function resolveRemoteUserNamespacesRoot(projectRoot) {
|
|
5116
|
+
const explicitRoot = cleanString3(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
|
|
5117
|
+
if (explicitRoot)
|
|
5118
|
+
return resolve16(explicitRoot);
|
|
5119
|
+
const stateDir2 = cleanString3(process.env.RIG_STATE_DIR);
|
|
5120
|
+
if (stateDir2)
|
|
5121
|
+
return resolve16(dirname11(resolve16(stateDir2)), "users");
|
|
5122
|
+
return resolve16(projectRoot, ".rig", "users");
|
|
5123
|
+
}
|
|
5124
|
+
function resolveRemoteUserNamespace(projectRoot, identity) {
|
|
5125
|
+
const key = deriveGitHubUserNamespaceKey(identity);
|
|
5126
|
+
const root = resolve16(resolveRemoteUserNamespacesRoot(projectRoot), key);
|
|
5127
|
+
const stateDir2 = resolve16(root, ".rig", "state");
|
|
5128
|
+
return {
|
|
5129
|
+
key,
|
|
5130
|
+
userId: cleanString3(identity.userId),
|
|
5131
|
+
login: cleanString3(identity.login),
|
|
5132
|
+
root,
|
|
5133
|
+
stateDir: stateDir2,
|
|
5134
|
+
authStateFile: resolve16(stateDir2, "github-auth.json"),
|
|
5135
|
+
metadataFile: resolve16(stateDir2, "user-namespace.json"),
|
|
5136
|
+
checkoutBaseDir: resolve16(root, "remote-checkouts"),
|
|
5137
|
+
snapshotBaseDir: resolve16(root, "remote-snapshots")
|
|
5138
|
+
};
|
|
5139
|
+
}
|
|
5140
|
+
function serializeRemoteUserNamespace(namespace) {
|
|
5141
|
+
return {
|
|
5142
|
+
key: namespace.key,
|
|
5143
|
+
userId: namespace.userId,
|
|
5144
|
+
login: namespace.login,
|
|
5145
|
+
root: namespace.root,
|
|
5146
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
5147
|
+
snapshotBaseDir: namespace.snapshotBaseDir
|
|
5148
|
+
};
|
|
5149
|
+
}
|
|
5150
|
+
function isPathInsideNamespace(namespaceRoot, candidatePath) {
|
|
5151
|
+
const root = resolve16(namespaceRoot);
|
|
5152
|
+
const candidate = resolve16(candidatePath);
|
|
5153
|
+
const rel = relative3(root, candidate);
|
|
5154
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
5155
|
+
}
|
|
5156
|
+
function writeRemoteUserNamespaceMetadata(namespace) {
|
|
5157
|
+
mkdirSync9(namespace.stateDir, { recursive: true });
|
|
5158
|
+
const previous = (() => {
|
|
5159
|
+
if (!existsSync9(namespace.metadataFile))
|
|
5160
|
+
return null;
|
|
5161
|
+
try {
|
|
5162
|
+
const parsed = JSON.parse(readFileSync6(namespace.metadataFile, "utf8"));
|
|
5163
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
5164
|
+
} catch {
|
|
5165
|
+
return null;
|
|
5166
|
+
}
|
|
5167
|
+
})();
|
|
5168
|
+
const now = new Date().toISOString();
|
|
5169
|
+
writeFileSync8(namespace.metadataFile, `${JSON.stringify({
|
|
5170
|
+
key: namespace.key,
|
|
5171
|
+
userId: namespace.userId,
|
|
5172
|
+
login: namespace.login,
|
|
5173
|
+
root: namespace.root,
|
|
5174
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
5175
|
+
snapshotBaseDir: namespace.snapshotBaseDir,
|
|
5176
|
+
createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
|
|
5177
|
+
updatedAt: now
|
|
5178
|
+
}, null, 2)}
|
|
5179
|
+
`, { encoding: "utf8", mode: 384 });
|
|
5180
|
+
try {
|
|
5181
|
+
chmodSync2(namespace.metadataFile, 384);
|
|
5182
|
+
} catch {}
|
|
5183
|
+
}
|
|
5184
|
+
|
|
5185
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
5186
|
+
function cleanString4(value) {
|
|
5187
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
5188
|
+
}
|
|
5189
|
+
function resolveGitHubApiSessionIndexFile(projectRoot) {
|
|
5190
|
+
return resolve17(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
|
|
5191
|
+
}
|
|
5192
|
+
function parseEntry(value) {
|
|
5193
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5194
|
+
return null;
|
|
5195
|
+
const record = value;
|
|
5196
|
+
const token = cleanString4(record.token);
|
|
5197
|
+
const namespaceKey = cleanString4(record.namespaceKey);
|
|
5198
|
+
const namespaceRoot = cleanString4(record.namespaceRoot);
|
|
5199
|
+
const authStateFile = cleanString4(record.authStateFile);
|
|
5200
|
+
const checkoutBaseDir = cleanString4(record.checkoutBaseDir);
|
|
5201
|
+
const snapshotBaseDir = cleanString4(record.snapshotBaseDir);
|
|
5202
|
+
const createdAt = cleanString4(record.createdAt);
|
|
5203
|
+
if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
|
|
5204
|
+
return null;
|
|
5205
|
+
return {
|
|
5206
|
+
token,
|
|
5207
|
+
namespaceKey,
|
|
5208
|
+
namespaceRoot,
|
|
5209
|
+
authStateFile,
|
|
5210
|
+
checkoutBaseDir,
|
|
5211
|
+
snapshotBaseDir,
|
|
5212
|
+
createdAt,
|
|
5213
|
+
login: cleanString4(record.login),
|
|
5214
|
+
userId: cleanString4(record.userId),
|
|
5215
|
+
selectedRepo: cleanString4(record.selectedRepo)
|
|
5216
|
+
};
|
|
5217
|
+
}
|
|
5218
|
+
function readIndex(indexFile) {
|
|
5219
|
+
if (!existsSync10(indexFile))
|
|
5220
|
+
return [];
|
|
5221
|
+
try {
|
|
5222
|
+
const parsed = JSON.parse(readFileSync7(indexFile, "utf8"));
|
|
5223
|
+
return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
|
|
5224
|
+
const parsedEntry = parseEntry(entry);
|
|
5225
|
+
return parsedEntry ? [parsedEntry] : [];
|
|
5226
|
+
}) : [];
|
|
5227
|
+
} catch {
|
|
5228
|
+
return [];
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
function writeIndex(indexFile, sessions) {
|
|
5232
|
+
mkdirSync10(dirname12(indexFile), { recursive: true });
|
|
5233
|
+
writeFileSync9(indexFile, `${JSON.stringify({ sessions }, null, 2)}
|
|
5234
|
+
`, { encoding: "utf8", mode: 384 });
|
|
5235
|
+
try {
|
|
5236
|
+
chmodSync3(indexFile, 384);
|
|
5237
|
+
} catch {}
|
|
5238
|
+
}
|
|
5239
|
+
function registerGitHubApiSession(input) {
|
|
5240
|
+
const cleanToken = cleanString4(input.token);
|
|
5241
|
+
if (!cleanToken)
|
|
5242
|
+
throw new Error("GitHub API session token is required");
|
|
5243
|
+
const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
|
|
5244
|
+
const createdAt = new Date().toISOString();
|
|
5245
|
+
const entry = {
|
|
5246
|
+
token: cleanToken,
|
|
5247
|
+
login: input.namespace.login,
|
|
5248
|
+
userId: input.namespace.userId,
|
|
5249
|
+
namespaceKey: input.namespace.key,
|
|
5250
|
+
namespaceRoot: input.namespace.root,
|
|
5251
|
+
authStateFile: input.namespace.authStateFile,
|
|
5252
|
+
checkoutBaseDir: input.namespace.checkoutBaseDir,
|
|
5253
|
+
snapshotBaseDir: input.namespace.snapshotBaseDir,
|
|
5254
|
+
selectedRepo: cleanString4(input.selectedRepo),
|
|
5255
|
+
createdAt
|
|
5256
|
+
};
|
|
5257
|
+
const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
|
|
5258
|
+
writeIndex(indexFile, [...previous.slice(-199), entry]);
|
|
5259
|
+
return entry;
|
|
5260
|
+
}
|
|
5261
|
+
function readGitHubApiSession(input) {
|
|
5262
|
+
const cleanToken = cleanString4(input.token);
|
|
5263
|
+
if (!cleanToken)
|
|
5264
|
+
return null;
|
|
5265
|
+
return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
|
|
5266
|
+
}
|
|
5267
|
+
|
|
4571
5268
|
// packages/server/src/server-helpers/inspector-agent-lifecycle.ts
|
|
4572
5269
|
function createInspectorAgentLifecycleController(options) {
|
|
4573
5270
|
const initialDelayMs = Math.max(1, options.initialDelayMs ?? 5000);
|
|
@@ -4671,21 +5368,21 @@ function inspectorAgentLifecycleSnapshot(input) {
|
|
|
4671
5368
|
// packages/server/src/server-helpers/project-registry.ts
|
|
4672
5369
|
import { createHash as createHash2 } from "crypto";
|
|
4673
5370
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
4674
|
-
import { existsSync as
|
|
4675
|
-
import { dirname as
|
|
5371
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync8, readdirSync as readdirSync3, writeFileSync as writeFileSync10 } from "fs";
|
|
5372
|
+
import { dirname as dirname13, resolve as resolve18 } from "path";
|
|
4676
5373
|
function normalizeRepoSlug(value) {
|
|
4677
5374
|
const trimmed = value.trim();
|
|
4678
5375
|
return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
|
|
4679
5376
|
}
|
|
4680
5377
|
function registryPath(projectRoot) {
|
|
4681
|
-
return
|
|
5378
|
+
return resolve18(projectRoot, ".rig", "state", "projects.json");
|
|
4682
5379
|
}
|
|
4683
5380
|
function readRegistry(projectRoot) {
|
|
4684
5381
|
const path = registryPath(projectRoot);
|
|
4685
|
-
if (!
|
|
5382
|
+
if (!existsSync11(path))
|
|
4686
5383
|
return {};
|
|
4687
5384
|
try {
|
|
4688
|
-
const payload = JSON.parse(
|
|
5385
|
+
const payload = JSON.parse(readFileSync8(path, "utf8"));
|
|
4689
5386
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
4690
5387
|
return {};
|
|
4691
5388
|
const projects = payload.projects;
|
|
@@ -4696,14 +5393,14 @@ function readRegistry(projectRoot) {
|
|
|
4696
5393
|
}
|
|
4697
5394
|
function writeRegistry(projectRoot, projects) {
|
|
4698
5395
|
const path = registryPath(projectRoot);
|
|
4699
|
-
|
|
4700
|
-
|
|
5396
|
+
mkdirSync11(dirname13(path), { recursive: true });
|
|
5397
|
+
writeFileSync10(path, `${JSON.stringify({ projects }, null, 2)}
|
|
4701
5398
|
`, "utf8");
|
|
4702
5399
|
}
|
|
4703
5400
|
function resolveConfigPath(projectRoot) {
|
|
4704
5401
|
for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
|
|
4705
|
-
const path =
|
|
4706
|
-
if (
|
|
5402
|
+
const path = resolve18(projectRoot, name);
|
|
5403
|
+
if (existsSync11(path))
|
|
4707
5404
|
return path;
|
|
4708
5405
|
}
|
|
4709
5406
|
return null;
|
|
@@ -4712,7 +5409,7 @@ function hashFile(path) {
|
|
|
4712
5409
|
if (!path)
|
|
4713
5410
|
return null;
|
|
4714
5411
|
try {
|
|
4715
|
-
return createHash2("sha256").update(
|
|
5412
|
+
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
4716
5413
|
} catch {
|
|
4717
5414
|
return null;
|
|
4718
5415
|
}
|
|
@@ -4728,11 +5425,11 @@ function readDefaultBranch(projectRoot) {
|
|
|
4728
5425
|
return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
|
|
4729
5426
|
}
|
|
4730
5427
|
function buildRunSummary(projectRoot) {
|
|
4731
|
-
const runsDir =
|
|
5428
|
+
const runsDir = resolve18(projectRoot, ".rig", "runs");
|
|
4732
5429
|
try {
|
|
4733
5430
|
const runs = readdirSync3(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
|
|
4734
5431
|
try {
|
|
4735
|
-
const run = JSON.parse(
|
|
5432
|
+
const run = JSON.parse(readFileSync8(resolve18(runsDir, entry.name, "run.json"), "utf8"));
|
|
4736
5433
|
return [{ runId: typeof run.runId === "string" ? run.runId : entry.name, status: typeof run.status === "string" ? run.status : "unknown", updatedAt: typeof run.updatedAt === "string" ? run.updatedAt : "" }];
|
|
4737
5434
|
} catch {
|
|
4738
5435
|
return [];
|
|
@@ -4784,10 +5481,14 @@ function upsertProjectRecord(projectRoot, input) {
|
|
|
4784
5481
|
function linkProjectCheckout(projectRoot, repoSlug, checkout) {
|
|
4785
5482
|
return upsertProjectRecord(projectRoot, { repoSlug, checkout });
|
|
4786
5483
|
}
|
|
5484
|
+
function projectRegistryContainsCheckout(projectRoot, checkoutPath) {
|
|
5485
|
+
const target = resolve18(checkoutPath);
|
|
5486
|
+
return Object.values(readRegistry(projectRoot)).some((project) => project.checkouts.some((checkout) => checkout.path ? resolve18(checkout.path) === target : false));
|
|
5487
|
+
}
|
|
4787
5488
|
|
|
4788
5489
|
// packages/server/src/server-helpers/remote-checkout.ts
|
|
4789
|
-
import { existsSync as
|
|
4790
|
-
import { dirname as
|
|
5490
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
|
|
5491
|
+
import { dirname as dirname14, isAbsolute as isAbsolute3, relative as relative4, resolve as resolve19 } from "path";
|
|
4791
5492
|
function safeSlugSegments(repoSlug) {
|
|
4792
5493
|
const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
|
|
4793
5494
|
if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
|
|
@@ -4804,7 +5505,7 @@ function safeCheckoutKey(value) {
|
|
|
4804
5505
|
}
|
|
4805
5506
|
function repoSlugPath(baseDir, repoSlug, checkoutKey) {
|
|
4806
5507
|
const key = safeCheckoutKey(checkoutKey);
|
|
4807
|
-
return
|
|
5508
|
+
return resolve19(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
|
|
4808
5509
|
}
|
|
4809
5510
|
function sanitizeSnapshotId(value, fallback) {
|
|
4810
5511
|
const raw = (value ?? fallback).trim();
|
|
@@ -4812,7 +5513,7 @@ function sanitizeSnapshotId(value, fallback) {
|
|
|
4812
5513
|
return safe || fallback;
|
|
4813
5514
|
}
|
|
4814
5515
|
function assertWithinRoot(root, relativePath) {
|
|
4815
|
-
if (!relativePath ||
|
|
5516
|
+
if (!relativePath || isAbsolute3(relativePath) || relativePath.includes("\x00")) {
|
|
4816
5517
|
throw new Error(`Invalid snapshot file path: ${relativePath}`);
|
|
4817
5518
|
}
|
|
4818
5519
|
const normalizedRelative = relativePath.replace(/\\/g, "/");
|
|
@@ -4820,9 +5521,9 @@ function assertWithinRoot(root, relativePath) {
|
|
|
4820
5521
|
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
4821
5522
|
throw new Error(`Unsafe snapshot file path: ${relativePath}`);
|
|
4822
5523
|
}
|
|
4823
|
-
const target =
|
|
4824
|
-
const rel =
|
|
4825
|
-
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." ||
|
|
5524
|
+
const target = resolve19(root, ...segments);
|
|
5525
|
+
const rel = relative4(root, target);
|
|
5526
|
+
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute3(rel)) {
|
|
4826
5527
|
throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
|
|
4827
5528
|
}
|
|
4828
5529
|
return target;
|
|
@@ -4840,17 +5541,17 @@ function parseSnapshotArchiveContentBase64(contentBase64) {
|
|
|
4840
5541
|
function extractUploadedSnapshotArchive(input) {
|
|
4841
5542
|
const archive = decodeSnapshotArchive(input.archive);
|
|
4842
5543
|
const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
|
|
4843
|
-
const checkoutPath =
|
|
4844
|
-
|
|
5544
|
+
const checkoutPath = resolve19(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
|
|
5545
|
+
mkdirSync12(checkoutPath, { recursive: true });
|
|
4845
5546
|
for (const file of archive.files) {
|
|
4846
5547
|
if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
|
|
4847
5548
|
throw new Error("Invalid snapshot archive file entry");
|
|
4848
5549
|
}
|
|
4849
5550
|
const target = assertWithinRoot(checkoutPath, file.path);
|
|
4850
|
-
|
|
4851
|
-
|
|
5551
|
+
mkdirSync12(dirname14(target), { recursive: true });
|
|
5552
|
+
writeFileSync11(target, Buffer.from(file.contentBase64, "base64"));
|
|
4852
5553
|
}
|
|
4853
|
-
|
|
5554
|
+
writeFileSync11(resolve19(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
|
|
4854
5555
|
repoSlug: input.repoSlug,
|
|
4855
5556
|
snapshotId,
|
|
4856
5557
|
fileCount: archive.files.length,
|
|
@@ -4885,7 +5586,7 @@ function gitCredentialConfig(token) {
|
|
|
4885
5586
|
};
|
|
4886
5587
|
}
|
|
4887
5588
|
async function prepareRemoteCheckout(input) {
|
|
4888
|
-
const exists = input.exists ??
|
|
5589
|
+
const exists = input.exists ?? existsSync12;
|
|
4889
5590
|
const strategy = input.strategy;
|
|
4890
5591
|
if (strategy.kind === "uploaded-snapshot") {
|
|
4891
5592
|
return extractUploadedSnapshotArchive({
|
|
@@ -4897,7 +5598,7 @@ async function prepareRemoteCheckout(input) {
|
|
|
4897
5598
|
});
|
|
4898
5599
|
}
|
|
4899
5600
|
if (strategy.kind === "existing-path") {
|
|
4900
|
-
const checkoutPath2 =
|
|
5601
|
+
const checkoutPath2 = resolve19(strategy.path);
|
|
4901
5602
|
if (!exists(checkoutPath2)) {
|
|
4902
5603
|
throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
|
|
4903
5604
|
}
|
|
@@ -4933,9 +5634,9 @@ function buildServerControlStatus() {
|
|
|
4933
5634
|
};
|
|
4934
5635
|
}
|
|
4935
5636
|
function buildProjectConfigStatus(root) {
|
|
4936
|
-
const hasConfigTs =
|
|
4937
|
-
const hasConfigJson =
|
|
4938
|
-
const hasLegacyTaskConfig =
|
|
5637
|
+
const hasConfigTs = existsSync13(resolve20(root, "rig.config.ts"));
|
|
5638
|
+
const hasConfigJson = existsSync13(resolve20(root, "rig.config.json"));
|
|
5639
|
+
const hasLegacyTaskConfig = existsSync13(resolve20(root, ".rig", "task-config.json"));
|
|
4939
5640
|
let kind = "missing";
|
|
4940
5641
|
if (hasConfigTs)
|
|
4941
5642
|
kind = "rig-config-ts";
|
|
@@ -4952,6 +5653,75 @@ function buildProjectConfigStatus(root) {
|
|
|
4952
5653
|
suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
|
|
4953
5654
|
};
|
|
4954
5655
|
}
|
|
5656
|
+
var RIG_GITHUB_LIFECYCLE_LABELS = [
|
|
5657
|
+
"ready",
|
|
5658
|
+
"blocked",
|
|
5659
|
+
"in-progress",
|
|
5660
|
+
"under-review",
|
|
5661
|
+
"failed",
|
|
5662
|
+
"cancelled",
|
|
5663
|
+
"rig:running",
|
|
5664
|
+
"rig:pr-open",
|
|
5665
|
+
"rig:ci-fixing",
|
|
5666
|
+
"rig:merging",
|
|
5667
|
+
"rig:done",
|
|
5668
|
+
"rig:needs-attention"
|
|
5669
|
+
];
|
|
5670
|
+
function githubProjectsEnabled2(config) {
|
|
5671
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
5672
|
+
return false;
|
|
5673
|
+
const root = config;
|
|
5674
|
+
const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
|
|
5675
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
5676
|
+
return projects?.enabled === true;
|
|
5677
|
+
}
|
|
5678
|
+
function githubIssueSourceRepo(config) {
|
|
5679
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
5680
|
+
return null;
|
|
5681
|
+
const root = config;
|
|
5682
|
+
const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
|
|
5683
|
+
const owner = normalizeString(taskSource?.owner);
|
|
5684
|
+
const repo = normalizeString(taskSource?.repo);
|
|
5685
|
+
if (taskSource?.kind === "github-issues" && owner && repo)
|
|
5686
|
+
return { owner, repo };
|
|
5687
|
+
const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
|
|
5688
|
+
const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
|
|
5689
|
+
const match = slug?.match(/^([^/]+)\/([^/]+)$/);
|
|
5690
|
+
return match ? { owner: match[1], repo: match[2] } : null;
|
|
5691
|
+
}
|
|
5692
|
+
async function ensureGitHubLifecycleLabels(projectRoot, config) {
|
|
5693
|
+
const repo = githubIssueSourceRepo(config);
|
|
5694
|
+
if (!repo)
|
|
5695
|
+
return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
5696
|
+
const token = createGitHubAuthStore(projectRoot).readToken();
|
|
5697
|
+
if (!token)
|
|
5698
|
+
return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
5699
|
+
const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
|
|
5700
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
|
|
5701
|
+
});
|
|
5702
|
+
const existingJson = await existingResponse.json().catch(() => []);
|
|
5703
|
+
const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
|
|
5704
|
+
const created = [];
|
|
5705
|
+
const alreadyPresent = [];
|
|
5706
|
+
const failed = [];
|
|
5707
|
+
for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
|
|
5708
|
+
if (existing.has(label)) {
|
|
5709
|
+
alreadyPresent.push(label);
|
|
5710
|
+
continue;
|
|
5711
|
+
}
|
|
5712
|
+
const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
|
|
5713
|
+
method: "POST",
|
|
5714
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
|
|
5715
|
+
body: JSON.stringify({ name: label, color: label.startsWith("rig:") ? "6f42c1" : "ededed", description: label.startsWith("rig:") ? "Task status managed by Rig" : "Task lifecycle status managed by Rig" })
|
|
5716
|
+
});
|
|
5717
|
+
if (response.ok || response.status === 422) {
|
|
5718
|
+
(response.status === 422 ? alreadyPresent : created).push(label);
|
|
5719
|
+
} else {
|
|
5720
|
+
failed.push({ label, error: await response.text().catch(() => response.statusText) });
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
|
|
5724
|
+
}
|
|
4955
5725
|
function normalizeCommit(value) {
|
|
4956
5726
|
const raw = normalizeString(value);
|
|
4957
5727
|
return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
|
|
@@ -4971,24 +5741,24 @@ function repoParts(repoSlug) {
|
|
|
4971
5741
|
return { owner, repo, slug: `${owner}/${repo}` };
|
|
4972
5742
|
}
|
|
4973
5743
|
function repairDir(checkoutPath) {
|
|
4974
|
-
const dir =
|
|
4975
|
-
|
|
5744
|
+
const dir = resolve20(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
|
|
5745
|
+
mkdirSync13(dir, { recursive: true });
|
|
4976
5746
|
return dir;
|
|
4977
5747
|
}
|
|
4978
5748
|
function backupCheckoutFile(checkoutPath, relativePath) {
|
|
4979
|
-
const source =
|
|
4980
|
-
const backupPath =
|
|
4981
|
-
|
|
4982
|
-
|
|
5749
|
+
const source = resolve20(checkoutPath, relativePath);
|
|
5750
|
+
const backupPath = resolve20(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
|
|
5751
|
+
mkdirSync13(dirname15(backupPath), { recursive: true });
|
|
5752
|
+
copyFileSync2(source, backupPath);
|
|
4983
5753
|
return backupPath;
|
|
4984
5754
|
}
|
|
4985
5755
|
function parsePackageJsonLosslessly(checkoutPath) {
|
|
4986
|
-
const packagePath =
|
|
4987
|
-
if (!
|
|
5756
|
+
const packagePath = resolve20(checkoutPath, "package.json");
|
|
5757
|
+
if (!existsSync13(packagePath)) {
|
|
4988
5758
|
return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
|
|
4989
5759
|
}
|
|
4990
5760
|
try {
|
|
4991
|
-
const parsed = JSON.parse(
|
|
5761
|
+
const parsed = JSON.parse(readFileSync9(packagePath, "utf8"));
|
|
4992
5762
|
return {
|
|
4993
5763
|
existed: true,
|
|
4994
5764
|
packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
|
|
@@ -5002,9 +5772,9 @@ function parsePackageJsonLosslessly(checkoutPath) {
|
|
|
5002
5772
|
}
|
|
5003
5773
|
}
|
|
5004
5774
|
function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
5005
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
5006
|
-
const packagePath =
|
|
5007
|
-
if (!hasConfig && !
|
|
5775
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
|
|
5776
|
+
const packagePath = resolve20(checkoutPath, "package.json");
|
|
5777
|
+
if (!hasConfig && !existsSync13(packagePath)) {
|
|
5008
5778
|
return { skipped: true, reason: "package.json and rig.config missing" };
|
|
5009
5779
|
}
|
|
5010
5780
|
const parsed = parsePackageJsonLosslessly(checkoutPath);
|
|
@@ -5023,7 +5793,7 @@ function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
|
5023
5793
|
}
|
|
5024
5794
|
const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
|
|
5025
5795
|
if (changed) {
|
|
5026
|
-
|
|
5796
|
+
writeFileSync12(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
|
|
5027
5797
|
`, "utf8");
|
|
5028
5798
|
}
|
|
5029
5799
|
return {
|
|
@@ -5049,11 +5819,11 @@ function configLooksStructurallyUsable(source) {
|
|
|
5049
5819
|
return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
|
|
5050
5820
|
}
|
|
5051
5821
|
function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
|
|
5052
|
-
const configPath =
|
|
5053
|
-
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
5822
|
+
const configPath = resolve20(checkoutPath, "rig.config.ts");
|
|
5823
|
+
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
|
|
5054
5824
|
if (existingConfigName) {
|
|
5055
|
-
const existingPath =
|
|
5056
|
-
const source =
|
|
5825
|
+
const existingPath = resolve20(checkoutPath, existingConfigName);
|
|
5826
|
+
const source = readFileSync9(existingPath, "utf8");
|
|
5057
5827
|
if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
|
|
5058
5828
|
return { path: existingPath, changed: false, reason: "config structurally complete" };
|
|
5059
5829
|
}
|
|
@@ -5067,7 +5837,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
5067
5837
|
}
|
|
5068
5838
|
}
|
|
5069
5839
|
const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
|
|
5070
|
-
|
|
5840
|
+
writeFileSync12(configPath, generatedRigConfigSource(repoSlug), "utf8");
|
|
5071
5841
|
return {
|
|
5072
5842
|
path: configPath,
|
|
5073
5843
|
changed: true,
|
|
@@ -5076,7 +5846,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
5076
5846
|
};
|
|
5077
5847
|
}
|
|
5078
5848
|
function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
5079
|
-
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
5849
|
+
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
|
|
5080
5850
|
if (!configFile)
|
|
5081
5851
|
return { ok: false, error: "missing rig config" };
|
|
5082
5852
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -5098,7 +5868,7 @@ function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
|
5098
5868
|
return { ok: true, configFile };
|
|
5099
5869
|
}
|
|
5100
5870
|
function installRemoteCheckoutPackages(checkoutPath) {
|
|
5101
|
-
if (!
|
|
5871
|
+
if (!existsSync13(resolve20(checkoutPath, "package.json"))) {
|
|
5102
5872
|
return { skipped: true, reason: "package.json missing" };
|
|
5103
5873
|
}
|
|
5104
5874
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -5111,8 +5881,8 @@ function installRemoteCheckoutPackages(checkoutPath) {
|
|
|
5111
5881
|
return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
|
|
5112
5882
|
}
|
|
5113
5883
|
function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
|
|
5114
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
5115
|
-
const hasPackage =
|
|
5884
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
|
|
5885
|
+
const hasPackage = existsSync13(resolve20(checkoutPath, "package.json"));
|
|
5116
5886
|
if (!hasConfig && !hasPackage) {
|
|
5117
5887
|
return {
|
|
5118
5888
|
packageJson: { skipped: true, reason: "package.json and rig.config missing" },
|
|
@@ -5210,26 +5980,26 @@ function buildRemoteRunLogEntry(body, identifiers) {
|
|
|
5210
5980
|
}
|
|
5211
5981
|
function readGitHeadCommit(projectRoot) {
|
|
5212
5982
|
try {
|
|
5213
|
-
let gitDir =
|
|
5983
|
+
let gitDir = resolve20(projectRoot, ".git");
|
|
5214
5984
|
try {
|
|
5215
|
-
const dotGit =
|
|
5985
|
+
const dotGit = readFileSync9(gitDir, "utf8").trim();
|
|
5216
5986
|
const gitDirPrefix = "gitdir:";
|
|
5217
5987
|
if (dotGit.startsWith(gitDirPrefix)) {
|
|
5218
|
-
gitDir =
|
|
5988
|
+
gitDir = resolve20(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
|
|
5219
5989
|
}
|
|
5220
5990
|
} catch {}
|
|
5221
|
-
const head =
|
|
5991
|
+
const head = readFileSync9(resolve20(gitDir, "HEAD"), "utf8").trim();
|
|
5222
5992
|
const refPrefix = "ref:";
|
|
5223
5993
|
if (!head.startsWith(refPrefix)) {
|
|
5224
5994
|
return normalizeCommit(head);
|
|
5225
5995
|
}
|
|
5226
5996
|
const ref = head.slice(refPrefix.length).trim();
|
|
5227
|
-
const refPath =
|
|
5228
|
-
if (
|
|
5229
|
-
return normalizeCommit(
|
|
5997
|
+
const refPath = resolve20(gitDir, ref);
|
|
5998
|
+
if (existsSync13(refPath)) {
|
|
5999
|
+
return normalizeCommit(readFileSync9(refPath, "utf8").trim());
|
|
5230
6000
|
}
|
|
5231
|
-
const commonDir = normalizeString(
|
|
5232
|
-
return commonDir ? normalizeCommit(
|
|
6001
|
+
const commonDir = normalizeString(readFileSync9(resolve20(gitDir, "commondir"), "utf8"));
|
|
6002
|
+
return commonDir ? normalizeCommit(readFileSync9(resolve20(gitDir, commonDir, ref), "utf8").trim()) : null;
|
|
5233
6003
|
} catch {
|
|
5234
6004
|
return null;
|
|
5235
6005
|
}
|
|
@@ -5267,9 +6037,9 @@ function configuredRepoFromTaskSource(taskSource) {
|
|
|
5267
6037
|
const repo = normalizeString(taskSource?.repo);
|
|
5268
6038
|
return owner && repo ? `${owner}/${repo}` : null;
|
|
5269
6039
|
}
|
|
5270
|
-
async function buildTaskSourceStatus(state, config) {
|
|
6040
|
+
async function buildTaskSourceStatus(state, config, requestAuth) {
|
|
5271
6041
|
const diagnostics = state.snapshotService.getTaskSourceErrors();
|
|
5272
|
-
const selectedRepo =
|
|
6042
|
+
const selectedRepo = requestScopedAuthStore(state.projectRoot, requestAuth).status({
|
|
5273
6043
|
oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
|
|
5274
6044
|
}).selectedRepo;
|
|
5275
6045
|
try {
|
|
@@ -5322,37 +6092,146 @@ function isLoopbackRequest(req) {
|
|
|
5322
6092
|
}
|
|
5323
6093
|
}
|
|
5324
6094
|
function isPublicRigAuthBootstrapRoute(pathname) {
|
|
5325
|
-
return pathname === "/" || pathname === "/health" || pathname === "/api/health" || pathname === "/api/github/auth/status" || pathname === "/api/github/auth/token" || pathname === "/api/github/auth/device/start" || pathname === "/api/github/auth/device/poll";
|
|
6095
|
+
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";
|
|
6096
|
+
}
|
|
6097
|
+
function buildRigInstallScript() {
|
|
6098
|
+
return `#!/usr/bin/env bash
|
|
6099
|
+
set -euo pipefail
|
|
6100
|
+
|
|
6101
|
+
say() {
|
|
6102
|
+
printf 'rig-install: %s
|
|
6103
|
+
' "$*"
|
|
6104
|
+
}
|
|
6105
|
+
|
|
6106
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
6107
|
+
say "Bun not found; installing Bun first"
|
|
6108
|
+
curl -fsSL https://bun.sh/install | bash
|
|
6109
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
6110
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
6111
|
+
fi
|
|
6112
|
+
|
|
6113
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
6114
|
+
printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
|
|
6115
|
+
' >&2
|
|
6116
|
+
exit 1
|
|
6117
|
+
fi
|
|
6118
|
+
|
|
6119
|
+
say "Installing @h-rig/cli@latest"
|
|
6120
|
+
bun add -g @h-rig/cli@latest
|
|
6121
|
+
|
|
6122
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
6123
|
+
BUN_RIG="$BUN_INSTALL/bin/rig"
|
|
6124
|
+
if [ ! -x "$BUN_RIG" ]; then
|
|
6125
|
+
printf 'rig-install: expected Bun global rig at %s but it was not executable.
|
|
6126
|
+
' "$BUN_RIG" >&2
|
|
6127
|
+
exit 1
|
|
6128
|
+
fi
|
|
6129
|
+
|
|
6130
|
+
USER_BIN="$HOME/.local/bin"
|
|
6131
|
+
mkdir -p "$USER_BIN"
|
|
6132
|
+
cat > "$USER_BIN/rig" <<'RIG_SHIM'
|
|
6133
|
+
#!/usr/bin/env bash
|
|
6134
|
+
set -euo pipefail
|
|
6135
|
+
exec "\${BUN_INSTALL:-$HOME/.bun}/bin/rig" "$@"
|
|
6136
|
+
RIG_SHIM
|
|
6137
|
+
chmod +x "$USER_BIN/rig"
|
|
6138
|
+
|
|
6139
|
+
export PATH="$USER_BIN:$BUN_INSTALL/bin:$PATH"
|
|
6140
|
+
if command -v hash >/dev/null 2>&1; then hash -r; fi
|
|
6141
|
+
|
|
6142
|
+
if ! command -v rig >/dev/null 2>&1; then
|
|
6143
|
+
printf 'rig-install: rig installed, but rig is not on PATH. Add %s and %s/bin to PATH and retry.
|
|
6144
|
+
' "$USER_BIN" "$BUN_INSTALL" >&2
|
|
6145
|
+
exit 1
|
|
6146
|
+
fi
|
|
6147
|
+
|
|
6148
|
+
say "Verifying rig"
|
|
6149
|
+
"$BUN_RIG" --help >/dev/null
|
|
6150
|
+
rig --help >/dev/null
|
|
6151
|
+
say "Done. Run: rig --help"
|
|
6152
|
+
`;
|
|
5326
6153
|
}
|
|
5327
6154
|
function normalizePrMode(value) {
|
|
5328
6155
|
const mode = normalizeString(value);
|
|
5329
6156
|
return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
|
|
5330
6157
|
}
|
|
6158
|
+
function requestAuthResult(input) {
|
|
6159
|
+
return {
|
|
6160
|
+
authorized: input.authorized,
|
|
6161
|
+
actor: input.actor ?? null,
|
|
6162
|
+
reason: input.reason,
|
|
6163
|
+
login: input.login ?? null,
|
|
6164
|
+
userId: input.userId ?? null,
|
|
6165
|
+
userNamespace: input.userNamespace ?? null,
|
|
6166
|
+
authStateFile: input.authStateFile ?? null
|
|
6167
|
+
};
|
|
6168
|
+
}
|
|
6169
|
+
function namespaceFromSessionIndex(entry) {
|
|
6170
|
+
const stateDir2 = dirname15(entry.authStateFile);
|
|
6171
|
+
return {
|
|
6172
|
+
key: entry.namespaceKey,
|
|
6173
|
+
userId: entry.userId,
|
|
6174
|
+
login: entry.login,
|
|
6175
|
+
root: entry.namespaceRoot,
|
|
6176
|
+
stateDir: stateDir2,
|
|
6177
|
+
authStateFile: entry.authStateFile,
|
|
6178
|
+
metadataFile: resolve20(stateDir2, "user-namespace.json"),
|
|
6179
|
+
checkoutBaseDir: entry.checkoutBaseDir,
|
|
6180
|
+
snapshotBaseDir: entry.snapshotBaseDir
|
|
6181
|
+
};
|
|
6182
|
+
}
|
|
5331
6183
|
function authorizeRigHttpRequest(input) {
|
|
5332
6184
|
if (input.legacyAuthorized) {
|
|
5333
|
-
return { authorized: true, actor: "rig-local-server", reason: "server-token" };
|
|
6185
|
+
return requestAuthResult({ authorized: true, actor: "rig-local-server", reason: "server-token" });
|
|
5334
6186
|
}
|
|
5335
6187
|
const bearer = bearerTokenFromRequest(input.req);
|
|
5336
6188
|
const store = createGitHubAuthStore(input.projectRoot);
|
|
5337
6189
|
const storedToken = store.readToken();
|
|
5338
6190
|
const session = bearer ? store.readApiSession(bearer) : null;
|
|
5339
6191
|
if (session) {
|
|
5340
|
-
return {
|
|
6192
|
+
return requestAuthResult({
|
|
6193
|
+
authorized: true,
|
|
6194
|
+
actor: session.login ?? "github-operator",
|
|
6195
|
+
reason: "github-session",
|
|
6196
|
+
login: session.login,
|
|
6197
|
+
userId: session.userId,
|
|
6198
|
+
authStateFile: store.stateFile
|
|
6199
|
+
});
|
|
5341
6200
|
}
|
|
5342
6201
|
if (bearer && storedToken && bearer === storedToken) {
|
|
5343
6202
|
const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
5344
|
-
return {
|
|
6203
|
+
return requestAuthResult({
|
|
6204
|
+
authorized: true,
|
|
6205
|
+
actor: status.login ?? "github-operator",
|
|
6206
|
+
reason: "github-token",
|
|
6207
|
+
login: status.login,
|
|
6208
|
+
userId: status.userId,
|
|
6209
|
+
authStateFile: store.stateFile
|
|
6210
|
+
});
|
|
6211
|
+
}
|
|
6212
|
+
const indexedSession = readGitHubApiSession({ projectRoot: input.projectRoot, token: bearer });
|
|
6213
|
+
if (indexedSession) {
|
|
6214
|
+
const userNamespace = namespaceFromSessionIndex(indexedSession);
|
|
6215
|
+
return requestAuthResult({
|
|
6216
|
+
authorized: true,
|
|
6217
|
+
actor: indexedSession.login ?? "github-operator",
|
|
6218
|
+
reason: "github-user-session",
|
|
6219
|
+
login: indexedSession.login,
|
|
6220
|
+
userId: indexedSession.userId,
|
|
6221
|
+
userNamespace,
|
|
6222
|
+
authStateFile: indexedSession.authStateFile
|
|
6223
|
+
});
|
|
5345
6224
|
}
|
|
5346
6225
|
if (isPublicRigAuthBootstrapRoute(input.pathname)) {
|
|
5347
|
-
return { authorized: true, actor: null, reason: "public-bootstrap" };
|
|
6226
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "public-bootstrap" });
|
|
5348
6227
|
}
|
|
5349
6228
|
if (!input.serverAuthToken && !storedToken) {
|
|
5350
6229
|
if (isLoopbackRequest(input.req)) {
|
|
5351
|
-
return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
|
|
6230
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "loopback-dev-no-auth" });
|
|
5352
6231
|
}
|
|
5353
|
-
return { authorized: false, actor: null, reason: "auth-required" };
|
|
6232
|
+
return requestAuthResult({ authorized: false, actor: null, reason: "auth-required" });
|
|
5354
6233
|
}
|
|
5355
|
-
return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
|
|
6234
|
+
return requestAuthResult({ authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" });
|
|
5356
6235
|
}
|
|
5357
6236
|
async function fetchGitHubUserInfo(token) {
|
|
5358
6237
|
const response = await fetch("https://api.github.com/user", {
|
|
@@ -5372,6 +6251,67 @@ async function fetchGitHubUserInfo(token) {
|
|
|
5372
6251
|
scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
|
|
5373
6252
|
};
|
|
5374
6253
|
}
|
|
6254
|
+
function shouldWriteRootAuthCompat(projectRoot) {
|
|
6255
|
+
if (process.env.RIG_REMOTE_USER_NAMESPACE_ROOT?.trim())
|
|
6256
|
+
return false;
|
|
6257
|
+
const stateDir2 = normalizeString(process.env.RIG_STATE_DIR);
|
|
6258
|
+
if (!stateDir2)
|
|
6259
|
+
return true;
|
|
6260
|
+
return resolve20(stateDir2) === resolve20(projectRoot, ".rig", "state");
|
|
6261
|
+
}
|
|
6262
|
+
function requestScopedRegistryRoot(stateProjectRoot, requestAuth) {
|
|
6263
|
+
return requestAuth.userNamespace?.root ?? stateProjectRoot;
|
|
6264
|
+
}
|
|
6265
|
+
function requestScopedAuthStore(stateProjectRoot, requestAuth) {
|
|
6266
|
+
return requestAuth.authStateFile ? createGitHubAuthStoreFromStateFile(requestAuth.authStateFile) : createGitHubAuthStore(stateProjectRoot);
|
|
6267
|
+
}
|
|
6268
|
+
function userNamespaceResponse(namespace) {
|
|
6269
|
+
return namespace ? serializeRemoteUserNamespace(namespace) : undefined;
|
|
6270
|
+
}
|
|
6271
|
+
function resolveNamespacedBaseDir(input) {
|
|
6272
|
+
if (input.explicitBaseDir)
|
|
6273
|
+
return input.explicitBaseDir;
|
|
6274
|
+
const envBase = normalizeString(process.env[input.envName]);
|
|
6275
|
+
if (input.userNamespace) {
|
|
6276
|
+
return envBase ? resolve20(envBase, input.userNamespace.key) : input.userNamespace[input.legacySubdir === "remote-checkouts" ? "checkoutBaseDir" : "snapshotBaseDir"];
|
|
6277
|
+
}
|
|
6278
|
+
return envBase ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve20(normalizeString(process.env.RIG_STATE_DIR), input.legacySubdir) : resolve20(input.legacyProjectRoot, ".rig", input.legacySubdir));
|
|
6279
|
+
}
|
|
6280
|
+
function explicitCheckoutKey(body, checkoutInput, requestAuth) {
|
|
6281
|
+
return normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? (requestAuth.userNamespace ? undefined : "default");
|
|
6282
|
+
}
|
|
6283
|
+
function saveGitHubTokenForRemoteUser(input) {
|
|
6284
|
+
const namespace = resolveRemoteUserNamespace(input.projectRoot, { userId: input.user.userId, login: input.user.login });
|
|
6285
|
+
writeRemoteUserNamespaceMetadata(namespace);
|
|
6286
|
+
const store = createGitHubAuthStoreFromStateFile(namespace.authStateFile);
|
|
6287
|
+
store.saveToken({
|
|
6288
|
+
token: input.token,
|
|
6289
|
+
tokenSource: input.tokenSource,
|
|
6290
|
+
login: input.user.login,
|
|
6291
|
+
userId: input.user.userId,
|
|
6292
|
+
scopes: input.user.scopes,
|
|
6293
|
+
selectedRepo: input.selectedRepo
|
|
6294
|
+
});
|
|
6295
|
+
const apiSession = store.createApiSession();
|
|
6296
|
+
registerGitHubApiSession({ projectRoot: input.projectRoot, token: apiSession.token, namespace, selectedRepo: input.selectedRepo });
|
|
6297
|
+
const requestedRoot = normalizeString(input.requestedProjectRoot);
|
|
6298
|
+
if (requestedRoot && isAbsolute4(requestedRoot) && existsSync13(resolve20(requestedRoot))) {
|
|
6299
|
+
copyGitHubAuthStateToLocalProjectRoot(namespace.authStateFile, resolve20(requestedRoot));
|
|
6300
|
+
}
|
|
6301
|
+
if (shouldWriteRootAuthCompat(input.projectRoot)) {
|
|
6302
|
+
const rootStore = createGitHubAuthStore(input.projectRoot);
|
|
6303
|
+
rootStore.saveToken({
|
|
6304
|
+
token: input.token,
|
|
6305
|
+
tokenSource: input.tokenSource,
|
|
6306
|
+
login: input.user.login,
|
|
6307
|
+
userId: input.user.userId,
|
|
6308
|
+
scopes: input.user.scopes,
|
|
6309
|
+
selectedRepo: input.selectedRepo
|
|
6310
|
+
});
|
|
6311
|
+
rootStore.createApiSession();
|
|
6312
|
+
}
|
|
6313
|
+
return { store, namespace, apiSessionToken: apiSession.token };
|
|
6314
|
+
}
|
|
5375
6315
|
async function postGitHubForm(endpoint, body) {
|
|
5376
6316
|
const response = await fetch(endpoint, {
|
|
5377
6317
|
method: "POST",
|
|
@@ -5389,11 +6329,11 @@ function resolveRequestedProjectRoot(currentRoot, rawRoot) {
|
|
|
5389
6329
|
const requestedRoot = normalizeString(rawRoot);
|
|
5390
6330
|
if (!requestedRoot)
|
|
5391
6331
|
return currentRoot;
|
|
5392
|
-
if (!
|
|
6332
|
+
if (!isAbsolute4(requestedRoot)) {
|
|
5393
6333
|
throw new Error("projectRoot must be an absolute path on the Rig server host");
|
|
5394
6334
|
}
|
|
5395
|
-
const normalizedRoot =
|
|
5396
|
-
if (!
|
|
6335
|
+
const normalizedRoot = resolve20(requestedRoot);
|
|
6336
|
+
if (!existsSync13(normalizedRoot)) {
|
|
5397
6337
|
throw new Error("projectRoot does not exist on the Rig server host");
|
|
5398
6338
|
}
|
|
5399
6339
|
return normalizedRoot;
|
|
@@ -5621,6 +6561,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
|
|
|
5621
6561
|
}
|
|
5622
6562
|
return filtered;
|
|
5623
6563
|
}
|
|
6564
|
+
function issueAnalysisTargetFor(source) {
|
|
6565
|
+
if (!source)
|
|
6566
|
+
return null;
|
|
6567
|
+
const candidate = source;
|
|
6568
|
+
if (typeof candidate.updateTask !== "function")
|
|
6569
|
+
return null;
|
|
6570
|
+
return {
|
|
6571
|
+
...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
|
|
6572
|
+
updateTask: candidate.updateTask.bind(candidate),
|
|
6573
|
+
...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
|
|
6574
|
+
...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
|
|
6575
|
+
...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
|
|
6576
|
+
};
|
|
6577
|
+
}
|
|
6578
|
+
function uniqueStringList(value) {
|
|
6579
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
6580
|
+
return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
|
|
6581
|
+
}
|
|
6582
|
+
function taskRecordId(task) {
|
|
6583
|
+
return String(task.id ?? "");
|
|
6584
|
+
}
|
|
5624
6585
|
function redactRemoteEndpoint(endpoint) {
|
|
5625
6586
|
const { token, ...rest } = endpoint;
|
|
5626
6587
|
return {
|
|
@@ -5705,6 +6666,13 @@ function createRigServerFetch(state, deps) {
|
|
|
5705
6666
|
notifications: state.targets.length
|
|
5706
6667
|
});
|
|
5707
6668
|
}
|
|
6669
|
+
if (url.pathname === "/install" && req.method === "GET") {
|
|
6670
|
+
return new Response(buildRigInstallScript(), {
|
|
6671
|
+
headers: {
|
|
6672
|
+
"Content-Type": "text/x-shellscript; charset=utf-8"
|
|
6673
|
+
}
|
|
6674
|
+
});
|
|
6675
|
+
}
|
|
5708
6676
|
const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
|
|
5709
6677
|
const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
|
|
5710
6678
|
const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
|
|
@@ -5917,16 +6885,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
5917
6885
|
if (!source) {
|
|
5918
6886
|
return deps.badRequest("No task source is configured");
|
|
5919
6887
|
}
|
|
6888
|
+
if (!source.updateTask && !(update.status && source.updateStatus)) {
|
|
6889
|
+
return deps.badRequest("Configured task source does not support updates");
|
|
6890
|
+
}
|
|
5920
6891
|
const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
|
|
5921
6892
|
return;
|
|
5922
6893
|
}) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
|
|
5923
|
-
if (source.updateTask) {
|
|
5924
|
-
await source.updateTask(id, update);
|
|
5925
|
-
} else if (update.status && source.updateStatus) {
|
|
5926
|
-
await source.updateStatus(id, update.status);
|
|
5927
|
-
} else {
|
|
5928
|
-
return deps.badRequest("Configured task source does not support updates");
|
|
5929
|
-
}
|
|
5930
6894
|
const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
|
|
5931
6895
|
const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
|
|
5932
6896
|
taskId: id,
|
|
@@ -5935,6 +6899,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
5935
6899
|
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
5936
6900
|
config: ctx?.config
|
|
5937
6901
|
}).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
|
|
6902
|
+
if (update.status && githubProjectsEnabled2(ctx?.config) && projectSync.synced === false) {
|
|
6903
|
+
return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
|
|
6904
|
+
}
|
|
6905
|
+
try {
|
|
6906
|
+
if (source.updateTask) {
|
|
6907
|
+
await source.updateTask(id, update);
|
|
6908
|
+
} else if (update.status && source.updateStatus) {
|
|
6909
|
+
await source.updateStatus(id, update.status);
|
|
6910
|
+
}
|
|
6911
|
+
} catch (error) {
|
|
6912
|
+
let rollback = null;
|
|
6913
|
+
const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
|
|
6914
|
+
if (update.status && previousStatus && githubProjectsEnabled2(ctx?.config) && projectSync.synced !== false) {
|
|
6915
|
+
rollback = await syncGitHubProjectStatusForTaskUpdate({
|
|
6916
|
+
taskId: id,
|
|
6917
|
+
status: previousStatus,
|
|
6918
|
+
issueNodeId,
|
|
6919
|
+
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
6920
|
+
config: ctx?.config
|
|
6921
|
+
}).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
|
|
6922
|
+
}
|
|
6923
|
+
return deps.jsonResponse({
|
|
6924
|
+
ok: false,
|
|
6925
|
+
id,
|
|
6926
|
+
projectSync,
|
|
6927
|
+
rollback,
|
|
6928
|
+
error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
6929
|
+
}, 502);
|
|
6930
|
+
}
|
|
5938
6931
|
deps.snapshotService.invalidate("github-issue-updated");
|
|
5939
6932
|
await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
|
|
5940
6933
|
return;
|
|
@@ -5943,29 +6936,105 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
5943
6936
|
return deps.jsonResponse({ ok: true, id, projectSync });
|
|
5944
6937
|
}
|
|
5945
6938
|
if (url.pathname === "/api/workspace/task-labels") {
|
|
6939
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
|
|
6940
|
+
if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
|
|
6941
|
+
return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
|
|
6942
|
+
}
|
|
5946
6943
|
return deps.jsonResponse({
|
|
5947
6944
|
ok: true,
|
|
5948
6945
|
ready: true,
|
|
5949
6946
|
labelsReady: true,
|
|
5950
|
-
labels: [
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
6947
|
+
labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
|
|
6948
|
+
note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
|
|
6949
|
+
});
|
|
6950
|
+
}
|
|
6951
|
+
if (url.pathname === "/api/github/projects" && req.method === "GET") {
|
|
6952
|
+
const owner = normalizeString(url.searchParams.get("owner"));
|
|
6953
|
+
if (!owner)
|
|
6954
|
+
return deps.badRequest("owner is required");
|
|
6955
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
6956
|
+
if (!token)
|
|
6957
|
+
return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
|
|
6958
|
+
const projects = await listGitHubProjects({ owner, token }).catch((error) => {
|
|
6959
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
6960
|
+
});
|
|
6961
|
+
return deps.jsonResponse({ ok: true, projects });
|
|
6962
|
+
}
|
|
6963
|
+
const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
|
|
6964
|
+
if (projectStatusMatch && req.method === "GET") {
|
|
6965
|
+
const projectId = decodeURIComponent(projectStatusMatch[1]);
|
|
6966
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
6967
|
+
if (!token)
|
|
6968
|
+
return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
|
|
6969
|
+
const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
|
|
6970
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
6971
|
+
});
|
|
6972
|
+
return deps.jsonResponse({ ok: true, field });
|
|
6973
|
+
}
|
|
6974
|
+
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
6975
|
+
const body = await deps.readJsonBody(req);
|
|
6976
|
+
const ids = uniqueStringList(body.ids ?? body.id);
|
|
6977
|
+
const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
|
|
6978
|
+
if (ids.length === 0 && !analyzeAll) {
|
|
6979
|
+
return deps.badRequest("ids is required unless all=true");
|
|
6980
|
+
}
|
|
6981
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot);
|
|
6982
|
+
const [source] = ctx?.taskSourceRegistry.list() ?? [];
|
|
6983
|
+
const target = issueAnalysisTargetFor(source);
|
|
6984
|
+
if (!source || !target) {
|
|
6985
|
+
return deps.badRequest("Configured task source does not support issue-analysis writeback");
|
|
6986
|
+
}
|
|
6987
|
+
const allTasks = [...await source.list()];
|
|
6988
|
+
const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
|
|
6989
|
+
const cached = allTasks.find((task) => taskRecordId(task) === id);
|
|
6990
|
+
if (cached)
|
|
6991
|
+
return cached;
|
|
6992
|
+
return typeof source.get === "function" ? await source.get(id) : undefined;
|
|
6993
|
+
}))).filter((task) => Boolean(task));
|
|
6994
|
+
if (issues.length === 0) {
|
|
6995
|
+
return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
|
|
6996
|
+
}
|
|
6997
|
+
const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
|
|
6998
|
+
const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
|
|
6999
|
+
const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
|
|
7000
|
+
const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
|
|
7001
|
+
const service = createIssueAnalysisService({
|
|
7002
|
+
analyzer: createPiIssueAnalyzer({
|
|
7003
|
+
...model ? { model } : {},
|
|
7004
|
+
env: { RIG_PROJECT_ROOT: state.projectRoot }
|
|
7005
|
+
}),
|
|
7006
|
+
writeBack: createIssueAnalysisWriteBack({ target })
|
|
7007
|
+
});
|
|
7008
|
+
const reason = normalizeString(body.reason) ?? "http-issue-analysis";
|
|
7009
|
+
let results;
|
|
7010
|
+
try {
|
|
7011
|
+
results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
|
|
7012
|
+
} catch (error) {
|
|
7013
|
+
return deps.jsonResponse({
|
|
7014
|
+
ok: false,
|
|
7015
|
+
error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
7016
|
+
reason,
|
|
7017
|
+
ids: issues.map((issue) => issue.id)
|
|
7018
|
+
}, 502);
|
|
7019
|
+
}
|
|
7020
|
+
deps.snapshotService.invalidate("issue-analysis-http-run");
|
|
7021
|
+
await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
|
|
7022
|
+
return;
|
|
7023
|
+
});
|
|
7024
|
+
deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
|
|
7025
|
+
return deps.jsonResponse({
|
|
7026
|
+
ok: true,
|
|
7027
|
+
reason,
|
|
7028
|
+
analyzed: results.map((entry) => ({
|
|
7029
|
+
id: entry.issue.id,
|
|
7030
|
+
title: entry.issue.title ?? null,
|
|
7031
|
+
result: entry.result
|
|
7032
|
+
}))
|
|
5964
7033
|
});
|
|
5965
7034
|
}
|
|
5966
7035
|
if (url.pathname === "/api/server/status") {
|
|
5967
7036
|
const config = buildProjectConfigStatus(state.projectRoot);
|
|
5968
|
-
const taskSource = await buildTaskSourceStatus(state, config);
|
|
7037
|
+
const taskSource = await buildTaskSourceStatus(state, config, requestAuth);
|
|
5969
7038
|
return deps.jsonResponse({
|
|
5970
7039
|
ok: true,
|
|
5971
7040
|
projectRoot: state.projectRoot,
|
|
@@ -5989,8 +7058,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
5989
7058
|
path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
|
|
5990
7059
|
ref: normalizeString(rawCheckout?.ref) ?? undefined
|
|
5991
7060
|
} : undefined;
|
|
5992
|
-
const
|
|
5993
|
-
|
|
7061
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7062
|
+
const record = upsertProjectRecord(registryRoot, { repoSlug, checkout });
|
|
7063
|
+
return deps.jsonResponse({ ok: true, project: record, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
5994
7064
|
}
|
|
5995
7065
|
const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
|
|
5996
7066
|
if (snapshotUploadMatch && req.method === "POST") {
|
|
@@ -6003,8 +7073,15 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6003
7073
|
if (!archiveContentBase64) {
|
|
6004
7074
|
return deps.badRequest("archiveContentBase64 is required");
|
|
6005
7075
|
}
|
|
6006
|
-
const
|
|
6007
|
-
const
|
|
7076
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7077
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
7078
|
+
explicitBaseDir: normalizeString(body.baseDir),
|
|
7079
|
+
envName: "RIG_REMOTE_SNAPSHOT_BASE_DIR",
|
|
7080
|
+
userNamespace: requestAuth.userNamespace,
|
|
7081
|
+
legacyProjectRoot: state.projectRoot,
|
|
7082
|
+
legacySubdir: "remote-snapshots"
|
|
7083
|
+
});
|
|
7084
|
+
const checkoutKey = explicitCheckoutKey(body, body, requestAuth);
|
|
6008
7085
|
try {
|
|
6009
7086
|
const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
|
|
6010
7087
|
const checkout = extractUploadedSnapshotArchive({
|
|
@@ -6017,14 +7094,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6017
7094
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
6018
7095
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
6019
7096
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
6020
|
-
const project = linkProjectCheckout(
|
|
7097
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
6021
7098
|
kind: "uploaded-snapshot",
|
|
6022
7099
|
path: checkout.path,
|
|
6023
7100
|
ref: checkout.snapshotId
|
|
6024
7101
|
});
|
|
6025
7102
|
deps.snapshotService.invalidate("uploaded-snapshot-checkout");
|
|
6026
7103
|
deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
|
|
6027
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
7104
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6028
7105
|
} catch (error) {
|
|
6029
7106
|
return deps.jsonResponse({
|
|
6030
7107
|
ok: false,
|
|
@@ -6044,10 +7121,17 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6044
7121
|
if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
|
|
6045
7122
|
return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
|
|
6046
7123
|
}
|
|
6047
|
-
const
|
|
6048
|
-
const
|
|
7124
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7125
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
7126
|
+
explicitBaseDir: normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir),
|
|
7127
|
+
envName: "RIG_REMOTE_CHECKOUT_BASE_DIR",
|
|
7128
|
+
userNamespace: requestAuth.userNamespace,
|
|
7129
|
+
legacyProjectRoot: state.projectRoot,
|
|
7130
|
+
legacySubdir: "remote-checkouts"
|
|
7131
|
+
});
|
|
7132
|
+
const checkoutKey = explicitCheckoutKey(body, checkoutInput, requestAuth);
|
|
6049
7133
|
const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
|
|
6050
|
-
const credentialToken =
|
|
7134
|
+
const credentialToken = requestScopedAuthStore(state.projectRoot, requestAuth).readToken();
|
|
6051
7135
|
try {
|
|
6052
7136
|
const checkout = await prepareRemoteCheckout({
|
|
6053
7137
|
command: runRemoteCheckoutCommand,
|
|
@@ -6056,14 +7140,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6056
7140
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
6057
7141
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
6058
7142
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
6059
|
-
const project = linkProjectCheckout(
|
|
7143
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
6060
7144
|
kind: checkout.kind,
|
|
6061
7145
|
path: checkout.path,
|
|
6062
7146
|
ref: checkout.ref ?? checkout.snapshotId ?? undefined
|
|
6063
7147
|
});
|
|
6064
7148
|
deps.snapshotService.invalidate("remote-checkout-prepared");
|
|
6065
7149
|
deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
|
|
6066
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
7150
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6067
7151
|
} catch (error) {
|
|
6068
7152
|
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
6069
7153
|
}
|
|
@@ -6080,16 +7164,18 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6080
7164
|
if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
|
|
6081
7165
|
return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
|
|
6082
7166
|
}
|
|
6083
|
-
const
|
|
7167
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7168
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
6084
7169
|
kind,
|
|
6085
7170
|
path: normalizeString(body.path) ?? state.projectRoot,
|
|
6086
7171
|
ref: normalizeString(body.ref) ?? undefined
|
|
6087
7172
|
});
|
|
6088
|
-
return deps.jsonResponse({ ok: true, project });
|
|
7173
|
+
return deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6089
7174
|
}
|
|
6090
7175
|
if (req.method === "GET") {
|
|
6091
|
-
const
|
|
6092
|
-
|
|
7176
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7177
|
+
const project = getProjectRecord(registryRoot, repoSlug);
|
|
7178
|
+
return project ? deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) }) : deps.notFound();
|
|
6093
7179
|
}
|
|
6094
7180
|
}
|
|
6095
7181
|
if (url.pathname === "/api/server/project-root" && req.method === "POST") {
|
|
@@ -6098,13 +7184,26 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6098
7184
|
if (!requestedRoot) {
|
|
6099
7185
|
return deps.badRequest("projectRoot is required");
|
|
6100
7186
|
}
|
|
6101
|
-
if (!
|
|
7187
|
+
if (!isAbsolute4(requestedRoot)) {
|
|
6102
7188
|
return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
|
|
6103
7189
|
}
|
|
6104
|
-
const normalizedRoot =
|
|
6105
|
-
const exists =
|
|
6106
|
-
if (exists) {
|
|
6107
|
-
|
|
7190
|
+
const normalizedRoot = resolve20(requestedRoot);
|
|
7191
|
+
const exists = existsSync13(normalizedRoot);
|
|
7192
|
+
if (exists && requestAuth.userNamespace) {
|
|
7193
|
+
const allowedByNamespace = isPathInsideNamespace(requestAuth.userNamespace.root, normalizedRoot);
|
|
7194
|
+
const allowedByRegistry = projectRegistryContainsCheckout(requestAuth.userNamespace.root, normalizedRoot);
|
|
7195
|
+
if (!allowedByNamespace && !allowedByRegistry) {
|
|
7196
|
+
return deps.jsonResponse({
|
|
7197
|
+
ok: false,
|
|
7198
|
+
error: "Requested project root is outside the authenticated GitHub user namespace.",
|
|
7199
|
+
projectRoot: state.projectRoot,
|
|
7200
|
+
requestedProjectRoot: normalizedRoot,
|
|
7201
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace)
|
|
7202
|
+
}, 403);
|
|
7203
|
+
}
|
|
7204
|
+
copyGitHubAuthStateToLocalProjectRoot(requestAuth.userNamespace.authStateFile, normalizedRoot);
|
|
7205
|
+
} else if (exists) {
|
|
7206
|
+
createGitHubAuthStore(state.projectRoot).copyToLocalProjectRoot(normalizedRoot);
|
|
6108
7207
|
}
|
|
6109
7208
|
const control = buildServerControlStatus();
|
|
6110
7209
|
const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
|
|
@@ -6119,7 +7218,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6119
7218
|
message: "Requested project root does not exist on the Rig server host."
|
|
6120
7219
|
}, 404);
|
|
6121
7220
|
}
|
|
6122
|
-
if (!
|
|
7221
|
+
if (!existsSync13(resolve20(normalizedRoot, "rig.config.ts")) && !existsSync13(resolve20(normalizedRoot, "rig.config.json"))) {
|
|
6123
7222
|
return deps.jsonResponse({
|
|
6124
7223
|
ok: false,
|
|
6125
7224
|
projectRoot: state.projectRoot,
|
|
@@ -6154,6 +7253,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6154
7253
|
exists,
|
|
6155
7254
|
control,
|
|
6156
7255
|
requiresRestart: false,
|
|
7256
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace),
|
|
6157
7257
|
message: "Project-root switch accepted. Rig server restart has been scheduled."
|
|
6158
7258
|
}, 202);
|
|
6159
7259
|
}
|
|
@@ -6168,11 +7268,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6168
7268
|
}, 409);
|
|
6169
7269
|
}
|
|
6170
7270
|
if (url.pathname === "/api/github/auth/status") {
|
|
6171
|
-
const store =
|
|
6172
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
|
|
7271
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
7272
|
+
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6173
7273
|
}
|
|
6174
7274
|
if (url.pathname === "/api/github/repo/permissions") {
|
|
6175
|
-
const store =
|
|
7275
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
6176
7276
|
const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
6177
7277
|
if (!auth.signedIn) {
|
|
6178
7278
|
return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
|
|
@@ -6200,24 +7300,20 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6200
7300
|
}
|
|
6201
7301
|
try {
|
|
6202
7302
|
const user = await fetchGitHubUserInfo(token);
|
|
6203
|
-
const
|
|
6204
|
-
state.projectRoot,
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
}
|
|
6218
|
-
const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
|
|
6219
|
-
const apiSession = store.createApiSession();
|
|
6220
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
|
|
7303
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
7304
|
+
projectRoot: state.projectRoot,
|
|
7305
|
+
token,
|
|
7306
|
+
tokenSource: "manual-token",
|
|
7307
|
+
user,
|
|
7308
|
+
selectedRepo,
|
|
7309
|
+
requestedProjectRoot
|
|
7310
|
+
});
|
|
7311
|
+
return deps.jsonResponse({
|
|
7312
|
+
ok: true,
|
|
7313
|
+
...saved.store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }),
|
|
7314
|
+
apiSessionToken: saved.apiSessionToken,
|
|
7315
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
7316
|
+
});
|
|
6221
7317
|
} catch (error) {
|
|
6222
7318
|
const message = error instanceof Error ? error.message : String(error);
|
|
6223
7319
|
return deps.jsonResponse({ ok: false, error: message }, 400);
|
|
@@ -6284,9 +7380,21 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6284
7380
|
}
|
|
6285
7381
|
const token = result.payload.access_token;
|
|
6286
7382
|
const user = await fetchGitHubUserInfo(token);
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
7383
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
7384
|
+
projectRoot: state.projectRoot,
|
|
7385
|
+
token,
|
|
7386
|
+
tokenSource: "oauth-device",
|
|
7387
|
+
user,
|
|
7388
|
+
selectedRepo: null
|
|
7389
|
+
});
|
|
7390
|
+
store.clearPendingDevice(pollId);
|
|
7391
|
+
return deps.jsonResponse({
|
|
7392
|
+
ok: true,
|
|
7393
|
+
status: "signed-in",
|
|
7394
|
+
...saved.store.status({ oauthConfigured: true }),
|
|
7395
|
+
apiSessionToken: saved.apiSessionToken,
|
|
7396
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
7397
|
+
});
|
|
6290
7398
|
}
|
|
6291
7399
|
if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
|
|
6292
7400
|
const body = await deps.readJsonBody(req);
|
|
@@ -6295,7 +7403,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6295
7403
|
if (!owner || !repo) {
|
|
6296
7404
|
return deps.badRequest("owner and repo are required");
|
|
6297
7405
|
}
|
|
6298
|
-
const store =
|
|
7406
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
6299
7407
|
const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
6300
7408
|
const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
|
|
6301
7409
|
return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
|
|
@@ -6316,7 +7424,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6316
7424
|
return deps.badRequest(error instanceof Error ? error.message : String(error));
|
|
6317
7425
|
}
|
|
6318
7426
|
const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
6319
|
-
const configPath =
|
|
7427
|
+
const configPath = resolve20(targetRoot, "rig.config.ts");
|
|
6320
7428
|
const source = buildGitHubProjectConfigSource({
|
|
6321
7429
|
projectName: rawProjectName,
|
|
6322
7430
|
owner,
|
|
@@ -6328,8 +7436,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6328
7436
|
ok: true,
|
|
6329
7437
|
projectRoot: targetRoot,
|
|
6330
7438
|
configPath,
|
|
6331
|
-
exists:
|
|
6332
|
-
requiresOverwrite:
|
|
7439
|
+
exists: existsSync13(configPath),
|
|
7440
|
+
requiresOverwrite: existsSync13(configPath),
|
|
6333
7441
|
source,
|
|
6334
7442
|
owner,
|
|
6335
7443
|
repo,
|
|
@@ -6365,8 +7473,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6365
7473
|
assignee,
|
|
6366
7474
|
githubUserId: authStatus.userId ?? authStatus.login
|
|
6367
7475
|
});
|
|
6368
|
-
const configPath =
|
|
6369
|
-
if (
|
|
7476
|
+
const configPath = resolve20(targetRoot, "rig.config.ts");
|
|
7477
|
+
if (existsSync13(configPath) && !overwrite) {
|
|
6370
7478
|
return deps.jsonResponse({
|
|
6371
7479
|
ok: false,
|
|
6372
7480
|
error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
|
|
@@ -6382,11 +7490,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6382
7490
|
return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
|
|
6383
7491
|
}
|
|
6384
7492
|
let backupPath = null;
|
|
6385
|
-
if (
|
|
7493
|
+
if (existsSync13(configPath)) {
|
|
6386
7494
|
backupPath = backupConfigPath(configPath);
|
|
6387
|
-
|
|
7495
|
+
copyFileSync2(configPath, backupPath);
|
|
6388
7496
|
}
|
|
6389
|
-
|
|
7497
|
+
writeFileSync12(configPath, source, "utf8");
|
|
6390
7498
|
const selectedRepo = `${owner}/${repo}`;
|
|
6391
7499
|
store.saveSelectedRepo(selectedRepo);
|
|
6392
7500
|
const targetStore = createGitHubAuthStore(targetRoot);
|
|
@@ -6658,11 +7766,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6658
7766
|
const runId = normalizeString(body.runId);
|
|
6659
7767
|
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
6660
7768
|
const promptOverride = normalizeString(body.promptOverride);
|
|
7769
|
+
const restart = body.restart === true;
|
|
6661
7770
|
if (!runId) {
|
|
6662
7771
|
return deps.badRequest("runId is required");
|
|
6663
7772
|
}
|
|
6664
7773
|
try {
|
|
6665
|
-
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
|
|
7774
|
+
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
|
|
6666
7775
|
deps.broadcastSnapshotInvalidation(state);
|
|
6667
7776
|
return deps.jsonResponse({ ok: true, runId, createdAt });
|
|
6668
7777
|
} catch (error) {
|
|
@@ -6671,7 +7780,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6671
7780
|
}
|
|
6672
7781
|
if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
|
|
6673
7782
|
const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
|
|
6674
|
-
const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) =>
|
|
7783
|
+
const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) => resolve20(root, "packages", "pi-rig")).find((candidate) => existsSync13(resolve20(candidate, "package.json"))) ?? "npm:@h-rig/pi-rig";
|
|
6675
7784
|
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
6676
7785
|
return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
|
|
6677
7786
|
}
|
|
@@ -6987,9 +8096,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6987
8096
|
} catch {
|
|
6988
8097
|
return deps.badRequest("Invalid artifact path");
|
|
6989
8098
|
}
|
|
6990
|
-
|
|
8099
|
+
mkdirSync13(dirname15(artifactPath), { recursive: true });
|
|
6991
8100
|
const bytes = Buffer.from(contentBase64, "base64");
|
|
6992
|
-
|
|
8101
|
+
writeFileSync12(artifactPath, bytes);
|
|
6993
8102
|
writeJsonFile4(`${artifactPath}.json`, {
|
|
6994
8103
|
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
6995
8104
|
runId,
|
|
@@ -7026,13 +8135,75 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7026
8135
|
}
|
|
7027
8136
|
const run = leaseValidation.run;
|
|
7028
8137
|
const completedAt = new Date().toISOString();
|
|
8138
|
+
const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
|
|
8139
|
+
if (run.taskId && workspaceDir) {
|
|
8140
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
8141
|
+
status: "reviewing",
|
|
8142
|
+
completedAt: null,
|
|
8143
|
+
hostId,
|
|
8144
|
+
endpointId: leaseId,
|
|
8145
|
+
worktreePath: workspaceDir,
|
|
8146
|
+
serverCloseout: {
|
|
8147
|
+
status: "pending",
|
|
8148
|
+
phase: "queued",
|
|
8149
|
+
requestedAt: completedAt,
|
|
8150
|
+
updatedAt: completedAt,
|
|
8151
|
+
runtimeWorkspace: workspaceDir,
|
|
8152
|
+
branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
|
|
8153
|
+
taskId: run.taskId,
|
|
8154
|
+
source: "remote-complete"
|
|
8155
|
+
}
|
|
8156
|
+
});
|
|
8157
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
8158
|
+
id: `log:${runId}:remote-server-closeout-requested`,
|
|
8159
|
+
title: "Server-owned closeout requested",
|
|
8160
|
+
detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
|
|
8161
|
+
tone: "info",
|
|
8162
|
+
status: "reviewing",
|
|
8163
|
+
createdAt: completedAt,
|
|
8164
|
+
payload: { workspaceDir, hostId, leaseId }
|
|
8165
|
+
}, "remote-server-closeout-requested");
|
|
8166
|
+
deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
|
|
8167
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8168
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
8169
|
+
status: "failed",
|
|
8170
|
+
completedAt: new Date().toISOString(),
|
|
8171
|
+
errorText: detail,
|
|
8172
|
+
serverCloseout: {
|
|
8173
|
+
status: "failed",
|
|
8174
|
+
phase: "failed",
|
|
8175
|
+
updatedAt: new Date().toISOString(),
|
|
8176
|
+
error: detail
|
|
8177
|
+
}
|
|
8178
|
+
});
|
|
8179
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
8180
|
+
id: `log:${runId}:remote-server-closeout-failed`,
|
|
8181
|
+
title: "Server-owned closeout failed",
|
|
8182
|
+
detail,
|
|
8183
|
+
tone: "error",
|
|
8184
|
+
status: "failed",
|
|
8185
|
+
createdAt: new Date().toISOString()
|
|
8186
|
+
}, "remote-server-closeout-failed");
|
|
8187
|
+
}).finally(() => {
|
|
8188
|
+
deps.reconcileScheduler(state, "remote-server-closeout-terminal");
|
|
8189
|
+
});
|
|
8190
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
8191
|
+
return deps.jsonResponse({
|
|
8192
|
+
ok: true,
|
|
8193
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
8194
|
+
hostId,
|
|
8195
|
+
runId,
|
|
8196
|
+
leaseId,
|
|
8197
|
+
closeout: "server-owned",
|
|
8198
|
+
acceptedAt: new Date().toISOString()
|
|
8199
|
+
});
|
|
8200
|
+
}
|
|
7029
8201
|
patchRunRecord(state.projectRoot, runId, {
|
|
7030
8202
|
status: "completed",
|
|
7031
8203
|
completedAt,
|
|
7032
8204
|
hostId,
|
|
7033
8205
|
endpointId: leaseId
|
|
7034
8206
|
});
|
|
7035
|
-
await updateRemoteRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "completed", completedAt, hostId, endpointId: leaseId }, "closed", "Remote Rig task run completed and closed this task.");
|
|
7036
8207
|
await deps.enqueueRunLinearEvent(state.projectRoot, {
|
|
7037
8208
|
type: "run.completed",
|
|
7038
8209
|
runId,
|
|
@@ -7151,12 +8322,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7151
8322
|
try {
|
|
7152
8323
|
const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
|
|
7153
8324
|
const runRoot = deps.normalizeRelativePath(runsRoot, runId);
|
|
7154
|
-
const artifactsRoot =
|
|
8325
|
+
const artifactsRoot = resolve20(runRoot, "remote-artifacts");
|
|
7155
8326
|
artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
|
|
7156
8327
|
} catch {
|
|
7157
8328
|
return deps.badRequest("Invalid artifact path");
|
|
7158
8329
|
}
|
|
7159
|
-
if (!
|
|
8330
|
+
if (!existsSync13(artifactPath)) {
|
|
7160
8331
|
return deps.notFound();
|
|
7161
8332
|
}
|
|
7162
8333
|
return new Response(Bun.file(artifactPath));
|
|
@@ -7169,6 +8340,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7169
8340
|
const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
|
|
7170
8341
|
return deps.jsonResponse(page);
|
|
7171
8342
|
}
|
|
8343
|
+
const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
|
|
8344
|
+
if (runTimelineMatch) {
|
|
8345
|
+
const runId = decodeURIComponent(runTimelineMatch[1]);
|
|
8346
|
+
const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
|
|
8347
|
+
const cursor = normalizeString(url.searchParams.get("cursor"));
|
|
8348
|
+
const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
|
|
8349
|
+
return deps.jsonResponse(page);
|
|
8350
|
+
}
|
|
7172
8351
|
const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
|
|
7173
8352
|
if (runSteerMatch && req.method === "POST") {
|
|
7174
8353
|
const runId = decodeURIComponent(runSteerMatch[1]);
|
|
@@ -8049,8 +9228,8 @@ async function routeWebSocketRequest(state, deps, request) {
|
|
|
8049
9228
|
}
|
|
8050
9229
|
|
|
8051
9230
|
// packages/server/src/server-helpers/inspector-jobs.ts
|
|
8052
|
-
import { existsSync as
|
|
8053
|
-
import { dirname as
|
|
9231
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync16, readFileSync as readFileSync12, writeFileSync as writeFileSync15 } from "fs";
|
|
9232
|
+
import { dirname as dirname18, resolve as resolve23 } from "path";
|
|
8054
9233
|
import { readJsonFile as readJsonFile3 } from "@rig/runtime/control-plane/authority-files";
|
|
8055
9234
|
import { resolveMonorepoRoot as resolveMonorepoRoot5 } from "@rig/runtime/control-plane/native/utils";
|
|
8056
9235
|
import { normalizeTaskLifecycleStatus as normalizeTaskLifecycleStatus2 } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -8158,8 +9337,8 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
8158
9337
|
|
|
8159
9338
|
// packages/server/src/inspector/mission.ts
|
|
8160
9339
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8161
|
-
import { appendFileSync, existsSync as
|
|
8162
|
-
import { dirname as
|
|
9340
|
+
import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync14, readFileSync as readFileSync10, readdirSync as readdirSync4, renameSync, writeFileSync as writeFileSync13 } from "fs";
|
|
9341
|
+
import { dirname as dirname16, join, resolve as resolve21 } from "path";
|
|
8163
9342
|
function isJsonValue(value) {
|
|
8164
9343
|
if (value === null)
|
|
8165
9344
|
return true;
|
|
@@ -8199,7 +9378,7 @@ function isRecord2(value) {
|
|
|
8199
9378
|
}
|
|
8200
9379
|
function readJsonRecord(path) {
|
|
8201
9380
|
try {
|
|
8202
|
-
const parsed = JSON.parse(
|
|
9381
|
+
const parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
8203
9382
|
if (!isRecord2(parsed)) {
|
|
8204
9383
|
return { ok: false, error: `Mission file ${path} does not contain an object` };
|
|
8205
9384
|
}
|
|
@@ -8279,14 +9458,14 @@ function missionActionDetails(mission) {
|
|
|
8279
9458
|
};
|
|
8280
9459
|
}
|
|
8281
9460
|
function writeJsonFile5(path, value) {
|
|
8282
|
-
|
|
9461
|
+
mkdirSync14(dirname16(path), { recursive: true });
|
|
8283
9462
|
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
8284
|
-
|
|
9463
|
+
writeFileSync13(tempPath, `${JSON.stringify(value, null, 2)}
|
|
8285
9464
|
`, "utf8");
|
|
8286
9465
|
renameSync(tempPath, path);
|
|
8287
9466
|
}
|
|
8288
9467
|
function resolveInspectorMissionPaths(projectRoot) {
|
|
8289
|
-
const inspectorDir =
|
|
9468
|
+
const inspectorDir = resolve21(resolveRigServerPaths(projectRoot).stateDir, "inspector");
|
|
8290
9469
|
return {
|
|
8291
9470
|
inspectorDir,
|
|
8292
9471
|
missionsDir: join(inspectorDir, "missions"),
|
|
@@ -8295,8 +9474,8 @@ function resolveInspectorMissionPaths(projectRoot) {
|
|
|
8295
9474
|
}
|
|
8296
9475
|
function createInspectorMissionController(options) {
|
|
8297
9476
|
const paths = resolveInspectorMissionPaths(options.projectRoot);
|
|
8298
|
-
|
|
8299
|
-
|
|
9477
|
+
mkdirSync14(paths.missionsDir, { recursive: true });
|
|
9478
|
+
mkdirSync14(paths.journalsDir, { recursive: true });
|
|
8300
9479
|
const now = options.now ?? (() => new Date().toISOString());
|
|
8301
9480
|
const nextId = options.idGenerator ?? (() => `mission:${randomUUID2()}`);
|
|
8302
9481
|
function missionPath(missionId) {
|
|
@@ -8306,15 +9485,15 @@ function createInspectorMissionController(options) {
|
|
|
8306
9485
|
return join(paths.journalsDir, `${missionId}.jsonl`);
|
|
8307
9486
|
}
|
|
8308
9487
|
function appendMissionJournal(entry) {
|
|
8309
|
-
|
|
9488
|
+
mkdirSync14(paths.journalsDir, { recursive: true });
|
|
8310
9489
|
appendFileSync(journalPath(entry.missionId), `${JSON.stringify(entry)}
|
|
8311
9490
|
`, "utf8");
|
|
8312
9491
|
}
|
|
8313
9492
|
function listMissionJournal(missionId) {
|
|
8314
9493
|
const path = journalPath(missionId);
|
|
8315
|
-
if (!
|
|
9494
|
+
if (!existsSync14(path))
|
|
8316
9495
|
return [];
|
|
8317
|
-
return
|
|
9496
|
+
return readFileSync10(path, "utf8").split(`
|
|
8318
9497
|
`).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line)).filter(isRecord2).map((entry) => ({
|
|
8319
9498
|
id: typeof entry.id === "string" ? entry.id : `journal:${randomUUID2()}`,
|
|
8320
9499
|
missionId,
|
|
@@ -8330,7 +9509,7 @@ function createInspectorMissionController(options) {
|
|
|
8330
9509
|
}
|
|
8331
9510
|
function readMissionOnly(missionId) {
|
|
8332
9511
|
const path = missionPath(missionId);
|
|
8333
|
-
if (!
|
|
9512
|
+
if (!existsSync14(path)) {
|
|
8334
9513
|
return { ok: false, error: `Mission ${missionId} was not found` };
|
|
8335
9514
|
}
|
|
8336
9515
|
const read = readJsonRecord(path);
|
|
@@ -8381,7 +9560,7 @@ function createInspectorMissionController(options) {
|
|
|
8381
9560
|
const source = cloneJsonRecord(input.sourceTask);
|
|
8382
9561
|
const missionId = nextId();
|
|
8383
9562
|
const path = missionPath(missionId);
|
|
8384
|
-
if (
|
|
9563
|
+
if (existsSync14(path)) {
|
|
8385
9564
|
const existing = readMissionOnly(missionId);
|
|
8386
9565
|
if (!existing.ok)
|
|
8387
9566
|
return existing;
|
|
@@ -9981,8 +11160,8 @@ function createCodexInspectorTransport(options) {
|
|
|
9981
11160
|
const sendRequest = async (method, params) => {
|
|
9982
11161
|
const id = nextRequestId;
|
|
9983
11162
|
nextRequestId += 1;
|
|
9984
|
-
const response = new Promise((
|
|
9985
|
-
pendingResponses.set(id, { resolve:
|
|
11163
|
+
const response = new Promise((resolve22, reject) => {
|
|
11164
|
+
pendingResponses.set(id, { resolve: resolve22, reject });
|
|
9986
11165
|
});
|
|
9987
11166
|
response.catch(() => {});
|
|
9988
11167
|
try {
|
|
@@ -10292,9 +11471,9 @@ function createCodexInspectorTransport(options) {
|
|
|
10292
11471
|
}
|
|
10293
11472
|
lastAssistantMessage = null;
|
|
10294
11473
|
lastError = null;
|
|
10295
|
-
const turnResult = new Promise((
|
|
11474
|
+
const turnResult = new Promise((resolve22, reject) => {
|
|
10296
11475
|
currentTurn = {
|
|
10297
|
-
resolve:
|
|
11476
|
+
resolve: resolve22,
|
|
10298
11477
|
reject,
|
|
10299
11478
|
events: []
|
|
10300
11479
|
};
|
|
@@ -10354,13 +11533,13 @@ function createCodexInspectorTransport(options) {
|
|
|
10354
11533
|
};
|
|
10355
11534
|
}
|
|
10356
11535
|
function writeChildLine(child, line) {
|
|
10357
|
-
return new Promise((
|
|
11536
|
+
return new Promise((resolve22, reject) => {
|
|
10358
11537
|
child.stdin.write(line, (error) => {
|
|
10359
11538
|
if (error) {
|
|
10360
11539
|
reject(error);
|
|
10361
11540
|
return;
|
|
10362
11541
|
}
|
|
10363
|
-
|
|
11542
|
+
resolve22();
|
|
10364
11543
|
});
|
|
10365
11544
|
});
|
|
10366
11545
|
}
|
|
@@ -10373,10 +11552,10 @@ function terminateChild(child) {
|
|
|
10373
11552
|
} catch {}
|
|
10374
11553
|
}
|
|
10375
11554
|
async function waitForChildSpawn(child) {
|
|
10376
|
-
await new Promise((
|
|
11555
|
+
await new Promise((resolve22, reject) => {
|
|
10377
11556
|
const onSpawn = () => {
|
|
10378
11557
|
cleanup();
|
|
10379
|
-
|
|
11558
|
+
resolve22();
|
|
10380
11559
|
};
|
|
10381
11560
|
const onError = (error) => {
|
|
10382
11561
|
cleanup();
|
|
@@ -10888,8 +12067,8 @@ function createGlobalInspectorService(options) {
|
|
|
10888
12067
|
|
|
10889
12068
|
// packages/server/src/inspector/upstream-sync.ts
|
|
10890
12069
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
10891
|
-
import { existsSync as
|
|
10892
|
-
import { dirname as
|
|
12070
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync14 } from "fs";
|
|
12071
|
+
import { dirname as dirname17, resolve as resolve22 } from "path";
|
|
10893
12072
|
import { resolveMonorepoRoot as resolveMonorepoRoot4 } from "@rig/runtime/control-plane/native/utils";
|
|
10894
12073
|
var UPSTREAM_VALIDATION_DESCRIPTIONS = {
|
|
10895
12074
|
"integration:hg-auth-backport": "Preserves the upstream auth hardening cluster: nonce-backed node-client login, signature-aware JWT verification, shared-token onboarding semantics, and regression coverage.",
|
|
@@ -11027,34 +12206,34 @@ function defaultGitRunner(repoRoot, args) {
|
|
|
11027
12206
|
}
|
|
11028
12207
|
function upstreamStatePath(projectRoot, override) {
|
|
11029
12208
|
if (override) {
|
|
11030
|
-
return
|
|
12209
|
+
return resolve22(override);
|
|
11031
12210
|
}
|
|
11032
|
-
return
|
|
12211
|
+
return resolve22(resolveRigServerPaths(projectRoot).stateDir, "inspector", "upstream-sync.json");
|
|
11033
12212
|
}
|
|
11034
12213
|
function readUpstreamState(projectRoot, statePath) {
|
|
11035
12214
|
const path = upstreamStatePath(projectRoot, statePath);
|
|
11036
|
-
if (!
|
|
12215
|
+
if (!existsSync15(path)) {
|
|
11037
12216
|
return null;
|
|
11038
12217
|
}
|
|
11039
12218
|
try {
|
|
11040
|
-
return JSON.parse(
|
|
12219
|
+
return JSON.parse(readFileSync11(path, "utf-8"));
|
|
11041
12220
|
} catch {
|
|
11042
12221
|
return null;
|
|
11043
12222
|
}
|
|
11044
12223
|
}
|
|
11045
12224
|
function writeUpstreamState(projectRoot, state, statePath) {
|
|
11046
12225
|
const path = upstreamStatePath(projectRoot, statePath);
|
|
11047
|
-
|
|
11048
|
-
|
|
12226
|
+
mkdirSync15(dirname17(path), { recursive: true });
|
|
12227
|
+
writeFileSync14(path, `${JSON.stringify(state, null, 2)}
|
|
11049
12228
|
`, "utf8");
|
|
11050
12229
|
}
|
|
11051
12230
|
function readImportedRevision(projectRoot, upstreamsDocPath) {
|
|
11052
12231
|
const monorepoRoot = resolveMonorepoRoot4(projectRoot);
|
|
11053
|
-
const docPath = upstreamsDocPath ?
|
|
11054
|
-
if (!
|
|
12232
|
+
const docPath = upstreamsDocPath ? resolve22(upstreamsDocPath) : resolve22(monorepoRoot, "docs", "UPSTREAMS.md");
|
|
12233
|
+
if (!existsSync15(docPath)) {
|
|
11055
12234
|
throw new Error(`UPSTREAMS.md not found at ${docPath}`);
|
|
11056
12235
|
}
|
|
11057
|
-
const docContent =
|
|
12236
|
+
const docContent = readFileSync11(docPath, "utf-8");
|
|
11058
12237
|
const revision = parseImportedUpstreamRevision(docContent, "upstream") ?? parseImportedUpstreamRevision(docContent, "humoongate");
|
|
11059
12238
|
if (!revision) {
|
|
11060
12239
|
throw new Error(`Failed to parse upstream imported revision from ${docPath}`);
|
|
@@ -11076,7 +12255,7 @@ function resolveRemoteBranch(repoRoot, remote, gitRunner) {
|
|
|
11076
12255
|
return null;
|
|
11077
12256
|
}
|
|
11078
12257
|
function isGitCheckout(path, gitRunner) {
|
|
11079
|
-
if (!
|
|
12258
|
+
if (!existsSync15(resolve22(path, ".git"))) {
|
|
11080
12259
|
return false;
|
|
11081
12260
|
}
|
|
11082
12261
|
const result = gitRunner(path, ["rev-parse", "--is-inside-work-tree"]);
|
|
@@ -11085,12 +12264,12 @@ function isGitCheckout(path, gitRunner) {
|
|
|
11085
12264
|
function resolveUpstreamCheckout(projectRoot, explicitCheckout, gitRunner) {
|
|
11086
12265
|
const monorepoRoot = resolveMonorepoRoot4(projectRoot);
|
|
11087
12266
|
const candidates = [
|
|
11088
|
-
explicitCheckout ?
|
|
11089
|
-
process.env.UPSTREAM_CHECKOUT?.trim() ?
|
|
11090
|
-
process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ?
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
12267
|
+
explicitCheckout ? resolve22(explicitCheckout) : "",
|
|
12268
|
+
process.env.UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.UPSTREAM_CHECKOUT.trim()) : "",
|
|
12269
|
+
process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.HUMOONGATE_UPSTREAM_CHECKOUT.trim()) : "",
|
|
12270
|
+
resolve22(projectRoot, "..", "humoongate"),
|
|
12271
|
+
resolve22(monorepoRoot, "..", "humoongate"),
|
|
12272
|
+
resolve22(monorepoRoot, "humoongate")
|
|
11094
12273
|
].filter(Boolean);
|
|
11095
12274
|
for (const candidate of candidates) {
|
|
11096
12275
|
if (isGitCheckout(candidate, gitRunner)) {
|
|
@@ -11326,10 +12505,10 @@ async function runUpstreamSyncScan(options) {
|
|
|
11326
12505
|
}
|
|
11327
12506
|
|
|
11328
12507
|
// packages/server/src/server-helpers/task-config.ts
|
|
11329
|
-
import { existsSync as
|
|
12508
|
+
import { existsSync as existsSync16 } from "fs";
|
|
11330
12509
|
async function readTaskConfig(projectRoot) {
|
|
11331
12510
|
const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
|
|
11332
|
-
if (!
|
|
12511
|
+
if (!existsSync16(taskConfigPath)) {
|
|
11333
12512
|
return {};
|
|
11334
12513
|
}
|
|
11335
12514
|
try {
|
|
@@ -11365,11 +12544,11 @@ function resolveFollowupSourceCommit(input) {
|
|
|
11365
12544
|
}
|
|
11366
12545
|
async function createInspectorFollowupTask(projectRoot, input) {
|
|
11367
12546
|
const monorepoRoot = resolveMonorepoRoot5(projectRoot);
|
|
11368
|
-
const issuesPath =
|
|
11369
|
-
const taskStatePath =
|
|
11370
|
-
const taskConfigPath =
|
|
11371
|
-
|
|
11372
|
-
|
|
12547
|
+
const issuesPath = resolve23(monorepoRoot, ".beads", "issues.jsonl");
|
|
12548
|
+
const taskStatePath = resolve23(monorepoRoot, ".beads", "task-state.json");
|
|
12549
|
+
const taskConfigPath = resolve23(monorepoRoot, ".rig", "task-config.json");
|
|
12550
|
+
mkdirSync16(dirname18(issuesPath), { recursive: true });
|
|
12551
|
+
mkdirSync16(dirname18(taskConfigPath), { recursive: true });
|
|
11373
12552
|
const summary = normalizeString(input.summary) ?? "Inspector follow-up";
|
|
11374
12553
|
const description = normalizeString(input.description) ?? normalizeString(input.details?.description) ?? `Created by the global inspector: ${summary}`;
|
|
11375
12554
|
const acceptanceCriteria = normalizeString(input.acceptanceCriteria) ?? "Investigate the detected drift and port the relevant changes into Rig.";
|
|
@@ -11388,7 +12567,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
|
|
|
11388
12567
|
const sourceKey = normalizeString(input.sourceKey) ?? normalizeString(input.details?.sourceKey);
|
|
11389
12568
|
const createdAt = normalizeString(input.createdAt) ?? new Date().toISOString();
|
|
11390
12569
|
const status = normalizeTaskLifecycleStatus2(normalizeString(input.status) ?? "open") ?? "open";
|
|
11391
|
-
const existingIssueLines =
|
|
12570
|
+
const existingIssueLines = existsSync17(issuesPath) ? readFileSync12(issuesPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
|
|
11392
12571
|
const existingIssues = existingIssueLines.map((line) => {
|
|
11393
12572
|
try {
|
|
11394
12573
|
return JSON.parse(line);
|
|
@@ -11397,7 +12576,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
|
|
|
11397
12576
|
}
|
|
11398
12577
|
}).filter((value) => value !== null);
|
|
11399
12578
|
const existingIds = new Set(existingIssues.map((issue) => typeof issue.id === "string" ? issue.id : null).filter((value) => value !== null));
|
|
11400
|
-
const rawTaskState =
|
|
12579
|
+
const rawTaskState = existsSync17(taskStatePath) ? readJsonFile3(taskStatePath, {}) : {};
|
|
11401
12580
|
const tasks = rawTaskState.tasks && typeof rawTaskState.tasks === "object" && !Array.isArray(rawTaskState.tasks) ? rawTaskState.tasks : {};
|
|
11402
12581
|
const existingTaskIdFromSourceKey = sourceKey == null ? null : Object.entries(tasks).find(([, metadata]) => {
|
|
11403
12582
|
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
@@ -11423,7 +12602,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
|
|
|
11423
12602
|
updated_at: createdAt,
|
|
11424
12603
|
labels: mergedLabels
|
|
11425
12604
|
};
|
|
11426
|
-
|
|
12605
|
+
writeFileSync15(issuesPath, existingIssueLines.length > 0 ? `${existingIssueLines.join(`
|
|
11427
12606
|
`)}
|
|
11428
12607
|
${JSON.stringify(issueRecord)}
|
|
11429
12608
|
` : `${JSON.stringify(issueRecord)}
|
|
@@ -11441,7 +12620,7 @@ ${JSON.stringify(issueRecord)}
|
|
|
11441
12620
|
labels: mergedLabels
|
|
11442
12621
|
};
|
|
11443
12622
|
});
|
|
11444
|
-
|
|
12623
|
+
writeFileSync15(issuesPath, `${updatedIssues.map((issue) => JSON.stringify(issue)).join(`
|
|
11445
12624
|
`)}
|
|
11446
12625
|
`, "utf8");
|
|
11447
12626
|
}
|
|
@@ -11464,14 +12643,14 @@ ${JSON.stringify(issueRecord)}
|
|
|
11464
12643
|
}
|
|
11465
12644
|
};
|
|
11466
12645
|
}
|
|
11467
|
-
|
|
12646
|
+
writeFileSync15(taskConfigPath, `${JSON.stringify(taskConfig, null, 2)}
|
|
11468
12647
|
`, "utf8");
|
|
11469
12648
|
tasks[taskId] = {
|
|
11470
12649
|
status,
|
|
11471
12650
|
sourceCommit: resolveFollowupSourceCommit(input),
|
|
11472
12651
|
...sourceKey ? { sourceKey } : {}
|
|
11473
12652
|
};
|
|
11474
|
-
|
|
12653
|
+
writeFileSync15(taskStatePath, `${JSON.stringify({
|
|
11475
12654
|
schemaVersion: 1,
|
|
11476
12655
|
baseTrackerCommit: typeof rawTaskState.baseTrackerCommit === "string" ? rawTaskState.baseTrackerCommit : null,
|
|
11477
12656
|
tasks
|
|
@@ -11779,12 +12958,12 @@ function isAuthorizedLinearWebhookRequest(req) {
|
|
|
11779
12958
|
}
|
|
11780
12959
|
|
|
11781
12960
|
// packages/server/src/server-helpers/notifications.ts
|
|
11782
|
-
import { existsSync as
|
|
11783
|
-
import { dirname as
|
|
12961
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync17, readFileSync as readFileSync13 } from "fs";
|
|
12962
|
+
import { dirname as dirname19 } from "path";
|
|
11784
12963
|
async function loadNotificationConfig(path) {
|
|
11785
|
-
if (!
|
|
12964
|
+
if (!existsSync18(path)) {
|
|
11786
12965
|
const defaultConfig = { targets: [] };
|
|
11787
|
-
|
|
12966
|
+
mkdirSync17(dirname19(path), { recursive: true });
|
|
11788
12967
|
await Bun.write(path, `${JSON.stringify(defaultConfig, null, 2)}
|
|
11789
12968
|
`);
|
|
11790
12969
|
return defaultConfig;
|
|
@@ -11799,10 +12978,10 @@ async function loadNotificationConfig(path) {
|
|
|
11799
12978
|
}
|
|
11800
12979
|
}
|
|
11801
12980
|
function readRecentEvents(file, limit) {
|
|
11802
|
-
if (!
|
|
12981
|
+
if (!existsSync18(file)) {
|
|
11803
12982
|
return [];
|
|
11804
12983
|
}
|
|
11805
|
-
const lines =
|
|
12984
|
+
const lines = readFileSync13(file, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-limit);
|
|
11806
12985
|
const events = [];
|
|
11807
12986
|
for (const line of lines) {
|
|
11808
12987
|
try {
|
|
@@ -11897,11 +13076,11 @@ function extractObjectLiteralBlock(source, property) {
|
|
|
11897
13076
|
}
|
|
11898
13077
|
function readFallbackIssueAnalysisConfig(projectRoot) {
|
|
11899
13078
|
for (const fileName of ["rig.config.ts", "rig.config.json"]) {
|
|
11900
|
-
const path =
|
|
11901
|
-
if (!
|
|
13079
|
+
const path = resolve24(projectRoot, fileName);
|
|
13080
|
+
if (!existsSync19(path))
|
|
11902
13081
|
continue;
|
|
11903
13082
|
try {
|
|
11904
|
-
const source =
|
|
13083
|
+
const source = readFileSync14(path, "utf8");
|
|
11905
13084
|
if (fileName.endsWith(".json"))
|
|
11906
13085
|
return JSON.parse(source);
|
|
11907
13086
|
const issueBlock = extractObjectLiteralBlock(source, "issueAnalysis");
|
|
@@ -12034,8 +13213,8 @@ async function createIssueAnalysisRunnerForServerState(state, input) {
|
|
|
12034
13213
|
async function withServerPathEnv(projectRoot, fn) {
|
|
12035
13214
|
const waitForTurn = serverPathEnvQueue;
|
|
12036
13215
|
let releaseTurn;
|
|
12037
|
-
serverPathEnvQueue = new Promise((
|
|
12038
|
-
releaseTurn =
|
|
13216
|
+
serverPathEnvQueue = new Promise((resolve25) => {
|
|
13217
|
+
releaseTurn = resolve25;
|
|
12039
13218
|
});
|
|
12040
13219
|
await waitForTurn;
|
|
12041
13220
|
const paths = resolveServerAuthorityPaths(projectRoot);
|
|
@@ -12071,9 +13250,9 @@ async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
|
|
|
12071
13250
|
return withServerPathEnv(projectRoot, fn);
|
|
12072
13251
|
}
|
|
12073
13252
|
async function readWorkspaceTasks(projectRoot) {
|
|
12074
|
-
const issuesPath =
|
|
13253
|
+
const issuesPath = resolve24(resolveMonorepoRoot6(projectRoot), ".beads", "issues.jsonl");
|
|
12075
13254
|
const taskConfig = await readTaskConfig(projectRoot);
|
|
12076
|
-
if (!
|
|
13255
|
+
if (!existsSync19(issuesPath)) {
|
|
12077
13256
|
return [];
|
|
12078
13257
|
}
|
|
12079
13258
|
const latestById = new Map;
|
|
@@ -12147,11 +13326,11 @@ function resolveTaskArtifactDirsFromRuns(projectRoot, taskId, knownRuns) {
|
|
|
12147
13326
|
continue;
|
|
12148
13327
|
add(run.artifactRoot);
|
|
12149
13328
|
if (run.worktreePath) {
|
|
12150
|
-
add(
|
|
13329
|
+
add(resolve24(run.worktreePath, "artifacts", taskId));
|
|
12151
13330
|
}
|
|
12152
13331
|
}
|
|
12153
13332
|
for (const artifactsRoot of listAuthorityArtifactRoots(projectRoot)) {
|
|
12154
|
-
add(
|
|
13333
|
+
add(resolve24(artifactsRoot, taskId));
|
|
12155
13334
|
}
|
|
12156
13335
|
return candidates;
|
|
12157
13336
|
}
|
|
@@ -12165,7 +13344,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
|
|
|
12165
13344
|
}
|
|
12166
13345
|
}
|
|
12167
13346
|
return taskIds.flatMap((currentTaskId) => {
|
|
12168
|
-
const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) =>
|
|
13347
|
+
const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) => existsSync19(path));
|
|
12169
13348
|
if (!currentRoot) {
|
|
12170
13349
|
return [];
|
|
12171
13350
|
}
|
|
@@ -12177,7 +13356,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
|
|
|
12177
13356
|
taskId: currentTaskId,
|
|
12178
13357
|
kind: "file",
|
|
12179
13358
|
label: fileName,
|
|
12180
|
-
path:
|
|
13359
|
+
path: resolve24(currentRoot, fileName),
|
|
12181
13360
|
url: null,
|
|
12182
13361
|
metadata: {
|
|
12183
13362
|
fileName
|
|
@@ -12220,11 +13399,11 @@ function buildInspectorStreamPayload(state, sequence) {
|
|
|
12220
13399
|
}
|
|
12221
13400
|
function listRemoteRunArtifacts(projectRoot, runId) {
|
|
12222
13401
|
const root = remoteArtifactsRoot(projectRoot, runId);
|
|
12223
|
-
if (!
|
|
13402
|
+
if (!existsSync19(root)) {
|
|
12224
13403
|
return [];
|
|
12225
13404
|
}
|
|
12226
13405
|
return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isFile()).filter((entry) => !entry.name.endsWith(".json")).map((entry) => {
|
|
12227
|
-
const artifactPath =
|
|
13406
|
+
const artifactPath = resolve24(root, entry.name);
|
|
12228
13407
|
const stat = statSync6(artifactPath);
|
|
12229
13408
|
const meta = readJsonFile4(`${artifactPath}.json`, null);
|
|
12230
13409
|
return {
|
|
@@ -12385,6 +13564,7 @@ function buildHttpRouterDeps(state) {
|
|
|
12385
13564
|
startLocalRun,
|
|
12386
13565
|
stopRunRecord,
|
|
12387
13566
|
resumeRunRecord,
|
|
13567
|
+
runServerOwnedPrCloseout,
|
|
12388
13568
|
claimRemoteRun,
|
|
12389
13569
|
listRemoteRunArtifacts,
|
|
12390
13570
|
broadcastSnapshotInvalidation,
|
|
@@ -12466,8 +13646,8 @@ function fileStats(path) {
|
|
|
12466
13646
|
}
|
|
12467
13647
|
}
|
|
12468
13648
|
function runFileCursor(projectRoot, run) {
|
|
12469
|
-
const runDir =
|
|
12470
|
-
const runJson = fileStats(
|
|
13649
|
+
const runDir = dirname20(runLogsPath(projectRoot, run.runId));
|
|
13650
|
+
const runJson = fileStats(resolve24(runDir, "run.json"));
|
|
12471
13651
|
const timeline = fileStats(runTimelinePath(projectRoot, run.runId));
|
|
12472
13652
|
const logs = fileStats(runLogsPath(projectRoot, run.runId));
|
|
12473
13653
|
return {
|
|
@@ -12517,10 +13697,10 @@ function startRunFileWatcher(state, pollMs) {
|
|
|
12517
13697
|
}, Math.max(250, Math.min(pollMs, 1000)));
|
|
12518
13698
|
}
|
|
12519
13699
|
function startPoller(state, pollMs) {
|
|
12520
|
-
let offset =
|
|
13700
|
+
let offset = existsSync19(state.eventsFile) ? statSync6(state.eventsFile).size : 0;
|
|
12521
13701
|
return setInterval(async () => {
|
|
12522
13702
|
try {
|
|
12523
|
-
if (!
|
|
13703
|
+
if (!existsSync19(state.eventsFile)) {
|
|
12524
13704
|
return;
|
|
12525
13705
|
}
|
|
12526
13706
|
const file = await open(state.eventsFile, "r");
|
|
@@ -12572,6 +13752,7 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
|
|
|
12572
13752
|
const server = Bun.serve({
|
|
12573
13753
|
hostname: options.host,
|
|
12574
13754
|
port: options.port,
|
|
13755
|
+
idleTimeout: Math.max(10, Math.min(255, Number.parseInt(process.env.RIG_SERVER_IDLE_TIMEOUT_SECONDS || "255", 10) || 255)),
|
|
12575
13756
|
fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
|
|
12576
13757
|
websocket: {
|
|
12577
13758
|
open(ws) {
|
|
@@ -12647,7 +13828,7 @@ function resolveProjectRoot() {
|
|
|
12647
13828
|
return resolveRigProjectRoot({
|
|
12648
13829
|
envProjectRoot: process.env.PROJECT_RIG_ROOT ?? null,
|
|
12649
13830
|
cwd: process.cwd(),
|
|
12650
|
-
fallbackRoot:
|
|
13831
|
+
fallbackRoot: resolve24(import.meta.dir, "../..")
|
|
12651
13832
|
});
|
|
12652
13833
|
}
|
|
12653
13834
|
var __testOnly = {
|