@h-rig/server 0.0.6-alpha.3 → 0.0.6-alpha.30
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/index.js
CHANGED
|
@@ -663,9 +663,9 @@ function createRemoteOrchestrationSummary(input) {
|
|
|
663
663
|
}
|
|
664
664
|
// packages/server/src/server.ts
|
|
665
665
|
import { spawn as spawn5 } from "child_process";
|
|
666
|
-
import { existsSync as
|
|
666
|
+
import { existsSync as existsSync19, readdirSync as readdirSync5, readFileSync as readFileSync14, statSync as statSync6 } from "fs";
|
|
667
667
|
import { open } from "fs/promises";
|
|
668
|
-
import { dirname as
|
|
668
|
+
import { dirname as dirname20, resolve as resolve24 } from "path";
|
|
669
669
|
import {
|
|
670
670
|
listAuthorityArtifactRoots,
|
|
671
671
|
listAuthorityRuns as listAuthorityRuns7,
|
|
@@ -1530,14 +1530,29 @@ function summarizeUsefulRunError(projectRoot, runId, fallback) {
|
|
|
1530
1530
|
const nonGeneric = errorLines.at(-1);
|
|
1531
1531
|
return nonGeneric ?? (typeof fallback === "string" ? fallback : null);
|
|
1532
1532
|
}
|
|
1533
|
+
function readRunPiSessionMetadata(projectRoot, runId) {
|
|
1534
|
+
const run = readAuthorityRun(projectRoot, runId);
|
|
1535
|
+
const metadata = run?.piSessionPrivate;
|
|
1536
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
|
|
1537
|
+
return null;
|
|
1538
|
+
const record = metadata;
|
|
1539
|
+
const publicMetadata = record.public;
|
|
1540
|
+
const daemonConnection = record.daemonConnection;
|
|
1541
|
+
if (!publicMetadata || typeof publicMetadata !== "object" || Array.isArray(publicMetadata))
|
|
1542
|
+
return null;
|
|
1543
|
+
if (!daemonConnection || typeof daemonConnection !== "object" || Array.isArray(daemonConnection))
|
|
1544
|
+
return null;
|
|
1545
|
+
return metadata;
|
|
1546
|
+
}
|
|
1533
1547
|
function readRunDetails(projectRoot, runId) {
|
|
1534
1548
|
const run = readAuthorityRun(projectRoot, runId);
|
|
1535
1549
|
if (!run) {
|
|
1536
1550
|
return null;
|
|
1537
1551
|
}
|
|
1538
1552
|
const usefulErrorText = isGenericRunFailure(run.errorText) ? summarizeUsefulRunError(projectRoot, runId, run.errorText) : null;
|
|
1553
|
+
const { piSessionPrivate: _piSessionPrivate, ...publicRun } = run;
|
|
1539
1554
|
return {
|
|
1540
|
-
run: usefulErrorText ? { ...
|
|
1555
|
+
run: usefulErrorText ? { ...publicRun, errorText: usefulErrorText } : publicRun,
|
|
1541
1556
|
timeline: readJsonlFile(resolve4(resolveAuthorityRunDir(projectRoot, runId), "timeline.jsonl")),
|
|
1542
1557
|
approvals: readApprovals(projectRoot, { runId }),
|
|
1543
1558
|
userInputs: readUserInputs(projectRoot, { runId })
|
|
@@ -1580,6 +1595,18 @@ function readJsonlFileTail(path, options) {
|
|
|
1580
1595
|
const completeLines = start > 0 ? lines.slice(1) : lines;
|
|
1581
1596
|
return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
|
|
1582
1597
|
}
|
|
1598
|
+
async function readRunTimelinePage(projectRoot, runId, options = {}) {
|
|
1599
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
1600
|
+
const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
|
|
1601
|
+
const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
1602
|
+
const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
|
|
1603
|
+
const endExclusive = Math.min(entries.length, startInclusive + limit);
|
|
1604
|
+
return {
|
|
1605
|
+
entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
|
|
1606
|
+
nextCursor: String(endExclusive),
|
|
1607
|
+
hasMore: endExclusive < entries.length
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1583
1610
|
var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
1584
1611
|
async function readRunLogsPage(projectRoot, runId, options = {}) {
|
|
1585
1612
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
@@ -2819,7 +2846,6 @@ function createGitHubTaskReconciler(input) {
|
|
|
2819
2846
|
}
|
|
2820
2847
|
|
|
2821
2848
|
// packages/server/src/server-helpers/issue-analysis.ts
|
|
2822
|
-
import { execFile } from "child_process";
|
|
2823
2849
|
import { createHash } from "crypto";
|
|
2824
2850
|
function stableIssueHash(issue) {
|
|
2825
2851
|
const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
|
|
@@ -2953,16 +2979,33 @@ function parseIssueAnalysisResult(raw) {
|
|
|
2953
2979
|
return result;
|
|
2954
2980
|
}
|
|
2955
2981
|
function createDefaultPiIssueAnalysisCommandRunner() {
|
|
2956
|
-
return (command, args, options) =>
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
const exitCode = typeof error?.code === "number" ? error.code : error ? 1 : 0;
|
|
2963
|
-
resolve11({ exitCode, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
|
|
2982
|
+
return async (command, args, options) => {
|
|
2983
|
+
const env = options.env ? { ...process.env, ...options.env } : process.env;
|
|
2984
|
+
const proc = Bun.spawn([command, ...args], {
|
|
2985
|
+
stdout: "pipe",
|
|
2986
|
+
stderr: "pipe",
|
|
2987
|
+
env
|
|
2964
2988
|
});
|
|
2965
|
-
|
|
2989
|
+
let timedOut = false;
|
|
2990
|
+
const timer = setTimeout(() => {
|
|
2991
|
+
timedOut = true;
|
|
2992
|
+
proc.kill();
|
|
2993
|
+
}, options.timeoutMs);
|
|
2994
|
+
try {
|
|
2995
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2996
|
+
new Response(proc.stdout).text(),
|
|
2997
|
+
new Response(proc.stderr).text(),
|
|
2998
|
+
proc.exited
|
|
2999
|
+
]);
|
|
3000
|
+
return {
|
|
3001
|
+
exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
|
|
3002
|
+
stdout,
|
|
3003
|
+
stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
|
|
3004
|
+
};
|
|
3005
|
+
} finally {
|
|
3006
|
+
clearTimeout(timer);
|
|
3007
|
+
}
|
|
3008
|
+
};
|
|
2966
3009
|
}
|
|
2967
3010
|
function createPiIssueAnalyzer(input = {}) {
|
|
2968
3011
|
const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
|
|
@@ -3216,13 +3259,18 @@ function patchRunRecord(projectRoot, runId, patch) {
|
|
|
3216
3259
|
writeJsonFile2(resolve11(resolveAuthorityRunDir2(projectRoot, runId), "run.json"), next);
|
|
3217
3260
|
return next;
|
|
3218
3261
|
}
|
|
3262
|
+
function patchRunPiSessionMetadata(projectRoot, runId, metadata) {
|
|
3263
|
+
return patchRunRecord(projectRoot, runId, {
|
|
3264
|
+
piSession: metadata?.public ?? null,
|
|
3265
|
+
piSessionPrivate: metadata
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3219
3268
|
function buildRunStartPatch(startedAt) {
|
|
3220
3269
|
return {
|
|
3221
3270
|
status: "preparing",
|
|
3222
3271
|
startedAt,
|
|
3223
3272
|
completedAt: null,
|
|
3224
|
-
errorText: null
|
|
3225
|
-
serverPid: process.pid
|
|
3273
|
+
errorText: null
|
|
3226
3274
|
};
|
|
3227
3275
|
}
|
|
3228
3276
|
|
|
@@ -3691,7 +3739,7 @@ function applyOrchestrationCommand2(state, command) {
|
|
|
3691
3739
|
import { spawn as spawn3 } from "child_process";
|
|
3692
3740
|
import { loadConfig } from "@rig/core/load-config";
|
|
3693
3741
|
import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
3694
|
-
import { dirname as
|
|
3742
|
+
import { dirname as dirname9, relative as relative2, resolve as resolve14 } from "path";
|
|
3695
3743
|
import {
|
|
3696
3744
|
listAuthorityRuns as listAuthorityRuns4,
|
|
3697
3745
|
readAuthorityRun as readAuthorityRun4,
|
|
@@ -3704,6 +3752,11 @@ import {
|
|
|
3704
3752
|
buildTaskRunLifecycleComment,
|
|
3705
3753
|
updateConfiguredTaskSourceTask
|
|
3706
3754
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
3755
|
+
import {
|
|
3756
|
+
closeIssueAfterMergedPr,
|
|
3757
|
+
commitRunChanges,
|
|
3758
|
+
runPrAutomation
|
|
3759
|
+
} from "@rig/runtime/control-plane/native/pr-automation";
|
|
3707
3760
|
|
|
3708
3761
|
// packages/server/src/scheduler.ts
|
|
3709
3762
|
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -3815,8 +3868,8 @@ function summarizeRunValidationFailure(projectRoot, run) {
|
|
|
3815
3868
|
|
|
3816
3869
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
3817
3870
|
import { randomBytes } from "crypto";
|
|
3818
|
-
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
3819
|
-
import { resolve as resolve13 } from "path";
|
|
3871
|
+
import { chmodSync, copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
3872
|
+
import { dirname as dirname8, resolve as resolve13 } from "path";
|
|
3820
3873
|
function cleanString(value) {
|
|
3821
3874
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
3822
3875
|
}
|
|
@@ -3846,6 +3899,26 @@ function parseApiSessions(value) {
|
|
|
3846
3899
|
}];
|
|
3847
3900
|
});
|
|
3848
3901
|
}
|
|
3902
|
+
function parsePendingDevice(value) {
|
|
3903
|
+
if (!value || typeof value !== "object")
|
|
3904
|
+
return null;
|
|
3905
|
+
const record = value;
|
|
3906
|
+
const pollId = cleanString(record.pollId);
|
|
3907
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
3908
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
3909
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
3910
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
3911
|
+
return null;
|
|
3912
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
3913
|
+
}
|
|
3914
|
+
function parsePendingDevices(value) {
|
|
3915
|
+
if (!Array.isArray(value))
|
|
3916
|
+
return [];
|
|
3917
|
+
return value.flatMap((entry) => {
|
|
3918
|
+
const pending = parsePendingDevice(entry);
|
|
3919
|
+
return pending ? [pending] : [];
|
|
3920
|
+
});
|
|
3921
|
+
}
|
|
3849
3922
|
function readStoredAuth(stateFile) {
|
|
3850
3923
|
if (!existsSync6(stateFile))
|
|
3851
3924
|
return {};
|
|
@@ -3859,6 +3932,7 @@ function readStoredAuth(stateFile) {
|
|
|
3859
3932
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
3860
3933
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
3861
3934
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
3935
|
+
pendingDevices: parsePendingDevices(parsed.pendingDevices),
|
|
3862
3936
|
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
3863
3937
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
3864
3938
|
};
|
|
@@ -3866,34 +3940,36 @@ function readStoredAuth(stateFile) {
|
|
|
3866
3940
|
return {};
|
|
3867
3941
|
}
|
|
3868
3942
|
}
|
|
3869
|
-
function parsePendingDevice(value) {
|
|
3870
|
-
if (!value || typeof value !== "object")
|
|
3871
|
-
return null;
|
|
3872
|
-
const record = value;
|
|
3873
|
-
const pollId = cleanString(record.pollId);
|
|
3874
|
-
const deviceCode = cleanString(record.deviceCode);
|
|
3875
|
-
const expiresAt = cleanString(record.expiresAt);
|
|
3876
|
-
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
3877
|
-
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
3878
|
-
return null;
|
|
3879
|
-
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
3880
|
-
}
|
|
3881
3943
|
function newApiSessionToken() {
|
|
3882
3944
|
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
3883
3945
|
}
|
|
3884
3946
|
function writeStoredAuth(stateFile, payload) {
|
|
3885
|
-
mkdirSync6(
|
|
3947
|
+
mkdirSync6(dirname8(stateFile), { recursive: true });
|
|
3886
3948
|
writeFileSync5(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
3887
3949
|
`, { encoding: "utf8", mode: 384 });
|
|
3888
3950
|
try {
|
|
3889
3951
|
chmodSync(stateFile, 384);
|
|
3890
3952
|
} catch {}
|
|
3891
3953
|
}
|
|
3954
|
+
function localProjectAuthStateFile(projectRoot) {
|
|
3955
|
+
return resolve13(projectRoot, ".rig", "state", "github-auth.json");
|
|
3956
|
+
}
|
|
3892
3957
|
function resolveGitHubAuthStateFile(projectRoot) {
|
|
3893
3958
|
return resolve13(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
3894
3959
|
}
|
|
3895
|
-
function
|
|
3896
|
-
const
|
|
3960
|
+
function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
|
|
3961
|
+
const targetFile = localProjectAuthStateFile(projectRoot);
|
|
3962
|
+
mkdirSync6(dirname8(targetFile), { recursive: true });
|
|
3963
|
+
if (existsSync6(stateFile)) {
|
|
3964
|
+
copyFileSync(stateFile, targetFile);
|
|
3965
|
+
try {
|
|
3966
|
+
chmodSync(targetFile, 384);
|
|
3967
|
+
} catch {}
|
|
3968
|
+
return;
|
|
3969
|
+
}
|
|
3970
|
+
writeStoredAuth(targetFile, {});
|
|
3971
|
+
}
|
|
3972
|
+
function createGitHubAuthStoreFromStateFile(stateFile) {
|
|
3897
3973
|
return {
|
|
3898
3974
|
stateFile,
|
|
3899
3975
|
status(options) {
|
|
@@ -3923,6 +3999,7 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
3923
3999
|
scopes: input.scopes ?? [],
|
|
3924
4000
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
3925
4001
|
pendingDevice: null,
|
|
4002
|
+
pendingDevices: [],
|
|
3926
4003
|
apiSessions: previous.apiSessions ?? [],
|
|
3927
4004
|
updatedAt: new Date().toISOString()
|
|
3928
4005
|
});
|
|
@@ -3951,15 +4028,24 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
3951
4028
|
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
3952
4029
|
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
3953
4030
|
},
|
|
3954
|
-
copyToProjectRoot(
|
|
3955
|
-
const targetFile = resolveGitHubAuthStateFile(
|
|
4031
|
+
copyToProjectRoot(projectRoot) {
|
|
4032
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot);
|
|
3956
4033
|
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
3957
4034
|
},
|
|
4035
|
+
copyToLocalProjectRoot(projectRoot) {
|
|
4036
|
+
copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
|
|
4037
|
+
},
|
|
3958
4038
|
savePendingDevice(input) {
|
|
3959
4039
|
const previous = readStoredAuth(stateFile);
|
|
4040
|
+
const pendingDevices = [
|
|
4041
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
4042
|
+
...previous.pendingDevices ?? [],
|
|
4043
|
+
input
|
|
4044
|
+
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
|
|
3960
4045
|
writeStoredAuth(stateFile, {
|
|
3961
4046
|
...previous,
|
|
3962
|
-
pendingDevice:
|
|
4047
|
+
pendingDevice: null,
|
|
4048
|
+
pendingDevices,
|
|
3963
4049
|
updatedAt: new Date().toISOString()
|
|
3964
4050
|
});
|
|
3965
4051
|
},
|
|
@@ -3972,23 +4058,32 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
3972
4058
|
});
|
|
3973
4059
|
},
|
|
3974
4060
|
readPendingDevice(pollId) {
|
|
3975
|
-
const
|
|
3976
|
-
|
|
4061
|
+
const previous = readStoredAuth(stateFile);
|
|
4062
|
+
const pending = [
|
|
4063
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
4064
|
+
...previous.pendingDevices ?? []
|
|
4065
|
+
].find((entry) => entry.pollId === pollId) ?? null;
|
|
4066
|
+
if (!pending)
|
|
3977
4067
|
return null;
|
|
3978
4068
|
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
3979
4069
|
return null;
|
|
3980
4070
|
return pending;
|
|
3981
4071
|
},
|
|
3982
|
-
clearPendingDevice() {
|
|
4072
|
+
clearPendingDevice(pollId) {
|
|
3983
4073
|
const previous = readStoredAuth(stateFile);
|
|
4074
|
+
const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
|
|
3984
4075
|
writeStoredAuth(stateFile, {
|
|
3985
4076
|
...previous,
|
|
3986
4077
|
pendingDevice: null,
|
|
4078
|
+
pendingDevices: remaining,
|
|
3987
4079
|
updatedAt: new Date().toISOString()
|
|
3988
4080
|
});
|
|
3989
4081
|
}
|
|
3990
4082
|
};
|
|
3991
4083
|
}
|
|
4084
|
+
function createGitHubAuthStore(projectRoot) {
|
|
4085
|
+
return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
|
|
4086
|
+
}
|
|
3992
4087
|
|
|
3993
4088
|
// packages/server/src/server-helpers/github-projects.ts
|
|
3994
4089
|
function asRecord(value) {
|
|
@@ -3997,6 +4092,9 @@ function asRecord(value) {
|
|
|
3997
4092
|
function asString(value) {
|
|
3998
4093
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
3999
4094
|
}
|
|
4095
|
+
function asNumber(value) {
|
|
4096
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
4097
|
+
}
|
|
4000
4098
|
async function defaultGraphQLFetch(query, variables, token) {
|
|
4001
4099
|
const response = await fetch("https://api.github.com/graphql", {
|
|
4002
4100
|
method: "POST",
|
|
@@ -4013,6 +4111,32 @@ async function defaultGraphQLFetch(query, variables, token) {
|
|
|
4013
4111
|
}
|
|
4014
4112
|
return json.data;
|
|
4015
4113
|
}
|
|
4114
|
+
function projectNodesFrom(data) {
|
|
4115
|
+
const root = asRecord(data);
|
|
4116
|
+
const owner = asRecord(root?.organization) ?? asRecord(root?.user);
|
|
4117
|
+
const projects = asRecord(owner?.projectsV2);
|
|
4118
|
+
const nodes = projects?.nodes;
|
|
4119
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
4120
|
+
}
|
|
4121
|
+
async function listGitHubProjects(input) {
|
|
4122
|
+
const query = `
|
|
4123
|
+
query RigListProjects($owner: String!, $first: Int!) {
|
|
4124
|
+
organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
4125
|
+
user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
4126
|
+
}
|
|
4127
|
+
`;
|
|
4128
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
4129
|
+
const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
|
|
4130
|
+
return projectNodesFrom(data).flatMap((node) => {
|
|
4131
|
+
const record = asRecord(node);
|
|
4132
|
+
const id = asString(record?.id);
|
|
4133
|
+
const number = asNumber(record?.number);
|
|
4134
|
+
const title = asString(record?.title);
|
|
4135
|
+
if (!id || number === undefined || !title)
|
|
4136
|
+
return [];
|
|
4137
|
+
return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
|
|
4138
|
+
});
|
|
4139
|
+
}
|
|
4016
4140
|
async function resolveProjectStatusField(input) {
|
|
4017
4141
|
const query = `
|
|
4018
4142
|
query RigProjectStatusField($projectId: ID!) {
|
|
@@ -4107,6 +4231,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
4107
4231
|
running: "In Progress",
|
|
4108
4232
|
prOpen: "In Review",
|
|
4109
4233
|
ciFixing: "In Review",
|
|
4234
|
+
merging: "Merging",
|
|
4110
4235
|
done: "Done",
|
|
4111
4236
|
needsAttention: "Needs Attention"
|
|
4112
4237
|
};
|
|
@@ -4120,6 +4245,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
4120
4245
|
return "prOpen";
|
|
4121
4246
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
4122
4247
|
return "ciFixing";
|
|
4248
|
+
if (normalized === "merging" || normalized === "merge")
|
|
4249
|
+
return "merging";
|
|
4123
4250
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
4124
4251
|
return "needsAttention";
|
|
4125
4252
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -4248,9 +4375,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
|
|
|
4248
4375
|
return null;
|
|
4249
4376
|
return null;
|
|
4250
4377
|
}
|
|
4378
|
+
function githubProjectsEnabled(config) {
|
|
4379
|
+
const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
|
|
4380
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
4381
|
+
return projects?.enabled === true;
|
|
4382
|
+
}
|
|
4251
4383
|
async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
|
|
4252
4384
|
if (!run.taskId)
|
|
4253
|
-
return;
|
|
4385
|
+
return false;
|
|
4254
4386
|
const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
|
|
4255
4387
|
try {
|
|
4256
4388
|
const result = await syncGitHubProjectStatusForTaskUpdate({
|
|
@@ -4261,28 +4393,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
|
|
|
4261
4393
|
config
|
|
4262
4394
|
});
|
|
4263
4395
|
if (!result.synced && result.reason !== "project-sync-disabled") {
|
|
4396
|
+
const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
|
|
4264
4397
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
4265
4398
|
id: `log:${run.runId}:github-project-sync:${status}`,
|
|
4266
4399
|
title: "GitHub Project sync skipped",
|
|
4267
|
-
detail
|
|
4400
|
+
detail,
|
|
4268
4401
|
tone: "warn",
|
|
4269
4402
|
status: "running",
|
|
4270
4403
|
createdAt: new Date().toISOString(),
|
|
4271
4404
|
payload: { reason: result.reason, issueNodeId }
|
|
4272
4405
|
});
|
|
4406
|
+
if (githubProjectsEnabled(config)) {
|
|
4407
|
+
throw new Error(detail);
|
|
4408
|
+
}
|
|
4409
|
+
return false;
|
|
4273
4410
|
}
|
|
4411
|
+
return result.synced === true;
|
|
4274
4412
|
} catch (error) {
|
|
4413
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4275
4414
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
4276
4415
|
id: `log:${run.runId}:github-project-sync-error:${status}`,
|
|
4277
4416
|
title: "GitHub Project sync failed",
|
|
4278
|
-
detail
|
|
4417
|
+
detail,
|
|
4279
4418
|
tone: "error",
|
|
4280
4419
|
status: "running",
|
|
4281
4420
|
createdAt: new Date().toISOString(),
|
|
4282
4421
|
payload: { issueNodeId }
|
|
4283
4422
|
});
|
|
4423
|
+
if (githubProjectsEnabled(config)) {
|
|
4424
|
+
throw new Error(detail);
|
|
4425
|
+
}
|
|
4426
|
+
return false;
|
|
4284
4427
|
}
|
|
4285
4428
|
}
|
|
4429
|
+
function createCommandRunner(binary, extraEnv = {}) {
|
|
4430
|
+
return async (args, options) => {
|
|
4431
|
+
const child = spawn3(binary, [...args], {
|
|
4432
|
+
cwd: options?.cwd,
|
|
4433
|
+
env: { ...process.env, ...extraEnv },
|
|
4434
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4435
|
+
});
|
|
4436
|
+
const stdoutChunks = [];
|
|
4437
|
+
const stderrChunks = [];
|
|
4438
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
4439
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
4440
|
+
const exitCode = await new Promise((resolve15) => {
|
|
4441
|
+
child.once("error", () => resolve15(1));
|
|
4442
|
+
child.once("close", (code) => resolve15(code ?? 1));
|
|
4443
|
+
});
|
|
4444
|
+
return {
|
|
4445
|
+
exitCode,
|
|
4446
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
4447
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8")
|
|
4448
|
+
};
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4451
|
+
function closeoutRecord(run) {
|
|
4452
|
+
const value = run.serverCloseout;
|
|
4453
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4454
|
+
}
|
|
4455
|
+
function closeoutPhasePatch(phase, status, extra = {}) {
|
|
4456
|
+
const updatedAt = new Date().toISOString();
|
|
4457
|
+
return {
|
|
4458
|
+
serverCloseout: {
|
|
4459
|
+
...extra,
|
|
4460
|
+
phase,
|
|
4461
|
+
status,
|
|
4462
|
+
updatedAt
|
|
4463
|
+
}
|
|
4464
|
+
};
|
|
4465
|
+
}
|
|
4466
|
+
function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
|
|
4467
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
4468
|
+
id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
|
|
4469
|
+
title: `Server closeout: ${phase}`,
|
|
4470
|
+
detail,
|
|
4471
|
+
tone,
|
|
4472
|
+
status,
|
|
4473
|
+
createdAt: new Date().toISOString()
|
|
4474
|
+
}, `server-closeout-${phase}`);
|
|
4475
|
+
}
|
|
4286
4476
|
async function autoAssignRunIssue(projectRoot, run) {
|
|
4287
4477
|
if (!run.taskId)
|
|
4288
4478
|
return;
|
|
@@ -4312,7 +4502,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
4312
4502
|
return;
|
|
4313
4503
|
}
|
|
4314
4504
|
const config = await loadRigLifecycleConfig(projectRoot);
|
|
4315
|
-
await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
4505
|
+
const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
4316
4506
|
if (status === "in_progress") {
|
|
4317
4507
|
await autoAssignRunIssue(projectRoot, run);
|
|
4318
4508
|
}
|
|
@@ -4328,24 +4518,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
4328
4518
|
});
|
|
4329
4519
|
return;
|
|
4330
4520
|
}
|
|
4331
|
-
const
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4521
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
4522
|
+
const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
|
|
4523
|
+
const rollbackProjectSync = async () => {
|
|
4524
|
+
if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
|
|
4525
|
+
return;
|
|
4526
|
+
await syncGitHubProjectStatusForTaskUpdate({
|
|
4527
|
+
taskId: run.taskId,
|
|
4528
|
+
status: previousStatus,
|
|
4529
|
+
issueNodeId: extractGitHubIssueNodeId(sourceTask),
|
|
4530
|
+
token: createGitHubAuthStore(projectRoot).readToken(),
|
|
4531
|
+
config
|
|
4532
|
+
}).catch((rollbackError) => {
|
|
4533
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
4534
|
+
id: `log:${run.runId}:github-project-sync-rollback:${status}`,
|
|
4535
|
+
title: "GitHub Project sync rollback failed",
|
|
4536
|
+
detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
|
|
4537
|
+
tone: "error",
|
|
4538
|
+
status: "running",
|
|
4539
|
+
createdAt: new Date().toISOString()
|
|
4540
|
+
});
|
|
4541
|
+
});
|
|
4542
|
+
};
|
|
4543
|
+
let result;
|
|
4544
|
+
try {
|
|
4545
|
+
result = await updateConfiguredTaskSourceTask(projectRoot, {
|
|
4546
|
+
taskId: run.taskId,
|
|
4547
|
+
sourceTask,
|
|
4548
|
+
update: {
|
|
4338
4549
|
status,
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4550
|
+
comment: buildTaskRunLifecycleComment({
|
|
4551
|
+
runId: run.runId,
|
|
4552
|
+
status,
|
|
4553
|
+
summary,
|
|
4554
|
+
runtimeWorkspace: normalizeString(run.worktreePath),
|
|
4555
|
+
logsDir: normalizeString(run.logRoot),
|
|
4556
|
+
sessionDir: normalizeString(run.sessionPath),
|
|
4557
|
+
errorText: options.errorText ?? normalizeString(run.errorText)
|
|
4558
|
+
})
|
|
4559
|
+
}
|
|
4560
|
+
});
|
|
4561
|
+
} catch (error) {
|
|
4562
|
+
await rollbackProjectSync();
|
|
4563
|
+
throw error;
|
|
4564
|
+
}
|
|
4347
4565
|
if (!result.updated) {
|
|
4348
4566
|
if (result.source === "plugin" || result.sourceKind) {
|
|
4567
|
+
await rollbackProjectSync();
|
|
4349
4568
|
throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
|
|
4350
4569
|
}
|
|
4351
4570
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
@@ -4358,6 +4577,277 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
4358
4577
|
});
|
|
4359
4578
|
}
|
|
4360
4579
|
}
|
|
4580
|
+
async function markServerOwnedCloseoutFailed(state, runId, error) {
|
|
4581
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4582
|
+
const current = readAuthorityRun4(state.projectRoot, runId);
|
|
4583
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4584
|
+
status: "failed",
|
|
4585
|
+
completedAt: new Date().toISOString(),
|
|
4586
|
+
errorText: detail,
|
|
4587
|
+
...closeoutPhasePatch("failed", "failed", { error: detail })
|
|
4588
|
+
});
|
|
4589
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
4590
|
+
id: `log:${runId}:server-closeout-failed`,
|
|
4591
|
+
title: "Server-owned closeout failed",
|
|
4592
|
+
detail,
|
|
4593
|
+
tone: "error",
|
|
4594
|
+
status: "failed",
|
|
4595
|
+
createdAt: new Date().toISOString()
|
|
4596
|
+
}, "server-closeout-failed");
|
|
4597
|
+
if (current?.taskId) {
|
|
4598
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: detail }, "failed", "Rig server-owned closeout failed.", { errorText: detail }).catch((sourceError) => {
|
|
4599
|
+
appendRunLogEntry(state.projectRoot, runId, {
|
|
4600
|
+
id: `log:${runId}:task-source-closeout-failed-update`,
|
|
4601
|
+
title: "Task source closeout failure update failed",
|
|
4602
|
+
detail: sourceError instanceof Error ? sourceError.message : String(sourceError),
|
|
4603
|
+
tone: "error",
|
|
4604
|
+
status: "failed",
|
|
4605
|
+
createdAt: new Date().toISOString()
|
|
4606
|
+
});
|
|
4607
|
+
});
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
function scheduleServerOwnedPrCloseout(state, runId, reason) {
|
|
4611
|
+
const startedAt = new Date().toISOString();
|
|
4612
|
+
state.runProcesses.set(runId, {
|
|
4613
|
+
runId,
|
|
4614
|
+
child: null,
|
|
4615
|
+
startedAt,
|
|
4616
|
+
stopped: false
|
|
4617
|
+
});
|
|
4618
|
+
queueMicrotask(() => {
|
|
4619
|
+
withServerAuthorityEnvIfNeeded(state.projectRoot, async () => {
|
|
4620
|
+
try {
|
|
4621
|
+
await runServerOwnedPrCloseout(state, runId);
|
|
4622
|
+
} catch (error) {
|
|
4623
|
+
await markServerOwnedCloseoutFailed(state, runId, error);
|
|
4624
|
+
} finally {
|
|
4625
|
+
state.runProcesses.delete(runId);
|
|
4626
|
+
broadcastSnapshotInvalidation(state, `server-closeout-${reason}-terminal`);
|
|
4627
|
+
await reconcileScheduler(state, `server-closeout-${reason}-terminal`);
|
|
4628
|
+
}
|
|
4629
|
+
});
|
|
4630
|
+
});
|
|
4631
|
+
}
|
|
4632
|
+
async function runServerOwnedPrCloseout(state, runId) {
|
|
4633
|
+
const run = readAuthorityRun4(state.projectRoot, runId);
|
|
4634
|
+
if (!run)
|
|
4635
|
+
throw new Error(`Run not found: ${runId}`);
|
|
4636
|
+
const closeout = closeoutRecord(run);
|
|
4637
|
+
if (!closeout)
|
|
4638
|
+
return;
|
|
4639
|
+
const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
|
|
4640
|
+
if (!taskId)
|
|
4641
|
+
throw new Error("Server-owned closeout requires a task id.");
|
|
4642
|
+
const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
|
|
4643
|
+
let branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
|
|
4644
|
+
const config = await loadRigLifecycleConfig(state.projectRoot);
|
|
4645
|
+
const runPrMode = normalizeString(run.prMode);
|
|
4646
|
+
const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
|
|
4647
|
+
const effectiveConfig = {
|
|
4648
|
+
...config ?? {},
|
|
4649
|
+
pr: {
|
|
4650
|
+
...config?.pr ?? {},
|
|
4651
|
+
mode: prMode,
|
|
4652
|
+
autoFixChecks: false,
|
|
4653
|
+
autoFixReview: false
|
|
4654
|
+
}
|
|
4655
|
+
};
|
|
4656
|
+
const readCurrentRun = () => readAuthorityRun4(state.projectRoot, runId) ?? run;
|
|
4657
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
4658
|
+
const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
|
|
4659
|
+
const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
|
|
4660
|
+
const closeoutPrUrl = normalizeString(closeout.prUrl);
|
|
4661
|
+
if (closeoutPhase === "completed" || closeoutStatus === "completed") {
|
|
4662
|
+
return;
|
|
4663
|
+
}
|
|
4664
|
+
if (closeoutPhase === "close-source" && closeoutPrUrl) {
|
|
4665
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4666
|
+
status: "reviewing",
|
|
4667
|
+
...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
|
|
4668
|
+
});
|
|
4669
|
+
await closeIssueAfterMergedPr({
|
|
4670
|
+
projectRoot: state.projectRoot,
|
|
4671
|
+
taskId,
|
|
4672
|
+
runId,
|
|
4673
|
+
prUrl: closeoutPrUrl,
|
|
4674
|
+
sourceTask,
|
|
4675
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
4676
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
4677
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
4678
|
+
}
|
|
4679
|
+
});
|
|
4680
|
+
const completedAt = new Date().toISOString();
|
|
4681
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4682
|
+
status: "completed",
|
|
4683
|
+
completedAt,
|
|
4684
|
+
errorText: null,
|
|
4685
|
+
...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
|
|
4686
|
+
});
|
|
4687
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
|
|
4688
|
+
emitRigEvent(state, {
|
|
4689
|
+
type: "rig.run.completed",
|
|
4690
|
+
aggregateId: runId,
|
|
4691
|
+
payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
|
|
4692
|
+
createdAt: completedAt
|
|
4693
|
+
});
|
|
4694
|
+
return;
|
|
4695
|
+
}
|
|
4696
|
+
if (prMode === "off" || prMode === "ask") {
|
|
4697
|
+
const completedAt = new Date().toISOString();
|
|
4698
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4699
|
+
status: "completed",
|
|
4700
|
+
completedAt,
|
|
4701
|
+
errorText: null,
|
|
4702
|
+
...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
|
|
4703
|
+
});
|
|
4704
|
+
appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
|
|
4705
|
+
emitRigEvent(state, {
|
|
4706
|
+
type: "rig.run.completed",
|
|
4707
|
+
aggregateId: runId,
|
|
4708
|
+
payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
|
|
4709
|
+
createdAt: completedAt
|
|
4710
|
+
});
|
|
4711
|
+
return;
|
|
4712
|
+
}
|
|
4713
|
+
const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
|
|
4714
|
+
const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
|
|
4715
|
+
const gitCommand = createCommandRunner("git", githubEnv);
|
|
4716
|
+
const ghCommand = createCommandRunner("gh", githubEnv);
|
|
4717
|
+
const workspaceBranch = await gitCommand(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: workspace });
|
|
4718
|
+
const currentWorkspaceBranch = workspaceBranch.exitCode === 0 ? normalizeString(workspaceBranch.stdout) : null;
|
|
4719
|
+
if (currentWorkspaceBranch && currentWorkspaceBranch !== "HEAD" && currentWorkspaceBranch !== branch) {
|
|
4720
|
+
appendCloseoutStage(state, runId, "branch", `Using runtime workspace branch ${currentWorkspaceBranch} instead of recorded branch ${branch}.`, "reviewing", "info");
|
|
4721
|
+
branch = currentWorkspaceBranch;
|
|
4722
|
+
}
|
|
4723
|
+
const setCloseout = (phase, status, extra = {}) => {
|
|
4724
|
+
const previous = closeoutRecord(readCurrentRun()) ?? closeout;
|
|
4725
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4726
|
+
status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
|
|
4727
|
+
...closeoutPhasePatch(phase, status, { ...previous, ...extra })
|
|
4728
|
+
});
|
|
4729
|
+
};
|
|
4730
|
+
setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
4731
|
+
appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
|
|
4732
|
+
const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
|
|
4733
|
+
appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
|
|
4734
|
+
setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
4735
|
+
const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
|
|
4736
|
+
if (push.exitCode !== 0) {
|
|
4737
|
+
throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
|
|
4738
|
+
}
|
|
4739
|
+
const sourceTaskForPr = {
|
|
4740
|
+
title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
|
|
4741
|
+
};
|
|
4742
|
+
const artifactRoot = resolve14(state.projectRoot, "artifacts", taskId);
|
|
4743
|
+
setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4744
|
+
const pr = await runPrAutomation({
|
|
4745
|
+
projectRoot: workspace,
|
|
4746
|
+
taskId,
|
|
4747
|
+
runId,
|
|
4748
|
+
branch,
|
|
4749
|
+
config: effectiveConfig,
|
|
4750
|
+
sourceTask: sourceTaskForPr,
|
|
4751
|
+
artifactRoot,
|
|
4752
|
+
command: ghCommand,
|
|
4753
|
+
gitCommand,
|
|
4754
|
+
steerPi: async (message) => {
|
|
4755
|
+
appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
|
|
4756
|
+
appendRunTimelineEntry(state.projectRoot, runId, {
|
|
4757
|
+
id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
|
|
4758
|
+
type: "user_message",
|
|
4759
|
+
text: message,
|
|
4760
|
+
createdAt: new Date().toISOString(),
|
|
4761
|
+
state: "completed"
|
|
4762
|
+
});
|
|
4763
|
+
},
|
|
4764
|
+
lifecycle: {
|
|
4765
|
+
onPrOpened: async ({ prUrl }) => {
|
|
4766
|
+
setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4767
|
+
appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
|
|
4768
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
|
|
4769
|
+
},
|
|
4770
|
+
onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
|
|
4771
|
+
onFeedback: async ({ feedback }) => {
|
|
4772
|
+
appendCloseoutStage(state, runId, "feedback", feedback.join(`
|
|
4773
|
+
`), "reviewing", "error");
|
|
4774
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
|
|
4775
|
+
},
|
|
4776
|
+
onMergeStarted: async ({ prUrl }) => {
|
|
4777
|
+
setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4778
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
4779
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
|
|
4780
|
+
},
|
|
4781
|
+
onMerged: ({ prUrl }) => {
|
|
4782
|
+
setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
|
|
4783
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
});
|
|
4787
|
+
if (pr.status === "merged" && pr.prUrl) {
|
|
4788
|
+
setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
|
|
4789
|
+
await closeIssueAfterMergedPr({
|
|
4790
|
+
projectRoot: state.projectRoot,
|
|
4791
|
+
taskId,
|
|
4792
|
+
runId,
|
|
4793
|
+
prUrl: pr.prUrl,
|
|
4794
|
+
sourceTask,
|
|
4795
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
4796
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
4797
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
4798
|
+
}
|
|
4799
|
+
});
|
|
4800
|
+
const completedAt = new Date().toISOString();
|
|
4801
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4802
|
+
status: "completed",
|
|
4803
|
+
completedAt,
|
|
4804
|
+
errorText: null,
|
|
4805
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
|
|
4806
|
+
});
|
|
4807
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
|
|
4808
|
+
emitRigEvent(state, {
|
|
4809
|
+
type: "rig.run.completed",
|
|
4810
|
+
aggregateId: runId,
|
|
4811
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
|
|
4812
|
+
createdAt: completedAt
|
|
4813
|
+
});
|
|
4814
|
+
return;
|
|
4815
|
+
}
|
|
4816
|
+
if (pr.status === "opened" && pr.prUrl) {
|
|
4817
|
+
const completedAt = new Date().toISOString();
|
|
4818
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4819
|
+
status: "completed",
|
|
4820
|
+
completedAt,
|
|
4821
|
+
errorText: null,
|
|
4822
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
|
|
4823
|
+
});
|
|
4824
|
+
appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
|
|
4825
|
+
emitRigEvent(state, {
|
|
4826
|
+
type: "rig.run.completed",
|
|
4827
|
+
aggregateId: runId,
|
|
4828
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
|
|
4829
|
+
createdAt: completedAt
|
|
4830
|
+
});
|
|
4831
|
+
return;
|
|
4832
|
+
}
|
|
4833
|
+
const detail = pr.actionableFeedback.join(`
|
|
4834
|
+
`) || "PR automation did not merge the PR.";
|
|
4835
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
|
|
4836
|
+
appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
|
|
4837
|
+
});
|
|
4838
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4839
|
+
status: "needs_attention",
|
|
4840
|
+
completedAt: new Date().toISOString(),
|
|
4841
|
+
errorText: detail,
|
|
4842
|
+
...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
|
|
4843
|
+
});
|
|
4844
|
+
appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
|
|
4845
|
+
emitRigEvent(state, {
|
|
4846
|
+
type: "rig.run.needs-attention",
|
|
4847
|
+
aggregateId: runId,
|
|
4848
|
+
payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
|
|
4849
|
+
});
|
|
4850
|
+
}
|
|
4361
4851
|
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
4362
4852
|
"completed",
|
|
4363
4853
|
"complete",
|
|
@@ -4383,11 +4873,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
|
|
|
4383
4873
|
return;
|
|
4384
4874
|
throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
|
|
4385
4875
|
}
|
|
4876
|
+
async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
|
|
4877
|
+
const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
4878
|
+
if (fromReader)
|
|
4879
|
+
return fromReader;
|
|
4880
|
+
const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
|
|
4881
|
+
if (projected)
|
|
4882
|
+
return projected;
|
|
4883
|
+
if (readTasks !== readWorkspaceTasks) {
|
|
4884
|
+
return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
4885
|
+
}
|
|
4886
|
+
return null;
|
|
4887
|
+
}
|
|
4386
4888
|
async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
|
|
4387
4889
|
if ("taskId" in input && input.taskId) {
|
|
4388
4890
|
assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
|
|
4389
4891
|
}
|
|
4390
|
-
const sourceTask = "taskId" in input && input.taskId ?
|
|
4892
|
+
const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
|
|
4391
4893
|
const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
|
|
4392
4894
|
const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
|
|
4393
4895
|
const runRecord = {
|
|
@@ -4459,6 +4961,7 @@ async function startLocalRun(state, runId, options) {
|
|
|
4459
4961
|
throw new Error(`Run not found: ${runId}`);
|
|
4460
4962
|
}
|
|
4461
4963
|
const startedAt = new Date().toISOString();
|
|
4964
|
+
const resumeMode = options?.resume === true;
|
|
4462
4965
|
state.runProcesses.set(runId, {
|
|
4463
4966
|
runId,
|
|
4464
4967
|
child: null,
|
|
@@ -4475,9 +4978,9 @@ async function startLocalRun(state, runId, options) {
|
|
|
4475
4978
|
summary: run.title
|
|
4476
4979
|
});
|
|
4477
4980
|
appendRunLogEntry(state.projectRoot, runId, {
|
|
4478
|
-
id: `log:${runId}:prepare`,
|
|
4479
|
-
title: "Rig task run starting",
|
|
4480
|
-
detail: run.taskId ?? run.title,
|
|
4981
|
+
id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
|
|
4982
|
+
title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
|
|
4983
|
+
detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
|
|
4481
4984
|
tone: "info",
|
|
4482
4985
|
status: "preparing",
|
|
4483
4986
|
createdAt: startedAt
|
|
@@ -4553,12 +5056,17 @@ async function startLocalRun(state, runId, options) {
|
|
|
4553
5056
|
RIG_HOST_PROJECT_ROOT: cliProjectRoot,
|
|
4554
5057
|
RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
|
|
4555
5058
|
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
5059
|
+
RIG_SERVER_OWNS_CLOSEOUT: "1",
|
|
4556
5060
|
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
4557
5061
|
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
4558
5062
|
...bridgeGitHubToken ? {
|
|
4559
5063
|
RIG_GITHUB_TOKEN: bridgeGitHubToken,
|
|
4560
5064
|
GITHUB_TOKEN: bridgeGitHubToken,
|
|
4561
5065
|
GH_TOKEN: bridgeGitHubToken
|
|
5066
|
+
} : {},
|
|
5067
|
+
...resumeMode ? {
|
|
5068
|
+
RIG_RUN_RESUME: "1",
|
|
5069
|
+
RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
|
|
4562
5070
|
} : {}
|
|
4563
5071
|
},
|
|
4564
5072
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -4582,6 +5090,25 @@ async function startLocalRun(state, runId, options) {
|
|
|
4582
5090
|
broadcastSnapshotInvalidation(state);
|
|
4583
5091
|
continue;
|
|
4584
5092
|
}
|
|
5093
|
+
if (line.startsWith("__RIG_WRAPPER_EVENT__")) {
|
|
5094
|
+
try {
|
|
5095
|
+
const wrapperEvent = JSON.parse(line.slice("__RIG_WRAPPER_EVENT__".length));
|
|
5096
|
+
const eventType = normalizeString(wrapperEvent.type);
|
|
5097
|
+
const payload = wrapperEvent.payload && typeof wrapperEvent.payload === "object" && !Array.isArray(wrapperEvent.payload) ? wrapperEvent.payload : {};
|
|
5098
|
+
if (eventType === "pi.session.ready" && payload.privateMetadata && typeof payload.privateMetadata === "object" && !Array.isArray(payload.privateMetadata)) {
|
|
5099
|
+
patchRunPiSessionMetadata(state.projectRoot, runId, payload.privateMetadata);
|
|
5100
|
+
}
|
|
5101
|
+
appendRunTimelineEntry(state.projectRoot, runId, {
|
|
5102
|
+
id: `timeline:${runId}:${Date.now()}:wrapper:${eventType ?? "event"}`,
|
|
5103
|
+
type: "wrapper-event",
|
|
5104
|
+
eventType,
|
|
5105
|
+
payload,
|
|
5106
|
+
createdAt: normalizeString(wrapperEvent.at) ?? new Date().toISOString()
|
|
5107
|
+
});
|
|
5108
|
+
} catch {}
|
|
5109
|
+
broadcastSnapshotInvalidation(state, "wrapper-event");
|
|
5110
|
+
continue;
|
|
5111
|
+
}
|
|
4585
5112
|
appendRunLogEntryAndBroadcast(state, runId, {
|
|
4586
5113
|
id: `log:${runId}:${Date.now()}`,
|
|
4587
5114
|
title,
|
|
@@ -4610,7 +5137,13 @@ async function startLocalRun(state, runId, options) {
|
|
|
4610
5137
|
if (!current) {
|
|
4611
5138
|
return;
|
|
4612
5139
|
}
|
|
4613
|
-
if (
|
|
5140
|
+
if (closeoutRecord(current)?.status === "pending") {
|
|
5141
|
+
try {
|
|
5142
|
+
await runServerOwnedPrCloseout(state, runId);
|
|
5143
|
+
} catch (closeoutError) {
|
|
5144
|
+
await markServerOwnedCloseoutFailed(state, runId, closeoutError);
|
|
5145
|
+
}
|
|
5146
|
+
} else if (exit.code !== 0 && current.status !== "completed" && current.status !== "stopped") {
|
|
4614
5147
|
const completedAt = current.completedAt ?? new Date().toISOString();
|
|
4615
5148
|
const failureSummary = normalizeString(current.errorText) ?? summarizeRunValidationFailure(state.projectRoot, current) ?? `Rig task-run exited with code ${String(exit.code ?? "unknown")}`;
|
|
4616
5149
|
if (current.status !== "failed") {
|
|
@@ -4710,7 +5243,7 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
|
|
|
4710
5243
|
}
|
|
4711
5244
|
try {
|
|
4712
5245
|
const monorepoRoot = resolveMonorepoRoot3(projectRoot);
|
|
4713
|
-
const outerProjectRoot =
|
|
5246
|
+
const outerProjectRoot = dirname9(dirname9(monorepoRoot));
|
|
4714
5247
|
if (existsSync7(resolve14(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
|
|
4715
5248
|
return outerProjectRoot;
|
|
4716
5249
|
}
|
|
@@ -4731,7 +5264,19 @@ async function resumeRunRecord(state, input) {
|
|
|
4731
5264
|
if (run.status === "completed") {
|
|
4732
5265
|
throw new Error("Completed runs cannot be resumed.");
|
|
4733
5266
|
}
|
|
4734
|
-
|
|
5267
|
+
const closeout = closeoutRecord(run);
|
|
5268
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
5269
|
+
if (EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
|
|
5270
|
+
patchRunRecord(state.projectRoot, input.runId, {
|
|
5271
|
+
status: "reviewing",
|
|
5272
|
+
completedAt: null,
|
|
5273
|
+
errorText: null,
|
|
5274
|
+
...closeoutPhasePatch("queued", "pending", { ...closeout, resumedAt: input.createdAt })
|
|
5275
|
+
});
|
|
5276
|
+
scheduleServerOwnedPrCloseout(state, input.runId, "explicit-resume");
|
|
5277
|
+
return;
|
|
5278
|
+
}
|
|
5279
|
+
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
|
|
4735
5280
|
}
|
|
4736
5281
|
function appendRunMessage(projectRoot, input) {
|
|
4737
5282
|
const run = readAuthorityRun4(projectRoot, input.runId);
|
|
@@ -4815,34 +5360,63 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
|
|
|
4815
5360
|
writeQueueState(projectRoot, next);
|
|
4816
5361
|
return next;
|
|
4817
5362
|
}
|
|
4818
|
-
var
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
5363
|
+
var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
|
|
5364
|
+
var EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running", "needs_attention"]);
|
|
5365
|
+
var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
5366
|
+
function processExists(pid) {
|
|
5367
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
5368
|
+
return false;
|
|
5369
|
+
try {
|
|
5370
|
+
process.kill(pid, 0);
|
|
5371
|
+
return true;
|
|
5372
|
+
} catch {
|
|
5373
|
+
return false;
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5376
|
+
function recoverStaleLocalRun(projectRoot, run) {
|
|
5377
|
+
const record = run;
|
|
5378
|
+
if (run.mode !== "local")
|
|
5379
|
+
return false;
|
|
5380
|
+
const closeout = closeoutRecord(record);
|
|
5381
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
5382
|
+
const status = normalizeString(record.status)?.toLowerCase() ?? "";
|
|
5383
|
+
if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus))
|
|
5384
|
+
return false;
|
|
5385
|
+
if (closeoutStatus === "needs_attention") {
|
|
5386
|
+
if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
|
|
5387
|
+
return false;
|
|
5388
|
+
const completedAt2 = record.completedAt ?? new Date().toISOString();
|
|
5389
|
+
patchRunRecord(projectRoot, run.runId, {
|
|
5390
|
+
status: "needs_attention",
|
|
5391
|
+
completedAt: completedAt2,
|
|
5392
|
+
errorText: normalizeString(record.errorText) ?? (Array.isArray(closeout?.feedback) ? closeout.feedback.map(String).join(`
|
|
5393
|
+
`) : null)
|
|
4842
5394
|
});
|
|
4843
|
-
|
|
5395
|
+
return true;
|
|
4844
5396
|
}
|
|
4845
|
-
|
|
5397
|
+
if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
|
|
5398
|
+
return false;
|
|
5399
|
+
const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
|
|
5400
|
+
const childPid = typeof record.pid === "number" ? record.pid : null;
|
|
5401
|
+
if (serverPid === null && childPid === null)
|
|
5402
|
+
return false;
|
|
5403
|
+
const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
|
|
5404
|
+
if (hasLiveRecordedProcess && serverPid === process.pid)
|
|
5405
|
+
return false;
|
|
5406
|
+
const completedAt = new Date().toISOString();
|
|
5407
|
+
patchRunRecord(projectRoot, run.runId, {
|
|
5408
|
+
status: "failed",
|
|
5409
|
+
completedAt,
|
|
5410
|
+
errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
|
|
5411
|
+
});
|
|
5412
|
+
return true;
|
|
5413
|
+
}
|
|
5414
|
+
function collectResumableServerCloseouts(state, runs) {
|
|
5415
|
+
return runs.filter((run) => {
|
|
5416
|
+
const closeout = closeoutRecord(run);
|
|
5417
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
5418
|
+
return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
|
|
5419
|
+
});
|
|
4846
5420
|
}
|
|
4847
5421
|
async function reconcileScheduler(state, reason) {
|
|
4848
5422
|
if (state.scheduler.reconciling) {
|
|
@@ -4857,7 +5431,28 @@ async function reconcileScheduler(state, reason) {
|
|
|
4857
5431
|
const queue = readQueueState(state.projectRoot);
|
|
4858
5432
|
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
4859
5433
|
let runs = listAuthorityRuns4(state.projectRoot);
|
|
4860
|
-
let changed =
|
|
5434
|
+
let changed = false;
|
|
5435
|
+
for (const run of runs) {
|
|
5436
|
+
if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
|
|
5437
|
+
changed = true;
|
|
5438
|
+
}
|
|
5439
|
+
}
|
|
5440
|
+
if (changed) {
|
|
5441
|
+
runs = listAuthorityRuns4(state.projectRoot);
|
|
5442
|
+
}
|
|
5443
|
+
const resumableCloseouts = collectResumableServerCloseouts(state, runs);
|
|
5444
|
+
for (const run of resumableCloseouts) {
|
|
5445
|
+
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
5446
|
+
id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
|
|
5447
|
+
title: "Server-owned closeout auto-resume scheduled",
|
|
5448
|
+
detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
|
|
5449
|
+
tone: "info",
|
|
5450
|
+
status: "reviewing",
|
|
5451
|
+
createdAt: new Date().toISOString()
|
|
5452
|
+
});
|
|
5453
|
+
scheduleServerOwnedPrCloseout(state, run.runId, "auto-resume");
|
|
5454
|
+
changed = true;
|
|
5455
|
+
}
|
|
4861
5456
|
if (changed) {
|
|
4862
5457
|
runs = listAuthorityRuns4(state.projectRoot);
|
|
4863
5458
|
}
|
|
@@ -4933,11 +5528,11 @@ async function reconcileScheduler(state, reason) {
|
|
|
4933
5528
|
// packages/server/src/server-helpers/http-router.ts
|
|
4934
5529
|
import { randomUUID } from "crypto";
|
|
4935
5530
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
4936
|
-
import { basename, dirname as
|
|
4937
|
-
import { copyFileSync, existsSync as
|
|
5531
|
+
import { basename, dirname as dirname15, isAbsolute as isAbsolute4, resolve as resolve20 } from "path";
|
|
5532
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync12 } from "fs";
|
|
4938
5533
|
import {
|
|
4939
5534
|
listAuthorityRuns as listAuthorityRuns5,
|
|
4940
|
-
readAuthorityRun as
|
|
5535
|
+
readAuthorityRun as readAuthorityRun7,
|
|
4941
5536
|
resolveAuthorityPaths,
|
|
4942
5537
|
writeJsonFile as writeJsonFile4
|
|
4943
5538
|
} from "@rig/runtime/control-plane/authority-files";
|
|
@@ -4957,10 +5552,64 @@ import {
|
|
|
4957
5552
|
RemoteWsClient
|
|
4958
5553
|
} from "@rig/runtime/control-plane/remote";
|
|
4959
5554
|
|
|
5555
|
+
// packages/server/src/server-helpers/pi-session-proxy.ts
|
|
5556
|
+
import { readAuthorityRun as readAuthorityRun5 } from "@rig/runtime/control-plane/authority-files";
|
|
5557
|
+
function resolveRunPiSessionProxy(projectRoot, runId) {
|
|
5558
|
+
const run = readAuthorityRun5(projectRoot, runId);
|
|
5559
|
+
if (!run)
|
|
5560
|
+
return null;
|
|
5561
|
+
const privateMetadata = readRunPiSessionMetadata(projectRoot, runId);
|
|
5562
|
+
if (!privateMetadata)
|
|
5563
|
+
return { pending: true, runId, status: typeof run.status === "string" ? run.status : undefined };
|
|
5564
|
+
const connection = privateMetadata.daemonConnection;
|
|
5565
|
+
if (connection.mode !== "http")
|
|
5566
|
+
throw new Error("Only loopback HTTP Rig Pi session daemon connections are supported");
|
|
5567
|
+
const token = tokenFromRef(connection.tokenRef);
|
|
5568
|
+
if (!token)
|
|
5569
|
+
throw new Error("Rig Pi session daemon token is unavailable");
|
|
5570
|
+
return {
|
|
5571
|
+
runId,
|
|
5572
|
+
sessionId: privateMetadata.public.sessionId,
|
|
5573
|
+
metadata: privateMetadata.public,
|
|
5574
|
+
privateMetadata,
|
|
5575
|
+
baseUrl: connection.baseUrl.replace(/\/+$/, ""),
|
|
5576
|
+
token
|
|
5577
|
+
};
|
|
5578
|
+
}
|
|
5579
|
+
async function proxyRunPiHttp(projectRoot, runId, input) {
|
|
5580
|
+
const resolved = resolveRunPiSessionProxy(projectRoot, runId);
|
|
5581
|
+
if (resolved === null)
|
|
5582
|
+
return { status: 404, payload: { ok: false, error: "Run not found" } };
|
|
5583
|
+
if ("pending" in resolved)
|
|
5584
|
+
return { status: 409, payload: { ready: false, runId, status: resolved.status, retryAfterMs: 500 } };
|
|
5585
|
+
const response = await fetch(`${resolved.baseUrl}${input.daemonPath}`, {
|
|
5586
|
+
method: input.method,
|
|
5587
|
+
headers: {
|
|
5588
|
+
authorization: `Bearer ${resolved.token}`,
|
|
5589
|
+
...input.body === undefined ? {} : { "content-type": "application/json" }
|
|
5590
|
+
},
|
|
5591
|
+
body: input.body === undefined ? undefined : JSON.stringify(input.body)
|
|
5592
|
+
});
|
|
5593
|
+
const text = await response.text();
|
|
5594
|
+
const payload = text.trim() ? JSON.parse(text) : null;
|
|
5595
|
+
return { status: response.status, payload };
|
|
5596
|
+
}
|
|
5597
|
+
function buildRunPiDaemonWebSocketUrl(resolved) {
|
|
5598
|
+
const url = new URL(`${resolved.baseUrl}/sessions/${encodeURIComponent(resolved.sessionId)}/events`);
|
|
5599
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
5600
|
+
url.searchParams.set("token", resolved.token);
|
|
5601
|
+
return url.toString();
|
|
5602
|
+
}
|
|
5603
|
+
function tokenFromRef(ref) {
|
|
5604
|
+
if (ref.startsWith("inline:"))
|
|
5605
|
+
return ref.slice("inline:".length) || null;
|
|
5606
|
+
return null;
|
|
5607
|
+
}
|
|
5608
|
+
|
|
4960
5609
|
// packages/server/src/server-helpers/run-steering.ts
|
|
4961
|
-
import { dirname as
|
|
5610
|
+
import { dirname as dirname10, resolve as resolve15 } from "path";
|
|
4962
5611
|
import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync5 } from "fs";
|
|
4963
|
-
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as
|
|
5612
|
+
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun6, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
|
|
4964
5613
|
var steeringSequence = 0;
|
|
4965
5614
|
function runSteeringPath(projectRoot, runId) {
|
|
4966
5615
|
return resolve15(resolveAuthorityRunDir4(projectRoot, runId), "steering.jsonl");
|
|
@@ -5029,7 +5678,7 @@ function markQueuedRunSteeringMessagesDelivered(projectRoot, runId, ids) {
|
|
|
5029
5678
|
return delivered;
|
|
5030
5679
|
}
|
|
5031
5680
|
function queueRunSteeringMessage(projectRoot, runId, input) {
|
|
5032
|
-
const run =
|
|
5681
|
+
const run = readAuthorityRun6(projectRoot, runId);
|
|
5033
5682
|
if (!run)
|
|
5034
5683
|
throw new Error(`Run not found: ${runId}`);
|
|
5035
5684
|
const text = input.message.trim();
|
|
@@ -5045,7 +5694,7 @@ function queueRunSteeringMessage(projectRoot, runId, input) {
|
|
|
5045
5694
|
delivered: false
|
|
5046
5695
|
};
|
|
5047
5696
|
const path = runSteeringPath(projectRoot, runId);
|
|
5048
|
-
mkdirSync8(
|
|
5697
|
+
mkdirSync8(dirname10(path), { recursive: true });
|
|
5049
5698
|
appendJsonlRecord2(path, entry);
|
|
5050
5699
|
appendRunTimelineEntry(projectRoot, runId, {
|
|
5051
5700
|
id: entry.id,
|
|
@@ -5082,6 +5731,187 @@ import {
|
|
|
5082
5731
|
updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
|
|
5083
5732
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
5084
5733
|
|
|
5734
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
5735
|
+
import { chmodSync as chmodSync3, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
5736
|
+
import { dirname as dirname12, resolve as resolve17 } from "path";
|
|
5737
|
+
|
|
5738
|
+
// packages/server/src/server-helpers/github-user-namespace.ts
|
|
5739
|
+
import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
5740
|
+
import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve16 } from "path";
|
|
5741
|
+
function cleanString3(value) {
|
|
5742
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
5743
|
+
}
|
|
5744
|
+
function sanitizePathSegment(value) {
|
|
5745
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
|
|
5746
|
+
}
|
|
5747
|
+
function deriveGitHubUserNamespaceKey(identity) {
|
|
5748
|
+
const userId = cleanString3(identity.userId);
|
|
5749
|
+
if (userId) {
|
|
5750
|
+
const safeId = sanitizePathSegment(userId);
|
|
5751
|
+
if (safeId)
|
|
5752
|
+
return `ghu-${safeId}`;
|
|
5753
|
+
}
|
|
5754
|
+
const login = cleanString3(identity.login);
|
|
5755
|
+
if (login) {
|
|
5756
|
+
const safeLogin = sanitizePathSegment(login);
|
|
5757
|
+
if (safeLogin)
|
|
5758
|
+
return `ghu-login-${safeLogin}`;
|
|
5759
|
+
}
|
|
5760
|
+
throw new Error("GitHub user namespace requires a user id or login");
|
|
5761
|
+
}
|
|
5762
|
+
function resolveRemoteUserNamespacesRoot(projectRoot) {
|
|
5763
|
+
const explicitRoot = cleanString3(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
|
|
5764
|
+
if (explicitRoot)
|
|
5765
|
+
return resolve16(explicitRoot);
|
|
5766
|
+
const stateDir2 = cleanString3(process.env.RIG_STATE_DIR);
|
|
5767
|
+
if (stateDir2)
|
|
5768
|
+
return resolve16(dirname11(resolve16(stateDir2)), "users");
|
|
5769
|
+
return resolve16(projectRoot, ".rig", "users");
|
|
5770
|
+
}
|
|
5771
|
+
function resolveRemoteUserNamespace(projectRoot, identity) {
|
|
5772
|
+
const key = deriveGitHubUserNamespaceKey(identity);
|
|
5773
|
+
const root = resolve16(resolveRemoteUserNamespacesRoot(projectRoot), key);
|
|
5774
|
+
const stateDir2 = resolve16(root, ".rig", "state");
|
|
5775
|
+
return {
|
|
5776
|
+
key,
|
|
5777
|
+
userId: cleanString3(identity.userId),
|
|
5778
|
+
login: cleanString3(identity.login),
|
|
5779
|
+
root,
|
|
5780
|
+
stateDir: stateDir2,
|
|
5781
|
+
authStateFile: resolve16(stateDir2, "github-auth.json"),
|
|
5782
|
+
metadataFile: resolve16(stateDir2, "user-namespace.json"),
|
|
5783
|
+
checkoutBaseDir: resolve16(root, "remote-checkouts"),
|
|
5784
|
+
snapshotBaseDir: resolve16(root, "remote-snapshots")
|
|
5785
|
+
};
|
|
5786
|
+
}
|
|
5787
|
+
function serializeRemoteUserNamespace(namespace) {
|
|
5788
|
+
return {
|
|
5789
|
+
key: namespace.key,
|
|
5790
|
+
userId: namespace.userId,
|
|
5791
|
+
login: namespace.login,
|
|
5792
|
+
root: namespace.root,
|
|
5793
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
5794
|
+
snapshotBaseDir: namespace.snapshotBaseDir
|
|
5795
|
+
};
|
|
5796
|
+
}
|
|
5797
|
+
function isPathInsideNamespace(namespaceRoot, candidatePath) {
|
|
5798
|
+
const root = resolve16(namespaceRoot);
|
|
5799
|
+
const candidate = resolve16(candidatePath);
|
|
5800
|
+
const rel = relative3(root, candidate);
|
|
5801
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
5802
|
+
}
|
|
5803
|
+
function writeRemoteUserNamespaceMetadata(namespace) {
|
|
5804
|
+
mkdirSync9(namespace.stateDir, { recursive: true });
|
|
5805
|
+
const previous = (() => {
|
|
5806
|
+
if (!existsSync9(namespace.metadataFile))
|
|
5807
|
+
return null;
|
|
5808
|
+
try {
|
|
5809
|
+
const parsed = JSON.parse(readFileSync6(namespace.metadataFile, "utf8"));
|
|
5810
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
5811
|
+
} catch {
|
|
5812
|
+
return null;
|
|
5813
|
+
}
|
|
5814
|
+
})();
|
|
5815
|
+
const now = new Date().toISOString();
|
|
5816
|
+
writeFileSync8(namespace.metadataFile, `${JSON.stringify({
|
|
5817
|
+
key: namespace.key,
|
|
5818
|
+
userId: namespace.userId,
|
|
5819
|
+
login: namespace.login,
|
|
5820
|
+
root: namespace.root,
|
|
5821
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
5822
|
+
snapshotBaseDir: namespace.snapshotBaseDir,
|
|
5823
|
+
createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
|
|
5824
|
+
updatedAt: now
|
|
5825
|
+
}, null, 2)}
|
|
5826
|
+
`, { encoding: "utf8", mode: 384 });
|
|
5827
|
+
try {
|
|
5828
|
+
chmodSync2(namespace.metadataFile, 384);
|
|
5829
|
+
} catch {}
|
|
5830
|
+
}
|
|
5831
|
+
|
|
5832
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
5833
|
+
function cleanString4(value) {
|
|
5834
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
5835
|
+
}
|
|
5836
|
+
function resolveGitHubApiSessionIndexFile(projectRoot) {
|
|
5837
|
+
return resolve17(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
|
|
5838
|
+
}
|
|
5839
|
+
function parseEntry(value) {
|
|
5840
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5841
|
+
return null;
|
|
5842
|
+
const record = value;
|
|
5843
|
+
const token = cleanString4(record.token);
|
|
5844
|
+
const namespaceKey = cleanString4(record.namespaceKey);
|
|
5845
|
+
const namespaceRoot = cleanString4(record.namespaceRoot);
|
|
5846
|
+
const authStateFile = cleanString4(record.authStateFile);
|
|
5847
|
+
const checkoutBaseDir = cleanString4(record.checkoutBaseDir);
|
|
5848
|
+
const snapshotBaseDir = cleanString4(record.snapshotBaseDir);
|
|
5849
|
+
const createdAt = cleanString4(record.createdAt);
|
|
5850
|
+
if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
|
|
5851
|
+
return null;
|
|
5852
|
+
return {
|
|
5853
|
+
token,
|
|
5854
|
+
namespaceKey,
|
|
5855
|
+
namespaceRoot,
|
|
5856
|
+
authStateFile,
|
|
5857
|
+
checkoutBaseDir,
|
|
5858
|
+
snapshotBaseDir,
|
|
5859
|
+
createdAt,
|
|
5860
|
+
login: cleanString4(record.login),
|
|
5861
|
+
userId: cleanString4(record.userId),
|
|
5862
|
+
selectedRepo: cleanString4(record.selectedRepo)
|
|
5863
|
+
};
|
|
5864
|
+
}
|
|
5865
|
+
function readIndex(indexFile) {
|
|
5866
|
+
if (!existsSync10(indexFile))
|
|
5867
|
+
return [];
|
|
5868
|
+
try {
|
|
5869
|
+
const parsed = JSON.parse(readFileSync7(indexFile, "utf8"));
|
|
5870
|
+
return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
|
|
5871
|
+
const parsedEntry = parseEntry(entry);
|
|
5872
|
+
return parsedEntry ? [parsedEntry] : [];
|
|
5873
|
+
}) : [];
|
|
5874
|
+
} catch {
|
|
5875
|
+
return [];
|
|
5876
|
+
}
|
|
5877
|
+
}
|
|
5878
|
+
function writeIndex(indexFile, sessions) {
|
|
5879
|
+
mkdirSync10(dirname12(indexFile), { recursive: true });
|
|
5880
|
+
writeFileSync9(indexFile, `${JSON.stringify({ sessions }, null, 2)}
|
|
5881
|
+
`, { encoding: "utf8", mode: 384 });
|
|
5882
|
+
try {
|
|
5883
|
+
chmodSync3(indexFile, 384);
|
|
5884
|
+
} catch {}
|
|
5885
|
+
}
|
|
5886
|
+
function registerGitHubApiSession(input) {
|
|
5887
|
+
const cleanToken = cleanString4(input.token);
|
|
5888
|
+
if (!cleanToken)
|
|
5889
|
+
throw new Error("GitHub API session token is required");
|
|
5890
|
+
const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
|
|
5891
|
+
const createdAt = new Date().toISOString();
|
|
5892
|
+
const entry = {
|
|
5893
|
+
token: cleanToken,
|
|
5894
|
+
login: input.namespace.login,
|
|
5895
|
+
userId: input.namespace.userId,
|
|
5896
|
+
namespaceKey: input.namespace.key,
|
|
5897
|
+
namespaceRoot: input.namespace.root,
|
|
5898
|
+
authStateFile: input.namespace.authStateFile,
|
|
5899
|
+
checkoutBaseDir: input.namespace.checkoutBaseDir,
|
|
5900
|
+
snapshotBaseDir: input.namespace.snapshotBaseDir,
|
|
5901
|
+
selectedRepo: cleanString4(input.selectedRepo),
|
|
5902
|
+
createdAt
|
|
5903
|
+
};
|
|
5904
|
+
const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
|
|
5905
|
+
writeIndex(indexFile, [...previous.slice(-199), entry]);
|
|
5906
|
+
return entry;
|
|
5907
|
+
}
|
|
5908
|
+
function readGitHubApiSession(input) {
|
|
5909
|
+
const cleanToken = cleanString4(input.token);
|
|
5910
|
+
if (!cleanToken)
|
|
5911
|
+
return null;
|
|
5912
|
+
return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
|
|
5913
|
+
}
|
|
5914
|
+
|
|
5085
5915
|
// packages/server/src/server-helpers/inspector-agent-lifecycle.ts
|
|
5086
5916
|
function createInspectorAgentLifecycleController(options) {
|
|
5087
5917
|
const initialDelayMs = Math.max(1, options.initialDelayMs ?? 5000);
|
|
@@ -5185,21 +6015,21 @@ function inspectorAgentLifecycleSnapshot(input) {
|
|
|
5185
6015
|
// packages/server/src/server-helpers/project-registry.ts
|
|
5186
6016
|
import { createHash as createHash2 } from "crypto";
|
|
5187
6017
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5188
|
-
import { existsSync as
|
|
5189
|
-
import { dirname as
|
|
6018
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync8, readdirSync as readdirSync3, writeFileSync as writeFileSync10 } from "fs";
|
|
6019
|
+
import { dirname as dirname13, resolve as resolve18 } from "path";
|
|
5190
6020
|
function normalizeRepoSlug(value) {
|
|
5191
6021
|
const trimmed = value.trim();
|
|
5192
6022
|
return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
|
|
5193
6023
|
}
|
|
5194
6024
|
function registryPath(projectRoot) {
|
|
5195
|
-
return
|
|
6025
|
+
return resolve18(projectRoot, ".rig", "state", "projects.json");
|
|
5196
6026
|
}
|
|
5197
6027
|
function readRegistry(projectRoot) {
|
|
5198
6028
|
const path = registryPath(projectRoot);
|
|
5199
|
-
if (!
|
|
6029
|
+
if (!existsSync11(path))
|
|
5200
6030
|
return {};
|
|
5201
6031
|
try {
|
|
5202
|
-
const payload = JSON.parse(
|
|
6032
|
+
const payload = JSON.parse(readFileSync8(path, "utf8"));
|
|
5203
6033
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
5204
6034
|
return {};
|
|
5205
6035
|
const projects = payload.projects;
|
|
@@ -5210,14 +6040,14 @@ function readRegistry(projectRoot) {
|
|
|
5210
6040
|
}
|
|
5211
6041
|
function writeRegistry(projectRoot, projects) {
|
|
5212
6042
|
const path = registryPath(projectRoot);
|
|
5213
|
-
|
|
5214
|
-
|
|
6043
|
+
mkdirSync11(dirname13(path), { recursive: true });
|
|
6044
|
+
writeFileSync10(path, `${JSON.stringify({ projects }, null, 2)}
|
|
5215
6045
|
`, "utf8");
|
|
5216
6046
|
}
|
|
5217
6047
|
function resolveConfigPath(projectRoot) {
|
|
5218
6048
|
for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
|
|
5219
|
-
const path =
|
|
5220
|
-
if (
|
|
6049
|
+
const path = resolve18(projectRoot, name);
|
|
6050
|
+
if (existsSync11(path))
|
|
5221
6051
|
return path;
|
|
5222
6052
|
}
|
|
5223
6053
|
return null;
|
|
@@ -5226,7 +6056,7 @@ function hashFile(path) {
|
|
|
5226
6056
|
if (!path)
|
|
5227
6057
|
return null;
|
|
5228
6058
|
try {
|
|
5229
|
-
return createHash2("sha256").update(
|
|
6059
|
+
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
5230
6060
|
} catch {
|
|
5231
6061
|
return null;
|
|
5232
6062
|
}
|
|
@@ -5242,11 +6072,11 @@ function readDefaultBranch(projectRoot) {
|
|
|
5242
6072
|
return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
|
|
5243
6073
|
}
|
|
5244
6074
|
function buildRunSummary(projectRoot) {
|
|
5245
|
-
const runsDir =
|
|
6075
|
+
const runsDir = resolve18(projectRoot, ".rig", "runs");
|
|
5246
6076
|
try {
|
|
5247
6077
|
const runs = readdirSync3(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
|
|
5248
6078
|
try {
|
|
5249
|
-
const run = JSON.parse(
|
|
6079
|
+
const run = JSON.parse(readFileSync8(resolve18(runsDir, entry.name, "run.json"), "utf8"));
|
|
5250
6080
|
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 : "" }];
|
|
5251
6081
|
} catch {
|
|
5252
6082
|
return [];
|
|
@@ -5298,10 +6128,14 @@ function upsertProjectRecord(projectRoot, input) {
|
|
|
5298
6128
|
function linkProjectCheckout(projectRoot, repoSlug, checkout) {
|
|
5299
6129
|
return upsertProjectRecord(projectRoot, { repoSlug, checkout });
|
|
5300
6130
|
}
|
|
6131
|
+
function projectRegistryContainsCheckout(projectRoot, checkoutPath) {
|
|
6132
|
+
const target = resolve18(checkoutPath);
|
|
6133
|
+
return Object.values(readRegistry(projectRoot)).some((project) => project.checkouts.some((checkout) => checkout.path ? resolve18(checkout.path) === target : false));
|
|
6134
|
+
}
|
|
5301
6135
|
|
|
5302
6136
|
// packages/server/src/server-helpers/remote-checkout.ts
|
|
5303
|
-
import { existsSync as
|
|
5304
|
-
import { dirname as
|
|
6137
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
|
|
6138
|
+
import { dirname as dirname14, isAbsolute as isAbsolute3, relative as relative4, resolve as resolve19 } from "path";
|
|
5305
6139
|
function safeSlugSegments(repoSlug) {
|
|
5306
6140
|
const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
|
|
5307
6141
|
if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
|
|
@@ -5318,7 +6152,7 @@ function safeCheckoutKey(value) {
|
|
|
5318
6152
|
}
|
|
5319
6153
|
function repoSlugPath(baseDir, repoSlug, checkoutKey) {
|
|
5320
6154
|
const key = safeCheckoutKey(checkoutKey);
|
|
5321
|
-
return
|
|
6155
|
+
return resolve19(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
|
|
5322
6156
|
}
|
|
5323
6157
|
function sanitizeSnapshotId(value, fallback) {
|
|
5324
6158
|
const raw = (value ?? fallback).trim();
|
|
@@ -5326,7 +6160,7 @@ function sanitizeSnapshotId(value, fallback) {
|
|
|
5326
6160
|
return safe || fallback;
|
|
5327
6161
|
}
|
|
5328
6162
|
function assertWithinRoot(root, relativePath) {
|
|
5329
|
-
if (!relativePath ||
|
|
6163
|
+
if (!relativePath || isAbsolute3(relativePath) || relativePath.includes("\x00")) {
|
|
5330
6164
|
throw new Error(`Invalid snapshot file path: ${relativePath}`);
|
|
5331
6165
|
}
|
|
5332
6166
|
const normalizedRelative = relativePath.replace(/\\/g, "/");
|
|
@@ -5334,9 +6168,9 @@ function assertWithinRoot(root, relativePath) {
|
|
|
5334
6168
|
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
5335
6169
|
throw new Error(`Unsafe snapshot file path: ${relativePath}`);
|
|
5336
6170
|
}
|
|
5337
|
-
const target =
|
|
5338
|
-
const rel =
|
|
5339
|
-
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." ||
|
|
6171
|
+
const target = resolve19(root, ...segments);
|
|
6172
|
+
const rel = relative4(root, target);
|
|
6173
|
+
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute3(rel)) {
|
|
5340
6174
|
throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
|
|
5341
6175
|
}
|
|
5342
6176
|
return target;
|
|
@@ -5354,17 +6188,17 @@ function parseSnapshotArchiveContentBase64(contentBase64) {
|
|
|
5354
6188
|
function extractUploadedSnapshotArchive(input) {
|
|
5355
6189
|
const archive = decodeSnapshotArchive(input.archive);
|
|
5356
6190
|
const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
|
|
5357
|
-
const checkoutPath =
|
|
5358
|
-
|
|
6191
|
+
const checkoutPath = resolve19(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
|
|
6192
|
+
mkdirSync12(checkoutPath, { recursive: true });
|
|
5359
6193
|
for (const file of archive.files) {
|
|
5360
6194
|
if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
|
|
5361
6195
|
throw new Error("Invalid snapshot archive file entry");
|
|
5362
6196
|
}
|
|
5363
6197
|
const target = assertWithinRoot(checkoutPath, file.path);
|
|
5364
|
-
|
|
5365
|
-
|
|
6198
|
+
mkdirSync12(dirname14(target), { recursive: true });
|
|
6199
|
+
writeFileSync11(target, Buffer.from(file.contentBase64, "base64"));
|
|
5366
6200
|
}
|
|
5367
|
-
|
|
6201
|
+
writeFileSync11(resolve19(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
|
|
5368
6202
|
repoSlug: input.repoSlug,
|
|
5369
6203
|
snapshotId,
|
|
5370
6204
|
fileCount: archive.files.length,
|
|
@@ -5399,7 +6233,7 @@ function gitCredentialConfig(token) {
|
|
|
5399
6233
|
};
|
|
5400
6234
|
}
|
|
5401
6235
|
async function prepareRemoteCheckout(input) {
|
|
5402
|
-
const exists = input.exists ??
|
|
6236
|
+
const exists = input.exists ?? existsSync12;
|
|
5403
6237
|
const strategy = input.strategy;
|
|
5404
6238
|
if (strategy.kind === "uploaded-snapshot") {
|
|
5405
6239
|
return extractUploadedSnapshotArchive({
|
|
@@ -5411,7 +6245,7 @@ async function prepareRemoteCheckout(input) {
|
|
|
5411
6245
|
});
|
|
5412
6246
|
}
|
|
5413
6247
|
if (strategy.kind === "existing-path") {
|
|
5414
|
-
const checkoutPath2 =
|
|
6248
|
+
const checkoutPath2 = resolve19(strategy.path);
|
|
5415
6249
|
if (!exists(checkoutPath2)) {
|
|
5416
6250
|
throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
|
|
5417
6251
|
}
|
|
@@ -5447,9 +6281,9 @@ function buildServerControlStatus() {
|
|
|
5447
6281
|
};
|
|
5448
6282
|
}
|
|
5449
6283
|
function buildProjectConfigStatus(root) {
|
|
5450
|
-
const hasConfigTs =
|
|
5451
|
-
const hasConfigJson =
|
|
5452
|
-
const hasLegacyTaskConfig =
|
|
6284
|
+
const hasConfigTs = existsSync13(resolve20(root, "rig.config.ts"));
|
|
6285
|
+
const hasConfigJson = existsSync13(resolve20(root, "rig.config.json"));
|
|
6286
|
+
const hasLegacyTaskConfig = existsSync13(resolve20(root, ".rig", "task-config.json"));
|
|
5453
6287
|
let kind = "missing";
|
|
5454
6288
|
if (hasConfigTs)
|
|
5455
6289
|
kind = "rig-config-ts";
|
|
@@ -5466,6 +6300,75 @@ function buildProjectConfigStatus(root) {
|
|
|
5466
6300
|
suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
|
|
5467
6301
|
};
|
|
5468
6302
|
}
|
|
6303
|
+
var RIG_GITHUB_LIFECYCLE_LABELS = [
|
|
6304
|
+
"ready",
|
|
6305
|
+
"blocked",
|
|
6306
|
+
"in-progress",
|
|
6307
|
+
"under-review",
|
|
6308
|
+
"failed",
|
|
6309
|
+
"cancelled",
|
|
6310
|
+
"rig:running",
|
|
6311
|
+
"rig:pr-open",
|
|
6312
|
+
"rig:ci-fixing",
|
|
6313
|
+
"rig:merging",
|
|
6314
|
+
"rig:done",
|
|
6315
|
+
"rig:needs-attention"
|
|
6316
|
+
];
|
|
6317
|
+
function githubProjectsEnabled2(config) {
|
|
6318
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
6319
|
+
return false;
|
|
6320
|
+
const root = config;
|
|
6321
|
+
const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
|
|
6322
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
6323
|
+
return projects?.enabled === true;
|
|
6324
|
+
}
|
|
6325
|
+
function githubIssueSourceRepo(config) {
|
|
6326
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
6327
|
+
return null;
|
|
6328
|
+
const root = config;
|
|
6329
|
+
const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
|
|
6330
|
+
const owner = normalizeString(taskSource?.owner);
|
|
6331
|
+
const repo = normalizeString(taskSource?.repo);
|
|
6332
|
+
if (taskSource?.kind === "github-issues" && owner && repo)
|
|
6333
|
+
return { owner, repo };
|
|
6334
|
+
const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
|
|
6335
|
+
const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
|
|
6336
|
+
const match = slug?.match(/^([^/]+)\/([^/]+)$/);
|
|
6337
|
+
return match ? { owner: match[1], repo: match[2] } : null;
|
|
6338
|
+
}
|
|
6339
|
+
async function ensureGitHubLifecycleLabels(projectRoot, config) {
|
|
6340
|
+
const repo = githubIssueSourceRepo(config);
|
|
6341
|
+
if (!repo)
|
|
6342
|
+
return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
6343
|
+
const token = createGitHubAuthStore(projectRoot).readToken();
|
|
6344
|
+
if (!token)
|
|
6345
|
+
return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
6346
|
+
const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
|
|
6347
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
|
|
6348
|
+
});
|
|
6349
|
+
const existingJson = await existingResponse.json().catch(() => []);
|
|
6350
|
+
const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
|
|
6351
|
+
const created = [];
|
|
6352
|
+
const alreadyPresent = [];
|
|
6353
|
+
const failed = [];
|
|
6354
|
+
for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
|
|
6355
|
+
if (existing.has(label)) {
|
|
6356
|
+
alreadyPresent.push(label);
|
|
6357
|
+
continue;
|
|
6358
|
+
}
|
|
6359
|
+
const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
|
|
6360
|
+
method: "POST",
|
|
6361
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
|
|
6362
|
+
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" })
|
|
6363
|
+
});
|
|
6364
|
+
if (response.ok || response.status === 422) {
|
|
6365
|
+
(response.status === 422 ? alreadyPresent : created).push(label);
|
|
6366
|
+
} else {
|
|
6367
|
+
failed.push({ label, error: await response.text().catch(() => response.statusText) });
|
|
6368
|
+
}
|
|
6369
|
+
}
|
|
6370
|
+
return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
|
|
6371
|
+
}
|
|
5469
6372
|
function normalizeCommit(value) {
|
|
5470
6373
|
const raw = normalizeString(value);
|
|
5471
6374
|
return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
|
|
@@ -5485,24 +6388,24 @@ function repoParts(repoSlug) {
|
|
|
5485
6388
|
return { owner, repo, slug: `${owner}/${repo}` };
|
|
5486
6389
|
}
|
|
5487
6390
|
function repairDir(checkoutPath) {
|
|
5488
|
-
const dir =
|
|
5489
|
-
|
|
6391
|
+
const dir = resolve20(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
|
|
6392
|
+
mkdirSync13(dir, { recursive: true });
|
|
5490
6393
|
return dir;
|
|
5491
6394
|
}
|
|
5492
6395
|
function backupCheckoutFile(checkoutPath, relativePath) {
|
|
5493
|
-
const source =
|
|
5494
|
-
const backupPath =
|
|
5495
|
-
|
|
5496
|
-
|
|
6396
|
+
const source = resolve20(checkoutPath, relativePath);
|
|
6397
|
+
const backupPath = resolve20(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
|
|
6398
|
+
mkdirSync13(dirname15(backupPath), { recursive: true });
|
|
6399
|
+
copyFileSync2(source, backupPath);
|
|
5497
6400
|
return backupPath;
|
|
5498
6401
|
}
|
|
5499
6402
|
function parsePackageJsonLosslessly(checkoutPath) {
|
|
5500
|
-
const packagePath =
|
|
5501
|
-
if (!
|
|
6403
|
+
const packagePath = resolve20(checkoutPath, "package.json");
|
|
6404
|
+
if (!existsSync13(packagePath)) {
|
|
5502
6405
|
return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
|
|
5503
6406
|
}
|
|
5504
6407
|
try {
|
|
5505
|
-
const parsed = JSON.parse(
|
|
6408
|
+
const parsed = JSON.parse(readFileSync9(packagePath, "utf8"));
|
|
5506
6409
|
return {
|
|
5507
6410
|
existed: true,
|
|
5508
6411
|
packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
|
|
@@ -5516,9 +6419,9 @@ function parsePackageJsonLosslessly(checkoutPath) {
|
|
|
5516
6419
|
}
|
|
5517
6420
|
}
|
|
5518
6421
|
function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
5519
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
5520
|
-
const packagePath =
|
|
5521
|
-
if (!hasConfig && !
|
|
6422
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
|
|
6423
|
+
const packagePath = resolve20(checkoutPath, "package.json");
|
|
6424
|
+
if (!hasConfig && !existsSync13(packagePath)) {
|
|
5522
6425
|
return { skipped: true, reason: "package.json and rig.config missing" };
|
|
5523
6426
|
}
|
|
5524
6427
|
const parsed = parsePackageJsonLosslessly(checkoutPath);
|
|
@@ -5537,7 +6440,7 @@ function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
|
5537
6440
|
}
|
|
5538
6441
|
const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
|
|
5539
6442
|
if (changed) {
|
|
5540
|
-
|
|
6443
|
+
writeFileSync12(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
|
|
5541
6444
|
`, "utf8");
|
|
5542
6445
|
}
|
|
5543
6446
|
return {
|
|
@@ -5563,11 +6466,11 @@ function configLooksStructurallyUsable(source) {
|
|
|
5563
6466
|
return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
|
|
5564
6467
|
}
|
|
5565
6468
|
function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
|
|
5566
|
-
const configPath =
|
|
5567
|
-
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
6469
|
+
const configPath = resolve20(checkoutPath, "rig.config.ts");
|
|
6470
|
+
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
|
|
5568
6471
|
if (existingConfigName) {
|
|
5569
|
-
const existingPath =
|
|
5570
|
-
const source =
|
|
6472
|
+
const existingPath = resolve20(checkoutPath, existingConfigName);
|
|
6473
|
+
const source = readFileSync9(existingPath, "utf8");
|
|
5571
6474
|
if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
|
|
5572
6475
|
return { path: existingPath, changed: false, reason: "config structurally complete" };
|
|
5573
6476
|
}
|
|
@@ -5581,7 +6484,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
5581
6484
|
}
|
|
5582
6485
|
}
|
|
5583
6486
|
const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
|
|
5584
|
-
|
|
6487
|
+
writeFileSync12(configPath, generatedRigConfigSource(repoSlug), "utf8");
|
|
5585
6488
|
return {
|
|
5586
6489
|
path: configPath,
|
|
5587
6490
|
changed: true,
|
|
@@ -5590,7 +6493,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
5590
6493
|
};
|
|
5591
6494
|
}
|
|
5592
6495
|
function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
5593
|
-
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
6496
|
+
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync13(resolve20(checkoutPath, name)));
|
|
5594
6497
|
if (!configFile)
|
|
5595
6498
|
return { ok: false, error: "missing rig config" };
|
|
5596
6499
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -5612,7 +6515,7 @@ function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
|
5612
6515
|
return { ok: true, configFile };
|
|
5613
6516
|
}
|
|
5614
6517
|
function installRemoteCheckoutPackages(checkoutPath) {
|
|
5615
|
-
if (!
|
|
6518
|
+
if (!existsSync13(resolve20(checkoutPath, "package.json"))) {
|
|
5616
6519
|
return { skipped: true, reason: "package.json missing" };
|
|
5617
6520
|
}
|
|
5618
6521
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -5625,8 +6528,8 @@ function installRemoteCheckoutPackages(checkoutPath) {
|
|
|
5625
6528
|
return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
|
|
5626
6529
|
}
|
|
5627
6530
|
function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
|
|
5628
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
5629
|
-
const hasPackage =
|
|
6531
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync13(resolve20(checkoutPath, name)));
|
|
6532
|
+
const hasPackage = existsSync13(resolve20(checkoutPath, "package.json"));
|
|
5630
6533
|
if (!hasConfig && !hasPackage) {
|
|
5631
6534
|
return {
|
|
5632
6535
|
packageJson: { skipped: true, reason: "package.json and rig.config missing" },
|
|
@@ -5724,26 +6627,26 @@ function buildRemoteRunLogEntry(body, identifiers) {
|
|
|
5724
6627
|
}
|
|
5725
6628
|
function readGitHeadCommit(projectRoot) {
|
|
5726
6629
|
try {
|
|
5727
|
-
let gitDir =
|
|
6630
|
+
let gitDir = resolve20(projectRoot, ".git");
|
|
5728
6631
|
try {
|
|
5729
|
-
const dotGit =
|
|
6632
|
+
const dotGit = readFileSync9(gitDir, "utf8").trim();
|
|
5730
6633
|
const gitDirPrefix = "gitdir:";
|
|
5731
6634
|
if (dotGit.startsWith(gitDirPrefix)) {
|
|
5732
|
-
gitDir =
|
|
6635
|
+
gitDir = resolve20(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
|
|
5733
6636
|
}
|
|
5734
6637
|
} catch {}
|
|
5735
|
-
const head =
|
|
6638
|
+
const head = readFileSync9(resolve20(gitDir, "HEAD"), "utf8").trim();
|
|
5736
6639
|
const refPrefix = "ref:";
|
|
5737
6640
|
if (!head.startsWith(refPrefix)) {
|
|
5738
6641
|
return normalizeCommit(head);
|
|
5739
6642
|
}
|
|
5740
6643
|
const ref = head.slice(refPrefix.length).trim();
|
|
5741
|
-
const refPath =
|
|
5742
|
-
if (
|
|
5743
|
-
return normalizeCommit(
|
|
6644
|
+
const refPath = resolve20(gitDir, ref);
|
|
6645
|
+
if (existsSync13(refPath)) {
|
|
6646
|
+
return normalizeCommit(readFileSync9(refPath, "utf8").trim());
|
|
5744
6647
|
}
|
|
5745
|
-
const commonDir = normalizeString(
|
|
5746
|
-
return commonDir ? normalizeCommit(
|
|
6648
|
+
const commonDir = normalizeString(readFileSync9(resolve20(gitDir, "commondir"), "utf8"));
|
|
6649
|
+
return commonDir ? normalizeCommit(readFileSync9(resolve20(gitDir, commonDir, ref), "utf8").trim()) : null;
|
|
5747
6650
|
} catch {
|
|
5748
6651
|
return null;
|
|
5749
6652
|
}
|
|
@@ -5763,7 +6666,8 @@ function isAuthorizedInspectorStreamRequest(req, authToken) {
|
|
|
5763
6666
|
}
|
|
5764
6667
|
function buildDeploymentStatus(projectRoot) {
|
|
5765
6668
|
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);
|
|
5766
|
-
const
|
|
6669
|
+
const deploymentRoot = normalizeString(process.env.RIG_HOST_PROJECT_ROOT) ?? projectRoot;
|
|
6670
|
+
const gitCommit = envCommit ?? readGitHeadCommit(deploymentRoot) ?? readGitHeadCommit(projectRoot);
|
|
5767
6671
|
return {
|
|
5768
6672
|
currentCommit: gitCommit,
|
|
5769
6673
|
commitSource: envCommit ? "env" : gitCommit ? "git" : null,
|
|
@@ -5781,9 +6685,9 @@ function configuredRepoFromTaskSource(taskSource) {
|
|
|
5781
6685
|
const repo = normalizeString(taskSource?.repo);
|
|
5782
6686
|
return owner && repo ? `${owner}/${repo}` : null;
|
|
5783
6687
|
}
|
|
5784
|
-
async function buildTaskSourceStatus(state, config) {
|
|
6688
|
+
async function buildTaskSourceStatus(state, config, requestAuth) {
|
|
5785
6689
|
const diagnostics = state.snapshotService.getTaskSourceErrors();
|
|
5786
|
-
const selectedRepo =
|
|
6690
|
+
const selectedRepo = requestScopedAuthStore(state.projectRoot, requestAuth).status({
|
|
5787
6691
|
oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
|
|
5788
6692
|
}).selectedRepo;
|
|
5789
6693
|
try {
|
|
@@ -5836,37 +6740,146 @@ function isLoopbackRequest(req) {
|
|
|
5836
6740
|
}
|
|
5837
6741
|
}
|
|
5838
6742
|
function isPublicRigAuthBootstrapRoute(pathname) {
|
|
5839
|
-
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";
|
|
6743
|
+
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";
|
|
6744
|
+
}
|
|
6745
|
+
function buildRigInstallScript() {
|
|
6746
|
+
return `#!/usr/bin/env bash
|
|
6747
|
+
set -euo pipefail
|
|
6748
|
+
|
|
6749
|
+
say() {
|
|
6750
|
+
printf 'rig-install: %s
|
|
6751
|
+
' "$*"
|
|
6752
|
+
}
|
|
6753
|
+
|
|
6754
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
6755
|
+
say "Bun not found; installing Bun first"
|
|
6756
|
+
curl -fsSL https://bun.sh/install | bash
|
|
6757
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
6758
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
6759
|
+
fi
|
|
6760
|
+
|
|
6761
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
6762
|
+
printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
|
|
6763
|
+
' >&2
|
|
6764
|
+
exit 1
|
|
6765
|
+
fi
|
|
6766
|
+
|
|
6767
|
+
say "Installing @h-rig/cli@latest"
|
|
6768
|
+
bun add -g --force @h-rig/cli@latest
|
|
6769
|
+
|
|
6770
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
6771
|
+
BUN_RIG="$BUN_INSTALL/bin/rig"
|
|
6772
|
+
if [ ! -x "$BUN_RIG" ]; then
|
|
6773
|
+
printf 'rig-install: expected Bun global rig at %s but it was not executable.
|
|
6774
|
+
' "$BUN_RIG" >&2
|
|
6775
|
+
exit 1
|
|
6776
|
+
fi
|
|
6777
|
+
|
|
6778
|
+
USER_BIN="$HOME/.local/bin"
|
|
6779
|
+
mkdir -p "$USER_BIN"
|
|
6780
|
+
cat > "$USER_BIN/rig" <<'RIG_SHIM'
|
|
6781
|
+
#!/usr/bin/env bash
|
|
6782
|
+
set -euo pipefail
|
|
6783
|
+
exec "\${BUN_INSTALL:-$HOME/.bun}/bin/rig" "$@"
|
|
6784
|
+
RIG_SHIM
|
|
6785
|
+
chmod +x "$USER_BIN/rig"
|
|
6786
|
+
|
|
6787
|
+
export PATH="$USER_BIN:$BUN_INSTALL/bin:$PATH"
|
|
6788
|
+
if command -v hash >/dev/null 2>&1; then hash -r; fi
|
|
6789
|
+
|
|
6790
|
+
if ! command -v rig >/dev/null 2>&1; then
|
|
6791
|
+
printf 'rig-install: rig installed, but rig is not on PATH. Add %s and %s/bin to PATH and retry.
|
|
6792
|
+
' "$USER_BIN" "$BUN_INSTALL" >&2
|
|
6793
|
+
exit 1
|
|
6794
|
+
fi
|
|
6795
|
+
|
|
6796
|
+
say "Verifying rig"
|
|
6797
|
+
"$BUN_RIG" --help >/dev/null
|
|
6798
|
+
rig --help >/dev/null
|
|
6799
|
+
say "Done. Run: rig --help"
|
|
6800
|
+
`;
|
|
5840
6801
|
}
|
|
5841
6802
|
function normalizePrMode(value) {
|
|
5842
6803
|
const mode = normalizeString(value);
|
|
5843
6804
|
return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
|
|
5844
6805
|
}
|
|
6806
|
+
function requestAuthResult(input) {
|
|
6807
|
+
return {
|
|
6808
|
+
authorized: input.authorized,
|
|
6809
|
+
actor: input.actor ?? null,
|
|
6810
|
+
reason: input.reason,
|
|
6811
|
+
login: input.login ?? null,
|
|
6812
|
+
userId: input.userId ?? null,
|
|
6813
|
+
userNamespace: input.userNamespace ?? null,
|
|
6814
|
+
authStateFile: input.authStateFile ?? null
|
|
6815
|
+
};
|
|
6816
|
+
}
|
|
6817
|
+
function namespaceFromSessionIndex(entry) {
|
|
6818
|
+
const stateDir2 = dirname15(entry.authStateFile);
|
|
6819
|
+
return {
|
|
6820
|
+
key: entry.namespaceKey,
|
|
6821
|
+
userId: entry.userId,
|
|
6822
|
+
login: entry.login,
|
|
6823
|
+
root: entry.namespaceRoot,
|
|
6824
|
+
stateDir: stateDir2,
|
|
6825
|
+
authStateFile: entry.authStateFile,
|
|
6826
|
+
metadataFile: resolve20(stateDir2, "user-namespace.json"),
|
|
6827
|
+
checkoutBaseDir: entry.checkoutBaseDir,
|
|
6828
|
+
snapshotBaseDir: entry.snapshotBaseDir
|
|
6829
|
+
};
|
|
6830
|
+
}
|
|
5845
6831
|
function authorizeRigHttpRequest(input) {
|
|
5846
6832
|
if (input.legacyAuthorized) {
|
|
5847
|
-
return { authorized: true, actor: "rig-local-server", reason: "server-token" };
|
|
6833
|
+
return requestAuthResult({ authorized: true, actor: "rig-local-server", reason: "server-token" });
|
|
5848
6834
|
}
|
|
5849
6835
|
const bearer = bearerTokenFromRequest(input.req);
|
|
5850
6836
|
const store = createGitHubAuthStore(input.projectRoot);
|
|
5851
6837
|
const storedToken = store.readToken();
|
|
5852
6838
|
const session = bearer ? store.readApiSession(bearer) : null;
|
|
5853
6839
|
if (session) {
|
|
5854
|
-
return {
|
|
6840
|
+
return requestAuthResult({
|
|
6841
|
+
authorized: true,
|
|
6842
|
+
actor: session.login ?? "github-operator",
|
|
6843
|
+
reason: "github-session",
|
|
6844
|
+
login: session.login,
|
|
6845
|
+
userId: session.userId,
|
|
6846
|
+
authStateFile: store.stateFile
|
|
6847
|
+
});
|
|
5855
6848
|
}
|
|
5856
6849
|
if (bearer && storedToken && bearer === storedToken) {
|
|
5857
6850
|
const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
5858
|
-
return {
|
|
6851
|
+
return requestAuthResult({
|
|
6852
|
+
authorized: true,
|
|
6853
|
+
actor: status.login ?? "github-operator",
|
|
6854
|
+
reason: "github-token",
|
|
6855
|
+
login: status.login,
|
|
6856
|
+
userId: status.userId,
|
|
6857
|
+
authStateFile: store.stateFile
|
|
6858
|
+
});
|
|
6859
|
+
}
|
|
6860
|
+
const indexedSession = readGitHubApiSession({ projectRoot: input.projectRoot, token: bearer });
|
|
6861
|
+
if (indexedSession) {
|
|
6862
|
+
const userNamespace = namespaceFromSessionIndex(indexedSession);
|
|
6863
|
+
return requestAuthResult({
|
|
6864
|
+
authorized: true,
|
|
6865
|
+
actor: indexedSession.login ?? "github-operator",
|
|
6866
|
+
reason: "github-user-session",
|
|
6867
|
+
login: indexedSession.login,
|
|
6868
|
+
userId: indexedSession.userId,
|
|
6869
|
+
userNamespace,
|
|
6870
|
+
authStateFile: indexedSession.authStateFile
|
|
6871
|
+
});
|
|
5859
6872
|
}
|
|
5860
6873
|
if (isPublicRigAuthBootstrapRoute(input.pathname)) {
|
|
5861
|
-
return { authorized: true, actor: null, reason: "public-bootstrap" };
|
|
6874
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "public-bootstrap" });
|
|
5862
6875
|
}
|
|
5863
6876
|
if (!input.serverAuthToken && !storedToken) {
|
|
5864
6877
|
if (isLoopbackRequest(input.req)) {
|
|
5865
|
-
return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
|
|
6878
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "loopback-dev-no-auth" });
|
|
5866
6879
|
}
|
|
5867
|
-
return { authorized: false, actor: null, reason: "auth-required" };
|
|
6880
|
+
return requestAuthResult({ authorized: false, actor: null, reason: "auth-required" });
|
|
5868
6881
|
}
|
|
5869
|
-
return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
|
|
6882
|
+
return requestAuthResult({ authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" });
|
|
5870
6883
|
}
|
|
5871
6884
|
async function fetchGitHubUserInfo(token) {
|
|
5872
6885
|
const response = await fetch("https://api.github.com/user", {
|
|
@@ -5886,6 +6899,67 @@ async function fetchGitHubUserInfo(token) {
|
|
|
5886
6899
|
scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
|
|
5887
6900
|
};
|
|
5888
6901
|
}
|
|
6902
|
+
function shouldWriteRootAuthCompat(projectRoot) {
|
|
6903
|
+
if (process.env.RIG_REMOTE_USER_NAMESPACE_ROOT?.trim())
|
|
6904
|
+
return false;
|
|
6905
|
+
const stateDir2 = normalizeString(process.env.RIG_STATE_DIR);
|
|
6906
|
+
if (!stateDir2)
|
|
6907
|
+
return true;
|
|
6908
|
+
return resolve20(stateDir2) === resolve20(projectRoot, ".rig", "state");
|
|
6909
|
+
}
|
|
6910
|
+
function requestScopedRegistryRoot(stateProjectRoot, requestAuth) {
|
|
6911
|
+
return requestAuth.userNamespace?.root ?? stateProjectRoot;
|
|
6912
|
+
}
|
|
6913
|
+
function requestScopedAuthStore(stateProjectRoot, requestAuth) {
|
|
6914
|
+
return requestAuth.authStateFile ? createGitHubAuthStoreFromStateFile(requestAuth.authStateFile) : createGitHubAuthStore(stateProjectRoot);
|
|
6915
|
+
}
|
|
6916
|
+
function userNamespaceResponse(namespace) {
|
|
6917
|
+
return namespace ? serializeRemoteUserNamespace(namespace) : undefined;
|
|
6918
|
+
}
|
|
6919
|
+
function resolveNamespacedBaseDir(input) {
|
|
6920
|
+
if (input.explicitBaseDir)
|
|
6921
|
+
return input.explicitBaseDir;
|
|
6922
|
+
const envBase = normalizeString(process.env[input.envName]);
|
|
6923
|
+
if (input.userNamespace) {
|
|
6924
|
+
return envBase ? resolve20(envBase, input.userNamespace.key) : input.userNamespace[input.legacySubdir === "remote-checkouts" ? "checkoutBaseDir" : "snapshotBaseDir"];
|
|
6925
|
+
}
|
|
6926
|
+
return envBase ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve20(normalizeString(process.env.RIG_STATE_DIR), input.legacySubdir) : resolve20(input.legacyProjectRoot, ".rig", input.legacySubdir));
|
|
6927
|
+
}
|
|
6928
|
+
function explicitCheckoutKey(body, checkoutInput, requestAuth) {
|
|
6929
|
+
return normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? (requestAuth.userNamespace ? undefined : "default");
|
|
6930
|
+
}
|
|
6931
|
+
function saveGitHubTokenForRemoteUser(input) {
|
|
6932
|
+
const namespace = resolveRemoteUserNamespace(input.projectRoot, { userId: input.user.userId, login: input.user.login });
|
|
6933
|
+
writeRemoteUserNamespaceMetadata(namespace);
|
|
6934
|
+
const store = createGitHubAuthStoreFromStateFile(namespace.authStateFile);
|
|
6935
|
+
store.saveToken({
|
|
6936
|
+
token: input.token,
|
|
6937
|
+
tokenSource: input.tokenSource,
|
|
6938
|
+
login: input.user.login,
|
|
6939
|
+
userId: input.user.userId,
|
|
6940
|
+
scopes: input.user.scopes,
|
|
6941
|
+
selectedRepo: input.selectedRepo
|
|
6942
|
+
});
|
|
6943
|
+
const apiSession = store.createApiSession();
|
|
6944
|
+
registerGitHubApiSession({ projectRoot: input.projectRoot, token: apiSession.token, namespace, selectedRepo: input.selectedRepo });
|
|
6945
|
+
const requestedRoot = normalizeString(input.requestedProjectRoot);
|
|
6946
|
+
if (requestedRoot && isAbsolute4(requestedRoot) && existsSync13(resolve20(requestedRoot))) {
|
|
6947
|
+
copyGitHubAuthStateToLocalProjectRoot(namespace.authStateFile, resolve20(requestedRoot));
|
|
6948
|
+
}
|
|
6949
|
+
if (shouldWriteRootAuthCompat(input.projectRoot)) {
|
|
6950
|
+
const rootStore = createGitHubAuthStore(input.projectRoot);
|
|
6951
|
+
rootStore.saveToken({
|
|
6952
|
+
token: input.token,
|
|
6953
|
+
tokenSource: input.tokenSource,
|
|
6954
|
+
login: input.user.login,
|
|
6955
|
+
userId: input.user.userId,
|
|
6956
|
+
scopes: input.user.scopes,
|
|
6957
|
+
selectedRepo: input.selectedRepo
|
|
6958
|
+
});
|
|
6959
|
+
rootStore.createApiSession();
|
|
6960
|
+
}
|
|
6961
|
+
return { store, namespace, apiSessionToken: apiSession.token };
|
|
6962
|
+
}
|
|
5889
6963
|
async function postGitHubForm(endpoint, body) {
|
|
5890
6964
|
const response = await fetch(endpoint, {
|
|
5891
6965
|
method: "POST",
|
|
@@ -5903,11 +6977,11 @@ function resolveRequestedProjectRoot(currentRoot, rawRoot) {
|
|
|
5903
6977
|
const requestedRoot = normalizeString(rawRoot);
|
|
5904
6978
|
if (!requestedRoot)
|
|
5905
6979
|
return currentRoot;
|
|
5906
|
-
if (!
|
|
6980
|
+
if (!isAbsolute4(requestedRoot)) {
|
|
5907
6981
|
throw new Error("projectRoot must be an absolute path on the Rig server host");
|
|
5908
6982
|
}
|
|
5909
|
-
const normalizedRoot =
|
|
5910
|
-
if (!
|
|
6983
|
+
const normalizedRoot = resolve20(requestedRoot);
|
|
6984
|
+
if (!existsSync13(normalizedRoot)) {
|
|
5911
6985
|
throw new Error("projectRoot does not exist on the Rig server host");
|
|
5912
6986
|
}
|
|
5913
6987
|
return normalizedRoot;
|
|
@@ -6183,7 +7257,7 @@ function redactSecretFields(value) {
|
|
|
6183
7257
|
return redacted;
|
|
6184
7258
|
}
|
|
6185
7259
|
function validateRemoteLease(deps, state, input) {
|
|
6186
|
-
const run =
|
|
7260
|
+
const run = readAuthorityRun7(state.projectRoot, input.runId);
|
|
6187
7261
|
if (!run) {
|
|
6188
7262
|
return { ok: false, response: deps.jsonResponse({ ok: false, error: "Remote run not found" }, 404) };
|
|
6189
7263
|
}
|
|
@@ -6203,6 +7277,43 @@ function createRigServerFetch(state, deps) {
|
|
|
6203
7277
|
return deps.withServerPathEnv(state.projectRoot, async () => {
|
|
6204
7278
|
const browserOrigin = deps.resolveAllowedBrowserOrigin(req);
|
|
6205
7279
|
const finalizeResponse = (response) => deps.withCorsHeaders(response, req, browserOrigin);
|
|
7280
|
+
const earlyUrl = new URL(req.url);
|
|
7281
|
+
const piEventsWsMatch = earlyUrl.pathname.match(/^\/api\/runs\/([^/]+)\/pi\/events$/);
|
|
7282
|
+
if (piEventsWsMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
7283
|
+
const queryToken = earlyUrl.searchParams.get("token");
|
|
7284
|
+
const authHeaders = new Headers(req.headers);
|
|
7285
|
+
if (queryToken && !authHeaders.has("authorization")) {
|
|
7286
|
+
authHeaders.set("authorization", `Bearer ${queryToken}`);
|
|
7287
|
+
}
|
|
7288
|
+
const legacyAuthorized = Boolean(state.authToken && queryToken === state.authToken);
|
|
7289
|
+
const requestAuth = authorizeRigHttpRequest({
|
|
7290
|
+
req: new Request(req.url, { method: req.method, headers: authHeaders }),
|
|
7291
|
+
pathname: earlyUrl.pathname,
|
|
7292
|
+
projectRoot: state.projectRoot,
|
|
7293
|
+
serverAuthToken: state.authToken,
|
|
7294
|
+
legacyAuthorized
|
|
7295
|
+
});
|
|
7296
|
+
if (!requestAuth.authorized) {
|
|
7297
|
+
return deps.jsonResponse({ ok: false, error: "Unauthorized WebSocket connection", reason: requestAuth.reason }, 401);
|
|
7298
|
+
}
|
|
7299
|
+
const runId = decodeURIComponent(piEventsWsMatch[1]);
|
|
7300
|
+
const resolved = resolveRunPiSessionProxy(state.projectRoot, runId);
|
|
7301
|
+
if (resolved === null)
|
|
7302
|
+
return deps.jsonResponse({ ok: false, error: "Run not found" }, 404);
|
|
7303
|
+
if ("pending" in resolved)
|
|
7304
|
+
return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500 }, 202);
|
|
7305
|
+
if (!server)
|
|
7306
|
+
return deps.jsonResponse({ ok: false, error: "WebSocket upgrade unavailable" }, 400);
|
|
7307
|
+
const upgraded = server.upgrade(req, {
|
|
7308
|
+
data: {
|
|
7309
|
+
kind: "pi-session-proxy",
|
|
7310
|
+
connectedAt: new Date().toISOString(),
|
|
7311
|
+
upstreamUrl: buildRunPiDaemonWebSocketUrl(resolved),
|
|
7312
|
+
runId
|
|
7313
|
+
}
|
|
7314
|
+
});
|
|
7315
|
+
return upgraded ? new Response(null) : deps.jsonResponse({ ok: false, error: "WebSocket upgrade failed" }, 400);
|
|
7316
|
+
}
|
|
6206
7317
|
const upgradeResponse = handleWebSocketUpgrade({
|
|
6207
7318
|
req,
|
|
6208
7319
|
server,
|
|
@@ -6240,6 +7351,13 @@ function createRigServerFetch(state, deps) {
|
|
|
6240
7351
|
notifications: state.targets.length
|
|
6241
7352
|
});
|
|
6242
7353
|
}
|
|
7354
|
+
if (url.pathname === "/install" && req.method === "GET") {
|
|
7355
|
+
return new Response(buildRigInstallScript(), {
|
|
7356
|
+
headers: {
|
|
7357
|
+
"Content-Type": "text/x-shellscript; charset=utf-8"
|
|
7358
|
+
}
|
|
7359
|
+
});
|
|
7360
|
+
}
|
|
6243
7361
|
const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
|
|
6244
7362
|
const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
|
|
6245
7363
|
const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
|
|
@@ -6452,16 +7570,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6452
7570
|
if (!source) {
|
|
6453
7571
|
return deps.badRequest("No task source is configured");
|
|
6454
7572
|
}
|
|
7573
|
+
if (!source.updateTask && !(update.status && source.updateStatus)) {
|
|
7574
|
+
return deps.badRequest("Configured task source does not support updates");
|
|
7575
|
+
}
|
|
6455
7576
|
const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
|
|
6456
7577
|
return;
|
|
6457
7578
|
}) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
|
|
6458
|
-
if (source.updateTask) {
|
|
6459
|
-
await source.updateTask(id, update);
|
|
6460
|
-
} else if (update.status && source.updateStatus) {
|
|
6461
|
-
await source.updateStatus(id, update.status);
|
|
6462
|
-
} else {
|
|
6463
|
-
return deps.badRequest("Configured task source does not support updates");
|
|
6464
|
-
}
|
|
6465
7579
|
const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
|
|
6466
7580
|
const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
|
|
6467
7581
|
taskId: id,
|
|
@@ -6470,6 +7584,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6470
7584
|
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
6471
7585
|
config: ctx?.config
|
|
6472
7586
|
}).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
|
|
7587
|
+
if (update.status && githubProjectsEnabled2(ctx?.config) && projectSync.synced === false) {
|
|
7588
|
+
return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
|
|
7589
|
+
}
|
|
7590
|
+
try {
|
|
7591
|
+
if (source.updateTask) {
|
|
7592
|
+
await source.updateTask(id, update);
|
|
7593
|
+
} else if (update.status && source.updateStatus) {
|
|
7594
|
+
await source.updateStatus(id, update.status);
|
|
7595
|
+
}
|
|
7596
|
+
} catch (error) {
|
|
7597
|
+
let rollback = null;
|
|
7598
|
+
const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
|
|
7599
|
+
if (update.status && previousStatus && githubProjectsEnabled2(ctx?.config) && projectSync.synced !== false) {
|
|
7600
|
+
rollback = await syncGitHubProjectStatusForTaskUpdate({
|
|
7601
|
+
taskId: id,
|
|
7602
|
+
status: previousStatus,
|
|
7603
|
+
issueNodeId,
|
|
7604
|
+
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
7605
|
+
config: ctx?.config
|
|
7606
|
+
}).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
|
|
7607
|
+
}
|
|
7608
|
+
return deps.jsonResponse({
|
|
7609
|
+
ok: false,
|
|
7610
|
+
id,
|
|
7611
|
+
projectSync,
|
|
7612
|
+
rollback,
|
|
7613
|
+
error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
7614
|
+
}, 502);
|
|
7615
|
+
}
|
|
6473
7616
|
deps.snapshotService.invalidate("github-issue-updated");
|
|
6474
7617
|
await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
|
|
6475
7618
|
return;
|
|
@@ -6478,25 +7621,40 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6478
7621
|
return deps.jsonResponse({ ok: true, id, projectSync });
|
|
6479
7622
|
}
|
|
6480
7623
|
if (url.pathname === "/api/workspace/task-labels") {
|
|
7624
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
|
|
7625
|
+
if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
|
|
7626
|
+
return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
|
|
7627
|
+
}
|
|
6481
7628
|
return deps.jsonResponse({
|
|
6482
7629
|
ok: true,
|
|
6483
7630
|
ready: true,
|
|
6484
7631
|
labelsReady: true,
|
|
6485
|
-
labels: [
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
7632
|
+
labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
|
|
7633
|
+
note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
|
|
7634
|
+
});
|
|
7635
|
+
}
|
|
7636
|
+
if (url.pathname === "/api/github/projects" && req.method === "GET") {
|
|
7637
|
+
const owner = normalizeString(url.searchParams.get("owner"));
|
|
7638
|
+
if (!owner)
|
|
7639
|
+
return deps.badRequest("owner is required");
|
|
7640
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
7641
|
+
if (!token)
|
|
7642
|
+
return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
|
|
7643
|
+
const projects = await listGitHubProjects({ owner, token }).catch((error) => {
|
|
7644
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
7645
|
+
});
|
|
7646
|
+
return deps.jsonResponse({ ok: true, projects });
|
|
7647
|
+
}
|
|
7648
|
+
const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
|
|
7649
|
+
if (projectStatusMatch && req.method === "GET") {
|
|
7650
|
+
const projectId = decodeURIComponent(projectStatusMatch[1]);
|
|
7651
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
7652
|
+
if (!token)
|
|
7653
|
+
return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
|
|
7654
|
+
const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
|
|
7655
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
6499
7656
|
});
|
|
7657
|
+
return deps.jsonResponse({ ok: true, field });
|
|
6500
7658
|
}
|
|
6501
7659
|
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
6502
7660
|
const body = await deps.readJsonBody(req);
|
|
@@ -6561,7 +7719,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6561
7719
|
}
|
|
6562
7720
|
if (url.pathname === "/api/server/status") {
|
|
6563
7721
|
const config = buildProjectConfigStatus(state.projectRoot);
|
|
6564
|
-
const taskSource = await buildTaskSourceStatus(state, config);
|
|
7722
|
+
const taskSource = await buildTaskSourceStatus(state, config, requestAuth);
|
|
6565
7723
|
return deps.jsonResponse({
|
|
6566
7724
|
ok: true,
|
|
6567
7725
|
projectRoot: state.projectRoot,
|
|
@@ -6585,8 +7743,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6585
7743
|
path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
|
|
6586
7744
|
ref: normalizeString(rawCheckout?.ref) ?? undefined
|
|
6587
7745
|
} : undefined;
|
|
6588
|
-
const
|
|
6589
|
-
|
|
7746
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7747
|
+
const record = upsertProjectRecord(registryRoot, { repoSlug, checkout });
|
|
7748
|
+
return deps.jsonResponse({ ok: true, project: record, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6590
7749
|
}
|
|
6591
7750
|
const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
|
|
6592
7751
|
if (snapshotUploadMatch && req.method === "POST") {
|
|
@@ -6599,8 +7758,15 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6599
7758
|
if (!archiveContentBase64) {
|
|
6600
7759
|
return deps.badRequest("archiveContentBase64 is required");
|
|
6601
7760
|
}
|
|
6602
|
-
const
|
|
6603
|
-
const
|
|
7761
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7762
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
7763
|
+
explicitBaseDir: normalizeString(body.baseDir),
|
|
7764
|
+
envName: "RIG_REMOTE_SNAPSHOT_BASE_DIR",
|
|
7765
|
+
userNamespace: requestAuth.userNamespace,
|
|
7766
|
+
legacyProjectRoot: state.projectRoot,
|
|
7767
|
+
legacySubdir: "remote-snapshots"
|
|
7768
|
+
});
|
|
7769
|
+
const checkoutKey = explicitCheckoutKey(body, body, requestAuth);
|
|
6604
7770
|
try {
|
|
6605
7771
|
const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
|
|
6606
7772
|
const checkout = extractUploadedSnapshotArchive({
|
|
@@ -6613,14 +7779,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6613
7779
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
6614
7780
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
6615
7781
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
6616
|
-
const project = linkProjectCheckout(
|
|
7782
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
6617
7783
|
kind: "uploaded-snapshot",
|
|
6618
7784
|
path: checkout.path,
|
|
6619
7785
|
ref: checkout.snapshotId
|
|
6620
7786
|
});
|
|
6621
7787
|
deps.snapshotService.invalidate("uploaded-snapshot-checkout");
|
|
6622
7788
|
deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
|
|
6623
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
7789
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6624
7790
|
} catch (error) {
|
|
6625
7791
|
return deps.jsonResponse({
|
|
6626
7792
|
ok: false,
|
|
@@ -6640,10 +7806,17 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6640
7806
|
if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
|
|
6641
7807
|
return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
|
|
6642
7808
|
}
|
|
6643
|
-
const
|
|
6644
|
-
const
|
|
7809
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7810
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
7811
|
+
explicitBaseDir: normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir),
|
|
7812
|
+
envName: "RIG_REMOTE_CHECKOUT_BASE_DIR",
|
|
7813
|
+
userNamespace: requestAuth.userNamespace,
|
|
7814
|
+
legacyProjectRoot: state.projectRoot,
|
|
7815
|
+
legacySubdir: "remote-checkouts"
|
|
7816
|
+
});
|
|
7817
|
+
const checkoutKey = explicitCheckoutKey(body, checkoutInput, requestAuth);
|
|
6645
7818
|
const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
|
|
6646
|
-
const credentialToken =
|
|
7819
|
+
const credentialToken = requestScopedAuthStore(state.projectRoot, requestAuth).readToken();
|
|
6647
7820
|
try {
|
|
6648
7821
|
const checkout = await prepareRemoteCheckout({
|
|
6649
7822
|
command: runRemoteCheckoutCommand,
|
|
@@ -6652,14 +7825,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6652
7825
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
6653
7826
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
6654
7827
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
6655
|
-
const project = linkProjectCheckout(
|
|
7828
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
6656
7829
|
kind: checkout.kind,
|
|
6657
7830
|
path: checkout.path,
|
|
6658
7831
|
ref: checkout.ref ?? checkout.snapshotId ?? undefined
|
|
6659
7832
|
});
|
|
6660
7833
|
deps.snapshotService.invalidate("remote-checkout-prepared");
|
|
6661
7834
|
deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
|
|
6662
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
7835
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6663
7836
|
} catch (error) {
|
|
6664
7837
|
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
6665
7838
|
}
|
|
@@ -6676,16 +7849,18 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6676
7849
|
if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
|
|
6677
7850
|
return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
|
|
6678
7851
|
}
|
|
6679
|
-
const
|
|
7852
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7853
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
6680
7854
|
kind,
|
|
6681
7855
|
path: normalizeString(body.path) ?? state.projectRoot,
|
|
6682
7856
|
ref: normalizeString(body.ref) ?? undefined
|
|
6683
7857
|
});
|
|
6684
|
-
return deps.jsonResponse({ ok: true, project });
|
|
7858
|
+
return deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6685
7859
|
}
|
|
6686
7860
|
if (req.method === "GET") {
|
|
6687
|
-
const
|
|
6688
|
-
|
|
7861
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
7862
|
+
const project = getProjectRecord(registryRoot, repoSlug);
|
|
7863
|
+
return project ? deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) }) : deps.notFound();
|
|
6689
7864
|
}
|
|
6690
7865
|
}
|
|
6691
7866
|
if (url.pathname === "/api/server/project-root" && req.method === "POST") {
|
|
@@ -6694,13 +7869,26 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6694
7869
|
if (!requestedRoot) {
|
|
6695
7870
|
return deps.badRequest("projectRoot is required");
|
|
6696
7871
|
}
|
|
6697
|
-
if (!
|
|
7872
|
+
if (!isAbsolute4(requestedRoot)) {
|
|
6698
7873
|
return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
|
|
6699
7874
|
}
|
|
6700
|
-
const normalizedRoot =
|
|
6701
|
-
const exists =
|
|
6702
|
-
if (exists) {
|
|
6703
|
-
|
|
7875
|
+
const normalizedRoot = resolve20(requestedRoot);
|
|
7876
|
+
const exists = existsSync13(normalizedRoot);
|
|
7877
|
+
if (exists && requestAuth.userNamespace) {
|
|
7878
|
+
const allowedByNamespace = isPathInsideNamespace(requestAuth.userNamespace.root, normalizedRoot);
|
|
7879
|
+
const allowedByRegistry = projectRegistryContainsCheckout(requestAuth.userNamespace.root, normalizedRoot);
|
|
7880
|
+
if (!allowedByNamespace && !allowedByRegistry) {
|
|
7881
|
+
return deps.jsonResponse({
|
|
7882
|
+
ok: false,
|
|
7883
|
+
error: "Requested project root is outside the authenticated GitHub user namespace.",
|
|
7884
|
+
projectRoot: state.projectRoot,
|
|
7885
|
+
requestedProjectRoot: normalizedRoot,
|
|
7886
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace)
|
|
7887
|
+
}, 403);
|
|
7888
|
+
}
|
|
7889
|
+
copyGitHubAuthStateToLocalProjectRoot(requestAuth.userNamespace.authStateFile, normalizedRoot);
|
|
7890
|
+
} else if (exists) {
|
|
7891
|
+
createGitHubAuthStore(state.projectRoot).copyToLocalProjectRoot(normalizedRoot);
|
|
6704
7892
|
}
|
|
6705
7893
|
const control = buildServerControlStatus();
|
|
6706
7894
|
const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
|
|
@@ -6715,7 +7903,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6715
7903
|
message: "Requested project root does not exist on the Rig server host."
|
|
6716
7904
|
}, 404);
|
|
6717
7905
|
}
|
|
6718
|
-
if (!
|
|
7906
|
+
if (!existsSync13(resolve20(normalizedRoot, "rig.config.ts")) && !existsSync13(resolve20(normalizedRoot, "rig.config.json"))) {
|
|
6719
7907
|
return deps.jsonResponse({
|
|
6720
7908
|
ok: false,
|
|
6721
7909
|
projectRoot: state.projectRoot,
|
|
@@ -6750,6 +7938,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6750
7938
|
exists,
|
|
6751
7939
|
control,
|
|
6752
7940
|
requiresRestart: false,
|
|
7941
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace),
|
|
6753
7942
|
message: "Project-root switch accepted. Rig server restart has been scheduled."
|
|
6754
7943
|
}, 202);
|
|
6755
7944
|
}
|
|
@@ -6764,11 +7953,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6764
7953
|
}, 409);
|
|
6765
7954
|
}
|
|
6766
7955
|
if (url.pathname === "/api/github/auth/status") {
|
|
6767
|
-
const store =
|
|
6768
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
|
|
7956
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
7957
|
+
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
6769
7958
|
}
|
|
6770
7959
|
if (url.pathname === "/api/github/repo/permissions") {
|
|
6771
|
-
const store =
|
|
7960
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
6772
7961
|
const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
6773
7962
|
if (!auth.signedIn) {
|
|
6774
7963
|
return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
|
|
@@ -6796,24 +7985,20 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6796
7985
|
}
|
|
6797
7986
|
try {
|
|
6798
7987
|
const user = await fetchGitHubUserInfo(token);
|
|
6799
|
-
const
|
|
6800
|
-
state.projectRoot,
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
}
|
|
6814
|
-
const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
|
|
6815
|
-
const apiSession = store.createApiSession();
|
|
6816
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
|
|
7988
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
7989
|
+
projectRoot: state.projectRoot,
|
|
7990
|
+
token,
|
|
7991
|
+
tokenSource: "manual-token",
|
|
7992
|
+
user,
|
|
7993
|
+
selectedRepo,
|
|
7994
|
+
requestedProjectRoot
|
|
7995
|
+
});
|
|
7996
|
+
return deps.jsonResponse({
|
|
7997
|
+
ok: true,
|
|
7998
|
+
...saved.store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }),
|
|
7999
|
+
apiSessionToken: saved.apiSessionToken,
|
|
8000
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
8001
|
+
});
|
|
6817
8002
|
} catch (error) {
|
|
6818
8003
|
const message = error instanceof Error ? error.message : String(error);
|
|
6819
8004
|
return deps.jsonResponse({ ok: false, error: message }, 400);
|
|
@@ -6880,9 +8065,21 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6880
8065
|
}
|
|
6881
8066
|
const token = result.payload.access_token;
|
|
6882
8067
|
const user = await fetchGitHubUserInfo(token);
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
8068
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
8069
|
+
projectRoot: state.projectRoot,
|
|
8070
|
+
token,
|
|
8071
|
+
tokenSource: "oauth-device",
|
|
8072
|
+
user,
|
|
8073
|
+
selectedRepo: null
|
|
8074
|
+
});
|
|
8075
|
+
store.clearPendingDevice(pollId);
|
|
8076
|
+
return deps.jsonResponse({
|
|
8077
|
+
ok: true,
|
|
8078
|
+
status: "signed-in",
|
|
8079
|
+
...saved.store.status({ oauthConfigured: true }),
|
|
8080
|
+
apiSessionToken: saved.apiSessionToken,
|
|
8081
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
8082
|
+
});
|
|
6886
8083
|
}
|
|
6887
8084
|
if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
|
|
6888
8085
|
const body = await deps.readJsonBody(req);
|
|
@@ -6891,7 +8088,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6891
8088
|
if (!owner || !repo) {
|
|
6892
8089
|
return deps.badRequest("owner and repo are required");
|
|
6893
8090
|
}
|
|
6894
|
-
const store =
|
|
8091
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
6895
8092
|
const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
6896
8093
|
const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
|
|
6897
8094
|
return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
|
|
@@ -6912,7 +8109,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6912
8109
|
return deps.badRequest(error instanceof Error ? error.message : String(error));
|
|
6913
8110
|
}
|
|
6914
8111
|
const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
6915
|
-
const configPath =
|
|
8112
|
+
const configPath = resolve20(targetRoot, "rig.config.ts");
|
|
6916
8113
|
const source = buildGitHubProjectConfigSource({
|
|
6917
8114
|
projectName: rawProjectName,
|
|
6918
8115
|
owner,
|
|
@@ -6924,8 +8121,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6924
8121
|
ok: true,
|
|
6925
8122
|
projectRoot: targetRoot,
|
|
6926
8123
|
configPath,
|
|
6927
|
-
exists:
|
|
6928
|
-
requiresOverwrite:
|
|
8124
|
+
exists: existsSync13(configPath),
|
|
8125
|
+
requiresOverwrite: existsSync13(configPath),
|
|
6929
8126
|
source,
|
|
6930
8127
|
owner,
|
|
6931
8128
|
repo,
|
|
@@ -6961,8 +8158,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6961
8158
|
assignee,
|
|
6962
8159
|
githubUserId: authStatus.userId ?? authStatus.login
|
|
6963
8160
|
});
|
|
6964
|
-
const configPath =
|
|
6965
|
-
if (
|
|
8161
|
+
const configPath = resolve20(targetRoot, "rig.config.ts");
|
|
8162
|
+
if (existsSync13(configPath) && !overwrite) {
|
|
6966
8163
|
return deps.jsonResponse({
|
|
6967
8164
|
ok: false,
|
|
6968
8165
|
error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
|
|
@@ -6978,11 +8175,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6978
8175
|
return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
|
|
6979
8176
|
}
|
|
6980
8177
|
let backupPath = null;
|
|
6981
|
-
if (
|
|
8178
|
+
if (existsSync13(configPath)) {
|
|
6982
8179
|
backupPath = backupConfigPath(configPath);
|
|
6983
|
-
|
|
8180
|
+
copyFileSync2(configPath, backupPath);
|
|
6984
8181
|
}
|
|
6985
|
-
|
|
8182
|
+
writeFileSync12(configPath, source, "utf8");
|
|
6986
8183
|
const selectedRepo = `${owner}/${repo}`;
|
|
6987
8184
|
store.saveSelectedRepo(selectedRepo);
|
|
6988
8185
|
const targetStore = createGitHubAuthStore(targetRoot);
|
|
@@ -7254,11 +8451,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7254
8451
|
const runId = normalizeString(body.runId);
|
|
7255
8452
|
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
7256
8453
|
const promptOverride = normalizeString(body.promptOverride);
|
|
8454
|
+
const restart = body.restart === true;
|
|
7257
8455
|
if (!runId) {
|
|
7258
8456
|
return deps.badRequest("runId is required");
|
|
7259
8457
|
}
|
|
7260
8458
|
try {
|
|
7261
|
-
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
|
|
8459
|
+
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
|
|
7262
8460
|
deps.broadcastSnapshotInvalidation(state);
|
|
7263
8461
|
return deps.jsonResponse({ ok: true, runId, createdAt });
|
|
7264
8462
|
} catch (error) {
|
|
@@ -7267,7 +8465,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7267
8465
|
}
|
|
7268
8466
|
if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
|
|
7269
8467
|
const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
|
|
7270
|
-
const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) =>
|
|
8468
|
+
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";
|
|
7271
8469
|
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
7272
8470
|
return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
|
|
7273
8471
|
}
|
|
@@ -7583,9 +8781,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7583
8781
|
} catch {
|
|
7584
8782
|
return deps.badRequest("Invalid artifact path");
|
|
7585
8783
|
}
|
|
7586
|
-
|
|
8784
|
+
mkdirSync13(dirname15(artifactPath), { recursive: true });
|
|
7587
8785
|
const bytes = Buffer.from(contentBase64, "base64");
|
|
7588
|
-
|
|
8786
|
+
writeFileSync12(artifactPath, bytes);
|
|
7589
8787
|
writeJsonFile4(`${artifactPath}.json`, {
|
|
7590
8788
|
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
7591
8789
|
runId,
|
|
@@ -7622,13 +8820,75 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7622
8820
|
}
|
|
7623
8821
|
const run = leaseValidation.run;
|
|
7624
8822
|
const completedAt = new Date().toISOString();
|
|
8823
|
+
const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
|
|
8824
|
+
if (run.taskId && workspaceDir) {
|
|
8825
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
8826
|
+
status: "reviewing",
|
|
8827
|
+
completedAt: null,
|
|
8828
|
+
hostId,
|
|
8829
|
+
endpointId: leaseId,
|
|
8830
|
+
worktreePath: workspaceDir,
|
|
8831
|
+
serverCloseout: {
|
|
8832
|
+
status: "pending",
|
|
8833
|
+
phase: "queued",
|
|
8834
|
+
requestedAt: completedAt,
|
|
8835
|
+
updatedAt: completedAt,
|
|
8836
|
+
runtimeWorkspace: workspaceDir,
|
|
8837
|
+
branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
|
|
8838
|
+
taskId: run.taskId,
|
|
8839
|
+
source: "remote-complete"
|
|
8840
|
+
}
|
|
8841
|
+
});
|
|
8842
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
8843
|
+
id: `log:${runId}:remote-server-closeout-requested`,
|
|
8844
|
+
title: "Server-owned closeout requested",
|
|
8845
|
+
detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
|
|
8846
|
+
tone: "info",
|
|
8847
|
+
status: "reviewing",
|
|
8848
|
+
createdAt: completedAt,
|
|
8849
|
+
payload: { workspaceDir, hostId, leaseId }
|
|
8850
|
+
}, "remote-server-closeout-requested");
|
|
8851
|
+
deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
|
|
8852
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8853
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
8854
|
+
status: "failed",
|
|
8855
|
+
completedAt: new Date().toISOString(),
|
|
8856
|
+
errorText: detail,
|
|
8857
|
+
serverCloseout: {
|
|
8858
|
+
status: "failed",
|
|
8859
|
+
phase: "failed",
|
|
8860
|
+
updatedAt: new Date().toISOString(),
|
|
8861
|
+
error: detail
|
|
8862
|
+
}
|
|
8863
|
+
});
|
|
8864
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
8865
|
+
id: `log:${runId}:remote-server-closeout-failed`,
|
|
8866
|
+
title: "Server-owned closeout failed",
|
|
8867
|
+
detail,
|
|
8868
|
+
tone: "error",
|
|
8869
|
+
status: "failed",
|
|
8870
|
+
createdAt: new Date().toISOString()
|
|
8871
|
+
}, "remote-server-closeout-failed");
|
|
8872
|
+
}).finally(() => {
|
|
8873
|
+
deps.reconcileScheduler(state, "remote-server-closeout-terminal");
|
|
8874
|
+
});
|
|
8875
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
8876
|
+
return deps.jsonResponse({
|
|
8877
|
+
ok: true,
|
|
8878
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
8879
|
+
hostId,
|
|
8880
|
+
runId,
|
|
8881
|
+
leaseId,
|
|
8882
|
+
closeout: "server-owned",
|
|
8883
|
+
acceptedAt: new Date().toISOString()
|
|
8884
|
+
});
|
|
8885
|
+
}
|
|
7625
8886
|
patchRunRecord(state.projectRoot, runId, {
|
|
7626
8887
|
status: "completed",
|
|
7627
8888
|
completedAt,
|
|
7628
8889
|
hostId,
|
|
7629
8890
|
endpointId: leaseId
|
|
7630
8891
|
});
|
|
7631
|
-
await updateRemoteRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "completed", completedAt, hostId, endpointId: leaseId }, "closed", "Remote Rig task run completed and closed this task.");
|
|
7632
8892
|
await deps.enqueueRunLinearEvent(state.projectRoot, {
|
|
7633
8893
|
type: "run.completed",
|
|
7634
8894
|
runId,
|
|
@@ -7747,12 +9007,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7747
9007
|
try {
|
|
7748
9008
|
const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
|
|
7749
9009
|
const runRoot = deps.normalizeRelativePath(runsRoot, runId);
|
|
7750
|
-
const artifactsRoot =
|
|
9010
|
+
const artifactsRoot = resolve20(runRoot, "remote-artifacts");
|
|
7751
9011
|
artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
|
|
7752
9012
|
} catch {
|
|
7753
9013
|
return deps.badRequest("Invalid artifact path");
|
|
7754
9014
|
}
|
|
7755
|
-
if (!
|
|
9015
|
+
if (!existsSync13(artifactPath)) {
|
|
7756
9016
|
return deps.notFound();
|
|
7757
9017
|
}
|
|
7758
9018
|
return new Response(Bun.file(artifactPath));
|
|
@@ -7765,6 +9025,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7765
9025
|
const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
|
|
7766
9026
|
return deps.jsonResponse(page);
|
|
7767
9027
|
}
|
|
9028
|
+
const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
|
|
9029
|
+
if (runTimelineMatch) {
|
|
9030
|
+
const runId = decodeURIComponent(runTimelineMatch[1]);
|
|
9031
|
+
const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
|
|
9032
|
+
const cursor = normalizeString(url.searchParams.get("cursor"));
|
|
9033
|
+
const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
|
|
9034
|
+
return deps.jsonResponse(page);
|
|
9035
|
+
}
|
|
7768
9036
|
const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
|
|
7769
9037
|
if (runSteerMatch && req.method === "POST") {
|
|
7770
9038
|
const runId = decodeURIComponent(runSteerMatch[1]);
|
|
@@ -7784,6 +9052,49 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7784
9052
|
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 404);
|
|
7785
9053
|
}
|
|
7786
9054
|
}
|
|
9055
|
+
const runPiMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/pi(?:\/(.*))?$/);
|
|
9056
|
+
if (runPiMatch) {
|
|
9057
|
+
const runId = decodeURIComponent(runPiMatch[1]);
|
|
9058
|
+
const action = runPiMatch[2] || "";
|
|
9059
|
+
const resolved = resolveRunPiSessionProxy(state.projectRoot, runId);
|
|
9060
|
+
if (resolved === null)
|
|
9061
|
+
return deps.notFound();
|
|
9062
|
+
if ("pending" in resolved) {
|
|
9063
|
+
if (action === "" && req.method === "GET") {
|
|
9064
|
+
return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500 }, 202);
|
|
9065
|
+
}
|
|
9066
|
+
return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500, error: "Pi session is not ready" }, 409);
|
|
9067
|
+
}
|
|
9068
|
+
if (action === "" && req.method === "GET")
|
|
9069
|
+
return deps.jsonResponse({ ready: true, metadata: resolved.metadata });
|
|
9070
|
+
const body = req.method === "GET" ? undefined : await deps.readJsonBody(req);
|
|
9071
|
+
const sessionPath = `/sessions/${encodeURIComponent(resolved.sessionId)}`;
|
|
9072
|
+
const daemonPath = (() => {
|
|
9073
|
+
if (action === "messages" && req.method === "GET")
|
|
9074
|
+
return `${sessionPath}/messages`;
|
|
9075
|
+
if (action === "status" && req.method === "GET")
|
|
9076
|
+
return `${sessionPath}/status`;
|
|
9077
|
+
if (action === "commands" && req.method === "GET")
|
|
9078
|
+
return `${sessionPath}/commands`;
|
|
9079
|
+
if (action === "prompt" && req.method === "POST")
|
|
9080
|
+
return `${sessionPath}/prompt`;
|
|
9081
|
+
if (action === "shell" && req.method === "POST")
|
|
9082
|
+
return `${sessionPath}/shell`;
|
|
9083
|
+
if (action === "commands/run" && req.method === "POST")
|
|
9084
|
+
return `${sessionPath}/commands/run`;
|
|
9085
|
+
if (action === "commands/respond" && req.method === "POST")
|
|
9086
|
+
return `${sessionPath}/commands/respond`;
|
|
9087
|
+
if (action === "extension-ui/respond" && req.method === "POST")
|
|
9088
|
+
return `${sessionPath}/extension-ui/respond`;
|
|
9089
|
+
if (action === "abort" && req.method === "POST")
|
|
9090
|
+
return `${sessionPath}/abort`;
|
|
9091
|
+
return null;
|
|
9092
|
+
})();
|
|
9093
|
+
if (!daemonPath)
|
|
9094
|
+
return deps.notFound();
|
|
9095
|
+
const proxied = await proxyRunPiHttp(state.projectRoot, runId, { method: req.method, daemonPath, body });
|
|
9096
|
+
return deps.jsonResponse(proxied.payload, proxied.status);
|
|
9097
|
+
}
|
|
7787
9098
|
const runSteeringAckMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steering\/ack$/);
|
|
7788
9099
|
if (runSteeringAckMatch && req.method === "POST") {
|
|
7789
9100
|
const runId = decodeURIComponent(runSteeringAckMatch[1]);
|
|
@@ -7906,7 +9217,7 @@ import {
|
|
|
7906
9217
|
RemoteWsClient as RemoteWsClient2
|
|
7907
9218
|
} from "@rig/runtime/control-plane/remote";
|
|
7908
9219
|
import { deleteRunState } from "@rig/runtime/control-plane/native/run-ops";
|
|
7909
|
-
import { readAuthorityRun as
|
|
9220
|
+
import { readAuthorityRun as readAuthorityRun8 } from "@rig/runtime/control-plane/authority-files";
|
|
7910
9221
|
function redactRemoteEndpoint2(endpoint) {
|
|
7911
9222
|
const { token, ...rest } = endpoint;
|
|
7912
9223
|
return {
|
|
@@ -8120,7 +9431,7 @@ async function routeWebSocketRequest(state, deps, request) {
|
|
|
8120
9431
|
if (!runId || !messageId || text === null) {
|
|
8121
9432
|
throw new Error("runId, messageId, and text are required");
|
|
8122
9433
|
}
|
|
8123
|
-
const run =
|
|
9434
|
+
const run = readAuthorityRun8(state.projectRoot, runId);
|
|
8124
9435
|
if (!run) {
|
|
8125
9436
|
throw new Error(`Run not found: ${runId}`);
|
|
8126
9437
|
}
|
|
@@ -8645,8 +9956,8 @@ async function routeWebSocketRequest(state, deps, request) {
|
|
|
8645
9956
|
}
|
|
8646
9957
|
|
|
8647
9958
|
// packages/server/src/server-helpers/inspector-jobs.ts
|
|
8648
|
-
import { existsSync as
|
|
8649
|
-
import { dirname as
|
|
9959
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync16, readFileSync as readFileSync12, writeFileSync as writeFileSync15 } from "fs";
|
|
9960
|
+
import { dirname as dirname18, resolve as resolve23 } from "path";
|
|
8650
9961
|
import { readJsonFile as readJsonFile3 } from "@rig/runtime/control-plane/authority-files";
|
|
8651
9962
|
import { resolveMonorepoRoot as resolveMonorepoRoot5 } from "@rig/runtime/control-plane/native/utils";
|
|
8652
9963
|
import { normalizeTaskLifecycleStatus as normalizeTaskLifecycleStatus2 } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -8754,8 +10065,8 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
8754
10065
|
|
|
8755
10066
|
// packages/server/src/inspector/mission.ts
|
|
8756
10067
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8757
|
-
import { appendFileSync, existsSync as
|
|
8758
|
-
import { dirname as
|
|
10068
|
+
import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync14, readFileSync as readFileSync10, readdirSync as readdirSync4, renameSync, writeFileSync as writeFileSync13 } from "fs";
|
|
10069
|
+
import { dirname as dirname16, join, resolve as resolve21 } from "path";
|
|
8759
10070
|
function isJsonValue(value) {
|
|
8760
10071
|
if (value === null)
|
|
8761
10072
|
return true;
|
|
@@ -8795,7 +10106,7 @@ function isRecord2(value) {
|
|
|
8795
10106
|
}
|
|
8796
10107
|
function readJsonRecord(path) {
|
|
8797
10108
|
try {
|
|
8798
|
-
const parsed = JSON.parse(
|
|
10109
|
+
const parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
8799
10110
|
if (!isRecord2(parsed)) {
|
|
8800
10111
|
return { ok: false, error: `Mission file ${path} does not contain an object` };
|
|
8801
10112
|
}
|
|
@@ -8875,14 +10186,14 @@ function missionActionDetails(mission) {
|
|
|
8875
10186
|
};
|
|
8876
10187
|
}
|
|
8877
10188
|
function writeJsonFile5(path, value) {
|
|
8878
|
-
|
|
10189
|
+
mkdirSync14(dirname16(path), { recursive: true });
|
|
8879
10190
|
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
8880
|
-
|
|
10191
|
+
writeFileSync13(tempPath, `${JSON.stringify(value, null, 2)}
|
|
8881
10192
|
`, "utf8");
|
|
8882
10193
|
renameSync(tempPath, path);
|
|
8883
10194
|
}
|
|
8884
10195
|
function resolveInspectorMissionPaths(projectRoot) {
|
|
8885
|
-
const inspectorDir =
|
|
10196
|
+
const inspectorDir = resolve21(resolveRigServerPaths(projectRoot).stateDir, "inspector");
|
|
8886
10197
|
return {
|
|
8887
10198
|
inspectorDir,
|
|
8888
10199
|
missionsDir: join(inspectorDir, "missions"),
|
|
@@ -8891,8 +10202,8 @@ function resolveInspectorMissionPaths(projectRoot) {
|
|
|
8891
10202
|
}
|
|
8892
10203
|
function createInspectorMissionController(options) {
|
|
8893
10204
|
const paths = resolveInspectorMissionPaths(options.projectRoot);
|
|
8894
|
-
|
|
8895
|
-
|
|
10205
|
+
mkdirSync14(paths.missionsDir, { recursive: true });
|
|
10206
|
+
mkdirSync14(paths.journalsDir, { recursive: true });
|
|
8896
10207
|
const now = options.now ?? (() => new Date().toISOString());
|
|
8897
10208
|
const nextId = options.idGenerator ?? (() => `mission:${randomUUID2()}`);
|
|
8898
10209
|
function missionPath(missionId) {
|
|
@@ -8902,15 +10213,15 @@ function createInspectorMissionController(options) {
|
|
|
8902
10213
|
return join(paths.journalsDir, `${missionId}.jsonl`);
|
|
8903
10214
|
}
|
|
8904
10215
|
function appendMissionJournal(entry) {
|
|
8905
|
-
|
|
10216
|
+
mkdirSync14(paths.journalsDir, { recursive: true });
|
|
8906
10217
|
appendFileSync(journalPath(entry.missionId), `${JSON.stringify(entry)}
|
|
8907
10218
|
`, "utf8");
|
|
8908
10219
|
}
|
|
8909
10220
|
function listMissionJournal(missionId) {
|
|
8910
10221
|
const path = journalPath(missionId);
|
|
8911
|
-
if (!
|
|
10222
|
+
if (!existsSync14(path))
|
|
8912
10223
|
return [];
|
|
8913
|
-
return
|
|
10224
|
+
return readFileSync10(path, "utf8").split(`
|
|
8914
10225
|
`).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line)).filter(isRecord2).map((entry) => ({
|
|
8915
10226
|
id: typeof entry.id === "string" ? entry.id : `journal:${randomUUID2()}`,
|
|
8916
10227
|
missionId,
|
|
@@ -8926,7 +10237,7 @@ function createInspectorMissionController(options) {
|
|
|
8926
10237
|
}
|
|
8927
10238
|
function readMissionOnly(missionId) {
|
|
8928
10239
|
const path = missionPath(missionId);
|
|
8929
|
-
if (!
|
|
10240
|
+
if (!existsSync14(path)) {
|
|
8930
10241
|
return { ok: false, error: `Mission ${missionId} was not found` };
|
|
8931
10242
|
}
|
|
8932
10243
|
const read = readJsonRecord(path);
|
|
@@ -8977,7 +10288,7 @@ function createInspectorMissionController(options) {
|
|
|
8977
10288
|
const source = cloneJsonRecord(input.sourceTask);
|
|
8978
10289
|
const missionId = nextId();
|
|
8979
10290
|
const path = missionPath(missionId);
|
|
8980
|
-
if (
|
|
10291
|
+
if (existsSync14(path)) {
|
|
8981
10292
|
const existing = readMissionOnly(missionId);
|
|
8982
10293
|
if (!existing.ok)
|
|
8983
10294
|
return existing;
|
|
@@ -10577,8 +11888,8 @@ function createCodexInspectorTransport(options) {
|
|
|
10577
11888
|
const sendRequest = async (method, params) => {
|
|
10578
11889
|
const id = nextRequestId;
|
|
10579
11890
|
nextRequestId += 1;
|
|
10580
|
-
const response = new Promise((
|
|
10581
|
-
pendingResponses.set(id, { resolve:
|
|
11891
|
+
const response = new Promise((resolve22, reject) => {
|
|
11892
|
+
pendingResponses.set(id, { resolve: resolve22, reject });
|
|
10582
11893
|
});
|
|
10583
11894
|
response.catch(() => {});
|
|
10584
11895
|
try {
|
|
@@ -10888,9 +12199,9 @@ function createCodexInspectorTransport(options) {
|
|
|
10888
12199
|
}
|
|
10889
12200
|
lastAssistantMessage = null;
|
|
10890
12201
|
lastError = null;
|
|
10891
|
-
const turnResult = new Promise((
|
|
12202
|
+
const turnResult = new Promise((resolve22, reject) => {
|
|
10892
12203
|
currentTurn = {
|
|
10893
|
-
resolve:
|
|
12204
|
+
resolve: resolve22,
|
|
10894
12205
|
reject,
|
|
10895
12206
|
events: []
|
|
10896
12207
|
};
|
|
@@ -10950,13 +12261,13 @@ function createCodexInspectorTransport(options) {
|
|
|
10950
12261
|
};
|
|
10951
12262
|
}
|
|
10952
12263
|
function writeChildLine(child, line) {
|
|
10953
|
-
return new Promise((
|
|
12264
|
+
return new Promise((resolve22, reject) => {
|
|
10954
12265
|
child.stdin.write(line, (error) => {
|
|
10955
12266
|
if (error) {
|
|
10956
12267
|
reject(error);
|
|
10957
12268
|
return;
|
|
10958
12269
|
}
|
|
10959
|
-
|
|
12270
|
+
resolve22();
|
|
10960
12271
|
});
|
|
10961
12272
|
});
|
|
10962
12273
|
}
|
|
@@ -10969,10 +12280,10 @@ function terminateChild(child) {
|
|
|
10969
12280
|
} catch {}
|
|
10970
12281
|
}
|
|
10971
12282
|
async function waitForChildSpawn(child) {
|
|
10972
|
-
await new Promise((
|
|
12283
|
+
await new Promise((resolve22, reject) => {
|
|
10973
12284
|
const onSpawn = () => {
|
|
10974
12285
|
cleanup();
|
|
10975
|
-
|
|
12286
|
+
resolve22();
|
|
10976
12287
|
};
|
|
10977
12288
|
const onError = (error) => {
|
|
10978
12289
|
cleanup();
|
|
@@ -11081,7 +12392,7 @@ import {
|
|
|
11081
12392
|
} from "@rig/runtime/control-plane/native/run-ops";
|
|
11082
12393
|
import {
|
|
11083
12394
|
listAuthorityRuns as listAuthorityRuns6,
|
|
11084
|
-
readAuthorityRun as
|
|
12395
|
+
readAuthorityRun as readAuthorityRun9
|
|
11085
12396
|
} from "@rig/runtime/control-plane/authority-files";
|
|
11086
12397
|
function providerFromRuntimeAdapter(runtimeAdapter) {
|
|
11087
12398
|
if (!runtimeAdapter) {
|
|
@@ -11161,7 +12472,7 @@ function discoverInspectorRuns(options) {
|
|
|
11161
12472
|
discovered.set(surface.runId, existing);
|
|
11162
12473
|
};
|
|
11163
12474
|
for (const authorityEntry of listAuthorityRuns6(options.projectRoot)) {
|
|
11164
|
-
const run =
|
|
12475
|
+
const run = readAuthorityRun9(options.projectRoot, authorityEntry.runId);
|
|
11165
12476
|
if (!run) {
|
|
11166
12477
|
continue;
|
|
11167
12478
|
}
|
|
@@ -11484,8 +12795,8 @@ function createGlobalInspectorService(options) {
|
|
|
11484
12795
|
|
|
11485
12796
|
// packages/server/src/inspector/upstream-sync.ts
|
|
11486
12797
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
11487
|
-
import { existsSync as
|
|
11488
|
-
import { dirname as
|
|
12798
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync14 } from "fs";
|
|
12799
|
+
import { dirname as dirname17, resolve as resolve22 } from "path";
|
|
11489
12800
|
import { resolveMonorepoRoot as resolveMonorepoRoot4 } from "@rig/runtime/control-plane/native/utils";
|
|
11490
12801
|
var UPSTREAM_VALIDATION_DESCRIPTIONS = {
|
|
11491
12802
|
"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.",
|
|
@@ -11623,34 +12934,34 @@ function defaultGitRunner(repoRoot, args) {
|
|
|
11623
12934
|
}
|
|
11624
12935
|
function upstreamStatePath(projectRoot, override) {
|
|
11625
12936
|
if (override) {
|
|
11626
|
-
return
|
|
12937
|
+
return resolve22(override);
|
|
11627
12938
|
}
|
|
11628
|
-
return
|
|
12939
|
+
return resolve22(resolveRigServerPaths(projectRoot).stateDir, "inspector", "upstream-sync.json");
|
|
11629
12940
|
}
|
|
11630
12941
|
function readUpstreamState(projectRoot, statePath) {
|
|
11631
12942
|
const path = upstreamStatePath(projectRoot, statePath);
|
|
11632
|
-
if (!
|
|
12943
|
+
if (!existsSync15(path)) {
|
|
11633
12944
|
return null;
|
|
11634
12945
|
}
|
|
11635
12946
|
try {
|
|
11636
|
-
return JSON.parse(
|
|
12947
|
+
return JSON.parse(readFileSync11(path, "utf-8"));
|
|
11637
12948
|
} catch {
|
|
11638
12949
|
return null;
|
|
11639
12950
|
}
|
|
11640
12951
|
}
|
|
11641
12952
|
function writeUpstreamState(projectRoot, state, statePath) {
|
|
11642
12953
|
const path = upstreamStatePath(projectRoot, statePath);
|
|
11643
|
-
|
|
11644
|
-
|
|
12954
|
+
mkdirSync15(dirname17(path), { recursive: true });
|
|
12955
|
+
writeFileSync14(path, `${JSON.stringify(state, null, 2)}
|
|
11645
12956
|
`, "utf8");
|
|
11646
12957
|
}
|
|
11647
12958
|
function readImportedRevision(projectRoot, upstreamsDocPath) {
|
|
11648
12959
|
const monorepoRoot = resolveMonorepoRoot4(projectRoot);
|
|
11649
|
-
const docPath = upstreamsDocPath ?
|
|
11650
|
-
if (!
|
|
12960
|
+
const docPath = upstreamsDocPath ? resolve22(upstreamsDocPath) : resolve22(monorepoRoot, "docs", "UPSTREAMS.md");
|
|
12961
|
+
if (!existsSync15(docPath)) {
|
|
11651
12962
|
throw new Error(`UPSTREAMS.md not found at ${docPath}`);
|
|
11652
12963
|
}
|
|
11653
|
-
const docContent =
|
|
12964
|
+
const docContent = readFileSync11(docPath, "utf-8");
|
|
11654
12965
|
const revision = parseImportedUpstreamRevision(docContent, "upstream") ?? parseImportedUpstreamRevision(docContent, "humoongate");
|
|
11655
12966
|
if (!revision) {
|
|
11656
12967
|
throw new Error(`Failed to parse upstream imported revision from ${docPath}`);
|
|
@@ -11672,7 +12983,7 @@ function resolveRemoteBranch(repoRoot, remote, gitRunner) {
|
|
|
11672
12983
|
return null;
|
|
11673
12984
|
}
|
|
11674
12985
|
function isGitCheckout(path, gitRunner) {
|
|
11675
|
-
if (!
|
|
12986
|
+
if (!existsSync15(resolve22(path, ".git"))) {
|
|
11676
12987
|
return false;
|
|
11677
12988
|
}
|
|
11678
12989
|
const result = gitRunner(path, ["rev-parse", "--is-inside-work-tree"]);
|
|
@@ -11681,12 +12992,12 @@ function isGitCheckout(path, gitRunner) {
|
|
|
11681
12992
|
function resolveUpstreamCheckout(projectRoot, explicitCheckout, gitRunner) {
|
|
11682
12993
|
const monorepoRoot = resolveMonorepoRoot4(projectRoot);
|
|
11683
12994
|
const candidates = [
|
|
11684
|
-
explicitCheckout ?
|
|
11685
|
-
process.env.UPSTREAM_CHECKOUT?.trim() ?
|
|
11686
|
-
process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ?
|
|
11687
|
-
|
|
11688
|
-
|
|
11689
|
-
|
|
12995
|
+
explicitCheckout ? resolve22(explicitCheckout) : "",
|
|
12996
|
+
process.env.UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.UPSTREAM_CHECKOUT.trim()) : "",
|
|
12997
|
+
process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ? resolve22(process.env.HUMOONGATE_UPSTREAM_CHECKOUT.trim()) : "",
|
|
12998
|
+
resolve22(projectRoot, "..", "humoongate"),
|
|
12999
|
+
resolve22(monorepoRoot, "..", "humoongate"),
|
|
13000
|
+
resolve22(monorepoRoot, "humoongate")
|
|
11690
13001
|
].filter(Boolean);
|
|
11691
13002
|
for (const candidate of candidates) {
|
|
11692
13003
|
if (isGitCheckout(candidate, gitRunner)) {
|
|
@@ -11922,10 +13233,10 @@ async function runUpstreamSyncScan(options) {
|
|
|
11922
13233
|
}
|
|
11923
13234
|
|
|
11924
13235
|
// packages/server/src/server-helpers/task-config.ts
|
|
11925
|
-
import { existsSync as
|
|
13236
|
+
import { existsSync as existsSync16 } from "fs";
|
|
11926
13237
|
async function readTaskConfig(projectRoot) {
|
|
11927
13238
|
const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
|
|
11928
|
-
if (!
|
|
13239
|
+
if (!existsSync16(taskConfigPath)) {
|
|
11929
13240
|
return {};
|
|
11930
13241
|
}
|
|
11931
13242
|
try {
|
|
@@ -11961,11 +13272,11 @@ function resolveFollowupSourceCommit(input) {
|
|
|
11961
13272
|
}
|
|
11962
13273
|
async function createInspectorFollowupTask(projectRoot, input) {
|
|
11963
13274
|
const monorepoRoot = resolveMonorepoRoot5(projectRoot);
|
|
11964
|
-
const issuesPath =
|
|
11965
|
-
const taskStatePath =
|
|
11966
|
-
const taskConfigPath =
|
|
11967
|
-
|
|
11968
|
-
|
|
13275
|
+
const issuesPath = resolve23(monorepoRoot, ".beads", "issues.jsonl");
|
|
13276
|
+
const taskStatePath = resolve23(monorepoRoot, ".beads", "task-state.json");
|
|
13277
|
+
const taskConfigPath = resolve23(monorepoRoot, ".rig", "task-config.json");
|
|
13278
|
+
mkdirSync16(dirname18(issuesPath), { recursive: true });
|
|
13279
|
+
mkdirSync16(dirname18(taskConfigPath), { recursive: true });
|
|
11969
13280
|
const summary = normalizeString(input.summary) ?? "Inspector follow-up";
|
|
11970
13281
|
const description = normalizeString(input.description) ?? normalizeString(input.details?.description) ?? `Created by the global inspector: ${summary}`;
|
|
11971
13282
|
const acceptanceCriteria = normalizeString(input.acceptanceCriteria) ?? "Investigate the detected drift and port the relevant changes into Rig.";
|
|
@@ -11984,7 +13295,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
|
|
|
11984
13295
|
const sourceKey = normalizeString(input.sourceKey) ?? normalizeString(input.details?.sourceKey);
|
|
11985
13296
|
const createdAt = normalizeString(input.createdAt) ?? new Date().toISOString();
|
|
11986
13297
|
const status = normalizeTaskLifecycleStatus2(normalizeString(input.status) ?? "open") ?? "open";
|
|
11987
|
-
const existingIssueLines =
|
|
13298
|
+
const existingIssueLines = existsSync17(issuesPath) ? readFileSync12(issuesPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
|
|
11988
13299
|
const existingIssues = existingIssueLines.map((line) => {
|
|
11989
13300
|
try {
|
|
11990
13301
|
return JSON.parse(line);
|
|
@@ -11993,7 +13304,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
|
|
|
11993
13304
|
}
|
|
11994
13305
|
}).filter((value) => value !== null);
|
|
11995
13306
|
const existingIds = new Set(existingIssues.map((issue) => typeof issue.id === "string" ? issue.id : null).filter((value) => value !== null));
|
|
11996
|
-
const rawTaskState =
|
|
13307
|
+
const rawTaskState = existsSync17(taskStatePath) ? readJsonFile3(taskStatePath, {}) : {};
|
|
11997
13308
|
const tasks = rawTaskState.tasks && typeof rawTaskState.tasks === "object" && !Array.isArray(rawTaskState.tasks) ? rawTaskState.tasks : {};
|
|
11998
13309
|
const existingTaskIdFromSourceKey = sourceKey == null ? null : Object.entries(tasks).find(([, metadata]) => {
|
|
11999
13310
|
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
@@ -12019,7 +13330,7 @@ async function createInspectorFollowupTask(projectRoot, input) {
|
|
|
12019
13330
|
updated_at: createdAt,
|
|
12020
13331
|
labels: mergedLabels
|
|
12021
13332
|
};
|
|
12022
|
-
|
|
13333
|
+
writeFileSync15(issuesPath, existingIssueLines.length > 0 ? `${existingIssueLines.join(`
|
|
12023
13334
|
`)}
|
|
12024
13335
|
${JSON.stringify(issueRecord)}
|
|
12025
13336
|
` : `${JSON.stringify(issueRecord)}
|
|
@@ -12037,7 +13348,7 @@ ${JSON.stringify(issueRecord)}
|
|
|
12037
13348
|
labels: mergedLabels
|
|
12038
13349
|
};
|
|
12039
13350
|
});
|
|
12040
|
-
|
|
13351
|
+
writeFileSync15(issuesPath, `${updatedIssues.map((issue) => JSON.stringify(issue)).join(`
|
|
12041
13352
|
`)}
|
|
12042
13353
|
`, "utf8");
|
|
12043
13354
|
}
|
|
@@ -12060,14 +13371,14 @@ ${JSON.stringify(issueRecord)}
|
|
|
12060
13371
|
}
|
|
12061
13372
|
};
|
|
12062
13373
|
}
|
|
12063
|
-
|
|
13374
|
+
writeFileSync15(taskConfigPath, `${JSON.stringify(taskConfig, null, 2)}
|
|
12064
13375
|
`, "utf8");
|
|
12065
13376
|
tasks[taskId] = {
|
|
12066
13377
|
status,
|
|
12067
13378
|
sourceCommit: resolveFollowupSourceCommit(input),
|
|
12068
13379
|
...sourceKey ? { sourceKey } : {}
|
|
12069
13380
|
};
|
|
12070
|
-
|
|
13381
|
+
writeFileSync15(taskStatePath, `${JSON.stringify({
|
|
12071
13382
|
schemaVersion: 1,
|
|
12072
13383
|
baseTrackerCommit: typeof rawTaskState.baseTrackerCommit === "string" ? rawTaskState.baseTrackerCommit : null,
|
|
12073
13384
|
tasks
|
|
@@ -12094,21 +13405,10 @@ async function runInspectorLocalReview(projectRoot, input) {
|
|
|
12094
13405
|
details: null
|
|
12095
13406
|
};
|
|
12096
13407
|
}
|
|
12097
|
-
const
|
|
12098
|
-
import("@rig/runtime/control-plane/runtime/events"),
|
|
12099
|
-
import("@rig/runtime/control-plane/runtime/plugins"),
|
|
12100
|
-
import("@rig/runtime/control-plane/native/verifier")
|
|
12101
|
-
]);
|
|
12102
|
-
const eventBus = new RuntimeEventBus({ projectRoot, runId: `inspector-review:${taskId}` });
|
|
12103
|
-
const plugins = await PluginManager.load({
|
|
12104
|
-
projectRoot,
|
|
12105
|
-
runId: `inspector-review:${taskId}`,
|
|
12106
|
-
eventBus
|
|
12107
|
-
});
|
|
13408
|
+
const { verifyTask } = await import("@rig/runtime/control-plane/native/verifier");
|
|
12108
13409
|
const outcome = await verifyTask({
|
|
12109
13410
|
projectRoot,
|
|
12110
13411
|
taskId,
|
|
12111
|
-
plugins,
|
|
12112
13412
|
skipAiReview: true
|
|
12113
13413
|
});
|
|
12114
13414
|
return {
|
|
@@ -12375,12 +13675,12 @@ function isAuthorizedLinearWebhookRequest(req) {
|
|
|
12375
13675
|
}
|
|
12376
13676
|
|
|
12377
13677
|
// packages/server/src/server-helpers/notifications.ts
|
|
12378
|
-
import { existsSync as
|
|
12379
|
-
import { dirname as
|
|
13678
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync17, readFileSync as readFileSync13 } from "fs";
|
|
13679
|
+
import { dirname as dirname19 } from "path";
|
|
12380
13680
|
async function loadNotificationConfig(path) {
|
|
12381
|
-
if (!
|
|
13681
|
+
if (!existsSync18(path)) {
|
|
12382
13682
|
const defaultConfig = { targets: [] };
|
|
12383
|
-
|
|
13683
|
+
mkdirSync17(dirname19(path), { recursive: true });
|
|
12384
13684
|
await Bun.write(path, `${JSON.stringify(defaultConfig, null, 2)}
|
|
12385
13685
|
`);
|
|
12386
13686
|
return defaultConfig;
|
|
@@ -12395,10 +13695,10 @@ async function loadNotificationConfig(path) {
|
|
|
12395
13695
|
}
|
|
12396
13696
|
}
|
|
12397
13697
|
function readRecentEvents(file, limit) {
|
|
12398
|
-
if (!
|
|
13698
|
+
if (!existsSync18(file)) {
|
|
12399
13699
|
return [];
|
|
12400
13700
|
}
|
|
12401
|
-
const lines =
|
|
13701
|
+
const lines = readFileSync13(file, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-limit);
|
|
12402
13702
|
const events = [];
|
|
12403
13703
|
for (const line of lines) {
|
|
12404
13704
|
try {
|
|
@@ -12493,11 +13793,11 @@ function extractObjectLiteralBlock(source, property) {
|
|
|
12493
13793
|
}
|
|
12494
13794
|
function readFallbackIssueAnalysisConfig(projectRoot) {
|
|
12495
13795
|
for (const fileName of ["rig.config.ts", "rig.config.json"]) {
|
|
12496
|
-
const path =
|
|
12497
|
-
if (!
|
|
13796
|
+
const path = resolve24(projectRoot, fileName);
|
|
13797
|
+
if (!existsSync19(path))
|
|
12498
13798
|
continue;
|
|
12499
13799
|
try {
|
|
12500
|
-
const source =
|
|
13800
|
+
const source = readFileSync14(path, "utf8");
|
|
12501
13801
|
if (fileName.endsWith(".json"))
|
|
12502
13802
|
return JSON.parse(source);
|
|
12503
13803
|
const issueBlock = extractObjectLiteralBlock(source, "issueAnalysis");
|
|
@@ -12630,8 +13930,8 @@ async function createIssueAnalysisRunnerForServerState(state, input) {
|
|
|
12630
13930
|
async function withServerPathEnv(projectRoot, fn) {
|
|
12631
13931
|
const waitForTurn = serverPathEnvQueue;
|
|
12632
13932
|
let releaseTurn;
|
|
12633
|
-
serverPathEnvQueue = new Promise((
|
|
12634
|
-
releaseTurn =
|
|
13933
|
+
serverPathEnvQueue = new Promise((resolve25) => {
|
|
13934
|
+
releaseTurn = resolve25;
|
|
12635
13935
|
});
|
|
12636
13936
|
await waitForTurn;
|
|
12637
13937
|
const paths = resolveServerAuthorityPaths(projectRoot);
|
|
@@ -12667,9 +13967,9 @@ async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
|
|
|
12667
13967
|
return withServerPathEnv(projectRoot, fn);
|
|
12668
13968
|
}
|
|
12669
13969
|
async function readWorkspaceTasks(projectRoot) {
|
|
12670
|
-
const issuesPath =
|
|
13970
|
+
const issuesPath = resolve24(resolveMonorepoRoot6(projectRoot), ".beads", "issues.jsonl");
|
|
12671
13971
|
const taskConfig = await readTaskConfig(projectRoot);
|
|
12672
|
-
if (!
|
|
13972
|
+
if (!existsSync19(issuesPath)) {
|
|
12673
13973
|
return [];
|
|
12674
13974
|
}
|
|
12675
13975
|
const latestById = new Map;
|
|
@@ -12743,11 +14043,11 @@ function resolveTaskArtifactDirsFromRuns(projectRoot, taskId, knownRuns) {
|
|
|
12743
14043
|
continue;
|
|
12744
14044
|
add(run.artifactRoot);
|
|
12745
14045
|
if (run.worktreePath) {
|
|
12746
|
-
add(
|
|
14046
|
+
add(resolve24(run.worktreePath, "artifacts", taskId));
|
|
12747
14047
|
}
|
|
12748
14048
|
}
|
|
12749
14049
|
for (const artifactsRoot of listAuthorityArtifactRoots(projectRoot)) {
|
|
12750
|
-
add(
|
|
14050
|
+
add(resolve24(artifactsRoot, taskId));
|
|
12751
14051
|
}
|
|
12752
14052
|
return candidates;
|
|
12753
14053
|
}
|
|
@@ -12761,7 +14061,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
|
|
|
12761
14061
|
}
|
|
12762
14062
|
}
|
|
12763
14063
|
return taskIds.flatMap((currentTaskId) => {
|
|
12764
|
-
const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) =>
|
|
14064
|
+
const currentRoot = resolveTaskArtifactDirsFromRuns(projectRoot, currentTaskId, runs).find((path) => existsSync19(path));
|
|
12765
14065
|
if (!currentRoot) {
|
|
12766
14066
|
return [];
|
|
12767
14067
|
}
|
|
@@ -12773,7 +14073,7 @@ async function listArtifactSummaries(projectRoot, taskId, knownTaskIds, knownRun
|
|
|
12773
14073
|
taskId: currentTaskId,
|
|
12774
14074
|
kind: "file",
|
|
12775
14075
|
label: fileName,
|
|
12776
|
-
path:
|
|
14076
|
+
path: resolve24(currentRoot, fileName),
|
|
12777
14077
|
url: null,
|
|
12778
14078
|
metadata: {
|
|
12779
14079
|
fileName
|
|
@@ -12816,11 +14116,11 @@ function buildInspectorStreamPayload(state, sequence) {
|
|
|
12816
14116
|
}
|
|
12817
14117
|
function listRemoteRunArtifacts(projectRoot, runId) {
|
|
12818
14118
|
const root = remoteArtifactsRoot(projectRoot, runId);
|
|
12819
|
-
if (!
|
|
14119
|
+
if (!existsSync19(root)) {
|
|
12820
14120
|
return [];
|
|
12821
14121
|
}
|
|
12822
14122
|
return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isFile()).filter((entry) => !entry.name.endsWith(".json")).map((entry) => {
|
|
12823
|
-
const artifactPath =
|
|
14123
|
+
const artifactPath = resolve24(root, entry.name);
|
|
12824
14124
|
const stat = statSync6(artifactPath);
|
|
12825
14125
|
const meta = readJsonFile4(`${artifactPath}.json`, null);
|
|
12826
14126
|
return {
|
|
@@ -12981,6 +14281,7 @@ function buildHttpRouterDeps(state) {
|
|
|
12981
14281
|
startLocalRun,
|
|
12982
14282
|
stopRunRecord,
|
|
12983
14283
|
resumeRunRecord,
|
|
14284
|
+
runServerOwnedPrCloseout,
|
|
12984
14285
|
claimRemoteRun,
|
|
12985
14286
|
listRemoteRunArtifacts,
|
|
12986
14287
|
broadcastSnapshotInvalidation,
|
|
@@ -13062,8 +14363,8 @@ function fileStats(path) {
|
|
|
13062
14363
|
}
|
|
13063
14364
|
}
|
|
13064
14365
|
function runFileCursor(projectRoot, run) {
|
|
13065
|
-
const runDir =
|
|
13066
|
-
const runJson = fileStats(
|
|
14366
|
+
const runDir = dirname20(runLogsPath(projectRoot, run.runId));
|
|
14367
|
+
const runJson = fileStats(resolve24(runDir, "run.json"));
|
|
13067
14368
|
const timeline = fileStats(runTimelinePath(projectRoot, run.runId));
|
|
13068
14369
|
const logs = fileStats(runLogsPath(projectRoot, run.runId));
|
|
13069
14370
|
return {
|
|
@@ -13113,10 +14414,10 @@ function startRunFileWatcher(state, pollMs) {
|
|
|
13113
14414
|
}, Math.max(250, Math.min(pollMs, 1000)));
|
|
13114
14415
|
}
|
|
13115
14416
|
function startPoller(state, pollMs) {
|
|
13116
|
-
let offset =
|
|
14417
|
+
let offset = existsSync19(state.eventsFile) ? statSync6(state.eventsFile).size : 0;
|
|
13117
14418
|
return setInterval(async () => {
|
|
13118
14419
|
try {
|
|
13119
|
-
if (!
|
|
14420
|
+
if (!existsSync19(state.eventsFile)) {
|
|
13120
14421
|
return;
|
|
13121
14422
|
}
|
|
13122
14423
|
const file = await open(state.eventsFile, "r");
|
|
@@ -13160,6 +14461,26 @@ function startPoller(state, pollMs) {
|
|
|
13160
14461
|
}
|
|
13161
14462
|
}, pollMs);
|
|
13162
14463
|
}
|
|
14464
|
+
function attachPiSessionProxySocket(ws) {
|
|
14465
|
+
if (ws.data.kind !== "pi-session-proxy")
|
|
14466
|
+
return;
|
|
14467
|
+
const upstream = new WebSocket(ws.data.upstreamUrl);
|
|
14468
|
+
ws.__rigPiUpstream = upstream;
|
|
14469
|
+
upstream.addEventListener("message", (event) => {
|
|
14470
|
+
if (ws.readyState === 1)
|
|
14471
|
+
ws.send(typeof event.data === "string" ? event.data : event.data);
|
|
14472
|
+
});
|
|
14473
|
+
upstream.addEventListener("close", () => {
|
|
14474
|
+
try {
|
|
14475
|
+
ws.close();
|
|
14476
|
+
} catch {}
|
|
14477
|
+
});
|
|
14478
|
+
upstream.addEventListener("error", () => {
|
|
14479
|
+
try {
|
|
14480
|
+
ws.close(1011, "Upstream Pi session stream failed");
|
|
14481
|
+
} catch {}
|
|
14482
|
+
});
|
|
14483
|
+
}
|
|
13163
14484
|
async function createRigServer(options, projectRoot = resolveProjectRoot()) {
|
|
13164
14485
|
const state = await createRigServerState(projectRoot, options.eventType, options.authToken, {
|
|
13165
14486
|
upstreamSyncMs: options.upstreamSyncMs,
|
|
@@ -13172,10 +14493,20 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
|
|
|
13172
14493
|
fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
|
|
13173
14494
|
websocket: {
|
|
13174
14495
|
open(ws) {
|
|
14496
|
+
if (ws.data.kind === "pi-session-proxy") {
|
|
14497
|
+
attachPiSessionProxySocket(ws);
|
|
14498
|
+
return;
|
|
14499
|
+
}
|
|
13175
14500
|
state.sockets.add(ws);
|
|
13176
14501
|
sendWebSocketResponse(ws, buildServerWelcomePush(state.projectRoot));
|
|
13177
14502
|
},
|
|
13178
14503
|
async message(ws, raw) {
|
|
14504
|
+
if (ws.data.kind === "pi-session-proxy") {
|
|
14505
|
+
const upstream = ws.__rigPiUpstream;
|
|
14506
|
+
if (upstream?.readyState === WebSocket.OPEN)
|
|
14507
|
+
upstream.send(raw);
|
|
14508
|
+
return;
|
|
14509
|
+
}
|
|
13179
14510
|
const request = parseWebSocketRequestEnvelope(typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8"));
|
|
13180
14511
|
if (!request) {
|
|
13181
14512
|
sendWebSocketResponse(ws, {
|
|
@@ -13198,6 +14529,10 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
|
|
|
13198
14529
|
}
|
|
13199
14530
|
},
|
|
13200
14531
|
close(ws) {
|
|
14532
|
+
if (ws.data.kind === "pi-session-proxy") {
|
|
14533
|
+
ws.__rigPiUpstream?.close();
|
|
14534
|
+
return;
|
|
14535
|
+
}
|
|
13201
14536
|
state.sockets.delete(ws);
|
|
13202
14537
|
}
|
|
13203
14538
|
}
|
|
@@ -13244,7 +14579,7 @@ function resolveProjectRoot() {
|
|
|
13244
14579
|
return resolveRigProjectRoot({
|
|
13245
14580
|
envProjectRoot: process.env.PROJECT_RIG_ROOT ?? null,
|
|
13246
14581
|
cwd: process.cwd(),
|
|
13247
|
-
fallbackRoot:
|
|
14582
|
+
fallbackRoot: resolve24(import.meta.dir, "../..")
|
|
13248
14583
|
});
|
|
13249
14584
|
}
|
|
13250
14585
|
var __testOnly = {
|
|
@@ -13305,6 +14640,7 @@ export {
|
|
|
13305
14640
|
resolveRigServerPaths,
|
|
13306
14641
|
resolveRigProjectRoot,
|
|
13307
14642
|
resolvePublishedRigServerStatePath,
|
|
14643
|
+
resolveProjectStatusField,
|
|
13308
14644
|
resolveProjectRoot,
|
|
13309
14645
|
registerRemoteHost,
|
|
13310
14646
|
readWorkspaceTasks,
|
|
@@ -13313,6 +14649,7 @@ export {
|
|
|
13313
14649
|
parseRigServerArgs,
|
|
13314
14650
|
parseArgs,
|
|
13315
14651
|
main,
|
|
14652
|
+
listGitHubProjects,
|
|
13316
14653
|
heartbeatRemoteHost,
|
|
13317
14654
|
handleWebSocketUpgrade,
|
|
13318
14655
|
encodeWebSocketPayload,
|