@h-rig/server 0.0.6-alpha.12 → 0.0.6-alpha.14
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 +678 -53
- package/dist/src/server-helpers/github-project-status-sync.js +3 -0
- package/dist/src/server-helpers/http-router.js +252 -22
- 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 +676 -53
- package/package.json +4 -4
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
buildTaskRunLifecycleComment as buildTaskRunLifecycleComment2,
|
|
17
17
|
updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
|
|
18
18
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
19
|
+
import {
|
|
20
|
+
closeIssueAfterMergedPr,
|
|
21
|
+
commitRunChanges,
|
|
22
|
+
runPrAutomation
|
|
23
|
+
} from "@rig/runtime/control-plane/native/pr-automation";
|
|
19
24
|
|
|
20
25
|
// packages/server/src/scheduler.ts
|
|
21
26
|
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -180,6 +185,9 @@ import {
|
|
|
180
185
|
readJsonlFile,
|
|
181
186
|
resolveAuthorityRunDir
|
|
182
187
|
} from "@rig/runtime/control-plane/authority-files";
|
|
188
|
+
function runTimelinePath(projectRoot, runId) {
|
|
189
|
+
return resolve2(resolveAuthorityRunDir(projectRoot, runId), "timeline.jsonl");
|
|
190
|
+
}
|
|
183
191
|
function runLogsPath(projectRoot, runId) {
|
|
184
192
|
return resolve2(resolveAuthorityRunDir(projectRoot, runId), "logs.jsonl");
|
|
185
193
|
}
|
|
@@ -327,6 +335,13 @@ import {
|
|
|
327
335
|
resolveAuthorityRunDir as resolveAuthorityRunDir2,
|
|
328
336
|
writeJsonFile as writeJsonFile2
|
|
329
337
|
} from "@rig/runtime/control-plane/authority-files";
|
|
338
|
+
function appendRunTimelineEntry(projectRoot, runId, value) {
|
|
339
|
+
if (!readAuthorityRun3(projectRoot, runId)) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
appendJsonlRecord(runTimelinePath(projectRoot, runId), value);
|
|
343
|
+
patchRunRecord(projectRoot, runId, {});
|
|
344
|
+
}
|
|
330
345
|
function appendRunLogEntry(projectRoot, runId, value) {
|
|
331
346
|
if (!readAuthorityRun3(projectRoot, runId)) {
|
|
332
347
|
return;
|
|
@@ -353,8 +368,7 @@ function buildRunStartPatch(startedAt) {
|
|
|
353
368
|
status: "preparing",
|
|
354
369
|
startedAt,
|
|
355
370
|
completedAt: null,
|
|
356
|
-
errorText: null
|
|
357
|
-
serverPid: process.pid
|
|
371
|
+
errorText: null
|
|
358
372
|
};
|
|
359
373
|
}
|
|
360
374
|
|
|
@@ -812,6 +826,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
812
826
|
running: "In Progress",
|
|
813
827
|
prOpen: "In Review",
|
|
814
828
|
ciFixing: "In Review",
|
|
829
|
+
merging: "Merging",
|
|
815
830
|
done: "Done",
|
|
816
831
|
needsAttention: "Needs Attention"
|
|
817
832
|
};
|
|
@@ -825,6 +840,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
825
840
|
return "prOpen";
|
|
826
841
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
827
842
|
return "ciFixing";
|
|
843
|
+
if (normalized === "merging" || normalized === "merge")
|
|
844
|
+
return "merging";
|
|
828
845
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
829
846
|
return "needsAttention";
|
|
830
847
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -1228,9 +1245,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
|
|
|
1228
1245
|
return null;
|
|
1229
1246
|
return null;
|
|
1230
1247
|
}
|
|
1248
|
+
function githubProjectsEnabled(config) {
|
|
1249
|
+
const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
|
|
1250
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
1251
|
+
return projects?.enabled === true;
|
|
1252
|
+
}
|
|
1231
1253
|
async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
|
|
1232
1254
|
if (!run.taskId)
|
|
1233
|
-
return;
|
|
1255
|
+
return false;
|
|
1234
1256
|
const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
|
|
1235
1257
|
try {
|
|
1236
1258
|
const result = await syncGitHubProjectStatusForTaskUpdate({
|
|
@@ -1241,28 +1263,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
|
|
|
1241
1263
|
config
|
|
1242
1264
|
});
|
|
1243
1265
|
if (!result.synced && result.reason !== "project-sync-disabled") {
|
|
1266
|
+
const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
|
|
1244
1267
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
1245
1268
|
id: `log:${run.runId}:github-project-sync:${status}`,
|
|
1246
1269
|
title: "GitHub Project sync skipped",
|
|
1247
|
-
detail
|
|
1270
|
+
detail,
|
|
1248
1271
|
tone: "warn",
|
|
1249
1272
|
status: "running",
|
|
1250
1273
|
createdAt: new Date().toISOString(),
|
|
1251
1274
|
payload: { reason: result.reason, issueNodeId }
|
|
1252
1275
|
});
|
|
1276
|
+
if (githubProjectsEnabled(config)) {
|
|
1277
|
+
throw new Error(detail);
|
|
1278
|
+
}
|
|
1279
|
+
return false;
|
|
1253
1280
|
}
|
|
1281
|
+
return result.synced === true;
|
|
1254
1282
|
} catch (error) {
|
|
1283
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1255
1284
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
1256
1285
|
id: `log:${run.runId}:github-project-sync-error:${status}`,
|
|
1257
1286
|
title: "GitHub Project sync failed",
|
|
1258
|
-
detail
|
|
1287
|
+
detail,
|
|
1259
1288
|
tone: "error",
|
|
1260
1289
|
status: "running",
|
|
1261
1290
|
createdAt: new Date().toISOString(),
|
|
1262
1291
|
payload: { issueNodeId }
|
|
1263
1292
|
});
|
|
1293
|
+
if (githubProjectsEnabled(config)) {
|
|
1294
|
+
throw new Error(detail);
|
|
1295
|
+
}
|
|
1296
|
+
return false;
|
|
1264
1297
|
}
|
|
1265
1298
|
}
|
|
1299
|
+
function createCommandRunner(binary, extraEnv = {}) {
|
|
1300
|
+
return async (args, options) => {
|
|
1301
|
+
const child = spawn(binary, [...args], {
|
|
1302
|
+
cwd: options?.cwd,
|
|
1303
|
+
env: { ...process.env, ...extraEnv },
|
|
1304
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1305
|
+
});
|
|
1306
|
+
const stdoutChunks = [];
|
|
1307
|
+
const stderrChunks = [];
|
|
1308
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
1309
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
1310
|
+
const exitCode = await new Promise((resolve11) => {
|
|
1311
|
+
child.once("error", () => resolve11(1));
|
|
1312
|
+
child.once("close", (code) => resolve11(code ?? 1));
|
|
1313
|
+
});
|
|
1314
|
+
return {
|
|
1315
|
+
exitCode,
|
|
1316
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
1317
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8")
|
|
1318
|
+
};
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function closeoutRecord(run) {
|
|
1322
|
+
const value = run.serverCloseout;
|
|
1323
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1324
|
+
}
|
|
1325
|
+
function closeoutPhasePatch(phase, status, extra = {}) {
|
|
1326
|
+
const updatedAt = new Date().toISOString();
|
|
1327
|
+
return {
|
|
1328
|
+
serverCloseout: {
|
|
1329
|
+
phase,
|
|
1330
|
+
status,
|
|
1331
|
+
updatedAt,
|
|
1332
|
+
...extra
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
|
|
1337
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
1338
|
+
id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
|
|
1339
|
+
title: `Server closeout: ${phase}`,
|
|
1340
|
+
detail,
|
|
1341
|
+
tone,
|
|
1342
|
+
status,
|
|
1343
|
+
createdAt: new Date().toISOString()
|
|
1344
|
+
}, `server-closeout-${phase}`);
|
|
1345
|
+
}
|
|
1266
1346
|
async function autoAssignRunIssue(projectRoot, run) {
|
|
1267
1347
|
if (!run.taskId)
|
|
1268
1348
|
return;
|
|
@@ -1292,7 +1372,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
1292
1372
|
return;
|
|
1293
1373
|
}
|
|
1294
1374
|
const config = await loadRigLifecycleConfig(projectRoot);
|
|
1295
|
-
await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
1375
|
+
const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
1296
1376
|
if (status === "in_progress") {
|
|
1297
1377
|
await autoAssignRunIssue(projectRoot, run);
|
|
1298
1378
|
}
|
|
@@ -1308,24 +1388,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
1308
1388
|
});
|
|
1309
1389
|
return;
|
|
1310
1390
|
}
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1391
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
1392
|
+
const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
|
|
1393
|
+
const rollbackProjectSync = async () => {
|
|
1394
|
+
if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
|
|
1395
|
+
return;
|
|
1396
|
+
await syncGitHubProjectStatusForTaskUpdate({
|
|
1397
|
+
taskId: run.taskId,
|
|
1398
|
+
status: previousStatus,
|
|
1399
|
+
issueNodeId: extractGitHubIssueNodeId(sourceTask),
|
|
1400
|
+
token: createGitHubAuthStore(projectRoot).readToken(),
|
|
1401
|
+
config
|
|
1402
|
+
}).catch((rollbackError) => {
|
|
1403
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
1404
|
+
id: `log:${run.runId}:github-project-sync-rollback:${status}`,
|
|
1405
|
+
title: "GitHub Project sync rollback failed",
|
|
1406
|
+
detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
|
|
1407
|
+
tone: "error",
|
|
1408
|
+
status: "running",
|
|
1409
|
+
createdAt: new Date().toISOString()
|
|
1410
|
+
});
|
|
1411
|
+
});
|
|
1412
|
+
};
|
|
1413
|
+
let result;
|
|
1414
|
+
try {
|
|
1415
|
+
result = await updateConfiguredTaskSourceTask2(projectRoot, {
|
|
1416
|
+
taskId: run.taskId,
|
|
1417
|
+
sourceTask,
|
|
1418
|
+
update: {
|
|
1318
1419
|
status,
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1420
|
+
comment: buildTaskRunLifecycleComment2({
|
|
1421
|
+
runId: run.runId,
|
|
1422
|
+
status,
|
|
1423
|
+
summary,
|
|
1424
|
+
runtimeWorkspace: normalizeString(run.worktreePath),
|
|
1425
|
+
logsDir: normalizeString(run.logRoot),
|
|
1426
|
+
sessionDir: normalizeString(run.sessionPath),
|
|
1427
|
+
errorText: options.errorText ?? normalizeString(run.errorText)
|
|
1428
|
+
})
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
await rollbackProjectSync();
|
|
1433
|
+
throw error;
|
|
1434
|
+
}
|
|
1327
1435
|
if (!result.updated) {
|
|
1328
1436
|
if (result.source === "plugin" || result.sourceKind) {
|
|
1437
|
+
await rollbackProjectSync();
|
|
1329
1438
|
throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
|
|
1330
1439
|
}
|
|
1331
1440
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
@@ -1338,6 +1447,219 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
1338
1447
|
});
|
|
1339
1448
|
}
|
|
1340
1449
|
}
|
|
1450
|
+
async function runServerOwnedPrCloseout(state, runId) {
|
|
1451
|
+
const run = readAuthorityRun8(state.projectRoot, runId);
|
|
1452
|
+
if (!run)
|
|
1453
|
+
throw new Error(`Run not found: ${runId}`);
|
|
1454
|
+
const closeout = closeoutRecord(run);
|
|
1455
|
+
if (!closeout)
|
|
1456
|
+
return;
|
|
1457
|
+
const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
|
|
1458
|
+
if (!taskId)
|
|
1459
|
+
throw new Error("Server-owned closeout requires a task id.");
|
|
1460
|
+
const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
|
|
1461
|
+
const branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
|
|
1462
|
+
const config = await loadRigLifecycleConfig(state.projectRoot);
|
|
1463
|
+
const runPrMode = normalizeString(run.prMode);
|
|
1464
|
+
const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
|
|
1465
|
+
const effectiveConfig = {
|
|
1466
|
+
...config ?? {},
|
|
1467
|
+
pr: {
|
|
1468
|
+
...config?.pr ?? {},
|
|
1469
|
+
mode: prMode,
|
|
1470
|
+
autoFixChecks: false,
|
|
1471
|
+
autoFixReview: false
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
const readCurrentRun = () => readAuthorityRun8(state.projectRoot, runId) ?? run;
|
|
1475
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
1476
|
+
const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
|
|
1477
|
+
const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
|
|
1478
|
+
const closeoutPrUrl = normalizeString(closeout.prUrl);
|
|
1479
|
+
if (closeoutPhase === "completed" || closeoutStatus === "completed") {
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
if (closeoutPhase === "close-source" && closeoutPrUrl) {
|
|
1483
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1484
|
+
status: "reviewing",
|
|
1485
|
+
...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
|
|
1486
|
+
});
|
|
1487
|
+
await closeIssueAfterMergedPr({
|
|
1488
|
+
projectRoot: state.projectRoot,
|
|
1489
|
+
taskId,
|
|
1490
|
+
runId,
|
|
1491
|
+
prUrl: closeoutPrUrl,
|
|
1492
|
+
sourceTask,
|
|
1493
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
1494
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
1495
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
const completedAt = new Date().toISOString();
|
|
1499
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1500
|
+
status: "completed",
|
|
1501
|
+
completedAt,
|
|
1502
|
+
errorText: null,
|
|
1503
|
+
...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
|
|
1504
|
+
});
|
|
1505
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
|
|
1506
|
+
emitRigEvent(state, {
|
|
1507
|
+
type: "rig.run.completed",
|
|
1508
|
+
aggregateId: runId,
|
|
1509
|
+
payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
|
|
1510
|
+
createdAt: completedAt
|
|
1511
|
+
});
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (prMode === "off" || prMode === "ask") {
|
|
1515
|
+
const completedAt = new Date().toISOString();
|
|
1516
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1517
|
+
status: "completed",
|
|
1518
|
+
completedAt,
|
|
1519
|
+
errorText: null,
|
|
1520
|
+
...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
|
|
1521
|
+
});
|
|
1522
|
+
appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
|
|
1523
|
+
emitRigEvent(state, {
|
|
1524
|
+
type: "rig.run.completed",
|
|
1525
|
+
aggregateId: runId,
|
|
1526
|
+
payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
|
|
1527
|
+
createdAt: completedAt
|
|
1528
|
+
});
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
|
|
1532
|
+
const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
|
|
1533
|
+
const gitCommand = createCommandRunner("git", githubEnv);
|
|
1534
|
+
const ghCommand = createCommandRunner("gh", githubEnv);
|
|
1535
|
+
const setCloseout = (phase, status, extra = {}) => {
|
|
1536
|
+
const previous = closeoutRecord(readCurrentRun()) ?? closeout;
|
|
1537
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1538
|
+
status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
|
|
1539
|
+
...closeoutPhasePatch(phase, status, { ...previous, ...extra })
|
|
1540
|
+
});
|
|
1541
|
+
};
|
|
1542
|
+
setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
1543
|
+
appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
|
|
1544
|
+
const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
|
|
1545
|
+
appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
|
|
1546
|
+
setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
1547
|
+
const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
|
|
1548
|
+
if (push.exitCode !== 0) {
|
|
1549
|
+
throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
|
|
1550
|
+
}
|
|
1551
|
+
const sourceTaskForPr = {
|
|
1552
|
+
title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
|
|
1553
|
+
};
|
|
1554
|
+
const artifactRoot = resolve10(state.projectRoot, "artifacts", taskId);
|
|
1555
|
+
setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
1556
|
+
const pr = await runPrAutomation({
|
|
1557
|
+
projectRoot: workspace,
|
|
1558
|
+
taskId,
|
|
1559
|
+
runId,
|
|
1560
|
+
branch,
|
|
1561
|
+
config: effectiveConfig,
|
|
1562
|
+
sourceTask: sourceTaskForPr,
|
|
1563
|
+
artifactRoot,
|
|
1564
|
+
command: ghCommand,
|
|
1565
|
+
gitCommand,
|
|
1566
|
+
steerPi: async (message) => {
|
|
1567
|
+
appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
|
|
1568
|
+
appendRunTimelineEntry(state.projectRoot, runId, {
|
|
1569
|
+
id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
|
|
1570
|
+
type: "user_message",
|
|
1571
|
+
text: message,
|
|
1572
|
+
createdAt: new Date().toISOString(),
|
|
1573
|
+
state: "completed"
|
|
1574
|
+
});
|
|
1575
|
+
},
|
|
1576
|
+
lifecycle: {
|
|
1577
|
+
onPrOpened: async ({ prUrl }) => {
|
|
1578
|
+
setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
1579
|
+
appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
|
|
1580
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
|
|
1581
|
+
},
|
|
1582
|
+
onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
|
|
1583
|
+
onFeedback: async ({ feedback }) => {
|
|
1584
|
+
appendCloseoutStage(state, runId, "feedback", feedback.join(`
|
|
1585
|
+
`), "reviewing", "error");
|
|
1586
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
|
|
1587
|
+
},
|
|
1588
|
+
onMergeStarted: async ({ prUrl }) => {
|
|
1589
|
+
setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
1590
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
1591
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
|
|
1592
|
+
},
|
|
1593
|
+
onMerged: ({ prUrl }) => {
|
|
1594
|
+
setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
|
|
1595
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
if (pr.status === "merged" && pr.prUrl) {
|
|
1600
|
+
setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
|
|
1601
|
+
await closeIssueAfterMergedPr({
|
|
1602
|
+
projectRoot: state.projectRoot,
|
|
1603
|
+
taskId,
|
|
1604
|
+
runId,
|
|
1605
|
+
prUrl: pr.prUrl,
|
|
1606
|
+
sourceTask,
|
|
1607
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
1608
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
1609
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
const completedAt = new Date().toISOString();
|
|
1613
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1614
|
+
status: "completed",
|
|
1615
|
+
completedAt,
|
|
1616
|
+
errorText: null,
|
|
1617
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
|
|
1618
|
+
});
|
|
1619
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
|
|
1620
|
+
emitRigEvent(state, {
|
|
1621
|
+
type: "rig.run.completed",
|
|
1622
|
+
aggregateId: runId,
|
|
1623
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
|
|
1624
|
+
createdAt: completedAt
|
|
1625
|
+
});
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
if (pr.status === "opened" && pr.prUrl) {
|
|
1629
|
+
const completedAt = new Date().toISOString();
|
|
1630
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1631
|
+
status: "completed",
|
|
1632
|
+
completedAt,
|
|
1633
|
+
errorText: null,
|
|
1634
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
|
|
1635
|
+
});
|
|
1636
|
+
appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
|
|
1637
|
+
emitRigEvent(state, {
|
|
1638
|
+
type: "rig.run.completed",
|
|
1639
|
+
aggregateId: runId,
|
|
1640
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
|
|
1641
|
+
createdAt: completedAt
|
|
1642
|
+
});
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
const detail = pr.actionableFeedback.join(`
|
|
1646
|
+
`) || "PR automation did not merge the PR.";
|
|
1647
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
|
|
1648
|
+
appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
|
|
1649
|
+
});
|
|
1650
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1651
|
+
status: "needs_attention",
|
|
1652
|
+
completedAt: new Date().toISOString(),
|
|
1653
|
+
errorText: detail,
|
|
1654
|
+
...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
|
|
1655
|
+
});
|
|
1656
|
+
appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
|
|
1657
|
+
emitRigEvent(state, {
|
|
1658
|
+
type: "rig.run.needs-attention",
|
|
1659
|
+
aggregateId: runId,
|
|
1660
|
+
payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1341
1663
|
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
1342
1664
|
"completed",
|
|
1343
1665
|
"complete",
|
|
@@ -1546,6 +1868,7 @@ async function startLocalRun(state, runId, options) {
|
|
|
1546
1868
|
RIG_HOST_PROJECT_ROOT: cliProjectRoot,
|
|
1547
1869
|
RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
|
|
1548
1870
|
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
1871
|
+
RIG_SERVER_OWNS_CLOSEOUT: "1",
|
|
1549
1872
|
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
1550
1873
|
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
1551
1874
|
...bridgeGitHubToken ? {
|
|
@@ -1642,6 +1965,38 @@ ${sourceFailure}` });
|
|
|
1642
1965
|
agent: current.runtimeAdapter,
|
|
1643
1966
|
summary: failureSummary
|
|
1644
1967
|
});
|
|
1968
|
+
} else if (closeoutRecord(current)?.status === "pending") {
|
|
1969
|
+
try {
|
|
1970
|
+
await runServerOwnedPrCloseout(state, runId);
|
|
1971
|
+
} catch (closeoutError) {
|
|
1972
|
+
const closeoutFailure = closeoutError instanceof Error ? closeoutError.message : String(closeoutError);
|
|
1973
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1974
|
+
status: "failed",
|
|
1975
|
+
completedAt: new Date().toISOString(),
|
|
1976
|
+
errorText: closeoutFailure,
|
|
1977
|
+
...closeoutPhasePatch("failed", "failed", { error: closeoutFailure })
|
|
1978
|
+
});
|
|
1979
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
1980
|
+
id: `log:${runId}:server-closeout-failed`,
|
|
1981
|
+
title: "Server-owned closeout failed",
|
|
1982
|
+
detail: closeoutFailure,
|
|
1983
|
+
tone: "error",
|
|
1984
|
+
status: "failed",
|
|
1985
|
+
createdAt: new Date().toISOString()
|
|
1986
|
+
}, "server-closeout-failed");
|
|
1987
|
+
if (current.taskId) {
|
|
1988
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: closeoutFailure }, "failed", "Rig server-owned closeout failed.", { errorText: closeoutFailure }).catch((error) => {
|
|
1989
|
+
appendRunLogEntry(state.projectRoot, runId, {
|
|
1990
|
+
id: `log:${runId}:task-source-closeout-failed-update`,
|
|
1991
|
+
title: "Task source closeout failure update failed",
|
|
1992
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
1993
|
+
tone: "error",
|
|
1994
|
+
status: "failed",
|
|
1995
|
+
createdAt: new Date().toISOString()
|
|
1996
|
+
});
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1645
2000
|
}
|
|
1646
2001
|
broadcastSnapshotInvalidation(state);
|
|
1647
2002
|
} catch (error) {
|
|
@@ -1728,6 +2083,12 @@ async function resumeRunRecord(state, input) {
|
|
|
1728
2083
|
if (run.status === "completed") {
|
|
1729
2084
|
throw new Error("Completed runs cannot be resumed.");
|
|
1730
2085
|
}
|
|
2086
|
+
const closeout = closeoutRecord(run);
|
|
2087
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
2088
|
+
if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
|
|
2089
|
+
await runServerOwnedPrCloseout(state, input.runId);
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
1731
2092
|
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
|
|
1732
2093
|
}
|
|
1733
2094
|
function appendRunMessage(projectRoot, input) {
|
|
@@ -1812,11 +2173,45 @@ function removeTaskIdsFromQueueState2(projectRoot, taskIds) {
|
|
|
1812
2173
|
writeQueueState(projectRoot, next);
|
|
1813
2174
|
return next;
|
|
1814
2175
|
}
|
|
1815
|
-
var
|
|
1816
|
-
|
|
2176
|
+
var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
|
|
2177
|
+
var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
2178
|
+
function processExists(pid) {
|
|
2179
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
2180
|
+
return false;
|
|
2181
|
+
try {
|
|
2182
|
+
process.kill(pid, 0);
|
|
2183
|
+
return true;
|
|
2184
|
+
} catch {
|
|
2185
|
+
return false;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
function recoverStaleLocalRun(projectRoot, run) {
|
|
2189
|
+
const record = run;
|
|
2190
|
+
if (run.mode !== "local")
|
|
2191
|
+
return false;
|
|
2192
|
+
const status = normalizeString(record.status)?.toLowerCase() ?? "";
|
|
2193
|
+
if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
|
|
2194
|
+
return false;
|
|
2195
|
+
const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
|
|
2196
|
+
const childPid = typeof record.pid === "number" ? record.pid : null;
|
|
2197
|
+
if (serverPid === null && childPid === null)
|
|
2198
|
+
return false;
|
|
2199
|
+
const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
|
|
2200
|
+
if (hasLiveRecordedProcess && serverPid === process.pid)
|
|
2201
|
+
return false;
|
|
2202
|
+
const completedAt = new Date().toISOString();
|
|
2203
|
+
patchRunRecord(projectRoot, run.runId, {
|
|
2204
|
+
status: "failed",
|
|
2205
|
+
completedAt,
|
|
2206
|
+
errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
|
|
2207
|
+
});
|
|
2208
|
+
return true;
|
|
2209
|
+
}
|
|
2210
|
+
function collectResumableServerCloseouts(state, runs) {
|
|
1817
2211
|
return runs.filter((run) => {
|
|
1818
|
-
const
|
|
1819
|
-
|
|
2212
|
+
const closeout = closeoutRecord(run);
|
|
2213
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
2214
|
+
return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
|
|
1820
2215
|
});
|
|
1821
2216
|
}
|
|
1822
2217
|
async function reconcileScheduler(state, reason) {
|
|
@@ -1833,17 +2228,33 @@ async function reconcileScheduler(state, reason) {
|
|
|
1833
2228
|
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
1834
2229
|
let runs = listAuthorityRuns7(state.projectRoot);
|
|
1835
2230
|
let changed = false;
|
|
1836
|
-
const
|
|
1837
|
-
|
|
2231
|
+
for (const run of runs) {
|
|
2232
|
+
if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
|
|
2233
|
+
changed = true;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
if (changed) {
|
|
2237
|
+
runs = listAuthorityRuns7(state.projectRoot);
|
|
2238
|
+
}
|
|
2239
|
+
const resumableCloseouts = collectResumableServerCloseouts(state, runs);
|
|
2240
|
+
for (const run of resumableCloseouts) {
|
|
1838
2241
|
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
1839
|
-
id: `log:${run.runId}:auto-resume:${Date.now()}`,
|
|
1840
|
-
title: "
|
|
1841
|
-
detail: `Rig server recovered
|
|
2242
|
+
id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
|
|
2243
|
+
title: "Server-owned closeout auto-resume scheduled",
|
|
2244
|
+
detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
|
|
1842
2245
|
tone: "info",
|
|
1843
|
-
status: "
|
|
2246
|
+
status: "reviewing",
|
|
1844
2247
|
createdAt: new Date().toISOString()
|
|
1845
2248
|
});
|
|
1846
|
-
await
|
|
2249
|
+
await runServerOwnedPrCloseout(state, run.runId).catch((error) => {
|
|
2250
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2251
|
+
patchRunRecord(state.projectRoot, run.runId, {
|
|
2252
|
+
status: "failed",
|
|
2253
|
+
completedAt: new Date().toISOString(),
|
|
2254
|
+
errorText: detail,
|
|
2255
|
+
...closeoutPhasePatch("failed", "failed", { error: detail })
|
|
2256
|
+
});
|
|
2257
|
+
});
|
|
1847
2258
|
changed = true;
|
|
1848
2259
|
}
|
|
1849
2260
|
if (changed) {
|
|
@@ -1918,8 +2329,10 @@ async function reconcileScheduler(state, reason) {
|
|
|
1918
2329
|
}
|
|
1919
2330
|
}
|
|
1920
2331
|
export {
|
|
2332
|
+
updateRunTaskSourceLifecycle,
|
|
1921
2333
|
stopRunRecord,
|
|
1922
2334
|
startLocalRun,
|
|
2335
|
+
runServerOwnedPrCloseout,
|
|
1923
2336
|
resumeRunRecord,
|
|
1924
2337
|
resolveLocalRunCliProjectRoot,
|
|
1925
2338
|
removeTaskIdsFromQueueState2 as removeTaskIdsFromQueueState,
|
|
@@ -376,6 +376,11 @@ import {
|
|
|
376
376
|
buildTaskRunLifecycleComment,
|
|
377
377
|
updateConfiguredTaskSourceTask
|
|
378
378
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
379
|
+
import {
|
|
380
|
+
closeIssueAfterMergedPr,
|
|
381
|
+
commitRunChanges,
|
|
382
|
+
runPrAutomation
|
|
383
|
+
} from "@rig/runtime/control-plane/native/pr-automation";
|
|
379
384
|
|
|
380
385
|
// packages/server/src/scheduler.ts
|
|
381
386
|
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -403,7 +408,8 @@ var TERMINAL_RUN_STATUSES2 = new Set([
|
|
|
403
408
|
"needs-attention",
|
|
404
409
|
"stopped"
|
|
405
410
|
]);
|
|
406
|
-
var
|
|
411
|
+
var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
|
|
412
|
+
var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
407
413
|
|
|
408
414
|
// packages/server/src/server-helpers/http-router.ts
|
|
409
415
|
import {
|