patchrelay 0.51.1 → 0.51.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 +16 -0
- package/dist/issue-session-events.js +3 -0
- package/dist/linear-session-sync.js +8 -1
- package/dist/linear-status-comment-sync.js +41 -16
- package/dist/publication-recap.js +113 -0
- package/dist/run-finalizer.js +112 -10
- package/dist/run-orchestrator.js +4 -1
- package/dist/status-note.js +2 -1
- package/dist/webhooks/comment-policy.js +1 -2
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli/data.js
CHANGED
|
@@ -70,6 +70,9 @@ function summarizeRun(run) {
|
|
|
70
70
|
: completionCheck.summary;
|
|
71
71
|
}
|
|
72
72
|
const summary = parseObjectJson(run.summaryJson);
|
|
73
|
+
if (typeof summary?.publicationRecapSummary === "string" && summary.publicationRecapSummary.trim()) {
|
|
74
|
+
return summary.publicationRecapSummary.trim();
|
|
75
|
+
}
|
|
73
76
|
if (typeof summary?.latestAssistantMessage === "string" && summary.latestAssistantMessage.trim()) {
|
|
74
77
|
return summary.latestAssistantMessage.trim();
|
|
75
78
|
}
|
package/dist/codex-app-server.js
CHANGED
|
@@ -9,6 +9,14 @@ 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");
|
|
12
20
|
export function resolveCodexAppServerLaunch(config) {
|
|
13
21
|
if (!config.sourceBashrc) {
|
|
14
22
|
return {
|
|
@@ -210,6 +218,14 @@ export class CodexAppServerClient extends EventEmitter {
|
|
|
210
218
|
developerInstructions: COMPLETION_CHECK_DEVELOPER_INSTRUCTIONS,
|
|
211
219
|
});
|
|
212
220
|
}
|
|
221
|
+
async forkThreadForPublicationRecap(threadId) {
|
|
222
|
+
return await this.forkThread(threadId, tmpdir(), {
|
|
223
|
+
approvalPolicy: "never",
|
|
224
|
+
sandboxMode: "read-only",
|
|
225
|
+
reasoningEffort: "low",
|
|
226
|
+
developerInstructions: PUBLICATION_RECAP_DEVELOPER_INSTRUCTIONS,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
213
229
|
async startTurn(options) {
|
|
214
230
|
const response = (await this.sendRequest("turn/start", {
|
|
215
231
|
threadId: options.threadId,
|
|
@@ -149,6 +149,9 @@ export function extractLatestAssistantSummary(run) {
|
|
|
149
149
|
if (run.summaryJson) {
|
|
150
150
|
try {
|
|
151
151
|
const parsed = JSON.parse(run.summaryJson);
|
|
152
|
+
if (typeof parsed.publicationRecapSummary === "string" && parsed.publicationRecapSummary.trim()) {
|
|
153
|
+
return sanitizeOperatorFacingText(parsed.publicationRecapSummary);
|
|
154
|
+
}
|
|
152
155
|
if (typeof parsed.latestAssistantMessage === "string" && parsed.latestAssistantMessage.trim()) {
|
|
153
156
|
return sanitizeOperatorFacingText(parsed.latestAssistantMessage);
|
|
154
157
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { shouldSyncVisibleIssueComment, syncVisibleStatusComment, } from "./linear-status-comment-sync.js";
|
|
1
|
+
import { collapseVisibleStatusComment, shouldSyncVisibleIssueComment, syncVisibleStatusComment, } from "./linear-status-comment-sync.js";
|
|
2
2
|
import { LinearAgentSessionClient } from "./linear-agent-session-client.js";
|
|
3
3
|
import { LinearProgressReporter } from "./linear-progress-reporter.js";
|
|
4
4
|
import { syncActiveWorkflowState } from "./linear-workflow-state-sync.js";
|
|
@@ -49,6 +49,13 @@ export class LinearSessionSync {
|
|
|
49
49
|
...(options ? { options } : {}),
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
+
else if (syncedIssue.statusCommentId) {
|
|
53
|
+
await collapseVisibleStatusComment({
|
|
54
|
+
issue: syncedIssue,
|
|
55
|
+
linear,
|
|
56
|
+
logger: this.logger,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
52
59
|
}
|
|
53
60
|
catch (error) {
|
|
54
61
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -25,8 +25,40 @@ export async function syncVisibleStatusComment(params) {
|
|
|
25
25
|
logger.warn({ issueKey: issue.issueKey, error: msg }, "Failed to sync Linear status comment");
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
export function shouldSyncVisibleIssueComment(
|
|
29
|
-
|
|
28
|
+
export function shouldSyncVisibleIssueComment(issue, hasAgentSession) {
|
|
29
|
+
if (!hasAgentSession) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (!issue.delegatedToPatchRelay) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (issue.sessionState === "waiting_input" || issue.factoryState === "awaiting_input") {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (issue.sessionState === "failed" || issue.factoryState === "failed" || issue.factoryState === "escalated") {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (issue.factoryState === "done") {
|
|
42
|
+
return issue.prState !== "merged";
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
export async function collapseVisibleStatusComment(params) {
|
|
47
|
+
const { issue, linear, logger } = params;
|
|
48
|
+
if (!issue.statusCommentId) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
await linear.upsertIssueComment({
|
|
53
|
+
issueId: issue.linearIssueId,
|
|
54
|
+
commentId: issue.statusCommentId,
|
|
55
|
+
body: renderCollapsedStatusComment(),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
60
|
+
logger.warn({ issueId: issue.linearIssueId, error: msg }, "Failed to collapse Linear status comment");
|
|
61
|
+
}
|
|
30
62
|
}
|
|
31
63
|
function renderStatusComment(db, issue, trackedIssue, options) {
|
|
32
64
|
const activeRun = issue.activeRunId ? db.runs.getRunById(issue.activeRunId) : undefined;
|
|
@@ -97,21 +129,18 @@ function renderStatusComment(db, issue, trackedIssue, options) {
|
|
|
97
129
|
: `PR: ${linkedLabel}`;
|
|
98
130
|
lines.push("", prLine);
|
|
99
131
|
}
|
|
100
|
-
if (latestRun) {
|
|
101
|
-
lines.push("", `Latest run: ${formatLatestRun(latestRun)}`);
|
|
102
|
-
if (latestRun.failureReason) {
|
|
103
|
-
lines.push("", `Failure: ${latestRun.failureReason}`);
|
|
104
|
-
}
|
|
105
|
-
if (completionCheck && completionCheck.outcome !== "needs_input" && completionCheck.summary !== statusNote) {
|
|
106
|
-
lines.push("", `Completion check: ${completionCheck.summary}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
132
|
if (issue.lastGitHubFailureCheckName && (issue.factoryState === "repairing_ci" || issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure")) {
|
|
110
133
|
lines.push("", `Latest failing check: ${issue.lastGitHubFailureCheckName}`);
|
|
111
134
|
}
|
|
112
|
-
lines.push("", "_PatchRelay updates this comment as it works. Review and merge remain downstream._");
|
|
113
135
|
return lines.join("\n");
|
|
114
136
|
}
|
|
137
|
+
function renderCollapsedStatusComment() {
|
|
138
|
+
return [
|
|
139
|
+
"## PatchRelay status",
|
|
140
|
+
"",
|
|
141
|
+
"Live status is in the agent session and activity feed. This comment is reused only when PatchRelay needs human input or intervention.",
|
|
142
|
+
].join("\n");
|
|
143
|
+
}
|
|
115
144
|
function statusHeadline(issue, activeRunType) {
|
|
116
145
|
const prContext = derivePrDisplayContext(issue);
|
|
117
146
|
if (activeRunType) {
|
|
@@ -193,10 +222,6 @@ function statusHeadline(issue, activeRunType) {
|
|
|
193
222
|
return humanize(issue.factoryState);
|
|
194
223
|
}
|
|
195
224
|
}
|
|
196
|
-
function formatLatestRun(run) {
|
|
197
|
-
const at = run.endedAt ?? run.startedAt;
|
|
198
|
-
return `${humanize(run.runType)} ${run.status} at ${at}`;
|
|
199
|
-
}
|
|
200
225
|
function humanize(value) {
|
|
201
226
|
return value.replaceAll("_", " ");
|
|
202
227
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
}
|
package/dist/run-finalizer.js
CHANGED
|
@@ -2,6 +2,30 @@ import { buildStageReport, countEventMethods } from "./run-reporting.js";
|
|
|
2
2
|
import { buildRunCompletedActivity, buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
3
3
|
import { handleNoPrCompletionCheck } from "./no-pr-completion-check.js";
|
|
4
4
|
import { resolveCompletedRunState } from "./run-completion-policy.js";
|
|
5
|
+
function parseEventJson(eventJson) {
|
|
6
|
+
if (!eventJson)
|
|
7
|
+
return undefined;
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(eventJson);
|
|
10
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function buildRunSummaryJson(report, publicationRecapSummary) {
|
|
17
|
+
return JSON.stringify({
|
|
18
|
+
latestAssistantMessage: report.assistantMessages.at(-1) ?? null,
|
|
19
|
+
publicationRecapSummary: publicationRecapSummary ?? null,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function shouldGeneratePublicationRecap(runType) {
|
|
23
|
+
return runType === "implementation"
|
|
24
|
+
|| runType === "review_fix"
|
|
25
|
+
|| runType === "branch_upkeep"
|
|
26
|
+
|| runType === "ci_repair"
|
|
27
|
+
|| runType === "queue_repair";
|
|
28
|
+
}
|
|
5
29
|
export class RunFinalizer {
|
|
6
30
|
db;
|
|
7
31
|
logger;
|
|
@@ -13,8 +37,9 @@ export class RunFinalizer {
|
|
|
13
37
|
failRunAndClear;
|
|
14
38
|
completionPolicy;
|
|
15
39
|
completionCheck;
|
|
40
|
+
publicationRecap;
|
|
16
41
|
feed;
|
|
17
|
-
constructor(db, logger, linearSync, enqueueIssue, withHeldLease, releaseLease, appendWakeEventWithLease, failRunAndClear, completionPolicy, completionCheck, feed) {
|
|
42
|
+
constructor(db, logger, linearSync, enqueueIssue, withHeldLease, releaseLease, appendWakeEventWithLease, failRunAndClear, completionPolicy, completionCheck, publicationRecap, feed) {
|
|
18
43
|
this.db = db;
|
|
19
44
|
this.logger = logger;
|
|
20
45
|
this.linearSync = linearSync;
|
|
@@ -25,6 +50,7 @@ export class RunFinalizer {
|
|
|
25
50
|
this.failRunAndClear = failRunAndClear;
|
|
26
51
|
this.completionPolicy = completionPolicy;
|
|
27
52
|
this.completionCheck = completionCheck;
|
|
53
|
+
this.publicationRecap = publicationRecap;
|
|
28
54
|
this.feed = feed;
|
|
29
55
|
}
|
|
30
56
|
buildCompletedRunUpdate(params) {
|
|
@@ -32,10 +58,79 @@ export class RunFinalizer {
|
|
|
32
58
|
status: "completed",
|
|
33
59
|
threadId: params.threadId,
|
|
34
60
|
...(params.completedTurnId ? { turnId: params.completedTurnId } : {}),
|
|
35
|
-
summaryJson:
|
|
61
|
+
summaryJson: buildRunSummaryJson(params.report, params.publicationRecapSummary),
|
|
36
62
|
reportJson: JSON.stringify(params.report),
|
|
37
63
|
};
|
|
38
64
|
}
|
|
65
|
+
resolveConsumedWakeEvent(run) {
|
|
66
|
+
return this.db.issueSessions
|
|
67
|
+
.listIssueSessionEvents(run.projectId, run.linearIssueId)
|
|
68
|
+
.filter((event) => event.consumedByRunId === run.id)
|
|
69
|
+
.at(-1);
|
|
70
|
+
}
|
|
71
|
+
resolvePublicationRecapFacts(params) {
|
|
72
|
+
const session = this.db.issueSessions.getIssueSession(params.run.projectId, params.run.linearIssueId);
|
|
73
|
+
const facts = {
|
|
74
|
+
...(session?.lastWakeReason ? { wakeReason: session.lastWakeReason } : {}),
|
|
75
|
+
...(params.postRunState ? { postRunState: params.postRunState } : {}),
|
|
76
|
+
...(params.issue.prNumber !== undefined ? { prNumber: params.issue.prNumber } : {}),
|
|
77
|
+
...(params.latestAssistantSummary ? { latestAssistantSummary: params.latestAssistantSummary } : {}),
|
|
78
|
+
};
|
|
79
|
+
const wakeEvent = this.resolveConsumedWakeEvent(params.run);
|
|
80
|
+
const payload = parseEventJson(wakeEvent?.eventJson);
|
|
81
|
+
if (!wakeEvent || !payload) {
|
|
82
|
+
return facts;
|
|
83
|
+
}
|
|
84
|
+
switch (wakeEvent.eventType) {
|
|
85
|
+
case "review_changes_requested":
|
|
86
|
+
return {
|
|
87
|
+
...facts,
|
|
88
|
+
...(typeof payload.reviewerName === "string" ? { reviewerName: payload.reviewerName } : {}),
|
|
89
|
+
...(typeof payload.reviewBody === "string" ? { reviewSummary: payload.reviewBody } : {}),
|
|
90
|
+
};
|
|
91
|
+
case "settled_red_ci":
|
|
92
|
+
return {
|
|
93
|
+
...facts,
|
|
94
|
+
...(typeof payload.jobName === "string"
|
|
95
|
+
? { failingCheckName: payload.jobName }
|
|
96
|
+
: typeof payload.checkName === "string" ? { failingCheckName: payload.checkName } : {}),
|
|
97
|
+
...(typeof payload.summary === "string" ? { failureSummary: payload.summary } : {}),
|
|
98
|
+
};
|
|
99
|
+
case "merge_steward_incident":
|
|
100
|
+
return {
|
|
101
|
+
...facts,
|
|
102
|
+
...(typeof payload.incidentSummary === "string" ? { queueIncidentSummary: payload.incidentSummary } : {}),
|
|
103
|
+
};
|
|
104
|
+
default:
|
|
105
|
+
return facts;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async generatePublicationRecap(params) {
|
|
109
|
+
if (!this.publicationRecap || !shouldGeneratePublicationRecap(params.run.runType)) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const result = await this.publicationRecap.run({
|
|
114
|
+
issue: params.issue,
|
|
115
|
+
run: params.run,
|
|
116
|
+
facts: this.resolvePublicationRecapFacts({
|
|
117
|
+
run: params.run,
|
|
118
|
+
issue: params.issue,
|
|
119
|
+
postRunState: params.postRunState,
|
|
120
|
+
latestAssistantSummary: params.latestAssistantSummary,
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
return result.summary;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
this.logger.warn({
|
|
127
|
+
runId: params.run.id,
|
|
128
|
+
issueKey: params.issue.issueKey,
|
|
129
|
+
error: error instanceof Error ? error.message : String(error),
|
|
130
|
+
}, "Publication recap failed; falling back to the main run summary");
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
39
134
|
clearProgressAndRelease(run) {
|
|
40
135
|
this.linearSync.clearProgress(run.id);
|
|
41
136
|
this.releaseLease(run.projectId, run.linearIssueId);
|
|
@@ -142,14 +237,19 @@ export class RunFinalizer {
|
|
|
142
237
|
const refreshedIssue = await this.completionPolicy.refreshIssueAfterReactivePublish(run, freshIssue);
|
|
143
238
|
const postRunFollowUp = await this.completionPolicy.resolvePostRunFollowUp(run, refreshedIssue);
|
|
144
239
|
const postRunState = postRunFollowUp?.factoryState ?? resolveCompletedRunState(refreshedIssue, run);
|
|
240
|
+
const publicationRecapSummary = await this.generatePublicationRecap({
|
|
241
|
+
run,
|
|
242
|
+
issue: refreshedIssue,
|
|
243
|
+
postRunState,
|
|
244
|
+
latestAssistantSummary: report.assistantMessages.at(-1),
|
|
245
|
+
});
|
|
145
246
|
const completed = this.withHeldLease(run.projectId, run.linearIssueId, (lease) => {
|
|
146
|
-
this.db.runs.finishRun(run.id, {
|
|
147
|
-
status: "completed",
|
|
247
|
+
this.db.runs.finishRun(run.id, this.buildCompletedRunUpdate({
|
|
148
248
|
threadId,
|
|
149
|
-
...(params.completedTurnId ? {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
});
|
|
249
|
+
...(params.completedTurnId ? { completedTurnId: params.completedTurnId } : {}),
|
|
250
|
+
report,
|
|
251
|
+
publicationRecapSummary,
|
|
252
|
+
}));
|
|
153
253
|
this.db.issues.upsertIssue({
|
|
154
254
|
projectId: run.projectId,
|
|
155
255
|
linearIssueId: run.linearIssueId,
|
|
@@ -204,10 +304,12 @@ export class RunFinalizer {
|
|
|
204
304
|
summary: params.source === "notification"
|
|
205
305
|
? `Turn completed for ${run.runType}`
|
|
206
306
|
: `Reconciliation: ${run.runType} completed${postRunState ? ` -> ${postRunState}` : ""}`,
|
|
207
|
-
detail: report.assistantMessages.at(-1),
|
|
307
|
+
detail: publicationRecapSummary ?? report.assistantMessages.at(-1),
|
|
208
308
|
});
|
|
209
309
|
const updatedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? refreshedIssue;
|
|
210
|
-
const completionSummary =
|
|
310
|
+
const completionSummary = publicationRecapSummary
|
|
311
|
+
?? report.assistantMessages.at(-1)?.slice(0, 300)
|
|
312
|
+
?? `${run.runType} completed.`;
|
|
211
313
|
const linearActivity = buildRunCompletedActivity({
|
|
212
314
|
runType: run.runType,
|
|
213
315
|
completionSummary,
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { summarizeCurrentThread } from "./run-reporting.js";
|
|
2
2
|
import { buildRunStartedActivity, } from "./linear-session-reporting.js";
|
|
3
3
|
import { CompletionCheckService } from "./completion-check.js";
|
|
4
|
+
import { PublicationRecapService } from "./publication-recap.js";
|
|
4
5
|
import { WorktreeManager } from "./worktree-manager.js";
|
|
5
6
|
import { MergedLinearCompletionReconciler } from "./merged-linear-completion-reconciler.js";
|
|
6
7
|
import { MainBranchHealthMonitor } from "./main-branch-health-monitor.js";
|
|
@@ -59,6 +60,7 @@ export class RunOrchestrator {
|
|
|
59
60
|
interruptedRunRecovery;
|
|
60
61
|
runCompletionPolicy;
|
|
61
62
|
completionCheck;
|
|
63
|
+
publicationRecap;
|
|
62
64
|
runNotificationHandler;
|
|
63
65
|
runReconciler;
|
|
64
66
|
mergedLinearCompletionReconciler;
|
|
@@ -95,7 +97,8 @@ export class RunOrchestrator {
|
|
|
95
97
|
this.activeSessionLeases = this.leaseService.activeSessionLeases;
|
|
96
98
|
this.runCompletionPolicy = new RunCompletionPolicy(config, db, logger, this.leasePorts.withHeldLease);
|
|
97
99
|
this.completionCheck = new CompletionCheckService(codex, logger);
|
|
98
|
-
this.
|
|
100
|
+
this.publicationRecap = new PublicationRecapService(codex, logger);
|
|
101
|
+
this.runFinalizer = new RunFinalizer(db, logger, this.linearSync, this.enqueueIssue, 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, this.publicationRecap, feed);
|
|
99
102
|
this.runLauncher = new RunLauncher(config, db, codex, logger, this.worktreeManager);
|
|
100
103
|
this.runNotificationHandler = new RunNotificationHandler(config, db, logger, this.linearSync, this.runFinalizer, this.threadPorts.readThreadWithRetry, this.leasePorts.withHeldLease, this.leasePorts.heartbeatLease, this.leasePorts.releaseLease, feed);
|
|
101
104
|
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);
|
package/dist/status-note.js
CHANGED
|
@@ -36,6 +36,7 @@ export function deriveIssueStatusNote(params) {
|
|
|
36
36
|
? completionCheck.question ?? completionCheck.summary
|
|
37
37
|
: completionCheck?.summary);
|
|
38
38
|
const latestRunNote = clean(extractLatestAssistantSummary(params.latestRun));
|
|
39
|
+
const latestFailureReason = clean(params.latestRun?.failureReason);
|
|
39
40
|
const latestEventNote = clean(eventStatusNote(params.latestEvent));
|
|
40
41
|
const failureSummary = clean(params.failureSummary);
|
|
41
42
|
const waitingReason = clean(params.waitingReason);
|
|
@@ -50,7 +51,7 @@ export function deriveIssueStatusNote(params) {
|
|
|
50
51
|
break;
|
|
51
52
|
case "failed":
|
|
52
53
|
case "escalated":
|
|
53
|
-
note = latestEventNote ?? completionCheckNote ?? failureSummary ?? latestRunNote ?? sessionSummary;
|
|
54
|
+
note = latestEventNote ?? completionCheckNote ?? failureSummary ?? latestFailureReason ?? latestRunNote ?? sessionSummary;
|
|
54
55
|
break;
|
|
55
56
|
case "done":
|
|
56
57
|
note = completionCheckNote ?? sessionSummary ?? latestRunNote ?? failureSummary;
|
|
@@ -2,8 +2,7 @@ export function isInertPatchRelayComment(issue, commentId, body, actorType) {
|
|
|
2
2
|
if (commentId === issue.statusCommentId) {
|
|
3
3
|
return true;
|
|
4
4
|
}
|
|
5
|
-
if (body.startsWith("## PatchRelay status")
|
|
6
|
-
&& body.includes("_PatchRelay updates this comment as it works. Review and merge remain downstream._")) {
|
|
5
|
+
if (body.startsWith("## PatchRelay status")) {
|
|
7
6
|
return true;
|
|
8
7
|
}
|
|
9
8
|
const normalizedActorType = actorType?.trim().toLowerCase();
|