@h-rig/server 0.0.6-alpha.3 → 0.0.6-alpha.31
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 +1688 -351
- 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 +1040 -200
- package/dist/src/server-helpers/inspector-jobs.js +1 -12
- package/dist/src/server-helpers/issue-analysis.js +26 -10
- package/dist/src/server-helpers/pi-session-proxy.js +84 -0
- package/dist/src/server-helpers/project-registry.js +5 -0
- package/dist/src/server-helpers/run-io.js +30 -1
- package/dist/src/server-helpers/run-mutations.js +679 -123
- package/dist/src/server-helpers/run-writers.js +8 -2
- package/dist/src/server-helpers/ws-router.js +16 -6
- package/dist/src/server.js +1687 -351
- 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,
|
|
@@ -1023,14 +1024,29 @@ function summarizeUsefulRunError(projectRoot, runId, fallback) {
|
|
|
1023
1024
|
const nonGeneric = errorLines.at(-1);
|
|
1024
1025
|
return nonGeneric ?? (typeof fallback === "string" ? fallback : null);
|
|
1025
1026
|
}
|
|
1027
|
+
function readRunPiSessionMetadata(projectRoot, runId) {
|
|
1028
|
+
const run = readAuthorityRun(projectRoot, runId);
|
|
1029
|
+
const metadata = run?.piSessionPrivate;
|
|
1030
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
|
|
1031
|
+
return null;
|
|
1032
|
+
const record = metadata;
|
|
1033
|
+
const publicMetadata = record.public;
|
|
1034
|
+
const daemonConnection = record.daemonConnection;
|
|
1035
|
+
if (!publicMetadata || typeof publicMetadata !== "object" || Array.isArray(publicMetadata))
|
|
1036
|
+
return null;
|
|
1037
|
+
if (!daemonConnection || typeof daemonConnection !== "object" || Array.isArray(daemonConnection))
|
|
1038
|
+
return null;
|
|
1039
|
+
return metadata;
|
|
1040
|
+
}
|
|
1026
1041
|
function readRunDetails(projectRoot, runId) {
|
|
1027
1042
|
const run = readAuthorityRun(projectRoot, runId);
|
|
1028
1043
|
if (!run) {
|
|
1029
1044
|
return null;
|
|
1030
1045
|
}
|
|
1031
1046
|
const usefulErrorText = isGenericRunFailure(run.errorText) ? summarizeUsefulRunError(projectRoot, runId, run.errorText) : null;
|
|
1047
|
+
const { piSessionPrivate: _piSessionPrivate, ...publicRun } = run;
|
|
1032
1048
|
return {
|
|
1033
|
-
run: usefulErrorText ? { ...
|
|
1049
|
+
run: usefulErrorText ? { ...publicRun, errorText: usefulErrorText } : publicRun,
|
|
1034
1050
|
timeline: readJsonlFile(resolve4(resolveAuthorityRunDir(projectRoot, runId), "timeline.jsonl")),
|
|
1035
1051
|
approvals: readApprovals(projectRoot, { runId }),
|
|
1036
1052
|
userInputs: readUserInputs(projectRoot, { runId })
|
|
@@ -1073,6 +1089,18 @@ function readJsonlFileTail(path, options) {
|
|
|
1073
1089
|
const completeLines = start > 0 ? lines.slice(1) : lines;
|
|
1074
1090
|
return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
|
|
1075
1091
|
}
|
|
1092
|
+
async function readRunTimelinePage(projectRoot, runId, options = {}) {
|
|
1093
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
1094
|
+
const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
|
|
1095
|
+
const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
1096
|
+
const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
|
|
1097
|
+
const endExclusive = Math.min(entries.length, startInclusive + limit);
|
|
1098
|
+
return {
|
|
1099
|
+
entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
|
|
1100
|
+
nextCursor: String(endExclusive),
|
|
1101
|
+
hasMore: endExclusive < entries.length
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1076
1104
|
var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
1077
1105
|
async function readRunLogsPage(projectRoot, runId, options = {}) {
|
|
1078
1106
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
@@ -2312,7 +2340,6 @@ function createGitHubTaskReconciler(input) {
|
|
|
2312
2340
|
}
|
|
2313
2341
|
|
|
2314
2342
|
// packages/server/src/server-helpers/issue-analysis.ts
|
|
2315
|
-
import { execFile } from "child_process";
|
|
2316
2343
|
import { createHash } from "crypto";
|
|
2317
2344
|
function stableIssueHash(issue) {
|
|
2318
2345
|
const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
|
|
@@ -2446,16 +2473,33 @@ function parseIssueAnalysisResult(raw) {
|
|
|
2446
2473
|
return result;
|
|
2447
2474
|
}
|
|
2448
2475
|
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 ?? "") });
|
|
2476
|
+
return async (command, args, options) => {
|
|
2477
|
+
const env = options.env ? { ...process.env, ...options.env } : process.env;
|
|
2478
|
+
const proc = Bun.spawn([command, ...args], {
|
|
2479
|
+
stdout: "pipe",
|
|
2480
|
+
stderr: "pipe",
|
|
2481
|
+
env
|
|
2457
2482
|
});
|
|
2458
|
-
|
|
2483
|
+
let timedOut = false;
|
|
2484
|
+
const timer = setTimeout(() => {
|
|
2485
|
+
timedOut = true;
|
|
2486
|
+
proc.kill();
|
|
2487
|
+
}, options.timeoutMs);
|
|
2488
|
+
try {
|
|
2489
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2490
|
+
new Response(proc.stdout).text(),
|
|
2491
|
+
new Response(proc.stderr).text(),
|
|
2492
|
+
proc.exited
|
|
2493
|
+
]);
|
|
2494
|
+
return {
|
|
2495
|
+
exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
|
|
2496
|
+
stdout,
|
|
2497
|
+
stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
|
|
2498
|
+
};
|
|
2499
|
+
} finally {
|
|
2500
|
+
clearTimeout(timer);
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2459
2503
|
}
|
|
2460
2504
|
function createPiIssueAnalyzer(input = {}) {
|
|
2461
2505
|
const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
|
|
@@ -2709,13 +2753,18 @@ function patchRunRecord(projectRoot, runId, patch) {
|
|
|
2709
2753
|
writeJsonFile2(resolve11(resolveAuthorityRunDir2(projectRoot, runId), "run.json"), next);
|
|
2710
2754
|
return next;
|
|
2711
2755
|
}
|
|
2756
|
+
function patchRunPiSessionMetadata(projectRoot, runId, metadata) {
|
|
2757
|
+
return patchRunRecord(projectRoot, runId, {
|
|
2758
|
+
piSession: metadata?.public ?? null,
|
|
2759
|
+
piSessionPrivate: metadata
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2712
2762
|
function buildRunStartPatch(startedAt) {
|
|
2713
2763
|
return {
|
|
2714
2764
|
status: "preparing",
|
|
2715
2765
|
startedAt,
|
|
2716
2766
|
completedAt: null,
|
|
2717
|
-
errorText: null
|
|
2718
|
-
serverPid: process.pid
|
|
2767
|
+
errorText: null
|
|
2719
2768
|
};
|
|
2720
2769
|
}
|
|
2721
2770
|
|
|
@@ -3184,7 +3233,7 @@ function applyOrchestrationCommand(state, command) {
|
|
|
3184
3233
|
import { spawn as spawn3 } from "child_process";
|
|
3185
3234
|
import { loadConfig } from "@rig/core/load-config";
|
|
3186
3235
|
import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
3187
|
-
import { dirname as
|
|
3236
|
+
import { dirname as dirname9, relative as relative2, resolve as resolve14 } from "path";
|
|
3188
3237
|
import {
|
|
3189
3238
|
listAuthorityRuns as listAuthorityRuns4,
|
|
3190
3239
|
readAuthorityRun as readAuthorityRun4,
|
|
@@ -3197,6 +3246,11 @@ import {
|
|
|
3197
3246
|
buildTaskRunLifecycleComment,
|
|
3198
3247
|
updateConfiguredTaskSourceTask
|
|
3199
3248
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
3249
|
+
import {
|
|
3250
|
+
closeIssueAfterMergedPr,
|
|
3251
|
+
commitRunChanges,
|
|
3252
|
+
runPrAutomation
|
|
3253
|
+
} from "@rig/runtime/control-plane/native/pr-automation";
|
|
3200
3254
|
|
|
3201
3255
|
// packages/server/src/scheduler.ts
|
|
3202
3256
|
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -3308,8 +3362,8 @@ function summarizeRunValidationFailure(projectRoot, run) {
|
|
|
3308
3362
|
|
|
3309
3363
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
3310
3364
|
import { randomBytes } from "crypto";
|
|
3311
|
-
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
3312
|
-
import { resolve as resolve13 } from "path";
|
|
3365
|
+
import { chmodSync, copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
3366
|
+
import { dirname as dirname8, resolve as resolve13 } from "path";
|
|
3313
3367
|
function cleanString(value) {
|
|
3314
3368
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
3315
3369
|
}
|
|
@@ -3339,6 +3393,26 @@ function parseApiSessions(value) {
|
|
|
3339
3393
|
}];
|
|
3340
3394
|
});
|
|
3341
3395
|
}
|
|
3396
|
+
function parsePendingDevice(value) {
|
|
3397
|
+
if (!value || typeof value !== "object")
|
|
3398
|
+
return null;
|
|
3399
|
+
const record = value;
|
|
3400
|
+
const pollId = cleanString(record.pollId);
|
|
3401
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
3402
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
3403
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
3404
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
3405
|
+
return null;
|
|
3406
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
3407
|
+
}
|
|
3408
|
+
function parsePendingDevices(value) {
|
|
3409
|
+
if (!Array.isArray(value))
|
|
3410
|
+
return [];
|
|
3411
|
+
return value.flatMap((entry) => {
|
|
3412
|
+
const pending = parsePendingDevice(entry);
|
|
3413
|
+
return pending ? [pending] : [];
|
|
3414
|
+
});
|
|
3415
|
+
}
|
|
3342
3416
|
function readStoredAuth(stateFile) {
|
|
3343
3417
|
if (!existsSync6(stateFile))
|
|
3344
3418
|
return {};
|
|
@@ -3352,6 +3426,7 @@ function readStoredAuth(stateFile) {
|
|
|
3352
3426
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
3353
3427
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
3354
3428
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
3429
|
+
pendingDevices: parsePendingDevices(parsed.pendingDevices),
|
|
3355
3430
|
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
3356
3431
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
3357
3432
|
};
|
|
@@ -3359,34 +3434,36 @@ function readStoredAuth(stateFile) {
|
|
|
3359
3434
|
return {};
|
|
3360
3435
|
}
|
|
3361
3436
|
}
|
|
3362
|
-
function parsePendingDevice(value) {
|
|
3363
|
-
if (!value || typeof value !== "object")
|
|
3364
|
-
return null;
|
|
3365
|
-
const record = value;
|
|
3366
|
-
const pollId = cleanString(record.pollId);
|
|
3367
|
-
const deviceCode = cleanString(record.deviceCode);
|
|
3368
|
-
const expiresAt = cleanString(record.expiresAt);
|
|
3369
|
-
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
3370
|
-
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
3371
|
-
return null;
|
|
3372
|
-
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
3373
|
-
}
|
|
3374
3437
|
function newApiSessionToken() {
|
|
3375
3438
|
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
3376
3439
|
}
|
|
3377
3440
|
function writeStoredAuth(stateFile, payload) {
|
|
3378
|
-
mkdirSync6(
|
|
3441
|
+
mkdirSync6(dirname8(stateFile), { recursive: true });
|
|
3379
3442
|
writeFileSync5(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
3380
3443
|
`, { encoding: "utf8", mode: 384 });
|
|
3381
3444
|
try {
|
|
3382
3445
|
chmodSync(stateFile, 384);
|
|
3383
3446
|
} catch {}
|
|
3384
3447
|
}
|
|
3448
|
+
function localProjectAuthStateFile(projectRoot) {
|
|
3449
|
+
return resolve13(projectRoot, ".rig", "state", "github-auth.json");
|
|
3450
|
+
}
|
|
3385
3451
|
function resolveGitHubAuthStateFile(projectRoot) {
|
|
3386
3452
|
return resolve13(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
3387
3453
|
}
|
|
3388
|
-
function
|
|
3389
|
-
const
|
|
3454
|
+
function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
|
|
3455
|
+
const targetFile = localProjectAuthStateFile(projectRoot);
|
|
3456
|
+
mkdirSync6(dirname8(targetFile), { recursive: true });
|
|
3457
|
+
if (existsSync6(stateFile)) {
|
|
3458
|
+
copyFileSync(stateFile, targetFile);
|
|
3459
|
+
try {
|
|
3460
|
+
chmodSync(targetFile, 384);
|
|
3461
|
+
} catch {}
|
|
3462
|
+
return;
|
|
3463
|
+
}
|
|
3464
|
+
writeStoredAuth(targetFile, {});
|
|
3465
|
+
}
|
|
3466
|
+
function createGitHubAuthStoreFromStateFile(stateFile) {
|
|
3390
3467
|
return {
|
|
3391
3468
|
stateFile,
|
|
3392
3469
|
status(options) {
|
|
@@ -3416,6 +3493,7 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
3416
3493
|
scopes: input.scopes ?? [],
|
|
3417
3494
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
3418
3495
|
pendingDevice: null,
|
|
3496
|
+
pendingDevices: [],
|
|
3419
3497
|
apiSessions: previous.apiSessions ?? [],
|
|
3420
3498
|
updatedAt: new Date().toISOString()
|
|
3421
3499
|
});
|
|
@@ -3444,15 +3522,24 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
3444
3522
|
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
3445
3523
|
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
3446
3524
|
},
|
|
3447
|
-
copyToProjectRoot(
|
|
3448
|
-
const targetFile = resolveGitHubAuthStateFile(
|
|
3525
|
+
copyToProjectRoot(projectRoot) {
|
|
3526
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot);
|
|
3449
3527
|
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
3450
3528
|
},
|
|
3529
|
+
copyToLocalProjectRoot(projectRoot) {
|
|
3530
|
+
copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
|
|
3531
|
+
},
|
|
3451
3532
|
savePendingDevice(input) {
|
|
3452
3533
|
const previous = readStoredAuth(stateFile);
|
|
3534
|
+
const pendingDevices = [
|
|
3535
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
3536
|
+
...previous.pendingDevices ?? [],
|
|
3537
|
+
input
|
|
3538
|
+
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
|
|
3453
3539
|
writeStoredAuth(stateFile, {
|
|
3454
3540
|
...previous,
|
|
3455
|
-
pendingDevice:
|
|
3541
|
+
pendingDevice: null,
|
|
3542
|
+
pendingDevices,
|
|
3456
3543
|
updatedAt: new Date().toISOString()
|
|
3457
3544
|
});
|
|
3458
3545
|
},
|
|
@@ -3465,23 +3552,32 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
3465
3552
|
});
|
|
3466
3553
|
},
|
|
3467
3554
|
readPendingDevice(pollId) {
|
|
3468
|
-
const
|
|
3469
|
-
|
|
3555
|
+
const previous = readStoredAuth(stateFile);
|
|
3556
|
+
const pending = [
|
|
3557
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
3558
|
+
...previous.pendingDevices ?? []
|
|
3559
|
+
].find((entry) => entry.pollId === pollId) ?? null;
|
|
3560
|
+
if (!pending)
|
|
3470
3561
|
return null;
|
|
3471
3562
|
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
3472
3563
|
return null;
|
|
3473
3564
|
return pending;
|
|
3474
3565
|
},
|
|
3475
|
-
clearPendingDevice() {
|
|
3566
|
+
clearPendingDevice(pollId) {
|
|
3476
3567
|
const previous = readStoredAuth(stateFile);
|
|
3568
|
+
const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
|
|
3477
3569
|
writeStoredAuth(stateFile, {
|
|
3478
3570
|
...previous,
|
|
3479
3571
|
pendingDevice: null,
|
|
3572
|
+
pendingDevices: remaining,
|
|
3480
3573
|
updatedAt: new Date().toISOString()
|
|
3481
3574
|
});
|
|
3482
3575
|
}
|
|
3483
3576
|
};
|
|
3484
3577
|
}
|
|
3578
|
+
function createGitHubAuthStore(projectRoot) {
|
|
3579
|
+
return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
|
|
3580
|
+
}
|
|
3485
3581
|
|
|
3486
3582
|
// packages/server/src/server-helpers/github-projects.ts
|
|
3487
3583
|
function asRecord(value) {
|
|
@@ -3490,6 +3586,9 @@ function asRecord(value) {
|
|
|
3490
3586
|
function asString(value) {
|
|
3491
3587
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
3492
3588
|
}
|
|
3589
|
+
function asNumber(value) {
|
|
3590
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
3591
|
+
}
|
|
3493
3592
|
async function defaultGraphQLFetch(query, variables, token) {
|
|
3494
3593
|
const response = await fetch("https://api.github.com/graphql", {
|
|
3495
3594
|
method: "POST",
|
|
@@ -3506,6 +3605,32 @@ async function defaultGraphQLFetch(query, variables, token) {
|
|
|
3506
3605
|
}
|
|
3507
3606
|
return json.data;
|
|
3508
3607
|
}
|
|
3608
|
+
function projectNodesFrom(data) {
|
|
3609
|
+
const root = asRecord(data);
|
|
3610
|
+
const owner = asRecord(root?.organization) ?? asRecord(root?.user);
|
|
3611
|
+
const projects = asRecord(owner?.projectsV2);
|
|
3612
|
+
const nodes = projects?.nodes;
|
|
3613
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
3614
|
+
}
|
|
3615
|
+
async function listGitHubProjects(input) {
|
|
3616
|
+
const query = `
|
|
3617
|
+
query RigListProjects($owner: String!, $first: Int!) {
|
|
3618
|
+
organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
3619
|
+
user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
3620
|
+
}
|
|
3621
|
+
`;
|
|
3622
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
3623
|
+
const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
|
|
3624
|
+
return projectNodesFrom(data).flatMap((node) => {
|
|
3625
|
+
const record = asRecord(node);
|
|
3626
|
+
const id = asString(record?.id);
|
|
3627
|
+
const number = asNumber(record?.number);
|
|
3628
|
+
const title = asString(record?.title);
|
|
3629
|
+
if (!id || number === undefined || !title)
|
|
3630
|
+
return [];
|
|
3631
|
+
return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
|
|
3632
|
+
});
|
|
3633
|
+
}
|
|
3509
3634
|
async function resolveProjectStatusField(input) {
|
|
3510
3635
|
const query = `
|
|
3511
3636
|
query RigProjectStatusField($projectId: ID!) {
|
|
@@ -3600,6 +3725,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
3600
3725
|
running: "In Progress",
|
|
3601
3726
|
prOpen: "In Review",
|
|
3602
3727
|
ciFixing: "In Review",
|
|
3728
|
+
merging: "Merging",
|
|
3603
3729
|
done: "Done",
|
|
3604
3730
|
needsAttention: "Needs Attention"
|
|
3605
3731
|
};
|
|
@@ -3613,6 +3739,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
3613
3739
|
return "prOpen";
|
|
3614
3740
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
3615
3741
|
return "ciFixing";
|
|
3742
|
+
if (normalized === "merging" || normalized === "merge")
|
|
3743
|
+
return "merging";
|
|
3616
3744
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
3617
3745
|
return "needsAttention";
|
|
3618
3746
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -3741,9 +3869,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
|
|
|
3741
3869
|
return null;
|
|
3742
3870
|
return null;
|
|
3743
3871
|
}
|
|
3872
|
+
function githubProjectsEnabled(config) {
|
|
3873
|
+
const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
|
|
3874
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
3875
|
+
return projects?.enabled === true;
|
|
3876
|
+
}
|
|
3744
3877
|
async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
|
|
3745
3878
|
if (!run.taskId)
|
|
3746
|
-
return;
|
|
3879
|
+
return false;
|
|
3747
3880
|
const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
|
|
3748
3881
|
try {
|
|
3749
3882
|
const result = await syncGitHubProjectStatusForTaskUpdate({
|
|
@@ -3754,28 +3887,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
|
|
|
3754
3887
|
config
|
|
3755
3888
|
});
|
|
3756
3889
|
if (!result.synced && result.reason !== "project-sync-disabled") {
|
|
3890
|
+
const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
|
|
3757
3891
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
3758
3892
|
id: `log:${run.runId}:github-project-sync:${status}`,
|
|
3759
3893
|
title: "GitHub Project sync skipped",
|
|
3760
|
-
detail
|
|
3894
|
+
detail,
|
|
3761
3895
|
tone: "warn",
|
|
3762
3896
|
status: "running",
|
|
3763
3897
|
createdAt: new Date().toISOString(),
|
|
3764
3898
|
payload: { reason: result.reason, issueNodeId }
|
|
3765
3899
|
});
|
|
3900
|
+
if (githubProjectsEnabled(config)) {
|
|
3901
|
+
throw new Error(detail);
|
|
3902
|
+
}
|
|
3903
|
+
return false;
|
|
3766
3904
|
}
|
|
3905
|
+
return result.synced === true;
|
|
3767
3906
|
} catch (error) {
|
|
3907
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3768
3908
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
3769
3909
|
id: `log:${run.runId}:github-project-sync-error:${status}`,
|
|
3770
3910
|
title: "GitHub Project sync failed",
|
|
3771
|
-
detail
|
|
3911
|
+
detail,
|
|
3772
3912
|
tone: "error",
|
|
3773
3913
|
status: "running",
|
|
3774
3914
|
createdAt: new Date().toISOString(),
|
|
3775
3915
|
payload: { issueNodeId }
|
|
3776
3916
|
});
|
|
3917
|
+
if (githubProjectsEnabled(config)) {
|
|
3918
|
+
throw new Error(detail);
|
|
3919
|
+
}
|
|
3920
|
+
return false;
|
|
3777
3921
|
}
|
|
3778
3922
|
}
|
|
3923
|
+
function createCommandRunner(binary, extraEnv = {}) {
|
|
3924
|
+
return async (args, options) => {
|
|
3925
|
+
const child = spawn3(binary, [...args], {
|
|
3926
|
+
cwd: options?.cwd,
|
|
3927
|
+
env: { ...process.env, ...extraEnv },
|
|
3928
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3929
|
+
});
|
|
3930
|
+
const stdoutChunks = [];
|
|
3931
|
+
const stderrChunks = [];
|
|
3932
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
3933
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
3934
|
+
const exitCode = await new Promise((resolve15) => {
|
|
3935
|
+
child.once("error", () => resolve15(1));
|
|
3936
|
+
child.once("close", (code) => resolve15(code ?? 1));
|
|
3937
|
+
});
|
|
3938
|
+
return {
|
|
3939
|
+
exitCode,
|
|
3940
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
3941
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8")
|
|
3942
|
+
};
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3945
|
+
function closeoutRecord(run) {
|
|
3946
|
+
const value = run.serverCloseout;
|
|
3947
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3948
|
+
}
|
|
3949
|
+
function closeoutPhasePatch(phase, status, extra = {}) {
|
|
3950
|
+
const updatedAt = new Date().toISOString();
|
|
3951
|
+
return {
|
|
3952
|
+
serverCloseout: {
|
|
3953
|
+
...extra,
|
|
3954
|
+
phase,
|
|
3955
|
+
status,
|
|
3956
|
+
updatedAt
|
|
3957
|
+
}
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3960
|
+
function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
|
|
3961
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
3962
|
+
id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
|
|
3963
|
+
title: `Server closeout: ${phase}`,
|
|
3964
|
+
detail,
|
|
3965
|
+
tone,
|
|
3966
|
+
status,
|
|
3967
|
+
createdAt: new Date().toISOString()
|
|
3968
|
+
}, `server-closeout-${phase}`);
|
|
3969
|
+
}
|
|
3779
3970
|
async function autoAssignRunIssue(projectRoot, run) {
|
|
3780
3971
|
if (!run.taskId)
|
|
3781
3972
|
return;
|
|
@@ -3805,7 +3996,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
3805
3996
|
return;
|
|
3806
3997
|
}
|
|
3807
3998
|
const config = await loadRigLifecycleConfig(projectRoot);
|
|
3808
|
-
await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
3999
|
+
const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
3809
4000
|
if (status === "in_progress") {
|
|
3810
4001
|
await autoAssignRunIssue(projectRoot, run);
|
|
3811
4002
|
}
|
|
@@ -3821,24 +4012,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
3821
4012
|
});
|
|
3822
4013
|
return;
|
|
3823
4014
|
}
|
|
3824
|
-
const
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
4015
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
4016
|
+
const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
|
|
4017
|
+
const rollbackProjectSync = async () => {
|
|
4018
|
+
if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
|
|
4019
|
+
return;
|
|
4020
|
+
await syncGitHubProjectStatusForTaskUpdate({
|
|
4021
|
+
taskId: run.taskId,
|
|
4022
|
+
status: previousStatus,
|
|
4023
|
+
issueNodeId: extractGitHubIssueNodeId(sourceTask),
|
|
4024
|
+
token: createGitHubAuthStore(projectRoot).readToken(),
|
|
4025
|
+
config
|
|
4026
|
+
}).catch((rollbackError) => {
|
|
4027
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
4028
|
+
id: `log:${run.runId}:github-project-sync-rollback:${status}`,
|
|
4029
|
+
title: "GitHub Project sync rollback failed",
|
|
4030
|
+
detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
|
|
4031
|
+
tone: "error",
|
|
4032
|
+
status: "running",
|
|
4033
|
+
createdAt: new Date().toISOString()
|
|
4034
|
+
});
|
|
4035
|
+
});
|
|
4036
|
+
};
|
|
4037
|
+
let result;
|
|
4038
|
+
try {
|
|
4039
|
+
result = await updateConfiguredTaskSourceTask(projectRoot, {
|
|
4040
|
+
taskId: run.taskId,
|
|
4041
|
+
sourceTask,
|
|
4042
|
+
update: {
|
|
3831
4043
|
status,
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
4044
|
+
comment: buildTaskRunLifecycleComment({
|
|
4045
|
+
runId: run.runId,
|
|
4046
|
+
status,
|
|
4047
|
+
summary,
|
|
4048
|
+
runtimeWorkspace: normalizeString(run.worktreePath),
|
|
4049
|
+
logsDir: normalizeString(run.logRoot),
|
|
4050
|
+
sessionDir: normalizeString(run.sessionPath),
|
|
4051
|
+
errorText: options.errorText ?? normalizeString(run.errorText)
|
|
4052
|
+
})
|
|
4053
|
+
}
|
|
4054
|
+
});
|
|
4055
|
+
} catch (error) {
|
|
4056
|
+
await rollbackProjectSync();
|
|
4057
|
+
throw error;
|
|
4058
|
+
}
|
|
3840
4059
|
if (!result.updated) {
|
|
3841
4060
|
if (result.source === "plugin" || result.sourceKind) {
|
|
4061
|
+
await rollbackProjectSync();
|
|
3842
4062
|
throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
|
|
3843
4063
|
}
|
|
3844
4064
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
@@ -3851,6 +4071,277 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
3851
4071
|
});
|
|
3852
4072
|
}
|
|
3853
4073
|
}
|
|
4074
|
+
async function markServerOwnedCloseoutFailed(state, runId, error) {
|
|
4075
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4076
|
+
const current = readAuthorityRun4(state.projectRoot, runId);
|
|
4077
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4078
|
+
status: "failed",
|
|
4079
|
+
completedAt: new Date().toISOString(),
|
|
4080
|
+
errorText: detail,
|
|
4081
|
+
...closeoutPhasePatch("failed", "failed", { error: detail })
|
|
4082
|
+
});
|
|
4083
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
4084
|
+
id: `log:${runId}:server-closeout-failed`,
|
|
4085
|
+
title: "Server-owned closeout failed",
|
|
4086
|
+
detail,
|
|
4087
|
+
tone: "error",
|
|
4088
|
+
status: "failed",
|
|
4089
|
+
createdAt: new Date().toISOString()
|
|
4090
|
+
}, "server-closeout-failed");
|
|
4091
|
+
if (current?.taskId) {
|
|
4092
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: detail }, "failed", "Rig server-owned closeout failed.", { errorText: detail }).catch((sourceError) => {
|
|
4093
|
+
appendRunLogEntry(state.projectRoot, runId, {
|
|
4094
|
+
id: `log:${runId}:task-source-closeout-failed-update`,
|
|
4095
|
+
title: "Task source closeout failure update failed",
|
|
4096
|
+
detail: sourceError instanceof Error ? sourceError.message : String(sourceError),
|
|
4097
|
+
tone: "error",
|
|
4098
|
+
status: "failed",
|
|
4099
|
+
createdAt: new Date().toISOString()
|
|
4100
|
+
});
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
function scheduleServerOwnedPrCloseout(state, runId, reason) {
|
|
4105
|
+
const startedAt = new Date().toISOString();
|
|
4106
|
+
state.runProcesses.set(runId, {
|
|
4107
|
+
runId,
|
|
4108
|
+
child: null,
|
|
4109
|
+
startedAt,
|
|
4110
|
+
stopped: false
|
|
4111
|
+
});
|
|
4112
|
+
queueMicrotask(() => {
|
|
4113
|
+
withServerAuthorityEnvIfNeeded(state.projectRoot, async () => {
|
|
4114
|
+
try {
|
|
4115
|
+
await runServerOwnedPrCloseout(state, runId);
|
|
4116
|
+
} catch (error) {
|
|
4117
|
+
await markServerOwnedCloseoutFailed(state, runId, error);
|
|
4118
|
+
} finally {
|
|
4119
|
+
state.runProcesses.delete(runId);
|
|
4120
|
+
broadcastSnapshotInvalidation(state, `server-closeout-${reason}-terminal`);
|
|
4121
|
+
await reconcileScheduler(state, `server-closeout-${reason}-terminal`);
|
|
4122
|
+
}
|
|
4123
|
+
});
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
async function runServerOwnedPrCloseout(state, runId) {
|
|
4127
|
+
const run = readAuthorityRun4(state.projectRoot, runId);
|
|
4128
|
+
if (!run)
|
|
4129
|
+
throw new Error(`Run not found: ${runId}`);
|
|
4130
|
+
const closeout = closeoutRecord(run);
|
|
4131
|
+
if (!closeout)
|
|
4132
|
+
return;
|
|
4133
|
+
const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
|
|
4134
|
+
if (!taskId)
|
|
4135
|
+
throw new Error("Server-owned closeout requires a task id.");
|
|
4136
|
+
const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
|
|
4137
|
+
let branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
|
|
4138
|
+
const config = await loadRigLifecycleConfig(state.projectRoot);
|
|
4139
|
+
const runPrMode = normalizeString(run.prMode);
|
|
4140
|
+
const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
|
|
4141
|
+
const effectiveConfig = {
|
|
4142
|
+
...config ?? {},
|
|
4143
|
+
pr: {
|
|
4144
|
+
...config?.pr ?? {},
|
|
4145
|
+
mode: prMode,
|
|
4146
|
+
autoFixChecks: false,
|
|
4147
|
+
autoFixReview: false
|
|
4148
|
+
}
|
|
4149
|
+
};
|
|
4150
|
+
const readCurrentRun = () => readAuthorityRun4(state.projectRoot, runId) ?? run;
|
|
4151
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
4152
|
+
const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
|
|
4153
|
+
const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
|
|
4154
|
+
const closeoutPrUrl = normalizeString(closeout.prUrl);
|
|
4155
|
+
if (closeoutPhase === "completed" || closeoutStatus === "completed") {
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
if (closeoutPhase === "close-source" && closeoutPrUrl) {
|
|
4159
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4160
|
+
status: "reviewing",
|
|
4161
|
+
...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
|
|
4162
|
+
});
|
|
4163
|
+
await closeIssueAfterMergedPr({
|
|
4164
|
+
projectRoot: state.projectRoot,
|
|
4165
|
+
taskId,
|
|
4166
|
+
runId,
|
|
4167
|
+
prUrl: closeoutPrUrl,
|
|
4168
|
+
sourceTask,
|
|
4169
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
4170
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
4171
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
4172
|
+
}
|
|
4173
|
+
});
|
|
4174
|
+
const completedAt = new Date().toISOString();
|
|
4175
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4176
|
+
status: "completed",
|
|
4177
|
+
completedAt,
|
|
4178
|
+
errorText: null,
|
|
4179
|
+
...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
|
|
4180
|
+
});
|
|
4181
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
|
|
4182
|
+
emitRigEvent(state, {
|
|
4183
|
+
type: "rig.run.completed",
|
|
4184
|
+
aggregateId: runId,
|
|
4185
|
+
payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
|
|
4186
|
+
createdAt: completedAt
|
|
4187
|
+
});
|
|
4188
|
+
return;
|
|
4189
|
+
}
|
|
4190
|
+
if (prMode === "off" || prMode === "ask") {
|
|
4191
|
+
const completedAt = new Date().toISOString();
|
|
4192
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4193
|
+
status: "completed",
|
|
4194
|
+
completedAt,
|
|
4195
|
+
errorText: null,
|
|
4196
|
+
...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
|
|
4197
|
+
});
|
|
4198
|
+
appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
|
|
4199
|
+
emitRigEvent(state, {
|
|
4200
|
+
type: "rig.run.completed",
|
|
4201
|
+
aggregateId: runId,
|
|
4202
|
+
payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
|
|
4203
|
+
createdAt: completedAt
|
|
4204
|
+
});
|
|
4205
|
+
return;
|
|
4206
|
+
}
|
|
4207
|
+
const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
|
|
4208
|
+
const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
|
|
4209
|
+
const gitCommand = createCommandRunner("git", githubEnv);
|
|
4210
|
+
const ghCommand = createCommandRunner("gh", githubEnv);
|
|
4211
|
+
const workspaceBranch = await gitCommand(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: workspace });
|
|
4212
|
+
const currentWorkspaceBranch = workspaceBranch.exitCode === 0 ? normalizeString(workspaceBranch.stdout) : null;
|
|
4213
|
+
if (currentWorkspaceBranch && currentWorkspaceBranch !== "HEAD" && currentWorkspaceBranch !== branch) {
|
|
4214
|
+
appendCloseoutStage(state, runId, "branch", `Using runtime workspace branch ${currentWorkspaceBranch} instead of recorded branch ${branch}.`, "reviewing", "info");
|
|
4215
|
+
branch = currentWorkspaceBranch;
|
|
4216
|
+
}
|
|
4217
|
+
const setCloseout = (phase, status, extra = {}) => {
|
|
4218
|
+
const previous = closeoutRecord(readCurrentRun()) ?? closeout;
|
|
4219
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4220
|
+
status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
|
|
4221
|
+
...closeoutPhasePatch(phase, status, { ...previous, ...extra })
|
|
4222
|
+
});
|
|
4223
|
+
};
|
|
4224
|
+
setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
4225
|
+
appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
|
|
4226
|
+
const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
|
|
4227
|
+
appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
|
|
4228
|
+
setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
4229
|
+
const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
|
|
4230
|
+
if (push.exitCode !== 0) {
|
|
4231
|
+
throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
|
|
4232
|
+
}
|
|
4233
|
+
const sourceTaskForPr = {
|
|
4234
|
+
title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
|
|
4235
|
+
};
|
|
4236
|
+
const artifactRoot = resolve14(state.projectRoot, "artifacts", taskId);
|
|
4237
|
+
setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4238
|
+
const pr = await runPrAutomation({
|
|
4239
|
+
projectRoot: workspace,
|
|
4240
|
+
taskId,
|
|
4241
|
+
runId,
|
|
4242
|
+
branch,
|
|
4243
|
+
config: effectiveConfig,
|
|
4244
|
+
sourceTask: sourceTaskForPr,
|
|
4245
|
+
artifactRoot,
|
|
4246
|
+
command: ghCommand,
|
|
4247
|
+
gitCommand,
|
|
4248
|
+
steerPi: async (message) => {
|
|
4249
|
+
appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
|
|
4250
|
+
appendRunTimelineEntry(state.projectRoot, runId, {
|
|
4251
|
+
id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
|
|
4252
|
+
type: "user_message",
|
|
4253
|
+
text: message,
|
|
4254
|
+
createdAt: new Date().toISOString(),
|
|
4255
|
+
state: "completed"
|
|
4256
|
+
});
|
|
4257
|
+
},
|
|
4258
|
+
lifecycle: {
|
|
4259
|
+
onPrOpened: async ({ prUrl }) => {
|
|
4260
|
+
setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4261
|
+
appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
|
|
4262
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
|
|
4263
|
+
},
|
|
4264
|
+
onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
|
|
4265
|
+
onFeedback: async ({ feedback }) => {
|
|
4266
|
+
appendCloseoutStage(state, runId, "feedback", feedback.join(`
|
|
4267
|
+
`), "reviewing", "error");
|
|
4268
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
|
|
4269
|
+
},
|
|
4270
|
+
onMergeStarted: async ({ prUrl }) => {
|
|
4271
|
+
setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4272
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
4273
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
|
|
4274
|
+
},
|
|
4275
|
+
onMerged: ({ prUrl }) => {
|
|
4276
|
+
setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
|
|
4277
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
});
|
|
4281
|
+
if (pr.status === "merged" && pr.prUrl) {
|
|
4282
|
+
setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
|
|
4283
|
+
await closeIssueAfterMergedPr({
|
|
4284
|
+
projectRoot: state.projectRoot,
|
|
4285
|
+
taskId,
|
|
4286
|
+
runId,
|
|
4287
|
+
prUrl: pr.prUrl,
|
|
4288
|
+
sourceTask,
|
|
4289
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
4290
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
4291
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
4292
|
+
}
|
|
4293
|
+
});
|
|
4294
|
+
const completedAt = new Date().toISOString();
|
|
4295
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4296
|
+
status: "completed",
|
|
4297
|
+
completedAt,
|
|
4298
|
+
errorText: null,
|
|
4299
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
|
|
4300
|
+
});
|
|
4301
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
|
|
4302
|
+
emitRigEvent(state, {
|
|
4303
|
+
type: "rig.run.completed",
|
|
4304
|
+
aggregateId: runId,
|
|
4305
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
|
|
4306
|
+
createdAt: completedAt
|
|
4307
|
+
});
|
|
4308
|
+
return;
|
|
4309
|
+
}
|
|
4310
|
+
if (pr.status === "opened" && pr.prUrl) {
|
|
4311
|
+
const completedAt = new Date().toISOString();
|
|
4312
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4313
|
+
status: "completed",
|
|
4314
|
+
completedAt,
|
|
4315
|
+
errorText: null,
|
|
4316
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
|
|
4317
|
+
});
|
|
4318
|
+
appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
|
|
4319
|
+
emitRigEvent(state, {
|
|
4320
|
+
type: "rig.run.completed",
|
|
4321
|
+
aggregateId: runId,
|
|
4322
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
|
|
4323
|
+
createdAt: completedAt
|
|
4324
|
+
});
|
|
4325
|
+
return;
|
|
4326
|
+
}
|
|
4327
|
+
const detail = pr.actionableFeedback.join(`
|
|
4328
|
+
`) || "PR automation did not merge the PR.";
|
|
4329
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
|
|
4330
|
+
appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
|
|
4331
|
+
});
|
|
4332
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4333
|
+
status: "needs_attention",
|
|
4334
|
+
completedAt: new Date().toISOString(),
|
|
4335
|
+
errorText: detail,
|
|
4336
|
+
...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
|
|
4337
|
+
});
|
|
4338
|
+
appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
|
|
4339
|
+
emitRigEvent(state, {
|
|
4340
|
+
type: "rig.run.needs-attention",
|
|
4341
|
+
aggregateId: runId,
|
|
4342
|
+
payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
|
|
4343
|
+
});
|
|
4344
|
+
}
|
|
3854
4345
|
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
3855
4346
|
"completed",
|
|
3856
4347
|
"complete",
|
|
@@ -3876,11 +4367,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
|
|
|
3876
4367
|
return;
|
|
3877
4368
|
throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
|
|
3878
4369
|
}
|
|
4370
|
+
async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
|
|
4371
|
+
const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
4372
|
+
if (fromReader)
|
|
4373
|
+
return fromReader;
|
|
4374
|
+
const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
|
|
4375
|
+
if (projected)
|
|
4376
|
+
return projected;
|
|
4377
|
+
if (readTasks !== readWorkspaceTasks) {
|
|
4378
|
+
return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
4379
|
+
}
|
|
4380
|
+
return null;
|
|
4381
|
+
}
|
|
3879
4382
|
async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
|
|
3880
4383
|
if ("taskId" in input && input.taskId) {
|
|
3881
4384
|
assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
|
|
3882
4385
|
}
|
|
3883
|
-
const sourceTask = "taskId" in input && input.taskId ?
|
|
4386
|
+
const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
|
|
3884
4387
|
const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
|
|
3885
4388
|
const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
|
|
3886
4389
|
const runRecord = {
|
|
@@ -3952,6 +4455,7 @@ async function startLocalRun(state, runId, options) {
|
|
|
3952
4455
|
throw new Error(`Run not found: ${runId}`);
|
|
3953
4456
|
}
|
|
3954
4457
|
const startedAt = new Date().toISOString();
|
|
4458
|
+
const resumeMode = options?.resume === true;
|
|
3955
4459
|
state.runProcesses.set(runId, {
|
|
3956
4460
|
runId,
|
|
3957
4461
|
child: null,
|
|
@@ -3968,9 +4472,9 @@ async function startLocalRun(state, runId, options) {
|
|
|
3968
4472
|
summary: run.title
|
|
3969
4473
|
});
|
|
3970
4474
|
appendRunLogEntry(state.projectRoot, runId, {
|
|
3971
|
-
id: `log:${runId}:prepare`,
|
|
3972
|
-
title: "Rig task run starting",
|
|
3973
|
-
detail: run.taskId ?? run.title,
|
|
4475
|
+
id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
|
|
4476
|
+
title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
|
|
4477
|
+
detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
|
|
3974
4478
|
tone: "info",
|
|
3975
4479
|
status: "preparing",
|
|
3976
4480
|
createdAt: startedAt
|
|
@@ -4046,12 +4550,17 @@ async function startLocalRun(state, runId, options) {
|
|
|
4046
4550
|
RIG_HOST_PROJECT_ROOT: cliProjectRoot,
|
|
4047
4551
|
RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
|
|
4048
4552
|
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
4553
|
+
RIG_SERVER_OWNS_CLOSEOUT: "1",
|
|
4049
4554
|
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
4050
4555
|
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
4051
4556
|
...bridgeGitHubToken ? {
|
|
4052
4557
|
RIG_GITHUB_TOKEN: bridgeGitHubToken,
|
|
4053
4558
|
GITHUB_TOKEN: bridgeGitHubToken,
|
|
4054
4559
|
GH_TOKEN: bridgeGitHubToken
|
|
4560
|
+
} : {},
|
|
4561
|
+
...resumeMode ? {
|
|
4562
|
+
RIG_RUN_RESUME: "1",
|
|
4563
|
+
RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
|
|
4055
4564
|
} : {}
|
|
4056
4565
|
},
|
|
4057
4566
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -4075,6 +4584,25 @@ async function startLocalRun(state, runId, options) {
|
|
|
4075
4584
|
broadcastSnapshotInvalidation(state);
|
|
4076
4585
|
continue;
|
|
4077
4586
|
}
|
|
4587
|
+
if (line.startsWith("__RIG_WRAPPER_EVENT__")) {
|
|
4588
|
+
try {
|
|
4589
|
+
const wrapperEvent = JSON.parse(line.slice("__RIG_WRAPPER_EVENT__".length));
|
|
4590
|
+
const eventType = normalizeString(wrapperEvent.type);
|
|
4591
|
+
const payload = wrapperEvent.payload && typeof wrapperEvent.payload === "object" && !Array.isArray(wrapperEvent.payload) ? wrapperEvent.payload : {};
|
|
4592
|
+
if (eventType === "pi.session.ready" && payload.privateMetadata && typeof payload.privateMetadata === "object" && !Array.isArray(payload.privateMetadata)) {
|
|
4593
|
+
patchRunPiSessionMetadata(state.projectRoot, runId, payload.privateMetadata);
|
|
4594
|
+
}
|
|
4595
|
+
appendRunTimelineEntry(state.projectRoot, runId, {
|
|
4596
|
+
id: `timeline:${runId}:${Date.now()}:wrapper:${eventType ?? "event"}`,
|
|
4597
|
+
type: "wrapper-event",
|
|
4598
|
+
eventType,
|
|
4599
|
+
payload,
|
|
4600
|
+
createdAt: normalizeString(wrapperEvent.at) ?? new Date().toISOString()
|
|
4601
|
+
});
|
|
4602
|
+
} catch {}
|
|
4603
|
+
broadcastSnapshotInvalidation(state, "wrapper-event");
|
|
4604
|
+
continue;
|
|
4605
|
+
}
|
|
4078
4606
|
appendRunLogEntryAndBroadcast(state, runId, {
|
|
4079
4607
|
id: `log:${runId}:${Date.now()}`,
|
|
4080
4608
|
title,
|
|
@@ -4103,7 +4631,13 @@ async function startLocalRun(state, runId, options) {
|
|
|
4103
4631
|
if (!current) {
|
|
4104
4632
|
return;
|
|
4105
4633
|
}
|
|
4106
|
-
if (
|
|
4634
|
+
if (closeoutRecord(current)?.status === "pending") {
|
|
4635
|
+
try {
|
|
4636
|
+
await runServerOwnedPrCloseout(state, runId);
|
|
4637
|
+
} catch (closeoutError) {
|
|
4638
|
+
await markServerOwnedCloseoutFailed(state, runId, closeoutError);
|
|
4639
|
+
}
|
|
4640
|
+
} else if (exit.code !== 0 && current.status !== "completed" && current.status !== "stopped") {
|
|
4107
4641
|
const completedAt = current.completedAt ?? new Date().toISOString();
|
|
4108
4642
|
const failureSummary = normalizeString(current.errorText) ?? summarizeRunValidationFailure(state.projectRoot, current) ?? `Rig task-run exited with code ${String(exit.code ?? "unknown")}`;
|
|
4109
4643
|
if (current.status !== "failed") {
|
|
@@ -4203,7 +4737,7 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
|
|
|
4203
4737
|
}
|
|
4204
4738
|
try {
|
|
4205
4739
|
const monorepoRoot = resolveMonorepoRoot3(projectRoot);
|
|
4206
|
-
const outerProjectRoot =
|
|
4740
|
+
const outerProjectRoot = dirname9(dirname9(monorepoRoot));
|
|
4207
4741
|
if (existsSync7(resolve14(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
|
|
4208
4742
|
return outerProjectRoot;
|
|
4209
4743
|
}
|
|
@@ -4224,7 +4758,19 @@ async function resumeRunRecord(state, input) {
|
|
|
4224
4758
|
if (run.status === "completed") {
|
|
4225
4759
|
throw new Error("Completed runs cannot be resumed.");
|
|
4226
4760
|
}
|
|
4227
|
-
|
|
4761
|
+
const closeout = closeoutRecord(run);
|
|
4762
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
4763
|
+
if (EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
|
|
4764
|
+
patchRunRecord(state.projectRoot, input.runId, {
|
|
4765
|
+
status: "reviewing",
|
|
4766
|
+
completedAt: null,
|
|
4767
|
+
errorText: null,
|
|
4768
|
+
...closeoutPhasePatch("queued", "pending", { ...closeout, resumedAt: input.createdAt })
|
|
4769
|
+
});
|
|
4770
|
+
scheduleServerOwnedPrCloseout(state, input.runId, "explicit-resume");
|
|
4771
|
+
return;
|
|
4772
|
+
}
|
|
4773
|
+
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
|
|
4228
4774
|
}
|
|
4229
4775
|
function appendRunMessage(projectRoot, input) {
|
|
4230
4776
|
const run = readAuthorityRun4(projectRoot, input.runId);
|
|
@@ -4308,34 +4854,63 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
|
|
|
4308
4854
|
writeQueueState(projectRoot, next);
|
|
4309
4855
|
return next;
|
|
4310
4856
|
}
|
|
4311
|
-
var
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4857
|
+
var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
|
|
4858
|
+
var EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running", "needs_attention"]);
|
|
4859
|
+
var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
4860
|
+
function processExists(pid) {
|
|
4861
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
4862
|
+
return false;
|
|
4863
|
+
try {
|
|
4864
|
+
process.kill(pid, 0);
|
|
4865
|
+
return true;
|
|
4866
|
+
} catch {
|
|
4867
|
+
return false;
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
4870
|
+
function recoverStaleLocalRun(projectRoot, run) {
|
|
4871
|
+
const record = run;
|
|
4872
|
+
if (run.mode !== "local")
|
|
4873
|
+
return false;
|
|
4874
|
+
const closeout = closeoutRecord(record);
|
|
4875
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
4876
|
+
const status = normalizeString(record.status)?.toLowerCase() ?? "";
|
|
4877
|
+
if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus))
|
|
4878
|
+
return false;
|
|
4879
|
+
if (closeoutStatus === "needs_attention") {
|
|
4880
|
+
if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
|
|
4881
|
+
return false;
|
|
4882
|
+
const completedAt2 = record.completedAt ?? new Date().toISOString();
|
|
4883
|
+
patchRunRecord(projectRoot, run.runId, {
|
|
4884
|
+
status: "needs_attention",
|
|
4885
|
+
completedAt: completedAt2,
|
|
4886
|
+
errorText: normalizeString(record.errorText) ?? (Array.isArray(closeout?.feedback) ? closeout.feedback.map(String).join(`
|
|
4887
|
+
`) : null)
|
|
4335
4888
|
});
|
|
4336
|
-
|
|
4889
|
+
return true;
|
|
4337
4890
|
}
|
|
4338
|
-
|
|
4891
|
+
if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
|
|
4892
|
+
return false;
|
|
4893
|
+
const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
|
|
4894
|
+
const childPid = typeof record.pid === "number" ? record.pid : null;
|
|
4895
|
+
if (serverPid === null && childPid === null)
|
|
4896
|
+
return false;
|
|
4897
|
+
const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
|
|
4898
|
+
if (hasLiveRecordedProcess && serverPid === process.pid)
|
|
4899
|
+
return false;
|
|
4900
|
+
const completedAt = new Date().toISOString();
|
|
4901
|
+
patchRunRecord(projectRoot, run.runId, {
|
|
4902
|
+
status: "failed",
|
|
4903
|
+
completedAt,
|
|
4904
|
+
errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
|
|
4905
|
+
});
|
|
4906
|
+
return true;
|
|
4907
|
+
}
|
|
4908
|
+
function collectResumableServerCloseouts(state, runs) {
|
|
4909
|
+
return runs.filter((run) => {
|
|
4910
|
+
const closeout = closeoutRecord(run);
|
|
4911
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
4912
|
+
return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
|
|
4913
|
+
});
|
|
4339
4914
|
}
|
|
4340
4915
|
async function reconcileScheduler(state, reason) {
|
|
4341
4916
|
if (state.scheduler.reconciling) {
|
|
@@ -4350,7 +4925,28 @@ async function reconcileScheduler(state, reason) {
|
|
|
4350
4925
|
const queue = readQueueState(state.projectRoot);
|
|
4351
4926
|
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
4352
4927
|
let runs = listAuthorityRuns4(state.projectRoot);
|
|
4353
|
-
let changed =
|
|
4928
|
+
let changed = false;
|
|
4929
|
+
for (const run of runs) {
|
|
4930
|
+
if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
|
|
4931
|
+
changed = true;
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
if (changed) {
|
|
4935
|
+
runs = listAuthorityRuns4(state.projectRoot);
|
|
4936
|
+
}
|
|
4937
|
+
const resumableCloseouts = collectResumableServerCloseouts(state, runs);
|
|
4938
|
+
for (const run of resumableCloseouts) {
|
|
4939
|
+
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
4940
|
+
id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
|
|
4941
|
+
title: "Server-owned closeout auto-resume scheduled",
|
|
4942
|
+
detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
|
|
4943
|
+
tone: "info",
|
|
4944
|
+
status: "reviewing",
|
|
4945
|
+
createdAt: new Date().toISOString()
|
|
4946
|
+
});
|
|
4947
|
+
scheduleServerOwnedPrCloseout(state, run.runId, "auto-resume");
|
|
4948
|
+
changed = true;
|
|
4949
|
+
}
|
|
4354
4950
|
if (changed) {
|
|
4355
4951
|
runs = listAuthorityRuns4(state.projectRoot);
|
|
4356
4952
|
}
|
|
@@ -4426,11 +5022,11 @@ async function reconcileScheduler(state, reason) {
|
|
|
4426
5022
|
// packages/server/src/server-helpers/http-router.ts
|
|
4427
5023
|
import { randomUUID } from "crypto";
|
|
4428
5024
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
4429
|
-
import { basename, dirname as
|
|
4430
|
-
import { copyFileSync, existsSync as
|
|
5025
|
+
import { basename, dirname as dirname15, isAbsolute as isAbsolute4, resolve as resolve20 } from "path";
|
|
5026
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync12 } from "fs";
|
|
4431
5027
|
import {
|
|
4432
5028
|
listAuthorityRuns as listAuthorityRuns5,
|
|
4433
|
-
readAuthorityRun as
|
|
5029
|
+
readAuthorityRun as readAuthorityRun7,
|
|
4434
5030
|
resolveAuthorityPaths,
|
|
4435
5031
|
writeJsonFile as writeJsonFile4
|
|
4436
5032
|
} from "@rig/runtime/control-plane/authority-files";
|
|
@@ -4450,10 +5046,64 @@ import {
|
|
|
4450
5046
|
RemoteWsClient
|
|
4451
5047
|
} from "@rig/runtime/control-plane/remote";
|
|
4452
5048
|
|
|
5049
|
+
// packages/server/src/server-helpers/pi-session-proxy.ts
|
|
5050
|
+
import { readAuthorityRun as readAuthorityRun5 } from "@rig/runtime/control-plane/authority-files";
|
|
5051
|
+
function resolveRunPiSessionProxy(projectRoot, runId) {
|
|
5052
|
+
const run = readAuthorityRun5(projectRoot, runId);
|
|
5053
|
+
if (!run)
|
|
5054
|
+
return null;
|
|
5055
|
+
const privateMetadata = readRunPiSessionMetadata(projectRoot, runId);
|
|
5056
|
+
if (!privateMetadata)
|
|
5057
|
+
return { pending: true, runId, status: typeof run.status === "string" ? run.status : undefined };
|
|
5058
|
+
const connection = privateMetadata.daemonConnection;
|
|
5059
|
+
if (connection.mode !== "http")
|
|
5060
|
+
throw new Error("Only loopback HTTP Rig Pi session daemon connections are supported");
|
|
5061
|
+
const token = tokenFromRef(connection.tokenRef);
|
|
5062
|
+
if (!token)
|
|
5063
|
+
throw new Error("Rig Pi session daemon token is unavailable");
|
|
5064
|
+
return {
|
|
5065
|
+
runId,
|
|
5066
|
+
sessionId: privateMetadata.public.sessionId,
|
|
5067
|
+
metadata: privateMetadata.public,
|
|
5068
|
+
privateMetadata,
|
|
5069
|
+
baseUrl: connection.baseUrl.replace(/\/+$/, ""),
|
|
5070
|
+
token
|
|
5071
|
+
};
|
|
5072
|
+
}
|
|
5073
|
+
async function proxyRunPiHttp(projectRoot, runId, input) {
|
|
5074
|
+
const resolved = resolveRunPiSessionProxy(projectRoot, runId);
|
|
5075
|
+
if (resolved === null)
|
|
5076
|
+
return { status: 404, payload: { ok: false, error: "Run not found" } };
|
|
5077
|
+
if ("pending" in resolved)
|
|
5078
|
+
return { status: 409, payload: { ready: false, runId, status: resolved.status, retryAfterMs: 500 } };
|
|
5079
|
+
const response = await fetch(`${resolved.baseUrl}${input.daemonPath}`, {
|
|
5080
|
+
method: input.method,
|
|
5081
|
+
headers: {
|
|
5082
|
+
authorization: `Bearer ${resolved.token}`,
|
|
5083
|
+
...input.body === undefined ? {} : { "content-type": "application/json" }
|
|
5084
|
+
},
|
|
5085
|
+
body: input.body === undefined ? undefined : JSON.stringify(input.body)
|
|
5086
|
+
});
|
|
5087
|
+
const text = await response.text();
|
|
5088
|
+
const payload = text.trim() ? JSON.parse(text) : null;
|
|
5089
|
+
return { status: response.status, payload };
|
|
5090
|
+
}
|
|
5091
|
+
function buildRunPiDaemonWebSocketUrl(resolved) {
|
|
5092
|
+
const url = new URL(`${resolved.baseUrl}/sessions/${encodeURIComponent(resolved.sessionId)}/events`);
|
|
5093
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
5094
|
+
url.searchParams.set("token", resolved.token);
|
|
5095
|
+
return url.toString();
|
|
5096
|
+
}
|
|
5097
|
+
function tokenFromRef(ref) {
|
|
5098
|
+
if (ref.startsWith("inline:"))
|
|
5099
|
+
return ref.slice("inline:".length) || null;
|
|
5100
|
+
return null;
|
|
5101
|
+
}
|
|
5102
|
+
|
|
4453
5103
|
// packages/server/src/server-helpers/run-steering.ts
|
|
4454
|
-
import { dirname as
|
|
5104
|
+
import { dirname as dirname10, resolve as resolve15 } from "path";
|
|
4455
5105
|
import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync5 } from "fs";
|
|
4456
|
-
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as
|
|
5106
|
+
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun6, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
|
|
4457
5107
|
var steeringSequence = 0;
|
|
4458
5108
|
function runSteeringPath(projectRoot, runId) {
|
|
4459
5109
|
return resolve15(resolveAuthorityRunDir4(projectRoot, runId), "steering.jsonl");
|
|
@@ -4522,7 +5172,7 @@ function markQueuedRunSteeringMessagesDelivered(projectRoot, runId, ids) {
|
|
|
4522
5172
|
return delivered;
|
|
4523
5173
|
}
|
|
4524
5174
|
function queueRunSteeringMessage(projectRoot, runId, input) {
|
|
4525
|
-
const run =
|
|
5175
|
+
const run = readAuthorityRun6(projectRoot, runId);
|
|
4526
5176
|
if (!run)
|
|
4527
5177
|
throw new Error(`Run not found: ${runId}`);
|
|
4528
5178
|
const text = input.message.trim();
|
|
@@ -4538,7 +5188,7 @@ function queueRunSteeringMessage(projectRoot, runId, input) {
|
|
|
4538
5188
|
delivered: false
|
|
4539
5189
|
};
|
|
4540
5190
|
const path = runSteeringPath(projectRoot, runId);
|
|
4541
|
-
mkdirSync8(
|
|
5191
|
+
mkdirSync8(dirname10(path), { recursive: true });
|
|
4542
5192
|
appendJsonlRecord2(path, entry);
|
|
4543
5193
|
appendRunTimelineEntry(projectRoot, runId, {
|
|
4544
5194
|
id: entry.id,
|
|
@@ -4575,6 +5225,187 @@ import {
|
|
|
4575
5225
|
updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
|
|
4576
5226
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
4577
5227
|
|
|
5228
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
5229
|
+
import { chmodSync as chmodSync3, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
5230
|
+
import { dirname as dirname12, resolve as resolve17 } from "path";
|
|
5231
|
+
|
|
5232
|
+
// packages/server/src/server-helpers/github-user-namespace.ts
|
|
5233
|
+
import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
5234
|
+
import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve16 } from "path";
|
|
5235
|
+
function cleanString3(value) {
|
|
5236
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
5237
|
+
}
|
|
5238
|
+
function sanitizePathSegment(value) {
|
|
5239
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
|
|
5240
|
+
}
|
|
5241
|
+
function deriveGitHubUserNamespaceKey(identity) {
|
|
5242
|
+
const userId = cleanString3(identity.userId);
|
|
5243
|
+
if (userId) {
|
|
5244
|
+
const safeId = sanitizePathSegment(userId);
|
|
5245
|
+
if (safeId)
|
|
5246
|
+
return `ghu-${safeId}`;
|
|
5247
|
+
}
|
|
5248
|
+
const login = cleanString3(identity.login);
|
|
5249
|
+
if (login) {
|
|
5250
|
+
const safeLogin = sanitizePathSegment(login);
|
|
5251
|
+
if (safeLogin)
|
|
5252
|
+
return `ghu-login-${safeLogin}`;
|
|
5253
|
+
}
|
|
5254
|
+
throw new Error("GitHub user namespace requires a user id or login");
|
|
5255
|
+
}
|
|
5256
|
+
function resolveRemoteUserNamespacesRoot(projectRoot) {
|
|
5257
|
+
const explicitRoot = cleanString3(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
|
|
5258
|
+
if (explicitRoot)
|
|
5259
|
+
return resolve16(explicitRoot);
|
|
5260
|
+
const stateDir2 = cleanString3(process.env.RIG_STATE_DIR);
|
|
5261
|
+
if (stateDir2)
|
|
5262
|
+
return resolve16(dirname11(resolve16(stateDir2)), "users");
|
|
5263
|
+
return resolve16(projectRoot, ".rig", "users");
|
|
5264
|
+
}
|
|
5265
|
+
function resolveRemoteUserNamespace(projectRoot, identity) {
|
|
5266
|
+
const key = deriveGitHubUserNamespaceKey(identity);
|
|
5267
|
+
const root = resolve16(resolveRemoteUserNamespacesRoot(projectRoot), key);
|
|
5268
|
+
const stateDir2 = resolve16(root, ".rig", "state");
|
|
5269
|
+
return {
|
|
5270
|
+
key,
|
|
5271
|
+
userId: cleanString3(identity.userId),
|
|
5272
|
+
login: cleanString3(identity.login),
|
|
5273
|
+
root,
|
|
5274
|
+
stateDir: stateDir2,
|
|
5275
|
+
authStateFile: resolve16(stateDir2, "github-auth.json"),
|
|
5276
|
+
metadataFile: resolve16(stateDir2, "user-namespace.json"),
|
|
5277
|
+
checkoutBaseDir: resolve16(root, "remote-checkouts"),
|
|
5278
|
+
snapshotBaseDir: resolve16(root, "remote-snapshots")
|
|
5279
|
+
};
|
|
5280
|
+
}
|
|
5281
|
+
function serializeRemoteUserNamespace(namespace) {
|
|
5282
|
+
return {
|
|
5283
|
+
key: namespace.key,
|
|
5284
|
+
userId: namespace.userId,
|
|
5285
|
+
login: namespace.login,
|
|
5286
|
+
root: namespace.root,
|
|
5287
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
5288
|
+
snapshotBaseDir: namespace.snapshotBaseDir
|
|
5289
|
+
};
|
|
5290
|
+
}
|
|
5291
|
+
function isPathInsideNamespace(namespaceRoot, candidatePath) {
|
|
5292
|
+
const root = resolve16(namespaceRoot);
|
|
5293
|
+
const candidate = resolve16(candidatePath);
|
|
5294
|
+
const rel = relative3(root, candidate);
|
|
5295
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
5296
|
+
}
|
|
5297
|
+
function writeRemoteUserNamespaceMetadata(namespace) {
|
|
5298
|
+
mkdirSync9(namespace.stateDir, { recursive: true });
|
|
5299
|
+
const previous = (() => {
|
|
5300
|
+
if (!existsSync9(namespace.metadataFile))
|
|
5301
|
+
return null;
|
|
5302
|
+
try {
|
|
5303
|
+
const parsed = JSON.parse(readFileSync6(namespace.metadataFile, "utf8"));
|
|
5304
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
5305
|
+
} catch {
|
|
5306
|
+
return null;
|
|
5307
|
+
}
|
|
5308
|
+
})();
|
|
5309
|
+
const now = new Date().toISOString();
|
|
5310
|
+
writeFileSync8(namespace.metadataFile, `${JSON.stringify({
|
|
5311
|
+
key: namespace.key,
|
|
5312
|
+
userId: namespace.userId,
|
|
5313
|
+
login: namespace.login,
|
|
5314
|
+
root: namespace.root,
|
|
5315
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
5316
|
+
snapshotBaseDir: namespace.snapshotBaseDir,
|
|
5317
|
+
createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
|
|
5318
|
+
updatedAt: now
|
|
5319
|
+
}, null, 2)}
|
|
5320
|
+
`, { encoding: "utf8", mode: 384 });
|
|
5321
|
+
try {
|
|
5322
|
+
chmodSync2(namespace.metadataFile, 384);
|
|
5323
|
+
} catch {}
|
|
5324
|
+
}
|
|
5325
|
+
|
|
5326
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
5327
|
+
function cleanString4(value) {
|
|
5328
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
5329
|
+
}
|
|
5330
|
+
function resolveGitHubApiSessionIndexFile(projectRoot) {
|
|
5331
|
+
return resolve17(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
|
|
5332
|
+
}
|
|
5333
|
+
function parseEntry(value) {
|
|
5334
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5335
|
+
return null;
|
|
5336
|
+
const record = value;
|
|
5337
|
+
const token = cleanString4(record.token);
|
|
5338
|
+
const namespaceKey = cleanString4(record.namespaceKey);
|
|
5339
|
+
const namespaceRoot = cleanString4(record.namespaceRoot);
|
|
5340
|
+
const authStateFile = cleanString4(record.authStateFile);
|
|
5341
|
+
const checkoutBaseDir = cleanString4(record.checkoutBaseDir);
|
|
5342
|
+
const snapshotBaseDir = cleanString4(record.snapshotBaseDir);
|
|
5343
|
+
const createdAt = cleanString4(record.createdAt);
|
|
5344
|
+
if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
|
|
5345
|
+
return null;
|
|
5346
|
+
return {
|
|
5347
|
+
token,
|
|
5348
|
+
namespaceKey,
|
|
5349
|
+
namespaceRoot,
|
|
5350
|
+
authStateFile,
|
|
5351
|
+
checkoutBaseDir,
|
|
5352
|
+
snapshotBaseDir,
|
|
5353
|
+
createdAt,
|
|
5354
|
+
login: cleanString4(record.login),
|
|
5355
|
+
userId: cleanString4(record.userId),
|
|
5356
|
+
selectedRepo: cleanString4(record.selectedRepo)
|
|
5357
|
+
};
|
|
5358
|
+
}
|
|
5359
|
+
function readIndex(indexFile) {
|
|
5360
|
+
if (!existsSync10(indexFile))
|
|
5361
|
+
return [];
|
|
5362
|
+
try {
|
|
5363
|
+
const parsed = JSON.parse(readFileSync7(indexFile, "utf8"));
|
|
5364
|
+
return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
|
|
5365
|
+
const parsedEntry = parseEntry(entry);
|
|
5366
|
+
return parsedEntry ? [parsedEntry] : [];
|
|
5367
|
+
}) : [];
|
|
5368
|
+
} catch {
|
|
5369
|
+
return [];
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
function writeIndex(indexFile, sessions) {
|
|
5373
|
+
mkdirSync10(dirname12(indexFile), { recursive: true });
|
|
5374
|
+
writeFileSync9(indexFile, `${JSON.stringify({ sessions }, null, 2)}
|
|
5375
|
+
`, { encoding: "utf8", mode: 384 });
|
|
5376
|
+
try {
|
|
5377
|
+
chmodSync3(indexFile, 384);
|
|
5378
|
+
} catch {}
|
|
5379
|
+
}
|
|
5380
|
+
function registerGitHubApiSession(input) {
|
|
5381
|
+
const cleanToken = cleanString4(input.token);
|
|
5382
|
+
if (!cleanToken)
|
|
5383
|
+
throw new Error("GitHub API session token is required");
|
|
5384
|
+
const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
|
|
5385
|
+
const createdAt = new Date().toISOString();
|
|
5386
|
+
const entry = {
|
|
5387
|
+
token: cleanToken,
|
|
5388
|
+
login: input.namespace.login,
|
|
5389
|
+
userId: input.namespace.userId,
|
|
5390
|
+
namespaceKey: input.namespace.key,
|
|
5391
|
+
namespaceRoot: input.namespace.root,
|
|
5392
|
+
authStateFile: input.namespace.authStateFile,
|
|
5393
|
+
checkoutBaseDir: input.namespace.checkoutBaseDir,
|
|
5394
|
+
snapshotBaseDir: input.namespace.snapshotBaseDir,
|
|
5395
|
+
selectedRepo: cleanString4(input.selectedRepo),
|
|
5396
|
+
createdAt
|
|
5397
|
+
};
|
|
5398
|
+
const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
|
|
5399
|
+
writeIndex(indexFile, [...previous.slice(-199), entry]);
|
|
5400
|
+
return entry;
|
|
5401
|
+
}
|
|
5402
|
+
function readGitHubApiSession(input) {
|
|
5403
|
+
const cleanToken = cleanString4(input.token);
|
|
5404
|
+
if (!cleanToken)
|
|
5405
|
+
return null;
|
|
5406
|
+
return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
|
|
5407
|
+
}
|
|
5408
|
+
|
|
4578
5409
|
// packages/server/src/server-helpers/inspector-agent-lifecycle.ts
|
|
4579
5410
|
function createInspectorAgentLifecycleController(options) {
|
|
4580
5411
|
const initialDelayMs = Math.max(1, options.initialDelayMs ?? 5000);
|
|
@@ -4678,21 +5509,21 @@ function inspectorAgentLifecycleSnapshot(input) {
|
|
|
4678
5509
|
// packages/server/src/server-helpers/project-registry.ts
|
|
4679
5510
|
import { createHash as createHash2 } from "crypto";
|
|
4680
5511
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
4681
|
-
import { existsSync as
|
|
4682
|
-
import { dirname as
|
|
5512
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync8, readdirSync as readdirSync3, writeFileSync as writeFileSync10 } from "fs";
|
|
5513
|
+
import { dirname as dirname13, resolve as resolve18 } from "path";
|
|
4683
5514
|
function normalizeRepoSlug(value) {
|
|
4684
5515
|
const trimmed = value.trim();
|
|
4685
5516
|
return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
|
|
4686
5517
|
}
|
|
4687
5518
|
function registryPath(projectRoot) {
|
|
4688
|
-
return
|
|
5519
|
+
return resolve18(projectRoot, ".rig", "state", "projects.json");
|
|
4689
5520
|
}
|
|
4690
5521
|
function readRegistry(projectRoot) {
|
|
4691
5522
|
const path = registryPath(projectRoot);
|
|
4692
|
-
if (!
|
|
5523
|
+
if (!existsSync11(path))
|
|
4693
5524
|
return {};
|
|
4694
5525
|
try {
|
|
4695
|
-
const payload = JSON.parse(
|
|
5526
|
+
const payload = JSON.parse(readFileSync8(path, "utf8"));
|
|
4696
5527
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
4697
5528
|
return {};
|
|
4698
5529
|
const projects = payload.projects;
|
|
@@ -4703,14 +5534,14 @@ function readRegistry(projectRoot) {
|
|
|
4703
5534
|
}
|
|
4704
5535
|
function writeRegistry(projectRoot, projects) {
|
|
4705
5536
|
const path = registryPath(projectRoot);
|
|
4706
|
-
|
|
4707
|
-
|
|
5537
|
+
mkdirSync11(dirname13(path), { recursive: true });
|
|
5538
|
+
writeFileSync10(path, `${JSON.stringify({ projects }, null, 2)}
|
|
4708
5539
|
`, "utf8");
|
|
4709
5540
|
}
|
|
4710
5541
|
function resolveConfigPath(projectRoot) {
|
|
4711
5542
|
for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
|
|
4712
|
-
const path =
|
|
4713
|
-
if (
|
|
5543
|
+
const path = resolve18(projectRoot, name);
|
|
5544
|
+
if (existsSync11(path))
|
|
4714
5545
|
return path;
|
|
4715
5546
|
}
|
|
4716
5547
|
return null;
|
|
@@ -4719,7 +5550,7 @@ function hashFile(path) {
|
|
|
4719
5550
|
if (!path)
|
|
4720
5551
|
return null;
|
|
4721
5552
|
try {
|
|
4722
|
-
return createHash2("sha256").update(
|
|
5553
|
+
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
4723
5554
|
} catch {
|
|
4724
5555
|
return null;
|
|
4725
5556
|
}
|
|
@@ -4735,11 +5566,11 @@ function readDefaultBranch(projectRoot) {
|
|
|
4735
5566
|
return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
|
|
4736
5567
|
}
|
|
4737
5568
|
function buildRunSummary(projectRoot) {
|
|
4738
|
-
const runsDir =
|
|
5569
|
+
const runsDir = resolve18(projectRoot, ".rig", "runs");
|
|
4739
5570
|
try {
|
|
4740
5571
|
const runs = readdirSync3(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
|
|
4741
5572
|
try {
|
|
4742
|
-
const run = JSON.parse(
|
|
5573
|
+
const run = JSON.parse(readFileSync8(resolve18(runsDir, entry.name, "run.json"), "utf8"));
|
|
4743
5574
|
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 : "" }];
|
|
4744
5575
|
} catch {
|
|
4745
5576
|
return [];
|
|
@@ -4791,10 +5622,14 @@ function upsertProjectRecord(projectRoot, input) {
|
|
|
4791
5622
|
function linkProjectCheckout(projectRoot, repoSlug, checkout) {
|
|
4792
5623
|
return upsertProjectRecord(projectRoot, { repoSlug, checkout });
|
|
4793
5624
|
}
|
|
5625
|
+
function projectRegistryContainsCheckout(projectRoot, checkoutPath) {
|
|
5626
|
+
const target = resolve18(checkoutPath);
|
|
5627
|
+
return Object.values(readRegistry(projectRoot)).some((project) => project.checkouts.some((checkout) => checkout.path ? resolve18(checkout.path) === target : false));
|
|
5628
|
+
}
|
|
4794
5629
|
|
|
4795
5630
|
// packages/server/src/server-helpers/remote-checkout.ts
|
|
4796
|
-
import { existsSync as
|
|
4797
|
-
import { dirname as
|
|
5631
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
|
|
5632
|
+
import { dirname as dirname14, isAbsolute as isAbsolute3, relative as relative4, resolve as resolve19 } from "path";
|
|
4798
5633
|
function safeSlugSegments(repoSlug) {
|
|
4799
5634
|
const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
|
|
4800
5635
|
if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
|
|
@@ -4811,7 +5646,7 @@ function safeCheckoutKey(value) {
|
|
|
4811
5646
|
}
|
|
4812
5647
|
function repoSlugPath(baseDir, repoSlug, checkoutKey) {
|
|
4813
5648
|
const key = safeCheckoutKey(checkoutKey);
|
|
4814
|
-
return
|
|
5649
|
+
return resolve19(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
|
|
4815
5650
|
}
|
|
4816
5651
|
function sanitizeSnapshotId(value, fallback) {
|
|
4817
5652
|
const raw = (value ?? fallback).trim();
|
|
@@ -4819,7 +5654,7 @@ function sanitizeSnapshotId(value, fallback) {
|
|
|
4819
5654
|
return safe || fallback;
|
|
4820
5655
|
}
|
|
4821
5656
|
function assertWithinRoot(root, relativePath) {
|
|
4822
|
-
if (!relativePath ||
|
|
5657
|
+
if (!relativePath || isAbsolute3(relativePath) || relativePath.includes("\x00")) {
|
|
4823
5658
|
throw new Error(`Invalid snapshot file path: ${relativePath}`);
|
|
4824
5659
|
}
|
|
4825
5660
|
const normalizedRelative = relativePath.replace(/\\/g, "/");
|
|
@@ -4827,9 +5662,9 @@ function assertWithinRoot(root, relativePath) {
|
|
|
4827
5662
|
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
4828
5663
|
throw new Error(`Unsafe snapshot file path: ${relativePath}`);
|
|
4829
5664
|
}
|
|
4830
|
-
const target =
|
|
4831
|
-
const rel =
|
|
4832
|
-
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." ||
|
|
5665
|
+
const target = resolve19(root, ...segments);
|
|
5666
|
+
const rel = relative4(root, target);
|
|
5667
|
+
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute3(rel)) {
|
|
4833
5668
|
throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
|
|
4834
5669
|
}
|
|
4835
5670
|
return target;
|
|
@@ -4847,17 +5682,17 @@ function parseSnapshotArchiveContentBase64(contentBase64) {
|
|
|
4847
5682
|
function extractUploadedSnapshotArchive(input) {
|
|
4848
5683
|
const archive = decodeSnapshotArchive(input.archive);
|
|
4849
5684
|
const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
|
|
4850
|
-
const checkoutPath =
|
|
4851
|
-
|
|
5685
|
+
const checkoutPath = resolve19(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
|
|
5686
|
+
mkdirSync12(checkoutPath, { recursive: true });
|
|
4852
5687
|
for (const file of archive.files) {
|
|
4853
5688
|
if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
|
|
4854
5689
|
throw new Error("Invalid snapshot archive file entry");
|
|
4855
5690
|
}
|
|
4856
5691
|
const target = assertWithinRoot(checkoutPath, file.path);
|
|
4857
|
-
|
|
4858
|
-
|
|
5692
|
+
mkdirSync12(dirname14(target), { recursive: true });
|
|
5693
|
+
writeFileSync11(target, Buffer.from(file.contentBase64, "base64"));
|
|
4859
5694
|
}
|
|
4860
|
-
|
|
5695
|
+
writeFileSync11(resolve19(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
|
|
4861
5696
|
repoSlug: input.repoSlug,
|
|
4862
5697
|
snapshotId,
|
|
4863
5698
|
fileCount: archive.files.length,
|
|
@@ -4892,7 +5727,7 @@ function gitCredentialConfig(token) {
|
|
|
4892
5727
|
};
|
|
4893
5728
|
}
|
|
4894
5729
|
async function prepareRemoteCheckout(input) {
|
|
4895
|
-
const exists = input.exists ??
|
|
5730
|
+
const exists = input.exists ?? existsSync12;
|
|
4896
5731
|
const strategy = input.strategy;
|
|
4897
5732
|
if (strategy.kind === "uploaded-snapshot") {
|
|
4898
5733
|
return extractUploadedSnapshotArchive({
|
|
@@ -4904,7 +5739,7 @@ async function prepareRemoteCheckout(input) {
|
|
|
4904
5739
|
});
|
|
4905
5740
|
}
|
|
4906
5741
|
if (strategy.kind === "existing-path") {
|
|
4907
|
-
const checkoutPath2 =
|
|
5742
|
+
const checkoutPath2 = resolve19(strategy.path);
|
|
4908
5743
|
if (!exists(checkoutPath2)) {
|
|
4909
5744
|
throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
|
|
4910
5745
|
}
|
|
@@ -4940,9 +5775,9 @@ function buildServerControlStatus() {
|
|
|
4940
5775
|
};
|
|
4941
5776
|
}
|
|
4942
5777
|
function buildProjectConfigStatus(root) {
|
|
4943
|
-
const hasConfigTs =
|
|
4944
|
-
const hasConfigJson =
|
|
4945
|
-
const hasLegacyTaskConfig =
|
|
5778
|
+
const hasConfigTs = existsSync13(resolve20(root, "rig.config.ts"));
|
|
5779
|
+
const hasConfigJson = existsSync13(resolve20(root, "rig.config.json"));
|
|
5780
|
+
const hasLegacyTaskConfig = existsSync13(resolve20(root, ".rig", "task-config.json"));
|
|
4946
5781
|
let kind = "missing";
|
|
4947
5782
|
if (hasConfigTs)
|
|
4948
5783
|
kind = "rig-config-ts";
|
|
@@ -4959,6 +5794,75 @@ function buildProjectConfigStatus(root) {
|
|
|
4959
5794
|
suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
|
|
4960
5795
|
};
|
|
4961
5796
|
}
|
|
5797
|
+
var RIG_GITHUB_LIFECYCLE_LABELS = [
|
|
5798
|
+
"ready",
|
|
5799
|
+
"blocked",
|
|
5800
|
+
"in-progress",
|
|
5801
|
+
"under-review",
|
|
5802
|
+
"failed",
|
|
5803
|
+
"cancelled",
|
|
5804
|
+
"rig:running",
|
|
5805
|
+
"rig:pr-open",
|
|
5806
|
+
"rig:ci-fixing",
|
|
5807
|
+
"rig:merging",
|
|
5808
|
+
"rig:done",
|
|
5809
|
+
"rig:needs-attention"
|
|
5810
|
+
];
|
|
5811
|
+
function githubProjectsEnabled2(config) {
|
|
5812
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
5813
|
+
return false;
|
|
5814
|
+
const root = config;
|
|
5815
|
+
const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
|
|
5816
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
5817
|
+
return projects?.enabled === true;
|
|
5818
|
+
}
|
|
5819
|
+
function githubIssueSourceRepo(config) {
|
|
5820
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
5821
|
+
return null;
|
|
5822
|
+
const root = config;
|
|
5823
|
+
const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
|
|
5824
|
+
const owner = normalizeString(taskSource?.owner);
|
|
5825
|
+
const repo = normalizeString(taskSource?.repo);
|
|
5826
|
+
if (taskSource?.kind === "github-issues" && owner && repo)
|
|
5827
|
+
return { owner, repo };
|
|
5828
|
+
const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
|
|
5829
|
+
const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
|
|
5830
|
+
const match = slug?.match(/^([^/]+)\/([^/]+)$/);
|
|
5831
|
+
return match ? { owner: match[1], repo: match[2] } : null;
|
|
5832
|
+
}
|
|
5833
|
+
async function ensureGitHubLifecycleLabels(projectRoot, config) {
|
|
5834
|
+
const repo = githubIssueSourceRepo(config);
|
|
5835
|
+
if (!repo)
|
|
5836
|
+
return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
5837
|
+
const token = createGitHubAuthStore(projectRoot).readToken();
|
|
5838
|
+
if (!token)
|
|
5839
|
+
return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
5840
|
+
const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
|
|
5841
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
|
|
5842
|
+
});
|
|
5843
|
+
const existingJson = await existingResponse.json().catch(() => []);
|
|
5844
|
+
const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
|
|
5845
|
+
const created = [];
|
|
5846
|
+
const alreadyPresent = [];
|
|
5847
|
+
const failed = [];
|
|
5848
|
+
for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
|
|
5849
|
+
if (existing.has(label)) {
|
|
5850
|
+
alreadyPresent.push(label);
|
|
5851
|
+
continue;
|
|
5852
|
+
}
|
|
5853
|
+
const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
|
|
5854
|
+
method: "POST",
|
|
5855
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
|
|
5856
|
+
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" })
|
|
5857
|
+
});
|
|
5858
|
+
if (response.ok || response.status === 422) {
|
|
5859
|
+
(response.status === 422 ? alreadyPresent : created).push(label);
|
|
5860
|
+
} else {
|
|
5861
|
+
failed.push({ label, error: await response.text().catch(() => response.statusText) });
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5864
|
+
return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
|
|
5865
|
+
}
|
|
4962
5866
|
function normalizeCommit(value) {
|
|
4963
5867
|
const raw = normalizeString(value);
|
|
4964
5868
|
return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
|
|
@@ -4978,24 +5882,24 @@ function repoParts(repoSlug) {
|
|
|
4978
5882
|
return { owner, repo, slug: `${owner}/${repo}` };
|
|
4979
5883
|
}
|
|
4980
5884
|
function repairDir(checkoutPath) {
|
|
4981
|
-
const dir =
|
|
4982
|
-
|
|
5885
|
+
const dir = resolve20(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
|
|
5886
|
+
mkdirSync13(dir, { recursive: true });
|
|
4983
5887
|
return dir;
|
|
4984
5888
|
}
|
|
4985
5889
|
function backupCheckoutFile(checkoutPath, relativePath) {
|
|
4986
|
-
const source =
|
|
4987
|
-
const backupPath =
|
|
4988
|
-
|
|
4989
|
-
|
|
5890
|
+
const source = resolve20(checkoutPath, relativePath);
|
|
5891
|
+
const backupPath = resolve20(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
|
|
5892
|
+
mkdirSync13(dirname15(backupPath), { recursive: true });
|
|
5893
|
+
copyFileSync2(source, backupPath);
|
|
4990
5894
|
return backupPath;
|
|
4991
5895
|
}
|
|
4992
5896
|
function parsePackageJsonLosslessly(checkoutPath) {
|
|
4993
|
-
const packagePath =
|
|
4994
|
-
if (!
|
|
5897
|
+
const packagePath = resolve20(checkoutPath, "package.json");
|
|
5898
|
+
if (!existsSync13(packagePath)) {
|
|
4995
5899
|
return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
|
|
4996
5900
|
}
|
|
4997
5901
|
try {
|
|
4998
|
-
const parsed = JSON.parse(
|
|
5902
|
+
const parsed = JSON.parse(readFileSync9(packagePath, "utf8"));
|
|
4999
5903
|
return {
|
|
5000
5904
|
existed: true,
|
|
5001
5905
|
packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
|
|
@@ -5009,9 +5913,9 @@ function parsePackageJsonLosslessly(checkoutPath) {
|
|
|
5009
5913
|
}
|
|
5010
5914
|
}
|
|
5011
5915
|
function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
5012
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
5013
|
-
const packagePath =
|
|
5014
|
-
if (!hasConfig && !
|
|
5916
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
|
|
5917
|
+
const packagePath = resolve20(checkoutPath, "package.json");
|
|
5918
|
+
if (!hasConfig && !existsSync13(packagePath)) {
|
|
5015
5919
|
return { skipped: true, reason: "package.json and rig.config missing" };
|
|
5016
5920
|
}
|
|
5017
5921
|
const parsed = parsePackageJsonLosslessly(checkoutPath);
|
|
@@ -5030,7 +5934,7 @@ function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
|
5030
5934
|
}
|
|
5031
5935
|
const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
|
|
5032
5936
|
if (changed) {
|
|
5033
|
-
|
|
5937
|
+
writeFileSync12(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
|
|
5034
5938
|
`, "utf8");
|
|
5035
5939
|
}
|
|
5036
5940
|
return {
|
|
@@ -5056,11 +5960,11 @@ function configLooksStructurallyUsable(source) {
|
|
|
5056
5960
|
return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
|
|
5057
5961
|
}
|
|
5058
5962
|
function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
|
|
5059
|
-
const configPath =
|
|
5060
|
-
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
5963
|
+
const configPath = resolve20(checkoutPath, "rig.config.ts");
|
|
5964
|
+
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
|
|
5061
5965
|
if (existingConfigName) {
|
|
5062
|
-
const existingPath =
|
|
5063
|
-
const source =
|
|
5966
|
+
const existingPath = resolve20(checkoutPath, existingConfigName);
|
|
5967
|
+
const source = readFileSync9(existingPath, "utf8");
|
|
5064
5968
|
if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
|
|
5065
5969
|
return { path: existingPath, changed: false, reason: "config structurally complete" };
|
|
5066
5970
|
}
|
|
@@ -5074,7 +5978,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
5074
5978
|
}
|
|
5075
5979
|
}
|
|
5076
5980
|
const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
|
|
5077
|
-
|
|
5981
|
+
writeFileSync12(configPath, generatedRigConfigSource(repoSlug), "utf8");
|
|
5078
5982
|
return {
|
|
5079
5983
|
path: configPath,
|
|
5080
5984
|
changed: true,
|
|
@@ -5083,7 +5987,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
5083
5987
|
};
|
|
5084
5988
|
}
|
|
5085
5989
|
function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
5086
|
-
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
5990
|
+
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
|
|
5087
5991
|
if (!configFile)
|
|
5088
5992
|
return { ok: false, error: "missing rig config" };
|
|
5089
5993
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -5105,7 +6009,7 @@ function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
|
5105
6009
|
return { ok: true, configFile };
|
|
5106
6010
|
}
|
|
5107
6011
|
function installRemoteCheckoutPackages(checkoutPath) {
|
|
5108
|
-
if (!
|
|
6012
|
+
if (!existsSync13(resolve20(checkoutPath, "package.json"))) {
|
|
5109
6013
|
return { skipped: true, reason: "package.json missing" };
|
|
5110
6014
|
}
|
|
5111
6015
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -5118,8 +6022,8 @@ function installRemoteCheckoutPackages(checkoutPath) {
|
|
|
5118
6022
|
return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
|
|
5119
6023
|
}
|
|
5120
6024
|
function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
|
|
5121
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
5122
|
-
const hasPackage =
|
|
6025
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
|
|
6026
|
+
const hasPackage = existsSync13(resolve20(checkoutPath, "package.json"));
|
|
5123
6027
|
if (!hasConfig && !hasPackage) {
|
|
5124
6028
|
return {
|
|
5125
6029
|
packageJson: { skipped: true, reason: "package.json and rig.config missing" },
|
|
@@ -5217,26 +6121,26 @@ function buildRemoteRunLogEntry(body, identifiers) {
|
|
|
5217
6121
|
}
|
|
5218
6122
|
function readGitHeadCommit(projectRoot) {
|
|
5219
6123
|
try {
|
|
5220
|
-
let gitDir =
|
|
6124
|
+
let gitDir = resolve20(projectRoot, ".git");
|
|
5221
6125
|
try {
|
|
5222
|
-
const dotGit =
|
|
6126
|
+
const dotGit = readFileSync9(gitDir, "utf8").trim();
|
|
5223
6127
|
const gitDirPrefix = "gitdir:";
|
|
5224
6128
|
if (dotGit.startsWith(gitDirPrefix)) {
|
|
5225
|
-
gitDir =
|
|
6129
|
+
gitDir = resolve20(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
|
|
5226
6130
|
}
|
|
5227
6131
|
} catch {}
|
|
5228
|
-
const head =
|
|
6132
|
+
const head = readFileSync9(resolve20(gitDir, "HEAD"), "utf8").trim();
|
|
5229
6133
|
const refPrefix = "ref:";
|
|
5230
6134
|
if (!head.startsWith(refPrefix)) {
|
|
5231
6135
|
return normalizeCommit(head);
|
|
5232
6136
|
}
|
|
5233
6137
|
const ref = head.slice(refPrefix.length).trim();
|
|
5234
|
-
const refPath =
|
|
5235
|
-
if (
|
|
5236
|
-
return normalizeCommit(
|
|
6138
|
+
const refPath = resolve20(gitDir, ref);
|
|
6139
|
+
if (existsSync13(refPath)) {
|
|
6140
|
+
return normalizeCommit(readFileSync9(refPath, "utf8").trim());
|
|
5237
6141
|
}
|
|
5238
|
-
const commonDir = normalizeString(
|
|
5239
|
-
return commonDir ? normalizeCommit(
|
|
6142
|
+
const commonDir = normalizeString(readFileSync9(resolve20(gitDir, "commondir"), "utf8"));
|
|
6143
|
+
return commonDir ? normalizeCommit(readFileSync9(resolve20(gitDir, commonDir, ref), "utf8").trim()) : null;
|
|
5240
6144
|
} catch {
|
|
5241
6145
|
return null;
|
|
5242
6146
|
}
|
|
@@ -5256,7 +6160,8 @@ function isAuthorizedInspectorStreamRequest(req, authToken) {
|
|
|
5256
6160
|
}
|
|
5257
6161
|
function buildDeploymentStatus(projectRoot) {
|
|
5258
6162
|
const envCommit = normalizeCommit(process.env.RIG_COMMIT_SHA ?? process.env.GITHUB_SHA ?? process.env.VERCEL_GIT_COMMIT_SHA ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.COMMIT_SHA);
|
|
5259
|
-
const
|
|
6163
|
+
const deploymentRoot = normalizeString(process.env.RIG_HOST_PROJECT_ROOT) ?? projectRoot;
|
|
6164
|
+
const gitCommit = envCommit ?? readGitHeadCommit(deploymentRoot) ?? readGitHeadCommit(projectRoot);
|
|
5260
6165
|
return {
|
|
5261
6166
|
currentCommit: gitCommit,
|
|
5262
6167
|
commitSource: envCommit ? "env" : gitCommit ? "git" : null,
|
|
@@ -5274,9 +6179,9 @@ function configuredRepoFromTaskSource(taskSource) {
|
|
|
5274
6179
|
const repo = normalizeString(taskSource?.repo);
|
|
5275
6180
|
return owner && repo ? `${owner}/${repo}` : null;
|
|
5276
6181
|
}
|
|
5277
|
-
async function buildTaskSourceStatus(state, config) {
|
|
6182
|
+
async function buildTaskSourceStatus(state, config, requestAuth) {
|
|
5278
6183
|
const diagnostics = state.snapshotService.getTaskSourceErrors();
|
|
5279
|
-
const selectedRepo =
|
|
6184
|
+
const selectedRepo = requestScopedAuthStore(state.projectRoot, requestAuth).status({
|
|
5280
6185
|
oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
|
|
5281
6186
|
}).selectedRepo;
|
|
5282
6187
|
try {
|
|
@@ -5329,37 +6234,146 @@ function isLoopbackRequest(req) {
|
|
|
5329
6234
|
}
|
|
5330
6235
|
}
|
|
5331
6236
|
function isPublicRigAuthBootstrapRoute(pathname) {
|
|
5332
|
-
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";
|
|
6237
|
+
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";
|
|
6238
|
+
}
|
|
6239
|
+
function buildRigInstallScript() {
|
|
6240
|
+
return `#!/usr/bin/env bash
|
|
6241
|
+
set -euo pipefail
|
|
6242
|
+
|
|
6243
|
+
say() {
|
|
6244
|
+
printf 'rig-install: %s
|
|
6245
|
+
' "$*"
|
|
6246
|
+
}
|
|
6247
|
+
|
|
6248
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
6249
|
+
say "Bun not found; installing Bun first"
|
|
6250
|
+
curl -fsSL https://bun.sh/install | bash
|
|
6251
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
6252
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
6253
|
+
fi
|
|
6254
|
+
|
|
6255
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
6256
|
+
printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
|
|
6257
|
+
' >&2
|
|
6258
|
+
exit 1
|
|
6259
|
+
fi
|
|
6260
|
+
|
|
6261
|
+
say "Installing @h-rig/cli@latest"
|
|
6262
|
+
bun add -g --force @h-rig/cli@latest
|
|
6263
|
+
|
|
6264
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
6265
|
+
BUN_RIG="$BUN_INSTALL/bin/rig"
|
|
6266
|
+
if [ ! -x "$BUN_RIG" ]; then
|
|
6267
|
+
printf 'rig-install: expected Bun global rig at %s but it was not executable.
|
|
6268
|
+
' "$BUN_RIG" >&2
|
|
6269
|
+
exit 1
|
|
6270
|
+
fi
|
|
6271
|
+
|
|
6272
|
+
USER_BIN="$HOME/.local/bin"
|
|
6273
|
+
mkdir -p "$USER_BIN"
|
|
6274
|
+
cat > "$USER_BIN/rig" <<'RIG_SHIM'
|
|
6275
|
+
#!/usr/bin/env bash
|
|
6276
|
+
set -euo pipefail
|
|
6277
|
+
exec "\${BUN_INSTALL:-$HOME/.bun}/bin/rig" "$@"
|
|
6278
|
+
RIG_SHIM
|
|
6279
|
+
chmod +x "$USER_BIN/rig"
|
|
6280
|
+
|
|
6281
|
+
export PATH="$USER_BIN:$BUN_INSTALL/bin:$PATH"
|
|
6282
|
+
if command -v hash >/dev/null 2>&1; then hash -r; fi
|
|
6283
|
+
|
|
6284
|
+
if ! command -v rig >/dev/null 2>&1; then
|
|
6285
|
+
printf 'rig-install: rig installed, but rig is not on PATH. Add %s and %s/bin to PATH and retry.
|
|
6286
|
+
' "$USER_BIN" "$BUN_INSTALL" >&2
|
|
6287
|
+
exit 1
|
|
6288
|
+
fi
|
|
6289
|
+
|
|
6290
|
+
say "Verifying rig"
|
|
6291
|
+
"$BUN_RIG" --help >/dev/null
|
|
6292
|
+
rig --help >/dev/null
|
|
6293
|
+
say "Done. Run: rig --help"
|
|
6294
|
+
`;
|
|
5333
6295
|
}
|
|
5334
6296
|
function normalizePrMode(value) {
|
|
5335
6297
|
const mode = normalizeString(value);
|
|
5336
6298
|
return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
|
|
5337
6299
|
}
|
|
6300
|
+
function requestAuthResult(input) {
|
|
6301
|
+
return {
|
|
6302
|
+
authorized: input.authorized,
|
|
6303
|
+
actor: input.actor ?? null,
|
|
6304
|
+
reason: input.reason,
|
|
6305
|
+
login: input.login ?? null,
|
|
6306
|
+
userId: input.userId ?? null,
|
|
6307
|
+
userNamespace: input.userNamespace ?? null,
|
|
6308
|
+
authStateFile: input.authStateFile ?? null
|
|
6309
|
+
};
|
|
6310
|
+
}
|
|
6311
|
+
function namespaceFromSessionIndex(entry) {
|
|
6312
|
+
const stateDir2 = dirname15(entry.authStateFile);
|
|
6313
|
+
return {
|
|
6314
|
+
key: entry.namespaceKey,
|
|
6315
|
+
userId: entry.userId,
|
|
6316
|
+
login: entry.login,
|
|
6317
|
+
root: entry.namespaceRoot,
|
|
6318
|
+
stateDir: stateDir2,
|
|
6319
|
+
authStateFile: entry.authStateFile,
|
|
6320
|
+
metadataFile: resolve20(stateDir2, "user-namespace.json"),
|
|
6321
|
+
checkoutBaseDir: entry.checkoutBaseDir,
|
|
6322
|
+
snapshotBaseDir: entry.snapshotBaseDir
|
|
6323
|
+
};
|
|
6324
|
+
}
|
|
5338
6325
|
function authorizeRigHttpRequest(input) {
|
|
5339
6326
|
if (input.legacyAuthorized) {
|
|
5340
|
-
return { authorized: true, actor: "rig-local-server", reason: "server-token" };
|
|
6327
|
+
return requestAuthResult({ authorized: true, actor: "rig-local-server", reason: "server-token" });
|
|
5341
6328
|
}
|
|
5342
6329
|
const bearer = bearerTokenFromRequest(input.req);
|
|
5343
6330
|
const store = createGitHubAuthStore(input.projectRoot);
|
|
5344
6331
|
const storedToken = store.readToken();
|
|
5345
6332
|
const session = bearer ? store.readApiSession(bearer) : null;
|
|
5346
6333
|
if (session) {
|
|
5347
|
-
return {
|
|
6334
|
+
return requestAuthResult({
|
|
6335
|
+
authorized: true,
|
|
6336
|
+
actor: session.login ?? "github-operator",
|
|
6337
|
+
reason: "github-session",
|
|
6338
|
+
login: session.login,
|
|
6339
|
+
userId: session.userId,
|
|
6340
|
+
authStateFile: store.stateFile
|
|
6341
|
+
});
|
|
5348
6342
|
}
|
|
5349
6343
|
if (bearer && storedToken && bearer === storedToken) {
|
|
5350
6344
|
const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
5351
|
-
return {
|
|
6345
|
+
return requestAuthResult({
|
|
6346
|
+
authorized: true,
|
|
6347
|
+
actor: status.login ?? "github-operator",
|
|
6348
|
+
reason: "github-token",
|
|
6349
|
+
login: status.login,
|
|
6350
|
+
userId: status.userId,
|
|
6351
|
+
authStateFile: store.stateFile
|
|
6352
|
+
});
|
|
6353
|
+
}
|
|
6354
|
+
const indexedSession = readGitHubApiSession({ projectRoot: input.projectRoot, token: bearer });
|
|
6355
|
+
if (indexedSession) {
|
|
6356
|
+
const userNamespace = namespaceFromSessionIndex(indexedSession);
|
|
6357
|
+
return requestAuthResult({
|
|
6358
|
+
authorized: true,
|
|
6359
|
+
actor: indexedSession.login ?? "github-operator",
|
|
6360
|
+
reason: "github-user-session",
|
|
6361
|
+
login: indexedSession.login,
|
|
6362
|
+
userId: indexedSession.userId,
|
|
6363
|
+
userNamespace,
|
|
6364
|
+
authStateFile: indexedSession.authStateFile
|
|
6365
|
+
});
|
|
5352
6366
|
}
|
|
5353
6367
|
if (isPublicRigAuthBootstrapRoute(input.pathname)) {
|
|
5354
|
-
return { authorized: true, actor: null, reason: "public-bootstrap" };
|
|
6368
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "public-bootstrap" });
|
|
5355
6369
|
}
|
|
5356
6370
|
if (!input.serverAuthToken && !storedToken) {
|
|
5357
6371
|
if (isLoopbackRequest(input.req)) {
|
|
5358
|
-
return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
|
|
6372
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "loopback-dev-no-auth" });
|
|
5359
6373
|
}
|
|
5360
|
-
return { authorized: false, actor: null, reason: "auth-required" };
|
|
6374
|
+
return requestAuthResult({ authorized: false, actor: null, reason: "auth-required" });
|
|
5361
6375
|
}
|
|
5362
|
-
return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
|
|
6376
|
+
return requestAuthResult({ authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" });
|
|
5363
6377
|
}
|
|
5364
6378
|
async function fetchGitHubUserInfo(token) {
|
|
5365
6379
|
const response = await fetch("https://api.github.com/user", {
|
|
@@ -5379,6 +6393,67 @@ async function fetchGitHubUserInfo(token) {
|
|
|
5379
6393
|
scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
|
|
5380
6394
|
};
|
|
5381
6395
|
}
|
|
6396
|
+
function shouldWriteRootAuthCompat(projectRoot) {
|
|
6397
|
+
if (process.env.RIG_REMOTE_USER_NAMESPACE_ROOT?.trim())
|
|
6398
|
+
return false;
|
|
6399
|
+
const stateDir2 = normalizeString(process.env.RIG_STATE_DIR);
|
|
6400
|
+
if (!stateDir2)
|
|
6401
|
+
return true;
|
|
6402
|
+
return resolve20(stateDir2) === resolve20(projectRoot, ".rig", "state");
|
|
6403
|
+
}
|
|
6404
|
+
function requestScopedRegistryRoot(stateProjectRoot, requestAuth) {
|
|
6405
|
+
return requestAuth.userNamespace?.root ?? stateProjectRoot;
|
|
6406
|
+
}
|
|
6407
|
+
function requestScopedAuthStore(stateProjectRoot, requestAuth) {
|
|
6408
|
+
return requestAuth.authStateFile ? createGitHubAuthStoreFromStateFile(requestAuth.authStateFile) : createGitHubAuthStore(stateProjectRoot);
|
|
6409
|
+
}
|
|
6410
|
+
function userNamespaceResponse(namespace) {
|
|
6411
|
+
return namespace ? serializeRemoteUserNamespace(namespace) : undefined;
|
|
6412
|
+
}
|
|
6413
|
+
function resolveNamespacedBaseDir(input) {
|
|
6414
|
+
if (input.explicitBaseDir)
|
|
6415
|
+
return input.explicitBaseDir;
|
|
6416
|
+
const envBase = normalizeString(process.env[input.envName]);
|
|
6417
|
+
if (input.userNamespace) {
|
|
6418
|
+
return envBase ? resolve20(envBase, input.userNamespace.key) : input.userNamespace[input.legacySubdir === "remote-checkouts" ? "checkoutBaseDir" : "snapshotBaseDir"];
|
|
6419
|
+
}
|
|
6420
|
+
return envBase ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve20(normalizeString(process.env.RIG_STATE_DIR), input.legacySubdir) : resolve20(input.legacyProjectRoot, ".rig", input.legacySubdir));
|
|
6421
|
+
}
|
|
6422
|
+
function explicitCheckoutKey(body, checkoutInput, requestAuth) {
|
|
6423
|
+
return normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? (requestAuth.userNamespace ? undefined : "default");
|
|
6424
|
+
}
|
|
6425
|
+
function saveGitHubTokenForRemoteUser(input) {
|
|
6426
|
+
const namespace = resolveRemoteUserNamespace(input.projectRoot, { userId: input.user.userId, login: input.user.login });
|
|
6427
|
+
writeRemoteUserNamespaceMetadata(namespace);
|
|
6428
|
+
const store = createGitHubAuthStoreFromStateFile(namespace.authStateFile);
|
|
6429
|
+
store.saveToken({
|
|
6430
|
+
token: input.token,
|
|
6431
|
+
tokenSource: input.tokenSource,
|
|
6432
|
+
login: input.user.login,
|
|
6433
|
+
userId: input.user.userId,
|
|
6434
|
+
scopes: input.user.scopes,
|
|
6435
|
+
selectedRepo: input.selectedRepo
|
|
6436
|
+
});
|
|
6437
|
+
const apiSession = store.createApiSession();
|
|
6438
|
+
registerGitHubApiSession({ projectRoot: input.projectRoot, token: apiSession.token, namespace, selectedRepo: input.selectedRepo });
|
|
6439
|
+
const requestedRoot = normalizeString(input.requestedProjectRoot);
|
|
6440
|
+
if (requestedRoot && isAbsolute4(requestedRoot) && existsSync13(resolve20(requestedRoot))) {
|
|
6441
|
+
copyGitHubAuthStateToLocalProjectRoot(namespace.authStateFile, resolve20(requestedRoot));
|
|
6442
|
+
}
|
|
6443
|
+
if (shouldWriteRootAuthCompat(input.projectRoot)) {
|
|
6444
|
+
const rootStore = createGitHubAuthStore(input.projectRoot);
|
|
6445
|
+
rootStore.saveToken({
|
|
6446
|
+
token: input.token,
|
|
6447
|
+
tokenSource: input.tokenSource,
|
|
6448
|
+
login: input.user.login,
|
|
6449
|
+
userId: input.user.userId,
|
|
6450
|
+
scopes: input.user.scopes,
|
|
6451
|
+
selectedRepo: input.selectedRepo
|
|
6452
|
+
});
|
|
6453
|
+
rootStore.createApiSession();
|
|
6454
|
+
}
|
|
6455
|
+
return { store, namespace, apiSessionToken: apiSession.token };
|
|
6456
|
+
}
|
|
5382
6457
|
async function postGitHubForm(endpoint, body) {
|
|
5383
6458
|
const response = await fetch(endpoint, {
|
|
5384
6459
|
method: "POST",
|
|
@@ -5396,11 +6471,11 @@ function resolveRequestedProjectRoot(currentRoot, rawRoot) {
|
|
|
5396
6471
|
const requestedRoot = normalizeString(rawRoot);
|
|
5397
6472
|
if (!requestedRoot)
|
|
5398
6473
|
return currentRoot;
|
|
5399
|
-
if (!
|
|
6474
|
+
if (!isAbsolute4(requestedRoot)) {
|
|
5400
6475
|
throw new Error("projectRoot must be an absolute path on the Rig server host");
|
|
5401
6476
|
}
|
|
5402
|
-
const normalizedRoot =
|
|
5403
|
-
if (!
|
|
6477
|
+
const normalizedRoot = resolve20(requestedRoot);
|
|
6478
|
+
if (!existsSync13(normalizedRoot)) {
|
|
5404
6479
|
throw new Error("projectRoot does not exist on the Rig server host");
|
|
5405
6480
|
}
|
|
5406
6481
|
return normalizedRoot;
|
|
@@ -5676,7 +6751,7 @@ function redactSecretFields(value) {
|
|
|
5676
6751
|
return redacted;
|
|
5677
6752
|
}
|
|
5678
6753
|
function validateRemoteLease(deps, state, input) {
|
|
5679
|
-
const run =
|
|
6754
|
+
const run = readAuthorityRun7(state.projectRoot, input.runId);
|
|
5680
6755
|
if (!run) {
|
|
5681
6756
|
return { ok: false, response: deps.jsonResponse({ ok: false, error: "Remote run not found" }, 404) };
|
|
5682
6757
|
}
|
|
@@ -5696,6 +6771,43 @@ function createRigServerFetch(state, deps) {
|
|
|
5696
6771
|
return deps.withServerPathEnv(state.projectRoot, async () => {
|
|
5697
6772
|
const browserOrigin = deps.resolveAllowedBrowserOrigin(req);
|
|
5698
6773
|
const finalizeResponse = (response) => deps.withCorsHeaders(response, req, browserOrigin);
|
|
6774
|
+
const earlyUrl = new URL(req.url);
|
|
6775
|
+
const piEventsWsMatch = earlyUrl.pathname.match(/^\/api\/runs\/([^/]+)\/pi\/events$/);
|
|
6776
|
+
if (piEventsWsMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
6777
|
+
const queryToken = earlyUrl.searchParams.get("token");
|
|
6778
|
+
const authHeaders = new Headers(req.headers);
|
|
6779
|
+
if (queryToken && !authHeaders.has("authorization")) {
|
|
6780
|
+
authHeaders.set("authorization", `Bearer ${queryToken}`);
|
|
6781
|
+
}
|
|
6782
|
+
const legacyAuthorized = Boolean(state.authToken && queryToken === state.authToken);
|
|
6783
|
+
const requestAuth = authorizeRigHttpRequest({
|
|
6784
|
+
req: new Request(req.url, { method: req.method, headers: authHeaders }),
|
|
6785
|
+
pathname: earlyUrl.pathname,
|
|
6786
|
+
projectRoot: state.projectRoot,
|
|
6787
|
+
serverAuthToken: state.authToken,
|
|
6788
|
+
legacyAuthorized
|
|
6789
|
+
});
|
|
6790
|
+
if (!requestAuth.authorized) {
|
|
6791
|
+
return deps.jsonResponse({ ok: false, error: "Unauthorized WebSocket connection", reason: requestAuth.reason }, 401);
|
|
6792
|
+
}
|
|
6793
|
+
const runId = decodeURIComponent(piEventsWsMatch[1]);
|
|
6794
|
+
const resolved = resolveRunPiSessionProxy(state.projectRoot, runId);
|
|
6795
|
+
if (resolved === null)
|
|
6796
|
+
return deps.jsonResponse({ ok: false, error: "Run not found" }, 404);
|
|
6797
|
+
if ("pending" in resolved)
|
|
6798
|
+
return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500 }, 202);
|
|
6799
|
+
if (!server)
|
|
6800
|
+
return deps.jsonResponse({ ok: false, error: "WebSocket upgrade unavailable" }, 400);
|
|
6801
|
+
const upgraded = server.upgrade(req, {
|
|
6802
|
+
data: {
|
|
6803
|
+
kind: "pi-session-proxy",
|
|
6804
|
+
connectedAt: new Date().toISOString(),
|
|
6805
|
+
upstreamUrl: buildRunPiDaemonWebSocketUrl(resolved),
|
|
6806
|
+
runId
|
|
6807
|
+
}
|
|
6808
|
+
});
|
|
6809
|
+
return upgraded ? new Response(null) : deps.jsonResponse({ ok: false, error: "WebSocket upgrade failed" }, 400);
|
|
6810
|
+
}
|
|
5699
6811
|
const upgradeResponse = handleWebSocketUpgrade({
|
|
5700
6812
|
req,
|
|
5701
6813
|
server,
|
|
@@ -5733,6 +6845,13 @@ function createRigServerFetch(state, deps) {
|
|
|
5733
6845
|
notifications: state.targets.length
|
|
5734
6846
|
});
|
|
5735
6847
|
}
|
|
6848
|
+
if (url.pathname === "/install" && req.method === "GET") {
|
|
6849
|
+
return new Response(buildRigInstallScript(), {
|
|
6850
|
+
headers: {
|
|
6851
|
+
"Content-Type": "text/x-shellscript; charset=utf-8"
|
|
6852
|
+
}
|
|
6853
|
+
});
|
|
6854
|
+
}
|
|
5736
6855
|
const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
|
|
5737
6856
|
const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
|
|
5738
6857
|
const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
|
|
@@ -5945,16 +7064,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
5945
7064
|
if (!source) {
|
|
5946
7065
|
return deps.badRequest("No task source is configured");
|
|
5947
7066
|
}
|
|
7067
|
+
if (!source.updateTask && !(update.status && source.updateStatus)) {
|
|
7068
|
+
return deps.badRequest("Configured task source does not support updates");
|
|
7069
|
+
}
|
|
5948
7070
|
const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
|
|
5949
7071
|
return;
|
|
5950
7072
|
}) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
|
|
5951
|
-
if (source.updateTask) {
|
|
5952
|
-
await source.updateTask(id, update);
|
|
5953
|
-
} else if (update.status && source.updateStatus) {
|
|
5954
|
-
await source.updateStatus(id, update.status);
|
|
5955
|
-
} else {
|
|
5956
|
-
return deps.badRequest("Configured task source does not support updates");
|
|
5957
|
-
}
|
|
5958
7073
|
const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
|
|
5959
7074
|
const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
|
|
5960
7075
|
taskId: id,
|
|
@@ -5963,6 +7078,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
5963
7078
|
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
5964
7079
|
config: ctx?.config
|
|
5965
7080
|
}).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
|
|
7081
|
+
if (update.status && githubProjectsEnabled2(ctx?.config) && projectSync.synced === false) {
|
|
7082
|
+
return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
|
|
7083
|
+
}
|
|
7084
|
+
try {
|
|
7085
|
+
if (source.updateTask) {
|
|
7086
|
+
await source.updateTask(id, update);
|
|
7087
|
+
} else if (update.status && source.updateStatus) {
|
|
7088
|
+
await source.updateStatus(id, update.status);
|
|
7089
|
+
}
|
|
7090
|
+
} catch (error) {
|
|
7091
|
+
let rollback = null;
|
|
7092
|
+
const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
|
|
7093
|
+
if (update.status && previousStatus && githubProjectsEnabled2(ctx?.config) && projectSync.synced !== false) {
|
|
7094
|
+
rollback = await syncGitHubProjectStatusForTaskUpdate({
|
|
7095
|
+
taskId: id,
|
|
7096
|
+
status: previousStatus,
|
|
7097
|
+
issueNodeId,
|
|
7098
|
+
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
7099
|
+
config: ctx?.config
|
|
7100
|
+
}).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
|
|
7101
|
+
}
|
|
7102
|
+
return deps.jsonResponse({
|
|
7103
|
+
ok: false,
|
|
7104
|
+
id,
|
|
7105
|
+
projectSync,
|
|
7106
|
+
rollback,
|
|
7107
|
+
error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
7108
|
+
}, 502);
|
|
7109
|
+
}
|
|
5966
7110
|
deps.snapshotService.invalidate("github-issue-updated");
|
|
5967
7111
|
await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
|
|
5968
7112
|
return;
|
|
@@ -5971,25 +7115,40 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
5971
7115
|
return deps.jsonResponse({ ok: true, id, projectSync });
|
|
5972
7116
|
}
|
|
5973
7117
|
if (url.pathname === "/api/workspace/task-labels") {
|
|
7118
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
|
|
7119
|
+
if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
|
|
7120
|
+
return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
|
|
7121
|
+
}
|
|
5974
7122
|
return deps.jsonResponse({
|
|
5975
7123
|
ok: true,
|
|
5976
7124
|
ready: true,
|
|
5977
7125
|
labelsReady: true,
|
|
5978
|
-
labels: [
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
7126
|
+
labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
|
|
7127
|
+
note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
|
|
7128
|
+
});
|
|
7129
|
+
}
|
|
7130
|
+
if (url.pathname === "/api/github/projects" && req.method === "GET") {
|
|
7131
|
+
const owner = normalizeString(url.searchParams.get("owner"));
|
|
7132
|
+
if (!owner)
|
|
7133
|
+
return deps.badRequest("owner is required");
|
|
7134
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
7135
|
+
if (!token)
|
|
7136
|
+
return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
|
|
7137
|
+
const projects = await listGitHubProjects({ owner, token }).catch((error) => {
|
|
7138
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
7139
|
+
});
|
|
7140
|
+
return deps.jsonResponse({ ok: true, projects });
|
|
7141
|
+
}
|
|
7142
|
+
const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
|
|
7143
|
+
if (projectStatusMatch && req.method === "GET") {
|
|
7144
|
+
const projectId = decodeURIComponent(projectStatusMatch[1]);
|
|
7145
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
7146
|
+
if (!token)
|
|
7147
|
+
return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
|
|
7148
|
+
const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
|
|
7149
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
5992
7150
|
});
|
|
7151
|
+
return deps.jsonResponse({ ok: true, field });
|
|
5993
7152
|
}
|
|
5994
7153
|
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
5995
7154
|
const body = await deps.readJsonBody(req);
|
|
@@ -6054,7 +7213,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6054
7213
|
}
|
|
6055
7214
|
if (url.pathname === "/api/server/status") {
|
|
6056
7215
|
const config = buildProjectConfigStatus(state.projectRoot);
|
|
6057
|
-
const taskSource = await buildTaskSourceStatus(state, config);
|
|
7216
|
+
const taskSource = await buildTaskSourceStatus(state, config, requestAuth);
|
|
6058
7217
|
return deps.jsonResponse({
|
|
6059
7218
|
ok: true,
|
|
6060
7219
|
projectRoot: state.projectRoot,
|
|
@@ -6078,8 +7237,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6078
7237
|
path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
|
|
6079
7238
|
ref: normalizeString(rawCheckout?.ref) ?? undefined
|
|
6080
7239
|
} : undefined;
|
|
6081
|
-
const
|
|
6082
|
-
|
|
7240
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7241
|
+
const record = upsertProjectRecord(registryRoot, { repoSlug, checkout });
|
|
7242
|
+
return deps.jsonResponse({ ok: true, project: record, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6083
7243
|
}
|
|
6084
7244
|
const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
|
|
6085
7245
|
if (snapshotUploadMatch && req.method === "POST") {
|
|
@@ -6092,8 +7252,15 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6092
7252
|
if (!archiveContentBase64) {
|
|
6093
7253
|
return deps.badRequest("archiveContentBase64 is required");
|
|
6094
7254
|
}
|
|
6095
|
-
const
|
|
6096
|
-
const
|
|
7255
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7256
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
7257
|
+
explicitBaseDir: normalizeString(body.baseDir),
|
|
7258
|
+
envName: "RIG_REMOTE_SNAPSHOT_BASE_DIR",
|
|
7259
|
+
userNamespace: requestAuth.userNamespace,
|
|
7260
|
+
legacyProjectRoot: state.projectRoot,
|
|
7261
|
+
legacySubdir: "remote-snapshots"
|
|
7262
|
+
});
|
|
7263
|
+
const checkoutKey = explicitCheckoutKey(body, body, requestAuth);
|
|
6097
7264
|
try {
|
|
6098
7265
|
const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
|
|
6099
7266
|
const checkout = extractUploadedSnapshotArchive({
|
|
@@ -6106,14 +7273,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6106
7273
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
6107
7274
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
6108
7275
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
6109
|
-
const project = linkProjectCheckout(
|
|
7276
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
6110
7277
|
kind: "uploaded-snapshot",
|
|
6111
7278
|
path: checkout.path,
|
|
6112
7279
|
ref: checkout.snapshotId
|
|
6113
7280
|
});
|
|
6114
7281
|
deps.snapshotService.invalidate("uploaded-snapshot-checkout");
|
|
6115
7282
|
deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
|
|
6116
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
7283
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6117
7284
|
} catch (error) {
|
|
6118
7285
|
return deps.jsonResponse({
|
|
6119
7286
|
ok: false,
|
|
@@ -6133,10 +7300,17 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6133
7300
|
if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
|
|
6134
7301
|
return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
|
|
6135
7302
|
}
|
|
6136
|
-
const
|
|
6137
|
-
const
|
|
7303
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7304
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
7305
|
+
explicitBaseDir: normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir),
|
|
7306
|
+
envName: "RIG_REMOTE_CHECKOUT_BASE_DIR",
|
|
7307
|
+
userNamespace: requestAuth.userNamespace,
|
|
7308
|
+
legacyProjectRoot: state.projectRoot,
|
|
7309
|
+
legacySubdir: "remote-checkouts"
|
|
7310
|
+
});
|
|
7311
|
+
const checkoutKey = explicitCheckoutKey(body, checkoutInput, requestAuth);
|
|
6138
7312
|
const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
|
|
6139
|
-
const credentialToken =
|
|
7313
|
+
const credentialToken = requestScopedAuthStore(state.projectRoot, requestAuth).readToken();
|
|
6140
7314
|
try {
|
|
6141
7315
|
const checkout = await prepareRemoteCheckout({
|
|
6142
7316
|
command: runRemoteCheckoutCommand,
|
|
@@ -6145,14 +7319,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6145
7319
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
6146
7320
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
6147
7321
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
6148
|
-
const project = linkProjectCheckout(
|
|
7322
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
6149
7323
|
kind: checkout.kind,
|
|
6150
7324
|
path: checkout.path,
|
|
6151
7325
|
ref: checkout.ref ?? checkout.snapshotId ?? undefined
|
|
6152
7326
|
});
|
|
6153
7327
|
deps.snapshotService.invalidate("remote-checkout-prepared");
|
|
6154
7328
|
deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
|
|
6155
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
7329
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6156
7330
|
} catch (error) {
|
|
6157
7331
|
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
6158
7332
|
}
|
|
@@ -6169,16 +7343,18 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6169
7343
|
if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
|
|
6170
7344
|
return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
|
|
6171
7345
|
}
|
|
6172
|
-
const
|
|
7346
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7347
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
6173
7348
|
kind,
|
|
6174
7349
|
path: normalizeString(body.path) ?? state.projectRoot,
|
|
6175
7350
|
ref: normalizeString(body.ref) ?? undefined
|
|
6176
7351
|
});
|
|
6177
|
-
return deps.jsonResponse({ ok: true, project });
|
|
7352
|
+
return deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6178
7353
|
}
|
|
6179
7354
|
if (req.method === "GET") {
|
|
6180
|
-
const
|
|
6181
|
-
|
|
7355
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7356
|
+
const project = getProjectRecord(registryRoot, repoSlug);
|
|
7357
|
+
return project ? deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) }) : deps.notFound();
|
|
6182
7358
|
}
|
|
6183
7359
|
}
|
|
6184
7360
|
if (url.pathname === "/api/server/project-root" && req.method === "POST") {
|
|
@@ -6187,13 +7363,26 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6187
7363
|
if (!requestedRoot) {
|
|
6188
7364
|
return deps.badRequest("projectRoot is required");
|
|
6189
7365
|
}
|
|
6190
|
-
if (!
|
|
7366
|
+
if (!isAbsolute4(requestedRoot)) {
|
|
6191
7367
|
return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
|
|
6192
7368
|
}
|
|
6193
|
-
const normalizedRoot =
|
|
6194
|
-
const exists =
|
|
6195
|
-
if (exists) {
|
|
6196
|
-
|
|
7369
|
+
const normalizedRoot = resolve20(requestedRoot);
|
|
7370
|
+
const exists = existsSync13(normalizedRoot);
|
|
7371
|
+
if (exists && requestAuth.userNamespace) {
|
|
7372
|
+
const allowedByNamespace = isPathInsideNamespace(requestAuth.userNamespace.root, normalizedRoot);
|
|
7373
|
+
const allowedByRegistry = projectRegistryContainsCheckout(requestAuth.userNamespace.root, normalizedRoot);
|
|
7374
|
+
if (!allowedByNamespace && !allowedByRegistry) {
|
|
7375
|
+
return deps.jsonResponse({
|
|
7376
|
+
ok: false,
|
|
7377
|
+
error: "Requested project root is outside the authenticated GitHub user namespace.",
|
|
7378
|
+
projectRoot: state.projectRoot,
|
|
7379
|
+
requestedProjectRoot: normalizedRoot,
|
|
7380
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace)
|
|
7381
|
+
}, 403);
|
|
7382
|
+
}
|
|
7383
|
+
copyGitHubAuthStateToLocalProjectRoot(requestAuth.userNamespace.authStateFile, normalizedRoot);
|
|
7384
|
+
} else if (exists) {
|
|
7385
|
+
createGitHubAuthStore(state.projectRoot).copyToLocalProjectRoot(normalizedRoot);
|
|
6197
7386
|
}
|
|
6198
7387
|
const control = buildServerControlStatus();
|
|
6199
7388
|
const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
|
|
@@ -6208,7 +7397,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6208
7397
|
message: "Requested project root does not exist on the Rig server host."
|
|
6209
7398
|
}, 404);
|
|
6210
7399
|
}
|
|
6211
|
-
if (!
|
|
7400
|
+
if (!existsSync13(resolve20(normalizedRoot, "rig.config.ts")) && !existsSync13(resolve20(normalizedRoot, "rig.config.json"))) {
|
|
6212
7401
|
return deps.jsonResponse({
|
|
6213
7402
|
ok: false,
|
|
6214
7403
|
projectRoot: state.projectRoot,
|
|
@@ -6243,6 +7432,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6243
7432
|
exists,
|
|
6244
7433
|
control,
|
|
6245
7434
|
requiresRestart: false,
|
|
7435
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace),
|
|
6246
7436
|
message: "Project-root switch accepted. Rig server restart has been scheduled."
|
|
6247
7437
|
}, 202);
|
|
6248
7438
|
}
|
|
@@ -6257,11 +7447,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6257
7447
|
}, 409);
|
|
6258
7448
|
}
|
|
6259
7449
|
if (url.pathname === "/api/github/auth/status") {
|
|
6260
|
-
const store =
|
|
6261
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
|
|
7450
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
7451
|
+
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6262
7452
|
}
|
|
6263
7453
|
if (url.pathname === "/api/github/repo/permissions") {
|
|
6264
|
-
const store =
|
|
7454
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
6265
7455
|
const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
6266
7456
|
if (!auth.signedIn) {
|
|
6267
7457
|
return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
|
|
@@ -6289,24 +7479,20 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6289
7479
|
}
|
|
6290
7480
|
try {
|
|
6291
7481
|
const user = await fetchGitHubUserInfo(token);
|
|
6292
|
-
const
|
|
6293
|
-
state.projectRoot,
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
}
|
|
6307
|
-
const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
|
|
6308
|
-
const apiSession = store.createApiSession();
|
|
6309
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
|
|
7482
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
7483
|
+
projectRoot: state.projectRoot,
|
|
7484
|
+
token,
|
|
7485
|
+
tokenSource: "manual-token",
|
|
7486
|
+
user,
|
|
7487
|
+
selectedRepo,
|
|
7488
|
+
requestedProjectRoot
|
|
7489
|
+
});
|
|
7490
|
+
return deps.jsonResponse({
|
|
7491
|
+
ok: true,
|
|
7492
|
+
...saved.store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }),
|
|
7493
|
+
apiSessionToken: saved.apiSessionToken,
|
|
7494
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
7495
|
+
});
|
|
6310
7496
|
} catch (error) {
|
|
6311
7497
|
const message = error instanceof Error ? error.message : String(error);
|
|
6312
7498
|
return deps.jsonResponse({ ok: false, error: message }, 400);
|
|
@@ -6373,9 +7559,21 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6373
7559
|
}
|
|
6374
7560
|
const token = result.payload.access_token;
|
|
6375
7561
|
const user = await fetchGitHubUserInfo(token);
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
7562
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
7563
|
+
projectRoot: state.projectRoot,
|
|
7564
|
+
token,
|
|
7565
|
+
tokenSource: "oauth-device",
|
|
7566
|
+
user,
|
|
7567
|
+
selectedRepo: null
|
|
7568
|
+
});
|
|
7569
|
+
store.clearPendingDevice(pollId);
|
|
7570
|
+
return deps.jsonResponse({
|
|
7571
|
+
ok: true,
|
|
7572
|
+
status: "signed-in",
|
|
7573
|
+
...saved.store.status({ oauthConfigured: true }),
|
|
7574
|
+
apiSessionToken: saved.apiSessionToken,
|
|
7575
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
7576
|
+
});
|
|
6379
7577
|
}
|
|
6380
7578
|
if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
|
|
6381
7579
|
const body = await deps.readJsonBody(req);
|
|
@@ -6384,7 +7582,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6384
7582
|
if (!owner || !repo) {
|
|
6385
7583
|
return deps.badRequest("owner and repo are required");
|
|
6386
7584
|
}
|
|
6387
|
-
const store =
|
|
7585
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
6388
7586
|
const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
6389
7587
|
const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
|
|
6390
7588
|
return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
|
|
@@ -6405,7 +7603,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6405
7603
|
return deps.badRequest(error instanceof Error ? error.message : String(error));
|
|
6406
7604
|
}
|
|
6407
7605
|
const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
6408
|
-
const configPath =
|
|
7606
|
+
const configPath = resolve20(targetRoot, "rig.config.ts");
|
|
6409
7607
|
const source = buildGitHubProjectConfigSource({
|
|
6410
7608
|
projectName: rawProjectName,
|
|
6411
7609
|
owner,
|
|
@@ -6417,8 +7615,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6417
7615
|
ok: true,
|
|
6418
7616
|
projectRoot: targetRoot,
|
|
6419
7617
|
configPath,
|
|
6420
|
-
exists:
|
|
6421
|
-
requiresOverwrite:
|
|
7618
|
+
exists: existsSync13(configPath),
|
|
7619
|
+
requiresOverwrite: existsSync13(configPath),
|
|
6422
7620
|
source,
|
|
6423
7621
|
owner,
|
|
6424
7622
|
repo,
|
|
@@ -6454,8 +7652,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6454
7652
|
assignee,
|
|
6455
7653
|
githubUserId: authStatus.userId ?? authStatus.login
|
|
6456
7654
|
});
|
|
6457
|
-
const configPath =
|
|
6458
|
-
if (
|
|
7655
|
+
const configPath = resolve20(targetRoot, "rig.config.ts");
|
|
7656
|
+
if (existsSync13(configPath) && !overwrite) {
|
|
6459
7657
|
return deps.jsonResponse({
|
|
6460
7658
|
ok: false,
|
|
6461
7659
|
error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
|
|
@@ -6471,11 +7669,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6471
7669
|
return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
|
|
6472
7670
|
}
|
|
6473
7671
|
let backupPath = null;
|
|
6474
|
-
if (
|
|
7672
|
+
if (existsSync13(configPath)) {
|
|
6475
7673
|
backupPath = backupConfigPath(configPath);
|
|
6476
|
-
|
|
7674
|
+
copyFileSync2(configPath, backupPath);
|
|
6477
7675
|
}
|
|
6478
|
-
|
|
7676
|
+
writeFileSync12(configPath, source, "utf8");
|
|
6479
7677
|
const selectedRepo = `${owner}/${repo}`;
|
|
6480
7678
|
store.saveSelectedRepo(selectedRepo);
|
|
6481
7679
|
const targetStore = createGitHubAuthStore(targetRoot);
|
|
@@ -6747,11 +7945,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6747
7945
|
const runId = normalizeString(body.runId);
|
|
6748
7946
|
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
6749
7947
|
const promptOverride = normalizeString(body.promptOverride);
|
|
7948
|
+
const restart = body.restart === true;
|
|
6750
7949
|
if (!runId) {
|
|
6751
7950
|
return deps.badRequest("runId is required");
|
|
6752
7951
|
}
|
|
6753
7952
|
try {
|
|
6754
|
-
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
|
|
7953
|
+
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
|
|
6755
7954
|
deps.broadcastSnapshotInvalidation(state);
|
|
6756
7955
|
return deps.jsonResponse({ ok: true, runId, createdAt });
|
|
6757
7956
|
} catch (error) {
|
|
@@ -6760,7 +7959,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6760
7959
|
}
|
|
6761
7960
|
if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
|
|
6762
7961
|
const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
|
|
6763
|
-
const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) =>
|
|
7962
|
+
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";
|
|
6764
7963
|
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
6765
7964
|
return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
|
|
6766
7965
|
}
|
|
@@ -7076,9 +8275,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7076
8275
|
} catch {
|
|
7077
8276
|
return deps.badRequest("Invalid artifact path");
|
|
7078
8277
|
}
|
|
7079
|
-
|
|
8278
|
+
mkdirSync13(dirname15(artifactPath), { recursive: true });
|
|
7080
8279
|
const bytes = Buffer.from(contentBase64, "base64");
|
|
7081
|
-
|
|
8280
|
+
writeFileSync12(artifactPath, bytes);
|
|
7082
8281
|
writeJsonFile4(`${artifactPath}.json`, {
|
|
7083
8282
|
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
7084
8283
|
runId,
|
|
@@ -7115,13 +8314,75 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7115
8314
|
}
|
|
7116
8315
|
const run = leaseValidation.run;
|
|
7117
8316
|
const completedAt = new Date().toISOString();
|
|
8317
|
+
const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
|
|
8318
|
+
if (run.taskId && workspaceDir) {
|
|
8319
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
8320
|
+
status: "reviewing",
|
|
8321
|
+
completedAt: null,
|
|
8322
|
+
hostId,
|
|
8323
|
+
endpointId: leaseId,
|
|
8324
|
+
worktreePath: workspaceDir,
|
|
8325
|
+
serverCloseout: {
|
|
8326
|
+
status: "pending",
|
|
8327
|
+
phase: "queued",
|
|
8328
|
+
requestedAt: completedAt,
|
|
8329
|
+
updatedAt: completedAt,
|
|
8330
|
+
runtimeWorkspace: workspaceDir,
|
|
8331
|
+
branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
|
|
8332
|
+
taskId: run.taskId,
|
|
8333
|
+
source: "remote-complete"
|
|
8334
|
+
}
|
|
8335
|
+
});
|
|
8336
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
8337
|
+
id: `log:${runId}:remote-server-closeout-requested`,
|
|
8338
|
+
title: "Server-owned closeout requested",
|
|
8339
|
+
detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
|
|
8340
|
+
tone: "info",
|
|
8341
|
+
status: "reviewing",
|
|
8342
|
+
createdAt: completedAt,
|
|
8343
|
+
payload: { workspaceDir, hostId, leaseId }
|
|
8344
|
+
}, "remote-server-closeout-requested");
|
|
8345
|
+
deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
|
|
8346
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8347
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
8348
|
+
status: "failed",
|
|
8349
|
+
completedAt: new Date().toISOString(),
|
|
8350
|
+
errorText: detail,
|
|
8351
|
+
serverCloseout: {
|
|
8352
|
+
status: "failed",
|
|
8353
|
+
phase: "failed",
|
|
8354
|
+
updatedAt: new Date().toISOString(),
|
|
8355
|
+
error: detail
|
|
8356
|
+
}
|
|
8357
|
+
});
|
|
8358
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
8359
|
+
id: `log:${runId}:remote-server-closeout-failed`,
|
|
8360
|
+
title: "Server-owned closeout failed",
|
|
8361
|
+
detail,
|
|
8362
|
+
tone: "error",
|
|
8363
|
+
status: "failed",
|
|
8364
|
+
createdAt: new Date().toISOString()
|
|
8365
|
+
}, "remote-server-closeout-failed");
|
|
8366
|
+
}).finally(() => {
|
|
8367
|
+
deps.reconcileScheduler(state, "remote-server-closeout-terminal");
|
|
8368
|
+
});
|
|
8369
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
8370
|
+
return deps.jsonResponse({
|
|
8371
|
+
ok: true,
|
|
8372
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
8373
|
+
hostId,
|
|
8374
|
+
runId,
|
|
8375
|
+
leaseId,
|
|
8376
|
+
closeout: "server-owned",
|
|
8377
|
+
acceptedAt: new Date().toISOString()
|
|
8378
|
+
});
|
|
8379
|
+
}
|
|
7118
8380
|
patchRunRecord(state.projectRoot, runId, {
|
|
7119
8381
|
status: "completed",
|
|
7120
8382
|
completedAt,
|
|
7121
8383
|
hostId,
|
|
7122
8384
|
endpointId: leaseId
|
|
7123
8385
|
});
|
|
7124
|
-
await updateRemoteRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "completed", completedAt, hostId, endpointId: leaseId }, "closed", "Remote Rig task run completed and closed this task.");
|
|
7125
8386
|
await deps.enqueueRunLinearEvent(state.projectRoot, {
|
|
7126
8387
|
type: "run.completed",
|
|
7127
8388
|
runId,
|
|
@@ -7240,12 +8501,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7240
8501
|
try {
|
|
7241
8502
|
const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
|
|
7242
8503
|
const runRoot = deps.normalizeRelativePath(runsRoot, runId);
|
|
7243
|
-
const artifactsRoot =
|
|
8504
|
+
const artifactsRoot = resolve20(runRoot, "remote-artifacts");
|
|
7244
8505
|
artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
|
|
7245
8506
|
} catch {
|
|
7246
8507
|
return deps.badRequest("Invalid artifact path");
|
|
7247
8508
|
}
|
|
7248
|
-
if (!
|
|
8509
|
+
if (!existsSync13(artifactPath)) {
|
|
7249
8510
|
return deps.notFound();
|
|
7250
8511
|
}
|
|
7251
8512
|
return new Response(Bun.file(artifactPath));
|
|
@@ -7258,6 +8519,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7258
8519
|
const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
|
|
7259
8520
|
return deps.jsonResponse(page);
|
|
7260
8521
|
}
|
|
8522
|
+
const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
|
|
8523
|
+
if (runTimelineMatch) {
|
|
8524
|
+
const runId = decodeURIComponent(runTimelineMatch[1]);
|
|
8525
|
+
const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
|
|
8526
|
+
const cursor = normalizeString(url.searchParams.get("cursor"));
|
|
8527
|
+
const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
|
|
8528
|
+
return deps.jsonResponse(page);
|
|
8529
|
+
}
|
|
7261
8530
|
const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
|
|
7262
8531
|
if (runSteerMatch && req.method === "POST") {
|
|
7263
8532
|
const runId = decodeURIComponent(runSteerMatch[1]);
|
|
@@ -7277,6 +8546,49 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7277
8546
|
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 404);
|
|
7278
8547
|
}
|
|
7279
8548
|
}
|
|
8549
|
+
const runPiMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/pi(?:\/(.*))?$/);
|
|
8550
|
+
if (runPiMatch) {
|
|
8551
|
+
const runId = decodeURIComponent(runPiMatch[1]);
|
|
8552
|
+
const action = runPiMatch[2] || "";
|
|
8553
|
+
const resolved = resolveRunPiSessionProxy(state.projectRoot, runId);
|
|
8554
|
+
if (resolved === null)
|
|
8555
|
+
return deps.notFound();
|
|
8556
|
+
if ("pending" in resolved) {
|
|
8557
|
+
if (action === "" && req.method === "GET") {
|
|
8558
|
+
return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500 }, 202);
|
|
8559
|
+
}
|
|
8560
|
+
return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500, error: "Pi session is not ready" }, 409);
|
|
8561
|
+
}
|
|
8562
|
+
if (action === "" && req.method === "GET")
|
|
8563
|
+
return deps.jsonResponse({ ready: true, metadata: resolved.metadata });
|
|
8564
|
+
const body = req.method === "GET" ? undefined : await deps.readJsonBody(req);
|
|
8565
|
+
const sessionPath = `/sessions/${encodeURIComponent(resolved.sessionId)}`;
|
|
8566
|
+
const daemonPath = (() => {
|
|
8567
|
+
if (action === "messages" && req.method === "GET")
|
|
8568
|
+
return `${sessionPath}/messages`;
|
|
8569
|
+
if (action === "status" && req.method === "GET")
|
|
8570
|
+
return `${sessionPath}/status`;
|
|
8571
|
+
if (action === "commands" && req.method === "GET")
|
|
8572
|
+
return `${sessionPath}/commands`;
|
|
8573
|
+
if (action === "prompt" && req.method === "POST")
|
|
8574
|
+
return `${sessionPath}/prompt`;
|
|
8575
|
+
if (action === "shell" && req.method === "POST")
|
|
8576
|
+
return `${sessionPath}/shell`;
|
|
8577
|
+
if (action === "commands/run" && req.method === "POST")
|
|
8578
|
+
return `${sessionPath}/commands/run`;
|
|
8579
|
+
if (action === "commands/respond" && req.method === "POST")
|
|
8580
|
+
return `${sessionPath}/commands/respond`;
|
|
8581
|
+
if (action === "extension-ui/respond" && req.method === "POST")
|
|
8582
|
+
return `${sessionPath}/extension-ui/respond`;
|
|
8583
|
+
if (action === "abort" && req.method === "POST")
|
|
8584
|
+
return `${sessionPath}/abort`;
|
|
8585
|
+
return null;
|
|
8586
|
+
})();
|
|
8587
|
+
if (!daemonPath)
|
|
8588
|
+
return deps.notFound();
|
|
8589
|
+
const proxied = await proxyRunPiHttp(state.projectRoot, runId, { method: req.method, daemonPath, body });
|
|
8590
|
+
return deps.jsonResponse(proxied.payload, proxied.status);
|
|
8591
|
+
}
|
|
7280
8592
|
const runSteeringAckMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steering\/ack$/);
|
|
7281
8593
|
if (runSteeringAckMatch && req.method === "POST") {
|
|
7282
8594
|
const runId = decodeURIComponent(runSteeringAckMatch[1]);
|
|
@@ -7399,7 +8711,7 @@ import {
|
|
|
7399
8711
|
RemoteWsClient as RemoteWsClient2
|
|
7400
8712
|
} from "@rig/runtime/control-plane/remote";
|
|
7401
8713
|
import { deleteRunState } from "@rig/runtime/control-plane/native/run-ops";
|
|
7402
|
-
import { readAuthorityRun as
|
|
8714
|
+
import { readAuthorityRun as readAuthorityRun8 } from "@rig/runtime/control-plane/authority-files";
|
|
7403
8715
|
function redactRemoteEndpoint2(endpoint) {
|
|
7404
8716
|
const { token, ...rest } = endpoint;
|
|
7405
8717
|
return {
|
|
@@ -7613,7 +8925,7 @@ async function routeWebSocketRequest(state, deps, request) {
|
|
|
7613
8925
|
if (!runId || !messageId || text === null) {
|
|
7614
8926
|
throw new Error("runId, messageId, and text are required");
|
|
7615
8927
|
}
|
|
7616
|
-
const run =
|
|
8928
|
+
const run = readAuthorityRun8(state.projectRoot, runId);
|
|
7617
8929
|
if (!run) {
|
|
7618
8930
|
throw new Error(`Run not found: ${runId}`);
|
|
7619
8931
|
}
|
|
@@ -8138,8 +9450,8 @@ async function routeWebSocketRequest(state, deps, request) {
|
|
|
8138
9450
|
}
|
|
8139
9451
|
|
|
8140
9452
|
// packages/server/src/server-helpers/inspector-jobs.ts
|
|
8141
|
-
import { existsSync as
|
|
8142
|
-
import { dirname as
|
|
9453
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync16, readFileSync as readFileSync12, writeFileSync as writeFileSync15 } from "fs";
|
|
9454
|
+
import { dirname as dirname18, resolve as resolve23 } from "path";
|
|
8143
9455
|
import { readJsonFile as readJsonFile3 } from "@rig/runtime/control-plane/authority-files";
|
|
8144
9456
|
import { resolveMonorepoRoot as resolveMonorepoRoot5 } from "@rig/runtime/control-plane/native/utils";
|
|
8145
9457
|
import { normalizeTaskLifecycleStatus as normalizeTaskLifecycleStatus2 } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -8247,8 +9559,8 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
8247
9559
|
|
|
8248
9560
|
// packages/server/src/inspector/mission.ts
|
|
8249
9561
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8250
|
-
import { appendFileSync, existsSync as
|
|
8251
|
-
import { dirname as
|
|
9562
|
+
import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync14, readFileSync as readFileSync10, readdirSync as readdirSync4, renameSync, writeFileSync as writeFileSync13 } from "fs";
|
|
9563
|
+
import { dirname as dirname16, join, resolve as resolve21 } from "path";
|
|
8252
9564
|
function isJsonValue(value) {
|
|
8253
9565
|
if (value === null)
|
|
8254
9566
|
return true;
|
|
@@ -8288,7 +9600,7 @@ function isRecord2(value) {
|
|
|
8288
9600
|
}
|
|
8289
9601
|
function readJsonRecord(path) {
|
|
8290
9602
|
try {
|
|
8291
|
-
const parsed = JSON.parse(
|
|
9603
|
+
const parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
8292
9604
|
if (!isRecord2(parsed)) {
|
|
8293
9605
|
return { ok: false, error: `Mission file ${path} does not contain an object` };
|
|
8294
9606
|
}
|
|
@@ -8368,14 +9680,14 @@ function missionActionDetails(mission) {
|
|
|
8368
9680
|
};
|
|
8369
9681
|
}
|
|
8370
9682
|
function writeJsonFile5(path, value) {
|
|
8371
|
-
|
|
9683
|
+
mkdirSync14(dirname16(path), { recursive: true });
|
|
8372
9684
|
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
8373
|
-
|
|
9685
|
+
writeFileSync13(tempPath, `${JSON.stringify(value, null, 2)}
|
|
8374
9686
|
`, "utf8");
|
|
8375
9687
|
renameSync(tempPath, path);
|
|
8376
9688
|
}
|
|
8377
9689
|
function resolveInspectorMissionPaths(projectRoot) {
|
|
8378
|
-
const inspectorDir =
|
|
9690
|
+
const inspectorDir = resolve21(resolveRigServerPaths(projectRoot).stateDir, "inspector");
|
|
8379
9691
|
return {
|
|
8380
9692
|
inspectorDir,
|
|
8381
9693
|
missionsDir: join(inspectorDir, "missions"),
|
|
@@ -8384,8 +9696,8 @@ function resolveInspectorMissionPaths(projectRoot) {
|
|
|
8384
9696
|
}
|
|
8385
9697
|
function createInspectorMissionController(options) {
|
|
8386
9698
|
const paths = resolveInspectorMissionPaths(options.projectRoot);
|
|
8387
|
-
|
|
8388
|
-
|
|
9699
|
+
mkdirSync14(paths.missionsDir, { recursive: true });
|
|
9700
|
+
mkdirSync14(paths.journalsDir, { recursive: true });
|
|
8389
9701
|
const now = options.now ?? (() => new Date().toISOString());
|
|
8390
9702
|
const nextId = options.idGenerator ?? (() => `mission:${randomUUID2()}`);
|
|
8391
9703
|
function missionPath(missionId) {
|
|
@@ -8395,15 +9707,15 @@ function createInspectorMissionController(options) {
|
|
|
8395
9707
|
return join(paths.journalsDir, `${missionId}.jsonl`);
|
|
8396
9708
|
}
|
|
8397
9709
|
function appendMissionJournal(entry) {
|
|
8398
|
-
|
|
9710
|
+
mkdirSync14(paths.journalsDir, { recursive: true });
|
|
8399
9711
|
appendFileSync(journalPath(entry.missionId), `${JSON.stringify(entry)}
|
|
8400
9712
|
`, "utf8");
|
|
8401
9713
|
}
|
|
8402
9714
|
function listMissionJournal(missionId) {
|
|
8403
9715
|
const path = journalPath(missionId);
|
|
8404
|
-
if (!
|
|
9716
|
+
if (!existsSync14(path))
|
|
8405
9717
|
return [];
|
|
8406
|
-
return
|
|
9718
|
+
return readFileSync10(path, "utf8").split(`
|
|
8407
9719
|
`).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line)).filter(isRecord2).map((entry) => ({
|
|
8408
9720
|
id: typeof entry.id === "string" ? entry.id : `journal:${randomUUID2()}`,
|
|
8409
9721
|
missionId,
|
|
@@ -8419,7 +9731,7 @@ function createInspectorMissionController(options) {
|
|
|
8419
9731
|
}
|
|
8420
9732
|
function readMissionOnly(missionId) {
|
|
8421
9733
|
const path = missionPath(missionId);
|
|
8422
|
-
if (!
|
|
9734
|
+
if (!existsSync14(path)) {
|
|
8423
9735
|
return { ok: false, error: `Mission ${missionId} was not found` };
|
|
8424
9736
|
}
|
|
8425
9737
|
const read = readJsonRecord(path);
|
|
@@ -8470,7 +9782,7 @@ function createInspectorMissionController(options) {
|
|
|
8470
9782
|
const source = cloneJsonRecord(input.sourceTask);
|
|
8471
9783
|
const missionId = nextId();
|
|
8472
9784
|
const path = missionPath(missionId);
|
|
8473
|
-
if (
|
|
9785
|
+
if (existsSync14(path)) {
|
|
8474
9786
|
const existing = readMissionOnly(missionId);
|
|
8475
9787
|
if (!existing.ok)
|
|
8476
9788
|
return existing;
|
|
@@ -10070,8 +11382,8 @@ function createCodexInspectorTransport(options) {
|
|
|
10070
11382
|
const sendRequest = async (method, params) => {
|
|
10071
11383
|
const id = nextRequestId;
|
|
10072
11384
|
nextRequestId += 1;
|
|
10073
|
-
const response = new Promise((
|
|
10074
|
-
pendingResponses.set(id, { resolve:
|
|
11385
|
+
const response = new Promise((resolve22, reject) => {
|
|
11386
|
+
pendingResponses.set(id, { resolve: resolve22, reject });
|
|
10075
11387
|
});
|
|
10076
11388
|
response.catch(() => {});
|
|
10077
11389
|
try {
|
|
@@ -10381,9 +11693,9 @@ function createCodexInspectorTransport(options) {
|
|
|
10381
11693
|
}
|
|
10382
11694
|
lastAssistantMessage = null;
|
|
10383
11695
|
lastError = null;
|
|
10384
|
-
const turnResult = new Promise((
|
|
11696
|
+
const turnResult = new Promise((resolve22, reject) => {
|
|
10385
11697
|
currentTurn = {
|
|
10386
|
-
resolve:
|
|
11698
|
+
resolve: resolve22,
|
|
10387
11699
|
reject,
|
|
10388
11700
|
events: []
|
|
10389
11701
|
};
|
|
@@ -10443,13 +11755,13 @@ function createCodexInspectorTransport(options) {
|
|
|
10443
11755
|
};
|
|
10444
11756
|
}
|
|
10445
11757
|
function writeChildLine(child, line) {
|
|
10446
|
-
return new Promise((
|
|
11758
|
+
return new Promise((resolve22, reject) => {
|
|
10447
11759
|
child.stdin.write(line, (error) => {
|
|
10448
11760
|
if (error) {
|
|
10449
11761
|
reject(error);
|
|
10450
11762
|
return;
|
|
10451
11763
|
}
|
|
10452
|
-
|
|
11764
|
+
resolve22();
|
|
10453
11765
|
});
|
|
10454
11766
|
});
|
|
10455
11767
|
}
|
|
@@ -10462,10 +11774,10 @@ function terminateChild(child) {
|
|
|
10462
11774
|
} catch {}
|
|
10463
11775
|
}
|
|
10464
11776
|
async function waitForChildSpawn(child) {
|
|
10465
|
-
await new Promise((
|
|
11777
|
+
await new Promise((resolve22, reject) => {
|
|
10466
11778
|
const onSpawn = () => {
|
|
10467
11779
|
cleanup();
|
|
10468
|
-
|
|
11780
|
+
resolve22();
|
|
10469
11781
|
};
|
|
10470
11782
|
const onError = (error) => {
|
|
10471
11783
|
cleanup();
|
|
@@ -10574,7 +11886,7 @@ import {
|
|
|
10574
11886
|
} from "@rig/runtime/control-plane/native/run-ops";
|
|
10575
11887
|
import {
|
|
10576
11888
|
listAuthorityRuns as listAuthorityRuns6,
|
|
10577
|
-
readAuthorityRun as
|
|
11889
|
+
readAuthorityRun as readAuthorityRun9
|
|
10578
11890
|
} from "@rig/runtime/control-plane/authority-files";
|
|
10579
11891
|
function providerFromRuntimeAdapter(runtimeAdapter) {
|
|
10580
11892
|
if (!runtimeAdapter) {
|
|
@@ -10654,7 +11966,7 @@ function discoverInspectorRuns(options) {
|
|
|
10654
11966
|
discovered.set(surface.runId, existing);
|
|
10655
11967
|
};
|
|
10656
11968
|
for (const authorityEntry of listAuthorityRuns6(options.projectRoot)) {
|
|
10657
|
-
const run =
|
|
11969
|
+
const run = readAuthorityRun9(options.projectRoot, authorityEntry.runId);
|
|
10658
11970
|
if (!run) {
|
|
10659
11971
|
continue;
|
|
10660
11972
|
}
|
|
@@ -10977,8 +12289,8 @@ function createGlobalInspectorService(options) {
|
|
|
10977
12289
|
|
|
10978
12290
|
// packages/server/src/inspector/upstream-sync.ts
|
|
10979
12291
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
10980
|
-
import { existsSync as
|
|
10981
|
-
import { dirname as
|
|
12292
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync14 } from "fs";
|
|
12293
|
+
import { dirname as dirname17, resolve as resolve22 } from "path";
|
|
10982
12294
|
import { resolveMonorepoRoot as resolveMonorepoRoot4 } from "@rig/runtime/control-plane/native/utils";
|
|
10983
12295
|
var UPSTREAM_VALIDATION_DESCRIPTIONS = {
|
|
10984
12296
|
"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.",
|
|
@@ -11116,34 +12428,34 @@ function defaultGitRunner(repoRoot, args) {
|
|
|
11116
12428
|
}
|
|
11117
12429
|
function upstreamStatePath(projectRoot, override) {
|
|
11118
12430
|
if (override) {
|
|
11119
|
-
return
|
|
12431
|
+
return resolve22(override);
|
|
11120
12432
|
}
|
|
11121
|
-
return
|
|
12433
|
+
return resolve22(resolveRigServerPaths(projectRoot).stateDir, "inspector", "upstream-sync.json");
|
|
11122
12434
|
}
|
|
11123
12435
|
function readUpstreamState(projectRoot, statePath) {
|
|
11124
12436
|
const path = upstreamStatePath(projectRoot, statePath);
|
|
11125
|
-
if (!
|
|
12437
|
+
if (!existsSync15(path)) {
|
|
11126
12438
|
return null;
|
|
11127
12439
|
}
|
|
11128
12440
|
try {
|
|
11129
|
-
return JSON.parse(
|
|
12441
|
+
return JSON.parse(readFileSync11(path, "utf-8"));
|
|
11130
12442
|
} catch {
|
|
11131
12443
|
return null;
|
|
11132
12444
|
}
|
|
11133
12445
|
}
|
|
11134
12446
|
function writeUpstreamState(projectRoot, state, statePath) {
|
|
11135
12447
|
const path = upstreamStatePath(projectRoot, statePath);
|
|
11136
|
-
|
|
11137
|
-
|
|
12448
|
+
mkdirSync15(dirname17(path), { recursive: true });
|
|
12449
|
+
writeFileSync14(path, `${JSON.stringify(state, null, 2)}
|
|
11138
12450
|
`, "utf8");
|
|
11139
12451
|
}
|
|
11140
12452
|
function readImportedRevision(projectRoot, upstreamsDocPath) {
|
|
11141
12453
|
const monorepoRoot = resolveMonorepoRoot4(projectRoot);
|
|
11142
|
-
const docPath = upstreamsDocPath ?
|
|
11143
|
-
if (!
|
|
12454
|
+
const docPath = upstreamsDocPath ? resolve22(upstreamsDocPath) : resolve22(monorepoRoot, "docs", "UPSTREAMS.md");
|
|
12455
|
+
if (!existsSync15(docPath)) {
|
|
11144
12456
|
throw new Error(`UPSTREAMS.md not found at ${docPath}`);
|
|
11145
12457
|
}
|
|
11146
|
-
const docContent =
|
|
12458
|
+
const docContent = readFileSync11(docPath, "utf-8");
|
|
11147
12459
|
const revision = parseImportedUpstreamRevision(docContent, "upstream") ?? parseImportedUpstreamRevision(docContent, "humoongate");
|
|
11148
12460
|
if (!revision) {
|
|
11149
12461
|
throw new Error(`Failed to parse upstream imported revision from ${docPath}`);
|
|
@@ -11165,7 +12477,7 @@ function resolveRemoteBranch(repoRoot, remote, gitRunner) {
|
|
|
11165
12477
|
return null;
|
|
11166
12478
|
}
|
|
11167
12479
|
function isGitCheckout(path, gitRunner) {
|
|
11168
|
-
if (!
|
|
12480
|
+
if (!existsSync15(resolve22(path, ".git"))) {
|
|
11169
12481
|
return false;
|
|
11170
12482
|
}
|
|
11171
12483
|
const result = gitRunner(path, ["rev-parse", "--is-inside-work-tree"]);
|
|
@@ -11174,12 +12486,12 @@ function isGitCheckout(path, gitRunner) {
|
|
|
11174
12486
|
function resolveUpstreamCheckout(projectRoot, explicitCheckout, gitRunner) {
|
|
11175
12487
|
const monorepoRoot = resolveMonorepoRoot4(projectRoot);
|
|
11176
12488
|
const candidates = [
|
|
11177
|
-
explicitCheckout ?
|
|
11178
|
-
process.env.UPSTREAM_CHECKOUT?.trim() ?
|
|
11179
|
-
process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ?
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
12489
|
+
explicitCheckout ? resolve22(explicitCheckout) : "",
|
|
12490
|
+
process.env.UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.UPSTREAM_CHECKOUT.trim()) : "",
|
|
12491
|
+
process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.HUMOONGATE_UPSTREAM_CHECKOUT.trim()) : "",
|
|
12492
|
+
resolve22(projectRoot, "..", "humoongate"),
|
|
12493
|
+
resolve22(monorepoRoot, "..", "humoongate"),
|
|
12494
|
+
resolve22(monorepoRoot, "humoongate")
|
|
11183
12495
|
].filter(Boolean);
|
|
11184
12496
|
for (const candidate of candidates) {
|
|
11185
12497
|
if (isGitCheckout(candidate, gitRunner)) {
|
|
@@ -11415,10 +12727,10 @@ async function runUpstreamSyncScan(options) {
|
|
|
11415
12727
|
}
|
|
11416
12728
|
|
|
11417
12729
|
// packages/server/src/server-helpers/task-config.ts
|
|
11418
|
-
import { existsSync as
|
|
12730
|
+
import { existsSync as existsSync16 } from "fs";
|
|
11419
12731
|
async function readTaskConfig(projectRoot) {
|
|
11420
12732
|
const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
|
|
11421
|
-
if (!
|
|
12733
|
+
if (!existsSync16(taskConfigPath)) {
|
|
11422
12734
|
return {};
|
|
11423
12735
|
}
|
|
11424
12736
|
try {
|
|
@@ -11454,11 +12766,11 @@ function resolveFollowupSourceCommit(input) {
|
|
|
11454
12766
|
}
|
|
11455
12767
|
async function createInspectorFollowupTask(projectRoot, input) {
|
|
11456
12768
|
const monorepoRoot = resolveMonorepoRoot5(projectRoot);
|
|
11457
|
-
const issuesPath =
|
|
11458
|
-
const taskStatePath =
|
|
11459
|
-
const taskConfigPath =
|
|
11460
|
-
|
|
11461
|
-
|
|
12769
|
+
const issuesPath = resolve23(monorepoRoot, ".beads", "issues.jsonl");
|
|
12770
|
+
const taskStatePath = resolve23(monorepoRoot, ".beads", "task-state.json");
|
|
12771
|
+
const taskConfigPath = resolve23(monorepoRoot, ".rig", "task-config.json");
|
|
12772
|
+
mkdirSync16(dirname18(issuesPath), { recursive: true });
|
|
12773
|
+
mkdirSync16(dirname18(taskConfigPath), { recursive: true });
|
|
11462
12774
|
const summary = normalizeString(input.summary) ?? "Inspector follow-up";
|
|
11463
12775
|
const description = normalizeString(input.description) ?? normalizeString(input.details?.description) ?? `Created by the global inspector: ${summary}`;
|
|
11464
12776
|
const acceptanceCriteria = normalizeString(input.acceptanceCriteria) ?? "Investigate the detected drift and port the relevant changes into Rig.";
|
|
@@ -11477,7 +12789,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
|
|
|
11477
12789
|
const sourceKey = normalizeString(input.sourceKey) ?? normalizeString(input.details?.sourceKey);
|
|
11478
12790
|
const createdAt = normalizeString(input.createdAt) ?? new Date().toISOString();
|
|
11479
12791
|
const status = normalizeTaskLifecycleStatus2(normalizeString(input.status) ?? "open") ?? "open";
|
|
11480
|
-
const existingIssueLines =
|
|
12792
|
+
const existingIssueLines = existsSync17(issuesPath) ? readFileSync12(issuesPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
|
|
11481
12793
|
const existingIssues = existingIssueLines.map((line) => {
|
|
11482
12794
|
try {
|
|
11483
12795
|
return JSON.parse(line);
|
|
@@ -11486,7 +12798,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
|
|
|
11486
12798
|
}
|
|
11487
12799
|
}).filter((value) => value !== null);
|
|
11488
12800
|
const existingIds = new Set(existingIssues.map((issue) => typeof issue.id === "string" ? issue.id : null).filter((value) => value !== null));
|
|
11489
|
-
const rawTaskState =
|
|
12801
|
+
const rawTaskState = existsSync17(taskStatePath) ? readJsonFile3(taskStatePath, {}) : {};
|
|
11490
12802
|
const tasks = rawTaskState.tasks && typeof rawTaskState.tasks === "object" && !Array.isArray(rawTaskState.tasks) ? rawTaskState.tasks : {};
|
|
11491
12803
|
const existingTaskIdFromSourceKey = sourceKey == null ? null : Object.entries(tasks).find(([, metadata]) => {
|
|
11492
12804
|
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
@@ -11512,7 +12824,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
|
|
|
11512
12824
|
updated_at: createdAt,
|
|
11513
12825
|
labels: mergedLabels
|
|
11514
12826
|
};
|
|
11515
|
-
|
|
12827
|
+
writeFileSync15(issuesPath, existingIssueLines.length > 0 ? `${existingIssueLines.join(`
|
|
11516
12828
|
`)}
|
|
11517
12829
|
${JSON.stringify(issueRecord)}
|
|
11518
12830
|
` : `${JSON.stringify(issueRecord)}
|
|
@@ -11530,7 +12842,7 @@ ${JSON.stringify(issueRecord)}
|
|
|
11530
12842
|
labels: mergedLabels
|
|
11531
12843
|
};
|
|
11532
12844
|
});
|
|
11533
|
-
|
|
12845
|
+
writeFileSync15(issuesPath, `${updatedIssues.map((issue) => JSON.stringify(issue)).join(`
|
|
11534
12846
|
`)}
|
|
11535
12847
|
`, "utf8");
|
|
11536
12848
|
}
|
|
@@ -11553,14 +12865,14 @@ ${JSON.stringify(issueRecord)}
|
|
|
11553
12865
|
}
|
|
11554
12866
|
};
|
|
11555
12867
|
}
|
|
11556
|
-
|
|
12868
|
+
writeFileSync15(taskConfigPath, `${JSON.stringify(taskConfig, null, 2)}
|
|
11557
12869
|
`, "utf8");
|
|
11558
12870
|
tasks[taskId] = {
|
|
11559
12871
|
status,
|
|
11560
12872
|
sourceCommit: resolveFollowupSourceCommit(input),
|
|
11561
12873
|
...sourceKey ? { sourceKey } : {}
|
|
11562
12874
|
};
|
|
11563
|
-
|
|
12875
|
+
writeFileSync15(taskStatePath, `${JSON.stringify({
|
|
11564
12876
|
schemaVersion: 1,
|
|
11565
12877
|
baseTrackerCommit: typeof rawTaskState.baseTrackerCommit === "string" ? rawTaskState.baseTrackerCommit : null,
|
|
11566
12878
|
tasks
|
|
@@ -11587,21 +12899,10 @@ async function runInspectorLocalReview(projectRoot, input) {
|
|
|
11587
12899
|
details: null
|
|
11588
12900
|
};
|
|
11589
12901
|
}
|
|
11590
|
-
const
|
|
11591
|
-
import("@rig/runtime/control-plane/runtime/events"),
|
|
11592
|
-
import("@rig/runtime/control-plane/runtime/plugins"),
|
|
11593
|
-
import("@rig/runtime/control-plane/native/verifier")
|
|
11594
|
-
]);
|
|
11595
|
-
const eventBus = new RuntimeEventBus({ projectRoot, runId: `inspector-review:${taskId}` });
|
|
11596
|
-
const plugins = await PluginManager.load({
|
|
11597
|
-
projectRoot,
|
|
11598
|
-
runId: `inspector-review:${taskId}`,
|
|
11599
|
-
eventBus
|
|
11600
|
-
});
|
|
12902
|
+
const { verifyTask } = await import("@rig/runtime/control-plane/native/verifier");
|
|
11601
12903
|
const outcome = await verifyTask({
|
|
11602
12904
|
projectRoot,
|
|
11603
12905
|
taskId,
|
|
11604
|
-
plugins,
|
|
11605
12906
|
skipAiReview: true
|
|
11606
12907
|
});
|
|
11607
12908
|
return {
|
|
@@ -11868,12 +13169,12 @@ function isAuthorizedLinearWebhookRequest(req) {
|
|
|
11868
13169
|
}
|
|
11869
13170
|
|
|
11870
13171
|
// packages/server/src/server-helpers/notifications.ts
|
|
11871
|
-
import { existsSync as
|
|
11872
|
-
import { dirname as
|
|
13172
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync17, readFileSync as readFileSync13 } from "fs";
|
|
13173
|
+
import { dirname as dirname19 } from "path";
|
|
11873
13174
|
async function loadNotificationConfig(path) {
|
|
11874
|
-
if (!
|
|
13175
|
+
if (!existsSync18(path)) {
|
|
11875
13176
|
const defaultConfig = { targets: [] };
|
|
11876
|
-
|
|
13177
|
+
mkdirSync17(dirname19(path), { recursive: true });
|
|
11877
13178
|
await Bun.write(path, `${JSON.stringify(defaultConfig, null, 2)}
|
|
11878
13179
|
`);
|
|
11879
13180
|
return defaultConfig;
|
|
@@ -11888,10 +13189,10 @@ async function loadNotificationConfig(path) {
|
|
|
11888
13189
|
}
|
|
11889
13190
|
}
|
|
11890
13191
|
function readRecentEvents(file, limit) {
|
|
11891
|
-
if (!
|
|
13192
|
+
if (!existsSync18(file)) {
|
|
11892
13193
|
return [];
|
|
11893
13194
|
}
|
|
11894
|
-
const lines =
|
|
13195
|
+
const lines = readFileSync13(file, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-limit);
|
|
11895
13196
|
const events = [];
|
|
11896
13197
|
for (const line of lines) {
|
|
11897
13198
|
try {
|
|
@@ -11986,11 +13287,11 @@ function extractObjectLiteralBlock(source, property) {
|
|
|
11986
13287
|
}
|
|
11987
13288
|
function readFallbackIssueAnalysisConfig(projectRoot) {
|
|
11988
13289
|
for (const fileName of ["rig.config.ts", "rig.config.json"]) {
|
|
11989
|
-
const path =
|
|
11990
|
-
if (!
|
|
13290
|
+
const path = resolve24(projectRoot, fileName);
|
|
13291
|
+
if (!existsSync19(path))
|
|
11991
13292
|
continue;
|
|
11992
13293
|
try {
|
|
11993
|
-
const source =
|
|
13294
|
+
const source = readFileSync14(path, "utf8");
|
|
11994
13295
|
if (fileName.endsWith(".json"))
|
|
11995
13296
|
return JSON.parse(source);
|
|
11996
13297
|
const issueBlock = extractObjectLiteralBlock(source, "issueAnalysis");
|
|
@@ -12123,8 +13424,8 @@ async function createIssueAnalysisRunnerForServerState(state, input) {
|
|
|
12123
13424
|
async function withServerPathEnv(projectRoot, fn) {
|
|
12124
13425
|
const waitForTurn = serverPathEnvQueue;
|
|
12125
13426
|
let releaseTurn;
|
|
12126
|
-
serverPathEnvQueue = new Promise((
|
|
12127
|
-
releaseTurn =
|
|
13427
|
+
serverPathEnvQueue = new Promise((resolve25) => {
|
|
13428
|
+
releaseTurn = resolve25;
|
|
12128
13429
|
});
|
|
12129
13430
|
await waitForTurn;
|
|
12130
13431
|
const paths = resolveServerAuthorityPaths(projectRoot);
|
|
@@ -12160,9 +13461,9 @@ async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
|
|
|
12160
13461
|
return withServerPathEnv(projectRoot, fn);
|
|
12161
13462
|
}
|
|
12162
13463
|
async function readWorkspaceTasks(projectRoot) {
|
|
12163
|
-
const issuesPath =
|
|
13464
|
+
const issuesPath = resolve24(resolveMonorepoRoot6(projectRoot), ".beads", "issues.jsonl");
|
|
12164
13465
|
const taskConfig = await readTaskConfig(projectRoot);
|
|
12165
|
-
if (!
|
|
13466
|
+
if (!existsSync19(issuesPath)) {
|
|
12166
13467
|
return [];
|
|
12167
13468
|
}
|
|
12168
13469
|
const latestById = new Map;
|
|
@@ -12236,11 +13537,11 @@ function resolveTaskArtifactDirsFromRuns(projectRoot, taskId, knownRuns) {
|
|
|
12236
13537
|
continue;
|
|
12237
13538
|
add(run.artifactRoot);
|
|
12238
13539
|
if (run.worktreePath) {
|
|
12239
|
-
add(
|
|
13540
|
+
add(resolve24(run.worktreePath, "artifacts", taskId));
|
|
12240
13541
|
}
|
|
12241
13542
|
}
|
|
12242
13543
|
for (const artifactsRoot of listAuthorityArtifactRoots(projectRoot)) {
|
|
12243
|
-
add(
|
|
13544
|
+
add(resolve24(artifactsRoot, taskId));
|
|
12244
13545
|
}
|
|
12245
13546
|
return candidates;
|
|
12246
13547
|
}
|
|
@@ -12254,7 +13555,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
|
|
|
12254
13555
|
}
|
|
12255
13556
|
}
|
|
12256
13557
|
return taskIds.flatMap((currentTaskId) => {
|
|
12257
|
-
const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) =>
|
|
13558
|
+
const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) => existsSync19(path));
|
|
12258
13559
|
if (!currentRoot) {
|
|
12259
13560
|
return [];
|
|
12260
13561
|
}
|
|
@@ -12266,7 +13567,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
|
|
|
12266
13567
|
taskId: currentTaskId,
|
|
12267
13568
|
kind: "file",
|
|
12268
13569
|
label: fileName,
|
|
12269
|
-
path:
|
|
13570
|
+
path: resolve24(currentRoot, fileName),
|
|
12270
13571
|
url: null,
|
|
12271
13572
|
metadata: {
|
|
12272
13573
|
fileName
|
|
@@ -12309,11 +13610,11 @@ function buildInspectorStreamPayload(state, sequence) {
|
|
|
12309
13610
|
}
|
|
12310
13611
|
function listRemoteRunArtifacts(projectRoot, runId) {
|
|
12311
13612
|
const root = remoteArtifactsRoot(projectRoot, runId);
|
|
12312
|
-
if (!
|
|
13613
|
+
if (!existsSync19(root)) {
|
|
12313
13614
|
return [];
|
|
12314
13615
|
}
|
|
12315
13616
|
return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isFile()).filter((entry) => !entry.name.endsWith(".json")).map((entry) => {
|
|
12316
|
-
const artifactPath =
|
|
13617
|
+
const artifactPath = resolve24(root, entry.name);
|
|
12317
13618
|
const stat = statSync6(artifactPath);
|
|
12318
13619
|
const meta = readJsonFile4(`${artifactPath}.json`, null);
|
|
12319
13620
|
return {
|
|
@@ -12474,6 +13775,7 @@ function buildHttpRouterDeps(state) {
|
|
|
12474
13775
|
startLocalRun,
|
|
12475
13776
|
stopRunRecord,
|
|
12476
13777
|
resumeRunRecord,
|
|
13778
|
+
runServerOwnedPrCloseout,
|
|
12477
13779
|
claimRemoteRun,
|
|
12478
13780
|
listRemoteRunArtifacts,
|
|
12479
13781
|
broadcastSnapshotInvalidation,
|
|
@@ -12555,8 +13857,8 @@ function fileStats(path) {
|
|
|
12555
13857
|
}
|
|
12556
13858
|
}
|
|
12557
13859
|
function runFileCursor(projectRoot, run) {
|
|
12558
|
-
const runDir =
|
|
12559
|
-
const runJson = fileStats(
|
|
13860
|
+
const runDir = dirname20(runLogsPath(projectRoot, run.runId));
|
|
13861
|
+
const runJson = fileStats(resolve24(runDir, "run.json"));
|
|
12560
13862
|
const timeline = fileStats(runTimelinePath(projectRoot, run.runId));
|
|
12561
13863
|
const logs = fileStats(runLogsPath(projectRoot, run.runId));
|
|
12562
13864
|
return {
|
|
@@ -12606,10 +13908,10 @@ function startRunFileWatcher(state, pollMs) {
|
|
|
12606
13908
|
}, Math.max(250, Math.min(pollMs, 1000)));
|
|
12607
13909
|
}
|
|
12608
13910
|
function startPoller(state, pollMs) {
|
|
12609
|
-
let offset =
|
|
13911
|
+
let offset = existsSync19(state.eventsFile) ? statSync6(state.eventsFile).size : 0;
|
|
12610
13912
|
return setInterval(async () => {
|
|
12611
13913
|
try {
|
|
12612
|
-
if (!
|
|
13914
|
+
if (!existsSync19(state.eventsFile)) {
|
|
12613
13915
|
return;
|
|
12614
13916
|
}
|
|
12615
13917
|
const file = await open(state.eventsFile, "r");
|
|
@@ -12653,6 +13955,26 @@ function startPoller(state, pollMs) {
|
|
|
12653
13955
|
}
|
|
12654
13956
|
}, pollMs);
|
|
12655
13957
|
}
|
|
13958
|
+
function attachPiSessionProxySocket(ws) {
|
|
13959
|
+
if (ws.data.kind !== "pi-session-proxy")
|
|
13960
|
+
return;
|
|
13961
|
+
const upstream = new WebSocket(ws.data.upstreamUrl);
|
|
13962
|
+
ws.__rigPiUpstream = upstream;
|
|
13963
|
+
upstream.addEventListener("message", (event) => {
|
|
13964
|
+
if (ws.readyState === 1)
|
|
13965
|
+
ws.send(typeof event.data === "string" ? event.data : event.data);
|
|
13966
|
+
});
|
|
13967
|
+
upstream.addEventListener("close", () => {
|
|
13968
|
+
try {
|
|
13969
|
+
ws.close();
|
|
13970
|
+
} catch {}
|
|
13971
|
+
});
|
|
13972
|
+
upstream.addEventListener("error", () => {
|
|
13973
|
+
try {
|
|
13974
|
+
ws.close(1011, "Upstream Pi session stream failed");
|
|
13975
|
+
} catch {}
|
|
13976
|
+
});
|
|
13977
|
+
}
|
|
12656
13978
|
async function createRigServer(options, projectRoot = resolveProjectRoot()) {
|
|
12657
13979
|
const state = await createRigServerState(projectRoot, options.eventType, options.authToken, {
|
|
12658
13980
|
upstreamSyncMs: options.upstreamSyncMs,
|
|
@@ -12665,10 +13987,20 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
|
|
|
12665
13987
|
fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
|
|
12666
13988
|
websocket: {
|
|
12667
13989
|
open(ws) {
|
|
13990
|
+
if (ws.data.kind === "pi-session-proxy") {
|
|
13991
|
+
attachPiSessionProxySocket(ws);
|
|
13992
|
+
return;
|
|
13993
|
+
}
|
|
12668
13994
|
state.sockets.add(ws);
|
|
12669
13995
|
sendWebSocketResponse(ws, buildServerWelcomePush(state.projectRoot));
|
|
12670
13996
|
},
|
|
12671
13997
|
async message(ws, raw) {
|
|
13998
|
+
if (ws.data.kind === "pi-session-proxy") {
|
|
13999
|
+
const upstream = ws.__rigPiUpstream;
|
|
14000
|
+
if (upstream?.readyState === WebSocket.OPEN)
|
|
14001
|
+
upstream.send(raw);
|
|
14002
|
+
return;
|
|
14003
|
+
}
|
|
12672
14004
|
const request = parseWebSocketRequestEnvelope(typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8"));
|
|
12673
14005
|
if (!request) {
|
|
12674
14006
|
sendWebSocketResponse(ws, {
|
|
@@ -12691,6 +14023,10 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
|
|
|
12691
14023
|
}
|
|
12692
14024
|
},
|
|
12693
14025
|
close(ws) {
|
|
14026
|
+
if (ws.data.kind === "pi-session-proxy") {
|
|
14027
|
+
ws.__rigPiUpstream?.close();
|
|
14028
|
+
return;
|
|
14029
|
+
}
|
|
12694
14030
|
state.sockets.delete(ws);
|
|
12695
14031
|
}
|
|
12696
14032
|
}
|
|
@@ -12737,7 +14073,7 @@ function resolveProjectRoot() {
|
|
|
12737
14073
|
return resolveRigProjectRoot({
|
|
12738
14074
|
envProjectRoot: process.env.PROJECT_RIG_ROOT ?? null,
|
|
12739
14075
|
cwd: process.cwd(),
|
|
12740
|
-
fallbackRoot:
|
|
14076
|
+
fallbackRoot: resolve24(import.meta.dir, "../..")
|
|
12741
14077
|
});
|
|
12742
14078
|
}
|
|
12743
14079
|
var __testOnly = {
|