patchrelay 0.12.6 → 0.12.7

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.12.6",
4
- "commit": "62c29649ea46",
5
- "builtAt": "2026-03-24T21:48:34.334Z"
3
+ "version": "0.12.7",
4
+ "commit": "b46978c9149d",
5
+ "builtAt": "2026-03-24T22:34:10.024Z"
6
6
  }
package/dist/db.js CHANGED
@@ -217,6 +217,16 @@ export class PatchRelayDatabase {
217
217
  linearIssueId: String(row.linear_issue_id),
218
218
  }));
219
219
  }
220
+ /**
221
+ * Issues idle in pr_open with no active run — candidates for state
222
+ * advancement based on stored PR metadata (missed GitHub webhooks).
223
+ */
224
+ listIdlePrOpenIssues() {
225
+ const rows = this.connection
226
+ .prepare("SELECT * FROM issues WHERE factory_state = 'pr_open' AND active_run_id IS NULL AND pending_run_type IS NULL AND pr_number IS NOT NULL")
227
+ .all();
228
+ return rows.map(mapIssueRow);
229
+ }
220
230
  listIssuesByState(projectId, state) {
221
231
  const rows = this.connection
222
232
  .prepare("SELECT * FROM issues WHERE project_id = ? AND factory_state = ? ORDER BY pr_number ASC")
@@ -349,6 +349,57 @@ export class RunOrchestrator {
349
349
  for (const run of this.db.listRunningRuns()) {
350
350
  await this.reconcileRun(run);
351
351
  }
352
+ // Advance issues stuck in pr_open whose stored PR metadata already
353
+ // shows they should transition (e.g. approved PR, missed webhook).
354
+ await this.reconcileIdlePrOpenIssues();
355
+ }
356
+ async reconcileIdlePrOpenIssues() {
357
+ for (const issue of this.db.listIdlePrOpenIssues()) {
358
+ if (issue.prState === "merged") {
359
+ this.advanceIdleIssue(issue, "done");
360
+ continue;
361
+ }
362
+ if (issue.prReviewState === "approved") {
363
+ this.advanceIdleIssue(issue, "awaiting_queue");
364
+ continue;
365
+ }
366
+ // Stored metadata may be stale (missed webhooks during downtime).
367
+ // Query GitHub for the actual PR review state.
368
+ const project = this.config.projects.find((p) => p.id === issue.projectId);
369
+ if (!project?.github?.repoFullName || !issue.prNumber)
370
+ continue;
371
+ try {
372
+ const { stdout } = await execCommand("gh", [
373
+ "pr", "view", String(issue.prNumber),
374
+ "--repo", project.github.repoFullName,
375
+ "--json", "state,reviewDecision",
376
+ ], { timeoutMs: 10_000 });
377
+ const pr = JSON.parse(stdout);
378
+ if (pr.state === "MERGED") {
379
+ this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prState: "merged" });
380
+ this.advanceIdleIssue(issue, "done");
381
+ }
382
+ else if (pr.reviewDecision === "APPROVED") {
383
+ this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prReviewState: "approved" });
384
+ this.advanceIdleIssue(issue, "awaiting_queue");
385
+ }
386
+ }
387
+ catch (error) {
388
+ this.logger.debug({ issueKey: issue.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to query GitHub PR state during reconciliation");
389
+ }
390
+ }
391
+ }
392
+ advanceIdleIssue(issue, newState) {
393
+ this.logger.info({ issueKey: issue.issueKey, from: issue.factoryState, to: newState }, "Reconciliation: advancing idle issue from stored PR metadata");
394
+ this.db.upsertIssue({
395
+ projectId: issue.projectId,
396
+ linearIssueId: issue.linearIssueId,
397
+ factoryState: newState,
398
+ ...(newState === "awaiting_queue" ? { pendingMergePrep: true } : {}),
399
+ });
400
+ if (newState === "awaiting_queue") {
401
+ this.enqueueIssue(issue.projectId, issue.linearIssueId);
402
+ }
352
403
  }
353
404
  async reconcileRun(run) {
354
405
  const issue = this.db.getIssue(run.projectId, run.linearIssueId);
@@ -90,6 +90,11 @@ export class ServiceRuntime {
90
90
  this.reconcileInProgress = true;
91
91
  try {
92
92
  await promiseWithTimeout(this.runReconciler.reconcileActiveRuns(), this.options.reconcileTimeoutMs ?? DEFAULT_RECONCILE_TIMEOUT_MS, "Background active-run reconciliation");
93
+ // Pick up issues that became ready outside the webhook path
94
+ // (e.g. CLI retry, manual DB edits) without requiring a restart.
95
+ for (const issue of this.readyIssueSource.listIssuesReadyForExecution()) {
96
+ this.enqueueIssue(issue.projectId, issue.linearIssueId);
97
+ }
93
98
  }
94
99
  catch (error) {
95
100
  this.logger.warn({ error: error instanceof Error ? error.message : String(error) }, "Background active-run reconciliation failed");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.12.6",
3
+ "version": "0.12.7",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {