@h-rig/server 0.0.6-alpha.2 → 0.0.6-alpha.20

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.
@@ -2,8 +2,8 @@
2
2
  // packages/server/src/server-helpers/run-mutations.ts
3
3
  import { spawn } from "child_process";
4
4
  import { loadConfig } from "@rig/core/load-config";
5
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync3, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
6
- import { dirname as dirname5, relative as relative2, resolve as resolve9 } from "path";
5
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync4, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
6
+ import { dirname as dirname6, relative as relative2, resolve as resolve10 } from "path";
7
7
  import {
8
8
  listAuthorityRuns as listAuthorityRuns7,
9
9
  readAuthorityRun as readAuthorityRun8,
@@ -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";
@@ -98,8 +103,8 @@ function normalizeStatus(value) {
98
103
  }
99
104
 
100
105
  // packages/server/src/server.ts
101
- import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
102
- import { dirname as dirname4, resolve as resolve7 } from "path";
106
+ import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
107
+ import { dirname as dirname5, resolve as resolve8 } from "path";
103
108
  import {
104
109
  listAuthorityArtifactRoots,
105
110
  listAuthorityRuns as listAuthorityRuns6,
@@ -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
  }
@@ -295,6 +303,24 @@ var snapshotCache = new Map;
295
303
  var contextCache = new Map;
296
304
  var taskListCache = new Map;
297
305
 
306
+ // packages/server/src/server-helpers/task-projection.ts
307
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync, writeFileSync as writeFileSync3 } from "fs";
308
+ import { resolve as resolve5 } from "path";
309
+ function projectionPath(projectRoot) {
310
+ return resolve5(projectRoot, ".rig", "state", "task-projection.json");
311
+ }
312
+ function readTaskProjection(projectRoot) {
313
+ const file = projectionPath(projectRoot);
314
+ if (!existsSync3(file))
315
+ return null;
316
+ try {
317
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
318
+ return parsed && parsed.version === 1 && Array.isArray(parsed.tasks) ? parsed : null;
319
+ } catch {
320
+ return null;
321
+ }
322
+ }
323
+
298
324
  // packages/server/src/server-helpers/terminal-runtime.ts
299
325
  import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
300
326
 
@@ -302,13 +328,20 @@ import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
302
328
  import { RIG_WS_CHANNELS } from "@rig/contracts";
303
329
 
304
330
  // packages/server/src/server-helpers/run-writers.ts
305
- import { resolve as resolve5 } from "path";
331
+ import { resolve as resolve6 } from "path";
306
332
  import {
307
333
  appendJsonlRecord,
308
334
  readAuthorityRun as readAuthorityRun3,
309
335
  resolveAuthorityRunDir as resolveAuthorityRunDir2,
310
336
  writeJsonFile as writeJsonFile2
311
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
+ }
312
345
  function appendRunLogEntry(projectRoot, runId, value) {
313
346
  if (!readAuthorityRun3(projectRoot, runId)) {
314
347
  return;
@@ -327,7 +360,7 @@ function patchRunRecord(projectRoot, runId, patch) {
327
360
  ...patch,
328
361
  updatedAt: normalizeString(patch.updatedAt) ?? new Date().toISOString()
329
362
  };
330
- writeJsonFile2(resolve5(resolveAuthorityRunDir2(projectRoot, runId), "run.json"), next);
363
+ writeJsonFile2(resolve6(resolveAuthorityRunDir2(projectRoot, runId), "run.json"), next);
331
364
  return next;
332
365
  }
333
366
  function buildRunStartPatch(startedAt) {
@@ -335,8 +368,7 @@ function buildRunStartPatch(startedAt) {
335
368
  status: "preparing",
336
369
  startedAt,
337
370
  completedAt: null,
338
- errorText: null,
339
- serverPid: process.pid
371
+ errorText: null
340
372
  };
341
373
  }
342
374
 
@@ -460,8 +492,8 @@ import {
460
492
 
461
493
  // packages/server/src/server-helpers/github-auth-store.ts
462
494
  import { randomBytes } from "crypto";
463
- import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync, writeFileSync as writeFileSync3 } from "fs";
464
- import { resolve as resolve6 } from "path";
495
+ import { chmodSync, copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "fs";
496
+ import { dirname as dirname4, resolve as resolve7 } from "path";
465
497
  function cleanString(value) {
466
498
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
467
499
  }
@@ -491,11 +523,31 @@ function parseApiSessions(value) {
491
523
  }];
492
524
  });
