@h-rig/server 0.0.6-alpha.1 → 0.0.6-alpha.11

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,
@@ -98,8 +98,8 @@ function normalizeStatus(value) {
98
98
  }
99
99
 
100
100
  // 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";
101
+ import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
102
+ import { dirname as dirname5, resolve as resolve8 } from "path";
103
103
  import {
104
104
  listAuthorityArtifactRoots,
105
105
  listAuthorityRuns as listAuthorityRuns6,
@@ -295,6 +295,24 @@ var snapshotCache = new Map;
295
295
  var contextCache = new Map;
296
296
  var taskListCache = new Map;
297
297
 
298
+ // packages/server/src/server-helpers/task-projection.ts
299
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync, writeFileSync as writeFileSync3 } from "fs";
300
+ import { resolve as resolve5 } from "path";
301
+ function projectionPath(projectRoot) {
302
+ return resolve5(projectRoot, ".rig", "state", "task-projection.json");
303
+ }
304
+ function readTaskProjection(projectRoot) {
305
+ const file = projectionPath(projectRoot);
306
+ if (!existsSync3(file))
307
+ return null;
308
+ try {
309
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
310
+ return parsed && parsed.version === 1 && Array.isArray(parsed.tasks) ? parsed : null;
311
+ } catch {
312
+ return null;
313
+ }
314
+ }
315
+
298
316
  // packages/server/src/server-helpers/terminal-runtime.ts
299
317
  import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
300
318
 
@@ -302,7 +320,7 @@ import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
302
320
  import { RIG_WS_CHANNELS } from "@rig/contracts";
303
321
 
304
322
  // packages/server/src/server-helpers/run-writers.ts
305
- import { resolve as resolve5 } from "path";
323
+ import { resolve as resolve6 } from "path";
306
324
  import {
307
325
  appendJsonlRecord,
308
326
  readAuthorityRun as readAuthorityRun3,
@@ -327,7 +345,7 @@ function patchRunRecord(projectRoot, runId, patch) {
327
345
  ...patch,
328
346
  updatedAt: normalizeString(patch.updatedAt) ?? new Date().toISOString()
329
347
  };
330
- writeJsonFile2(resolve5(resolveAuthorityRunDir2(projectRoot, runId), "run.json"), next);
348
+ writeJsonFile2(resolve6(resolveAuthorityRunDir2(projectRoot, runId), "run.json"), next);
331
349
  return next;
332
350
  }
333
351
  function buildRunStartPatch(startedAt) {
@@ -459,8 +477,9 @@ import {
459
477
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
460
478
 
461
479
  // packages/server/src/server-helpers/github-auth-store.ts
462
- import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync, writeFileSync as writeFileSync3 } from "fs";
463
- import { resolve as resolve6 } from "path";
480
+ import { randomBytes } from "crypto";
481
+ import { chmodSync, copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "fs";
482
+ import { dirname as dirname4, resolve as resolve7 } from "path";
464
483
  function cleanString(value) {
465
484
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
466
485
  }
@@ -472,11 +491,49 @@ function cleanScopes(value) {
472
491
  return clean ? [clean] : [];
473
492
  });
474
493
  }
494
+ function parseApiSessions(value) {
495
+ if (!Array.isArray(value))
496
+ return [];
497
+ return value.flatMap((entry) => {
498
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
499
+ return [];
500
+ const record = entry;
501
+ const token = cleanString(record.token);
502
+ if (!token)
503
+ return [];
504
+ return [{
505
+ token,
506
+ login: cleanString(record.login),
507
+ userId: cleanString(record.userId),
508
+ createdAt: cleanString(record.createdAt) ?? undefined
509
+ }];
510
+ });
511
+ }
512
+ function parsePendingDevice(value) {
513
+ if (!value || typeof value !== "object")
514
+ return null;
515
+ const record = value;
516
+ const pollId = cleanString(record.pollId);
517
+ const deviceCode = cleanString(record.deviceCode);
518
+ const expiresAt = cleanString(record.expiresAt);
519
+ const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
520
+ if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
521
+ return null;
522
+ return { pollId, deviceCode, expiresAt, intervalSeconds };
523
+ }
524
+ function parsePendingDevices(value) {
525
+ if (!Array.isArray(value))
526
+ return [];
527
+ return value.flatMap((entry) => {
528
+ const pending = parsePendingDevice(entry);
529
+ return pending ? [pending] : [];
530
+ });
531
+ }
475
532
  function readStoredAuth(stateFile) {
476
- if (!existsSync3(stateFile))
533
+ if (!existsSync4(stateFile))
477
534
  return {};
478
535
  try {
479
- const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
536
+ const parsed = JSON.parse(readFileSync2(stateFile, "utf8"));
480
537
  return {
481
538
  ...cleanString(parsed.token) ? { token: cleanString(parsed.token) } : {},
482
539
  login: cleanString(parsed.login),
@@ -485,37 +542,44 @@ function readStoredAuth(stateFile) {
485
542
  selectedRepo: cleanString(parsed.selectedRepo),
486
543
  tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
487
544
  pendingDevice: parsePendingDevice(parsed.pendingDevice),
545
+ pendingDevices: parsePendingDevices(parsed.pendingDevices),
546
+ apiSessions: parseApiSessions(parsed.apiSessions),
488
547
  updatedAt: cleanString(parsed.updatedAt) ?? undefined
489
548
  };
490
549
  } catch {
491
550
  return {};
492
551
  }
493
552
  }
494
- function parsePendingDevice(value) {
495
- if (!value || typeof value !== "object")
496
- return null;
497
- const record = value;
498
- const pollId = cleanString(record.pollId);
499
- const deviceCode = cleanString(record.deviceCode);
500
- const expiresAt = cleanString(record.expiresAt);
501
- const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
502
- if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
503
- return null;
504
- return { pollId, deviceCode, expiresAt, intervalSeconds };
553
+ function newApiSessionToken() {
554
+ return `rig_${randomBytes(32).toString("base64url")}`;
505
555
  }
506
556
  function writeStoredAuth(stateFile, payload) {
507
- mkdirSync3(resolve6(stateFile, ".."), { recursive: true });
508
- writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
557
+ mkdirSync4(dirname4(stateFile), { recursive: true });
558
+ writeFileSync4(stateFile, `${JSON.stringify(payload, null, 2)}
509
559
  `, { encoding: "utf8", mode: 384 });
510
560
  try {
511
561
  chmodSync(stateFile, 384);
512
562
  } catch {}
513
563
  }
564
+ function localProjectAuthStateFile(projectRoot) {
565
+ return resolve7(projectRoot, ".rig", "state", "github-auth.json");
566
+ }
514
567
  function resolveGitHubAuthStateFile(projectRoot) {
515
- return resolve6(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
568
+ return resolve7(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
516
569
  }
517
- function createGitHubAuthStore(projectRoot) {
518
- const stateFile = resolveGitHubAuthStateFile(projectRoot);
570
+ function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
571
+ const targetFile = localProjectAuthStateFile(projectRoot);
572
+ mkdirSync4(dirname4(targetFile), { recursive: true });
573
+ if (existsSync4(stateFile)) {
574
+ copyFileSync(stateFile, targetFile);
575
+ try {
576
+ chmodSync(targetFile, 384);
577
+ } catch {}
578
+ return;
579
+ }
580
+ writeStoredAuth(targetFile, {});
581
+ }
582
+ function createGitHubAuthStoreFromStateFile(stateFile) {
519
583
  return {
520
584
  stateFile,
521
585
  status(options) {
@@ -545,14 +609,53 @@ function createGitHubAuthStore(projectRoot) {
545
609
  scopes: input.scopes ?? [],
546
610
  selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
547
611
  pendingDevice: null,
612
+ pendingDevices: [],
613
+ apiSessions: previous.apiSessions ?? [],
548
614
  updatedAt: new Date().toISOString()
549
615
  });
550
616
  },
617
+ createApiSession() {
618
+ const previous = readStoredAuth(stateFile);
619
+ const token = newApiSessionToken();
620
+ const session = {
621
+ token,
622
+ login: cleanString(previous.login),
623
+ userId: cleanString(previous.userId),
624
+ createdAt: new Date().toISOString()
625
+ };
626
+ writeStoredAuth(stateFile, {
627
+ ...previous,
628
+ apiSessions: [...(previous.apiSessions ?? []).slice(-9), session],
629
+ updatedAt: new Date().toISOString()
630
+ });
631
+ return { token, login: session.login ?? null, userId: session.userId ?? null };
632
+ },
633
+ readApiSession(token) {
634
+ const clean = cleanString(token);
635
+ if (!clean)
636
+ return null;
637
+ const previous = readStoredAuth(stateFile);
638
+ const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
639
+ return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
640
+ },
641
+ copyToProjectRoot(projectRoot) {
642
+ const targetFile = resolveGitHubAuthStateFile(projectRoot);
643
+ writeStoredAuth(targetFile, readStoredAuth(stateFile));
644
+ },
645
+ copyToLocalProjectRoot(projectRoot) {
646
+ copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
647
+ },
551
648
  savePendingDevice(input) {
552
649
  const previous = readStoredAuth(stateFile);
650
+ const pendingDevices = [
651
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
652
+ ...previous.pendingDevices ?? [],
653
+ input
654
+ ].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
553
655
  writeStoredAuth(stateFile, {
554
656
  ...previous,
555
- pendingDevice: input,
657
+ pendingDevice: null,
658
+ pendingDevices,
556
659
  updatedAt: new Date().toISOString()
557
660
  });
558
661
  },
@@ -565,23 +668,32 @@ function createGitHubAuthStore(projectRoot) {
565
668
  });
566
669
  },
567
670
  readPendingDevice(pollId) {
568
- const pending = readStoredAuth(stateFile).pendingDevice ?? null;
569
- if (!pending || pending.pollId !== pollId)
671
+ const previous = readStoredAuth(stateFile);
672
+ const pending = [
673
+ ...previous.pendingDevice ? [previous.pendingDevice] : [],
674
+ ...previous.pendingDevices ?? []
675
+ ].find((entry) => entry.pollId === pollId) ?? null;
676
+ if (!pending)
570
677
  return null;
571
678
  if (Date.parse(pending.expiresAt) <= Date.now())
572
679
  return null;
573
680
  return pending;
574
681
  },
575
- clearPendingDevice() {
682
+ clearPendingDevice(pollId) {
576
683
  const previous = readStoredAuth(stateFile);
684
+ const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
577
685
  writeStoredAuth(stateFile, {
578
686
  ...previous,
579
687
  pendingDevice: null,
688
+ pendingDevices: remaining,
580
689
  updatedAt: new Date().toISOString()
581
690
  });
582
691
  }
583
692
  };
584
693
  }
694
+ function createGitHubAuthStore(projectRoot) {
695
+ return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
696
+ }
585
697
 
586
698
  // packages/server/src/server-helpers/github-projects.ts
587
699
  function asRecord(value) {
@@ -775,10 +887,10 @@ function extractGitHubIssueNodeId(task) {
775
887
  }
776
888
 
777
889
  // packages/server/src/server-helpers/http-router.ts
778
- var RIG_CONFIG_PACKAGE_VERSION = "0.0.6-alpha.1";
890
+ var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
779
891
  var RIG_CONFIG_DEV_DEPENDENCIES = {
780
- "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_VERSION}`,
781
- "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_VERSION}`
892
+ "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
893
+ "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
782
894
  };
783
895
 
784
896
  // packages/server/src/server-helpers/ws-router.ts
@@ -917,10 +1029,10 @@ var CLUSTERS = {
917
1029
  };
918
1030
 
919
1031
  // packages/server/src/server-helpers/task-config.ts
920
- import { existsSync as existsSync4 } from "fs";
1032
+ import { existsSync as existsSync5 } from "fs";
921
1033
  async function readTaskConfig(projectRoot) {
922
1034
  const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
923
- if (!existsSync4(taskConfigPath)) {
1035
+ if (!existsSync5(taskConfigPath)) {
924
1036
  return {};
925
1037
  }
926
1038
  try {
@@ -937,8 +1049,8 @@ var serverPathEnvQueue = Promise.resolve();
937
1049
  async function withServerPathEnv(projectRoot, fn) {
938
1050
  const waitForTurn = serverPathEnvQueue;
939
1051
  let releaseTurn;
940
- serverPathEnvQueue = new Promise((resolve8) => {
941
- releaseTurn = resolve8;
1052
+ serverPathEnvQueue = new Promise((resolve9) => {
1053
+ releaseTurn = resolve9;
942
1054
  });
943
1055
  await waitForTurn;
944
1056
  const paths = resolveServerAuthorityPaths(projectRoot);
@@ -974,9 +1086,9 @@ async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
974
1086
  return withServerPathEnv(projectRoot, fn);
975
1087
  }
976
1088
  async function readWorkspaceTasks(projectRoot) {
977
- const issuesPath = resolve7(resolveMonorepoRoot5(projectRoot), ".beads", "issues.jsonl");
1089
+ const issuesPath = resolve8(resolveMonorepoRoot5(projectRoot), ".beads", "issues.jsonl");
978
1090
  const taskConfig = await readTaskConfig(projectRoot);
979
- if (!existsSync5(issuesPath)) {
1091
+ if (!existsSync6(issuesPath)) {
980
1092
  return [];
981
1093
  }
982
1094
  const latestById = new Map;
@@ -995,7 +1107,7 @@ async function readWorkspaceTasks(projectRoot) {
995
1107
  description: normalizeString(entry.description),
996
1108
  acceptanceCriteria: normalizeString(entry.acceptance_criteria),
997
1109
  status: normalizedStatus ?? "unknown",
998
- sourceStatus: normalizedStatus ? null : rawStatus,
1110
+ sourceStatus: normalizedStatus && rawStatus !== normalizedStatus ? rawStatus : normalizedStatus ? null : rawStatus,
999
1111
  priority: typeof entry.priority === "number" ? entry.priority : typeof entry.priority === "string" ? Number(entry.priority) : null,
1000
1112
  issueType: normalizeString(entry.issue_type),
1001
1113
  role: normalizeString(config.role) ?? null,
@@ -1022,7 +1134,7 @@ async function readWorkspaceTasks(projectRoot) {
1022
1134
  if (false) {}
1023
1135
 
1024
1136
  // packages/server/src/server-helpers/validation-failure.ts
1025
- import { resolve as resolve8 } from "path";
1137
+ import { resolve as resolve9 } from "path";
1026
1138
  import {
1027
1139
  readJsonFile as readJsonFile4,
1028
1140
  resolveTaskArtifactDirs
@@ -1036,7 +1148,7 @@ function summarizeRunValidationFailure(projectRoot, run) {
1036
1148
  continue;
1037
1149
  }
1038
1150
  seen.add(artifactRoot);
1039
- const summary = readJsonFile4(resolve8(artifactRoot, "validation-summary.json"), null);
1151
+ const summary = readJsonFile4(resolve9(artifactRoot, "validation-summary.json"), null);
1040
1152
  if (!summary || summary.status !== "fail") {
1041
1153
  continue;
1042
1154
  }
@@ -1120,15 +1232,36 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
1120
1232
  if (!run.taskId)
1121
1233
  return;
1122
1234
  const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
1123
- await syncGitHubProjectStatusForTaskUpdate({
1124
- taskId: run.taskId,
1125
- status,
1126
- issueNodeId,
1127
- token: createGitHubAuthStore(projectRoot).readToken(),
1128
- config
1129
- }).catch(() => {
1130
- return;
1131
- });
1235
+ try {
1236
+ const result = await syncGitHubProjectStatusForTaskUpdate({
1237
+ taskId: run.taskId,
1238
+ status,
1239
+ issueNodeId,
1240
+ token: createGitHubAuthStore(projectRoot).readToken(),
1241
+ config
1242
+ });
1243
+ if (!result.synced && result.reason !== "project-sync-disabled") {
1244
+ appendRunLogEntry(projectRoot, run.runId, {
1245
+ id: `log:${run.runId}:github-project-sync:${status}`,
1246
+ title: "GitHub Project sync skipped",
1247
+ detail: `Project status sync for ${run.taskId} could not run: ${result.reason}.`,
1248
+ tone: "warn",
1249
+ status: "running",
1250
+ createdAt: new Date().toISOString(),
1251
+ payload: { reason: result.reason, issueNodeId }
1252
+ });
1253
+ }
1254
+ } catch (error) {
1255
+ appendRunLogEntry(projectRoot, run.runId, {
1256
+ id: `log:${run.runId}:github-project-sync-error:${status}`,
1257
+ title: "GitHub Project sync failed",
1258
+ detail: error instanceof Error ? error.message : String(error),
1259
+ tone: "error",
1260
+ status: "running",
1261
+ createdAt: new Date().toISOString(),
1262
+ payload: { issueNodeId }
1263
+ });
1264
+ }
1132
1265
  }
1133
1266
  async function autoAssignRunIssue(projectRoot, run) {
1134
1267
  if (!run.taskId)
@@ -1230,11 +1363,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
1230
1363
  return;
1231
1364
  throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
1232
1365
  }
1366
+ async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
1367
+ const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
1368
+ if (fromReader)
1369
+ return fromReader;
1370
+ const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
1371
+ if (projected)
1372
+ return projected;
1373
+ if (readTasks !== readWorkspaceTasks) {
1374
+ return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
1375
+ }
1376
+ return null;
1377
+ }
1233
1378
  async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
1234
1379
  if ("taskId" in input && input.taskId) {
1235
1380
  assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
1236
1381
  }
1237
- const sourceTask = "taskId" in input && input.taskId ? (await readTasks(projectRoot)).find((task) => task.id === input.taskId) ?? null : null;
1382
+ const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
1238
1383
  const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
1239
1384
  const runDir = resolveAuthorityRunDir4(projectRoot, input.runId);
1240
1385
  const runRecord = {
@@ -1268,11 +1413,11 @@ async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTask
1268
1413
  initiatedBy: input.initiatedBy ?? null,
1269
1414
  ...sourceTask ? { sourceTask: sourceTaskContract(sourceTask) } : {}
1270
1415
  };
1271
- mkdirSync5(runDir, { recursive: true });
1272
- writeFileSync5(resolve9(runDir, "run.json"), `${JSON.stringify(runRecord, null, 2)}
1416
+ mkdirSync6(runDir, { recursive: true });
1417
+ writeFileSync6(resolve10(runDir, "run.json"), `${JSON.stringify(runRecord, null, 2)}
1273
1418
  `, "utf8");
1274
1419
  if ("initialPrompt" in input && input.initialPrompt && input.initialPrompt.trim().length > 0) {
1275
- writeFileSync5(resolve9(runDir, "timeline.jsonl"), `${JSON.stringify({
1420
+ writeFileSync6(resolve10(runDir, "timeline.jsonl"), `${JSON.stringify({
1276
1421
  id: `message-${Date.now()}`,
1277
1422
  type: "user_message",
1278
1423
  text: input.initialPrompt,
@@ -1306,6 +1451,7 @@ async function startLocalRun(state, runId, options) {
1306
1451
  throw new Error(`Run not found: ${runId}`);
1307
1452
  }
1308
1453
  const startedAt = new Date().toISOString();
1454
+ const resumeMode = options?.resume === true;
1309
1455
  state.runProcesses.set(runId, {
1310
1456
  runId,
1311
1457
  child: null,
@@ -1322,9 +1468,9 @@ async function startLocalRun(state, runId, options) {
1322
1468
  summary: run.title
1323
1469
  });
1324
1470
  appendRunLogEntry(state.projectRoot, runId, {
1325
- id: `log:${runId}:prepare`,
1326
- title: "Rig task run starting",
1327
- detail: run.taskId ?? run.title,
1471
+ id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
1472
+ title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
1473
+ detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
1328
1474
  tone: "info",
1329
1475
  status: "preparing",
1330
1476
  createdAt: startedAt
@@ -1332,8 +1478,8 @@ async function startLocalRun(state, runId, options) {
1332
1478
  broadcastRunLogAppended(state, runId, readLatestRawRunLog(state.projectRoot, runId));
1333
1479
  broadcastSnapshotInvalidation(state);
1334
1480
  const cliProjectRoot = resolveLocalRunCliProjectRoot(state.projectRoot);
1335
- const cliEntryPoint = resolve9(cliProjectRoot, "packages/cli/bin/rig.ts");
1336
- if (!existsSync6(cliEntryPoint)) {
1481
+ const cliEntryPoint = resolve10(cliProjectRoot, "packages/cli/bin/rig.ts");
1482
+ if (!existsSync7(cliEntryPoint)) {
1337
1483
  const completedAt = new Date().toISOString();
1338
1484
  const failureSummary = `Rig task-run entrypoint missing at ${relative2(state.projectRoot, cliEntryPoint)}`;
1339
1485
  patchRunRecord(state.projectRoot, runId, {
@@ -1402,7 +1548,15 @@ async function startLocalRun(state, runId, options) {
1402
1548
  RIG_SERVER_INTERNAL_EXEC: "1",
1403
1549
  ...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
1404
1550
  ...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
1405
- ...bridgeGitHubToken ? { RIG_GITHUB_TOKEN: bridgeGitHubToken } : {}
1551
+ ...bridgeGitHubToken ? {
1552
+ RIG_GITHUB_TOKEN: bridgeGitHubToken,
1553
+ GITHUB_TOKEN: bridgeGitHubToken,
1554
+ GH_TOKEN: bridgeGitHubToken
1555
+ } : {},
1556
+ ...resumeMode ? {
1557
+ RIG_RUN_RESUME: "1",
1558
+ RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
1559
+ } : {}
1406
1560
  },
1407
1561
  stdio: ["ignore", "pipe", "pipe"]
1408
1562
  });
@@ -1442,9 +1596,9 @@ async function startLocalRun(state, runId, options) {
1442
1596
  handleRunProcessOutput(Buffer.isBuffer(data) ? data.toString("utf8") : String(data), "error", "Rig task run stderr");
1443
1597
  });
1444
1598
  try {
1445
- const exit = await new Promise((resolve10) => {
1446
- child.once("error", (error) => resolve10({ code: 1, signal: null, error }));
1447
- child.once("close", (code, signal) => resolve10({ code, signal }));
1599
+ const exit = await new Promise((resolve11) => {
1600
+ child.once("error", (error) => resolve11({ code: 1, signal: null, error }));
1601
+ child.once("close", (code, signal) => resolve11({ code, signal }));
1448
1602
  });
1449
1603
  if (exit.error) {
1450
1604
  throw new Error(`Failed to start task run: ${exit.error.message}`);
@@ -1544,17 +1698,17 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
1544
1698
  process.env.PROJECT_RIG_ROOT?.trim()
1545
1699
  ].filter((value) => !!value);
1546
1700
  for (const candidate of envCandidates) {
1547
- if (existsSync6(resolve9(candidate, "packages/cli/bin/rig.ts"))) {
1548
- return resolve9(candidate);
1701
+ if (existsSync7(resolve10(candidate, "packages/cli/bin/rig.ts"))) {
1702
+ return resolve10(candidate);
1549
1703
  }
1550
1704
  }
1551
- if (existsSync6(resolve9(projectRoot, "packages/cli/bin/rig.ts"))) {
1705
+ if (existsSync7(resolve10(projectRoot, "packages/cli/bin/rig.ts"))) {
1552
1706
  return projectRoot;
1553
1707
  }
1554
1708
  try {
1555
1709
  const monorepoRoot = resolveMonorepoRoot6(projectRoot);
1556
- const outerProjectRoot = dirname5(dirname5(monorepoRoot));
1557
- if (existsSync6(resolve9(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
1710
+ const outerProjectRoot = dirname6(dirname6(monorepoRoot));
1711
+ if (existsSync7(resolve10(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
1558
1712
  return outerProjectRoot;
1559
1713
  }
1560
1714
  } catch {}
@@ -1574,15 +1728,15 @@ async function resumeRunRecord(state, input) {
1574
1728
  if (run.status === "completed") {
1575
1729
  throw new Error("Completed runs cannot be resumed.");
1576
1730
  }
1577
- await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
1731
+ await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
1578
1732
  }
1579
1733
  function appendRunMessage(projectRoot, input) {
1580
1734
  const run = readAuthorityRun8(projectRoot, input.runId);
1581
1735
  if (!run) {
1582
1736
  throw new Error(`Run not found: ${input.runId}`);
1583
1737
  }
1584
- const timelinePath = resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "timeline.jsonl");
1585
- const existingLines = fileExists(timelinePath) ? readFileSync3(timelinePath, "utf8").trim() : "";
1738
+ const timelinePath = resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "timeline.jsonl");
1739
+ const existingLines = fileExists(timelinePath) ? readFileSync4(timelinePath, "utf8").trim() : "";
1586
1740
  const nextLine = JSON.stringify({
1587
1741
  id: input.messageId,
1588
1742
  type: "user_message",
@@ -1590,11 +1744,11 @@ function appendRunMessage(projectRoot, input) {
1590
1744
  attachments: input.attachments ?? [],
1591
1745
  createdAt: input.createdAt
1592
1746
  });
1593
- writeFileSync5(timelinePath, existingLines.length > 0 ? `${existingLines}
1747
+ writeFileSync6(timelinePath, existingLines.length > 0 ? `${existingLines}
1594
1748
  ${nextLine}
1595
1749
  ` : `${nextLine}
1596
1750
  `, "utf8");
1597
- writeJsonFile4(resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), {
1751
+ writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), {
1598
1752
  ...run,
1599
1753
  updatedAt: input.createdAt
1600
1754
  });
@@ -1620,7 +1774,7 @@ async function stopRunRecord(stateOrProjectRoot, input) {
1620
1774
  completedAt: run.completedAt ?? input.createdAt,
1621
1775
  updatedAt: input.createdAt
1622
1776
  };
1623
- writeJsonFile4(resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), nextRun);
1777
+ writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), nextRun);
1624
1778
  if (run.status !== "completed" && run.taskId) {
1625
1779
  const taskId = run.taskId;
1626
1780
  (async () => {
@@ -1658,34 +1812,12 @@ function removeTaskIdsFromQueueState2(projectRoot, taskIds) {
1658
1812
  writeQueueState(projectRoot, next);
1659
1813
  return next;
1660
1814
  }
1661
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
1662
- function reconcileOrphanedLocalRuns(state, runs, nowIso) {
1663
- let changed = false;
1664
- for (const run of runs) {
1815
+ var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
1816
+ function collectResumableLocalRuns(state, runs) {
1817
+ return runs.filter((run) => {
1665
1818
  const status = normalizeString(run.status)?.toLowerCase() ?? "";
1666
- const serverPid = run.serverPid;
1667
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
1668
- if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
1669
- continue;
1670
- }
1671
- const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
1672
- patchRunRecord(state.projectRoot, run.runId, {
1673
- status: "failed",
1674
- completedAt: run.completedAt ?? nowIso,
1675
- updatedAt: nowIso,
1676
- errorText: detail
1677
- });
1678
- appendRunLogEntry(state.projectRoot, run.runId, {
1679
- id: `log:${run.runId}:stale-local-run`,
1680
- title: "Run marked stale after server restart",
1681
- detail,
1682
- tone: "error",
1683
- status: "failed",
1684
- createdAt: nowIso
1685
- });
1686
- changed = true;
1687
- }
1688
- return changed;
1819
+ return run.mode === "local" && RESUMABLE_LOCAL_RUN_STATUSES.has(status) && !state.runProcesses.has(run.runId);
1820
+ });
1689
1821
  }
1690
1822
  async function reconcileScheduler(state, reason) {
1691
1823
  if (state.scheduler.reconciling) {
@@ -1700,7 +1832,20 @@ async function reconcileScheduler(state, reason) {
1700
1832
  const queue = readQueueState(state.projectRoot);
1701
1833
  const tasks = await state.snapshotService.getWorkspaceTasks();
1702
1834
  let runs = listAuthorityRuns7(state.projectRoot);
1703
- let changed = reconcileOrphanedLocalRuns(state, runs, new Date().toISOString());
1835
+ let changed = false;
1836
+ const resumableRuns = collectResumableLocalRuns(state, runs);
1837
+ for (const run of resumableRuns) {
1838
+ appendRunLogEntry(state.projectRoot, run.runId, {
1839
+ id: `log:${run.runId}:auto-resume:${Date.now()}`,
1840
+ title: "Run auto-resume scheduled",
1841
+ detail: `Rig server recovered nonterminal run ${run.runId} after ${reason}; resuming the same lifecycle instead of restarting it.`,
1842
+ tone: "info",
1843
+ status: "preparing",
1844
+ createdAt: new Date().toISOString()
1845
+ });
1846
+ await startLocalRun(state, run.runId, { resume: true });
1847
+ changed = true;
1848
+ }
1704
1849
  if (changed) {
1705
1850
  runs = listAuthorityRuns7(state.projectRoot);
1706
1851
  }
@@ -699,7 +699,7 @@ async function buildRigSnapshotPayload(projectRoot, readers) {
699
699
  const taskSummaries = (await readers.readWorkspaceTasks(projectRoot)).map((task) => ({
700
700
  ...toTaskSummary(workspace.id, {
701
701
  ...task,
702
- status: queuedTaskIds.has(task.id) && (task.status === "open" || task.status === "ready" || task.status === "draft") ? "queued" : task.status
702
+ status: queuedTaskIds.has(task.id) && (task.status === "open" || task.status === "ready" || task.status === "draft" || task.status === "failed" || task.sourceStatus === "failed") ? "queued" : task.status
703
703
  }),
704
704
  graphId: graph.id
705
705
  }));
@@ -711,7 +711,7 @@ async function buildRigSnapshotPayload(projectRoot, readers) {
711
711
  const taskSummaries = (await readers.readWorkspaceTasks(projectRoot)).map((task) => ({
712
712
  ...toTaskSummary(workspace.id, {
713
713
  ...task,
714
- status: queuedTaskIds.has(task.id) && (task.status === "open" || task.status === "ready" || task.status === "draft") ? "queued" : task.status
714
+ status: queuedTaskIds.has(task.id) && (task.status === "open" || task.status === "ready" || task.status === "draft" || task.status === "failed" || task.sourceStatus === "failed") ? "queued" : task.status
715
715
  }),
716
716
  graphId: graph.id
717
717
  }));
@@ -403,7 +403,7 @@ var TERMINAL_RUN_STATUSES2 = new Set([
403
403
  "needs-attention",
404
404
  "stopped"
405
405
  ]);
406
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
406
+ var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
407
407
 
408
408
  // packages/server/src/server-helpers/http-router.ts
409
409
  import {
@@ -437,10 +437,10 @@ import {
437
437
  buildTaskRunLifecycleComment as buildTaskRunLifecycleComment2,
438
438
  updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
439
439
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
440
- var RIG_CONFIG_PACKAGE_VERSION = "0.0.6-alpha.1";
440
+ var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
441
441
  var RIG_CONFIG_DEV_DEPENDENCIES = {
442
- "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_VERSION}`,
443
- "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_VERSION}`
442
+ "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
443
+ "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
444
444
  };
445
445
 
446
446
  // packages/server/src/server-helpers/inspector-jobs.ts