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 +10 -2
- package/dist/build-info.json +3 -3
- package/dist/prompting/patchrelay.js +74 -2
- package/dist/run-orchestrator.js +42 -1
- package/package.json +1 -1
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
|
|
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.
|
|
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.
|
package/dist/build-info.json
CHANGED
|
@@ -65,7 +65,76 @@ function buildTaskObjective(issue) {
|
|
|
65
65
|
...(intro ? ["", intro] : []),
|
|
66
66
|
].join("\n");
|
|
67
67
|
}
|
|
68
|
-
function
|
|
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 });
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -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
|
|
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"
|