493
525
  }
526
+ function parsePendingDevice(value) {
527
+ if (!value || typeof value !== "object")
528
+ return null;
529
+ const record = value;
530
+ const pollId = cleanString(record.pollId);
531
+ const deviceCode = cleanString(record.deviceCode);
532
+ const expiresAt = cleanString(record.expiresAt);
533
+ const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
534
+ if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
535
+ return null;
536
+ return { pollId, deviceCode, expiresAt, intervalSeconds };
537
+ }
538
+ function parsePendingDevices(value) {
539
+ if (!Array.isArray(value))
540
+ return [];
541
+ return value.flatMap((entry) => {
542
+ const pending = parsePendingDevice(entry);
543
+ return pending ? [pending] : [];
544
+ });
545
+ }
494
546
  function readStoredAuth(stateFile) {
495
- if (!existsSync3(stateFile))
547
+ if (!existsSync4(stateFile))
496
548
  return {};
497
549
  try {
498
- const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
550
+ const parsed = JSON.parse(readFileSync2(stateFile, "utf8"));
499
551
  return {
500
552
  ...cleanString(parsed.token) ? { token: cleanString(parsed.token) } : {},
501
553
  login: cleanString(parsed.login),
@@ -504,6 +556,7 @@ function readStoredAuth(stateFile) {
504
556
  selectedRepo: cleanString(parsed.selectedRepo),
505
557
  tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
506
558
  pendingDevice: parsePendingDevice(parsed.pendingDevice),
559
+ pendingDevices: parsePendingDevices(parsed.pendingDevices),
507
560
  apiSessions: parseApiSessions(parsed.apiSessions),
508
561
  updatedAt: cleanString(parsed.updatedAt) ?? undefined
509
562
  };
@@ -511,34 +564,36 @@ function readStoredAuth(stateFile) {
511
564
  return {};
512
565
  }
513
566
  }
514
- function parsePendingDevice(value) {
515
- if (!value || typeof value !== "object")
516
- return null;
517
- const record = value;
518
- const pollId = cleanString(record.pollId);
519
- const deviceCode = cleanString(record.deviceCode);
520
- const expiresAt = cleanString(record.expiresAt);
521
- const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
522
- if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
523
- return null;
524
- return { pollId, deviceCode, expiresAt, intervalSeconds };
525
- }
526
567
  function newApiSessionToken() {
527
568
  return `rig_${randomBytes(32).toString("base64url")}`;
528
569
  }
529
570
  function writeStoredAuth(stateFile, payload) {
530
- mkdirSync3(resolve6(stateFile, ".."), { recursive: true });
531
- writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
571
+ mkdirSync4(dirname4(stateFile), { recursive: true });
572
+ writeFileSync4(stateFile, `${JSON.stringify(payload, null, 2)}
532
573
  `, { encoding: "utf8", mode: 384 });
533
574
  try {
534
575
  chmodSync(stateFile, 384);
535
576
  } catch {}
536
577
  }
578
+ function localProjectAuthStateFile(projectRoot) {
579
+ return resolve7(projectRoot, ".rig", "state", "github-auth.json");
580
+ }
537
581
  function resolveGitHubAuthStateFile(projectRoot) {
538
- return resolve6(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
582
+ return resolve7(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
539
583
  }
540
- function createGitHubAuthStore(projectRoot) {
541
- const stateFile = resolveGitHubAuthStateFile(projectRoot);
584
+ function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
585
+ const targetFile = localProjectAuthStateFile(projectRoot);
586
+ mkdirSync4(dirname4(targetFile), { recursive: true });
587
+ if (existsSync4(stateFile)) {
588
+ copyFileSync(stateFile, targetFile);
589
+ try {
590
+ chmodSync(targetFile, 384);
591
+ } catch {}
592
+ return;
593
+ }
594
+ writeStoredAuth(targetFile, {});
595
+ }
596
+ function createGitHubAuthStoreFromStateFile(stateFile) {
542
597
  return {
543
598
  stateFile,
544
599
  status(options) {
@@ -568,6 +623,7 @@ function createGitHubAuthStore(projectRoot) {
568
623
  scopes: input.scopes ?? [],
569
624
  selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
570
625
  pendingDevice: null,
626
+ pendingDevices: [],
571
627
  apiSessions: previous.apiSessions ?? [],
572
628
  updatedAt: new Date().toISOString()
573
629
  });
@@ -596,15 +652,24 @@ function createGitHubAuthStore(projectRoot) {
596
652
  const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
597
653
  return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
598
654
  },
599
- copyToProjectRoot(projectRoot2) {
600
- const targetFile = resolveGitHubAuthStateFile(projectRoot2);
655
+ copyToProjectRoot(projectRoot) {
656
+ const targetFile = resolveGitHubAuthStateFile(projectRoot);
601
657
  writeStoredAuth(targetFile, readStoredAuth(stateFile));
602
658
  },
659
+ copyToLocalProjectRoot(projectRoot) {
660
+ copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
661
+ },
603
662
  savePendingDevice(input) {
604
663
  const previous = readStoredAuth(stateFile);
664
+ const pendingDevices = [
665
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
666
+ ...previous.pendingDevices ?? [],
667
+ input
668
+ ].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
605
669
  writeStoredAuth(stateFile, {
606
670
  ...previous,
607
- pendingDevice: input,
671
+ pendingDevice: null,
672
+ pendingDevices,
608
673
  updatedAt: new Date().toISOString()
609
674
  });
610
675
  },
@@ -617,23 +682,32 @@ function createGitHubAuthStore(projectRoot) {
617
682
  });
618
683
  },
619
684
  readPendingDevice(pollId) {
620
- const pending = readStoredAuth(stateFile).pendingDevice ?? null;
621
- if (!pending || pending.pollId !== pollId)
685
+ const previous = readStoredAuth(stateFile);
686
+ const pending = [
687
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
688
+ ...previous.pendingDevices ?? []
689
+ ].find((entry) => entry.pollId === pollId) ?? null;
690
+ if (!pending)
622
691
  return null;
623
692
  if (Date.parse(pending.expiresAt) <= Date.now())
624
693
  return null;
625
694
  return pending;
626
695
  },
627
- clearPendingDevice() {
696
+ clearPendingDevice(pollId) {
628
697
  const previous = readStoredAuth(stateFile);
698
+ const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
629
699
  writeStoredAuth(stateFile, {
630
700
  ...previous,
631
701
  pendingDevice: null,
702
+ pendingDevices: remaining,
632
703
  updatedAt: new Date().toISOString()
633
704
  });
634
705
  }
635
706
  };
636
707
  }
708
+ function createGitHubAuthStore(projectRoot) {
709
+ return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
710
+ }
637
711
 
638
712
  // packages/server/src/server-helpers/github-projects.ts
639
713
  function asRecord(value) {
@@ -752,6 +826,7 @@ var DEFAULT_PROJECT_STATUSES = {
752
826
  running: "In Progress",
753
827
  prOpen: "In Review",
754
828
  ciFixing: "In Review",
829
+ merging: "Merging",
755
830
  done: "Done",
756
831
  needsAttention: "Needs Attention"
757
832
  };
@@ -765,6 +840,8 @@ function lifecycleStatusForTaskStatus(status) {
765
840
  return "prOpen";
766
841
  if (normalized === "ci_fixing" || normalized === "fixing")
767
842
  return "ciFixing";
843
+ if (normalized === "merging" || normalized === "merge")
844
+ return "merging";
768
845
  if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
769
846
  return "needsAttention";
770
847
  if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
@@ -969,10 +1046,10 @@ var CLUSTERS = {
969
1046
  };
970
1047
 
971
1048
  // packages/server/src/server-helpers/task-config.ts
972
- import { existsSync as existsSync4 } from "fs";
1049
+ import { existsSync as existsSync5 } from "fs";
973
1050
  async function readTaskConfig(projectRoot) {
974
1051
  const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
975
- if (!existsSync4(taskConfigPath)) {
1052
+ if (!existsSync5(taskConfigPath)) {
976
1053
  return {};
977
1054
  }
978
1055
  try {
@@ -989,8 +1066,8 @@ var serverPathEnvQueue = Promise.resolve();
989
1066
  async function withServerPathEnv(projectRoot, fn) {
990
1067
  const waitForTurn = serverPathEnvQueue;
991
1068
  let releaseTurn;
992
- serverPathEnvQueue = new Promise((resolve8) => {
993
- releaseTurn = resolve8;
1069
+ serverPathEnvQueue = new Promise((resolve9) => {
1070
+ releaseTurn = resolve9;
994
1071
  });
995
1072
  await waitForTurn;
996
1073
  const paths = resolveServerAuthorityPaths(projectRoot);
@@ -1026,9 +1103,9 @@ async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
1026
1103
  return withServerPathEnv(projectRoot, fn);
1027
1104
  }
1028
1105
  async function readWorkspaceTasks(projectRoot) {
1029
- const issuesPath = resolve7(resolveMonorepoRoot5(projectRoot), ".beads", "issues.jsonl");
1106
+ const issuesPath = resolve8(resolveMonorepoRoot5(projectRoot), ".beads", "issues.jsonl");
1030
1107
  const taskConfig = await readTaskConfig(projectRoot);
1031
- if (!existsSync5(issuesPath)) {
1108
+ if (!existsSync6(issuesPath)) {
1032
1109
  return [];
1033
1110
  }
1034
1111
  const latestById = new Map;
@@ -1074,7 +1151,7 @@ async function readWorkspaceTasks(projectRoot) {
1074
1151
  if (false) {}
1075
1152
 
1076
1153
  // packages/server/src/server-helpers/validation-failure.ts
1077
- import { resolve as resolve8 } from "path";
1154
+ import { resolve as resolve9 } from "path";
1078
1155
  import {
1079
1156
  readJsonFile as readJsonFile4,
1080
1157
  resolveTaskArtifactDirs
@@ -1088,7 +1165,7 @@ function summarizeRunValidationFailure(projectRoot, run) {
1088
1165
  continue;
1089
1166
  }
1090
1167
  seen.add(artifactRoot);
1091
- const summary = readJsonFile4(resolve8(artifactRoot, "validation-summary.json"), null);
1168
+ const summary = readJsonFile4(resolve9(artifactRoot, "validation-summary.json"), null);
1092
1169
  if (!summary || summary.status !== "fail") {
1093
1170
  continue;
1094
1171
  }
@@ -1168,9 +1245,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
1168
1245
  return null;
1169
1246
  return null;
1170
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
+ }
1171
1253
  async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
1172
1254
  if (!run.taskId)
1173
- return;
1255
+ return false;
1174
1256
  const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
1175
1257
  try {
1176
1258
  const result = await syncGitHubProjectStatusForTaskUpdate({
@@ -1181,28 +1263,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
1181
1263
  config
1182
1264
  });
1183
1265
  if (!result.synced && result.reason !== "project-sync-disabled") {
1266
+ const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
1184
1267
  appendRunLogEntry(projectRoot, run.runId, {
1185
1268
  id: `log:${run.runId}:github-project-sync:${status}`,
1186
1269
  title: "GitHub Project sync skipped",
1187
- detail: `Project status sync for ${run.taskId} could not run: ${result.reason}.`,
1270
+ detail,
1188
1271
  tone: "warn",
1189
1272
  status: "running",
1190
1273
  createdAt: new Date().toISOString(),
1191
1274
  payload: { reason: result.reason, issueNodeId }
1192
1275
  });
1276
+ if (githubProjectsEnabled(config)) {
1277
+ throw new Error(detail);
1278
+ }
1279
+ return false;
1193
1280
  }
1281
+ return result.synced === true;
1194
1282
  } catch (error) {
1283
+ const detail = error instanceof Error ? error.message : String(error);
1195
1284
  appendRunLogEntry(projectRoot, run.runId, {
1196
1285
  id: `log:${run.runId}:github-project-sync-error:${status}`,
1197
1286
  title: "GitHub Project sync failed",
1198
- detail: error instanceof Error ? error.message : String(error),
1287
+ detail,
1199
1288
  tone: "error",
1200
1289
  status: "running",
1201
1290
  createdAt: new Date().toISOString(),
1202
1291
  payload: { issueNodeId }
1203
1292
  });
1293
+ if (githubProjectsEnabled(config)) {
1294
+ throw new Error(detail);
1295
+ }
1296
+ return false;
1204
1297
  }
1205
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
+ }
1206
1346
  async function autoAssignRunIssue(projectRoot, run) {
1207
1347
  if (!run.taskId)
1208
1348
  return;
@@ -1232,7 +1372,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
1232
1372
  return;
1233
1373
  }
1234
1374
  const config = await loadRigLifecycleConfig(projectRoot);
1235
- await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
1375
+ const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
1236
1376
  if (status === "in_progress") {
1237
1377
  await autoAssignRunIssue(projectRoot, run);
1238
1378
  }
@@ -1248,24 +1388,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
1248
1388
  });
1249
1389
  return;
1250
1390
  }
1251
- const result = await updateConfiguredTaskSourceTask2(projectRoot, {
1252
- taskId: run.taskId,
1253
- sourceTask: runSourceTaskIdentity(run),
1254
- update: {
1255
- status,
1256
- comment: buildTaskRunLifecycleComment2({
1257
- 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: {
1258
1419
  status,
1259
- summary,
1260
- runtimeWorkspace: normalizeString(run.worktreePath),
1261
- logsDir: normalizeString(run.logRoot),
1262
- sessionDir: normalizeString(run.sessionPath),
1263
- errorText: options.errorText ?? normalizeString(run.errorText)
1264
- })
1265
- }
1266
- });
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
+ }
1267
1435
  if (!result.updated) {
1268
1436
  if (result.source === "plugin" || result.sourceKind) {
1437
+ await rollbackProjectSync();
1269
1438
  throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
1270
1439
  }
1271
1440
  appendRunLogEntry(projectRoot, run.runId, {
@@ -1278,6 +1447,219 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
1278
1447
  });
1279
1448
  }
1280
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
+ }
1281
1663
  var TERMINAL_RUN_STATUSES2 = new Set([
1282
1664
  "completed",
1283
1665
  "complete",
@@ -1303,11 +1685,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
1303
1685
  return;
1304
1686
  throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
1305
1687
  }
1688
+ async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
1689
+ const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
1690
+ if (fromReader)
1691
+ return fromReader;
1692
+ const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
1693
+ if (projected)
1694
+ return projected;
1695
+ if (readTasks !== readWorkspaceTasks) {
1696
+ return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
1697
+ }
1698
+ return null;
1699
+ }
1306
1700
  async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
1307
1701
  if ("taskId" in input && input.taskId) {
1308
1702
  assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
1309
1703
  }
1310
- const sourceTask = "taskId" in input && input.taskId ? (await readTasks(projectRoot)).find((task) => task.id === input.taskId) ?? null : null;
1704
+ const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
1311
1705
  const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
1312
1706
  const runDir = resolveAuthorityRunDir4(projectRoot, input.runId);
1313
1707
  const runRecord = {
@@ -1341,11 +1735,11 @@ async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTask
1341
1735
  initiatedBy: input.initiatedBy ?? null,
1342
1736
  ...sourceTask ? { sourceTask: sourceTaskContract(sourceTask) } : {}
1343
1737
  };
1344
- mkdirSync5(runDir, { recursive: true });
1345
- writeFileSync5(resolve9(runDir, "run.json"), `${JSON.stringify(runRecord, null, 2)}
1738
+ mkdirSync6(runDir, { recursive: true });
1739
+ writeFileSync6(resolve10(runDir, "run.json"), `${JSON.stringify(runRecord, null, 2)}
1346
1740
  `, "utf8");
1347
1741
  if ("initialPrompt" in input && input.initialPrompt && input.initialPrompt.trim().length > 0) {
1348
- writeFileSync5(resolve9(runDir, "timeline.jsonl"), `${JSON.stringify({
1742
+ writeFileSync6(resolve10(runDir, "timeline.jsonl"), `${JSON.stringify({
1349
1743
  id: `message-${Date.now()}`,
1350
1744
  type: "user_message",
1351
1745
  text: input.initialPrompt,
@@ -1379,6 +1773,7 @@ async function startLocalRun(state, runId, options) {
1379
1773
  throw new Error(`Run not found: ${runId}`);
1380
1774
  }
1381
1775
  const startedAt = new Date().toISOString();
1776
+ const resumeMode = options?.resume === true;
1382
1777
  state.runProcesses.set(runId, {
1383
1778
  runId,
1384
1779
  child: null,
@@ -1395,9 +1790,9 @@ async function startLocalRun(state, runId, options) {
1395
1790
  summary: run.title
1396
1791
  });
1397
1792
  appendRunLogEntry(state.projectRoot, runId, {
1398
- id: `log:${runId}:prepare`,
1399
- title: "Rig task run starting",
1400
- detail: run.taskId ?? run.title,
1793
+ id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
1794
+ title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
1795
+ detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
1401
1796
  tone: "info",
1402
1797
  status: "preparing",
1403
1798
  createdAt: startedAt
@@ -1405,8 +1800,8 @@ async function startLocalRun(state, runId, options) {
1405
1800
  broadcastRunLogAppended(state, runId, readLatestRawRunLog(state.projectRoot, runId));
1406
1801
  broadcastSnapshotInvalidation(state);
1407
1802
  const cliProjectRoot = resolveLocalRunCliProjectRoot(state.projectRoot);
1408
- const cliEntryPoint = resolve9(cliProjectRoot, "packages/cli/bin/rig.ts");
1409
- if (!existsSync6(cliEntryPoint)) {
1803
+ const cliEntryPoint = resolve10(cliProjectRoot, "packages/cli/bin/rig.ts");
1804
+ if (!existsSync7(cliEntryPoint)) {
1410
1805
  const completedAt = new Date().toISOString();
1411
1806
  const failureSummary = `Rig task-run entrypoint missing at ${relative2(state.projectRoot, cliEntryPoint)}`;
1412
1807
  patchRunRecord(state.projectRoot, runId, {
@@ -1473,9 +1868,18 @@ async function startLocalRun(state, runId, options) {
1473
1868
  RIG_HOST_PROJECT_ROOT: cliProjectRoot,
1474
1869
  RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
1475
1870
  RIG_SERVER_INTERNAL_EXEC: "1",
1871
+ RIG_SERVER_OWNS_CLOSEOUT: "1",
1476
1872
  ...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
1477
1873
  ...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
1478
- ...bridgeGitHubToken ? { RIG_GITHUB_TOKEN: bridgeGitHubToken } : {}
1874
+ ...bridgeGitHubToken ? {
1875
+ RIG_GITHUB_TOKEN: bridgeGitHubToken,
1876
+ GITHUB_TOKEN: bridgeGitHubToken,
1877
+ GH_TOKEN: bridgeGitHubToken
1878
+ } : {},
1879
+ ...resumeMode ? {
1880
+ RIG_RUN_RESUME: "1",
1881
+ RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
1882
+ } : {}
1479
1883
  },
1480
1884
  stdio: ["ignore", "pipe", "pipe"]
1481
1885
  });
@@ -1515,9 +1919,9 @@ async function startLocalRun(state, runId, options) {
1515
1919
  handleRunProcessOutput(Buffer.isBuffer(data) ? data.toString("utf8") : String(data), "error", "Rig task run stderr");
1516
1920
  });
1517
1921
  try {
1518
- const exit = await new Promise((resolve10) => {
1519
- child.once("error", (error) => resolve10({ code: 1, signal: null, error }));
1520
- child.once("close", (code, signal) => resolve10({ code, signal }));
1922
+ const exit = await new Promise((resolve11) => {
1923
+ child.once("error", (error) => resolve11({ code: 1, signal: null, error }));
1924
+ child.once("close", (code, signal) => resolve11({ code, signal }));
1521
1925
  });
1522
1926
  if (exit.error) {
1523
1927
  throw new Error(`Failed to start task run: ${exit.error.message}`);
@@ -1561,6 +1965,38 @@ ${sourceFailure}` });
1561
1965
  agent: current.runtimeAdapter,
1562
1966
  summary: failureSummary
1563
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
+ }
1564
2000
  }
1565
2001
  broadcastSnapshotInvalidation(state);
1566
2002
  } catch (error) {
@@ -1617,17 +2053,17 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
1617
2053
  process.env.PROJECT_RIG_ROOT?.trim()
1618
2054
  ].filter((value) => !!value);
1619
2055
  for (const candidate of envCandidates) {
1620
- if (existsSync6(resolve9(candidate, "packages/cli/bin/rig.ts"))) {
1621
- return resolve9(candidate);
2056
+ if (existsSync7(resolve10(candidate, "packages/cli/bin/rig.ts"))) {
2057
+ return resolve10(candidate);
1622
2058
  }
1623
2059
  }
1624
- if (existsSync6(resolve9(projectRoot, "packages/cli/bin/rig.ts"))) {
2060
+ if (existsSync7(resolve10(projectRoot, "packages/cli/bin/rig.ts"))) {
1625
2061
  return projectRoot;
1626
2062
  }
1627
2063
  try {
1628
2064
  const monorepoRoot = resolveMonorepoRoot6(projectRoot);
1629
- const outerProjectRoot = dirname5(dirname5(monorepoRoot));
1630
- if (existsSync6(resolve9(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
2065
+ const outerProjectRoot = dirname6(dirname6(monorepoRoot));
2066
+ if (existsSync7(resolve10(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
1631
2067
  return outerProjectRoot;
1632
2068
  }
1633
2069
  } catch {}
@@ -1647,15 +2083,21 @@ async function resumeRunRecord(state, input) {
1647
2083
  if (run.status === "completed") {
1648
2084
  throw new Error("Completed runs cannot be resumed.");
1649
2085
  }
1650
- await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
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
+ }
2092
+ await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
1651
2093
  }
1652
2094
  function appendRunMessage(projectRoot, input) {
1653
2095
  const run = readAuthorityRun8(projectRoot, input.runId);
1654
2096
  if (!run) {
1655
2097
  throw new Error(`Run not found: ${input.runId}`);
1656
2098
  }
1657
- const timelinePath = resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "timeline.jsonl");
1658
- const existingLines = fileExists(timelinePath) ? readFileSync3(timelinePath, "utf8").trim() : "";
2099
+ const timelinePath = resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "timeline.jsonl");
2100
+ const existingLines = fileExists(timelinePath) ? readFileSync4(timelinePath, "utf8").trim() : "";
1659
2101
  const nextLine = JSON.stringify({
1660
2102
  id: input.messageId,
1661
2103
  type: "user_message",
@@ -1663,11 +2105,11 @@ function appendRunMessage(projectRoot, input) {
1663
2105
  attachments: input.attachments ?? [],
1664
2106
  createdAt: input.createdAt
1665
2107
  });
1666
- writeFileSync5(timelinePath, existingLines.length > 0 ? `${existingLines}
2108
+ writeFileSync6(timelinePath, existingLines.length > 0 ? `${existingLines}
1667
2109
  ${nextLine}
1668
2110
  ` : `${nextLine}
1669
2111
  `, "utf8");
1670
- writeJsonFile4(resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), {
2112
+ writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), {
1671
2113
  ...run,
1672
2114
  updatedAt: input.createdAt
1673
2115
  });
@@ -1693,7 +2135,7 @@ async function stopRunRecord(stateOrProjectRoot, input) {
1693
2135
  completedAt: run.completedAt ?? input.createdAt,
1694
2136
  updatedAt: input.createdAt
1695
2137
  };
1696
- writeJsonFile4(resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), nextRun);
2138
+ writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), nextRun);
1697
2139
  if (run.status !== "completed" && run.taskId) {
1698
2140
  const taskId = run.taskId;
1699
2141
  (async () => {
@@ -1731,34 +2173,46 @@ function removeTaskIdsFromQueueState2(projectRoot, taskIds) {
1731
2173
  writeQueueState(projectRoot, next);
1732
2174
  return next;
1733
2175
  }
1734
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
1735
- function reconcileOrphanedLocalRuns(state, runs, nowIso) {
1736
- let changed = false;
1737
- for (const run of runs) {
1738
- const status = normalizeString(run.status)?.toLowerCase() ?? "";
1739
- const serverPid = run.serverPid;
1740
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
1741
- if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
1742
- continue;
1743
- }
1744
- const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
1745
- patchRunRecord(state.projectRoot, run.runId, {
1746
- status: "failed",
1747
- completedAt: run.completedAt ?? nowIso,
1748
- updatedAt: nowIso,
1749
- errorText: detail
1750
- });
1751
- appendRunLogEntry(state.projectRoot, run.runId, {
1752
- id: `log:${run.runId}:stale-local-run`,
1753
- title: "Run marked stale after server restart",
1754
- detail,
1755
- tone: "error",
1756
- status: "failed",
1757
- createdAt: nowIso
1758
- });
1759
- changed = true;
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;
1760
2186
  }
1761
- return changed;
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) {
2211
+ return runs.filter((run) => {
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);
2215
+ });
1762
2216
  }
1763
2217
  async function reconcileScheduler(state, reason) {
1764
2218
  if (state.scheduler.reconciling) {
@@ -1773,7 +2227,36 @@ async function reconcileScheduler(state, reason) {
1773
2227
  const queue = readQueueState(state.projectRoot);
1774
2228
  const tasks = await state.snapshotService.getWorkspaceTasks();
1775
2229
  let runs = listAuthorityRuns7(state.projectRoot);
1776
- let changed = reconcileOrphanedLocalRuns(state, runs, new Date().toISOString());
2230
+ let changed = false;
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) {
2241
+ appendRunLogEntry(state.projectRoot, run.runId, {
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.`,
2245
+ tone: "info",
2246
+ status: "reviewing",
2247
+ createdAt: new Date().toISOString()
2248
+ });
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
+ });
2258
+ changed = true;
2259
+ }
1777
2260
  if (changed) {
1778
2261
  runs = listAuthorityRuns7(state.projectRoot);
1779
2262
  }
@@ -1846,8 +2329,10 @@ async function reconcileScheduler(state, reason) {
1846
2329
  }
1847
2330
  }
1848
2331
  export {
2332
+ updateRunTaskSourceLifecycle,
1849
2333
  stopRunRecord,
1850
2334
  startLocalRun,
2335
+ runServerOwnedPrCloseout,
1851
2336
  resumeRunRecord,
1852
2337
  resolveLocalRunCliProjectRoot,
1853
2338
  removeTaskIdsFromQueueState2 as removeTaskIdsFromQueueState,