@h-rig/server 0.0.6-alpha.13 → 0.0.6-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -1580,6 +1580,18 @@ function readJsonlFileTail(path, options) {
1580
1580
  const completeLines = start > 0 ? lines.slice(1) : lines;
1581
1581
  return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
1582
1582
  }
1583
+ async function readRunTimelinePage(projectRoot, runId, options = {}) {
1584
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
1585
+ const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
1586
+ const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
1587
+ const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
1588
+ const endExclusive = Math.min(entries.length, startInclusive + limit);
1589
+ return {
1590
+ entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
1591
+ nextCursor: String(endExclusive),
1592
+ hasMore: endExclusive < entries.length
1593
+ };
1594
+ }
1583
1595
  var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
1584
1596
  async function readRunLogsPage(projectRoot, runId, options = {}) {
1585
1597
  const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
@@ -3237,8 +3249,7 @@ function buildRunStartPatch(startedAt) {
3237
3249
  status: "preparing",
3238
3250
  startedAt,
3239
3251
  completedAt: null,
3240
- errorText: null,
3241
- serverPid: process.pid
3252
+ errorText: null
3242
3253
  };
3243
3254
  }
3244
3255
 
@@ -3720,6 +3731,11 @@ import {
3720
3731
  buildTaskRunLifecycleComment,
3721
3732
  updateConfiguredTaskSourceTask
3722
3733
  } from "@rig/runtime/control-plane/tasks/source-lifecycle";
3734
+ import {
3735
+ closeIssueAfterMergedPr,
3736
+ commitRunChanges,
3737
+ runPrAutomation
3738
+ } from "@rig/runtime/control-plane/native/pr-automation";
3723
3739
 
3724
3740
  // packages/server/src/scheduler.ts
3725
3741
  import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
@@ -4055,6 +4071,9 @@ function asRecord(value) {
4055
4071
  function asString(value) {
4056
4072
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
4057
4073
  }
4074
+ function asNumber(value) {
4075
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
4076
+ }
4058
4077
  async function defaultGraphQLFetch(query, variables, token) {
4059
4078
  const response = await fetch("https://api.github.com/graphql", {
4060
4079
  method: "POST",
@@ -4071,6 +4090,32 @@ async function defaultGraphQLFetch(query, variables, token) {
4071
4090
  }
4072
4091
  return json.data;
4073
4092
  }
4093
+ function projectNodesFrom(data) {
4094
+ const root = asRecord(data);
4095
+ const owner = asRecord(root?.organization) ?? asRecord(root?.user);
4096
+ const projects = asRecord(owner?.projectsV2);
4097
+ const nodes = projects?.nodes;
4098
+ return Array.isArray(nodes) ? nodes : [];
4099
+ }
4100
+ async function listGitHubProjects(input) {
4101
+ const query = `
4102
+ query RigListProjects($owner: String!, $first: Int!) {
4103
+ organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
4104
+ user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
4105
+ }
4106
+ `;
4107
+ const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
4108
+ const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
4109
+ return projectNodesFrom(data).flatMap((node) => {
4110
+ const record = asRecord(node);
4111
+ const id = asString(record?.id);
4112
+ const number = asNumber(record?.number);
4113
+ const title = asString(record?.title);
4114
+ if (!id || number === undefined || !title)
4115
+ return [];
4116
+ return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
4117
+ });
4118
+ }
4074
4119
  async function resolveProjectStatusField(input) {
4075
4120
  const query = `
4076
4121
  query RigProjectStatusField($projectId: ID!) {
@@ -4165,6 +4210,7 @@ var DEFAULT_PROJECT_STATUSES = {
4165
4210
  running: "In Progress",
4166
4211
  prOpen: "In Review",
4167
4212
  ciFixing: "In Review",
4213
+ merging: "Merging",
4168
4214
  done: "Done",
4169
4215
  needsAttention: "Needs Attention"
4170
4216
  };
@@ -4178,6 +4224,8 @@ function lifecycleStatusForTaskStatus(status) {
4178
4224
  return "prOpen";
4179
4225
  if (normalized === "ci_fixing" || normalized === "fixing")
4180
4226
  return "ciFixing";
4227
+ if (normalized === "merging" || normalized === "merge")
4228
+ return "merging";
4181
4229
  if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
4182
4230
  return "needsAttention";
4183
4231
  if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
@@ -4306,9 +4354,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
4306
4354
  return null;
4307
4355
  return null;
4308
4356
  }
4357
+ function githubProjectsEnabled(config) {
4358
+ const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
4359
+ const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
4360
+ return projects?.enabled === true;
4361
+ }
4309
4362
  async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
4310
4363
  if (!run.taskId)
4311
- return;
4364
+ return false;
4312
4365
  const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
4313
4366
  try {
4314
4367
  const result = await syncGitHubProjectStatusForTaskUpdate({
@@ -4319,28 +4372,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
4319
4372
  config
4320
4373
  });
4321
4374
  if (!result.synced && result.reason !== "project-sync-disabled") {
4375
+ const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
4322
4376
  appendRunLogEntry(projectRoot, run.runId, {
4323
4377
  id: `log:${run.runId}:github-project-sync:${status}`,
4324
4378
  title: "GitHub Project sync skipped",
4325
- detail: `Project status sync for ${run.taskId} could not run: ${result.reason}.`,
4379
+ detail,
4326
4380
  tone: "warn",
4327
4381
  status: "running",
4328
4382
  createdAt: new Date().toISOString(),
4329
4383
  payload: { reason: result.reason, issueNodeId }
4330
4384
  });
4385
+ if (githubProjectsEnabled(config)) {
4386
+ throw new Error(detail);
4387
+ }
4388
+ return false;
4331
4389
  }
4390
+ return result.synced === true;
4332
4391
  } catch (error) {
4392
+ const detail = error instanceof Error ? error.message : String(error);
4333
4393
  appendRunLogEntry(projectRoot, run.runId, {
4334
4394
  id: `log:${run.runId}:github-project-sync-error:${status}`,
4335
4395
  title: "GitHub Project sync failed",
4336
- detail: error instanceof Error ? error.message : String(error),
4396
+ detail,
4337
4397
  tone: "error",
4338
4398
  status: "running",
4339
4399
  createdAt: new Date().toISOString(),
4340
4400
  payload: { issueNodeId }
4341
4401
  });
4402
+ if (githubProjectsEnabled(config)) {
4403
+ throw new Error(detail);
4404
+ }
4405
+ return false;
4342
4406
  }
4343
4407
  }
4408
+ function createCommandRunner(binary, extraEnv = {}) {
4409
+ return async (args, options) => {
4410
+ const child = spawn3(binary, [...args], {
4411
+ cwd: options?.cwd,
4412
+ env: { ...process.env, ...extraEnv },
4413
+ stdio: ["ignore", "pipe", "pipe"]
4414
+ });
4415
+ const stdoutChunks = [];
4416
+ const stderrChunks = [];
4417
+ child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
4418
+ child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
4419
+ const exitCode = await new Promise((resolve15) => {
4420
+ child.once("error", () => resolve15(1));
4421
+ child.once("close", (code) => resolve15(code ?? 1));
4422
+ });
4423
+ return {
4424
+ exitCode,
4425
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
4426
+ stderr: Buffer.concat(stderrChunks).toString("utf8")
4427
+ };
4428
+ };
4429
+ }
4430
+ function closeoutRecord(run) {
4431
+ const value = run.serverCloseout;
4432
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
4433
+ }
4434
+ function closeoutPhasePatch(phase, status, extra = {}) {
4435
+ const updatedAt = new Date().toISOString();
4436
+ return {
4437
+ serverCloseout: {
4438
+ phase,
4439
+ status,
4440
+ updatedAt,
4441
+ ...extra
4442
+ }
4443
+ };
4444
+ }
4445
+ function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
4446
+ appendRunLogEntryAndBroadcast(state, runId, {
4447
+ id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
4448
+ title: `Server closeout: ${phase}`,
4449
+ detail,
4450
+ tone,
4451
+ status,
4452
+ createdAt: new Date().toISOString()
4453
+ }, `server-closeout-${phase}`);
4454
+ }
4344
4455
  async function autoAssignRunIssue(projectRoot, run) {
4345
4456
  if (!run.taskId)
4346
4457
  return;
@@ -4370,7 +4481,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
4370
4481
  return;
4371
4482
  }
4372
4483
  const config = await loadRigLifecycleConfig(projectRoot);
4373
- await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
4484
+ const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
4374
4485
  if (status === "in_progress") {
4375
4486
  await autoAssignRunIssue(projectRoot, run);
4376
4487
  }
@@ -4386,24 +4497,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
4386
4497
  });
4387
4498
  return;
4388
4499
  }
4389
- const result = await updateConfiguredTaskSourceTask(projectRoot, {
4390
- taskId: run.taskId,
4391
- sourceTask: runSourceTaskIdentity(run),
4392
- update: {
4393
- status,
4394
- comment: buildTaskRunLifecycleComment({
4395
- runId: run.runId,
4500
+ const sourceTask = runSourceTaskIdentity(run);
4501
+ const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
4502
+ const rollbackProjectSync = async () => {
4503
+ if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
4504
+ return;
4505
+ await syncGitHubProjectStatusForTaskUpdate({
4506
+ taskId: run.taskId,
4507
+ status: previousStatus,
4508
+ issueNodeId: extractGitHubIssueNodeId(sourceTask),
4509
+ token: createGitHubAuthStore(projectRoot).readToken(),
4510
+ config
4511
+ }).catch((rollbackError) => {
4512
+ appendRunLogEntry(projectRoot, run.runId, {
4513
+ id: `log:${run.runId}:github-project-sync-rollback:${status}`,
4514
+ title: "GitHub Project sync rollback failed",
4515
+ detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
4516
+ tone: "error",
4517
+ status: "running",
4518
+ createdAt: new Date().toISOString()
4519
+ });
4520
+ });
4521
+ };
4522
+ let result;
4523
+ try {
4524
+ result = await updateConfiguredTaskSourceTask(projectRoot, {
4525
+ taskId: run.taskId,
4526
+ sourceTask,
4527
+ update: {
4396
4528
  status,
4397
- summary,
4398
- runtimeWorkspace: normalizeString(run.worktreePath),
4399
- logsDir: normalizeString(run.logRoot),
4400
- sessionDir: normalizeString(run.sessionPath),
4401
- errorText: options.errorText ?? normalizeString(run.errorText)
4402
- })
4403
- }
4404
- });
4529
+ comment: buildTaskRunLifecycleComment({
4530
+ runId: run.runId,
4531
+ status,
4532
+ summary,
4533
+ runtimeWorkspace: normalizeString(run.worktreePath),
4534
+ logsDir: normalizeString(run.logRoot),
4535
+ sessionDir: normalizeString(run.sessionPath),
4536
+ errorText: options.errorText ?? normalizeString(run.errorText)
4537
+ })
4538
+ }
4539
+ });
4540
+ } catch (error) {
4541
+ await rollbackProjectSync();
4542
+ throw error;
4543
+ }
4405
4544
  if (!result.updated) {
4406
4545
  if (result.source === "plugin" || result.sourceKind) {
4546
+ await rollbackProjectSync();
4407
4547
  throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
4408
4548
  }
4409
4549
  appendRunLogEntry(projectRoot, run.runId, {
@@ -4416,6 +4556,219 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
4416
4556
  });
4417
4557
  }
4418
4558
  }
4559
+ async function runServerOwnedPrCloseout(state, runId) {
4560
+ const run = readAuthorityRun4(state.projectRoot, runId);
4561
+ if (!run)
4562
+ throw new Error(`Run not found: ${runId}`);
4563
+ const closeout = closeoutRecord(run);
4564
+ if (!closeout)
4565
+ return;
4566
+ const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
4567
+ if (!taskId)
4568
+ throw new Error("Server-owned closeout requires a task id.");
4569
+ const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
4570
+ const branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
4571
+ const config = await loadRigLifecycleConfig(state.projectRoot);
4572
+ const runPrMode = normalizeString(run.prMode);
4573
+ const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
4574
+ const effectiveConfig = {
4575
+ ...config ?? {},
4576
+ pr: {
4577
+ ...config?.pr ?? {},
4578
+ mode: prMode,
4579
+ autoFixChecks: false,
4580
+ autoFixReview: false
4581
+ }
4582
+ };
4583
+ const readCurrentRun = () => readAuthorityRun4(state.projectRoot, runId) ?? run;
4584
+ const sourceTask = runSourceTaskIdentity(run);
4585
+ const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
4586
+ const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
4587
+ const closeoutPrUrl = normalizeString(closeout.prUrl);
4588
+ if (closeoutPhase === "completed" || closeoutStatus === "completed") {
4589
+ return;
4590
+ }
4591
+ if (closeoutPhase === "close-source" && closeoutPrUrl) {
4592
+ patchRunRecord(state.projectRoot, runId, {
4593
+ status: "reviewing",
4594
+ ...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
4595
+ });
4596
+ await closeIssueAfterMergedPr({
4597
+ projectRoot: state.projectRoot,
4598
+ taskId,
4599
+ runId,
4600
+ prUrl: closeoutPrUrl,
4601
+ sourceTask,
4602
+ updateTaskSource: async (projectRoot, input) => {
4603
+ await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
4604
+ return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
4605
+ }
4606
+ });
4607
+ const completedAt = new Date().toISOString();
4608
+ patchRunRecord(state.projectRoot, runId, {
4609
+ status: "completed",
4610
+ completedAt,
4611
+ errorText: null,
4612
+ ...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
4613
+ });
4614
+ appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
4615
+ emitRigEvent(state, {
4616
+ type: "rig.run.completed",
4617
+ aggregateId: runId,
4618
+ payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
4619
+ createdAt: completedAt
4620
+ });
4621
+ return;
4622
+ }
4623
+ if (prMode === "off" || prMode === "ask") {
4624
+ const completedAt = new Date().toISOString();
4625
+ patchRunRecord(state.projectRoot, runId, {
4626
+ status: "completed",
4627
+ completedAt,
4628
+ errorText: null,
4629
+ ...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
4630
+ });
4631
+ appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
4632
+ emitRigEvent(state, {
4633
+ type: "rig.run.completed",
4634
+ aggregateId: runId,
4635
+ payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
4636
+ createdAt: completedAt
4637
+ });
4638
+ return;
4639
+ }
4640
+ const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
4641
+ const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
4642
+ const gitCommand = createCommandRunner("git", githubEnv);
4643
+ const ghCommand = createCommandRunner("gh", githubEnv);
4644
+ const setCloseout = (phase, status, extra = {}) => {
4645
+ const previous = closeoutRecord(readCurrentRun()) ?? closeout;
4646
+ patchRunRecord(state.projectRoot, runId, {
4647
+ status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
4648
+ ...closeoutPhasePatch(phase, status, { ...previous, ...extra })
4649
+ });
4650
+ };
4651
+ setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
4652
+ appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
4653
+ const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
4654
+ appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
4655
+ setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
4656
+ const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
4657
+ if (push.exitCode !== 0) {
4658
+ throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
4659
+ }
4660
+ const sourceTaskForPr = {
4661
+ title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
4662
+ };
4663
+ const artifactRoot = resolve14(state.projectRoot, "artifacts", taskId);
4664
+ setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4665
+ const pr = await runPrAutomation({
4666
+ projectRoot: workspace,
4667
+ taskId,
4668
+ runId,
4669
+ branch,
4670
+ config: effectiveConfig,
4671
+ sourceTask: sourceTaskForPr,
4672
+ artifactRoot,
4673
+ command: ghCommand,
4674
+ gitCommand,
4675
+ steerPi: async (message) => {
4676
+ appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
4677
+ appendRunTimelineEntry(state.projectRoot, runId, {
4678
+ id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
4679
+ type: "user_message",
4680
+ text: message,
4681
+ createdAt: new Date().toISOString(),
4682
+ state: "completed"
4683
+ });
4684
+ },
4685
+ lifecycle: {
4686
+ onPrOpened: async ({ prUrl }) => {
4687
+ setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4688
+ appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
4689
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
4690
+ },
4691
+ onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
4692
+ onFeedback: async ({ feedback }) => {
4693
+ appendCloseoutStage(state, runId, "feedback", feedback.join(`
4694
+ `), "reviewing", "error");
4695
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
4696
+ },
4697
+ onMergeStarted: async ({ prUrl }) => {
4698
+ setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
4699
+ appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
4700
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
4701
+ },
4702
+ onMerged: ({ prUrl }) => {
4703
+ setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
4704
+ appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
4705
+ }
4706
+ }
4707
+ });
4708
+ if (pr.status === "merged" && pr.prUrl) {
4709
+ setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
4710
+ await closeIssueAfterMergedPr({
4711
+ projectRoot: state.projectRoot,
4712
+ taskId,
4713
+ runId,
4714
+ prUrl: pr.prUrl,
4715
+ sourceTask,
4716
+ updateTaskSource: async (projectRoot, input) => {
4717
+ await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
4718
+ return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
4719
+ }
4720
+ });
4721
+ const completedAt = new Date().toISOString();
4722
+ patchRunRecord(state.projectRoot, runId, {
4723
+ status: "completed",
4724
+ completedAt,
4725
+ errorText: null,
4726
+ ...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
4727
+ });
4728
+ appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
4729
+ emitRigEvent(state, {
4730
+ type: "rig.run.completed",
4731
+ aggregateId: runId,
4732
+ payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
4733
+ createdAt: completedAt
4734
+ });
4735
+ return;
4736
+ }
4737
+ if (pr.status === "opened" && pr.prUrl) {
4738
+ const completedAt = new Date().toISOString();
4739
+ patchRunRecord(state.projectRoot, runId, {
4740
+ status: "completed",
4741
+ completedAt,
4742
+ errorText: null,
4743
+ ...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
4744
+ });
4745
+ appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
4746
+ emitRigEvent(state, {
4747
+ type: "rig.run.completed",
4748
+ aggregateId: runId,
4749
+ payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
4750
+ createdAt: completedAt
4751
+ });
4752
+ return;
4753
+ }
4754
+ const detail = pr.actionableFeedback.join(`
4755
+ `) || "PR automation did not merge the PR.";
4756
+ await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
4757
+ appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
4758
+ });
4759
+ patchRunRecord(state.projectRoot, runId, {
4760
+ status: "needs_attention",
4761
+ completedAt: new Date().toISOString(),
4762
+ errorText: detail,
4763
+ ...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
4764
+ });
4765
+ appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
4766
+ emitRigEvent(state, {
4767
+ type: "rig.run.needs-attention",
4768
+ aggregateId: runId,
4769
+ payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
4770
+ });
4771
+ }
4419
4772
  var TERMINAL_RUN_STATUSES2 = new Set([
4420
4773
  "completed",
4421
4774
  "complete",
@@ -4624,6 +4977,7 @@ async function startLocalRun(state, runId, options) {
4624
4977
  RIG_HOST_PROJECT_ROOT: cliProjectRoot,
4625
4978
  RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
4626
4979
  RIG_SERVER_INTERNAL_EXEC: "1",
4980
+ RIG_SERVER_OWNS_CLOSEOUT: "1",
4627
4981
  ...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
4628
4982
  ...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
4629
4983
  ...bridgeGitHubToken ? {
@@ -4720,6 +5074,38 @@ ${sourceFailure}` });
4720
5074
  agent: current.runtimeAdapter,
4721
5075
  summary: failureSummary
4722
5076
  });
5077
+ } else if (closeoutRecord(current)?.status === "pending") {
5078
+ try {
5079
+ await runServerOwnedPrCloseout(state, runId);
5080
+ } catch (closeoutError) {
5081
+ const closeoutFailure = closeoutError instanceof Error ? closeoutError.message : String(closeoutError);
5082
+ patchRunRecord(state.projectRoot, runId, {
5083
+ status: "failed",
5084
+ completedAt: new Date().toISOString(),
5085
+ errorText: closeoutFailure,
5086
+ ...closeoutPhasePatch("failed", "failed", { error: closeoutFailure })
5087
+ });
5088
+ appendRunLogEntryAndBroadcast(state, runId, {
5089
+ id: `log:${runId}:server-closeout-failed`,
5090
+ title: "Server-owned closeout failed",
5091
+ detail: closeoutFailure,
5092
+ tone: "error",
5093
+ status: "failed",
5094
+ createdAt: new Date().toISOString()
5095
+ }, "server-closeout-failed");
5096
+ if (current.taskId) {
5097
+ await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: closeoutFailure }, "failed", "Rig server-owned closeout failed.", { errorText: closeoutFailure }).catch((error) => {
5098
+ appendRunLogEntry(state.projectRoot, runId, {
5099
+ id: `log:${runId}:task-source-closeout-failed-update`,
5100
+ title: "Task source closeout failure update failed",
5101
+ detail: error instanceof Error ? error.message : String(error),
5102
+ tone: "error",
5103
+ status: "failed",
5104
+ createdAt: new Date().toISOString()
5105
+ });
5106
+ });
5107
+ }
5108
+ }
4723
5109
  }
4724
5110
  broadcastSnapshotInvalidation(state);
4725
5111
  } catch (error) {
@@ -4806,6 +5192,12 @@ async function resumeRunRecord(state, input) {
4806
5192
  if (run.status === "completed") {
4807
5193
  throw new Error("Completed runs cannot be resumed.");
4808
5194
  }
5195
+ const closeout = closeoutRecord(run);
5196
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
5197
+ if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
5198
+ await runServerOwnedPrCloseout(state, input.runId);
5199
+ return;
5200
+ }
4809
5201
  await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
4810
5202
  }
4811
5203
  function appendRunMessage(projectRoot, input) {
@@ -4890,11 +5282,45 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
4890
5282
  writeQueueState(projectRoot, next);
4891
5283
  return next;
4892
5284
  }
4893
- var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
4894
- function collectResumableLocalRuns(state, runs) {
5285
+ var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
5286
+ var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
5287
+ function processExists(pid) {
5288
+ if (!Number.isInteger(pid) || pid <= 0)
5289
+ return false;
5290
+ try {
5291
+ process.kill(pid, 0);
5292
+ return true;
5293
+ } catch {
5294
+ return false;
5295
+ }
5296
+ }
5297
+ function recoverStaleLocalRun(projectRoot, run) {
5298
+ const record = run;
5299
+ if (run.mode !== "local")
5300
+ return false;
5301
+ const status = normalizeString(record.status)?.toLowerCase() ?? "";
5302
+ if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
5303
+ return false;
5304
+ const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
5305
+ const childPid = typeof record.pid === "number" ? record.pid : null;
5306
+ if (serverPid === null && childPid === null)
5307
+ return false;
5308
+ const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
5309
+ if (hasLiveRecordedProcess && serverPid === process.pid)
5310
+ return false;
5311
+ const completedAt = new Date().toISOString();
5312
+ patchRunRecord(projectRoot, run.runId, {
5313
+ status: "failed",
5314
+ completedAt,
5315
+ errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
5316
+ });
5317
+ return true;
5318
+ }
5319
+ function collectResumableServerCloseouts(state, runs) {
4895
5320
  return runs.filter((run) => {
4896
- const status = normalizeString(run.status)?.toLowerCase() ?? "";
4897
- return run.mode === "local" && RESUMABLE_LOCAL_RUN_STATUSES.has(status) && !state.runProcesses.has(run.runId);
5321
+ const closeout = closeoutRecord(run);
5322
+ const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
5323
+ return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
4898
5324
  });
4899
5325
  }
4900
5326
  async function reconcileScheduler(state, reason) {
@@ -4911,17 +5337,33 @@ async function reconcileScheduler(state, reason) {
4911
5337
  const tasks = await state.snapshotService.getWorkspaceTasks();
4912
5338
  let runs = listAuthorityRuns4(state.projectRoot);
4913
5339
  let changed = false;
4914
- const resumableRuns = collectResumableLocalRuns(state, runs);
4915
- for (const run of resumableRuns) {
5340
+ for (const run of runs) {
5341
+ if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
5342
+ changed = true;
5343
+ }
5344
+ }
5345
+ if (changed) {
5346
+ runs = listAuthorityRuns4(state.projectRoot);
5347
+ }
5348
+ const resumableCloseouts = collectResumableServerCloseouts(state, runs);
5349
+ for (const run of resumableCloseouts) {
4916
5350
  appendRunLogEntry(state.projectRoot, run.runId, {
4917
- id: `log:${run.runId}:auto-resume:${Date.now()}`,
4918
- title: "Run auto-resume scheduled",
4919
- detail: `Rig server recovered nonterminal run ${run.runId} after ${reason}; resuming the same lifecycle instead of restarting it.`,
5351
+ id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
5352
+ title: "Server-owned closeout auto-resume scheduled",
5353
+ detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
4920
5354
  tone: "info",
4921
- status: "preparing",
5355
+ status: "reviewing",
4922
5356
  createdAt: new Date().toISOString()
4923
5357
  });
4924
- await startLocalRun(state, run.runId, { resume: true });
5358
+ await runServerOwnedPrCloseout(state, run.runId).catch((error) => {
5359
+ const detail = error instanceof Error ? error.message : String(error);
5360
+ patchRunRecord(state.projectRoot, run.runId, {
5361
+ status: "failed",
5362
+ completedAt: new Date().toISOString(),
5363
+ errorText: detail,
5364
+ ...closeoutPhasePatch("failed", "failed", { error: detail })
5365
+ });
5366
+ });
4925
5367
  changed = true;
4926
5368
  }
4927
5369
  if (changed) {
@@ -5717,6 +6159,75 @@ function buildProjectConfigStatus(root) {
5717
6159
  suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
5718
6160
  };
5719
6161
  }
6162
+ var RIG_GITHUB_LIFECYCLE_LABELS = [
6163
+ "ready",
6164
+ "blocked",
6165
+ "in-progress",
6166
+ "under-review",
6167
+ "failed",
6168
+ "cancelled",
6169
+ "rig:running",
6170
+ "rig:pr-open",
6171
+ "rig:ci-fixing",
6172
+ "rig:merging",
6173
+ "rig:done",
6174
+ "rig:needs-attention"
6175
+ ];
6176
+ function githubProjectsEnabled2(config) {
6177
+ if (!config || typeof config !== "object" || Array.isArray(config))
6178
+ return false;
6179
+ const root = config;
6180
+ const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
6181
+ const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
6182
+ return projects?.enabled === true;
6183
+ }
6184
+ function githubIssueSourceRepo(config) {
6185
+ if (!config || typeof config !== "object" || Array.isArray(config))
6186
+ return null;
6187
+ const root = config;
6188
+ const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
6189
+ const owner = normalizeString(taskSource?.owner);
6190
+ const repo = normalizeString(taskSource?.repo);
6191
+ if (taskSource?.kind === "github-issues" && owner && repo)
6192
+ return { owner, repo };
6193
+ const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
6194
+ const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
6195
+ const match = slug?.match(/^([^/]+)\/([^/]+)$/);
6196
+ return match ? { owner: match[1], repo: match[2] } : null;
6197
+ }
6198
+ async function ensureGitHubLifecycleLabels(projectRoot, config) {
6199
+ const repo = githubIssueSourceRepo(config);
6200
+ if (!repo)
6201
+ return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
6202
+ const token = createGitHubAuthStore(projectRoot).readToken();
6203
+ if (!token)
6204
+ return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
6205
+ const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
6206
+ headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
6207
+ });
6208
+ const existingJson = await existingResponse.json().catch(() => []);
6209
+ const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
6210
+ const created = [];
6211
+ const alreadyPresent = [];
6212
+ const failed = [];
6213
+ for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
6214
+ if (existing.has(label)) {
6215
+ alreadyPresent.push(label);
6216
+ continue;
6217
+ }
6218
+ const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
6219
+ method: "POST",
6220
+ headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
6221
+ body: JSON.stringify({ name: label, color: label.startsWith("rig:") ? "6f42c1" : "ededed", description: label.startsWith("rig:") ? "Task status managed by Rig" : "Task lifecycle status managed by Rig" })
6222
+ });
6223
+ if (response.ok || response.status === 422) {
6224
+ (response.status === 422 ? alreadyPresent : created).push(label);
6225
+ } else {
6226
+ failed.push({ label, error: await response.text().catch(() => response.statusText) });
6227
+ }
6228
+ }
6229
+ return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
6230
+ }
5720
6231
  function normalizeCommit(value) {
5721
6232
  const raw = normalizeString(value);
5722
6233
  return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
@@ -6862,16 +7373,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6862
7373
  if (!source) {
6863
7374
  return deps.badRequest("No task source is configured");
6864
7375
  }
7376
+ if (!source.updateTask && !(update.status && source.updateStatus)) {
7377
+ return deps.badRequest("Configured task source does not support updates");
7378
+ }
6865
7379
  const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
6866
7380
  return;
6867
7381
  }) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
6868
- if (source.updateTask) {
6869
- await source.updateTask(id, update);
6870
- } else if (update.status && source.updateStatus) {
6871
- await source.updateStatus(id, update.status);
6872
- } else {
6873
- return deps.badRequest("Configured task source does not support updates");
6874
- }
6875
7382
  const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
6876
7383
  const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
6877
7384
  taskId: id,
@@ -6880,6 +7387,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6880
7387
  token: createGitHubAuthStore(state.projectRoot).readToken(),
6881
7388
  config: ctx?.config
6882
7389
  }).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
7390
+ if (update.status && githubProjectsEnabled2(ctx?.config) && projectSync.synced === false) {
7391
+ return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
7392
+ }
7393
+ try {
7394
+ if (source.updateTask) {
7395
+ await source.updateTask(id, update);
7396
+ } else if (update.status && source.updateStatus) {
7397
+ await source.updateStatus(id, update.status);
7398
+ }
7399
+ } catch (error) {
7400
+ let rollback = null;
7401
+ const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
7402
+ if (update.status && previousStatus && githubProjectsEnabled2(ctx?.config) && projectSync.synced !== false) {
7403
+ rollback = await syncGitHubProjectStatusForTaskUpdate({
7404
+ taskId: id,
7405
+ status: previousStatus,
7406
+ issueNodeId,
7407
+ token: createGitHubAuthStore(state.projectRoot).readToken(),
7408
+ config: ctx?.config
7409
+ }).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
7410
+ }
7411
+ return deps.jsonResponse({
7412
+ ok: false,
7413
+ id,
7414
+ projectSync,
7415
+ rollback,
7416
+ error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
7417
+ }, 502);
7418
+ }
6883
7419
  deps.snapshotService.invalidate("github-issue-updated");
6884
7420
  await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
6885
7421
  return;
@@ -6888,26 +7424,41 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6888
7424
  return deps.jsonResponse({ ok: true, id, projectSync });
6889
7425
  }
6890
7426
  if (url.pathname === "/api/workspace/task-labels") {
7427
+ const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
7428
+ if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
7429
+ return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
7430
+ }
6891
7431
  return deps.jsonResponse({
6892
7432
  ok: true,
6893
7433
  ready: true,
6894
7434
  labelsReady: true,
6895
- labels: [
6896
- "ready",
6897
- "blocked",
6898
- "in-progress",
6899
- "under-review",
6900
- "failed",
6901
- "cancelled",
6902
- "rig:running",
6903
- "rig:pr-open",
6904
- "rig:ci-fixing",
6905
- "rig:done",
6906
- "rig:needs-attention"
6907
- ],
6908
- note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
7435
+ labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
7436
+ note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
6909
7437
  });
6910
7438
  }
7439
+ if (url.pathname === "/api/github/projects" && req.method === "GET") {
7440
+ const owner = normalizeString(url.searchParams.get("owner"));
7441
+ if (!owner)
7442
+ return deps.badRequest("owner is required");
7443
+ const token = createGitHubAuthStore(state.projectRoot).readToken();
7444
+ if (!token)
7445
+ return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
7446
+ const projects = await listGitHubProjects({ owner, token }).catch((error) => {
7447
+ throw new Error(error instanceof Error ? error.message : String(error));
7448
+ });
7449
+ return deps.jsonResponse({ ok: true, projects });
7450
+ }
7451
+ const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
7452
+ if (projectStatusMatch && req.method === "GET") {
7453
+ const projectId = decodeURIComponent(projectStatusMatch[1]);
7454
+ const token = createGitHubAuthStore(state.projectRoot).readToken();
7455
+ if (!token)
7456
+ return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
7457
+ const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
7458
+ throw new Error(error instanceof Error ? error.message : String(error));
7459
+ });
7460
+ return deps.jsonResponse({ ok: true, field });
7461
+ }
6911
7462
  if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
6912
7463
  const body = await deps.readJsonBody(req);
6913
7464
  const ids = uniqueStringList(body.ids ?? body.id);
@@ -8072,6 +8623,69 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
8072
8623
  }
8073
8624
  const run = leaseValidation.run;
8074
8625
  const completedAt = new Date().toISOString();
8626
+ const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
8627
+ if (run.taskId && workspaceDir) {
8628
+ patchRunRecord(state.projectRoot, runId, {
8629
+ status: "reviewing",
8630
+ completedAt: null,
8631
+ hostId,
8632
+ endpointId: leaseId,
8633
+ worktreePath: workspaceDir,
8634
+ serverCloseout: {
8635
+ status: "pending",
8636
+ phase: "queued",
8637
+ requestedAt: completedAt,
8638
+ updatedAt: completedAt,
8639
+ runtimeWorkspace: workspaceDir,
8640
+ branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
8641
+ taskId: run.taskId,
8642
+ source: "remote-complete"
8643
+ }
8644
+ });
8645
+ deps.appendRunLogEntryAndBroadcast(state, runId, {
8646
+ id: `log:${runId}:remote-server-closeout-requested`,
8647
+ title: "Server-owned closeout requested",
8648
+ detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
8649
+ tone: "info",
8650
+ status: "reviewing",
8651
+ createdAt: completedAt,
8652
+ payload: { workspaceDir, hostId, leaseId }
8653
+ }, "remote-server-closeout-requested");
8654
+ deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
8655
+ const detail = error instanceof Error ? error.message : String(error);
8656
+ patchRunRecord(state.projectRoot, runId, {
8657
+ status: "failed",
8658
+ completedAt: new Date().toISOString(),
8659
+ errorText: detail,
8660
+ serverCloseout: {
8661
+ status: "failed",
8662
+ phase: "failed",
8663
+ updatedAt: new Date().toISOString(),
8664
+ error: detail
8665
+ }
8666
+ });
8667
+ deps.appendRunLogEntryAndBroadcast(state, runId, {
8668
+ id: `log:${runId}:remote-server-closeout-failed`,
8669
+ title: "Server-owned closeout failed",
8670
+ detail,
8671
+ tone: "error",
8672
+ status: "failed",
8673
+ createdAt: new Date().toISOString()
8674
+ }, "remote-server-closeout-failed");
8675
+ }).finally(() => {
8676
+ deps.reconcileScheduler(state, "remote-server-closeout-terminal");
8677
+ });
8678
+ deps.broadcastSnapshotInvalidation(state);
8679
+ return deps.jsonResponse({
8680
+ ok: true,
8681
+ workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
8682
+ hostId,
8683
+ runId,
8684
+ leaseId,
8685
+ closeout: "server-owned",
8686
+ acceptedAt: new Date().toISOString()
8687
+ });
8688
+ }
8075
8689
  patchRunRecord(state.projectRoot, runId, {
8076
8690
  status: "completed",
8077
8691
  completedAt,
@@ -8214,6 +8828,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
8214
8828
  const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
8215
8829
  return deps.jsonResponse(page);
8216
8830
  }
8831
+ const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
8832
+ if (runTimelineMatch) {
8833
+ const runId = decodeURIComponent(runTimelineMatch[1]);
8834
+ const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
8835
+ const cursor = normalizeString(url.searchParams.get("cursor"));
8836
+ const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
8837
+ return deps.jsonResponse(page);
8838
+ }
8217
8839
  const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
8218
8840
  if (runSteerMatch && req.method === "POST") {
8219
8841
  const runId = decodeURIComponent(runSteerMatch[1]);
@@ -13430,6 +14052,7 @@ function buildHttpRouterDeps(state) {
13430
14052
  startLocalRun,
13431
14053
  stopRunRecord,
13432
14054
  resumeRunRecord,
14055
+ runServerOwnedPrCloseout,
13433
14056
  claimRemoteRun,
13434
14057
  listRemoteRunArtifacts,
13435
14058
  broadcastSnapshotInvalidation,
@@ -13754,6 +14377,7 @@ export {
13754
14377
  resolveRigServerPaths,
13755
14378
  resolveRigProjectRoot,
13756
14379
  resolvePublishedRigServerStatePath,
14380
+ resolveProjectStatusField,
13757
14381
  resolveProjectRoot,
13758
14382
  registerRemoteHost,
13759
14383
  readWorkspaceTasks,
@@ -13762,6 +14386,7 @@ export {
13762
14386
  parseRigServerArgs,
13763
14387
  parseArgs,
13764
14388
  main,
14389
+ listGitHubProjects,
13765
14390
  heartbeatRemoteHost,
13766
14391
  handleWebSocketUpgrade,
13767
14392
  encodeWebSocketPayload,