@h-rig/server 0.0.6-alpha.3 → 0.0.6-alpha.31

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,11 +2,11 @@
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
- readAuthorityRun as readAuthorityRun8,
9
+ readAuthorityRun as readAuthorityRun9,
10
10
  resolveAuthorityRunDir as resolveAuthorityRunDir4,
11
11
  writeJsonFile as writeJsonFile4
12
12
  } from "@rig/runtime/control-plane/authority-files";
@@ -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,16 +360,21 @@ 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
  }
366
+ function patchRunPiSessionMetadata(projectRoot, runId, metadata) {
367
+ return patchRunRecord(projectRoot, runId, {
368
+ piSession: metadata?.public ?? null,
369
+ piSessionPrivate: metadata
370
+ });
371
+ }
333
372
  function buildRunStartPatch(startedAt) {
334
373
  return {
335
374
  status: "preparing",
336
375
  startedAt,
337
376
  completedAt: null,
338
- errorText: null,
339
- serverPid: process.pid
377
+ errorText: null
340
378
  };
341
379
  }
342
380
 
@@ -428,7 +466,7 @@ var DEFAULT_TERMINAL_SHELL = process.env.SHELL || "/bin/zsh";
428
466
  // packages/server/src/server-helpers/http-router.ts
429
467
  import {
430
468
  listAuthorityRuns as listAuthorityRuns4,
431
- readAuthorityRun as readAuthorityRun5,
469
+ readAuthorityRun as readAuthorityRun6,
432
470
  resolveAuthorityPaths,
433
471
  writeJsonFile as writeJsonFile3
434
472
  } from "@rig/runtime/control-plane/authority-files";
@@ -448,8 +486,11 @@ import {
448
486
  RemoteWsClient
449
487
  } from "@rig/runtime/control-plane/remote";
450
488
 
489
+ // packages/server/src/server-helpers/pi-session-proxy.ts
490
+ import { readAuthorityRun as readAuthorityRun4 } from "@rig/runtime/control-plane/authority-files";
491
+
451
492
  // packages/server/src/server-helpers/run-steering.ts
452
- import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun4, resolveAuthorityRunDir as resolveAuthorityRunDir3 } from "@rig/runtime/control-plane/authority-files";
493
+ import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun5, resolveAuthorityRunDir as resolveAuthorityRunDir3 } from "@rig/runtime/control-plane/authority-files";
453
494
 
454
495
  // packages/server/src/server-helpers/http-router.ts
455
496
  import { buildRigInitConfigSource } from "@rig/core";
@@ -460,8 +501,8 @@ import {
460
501
 
461
502
  // packages/server/src/server-helpers/github-auth-store.ts
462
503
  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";
504
+ import { chmodSync, copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "fs";
505
+ import { dirname as dirname4, resolve as resolve7 } from "path";
465
506
  function cleanString(value) {
466
507
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
467
508
  }
@@ -491,11 +532,31 @@ function parseApiSessions(value) {
491
532
  }];
492
533
  });
493
534
  }
535
+ function parsePendingDevice(value) {
536
+ if (!value || typeof value !== "object")
537
+ return null;
538
+ const record = value;
539
+ const pollId = cleanString(record.pollId);
540
+ const deviceCode = cleanString(record.deviceCode);
541
+ const expiresAt = cleanString(record.expiresAt);
542
+ const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
543
+ if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
544
+ return null;
545
+ return { pollId, deviceCode, expiresAt, intervalSeconds };
546
+ }
547
+ function parsePendingDevices(value) {
548
+ if (!Array.isArray(value))
549
+ return [];
550
+ return value.flatMap((entry) => {
551
+ const pending = parsePendingDevice(entry);
552
+ return pending ? [pending] : [];
553
+ });
554
+ }
494
555
  function readStoredAuth(stateFile) {
495
- if (!existsSync3(stateFile))
556
+ if (!existsSync4(stateFile))
496
557
  return {};
497
558
  try {
498
- const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
559
+ const parsed = JSON.parse(readFileSync2(stateFile, "utf8"));
499
560
  return {
500
561
  ...cleanString(parsed.token) ? { token: cleanString(parsed.token) } : {},
501
562
  login: cleanString(parsed.login),
@@ -504,6 +565,7 @@ function readStoredAuth(stateFile) {
504
565
  selectedRepo: cleanString(parsed.selectedRepo),
505
566
  tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
506
567
  pendingDevice: parsePendingDevice(parsed.pendingDevice),
568
+ pendingDevices: parsePendingDevices(parsed.pendingDevices),
507
569
  apiSessions: parseApiSessions(parsed.apiSessions),
508
570
  updatedAt: cleanString(parsed.updatedAt) ?? undefined
509
571
  };
@@ -511,34 +573,36 @@ function readStoredAuth(stateFile) {
511
573
  return {};
512
574
  }
513
575
  }
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
576
  function newApiSessionToken() {
527
577
  return `rig_${randomBytes(32).toString("base64url")}`;
528
578
  }
529
579
  function writeStoredAuth(stateFile, payload) {
530
- mkdirSync3(resolve6(stateFile, ".."), { recursive: true });
531
- writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
580
+ mkdirSync4(dirname4(stateFile), { recursive: true });
581
+ writeFileSync4(stateFile, `${JSON.stringify(payload, null, 2)}
532
582
  `, { encoding: "utf8", mode: 384 });
533
583
  try {
534
584
  chmodSync(stateFile, 384);
535
585
  } catch {}
536
586
  }
587
+ function localProjectAuthStateFile(projectRoot) {
588
+ return resolve7(projectRoot, ".rig", "state", "github-auth.json");
589
+ }
537
590
  function resolveGitHubAuthStateFile(projectRoot) {
538
- return resolve6(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
591
+ return resolve7(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
539
592
  }
540
- function createGitHubAuthStore(projectRoot) {
541
- const stateFile = resolveGitHubAuthStateFile(projectRoot);
593
+ function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
594
+ const targetFile = localProjectAuthStateFile(projectRoot);
595
+ mkdirSync4(dirname4(targetFile), { recursive: true });
596
+ if (existsSync4(stateFile)) {
597
+ copyFileSync(stateFile, targetFile);
598
+ try {
599
+ chmodSync(targetFile, 384);
600
+ } catch {}
601
+ return;
602
+ }
603
+ writeStoredAuth(targetFile, {});
604
+ }
605
+ function createGitHubAuthStoreFromStateFile(stateFile) {
542
606
  return {
543
607
  stateFile,
544
608
  status(options) {
@@ -568,6 +632,7 @@ function createGitHubAuthStore(projectRoot) {
568
632
  scopes: input.scopes ?? [],
569
633
  selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
570
634
  pendingDevice: null,
635
+ pendingDevices: [],
571
636
  apiSessions: previous.apiSessions ?? [],
572
637
  updatedAt: new Date().toISOString()
573
638
  });
@@ -596,15 +661,24 @@ function createGitHubAuthStore(projectRoot) {
596
661
  const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
597
662
  return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
598
663
  },
599
- copyToProjectRoot(projectRoot2) {
600
- const targetFile = resolveGitHubAuthStateFile(projectRoot2);
664
+ copyToProjectRoot(projectRoot) {
665
+ const targetFile = resolveGitHubAuthStateFile(projectRoot);
601
666
  writeStoredAuth(targetFile, readStoredAuth(stateFile));
602
667
  },
668
+ copyToLocalProjectRoot(projectRoot) {
669
+ copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
670
+ },
603
671
  savePendingDevice(input) {
604
672
  const previous = readStoredAuth(stateFile);
673
+ const pendingDevices = [
674
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
675
+ ...previous.pendingDevices ?? [],
676
+ input
677
+ ].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
605
678
  writeStoredAuth(stateFile, {
606
679
  ...previous,
607
- pendingDevice: input,
680
+ pendingDevice: null,
681
+ pendingDevices,
608
682
  updatedAt: new Date().toISOString()
609
683
  });
610
684
  },
@@ -617,23 +691,32 @@ function createGitHubAuthStore(projectRoot) {
617
691
  });
618
692
  },
619
693
  readPendingDevice(pollId) {
620
- const pending = readStoredAuth(stateFile).pendingDevice ?? null;
621
- if (!pending || pending.pollId !== pollId)
694
+ const previous = readStoredAuth(stateFile);
695
+ const pending = [
696
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
697
+ ...previous.pendingDevices ?? []
698
+ ].find((entry) => entry.pollId === pollId) ?? null;
699
+ if (!pending)
622
700
  return null;
623
701
  if (Date.parse(pending.expiresAt) <= Date.now())
624
702
  return null;
625
703
  return pending;
626
704
  },
627
- clearPendingDevice() {
705
+ clearPendingDevice(pollId) {
628
706
  const previous = readStoredAuth(stateFile);
707
+ const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
629
708
  writeStoredAuth(stateFile, {
630
709
  ...previous,
631
710
  pendingDevice: null,
711
+ pendingDevices: remaining,
632
712
  updatedAt: new Date().toISOString()
633
713
  });
634
714
  }
635
715
  };
636
716
  }
717
+ function createGitHubAuthStore(projectRoot) {
718
+ return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
719
+ }
637
720
 
638
721
  // packages/server/src/server-helpers/github-projects.ts
639
722
  function asRecord(value) {
@@ -752,6 +835,7 @@ var DEFAULT_PROJECT_STATUSES = {
752
835
  running: "In Progress",
753
836
  prOpen: "In Review",
754
837
  ciFixing: "In Review",
838
+ merging: "Merging",
755
839
  done: "Done",
756
840
  needsAttention: "Needs Attention"
757
841
  };
@@ -765,6 +849,8 @@ function lifecycleStatusForTaskStatus(status) {
765
849
  return "prOpen";
766
850
  if (normalized === "ci_fixing" || normalized === "fixing")
767
851
  return "ciFixing";
852
+ if (normalized === "merging" || normalized === "merge")
853
+ return "merging";
768
854
  if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
769
855
  return "needsAttention";
770
856
  if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
@@ -848,7 +934,7 @@ import {
848
934
  RemoteWsClient as RemoteWsClient2
849
935
  } from "@rig/runtime/control-plane/remote";
850
936
  import { deleteRunState } from "@rig/runtime/control-plane/native/run-ops";
851
- import { readAuthorityRun as readAuthorityRun6 } from "@rig/runtime/control-plane/authority-files";
937
+ import { readAuthorityRun as readAuthorityRun7 } from "@rig/runtime/control-plane/authority-files";
852
938
 
853
939
  // packages/server/src/server-helpers/inspector-jobs.ts
854
940
  import { readJsonFile as readJsonFile2 } from "@rig/runtime/control-plane/authority-files";
@@ -861,7 +947,7 @@ import {
861
947
  } from "@rig/runtime/control-plane/native/run-ops";
862
948
  import {
863
949
  listAuthorityRuns as listAuthorityRuns5,
864
- readAuthorityRun as readAuthorityRun7
950
+ readAuthorityRun as readAuthorityRun8
865
951
  } from "@rig/runtime/control-plane/authority-files";
866
952
 
867
953
  // packages/server/src/inspector/service.ts
@@ -969,10 +1055,10 @@ var CLUSTERS = {
969
1055
  };
970
1056
 
971
1057
  // packages/server/src/server-helpers/task-config.ts
972
- import { existsSync as existsSync4 } from "fs";
1058
+ import { existsSync as existsSync5 } from "fs";
973
1059
  async function readTaskConfig(projectRoot) {
974
1060
  const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
975
- if (!existsSync4(taskConfigPath)) {
1061
+ if (!existsSync5(taskConfigPath)) {
976
1062
  return {};
977
1063
  }
978
1064
  try {
@@ -989,8 +1075,8 @@ var serverPathEnvQueue = Promise.resolve();
989
1075
  async function withServerPathEnv(projectRoot, fn) {
990
1076
  const waitForTurn = serverPathEnvQueue;
991
1077
  let releaseTurn;
992
- serverPathEnvQueue = new Promise((resolve8) => {
993
- releaseTurn = resolve8;
1078
+ serverPathEnvQueue = new Promise((resolve9) => {
1079
+ releaseTurn = resolve9;
994
1080
  });
995
1081
  await waitForTurn;
996
1082
  const paths = resolveServerAuthorityPaths(projectRoot);
@@ -1026,9 +1112,9 @@ async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
1026
1112
  return withServerPathEnv(projectRoot, fn);
1027
1113
  }
1028
1114
  async function readWorkspaceTasks(projectRoot) {
1029
- const issuesPath = resolve7(resolveMonorepoRoot5(projectRoot), ".beads", "issues.jsonl");
1115
+ const issuesPath = resolve8(resolveMonorepoRoot5(projectRoot), ".beads", "issues.jsonl");
1030
1116
  const taskConfig = await readTaskConfig(projectRoot);
1031
- if (!existsSync5(issuesPath)) {
1117
+ if (!existsSync6(issuesPath)) {
1032
1118
  return [];
1033
1119
  }
1034
1120
  const latestById = new Map;
@@ -1074,7 +1160,7 @@ async function readWorkspaceTasks(projectRoot) {
1074
1160
  if (false) {}
1075
1161
 
1076
1162
  // packages/server/src/server-helpers/validation-failure.ts
1077
- import { resolve as resolve8 } from "path";
1163
+ import { resolve as resolve9 } from "path";
1078
1164
  import {
1079
1165
  readJsonFile as readJsonFile4,
1080
1166
  resolveTaskArtifactDirs
@@ -1088,7 +1174,7 @@ function summarizeRunValidationFailure(projectRoot, run) {
1088
1174
  continue;
1089
1175
  }
1090
1176
  seen.add(artifactRoot);
1091
- const summary = readJsonFile4(resolve8(artifactRoot, "validation-summary.json"), null);
1177
+ const summary = readJsonFile4(resolve9(artifactRoot, "validation-summary.json"), null);
1092
1178
  if (!summary || summary.status !== "fail") {
1093
1179
  continue;
1094
1180
  }
@@ -1168,9 +1254,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
1168
1254
  return null;
1169
1255
  return null;
1170
1256
  }
1257
+ function githubProjectsEnabled(config) {
1258
+ const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
1259
+ const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
1260
+ return projects?.enabled === true;
1261
+ }
1171
1262
  async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
1172
1263
  if (!run.taskId)
1173
- return;
1264
+ return false;
1174
1265
  const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
1175
1266
  try {
1176
1267
  const result = await syncGitHubProjectStatusForTaskUpdate({
@@ -1181,28 +1272,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
1181
1272
  config
1182
1273
  });
1183
1274
  if (!result.synced && result.reason !== "project-sync-disabled") {
1275
+ const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
1184
1276
  appendRunLogEntry(projectRoot, run.runId, {
1185
1277
  id: `log:${run.runId}:github-project-sync:${status}`,
1186
1278
  title: "GitHub Project sync skipped",
1187
- detail: `Project status sync for ${run.taskId} could not run: ${result.reason}.`,
1279
+ detail,
1188
1280
  tone: "warn",
1189
1281
  status: "running",
1190
1282
  createdAt: new Date().toISOString(),
1191
1283
  payload: { reason: result.reason, issueNodeId }
1192
1284
  });
1285
+ if (githubProjectsEnabled(config)) {
1286
+ throw new Error(detail);
1287
+ }
1288
+ return false;
1193
1289
  }
1290
+ return result.synced === true;
1194
1291
  } catch (error) {
1292
+ const detail = error instanceof Error ? error.message : String(error);
1195
1293
  appendRunLogEntry(projectRoot, run.runId, {
1196
1294
  id: `log:${run.runId}:github-project-sync-error:${status}`,
1197
1295
  title: "GitHub Project sync failed",
1198
- detail: error instanceof Error ? error.message : String(error),
1296
+ detail,
1199
1297
  tone: "error",
1200
1298
  status: "running",
1201
1299
  createdAt: new Date().toISOString(),
1202
1300
  payload: { issueNodeId }
1203
1301
  });
1302
+ if (githubProjectsEnabled(config)) {
1303
+ throw new Error(detail);
1304
+ }
1305
+ return false;
1204
1306
  }
1205
1307
  }
1308
+ function createCommandRunner(binary, extraEnv = {}) {
1309
+ return async (args, options) => {
1310
+ const child = spawn(binary, [...args], {
1311
+ cwd: options?.cwd,
1312
+ env: { ...process.env, ...extraEnv },
1313
+ stdio: ["ignore", "pipe", "pipe"]
1314
+ });
1315
+ const stdoutChunks = [];
1316
+ const stderrChunks = [];
1317
+ child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
1318
+ child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
1319
+ const exitCode = await new Promise((resolve11) => {
1320
+ child.once("error", () => resolve11(1));
1321
+ child.once("close", (code) => resolve11(code ?? 1));
1322
+ });
1323
+ return {
1324
+ exitCode,
1325
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
1326
+ stderr: Buffer.concat(stderrChunks).toString("utf8")
1327
+ };
1328
+ };
1329
+ }
1330
+ function closeoutRecord(run) {
1331
+ const value = run.serverCloseout;
1332
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1333
+ }
1334
+ function closeoutPhasePatch(phase, status, extra = {}) {
1335
+ const updatedAt = new Date().toISOString();
1336
+ return {
1337
+ serverCloseout: {
1338
+ ...extra,
1339
+ phase,
1340
+ status,
1341
+ updatedAt
1342
+ }
1343
+ };
1344
+ }
1345
+ function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
1346
+ appendRunLogEntryAndBroadcast(state, runId, {
1347
+ id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
1348
+ title: `Server closeout: ${phase}`,
1349
+ detail,
1350
+ tone,
1351
+ status,
1352
+ createdAt: new Date().toISOString()
1353
+ }, `server-closeout-${phase}`);
1354
+ }
1206
1355
  async function autoAssignRunIssue(projectRoot, run) {
1207
1356
  if (!run.taskId)
1208
1357
  return;
@@ -1232,7 +1381,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
1232
1381
  return;
1233
1382
  }
1234
1383
  const config = await loadRigLifecycleConfig(projectRoot);
1235
- await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
1384
+ const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
1236
1385
  if (status === "in_progress") {
1237
1386
  await autoAssignRunIssue(projectRoot, run);
1238
1387
  }
@@ -1248,24 +1397,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
1248
1397
  });
1249
1398
  return;
1250
1399
  }
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,
1400
+ const sourceTask = runSourceTaskIdentity(run);
1401
+ const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
1402
+ const rollbackProjectSync = async () => {
1403
+ if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
1404
+ return;
1405
+ await syncGitHubProjectStatusForTaskUpdate({
1406
+ taskId: run.taskId,
1407
+ status: previousStatus,
1408
+ issueNodeId: extractGitHubIssueNodeId(sourceTask),
1409
+ token: createGitHubAuthStore(projectRoot).readToken(),
1410
+ config
1411
+ }).catch((rollbackError) => {
1412
+ appendRunLogEntry(projectRoot, run.runId, {
1413
+ id: `log:${run.runId}:github-project-sync-rollback:${status}`,
1414
+ title: "GitHub Project sync rollback failed",
1415
+ detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
1416
+ tone: "error",
1417
+ status: "running",
1418
+ createdAt: new Date().toISOString()
1419
+ });
1420
+ });
1421
+ };
1422
+ let result;
1423
+ try {
1424
+ result = await updateConfiguredTaskSourceTask2(projectRoot, {
1425
+ taskId: run.taskId,
1426
+ sourceTask,
1427
+ update: {
1258
1428
  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
- });
1429
+ comment: buildTaskRunLifecycleComment2({
1430
+ runId: run.runId,
1431
+ status,
1432
+ summary,
1433
+ runtimeWorkspace: normalizeString(run.worktreePath),
1434
+ logsDir: normalizeString(run.logRoot),
1435
+ sessionDir: normalizeString(run.sessionPath),
1436
+ errorText: options.errorText ?? normalizeString(run.errorText)
1437
+ })
1438
+ }
1439
+ });
1440
+ } catch (error) {
1441
+ await rollbackProjectSync();
1442
+ throw error;
1443
+ }
1267
1444
  if (!result.updated) {
1268
1445
  if (result.source === "plugin" || result.sourceKind) {
1446
+ await rollbackProjectSync();
1269
1447
  throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
1270
1448
  }
1271
1449
  appendRunLogEntry(projectRoot, run.runId, {
@@ -1278,6 +1456,277 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
1278
1456
  });
1279
1457
  }
1280
1458
  }
1459
+ async function markServerOwnedCloseoutFailed(state, runId, error) {
1460
+ const detail = error instanceof Error ? error.message : String(error);
1461
+ const current = readAuthorityRun9(state.projectRoot, runId);
1462
+ patchRunRecord(state.projectRoot, runId, {
1463
+ status: "failed",
1464
+ completedAt: new Date().toISOString(),
1465
+ errorText: detail,
1466
+ ...closeoutPhasePatch("failed", "failed", { error: detail })
1467
+ });
1468
+ appendRunLogEntryAndBroadcast(state, runId, {
1469
+ id: `log:${runId}:server-closeout-failed`,
1470
+ title: "Server-owned closeout failed",
1471
+ detail,
1472
+ tone: "error",
1473
+ status: "failed",
1474
+ createdAt: new Date().toISOString()
1475
+ }, "server-closeout-failed");
1476
+ if (current?.taskId) {
1477
+ await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: detail }, "failed", "Rig server-owned closeout failed.", { errorText: detail }).catch((sourceError) => {
1478
+ appendRunLogEntry(state.projectRoot, runId, {
1479
+ id: `log:${runId}:task-source-closeout-failed-update`,
1480
+ title: "Task source closeout failure update failed",
1481
+ detail: sourceError instanceof Error ? sourceError.message : String(sourceError),
1482
+ tone: "error",
1483
+ status: "failed",
1484
+ createdAt: new Date().toISOString()
1485
+ });
1486
+ });
1487
+ }
1488
+ }
1489
+ function scheduleServerOwnedPrCloseout(state, runId, reason) {
1490
+ const startedAt = new Date().toISOString();
1491
+ state.runProcesses.set(runId, {
1492
+ runId,
1493
+ child: null,
1494
+ startedAt,
1495
+ stopped: false
1496
+ });
1497
+ queueMicrotask(() => {
1498
+ withServerAuthorityEnvIfNeeded(state.projectRoot, async () => {
1499
+ try {
1500
+ await runServerOwnedPrCloseout(state, runId);
1501
+ } catch (error) {
1502
+ await markServerOwnedCloseoutFailed(state, runId, error);
1503
+ } finally {
1504
+ state.runProcesses.delete(runId);
1505
+ broadcastSnapshotInvalidation(state, `server-closeout-${reason}-terminal`);
1506
+ await reconcileScheduler(state, `server-closeout-${reason}-terminal`);
1507
+ }
1508
+ });
1509
+ });
1510
+ }
1511
+ async function runServerOwnedPrCloseout(state, runId) {
1512
+ const run = readAuthorityRun9(state.projectRoot, runId);
1513
+ if (!run)
1514
+ throw new Error(`Run not found: ${runId}`);
1515
+ const closeout = closeoutRecord(run);
1516
+ if (!closeout)
1517
+ return;
1518
+ const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
1519
+ if (!taskId)
1520
+ throw new Error("Server-owned closeout requires a task id.");
1521
+ const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
1522
+ let branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
1523
+ const config = await loadRigLifecycleConfig(state.projectRoot);
1524
+ const runPrMode = normalizeString(run.prMode);
1525
+ const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
1526
+ const effectiveConfig = {
1527
+ ...config ?? {},
1528
+ pr: {
1529
+ ...config?.pr ?? {},
1530
+ mode: prMode,
1531
+ autoFixChecks: false,
1532
+ autoFixReview: false
1533
+ }
1534
+ };
1535
+ const readCurrentRun = () => readAuthorityRun9(state.projectRoot, runId) ?? run;
1536
+ const sourceTask = runSourceTaskIdentity(run);
1537
+ const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
1538
+ const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
1539
+ const closeoutPrUrl = normalizeString(closeout.prUrl);
1540
+ if (closeoutPhase === "completed" || closeoutStatus === "completed") {
1541
+ return;
1542
+ }
1543
+ if (closeoutPhase === "close-source" && closeoutPrUrl) {
1544
+ patchRunRecord(state.projectRoot, runId, {
1545
+ status: "reviewing",
1546
+ ...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
1547
+ });
1548
+ await closeIssueAfterMergedPr({
1549
+ projectRoot: state.projectRoot,
1550
+ taskId,
1551
+ runId,
1552
+ prUrl: closeoutPrUrl,
1553
+ sourceTask,
1554
+ updateTaskSource: async (projectRoot, input) => {
1555
+ await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
1556
+ return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
1557
+ }
1558
+ });
1559
+ const completedAt = new Date().toISOString();
1560
+ patchRunRecord(state.projectRoot, runId, {
1561
+ status: "completed",
1562
+ completedAt,
1563
+ errorText: null,
1564
+ ...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
1565
+ });
1566
+ appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
1567
+ emitRigEvent(state, {
1568
+ type: "rig.run.completed",
1569
+ aggregateId: runId,
1570
+ payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
1571
+ createdAt: completedAt
1572
+ });
1573
+ return;
1574
+ }
1575
+ if (prMode === "off" || prMode === "ask") {
1576
+ const completedAt = new Date().toISOString();
1577
+ patchRunRecord(state.projectRoot, runId, {
1578
+ status: "completed",
1579
+ completedAt,
1580
+ errorText: null,
1581
+ ...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
1582
+ });
1583
+ appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
1584
+ emitRigEvent(state, {
1585
+ type: "rig.run.completed",
1586
+ aggregateId: runId,
1587
+ payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
1588
+ createdAt: completedAt
1589
+ });
1590
+ return;
1591
+ }
1592
+ const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
1593
+ const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
1594
+ const gitCommand = createCommandRunner("git", githubEnv);
1595
+ const ghCommand = createCommandRunner("gh", githubEnv);
1596
+ const workspaceBranch = await gitCommand(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: workspace });
1597
+ const currentWorkspaceBranch = workspaceBranch.exitCode === 0 ? normalizeString(workspaceBranch.stdout) : null;
1598
+ if (currentWorkspaceBranch && currentWorkspaceBranch !== "HEAD" && currentWorkspaceBranch !== branch) {
1599
+ appendCloseoutStage(state, runId, "branch", `Using runtime workspace branch ${currentWorkspaceBranch} instead of recorded branch ${branch}.`, "reviewing", "info");
1600
+ branch = currentWorkspaceBranch;
1601
+ }
1602
+ const setCloseout = (phase, status, extra = {}) => {
1603
+ const previous = closeoutRecord(readCurrentRun()) ?? closeout;
1604
+ patchRunRecord(state.projectRoot, runId, {
1605
+ status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
1606
+ ...closeoutPhasePatch(phase, status, { ...previous, ...extra })
1607
+ });
1608
+ };
1609
+ setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
1610
+ appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
1611
+ const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
1612
+ appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
1613
+ setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
1614
+ const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
1615
+ if (push.exitCode !== 0) {
1616
+ throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
1617
+ }
1618
+ const sourceTaskForPr = {
1619
+ title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
1620
+ };
1621
+ const artifactRoot = resolve10(state.projectRoot, "artifacts", taskId);
1622
+ setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
1623
+ const pr = await runPrAutomation({
1624
+ projectRoot: workspace,
1625
+ taskId,
1626
+ runId,
1627
+ branch,
1628
+ config: effectiveConfig,
1629
+ sourceTask: sourceTaskForPr,
1630
+ artifactRoot,
1631
+ command: ghCommand,
1632
+ gitCommand,
1633
+ steerPi: async (message) => {
1634
+ appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
1635
+ appendRunTimelineEntry(state.projectRoot, runId, {
1636
+ id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
1637
+ type: "user_message",
1638
+ text: message,
1639
+ createdAt: new Date().toISOString(),
1640
+ state: "completed"
1641
+ });
1642
+ },
1643
+ lifecycle: {
1644
+ onPrOpened: async ({ prUrl }) => {
1645
+ setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
1646
+ appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
1647
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
1648
+ },
1649
+ onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
1650
+ onFeedback: async ({ feedback }) => {
1651
+ appendCloseoutStage(state, runId, "feedback", feedback.join(`
1652
+ `), "reviewing", "error");
1653
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
1654
+ },
1655
+ onMergeStarted: async ({ prUrl }) => {
1656
+ setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
1657
+ appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
1658
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
1659
+ },
1660
+ onMerged: ({ prUrl }) => {
1661
+ setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
1662
+ appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
1663
+ }
1664
+ }
1665
+ });
1666
+ if (pr.status === "merged" && pr.prUrl) {
1667
+ setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
1668
+ await closeIssueAfterMergedPr({
1669
+ projectRoot: state.projectRoot,
1670
+ taskId,
1671
+ runId,
1672
+ prUrl: pr.prUrl,
1673
+ sourceTask,
1674
+ updateTaskSource: async (projectRoot, input) => {
1675
+ await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
1676
+ return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
1677
+ }
1678
+ });
1679
+ const completedAt = new Date().toISOString();
1680
+ patchRunRecord(state.projectRoot, runId, {
1681
+ status: "completed",
1682
+ completedAt,
1683
+ errorText: null,
1684
+ ...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
1685
+ });
1686
+ appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
1687
+ emitRigEvent(state, {
1688
+ type: "rig.run.completed",
1689
+ aggregateId: runId,
1690
+ payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
1691
+ createdAt: completedAt
1692
+ });
1693
+ return;
1694
+ }
1695
+ if (pr.status === "opened" && pr.prUrl) {
1696
+ const completedAt = new Date().toISOString();
1697
+ patchRunRecord(state.projectRoot, runId, {
1698
+ status: "completed",
1699
+ completedAt,
1700
+ errorText: null,
1701
+ ...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
1702
+ });
1703
+ appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
1704
+ emitRigEvent(state, {
1705
+ type: "rig.run.completed",
1706
+ aggregateId: runId,
1707
+ payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
1708
+ createdAt: completedAt
1709
+ });
1710
+ return;
1711
+ }
1712
+ const detail = pr.actionableFeedback.join(`
1713
+ `) || "PR automation did not merge the PR.";
1714
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
1715
+ appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
1716
+ });
1717
+ patchRunRecord(state.projectRoot, runId, {
1718
+ status: "needs_attention",
1719
+ completedAt: new Date().toISOString(),
1720
+ errorText: detail,
1721
+ ...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
1722
+ });
1723
+ appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
1724
+ emitRigEvent(state, {
1725
+ type: "rig.run.needs-attention",
1726
+ aggregateId: runId,
1727
+ payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
1728
+ });
1729
+ }
1281
1730
  var TERMINAL_RUN_STATUSES2 = new Set([
1282
1731
  "completed",
1283
1732
  "complete",
@@ -1303,11 +1752,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
1303
1752
  return;
1304
1753
  throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
1305
1754
  }
1755
+ async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
1756
+ const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
1757
+ if (fromReader)
1758
+ return fromReader;
1759
+ const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
1760
+ if (projected)
1761
+ return projected;
1762
+ if (readTasks !== readWorkspaceTasks) {
1763
+ return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
1764
+ }
1765
+ return null;
1766
+ }
1306
1767
  async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
1307
1768
  if ("taskId" in input && input.taskId) {
1308
1769
  assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
1309
1770
  }
1310
- const sourceTask = "taskId" in input && input.taskId ? (await readTasks(projectRoot)).find((task) => task.id === input.taskId) ?? null : null;
1771
+ const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
1311
1772
  const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
1312
1773
  const runDir = resolveAuthorityRunDir4(projectRoot, input.runId);
1313
1774
  const runRecord = {
@@ -1341,11 +1802,11 @@ async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTask
1341
1802
  initiatedBy: input.initiatedBy ?? null,
1342
1803
  ...sourceTask ? { sourceTask: sourceTaskContract(sourceTask) } : {}
1343
1804
  };
1344
- mkdirSync5(runDir, { recursive: true });
1345
- writeFileSync5(resolve9(runDir, "run.json"), `${JSON.stringify(runRecord, null, 2)}
1805
+ mkdirSync6(runDir, { recursive: true });
1806
+ writeFileSync6(resolve10(runDir, "run.json"), `${JSON.stringify(runRecord, null, 2)}
1346
1807
  `, "utf8");
1347
1808
  if ("initialPrompt" in input && input.initialPrompt && input.initialPrompt.trim().length > 0) {
1348
- writeFileSync5(resolve9(runDir, "timeline.jsonl"), `${JSON.stringify({
1809
+ writeFileSync6(resolve10(runDir, "timeline.jsonl"), `${JSON.stringify({
1349
1810
  id: `message-${Date.now()}`,
1350
1811
  type: "user_message",
1351
1812
  text: input.initialPrompt,
@@ -1374,11 +1835,12 @@ async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTask
1374
1835
  }
1375
1836
  }
1376
1837
  async function startLocalRun(state, runId, options) {
1377
- const run = readAuthorityRun8(state.projectRoot, runId);
1838
+ const run = readAuthorityRun9(state.projectRoot, runId);
1378
1839
  if (!run) {
1379
1840
  throw new Error(`Run not found: ${runId}`);
1380
1841
  }
1381
1842
  const startedAt = new Date().toISOString();
1843
+ const resumeMode = options?.resume === true;
1382
1844
  state.runProcesses.set(runId, {
1383
1845
  runId,
1384
1846
  child: null,
@@ -1395,9 +1857,9 @@ async function startLocalRun(state, runId, options) {
1395
1857
  summary: run.title
1396
1858
  });
1397
1859
  appendRunLogEntry(state.projectRoot, runId, {
1398
- id: `log:${runId}:prepare`,
1399
- title: "Rig task run starting",
1400
- detail: run.taskId ?? run.title,
1860
+ id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
1861
+ title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
1862
+ detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
1401
1863
  tone: "info",
1402
1864
  status: "preparing",
1403
1865
  createdAt: startedAt
@@ -1405,8 +1867,8 @@ async function startLocalRun(state, runId, options) {
1405
1867
  broadcastRunLogAppended(state, runId, readLatestRawRunLog(state.projectRoot, runId));
1406
1868
  broadcastSnapshotInvalidation(state);
1407
1869
  const cliProjectRoot = resolveLocalRunCliProjectRoot(state.projectRoot);
1408
- const cliEntryPoint = resolve9(cliProjectRoot, "packages/cli/bin/rig.ts");
1409
- if (!existsSync6(cliEntryPoint)) {
1870
+ const cliEntryPoint = resolve10(cliProjectRoot, "packages/cli/bin/rig.ts");
1871
+ if (!existsSync7(cliEntryPoint)) {
1410
1872
  const completedAt = new Date().toISOString();
1411
1873
  const failureSummary = `Rig task-run entrypoint missing at ${relative2(state.projectRoot, cliEntryPoint)}`;
1412
1874
  patchRunRecord(state.projectRoot, runId, {
@@ -1473,12 +1935,17 @@ async function startLocalRun(state, runId, options) {
1473
1935
  RIG_HOST_PROJECT_ROOT: cliProjectRoot,
1474
1936
  RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
1475
1937
  RIG_SERVER_INTERNAL_EXEC: "1",
1938
+ RIG_SERVER_OWNS_CLOSEOUT: "1",
1476
1939
  ...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
1477
1940
  ...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
1478
1941
  ...bridgeGitHubToken ? {
1479
1942
  RIG_GITHUB_TOKEN: bridgeGitHubToken,
1480
1943
  GITHUB_TOKEN: bridgeGitHubToken,
1481
1944
  GH_TOKEN: bridgeGitHubToken
1945
+ } : {},
1946
+ ...resumeMode ? {
1947
+ RIG_RUN_RESUME: "1",
1948
+ RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
1482
1949
  } : {}
1483
1950
  },
1484
1951
  stdio: ["ignore", "pipe", "pipe"]
@@ -1502,6 +1969,25 @@ async function startLocalRun(state, runId, options) {
1502
1969
  broadcastSnapshotInvalidation(state);
1503
1970
  continue;
1504
1971
  }
1972
+ if (line.startsWith("__RIG_WRAPPER_EVENT__")) {
1973
+ try {
1974
+ const wrapperEvent = JSON.parse(line.slice("__RIG_WRAPPER_EVENT__".length));
1975
+ const eventType = normalizeString(wrapperEvent.type);
1976
+ const payload = wrapperEvent.payload && typeof wrapperEvent.payload === "object" && !Array.isArray(wrapperEvent.payload) ? wrapperEvent.payload : {};
1977
+ if (eventType === "pi.session.ready" && payload.privateMetadata && typeof payload.privateMetadata === "object" && !Array.isArray(payload.privateMetadata)) {
1978
+ patchRunPiSessionMetadata(state.projectRoot, runId, payload.privateMetadata);
1979
+ }
1980
+ appendRunTimelineEntry(state.projectRoot, runId, {
1981
+ id: `timeline:${runId}:${Date.now()}:wrapper:${eventType ?? "event"}`,
1982
+ type: "wrapper-event",
1983
+ eventType,
1984
+ payload,
1985
+ createdAt: normalizeString(wrapperEvent.at) ?? new Date().toISOString()
1986
+ });
1987
+ } catch {}
1988
+ broadcastSnapshotInvalidation(state, "wrapper-event");
1989
+ continue;
1990
+ }
1505
1991
  appendRunLogEntryAndBroadcast(state, runId, {
1506
1992
  id: `log:${runId}:${Date.now()}`,
1507
1993
  title,
@@ -1519,18 +2005,24 @@ async function startLocalRun(state, runId, options) {
1519
2005
  handleRunProcessOutput(Buffer.isBuffer(data) ? data.toString("utf8") : String(data), "error", "Rig task run stderr");
1520
2006
  });
1521
2007
  try {
1522
- const exit = await new Promise((resolve10) => {
1523
- child.once("error", (error) => resolve10({ code: 1, signal: null, error }));
1524
- child.once("close", (code, signal) => resolve10({ code, signal }));
2008
+ const exit = await new Promise((resolve11) => {
2009
+ child.once("error", (error) => resolve11({ code: 1, signal: null, error }));
2010
+ child.once("close", (code, signal) => resolve11({ code, signal }));
1525
2011
  });
1526
2012
  if (exit.error) {
1527
2013
  throw new Error(`Failed to start task run: ${exit.error.message}`);
1528
2014
  }
1529
- const current = readAuthorityRun8(state.projectRoot, runId);
2015
+ const current = readAuthorityRun9(state.projectRoot, runId);
1530
2016
  if (!current) {
1531
2017
  return;
1532
2018
  }
1533
- if (exit.code !== 0 && current.status !== "completed" && current.status !== "stopped") {
2019
+ if (closeoutRecord(current)?.status === "pending") {
2020
+ try {
2021
+ await runServerOwnedPrCloseout(state, runId);
2022
+ } catch (closeoutError) {
2023
+ await markServerOwnedCloseoutFailed(state, runId, closeoutError);
2024
+ }
2025
+ } else if (exit.code !== 0 && current.status !== "completed" && current.status !== "stopped") {
1534
2026
  const completedAt = current.completedAt ?? new Date().toISOString();
1535
2027
  const failureSummary = normalizeString(current.errorText) ?? summarizeRunValidationFailure(state.projectRoot, current) ?? `Rig task-run exited with code ${String(exit.code ?? "unknown")}`;
1536
2028
  if (current.status !== "failed") {
@@ -1621,24 +2113,24 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
1621
2113
  process.env.PROJECT_RIG_ROOT?.trim()
1622
2114
  ].filter((value) => !!value);
1623
2115
  for (const candidate of envCandidates) {
1624
- if (existsSync6(resolve9(candidate, "packages/cli/bin/rig.ts"))) {
1625
- return resolve9(candidate);
2116
+ if (existsSync7(resolve10(candidate, "packages/cli/bin/rig.ts"))) {
2117
+ return resolve10(candidate);
1626
2118
  }
1627
2119
  }
1628
- if (existsSync6(resolve9(projectRoot, "packages/cli/bin/rig.ts"))) {
2120
+ if (existsSync7(resolve10(projectRoot, "packages/cli/bin/rig.ts"))) {
1629
2121
  return projectRoot;
1630
2122
  }
1631
2123
  try {
1632
2124
  const monorepoRoot = resolveMonorepoRoot6(projectRoot);
1633
- const outerProjectRoot = dirname5(dirname5(monorepoRoot));
1634
- if (existsSync6(resolve9(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
2125
+ const outerProjectRoot = dirname6(dirname6(monorepoRoot));
2126
+ if (existsSync7(resolve10(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
1635
2127
  return outerProjectRoot;
1636
2128
  }
1637
2129
  } catch {}
1638
2130
  return projectRoot;
1639
2131
  }
1640
2132
  async function resumeRunRecord(state, input) {
1641
- const run = readAuthorityRun8(state.projectRoot, input.runId);
2133
+ const run = readAuthorityRun9(state.projectRoot, input.runId);
1642
2134
  if (!run) {
1643
2135
  throw new Error(`Run not found: ${input.runId}`);
1644
2136
  }
@@ -1651,15 +2143,27 @@ async function resumeRunRecord(state, input) {
1651
2143
  if (run.status === "completed") {
1652
2144
  throw new Error("Completed runs cannot be resumed.");
1653
2145
  }
1654
- await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
2146
+ const closeout = closeoutRecord(run);
2147
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
2148
+ if (EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
2149
+ patchRunRecord(state.projectRoot, input.runId, {
2150
+ status: "reviewing",
2151
+ completedAt: null,
2152
+ errorText: null,
2153
+ ...closeoutPhasePatch("queued", "pending", { ...closeout, resumedAt: input.createdAt })
2154
+ });
2155
+ scheduleServerOwnedPrCloseout(state, input.runId, "explicit-resume");
2156
+ return;
2157
+ }
2158
+ await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
1655
2159
  }
1656
2160
  function appendRunMessage(projectRoot, input) {
1657
- const run = readAuthorityRun8(projectRoot, input.runId);
2161
+ const run = readAuthorityRun9(projectRoot, input.runId);
1658
2162
  if (!run) {
1659
2163
  throw new Error(`Run not found: ${input.runId}`);
1660
2164
  }
1661
- const timelinePath = resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "timeline.jsonl");
1662
- const existingLines = fileExists(timelinePath) ? readFileSync3(timelinePath, "utf8").trim() : "";
2165
+ const timelinePath = resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "timeline.jsonl");
2166
+ const existingLines = fileExists(timelinePath) ? readFileSync4(timelinePath, "utf8").trim() : "";
1663
2167
  const nextLine = JSON.stringify({
1664
2168
  id: input.messageId,
1665
2169
  type: "user_message",
@@ -1667,18 +2171,18 @@ function appendRunMessage(projectRoot, input) {
1667
2171
  attachments: input.attachments ?? [],
1668
2172
  createdAt: input.createdAt
1669
2173
  });
1670
- writeFileSync5(timelinePath, existingLines.length > 0 ? `${existingLines}
2174
+ writeFileSync6(timelinePath, existingLines.length > 0 ? `${existingLines}
1671
2175
  ${nextLine}
1672
2176
  ` : `${nextLine}
1673
2177
  `, "utf8");
1674
- writeJsonFile4(resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), {
2178
+ writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), {
1675
2179
  ...run,
1676
2180
  updatedAt: input.createdAt
1677
2181
  });
1678
2182
  }
1679
2183
  async function stopRunRecord(stateOrProjectRoot, input) {
1680
2184
  const projectRoot = typeof stateOrProjectRoot === "string" ? stateOrProjectRoot : stateOrProjectRoot.projectRoot;
1681
- const run = readAuthorityRun8(projectRoot, input.runId);
2185
+ const run = readAuthorityRun9(projectRoot, input.runId);
1682
2186
  if (!run) {
1683
2187
  throw new Error(`Run not found: ${input.runId}`);
1684
2188
  }
@@ -1697,7 +2201,7 @@ async function stopRunRecord(stateOrProjectRoot, input) {
1697
2201
  completedAt: run.completedAt ?? input.createdAt,
1698
2202
  updatedAt: input.createdAt
1699
2203
  };
1700
- writeJsonFile4(resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), nextRun);
2204
+ writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), nextRun);
1701
2205
  if (run.status !== "completed" && run.taskId) {
1702
2206
  const taskId = run.taskId;
1703
2207
  (async () => {
@@ -1735,34 +2239,63 @@ function removeTaskIdsFromQueueState2(projectRoot, taskIds) {
1735
2239
  writeQueueState(projectRoot, next);
1736
2240
  return next;
1737
2241
  }
1738
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
1739
- function reconcileOrphanedLocalRuns(state, runs, nowIso) {
1740
- let changed = false;
1741
- for (const run of runs) {
1742
- const status = normalizeString(run.status)?.toLowerCase() ?? "";
1743
- const serverPid = run.serverPid;
1744
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
1745
- if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
1746
- continue;
1747
- }
1748
- const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
1749
- patchRunRecord(state.projectRoot, run.runId, {
1750
- status: "failed",
1751
- completedAt: run.completedAt ?? nowIso,
1752
- updatedAt: nowIso,
1753
- errorText: detail
1754
- });
1755
- appendRunLogEntry(state.projectRoot, run.runId, {
1756
- id: `log:${run.runId}:stale-local-run`,
1757
- title: "Run marked stale after server restart",
1758
- detail,
1759
- tone: "error",
1760
- status: "failed",
1761
- createdAt: nowIso
2242
+ var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
2243
+ var EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running", "needs_attention"]);
2244
+ var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
2245
+ function processExists(pid) {
2246
+ if (!Number.isInteger(pid) || pid <= 0)
2247
+ return false;
2248
+ try {
2249
+ process.kill(pid, 0);
2250
+ return true;
2251
+ } catch {
2252
+ return false;
2253
+ }
2254
+ }
2255
+ function recoverStaleLocalRun(projectRoot, run) {
2256
+ const record = run;
2257
+ if (run.mode !== "local")
2258
+ return false;
2259
+ const closeout = closeoutRecord(record);
2260
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
2261
+ const status = normalizeString(record.status)?.toLowerCase() ?? "";
2262
+ if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus))
2263
+ return false;
2264
+ if (closeoutStatus === "needs_attention") {
2265
+ if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
2266
+ return false;
2267
+ const completedAt2 = record.completedAt ?? new Date().toISOString();
2268
+ patchRunRecord(projectRoot, run.runId, {
2269
+ status: "needs_attention",
2270
+ completedAt: completedAt2,
2271
+ errorText: normalizeString(record.errorText) ?? (Array.isArray(closeout?.feedback) ? closeout.feedback.map(String).join(`
2272
+ `) : null)
1762
2273
  });
1763
- changed = true;
2274
+ return true;
1764
2275
  }
1765
- return changed;
2276
+ if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
2277
+ return false;
2278
+ const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
2279
+ const childPid = typeof record.pid === "number" ? record.pid : null;
2280
+ if (serverPid === null && childPid === null)
2281
+ return false;
2282
+ const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
2283
+ if (hasLiveRecordedProcess && serverPid === process.pid)
2284
+ return false;
2285
+ const completedAt = new Date().toISOString();
2286
+ patchRunRecord(projectRoot, run.runId, {
2287
+ status: "failed",
2288
+ completedAt,
2289
+ errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
2290
+ });
2291
+ return true;
2292
+ }
2293
+ function collectResumableServerCloseouts(state, runs) {
2294
+ return runs.filter((run) => {
2295
+ const closeout = closeoutRecord(run);
2296
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
2297
+ return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
2298
+ });
1766
2299
  }
1767
2300
  async function reconcileScheduler(state, reason) {
1768
2301
  if (state.scheduler.reconciling) {
@@ -1777,7 +2310,28 @@ async function reconcileScheduler(state, reason) {
1777
2310
  const queue = readQueueState(state.projectRoot);
1778
2311
  const tasks = await state.snapshotService.getWorkspaceTasks();
1779
2312
  let runs = listAuthorityRuns7(state.projectRoot);
1780
- let changed = reconcileOrphanedLocalRuns(state, runs, new Date().toISOString());
2313
+ let changed = false;
2314
+ for (const run of runs) {
2315
+ if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
2316
+ changed = true;
2317
+ }
2318
+ }
2319
+ if (changed) {
2320
+ runs = listAuthorityRuns7(state.projectRoot);
2321
+ }
2322
+ const resumableCloseouts = collectResumableServerCloseouts(state, runs);
2323
+ for (const run of resumableCloseouts) {
2324
+ appendRunLogEntry(state.projectRoot, run.runId, {
2325
+ id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
2326
+ title: "Server-owned closeout auto-resume scheduled",
2327
+ detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
2328
+ tone: "info",
2329
+ status: "reviewing",
2330
+ createdAt: new Date().toISOString()
2331
+ });
2332
+ scheduleServerOwnedPrCloseout(state, run.runId, "auto-resume");
2333
+ changed = true;
2334
+ }
1781
2335
  if (changed) {
1782
2336
  runs = listAuthorityRuns7(state.projectRoot);
1783
2337
  }
@@ -1850,8 +2404,10 @@ async function reconcileScheduler(state, reason) {
1850
2404
  }
1851
2405
  }
1852
2406
  export {
2407
+ updateRunTaskSourceLifecycle,
1853
2408
  stopRunRecord,
1854
2409
  startLocalRun,
2410
+ runServerOwnedPrCloseout,
1855
2411
  resumeRunRecord,
1856
2412
  resolveLocalRunCliProjectRoot,
1857
2413
  removeTaskIdsFromQueueState2 as removeTaskIdsFromQueueState,