patchrelay 0.47.1 → 0.47.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
@@ -83,14 +83,22 @@ You will also need:
83
83
 
84
84
  1. A human delegates PatchRelay on an issue to start automation.
85
85
  2. PatchRelay verifies the webhook, routes the issue to the right local project, and packages the issue context for the first loop.
86
- 3. Delegated issues create or reuse the issue worktree and launch an implementation run through `codex app-server`.
86
+ 3. Delegated issues create or reuse the issue worktree and launch the appropriate first run through `codex app-server`.
87
87
  4. PatchRelay persists thread ids, run state, and observations so the work stays inspectable and resumable.
88
88
  5. GitHub webhooks drive reactive verification and repair loops: CI repair on check failures and review fix on changes requested.
89
- 6. PatchRelay opens draft PRs while implementation is in progress and marks its own PR ready when implementation is complete.
89
+ 6. Implementation issues usually open draft PRs while work is in progress and mark PatchRelay-owned PRs ready when implementation is complete.
90
90
  7. Downstream automation reacts to GitHub truth: `reviewbot` reviews ready PRs with green CI, and Merge Steward admits ready PRs with green CI and approval into the merge queue.
91
91
  8. If requested changes, red CI, or a merge-steward incident lands on a linked delegated PR, PatchRelay resumes work on that same PR branch.
92
92
  9. Native agent prompts and Linear comments can steer the active run. An operator can take over from the exact same worktree when needed.
93
93
 
94
+ Not every delegated issue should produce its own PR. Some delegated issues are coordination-only:
95
+
96
+ - parent trackers that spawn or coordinate child implementation issues
97
+ - audit or convergence issues that should wait for child issues before doing a narrow final pass
98
+ - planning/specification issues that are complete once the right follow-up issues or decisions exist
99
+
100
+ In those cases, PatchRelay should avoid opening an overlapping umbrella PR and should finish through coordination, follow-up issue creation, or a no-PR completion path instead.
101
+
94
102
  ### Undelegation And Re-delegation
95
103
 
96
104
  Undelegation pauses PatchRelay authority. It does not erase PR truth.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.47.1",
4
- "commit": "919d86a4ee45",
5
- "builtAt": "2026-04-18T12:45:08.672Z"
3
+ "version": "0.47.2",
4
+ "commit": "8e7084588da7",
5
+ "builtAt": "2026-04-18T12:47:45.623Z"
6
6
  }
@@ -65,7 +65,76 @@ function buildTaskObjective(issue) {
65
65
  ...(intro ? ["", intro] : []),
66
66
  ].join("\n");
67
67
  }
