@h-rig/server 0.0.6-alpha.13 → 0.0.6-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.js +699 -56
- package/dist/src/server-helpers/github-project-status-sync.js +3 -0
- package/dist/src/server-helpers/http-router.js +273 -25
- package/dist/src/server-helpers/run-io.js +13 -0
- package/dist/src/server-helpers/run-mutations.js +445 -32
- package/dist/src/server-helpers/run-writers.js +1 -2
- package/dist/src/server-helpers/ws-router.js +7 -1
- package/dist/src/server.js +697 -56
- package/package.json +4 -4
package/dist/src/server.js
CHANGED
|
@@ -1074,6 +1074,18 @@ function readJsonlFileTail(path, options) {
|
|
|
1074
1074
|
const completeLines = start > 0 ? lines.slice(1) : lines;
|
|
1075
1075
|
return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
|
|
1076
1076
|
}
|
|
1077
|
+
async function readRunTimelinePage(projectRoot, runId, options = {}) {
|
|
1078
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
1079
|
+
const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
|
|
1080
|
+
const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
1081
|
+
const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
|
|
1082
|
+
const endExclusive = Math.min(entries.length, startInclusive + limit);
|
|
1083
|
+
return {
|
|
1084
|
+
entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
|
|
1085
|
+
nextCursor: String(endExclusive),
|
|
1086
|
+
hasMore: endExclusive < entries.length
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1077
1089
|
var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
1078
1090
|
async function readRunLogsPage(projectRoot, runId, options = {}) {
|
|
1079
1091
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
@@ -2731,8 +2743,7 @@ function buildRunStartPatch(startedAt) {
|
|
|
2731
2743
|
status: "preparing",
|
|
2732
2744
|
startedAt,
|
|
2733
2745
|
completedAt: null,
|
|
2734
|
-
errorText: null
|
|
2735
|
-
serverPid: process.pid
|
|
2746
|
+
errorText: null
|
|
2736
2747
|
};
|
|
2737
2748
|
}
|
|
2738
2749
|
|
|
@@ -3214,6 +3225,11 @@ import {
|
|
|
3214
3225
|
buildTaskRunLifecycleComment,
|
|
3215
3226
|
updateConfiguredTaskSourceTask
|
|
3216
3227
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
3228
|
+
import {
|
|
3229
|
+
closeIssueAfterMergedPr,
|
|
3230
|
+
commitRunChanges,
|
|
3231
|
+
runPrAutomation
|
|
3232
|
+
} from "@rig/runtime/control-plane/native/pr-automation";
|
|
3217
3233
|
|
|
3218
3234
|
// packages/server/src/scheduler.ts
|
|
3219
3235
|
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -3549,6 +3565,9 @@ function asRecord(value) {
|
|
|
3549
3565
|
function asString(value) {
|
|
3550
3566
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
3551
3567
|
}
|
|
3568
|
+
function asNumber(value) {
|
|
3569
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
3570
|
+
}
|
|
3552
3571
|
async function defaultGraphQLFetch(query, variables, token) {
|
|
3553
3572
|
const response = await fetch("https://api.github.com/graphql", {
|
|
3554
3573
|
method: "POST",
|
|
@@ -3565,6 +3584,32 @@ async function defaultGraphQLFetch(query, variables, token) {
|
|
|
3565
3584
|
}
|
|
3566
3585
|
return json.data;
|
|
3567
3586
|
}
|
|
3587
|
+
function projectNodesFrom(data) {
|
|
3588
|
+
const root = asRecord(data);
|
|
3589
|
+
const owner = asRecord(root?.organization) ?? asRecord(root?.user);
|
|
3590
|
+
const projects = asRecord(owner?.projectsV2);
|
|
3591
|
+
const nodes = projects?.nodes;
|
|
3592
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
3593
|
+
}
|
|
3594
|
+
async function listGitHubProjects(input) {
|
|
3595
|
+
const query = `
|
|
3596
|
+
query RigListProjects($owner: String!, $first: Int!) {
|
|
3597
|
+
organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
3598
|
+
user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
3599
|
+
}
|
|
3600
|
+
`;
|
|
3601
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
3602
|
+
const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
|
|
3603
|
+
return projectNodesFrom(data).flatMap((node) => {
|
|
3604
|
+
const record = asRecord(node);
|
|
3605
|
+
const id = asString(record?.id);
|
|
3606
|
+
const number = asNumber(record?.number);
|
|
3607
|
+
const title = asString(record?.title);
|
|
3608
|
+
if (!id || number === undefined || !title)
|
|
3609
|
+
return [];
|
|
3610
|
+
return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3568
3613
|
async function resolveProjectStatusField(input) {
|
|
3569
3614
|
const query = `
|
|
3570
3615
|
query RigProjectStatusField($projectId: ID!) {
|
|
@@ -3659,6 +3704,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
3659
3704
|
running: "In Progress",
|
|
3660
3705
|
prOpen: "In Review",
|
|
3661
3706
|
ciFixing: "In Review",
|
|
3707
|
+
merging: "Merging",
|
|
3662
3708
|
done: "Done",
|
|
3663
3709
|
needsAttention: "Needs Attention"
|
|
3664
3710
|
};
|
|
@@ -3672,6 +3718,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
3672
3718
|
return "prOpen";
|
|
3673
3719
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
3674
3720
|
return "ciFixing";
|
|
3721
|
+
if (normalized === "merging" || normalized === "merge")
|
|
3722
|
+
return "merging";
|
|
3675
3723
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
3676
3724
|
return "needsAttention";
|
|
3677
3725
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -3800,9 +3848,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
|
|
|
3800
3848
|
return null;
|
|
3801
3849
|
return null;
|
|
3802
3850
|
}
|
|
3851
|
+
function githubProjectsEnabled(config) {
|
|
3852
|
+
const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
|
|
3853
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
3854
|
+
return projects?.enabled === true;
|
|
3855
|
+
}
|
|
3803
3856
|
async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
|
|
3804
3857
|
if (!run.taskId)
|
|
3805
|
-
return;
|
|
3858
|
+
return false;
|
|
3806
3859
|
const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
|
|
3807
3860
|
try {
|
|
3808
3861
|
const result = await syncGitHubProjectStatusForTaskUpdate({
|
|
@@ -3813,28 +3866,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
|
|
|
3813
3866
|
config
|
|
3814
3867
|
});
|
|
3815
3868
|
if (!result.synced && result.reason !== "project-sync-disabled") {
|
|
3869
|
+
const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
|
|
3816
3870
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
3817
3871
|
id: `log:${run.runId}:github-project-sync:${status}`,
|
|
3818
3872
|
title: "GitHub Project sync skipped",
|
|
3819
|
-
detail
|
|
3873
|
+
detail,
|
|
3820
3874
|
tone: "warn",
|
|
3821
3875
|
status: "running",
|
|
3822
3876
|
createdAt: new Date().toISOString(),
|
|
3823
3877
|
payload: { reason: result.reason, issueNodeId }
|
|
3824
3878
|
});
|
|
3879
|
+
if (githubProjectsEnabled(config)) {
|
|
3880
|
+
throw new Error(detail);
|
|
3881
|
+
}
|
|
3882
|
+
return false;
|
|
3825
3883
|
}
|
|
3884
|
+
return result.synced === true;
|
|
3826
3885
|
} catch (error) {
|
|
3886
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3827
3887
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
3828
3888
|
id: `log:${run.runId}:github-project-sync-error:${status}`,
|
|
3829
3889
|
title: "GitHub Project sync failed",
|
|
3830
|
-
detail
|
|
3890
|
+
detail,
|
|
3831
3891
|
tone: "error",
|
|
3832
3892
|
status: "running",
|
|
3833
3893
|
createdAt: new Date().toISOString(),
|
|
3834
3894
|
payload: { issueNodeId }
|
|
3835
3895
|
});
|
|
3896
|
+
if (githubProjectsEnabled(config)) {
|
|
3897
|
+
throw new Error(detail);
|
|
3898
|
+
}
|
|
3899
|
+
return false;
|
|
3836
3900
|
}
|
|
3837
3901
|
}
|
|
3902
|
+
function createCommandRunner(binary, extraEnv = {}) {
|
|
3903
|
+
return async (args, options) => {
|
|
3904
|
+
const child = spawn3(binary, [...args], {
|
|
3905
|
+
cwd: options?.cwd,
|
|
3906
|
+
env: { ...process.env, ...extraEnv },
|
|
3907
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3908
|
+
});
|
|
3909
|
+
const stdoutChunks = [];
|
|
3910
|
+
const stderrChunks = [];
|
|
3911
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
3912
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
3913
|
+
const exitCode = await new Promise((resolve15) => {
|
|
3914
|
+
child.once("error", () => resolve15(1));
|
|
3915
|
+
child.once("close", (code) => resolve15(code ?? 1));
|
|
3916
|
+
});
|
|
3917
|
+
return {
|
|
3918
|
+
exitCode,
|
|
3919
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
3920
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8")
|
|
3921
|
+
};
|
|
3922
|
+
};
|
|
3923
|
+
}
|
|
3924
|
+
function closeoutRecord(run) {
|
|
3925
|
+
const value = run.serverCloseout;
|
|
3926
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3927
|
+
}
|
|
3928
|
+
function closeoutPhasePatch(phase, status, extra = {}) {
|
|
3929
|
+
const updatedAt = new Date().toISOString();
|
|
3930
|
+
return {
|
|
3931
|
+
serverCloseout: {
|
|
3932
|
+
phase,
|
|
3933
|
+
status,
|
|
3934
|
+
updatedAt,
|
|
3935
|
+
...extra
|
|
3936
|
+
}
|
|
3937
|
+
};
|
|
3938
|
+
}
|
|
3939
|
+
function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
|
|
3940
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
3941
|
+
id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
|
|
3942
|
+
title: `Server closeout: ${phase}`,
|
|
3943
|
+
detail,
|
|
3944
|
+
tone,
|
|
3945
|
+
status,
|
|
3946
|
+
createdAt: new Date().toISOString()
|
|
3947
|
+
}, `server-closeout-${phase}`);
|
|
3948
|
+
}
|
|
3838
3949
|
async function autoAssignRunIssue(projectRoot, run) {
|
|
3839
3950
|
if (!run.taskId)
|
|
3840
3951
|
return;
|
|
@@ -3864,7 +3975,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
3864
3975
|
return;
|
|
3865
3976
|
}
|
|
3866
3977
|
const config = await loadRigLifecycleConfig(projectRoot);
|
|
3867
|
-
await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
3978
|
+
const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
3868
3979
|
if (status === "in_progress") {
|
|
3869
3980
|
await autoAssignRunIssue(projectRoot, run);
|
|
3870
3981
|
}
|
|
@@ -3880,24 +3991,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
3880
3991
|
});
|
|
3881
3992
|
return;
|
|
3882
3993
|
}
|
|
3883
|
-
const
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3994
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
3995
|
+
const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
|
|
3996
|
+
const rollbackProjectSync = async () => {
|
|
3997
|
+
if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
|
|
3998
|
+
return;
|
|
3999
|
+
await syncGitHubProjectStatusForTaskUpdate({
|
|
4000
|
+
taskId: run.taskId,
|
|
4001
|
+
status: previousStatus,
|
|
4002
|
+
issueNodeId: extractGitHubIssueNodeId(sourceTask),
|
|
4003
|
+
token: createGitHubAuthStore(projectRoot).readToken(),
|
|
4004
|
+
config
|
|
4005
|
+
}).catch((rollbackError) => {
|
|
4006
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
4007
|
+
id: `log:${run.runId}:github-project-sync-rollback:${status}`,
|
|
4008
|
+
title: "GitHub Project sync rollback failed",
|
|
4009
|
+
detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
|
|
4010
|
+
tone: "error",
|
|
4011
|
+
status: "running",
|
|
4012
|
+
createdAt: new Date().toISOString()
|
|
4013
|
+
});
|
|
4014
|
+
});
|
|
4015
|
+
};
|
|
4016
|
+
let result;
|
|
4017
|
+
try {
|
|
4018
|
+
result = await updateConfiguredTaskSourceTask(projectRoot, {
|
|
4019
|
+
taskId: run.taskId,
|
|
4020
|
+
sourceTask,
|
|
4021
|
+
update: {
|
|
3890
4022
|
status,
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
4023
|
+
comment: buildTaskRunLifecycleComment({
|
|
4024
|
+
runId: run.runId,
|
|
4025
|
+
status,
|
|
4026
|
+
summary,
|
|
4027
|
+
runtimeWorkspace: normalizeString(run.worktreePath),
|
|
4028
|
+
logsDir: normalizeString(run.logRoot),
|
|
4029
|
+
sessionDir: normalizeString(run.sessionPath),
|
|
4030
|
+
errorText: options.errorText ?? normalizeString(run.errorText)
|
|
4031
|
+
})
|
|
4032
|
+
}
|
|
4033
|
+
});
|
|
4034
|
+
} catch (error) {
|
|
4035
|
+
await rollbackProjectSync();
|
|
4036
|
+
throw error;
|
|
4037
|
+
}
|
|
3899
4038
|
if (!result.updated) {
|
|
3900
4039
|
if (result.source === "plugin" || result.sourceKind) {
|
|
4040
|
+
await rollbackProjectSync();
|
|
3901
4041
|
throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
|
|
3902
4042
|
}
|
|
3903
4043
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
@@ -3910,6 +4050,219 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
3910
4050
|
});
|
|
3911
4051
|
}
|
|
3912
4052
|
}
|
|
4053
|
+
async function runServerOwnedPrCloseout(state, runId) {
|
|
4054
|
+
const run = readAuthorityRun4(state.projectRoot, runId);
|
|
4055
|
+
if (!run)
|
|
4056
|
+
throw new Error(`Run not found: ${runId}`);
|
|
4057
|
+
const closeout = closeoutRecord(run);
|
|
4058
|
+
if (!closeout)
|
|
4059
|
+
return;
|
|
4060
|
+
const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
|
|
4061
|
+
if (!taskId)
|
|
4062
|
+
throw new Error("Server-owned closeout requires a task id.");
|
|
4063
|
+
const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
|
|
4064
|
+
const branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
|
|
4065
|
+
const config = await loadRigLifecycleConfig(state.projectRoot);
|
|
4066
|
+
const runPrMode = normalizeString(run.prMode);
|
|
4067
|
+
const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
|
|
4068
|
+
const effectiveConfig = {
|
|
4069
|
+
...config ?? {},
|
|
4070
|
+
pr: {
|
|
4071
|
+
...config?.pr ?? {},
|
|
4072
|
+
mode: prMode,
|
|
4073
|
+
autoFixChecks: false,
|
|
4074
|
+
autoFixReview: false
|
|
4075
|
+
}
|
|
4076
|
+
};
|
|
4077
|
+
const readCurrentRun = () => readAuthorityRun4(state.projectRoot, runId) ?? run;
|
|
4078
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
4079
|
+
const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
|
|
4080
|
+
const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
|
|
4081
|
+
const closeoutPrUrl = normalizeString(closeout.prUrl);
|
|
4082
|
+
if (closeoutPhase === "completed" || closeoutStatus === "completed") {
|
|
4083
|
+
return;
|
|
4084
|
+
}
|
|
4085
|
+
if (closeoutPhase === "close-source" && closeoutPrUrl) {
|
|
4086
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4087
|
+
status: "reviewing",
|
|
4088
|
+
...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
|
|
4089
|
+
});
|
|
4090
|
+
await closeIssueAfterMergedPr({
|
|
4091
|
+
projectRoot: state.projectRoot,
|
|
4092
|
+
taskId,
|
|
4093
|
+
runId,
|
|
4094
|
+
prUrl: closeoutPrUrl,
|
|
4095
|
+
sourceTask,
|
|
4096
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
4097
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
4098
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
4099
|
+
}
|
|
4100
|
+
});
|
|
4101
|
+
const completedAt = new Date().toISOString();
|
|
4102
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4103
|
+
status: "completed",
|
|
4104
|
+
completedAt,
|
|
4105
|
+
errorText: null,
|
|
4106
|
+
...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
|
|
4107
|
+
});
|
|
4108
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
|
|
4109
|
+
emitRigEvent(state, {
|
|
4110
|
+
type: "rig.run.completed",
|
|
4111
|
+
aggregateId: runId,
|
|
4112
|
+
payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
|
|
4113
|
+
createdAt: completedAt
|
|
4114
|
+
});
|
|
4115
|
+
return;
|
|
4116
|
+
}
|
|
4117
|
+
if (prMode === "off" || prMode === "ask") {
|
|
4118
|
+
const completedAt = new Date().toISOString();
|
|
4119
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4120
|
+
status: "completed",
|
|
4121
|
+
completedAt,
|
|
4122
|
+
errorText: null,
|
|
4123
|
+
...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
|
|
4124
|
+
});
|
|
4125
|
+
appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
|
|
4126
|
+
emitRigEvent(state, {
|
|
4127
|
+
type: "rig.run.completed",
|
|
4128
|
+
aggregateId: runId,
|
|
4129
|
+
payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
|
|
4130
|
+
createdAt: completedAt
|
|
4131
|
+
});
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
|
|
4135
|
+
const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
|
|
4136
|
+
const gitCommand = createCommandRunner("git", githubEnv);
|
|
4137
|
+
const ghCommand = createCommandRunner("gh", githubEnv);
|
|
4138
|
+
const setCloseout = (phase, status, extra = {}) => {
|
|
4139
|
+
const previous = closeoutRecord(readCurrentRun()) ?? closeout;
|
|
4140
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4141
|
+
status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
|
|
4142
|
+
...closeoutPhasePatch(phase, status, { ...previous, ...extra })
|
|
4143
|
+
});
|
|
4144
|
+
};
|
|
4145
|
+
setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
4146
|
+
appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
|
|
4147
|
+
const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
|
|
4148
|
+
appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
|
|
4149
|
+
setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
4150
|
+
const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
|
|
4151
|
+
if (push.exitCode !== 0) {
|
|
4152
|
+
throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
|
|
4153
|
+
}
|
|
4154
|
+
const sourceTaskForPr = {
|
|
4155
|
+
title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
|
|
4156
|
+
};
|
|
4157
|
+
const artifactRoot = resolve14(state.projectRoot, "artifacts", taskId);
|
|
4158
|
+
setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4159
|
+
const pr = await runPrAutomation({
|
|
4160
|
+
projectRoot: workspace,
|
|
4161
|
+
taskId,
|
|
4162
|
+
runId,
|
|
4163
|
+
branch,
|
|
4164
|
+
config: effectiveConfig,
|
|
4165
|
+
sourceTask: sourceTaskForPr,
|
|
4166
|
+
artifactRoot,
|
|
4167
|
+
command: ghCommand,
|
|
4168
|
+
gitCommand,
|
|
4169
|
+
steerPi: async (message) => {
|
|
4170
|
+
appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
|
|
4171
|
+
appendRunTimelineEntry(state.projectRoot, runId, {
|
|
4172
|
+
id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
|
|
4173
|
+
type: "user_message",
|
|
4174
|
+
text: message,
|
|
4175
|
+
createdAt: new Date().toISOString(),
|
|
4176
|
+
state: "completed"
|
|
4177
|
+
});
|
|
4178
|
+
},
|
|
4179
|
+
lifecycle: {
|
|
4180
|
+
onPrOpened: async ({ prUrl }) => {
|
|
4181
|
+
setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4182
|
+
appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
|
|
4183
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
|
|
4184
|
+
},
|
|
4185
|
+
onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
|
|
4186
|
+
onFeedback: async ({ feedback }) => {
|
|
4187
|
+
appendCloseoutStage(state, runId, "feedback", feedback.join(`
|
|
4188
|
+
`), "reviewing", "error");
|
|
4189
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
|
|
4190
|
+
},
|
|
4191
|
+
onMergeStarted: async ({ prUrl }) => {
|
|
4192
|
+
setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4193
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
4194
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
|
|
4195
|
+
},
|
|
4196
|
+
onMerged: ({ prUrl }) => {
|
|
4197
|
+
setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
|
|
4198
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
});
|
|
4202
|
+
if (pr.status === "merged" && pr.prUrl) {
|
|
4203
|
+
setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
|
|
4204
|
+
await closeIssueAfterMergedPr({
|
|
4205
|
+
projectRoot: state.projectRoot,
|
|
4206
|
+
taskId,
|
|
4207
|
+
runId,
|
|
4208
|
+
prUrl: pr.prUrl,
|
|
4209
|
+
sourceTask,
|
|
4210
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
4211
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
4212
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
4213
|
+
}
|
|
4214
|
+
});
|
|
4215
|
+
const completedAt = new Date().toISOString();
|
|
4216
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4217
|
+
status: "completed",
|
|
4218
|
+
completedAt,
|
|
4219
|
+
errorText: null,
|
|
4220
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
|
|
4221
|
+
});
|
|
4222
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
|
|
4223
|
+
emitRigEvent(state, {
|
|
4224
|
+
type: "rig.run.completed",
|
|
4225
|
+
aggregateId: runId,
|
|
4226
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
|
|
4227
|
+
createdAt: completedAt
|
|
4228
|
+
});
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
if (pr.status === "opened" && pr.prUrl) {
|
|
4232
|
+
const completedAt = new Date().toISOString();
|
|
4233
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4234
|
+
status: "completed",
|
|
4235
|
+
completedAt,
|
|
4236
|
+
errorText: null,
|
|
4237
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
|
|
4238
|
+
});
|
|
4239
|
+
appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
|
|
4240
|
+
emitRigEvent(state, {
|
|
4241
|
+
type: "rig.run.completed",
|
|
4242
|
+
aggregateId: runId,
|
|
4243
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
|
|
4244
|
+
createdAt: completedAt
|
|
4245
|
+
});
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
const detail = pr.actionableFeedback.join(`
|
|
4249
|
+
`) || "PR automation did not merge the PR.";
|
|
4250
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
|
|
4251
|
+
appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
|
|
4252
|
+
});
|
|
4253
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4254
|
+
status: "needs_attention",
|
|
4255
|
+
completedAt: new Date().toISOString(),
|
|
4256
|
+
errorText: detail,
|
|
4257
|
+
...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
|
|
4258
|
+
});
|
|
4259
|
+
appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
|
|
4260
|
+
emitRigEvent(state, {
|
|
4261
|
+
type: "rig.run.needs-attention",
|
|
4262
|
+
aggregateId: runId,
|
|
4263
|
+
payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
|
|
4264
|
+
});
|
|
4265
|
+
}
|
|
3913
4266
|
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
3914
4267
|
"completed",
|
|
3915
4268
|
"complete",
|
|
@@ -4118,6 +4471,7 @@ async function startLocalRun(state, runId, options) {
|
|
|
4118
4471
|
RIG_HOST_PROJECT_ROOT: cliProjectRoot,
|
|
4119
4472
|
RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
|
|
4120
4473
|
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
4474
|
+
RIG_SERVER_OWNS_CLOSEOUT: "1",
|
|
4121
4475
|
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
4122
4476
|
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
4123
4477
|
...bridgeGitHubToken ? {
|
|
@@ -4214,6 +4568,38 @@ ${sourceFailure}` });
|
|
|
4214
4568
|
agent: current.runtimeAdapter,
|
|
4215
4569
|
summary: failureSummary
|
|
4216
4570
|
});
|
|
4571
|
+
} else if (closeoutRecord(current)?.status === "pending") {
|
|
4572
|
+
try {
|
|
4573
|
+
await runServerOwnedPrCloseout(state, runId);
|
|
4574
|
+
} catch (closeoutError) {
|
|
4575
|
+
const closeoutFailure = closeoutError instanceof Error ? closeoutError.message : String(closeoutError);
|
|
4576
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4577
|
+
status: "failed",
|
|
4578
|
+
completedAt: new Date().toISOString(),
|
|
4579
|
+
errorText: closeoutFailure,
|
|
4580
|
+
...closeoutPhasePatch("failed", "failed", { error: closeoutFailure })
|
|
4581
|
+
});
|
|
4582
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
4583
|
+
id: `log:${runId}:server-closeout-failed`,
|
|
4584
|
+
title: "Server-owned closeout failed",
|
|
4585
|
+
detail: closeoutFailure,
|
|
4586
|
+
tone: "error",
|
|
4587
|
+
status: "failed",
|
|
4588
|
+
createdAt: new Date().toISOString()
|
|
4589
|
+
}, "server-closeout-failed");
|
|
4590
|
+
if (current.taskId) {
|
|
4591
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: closeoutFailure }, "failed", "Rig server-owned closeout failed.", { errorText: closeoutFailure }).catch((error) => {
|
|
4592
|
+
appendRunLogEntry(state.projectRoot, runId, {
|
|
4593
|
+
id: `log:${runId}:task-source-closeout-failed-update`,
|
|
4594
|
+
title: "Task source closeout failure update failed",
|
|
4595
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
4596
|
+
tone: "error",
|
|
4597
|
+
status: "failed",
|
|
4598
|
+
createdAt: new Date().toISOString()
|
|
4599
|
+
});
|
|
4600
|
+
});
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4217
4603
|
}
|
|
4218
4604
|
broadcastSnapshotInvalidation(state);
|
|
4219
4605
|
} catch (error) {
|
|
@@ -4300,6 +4686,12 @@ async function resumeRunRecord(state, input) {
|
|
|
4300
4686
|
if (run.status === "completed") {
|
|
4301
4687
|
throw new Error("Completed runs cannot be resumed.");
|
|
4302
4688
|
}
|
|
4689
|
+
const closeout = closeoutRecord(run);
|
|
4690
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
4691
|
+
if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
|
|
4692
|
+
await runServerOwnedPrCloseout(state, input.runId);
|
|
4693
|
+
return;
|
|
4694
|
+
}
|
|
4303
4695
|
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
|
|
4304
4696
|
}
|
|
4305
4697
|
function appendRunMessage(projectRoot, input) {
|
|
@@ -4384,11 +4776,45 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
|
|
|
4384
4776
|
writeQueueState(projectRoot, next);
|
|
4385
4777
|
return next;
|
|
4386
4778
|
}
|
|
4387
|
-
var
|
|
4388
|
-
|
|
4779
|
+
var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
|
|
4780
|
+
var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
4781
|
+
function processExists(pid) {
|
|
4782
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
4783
|
+
return false;
|
|
4784
|
+
try {
|
|
4785
|
+
process.kill(pid, 0);
|
|
4786
|
+
return true;
|
|
4787
|
+
} catch {
|
|
4788
|
+
return false;
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
function recoverStaleLocalRun(projectRoot, run) {
|
|
4792
|
+
const record = run;
|
|
4793
|
+
if (run.mode !== "local")
|
|
4794
|
+
return false;
|
|
4795
|
+
const status = normalizeString(record.status)?.toLowerCase() ?? "";
|
|
4796
|
+
if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
|
|
4797
|
+
return false;
|
|
4798
|
+
const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
|
|
4799
|
+
const childPid = typeof record.pid === "number" ? record.pid : null;
|
|
4800
|
+
if (serverPid === null && childPid === null)
|
|
4801
|
+
return false;
|
|
4802
|
+
const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
|
|
4803
|
+
if (hasLiveRecordedProcess && serverPid === process.pid)
|
|
4804
|
+
return false;
|
|
4805
|
+
const completedAt = new Date().toISOString();
|
|
4806
|
+
patchRunRecord(projectRoot, run.runId, {
|
|
4807
|
+
status: "failed",
|
|
4808
|
+
completedAt,
|
|
4809
|
+
errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
|
|
4810
|
+
});
|
|
4811
|
+
return true;
|
|
4812
|
+
}
|
|
4813
|
+
function collectResumableServerCloseouts(state, runs) {
|
|
4389
4814
|
return runs.filter((run) => {
|
|
4390
|
-
const
|
|
4391
|
-
|
|
4815
|
+
const closeout = closeoutRecord(run);
|
|
4816
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
4817
|
+
return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
|
|
4392
4818
|
});
|
|
4393
4819
|
}
|
|
4394
4820
|
async function reconcileScheduler(state, reason) {
|
|
@@ -4405,17 +4831,33 @@ async function reconcileScheduler(state, reason) {
|
|
|
4405
4831
|
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
4406
4832
|
let runs = listAuthorityRuns4(state.projectRoot);
|
|
4407
4833
|
let changed = false;
|
|
4408
|
-
const
|
|
4409
|
-
|
|
4834
|
+
for (const run of runs) {
|
|
4835
|
+
if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
|
|
4836
|
+
changed = true;
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
if (changed) {
|
|
4840
|
+
runs = listAuthorityRuns4(state.projectRoot);
|
|
4841
|
+
}
|
|
4842
|
+
const resumableCloseouts = collectResumableServerCloseouts(state, runs);
|
|
4843
|
+
for (const run of resumableCloseouts) {
|
|
4410
4844
|
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
4411
|
-
id: `log:${run.runId}:auto-resume:${Date.now()}`,
|
|
4412
|
-
title: "
|
|
4413
|
-
detail: `Rig server recovered
|
|
4845
|
+
id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
|
|
4846
|
+
title: "Server-owned closeout auto-resume scheduled",
|
|
4847
|
+
detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
|
|
4414
4848
|
tone: "info",
|
|
4415
|
-
status: "
|
|
4849
|
+
status: "reviewing",
|
|
4416
4850
|
createdAt: new Date().toISOString()
|
|
4417
4851
|
});
|
|
4418
|
-
await
|
|
4852
|
+
await runServerOwnedPrCloseout(state, run.runId).catch((error) => {
|
|
4853
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4854
|
+
patchRunRecord(state.projectRoot, run.runId, {
|
|
4855
|
+
status: "failed",
|
|
4856
|
+
completedAt: new Date().toISOString(),
|
|
4857
|
+
errorText: detail,
|
|
4858
|
+
...closeoutPhasePatch("failed", "failed", { error: detail })
|
|
4859
|
+
});
|
|
4860
|
+
});
|
|
4419
4861
|
changed = true;
|
|
4420
4862
|
}
|
|
4421
4863
|
if (changed) {
|
|
@@ -5211,6 +5653,75 @@ function buildProjectConfigStatus(root) {
|
|
|
5211
5653
|
suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
|
|
5212
5654
|
};
|
|
5213
5655
|
}
|
|
5656
|
+
var RIG_GITHUB_LIFECYCLE_LABELS = [
|
|
5657
|
+
"ready",
|
|
5658
|
+
"blocked",
|
|
5659
|
+
"in-progress",
|
|
5660
|
+
"under-review",
|
|
5661
|
+
"failed",
|
|
5662
|
+
"cancelled",
|
|
5663
|
+
"rig:running",
|
|
5664
|
+
"rig:pr-open",
|
|
5665
|
+
"rig:ci-fixing",
|
|
5666
|
+
"rig:merging",
|
|
5667
|
+
"rig:done",
|
|
5668
|
+
"rig:needs-attention"
|
|
5669
|
+
];
|
|
5670
|
+
function githubProjectsEnabled2(config) {
|
|
5671
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
5672
|
+
return false;
|
|
5673
|
+
const root = config;
|
|
5674
|
+
const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
|
|
5675
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
5676
|
+
return projects?.enabled === true;
|
|
5677
|
+
}
|
|
5678
|
+
function githubIssueSourceRepo(config) {
|
|
5679
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
5680
|
+
return null;
|
|
5681
|
+
const root = config;
|
|
5682
|
+
const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
|
|
5683
|
+
const owner = normalizeString(taskSource?.owner);
|
|
5684
|
+
const repo = normalizeString(taskSource?.repo);
|
|
5685
|
+
if (taskSource?.kind === "github-issues" && owner && repo)
|
|
5686
|
+
return { owner, repo };
|
|
5687
|
+
const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
|
|
5688
|
+
const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
|
|
5689
|
+
const match = slug?.match(/^([^/]+)\/([^/]+)$/);
|
|
5690
|
+
return match ? { owner: match[1], repo: match[2] } : null;
|
|
5691
|
+
}
|
|
5692
|
+
async function ensureGitHubLifecycleLabels(projectRoot, config) {
|
|
5693
|
+
const repo = githubIssueSourceRepo(config);
|
|
5694
|
+
if (!repo)
|
|
5695
|
+
return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
5696
|
+
const token = createGitHubAuthStore(projectRoot).readToken();
|
|
5697
|
+
if (!token)
|
|
5698
|
+
return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
5699
|
+
const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
|
|
5700
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
|
|
5701
|
+
});
|
|
5702
|
+
const existingJson = await existingResponse.json().catch(() => []);
|
|
5703
|
+
const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
|
|
5704
|
+
const created = [];
|
|
5705
|
+
const alreadyPresent = [];
|
|
5706
|
+
const failed = [];
|
|
5707
|
+
for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
|
|
5708
|
+
if (existing.has(label)) {
|
|
5709
|
+
alreadyPresent.push(label);
|
|
5710
|
+
continue;
|
|
5711
|
+
}
|
|
5712
|
+
const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
|
|
5713
|
+
method: "POST",
|
|
5714
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
|
|
5715
|
+
body: JSON.stringify({ name: label, color: label.startsWith("rig:") ? "6f42c1" : "ededed", description: label.startsWith("rig:") ? "Task status managed by Rig" : "Task lifecycle status managed by Rig" })
|
|
5716
|
+
});
|
|
5717
|
+
if (response.ok || response.status === 422) {
|
|
5718
|
+
(response.status === 422 ? alreadyPresent : created).push(label);
|
|
5719
|
+
} else {
|
|
5720
|
+
failed.push({ label, error: await response.text().catch(() => response.statusText) });
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
|
|
5724
|
+
}
|
|
5214
5725
|
function normalizeCommit(value) {
|
|
5215
5726
|
const raw = normalizeString(value);
|
|
5216
5727
|
return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
|
|
@@ -5609,15 +6120,33 @@ say "Installing @h-rig/cli@latest"
|
|
|
5609
6120
|
bun add -g @h-rig/cli@latest
|
|
5610
6121
|
|
|
5611
6122
|
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
5612
|
-
|
|
6123
|
+
BUN_RIG="$BUN_INSTALL/bin/rig"
|
|
6124
|
+
if [ ! -x "$BUN_RIG" ]; then
|
|
6125
|
+
printf 'rig-install: expected Bun global rig at %s but it was not executable.
|
|
6126
|
+
' "$BUN_RIG" >&2
|
|
6127
|
+
exit 1
|
|
6128
|
+
fi
|
|
6129
|
+
|
|
6130
|
+
USER_BIN="$HOME/.local/bin"
|
|
6131
|
+
mkdir -p "$USER_BIN"
|
|
6132
|
+
cat > "$USER_BIN/rig" <<'RIG_SHIM'
|
|
6133
|
+
#!/usr/bin/env bash
|
|
6134
|
+
set -euo pipefail
|
|
6135
|
+
exec "\${BUN_INSTALL:-$HOME/.bun}/bin/rig" "$@"
|
|
6136
|
+
RIG_SHIM
|
|
6137
|
+
chmod +x "$USER_BIN/rig"
|
|
6138
|
+
|
|
6139
|
+
export PATH="$USER_BIN:$BUN_INSTALL/bin:$PATH"
|
|
6140
|
+
if command -v hash >/dev/null 2>&1; then hash -r; fi
|
|
5613
6141
|
|
|
5614
6142
|
if ! command -v rig >/dev/null 2>&1; then
|
|
5615
|
-
printf 'rig-install: rig installed, but rig is not on PATH. Add %s/bin to PATH and retry.
|
|
5616
|
-
' "$BUN_INSTALL" >&2
|
|
6143
|
+
printf 'rig-install: rig installed, but rig is not on PATH. Add %s and %s/bin to PATH and retry.
|
|
6144
|
+
' "$USER_BIN" "$BUN_INSTALL" >&2
|
|
5617
6145
|
exit 1
|
|
5618
6146
|
fi
|
|
5619
6147
|
|
|
5620
6148
|
say "Verifying rig"
|
|
6149
|
+
"$BUN_RIG" --help >/dev/null
|
|
5621
6150
|
rig --help >/dev/null
|
|
5622
6151
|
say "Done. Run: rig --help"
|
|
5623
6152
|
`;
|
|
@@ -6356,16 +6885,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6356
6885
|
if (!source) {
|
|
6357
6886
|
return deps.badRequest("No task source is configured");
|
|
6358
6887
|
}
|
|
6888
|
+
if (!source.updateTask && !(update.status && source.updateStatus)) {
|
|
6889
|
+
return deps.badRequest("Configured task source does not support updates");
|
|
6890
|
+
}
|
|
6359
6891
|
const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
|
|
6360
6892
|
return;
|
|
6361
6893
|
}) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
|
|
6362
|
-
if (source.updateTask) {
|
|
6363
|
-
await source.updateTask(id, update);
|
|
6364
|
-
} else if (update.status && source.updateStatus) {
|
|
6365
|
-
await source.updateStatus(id, update.status);
|
|
6366
|
-
} else {
|
|
6367
|
-
return deps.badRequest("Configured task source does not support updates");
|
|
6368
|
-
}
|
|
6369
6894
|
const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
|
|
6370
6895
|
const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
|
|
6371
6896
|
taskId: id,
|
|
@@ -6374,6 +6899,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6374
6899
|
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
6375
6900
|
config: ctx?.config
|
|
6376
6901
|
}).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
|
|
6902
|
+
if (update.status && githubProjectsEnabled2(ctx?.config) && projectSync.synced === false) {
|
|
6903
|
+
return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
|
|
6904
|
+
}
|
|
6905
|
+
try {
|
|
6906
|
+
if (source.updateTask) {
|
|
6907
|
+
await source.updateTask(id, update);
|
|
6908
|
+
} else if (update.status && source.updateStatus) {
|
|
6909
|
+
await source.updateStatus(id, update.status);
|
|
6910
|
+
}
|
|
6911
|
+
} catch (error) {
|
|
6912
|
+
let rollback = null;
|
|
6913
|
+
const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
|
|
6914
|
+
if (update.status && previousStatus && githubProjectsEnabled2(ctx?.config) && projectSync.synced !== false) {
|
|
6915
|
+
rollback = await syncGitHubProjectStatusForTaskUpdate({
|
|
6916
|
+
taskId: id,
|
|
6917
|
+
status: previousStatus,
|
|
6918
|
+
issueNodeId,
|
|
6919
|
+
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
6920
|
+
config: ctx?.config
|
|
6921
|
+
}).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
|
|
6922
|
+
}
|
|
6923
|
+
return deps.jsonResponse({
|
|
6924
|
+
ok: false,
|
|
6925
|
+
id,
|
|
6926
|
+
projectSync,
|
|
6927
|
+
rollback,
|
|
6928
|
+
error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
6929
|
+
}, 502);
|
|
6930
|
+
}
|
|
6377
6931
|
deps.snapshotService.invalidate("github-issue-updated");
|
|
6378
6932
|
await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
|
|
6379
6933
|
return;
|
|
@@ -6382,26 +6936,41 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6382
6936
|
return deps.jsonResponse({ ok: true, id, projectSync });
|
|
6383
6937
|
}
|
|
6384
6938
|
if (url.pathname === "/api/workspace/task-labels") {
|
|
6939
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
|
|
6940
|
+
if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
|
|
6941
|
+
return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
|
|
6942
|
+
}
|
|
6385
6943
|
return deps.jsonResponse({
|
|
6386
6944
|
ok: true,
|
|
6387
6945
|
ready: true,
|
|
6388
6946
|
labelsReady: true,
|
|
6389
|
-
labels: [
|
|
6390
|
-
|
|
6391
|
-
"blocked",
|
|
6392
|
-
"in-progress",
|
|
6393
|
-
"under-review",
|
|
6394
|
-
"failed",
|
|
6395
|
-
"cancelled",
|
|
6396
|
-
"rig:running",
|
|
6397
|
-
"rig:pr-open",
|
|
6398
|
-
"rig:ci-fixing",
|
|
6399
|
-
"rig:done",
|
|
6400
|
-
"rig:needs-attention"
|
|
6401
|
-
],
|
|
6402
|
-
note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
|
|
6947
|
+
labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
|
|
6948
|
+
note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
|
|
6403
6949
|
});
|
|
6404
6950
|
}
|
|
6951
|
+
if (url.pathname === "/api/github/projects" && req.method === "GET") {
|
|
6952
|
+
const owner = normalizeString(url.searchParams.get("owner"));
|
|
6953
|
+
if (!owner)
|
|
6954
|
+
return deps.badRequest("owner is required");
|
|
6955
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
6956
|
+
if (!token)
|
|
6957
|
+
return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
|
|
6958
|
+
const projects = await listGitHubProjects({ owner, token }).catch((error) => {
|
|
6959
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
6960
|
+
});
|
|
6961
|
+
return deps.jsonResponse({ ok: true, projects });
|
|
6962
|
+
}
|
|
6963
|
+
const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
|
|
6964
|
+
if (projectStatusMatch && req.method === "GET") {
|
|
6965
|
+
const projectId = decodeURIComponent(projectStatusMatch[1]);
|
|
6966
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
6967
|
+
if (!token)
|
|
6968
|
+
return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
|
|
6969
|
+
const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
|
|
6970
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
6971
|
+
});
|
|
6972
|
+
return deps.jsonResponse({ ok: true, field });
|
|
6973
|
+
}
|
|
6405
6974
|
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
6406
6975
|
const body = await deps.readJsonBody(req);
|
|
6407
6976
|
const ids = uniqueStringList(body.ids ?? body.id);
|
|
@@ -7566,6 +8135,69 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7566
8135
|
}
|
|
7567
8136
|
const run = leaseValidation.run;
|
|
7568
8137
|
const completedAt = new Date().toISOString();
|
|
8138
|
+
const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
|
|
8139
|
+
if (run.taskId && workspaceDir) {
|
|
8140
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
8141
|
+
status: "reviewing",
|
|
8142
|
+
completedAt: null,
|
|
8143
|
+
hostId,
|
|
8144
|
+
endpointId: leaseId,
|
|
8145
|
+
worktreePath: workspaceDir,
|
|
8146
|
+
serverCloseout: {
|
|
8147
|
+
status: "pending",
|
|
8148
|
+
phase: "queued",
|
|
8149
|
+
requestedAt: completedAt,
|
|
8150
|
+
updatedAt: completedAt,
|
|
8151
|
+
runtimeWorkspace: workspaceDir,
|
|
8152
|
+
branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
|
|
8153
|
+
taskId: run.taskId,
|
|
8154
|
+
source: "remote-complete"
|
|
8155
|
+
}
|
|
8156
|
+
});
|
|
8157
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
8158
|
+
id: `log:${runId}:remote-server-closeout-requested`,
|
|
8159
|
+
title: "Server-owned closeout requested",
|
|
8160
|
+
detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
|
|
8161
|
+
tone: "info",
|
|
8162
|
+
status: "reviewing",
|
|
8163
|
+
createdAt: completedAt,
|
|
8164
|
+
payload: { workspaceDir, hostId, leaseId }
|
|
8165
|
+
}, "remote-server-closeout-requested");
|
|
8166
|
+
deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
|
|
8167
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8168
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
8169
|
+
status: "failed",
|
|
8170
|
+
completedAt: new Date().toISOString(),
|
|
8171
|
+
errorText: detail,
|
|
8172
|
+
serverCloseout: {
|
|
8173
|
+
status: "failed",
|
|
8174
|
+
phase: "failed",
|
|
8175
|
+
updatedAt: new Date().toISOString(),
|
|
8176
|
+
error: detail
|
|
8177
|
+
}
|
|
8178
|
+
});
|
|
8179
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
8180
|
+
id: `log:${runId}:remote-server-closeout-failed`,
|
|
8181
|
+
title: "Server-owned closeout failed",
|
|
8182
|
+
detail,
|
|
8183
|
+
tone: "error",
|
|
8184
|
+
status: "failed",
|
|
8185
|
+
createdAt: new Date().toISOString()
|
|
8186
|
+
}, "remote-server-closeout-failed");
|
|
8187
|
+
}).finally(() => {
|
|
8188
|
+
deps.reconcileScheduler(state, "remote-server-closeout-terminal");
|
|
8189
|
+
});
|
|
8190
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
8191
|
+
return deps.jsonResponse({
|
|
8192
|
+
ok: true,
|
|
8193
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
8194
|
+
hostId,
|
|
8195
|
+
runId,
|
|
8196
|
+
leaseId,
|
|
8197
|
+
closeout: "server-owned",
|
|
8198
|
+
acceptedAt: new Date().toISOString()
|
|
8199
|
+
});
|
|
8200
|
+
}
|
|
7569
8201
|
patchRunRecord(state.projectRoot, runId, {
|
|
7570
8202
|
status: "completed",
|
|
7571
8203
|
completedAt,
|
|
@@ -7708,6 +8340,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7708
8340
|
const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
|
|
7709
8341
|
return deps.jsonResponse(page);
|
|
7710
8342
|
}
|
|
8343
|
+
const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
|
|
8344
|
+
if (runTimelineMatch) {
|
|
8345
|
+
const runId = decodeURIComponent(runTimelineMatch[1]);
|
|
8346
|
+
const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
|
|
8347
|
+
const cursor = normalizeString(url.searchParams.get("cursor"));
|
|
8348
|
+
const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
|
|
8349
|
+
return deps.jsonResponse(page);
|
|
8350
|
+
}
|
|
7711
8351
|
const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
|
|
7712
8352
|
if (runSteerMatch && req.method === "POST") {
|
|
7713
8353
|
const runId = decodeURIComponent(runSteerMatch[1]);
|
|
@@ -12924,6 +13564,7 @@ function buildHttpRouterDeps(state) {
|
|
|
12924
13564
|
startLocalRun,
|
|
12925
13565
|
stopRunRecord,
|
|
12926
13566
|
resumeRunRecord,
|
|
13567
|
+
runServerOwnedPrCloseout,
|
|
12927
13568
|
claimRemoteRun,
|
|
12928
13569
|
listRemoteRunArtifacts,
|
|
12929
13570
|
broadcastSnapshotInvalidation,
|