patchrelay 0.36.5 → 0.36.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,4 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import path from "node:path";
4
2
  import { ACTIVE_RUN_STATES, TERMINAL_STATES } from "./factory-state.js";
5
3
  import { buildHookEnv, runProjectHook } from "./hook-runner.js";
6
4
  import { buildStageReport, countEventMethods, extractTurnId, resolveRunCompletionStatus, summarizeCurrentThread, } from "./run-reporting.js";
@@ -10,9 +8,16 @@ import { resolveAuthoritativeLinearStopState, resolvePreferredCompletedLinearSta
10
8
  import { execCommand } from "./utils.js";
11
9
  import { getThreadTurns } from "./codex-thread-utils.js";
12
10
  import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
11
+ import { loadPatchRelayRepoPrompting } from "./patchrelay-customization.js";
12
+ import { buildRunPrompt as buildPatchRelayRunPrompt, findDisallowedPatchRelayPromptSectionIds, findUnknownPatchRelayPromptSectionIds, mergePromptCustomizationLayers, resolveImplementationDeliveryMode, resolvePromptLayers, } from "./prompting/patchrelay.js";
13
13
  const DEFAULT_CI_REPAIR_BUDGET = 3;
14
14
  const DEFAULT_QUEUE_REPAIR_BUDGET = 3;
15
- const DEFAULT_REVIEW_FIX_BUDGET = 6;
15
+ // Requested-changes loops can legitimately take more iterations than CI/queue
16
+ // repair when the reviewer is catching nuanced product or timing bugs across
17
+ // successive heads. Keep a hard ceiling to prevent infinite ping-pong, but make
18
+ // it wide enough that real review cycles can continue after multiple successful
19
+ // head advances.
20
+ const DEFAULT_REVIEW_FIX_BUDGET = 12;
16
21
  const DEFAULT_ZOMBIE_RECOVERY_BUDGET = 5;
17
22
  const ZOMBIE_RECOVERY_BASE_DELAY_MS = 15_000; // 15s, 30s, 60s, 120s, 240s
18
23
  const ISSUE_SESSION_LEASE_MS = 10 * 60_000;
@@ -30,156 +35,6 @@ function sanitizePathSegment(value) {
30
35
  function lowerCaseFirst(value) {
31
36
  return value ? `${value.slice(0, 1).toLowerCase()}${value.slice(1)}` : value;
32
37
  }
33
- const WORKFLOW_FILES = {
34
- implementation: "IMPLEMENTATION_WORKFLOW.md",
35
- review_fix: "REVIEW_WORKFLOW.md",
36
- branch_upkeep: "REVIEW_WORKFLOW.md",
37
- ci_repair: "IMPLEMENTATION_WORKFLOW.md",
38
- queue_repair: "IMPLEMENTATION_WORKFLOW.md",
39
- };
40
- function readWorkflowFile(repoPath, runType) {
41
- const filename = WORKFLOW_FILES[runType];
42
- const filePath = path.join(repoPath, filename);
43
- if (!existsSync(filePath))
44
- return undefined;
45
- return readFileSync(filePath, "utf8").trim();
46
- }
47
- function collectImplementationInstructionText(issue, context, promptText) {
48
- const parts = [];
49
- if (issue.title)
50
- parts.push(issue.title);
51
- if (issue.description)
52
- parts.push(issue.description);
53
- if (promptText)
54
- parts.push(promptText);
55
- const stringFields = ["promptContext", "promptBody", "operatorPrompt", "userComment"];
56
- for (const field of stringFields) {
57
- const value = context?.[field];
58
- if (typeof value === "string" && value.trim()) {
59
- parts.push(value);
60
- }
61
- }
62
- if (Array.isArray(context?.followUps)) {
63
- for (const entry of context.followUps) {
64
- if (!entry || typeof entry !== "object")
65
- continue;
66
- const text = entry.text;
67
- if (typeof text === "string" && text.trim()) {
68
- parts.push(text);
69
- }
70
- }
71
- }
72
- return parts.join("\n").toLowerCase();
73
- }
74
- export function resolveImplementationDeliveryMode(issue, context, promptText) {
75
- const instructionText = collectImplementationInstructionText(issue, context, promptText);
76
- if (!instructionText)
77
- return "publish_pr";
78
- const hasExplicitNoPr = [
79
- /\bdo not open (?:a |any )?pr\b/,
80
- /\bdo not open (?:a |any )?pull request\b/,
81
- /\bno pr is opened\b/,
82
- /\bpatchrelay should not open a pr\b/,
83
- /\bwithout opening a pr\b/,
84
- ].some((pattern) => pattern.test(instructionText));
85
- const forbidsRepoChanges = [
86
- /\bdo not make repository changes\b/,
87
- /\bdo not make repo changes\b/,
88
- /\bno repository changes\b/,
89
- /\bno repo changes\b/,
90
- /\bdo not modify repo files\b/,
91
- ].some((pattern) => pattern.test(instructionText));
92
- const planningOnly = [
93
- /\bplanning\/specification issue only\b/,
94
- /\bplanning[- ]only\b/,
95
- /\bspecification[- ]only\b/,
96
- /\bplanning issue only\b/,
97
- ].some((pattern) => pattern.test(instructionText));
98
- if (hasExplicitNoPr || (planningOnly && forbidsRepoChanges)) {
99
- return "linear_only";
100
- }
101
- return "publish_pr";
102
- }
103
- function appendPublicationContract(lines, runType, issue, context) {
104
- const deliveryMode = runType === "implementation" && issue
105
- ? resolveImplementationDeliveryMode(issue, context)
106
- : "publish_pr";
107
- if (runType === "implementation" && deliveryMode === "linear_only") {
108
- lines.push("## Delivery Requirements", "");
109
- lines.push("This issue is planning/specification only.", "Do not modify repo files or open a PR for this issue.", "Deliver the result through Linear artifacts such as follow-up issues, documents, and a concise summary.", "Leave the worktree clean before stopping.", "");
110
- return;
111
- }
112
- lines.push("## Publication Requirements", "");
113
- if (runType === "implementation") {
114
- lines.push("Before finishing, publish the result instead of leaving it only in the worktree.", "If the worktree already contains relevant changes for this issue, verify them and publish them.", "If you changed files for this issue, commit them, push the issue branch, and open or update the PR before stopping.", "Do not stop with only local commits or uncommitted changes.", "");
115
- return;
116
- }
117
- lines.push("Before finishing, publish the result to the existing PR branch.", "If you changed files for this repair, commit them and push the same branch before stopping.", "Do not open a new PR.", "Do not stop with only local commits or uncommitted changes.", "");
118
- }
119
- function buildPromptHeader(issue) {
120
- return [
121
- `Issue: ${issue.issueKey ?? issue.linearIssueId}`,
122
- issue.title ? `Title: ${issue.title}` : undefined,
123
- issue.branchName ? `Branch: ${issue.branchName}` : undefined,
124
- issue.prNumber ? `PR: #${issue.prNumber}` : undefined,
125
- "",
126
- ].filter(Boolean);
127
- }
128
- function appendTaskObjective(lines, issue) {
129
- const description = issue.description?.trim();
130
- lines.push("## Task Objective", "");
131
- lines.push(issue.title || `Complete ${issue.issueKey ?? issue.linearIssueId}.`);
132
- if (description) {
133
- lines.push("", description);
134
- }
135
- lines.push("");
136
- }
137
- function extractIssueSection(description, heading) {
138
- if (!description)
139
- return undefined;
140
- const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
141
- const pattern = new RegExp(`^## ${escaped}\\s*$([\\s\\S]*?)(?=^##\\s+|$)`, "im");
142
- const match = description.match(pattern);
143
- const body = match?.[1]?.trim();
144
- return body && body.length > 0 ? body : undefined;
145
- }
146
- function appendScopeDiscipline(lines, issue) {
147
- const description = issue.description?.trim();
148
- const scope = extractIssueSection(description, "Scope");
149
- const acceptance = extractIssueSection(description, "Acceptance criteria")
150
- ?? extractIssueSection(description, "Success criteria");
151
- const relevantCode = extractIssueSection(description, "Relevant code");
152
- lines.push("## Scope Discipline", "");
153
- lines.push("Stay inside the delegated task.", "Finish the issue completely enough to satisfy its stated scope and acceptance criteria, but do not widen it into unrelated product polish or follow-up cleanup.", "Only broaden to adjacent routes, copy, or supporting surfaces when the issue text or repository guidance explicitly says they are the same user flow.", "If you notice a worthwhile broader inconsistency that is not required to make this task correct, mention it in your summary as follow-up context instead of expanding the implementation.", "");
154
- if (scope) {
155
- lines.push("### In Scope", "", scope, "");
156
- }
157
- if (acceptance) {
158
- lines.push("### Acceptance / Done", "", acceptance, "");
159
- }
160
- if (relevantCode) {
161
- lines.push("### Relevant Code", "", relevantCode, "");
162
- }
163
- lines.push("### Likely Review Invariants", "", "- Check the surfaces explicitly named in the task before stopping.", "- If repository guidance says certain changed surfaces are one flow, verify that shared flow, but do not treat unrelated surrounding cleanup as part of this task.", "- A review repair should fix the concrete concern on the current head, not silently expand the Linear issue into a broader rewrite.", "");
164
- }
165
- function appendLinearContext(lines, context) {
166
- const promptContext = typeof context?.promptContext === "string" ? context.promptContext.trim() : "";
167
- const latestPrompt = typeof context?.promptBody === "string" ? context.promptBody.trim() : "";
168
- const operatorPrompt = typeof context?.operatorPrompt === "string" ? context.operatorPrompt.trim() : "";
169
- const userComment = typeof context?.userComment === "string" ? context.userComment.trim() : "";
170
- if (promptContext) {
171
- lines.push("## Linear Session Context", "", promptContext, "");
172
- }
173
- if (latestPrompt) {
174
- lines.push("## Latest Human Instruction", "", latestPrompt, "");
175
- }
176
- if (operatorPrompt) {
177
- lines.push("## Operator Prompt", "", operatorPrompt, "");
178
- }
179
- if (userComment) {
180
- lines.push("## Human Follow-up Comment", "", userComment, "");
181
- }
182
- }
183
38
  function isRequestedChangesRunType(runType) {
184
39
  return runType === "review_fix" || runType === "branch_upkeep";
185
40
  }
@@ -191,332 +46,6 @@ function resolveRequestedChangesMode(runType, context) {
191
46
  ? "branch_upkeep"
192
47
  : "address_review_feedback";
193
48
  }
194
- function readReviewFixComments(context) {
195
- const raw = context?.reviewComments;
196
- if (!Array.isArray(raw)) {
197
- return [];
198
- }
199
- const comments = [];
200
- for (const entry of raw) {
201
- if (!entry || typeof entry !== "object")
202
- continue;
203
- const record = entry;
204
- const body = typeof record.body === "string" ? record.body.trim() : "";
205
- if (!body)
206
- continue;
207
- comments.push({
208
- body,
209
- ...(typeof record.path === "string" ? { path: record.path } : {}),
210
- ...(typeof record.line === "number" ? { line: record.line } : {}),
211
- ...(typeof record.side === "string" ? { side: record.side } : {}),
212
- ...(typeof record.startLine === "number" ? { startLine: record.startLine } : {}),
213
- ...(typeof record.startSide === "string" ? { startSide: record.startSide } : {}),
214
- ...(typeof record.url === "string" ? { url: record.url } : {}),
215
- ...(typeof record.authorLogin === "string" ? { authorLogin: record.authorLogin } : {}),
216
- });
217
- }
218
- return comments;
219
- }
220
- function appendStructuredReviewContext(lines, context) {
221
- const reviewId = typeof context?.reviewId === "number" ? context.reviewId : undefined;
222
- const reviewCommitId = typeof context?.reviewCommitId === "string" ? context.reviewCommitId : undefined;
223
- const reviewUrl = typeof context?.reviewUrl === "string" ? context.reviewUrl : undefined;
224
- const reviewComments = readReviewFixComments(context);
225
- if (!reviewId && !reviewCommitId && !reviewUrl && reviewComments.length === 0) {
226
- return;
227
- }
228
- lines.push("## Structured Review Context", "");
229
- if (reviewId !== undefined) {
230
- lines.push(`Review ID: ${reviewId}`);
231
- }
232
- if (reviewCommitId) {
233
- lines.push(`Reviewed commit: ${reviewCommitId}`);
234
- }
235
- if (reviewUrl) {
236
- lines.push(`Review URL: ${reviewUrl}`);
237
- }
238
- if (reviewComments.length === 0) {
239
- lines.push("No inline review comments were captured for this review.", "");
240
- return;
241
- }
242
- lines.push(`Inline review comments captured: ${reviewComments.length}`, "Resolve each comment below or verify it is already fixed on the current head before you stop.", "A requested-changes turn is only complete if you push a newer PR head or deliberately escalate because you are blocked.", "");
243
- for (const comment of reviewComments) {
244
- const location = comment.path
245
- ? `${comment.path}${comment.line !== undefined ? `:${comment.line}` : ""}${comment.side ? ` (${comment.side})` : ""}`
246
- : "general";
247
- lines.push(`- ${location}`);
248
- lines.push(comment.body);
249
- if (comment.url) {
250
- lines.push(` URL: ${comment.url}`);
251
- }
252
- }
253
- lines.push("");
254
- }
255
- function collectFollowUpInputs(context) {
256
- const followUps = Array.isArray(context?.followUps) ? context.followUps : [];
257
- const inputs = [];
258
- for (const entry of followUps) {
259
- const followUp = entry && typeof entry === "object" ? entry : undefined;
260
- const type = typeof followUp?.type === "string" ? followUp.type : "followup";
261
- const author = typeof followUp?.author === "string" ? followUp.author : undefined;
262
- const text = typeof followUp?.text === "string" ? followUp.text.trim() : "";
263
- if (!text)
264
- continue;
265
- inputs.push({ type, text, ...(author ? { author } : {}) });
266
- }
267
- return inputs;
268
- }
269
- function resolveFollowUpWhy(runType, context) {
270
- const wakeReason = typeof context?.wakeReason === "string" ? context.wakeReason : undefined;
271
- switch (wakeReason) {
272
- case "direct_reply":
273
- return "A human reply arrived for the outstanding question from the previous turn.";
274
- case "followup_prompt":
275
- return "A new Linear agent prompt arrived after the previous turn.";
276
- case "followup_comment":
277
- return "A human follow-up comment arrived after the previous turn.";
278
- case "operator_prompt":
279
- return "An operator supplied new guidance for this issue.";
280
- case "review_changes_requested":
281
- return "GitHub review requested changes on the current PR head.";
282
- case "branch_upkeep":
283
- return "GitHub still shows the PR branch as needing upkeep after the requested code change was addressed.";
284
- case "settled_red_ci":
285
- return "Required CI settled red for the current PR head.";
286
- case "merge_steward_incident":
287
- return "Merge Steward reported an incident on the current PR head.";
288
- case "delegated":
289
- return runType === "implementation"
290
- ? "This is the first implementation turn for the delegated issue."
291
- : `This turn continues ${runType.replaceAll("_", " ")} work for the delegated issue.`;
292
- default:
293
- if (isRequestedChangesRunType(runType)) {
294
- return resolveRequestedChangesMode(runType, context) === "branch_upkeep"
295
- ? "This turn continues branch upkeep on the existing PR after requested changes."
296
- : "This turn continues requested-changes work on the existing PR.";
297
- }
298
- if (runType === "ci_repair")
299
- return "This turn continues CI repair work on the existing PR.";
300
- if (runType === "queue_repair")
301
- return "This turn continues merge-queue repair work on the existing PR.";
302
- return "This turn continues implementation on the existing issue session.";
303
- }
304
- }
305
- function resolveFollowUpAction(runType, context) {
306
- if (context?.directReplyMode === true) {
307
- return "Apply the latest human answer, continue from the current branch/session context, and only ask another question if you are still blocked.";
308
- }
309
- if (isRequestedChangesRunType(runType) && resolveRequestedChangesMode(runType, context) === "branch_upkeep") {
310
- const baseBranch = typeof context?.baseBranch === "string" ? context.baseBranch : "main";
311
- return `Update the existing PR branch onto latest ${baseBranch}, resolve conflicts if needed, rerun narrow verification, and push a newer head on the same branch.`;
312
- }
313
- switch (runType) {
314
- case "review_fix":
315
- return "Address the review feedback on the current PR branch, verify the fix, and push a newer head on the same branch.";
316
- case "branch_upkeep":
317
- return "Repair the existing PR branch after requested changes, rerun narrow verification, and push a newer head on the same branch.";
318
- case "ci_repair":
319
- return "Fix the failing CI root cause on the current PR branch, verify it locally, and push the same branch.";
320
- case "queue_repair":
321
- return "Repair the merge-queue incident on the current PR branch, verify the fix, and push the same branch.";
322
- case "implementation":
323
- default:
324
- return "Continue from the latest branch state, incorporate the new input, and publish updates to the existing issue branch if you make changes.";
325
- }
326
- }
327
- function hasAuthoritativeGitHubFacts(issue, runType, context) {
328
- return issue.prNumber !== undefined
329
- || issue.prHeadSha !== undefined
330
- || runType !== "implementation"
331
- || typeof context?.failureHeadSha === "string"
332
- || typeof context?.failingHeadSha === "string"
333
- || typeof context?.mergeStateStatus === "string"
334
- || typeof context?.checkName === "string"
335
- || typeof context?.reviewerName === "string";
336
- }
337
- function appendAuthoritativeGitHubFacts(lines, issue, runType, context) {
338
- if (!hasAuthoritativeGitHubFacts(issue, runType, context)) {
339
- return;
340
- }
341
- const prNumber = issue.prNumber !== undefined ? `#${issue.prNumber}` : undefined;
342
- const headSha = typeof context?.failureHeadSha === "string"
343
- ? context.failureHeadSha
344
- : typeof context?.failingHeadSha === "string"
345
- ? context.failingHeadSha
346
- : issue.prHeadSha;
347
- const mergeStateStatus = typeof context?.mergeStateStatus === "string" ? context.mergeStateStatus : undefined;
348
- const baseBranch = typeof context?.baseBranch === "string" ? context.baseBranch : undefined;
349
- const checkName = typeof context?.checkName === "string" ? context.checkName : undefined;
350
- const jobName = typeof context?.jobName === "string" ? context.jobName : undefined;
351
- const stepName = typeof context?.stepName === "string" ? context.stepName : undefined;
352
- const reviewerName = typeof context?.reviewerName === "string" ? context.reviewerName : undefined;
353
- const reviewBody = typeof context?.reviewBody === "string" ? context.reviewBody.trim() : "";
354
- const summary = typeof context?.summary === "string" ? context.summary : undefined;
355
- lines.push("## Authoritative GitHub Facts", "");
356
- if (prNumber) {
357
- lines.push(`- Current PR: ${prNumber}`);
358
- }
359
- if (headSha) {
360
- lines.push(`- Current relevant head SHA: ${headSha}`);
361
- }
362
- if (issue.prReviewState) {
363
- lines.push(`- Current review state: ${issue.prReviewState}`);
364
- }
365
- if (issue.prCheckStatus) {
366
- lines.push(`- Current check status: ${issue.prCheckStatus}`);
367
- }
368
- if (mergeStateStatus) {
369
- lines.push(`- Merge state against ${baseBranch ?? "base"}: ${mergeStateStatus}`);
370
- }
371
- if (checkName) {
372
- lines.push(`- Relevant check: ${checkName}`);
373
- }
374
- if (jobName && jobName !== checkName) {
375
- lines.push(`- Relevant job: ${jobName}`);
376
- }
377
- if (stepName) {
378
- lines.push(`- Relevant step: ${stepName}`);
379
- }
380
- if (reviewerName) {
381
- lines.push(`- Reviewer: ${reviewerName}`);
382
- }
383
- if (summary) {
384
- lines.push(`- Summary: ${summary}`);
385
- }
386
- if (reviewBody) {
387
- lines.push(`- Review body: ${reviewBody}`);
388
- }
389
- lines.push("");
390
- }
391
- function appendFactFreshness(lines, issue, runType, context) {
392
- if (!hasAuthoritativeGitHubFacts(issue, runType, context)) {
393
- return;
394
- }
395
- const hasFreshFacts = context?.githubFactsFresh === true || context?.branchUpkeepRequired === true;
396
- lines.push("## Fact Freshness", "");
397
- if (hasFreshFacts) {
398
- lines.push("GitHub facts below were refreshed immediately before this turn was created.");
399
- }
400
- else {
401
- lines.push("GitHub facts below came from the triggering event or last known reconciliation state and may now be stale.");
402
- lines.push("Verify the current PR head, review state, and check state in GitHub before making branch-mutating decisions.");
403
- }
404
- lines.push("");
405
- }
406
- function appendFollowUpPromptPrelude(lines, issue, runType, context) {
407
- lines.push("## Follow-up Turn", "");
408
- lines.push(`Why this turn exists: ${resolveFollowUpWhy(runType, context)}`);
409
- lines.push(`Required action now: ${resolveFollowUpAction(runType, context)}`);
410
- lines.push("");
411
- appendLinearContext(lines, context);
412
- const followUps = collectFollowUpInputs(context);
413
- if (followUps.length > 0) {
414
- lines.push("## What Changed Since The Last Turn", "");
415
- for (const followUp of followUps) {
416
- lines.push(`- ${followUp.type}${followUp.author ? ` from ${followUp.author}` : ""}: ${followUp.text}`);
417
- }
418
- lines.push("");
419
- }
420
- appendFactFreshness(lines, issue, runType, context);
421
- appendAuthoritativeGitHubFacts(lines, issue, runType, context);
422
- }
423
- function appendRequestedChangesInstructions(lines, runType, context) {
424
- if (resolveRequestedChangesMode(runType, context) === "branch_upkeep") {
425
- const baseBranch = typeof context?.baseBranch === "string" ? context.baseBranch : "main";
426
- lines.push("## Branch Upkeep After Requested Changes", "", "The requested code change may already be present, but the PR branch still needs upkeep before review can continue.", typeof context?.mergeStateStatus === "string" ? `Current merge state: ${String(context.mergeStateStatus)}` : "", "", "Steps:", `1. Update the existing PR branch onto latest ${baseBranch}.`, "2. Resolve conflicts or branch drift without reopening the review-feedback debate unless the merge introduces a new issue.", "3. Run the narrowest verification that proves the branch is healthy again.", "4. Commit and push a newer head on the existing PR branch.", "5. If you cannot produce a new pushed head, stop and surface the exact blocker.", "");
427
- return;
428
- }
429
- lines.push("## Review Changes Requested", "", "A reviewer has requested changes on your PR. Address the feedback and push.", context?.reviewerName ? `Reviewer: ${String(context.reviewerName)}` : "", context?.reviewBody ? `\n## Review comment\n\n${String(context.reviewBody)}` : "", "", "Steps:", "1. Start with the structured review context below. Treat the inline review comments as the primary repair checklist for this turn.", "2. Check the current diff (`git diff origin/main`) — a prior rebase may have already resolved some concerns (e.g., scope-bundling from stale commits).", "3. For each review point: if already resolved on the current head, note why. If not, fix it.", "4. If the structured review context looks incomplete, inspect the latest GitHub review threads directly before deciding you are done.", "5. Run verification, commit, and push a newer head on the existing PR branch.", "6. Do not try to hand the same head back to review. If you cannot produce a new pushed head, stop and surface the blocker clearly.", "7. GitHub review happens after the new head is pushed and CI is green. Do not use `gh pr edit --add-reviewer` as part of this workflow.", "");
430
- appendStructuredReviewContext(lines, context);
431
- }
432
- export function buildInitialRunPrompt(issue, runType, repoPath, context) {
433
- const lines = buildPromptHeader(issue);
434
- appendTaskObjective(lines, issue);
435
- appendScopeDiscipline(lines, issue);
436
- appendLinearContext(lines, context);
437
- // Add run-type-specific context for reactive runs
438
- switch (runType) {
439
- case "ci_repair": {
440
- const snapshot = context?.ciSnapshot && typeof context.ciSnapshot === "object"
441
- ? context.ciSnapshot
442
- : undefined;
443
- lines.push("## CI Repair", "", "A full CI iteration has settled failed on your PR. Start from the specific failing check/job/step below on the latest remote PR branch tip, fix that concrete failure first, then push to the same PR branch.", snapshot?.gateCheckName ? `Gate check: ${String(snapshot.gateCheckName)}` : "", snapshot?.gateCheckStatus ? `Gate status: ${String(snapshot.gateCheckStatus)}` : "", snapshot?.settledAt ? `Settled at: ${String(snapshot.settledAt)}` : "", context?.failureHeadSha ? `Failing head SHA: ${String(context.failureHeadSha)}` : "", context?.checkName ? `Failed check: ${String(context.checkName)}` : "", context?.jobName && context?.jobName !== context?.checkName ? `Failed job: ${String(context.jobName)}` : "", context?.stepName ? `Failed step: ${String(context.stepName)}` : "", context?.summary ? `Failure summary: ${String(context.summary)}` : "", Array.isArray(snapshot?.failedChecks) && snapshot.failedChecks.length > 0
444
- ? `Other failed checks in the settled snapshot (context only; ignore unless the logs show the same root cause):\n${snapshot.failedChecks.map((entry) => `- ${String(entry.name ?? "unknown")}${entry.summary ? `: ${String(entry.summary)}` : ""}`).join("\n")}`
445
- : "", context?.checkUrl ? `Check URL: ${String(context.checkUrl)}` : "", Array.isArray(context?.annotations) && context.annotations.length > 0
446
- ? `Annotations:\n${context.annotations.map((entry) => `- ${String(entry)}`).join("\n")}`
447
- : "", "", "Fetch the latest remote branch state first. If the branch moved since this failure, restart from the new tip instead of pushing older work.", "Read the latest logs for the named failing check, fix that root cause, and only broaden scope when the logs show direct fallout from the same issue.", "Do not change workflows, dependency installation, or unrelated tests unless the failing logs clearly point there.", "Run focused verification for the named failure, then commit and push.", "Do not open a new PR. Keep working on the existing branch until CI goes green or the situation is clearly stuck.", "Do not change test expectations unless the test is genuinely wrong.", "");
448
- break;
449
- }
450
- case "review_fix":
451
- case "branch_upkeep":
452
- appendRequestedChangesInstructions(lines, runType, context);
453
- break;
454
- case "queue_repair":
455
- appendQueueRepairContext(lines, context);
456
- lines.push("## Merge Queue Failure", "", "The merge queue rejected this PR. Rebase onto latest main and fix conflicts.", context?.failureReason ? `Failure reason: ${String(context.failureReason)}` : "", "", "Fetch and rebase onto latest main, resolve conflicts, run verification, push.", "If the conflict is a semantic contradiction, explain and stop.", "");
457
- break;
458
- }
459
- const workflowBody = readWorkflowFile(repoPath, runType);
460
- if (workflowBody) {
461
- lines.push(workflowBody);
462
- }
463
- else if (runType === "implementation") {
464
- lines.push("Implement the Linear issue. Read the issue via MCP for details.");
465
- }
466
- appendPublicationContract(lines, runType, issue, context);
467
- return lines.join("\n");
468
- }
469
- export function buildFollowUpRunPrompt(issue, runType, repoPath, context) {
470
- const lines = buildPromptHeader(issue);
471
- appendFollowUpPromptPrelude(lines, issue, runType, context);
472
- appendTaskObjective(lines, issue);
473
- appendScopeDiscipline(lines, issue);
474
- // Add run-type-specific context for reactive runs
475
- switch (runType) {
476
- case "ci_repair": {
477
- const snapshot = context?.ciSnapshot && typeof context.ciSnapshot === "object"
478
- ? context.ciSnapshot
479
- : undefined;
480
- lines.push("## CI Repair", "", "A full CI iteration has settled failed on your PR. Start from the specific failing check/job/step below on the latest remote PR branch tip, fix that concrete failure first, then push to the same PR branch.", snapshot?.gateCheckName ? `Gate check: ${String(snapshot.gateCheckName)}` : "", snapshot?.gateCheckStatus ? `Gate status: ${String(snapshot.gateCheckStatus)}` : "", snapshot?.settledAt ? `Settled at: ${String(snapshot.settledAt)}` : "", context?.failureHeadSha ? `Failing head SHA: ${String(context.failureHeadSha)}` : "", context?.checkName ? `Failed check: ${String(context.checkName)}` : "", context?.jobName && context?.jobName !== context?.checkName ? `Failed job: ${String(context.jobName)}` : "", context?.stepName ? `Failed step: ${String(context.stepName)}` : "", context?.summary ? `Failure summary: ${String(context.summary)}` : "", Array.isArray(snapshot?.failedChecks) && snapshot.failedChecks.length > 0
481
- ? `Other failed checks in the settled snapshot (context only; ignore unless the logs show the same root cause):\n${snapshot.failedChecks.map((entry) => `- ${String(entry.name ?? "unknown")}${entry.summary ? `: ${String(entry.summary)}` : ""}`).join("\n")}`
482
- : "", context?.checkUrl ? `Check URL: ${String(context.checkUrl)}` : "", Array.isArray(context?.annotations) && context.annotations.length > 0
483
- ? `Annotations:\n${context.annotations.map((entry) => `- ${String(entry)}`).join("\n")}`
484
- : "", "", "Fetch the latest remote branch state first. If the branch moved since this failure, restart from the new tip instead of pushing older work.", "Read the latest logs for the named failing check, fix that root cause, and only broaden scope when the logs show direct fallout from the same issue.", "Do not change workflows, dependency installation, or unrelated tests unless the failing logs clearly point there.", "Run focused verification for the named failure, then commit and push.", "Do not open a new PR. Keep working on the existing branch until CI goes green or the situation is clearly stuck.", "Do not change test expectations unless the test is genuinely wrong.", "");
485
- break;
486
- }
487
- case "review_fix":
488
- case "branch_upkeep":
489
- appendRequestedChangesInstructions(lines, runType, context);
490
- break;
491
- case "queue_repair":
492
- appendQueueRepairContext(lines, context);
493
- lines.push("## Merge Queue Failure", "", "The merge queue rejected this PR. Rebase onto latest main and fix conflicts.", context?.failureReason ? `Failure reason: ${String(context.failureReason)}` : "", "", "Fetch and rebase onto latest main, resolve conflicts, run verification, push.", "If the conflict is a semantic contradiction, explain and stop.", "");
494
- break;
495
- }
496
- const workflowBody = readWorkflowFile(repoPath, runType);
497
- if (workflowBody) {
498
- lines.push(workflowBody);
499
- }
500
- else if (runType === "implementation") {
501
- lines.push("Implement the Linear issue. Read the issue via MCP for details.");
502
- }
503
- appendPublicationContract(lines, runType, issue, context);
504
- return lines.join("\n");
505
- }
506
- function shouldBuildFollowUpPrompt(runType, context) {
507
- if (context?.followUpMode)
508
- return true;
509
- if (runType !== "implementation")
510
- return true;
511
- const wakeReason = typeof context?.wakeReason === "string" ? context.wakeReason : undefined;
512
- return Boolean(wakeReason && wakeReason !== "delegated");
513
- }
514
- export function buildRunPrompt(issue, runType, repoPath, context) {
515
- if (shouldBuildFollowUpPrompt(runType, context)) {
516
- return buildFollowUpRunPrompt(issue, runType, repoPath, context);
517
- }
518
- return buildInitialRunPrompt(issue, runType, repoPath, context);
519
- }
520
49
  function shouldCompactThread(issue, threadGeneration, context) {
521
50
  const followUpCount = typeof context?.followUpCount === "number" ? context.followUpCount : 0;
522
51
  return issue.threadId !== undefined
@@ -693,8 +222,26 @@ export class RunOrchestrator {
693
222
  return;
694
223
  }
695
224
  }
696
- // Build prompt
697
- const prompt = buildRunPrompt(issue, runType, project.repoPath, effectiveContext);
225
+ const repoPrompting = loadPatchRelayRepoPrompting({
226
+ repoRoot: project.repoPath,
227
+ logger: this.logger,
228
+ });
229
+ const promptLayer = mergePromptCustomizationLayers(resolvePromptLayers(this.config.prompting, runType), resolvePromptLayers(repoPrompting, runType));
230
+ const unknownPromptSections = findUnknownPatchRelayPromptSectionIds(promptLayer);
231
+ if (unknownPromptSections.length > 0) {
232
+ this.logger.warn({ issueKey: issue.issueKey, runType, unknownPromptSections }, "PatchRelay prompt customization references unknown section ids");
233
+ }
234
+ const disallowedPromptSections = findDisallowedPatchRelayPromptSectionIds(promptLayer);
235
+ if (disallowedPromptSections.length > 0) {
236
+ this.logger.warn({ issueKey: issue.issueKey, runType, disallowedPromptSections }, "PatchRelay prompt customization attempted to replace non-overridable sections");
237
+ }
238
+ const prompt = buildPatchRelayRunPrompt({
239
+ issue,
240
+ runType,
241
+ repoPath: project.repoPath,
242
+ ...(effectiveContext ? { context: effectiveContext } : {}),
243
+ ...(promptLayer ? { promptLayer } : {}),
244
+ });
698
245
  // Resolve workspace
699
246
  const issueRef = sanitizePathSegment(issue.issueKey ?? issue.linearIssueId);
700
247
  const slug = issue.title ? slugify(issue.title) : "";
@@ -2372,82 +1919,3 @@ function buildReviewFixBranchUpkeepContext(prNumber, baseBranch, pr, context) {
2372
1919
  baseBranch,
2373
1920
  };
2374
1921
  }
2375
- function appendQueueRepairContext(lines, context) {
2376
- const incidentTitle = typeof context?.incidentTitle === "string" ? context.incidentTitle.trim() : "";
2377
- const incidentSummary = typeof context?.incidentSummary === "string" ? context.incidentSummary.trim() : "";
2378
- const incidentId = typeof context?.incidentId === "string" ? context.incidentId.trim() : "";
2379
- const incidentUrl = typeof context?.incidentUrl === "string" ? context.incidentUrl.trim() : "";
2380
- const incidentContext = context?.incidentContext && typeof context.incidentContext === "object"
2381
- ? context.incidentContext
2382
- : undefined;
2383
- const failureClass = typeof incidentContext?.failureClass === "string" ? incidentContext.failureClass : "";
2384
- const baseSha = typeof incidentContext?.baseSha === "string" ? incidentContext.baseSha : "";
2385
- const prHeadSha = typeof incidentContext?.prHeadSha === "string" ? incidentContext.prHeadSha : "";
2386
- const baseBranch = typeof incidentContext?.baseBranch === "string" ? incidentContext.baseBranch : "";
2387
- const branch = typeof incidentContext?.branch === "string" ? incidentContext.branch : "";
2388
- const queuePosition = typeof incidentContext?.queuePosition === "number" ? String(incidentContext.queuePosition) : "";
2389
- const conflictFiles = Array.isArray(incidentContext?.conflictFiles)
2390
- ? incidentContext.conflictFiles.filter((entry) => typeof entry === "string")
2391
- : [];
2392
- const failedChecks = Array.isArray(incidentContext?.failedChecks)
2393
- ? incidentContext.failedChecks
2394
- .filter((entry) => Boolean(entry) && typeof entry === "object")
2395
- .map((entry) => ({
2396
- name: typeof entry.name === "string" ? entry.name : "unknown",
2397
- conclusion: typeof entry.conclusion === "string" ? entry.conclusion : "unknown",
2398
- ...(typeof entry.url === "string" ? { url: entry.url } : {}),
2399
- }))
2400
- : [];
2401
- const retryHistory = Array.isArray(incidentContext?.retryHistory)
2402
- ? incidentContext.retryHistory
2403
- .filter((entry) => Boolean(entry) && typeof entry === "object")
2404
- .map((entry) => ({
2405
- at: typeof entry.at === "string" ? entry.at : "unknown",
2406
- baseSha: typeof entry.baseSha === "string" ? entry.baseSha : "unknown",
2407
- outcome: typeof entry.outcome === "string" ? entry.outcome : "unknown",
2408
- }))
2409
- : [];
2410
- if (!incidentTitle && !incidentSummary && !incidentId && !incidentUrl && !failureClass && !baseSha && !prHeadSha
2411
- && !queuePosition && conflictFiles.length === 0 && failedChecks.length === 0 && retryHistory.length === 0) {
2412
- return;
2413
- }
2414
- lines.push("## Queue Incident Context", "");
2415
- if (incidentTitle)
2416
- lines.push(`Incident: ${incidentTitle}`);
2417
- if (incidentId)
2418
- lines.push(`Incident ID: ${incidentId}`);
2419
- if (incidentUrl)
2420
- lines.push(`Incident URL: ${incidentUrl}`);
2421
- if (incidentSummary)
2422
- lines.push("", incidentSummary, "");
2423
- if (failureClass)
2424
- lines.push(`Failure class: ${failureClass}`);
2425
- if (baseBranch)
2426
- lines.push(`Base branch: ${baseBranch}`);
2427
- if (baseSha)
2428
- lines.push(`Base SHA: ${baseSha}`);
2429
- if (branch)
2430
- lines.push(`Queue branch: ${branch}`);
2431
- if (prHeadSha)
2432
- lines.push(`Queue branch head SHA: ${prHeadSha}`);
2433
- if (queuePosition)
2434
- lines.push(`Queue position at eviction: ${queuePosition}`);
2435
- if (conflictFiles.length > 0) {
2436
- lines.push("", "Conflicting files:");
2437
- for (const file of conflictFiles)
2438
- lines.push(`- ${file}`);
2439
- }
2440
- if (failedChecks.length > 0) {
2441
- lines.push("", "Failed checks:");
2442
- for (const check of failedChecks) {
2443
- lines.push(`- ${check.name} (${check.conclusion})${check.url ? ` ${check.url}` : ""}`);
2444
- }
2445
- }
2446
- if (retryHistory.length > 0) {
2447
- lines.push("", "Retry history:");
2448
- for (const retry of retryHistory) {
2449
- lines.push(`- ${retry.at}: ${retry.outcome} on base ${retry.baseSha}`);
2450
- }
2451
- }
2452
- lines.push("");
2453
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.36.5",
3
+ "version": "0.36.7",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {