patchrelay 0.52.6 → 0.53.1

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.52.6",
4
- "commit": "cc8a389260f0",
5
- "builtAt": "2026-04-22T23:52:25.456Z"
3
+ "version": "0.53.1",
4
+ "commit": "6cfb95653641",
5
+ "builtAt": "2026-04-24T01:17:31.633Z"
6
6
  }
@@ -2,7 +2,7 @@ import { setTimeout as delay } from "node:timers/promises";
2
2
  import { getRunTypeFlag } from "../args.js";
3
3
  import { CliUsageError } from "../errors.js";
4
4
  import { formatJson } from "../formatters/json.js";
5
- import { formatAudit, formatClose, formatInspect, formatList, formatLive, formatOpen, formatRetry, formatSessionHistory, formatTranscriptSource, formatWorktree } from "../formatters/text.js";
5
+ import { formatAudit, formatClose, formatInspect, formatList, formatLive, formatOpen, formatPrompt, formatRetry, formatSessionHistory, formatTranscriptSource, formatWorktree } from "../formatters/text.js";
6
6
  import { buildOpenCommand } from "../interactive.js";
7
7
  import { writeOutput } from "../output.js";
8
8
  export async function handleIssueCommand(params) {
@@ -40,6 +40,8 @@ export async function handleIssueCommand(params) {
40
40
  return await handleAuditCommand(nested);
41
41
  case "transcript-source":
42
42
  return await handleTranscriptSourceCommand(nested);
43
+ case "prompt":
44
+ return await handlePromptCommand(nested);
43
45
  case "retry":
44
46
  return await handleRetryCommand(nested);
45
47
  case "close":
@@ -190,6 +192,20 @@ export async function handleRetryCommand(params) {
190
192
  writeOutput(params.stdout, params.json ? formatJson(result) : formatRetry(result));
191
193
  return 0;
192
194
  }
195
+ export async function handlePromptCommand(params) {
196
+ const issueKey = params.commandArgs[0];
197
+ if (!issueKey) {
198
+ throw new Error("prompt requires <issueKey>.");
199
+ }
200
+ const text = params.commandArgs.slice(1).join(" ").trim();
201
+ if (!text) {
202
+ throw new Error("prompt requires <text>.");
203
+ }
204
+ const result = await params.data.promptIssue(issueKey, text);
205
+ const payload = { issueKey, ...result };
206
+ writeOutput(params.stdout, params.json ? formatJson(payload) : formatPrompt(payload));
207
+ return 0;
208
+ }
193
209
  export async function handleCloseCommand(params) {
194
210
  const issueKey = params.commandArgs[0];
195
211
  if (!issueKey) {
@@ -84,6 +84,15 @@ export function formatRetry(result) {
84
84
  .filter(Boolean)
85
85
  .join("\n")}\n`;
86
86
  }
87
+ export function formatPrompt(result) {
88
+ return `${[
89
+ value("Issue", result.issueKey),
90
+ value("Delivered", result.delivered ? "yes" : "no"),
91
+ result.queued ? value("Queued", "yes") : undefined,
92
+ ]
93
+ .filter(Boolean)
94
+ .join("\n")}\n`;
95
+ }
87
96
  export function formatAudit(result) {
88
97
  const lines = [
89
98
  `${result.issue.issueKey ?? result.issue.linearIssueId}${result.issue.currentLinearState ? ` ${result.issue.currentLinearState}` : ""}`,
package/dist/cli/help.js CHANGED
@@ -39,6 +39,7 @@ export function rootHelpText() {
39
39
  " issue open <issueKey> [--print] [--json] Open Codex in the issue worktree",
40
40
  " issue sessions <issueKey> [--json] Show recorded Codex app-server sessions for one issue",
41
41
  " issue transcript-source <issueKey> [--run <id>] [--json] Show the raw Codex session file for one issue run",
42
+ " issue prompt <issueKey> <text> [--json] Send operator guidance to the active or next run",
42
43
  " issue close <issueKey> [--failed] [--reason <text>] [--json]",
43
44
  " Force-close one issue and release any active run",
44
45
  " service status [--json] Show systemd state and local health",
@@ -154,6 +155,7 @@ export function issueHelpText() {
154
155
  " open <issueKey> Open Codex in the issue worktree",
155
156
  " sessions <issueKey> Show recorded Codex app-server sessions",
156
157
  " transcript-source <issueKey> Show the raw Codex session file for one issue run",
158
+ " prompt <issueKey> <text> Send operator guidance to the active or next run",
157
159
  " retry <issueKey> Requeue a run",
158
160
  " close <issueKey> Force-close a stuck issue",
159
161
  "",
@@ -163,6 +165,7 @@ export function issueHelpText() {
163
165
  " patchrelay issue watch USE-54",
164
166
  " patchrelay issue sessions USE-54",
165
167
  " patchrelay issue transcript-source USE-54",
168
+ " patchrelay issue prompt USE-54 \"rebuild this branch cleanly from main\"",
166
169
  " patchrelay close USE-54 --reason \"already handled manually\"",
167
170
  ].join("\n");
168
171
  }
package/dist/cli/index.js CHANGED
@@ -67,6 +67,9 @@ function validateFlags(command, commandArgs, parsed) {
67
67
  case "transcript-source":
68
68
  assertKnownFlags(parsed, "issue", ["run", "json"]);
69
69
  return;
70
+ case "prompt":
71
+ assertKnownFlags(parsed, "issue", ["json"]);
72
+ return;
70
73
  case "retry":
71
74
  assertKnownFlags(parsed, "issue", ["run-type", "reason", "json"]);
72
75
  return;
@@ -15,6 +15,15 @@ export class CliOperatorApiClient {
15
15
  }
16
16
  return await this.requestJson(`/api/oauth/linear/state/${encodeURIComponent(state)}`);
17
17
  }
18
+ async promptIssue(issueKey, text) {
19
+ if (!issueKey.trim()) {
20
+ throw new Error("Issue key is required.");
21
+ }
22
+ if (!text.trim()) {
23
+ throw new Error("Prompt text is required.");
24
+ }
25
+ return await this.requestJson(`/api/issues/${encodeURIComponent(issueKey)}/prompt`, undefined, { method: "POST", body: { text } });
26
+ }
18
27
  async listInstallations() {
19
28
  return await this.requestJson("/api/installations");
20
29
  }
@@ -45,6 +45,7 @@ export class CodexAppServerClient extends EventEmitter {
45
45
  stdoutBuffer = "";
46
46
  started = false;
47
47
  stopping = false;
48
+ startPromise;
48
49
  constructor(config, logger, spawnProcess = spawn) {
49
50
  super();
50
51
  this.config = config;
@@ -68,76 +69,16 @@ export class CodexAppServerClient extends EventEmitter {
68
69
  if (this.started) {
69
70
  return;
70
71
  }
71
- this.stopping = false;
72
- const launch = resolveCodexAppServerLaunch(this.config);
73
- this.logger.info({ command: launch.command, args: launch.args }, "Starting Codex app-server");
74
- this.child = this.spawnProcess(launch.command, launch.args, {
75
- stdio: ["pipe", "pipe", "pipe"],
76
- });
77
- this.child.stdin.on("error", (error) => {
78
- this.logger.error({ error: sanitizeDiagnosticText(error.message) }, "Codex app-server stdin error");
79
- });
80
- this.child.stdout.on("error", (error) => {
81
- this.logger.error({ error: sanitizeDiagnosticText(error.message) }, "Codex app-server stdout error");
82
- });
83
- this.child.stderr.on("error", (error) => {
84
- this.logger.error({ error: sanitizeDiagnosticText(error.message) }, "Codex app-server stderr error");
85
- });
86
- this.child.stderr.on("data", (chunk) => {
87
- const line = chunk.toString().trim();
88
- if (line) {
89
- this.logger.warn({ output: sanitizeDiagnosticText(line) }, "Codex app-server stderr");
90
- }
91
- });
92
- this.child.on("error", (error) => {
93
- const err = error instanceof Error ? error : new Error(String(error));
94
- this.logger.error({
95
- error: sanitizeDiagnosticText(err.message),
96
- pendingRequestCount: this.pending.size,
97
- }, "Codex app-server process errored");
98
- this.rejectAllPending(err);
99
- });
100
- this.child.on("close", (code, signal) => {
101
- this.started = false;
102
- const log = this.stopping ? this.logger.info.bind(this.logger) : this.logger.warn.bind(this.logger);
103
- log({
104
- code: code ?? 1,
105
- signal: signal ?? null,
106
- pendingRequestCount: this.pending.size,
107
- }, this.stopping ? "Codex app-server stopped" : "Codex app-server exited");
108
- this.stopping = false;
109
- this.rejectAllPending(new Error(`Codex app-server exited with code ${code ?? 1}`));
110
- });
111
- this.child.stdout.on("data", (chunk) => {
112
- this.stdoutBuffer += chunk.toString("utf8");
113
- if (this.stdoutBuffer.length > 50 * 1024 * 1024) {
114
- this.logger.error({ bufferSize: this.stdoutBuffer.length }, "Codex app-server stdout buffer exceeded 50 MB — killing process");
115
- this.stdoutBuffer = "";
116
- this.rejectAllPending(new Error("Codex app-server stdout buffer overflow"));
117
- this.child?.kill("SIGTERM");
118
- return;
119
- }
120
- this.drainMessages();
121
- });
122
- const initializeResponse = await this.sendRequest("initialize", {
123
- clientInfo: {
124
- name: "patchrelay",
125
- title: "PatchRelay",
126
- version: "0.1.0",
127
- },
128
- capabilities: {
129
- experimentalApi: true,
130
- },
131
- });
132
- const serverInfo = initializeResponse && typeof initializeResponse === "object" && "serverInfo" in initializeResponse
133
- ? initializeResponse.serverInfo
134
- : undefined;
135
- this.logger.info({
136
- serverName: typeof serverInfo?.name === "string" ? serverInfo.name : undefined,
137
- serverVersion: typeof serverInfo?.version === "string" ? serverInfo.version : undefined,
138
- }, "Connected to Codex app-server");
139
- this.sendNotification("initialized");
140
- this.started = true;
72
+ if (this.startPromise) {
73
+ return await this.startPromise;
74
+ }
75
+ this.startPromise = this.startInternal();
76
+ try {
77
+ await this.startPromise;
78
+ }
79
+ finally {
80
+ this.startPromise = undefined;
81
+ }
141
82
  }
142
83
  async stop() {
143
84
  const child = this.child;
@@ -276,9 +217,7 @@ export class CodexAppServerClient extends EventEmitter {
276
217
  });
277
218
  }
278
219
  async sendRequest(method, params) {
279
- if (!this.child?.stdin) {
280
- throw new Error("Codex app-server is not running");
281
- }
220
+ await this.ensureRunningForRequest(method);
282
221
  const id = this.nextRequestId++;
283
222
  const requestTimeoutMs = this.config.requestTimeoutMs ?? CodexAppServerClient.DEFAULT_REQUEST_TIMEOUT_MS;
284
223
  const promise = new Promise((resolve, reject) => {
@@ -316,6 +255,93 @@ export class CodexAppServerClient extends EventEmitter {
316
255
  throw err;
317
256
  });
318
257
  }
258
+ async startInternal() {
259
+ this.stopping = false;
260
+ this.stdoutBuffer = "";
261
+ const launch = resolveCodexAppServerLaunch(this.config);
262
+ this.logger.info({ command: launch.command, args: launch.args }, "Starting Codex app-server");
263
+ this.child = this.spawnProcess(launch.command, launch.args, {
264
+ stdio: ["pipe", "pipe", "pipe"],
265
+ });
266
+ this.child.stdin.on("error", (error) => {
267
+ this.logger.error({ error: sanitizeDiagnosticText(error.message) }, "Codex app-server stdin error");
268
+ });
269
+ this.child.stdout.on("error", (error) => {
270
+ this.logger.error({ error: sanitizeDiagnosticText(error.message) }, "Codex app-server stdout error");
271
+ });
272
+ this.child.stderr.on("error", (error) => {
273
+ this.logger.error({ error: sanitizeDiagnosticText(error.message) }, "Codex app-server stderr error");
274
+ });
275
+ this.child.stderr.on("data", (chunk) => {
276
+ const line = chunk.toString().trim();
277
+ if (line) {
278
+ this.logger.warn({ output: sanitizeDiagnosticText(line) }, "Codex app-server stderr");
279
+ }
280
+ });
281
+ this.child.on("error", (error) => {
282
+ const err = error instanceof Error ? error : new Error(String(error));
283
+ this.logger.error({
284
+ error: sanitizeDiagnosticText(err.message),
285
+ pendingRequestCount: this.pending.size,
286
+ }, "Codex app-server process errored");
287
+ this.rejectAllPending(err);
288
+ });
289
+ this.child.on("close", (code, signal) => {
290
+ this.started = false;
291
+ this.child = undefined;
292
+ this.stdoutBuffer = "";
293
+ const log = this.stopping ? this.logger.info.bind(this.logger) : this.logger.warn.bind(this.logger);
294
+ log({
295
+ code: code ?? 1,
296
+ signal: signal ?? null,
297
+ pendingRequestCount: this.pending.size,
298
+ }, this.stopping ? "Codex app-server stopped" : "Codex app-server exited");
299
+ this.stopping = false;
300
+ this.rejectAllPending(new Error(`Codex app-server exited with code ${code ?? 1}`));
301
+ });
302
+ this.child.stdout.on("data", (chunk) => {
303
+ this.stdoutBuffer += chunk.toString("utf8");
304
+ if (this.stdoutBuffer.length > 50 * 1024 * 1024) {
305
+ this.logger.error({ bufferSize: this.stdoutBuffer.length }, "Codex app-server stdout buffer exceeded 50 MB — killing process");
306
+ this.stdoutBuffer = "";
307
+ this.rejectAllPending(new Error("Codex app-server stdout buffer overflow"));
308
+ this.child?.kill("SIGTERM");
309
+ return;
310
+ }
311
+ this.drainMessages();
312
+ });
313
+ const initializeResponse = await this.sendRequest("initialize", {
314
+ clientInfo: {
315
+ name: "patchrelay",
316
+ title: "PatchRelay",
317
+ version: "0.1.0",
318
+ },
319
+ capabilities: {
320
+ experimentalApi: true,
321
+ },
322
+ });
323
+ const serverInfo = initializeResponse && typeof initializeResponse === "object" && "serverInfo" in initializeResponse
324
+ ? initializeResponse.serverInfo
325
+ : undefined;
326
+ this.logger.info({
327
+ serverName: typeof serverInfo?.name === "string" ? serverInfo.name : undefined,
328
+ serverVersion: typeof serverInfo?.version === "string" ? serverInfo.version : undefined,
329
+ }, "Connected to Codex app-server");
330
+ this.sendNotification("initialized");
331
+ this.started = true;
332
+ }
333
+ async ensureRunningForRequest(method) {
334
+ if (this.child?.stdin) {
335
+ return;
336
+ }
337
+ if (method !== "initialize") {
338
+ this.logger.warn({ method }, "Codex app-server is unavailable before request; restarting");
339
+ }
340
+ await this.start();
341
+ if (!this.child?.stdin) {
342
+ throw new Error("Codex app-server is not running");
343
+ }
344
+ }
319
345
  writeMessage(message) {
320
346
  if (!this.child?.stdin) {
321
347
  throw new Error("Codex app-server stdin is unavailable");
@@ -306,6 +306,7 @@ function buildCiRepairContext(context) {
306
306
  "Goal: restore CI readiness and push a branch that is likely to pass the next full CI run.",
307
307
  "Before changing code or config, reproduce the failure on the exact failing head or identify the concrete log signature that justifies the fix.",
308
308
  "If the exact failing head does not reproduce locally and the logs do not support a scoped fix, prefer a rerun-only repair over speculative branch changes.",
309
+ "Do not use broad revert stacks or repo-wide package-manager/workflow/docs cleanups as a repair tactic; stay on the failing incident only.",
309
310
  snapshot?.gateCheckName ? `Gate check: ${String(snapshot.gateCheckName)}` : "",
310
311
  snapshot?.gateCheckStatus ? `Gate status: ${String(snapshot.gateCheckStatus)}` : "",
311
312
  snapshot?.settledAt ? `Settled at: ${String(snapshot.settledAt)}` : "",
@@ -510,6 +511,9 @@ function buildPrePushSelfReviewSection(target, runType) {
510
511
  lines.push("Name 2-4 concrete invariants most likely to regress in the touched flow, confirm which file or path enforces each one, and verify at least one adjacent path you did not edit directly.", "If you changed schema, enums, shared vocabulary, normalization helpers, or compatibility mappings, inspect the main read/write paths that can bypass the new abstraction and verify one legacy-flow and one new-flow case before publishing.");
511
512
  }
512
513
  lines.push("Do not widen scope for optional cleanup. If the issue explicitly allows a non-PR outcome, complete that outcome clearly; otherwise publish before stopping.");
514
+ if (runType === "review_fix" || runType === "branch_upkeep" || runType === "ci_repair" || runType === "queue_repair") {
515
+ lines.push("On reactive repair runs, do not publish broad revert stacks or unrelated workflow/package-manager/docs churn. If that seems necessary, stop and surface the blocker instead.");
516
+ }
513
517
  return lines;
514
518
  }
515
519
  function buildPublicationContract(runType, issueClass) {
@@ -0,0 +1,30 @@
1
+ import { execCommand, safeJsonParse } from "./utils.js";
2
+ export async function readReactivePublishDelta(repoFullName, baseHeadSha, publishedHeadSha) {
3
+ const { stdout, exitCode } = await execCommand("gh", [
4
+ "api",
5
+ `repos/${repoFullName}/compare/${baseHeadSha}...${publishedHeadSha}`,
6
+ ], { timeoutMs: 10_000 });
7
+ if (exitCode !== 0) {
8
+ return undefined;
9
+ }
10
+ const payload = safeJsonParse(stdout);
11
+ if (!payload) {
12
+ return undefined;
13
+ }
14
+ const changedFiles = (payload.files ?? [])
15
+ .map((entry) => entry.filename?.trim())
16
+ .filter((entry) => Boolean(entry));
17
+ const commitSubjects = (payload.commits ?? [])
18
+ .map((entry) => firstLine(entry.commit?.message))
19
+ .filter((entry) => Boolean(entry));
20
+ return {
21
+ changedFiles,
22
+ commitSubjects,
23
+ };
24
+ }
25
+ function firstLine(value) {
26
+ const trimmed = value?.trim();
27
+ if (!trimmed)
28
+ return undefined;
29
+ return trimmed.split(/\r?\n/, 1)[0]?.trim() || undefined;
30
+ }
@@ -1,5 +1,21 @@
1
1
  import { buildReviewFixBranchUpkeepContext, isDirtyMergeStateStatus, isRequestedChangesRunType, readReactivePrSnapshot, } from "./reactive-pr-state.js";
2
+ import { readReactivePublishDelta } from "./reactive-publish-delta.js";
2
3
  import { readLatestRequestedChangesReviewContext } from "./remote-pr-review.js";
4
+ const REACTIVE_SCOPE_RISK_PREFIXES = [
5
+ ".github/workflows/",
6
+ "scripts/bootstrap-worktree.",
7
+ ];
8
+ const REACTIVE_SCOPE_RISK_EXACT_PATHS = new Set([
9
+ ".patchrelay/hooks/prepare-worktree",
10
+ "AGENTS.md",
11
+ "CLAUDE.md",
12
+ "DEV_SETUP.md",
13
+ "IMPLEMENTATION_WORKFLOW.md",
14
+ "REVIEW_WORKFLOW.md",
15
+ "package.json",
16
+ "package-lock.json",
17
+ "pnpm-lock.yaml",
18
+ ]);
3
19
  export class ReactiveRunPolicy {
4
20
  config;
5
21
  db;
@@ -73,6 +89,57 @@ export class ReactiveRunPolicy {
73
89
  return undefined;
74
90
  }
75
91
  }
92
+ async verifyReactiveRunStayedInScope(run, issue) {
93
+ if (run.runType !== "ci_repair" && run.runType !== "review_fix" && run.runType !== "queue_repair" && run.runType !== "branch_upkeep") {
94
+ return undefined;
95
+ }
96
+ if (!issue.prNumber || issue.prState !== "open") {
97
+ return undefined;
98
+ }
99
+ const project = this.config.projects.find((entry) => entry.id === run.projectId);
100
+ const repoFullName = project?.github?.repoFullName;
101
+ const baselineHeadSha = resolveReactiveBaselineHead(run, issue);
102
+ if (!repoFullName || !baselineHeadSha) {
103
+ return undefined;
104
+ }
105
+ try {
106
+ const snapshot = await readReactivePrSnapshot(this.config, run.projectId, issue.prNumber);
107
+ if (!snapshot || snapshot.prState !== "open" || !snapshot.headSha || snapshot.headSha === baselineHeadSha) {
108
+ return undefined;
109
+ }
110
+ const delta = await readReactivePublishDelta(repoFullName, baselineHeadSha, snapshot.headSha);
111
+ if (!delta) {
112
+ return undefined;
113
+ }
114
+ const revertSubjects = delta.commitSubjects.filter((subject) => /^Revert\s+"/.test(subject));
115
+ if (revertSubjects.length > 0) {
116
+ return [
117
+ `Reactive ${run.runType.replace("_", "-")} for PR #${issue.prNumber} introduced revert commit(s) after ${baselineHeadSha.slice(0, 8)}.`,
118
+ `PatchRelay must use scoped edits or a clean reroll instead of revert-stack cleanup.`,
119
+ `Reverts: ${revertSubjects.slice(0, 3).join("; ")}`,
120
+ ].join(" ");
121
+ }
122
+ if (run.runType === "review_fix" || run.runType === "ci_repair") {
123
+ const riskyFiles = delta.changedFiles.filter(isReactiveScopeRiskPath);
124
+ if (riskyFiles.length > 0) {
125
+ return [
126
+ `Reactive ${run.runType.replace("_", "-")} for PR #${issue.prNumber} widened scope after ${baselineHeadSha.slice(0, 8)} by touching repo-meta files.`,
127
+ `PatchRelay should stop instead of publishing workflow, package-manager, bootstrap, or docs churn during reactive repair.`,
128
+ `Files: ${riskyFiles.slice(0, 6).join(", ")}`,
129
+ ].join(" ");
130
+ }
131
+ }
132
+ return undefined;
133
+ }
134
+ catch (error) {
135
+ this.logger.debug({
136
+ issueKey: issue.issueKey,
137
+ prNumber: issue.prNumber,
138
+ error: error instanceof Error ? error.message : String(error),
139
+ }, "Failed to verify reactive publish scope");
140
+ return undefined;
141
+ }
142
+ }
76
143
  async refreshIssueAfterReactivePublish(run, issue) {
77
144
  if (run.runType !== "ci_repair" && run.runType !== "queue_repair" && !isRequestedChangesRunType(run.runType)) {
78
145
  return this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? issue;
@@ -235,6 +302,19 @@ export class ReactiveRunPolicy {
235
302
  };
236
303
  }
237
304
  }
305
+ function resolveReactiveBaselineHead(run, issue) {
306
+ if (run.runType === "review_fix" || run.runType === "branch_upkeep") {
307
+ return run.sourceHeadSha;
308
+ }
309
+ if (run.runType === "ci_repair" || run.runType === "queue_repair") {
310
+ return issue.lastGitHubFailureHeadSha;
311
+ }
312
+ return undefined;
313
+ }
314
+ function isReactiveScopeRiskPath(filePath) {
315
+ return REACTIVE_SCOPE_RISK_EXACT_PATHS.has(filePath)
316
+ || REACTIVE_SCOPE_RISK_PREFIXES.some((prefix) => filePath.startsWith(prefix));
317
+ }
238
318
  function hasStructuredReviewContext(context) {
239
319
  if (!context)
240
320
  return false;
@@ -27,6 +27,9 @@ export class RunCompletionPolicy {
27
27
  async verifyReviewFixAdvancedHead(run, issue) {
28
28
  return await this.reactive.verifyReviewFixAdvancedHead(run, issue);
29
29
  }
30
+ async verifyReactiveRunStayedInScope(run, issue) {
31
+ return await this.reactive.verifyReactiveRunStayedInScope(run, issue);
32
+ }
30
33
  async refreshIssueAfterReactivePublish(run, issue) {
31
34
  return await this.reactive.refreshIssueAfterReactivePublish(run, issue);
32
35
  }
@@ -231,6 +231,19 @@ export class RunFinalizer {
231
231
  });
232
232
  return;
233
233
  }
234
+ const reactiveScopeError = await this.completionPolicy.verifyReactiveRunStayedInScope(run, freshIssue);
235
+ if (reactiveScopeError) {
236
+ this.failRunAndClear(run, reactiveScopeError, "escalated");
237
+ this.syncFailureOutcome({
238
+ run,
239
+ fallbackIssue: freshIssue,
240
+ message: reactiveScopeError,
241
+ level: "error",
242
+ status: "reactive_scope_drift_blocked",
243
+ summary: reactiveScopeError,
244
+ });
245
+ return;
246
+ }
234
247
  const publishedOutcomeError = await this.completionPolicy.verifyPublishedRunOutcome(run, freshIssue);
235
248
  if (publishedOutcomeError) {
236
249
  await handleNoPrCompletionCheck({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.52.6",
3
+ "version": "0.53.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {