patchrelay 0.51.0 → 0.51.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/main-branch-health-monitor.js +39 -6
- package/dist/publication-recap.js +113 -0
- package/dist/run-finalizer.js +112 -10
- package/dist/run-orchestrator.js +4 -1
- 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
|
}
|
|
@@ -2,6 +2,13 @@ import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
|
2
2
|
import { buildMainRepairBranchName, buildMainRepairDescription, buildMainRepairPromptContext, buildMainRepairTitle, isMainRepairIssue, } from "./main-repair.js";
|
|
3
3
|
import { execCommand } from "./utils.js";
|
|
4
4
|
const MAIN_BRANCH_HEALTH_GRACE_MS = 120_000;
|
|
5
|
+
function isUnhealthyMainConclusion(conclusion) {
|
|
6
|
+
return conclusion === "failure"
|
|
7
|
+
|| conclusion === "timed_out"
|
|
8
|
+
|| conclusion === "cancelled"
|
|
9
|
+
|| conclusion === "action_required"
|
|
10
|
+
|| conclusion === "stale";
|
|
11
|
+
}
|
|
5
12
|
export class MainBranchHealthMonitor {
|
|
6
13
|
db;
|
|
7
14
|
config;
|
|
@@ -36,17 +43,20 @@ export class MainBranchHealthMonitor {
|
|
|
36
43
|
&& issue.factoryState !== "done"
|
|
37
44
|
&& issue.factoryState !== "failed"
|
|
38
45
|
&& issue.factoryState !== "escalated"));
|
|
46
|
+
const summary = await this.readMainBranchFailure(project.github.repoFullName, baseBranch);
|
|
47
|
+
if (!summary) {
|
|
48
|
+
if (existing) {
|
|
49
|
+
this.resolveRecoveredMainRepair(existing);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const protocol = resolveMergeQueueProtocol(project);
|
|
39
54
|
if (existing) {
|
|
40
55
|
const age = Date.now() - Date.parse(existing.updatedAt);
|
|
41
56
|
if (age < MAIN_BRANCH_HEALTH_GRACE_MS) {
|
|
42
57
|
return;
|
|
43
58
|
}
|
|
44
59
|
}
|
|
45
|
-
const summary = await this.readMainBranchFailure(project.github.repoFullName, baseBranch);
|
|
46
|
-
if (!summary) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const protocol = resolveMergeQueueProtocol(project);
|
|
50
60
|
if (existing) {
|
|
51
61
|
this.queueExistingMainRepair(existing, summary, protocol.priorityLabel);
|
|
52
62
|
return;
|
|
@@ -128,6 +138,29 @@ export class MainBranchHealthMonitor {
|
|
|
128
138
|
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
129
139
|
}
|
|
130
140
|
}
|
|
141
|
+
resolveRecoveredMainRepair(issue) {
|
|
142
|
+
if (issue.activeRunId !== undefined)
|
|
143
|
+
return;
|
|
144
|
+
if (issue.prState === "open" || issue.factoryState === "awaiting_queue" || issue.factoryState === "pr_open") {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
148
|
+
this.db.upsertIssue({
|
|
149
|
+
projectId: issue.projectId,
|
|
150
|
+
linearIssueId: issue.linearIssueId,
|
|
151
|
+
factoryState: "done",
|
|
152
|
+
pendingRunType: null,
|
|
153
|
+
});
|
|
154
|
+
this.feed?.publish({
|
|
155
|
+
level: "info",
|
|
156
|
+
kind: "github",
|
|
157
|
+
issueKey: issue.issueKey,
|
|
158
|
+
projectId: issue.projectId,
|
|
159
|
+
stage: "done",
|
|
160
|
+
status: "main_repair_resolved",
|
|
161
|
+
summary: "Closed stale main_repair after main recovered externally",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
131
164
|
async readMainBranchFailure(repoFullName, baseBranch) {
|
|
132
165
|
const { stdout: shaOut } = await execCommand("gh", [
|
|
133
166
|
"api",
|
|
@@ -146,7 +179,7 @@ export class MainBranchHealthMonitor {
|
|
|
146
179
|
], { timeoutMs: 10_000 });
|
|
147
180
|
const runs = JSON.parse(checksOut || "[]");
|
|
148
181
|
const failingChecks = runs
|
|
149
|
-
.filter((run) => run.status === "completed" && run.conclusion
|
|
182
|
+
.filter((run) => run.status === "completed" && isUnhealthyMainConclusion(run.conclusion) && typeof run.name === "string" && run.name.trim())
|
|
150
183
|
.map((run) => ({ name: run.name.trim(), ...(run.details_url ? { url: run.details_url } : {}) }));
|
|
151
184
|
if (failingChecks.length === 0) {
|
|
152
185
|
return undefined;
|
|
@@ -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);
|