patchrelay 0.50.2 → 0.50.4
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/dist/build-info.json +3 -3
- package/dist/implementation-outcome-policy.js +63 -42
- package/dist/no-pr-completion-check.js +11 -8
- package/dist/prompting/patchrelay.js +5 -0
- package/dist/run-completion-policy.js +3 -0
- package/dist/run-finalizer.js +31 -0
- package/dist/run-notification-handler.js +14 -1
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -16,53 +16,74 @@ export class ImplementationOutcomePolicy {
|
|
|
16
16
|
}
|
|
17
17
|
const project = this.config.projects.find((entry) => entry.id === run.projectId);
|
|
18
18
|
const baseBranch = project?.github?.baseBranch ?? "main";
|
|
19
|
-
|
|
19
|
+
const publishedPrState = await this.detectPublishedPrState(issue, project?.github?.repoFullName);
|
|
20
|
+
if (publishedPrState === "open") {
|
|
20
21
|
return undefined;
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
23
|
+
const details = await this.describeLocalImplementationOutcome(issue, baseBranch);
|
|
24
|
+
return details ?? `Implementation completed without opening a PR for branch ${issue.branchName ?? issue.linearIssueId}`;
|
|
25
|
+
}
|
|
26
|
+
async detectRecoverableFailedImplementationOutcome(run, issue) {
|
|
27
|
+
if (run.runType !== "implementation") {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const project = this.config.projects.find((entry) => entry.id === run.projectId);
|
|
31
|
+
const publishedPrState = await this.detectPublishedPrState(issue, project?.github?.repoFullName);
|
|
32
|
+
if (publishedPrState === "open" || publishedPrState === "unknown") {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const baseBranch = project?.github?.baseBranch ?? "main";
|
|
36
|
+
return await this.describeLocalImplementationOutcome(issue, baseBranch);
|
|
37
|
+
}
|
|
38
|
+
async detectPublishedPrState(issue, repoFullName) {
|
|
39
|
+
if (issue.prNumber && issue.prState && issue.prState !== "closed") {
|
|
40
|
+
return "open";
|
|
41
|
+
}
|
|
42
|
+
if (!repoFullName || !issue.branchName) {
|
|
43
|
+
return "unknown";
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const { stdout, exitCode } = await execCommand("gh", [
|
|
47
|
+
"pr",
|
|
48
|
+
"list",
|
|
49
|
+
"--repo",
|
|
50
|
+
repoFullName,
|
|
51
|
+
"--head",
|
|
52
|
+
issue.branchName,
|
|
53
|
+
"--state",
|
|
54
|
+
"all",
|
|
55
|
+
"--json",
|
|
56
|
+
"number,url,state,author,headRefOid",
|
|
57
|
+
], { timeoutMs: 10_000 });
|
|
58
|
+
if (exitCode !== 0) {
|
|
59
|
+
return "unknown";
|
|
54
60
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
repoFullName: project.github.repoFullName,
|
|
60
|
-
error: error instanceof Error ? error.message : String(error),
|
|
61
|
-
}, "Failed to verify published PR state after implementation");
|
|
61
|
+
const matches = JSON.parse(stdout);
|
|
62
|
+
const pr = matches[0];
|
|
63
|
+
if (!pr?.number) {
|
|
64
|
+
return "none";
|
|
62
65
|
}
|
|
66
|
+
const state = pr.state?.toLowerCase();
|
|
67
|
+
this.upsertIssueIfLeaseHeld(issue.projectId, issue.linearIssueId, {
|
|
68
|
+
projectId: issue.projectId,
|
|
69
|
+
linearIssueId: issue.linearIssueId,
|
|
70
|
+
prNumber: pr.number,
|
|
71
|
+
...(pr.url ? { prUrl: pr.url } : {}),
|
|
72
|
+
...(state ? { prState: state } : {}),
|
|
73
|
+
...(pr.headRefOid ? { prHeadSha: pr.headRefOid } : {}),
|
|
74
|
+
...(pr.author?.login ? { prAuthorLogin: pr.author.login } : {}),
|
|
75
|
+
}, "published PR verification refresh");
|
|
76
|
+
return state === "closed" ? "closed" : "open";
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
this.logger.debug({
|
|
80
|
+
issueKey: issue.issueKey,
|
|
81
|
+
branchName: issue.branchName,
|
|
82
|
+
repoFullName,
|
|
83
|
+
error: error instanceof Error ? error.message : String(error),
|
|
84
|
+
}, "Failed to verify published PR state after implementation");
|
|
85
|
+
return "unknown";
|
|
63
86
|
}
|
|
64
|
-
const details = await this.describeLocalImplementationOutcome(issue, baseBranch);
|
|
65
|
-
return details ?? `Implementation completed without opening a PR for branch ${issue.branchName ?? issue.linearIssueId}`;
|
|
66
87
|
}
|
|
67
88
|
upsertIssueIfLeaseHeld(projectId, linearIssueId, params, context) {
|
|
68
89
|
const updated = this.withHeldLease(projectId, linearIssueId, (lease) => this.db.issueSessions.upsertIssueWithLease(lease, params));
|
|
@@ -8,10 +8,12 @@ function shouldContinueForUnpublishedLocalChanges(message) {
|
|
|
8
8
|
|| (normalized.includes("local commit") && normalized.includes("ahead of origin/"));
|
|
9
9
|
}
|
|
10
10
|
export async function handleNoPrCompletionCheck(params) {
|
|
11
|
-
const
|
|
11
|
+
const runUpdate = buildRunUpdate({
|
|
12
|
+
status: params.runStatus,
|
|
12
13
|
threadId: params.threadId,
|
|
13
14
|
...(params.completedTurnId ? { completedTurnId: params.completedTurnId } : {}),
|
|
14
15
|
report: params.report,
|
|
16
|
+
...(params.failureReason ? { failureReason: params.failureReason } : {}),
|
|
15
17
|
});
|
|
16
18
|
params.publishTurnEvent({
|
|
17
19
|
level: "info",
|
|
@@ -53,7 +55,7 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
53
55
|
}
|
|
54
56
|
if (completionCheck.outcome === "continue") {
|
|
55
57
|
const continued = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
56
|
-
params.db.runs.finishRun(params.run.id,
|
|
58
|
+
params.db.runs.finishRun(params.run.id, runUpdate);
|
|
57
59
|
params.db.runs.saveCompletionCheck(params.run.id, completionCheck);
|
|
58
60
|
params.db.issues.upsertIssue({
|
|
59
61
|
projectId: params.run.projectId,
|
|
@@ -93,7 +95,7 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
93
95
|
}
|
|
94
96
|
if (completionCheck.outcome === "needs_input") {
|
|
95
97
|
const completed = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
96
|
-
params.db.runs.finishRun(params.run.id,
|
|
98
|
+
params.db.runs.finishRun(params.run.id, runUpdate);
|
|
97
99
|
params.db.runs.saveCompletionCheck(params.run.id, completionCheck);
|
|
98
100
|
params.db.issueSessions.clearPendingIssueSessionEventsWithLease(lease);
|
|
99
101
|
params.db.issues.upsertIssue({
|
|
@@ -125,7 +127,7 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
125
127
|
if (completionCheck.outcome === "done") {
|
|
126
128
|
if (shouldContinueForUnpublishedLocalChanges(params.publishedOutcomeError)) {
|
|
127
129
|
const continued = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
128
|
-
params.db.runs.finishRun(params.run.id,
|
|
130
|
+
params.db.runs.finishRun(params.run.id, runUpdate);
|
|
129
131
|
params.db.runs.saveCompletionCheck(params.run.id, {
|
|
130
132
|
...completionCheck,
|
|
131
133
|
outcome: "continue",
|
|
@@ -172,7 +174,7 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
172
174
|
? params.db.issues.countOpenChildIssues(params.run.projectId, params.run.linearIssueId)
|
|
173
175
|
: 0;
|
|
174
176
|
const completed = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
175
|
-
params.db.runs.finishRun(params.run.id,
|
|
177
|
+
params.db.runs.finishRun(params.run.id, runUpdate);
|
|
176
178
|
params.db.runs.saveCompletionCheck(params.run.id, completionCheck);
|
|
177
179
|
params.db.issueSessions.clearPendingIssueSessionEventsWithLease(lease);
|
|
178
180
|
params.db.issues.upsertIssue({
|
|
@@ -226,7 +228,7 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
226
228
|
const failureReason = `No PR observed and the completion check failed this run: ${completionCheck.summary}`;
|
|
227
229
|
const failed = params.withHeldLease(params.run.projectId, params.run.linearIssueId, () => {
|
|
228
230
|
params.db.runs.finishRun(params.run.id, {
|
|
229
|
-
...
|
|
231
|
+
...runUpdate,
|
|
230
232
|
status: "failed",
|
|
231
233
|
failureReason,
|
|
232
234
|
});
|
|
@@ -256,12 +258,13 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
256
258
|
detail: completionCheck.summary,
|
|
257
259
|
});
|
|
258
260
|
}
|
|
259
|
-
function
|
|
261
|
+
function buildRunUpdate(params) {
|
|
260
262
|
return {
|
|
261
|
-
status:
|
|
263
|
+
status: params.status,
|
|
262
264
|
threadId: params.threadId,
|
|
263
265
|
...(params.completedTurnId ? { turnId: params.completedTurnId } : {}),
|
|
264
266
|
summaryJson: JSON.stringify({ latestAssistantMessage: params.report.assistantMessages.at(-1) ?? null }),
|
|
265
267
|
reportJson: JSON.stringify(params.report),
|
|
268
|
+
...(params.failureReason ? { failureReason: params.failureReason } : {}),
|
|
266
269
|
};
|
|
267
270
|
}
|
|
@@ -157,7 +157,10 @@ function buildOrchestrationConstraints(context) {
|
|
|
157
157
|
"## Constraints",
|
|
158
158
|
"",
|
|
159
159
|
"This issue is orchestration work. Coordinate convergence instead of duplicating child implementation.",
|
|
160
|
+
"Inspect the current child set before acting. Reuse existing child issues when they already cover the needed slices instead of creating duplicates.",
|
|
161
|
+
"Babysit child progress and solve parent-owned integration or convergence issues when the delivered pieces do not yet fit together cleanly.",
|
|
160
162
|
"Do not open an overlapping umbrella PR unless this parent owns unique direct work.",
|
|
163
|
+
"Create new child issues only for genuinely missing required work needed to satisfy the parent goal.",
|
|
161
164
|
"Leave later-wave child issues queued unless they are immediately actionable.",
|
|
162
165
|
"",
|
|
163
166
|
"### Child Issue Summaries",
|
|
@@ -435,6 +438,8 @@ function buildOrchestrationWorkflowGuidance() {
|
|
|
435
438
|
"## Workflow",
|
|
436
439
|
"",
|
|
437
440
|
"Use the wake reason and child issue summaries to decide the next orchestration step.",
|
|
441
|
+
"Prefer supervising, auditing, and unblocking existing child work over creating more issues.",
|
|
442
|
+
"If the parent goal now depends on an integration fix between delivered child slices, own that convergence work here without restating already-owned child implementation.",
|
|
438
443
|
"Keep outputs concise and observable in Linear.",
|
|
439
444
|
].join("\n");
|
|
440
445
|
}
|
|
@@ -39,4 +39,7 @@ export class RunCompletionPolicy {
|
|
|
39
39
|
async verifyPublishedRunOutcome(run, issue) {
|
|
40
40
|
return await this.implementationOutcomes.verifyPublishedRunOutcome(run, issue);
|
|
41
41
|
}
|
|
42
|
+
async detectRecoverableFailedImplementationOutcome(run, issue) {
|
|
43
|
+
return await this.implementationOutcomes.detectRecoverableFailedImplementationOutcome(run, issue);
|
|
44
|
+
}
|
|
42
45
|
}
|
package/dist/run-finalizer.js
CHANGED
|
@@ -126,6 +126,7 @@ export class RunFinalizer {
|
|
|
126
126
|
run,
|
|
127
127
|
issue: freshIssue,
|
|
128
128
|
report,
|
|
129
|
+
runStatus: "completed",
|
|
129
130
|
threadId,
|
|
130
131
|
...(params.completedTurnId ? { completedTurnId: params.completedTurnId } : {}),
|
|
131
132
|
publishedOutcomeError,
|
|
@@ -220,4 +221,34 @@ export class RunFinalizer {
|
|
|
220
221
|
this.linearSync.clearProgress(run.id);
|
|
221
222
|
this.releaseLease(run.projectId, run.linearIssueId);
|
|
222
223
|
}
|
|
224
|
+
async recoverFailedImplementationRun(params) {
|
|
225
|
+
const freshIssue = this.db.issues.getIssue(params.run.projectId, params.run.linearIssueId) ?? params.issue;
|
|
226
|
+
const publishedOutcomeError = await this.completionPolicy.detectRecoverableFailedImplementationOutcome(params.run, freshIssue);
|
|
227
|
+
if (!publishedOutcomeError) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
const trackedIssue = this.db.issueToTrackedIssue(freshIssue);
|
|
231
|
+
const report = buildStageReport({ ...params.run, status: "failed" }, trackedIssue, params.thread, countEventMethods(this.db.runs.listThreadEvents(params.run.id)));
|
|
232
|
+
await handleNoPrCompletionCheck({
|
|
233
|
+
db: this.db,
|
|
234
|
+
logger: this.logger,
|
|
235
|
+
withHeldLease: this.withHeldLease,
|
|
236
|
+
completionCheck: this.completionCheck,
|
|
237
|
+
run: params.run,
|
|
238
|
+
issue: freshIssue,
|
|
239
|
+
report,
|
|
240
|
+
runStatus: "failed",
|
|
241
|
+
threadId: params.threadId,
|
|
242
|
+
...(params.completedTurnId ? { completedTurnId: params.completedTurnId } : {}),
|
|
243
|
+
failureReason: params.failureReason,
|
|
244
|
+
publishedOutcomeError,
|
|
245
|
+
failRunAndClear: this.failRunAndClear,
|
|
246
|
+
emitActivity: (issueRecord, activity, options) => this.linearSync.emitActivity(issueRecord, activity, options),
|
|
247
|
+
publishTurnEvent: (event) => this.publishTurnEvent(event),
|
|
248
|
+
syncFailureOutcome: (event) => this.syncFailureOutcome(event),
|
|
249
|
+
syncCompletionCheckOutcome: (event) => this.syncCompletionCheckOutcome(event),
|
|
250
|
+
clearProgressAndRelease: (releaseRun) => this.clearProgressAndRelease(releaseRun),
|
|
251
|
+
});
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
223
254
|
}
|
|
@@ -75,13 +75,26 @@ export class RunNotificationHandler {
|
|
|
75
75
|
const completedTurnId = extractTurnId(notification.params);
|
|
76
76
|
const status = resolveRunCompletionStatus(notification.params);
|
|
77
77
|
if (status === "failed") {
|
|
78
|
+
const failureReason = "Codex reported the turn completed in a failed state";
|
|
79
|
+
const recovered = await this.runFinalizer.recoverFailedImplementationRun({
|
|
80
|
+
run,
|
|
81
|
+
issue,
|
|
82
|
+
thread,
|
|
83
|
+
threadId,
|
|
84
|
+
...(completedTurnId ? { completedTurnId } : {}),
|
|
85
|
+
failureReason,
|
|
86
|
+
});
|
|
87
|
+
if (recovered) {
|
|
88
|
+
this.activeThreadId = undefined;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
78
91
|
const nextState = isRequestedChangesRunType(run.runType) ? "escalated" : "failed";
|
|
79
92
|
const updated = this.withHeldIssueSessionLease(run.projectId, run.linearIssueId, (lease) => {
|
|
80
93
|
this.db.issueSessions.finishRunWithLease(lease, run.id, {
|
|
81
94
|
status: "failed",
|
|
82
95
|
threadId,
|
|
83
96
|
...(completedTurnId ? { turnId: completedTurnId } : {}),
|
|
84
|
-
failureReason
|
|
97
|
+
failureReason,
|
|
85
98
|
});
|
|
86
99
|
this.db.issueSessions.upsertIssueWithLease(lease, {
|
|
87
100
|
projectId: run.projectId,
|