68
- function buildScopeDiscipline(issue) {
68
+ function summarizeRelationEntries(entries, options) {
69
+ if (entries.length === 0) {
70
+ return options?.emptyText ? [options.emptyText] : [];
71
+ }
72
+ const maxItems = options?.maxItems ?? 5;
73
+ const lines = entries.slice(0, maxItems).map((entry) => {
74
+ const issueRef = typeof entry.issueKey === "string" && entry.issueKey.trim()
75
+ ? entry.issueKey.trim()
76
+ : typeof entry.linearIssueId === "string" && entry.linearIssueId.trim()
77
+ ? entry.linearIssueId.trim()
78
+ : "unknown issue";
79
+ const title = typeof entry.title === "string" && entry.title.trim() ? entry.title.trim() : undefined;
80
+ const stateName = typeof entry.stateName === "string" && entry.stateName.trim()
81
+ ? entry.stateName.trim()
82
+ : typeof entry.currentLinearState === "string" && entry.currentLinearState.trim()
83
+ ? entry.currentLinearState.trim()
84
+ : undefined;
85
+ const factoryState = typeof entry.factoryState === "string" && entry.factoryState.trim() ? entry.factoryState.trim() : undefined;
86
+ const delegated = typeof entry.delegatedToPatchRelay === "boolean"
87
+ ? (entry.delegatedToPatchRelay ? "delegated" : "not delegated")
88
+ : undefined;
89
+ const openPr = typeof entry.hasOpenPr === "boolean"
90
+ ? (entry.hasOpenPr ? "open PR" : "no open PR")
91
+ : undefined;
92
+ return [
93
+ `- ${issueRef}`,
94
+ title ? `: ${title}` : "",
95
+ [stateName, factoryState, delegated, openPr].filter(Boolean).length > 0
96
+ ? ` (${[stateName, factoryState, delegated, openPr].filter(Boolean).join("; ")})`
97
+ : "",
98
+ ].join("");
99
+ });
100
+ if (entries.length > maxItems) {
101
+ lines.push(`- ...and ${entries.length - maxItems} more`);
102
+ }
103
+ return lines;
104
+ }
105
+ function buildCoordinationGuidance(context) {
106
+ const unresolvedBlockers = Array.isArray(context?.unresolvedBlockers)
107
+ ? context.unresolvedBlockers.filter((entry) => Boolean(entry) && typeof entry === "object")
108
+ : [];
109
+ const trackedDependents = Array.isArray(context?.trackedDependents)
110
+ ? context.trackedDependents.filter((entry) => Boolean(entry) && typeof entry === "object")
111
+ : [];
112
+ const lines = [
113
+ "### Coordination / Issue Topology",
114
+ "",
115
+ "First decide whether this issue should publish code itself or mainly coordinate other issues.",
116
+ "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.",
117
+ "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.",
118
+ "Prefer one PR per concrete implementation issue over a broad parent branch that restates overlapping child work.",
119
+ ];
120
+ if (unresolvedBlockers.length === 0 && trackedDependents.length === 0) {
121
+ return lines;
122
+ }
123
+ lines.push("", "Known relations from PatchRelay:");
124
+ if (unresolvedBlockers.length > 0) {
125
+ lines.push("Unresolved blockers:");
126
+ lines.push(...summarizeRelationEntries(unresolvedBlockers));
127
+ }
128
+ if (trackedDependents.length > 0) {
129
+ if (unresolvedBlockers.length > 0) {
130
+ lines.push("");
131
+ }
132
+ lines.push("Tracked dependent issues:");
133
+ lines.push(...summarizeRelationEntries(trackedDependents));
134
+ }
135
+ return lines;
136
+ }
137
+ function buildScopeDiscipline(issue, context) {
69
138
  const description = issue.description?.trim();
70
139
  const scope = extractIssueSection(description, "Scope");
71
140
  const acceptance = extractIssueSection(description, "Acceptance criteria")
@@ -84,6 +153,8 @@ function buildScopeDiscipline(issue) {
84
153
  ...(scope ? ["### In Scope", "", scope, ""] : []),
85
154
  ...(acceptance ? ["### Acceptance / Done", "", acceptance, ""] : []),
86
155
  ...(relevantCode ? ["### Relevant Code", "", relevantCode, ""] : []),
156
+ ...buildCoordinationGuidance(context),
157
+ "",
87
158
  "### Likely Review Invariants",
88
159
  "",
89
160
  "- Check the surfaces explicitly named in the task before stopping.",
@@ -353,6 +424,7 @@ function buildPublicationContract(runType) {
353
424
  "",
354
425
  "Before finishing, publish the result instead of leaving it only in the worktree.",
355
426
  "If the task is genuinely complete without a PR, say so clearly in your normal summary instead of inventing one.",
427
+ "If the issue is acting as coordination-only work and the real implementation belongs in child issues, finish without opening an overlapping umbrella PR.",
356
428
  "If the worktree already contains relevant changes for this issue, verify them and publish them.",
357
429
  "If you changed files for this issue, commit them, push the issue branch, and open or update the PR before stopping.",
358
430
  "Do not stop with only local commits or uncommitted changes.",
@@ -391,7 +463,7 @@ function buildSections(issue, runType, repoPath, context, followUp = false) {
391
463
  if (followUp && reactiveContext) {
392
464
  sections.push({ id: "follow-up-turn", content: reactiveContext });
393
465
  }
394
- sections.push({ id: "task-objective", content: buildTaskObjective(issue) }, { id: "scope-discipline", content: buildScopeDiscipline(issue) });
466
+ sections.push({ id: "task-objective", content: buildTaskObjective(issue) }, { id: "scope-discipline", content: buildScopeDiscipline(issue, context) });
395
467
  const humanContext = buildHumanContext(context);
396
468
  if (humanContext) {
397
469
  sections.push({ id: "human-context", content: humanContext });
@@ -30,6 +30,9 @@ function shouldDelayZombieRecoveryLaunch(issue, issueSession, runType) {
30
30
  return 0;
31
31
  return getRemainingZombieRecoveryDelayMs(issue.lastZombieRecoveryAt, issue.zombieRecoveryAttempts);
32
32
  }
33
+ function isResolvedDependencyState(stateType) {
34
+ return stateType === "completed" || stateType?.trim().toLowerCase() === "done";
35
+ }
33
36
  export class RunOrchestrator {
34
37
  config;
35
38
  db;
@@ -135,6 +138,38 @@ export class RunOrchestrator {
135
138
  materializeLegacyPendingWake(issue, lease) {
136
139
  return this.runWakePlanner.materializeLegacyPendingWake(issue, lease);
137
140
  }
141
+ buildImplementationCoordinationContext(issue) {
142
+ const unresolvedBlockers = this.db.issues
143
+ .listIssueDependencies(issue.projectId, issue.linearIssueId)
144
+ .filter((entry) => !isResolvedDependencyState(entry.blockerCurrentLinearStateType))
145
+ .map((entry) => ({
146
+ linearIssueId: entry.blockerLinearIssueId,
147
+ ...(entry.blockerIssueKey ? { issueKey: entry.blockerIssueKey } : {}),
148
+ ...(entry.blockerTitle ? { title: entry.blockerTitle } : {}),
149
+ ...(entry.blockerCurrentLinearState ? { stateName: entry.blockerCurrentLinearState } : {}),
150
+ ...(entry.blockerCurrentLinearStateType ? { stateType: entry.blockerCurrentLinearStateType } : {}),
151
+ }));
152
+ const trackedDependents = this.db.issues
153
+ .listDependents(issue.projectId, issue.linearIssueId)
154
+ .map((entry) => this.db.issues.getIssue(issue.projectId, entry.linearIssueId))
155
+ .filter((entry) => Boolean(entry))
156
+ .map((entry) => ({
157
+ linearIssueId: entry.linearIssueId,
158
+ ...(entry.issueKey ? { issueKey: entry.issueKey } : {}),
159
+ ...(entry.title ? { title: entry.title } : {}),
160
+ factoryState: entry.factoryState,
161
+ ...(entry.currentLinearState ? { currentLinearState: entry.currentLinearState } : {}),
162
+ delegatedToPatchRelay: entry.delegatedToPatchRelay,
163
+ hasOpenPr: entry.prNumber !== undefined && entry.prState !== "closed" && entry.prState !== "merged",
164
+ }));
165
+ if (unresolvedBlockers.length === 0 && trackedDependents.length === 0) {
166
+ return {};
167
+ }
168
+ return {
169
+ ...(unresolvedBlockers.length > 0 ? { unresolvedBlockers } : {}),
170
+ ...(trackedDependents.length > 0 ? { trackedDependents } : {}),
171
+ };
172
+ }
138
173
  // ─── Run ────────────────────────────────────────────────────────
139
174
  async run(item) {
140
175
  await this.refreshCodexRuntimeConfig();
@@ -177,9 +212,15 @@ export class RunOrchestrator {
177
212
  this.releaseIssueSessionLease(item.projectId, item.issueId);
178
213
  return;
179
214
  }
180
- const effectiveContext = isRequestedChangesRunType(runType)
215
+ const baseContext = isRequestedChangesRunType(runType)
181
216
  ? await this.runCompletionPolicy.resolveRequestedChangesWakeContext(issue, runType, context)
182
217
  : context;
218
+ const coordinationContext = runType === "implementation"
219
+ ? this.buildImplementationCoordinationContext(issue)
220
+ : undefined;
221
+ const effectiveContext = coordinationContext
222
+ ? { ...coordinationContext, ...(baseContext ?? {}) }
223
+ : baseContext;
183
224
  const sourceHeadSha = typeof effectiveContext?.failureHeadSha === "string"
184
225
  ? effectiveContext.failureHeadSha
185
226
  : typeof effectiveContext?.headSha === "string"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.47.1",
3
+ "version": "0.47.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {