patchrelay 0.50.1 → 0.50.2

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/README.md CHANGED
@@ -61,12 +61,12 @@ patchrelay service status
61
61
  patchrelay dashboard
62
62
  ```
63
63
 
64
- Each repo needs two workflow files that act as agent prompts:
64
+ Each repo needs two workflow files for repo-specific run behavior:
65
65
 
66
66
  - `IMPLEMENTATION_WORKFLOW.md` — implementation, CI repair, queue repair runs
67
67
  - `REVIEW_WORKFLOW.md` — review fix runs
68
68
 
69
- Keep them short, action-oriented, human-authored. See [prompting.md](./docs/prompting.md) for how the built-in scaffold composes them with the rest of the prompt.
69
+ Keep them short, action-oriented, human-authored. Durable machine-level policy belongs in Codex `developer_instructions`; workflow files are for repo-local behavior and validation. See [prompting.md](./docs/prompting.md) for how the built-in scaffold composes them.
70
70
 
71
71
  Full install, ingress, and GitHub/Linear app setup: [self-hosting.md](./docs/self-hosting.md). Daily ops and CLI cheatsheet: [operator-guide.md](./docs/operator-guide.md).
72
72
 
@@ -6,7 +6,8 @@
6
6
  "git_bin": "git",
7
7
  "codex": {
8
8
  "model": "gpt-5.3-codex-spark",
9
- "reasoning_effort": "high"
9
+ "reasoning_effort": "high",
10
+ "developer_instructions": "Keep the public API stable for this installation unless the task explicitly requires a breaking change."
10
11
  }
11
12
  }
12
13
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.50.1",
4
- "commit": "27eb3a4a1840",
5
- "builtAt": "2026-04-20T12:44:08.573Z"
3
+ "version": "0.50.2",
4
+ "commit": "59ab92391401",
5
+ "builtAt": "2026-04-20T18:42:11.703Z"
6
6
  }
@@ -101,6 +101,12 @@ function buildCompletionCheckPrompt(params) {
101
101
  '- "done" if the task was successfully completed without a PR.',
102
102
  '- "failed" if the run stopped incorrectly and PatchRelay should not auto-continue.',
103
103
  "",
104
+ "Bias rules:",
105
+ '- Prefer "continue" when the task looks like normal code-delivery work and the run simply ended before publishing a branch or PR.',
106
+ '- Use "done" only when the issue explicitly permits a no-PR outcome or the deliverable is clearly non-repo work such as planning, orchestration, or follow-up issue creation.',
107
+ '- Use "needs_input" only when one specific missing human decision blocks the very next concrete action. Do not use it just because the branch might need more work.',
108
+ '- If the run appears unfinished but still actionable by PatchRelay, return "continue", not "done".',
109
+ "",
104
110
  "Facts:",
105
111
  `- Issue: ${params.issue.issueKey ?? params.issue.linearIssueId}`,
106
112
  ...(params.issue.title ? [`- Title: ${params.issue.title}`] : []),
package/dist/config.js CHANGED
@@ -9,6 +9,21 @@ import { ensureAbsolutePath } from "./utils.js";
9
9
  const LINEAR_OAUTH_CALLBACK_PATH = "/oauth/linear/callback";
10
10
  const REPO_SETTINGS_DIRNAME = ".patchrelay";
11
11
  const REPO_SETTINGS_FILENAME = "project.json";
12
+ const DEFAULT_PATCHRELAY_DEVELOPER_INSTRUCTIONS = [
13
+ "You are PatchRelay's coding agent.",
14
+ "",
15
+ "Core rules:",
16
+ "- Complete the delegated task in the current repository and worktree.",
17
+ "- Stay inside the issue's scope. Do not widen into unrelated cleanup or polish.",
18
+ "- Use repository docs and workflow files as the source of truth for local conventions.",
19
+ "- Prefer concrete fixes over speculative defenses.",
20
+ "- For code-delivery work, publish before stopping.",
21
+ "- If you change files for an implementation run, commit, push the issue branch, and open or update the PR.",
22
+ "- For repair runs, work on the existing PR branch and do not open a new PR.",
23
+ "- A requested-changes repair is only complete after a newer PR head is pushed, unless a genuine external blocker prevents correct publication.",
24
+ "- If a broader inconsistency is not required to make this task correct, mention it briefly instead of expanding scope.",
25
+ "- Before publishing, do one brief reviewer-minded pass on the current head and fix likely in-scope blockers.",
26
+ ].join("\n");
12
27
  const trustedActorsSchema = z
13
28
  .object({
14
29
  ids: z.array(z.string().min(1)).default([]),
@@ -238,6 +253,13 @@ function loadPromptLayer(configDir, layer) {
238
253
  replaceSections: Object.fromEntries(Object.entries(layer.replace_sections).map(([sectionId, fragmentPath]) => [sectionId, readPromptFile(configDir, fragmentPath)])),
239
254
  };
240
255
  }
256
+ function mergeDeveloperInstructions(custom) {
257
+ const normalized = custom?.trim();
258
+ if (!normalized) {
259
+ return DEFAULT_PATCHRELAY_DEVELOPER_INSTRUCTIONS;
260
+ }
261
+ return `${DEFAULT_PATCHRELAY_DEVELOPER_INSTRUCTIONS}\n\n## Local Developer Instructions\n\n${normalized}`;
262
+ }
241
263
  function expandEnv(value, env) {
242
264
  if (typeof value === "string") {
243
265
  return value.replace(/\$\{([A-Z0-9_]+)(?::-(.*?))?\}/g, (_match, name, fallback) => {
@@ -548,9 +570,7 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
548
570
  ...(parsed.runner.codex.reasoning_effort ? { reasoningEffort: parsed.runner.codex.reasoning_effort } : {}),
549
571
  ...(parsed.runner.codex.service_name ? { serviceName: parsed.runner.codex.service_name } : {}),
550
572
  ...(parsed.runner.codex.base_instructions ? { baseInstructions: parsed.runner.codex.base_instructions } : {}),
551
- ...(parsed.runner.codex.developer_instructions
552
- ? { developerInstructions: parsed.runner.codex.developer_instructions }
553
- : {}),
573
+ developerInstructions: mergeDeveloperInstructions(parsed.runner.codex.developer_instructions),
554
574
  approvalPolicy: parsed.runner.codex.approval_policy,
555
575
  sandboxMode: parsed.runner.codex.sandbox_mode,
556
576
  persistExtendedHistory: parsed.runner.codex.persist_extended_history,
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  const WORKFLOW_FILES = {
4
4
  implementation: "IMPLEMENTATION_WORKFLOW.md",
@@ -22,12 +22,10 @@ export const PATCHRELAY_REPLACEABLE_SECTION_IDS = [
22
22
  "workflow-guidance",
23
23
  "publication-contract",
24
24
  ];
25
- function readWorkflowFile(repoPath, runType) {
25
+ function hasWorkflowFile(repoPath, runType) {
26
26
  const filename = WORKFLOW_FILES[runType];
27
27
  const filePath = path.join(repoPath, filename);
28
- if (!existsSync(filePath))
29
- return undefined;
30
- return readFileSync(filePath, "utf8").trim();
28
+ return existsSync(filePath);
31
29
  }
32
30
  function buildPromptHeader(issue) {
33
31
  return [
@@ -102,7 +100,7 @@ function summarizeRelationEntries(entries, options) {
102
100
  }
103
101
  return lines;
104
102
  }
105
- function buildCoordinationGuidance(context) {
103
+ function buildIssueTopology(context) {
106
104
  const unresolvedBlockers = Array.isArray(context?.unresolvedBlockers)
107
105
  ? context.unresolvedBlockers.filter((entry) => Boolean(entry) && typeof entry === "object")
108
106
  : [];
@@ -111,18 +109,10 @@ function buildCoordinationGuidance(context) {
111
109
  : Array.isArray(context?.trackedDependents)
112
110
  ? context.trackedDependents.filter((entry) => Boolean(entry) && typeof entry === "object")
113
111
  : [];
114
- const lines = [
115
- "### Coordination / Issue Topology",
116
- "",
117
- "First decide whether this issue should publish code itself or mainly coordinate other issues.",
118
- "If this issue is a parent tracker, umbrella, migration program, or convergence container and the concrete implementation belongs in child issues, do not create a duplicate umbrella PR.",
119
- "When child issues already own the concrete code slices, use this issue to coordinate, create or refine follow-up issues, or verify convergence. Only ship code here if this issue still has unique implementation scope that is not already owned elsewhere.",
120
- "Prefer one PR per concrete implementation issue over a broad parent branch that restates overlapping child work.",
121
- ];
122
112
  if (unresolvedBlockers.length === 0 && childIssues.length === 0) {
123
- return lines;
113
+ return [];
124
114
  }
125
- lines.push("", "Known relations from PatchRelay:");
115
+ const lines = ["### Issue Topology", ""];
126
116
  if (unresolvedBlockers.length > 0) {
127
117
  lines.push("Unresolved blockers:");
128
118
  lines.push(...summarizeRelationEntries(unresolvedBlockers));
@@ -136,35 +126,25 @@ function buildCoordinationGuidance(context) {
136
126
  }
137
127
  return lines;
138
128
  }
139
- function buildScopeDiscipline(issue, context) {
129
+ function buildConstraints(issue, context) {
140
130
  const description = issue.description?.trim();
141
131
  const scope = extractIssueSection(description, "Scope");
142
132
  const acceptance = extractIssueSection(description, "Acceptance criteria")
143
133
  ?? extractIssueSection(description, "Success criteria");
144
134
  const relevantCode = extractIssueSection(description, "Relevant code");
135
+ const topology = buildIssueTopology(context);
145
136
  return [
146
- "## Scope Discipline",
137
+ "## Constraints",
147
138
  "",
148
- "Stay inside the delegated task.",
149
- "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.",
150
- "Only broaden to adjacent routes, copy, or supporting surfaces when the issue text or repository guidance explicitly says they are the same user flow.",
151
- "Your implementation goal is to leave the branch review-ready, not merely locally working: look for likely regressions or invariant breaks in the touched flow before you stop.",
152
- "If a narrow patch fixes the immediate symptom but leaves the same underlying risk elsewhere in the changed flow, keep going until the branch is likely to survive strict review or clearly explain the blocker.",
153
- "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.",
139
+ "Stay inside the delegated task. Do not widen scope into unrelated cleanup or optional polish.",
154
140
  "",
155
141
  ...(scope ? ["### In Scope", "", scope, ""] : []),
156
142
  ...(acceptance ? ["### Acceptance / Done", "", acceptance, ""] : []),
157
143
  ...(relevantCode ? ["### Relevant Code", "", relevantCode, ""] : []),
158
- ...buildCoordinationGuidance(context),
159
- "",
160
- "### Likely Review Invariants",
161
- "",
162
- "- Check the surfaces explicitly named in the task before stopping.",
163
- "- 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.",
164
- "- A review repair should fix the concrete concern on the current head, not silently expand the Linear issue into a broader rewrite.",
144
+ ...topology,
165
145
  ].join("\n");
166
146
  }
167
- function buildOrchestrationScopeDiscipline(context) {
147
+ function buildOrchestrationConstraints(context) {
168
148
  const unresolvedBlockers = Array.isArray(context?.unresolvedBlockers)
169
149
  ? context.unresolvedBlockers.filter((entry) => Boolean(entry) && typeof entry === "object")
170
150
  : [];
@@ -174,23 +154,11 @@ function buildOrchestrationScopeDiscipline(context) {
174
154
  ? context.trackedDependents.filter((entry) => Boolean(entry) && typeof entry === "object")
175
155
  : [];
176
156
  return [
177
- "## Scope Discipline",
157
+ "## Constraints",
178
158
  "",
179
- "This issue is orchestration work.",
180
- "Treat it as the owner of convergence across related issues rather than as a normal code-owning implementation branch.",
181
- "Inspect why this wake happened before acting.",
182
- "Adopt already-existing canonical child issues when they cover the intended split.",
183
- "Do not recreate child issues that already exist under this parent unless a genuinely missing required slice remains.",
184
- "Do not create an overlapping umbrella PR unless this parent clearly owns unique direct cleanup work that child issues do not already cover.",
185
- "When you split required implementation work out of the parent, make it a child issue of this umbrella rather than making the parent block the child.",
186
- "If sequencing matters, let the parent wait on the child; do not model the child as blocked by the parent umbrella it is supposed to satisfy.",
187
- "If child work is still in motion, babysit the plan, record useful observations, and return to waiting.",
188
- "If child work looks delivered, audit whether the original parent goal is actually satisfied.",
189
- "Create blocking follow-up work only when it is necessary to satisfy the original parent goal.",
190
- "Prefer non-blocking follow-up issues over keeping the umbrella open for optional polish or adjacent expansion.",
191
- "New child issues should stay in Backlog and undelegated by default.",
192
- "Only delegate or move a new child to Start when it is immediately actionable, unblocked, and you intend for PatchRelay to begin it right away.",
193
- "If you create multiple new child issues, keep later-wave or dependency-bound children queued rather than waking them all at once.",
159
+ "This issue is orchestration work. Coordinate convergence instead of duplicating child implementation.",
160
+ "Do not open an overlapping umbrella PR unless this parent owns unique direct work.",
161
+ "Leave later-wave child issues queued unless they are immediately actionable.",
194
162
  "",
195
163
  "### Child Issue Summaries",
196
164
  "",
@@ -204,29 +172,28 @@ function buildOrchestrationScopeDiscipline(context) {
204
172
  "### Convergence Rule",
205
173
  "",
206
174
  "- Close the umbrella when the original parent goal is satisfied.",
207
- "- If you discover one missing required slice, you may create a justified blocking follow-up.",
208
- "- Do not invent optional expansion without explicit human approval.",
175
+ "- Create blocking follow-up work only when it is required to satisfy that goal.",
209
176
  ].join("\n");
210
177
  }
211
- function buildHumanContext(context) {
178
+ function buildHumanContextLines(context) {
212
179
  const promptContext = typeof context?.promptContext === "string" ? context.promptContext.trim() : "";
213
180
  const latestPrompt = typeof context?.promptBody === "string" ? context.promptBody.trim() : "";
214
181
  const operatorPrompt = typeof context?.operatorPrompt === "string" ? context.operatorPrompt.trim() : "";
215
182
  const userComment = typeof context?.userComment === "string" ? context.userComment.trim() : "";
216
183
  const lines = [];
217
184
  if (promptContext) {
218
- lines.push("## Linear Session Context", "", promptContext, "");
185
+ lines.push("Linear session context:", promptContext, "");
219
186
  }
220
187
  if (latestPrompt) {
221
- lines.push("## Latest Human Instruction", "", latestPrompt, "");
188
+ lines.push("Latest human instruction:", latestPrompt, "");
222
189
  }
223
190
  if (operatorPrompt) {
224
- lines.push("## Operator Prompt", "", operatorPrompt, "");
191
+ lines.push("Operator prompt:", operatorPrompt, "");
225
192
  }
226
193
  if (userComment) {
227
- lines.push("## Human Follow-up Comment", "", userComment, "");
194
+ lines.push("Human follow-up comment:", userComment, "");
228
195
  }
229
- return lines.length > 0 ? lines.join("\n").trim() : undefined;
196
+ return lines;
230
197
  }
231
198
  function resolveRequestedChangesMode(runType, context) {
232
199
  if (runType === "branch_upkeep") {
@@ -303,12 +270,12 @@ function buildRequestedChangesContext(runType, context) {
303
270
  const mode = resolveRequestedChangesMode(runType, context);
304
271
  const lines = [];
305
272
  if (mode === "branch_upkeep") {
306
- lines.push("## Branch Upkeep After Requested Changes", "", "Goal: restore merge readiness on the existing PR branch without regressing review or CI readiness.", "The requested review changes may already be addressed, but GitHub still shows the PR branch as behind or dirty against the base branch.", "Update the existing PR branch onto the latest base branch, resolve conflicts carefully, rerun the narrowest relevant verification, and push a newer head.", "Do not open a new PR.", "", "1. Refresh the latest remote branch and base branch state first.", "2. Rebase or merge onto the latest base branch and resolve conflicts in a way that preserves the branch's current intent and prior fixes.", "3. Audit the conflicted areas for semantic regressions, not just textual conflicts.", "4. Run focused verification for the touched areas and enough surrounding checks to regain confidence that the branch is still review-ready.", "5. Commit and push a newer head on the existing PR branch.", "6. Do not stop at 'conflicts resolved' if the resulting branch is no longer likely to pass review or CI.", "");
273
+ lines.push("Branch upkeep is required on the existing PR branch.", "Goal: restore merge readiness on the current branch and push a newer head without regressing review or CI readiness.");
307
274
  }
308
275
  else {
309
276
  const reviewer = typeof context?.reviewerName === "string" ? context.reviewerName : undefined;
310
277
  const reviewBody = typeof context?.reviewBody === "string" ? context.reviewBody.trim() : "";
311
- lines.push("## Review Changes Requested", "", "Goal: restore review readiness on the existing PR branch, not merely patch the latest cited line.", "Treat the reviewer comments as evidence of what still makes the branch unready. Your job is to return the branch to a state that is likely to pass the next full review.", "", reviewer ? `Reviewer: ${reviewer}` : "", reviewBody ? `Review summary:\n${reviewBody}` : "", "", "1. Start with the structured review context below, then inspect the PR's inline review comments with `gh api repos/<owner>/<repo>/pulls/<pr>/comments?per_page=100` so you begin from the full code-review context, not just the latest triggering comment.", "2. Inspect the current diff with `review-quill diff` when available so you see the reviewer-oriented diff view from this checkout; use `git diff origin/main` as a fallback, then inspect the current code before deciding what still needs work.", "3. Infer the underlying concern or invariant behind the review feedback. Do not assume each comment is an isolated chore.", "4. For each review point: if already resolved on the current head, note why. If not, fix it. Then inspect adjacent code paths and flows that could fail for the same reason.", "5. Verify the branch as a whole for the relevant concern class: current review issue, nearby regressions, relevant tests, and compatibility with the latest base branch.", "6. Only finish when you believe the branch is review-ready again. If you cannot get it there, stop and surface the blocker clearly.", "7. Commit and push a newer head on the existing PR branch. Do not try to hand the same head back to review.", "8. 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.", "");
278
+ lines.push("Requested changes on the existing PR branch.", "Goal: restore review readiness and push a newer head on the current PR branch.", "Address the real concern behind the feedback and verify nearby invariants in the touched flow before you publish.", reviewer ? `Reviewer: ${reviewer}` : "", reviewBody ? `Review summary:\n${reviewBody}` : "");
312
279
  appendStructuredReviewContext(lines, context);
313
280
  }
314
281
  return lines.join("\n").trim();
@@ -318,10 +285,8 @@ function buildCiRepairContext(context) {
318
285
  ? context.ciSnapshot
319
286
  : undefined;
320
287
  return [
321
- "## CI Repair",
322
- "",
323
- "Goal: restore CI readiness on the existing PR branch so the next full CI run is likely to pass.",
324
- "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, but do not stop at a narrow patch if the same root cause is likely to fail other checks in the suite.",
288
+ "Settled CI failure on the existing PR branch.",
289
+ "Goal: restore CI readiness and push a branch that is likely to pass the next full CI run.",
325
290
  snapshot?.gateCheckName ? `Gate check: ${String(snapshot.gateCheckName)}` : "",
326
291
  snapshot?.gateCheckStatus ? `Gate status: ${String(snapshot.gateCheckStatus)}` : "",
327
292
  snapshot?.settledAt ? `Settled at: ${String(snapshot.settledAt)}` : "",
@@ -337,14 +302,6 @@ function buildCiRepairContext(context) {
337
302
  Array.isArray(context?.annotations) && context.annotations.length > 0
338
303
  ? `Annotations:\n${context.annotations.map((entry) => `- ${String(entry)}`).join("\n")}`
339
304
  : "",
340
- "",
341
- "Fetch the latest remote branch state first. If the branch moved since this failure, restart from the new tip instead of pushing older work.",
342
- "Read the latest logs for the named failing check, identify the root cause, and check whether that same cause is likely to affect other jobs or checks.",
343
- "Fix the root cause, not just the first visible symptom.",
344
- "Do not change workflows, dependency installation, or unrelated tests unless the failing logs clearly point there.",
345
- "Run the narrowest local verification that gives real confidence for the suite, then commit and push.",
346
- "Do not open a new PR. Keep working on the existing branch until the branch is likely to pass CI again or the situation is clearly stuck.",
347
- "Do not change test expectations unless the test is genuinely wrong.",
348
305
  ].filter(Boolean).join("\n");
349
306
  }
350
307
  function appendQueueRepairContext(lines, context) {
@@ -388,67 +345,57 @@ function appendQueueRepairContext(lines, context) {
388
345
  function buildQueueRepairContext(context) {
389
346
  const lines = [];
390
347
  appendQueueRepairContext(lines, context);
391
- 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.");
348
+ lines.push("Merge queue rejection on the existing PR branch.", "Goal: restore a mergeable branch, verify the queue-blocking fix, and push the existing PR branch.", context?.failureReason ? `Failure reason: ${String(context.failureReason)}` : "");
392
349
  return lines.filter(Boolean).join("\n");
393
350
  }
394
- function buildFollowUpPromptPrelude(issue, runType, context) {
351
+ function buildFollowUpContextLines(issue, runType, context) {
395
352
  const wakeReason = typeof context?.wakeReason === "string" ? context.wakeReason : undefined;
396
353
  const followUps = Array.isArray(context?.followUps) ? context.followUps : [];
397
354
  const followUpLines = followUps
398
355
  .filter((entry) => Boolean(entry) && typeof entry === "object")
399
356
  .map((entry) => `${String(entry.type ?? "follow_up")} from ${String(entry.author ?? "unknown")}: ${String(entry.text ?? "").trim()}`.trim())
400
357
  .filter((line) => !line.endsWith(":"));
401
- const lines = [
402
- "## Follow-up Turn",
403
- "",
404
- wakeReason === "direct_reply"
405
- ? "Why this turn exists: A human reply arrived for the outstanding question from the previous turn."
406
- : wakeReason === "initial_delegate"
407
- ? "Why this turn exists: This orchestration issue was just delegated and needs an initial plan."
408
- : wakeReason === "child_delivered"
409
- ? "Why this turn exists: A child issue was delivered and the umbrella needs to review the outcome."
410
- : wakeReason === "child_changed"
411
- ? "Why this turn exists: A child issue changed state and the umbrella may need to adjust."
412
- : wakeReason === "child_regressed"
413
- ? "Why this turn exists: A previously progressing child issue regressed and the umbrella needs to reassess."
414
- : wakeReason === "human_instruction"
415
- ? "Why this turn exists: A human added new guidance for this orchestration issue."
416
- : wakeReason === "completion_check_continue"
417
- ? "Why this turn exists: The previous turn ended without a PR, and PatchRelay's completion check decided the work should continue automatically."
418
- : wakeReason === "branch_upkeep"
419
- ? "Why this turn exists: GitHub still shows the PR branch as needing upkeep after the requested code change was addressed."
420
- : wakeReason === "followup_comment"
421
- ? "Why this turn exists: A human follow-up comment arrived after the previous turn."
422
- : `Why this turn exists: Continue the existing ${runType} run from the latest issue state.`,
423
- wakeReason === "direct_reply"
424
- ? "Required action now: Apply the latest human answer, continue from the current branch/session context, and publish the next concrete result."
425
- : wakeReason === "initial_delegate"
426
- ? "Required action now: Inspect the umbrella goal, review the child set, and record the next orchestration step."
427
- : wakeReason === "completion_check_continue"
428
- ? "Required action now: Continue from the current branch and thread context, finish the task, and publish the next concrete result."
429
- : "Required action now: Continue from the latest branch state, refresh any stale assumptions, and publish the next concrete result.",
430
- "",
431
- ];
358
+ const lines = [];
359
+ const turnReason = wakeReason === "direct_reply"
360
+ ? "Human reply to the previous question."
361
+ : wakeReason === "initial_delegate"
362
+ ? "Initial orchestration turn after delegation."
363
+ : wakeReason === "child_delivered"
364
+ ? "A child issue was delivered."
365
+ : wakeReason === "child_changed"
366
+ ? "A child issue changed state."
367
+ : wakeReason === "child_regressed"
368
+ ? "A child issue regressed."
369
+ : wakeReason === "human_instruction"
370
+ ? "A human added new orchestration guidance."
371
+ : wakeReason === "completion_check_continue"
372
+ ? "The previous turn ended without a PR and PatchRelay chose to continue automatically."
373
+ : wakeReason === "branch_upkeep"
374
+ ? "GitHub still shows the PR branch as needing upkeep."
375
+ : wakeReason === "followup_comment"
376
+ ? "A human follow-up comment arrived after the previous turn."
377
+ : `Continue the existing ${runType} run from the latest issue state.`;
378
+ lines.push(`Turn reason: ${turnReason}`);
432
379
  if (wakeReason === "completion_check_continue" && typeof context?.completionCheckSummary === "string" && context.completionCheckSummary.trim()) {
433
- lines.push(`Completion check summary: ${context.completionCheckSummary.trim()}`, "");
380
+ lines.push(`Completion check summary: ${context.completionCheckSummary.trim()}`);
434
381
  }
435
382
  if (followUpLines.length > 0) {
436
- lines.push("Recent updates:");
383
+ lines.push("", "Recent updates:");
437
384
  followUpLines.forEach((line) => lines.push(`- ${line}`));
438
- lines.push("");
439
385
  }
440
386
  if (issue.prNumber || issue.prHeadSha || issue.prReviewState || context?.mergeStateStatus) {
441
- lines.push("## Current PR Facts", "", `Fact freshness: ${context?.githubFactsFresh === true
387
+ lines.push("", "Current PR facts:", `Fact freshness: ${context?.githubFactsFresh === true
442
388
  ? "refreshed immediately before this turn was created."
443
389
  : "may now be stale; refresh before making irreversible decisions."}`, issue.prNumber ? `Current PR: #${issue.prNumber}` : "", issue.prHeadSha ? `Current relevant head SHA: ${issue.prHeadSha}` : "", issue.prReviewState ? `Current review state: ${issue.prReviewState}` : "", typeof context?.mergeStateStatus === "string" ? `Merge state against ${String(context?.baseBranch ?? "main")}: ${String(context.mergeStateStatus)}` : "");
444
390
  }
445
- return lines.filter(Boolean).join("\n");
391
+ return lines.filter(Boolean);
446
392
  }
447
- function buildReactiveContext(runType, issue, context, followUp = false) {
393
+ function buildCurrentContext(runType, issue, context, followUp = false) {
448
394
  const lines = [];
449
395
  if (followUp) {
450
- lines.push(buildFollowUpPromptPrelude(issue, runType, context), "");
396
+ lines.push(...buildFollowUpContextLines(issue, runType, context), "");
451
397
  }
398
+ lines.push(...buildHumanContextLines(context));
452
399
  switch (runType) {
453
400
  case "ci_repair":
454
401
  lines.push(buildCiRepairContext(context));
@@ -464,74 +411,72 @@ function buildReactiveContext(runType, issue, context, followUp = false) {
464
411
  break;
465
412
  }
466
413
  const content = lines.map((line) => line.trimEnd()).join("\n").trim();
467
- return content.length > 0 ? content : undefined;
414
+ if (!content.length)
415
+ return undefined;
416
+ return ["## Current Context", "", content].join("\n");
468
417
  }
469
418
  function buildWorkflowGuidance(repoPath, runType) {
470
- const workflowBody = readWorkflowFile(repoPath, runType);
471
- if (workflowBody)
472
- return workflowBody;
473
- if (runType === "implementation") {
474
- return "Implement the Linear issue. Read the issue via MCP for details.";
419
+ const filename = WORKFLOW_FILES[runType];
420
+ if (hasWorkflowFile(repoPath, runType)) {
421
+ return [
422
+ "## Workflow",
423
+ "",
424
+ `Read and follow \`${filename}\` in the repository for task-specific behavior before making irreversible changes.`,
425
+ ].join("\n");
475
426
  }
476
- return "";
427
+ return [
428
+ "## Workflow",
429
+ "",
430
+ "Use repository docs and local guidance as the source of truth for task-specific behavior.",
431
+ ].join("\n");
477
432
  }
478
433
  function buildOrchestrationWorkflowGuidance() {
479
434
  return [
480
- "## Workflow Guidance",
435
+ "## Workflow",
481
436
  "",
482
- "Use the wake reason and current child issue summaries to decide what kind of orchestration work is needed now.",
483
- "Typical orchestration phases are: initial setup, waiting on child progress, reviewing delivered child work, final audit, creating a justified follow-up, or closing the umbrella.",
484
- "When creating follow-up work, prefer one immediately runnable child at a time and keep the rest queued until their prerequisites are genuinely ready.",
437
+ "Use the wake reason and child issue summaries to decide the next orchestration step.",
485
438
  "Keep outputs concise and observable in Linear.",
486
439
  ].join("\n");
487
440
  }
441
+ function buildPrePushSelfReviewSection(target) {
442
+ const publishTarget = target === "new_pr"
443
+ ? "open or update the PR"
444
+ : "push the existing PR branch";
445
+ return [
446
+ "## Final Self-Review Before Push",
447
+ "",
448
+ `Before you ${publishTarget}, do one brief reviewer-minded pass on the current head.`,
449
+ "Fix any likely in-scope blocker you can see now: missing edge-case handling, broken adjacent invariant in the touched flow, mismatch between the PR explanation and the code, or an obviously unreviewable half-finished branch.",
450
+ "Do not widen scope for optional cleanup. If the issue explicitly allows a non-PR outcome, complete that outcome clearly; otherwise publish before stopping.",
451
+ ];
452
+ }
488
453
  function buildPublicationContract(runType, issueClass) {
489
454
  if (issueClass === "orchestration") {
490
455
  return [
491
- "## Publication Requirements",
456
+ "## Publish",
492
457
  "",
493
- "Before finishing, publish the orchestration outcome rather than leaving it implicit.",
494
- "By default, orchestration work should finish without opening an overlapping umbrella PR.",
495
- "Valid orchestration outcomes include: recording an observation, updating the rollout plan, creating follow-up issues, opening a small cleanup PR that the parent clearly owns, or closing the umbrella.",
496
- "If you create new blocking follow-up work, justify it against the original parent goal rather than optional polish.",
497
- "Blocking follow-up children must block the parent goal they satisfy, not the other way around.",
498
- "If you create new child issues, leave them in Backlog unless they are truly ready to start now.",
458
+ "Publish the orchestration outcome clearly: observation, follow-up issues, rollout update, closeout, or a small parent-owned cleanup PR.",
459
+ "Do not open an overlapping umbrella PR unless this parent owns unique direct work.",
499
460
  ].join("\n");
500
461
  }
501
462
  if (runType === "implementation") {
502
463
  return [
503
- "## Publication Requirements",
464
+ "## Publish",
504
465
  "",
505
- "Before finishing, publish the result instead of leaving it only in the worktree.",
506
- "If the task is genuinely complete without a PR, say so clearly in your normal summary instead of inventing one.",
507
- "If the worktree already contains relevant changes for this issue, verify them and publish them.",
508
- "If you changed files for this issue, commit them, push the issue branch, and open or update the PR before stopping.",
509
- "Do not stop with only local commits or uncommitted changes.",
466
+ "If this is code-delivery work, publish before stopping: commit, push the issue branch, and open or update the PR.",
467
+ "If the issue explicitly allows a non-PR outcome, complete that outcome clearly instead of inventing a PR.",
510
468
  "",
511
- "## PR Body Contract",
512
- "",
513
- "When you open or update a PR, shape the body so a strict reviewer can decide in one pass.",
514
- "",
515
- "Title: imperative, ≤72 chars. Do not prefix with the issue key — the branch carries it.",
516
- "",
517
- "Body sections, in this order. Omit any that do not apply but keep the order:",
518
- "",
519
- " ## Why — 1-3 sentences on the problem and motivation.",
520
- " ## What — ≤5 bullets naming the files or surfaces that change.",
521
- " ## Tradeoffs — one explicit tradeoff taken, or the single word \"None\".",
522
- " ## Risks — 1-3 things a strict reviewer would ask about. For each, either fix it before committing or explain why it is acceptable. This section is load-bearing; a strict reviewer reads it first.",
523
- "",
524
- "Do not restate the diff in prose. Quote the ambiguous fragment directly if the reader needs to see it.",
525
- "Do not add a \"Verification\" or \"I ran these commands\" section; CI owns pass/fail and posts check runs the reviewer already sees.",
469
+ ...buildPrePushSelfReviewSection("new_pr"),
526
470
  ].join("\n");
527
471
  }
528
472
  return [
529
- "## Publication Requirements",
473
+ "## Publish",
530
474
  "",
531
- "Before finishing, publish the result to the existing PR branch.",
532
- "If you changed files for this repair, commit them and push the same branch before stopping.",
475
+ "Restore and publish on the existing PR branch: commit and push the same branch.",
533
476
  "Do not open a new PR.",
534
- "Do not stop with only local commits or uncommitted changes.",
477
+ "A PR-less stop is not a successful outcome for a repair run unless a genuine external blocker prevents any correct push.",
478
+ "",
479
+ ...buildPrePushSelfReviewSection("existing_pr"),
535
480
  ].join("\n");
536
481
  }
537
482
  function buildSections(issue, runType, repoPath, context, followUp = false) {
@@ -539,20 +484,13 @@ function buildSections(issue, runType, repoPath, context, followUp = false) {
539
484
  const sections = [
540
485
  { id: "header", content: buildPromptHeader(issue) },
541
486
  ];
542
- const reactiveContext = buildReactiveContext(runType, issue, context, followUp);
543
- if (followUp && reactiveContext) {
544
- sections.push({ id: "follow-up-turn", content: reactiveContext });
545
- }
487
+ const currentContext = buildCurrentContext(runType, issue, context, followUp);
546
488
  sections.push({ id: "task-objective", content: buildTaskObjective(issue) }, {
547
489
  id: "scope-discipline",
548
- content: issueClass === "orchestration" ? buildOrchestrationScopeDiscipline(context) : buildScopeDiscipline(issue, context),
490
+ content: issueClass === "orchestration" ? buildOrchestrationConstraints(context) : buildConstraints(issue, context),
549
491
  });
550
- const humanContext = buildHumanContext(context);
551
- if (humanContext) {
552
- sections.push({ id: "human-context", content: humanContext });
553
- }
554
- if (!followUp && reactiveContext) {
555
- sections.push({ id: "reactive-context", content: reactiveContext });
492
+ if (currentContext) {
493
+ sections.push({ id: "reactive-context", content: currentContext });
556
494
  }
557
495
  const workflow = issueClass === "orchestration"
558
496
  ? buildOrchestrationWorkflowGuidance()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.50.1",
3
+ "version": "0.50.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {