patchrelay 0.12.5 → 0.12.6

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.5",
4
- "commit": "7de41a6b5840",
5
- "builtAt": "2026-03-24T20:31:57.125Z"
3
+ "version": "0.12.6",
4
+ "commit": "62c29649ea46",
5
+ "builtAt": "2026-03-24T21:48:34.334Z"
6
6
  }
@@ -226,32 +226,32 @@ export class GitHubWebhookHandler {
226
226
  async emitLinearActivity(issue, newState, event) {
227
227
  if (!issue.agentSessionId)
228
228
  return;
229
- const linear = await this.linearProvider.forProject(issue.projectId);
230
- if (!linear?.createAgentActivity)
231
- return;
232
- const messages = {
233
- pr_open: `PR #${event.prNumber ?? ""} opened.${event.prUrl ? ` ${event.prUrl}` : ""}`,
234
- awaiting_queue: "PR approved. Preparing merge.",
235
- changes_requested: `Review requested changes.${event.reviewerName ? ` Reviewer: ${event.reviewerName}` : ""}`,
236
- repairing_ci: `CI check failed${event.checkName ? `: ${event.checkName}` : ""}. Starting repair.`,
237
- repairing_queue: "Merge conflict with base branch. Starting repair.",
238
- done: `PR merged and deployed.${event.prNumber ? ` PR #${event.prNumber}` : ""}`,
239
- failed: "PR was closed without merging.",
240
- };
241
- const body = messages[newState];
242
- if (!body)
243
- return;
244
- const type = newState === "failed" || newState === "repairing_ci" || newState === "repairing_queue"
245
- ? "error"
246
- : "response";
247
229
  try {
230
+ const linear = await this.linearProvider.forProject(issue.projectId);
231
+ if (!linear?.createAgentActivity)
232
+ return;
233
+ const messages = {
234
+ pr_open: `PR #${event.prNumber ?? ""} opened.${event.prUrl ? ` ${event.prUrl}` : ""}`,
235
+ awaiting_queue: "PR approved. Preparing merge.",
236
+ changes_requested: `Review requested changes.${event.reviewerName ? ` Reviewer: ${event.reviewerName}` : ""}`,
237
+ repairing_ci: `CI check failed${event.checkName ? `: ${event.checkName}` : ""}. Starting repair.`,
238
+ repairing_queue: "Merge conflict with base branch. Starting repair.",
239
+ done: `PR merged and deployed.${event.prNumber ? ` PR #${event.prNumber}` : ""}`,
240
+ failed: "PR was closed without merging.",
241
+ };
242
+ const body = messages[newState];
243
+ if (!body)
244
+ return;
245
+ const type = newState === "failed" || newState === "repairing_ci" || newState === "repairing_queue"
246
+ ? "error"
247
+ : "response";
248
248
  await linear.createAgentActivity({
249
249
  agentSessionId: issue.agentSessionId,
250
250
  content: { type, body },
251
251
  });
252
252
  }
253
253
  catch {
254
- // Non-blocking — don't fail the webhook for a Linear activity error
254
+ // Non-blocking — don't crash the webhook handler for a Linear activity error
255
255
  }
256
256
  }
257
257
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { ACTIVE_RUN_STATES } from "./factory-state.js";
3
4
  import { buildHookEnv, runProjectHook } from "./hook-runner.js";
4
5
  import { buildRunningSessionPlan, buildCompletedSessionPlan, buildFailedSessionPlan, } from "./agent-session-plan.js";
5
6
  import { buildStageReport, countEventMethods, extractTurnId, resolveRunCompletionStatus, summarizeCurrentThread, } from "./run-reporting.js";
@@ -275,6 +276,22 @@ export class RunOrchestrator {
275
276
  // Complete the run
276
277
  const trackedIssue = this.db.issueToTrackedIssue(issue);
277
278
  const report = buildStageReport(run, trackedIssue, thread, countEventMethods(this.db.listThreadEvents(run.id)));
279
+ // Determine post-run state. When a re-run finds the PR already exists
280
+ // and makes no changes, no pr_opened webhook arrives — the state would
281
+ // stay in the active-run state forever. Advance based on PR metadata.
282
+ const freshIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? issue;
283
+ let postRunState;
284
+ if (ACTIVE_RUN_STATES.has(freshIssue.factoryState) && freshIssue.prNumber) {
285
+ if (freshIssue.prReviewState === "approved") {
286
+ postRunState = "awaiting_queue";
287
+ }
288
+ else if (freshIssue.prState === "merged") {
289
+ postRunState = "done";
290
+ }
291
+ else {
292
+ postRunState = "pr_open";
293
+ }
294
+ }
278
295
  this.db.transaction(() => {
279
296
  this.db.finishRun(run.id, {
280
297
  status: "completed",
@@ -287,8 +304,14 @@ export class RunOrchestrator {
287
304
  projectId: run.projectId,
288
305
  linearIssueId: run.linearIssueId,
289
306
  activeRunId: null,
307
+ ...(postRunState ? { factoryState: postRunState } : {}),
308
+ ...(postRunState === "awaiting_queue" ? { pendingMergePrep: true } : {}),
290
309
  });
291
310
  });
311
+ // If we advanced to awaiting_queue, enqueue for merge prep
312
+ if (postRunState === "awaiting_queue") {
313
+ this.enqueueIssue(run.projectId, run.linearIssueId);
314
+ }
292
315
  this.feed?.publish({
293
316
  level: "info",
294
317
  kind: "turn",
@@ -14,6 +14,7 @@ export class ServiceRuntime {
14
14
  webhookQueue;
15
15
  issueQueue;
16
16
  ready = false;
17
+ linearConnected = false;
17
18
  startupError;
18
19
  reconcileTimer;
19
20
  reconcileInProgress = false;
@@ -54,10 +55,14 @@ export class ServiceRuntime {
54
55
  enqueueIssue(projectId, issueId) {
55
56
  this.issueQueue.enqueue({ projectId, issueId });
56
57
  }
58
+ setLinearConnected(connected) {
59
+ this.linearConnected = connected;
60
+ }
57
61
  getReadiness() {
58
62
  return {
59
- ready: this.ready && this.codex.isStarted(),
63
+ ready: this.ready && this.codex.isStarted() && this.linearConnected,
60
64
  codexStarted: this.codex.isStarted(),
65
+ linearConnected: this.linearConnected,
61
66
  ...(this.startupError ? { startupError: this.startupError } : {}),
62
67
  };
63
68
  }
package/dist/service.js CHANGED
@@ -73,24 +73,37 @@ export class PatchRelayService {
73
73
  this.githubAppTokenManager = createGitHubAppTokenManager(ghAppCredentials, logger);
74
74
  }
75
75
  this.codex.on("notification", (notification) => {
76
- void this.orchestrator.handleCodexNotification(notification);
76
+ this.orchestrator.handleCodexNotification(notification).catch((error) => {
77
+ const msg = error instanceof Error ? error.message : String(error);
78
+ logger.error({ method: notification.method, error: msg }, "Unhandled error in Codex notification handler");
79
+ });
77
80
  });
78
81
  }
79
82
  async start() {
80
83
  // Verify Linear connectivity for all configured projects before starting.
81
- // Fail fast on auth errors rather than crashing mid-run.
84
+ // Auth errors do not prevent startup (the OAuth callback must be reachable
85
+ // for `patchrelay connect`), but the service reports NOT READY until at
86
+ // least one project has a working Linear token.
87
+ let anyLinearConnected = false;
82
88
  for (const project of this.config.projects) {
83
89
  try {
84
90
  const client = await this.linearProvider.forProject(project.id);
85
- if (!client) {
86
- this.logger.warn({ projectId: project.id }, "No Linear installation linked — project will not receive agent session events");
91
+ if (client) {
92
+ anyLinearConnected = true;
93
+ }
94
+ else {
95
+ this.logger.warn({ projectId: project.id }, "No Linear installation linked — run 'patchrelay connect' to authorize");
87
96
  }
88
97
  }
89
98
  catch (error) {
90
99
  const msg = error instanceof Error ? error.message : String(error);
91
- throw new Error(`Linear auth failed for project ${project.id}: ${msg}. Re-run "patchrelay connect" to refresh the token.`, { cause: error });
100
+ this.logger.error({ projectId: project.id, error: msg }, "Linear auth failed — run 'patchrelay connect' to refresh the token. Runs for this project will fail until re-authorized.");
92
101
  }
93
102
  }
103
+ this.runtime.setLinearConnected(anyLinearConnected);
104
+ if (!anyLinearConnected && this.config.projects.length > 0) {
105
+ this.logger.error("No projects have working Linear auth — service is NOT READY. Run 'patchrelay connect' to authorize.");
106
+ }
94
107
  if (this.githubAppTokenManager) {
95
108
  await ensureGhWrapper(this.logger);
96
109
  await this.githubAppTokenManager.start();
@@ -110,7 +123,11 @@ export class PatchRelayService {
110
123
  return await this.oauthService.createStart(params);
111
124
  }
112
125
  async completeLinearOAuth(params) {
113
- return await this.oauthService.complete(params);
126
+ const result = await this.oauthService.complete(params);
127
+ // A successful OAuth completion means at least one project now has
128
+ // working Linear auth — update readiness.
129
+ this.runtime.setLinearConnected(true);
130
+ return result;
114
131
  }
115
132
  getLinearOAuthStateStatus(state) {
116
133
  return this.oauthService.getStateStatus(state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.12.5",
3
+ "version": "0.12.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {