@h-rig/server 0.0.6-alpha.13 → 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.
@@ -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: `Project status sync for ${run.taskId} could not run: ${result.reason}.`,
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: error instanceof Error ? error.message : String(error),
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 result = await updateConfiguredTaskSourceTask2(projectRoot, {
1312
- taskId: run.taskId,
1313
- sourceTask: runSourceTaskIdentity(run),
1314
- update: {
1315
- status,
1316
- comment: buildTaskRunLifecycleComment2({
1317
- runId: run.runId,
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
- summary,
1320
- runtimeWorkspace: normalizeString(run.worktreePath),
1321
- logsDir: normalizeString(run.logRoot),
1322
- sessionDir: normalizeString(run.sessionPath),
1323
- errorText: options.errorText ?? normalizeString(run.errorText)
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 RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
1816
- function collectResumableLocalRuns(state, runs) {
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 status = normalizeString(run.status)?.toLowerCase() ?? "";
1819
- return run.mode === "local" && RESUMABLE_LOCAL_RUN_STATUSES.has(status) && !state.runProcesses.has(run.runId);
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 resumableRuns = collectResumableLocalRuns(state, runs);
1837
- for (const run of resumableRuns) {
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: "Run auto-resume scheduled",
1841
- detail: `Rig server recovered nonterminal run ${run.runId} after ${reason}; resuming the same lifecycle instead of restarting it.`,
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: "preparing",
2246
+ status: "reviewing",
1844
2247
  createdAt: new Date().toISOString()
1845
2248
  });
1846
- await startLocalRun(state, run.runId, { resume: true });
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,
@@ -63,8 +63,7 @@ function buildRunStartPatch(startedAt) {
63
63
  status: "preparing",
64
64
  startedAt,
65
65
  completedAt: null,
66
- errorText: null,
67
- serverPid: process.pid
66
+ errorText: null
68
67
  };
69
68
  }
70
69
  export {
@@ -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 RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
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 {