patchrelay 0.12.8 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-session-plan.js +155 -11
- package/dist/agent-session-presentation.js +19 -14
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +1 -0
- package/dist/cli/commands/watch.js +30 -0
- package/dist/cli/help.js +1 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/watch/App.js +43 -0
- package/dist/cli/watch/HelpBar.js +7 -0
- package/dist/cli/watch/IssueDetailView.js +11 -0
- package/dist/cli/watch/IssueListView.js +9 -0
- package/dist/cli/watch/IssueRow.js +42 -0
- package/dist/cli/watch/ItemLine.js +72 -0
- package/dist/cli/watch/StatusBar.js +6 -0
- package/dist/cli/watch/ThreadView.js +20 -0
- package/dist/cli/watch/TurnSection.js +15 -0
- package/dist/cli/watch/use-detail-stream.js +160 -0
- package/dist/cli/watch/use-watch-stream.js +102 -0
- package/dist/cli/watch/watch-state.js +261 -0
- package/dist/codex-app-server.js +1 -4
- package/dist/config.js +2 -0
- package/dist/github-webhook-handler.js +44 -19
- package/dist/http.js +81 -0
- package/dist/issue-query-service.js +17 -2
- package/dist/linear-session-reporting.js +134 -0
- package/dist/run-orchestrator.js +65 -18
- package/dist/run-reporting.js +31 -0
- package/dist/service.js +60 -0
- package/dist/webhook-handler.js +49 -28
- package/package.json +4 -1
package/dist/http.js
CHANGED
|
@@ -330,6 +330,49 @@ export async function buildHttpServer(config, service, logger) {
|
|
|
330
330
|
reply.raw.on("error", cleanup);
|
|
331
331
|
request.raw.on("close", cleanup);
|
|
332
332
|
});
|
|
333
|
+
app.get("/api/watch", async (request, reply) => {
|
|
334
|
+
reply.hijack();
|
|
335
|
+
reply.raw.writeHead(200, {
|
|
336
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
337
|
+
"cache-control": "no-cache, no-transform",
|
|
338
|
+
connection: "keep-alive",
|
|
339
|
+
"x-accel-buffering": "no",
|
|
340
|
+
});
|
|
341
|
+
const writeSse = (eventType, data) => {
|
|
342
|
+
reply.raw.write(`event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
343
|
+
};
|
|
344
|
+
// Send initial issue snapshot
|
|
345
|
+
writeSse("issues", service.listTrackedIssues());
|
|
346
|
+
// Stream operator feed events
|
|
347
|
+
const issueFilter = getQueryParam(request, "issue");
|
|
348
|
+
const unsubscribeFeed = service.subscribeOperatorFeed((event) => {
|
|
349
|
+
if (issueFilter && event.issueKey !== issueFilter) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
writeSse("feed", event);
|
|
353
|
+
});
|
|
354
|
+
// When filtered to a specific issue, also stream codex notifications
|
|
355
|
+
const unsubscribeCodex = issueFilter
|
|
356
|
+
? service.subscribeCodexNotifications((event) => {
|
|
357
|
+
if (event.issueKey !== issueFilter) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
writeSse("codex", { method: event.method, params: event.params });
|
|
361
|
+
})
|
|
362
|
+
: undefined;
|
|
363
|
+
const cleanup = () => {
|
|
364
|
+
clearInterval(keepAlive);
|
|
365
|
+
unsubscribeFeed();
|
|
366
|
+
unsubscribeCodex?.();
|
|
367
|
+
if (!reply.raw.destroyed)
|
|
368
|
+
reply.raw.end();
|
|
369
|
+
};
|
|
370
|
+
const keepAlive = setInterval(() => {
|
|
371
|
+
reply.raw.write(": keepalive\n\n");
|
|
372
|
+
}, 15000);
|
|
373
|
+
reply.raw.on("error", cleanup);
|
|
374
|
+
request.raw.on("close", cleanup);
|
|
375
|
+
});
|
|
333
376
|
app.get("/api/installations", async (_request, reply) => {
|
|
334
377
|
return reply.send({ ok: true, installations: service.listLinearInstallations() });
|
|
335
378
|
});
|
|
@@ -477,10 +520,25 @@ function renderAgentSessionStatusErrorPage(message) {
|
|
|
477
520
|
function renderAgentSessionStatusPage(params) {
|
|
478
521
|
const issueTitle = params.sessionStatus.issue.title ?? params.sessionStatus.issue.issueKey ?? params.issueKey;
|
|
479
522
|
const issueUrl = params.sessionStatus.issue.issueUrl;
|
|
523
|
+
const prUrl = params.sessionStatus.issue.prUrl;
|
|
524
|
+
const prLabel = params.sessionStatus.issue.prNumber ? `#${params.sessionStatus.issue.prNumber}` : undefined;
|
|
480
525
|
const activeStage = formatStageChip(params.sessionStatus.activeRun);
|
|
481
526
|
const latestStage = formatStageChip(params.sessionStatus.latestRun);
|
|
482
527
|
const threadInfo = formatThread(params.sessionStatus.liveThread);
|
|
483
528
|
const stagesRows = params.sessionStatus.runs.slice(-8).map((entry) => formatStageRow(entry.run)).join("");
|
|
529
|
+
const latestAgentMessage = params.sessionStatus.liveThread?.latestAgentMessage ?? params.sessionStatus.latestReportSummary?.latestAssistantMessage ?? "No agent summary yet.";
|
|
530
|
+
const latestPlan = params.sessionStatus.liveThread?.latestPlan ?? "No live plan available.";
|
|
531
|
+
const activeCommand = params.sessionStatus.liveThread?.activeCommand ?? "idle";
|
|
532
|
+
const commandCount = params.sessionStatus.liveThread?.commandCount ?? params.sessionStatus.latestReportSummary?.commandCount ?? 0;
|
|
533
|
+
const fileChangeCount = params.sessionStatus.liveThread?.fileChangeCount ?? params.sessionStatus.latestReportSummary?.fileChangeCount ?? 0;
|
|
534
|
+
const toolCallCount = params.sessionStatus.liveThread?.toolCallCount ?? params.sessionStatus.latestReportSummary?.toolCallCount ?? 0;
|
|
535
|
+
const factoryState = params.sessionStatus.issue.factoryState ?? "unknown";
|
|
536
|
+
const linearState = params.sessionStatus.issue.currentLinearState ?? "unknown";
|
|
537
|
+
const prState = params.sessionStatus.issue.prState ?? "unknown";
|
|
538
|
+
const reviewState = params.sessionStatus.issue.prReviewState ?? "unknown";
|
|
539
|
+
const checkState = params.sessionStatus.issue.prCheckStatus ?? "unknown";
|
|
540
|
+
const ciAttempts = params.sessionStatus.issue.ciRepairAttempts ?? 0;
|
|
541
|
+
const queueAttempts = params.sessionStatus.issue.queueRepairAttempts ?? 0;
|
|
484
542
|
return `<!doctype html>
|
|
485
543
|
<html lang="en">
|
|
486
544
|
<head>
|
|
@@ -538,11 +596,34 @@ function renderAgentSessionStatusPage(params) {
|
|
|
538
596
|
<h1>${escapeHtml(issueTitle)}</h1>
|
|
539
597
|
<p>PatchRelay read-only agent session status for <code>${escapeHtml(params.issueKey)}</code>.</p>
|
|
540
598
|
${issueUrl ? `<p><a href="${escapeHtml(issueUrl)}" target="_blank" rel="noopener noreferrer">Open issue in Linear</a></p>` : ""}
|
|
599
|
+
${prUrl ? `<p><a href="${escapeHtml(prUrl)}" target="_blank" rel="noopener noreferrer">Open pull request ${escapeHtml(prLabel ?? "")}</a></p>` : ""}
|
|
541
600
|
<div class="chips">
|
|
601
|
+
<span class="chip"><strong>Factory:</strong> <code>${escapeHtml(factoryState)}</code></span>
|
|
602
|
+
<span class="chip"><strong>Linear:</strong> <code>${escapeHtml(linearState)}</code></span>
|
|
542
603
|
<span class="chip"><strong>Active:</strong> ${activeStage}</span>
|
|
543
604
|
<span class="chip"><strong>Latest:</strong> ${latestStage}</span>
|
|
544
605
|
<span class="chip"><strong>Thread:</strong> ${threadInfo}</span>
|
|
545
606
|
</div>
|
|
607
|
+
<div class="section">
|
|
608
|
+
<h2>Current View</h2>
|
|
609
|
+
<table>
|
|
610
|
+
<tbody>
|
|
611
|
+
<tr><th>Pull request</th><td>${escapeHtml(prLabel ?? "none")} (${escapeHtml(prState)})</td></tr>
|
|
612
|
+
<tr><th>Review</th><td>${escapeHtml(reviewState)}</td></tr>
|
|
613
|
+
<tr><th>Checks</th><td>${escapeHtml(checkState)}</td></tr>
|
|
614
|
+
<tr><th>Latest plan</th><td>${escapeHtml(latestPlan)}</td></tr>
|
|
615
|
+
<tr><th>Active command</th><td><code>${escapeHtml(activeCommand)}</code></td></tr>
|
|
616
|
+
<tr><th>Latest summary</th><td>${escapeHtml(latestAgentMessage)}</td></tr>
|
|
617
|
+
</tbody>
|
|
618
|
+
</table>
|
|
619
|
+
</div>
|
|
620
|
+
<div class="chips">
|
|
621
|
+
<span class="chip"><strong>Commands:</strong> ${escapeHtml(String(commandCount))}</span>
|
|
622
|
+
<span class="chip"><strong>File changes:</strong> ${escapeHtml(String(fileChangeCount))}</span>
|
|
623
|
+
<span class="chip"><strong>Tool calls:</strong> ${escapeHtml(String(toolCallCount))}</span>
|
|
624
|
+
<span class="chip"><strong>CI repairs:</strong> ${escapeHtml(String(ciAttempts))}</span>
|
|
625
|
+
<span class="chip"><strong>Queue repairs:</strong> ${escapeHtml(String(queueAttempts))}</span>
|
|
626
|
+
</div>
|
|
546
627
|
<div class="section">
|
|
547
628
|
<h2>Recent Stages</h2>
|
|
548
629
|
<table>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { summarizeCurrentThread } from "./run-reporting.js";
|
|
1
|
+
import { extractStageSummary, summarizeCurrentThread } from "./run-reporting.js";
|
|
2
2
|
import { safeJsonParse } from "./utils.js";
|
|
3
3
|
export class IssueQueryService {
|
|
4
4
|
db;
|
|
@@ -65,12 +65,27 @@ export class IssueQueryService {
|
|
|
65
65
|
const overview = await this.getIssueOverview(issueKey);
|
|
66
66
|
if (!overview)
|
|
67
67
|
return undefined;
|
|
68
|
+
const issueRecord = this.db.getIssueByKey(issueKey);
|
|
68
69
|
const report = await this.getIssueReport(issueKey);
|
|
70
|
+
const latestRunReport = report?.runs.at(-1)?.report;
|
|
69
71
|
return {
|
|
70
|
-
issue:
|
|
72
|
+
issue: {
|
|
73
|
+
issueKey: overview.issue.issueKey,
|
|
74
|
+
title: overview.issue.title,
|
|
75
|
+
issueUrl: overview.issue.issueUrl,
|
|
76
|
+
currentLinearState: overview.issue.currentLinearState,
|
|
77
|
+
factoryState: overview.issue.factoryState,
|
|
78
|
+
...(issueRecord?.prNumber !== undefined ? { prNumber: issueRecord.prNumber } : {}),
|
|
79
|
+
...(issueRecord?.prUrl ? { prUrl: issueRecord.prUrl } : {}),
|
|
80
|
+
...(issueRecord?.prState ? { prState: issueRecord.prState } : {}),
|
|
81
|
+
...(issueRecord?.prReviewState ? { prReviewState: issueRecord.prReviewState } : {}),
|
|
82
|
+
...(issueRecord?.prCheckStatus ? { prCheckStatus: issueRecord.prCheckStatus } : {}),
|
|
83
|
+
...(issueRecord ? { ciRepairAttempts: issueRecord.ciRepairAttempts, queueRepairAttempts: issueRecord.queueRepairAttempts } : {}),
|
|
84
|
+
},
|
|
71
85
|
...(overview.activeRun ? { activeRun: overview.activeRun } : {}),
|
|
72
86
|
...(overview.latestRun ? { latestRun: overview.latestRun } : {}),
|
|
73
87
|
...(overview.liveThread ? { liveThread: overview.liveThread } : {}),
|
|
88
|
+
...(latestRunReport ? { latestReportSummary: extractStageSummary(latestRunReport) } : {}),
|
|
74
89
|
runs: report?.runs ?? [],
|
|
75
90
|
generatedAt: new Date().toISOString(),
|
|
76
91
|
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { formatRunTypeLabel } from "./agent-session-plan.js";
|
|
2
|
+
function lowerRunTypeLabel(runType) {
|
|
3
|
+
return formatRunTypeLabel(runType).toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
function trimSummary(summary, maxLength = 300) {
|
|
6
|
+
const value = summary?.trim();
|
|
7
|
+
if (!value) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
return value.length <= maxLength ? value : `${value.slice(0, maxLength).trimEnd()}...`;
|
|
11
|
+
}
|
|
12
|
+
function describeNextState(state, prNumber) {
|
|
13
|
+
const prLabel = prNumber ? `PR #${prNumber}` : "the pull request";
|
|
14
|
+
switch (state) {
|
|
15
|
+
case "pr_open":
|
|
16
|
+
case "awaiting_review":
|
|
17
|
+
return `${prLabel} is ready for review.`;
|
|
18
|
+
case "awaiting_queue":
|
|
19
|
+
return `${prLabel} is approved and back in the merge flow.`;
|
|
20
|
+
case "done":
|
|
21
|
+
return `${prLabel} has merged.`;
|
|
22
|
+
case "awaiting_input":
|
|
23
|
+
return "PatchRelay is waiting for guidance before continuing.";
|
|
24
|
+
case "failed":
|
|
25
|
+
return "PatchRelay needs help to recover this workflow.";
|
|
26
|
+
default:
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function buildDelegationThought(runType, source = "delegation") {
|
|
31
|
+
const sourceText = source === "prompt" ? "latest instructions" : "delegation";
|
|
32
|
+
return {
|
|
33
|
+
type: "thought",
|
|
34
|
+
body: `PatchRelay received the ${sourceText} and is preparing the ${lowerRunTypeLabel(runType)} workflow.`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function buildAlreadyRunningThought(runType) {
|
|
38
|
+
return {
|
|
39
|
+
type: "thought",
|
|
40
|
+
body: `PatchRelay is already working on the ${lowerRunTypeLabel(runType)} workflow.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function buildPromptDeliveredThought(runType) {
|
|
44
|
+
return {
|
|
45
|
+
type: "thought",
|
|
46
|
+
body: `PatchRelay routed your latest instructions into the active ${lowerRunTypeLabel(runType)} workflow.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function buildRunStartedActivity(runType) {
|
|
50
|
+
switch (runType) {
|
|
51
|
+
case "review_fix":
|
|
52
|
+
return { type: "action", action: "Addressing", parameter: "review feedback" };
|
|
53
|
+
case "ci_repair":
|
|
54
|
+
return { type: "action", action: "Repairing", parameter: "failing CI checks" };
|
|
55
|
+
case "queue_repair":
|
|
56
|
+
return { type: "action", action: "Repairing", parameter: "merge queue failure" };
|
|
57
|
+
case "implementation":
|
|
58
|
+
default:
|
|
59
|
+
return { type: "action", action: "Implementing", parameter: "requested change" };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function buildRunCompletedActivity(params) {
|
|
63
|
+
const label = formatRunTypeLabel(params.runType);
|
|
64
|
+
const nextState = describeNextState(params.postRunState, params.prNumber);
|
|
65
|
+
const summary = trimSummary(params.completionSummary);
|
|
66
|
+
const lines = [`${label} completed.`];
|
|
67
|
+
if (nextState) {
|
|
68
|
+
lines.push("", nextState);
|
|
69
|
+
}
|
|
70
|
+
if (summary) {
|
|
71
|
+
lines.push("", summary);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
type: "response",
|
|
75
|
+
body: lines.join("\n"),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function buildRunFailureActivity(runType, reason) {
|
|
79
|
+
const label = formatRunTypeLabel(runType);
|
|
80
|
+
return {
|
|
81
|
+
type: "error",
|
|
82
|
+
body: reason ? `${label} failed.\n\n${reason}` : `${label} failed.`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function buildGitHubStateActivity(newState, event) {
|
|
86
|
+
switch (newState) {
|
|
87
|
+
case "pr_open": {
|
|
88
|
+
const parts = [`PR #${event.prNumber ?? "?"} is open and ready for review.`];
|
|
89
|
+
if (event.prUrl) {
|
|
90
|
+
parts.push("", event.prUrl);
|
|
91
|
+
}
|
|
92
|
+
return { type: "response", body: parts.join("\n") };
|
|
93
|
+
}
|
|
94
|
+
case "awaiting_queue":
|
|
95
|
+
return { type: "response", body: "Review approved. PatchRelay is moving the PR toward merge." };
|
|
96
|
+
case "changes_requested":
|
|
97
|
+
return {
|
|
98
|
+
type: "action",
|
|
99
|
+
action: "Addressing",
|
|
100
|
+
parameter: event.reviewerName ? `review feedback from ${event.reviewerName}` : "review feedback",
|
|
101
|
+
};
|
|
102
|
+
case "repairing_ci":
|
|
103
|
+
return {
|
|
104
|
+
type: "action",
|
|
105
|
+
action: "Repairing",
|
|
106
|
+
parameter: event.checkName ? `CI failure: ${event.checkName}` : "failing CI checks",
|
|
107
|
+
};
|
|
108
|
+
case "repairing_queue":
|
|
109
|
+
return {
|
|
110
|
+
type: "action",
|
|
111
|
+
action: "Repairing",
|
|
112
|
+
parameter: "merge queue validation",
|
|
113
|
+
};
|
|
114
|
+
case "done":
|
|
115
|
+
return { type: "response", body: `PR merged.${event.prNumber ? ` PR #${event.prNumber}` : ""}` };
|
|
116
|
+
case "failed":
|
|
117
|
+
return { type: "error", body: "The pull request was closed without merging." };
|
|
118
|
+
default:
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export function summarizeIssueStateForLinear(issue) {
|
|
123
|
+
switch (issue.factoryState) {
|
|
124
|
+
case "awaiting_review":
|
|
125
|
+
case "pr_open":
|
|
126
|
+
return issue.prNumber ? `PR #${issue.prNumber} is awaiting review.` : "Awaiting review.";
|
|
127
|
+
case "awaiting_queue":
|
|
128
|
+
return issue.prNumber ? `PR #${issue.prNumber} is approved and awaiting merge.` : "Approved and awaiting merge.";
|
|
129
|
+
case "done":
|
|
130
|
+
return issue.prNumber ? `PR #${issue.prNumber} has merged.` : "Change merged.";
|
|
131
|
+
default:
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -2,8 +2,10 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ACTIVE_RUN_STATES } from "./factory-state.js";
|
|
4
4
|
import { buildHookEnv, runProjectHook } from "./hook-runner.js";
|
|
5
|
-
import {
|
|
5
|
+
import { buildAgentSessionPlanForIssue, } from "./agent-session-plan.js";
|
|
6
6
|
import { buildStageReport, countEventMethods, extractTurnId, resolveRunCompletionStatus, summarizeCurrentThread, } from "./run-reporting.js";
|
|
7
|
+
import { buildRunCompletedActivity, buildRunFailureActivity, buildRunStartedActivity, } from "./linear-session-reporting.js";
|
|
8
|
+
import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
|
|
7
9
|
import { WorktreeManager } from "./worktree-manager.js";
|
|
8
10
|
import { resolveAuthoritativeLinearStopState } from "./linear-workflow.js";
|
|
9
11
|
import { execCommand } from "./utils.js";
|
|
@@ -15,6 +17,9 @@ function slugify(value) {
|
|
|
15
17
|
function sanitizePathSegment(value) {
|
|
16
18
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
17
19
|
}
|
|
20
|
+
function lowerCaseFirst(value) {
|
|
21
|
+
return value ? `${value.slice(0, 1).toLowerCase()}${value.slice(1)}` : value;
|
|
22
|
+
}
|
|
18
23
|
const WORKFLOW_FILES = {
|
|
19
24
|
implementation: "IMPLEMENTATION_WORKFLOW.md",
|
|
20
25
|
review_fix: "REVIEW_WORKFLOW.md",
|
|
@@ -36,6 +41,14 @@ function buildRunPrompt(issue, runType, repoPath, context) {
|
|
|
36
41
|
issue.prNumber ? `PR: #${issue.prNumber}` : undefined,
|
|
37
42
|
"",
|
|
38
43
|
].filter(Boolean);
|
|
44
|
+
const promptContext = typeof context?.promptContext === "string" ? context.promptContext.trim() : "";
|
|
45
|
+
const latestPrompt = typeof context?.promptBody === "string" ? context.promptBody.trim() : "";
|
|
46
|
+
if (promptContext) {
|
|
47
|
+
lines.push("## Linear Session Context", "", promptContext, "");
|
|
48
|
+
}
|
|
49
|
+
if (latestPrompt) {
|
|
50
|
+
lines.push("## Latest Human Instruction", "", latestPrompt, "");
|
|
51
|
+
}
|
|
39
52
|
// Add run-type-specific context for reactive runs
|
|
40
53
|
switch (runType) {
|
|
41
54
|
case "ci_repair":
|
|
@@ -210,16 +223,17 @@ export class RunOrchestrator {
|
|
|
210
223
|
factoryState: "failed",
|
|
211
224
|
});
|
|
212
225
|
this.logger.error({ issueKey: issue.issueKey, runType, error: message }, `Failed to launch ${runType} run`);
|
|
213
|
-
|
|
214
|
-
void this.
|
|
226
|
+
const failedIssue = this.db.getIssue(item.projectId, item.issueId) ?? issue;
|
|
227
|
+
void this.emitLinearActivity(failedIssue, buildRunFailureActivity(runType, `Failed to start ${lowerCaseFirst(message)}`));
|
|
228
|
+
void this.syncLinearSession(failedIssue, { activeRunType: runType });
|
|
215
229
|
throw error;
|
|
216
230
|
}
|
|
217
231
|
this.db.updateRunThread(run.id, { threadId, turnId });
|
|
218
232
|
this.logger.info({ issueKey: issue.issueKey, runType, threadId, turnId }, `Started ${runType} run`);
|
|
219
233
|
// Emit Linear activity + plan
|
|
220
234
|
const freshIssue = this.db.getIssue(item.projectId, item.issueId) ?? issue;
|
|
221
|
-
void this.emitLinearActivity(freshIssue,
|
|
222
|
-
void this.
|
|
235
|
+
void this.emitLinearActivity(freshIssue, buildRunStartedActivity(runType));
|
|
236
|
+
void this.syncLinearSession(freshIssue, { activeRunType: runType });
|
|
223
237
|
}
|
|
224
238
|
// ─── Notification handler ─────────────────────────────────────────
|
|
225
239
|
async handleCodexNotification(notification) {
|
|
@@ -269,8 +283,9 @@ export class RunOrchestrator {
|
|
|
269
283
|
status: "failed",
|
|
270
284
|
summary: `Turn failed for ${run.runType}`,
|
|
271
285
|
});
|
|
272
|
-
|
|
273
|
-
void this.
|
|
286
|
+
const failedIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
287
|
+
void this.emitLinearActivity(failedIssue, buildRunFailureActivity(run.runType));
|
|
288
|
+
void this.syncLinearSession(failedIssue, { activeRunType: run.runType });
|
|
274
289
|
return;
|
|
275
290
|
}
|
|
276
291
|
// Complete the run
|
|
@@ -323,10 +338,15 @@ export class RunOrchestrator {
|
|
|
323
338
|
detail: summarizeCurrentThread(thread).latestAgentMessage,
|
|
324
339
|
});
|
|
325
340
|
// Emit Linear completion activity + plan
|
|
341
|
+
const updatedIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
326
342
|
const completionSummary = report.assistantMessages.at(-1)?.slice(0, 300) ?? `${run.runType} completed.`;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
343
|
+
void this.emitLinearActivity(updatedIssue, buildRunCompletedActivity({
|
|
344
|
+
runType: run.runType,
|
|
345
|
+
completionSummary,
|
|
346
|
+
postRunState: updatedIssue.factoryState,
|
|
347
|
+
...(updatedIssue.prNumber !== undefined ? { prNumber: updatedIssue.prNumber } : {}),
|
|
348
|
+
}));
|
|
349
|
+
void this.syncLinearSession(updatedIssue);
|
|
330
350
|
}
|
|
331
351
|
// ─── Active status for query ──────────────────────────────────────
|
|
332
352
|
async getActiveRunStatus(issueKey) {
|
|
@@ -471,7 +491,9 @@ export class RunOrchestrator {
|
|
|
471
491
|
if (latestTurn?.status === "interrupted") {
|
|
472
492
|
this.logger.warn({ issueKey: issue.issueKey, runType: run.runType, threadId: run.threadId }, "Run has interrupted turn — marking as failed");
|
|
473
493
|
this.failRunAndClear(run, "Codex turn was interrupted");
|
|
474
|
-
|
|
494
|
+
const failedIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
495
|
+
void this.emitLinearActivity(failedIssue, buildRunFailureActivity(run.runType, "The Codex turn was interrupted."));
|
|
496
|
+
void this.syncLinearSession(failedIssue, { activeRunType: run.runType });
|
|
475
497
|
return;
|
|
476
498
|
}
|
|
477
499
|
// Handle completed turn discovered during reconciliation
|
|
@@ -513,6 +535,12 @@ export class RunOrchestrator {
|
|
|
513
535
|
status: "escalated",
|
|
514
536
|
summary: `Escalated: ${reason}`,
|
|
515
537
|
});
|
|
538
|
+
const escalatedIssue = this.db.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
539
|
+
void this.emitLinearActivity(escalatedIssue, {
|
|
540
|
+
type: "error",
|
|
541
|
+
body: `PatchRelay needs human help to continue.\n\n${reason}`,
|
|
542
|
+
});
|
|
543
|
+
void this.syncLinearSession(escalatedIssue);
|
|
516
544
|
}
|
|
517
545
|
failRunAndClear(run, message) {
|
|
518
546
|
this.db.transaction(() => {
|
|
@@ -525,34 +553,53 @@ export class RunOrchestrator {
|
|
|
525
553
|
});
|
|
526
554
|
});
|
|
527
555
|
}
|
|
528
|
-
async emitLinearActivity(issue,
|
|
556
|
+
async emitLinearActivity(issue, content, options) {
|
|
529
557
|
if (!issue.agentSessionId)
|
|
530
558
|
return;
|
|
531
559
|
try {
|
|
532
560
|
const linear = await this.linearProvider.forProject(issue.projectId);
|
|
533
561
|
if (!linear)
|
|
534
562
|
return;
|
|
563
|
+
const allowEphemeral = content.type === "thought" || content.type === "action";
|
|
535
564
|
await linear.createAgentActivity({
|
|
536
565
|
agentSessionId: issue.agentSessionId,
|
|
537
|
-
content
|
|
538
|
-
...(options?.ephemeral ? { ephemeral: true } : {}),
|
|
566
|
+
content,
|
|
567
|
+
...(options?.ephemeral && allowEphemeral ? { ephemeral: true } : {}),
|
|
539
568
|
});
|
|
540
569
|
}
|
|
541
570
|
catch (error) {
|
|
542
|
-
|
|
571
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
572
|
+
this.logger.warn({ issueKey: issue.issueKey, type: content.type, error: msg }, "Failed to emit Linear activity");
|
|
573
|
+
this.feed?.publish({
|
|
574
|
+
level: "warn",
|
|
575
|
+
kind: "linear",
|
|
576
|
+
issueKey: issue.issueKey,
|
|
577
|
+
projectId: issue.projectId,
|
|
578
|
+
status: "linear_error",
|
|
579
|
+
summary: `Linear activity failed: ${msg}`,
|
|
580
|
+
});
|
|
543
581
|
}
|
|
544
582
|
}
|
|
545
|
-
async
|
|
583
|
+
async syncLinearSession(issue, options) {
|
|
546
584
|
if (!issue.agentSessionId)
|
|
547
585
|
return;
|
|
548
586
|
try {
|
|
549
587
|
const linear = await this.linearProvider.forProject(issue.projectId);
|
|
550
588
|
if (!linear?.updateAgentSession)
|
|
551
589
|
return;
|
|
552
|
-
|
|
590
|
+
const externalUrls = buildAgentSessionExternalUrls(this.config, {
|
|
591
|
+
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
592
|
+
...(issue.prUrl ? { prUrl: issue.prUrl } : {}),
|
|
593
|
+
});
|
|
594
|
+
await linear.updateAgentSession({
|
|
595
|
+
agentSessionId: issue.agentSessionId,
|
|
596
|
+
plan: buildAgentSessionPlanForIssue(issue, options),
|
|
597
|
+
...(externalUrls ? { externalUrls } : {}),
|
|
598
|
+
});
|
|
553
599
|
}
|
|
554
600
|
catch (error) {
|
|
555
|
-
|
|
601
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
602
|
+
this.logger.warn({ issueKey: issue.issueKey, error: msg }, "Failed to update Linear plan");
|
|
556
603
|
}
|
|
557
604
|
}
|
|
558
605
|
async readThreadWithRetry(threadId, maxRetries = 3) {
|
package/dist/run-reporting.js
CHANGED
|
@@ -12,11 +12,42 @@ export function summarizeCurrentThread(thread) {
|
|
|
12
12
|
const latestAgentMessage = latestTurn?.items
|
|
13
13
|
.filter((item) => item.type === "agentMessage")
|
|
14
14
|
.at(-1)?.text;
|
|
15
|
+
const latestPlan = latestTurn?.items
|
|
16
|
+
.filter((item) => item.type === "plan")
|
|
17
|
+
.at(-1)?.text;
|
|
18
|
+
const activeCommand = latestTurn?.items
|
|
19
|
+
.filter((item) => item.type === "commandExecution")
|
|
20
|
+
.filter((item) => item.status === "inProgress" || item.status === "running")
|
|
21
|
+
.at(-1)?.command
|
|
22
|
+
?? latestTurn?.items
|
|
23
|
+
.filter((item) => item.type === "commandExecution")
|
|
24
|
+
.at(-1)?.command;
|
|
25
|
+
let commandCount = 0;
|
|
26
|
+
let fileChangeCount = 0;
|
|
27
|
+
let toolCallCount = 0;
|
|
28
|
+
for (const turn of thread.turns) {
|
|
29
|
+
for (const item of turn.items) {
|
|
30
|
+
if (item.type === "commandExecution") {
|
|
31
|
+
commandCount += 1;
|
|
32
|
+
}
|
|
33
|
+
else if (item.type === "fileChange" && Array.isArray(item.changes)) {
|
|
34
|
+
fileChangeCount += item.changes.length;
|
|
35
|
+
}
|
|
36
|
+
else if (item.type === "mcpToolCall" || item.type === "dynamicToolCall") {
|
|
37
|
+
toolCallCount += 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
15
41
|
return {
|
|
16
42
|
threadId: thread.id,
|
|
17
43
|
threadStatus: thread.status,
|
|
44
|
+
commandCount,
|
|
45
|
+
fileChangeCount,
|
|
46
|
+
toolCallCount,
|
|
18
47
|
...(latestTurn ? { latestTurnId: latestTurn.id, latestTurnStatus: latestTurn.status } : {}),
|
|
19
48
|
...(latestAgentMessage ? { latestAgentMessage } : {}),
|
|
49
|
+
...(latestPlan ? { latestPlan } : {}),
|
|
50
|
+
...(activeCommand ? { activeCommand } : {}),
|
|
20
51
|
};
|
|
21
52
|
}
|
|
22
53
|
export function buildStageReport(run, issue, thread, eventCounts) {
|
package/dist/service.js
CHANGED
|
@@ -138,6 +138,66 @@ export class PatchRelayService {
|
|
|
138
138
|
getReadiness() {
|
|
139
139
|
return this.runtime.getReadiness();
|
|
140
140
|
}
|
|
141
|
+
listTrackedIssues() {
|
|
142
|
+
const rows = this.db.connection
|
|
143
|
+
.prepare(`SELECT
|
|
144
|
+
i.project_id, i.linear_issue_id, i.issue_key, i.title,
|
|
145
|
+
i.current_linear_state, i.factory_state, i.updated_at,
|
|
146
|
+
i.pr_number, i.pr_review_state, i.pr_check_status,
|
|
147
|
+
active_run.run_type AS active_run_type,
|
|
148
|
+
latest_run.run_type AS latest_run_type,
|
|
149
|
+
latest_run.status AS latest_run_status
|
|
150
|
+
FROM issues i
|
|
151
|
+
LEFT JOIN runs active_run ON active_run.id = i.active_run_id
|
|
152
|
+
LEFT JOIN runs latest_run ON latest_run.id = (
|
|
153
|
+
SELECT r.id FROM runs r
|
|
154
|
+
WHERE r.project_id = i.project_id AND r.linear_issue_id = i.linear_issue_id
|
|
155
|
+
ORDER BY r.id DESC LIMIT 1
|
|
156
|
+
)
|
|
157
|
+
ORDER BY i.updated_at DESC, i.issue_key ASC`)
|
|
158
|
+
.all();
|
|
159
|
+
return rows.map((row) => ({
|
|
160
|
+
...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
|
|
161
|
+
...(row.title !== null ? { title: String(row.title) } : {}),
|
|
162
|
+
projectId: String(row.project_id),
|
|
163
|
+
factoryState: String(row.factory_state ?? "delegated"),
|
|
164
|
+
...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
|
|
165
|
+
...(row.active_run_type !== null ? { activeRunType: String(row.active_run_type) } : {}),
|
|
166
|
+
...(row.latest_run_type !== null ? { latestRunType: String(row.latest_run_type) } : {}),
|
|
167
|
+
...(row.latest_run_status !== null ? { latestRunStatus: String(row.latest_run_status) } : {}),
|
|
168
|
+
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
169
|
+
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
170
|
+
...(row.pr_check_status !== null ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
171
|
+
updatedAt: String(row.updated_at),
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
subscribeCodexNotifications(listener) {
|
|
175
|
+
const handler = (notification) => {
|
|
176
|
+
const threadId = typeof notification.params.threadId === "string"
|
|
177
|
+
? notification.params.threadId
|
|
178
|
+
: typeof notification.params.thread === "object" && notification.params.thread !== null && "id" in notification.params.thread
|
|
179
|
+
? String(notification.params.thread.id)
|
|
180
|
+
: undefined;
|
|
181
|
+
let issueKey;
|
|
182
|
+
let runId;
|
|
183
|
+
if (threadId) {
|
|
184
|
+
const run = this.db.getRunByThreadId(threadId);
|
|
185
|
+
if (run) {
|
|
186
|
+
runId = run.id;
|
|
187
|
+
const issue = this.db.getIssue(run.projectId, run.linearIssueId);
|
|
188
|
+
issueKey = issue?.issueKey ?? undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
listener({
|
|
192
|
+
method: notification.method,
|
|
193
|
+
params: notification.params,
|
|
194
|
+
...(issueKey ? { issueKey } : {}),
|
|
195
|
+
...(runId !== undefined ? { runId } : {}),
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
this.codex.on("notification", handler);
|
|
199
|
+
return () => { this.codex.off("notification", handler); };
|
|
200
|
+
}
|
|
141
201
|
listOperatorFeed(options) {
|
|
142
202
|
return this.feed.list(options);
|
|
143
203
|
}
|