patchrelay 0.74.1 → 0.74.3
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 +3 -0
- package/dist/codex-app-server.js +0 -16
- package/dist/issue-session-events.js +4 -0
- package/dist/linear-progress-facts.js +13 -0
- package/dist/linear-session-reporting.js +14 -25
- package/dist/prompting/patchrelay.js +18 -0
- package/dist/run-finalizer.js +94 -57
- package/dist/run-launcher.js +20 -1
- package/dist/run-orchestrator.js +1 -4
- package/dist/run-outcome-summary.js +89 -0
- package/package.json +1 -1
- package/dist/publication-recap.js +0 -113
package/dist/build-info.json
CHANGED
package/dist/cli/data.js
CHANGED
|
@@ -71,6 +71,9 @@ function summarizeRun(run) {
|
|
|
71
71
|
: completionCheck.summary;
|
|
72
72
|
}
|
|
73
73
|
const summary = parseObjectJson(run.summaryJson);
|
|
74
|
+
if (typeof summary?.outcomeSummary === "string" && summary.outcomeSummary.trim()) {
|
|
75
|
+
return summary.outcomeSummary.trim();
|
|
76
|
+
}
|
|
74
77
|
if (typeof summary?.publicationRecapSummary === "string" && summary.publicationRecapSummary.trim()) {
|
|
75
78
|
return summary.publicationRecapSummary.trim();
|
|
76
79
|
}
|
package/dist/codex-app-server.js
CHANGED
|
@@ -9,14 +9,6 @@ const COMPLETION_CHECK_DEVELOPER_INSTRUCTIONS = [
|
|
|
9
9
|
"Use only the prior thread context and the facts in the current prompt.",
|
|
10
10
|
"Return only the requested JSON object.",
|
|
11
11
|
].join("\n");
|
|
12
|
-
const PUBLICATION_RECAP_DEVELOPER_INSTRUCTIONS = [
|
|
13
|
-
"You are PatchRelay's publication recap helper.",
|
|
14
|
-
"This is a read-only follow-up used only to produce one concise Linear-visible summary for a successful run.",
|
|
15
|
-
"Keep reasoning light and concise.",
|
|
16
|
-
"Do not run commands, do not call tools, do not edit files, and do not inspect or modify the repository.",
|
|
17
|
-
"Use only the prior thread context and the facts in the current prompt.",
|
|
18
|
-
"Return only the requested JSON object.",
|
|
19
|
-
].join("\n");
|
|
20
12
|
const ISSUE_TRIAGE_DEVELOPER_INSTRUCTIONS = [
|
|
21
13
|
"You are PatchRelay's issue triage classifier.",
|
|
22
14
|
"This is a read-only preflight step used only to choose the execution shape for one Linear issue.",
|
|
@@ -202,14 +194,6 @@ export class CodexAppServerClient extends EventEmitter {
|
|
|
202
194
|
developerInstructions: COMPLETION_CHECK_DEVELOPER_INSTRUCTIONS,
|
|
203
195
|
});
|
|
204
196
|
}
|
|
205
|
-
async forkThreadForPublicationRecap(threadId) {
|
|
206
|
-
return await this.forkThread(threadId, tmpdir(), {
|
|
207
|
-
approvalPolicy: "never",
|
|
208
|
-
sandboxMode: "read-only",
|
|
209
|
-
reasoningEffort: "low",
|
|
210
|
-
developerInstructions: PUBLICATION_RECAP_DEVELOPER_INSTRUCTIONS,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
197
|
async startTurn(options) {
|
|
214
198
|
const response = (await this.sendRequest("turn/start", {
|
|
215
199
|
threadId: options.threadId,
|
|
@@ -116,6 +116,7 @@ export function deriveSessionWakePlan(issue, events) {
|
|
|
116
116
|
else {
|
|
117
117
|
eventIds.push(event.id);
|
|
118
118
|
}
|
|
119
|
+
Object.assign(context, payload ?? {});
|
|
119
120
|
if (typeof payload?.summary === "string" && payload.summary.trim()) {
|
|
120
121
|
context.completionCheckSummary = payload.summary.trim();
|
|
121
122
|
}
|
|
@@ -187,6 +188,9 @@ export function extractLatestAssistantSummary(run) {
|
|
|
187
188
|
if (run.summaryJson) {
|
|
188
189
|
try {
|
|
189
190
|
const parsed = JSON.parse(run.summaryJson);
|
|
191
|
+
if (typeof parsed.outcomeSummary === "string" && parsed.outcomeSummary.trim()) {
|
|
192
|
+
return sanitizeOperatorFacingText(parsed.outcomeSummary);
|
|
193
|
+
}
|
|
190
194
|
if (typeof parsed.publicationRecapSummary === "string" && parsed.publicationRecapSummary.trim()) {
|
|
191
195
|
return sanitizeOperatorFacingText(parsed.publicationRecapSummary);
|
|
192
196
|
}
|
|
@@ -23,6 +23,9 @@ function deriveProgressFactFromCompletedItem(rawItem, issue) {
|
|
|
23
23
|
return undefined;
|
|
24
24
|
}
|
|
25
25
|
const ephemeralBody = compactOperatorSentence(fullBody) ?? fullBody;
|
|
26
|
+
if (looksLikeOperationalChatter(fullBody)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
26
29
|
if (looksLikeVerification(fullBody)) {
|
|
27
30
|
return {
|
|
28
31
|
kind: "verification_started",
|
|
@@ -131,6 +134,16 @@ function looksLikePublishing(text) {
|
|
|
131
134
|
|| normalized.includes("opening the pr")
|
|
132
135
|
|| normalized.includes("opening pull request");
|
|
133
136
|
}
|
|
137
|
+
function looksLikeOperationalChatter(text) {
|
|
138
|
+
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
139
|
+
const firstPerson = /\b(i('|’)m|i am|i('|’)ll|i will|i need|i’m|i’ll)\b/.test(normalized);
|
|
140
|
+
const operational = /\b(waiting|watching|checking|running|rerunning|pushing|publishing|creating|opening|committing|preparing|verification|publish pass|push)\b/.test(normalized);
|
|
141
|
+
return (firstPerson && operational)
|
|
142
|
+
|| /^(i('|’)m|i am|i('|’)ll|i will|i need|i’m|i’ll)\b/.test(normalized)
|
|
143
|
+
|| /^(continuing|resuming) from\b/.test(normalized)
|
|
144
|
+
|| /\b(i('|’)m|i am|i('|’)ll|i will|i need|i’m|i’ll)\s+(waiting|watching|checking|running|rerunning|pushing|publishing|creating|opening|committing)\b/.test(normalized)
|
|
145
|
+
|| /\b(is|are)\s+(now\s+)?(running|still running|in progress)\b/.test(normalized);
|
|
146
|
+
}
|
|
134
147
|
function compactOperatorSentence(text, maxLength = 160) {
|
|
135
148
|
const sanitized = sanitizeOperatorFacingText(text)?.replace(/\s+/g, " ").trim();
|
|
136
149
|
if (!sanitized) {
|
|
@@ -108,14 +108,13 @@ export function buildRunStartedActivity(runType) {
|
|
|
108
108
|
}
|
|
109
109
|
export function buildReviewRoundStartedActivity(params) {
|
|
110
110
|
const reviewer = params.reviewerName ? ` from @${params.reviewerName}` : "";
|
|
111
|
-
const head = params.headSha ? ` on head ${params.headSha.slice(0, 8)}` : "";
|
|
112
111
|
const comments = params.commentCount !== undefined
|
|
113
112
|
? `; ${params.commentCount} inline comment${params.commentCount === 1 ? "" : "s"} captured`
|
|
114
113
|
: "";
|
|
115
114
|
return {
|
|
116
115
|
type: "action",
|
|
117
116
|
action: "Review round",
|
|
118
|
-
parameter: `${params.round}${reviewer}${
|
|
117
|
+
parameter: `${params.round}${reviewer}${comments}`,
|
|
119
118
|
};
|
|
120
119
|
}
|
|
121
120
|
function formatFactoryState(state) {
|
|
@@ -123,13 +122,13 @@ function formatFactoryState(state) {
|
|
|
123
122
|
}
|
|
124
123
|
export function buildRunCompletedActivity(params) {
|
|
125
124
|
const prLabel = params.prNumber ? `PR #${params.prNumber}` : "the pull request";
|
|
126
|
-
const summary = trimSummary(params.completionSummary);
|
|
125
|
+
const summary = cleanOutcomeSummary(trimSummary(params.completionSummary));
|
|
127
126
|
const detail = summary ? ` ${summary}` : "";
|
|
128
127
|
const steeringSummary = buildSteeringSummary(params.steeringDeliveredCount, params.steeringFailedCount);
|
|
129
128
|
switch (params.runType) {
|
|
130
129
|
case "implementation":
|
|
131
130
|
if (params.postRunState === "pr_open") {
|
|
132
|
-
const body = `${prLabel} opened:${detail || "
|
|
131
|
+
const body = `${prLabel} opened:${detail || " Ready for review."}`;
|
|
133
132
|
return {
|
|
134
133
|
type: "response",
|
|
135
134
|
body: steeringSummary ? `${body}\n\n${steeringSummary}` : body,
|
|
@@ -140,15 +139,10 @@ export function buildRunCompletedActivity(params) {
|
|
|
140
139
|
{
|
|
141
140
|
const lines = [];
|
|
142
141
|
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
142
|
if (steeringSummary)
|
|
146
143
|
lines.push(steeringSummary);
|
|
147
|
-
const
|
|
148
|
-
lines.push("", "Addressed:",
|
|
149
|
-
if (!summary && !params.resultHeadSha) {
|
|
150
|
-
lines[0] = `Updated ${prLabel} to address review feedback.`;
|
|
151
|
-
}
|
|
144
|
+
const addressed = summary ? `- ${summary}` : "- Review feedback addressed.";
|
|
145
|
+
lines.push("", "Addressed:", addressed);
|
|
152
146
|
return {
|
|
153
147
|
type: "response",
|
|
154
148
|
body: lines.join("\n").trim(),
|
|
@@ -196,6 +190,15 @@ export function buildRunCompletedActivity(params) {
|
|
|
196
190
|
}
|
|
197
191
|
}
|
|
198
192
|
}
|
|
193
|
+
function cleanOutcomeSummary(summary) {
|
|
194
|
+
if (!summary)
|
|
195
|
+
return undefined;
|
|
196
|
+
return summary
|
|
197
|
+
.replace(/\s*(?:,?\s*(?:and|then)\s+)?(?:force-)?pushed(?:\s+(?:a\s+)?(?:new\s+)?head|\s+the\s+branch|\s+changes|\s+an?\s+update|\s+the\s+repaired\s+branch)?\.?$/i, ".")
|
|
198
|
+
.replace(/\s*(?:,?\s*(?:and|then)\s+)?published(?:\s+(?:a\s+)?(?:new\s+)?head|\s+the\s+branch|\s+changes|\s+an?\s+update)?\.?$/i, ".")
|
|
199
|
+
.replace(/\.\.+$/, ".")
|
|
200
|
+
.trim();
|
|
201
|
+
}
|
|
199
202
|
function buildSteeringSummary(delivered = 0, failed = 0) {
|
|
200
203
|
if (delivered === 0 && failed === 0)
|
|
201
204
|
return undefined;
|
|
@@ -208,20 +211,6 @@ function buildSteeringSummary(delivered = 0, failed = 0) {
|
|
|
208
211
|
}
|
|
209
212
|
return `Steering: ${parts.join("; ")}.`;
|
|
210
213
|
}
|
|
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
|
-
}
|
|
225
214
|
export function buildRunFailureActivity(runType, reason) {
|
|
226
215
|
const label = formatRunTypeLabel(runType);
|
|
227
216
|
return {
|
|
@@ -421,6 +421,22 @@ function buildFollowUpContextLines(issue, runType, context) {
|
|
|
421
421
|
if (wakeReason === "completion_check_continue" && typeof context?.completionCheckSummary === "string" && context.completionCheckSummary.trim()) {
|
|
422
422
|
lines.push(`Completion check summary: ${context.completionCheckSummary.trim()}`);
|
|
423
423
|
}
|
|
424
|
+
if (context?.preserveDirtyWorktree === true) {
|
|
425
|
+
lines.push("", "Unpublished local work:", "PatchRelay detected that the previous repair turn ended with uncommitted changes in this worktree.", "Do not reset, clean, stash-drop, or otherwise discard the current worktree. Inspect the existing local diff, keep the intended in-scope repair, then commit and push a fresh PR head.");
|
|
426
|
+
if (typeof context.dirtyWorktreeSummary === "string" && context.dirtyWorktreeSummary.trim()) {
|
|
427
|
+
lines.push(`Dirty worktree summary: ${context.dirtyWorktreeSummary.trim()}`);
|
|
428
|
+
}
|
|
429
|
+
const changedPaths = Array.isArray(context.dirtyWorktreeChangedPaths)
|
|
430
|
+
? context.dirtyWorktreeChangedPaths.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
431
|
+
: [];
|
|
432
|
+
if (changedPaths.length > 0) {
|
|
433
|
+
lines.push("Changed paths:");
|
|
434
|
+
changedPaths.slice(0, 12).forEach((entry) => lines.push(`- ${entry}`));
|
|
435
|
+
if (changedPaths.length > 12) {
|
|
436
|
+
lines.push(`- ...and ${changedPaths.length - 12} more`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
424
440
|
if (followUpLines.length > 0) {
|
|
425
441
|
lines.push("", "Recent updates:");
|
|
426
442
|
followUpLines.forEach((line) => lines.push(`- ${line}`));
|
|
@@ -567,6 +583,8 @@ function buildPublicationContract(runType, issueClass, context) {
|
|
|
567
583
|
"Do not run blocking wait commands such as `gh pr checks --watch`, `gh pr view` polling loops, `review-quill pr status --wait`, `merge-steward pr status --wait`, or `gh pr merge` from the agent turn.",
|
|
568
584
|
"PatchRelay receives GitHub webhooks for check, review, and base-branch changes; those events will re-enter automation if more work is needed.",
|
|
569
585
|
"If the issue text asks you to watch CI, wait for approval, or merge after checks pass, treat that as PatchRelay service responsibility rather than agent-turn work.",
|
|
586
|
+
"Keep reactive repairs narrow: do not run TypeScript, lint, full test suites, Playwright, browser UI suites, or screenshot capture unless the human explicitly asks for that evidence, the reviewer specifically asks for a screenshot or visual proof, or the reactive failure is itself from that exact check.",
|
|
587
|
+
"Use code inspection and the smallest directly relevant command only when it materially reduces risk. If the repair is a tiny reviewer-requested edit, commit and push the fresh head without broad local verification.",
|
|
570
588
|
"",
|
|
571
589
|
...(requiresFreshQueueHead
|
|
572
590
|
? [
|
package/dist/run-finalizer.js
CHANGED
|
@@ -4,6 +4,7 @@ import { handleNoPrCompletionCheck } from "./no-pr-completion-check.js";
|
|
|
4
4
|
import { resolveCompletedRunState } from "./run-completion-policy.js";
|
|
5
5
|
import { computeChangeIdentityFromWorktree } from "./change-identity.js";
|
|
6
6
|
import { inspectGitWorktreeStatus, isRepairRunType } from "./git-worktree-status.js";
|
|
7
|
+
import { buildRunOutcomeSummary } from "./run-outcome-summary.js";
|
|
7
8
|
function parseEventJson(eventJson) {
|
|
8
9
|
if (!eventJson)
|
|
9
10
|
return undefined;
|
|
@@ -15,10 +16,12 @@ function parseEventJson(eventJson) {
|
|
|
15
16
|
return undefined;
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
|
-
function buildRunSummaryJson(report,
|
|
19
|
+
function buildRunSummaryJson(report, outcomeSummary) {
|
|
19
20
|
return JSON.stringify({
|
|
20
21
|
latestAssistantMessage: report.assistantMessages.at(-1) ?? null,
|
|
21
|
-
|
|
22
|
+
outcomeSummary: outcomeSummary ?? null,
|
|
23
|
+
// Backward compatibility for older CLI/status readers.
|
|
24
|
+
publicationRecapSummary: outcomeSummary ?? null,
|
|
22
25
|
});
|
|
23
26
|
}
|
|
24
27
|
function summarizePromptDeliveryEvents(events, run) {
|
|
@@ -39,13 +42,6 @@ function summarizePromptDeliveryEvents(events, run) {
|
|
|
39
42
|
}
|
|
40
43
|
return { delivered, failed };
|
|
41
44
|
}
|
|
42
|
-
function shouldGeneratePublicationRecap(runType) {
|
|
43
|
-
return runType === "implementation"
|
|
44
|
-
|| runType === "review_fix"
|
|
45
|
-
|| runType === "branch_upkeep"
|
|
46
|
-
|| runType === "ci_repair"
|
|
47
|
-
|| runType === "queue_repair";
|
|
48
|
-
}
|
|
49
45
|
export class RunFinalizer {
|
|
50
46
|
db;
|
|
51
47
|
logger;
|
|
@@ -57,9 +53,8 @@ export class RunFinalizer {
|
|
|
57
53
|
failRunAndClear;
|
|
58
54
|
completionPolicy;
|
|
59
55
|
completionCheck;
|
|
60
|
-
publicationRecap;
|
|
61
56
|
feed;
|
|
62
|
-
constructor(db, logger, linearSync, wakeDispatcher, withHeldLease, releaseLease, appendWakeEventWithLease, failRunAndClear, completionPolicy, completionCheck,
|
|
57
|
+
constructor(db, logger, linearSync, wakeDispatcher, withHeldLease, releaseLease, appendWakeEventWithLease, failRunAndClear, completionPolicy, completionCheck, feed) {
|
|
63
58
|
this.db = db;
|
|
64
59
|
this.logger = logger;
|
|
65
60
|
this.linearSync = linearSync;
|
|
@@ -70,7 +65,6 @@ export class RunFinalizer {
|
|
|
70
65
|
this.failRunAndClear = failRunAndClear;
|
|
71
66
|
this.completionPolicy = completionPolicy;
|
|
72
67
|
this.completionCheck = completionCheck;
|
|
73
|
-
this.publicationRecap = publicationRecap;
|
|
74
68
|
this.feed = feed;
|
|
75
69
|
}
|
|
76
70
|
buildCompletedRunUpdate(params) {
|
|
@@ -78,7 +72,7 @@ export class RunFinalizer {
|
|
|
78
72
|
status: "completed",
|
|
79
73
|
threadId: params.threadId,
|
|
80
74
|
...(params.completedTurnId ? { turnId: params.completedTurnId } : {}),
|
|
81
|
-
summaryJson: buildRunSummaryJson(params.report, params.
|
|
75
|
+
summaryJson: buildRunSummaryJson(params.report, params.outcomeSummary),
|
|
82
76
|
reportJson: JSON.stringify(params.report),
|
|
83
77
|
};
|
|
84
78
|
}
|
|
@@ -88,7 +82,7 @@ export class RunFinalizer {
|
|
|
88
82
|
.filter((event) => event.consumedByRunId === run.id)
|
|
89
83
|
.at(-1);
|
|
90
84
|
}
|
|
91
|
-
|
|
85
|
+
resolveRunOutcomeFacts(params) {
|
|
92
86
|
const session = this.db.issueSessions.getIssueSession(params.run.projectId, params.run.linearIssueId);
|
|
93
87
|
const facts = {
|
|
94
88
|
...(session?.lastWakeReason ? { wakeReason: session.lastWakeReason } : {}),
|
|
@@ -125,31 +119,16 @@ export class RunFinalizer {
|
|
|
125
119
|
return facts;
|
|
126
120
|
}
|
|
127
121
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
const result = await this.publicationRecap.run({
|
|
134
|
-
issue: params.issue,
|
|
122
|
+
buildOutcomeSummary(params) {
|
|
123
|
+
return buildRunOutcomeSummary({
|
|
124
|
+
runType: params.run.runType,
|
|
125
|
+
facts: this.resolveRunOutcomeFacts({
|
|
135
126
|
run: params.run,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}),
|
|
142
|
-
});
|
|
143
|
-
return result.summary;
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
this.logger.warn({
|
|
147
|
-
runId: params.run.id,
|
|
148
|
-
issueKey: params.issue.issueKey,
|
|
149
|
-
error: error instanceof Error ? error.message : String(error),
|
|
150
|
-
}, "Publication recap failed; falling back to the main run summary");
|
|
151
|
-
return undefined;
|
|
152
|
-
}
|
|
127
|
+
issue: params.issue,
|
|
128
|
+
postRunState: params.postRunState,
|
|
129
|
+
latestAssistantSummary: params.latestAssistantSummary,
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
153
132
|
}
|
|
154
133
|
// Plan §4.2(c): record the identity of the head we just published
|
|
155
134
|
// so subsequent runs can recognize a patch-id-equivalent re-push.
|
|
@@ -274,15 +253,77 @@ export class RunFinalizer {
|
|
|
274
253
|
// exists. Keeping the parameter would be redundant.
|
|
275
254
|
this.clearProgressAndRelease(params.run);
|
|
276
255
|
}
|
|
277
|
-
|
|
256
|
+
inspectDirtyRepairWorktree(run, issue) {
|
|
278
257
|
if (!isRepairRunType(run.runType) || !issue.worktreePath)
|
|
279
258
|
return undefined;
|
|
280
259
|
const status = inspectGitWorktreeStatus(issue.worktreePath);
|
|
281
260
|
if (!status.dirty)
|
|
282
261
|
return undefined;
|
|
283
|
-
return status
|
|
284
|
-
|
|
262
|
+
return status;
|
|
263
|
+
}
|
|
264
|
+
continueDirtyRepairWorktree(params) {
|
|
265
|
+
const message = params.status.summary
|
|
266
|
+
? `Repair run finished with a dirty worktree; ${params.status.summary}`
|
|
285
267
|
: "Repair run finished with a dirty worktree";
|
|
268
|
+
const outcomeSummary = "Repair left unpublished local changes; continuing automatically to publish them.";
|
|
269
|
+
const continued = this.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
270
|
+
this.db.runs.finishRun(params.run.id, this.buildCompletedRunUpdate({
|
|
271
|
+
threadId: params.threadId,
|
|
272
|
+
...(params.completedTurnId ? { completedTurnId: params.completedTurnId } : {}),
|
|
273
|
+
report: params.report,
|
|
274
|
+
outcomeSummary,
|
|
275
|
+
}));
|
|
276
|
+
this.db.issueSessions.upsertIssueWithLease(lease, {
|
|
277
|
+
projectId: params.run.projectId,
|
|
278
|
+
linearIssueId: params.run.linearIssueId,
|
|
279
|
+
activeRunId: null,
|
|
280
|
+
factoryState: "delegated",
|
|
281
|
+
pendingRunType: null,
|
|
282
|
+
pendingRunContextJson: null,
|
|
283
|
+
...(params.run.runType === "ci_repair" && params.issue.ciRepairAttempts > 0
|
|
284
|
+
? { ciRepairAttempts: params.issue.ciRepairAttempts - 1 }
|
|
285
|
+
: {}),
|
|
286
|
+
...(params.run.runType === "queue_repair" && params.issue.queueRepairAttempts > 0
|
|
287
|
+
? { queueRepairAttempts: params.issue.queueRepairAttempts - 1 }
|
|
288
|
+
: {}),
|
|
289
|
+
...((params.run.runType === "review_fix" || params.run.runType === "branch_upkeep") && params.issue.reviewFixAttempts > 0
|
|
290
|
+
? { reviewFixAttempts: params.issue.reviewFixAttempts - 1 }
|
|
291
|
+
: {}),
|
|
292
|
+
});
|
|
293
|
+
return Boolean(this.db.issueSessions.appendIssueSessionEventWithLease(lease, {
|
|
294
|
+
projectId: params.run.projectId,
|
|
295
|
+
linearIssueId: params.run.linearIssueId,
|
|
296
|
+
eventType: "completion_check_continue",
|
|
297
|
+
eventJson: JSON.stringify({
|
|
298
|
+
runType: params.run.runType,
|
|
299
|
+
summary: message,
|
|
300
|
+
preserveDirtyWorktree: true,
|
|
301
|
+
dirtyWorktreeSummary: params.status.summary,
|
|
302
|
+
dirtyWorktreeChangedPaths: params.status.changedPaths,
|
|
303
|
+
dirtyWorktreeMergeInProgress: params.status.mergeInProgress,
|
|
304
|
+
}),
|
|
305
|
+
dedupeKey: `dirty_repair_continue:${params.run.id}`,
|
|
306
|
+
}));
|
|
307
|
+
});
|
|
308
|
+
if (!continued) {
|
|
309
|
+
this.logger.warn({ runId: params.run.id, issueId: params.run.linearIssueId }, "Skipping dirty-repair continuation after losing issue-session lease");
|
|
310
|
+
this.clearProgressAndRelease(params.run);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.publishTurnEvent({
|
|
314
|
+
level: "warn",
|
|
315
|
+
run: params.run,
|
|
316
|
+
issueKey: params.issue.issueKey,
|
|
317
|
+
status: "dirty_repair_continue",
|
|
318
|
+
summary: "Repair left unpublished local changes; continuing automatically",
|
|
319
|
+
detail: message,
|
|
320
|
+
});
|
|
321
|
+
void this.linearSync.emitActivity(params.issue, {
|
|
322
|
+
type: "thought",
|
|
323
|
+
body: "PatchRelay found unpublished repair changes and is continuing automatically to commit and push them.",
|
|
324
|
+
}, { ephemeral: true });
|
|
325
|
+
void this.linearSync.syncSession(params.issue, { activeRunType: params.run.runType });
|
|
326
|
+
this.clearProgressAndRelease(params.run);
|
|
286
327
|
}
|
|
287
328
|
async finalizeCompletedRun(params) {
|
|
288
329
|
const { run, issue, thread, threadId } = params;
|
|
@@ -302,16 +343,15 @@ export class RunFinalizer {
|
|
|
302
343
|
const trackedIssue = this.db.issueToTrackedIssue(issue);
|
|
303
344
|
const report = buildStageReport({ ...run, status: "completed" }, trackedIssue, thread, countEventMethods(this.db.runs.listThreadEvents(run.id)));
|
|
304
345
|
const freshIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
305
|
-
const
|
|
306
|
-
if (
|
|
307
|
-
this.
|
|
308
|
-
this.syncFailureOutcome({
|
|
346
|
+
const dirtyRepairWorktree = this.inspectDirtyRepairWorktree(run, freshIssue);
|
|
347
|
+
if (dirtyRepairWorktree) {
|
|
348
|
+
this.continueDirtyRepairWorktree({
|
|
309
349
|
run,
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
350
|
+
issue: freshIssue,
|
|
351
|
+
status: dirtyRepairWorktree,
|
|
352
|
+
threadId,
|
|
353
|
+
...(params.completedTurnId ? { completedTurnId: params.completedTurnId } : {}),
|
|
354
|
+
report,
|
|
315
355
|
});
|
|
316
356
|
return;
|
|
317
357
|
}
|
|
@@ -388,7 +428,7 @@ export class RunFinalizer {
|
|
|
388
428
|
this.maybeUpdateLastPublishedIdentity(run, refreshedIssue);
|
|
389
429
|
const postRunFollowUp = await this.completionPolicy.resolvePostRunFollowUp(run, refreshedIssue);
|
|
390
430
|
const postRunState = postRunFollowUp?.factoryState ?? resolveCompletedRunState(refreshedIssue, run);
|
|
391
|
-
const
|
|
431
|
+
const outcomeSummary = this.buildOutcomeSummary({
|
|
392
432
|
run,
|
|
393
433
|
issue: refreshedIssue,
|
|
394
434
|
postRunState,
|
|
@@ -399,7 +439,7 @@ export class RunFinalizer {
|
|
|
399
439
|
threadId,
|
|
400
440
|
...(params.completedTurnId ? { completedTurnId: params.completedTurnId } : {}),
|
|
401
441
|
report,
|
|
402
|
-
|
|
442
|
+
outcomeSummary,
|
|
403
443
|
}));
|
|
404
444
|
this.db.issues.upsertIssue({
|
|
405
445
|
projectId: run.projectId,
|
|
@@ -453,12 +493,10 @@ export class RunFinalizer {
|
|
|
453
493
|
summary: params.source === "notification"
|
|
454
494
|
? `Turn completed for ${run.runType}`
|
|
455
495
|
: `Reconciliation: ${run.runType} completed${postRunState ? ` -> ${postRunState}` : ""}`,
|
|
456
|
-
detail:
|
|
496
|
+
detail: outcomeSummary,
|
|
457
497
|
});
|
|
458
498
|
const updatedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? refreshedIssue;
|
|
459
|
-
const completionSummary =
|
|
460
|
-
?? report.assistantMessages.at(-1)?.slice(0, 300)
|
|
461
|
-
?? `${run.runType} completed.`;
|
|
499
|
+
const completionSummary = outcomeSummary;
|
|
462
500
|
const steeringSummary = summarizePromptDeliveryEvents(this.db.issueSessions.listIssueSessionEvents(run.projectId, run.linearIssueId), run);
|
|
463
501
|
const linearActivity = buildRunCompletedActivity({
|
|
464
502
|
runType: run.runType,
|
|
@@ -466,7 +504,6 @@ export class RunFinalizer {
|
|
|
466
504
|
postRunState: updatedIssue.factoryState,
|
|
467
505
|
...(updatedIssue.prNumber !== undefined ? { prNumber: updatedIssue.prNumber } : {}),
|
|
468
506
|
...(run.runType === "review_fix" ? { reviewRound: Math.max(1, updatedIssue.reviewFixAttempts) } : {}),
|
|
469
|
-
...(run.runType === "review_fix" && updatedIssue.prHeadSha ? { resultHeadSha: updatedIssue.prHeadSha } : {}),
|
|
470
507
|
...(steeringSummary.delivered > 0 ? { steeringDeliveredCount: steeringSummary.delivered } : {}),
|
|
471
508
|
...(steeringSummary.failed > 0 ? { steeringFailedCount: steeringSummary.failed } : {}),
|
|
472
509
|
});
|
package/dist/run-launcher.js
CHANGED
|
@@ -41,6 +41,9 @@ export function shouldReuseIssueThread(params) {
|
|
|
41
41
|
return Boolean(params.existingThreadId) && !params.compactThread && params.resumeThread;
|
|
42
42
|
}
|
|
43
43
|
export function shouldFreshenWorktreeBeforeLaunch(params) {
|
|
44
|
+
if (shouldPreserveDirtyWorktreeBeforeLaunch(params)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
44
47
|
if (params.runType === "queue_repair") {
|
|
45
48
|
return false;
|
|
46
49
|
}
|
|
@@ -50,6 +53,13 @@ export function shouldFreshenWorktreeBeforeLaunch(params) {
|
|
|
50
53
|
}
|
|
51
54
|
return true;
|
|
52
55
|
}
|
|
56
|
+
export function shouldPreserveDirtyWorktreeBeforeLaunch(params) {
|
|
57
|
+
return params.effectiveContext?.preserveDirtyWorktree === true
|
|
58
|
+
&& (params.runType === "review_fix"
|
|
59
|
+
|| params.runType === "branch_upkeep"
|
|
60
|
+
|| params.runType === "ci_repair"
|
|
61
|
+
|| params.runType === "queue_repair");
|
|
62
|
+
}
|
|
53
63
|
export class RunLauncher {
|
|
54
64
|
config;
|
|
55
65
|
db;
|
|
@@ -154,7 +164,16 @@ export class RunLauncher {
|
|
|
154
164
|
// process env (GH_CONFIG_DIR + gh credential helper + GIT_AUTHOR/COMMITTER). Nothing
|
|
155
165
|
// is written into the worktree git config, so credentials never leak into interactive
|
|
156
166
|
// shell sessions on the shared clone.
|
|
157
|
-
|
|
167
|
+
const preserveDirtyWorktree = shouldPreserveDirtyWorktreeBeforeLaunch({
|
|
168
|
+
runType: params.runType,
|
|
169
|
+
...(params.effectiveContext ? { effectiveContext: params.effectiveContext } : {}),
|
|
170
|
+
});
|
|
171
|
+
if (preserveDirtyWorktree) {
|
|
172
|
+
this.logger.warn({ issueKey: params.issue.issueKey, runType: params.runType, worktreePath: params.worktreePath }, "Preserving dirty repair worktree for automatic publication continuation");
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
await this.worktreeManager.resetWorktreeToTrackedBranch(params.worktreePath, params.branchName, params.issue, this.logger);
|
|
176
|
+
}
|
|
158
177
|
if (shouldFreshenWorktreeBeforeLaunch({
|
|
159
178
|
runType: params.runType,
|
|
160
179
|
...(params.effectiveContext ? { effectiveContext: params.effectiveContext } : {}),
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { summarizeCurrentThread } from "./run-reporting.js";
|
|
2
2
|
import { buildReviewRoundStartedActivity, buildRunStartedActivity, } from "./linear-session-reporting.js";
|
|
3
3
|
import { CompletionCheckService } from "./completion-check.js";
|
|
4
|
-
import { PublicationRecapService } from "./publication-recap.js";
|
|
5
4
|
import { WorktreeManager } from "./worktree-manager.js";
|
|
6
5
|
import { MergedLinearCompletionReconciler } from "./merged-linear-completion-reconciler.js";
|
|
7
6
|
import { QueueHealthMonitor } from "./queue-health-monitor.js";
|
|
@@ -62,7 +61,6 @@ export class RunOrchestrator {
|
|
|
62
61
|
interruptedRunRecovery;
|
|
63
62
|
runCompletionPolicy;
|
|
64
63
|
completionCheck;
|
|
65
|
-
publicationRecap;
|
|
66
64
|
issueTriage;
|
|
67
65
|
runNotificationHandler;
|
|
68
66
|
runReconciler;
|
|
@@ -125,9 +123,8 @@ export class RunOrchestrator {
|
|
|
125
123
|
this.activeSessionLeases = this.leaseService.activeSessionLeases;
|
|
126
124
|
this.runCompletionPolicy = new RunCompletionPolicy(config, db, logger, this.leasePorts.withHeldLease);
|
|
127
125
|
this.completionCheck = new CompletionCheckService(codex, logger);
|
|
128
|
-
this.publicationRecap = new PublicationRecapService(codex, logger);
|
|
129
126
|
this.issueTriage = new IssueTriageService(codex, logger);
|
|
130
|
-
this.runFinalizer = new RunFinalizer(db, logger, this.linearSync, this.wakeDispatcher, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), this.recoveryPorts.failRunAndClear, this.runCompletionPolicy, this.completionCheck,
|
|
127
|
+
this.runFinalizer = new RunFinalizer(db, logger, this.linearSync, this.wakeDispatcher, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), this.recoveryPorts.failRunAndClear, this.runCompletionPolicy, this.completionCheck, feed);
|
|
131
128
|
this.runLauncher = new RunLauncher(config, db, codex, logger, this.worktreeManager);
|
|
132
129
|
this.runNotificationHandler = new RunNotificationHandler(config, db, logger, this.linearSync, this.runFinalizer, this.threadPorts.readThreadWithRetry, this.leasePorts.withHeldLease, this.leasePorts.heartbeatLease, this.leasePorts.releaseLease, feed, { interruptTurn: (options) => codex.interruptTurn(options) });
|
|
133
130
|
this.runRecovery = new RunRecoveryService(db, logger, this.linearSync, this.leasePorts.withHeldLease, this.leasePorts.getHeldLease, (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), this.leasePorts.releaseLease, (projectId, issueId) => this.enqueueIssue(projectId, issueId), feed);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { sanitizeOperatorFacingText } from "./presentation-text.js";
|
|
2
|
+
export function buildRunOutcomeSummary(params) {
|
|
3
|
+
switch (params.runType) {
|
|
4
|
+
case "implementation":
|
|
5
|
+
return summarizeImplementation(params.facts.postRunState);
|
|
6
|
+
case "review_fix":
|
|
7
|
+
return summarizeReviewFix(params.facts);
|
|
8
|
+
case "ci_repair":
|
|
9
|
+
return summarizeCiRepair(params.facts);
|
|
10
|
+
case "queue_repair":
|
|
11
|
+
return summarizeQueueRepair(params.facts);
|
|
12
|
+
case "branch_upkeep":
|
|
13
|
+
return "Branch updated.";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function summarizeImplementation(postRunState) {
|
|
17
|
+
switch (postRunState) {
|
|
18
|
+
case "awaiting_queue":
|
|
19
|
+
return "Ready for merge.";
|
|
20
|
+
case "done":
|
|
21
|
+
return "Completed.";
|
|
22
|
+
case "pr_open":
|
|
23
|
+
default:
|
|
24
|
+
return "Ready for review.";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function summarizeReviewFix(facts) {
|
|
28
|
+
const concern = summarizeKnownConcern(facts.reviewSummary);
|
|
29
|
+
return concern ? rewriteConcernAsOutcome(concern) : "Review feedback addressed.";
|
|
30
|
+
}
|
|
31
|
+
function summarizeCiRepair(facts) {
|
|
32
|
+
const check = sanitizeOperatorFacingText(facts.failingCheckName)?.replace(/\s+/g, " ").trim();
|
|
33
|
+
if (check) {
|
|
34
|
+
return `${check} fixed.`;
|
|
35
|
+
}
|
|
36
|
+
const concern = summarizeKnownConcern(facts.failureSummary);
|
|
37
|
+
if (concern) {
|
|
38
|
+
return rewriteConcernAsOutcome(concern);
|
|
39
|
+
}
|
|
40
|
+
return "CI fixed.";
|
|
41
|
+
}
|
|
42
|
+
function summarizeQueueRepair(facts) {
|
|
43
|
+
const concern = summarizeKnownConcern(facts.queueIncidentSummary);
|
|
44
|
+
if (concern) {
|
|
45
|
+
return rewriteConcernAsOutcome(concern);
|
|
46
|
+
}
|
|
47
|
+
return "Merge queue issue resolved.";
|
|
48
|
+
}
|
|
49
|
+
function summarizeKnownConcern(value) {
|
|
50
|
+
const sanitized = sanitizeOperatorFacingText(value)?.replace(/\s+/g, " ").trim();
|
|
51
|
+
if (!sanitized) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const stripped = sanitized
|
|
55
|
+
.replace(/^please\s+/i, "")
|
|
56
|
+
.replace(/[.]+$/, "")
|
|
57
|
+
.trim();
|
|
58
|
+
if (!stripped) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return stripped.length <= 80 ? stripped : `${stripped.slice(0, 80).trimEnd()}...`;
|
|
62
|
+
}
|
|
63
|
+
function rewriteConcernAsOutcome(concern) {
|
|
64
|
+
const normalized = concern.trim().replace(/[.]+$/, "");
|
|
65
|
+
const tightened = normalized.match(/^tighten\s+(.+)$/i);
|
|
66
|
+
if (tightened?.[1]) {
|
|
67
|
+
return `${capitalizeFirst(stripLeadingArticle(tightened[1]))} tightened.`;
|
|
68
|
+
}
|
|
69
|
+
const fixed = normalized.match(/^(fix|repair)\s+(.+)$/i);
|
|
70
|
+
if (fixed?.[2]) {
|
|
71
|
+
return `${capitalizeFirst(stripLeadingArticle(fixed[2]))} fixed.`;
|
|
72
|
+
}
|
|
73
|
+
const resolved = normalized.match(/^resolve\s+(.+)$/i);
|
|
74
|
+
if (resolved?.[1]) {
|
|
75
|
+
return `${capitalizeFirst(stripLeadingArticle(resolved[1]))} resolved.`;
|
|
76
|
+
}
|
|
77
|
+
const updated = normalized.match(/^update\s+(.+)$/i);
|
|
78
|
+
if (updated?.[1]) {
|
|
79
|
+
return `${capitalizeFirst(stripLeadingArticle(updated[1]))} updated.`;
|
|
80
|
+
}
|
|
81
|
+
return `${capitalizeFirst(normalized)}.`;
|
|
82
|
+
}
|
|
83
|
+
function capitalizeFirst(value) {
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
return trimmed ? `${trimmed.slice(0, 1).toUpperCase()}${trimmed.slice(1)}` : trimmed;
|
|
86
|
+
}
|
|
87
|
+
function stripLeadingArticle(value) {
|
|
88
|
+
return value.trim().replace(/^the\s+/i, "");
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { getThreadTurns } from "./codex-thread-utils.js";
|
|
2
|
-
import { extractLatestAssistantSummary } from "./issue-session-events.js";
|
|
3
|
-
import { sanitizeOperatorFacingText } from "./presentation-text.js";
|
|
4
|
-
import { extractFirstJsonObject, safeJsonParse } from "./utils.js";
|
|
5
|
-
const PUBLICATION_RECAP_TIMEOUT_MS = 45_000;
|
|
6
|
-
const PUBLICATION_RECAP_POLL_MS = 1_000;
|
|
7
|
-
export class PublicationRecapService {
|
|
8
|
-
codex;
|
|
9
|
-
logger;
|
|
10
|
-
constructor(codex, logger) {
|
|
11
|
-
this.codex = codex;
|
|
12
|
-
this.logger = logger;
|
|
13
|
-
}
|
|
14
|
-
async run(params) {
|
|
15
|
-
const threadId = params.run.threadId;
|
|
16
|
-
if (!threadId) {
|
|
17
|
-
throw new Error("Publication recap could not run because the main thread is missing.");
|
|
18
|
-
}
|
|
19
|
-
const fork = await this.codex.forkThreadForPublicationRecap(threadId);
|
|
20
|
-
const turn = await this.codex.startTurn({
|
|
21
|
-
threadId: fork.id,
|
|
22
|
-
...(fork.cwd ? { cwd: fork.cwd } : {}),
|
|
23
|
-
input: buildPublicationRecapPrompt(params),
|
|
24
|
-
});
|
|
25
|
-
const completedThread = await this.waitForTurn(fork.id, turn.turnId);
|
|
26
|
-
const completedTurn = getThreadTurns(completedThread).find((entry) => entry.id === turn.turnId);
|
|
27
|
-
const latestMessage = completedTurn?.items
|
|
28
|
-
.filter((item) => item.type === "agentMessage")
|
|
29
|
-
.at(-1)?.text;
|
|
30
|
-
const parsed = parsePublicationRecapResult(latestMessage);
|
|
31
|
-
if (!parsed) {
|
|
32
|
-
this.logger.warn({ runId: params.run.id, issueKey: params.issue.issueKey, threadId: fork.id, turnId: turn.turnId }, "Publication recap returned invalid JSON");
|
|
33
|
-
throw new Error("Publication recap returned an invalid result.");
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
...parsed,
|
|
37
|
-
threadId: fork.id,
|
|
38
|
-
turnId: turn.turnId,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
async waitForTurn(threadId, turnId) {
|
|
42
|
-
const deadline = Date.now() + PUBLICATION_RECAP_TIMEOUT_MS;
|
|
43
|
-
while (Date.now() < deadline) {
|
|
44
|
-
const thread = await this.codex.readThread(threadId, true);
|
|
45
|
-
const turn = getThreadTurns(thread).find((entry) => entry.id === turnId);
|
|
46
|
-
if (turn?.status === "completed") {
|
|
47
|
-
return thread;
|
|
48
|
-
}
|
|
49
|
-
if (turn?.status === "failed" || turn?.status === "interrupted") {
|
|
50
|
-
throw new Error(`Publication recap turn ${turnId} ended with status ${turn.status}`);
|
|
51
|
-
}
|
|
52
|
-
await new Promise((resolve) => setTimeout(resolve, PUBLICATION_RECAP_POLL_MS));
|
|
53
|
-
}
|
|
54
|
-
throw new Error(`Publication recap timed out after ${PUBLICATION_RECAP_TIMEOUT_MS}ms`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
function buildPublicationRecapPrompt(params) {
|
|
58
|
-
const latestSummary = params.facts?.latestAssistantSummary
|
|
59
|
-
? sanitizeOperatorFacingText(params.facts.latestAssistantSummary)
|
|
60
|
-
: extractLatestAssistantSummary(params.run);
|
|
61
|
-
return [
|
|
62
|
-
"PatchRelay publication recap",
|
|
63
|
-
"",
|
|
64
|
-
"The main task run succeeded.",
|
|
65
|
-
"This is a read-only follow-up used only to produce one concise Linear-visible recap for that successful run.",
|
|
66
|
-
"Do not run commands, call tools, edit files, or inspect the repository.",
|
|
67
|
-
"Use only the prior thread context and the facts in this prompt.",
|
|
68
|
-
"Return exactly one JSON object and no extra prose.",
|
|
69
|
-
"",
|
|
70
|
-
"Schema:",
|
|
71
|
-
'{',
|
|
72
|
-
' "summary": "one short sentence, max 30 words"',
|
|
73
|
-
'}',
|
|
74
|
-
"",
|
|
75
|
-
"Writing rules:",
|
|
76
|
-
"- Focus on what this session chunk achieved.",
|
|
77
|
-
"- Mention the wake reason only when it makes the change clearer, for example requested changes, a failing CI check, or a merge queue incident.",
|
|
78
|
-
"- Do not list touched files, test commands, branch names, commit SHAs, or internal process details.",
|
|
79
|
-
"- Do not say that you reviewed files or ran checks unless that is the only meaningful achievement.",
|
|
80
|
-
"- For implementation runs, summarize the delivered user-facing or system-facing change.",
|
|
81
|
-
"- For review-fix runs, summarize the concern that was addressed and imply that a newer head was published.",
|
|
82
|
-
"- For CI repair runs, summarize the failure that was fixed if known.",
|
|
83
|
-
"- For queue repair runs, summarize the queue or merge issue that was resolved if known.",
|
|
84
|
-
"",
|
|
85
|
-
"Facts:",
|
|
86
|
-
`- Issue: ${params.issue.issueKey ?? params.issue.linearIssueId}`,
|
|
87
|
-
...(params.issue.title ? [`- Title: ${params.issue.title}`] : []),
|
|
88
|
-
`- Run type: ${params.run.runType}`,
|
|
89
|
-
...(params.facts?.postRunState ? [`- Post-run state: ${params.facts.postRunState}`] : []),
|
|
90
|
-
...(params.facts?.wakeReason ? [`- Wake reason: ${params.facts.wakeReason}`] : []),
|
|
91
|
-
...(params.facts?.prNumber !== undefined ? [`- PR number: ${params.facts.prNumber}`] : []),
|
|
92
|
-
...(params.facts?.reviewerName ? [`- Reviewer: ${params.facts.reviewerName}`] : []),
|
|
93
|
-
...(params.facts?.reviewSummary ? [`- Review summary: ${sanitizeOperatorFacingText(params.facts.reviewSummary)}`] : []),
|
|
94
|
-
...(params.facts?.failingCheckName ? [`- Failing check: ${params.facts.failingCheckName}`] : []),
|
|
95
|
-
...(params.facts?.failureSummary ? [`- Failure summary: ${sanitizeOperatorFacingText(params.facts.failureSummary)}`] : []),
|
|
96
|
-
...(params.facts?.queueIncidentSummary ? [`- Queue incident: ${sanitizeOperatorFacingText(params.facts.queueIncidentSummary)}`] : []),
|
|
97
|
-
...(latestSummary ? [`- Latest assistant summary: ${latestSummary}`] : []),
|
|
98
|
-
...(params.run.failureReason ? [`- Failure reason: ${sanitizeOperatorFacingText(params.run.failureReason)}`] : []),
|
|
99
|
-
...(params.issue.description ? ["", "Issue description:", params.issue.description] : []),
|
|
100
|
-
].join("\n");
|
|
101
|
-
}
|
|
102
|
-
function parsePublicationRecapResult(text) {
|
|
103
|
-
const raw = sanitizeOperatorFacingText(text);
|
|
104
|
-
if (!raw)
|
|
105
|
-
return undefined;
|
|
106
|
-
const candidate = safeJsonParse(raw) ?? safeJsonParse(extractFirstJsonObject(raw) ?? "");
|
|
107
|
-
if (!candidate)
|
|
108
|
-
return undefined;
|
|
109
|
-
const summary = typeof candidate.summary === "string" ? sanitizeOperatorFacingText(candidate.summary) : undefined;
|
|
110
|
-
if (!summary)
|
|
111
|
-
return undefined;
|
|
112
|
-
return { summary };
|
|
113
|
-
}
|