patchrelay 0.68.6 → 0.69.0
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/cli/data.js +1 -0
- package/dist/codex-app-server.js +18 -0
- package/dist/codex-conversation-adapter.js +270 -0
- package/dist/followup-intent.js +167 -54
- package/dist/issue-session-events.js +12 -0
- package/dist/linear-progress-reporter.js +81 -2
- package/dist/linear-session-reporting.js +74 -14
- package/dist/manual-issue-actions.js +5 -0
- package/dist/operator-retry-event.js +7 -0
- package/dist/prompting/patchrelay.js +39 -7
- package/dist/queue-health-monitor.js +16 -1
- package/dist/reactive-run-policy.js +12 -25
- package/dist/run-finalizer.js +23 -0
- package/dist/run-orchestrator.js +11 -2
- package/dist/service-issue-actions.js +1 -0
- package/dist/webhook-handler.js +8 -4
- package/dist/webhooks/agent-session-handler.js +18 -93
- package/dist/webhooks/comment-policy.js +19 -1
- package/dist/webhooks/comment-wake-handler.js +25 -161
- package/package.json +1 -1
|
@@ -2,10 +2,13 @@ import { deriveLinearProgressFact } from "./linear-progress-facts.js";
|
|
|
2
2
|
export class LinearProgressReporter {
|
|
3
3
|
db;
|
|
4
4
|
emitActivity;
|
|
5
|
+
options;
|
|
6
|
+
static DEFAULT_HEARTBEAT_INTERVAL_MS = 15 * 60 * 1000;
|
|
5
7
|
publicationsByRun = new Map();
|
|
6
|
-
constructor(db, emitActivity) {
|
|
8
|
+
constructor(db, emitActivity, options = {}) {
|
|
7
9
|
this.db = db;
|
|
8
10
|
this.emitActivity = emitActivity;
|
|
11
|
+
this.options = options;
|
|
9
12
|
}
|
|
10
13
|
maybeEmitProgress(notification, run) {
|
|
11
14
|
const issue = this.db.getIssue(run.projectId, run.linearIssueId);
|
|
@@ -14,6 +17,7 @@ export class LinearProgressReporter {
|
|
|
14
17
|
}
|
|
15
18
|
const fact = deriveLinearProgressFact(notification, issue);
|
|
16
19
|
if (!fact) {
|
|
20
|
+
this.maybeEmitHeartbeat(notification, run);
|
|
17
21
|
return;
|
|
18
22
|
}
|
|
19
23
|
const previous = this.publicationsByRun.get(run.id);
|
|
@@ -22,9 +26,10 @@ export class LinearProgressReporter {
|
|
|
22
26
|
if (!shouldEmitEphemeral && !shouldEmitHistory) {
|
|
23
27
|
return;
|
|
24
28
|
}
|
|
25
|
-
const now =
|
|
29
|
+
const now = this.now();
|
|
26
30
|
const publication = {
|
|
27
31
|
...previous,
|
|
32
|
+
quietSinceMs: now,
|
|
28
33
|
...(shouldEmitEphemeral
|
|
29
34
|
? {
|
|
30
35
|
ephemeralMeaningKey: fact.meaningKey,
|
|
@@ -53,6 +58,56 @@ export class LinearProgressReporter {
|
|
|
53
58
|
clearProgress(runId) {
|
|
54
59
|
this.publicationsByRun.delete(runId);
|
|
55
60
|
}
|
|
61
|
+
maybeEmitHeartbeat(notification, run) {
|
|
62
|
+
const previous = this.publicationsByRun.get(run.id);
|
|
63
|
+
const now = this.now();
|
|
64
|
+
const quietSinceMs = previous?.quietSinceMs ?? previous?.ephemeralPublishedAtMs ?? previous?.historyPublishedAtMs ?? now;
|
|
65
|
+
const elapsedMs = now - quietSinceMs;
|
|
66
|
+
const intervalMs = this.options.heartbeatIntervalMs ?? LinearProgressReporter.DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
67
|
+
if (elapsedMs < intervalMs) {
|
|
68
|
+
if (!previous) {
|
|
69
|
+
this.publicationsByRun.set(run.id, { quietSinceMs });
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (previous?.lastHeartbeatAtMs !== undefined && now - previous.lastHeartbeatAtMs < intervalMs) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const issue = this.db.getIssue(run.projectId, run.linearIssueId);
|
|
77
|
+
if (!issue) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const detail = describeHeartbeatDetail(notification);
|
|
81
|
+
const content = {
|
|
82
|
+
type: "thought",
|
|
83
|
+
body: detail
|
|
84
|
+
? `PatchRelay is still working on ${run.runType.replaceAll("_", " ")}. Latest signal: ${detail}.`
|
|
85
|
+
: `PatchRelay is still working on ${run.runType.replaceAll("_", " ")}.`,
|
|
86
|
+
};
|
|
87
|
+
this.publicationsByRun.set(run.id, {
|
|
88
|
+
...previous,
|
|
89
|
+
quietSinceMs,
|
|
90
|
+
lastHeartbeatAtMs: now,
|
|
91
|
+
});
|
|
92
|
+
void this.emitActivity(issue, content, { ephemeral: true }).catch(() => {
|
|
93
|
+
const current = this.publicationsByRun.get(run.id);
|
|
94
|
+
if (current?.lastHeartbeatAtMs === now) {
|
|
95
|
+
const restored = {
|
|
96
|
+
...current,
|
|
97
|
+
};
|
|
98
|
+
if (previous?.lastHeartbeatAtMs !== undefined) {
|
|
99
|
+
restored.lastHeartbeatAtMs = previous.lastHeartbeatAtMs;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
delete restored.lastHeartbeatAtMs;
|
|
103
|
+
}
|
|
104
|
+
this.publicationsByRun.set(run.id, restored);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
now() {
|
|
109
|
+
return this.options.now?.() ?? Date.now();
|
|
110
|
+
}
|
|
56
111
|
clearFailedPublication(runId, channel, meaningKey, publishedAtMs) {
|
|
57
112
|
const current = this.publicationsByRun.get(runId);
|
|
58
113
|
if (!current) {
|
|
@@ -93,3 +148,27 @@ export class LinearProgressReporter {
|
|
|
93
148
|
this.publicationsByRun.set(runId, next);
|
|
94
149
|
}
|
|
95
150
|
}
|
|
151
|
+
function describeHeartbeatDetail(notification) {
|
|
152
|
+
if (notification.method === "item/started" || notification.method === "item/updated" || notification.method === "item/completed") {
|
|
153
|
+
const item = notification.params.item;
|
|
154
|
+
if (item && typeof item === "object") {
|
|
155
|
+
const typed = item;
|
|
156
|
+
if (typed.type === "commandExecution" && typeof typed.command === "string") {
|
|
157
|
+
return `command ${trimHeartbeatDetail(typed.command)}`;
|
|
158
|
+
}
|
|
159
|
+
if (typed.type === "dynamicToolCall" && typeof typed.tool === "string") {
|
|
160
|
+
return `tool ${trimHeartbeatDetail(typed.tool)}`;
|
|
161
|
+
}
|
|
162
|
+
if (typed.type === "mcpToolCall" && typeof typed.server === "string" && typeof typed.tool === "string") {
|
|
163
|
+
return `tool ${trimHeartbeatDetail(`${typed.server}/${typed.tool}`)}`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return notification.method;
|
|
168
|
+
}
|
|
169
|
+
function trimHeartbeatDetail(value, maxLength = 120) {
|
|
170
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
171
|
+
if (compact.length <= maxLength)
|
|
172
|
+
return compact;
|
|
173
|
+
return `${compact.slice(0, maxLength).trimEnd()}...`;
|
|
174
|
+
}
|
|
@@ -61,7 +61,14 @@ export function buildBlockedDelegationActivity(blockedByKeys = []) {
|
|
|
61
61
|
export function buildPromptDeliveredThought(runType) {
|
|
62
62
|
return {
|
|
63
63
|
type: "thought",
|
|
64
|
-
body: `PatchRelay routed your latest instructions into the active ${lowerRunTypeLabel(runType)} workflow.`,
|
|
64
|
+
body: `PatchRelay routed your latest instructions into the active ${lowerRunTypeLabel(runType)} workflow; it will fold them in at the next checkpoint.`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function buildPromptDeliveryFailedActivity(runType, reason) {
|
|
68
|
+
const suffix = reason ? ` ${trimSummary(reason, 180)}` : "";
|
|
69
|
+
return {
|
|
70
|
+
type: "thought",
|
|
71
|
+
body: `PatchRelay could not route your latest instructions into the active ${lowerRunTypeLabel(runType)} workflow.${suffix}`,
|
|
65
72
|
};
|
|
66
73
|
}
|
|
67
74
|
export function buildFollowupStatusActivity(params) {
|
|
@@ -72,14 +79,16 @@ export function buildFollowupStatusActivity(params) {
|
|
|
72
79
|
const prNote = params.issue.prNumber ? ` PR #${params.issue.prNumber}.` : "";
|
|
73
80
|
const statusNote = params.statusNote ? ` ${params.statusNote}` : "";
|
|
74
81
|
return {
|
|
75
|
-
type: "response",
|
|
82
|
+
type: params.activityType ?? "response",
|
|
76
83
|
body: `PatchRelay status: ${subject} is ${formatFactoryState(params.issue.factoryState)}.${prNote}${runNote}${statusNote}`.trim(),
|
|
77
84
|
};
|
|
78
85
|
}
|
|
79
86
|
export function buildNonActionableFollowupActivity(intent) {
|
|
80
87
|
const body = intent === "status"
|
|
81
88
|
? "PatchRelay status is available in the current agent session."
|
|
82
|
-
:
|
|
89
|
+
: intent === "context_only"
|
|
90
|
+
? "PatchRelay recorded this as context and did not start a new run. Ask PatchRelay to continue, retry, or implement when you want work to run."
|
|
91
|
+
: "PatchRelay did not start a run because this did not clearly request work. Ask PatchRelay to continue, retry, or implement when you want work to run.";
|
|
83
92
|
return { type: "response", body };
|
|
84
93
|
}
|
|
85
94
|
export function buildRunStartedActivity(runType) {
|
|
@@ -97,6 +106,18 @@ export function buildRunStartedActivity(runType) {
|
|
|
97
106
|
return { type: "action", action: "Implementing", parameter: "requested change" };
|
|
98
107
|
}
|
|
99
108
|
}
|
|
109
|
+
export function buildReviewRoundStartedActivity(params) {
|
|
110
|
+
const reviewer = params.reviewerName ? ` from @${params.reviewerName}` : "";
|
|
111
|
+
const head = params.headSha ? ` on head ${params.headSha.slice(0, 8)}` : "";
|
|
112
|
+
const comments = params.commentCount !== undefined
|
|
113
|
+
? `; ${params.commentCount} inline comment${params.commentCount === 1 ? "" : "s"} captured`
|
|
114
|
+
: "";
|
|
115
|
+
return {
|
|
116
|
+
type: "action",
|
|
117
|
+
action: "Review round",
|
|
118
|
+
parameter: `${params.round}${reviewer}${head}${comments}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
100
121
|
function formatFactoryState(state) {
|
|
101
122
|
return state.replaceAll("_", " ");
|
|
102
123
|
}
|
|
@@ -104,44 +125,54 @@ export function buildRunCompletedActivity(params) {
|
|
|
104
125
|
const prLabel = params.prNumber ? `PR #${params.prNumber}` : "the pull request";
|
|
105
126
|
const summary = trimSummary(params.completionSummary);
|
|
106
127
|
const detail = summary ? ` ${summary}` : "";
|
|
128
|
+
const steeringSummary = buildSteeringSummary(params.steeringDeliveredCount, params.steeringFailedCount);
|
|
107
129
|
switch (params.runType) {
|
|
108
130
|
case "implementation":
|
|
109
131
|
if (params.postRunState === "pr_open") {
|
|
132
|
+
const body = `${prLabel} opened:${detail || " Published and ready for review."}`;
|
|
110
133
|
return {
|
|
111
134
|
type: "response",
|
|
112
|
-
body: `${
|
|
135
|
+
body: steeringSummary ? `${body}\n\n${steeringSummary}` : body,
|
|
113
136
|
};
|
|
114
137
|
}
|
|
115
138
|
return undefined;
|
|
116
139
|
case "review_fix":
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
140
|
+
{
|
|
141
|
+
const lines = [];
|
|
142
|
+
lines.push(params.reviewRound ? `Review round ${params.reviewRound} completed.` : "Review fix completed.");
|
|
143
|
+
if (params.resultHeadSha)
|
|
144
|
+
lines.push(`Resulting head: ${params.resultHeadSha.slice(0, 8)}.`);
|
|
145
|
+
if (steeringSummary)
|
|
146
|
+
lines.push(steeringSummary);
|
|
147
|
+
const resolution = buildReviewResolutionSections(summary);
|
|
148
|
+
lines.push("", "Addressed:", resolution.addressed, "", "Deferred:", resolution.deferred, "", "Not applicable:", resolution.notApplicable);
|
|
149
|
+
if (!summary && !params.resultHeadSha) {
|
|
150
|
+
lines[0] = `Updated ${prLabel} to address review feedback.`;
|
|
121
151
|
}
|
|
122
|
-
|
|
152
|
+
return {
|
|
123
153
|
type: "response",
|
|
124
|
-
body:
|
|
154
|
+
body: lines.join("\n").trim(),
|
|
125
155
|
};
|
|
156
|
+
}
|
|
126
157
|
case "ci_repair":
|
|
127
158
|
return summary
|
|
128
159
|
? {
|
|
129
160
|
type: "response",
|
|
130
|
-
body: summary,
|
|
161
|
+
body: steeringSummary ? `${summary}\n\n${steeringSummary}` : summary,
|
|
131
162
|
}
|
|
132
163
|
: {
|
|
133
164
|
type: "response",
|
|
134
|
-
body: `Updated ${prLabel} after CI repair.`,
|
|
165
|
+
body: steeringSummary ? `Updated ${prLabel} after CI repair.\n\n${steeringSummary}` : `Updated ${prLabel} after CI repair.`,
|
|
135
166
|
};
|
|
136
167
|
case "queue_repair":
|
|
137
168
|
return summary
|
|
138
169
|
? {
|
|
139
170
|
type: "response",
|
|
140
|
-
body: summary,
|
|
171
|
+
body: steeringSummary ? `${summary}\n\n${steeringSummary}` : summary,
|
|
141
172
|
}
|
|
142
173
|
: {
|
|
143
174
|
type: "response",
|
|
144
|
-
body: `Updated ${prLabel} after merge-queue repair.`,
|
|
175
|
+
body: steeringSummary ? `Updated ${prLabel} after merge-queue repair.\n\n${steeringSummary}` : `Updated ${prLabel} after merge-queue repair.`,
|
|
145
176
|
};
|
|
146
177
|
case "branch_upkeep":
|
|
147
178
|
return undefined;
|
|
@@ -155,6 +186,9 @@ export function buildRunCompletedActivity(params) {
|
|
|
155
186
|
if (summary) {
|
|
156
187
|
lines.push("", summary);
|
|
157
188
|
}
|
|
189
|
+
if (steeringSummary) {
|
|
190
|
+
lines.push("", steeringSummary);
|
|
191
|
+
}
|
|
158
192
|
return {
|
|
159
193
|
type: "response",
|
|
160
194
|
body: lines.join("\n"),
|
|
@@ -162,6 +196,32 @@ export function buildRunCompletedActivity(params) {
|
|
|
162
196
|
}
|
|
163
197
|
}
|
|
164
198
|
}
|
|
199
|
+
function buildSteeringSummary(delivered = 0, failed = 0) {
|
|
200
|
+
if (delivered === 0 && failed === 0)
|
|
201
|
+
return undefined;
|
|
202
|
+
const parts = [];
|
|
203
|
+
if (delivered > 0) {
|
|
204
|
+
parts.push(`${delivered} follow-up prompt${delivered === 1 ? "" : "s"} delivered`);
|
|
205
|
+
}
|
|
206
|
+
if (failed > 0) {
|
|
207
|
+
parts.push(`${failed} follow-up delivery failure${failed === 1 ? "" : "s"}`);
|
|
208
|
+
}
|
|
209
|
+
return `Steering: ${parts.join("; ")}.`;
|
|
210
|
+
}
|
|
211
|
+
function buildReviewResolutionSections(summary) {
|
|
212
|
+
if (!summary) {
|
|
213
|
+
return {
|
|
214
|
+
addressed: "- Review feedback addressed and published.",
|
|
215
|
+
deferred: "- None reported.",
|
|
216
|
+
notApplicable: "- None reported.",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
addressed: `- ${summary}`,
|
|
221
|
+
deferred: "- None reported.",
|
|
222
|
+
notApplicable: "- None reported.",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
165
225
|
export function buildRunFailureActivity(runType, reason) {
|
|
166
226
|
const label = formatRunTypeLabel(runType);
|
|
167
227
|
return {
|
|
@@ -6,6 +6,11 @@ export function resolveRetryTarget(params) {
|
|
|
6
6
|
if (hasOpenPr(params.prNumber, params.prState) && params.lastGitHubFailureSource === "queue_eviction") {
|
|
7
7
|
return { runType: "queue_repair", factoryState: "repairing_queue" };
|
|
8
8
|
}
|
|
9
|
+
if (hasOpenPr(params.prNumber, params.prState)
|
|
10
|
+
&& params.prReviewState === "approved"
|
|
11
|
+
&& (params.factoryState === "awaiting_queue" || params.lastRunType === "queue_repair")) {
|
|
12
|
+
return { runType: "queue_repair", factoryState: "repairing_queue" };
|
|
13
|
+
}
|
|
9
14
|
if (hasOpenPr(params.prNumber, params.prState)
|
|
10
15
|
&& (params.prCheckStatus === "failed" || params.prCheckStatus === "failure" || params.lastGitHubFailureSource === "branch_ci")) {
|
|
11
16
|
return { runType: "ci_repair", factoryState: "repairing_ci" };
|
|
@@ -19,6 +19,13 @@ export function buildOperatorRetryEvent(issue, runType, source = "operator_retry
|
|
|
19
19
|
...(queueIncident ?? {}),
|
|
20
20
|
...(failureContext ?? {}),
|
|
21
21
|
source,
|
|
22
|
+
requiresFreshHead: true,
|
|
23
|
+
promptContext: [
|
|
24
|
+
"Operator retry is recovering a merge queue rejection on an approved PR.",
|
|
25
|
+
"If the previous repair left the same head SHA in place, merge-steward may still consider it terminally evicted.",
|
|
26
|
+
"Preserve the approved diff, but publish a new head SHA on the existing PR branch before finishing.",
|
|
27
|
+
"If rebasing onto the current base produces no content change, create an empty queue-kick commit.",
|
|
28
|
+
].join(" "),
|
|
22
29
|
}),
|
|
23
30
|
dedupeKey: `${source}:queue_repair:${issue.linearIssueId}:${issue.prHeadSha ?? issue.lastGitHubFailureHeadSha ?? "unknown-sha"}`,
|
|
24
31
|
};
|
|
@@ -266,10 +266,17 @@ function buildStructuredReviewContext(context) {
|
|
|
266
266
|
const reviewCommitId = typeof context?.reviewCommitId === "string" ? context.reviewCommitId : undefined;
|
|
267
267
|
const reviewUrl = typeof context?.reviewUrl === "string" ? context.reviewUrl : undefined;
|
|
268
268
|
const reviewComments = readReviewFixComments(context);
|
|
269
|
-
|
|
269
|
+
const degraded = context?.reviewContextDegraded === true;
|
|
270
|
+
if (!degraded && !reviewId && !reviewCommitId && !reviewUrl && reviewComments.length === 0) {
|
|
270
271
|
return undefined;
|
|
271
272
|
}
|
|
272
273
|
const lines = ["## Structured Review Context", ""];
|
|
274
|
+
if (degraded) {
|
|
275
|
+
const reason = typeof context?.reviewContextDegradedReason === "string" && context.reviewContextDegradedReason.trim()
|
|
276
|
+
? context.reviewContextDegradedReason.trim()
|
|
277
|
+
: "GitHub requested-changes context could not be refreshed before launch.";
|
|
278
|
+
lines.push("GitHub review context refresh: degraded", reason, "Do not assume cached review details are current. Re-read the PR review in GitHub before making review-fix changes.");
|
|
279
|
+
}
|
|
273
280
|
if (reviewId !== undefined)
|
|
274
281
|
lines.push(`Review ID: ${reviewId}`);
|
|
275
282
|
if (reviewCommitId)
|
|
@@ -436,6 +443,22 @@ function buildFollowUpContextLines(issue, runType, context) {
|
|
|
436
443
|
lines.push("", "Recent updates:");
|
|
437
444
|
followUpLines.forEach((line) => lines.push(`- ${line}`));
|
|
438
445
|
}
|
|
446
|
+
if (context?.replacementPrRequired === true) {
|
|
447
|
+
lines.push("", "Previous PR facts:");
|
|
448
|
+
if (typeof context.previousPrNumber === "number") {
|
|
449
|
+
lines.push(`Previous PR: #${context.previousPrNumber} (replacement PR needed)`);
|
|
450
|
+
}
|
|
451
|
+
if (typeof context.previousPrUrl === "string") {
|
|
452
|
+
lines.push(`Previous PR URL: ${context.previousPrUrl}`);
|
|
453
|
+
}
|
|
454
|
+
if (typeof context.previousPrState === "string") {
|
|
455
|
+
lines.push(`Previous PR state: ${context.previousPrState}`);
|
|
456
|
+
}
|
|
457
|
+
if (typeof context.previousPrHeadSha === "string") {
|
|
458
|
+
lines.push(`Previous PR head SHA: ${context.previousPrHeadSha}`);
|
|
459
|
+
}
|
|
460
|
+
lines.push("Create a fresh replacement PR for the new requested changes; do not mutate or republish the completed PR.");
|
|
461
|
+
}
|
|
439
462
|
if (issue.prNumber || issue.prHeadSha || issue.prReviewState || context?.mergeStateStatus) {
|
|
440
463
|
const prHeading = prContext.kind === "closed_historical_pr"
|
|
441
464
|
|| prContext.kind === "closed_replacement_pending"
|
|
@@ -531,7 +554,7 @@ function buildPrePushSelfReviewSection(target, runType) {
|
|
|
531
554
|
}
|
|
532
555
|
return lines;
|
|
533
556
|
}
|
|
534
|
-
function buildPublicationContract(runType, issueClass) {
|
|
557
|
+
function buildPublicationContract(runType, issueClass, context) {
|
|
535
558
|
if (issueClass === "orchestration") {
|
|
536
559
|
return [
|
|
537
560
|
"## Publish",
|
|
@@ -554,6 +577,7 @@ function buildPublicationContract(runType, issueClass) {
|
|
|
554
577
|
...buildPrePushSelfReviewSection("new_pr", runType),
|
|
555
578
|
].join("\n");
|
|
556
579
|
}
|
|
580
|
+
const requiresFreshQueueHead = runType === "queue_repair" && context?.requiresFreshHead === true;
|
|
557
581
|
return [
|
|
558
582
|
"## Publish",
|
|
559
583
|
"",
|
|
@@ -561,8 +585,16 @@ function buildPublicationContract(runType, issueClass) {
|
|
|
561
585
|
"Do not open a new PR.",
|
|
562
586
|
"A PR-less stop is not a successful outcome for a repair run unless a genuine external blocker prevents any correct push.",
|
|
563
587
|
"",
|
|
564
|
-
|
|
565
|
-
|
|
588
|
+
...(requiresFreshQueueHead
|
|
589
|
+
? [
|
|
590
|
+
"This queue repair requires a fresh PR head SHA because the previous head was terminally evicted by merge-steward.",
|
|
591
|
+
"Before pushing, compute `git diff $(git merge-base origin/main HEAD)..HEAD | git patch-id --stable` and compare its first field to the `Last published patch-id` shown in the prompt header (if any).",
|
|
592
|
+
"If the patch-id matches, preserve the approved diff and still push a new head SHA on the existing PR branch. Prefer a real rebase onto current `origin/main`; if that produces no content change, create an empty queue-kick commit.",
|
|
593
|
+
]
|
|
594
|
+
: [
|
|
595
|
+
"Before pushing, compute `git diff $(git merge-base origin/main HEAD)..HEAD | git patch-id --stable` and compare its first field to the `Last published patch-id` shown in the prompt header (if any).",
|
|
596
|
+
"If they match, do not push — finish the run as a no-op. Edit the PR body via `gh pr edit` instead if a textual update is needed.",
|
|
597
|
+
]),
|
|
566
598
|
"",
|
|
567
599
|
...buildPrePushSelfReviewSection("existing_pr", runType),
|
|
568
600
|
].join("\n");
|
|
@@ -586,7 +618,7 @@ function buildSections(issue, runType, repoPath, context, followUp = false) {
|
|
|
586
618
|
if (workflow) {
|
|
587
619
|
sections.push({ id: "workflow-guidance", content: workflow });
|
|
588
620
|
}
|
|
589
|
-
sections.push({ id: "publication-contract", content: buildPublicationContract(runType, issueClass) });
|
|
621
|
+
sections.push({ id: "publication-contract", content: buildPublicationContract(runType, issueClass, context) });
|
|
590
622
|
return sections;
|
|
591
623
|
}
|
|
592
624
|
function filterAllowedReplacements(promptLayer) {
|
|
@@ -654,8 +686,8 @@ export function mergePromptCustomizationLayers(base, override) {
|
|
|
654
686
|
? { extraInstructions: base.extraInstructions }
|
|
655
687
|
: {}),
|
|
656
688
|
replaceSections: {
|
|
657
|
-
...
|
|
658
|
-
...
|
|
689
|
+
...base?.replaceSections,
|
|
690
|
+
...override?.replaceSections,
|
|
659
691
|
},
|
|
660
692
|
};
|
|
661
693
|
}
|
|
@@ -12,6 +12,8 @@ function isDuplicateProbe(issue, context) {
|
|
|
12
12
|
const headSha = typeof context?.failureHeadSha === "string" ? context.failureHeadSha : undefined;
|
|
13
13
|
if (!signature)
|
|
14
14
|
return false;
|
|
15
|
+
if (context?.requiresFreshHead === true)
|
|
16
|
+
return false;
|
|
15
17
|
return issue.lastAttemptedFailureSignature === signature
|
|
16
18
|
&& (headSha === undefined || issue.lastAttemptedFailureHeadSha === headSha);
|
|
17
19
|
}
|
|
@@ -133,12 +135,25 @@ export class QueueHealthMonitor {
|
|
|
133
135
|
if (isDirty || hasEvictionCheckRun) {
|
|
134
136
|
const headRefOid = pr.headRefOid ?? "unknown";
|
|
135
137
|
const reason = hasEvictionCheckRun ? "queue_eviction_missed" : "preemptive_conflict";
|
|
136
|
-
const signature =
|
|
138
|
+
const signature = hasEvictionCheckRun
|
|
139
|
+
? `same_head_queue_eviction:${headRefOid}`
|
|
140
|
+
: `preemptive_queue_conflict:${headRefOid}`;
|
|
137
141
|
const pendingRunContext = {
|
|
138
142
|
source: "queue_health_monitor",
|
|
139
143
|
failureReason: reason,
|
|
140
144
|
failureHeadSha: headRefOid,
|
|
141
145
|
failureSignature: signature,
|
|
146
|
+
...(hasEvictionCheckRun
|
|
147
|
+
? {
|
|
148
|
+
requiresFreshHead: true,
|
|
149
|
+
promptContext: [
|
|
150
|
+
`merge-steward/queue is already failed on PR #${issue.prNumber} at head ${headRefOid}.`,
|
|
151
|
+
"merge-steward will not re-admit the same evicted head SHA.",
|
|
152
|
+
"Preserve the approved diff, but publish a new head SHA on the existing PR branch before finishing.",
|
|
153
|
+
"If rebasing onto the current base produces no content change, create an empty queue-kick commit.",
|
|
154
|
+
].join(" "),
|
|
155
|
+
}
|
|
156
|
+
: {}),
|
|
142
157
|
};
|
|
143
158
|
if (isDuplicateProbe(issue, pendingRunContext)) {
|
|
144
159
|
return;
|
|
@@ -289,19 +289,24 @@ export class ReactiveRunPolicy {
|
|
|
289
289
|
return updated;
|
|
290
290
|
}
|
|
291
291
|
async hydrateRequestedChangesContext(projectId, prNumber, repoFullName, headSha, context) {
|
|
292
|
-
const merged = {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (hasStructuredReviewContext(merged)) {
|
|
297
|
-
return merged;
|
|
292
|
+
const merged = { ...context };
|
|
293
|
+
if (headSha) {
|
|
294
|
+
merged.headSha = headSha;
|
|
295
|
+
merged.currentPrHeadSha = headSha;
|
|
298
296
|
}
|
|
299
297
|
const liveReview = await readLatestRequestedChangesReviewContext(repoFullName, prNumber);
|
|
300
298
|
if (!liveReview) {
|
|
301
|
-
return
|
|
299
|
+
return {
|
|
300
|
+
...merged,
|
|
301
|
+
reviewContextStatus: "degraded",
|
|
302
|
+
reviewContextDegraded: true,
|
|
303
|
+
reviewContextDegradedReason: "GitHub requested-changes review context could not be fetched before launch.",
|
|
304
|
+
};
|
|
302
305
|
}
|
|
303
306
|
return {
|
|
304
307
|
...merged,
|
|
308
|
+
reviewContextStatus: "fresh",
|
|
309
|
+
reviewContextDegraded: false,
|
|
305
310
|
...(liveReview.reviewId !== undefined ? { reviewId: liveReview.reviewId } : {}),
|
|
306
311
|
...(liveReview.reviewCommitId ? { reviewCommitId: liveReview.reviewCommitId } : {}),
|
|
307
312
|
...(liveReview.reviewUrl ? { reviewUrl: liveReview.reviewUrl } : {}),
|
|
@@ -327,21 +332,3 @@ function isReactiveScopeRiskPath(filePath) {
|
|
|
327
332
|
return REACTIVE_SCOPE_RISK_EXACT_PATHS.has(filePath)
|
|
328
333
|
|| REACTIVE_SCOPE_RISK_PREFIXES.some((prefix) => filePath.startsWith(prefix));
|
|
329
334
|
}
|
|
330
|
-
function hasStructuredReviewContext(context) {
|
|
331
|
-
if (!context)
|
|
332
|
-
return false;
|
|
333
|
-
const reviewBody = typeof context.reviewBody === "string" ? context.reviewBody.trim() : "";
|
|
334
|
-
const isOperatorRetryPlaceholder = typeof context.source === "string"
|
|
335
|
-
&& context.source === "operator_retry"
|
|
336
|
-
&& /^operator requested retry of review-fix work\.?$/i.test(reviewBody);
|
|
337
|
-
if (isOperatorRetryPlaceholder) {
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
340
|
-
if (reviewBody)
|
|
341
|
-
return true;
|
|
342
|
-
if (typeof context.reviewUrl === "string" && context.reviewUrl.trim())
|
|
343
|
-
return true;
|
|
344
|
-
if (typeof context.reviewerName === "string" && context.reviewerName.trim())
|
|
345
|
-
return true;
|
|
346
|
-
return Array.isArray(context.reviewComments) && context.reviewComments.length > 0;
|
|
347
|
-
}
|
package/dist/run-finalizer.js
CHANGED
|
@@ -20,6 +20,24 @@ function buildRunSummaryJson(report, publicationRecapSummary) {
|
|
|
20
20
|
publicationRecapSummary: publicationRecapSummary ?? null,
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
+
function summarizePromptDeliveryEvents(events, run) {
|
|
24
|
+
let delivered = 0;
|
|
25
|
+
let failed = 0;
|
|
26
|
+
for (const event of events) {
|
|
27
|
+
if (event.eventType !== "prompt_delivered")
|
|
28
|
+
continue;
|
|
29
|
+
const payload = parseEventJson(event.eventJson);
|
|
30
|
+
if (payload?.runId !== run.id)
|
|
31
|
+
continue;
|
|
32
|
+
if (payload.status === "delivered") {
|
|
33
|
+
delivered += 1;
|
|
34
|
+
}
|
|
35
|
+
else if (payload.status === "delivery_failed") {
|
|
36
|
+
failed += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { delivered, failed };
|
|
40
|
+
}
|
|
23
41
|
function shouldGeneratePublicationRecap(runType) {
|
|
24
42
|
return runType === "implementation"
|
|
25
43
|
|| runType === "review_fix"
|
|
@@ -417,11 +435,16 @@ export class RunFinalizer {
|
|
|
417
435
|
const completionSummary = publicationRecapSummary
|
|
418
436
|
?? report.assistantMessages.at(-1)?.slice(0, 300)
|
|
419
437
|
?? `${run.runType} completed.`;
|
|
438
|
+
const steeringSummary = summarizePromptDeliveryEvents(this.db.issueSessions.listIssueSessionEvents(run.projectId, run.linearIssueId), run);
|
|
420
439
|
const linearActivity = buildRunCompletedActivity({
|
|
421
440
|
runType: run.runType,
|
|
422
441
|
completionSummary,
|
|
423
442
|
postRunState: updatedIssue.factoryState,
|
|
424
443
|
...(updatedIssue.prNumber !== undefined ? { prNumber: updatedIssue.prNumber } : {}),
|
|
444
|
+
...(run.runType === "review_fix" ? { reviewRound: Math.max(1, updatedIssue.reviewFixAttempts) } : {}),
|
|
445
|
+
...(run.runType === "review_fix" && updatedIssue.prHeadSha ? { resultHeadSha: updatedIssue.prHeadSha } : {}),
|
|
446
|
+
...(steeringSummary.delivered > 0 ? { steeringDeliveredCount: steeringSummary.delivered } : {}),
|
|
447
|
+
...(steeringSummary.failed > 0 ? { steeringFailedCount: steeringSummary.failed } : {}),
|
|
425
448
|
});
|
|
426
449
|
if (linearActivity) {
|
|
427
450
|
void this.linearSync.emitActivity(updatedIssue, linearActivity);
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { summarizeCurrentThread } from "./run-reporting.js";
|
|
2
|
-
import { buildRunStartedActivity, } from "./linear-session-reporting.js";
|
|
2
|
+
import { buildReviewRoundStartedActivity, buildRunStartedActivity, } from "./linear-session-reporting.js";
|
|
3
3
|
import { CompletionCheckService } from "./completion-check.js";
|
|
4
4
|
import { PublicationRecapService } from "./publication-recap.js";
|
|
5
5
|
import { WorktreeManager } from "./worktree-manager.js";
|
|
@@ -411,7 +411,16 @@ export class RunOrchestrator {
|
|
|
411
411
|
this.logger.info({ issueKey: issue.issueKey, runType, threadId, turnId }, `Started ${runType} run`);
|
|
412
412
|
// Emit Linear activity + plan
|
|
413
413
|
const freshIssue = this.db.issues.getIssue(item.projectId, item.issueId) ?? issue;
|
|
414
|
-
|
|
414
|
+
const reviewComments = Array.isArray(effectiveContext?.reviewComments) ? effectiveContext.reviewComments : undefined;
|
|
415
|
+
const reviewRoundActivity = runType === "review_fix"
|
|
416
|
+
? buildReviewRoundStartedActivity({
|
|
417
|
+
round: Math.max(1, freshIssue.reviewFixAttempts),
|
|
418
|
+
...(typeof effectiveContext?.reviewerName === "string" ? { reviewerName: effectiveContext.reviewerName } : {}),
|
|
419
|
+
...(reviewComments ? { commentCount: reviewComments.length } : {}),
|
|
420
|
+
...(typeof sourceHeadSha === "string" ? { headSha: sourceHeadSha } : {}),
|
|
421
|
+
})
|
|
422
|
+
: undefined;
|
|
423
|
+
void this.linearSync.emitActivity(freshIssue, reviewRoundActivity ?? buildRunStartedActivity(runType));
|
|
415
424
|
void this.linearSync.syncSession(freshIssue, { activeRunType: runType });
|
|
416
425
|
}
|
|
417
426
|
async resetWorktreeToTrackedBranch(worktreePath, branchName, issue) {
|
|
@@ -108,6 +108,7 @@ export class ServiceIssueActions {
|
|
|
108
108
|
prState: issue.prState,
|
|
109
109
|
prReviewState: issue.prReviewState,
|
|
110
110
|
prCheckStatus: issue.prCheckStatus,
|
|
111
|
+
factoryState: issue.factoryState,
|
|
111
112
|
pendingRunType: issue.pendingRunType,
|
|
112
113
|
lastRunType: issueSession?.lastRunType,
|
|
113
114
|
lastGitHubFailureSource: issue.lastGitHubFailureSource,
|
package/dist/webhook-handler.js
CHANGED
|
@@ -12,6 +12,8 @@ import { IssueRemovalHandler } from "./webhooks/issue-removal-handler.js";
|
|
|
12
12
|
import { safeJsonParse, sanitizeDiagnosticText } from "./utils.js";
|
|
13
13
|
import { extractLatestAssistantSummary } from "./issue-session-events.js";
|
|
14
14
|
import { WakeDispatcher } from "./wake-dispatcher.js";
|
|
15
|
+
import { CodexFollowupIntentClassifier } from "./followup-intent.js";
|
|
16
|
+
import { CodexConversationAdapter } from "./codex-conversation-adapter.js";
|
|
15
17
|
export class WebhookHandler {
|
|
16
18
|
config;
|
|
17
19
|
db;
|
|
@@ -28,7 +30,7 @@ export class WebhookHandler {
|
|
|
28
30
|
dependencyReadinessHandler;
|
|
29
31
|
linearSync;
|
|
30
32
|
wakeDispatcher;
|
|
31
|
-
constructor(config, db, linearProvider, codex, wakeDispatcherOrEnqueueIssue, logger, feed) {
|
|
33
|
+
constructor(config, db, linearProvider, codex, wakeDispatcherOrEnqueueIssue, logger, feed, followupClassifier) {
|
|
32
34
|
this.config = config;
|
|
33
35
|
this.db = db;
|
|
34
36
|
this.linearProvider = linearProvider;
|
|
@@ -44,11 +46,13 @@ export class WebhookHandler {
|
|
|
44
46
|
: new WakeDispatcher(db, wakeDispatcherOrEnqueueIssue, () => undefined, logger, feed);
|
|
45
47
|
this.installationHandler = new InstallationWebhookHandler(config, { linearInstallations: db.linearInstallations }, logger, feed);
|
|
46
48
|
this.issueRemovalHandler = new IssueRemovalHandler(db, feed);
|
|
47
|
-
this.
|
|
48
|
-
|
|
49
|
+
this.linearSync = new LinearSessionSync(config, db, linearProvider, logger, feed);
|
|
50
|
+
const intentClassifier = followupClassifier ?? new CodexFollowupIntentClassifier(codex, logger);
|
|
51
|
+
const conversationAdapter = new CodexConversationAdapter(db, codex, this.wakeDispatcher, logger, feed, intentClassifier);
|
|
52
|
+
this.commentWakeHandler = new CommentWakeHandler(db, this.wakeDispatcher, feed, conversationAdapter, (issue, content, options) => this.linearSync.emitActivity(issue, content, options));
|
|
53
|
+
this.agentSessionHandler = new AgentSessionHandler(config, db, linearProvider, codex, this.wakeDispatcher, logger, feed, conversationAdapter);
|
|
49
54
|
this.desiredStageRecorder = new DesiredStageRecorder(db, linearProvider, this.wakeDispatcher, feed);
|
|
50
55
|
this.contextLoader = new WebhookContextLoader(config, linearProvider);
|
|
51
|
-
this.linearSync = new LinearSessionSync(config, db, linearProvider, logger, feed);
|
|
52
56
|
this.dependencyReadinessHandler = new DependencyReadinessHandler(db, this.wakeDispatcher, (projectId, issueId) => this.peekPendingSessionWakeRunType(projectId, issueId));
|
|
53
57
|
}
|
|
54
58
|
async processWebhookEvent(webhookEventId) {
|