@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/index.js
CHANGED
|
@@ -1580,6 +1580,18 @@ function readJsonlFileTail(path, options) {
|
|
|
1580
1580
|
const completeLines = start > 0 ? lines.slice(1) : lines;
|
|
1581
1581
|
return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
|
|
1582
1582
|
}
|
|
1583
|
+
async function readRunTimelinePage(projectRoot, runId, options = {}) {
|
|
1584
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
1585
|
+
const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
|
|
1586
|
+
const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
1587
|
+
const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
|
|
1588
|
+
const endExclusive = Math.min(entries.length, startInclusive + limit);
|
|
1589
|
+
return {
|
|
1590
|
+
entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
|
|
1591
|
+
nextCursor: String(endExclusive),
|
|
1592
|
+
hasMore: endExclusive < entries.length
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1583
1595
|
var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
1584
1596
|
async function readRunLogsPage(projectRoot, runId, options = {}) {
|
|
1585
1597
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
@@ -3237,8 +3249,7 @@ function buildRunStartPatch(startedAt) {
|
|
|
3237
3249
|
status: "preparing",
|
|
3238
3250
|
startedAt,
|
|
3239
3251
|
completedAt: null,
|
|
3240
|
-
errorText: null
|
|
3241
|
-
serverPid: process.pid
|
|
3252
|
+
errorText: null
|
|
3242
3253
|
};
|
|
3243
3254
|
}
|
|
3244
3255
|
|
|
@@ -3720,6 +3731,11 @@ import {
|
|
|
3720
3731
|
buildTaskRunLifecycleComment,
|
|
3721
3732
|
updateConfiguredTaskSourceTask
|
|
3722
3733
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
3734
|
+
import {
|
|
3735
|
+
closeIssueAfterMergedPr,
|
|
3736
|
+
commitRunChanges,
|
|
3737
|
+
runPrAutomation
|
|
3738
|
+
} from "@rig/runtime/control-plane/native/pr-automation";
|
|
3723
3739
|
|
|
3724
3740
|
// packages/server/src/scheduler.ts
|
|
3725
3741
|
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -4055,6 +4071,9 @@ function asRecord(value) {
|
|
|
4055
4071
|
function asString(value) {
|
|
4056
4072
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
4057
4073
|
}
|
|
4074
|
+
function asNumber(value) {
|
|
4075
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
4076
|
+
}
|
|
4058
4077
|
async function defaultGraphQLFetch(query, variables, token) {
|
|
4059
4078
|
const response = await fetch("https://api.github.com/graphql", {
|
|
4060
4079
|
method: "POST",
|
|
@@ -4071,6 +4090,32 @@ async function defaultGraphQLFetch(query, variables, token) {
|
|
|
4071
4090
|
}
|
|
4072
4091
|
return json.data;
|
|
4073
4092
|
}
|
|
4093
|
+
function projectNodesFrom(data) {
|
|
4094
|
+
const root = asRecord(data);
|
|
4095
|
+
const owner = asRecord(root?.organization) ?? asRecord(root?.user);
|
|
4096
|
+
const projects = asRecord(owner?.projectsV2);
|
|
4097
|
+
const nodes = projects?.nodes;
|
|
4098
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
4099
|
+
}
|
|
4100
|
+
async function listGitHubProjects(input) {
|
|
4101
|
+
const query = `
|
|
4102
|
+
query RigListProjects($owner: String!, $first: Int!) {
|
|
4103
|
+
organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
4104
|
+
user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
4105
|
+
}
|
|
4106
|
+
`;
|
|
4107
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
4108
|
+
const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
|
|
4109
|
+
return projectNodesFrom(data).flatMap((node) => {
|
|
4110
|
+
const record = asRecord(node);
|
|
4111
|
+
const id = asString(record?.id);
|
|
4112
|
+
const number = asNumber(record?.number);
|
|
4113
|
+
const title = asString(record?.title);
|
|
4114
|
+
if (!id || number === undefined || !title)
|
|
4115
|
+
return [];
|
|
4116
|
+
return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
|
|
4117
|
+
});
|
|
4118
|
+
}
|
|
4074
4119
|
async function resolveProjectStatusField(input) {
|
|
4075
4120
|
const query = `
|
|
4076
4121
|
query RigProjectStatusField($projectId: ID!) {
|
|
@@ -4165,6 +4210,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
4165
4210
|
running: "In Progress",
|
|
4166
4211
|
prOpen: "In Review",
|
|
4167
4212
|
ciFixing: "In Review",
|
|
4213
|
+
merging: "Merging",
|
|
4168
4214
|
done: "Done",
|
|
4169
4215
|
needsAttention: "Needs Attention"
|
|
4170
4216
|
};
|
|
@@ -4178,6 +4224,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
4178
4224
|
return "prOpen";
|
|
4179
4225
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
4180
4226
|
return "ciFixing";
|
|
4227
|
+
if (normalized === "merging" || normalized === "merge")
|
|
4228
|
+
return "merging";
|
|
4181
4229
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
4182
4230
|
return "needsAttention";
|
|
4183
4231
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -4306,9 +4354,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
|
|
|
4306
4354
|
return null;
|
|
4307
4355
|
return null;
|
|
4308
4356
|
}
|
|
4357
|
+
function githubProjectsEnabled(config) {
|
|
4358
|
+
const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
|
|
4359
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
4360
|
+
return projects?.enabled === true;
|
|
4361
|
+
}
|
|
4309
4362
|
async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
|
|
4310
4363
|
if (!run.taskId)
|
|
4311
|
-
return;
|
|
4364
|
+
return false;
|
|
4312
4365
|
const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
|
|
4313
4366
|
try {
|
|
4314
4367
|
const result = await syncGitHubProjectStatusForTaskUpdate({
|
|
@@ -4319,28 +4372,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
|
|
|
4319
4372
|
config
|
|
4320
4373
|
});
|
|
4321
4374
|
if (!result.synced && result.reason !== "project-sync-disabled") {
|
|
4375
|
+
const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
|
|
4322
4376
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
4323
4377
|
id: `log:${run.runId}:github-project-sync:${status}`,
|
|
4324
4378
|
title: "GitHub Project sync skipped",
|
|
4325
|
-
detail
|
|
4379
|
+
detail,
|
|
4326
4380
|
tone: "warn",
|
|
4327
4381
|
status: "running",
|
|
4328
4382
|
createdAt: new Date().toISOString(),
|
|
4329
4383
|
payload: { reason: result.reason, issueNodeId }
|
|
4330
4384
|
});
|
|
4385
|
+
if (githubProjectsEnabled(config)) {
|
|
4386
|
+
throw new Error(detail);
|
|
4387
|
+
}
|
|
4388
|
+
return false;
|
|
4331
4389
|
}
|
|
4390
|
+
return result.synced === true;
|
|
4332
4391
|
} catch (error) {
|
|
4392
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4333
4393
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
4334
4394
|
id: `log:${run.runId}:github-project-sync-error:${status}`,
|
|
4335
4395
|
title: "GitHub Project sync failed",
|
|
4336
|
-
detail
|
|
4396
|
+
detail,
|
|
4337
4397
|
tone: "error",
|
|
4338
4398
|
status: "running",
|
|
4339
4399
|
createdAt: new Date().toISOString(),
|
|
4340
4400
|
payload: { issueNodeId }
|
|
4341
4401
|
});
|
|
4402
|
+
if (githubProjectsEnabled(config)) {
|
|
4403
|
+
throw new Error(detail);
|
|
4404
|
+
}
|
|
4405
|
+
return false;
|
|
4342
4406
|
}
|
|
4343
4407
|
}
|
|
4408
|
+
function createCommandRunner(binary, extraEnv = {}) {
|
|
4409
|
+
return async (args, options) => {
|
|
4410
|
+
const child = spawn3(binary, [...args], {
|
|
4411
|
+
cwd: options?.cwd,
|
|
4412
|
+
env: { ...process.env, ...extraEnv },
|
|
4413
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4414
|
+
});
|
|
4415
|
+
const stdoutChunks = [];
|
|
4416
|
+
const stderrChunks = [];
|
|
4417
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
4418
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
4419
|
+
const exitCode = await new Promise((resolve15) => {
|
|
4420
|
+
child.once("error", () => resolve15(1));
|
|
4421
|
+
child.once("close", (code) => resolve15(code ?? 1));
|
|
4422
|
+
});
|
|
4423
|
+
return {
|
|
4424
|
+
exitCode,
|
|
4425
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
4426
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8")
|
|
4427
|
+
};
|
|
4428
|
+
};
|
|
4429
|
+
}
|
|
4430
|
+
function closeoutRecord(run) {
|
|
4431
|
+
const value = run.serverCloseout;
|
|
4432
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4433
|
+
}
|
|
4434
|
+
function closeoutPhasePatch(phase, status, extra = {}) {
|
|
4435
|
+
const updatedAt = new Date().toISOString();
|
|
4436
|
+
return {
|
|
4437
|
+
serverCloseout: {
|
|
4438
|
+
phase,
|
|
4439
|
+
status,
|
|
4440
|
+
updatedAt,
|
|
4441
|
+
...extra
|
|
4442
|
+
}
|
|
4443
|
+
};
|
|
4444
|
+
}
|
|
4445
|
+
function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
|
|
4446
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
4447
|
+
id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
|
|
4448
|
+
title: `Server closeout: ${phase}`,
|
|
4449
|
+
detail,
|
|
4450
|
+
tone,
|
|
4451
|
+
status,
|
|
4452
|
+
createdAt: new Date().toISOString()
|
|
4453
|
+
}, `server-closeout-${phase}`);
|
|
4454
|
+
}
|
|
4344
4455
|
async function autoAssignRunIssue(projectRoot, run) {
|
|
4345
4456
|
if (!run.taskId)
|
|
4346
4457
|
return;
|
|
@@ -4370,7 +4481,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
4370
4481
|
return;
|
|
4371
4482
|
}
|
|
4372
4483
|
const config = await loadRigLifecycleConfig(projectRoot);
|
|
4373
|
-
await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
4484
|
+
const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
4374
4485
|
if (status === "in_progress") {
|
|
4375
4486
|
await autoAssignRunIssue(projectRoot, run);
|
|
4376
4487
|
}
|
|
@@ -4386,24 +4497,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
4386
4497
|
});
|
|
4387
4498
|
return;
|
|
4388
4499
|
}
|
|
4389
|
-
const
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4500
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
4501
|
+
const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
|
|
4502
|
+
const rollbackProjectSync = async () => {
|
|
4503
|
+
if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
|
|
4504
|
+
return;
|
|
4505
|
+
await syncGitHubProjectStatusForTaskUpdate({
|
|
4506
|
+
taskId: run.taskId,
|
|
4507
|
+
status: previousStatus,
|
|
4508
|
+
issueNodeId: extractGitHubIssueNodeId(sourceTask),
|
|
4509
|
+
token: createGitHubAuthStore(projectRoot).readToken(),
|
|
4510
|
+
config
|
|
4511
|
+
}).catch((rollbackError) => {
|
|
4512
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
4513
|
+
id: `log:${run.runId}:github-project-sync-rollback:${status}`,
|
|
4514
|
+
title: "GitHub Project sync rollback failed",
|
|
4515
|
+
detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
|
|
4516
|
+
tone: "error",
|
|
4517
|
+
status: "running",
|
|
4518
|
+
createdAt: new Date().toISOString()
|
|
4519
|
+
});
|
|
4520
|
+
});
|
|
4521
|
+
};
|
|
4522
|
+
let result;
|
|
4523
|
+
try {
|
|
4524
|
+
result = await updateConfiguredTaskSourceTask(projectRoot, {
|
|
4525
|
+
taskId: run.taskId,
|
|
4526
|
+
sourceTask,
|
|
4527
|
+
update: {
|
|
4396
4528
|
status,
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4529
|
+
comment: buildTaskRunLifecycleComment({
|
|
4530
|
+
runId: run.runId,
|
|
4531
|
+
status,
|
|
4532
|
+
summary,
|
|
4533
|
+
runtimeWorkspace: normalizeString(run.worktreePath),
|
|
4534
|
+
logsDir: normalizeString(run.logRoot),
|
|
4535
|
+
sessionDir: normalizeString(run.sessionPath),
|
|
4536
|
+
errorText: options.errorText ?? normalizeString(run.errorText)
|
|
4537
|
+
})
|
|
4538
|
+
}
|
|
4539
|
+
});
|
|
4540
|
+
} catch (error) {
|
|
4541
|
+
await rollbackProjectSync();
|
|
4542
|
+
throw error;
|
|
4543
|
+
}
|
|
4405
4544
|
if (!result.updated) {
|
|
4406
4545
|
if (result.source === "plugin" || result.sourceKind) {
|
|
4546
|
+
await rollbackProjectSync();
|
|
4407
4547
|
throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
|
|
4408
4548
|
}
|
|
4409
4549
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
@@ -4416,6 +4556,219 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
4416
4556
|
});
|
|
4417
4557
|
}
|
|
4418
4558
|
}
|
|
4559
|
+
async function runServerOwnedPrCloseout(state, runId) {
|
|
4560
|
+
const run = readAuthorityRun4(state.projectRoot, runId);
|
|
4561
|
+
if (!run)
|
|
4562
|
+
throw new Error(`Run not found: ${runId}`);
|
|
4563
|
+
const closeout = closeoutRecord(run);
|
|
4564
|
+
if (!closeout)
|
|
4565
|
+
return;
|
|
4566
|
+
const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
|
|
4567
|
+
if (!taskId)
|
|
4568
|
+
throw new Error("Server-owned closeout requires a task id.");
|
|
4569
|
+
const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
|
|
4570
|
+
const branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
|
|
4571
|
+
const config = await loadRigLifecycleConfig(state.projectRoot);
|
|
4572
|
+
const runPrMode = normalizeString(run.prMode);
|
|
4573
|
+
const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
|
|
4574
|
+
const effectiveConfig = {
|
|
4575
|
+
...config ?? {},
|
|
4576
|
+
pr: {
|
|
4577
|
+
...config?.pr ?? {},
|
|
4578
|
+
mode: prMode,
|
|
4579
|
+
autoFixChecks: false,
|
|
4580
|
+
autoFixReview: false
|
|
4581
|
+
}
|
|
4582
|
+
};
|
|
4583
|
+
const readCurrentRun = () => readAuthorityRun4(state.projectRoot, runId) ?? run;
|
|
4584
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
4585
|
+
const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
|
|
4586
|
+
const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
|
|
4587
|
+
const closeoutPrUrl = normalizeString(closeout.prUrl);
|
|
4588
|
+
if (closeoutPhase === "completed" || closeoutStatus === "completed") {
|
|
4589
|
+
return;
|
|
4590
|
+
}
|
|
4591
|
+
if (closeoutPhase === "close-source" && closeoutPrUrl) {
|
|
4592
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4593
|
+
status: "reviewing",
|
|
4594
|
+
...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
|
|
4595
|
+
});
|
|
4596
|
+
await closeIssueAfterMergedPr({
|
|
4597
|
+
projectRoot: state.projectRoot,
|
|
4598
|
+
taskId,
|
|
4599
|
+
runId,
|
|
4600
|
+
prUrl: closeoutPrUrl,
|
|
4601
|
+
sourceTask,
|
|
4602
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
4603
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
4604
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
4605
|
+
}
|
|
4606
|
+
});
|
|
4607
|
+
const completedAt = new Date().toISOString();
|
|
4608
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4609
|
+
status: "completed",
|
|
4610
|
+
completedAt,
|
|
4611
|
+
errorText: null,
|
|
4612
|
+
...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
|
|
4613
|
+
});
|
|
4614
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
|
|
4615
|
+
emitRigEvent(state, {
|
|
4616
|
+
type: "rig.run.completed",
|
|
4617
|
+
aggregateId: runId,
|
|
4618
|
+
payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
|
|
4619
|
+
createdAt: completedAt
|
|
4620
|
+
});
|
|
4621
|
+
return;
|
|
4622
|
+
}
|
|
4623
|
+
if (prMode === "off" || prMode === "ask") {
|
|
4624
|
+
const completedAt = new Date().toISOString();
|
|
4625
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4626
|
+
status: "completed",
|
|
4627
|
+
completedAt,
|
|
4628
|
+
errorText: null,
|
|
4629
|
+
...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
|
|
4630
|
+
});
|
|
4631
|
+
appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
|
|
4632
|
+
emitRigEvent(state, {
|
|
4633
|
+
type: "rig.run.completed",
|
|
4634
|
+
aggregateId: runId,
|
|
4635
|
+
payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
|
|
4636
|
+
createdAt: completedAt
|
|
4637
|
+
});
|
|
4638
|
+
return;
|
|
4639
|
+
}
|
|
4640
|
+
const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
|
|
4641
|
+
const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
|
|
4642
|
+
const gitCommand = createCommandRunner("git", githubEnv);
|
|
4643
|
+
const ghCommand = createCommandRunner("gh", githubEnv);
|
|
4644
|
+
const setCloseout = (phase, status, extra = {}) => {
|
|
4645
|
+
const previous = closeoutRecord(readCurrentRun()) ?? closeout;
|
|
4646
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4647
|
+
status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
|
|
4648
|
+
...closeoutPhasePatch(phase, status, { ...previous, ...extra })
|
|
4649
|
+
});
|
|
4650
|
+
};
|
|
4651
|
+
setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
4652
|
+
appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
|
|
4653
|
+
const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
|
|
4654
|
+
appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
|
|
4655
|
+
setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
4656
|
+
const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
|
|
4657
|
+
if (push.exitCode !== 0) {
|
|
4658
|
+
throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
|
|
4659
|
+
}
|
|
4660
|
+
const sourceTaskForPr = {
|
|
4661
|
+
title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
|
|
4662
|
+
};
|
|
4663
|
+
const artifactRoot = resolve14(state.projectRoot, "artifacts", taskId);
|
|
4664
|
+
setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4665
|
+
const pr = await runPrAutomation({
|
|
4666
|
+
projectRoot: workspace,
|
|
4667
|
+
taskId,
|
|
4668
|
+
runId,
|
|
4669
|
+
branch,
|
|
4670
|
+
config: effectiveConfig,
|
|
4671
|
+
sourceTask: sourceTaskForPr,
|
|
4672
|
+
artifactRoot,
|
|
4673
|
+
command: ghCommand,
|
|
4674
|
+
gitCommand,
|
|
4675
|
+
steerPi: async (message) => {
|
|
4676
|
+
appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
|
|
4677
|
+
appendRunTimelineEntry(state.projectRoot, runId, {
|
|
4678
|
+
id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
|
|
4679
|
+
type: "user_message",
|
|
4680
|
+
text: message,
|
|
4681
|
+
createdAt: new Date().toISOString(),
|
|
4682
|
+
state: "completed"
|
|
4683
|
+
});
|
|
4684
|
+
},
|
|
4685
|
+
lifecycle: {
|
|
4686
|
+
onPrOpened: async ({ prUrl }) => {
|
|
4687
|
+
setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4688
|
+
appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
|
|
4689
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
|
|
4690
|
+
},
|
|
4691
|
+
onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
|
|
4692
|
+
onFeedback: async ({ feedback }) => {
|
|
4693
|
+
appendCloseoutStage(state, runId, "feedback", feedback.join(`
|
|
4694
|
+
`), "reviewing", "error");
|
|
4695
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
|
|
4696
|
+
},
|
|
4697
|
+
onMergeStarted: async ({ prUrl }) => {
|
|
4698
|
+
setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
4699
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
4700
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
|
|
4701
|
+
},
|
|
4702
|
+
onMerged: ({ prUrl }) => {
|
|
4703
|
+
setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
|
|
4704
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
});
|
|
4708
|
+
if (pr.status === "merged" && pr.prUrl) {
|
|
4709
|
+
setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
|
|
4710
|
+
await closeIssueAfterMergedPr({
|
|
4711
|
+
projectRoot: state.projectRoot,
|
|
4712
|
+
taskId,
|
|
4713
|
+
runId,
|
|
4714
|
+
prUrl: pr.prUrl,
|
|
4715
|
+
sourceTask,
|
|
4716
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
4717
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
4718
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
4719
|
+
}
|
|
4720
|
+
});
|
|
4721
|
+
const completedAt = new Date().toISOString();
|
|
4722
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4723
|
+
status: "completed",
|
|
4724
|
+
completedAt,
|
|
4725
|
+
errorText: null,
|
|
4726
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
|
|
4727
|
+
});
|
|
4728
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
|
|
4729
|
+
emitRigEvent(state, {
|
|
4730
|
+
type: "rig.run.completed",
|
|
4731
|
+
aggregateId: runId,
|
|
4732
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
|
|
4733
|
+
createdAt: completedAt
|
|
4734
|
+
});
|
|
4735
|
+
return;
|
|
4736
|
+
}
|
|
4737
|
+
if (pr.status === "opened" && pr.prUrl) {
|
|
4738
|
+
const completedAt = new Date().toISOString();
|
|
4739
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4740
|
+
status: "completed",
|
|
4741
|
+
completedAt,
|
|
4742
|
+
errorText: null,
|
|
4743
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
|
|
4744
|
+
});
|
|
4745
|
+
appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
|
|
4746
|
+
emitRigEvent(state, {
|
|
4747
|
+
type: "rig.run.completed",
|
|
4748
|
+
aggregateId: runId,
|
|
4749
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
|
|
4750
|
+
createdAt: completedAt
|
|
4751
|
+
});
|
|
4752
|
+
return;
|
|
4753
|
+
}
|
|
4754
|
+
const detail = pr.actionableFeedback.join(`
|
|
4755
|
+
`) || "PR automation did not merge the PR.";
|
|
4756
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
|
|
4757
|
+
appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
|
|
4758
|
+
});
|
|
4759
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4760
|
+
status: "needs_attention",
|
|
4761
|
+
completedAt: new Date().toISOString(),
|
|
4762
|
+
errorText: detail,
|
|
4763
|
+
...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
|
|
4764
|
+
});
|
|
4765
|
+
appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
|
|
4766
|
+
emitRigEvent(state, {
|
|
4767
|
+
type: "rig.run.needs-attention",
|
|
4768
|
+
aggregateId: runId,
|
|
4769
|
+
payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
|
|
4770
|
+
});
|
|
4771
|
+
}
|
|
4419
4772
|
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
4420
4773
|
"completed",
|
|
4421
4774
|
"complete",
|
|
@@ -4624,6 +4977,7 @@ async function startLocalRun(state, runId, options) {
|
|
|
4624
4977
|
RIG_HOST_PROJECT_ROOT: cliProjectRoot,
|
|
4625
4978
|
RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
|
|
4626
4979
|
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
4980
|
+
RIG_SERVER_OWNS_CLOSEOUT: "1",
|
|
4627
4981
|
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
4628
4982
|
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
4629
4983
|
...bridgeGitHubToken ? {
|
|
@@ -4720,6 +5074,38 @@ ${sourceFailure}` });
|
|
|
4720
5074
|
agent: current.runtimeAdapter,
|
|
4721
5075
|
summary: failureSummary
|
|
4722
5076
|
});
|
|
5077
|
+
} else if (closeoutRecord(current)?.status === "pending") {
|
|
5078
|
+
try {
|
|
5079
|
+
await runServerOwnedPrCloseout(state, runId);
|
|
5080
|
+
} catch (closeoutError) {
|
|
5081
|
+
const closeoutFailure = closeoutError instanceof Error ? closeoutError.message : String(closeoutError);
|
|
5082
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
5083
|
+
status: "failed",
|
|
5084
|
+
completedAt: new Date().toISOString(),
|
|
5085
|
+
errorText: closeoutFailure,
|
|
5086
|
+
...closeoutPhasePatch("failed", "failed", { error: closeoutFailure })
|
|
5087
|
+
});
|
|
5088
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
5089
|
+
id: `log:${runId}:server-closeout-failed`,
|
|
5090
|
+
title: "Server-owned closeout failed",
|
|
5091
|
+
detail: closeoutFailure,
|
|
5092
|
+
tone: "error",
|
|
5093
|
+
status: "failed",
|
|
5094
|
+
createdAt: new Date().toISOString()
|
|
5095
|
+
}, "server-closeout-failed");
|
|
5096
|
+
if (current.taskId) {
|
|
5097
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: closeoutFailure }, "failed", "Rig server-owned closeout failed.", { errorText: closeoutFailure }).catch((error) => {
|
|
5098
|
+
appendRunLogEntry(state.projectRoot, runId, {
|
|
5099
|
+
id: `log:${runId}:task-source-closeout-failed-update`,
|
|
5100
|
+
title: "Task source closeout failure update failed",
|
|
5101
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
5102
|
+
tone: "error",
|
|
5103
|
+
status: "failed",
|
|
5104
|
+
createdAt: new Date().toISOString()
|
|
5105
|
+
});
|
|
5106
|
+
});
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
4723
5109
|
}
|
|
4724
5110
|
broadcastSnapshotInvalidation(state);
|
|
4725
5111
|
} catch (error) {
|
|
@@ -4806,6 +5192,12 @@ async function resumeRunRecord(state, input) {
|
|
|
4806
5192
|
if (run.status === "completed") {
|
|
4807
5193
|
throw new Error("Completed runs cannot be resumed.");
|
|
4808
5194
|
}
|
|
5195
|
+
const closeout = closeoutRecord(run);
|
|
5196
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
5197
|
+
if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
|
|
5198
|
+
await runServerOwnedPrCloseout(state, input.runId);
|
|
5199
|
+
return;
|
|
5200
|
+
}
|
|
4809
5201
|
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
|
|
4810
5202
|
}
|
|
4811
5203
|
function appendRunMessage(projectRoot, input) {
|
|
@@ -4890,11 +5282,45 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
|
|
|
4890
5282
|
writeQueueState(projectRoot, next);
|
|
4891
5283
|
return next;
|
|
4892
5284
|
}
|
|
4893
|
-
var
|
|
4894
|
-
|
|
5285
|
+
var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
|
|
5286
|
+
var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
5287
|
+
function processExists(pid) {
|
|
5288
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
5289
|
+
return false;
|
|
5290
|
+
try {
|
|
5291
|
+
process.kill(pid, 0);
|
|
5292
|
+
return true;
|
|
5293
|
+
} catch {
|
|
5294
|
+
return false;
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
function recoverStaleLocalRun(projectRoot, run) {
|
|
5298
|
+
const record = run;
|
|
5299
|
+
if (run.mode !== "local")
|
|
5300
|
+
return false;
|
|
5301
|
+
const status = normalizeString(record.status)?.toLowerCase() ?? "";
|
|
5302
|
+
if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
|
|
5303
|
+
return false;
|
|
5304
|
+
const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
|
|
5305
|
+
const childPid = typeof record.pid === "number" ? record.pid : null;
|
|
5306
|
+
if (serverPid === null && childPid === null)
|
|
5307
|
+
return false;
|
|
5308
|
+
const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
|
|
5309
|
+
if (hasLiveRecordedProcess && serverPid === process.pid)
|
|
5310
|
+
return false;
|
|
5311
|
+
const completedAt = new Date().toISOString();
|
|
5312
|
+
patchRunRecord(projectRoot, run.runId, {
|
|
5313
|
+
status: "failed",
|
|
5314
|
+
completedAt,
|
|
5315
|
+
errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
|
|
5316
|
+
});
|
|
5317
|
+
return true;
|
|
5318
|
+
}
|
|
5319
|
+
function collectResumableServerCloseouts(state, runs) {
|
|
4895
5320
|
return runs.filter((run) => {
|
|
4896
|
-
const
|
|
4897
|
-
|
|
5321
|
+
const closeout = closeoutRecord(run);
|
|
5322
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
5323
|
+
return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
|
|
4898
5324
|
});
|
|
4899
5325
|
}
|
|
4900
5326
|
async function reconcileScheduler(state, reason) {
|
|
@@ -4911,17 +5337,33 @@ async function reconcileScheduler(state, reason) {
|
|
|
4911
5337
|
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
4912
5338
|
let runs = listAuthorityRuns4(state.projectRoot);
|
|
4913
5339
|
let changed = false;
|
|
4914
|
-
const
|
|
4915
|
-
|
|
5340
|
+
for (const run of runs) {
|
|
5341
|
+
if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
|
|
5342
|
+
changed = true;
|
|
5343
|
+
}
|
|
5344
|
+
}
|
|
5345
|
+
if (changed) {
|
|
5346
|
+
runs = listAuthorityRuns4(state.projectRoot);
|
|
5347
|
+
}
|
|
5348
|
+
const resumableCloseouts = collectResumableServerCloseouts(state, runs);
|
|
5349
|
+
for (const run of resumableCloseouts) {
|
|
4916
5350
|
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
4917
|
-
id: `log:${run.runId}:auto-resume:${Date.now()}`,
|
|
4918
|
-
title: "
|
|
4919
|
-
detail: `Rig server recovered
|
|
5351
|
+
id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
|
|
5352
|
+
title: "Server-owned closeout auto-resume scheduled",
|
|
5353
|
+
detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
|
|
4920
5354
|
tone: "info",
|
|
4921
|
-
status: "
|
|
5355
|
+
status: "reviewing",
|
|
4922
5356
|
createdAt: new Date().toISOString()
|
|
4923
5357
|
});
|
|
4924
|
-
await
|
|
5358
|
+
await runServerOwnedPrCloseout(state, run.runId).catch((error) => {
|
|
5359
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
5360
|
+
patchRunRecord(state.projectRoot, run.runId, {
|
|
5361
|
+
status: "failed",
|
|
5362
|
+
completedAt: new Date().toISOString(),
|
|
5363
|
+
errorText: detail,
|
|
5364
|
+
...closeoutPhasePatch("failed", "failed", { error: detail })
|
|
5365
|
+
});
|
|
5366
|
+
});
|
|
4925
5367
|
changed = true;
|
|
4926
5368
|
}
|
|
4927
5369
|
if (changed) {
|
|
@@ -5717,6 +6159,75 @@ function buildProjectConfigStatus(root) {
|
|
|
5717
6159
|
suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
|
|
5718
6160
|
};
|
|
5719
6161
|
}
|
|
6162
|
+
var RIG_GITHUB_LIFECYCLE_LABELS = [
|
|
6163
|
+
"ready",
|
|
6164
|
+
"blocked",
|
|
6165
|
+
"in-progress",
|
|
6166
|
+
"under-review",
|
|
6167
|
+
"failed",
|
|
6168
|
+
"cancelled",
|
|
6169
|
+
"rig:running",
|
|
6170
|
+
"rig:pr-open",
|
|
6171
|
+
"rig:ci-fixing",
|
|
6172
|
+
"rig:merging",
|
|
6173
|
+
"rig:done",
|
|
6174
|
+
"rig:needs-attention"
|
|
6175
|
+
];
|
|
6176
|
+
function githubProjectsEnabled2(config) {
|
|
6177
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
6178
|
+
return false;
|
|
6179
|
+
const root = config;
|
|
6180
|
+
const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
|
|
6181
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
6182
|
+
return projects?.enabled === true;
|
|
6183
|
+
}
|
|
6184
|
+
function githubIssueSourceRepo(config) {
|
|
6185
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
6186
|
+
return null;
|
|
6187
|
+
const root = config;
|
|
6188
|
+
const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
|
|
6189
|
+
const owner = normalizeString(taskSource?.owner);
|
|
6190
|
+
const repo = normalizeString(taskSource?.repo);
|
|
6191
|
+
if (taskSource?.kind === "github-issues" && owner && repo)
|
|
6192
|
+
return { owner, repo };
|
|
6193
|
+
const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
|
|
6194
|
+
const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
|
|
6195
|
+
const match = slug?.match(/^([^/]+)\/([^/]+)$/);
|
|
6196
|
+
return match ? { owner: match[1], repo: match[2] } : null;
|
|
6197
|
+
}
|
|
6198
|
+
async function ensureGitHubLifecycleLabels(projectRoot, config) {
|
|
6199
|
+
const repo = githubIssueSourceRepo(config);
|
|
6200
|
+
if (!repo)
|
|
6201
|
+
return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
6202
|
+
const token = createGitHubAuthStore(projectRoot).readToken();
|
|
6203
|
+
if (!token)
|
|
6204
|
+
return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
6205
|
+
const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
|
|
6206
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
|
|
6207
|
+
});
|
|
6208
|
+
const existingJson = await existingResponse.json().catch(() => []);
|
|
6209
|
+
const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
|
|
6210
|
+
const created = [];
|
|
6211
|
+
const alreadyPresent = [];
|
|
6212
|
+
const failed = [];
|
|
6213
|
+
for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
|
|
6214
|
+
if (existing.has(label)) {
|
|
6215
|
+
alreadyPresent.push(label);
|
|
6216
|
+
continue;
|
|
6217
|
+
}
|
|
6218
|
+
const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
|
|
6219
|
+
method: "POST",
|
|
6220
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
|
|
6221
|
+
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" })
|
|
6222
|
+
});
|
|
6223
|
+
if (response.ok || response.status === 422) {
|
|
6224
|
+
(response.status === 422 ? alreadyPresent : created).push(label);
|
|
6225
|
+
} else {
|
|
6226
|
+
failed.push({ label, error: await response.text().catch(() => response.statusText) });
|
|
6227
|
+
}
|
|
6228
|
+
}
|
|
6229
|
+
return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
|
|
6230
|
+
}
|
|
5720
6231
|
function normalizeCommit(value) {
|
|
5721
6232
|
const raw = normalizeString(value);
|
|
5722
6233
|
return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
|
|
@@ -6115,15 +6626,33 @@ say "Installing @h-rig/cli@latest"
|
|
|
6115
6626
|
bun add -g @h-rig/cli@latest
|
|
6116
6627
|
|
|
6117
6628
|
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
6118
|
-
|
|
6629
|
+
BUN_RIG="$BUN_INSTALL/bin/rig"
|
|
6630
|
+
if [ ! -x "$BUN_RIG" ]; then
|
|
6631
|
+
printf 'rig-install: expected Bun global rig at %s but it was not executable.
|
|
6632
|
+
' "$BUN_RIG" >&2
|
|
6633
|
+
exit 1
|
|
6634
|
+
fi
|
|
6635
|
+
|
|
6636
|
+
USER_BIN="$HOME/.local/bin"
|
|
6637
|
+
mkdir -p "$USER_BIN"
|
|
6638
|
+
cat > "$USER_BIN/rig" <<'RIG_SHIM'
|
|
6639
|
+
#!/usr/bin/env bash
|
|
6640
|
+
set -euo pipefail
|
|
6641
|
+
exec "\${BUN_INSTALL:-$HOME/.bun}/bin/rig" "$@"
|
|
6642
|
+
RIG_SHIM
|
|
6643
|
+
chmod +x "$USER_BIN/rig"
|
|
6644
|
+
|
|
6645
|
+
export PATH="$USER_BIN:$BUN_INSTALL/bin:$PATH"
|
|
6646
|
+
if command -v hash >/dev/null 2>&1; then hash -r; fi
|
|
6119
6647
|
|
|
6120
6648
|
if ! command -v rig >/dev/null 2>&1; then
|
|
6121
|
-
printf 'rig-install: rig installed, but rig is not on PATH. Add %s/bin to PATH and retry.
|
|
6122
|
-
' "$BUN_INSTALL" >&2
|
|
6649
|
+
printf 'rig-install: rig installed, but rig is not on PATH. Add %s and %s/bin to PATH and retry.
|
|
6650
|
+
' "$USER_BIN" "$BUN_INSTALL" >&2
|
|
6123
6651
|
exit 1
|
|
6124
6652
|
fi
|
|
6125
6653
|
|
|
6126
6654
|
say "Verifying rig"
|
|
6655
|
+
"$BUN_RIG" --help >/dev/null
|
|
6127
6656
|
rig --help >/dev/null
|
|
6128
6657
|
say "Done. Run: rig --help"
|
|
6129
6658
|
`;
|
|
@@ -6862,16 +7391,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6862
7391
|
if (!source) {
|
|
6863
7392
|
return deps.badRequest("No task source is configured");
|
|
6864
7393
|
}
|
|
7394
|
+
if (!source.updateTask && !(update.status && source.updateStatus)) {
|
|
7395
|
+
return deps.badRequest("Configured task source does not support updates");
|
|
7396
|
+
}
|
|
6865
7397
|
const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
|
|
6866
7398
|
return;
|
|
6867
7399
|
}) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
|
|
6868
|
-
if (source.updateTask) {
|
|
6869
|
-
await source.updateTask(id, update);
|
|
6870
|
-
} else if (update.status && source.updateStatus) {
|
|
6871
|
-
await source.updateStatus(id, update.status);
|
|
6872
|
-
} else {
|
|
6873
|
-
return deps.badRequest("Configured task source does not support updates");
|
|
6874
|
-
}
|
|
6875
7400
|
const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
|
|
6876
7401
|
const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
|
|
6877
7402
|
taskId: id,
|
|
@@ -6880,6 +7405,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6880
7405
|
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
6881
7406
|
config: ctx?.config
|
|
6882
7407
|
}).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
|
|
7408
|
+
if (update.status && githubProjectsEnabled2(ctx?.config) && projectSync.synced === false) {
|
|
7409
|
+
return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
|
|
7410
|
+
}
|
|
7411
|
+
try {
|
|
7412
|
+
if (source.updateTask) {
|
|
7413
|
+
await source.updateTask(id, update);
|
|
7414
|
+
} else if (update.status && source.updateStatus) {
|
|
7415
|
+
await source.updateStatus(id, update.status);
|
|
7416
|
+
}
|
|
7417
|
+
} catch (error) {
|
|
7418
|
+
let rollback = null;
|
|
7419
|
+
const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
|
|
7420
|
+
if (update.status && previousStatus && githubProjectsEnabled2(ctx?.config) && projectSync.synced !== false) {
|
|
7421
|
+
rollback = await syncGitHubProjectStatusForTaskUpdate({
|
|
7422
|
+
taskId: id,
|
|
7423
|
+
status: previousStatus,
|
|
7424
|
+
issueNodeId,
|
|
7425
|
+
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
7426
|
+
config: ctx?.config
|
|
7427
|
+
}).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
|
|
7428
|
+
}
|
|
7429
|
+
return deps.jsonResponse({
|
|
7430
|
+
ok: false,
|
|
7431
|
+
id,
|
|
7432
|
+
projectSync,
|
|
7433
|
+
rollback,
|
|
7434
|
+
error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
7435
|
+
}, 502);
|
|
7436
|
+
}
|
|
6883
7437
|
deps.snapshotService.invalidate("github-issue-updated");
|
|
6884
7438
|
await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
|
|
6885
7439
|
return;
|
|
@@ -6888,26 +7442,41 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6888
7442
|
return deps.jsonResponse({ ok: true, id, projectSync });
|
|
6889
7443
|
}
|
|
6890
7444
|
if (url.pathname === "/api/workspace/task-labels") {
|
|
7445
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
|
|
7446
|
+
if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
|
|
7447
|
+
return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
|
|
7448
|
+
}
|
|
6891
7449
|
return deps.jsonResponse({
|
|
6892
7450
|
ok: true,
|
|
6893
7451
|
ready: true,
|
|
6894
7452
|
labelsReady: true,
|
|
6895
|
-
labels: [
|
|
6896
|
-
|
|
6897
|
-
"blocked",
|
|
6898
|
-
"in-progress",
|
|
6899
|
-
"under-review",
|
|
6900
|
-
"failed",
|
|
6901
|
-
"cancelled",
|
|
6902
|
-
"rig:running",
|
|
6903
|
-
"rig:pr-open",
|
|
6904
|
-
"rig:ci-fixing",
|
|
6905
|
-
"rig:done",
|
|
6906
|
-
"rig:needs-attention"
|
|
6907
|
-
],
|
|
6908
|
-
note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
|
|
7453
|
+
labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
|
|
7454
|
+
note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
|
|
6909
7455
|
});
|
|
6910
7456
|
}
|
|
7457
|
+
if (url.pathname === "/api/github/projects" && req.method === "GET") {
|
|
7458
|
+
const owner = normalizeString(url.searchParams.get("owner"));
|
|
7459
|
+
if (!owner)
|
|
7460
|
+
return deps.badRequest("owner is required");
|
|
7461
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
7462
|
+
if (!token)
|
|
7463
|
+
return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
|
|
7464
|
+
const projects = await listGitHubProjects({ owner, token }).catch((error) => {
|
|
7465
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
7466
|
+
});
|
|
7467
|
+
return deps.jsonResponse({ ok: true, projects });
|
|
7468
|
+
}
|
|
7469
|
+
const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
|
|
7470
|
+
if (projectStatusMatch && req.method === "GET") {
|
|
7471
|
+
const projectId = decodeURIComponent(projectStatusMatch[1]);
|
|
7472
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
7473
|
+
if (!token)
|
|
7474
|
+
return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
|
|
7475
|
+
const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
|
|
7476
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
7477
|
+
});
|
|
7478
|
+
return deps.jsonResponse({ ok: true, field });
|
|
7479
|
+
}
|
|
6911
7480
|
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
6912
7481
|
const body = await deps.readJsonBody(req);
|
|
6913
7482
|
const ids = uniqueStringList(body.ids ?? body.id);
|
|
@@ -8072,6 +8641,69 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
8072
8641
|
}
|
|
8073
8642
|
const run = leaseValidation.run;
|
|
8074
8643
|
const completedAt = new Date().toISOString();
|
|
8644
|
+
const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
|
|
8645
|
+
if (run.taskId && workspaceDir) {
|
|
8646
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
8647
|
+
status: "reviewing",
|
|
8648
|
+
completedAt: null,
|
|
8649
|
+
hostId,
|
|
8650
|
+
endpointId: leaseId,
|
|
8651
|
+
worktreePath: workspaceDir,
|
|
8652
|
+
serverCloseout: {
|
|
8653
|
+
status: "pending",
|
|
8654
|
+
phase: "queued",
|
|
8655
|
+
requestedAt: completedAt,
|
|
8656
|
+
updatedAt: completedAt,
|
|
8657
|
+
runtimeWorkspace: workspaceDir,
|
|
8658
|
+
branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
|
|
8659
|
+
taskId: run.taskId,
|
|
8660
|
+
source: "remote-complete"
|
|
8661
|
+
}
|
|
8662
|
+
});
|
|
8663
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
8664
|
+
id: `log:${runId}:remote-server-closeout-requested`,
|
|
8665
|
+
title: "Server-owned closeout requested",
|
|
8666
|
+
detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
|
|
8667
|
+
tone: "info",
|
|
8668
|
+
status: "reviewing",
|
|
8669
|
+
createdAt: completedAt,
|
|
8670
|
+
payload: { workspaceDir, hostId, leaseId }
|
|
8671
|
+
}, "remote-server-closeout-requested");
|
|
8672
|
+
deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
|
|
8673
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8674
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
8675
|
+
status: "failed",
|
|
8676
|
+
completedAt: new Date().toISOString(),
|
|
8677
|
+
errorText: detail,
|
|
8678
|
+
serverCloseout: {
|
|
8679
|
+
status: "failed",
|
|
8680
|
+
phase: "failed",
|
|
8681
|
+
updatedAt: new Date().toISOString(),
|
|
8682
|
+
error: detail
|
|
8683
|
+
}
|
|
8684
|
+
});
|
|
8685
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
8686
|
+
id: `log:${runId}:remote-server-closeout-failed`,
|
|
8687
|
+
title: "Server-owned closeout failed",
|
|
8688
|
+
detail,
|
|
8689
|
+
tone: "error",
|
|
8690
|
+
status: "failed",
|
|
8691
|
+
createdAt: new Date().toISOString()
|
|
8692
|
+
}, "remote-server-closeout-failed");
|
|
8693
|
+
}).finally(() => {
|
|
8694
|
+
deps.reconcileScheduler(state, "remote-server-closeout-terminal");
|
|
8695
|
+
});
|
|
8696
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
8697
|
+
return deps.jsonResponse({
|
|
8698
|
+
ok: true,
|
|
8699
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
8700
|
+
hostId,
|
|
8701
|
+
runId,
|
|
8702
|
+
leaseId,
|
|
8703
|
+
closeout: "server-owned",
|
|
8704
|
+
acceptedAt: new Date().toISOString()
|
|
8705
|
+
});
|
|
8706
|
+
}
|
|
8075
8707
|
patchRunRecord(state.projectRoot, runId, {
|
|
8076
8708
|
status: "completed",
|
|
8077
8709
|
completedAt,
|
|
@@ -8214,6 +8846,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
8214
8846
|
const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
|
|
8215
8847
|
return deps.jsonResponse(page);
|
|
8216
8848
|
}
|
|
8849
|
+
const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
|
|
8850
|
+
if (runTimelineMatch) {
|
|
8851
|
+
const runId = decodeURIComponent(runTimelineMatch[1]);
|
|
8852
|
+
const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
|
|
8853
|
+
const cursor = normalizeString(url.searchParams.get("cursor"));
|
|
8854
|
+
const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
|
|
8855
|
+
return deps.jsonResponse(page);
|
|
8856
|
+
}
|
|
8217
8857
|
const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
|
|
8218
8858
|
if (runSteerMatch && req.method === "POST") {
|
|
8219
8859
|
const runId = decodeURIComponent(runSteerMatch[1]);
|
|
@@ -13430,6 +14070,7 @@ function buildHttpRouterDeps(state) {
|
|
|
13430
14070
|
startLocalRun,
|
|
13431
14071
|
stopRunRecord,
|
|
13432
14072
|
resumeRunRecord,
|
|
14073
|
+
runServerOwnedPrCloseout,
|
|
13433
14074
|
claimRemoteRun,
|
|
13434
14075
|
listRemoteRunArtifacts,
|
|
13435
14076
|
broadcastSnapshotInvalidation,
|
|
@@ -13754,6 +14395,7 @@ export {
|
|
|
13754
14395
|
resolveRigServerPaths,
|
|
13755
14396
|
resolveRigProjectRoot,
|
|
13756
14397
|
resolvePublishedRigServerStatePath,
|
|
14398
|
+
resolveProjectStatusField,
|
|
13757
14399
|
resolveProjectRoot,
|
|
13758
14400
|
registerRemoteHost,
|
|
13759
14401
|
readWorkspaceTasks,
|
|
@@ -13762,6 +14404,7 @@ export {
|
|
|
13762
14404
|
parseRigServerArgs,
|
|
13763
14405
|
parseArgs,
|
|
13764
14406
|
main,
|
|
14407
|
+
listGitHubProjects,
|
|
13765
14408
|
heartbeatRemoteHost,
|
|
13766
14409
|
handleWebSocketUpgrade,
|
|
13767
14410
|
encodeWebSocketPayload,
|