patchrelay 0.38.1 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +4 -0
- package/dist/cli/commands/issues.js +20 -1
- package/dist/cli/data.js +54 -7
- package/dist/cli/formatters/text.js +10 -0
- package/dist/cli/help.js +4 -0
- package/dist/cli/index.js +3 -0
- package/dist/config.js +26 -0
- package/dist/db/issue-store.js +10 -2
- package/dist/db/migrations.js +5 -0
- package/dist/factory-state.js +1 -0
- package/dist/github-webhook-handler.js +12 -0
- package/dist/github-webhook-late-publication-guard.js +94 -0
- package/dist/github-webhook-state-projector.js +15 -1
- package/dist/github-webhooks.js +39 -4
- package/dist/github-worktree-auth.js +18 -0
- package/dist/http.js +17 -0
- package/dist/idle-reconciliation.js +4 -2
- package/dist/issue-session-events.js +1 -0
- package/dist/linear-activity-key.js +11 -0
- package/dist/linear-agent-session-client.js +14 -1
- package/dist/linear-progress-facts.js +170 -0
- package/dist/linear-progress-reporter.js +21 -168
- package/dist/linear-status-comment-sync.js +3 -19
- package/dist/linear-workflow-state-sync.js +37 -18
- package/dist/manual-issue-actions.js +37 -0
- package/dist/merged-linear-completion-reconciler.js +102 -22
- package/dist/no-pr-completion-check.js +52 -0
- package/dist/presentation-text.js +11 -1
- package/dist/prompting/patchrelay.js +8 -6
- package/dist/run-budgets.js +12 -0
- package/dist/run-launcher.js +6 -6
- package/dist/run-notification-handler.js +4 -0
- package/dist/run-orchestrator.js +7 -1
- package/dist/run-wake-planner.js +11 -10
- package/dist/service-issue-actions.js +80 -27
- package/dist/service.js +3 -0
- package/dist/trusted-no-pr-completion.js +7 -0
- package/dist/webhooks/desired-stage-recorder.js +34 -10
- package/package.json +1 -1
|
@@ -4,8 +4,8 @@ import { deriveGateCheckStatusFromRollup } from "./github-rollup.js";
|
|
|
4
4
|
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
5
5
|
import { parseStoredQueueRepairContext } from "./merge-queue-incident.js";
|
|
6
6
|
import { buildClosedPrCleanupFields, resolveClosedPrDisposition } from "./pr-state.js";
|
|
7
|
+
import { getReviewFixBudget } from "./run-budgets.js";
|
|
7
8
|
import { execCommand } from "./utils.js";
|
|
8
|
-
const DEFAULT_REVIEW_FIX_BUDGET = 12;
|
|
9
9
|
function isFailingCheckStatus(status) {
|
|
10
10
|
return status === "failed" || status === "failure";
|
|
11
11
|
}
|
|
@@ -510,13 +510,15 @@ export class IdleIssueReconciler {
|
|
|
510
510
|
if (issue.delegatedToPatchRelay
|
|
511
511
|
&& (issue.factoryState === "escalated" || issue.factoryState === "failed")
|
|
512
512
|
&& (reactiveIntent?.runType === "review_fix" || reactiveIntent?.runType === "branch_upkeep")) {
|
|
513
|
-
|
|
513
|
+
const reviewFixBudget = getReviewFixBudget(project);
|
|
514
|
+
if (issue.reviewFixAttempts >= reviewFixBudget) {
|
|
514
515
|
this.logger.debug({
|
|
515
516
|
issueKey: issue.issueKey,
|
|
516
517
|
prNumber: issue.prNumber,
|
|
517
518
|
from: issue.factoryState,
|
|
518
519
|
runType: reactiveIntent.runType,
|
|
519
520
|
reviewFixAttempts: issue.reviewFixAttempts,
|
|
521
|
+
reviewFixBudget,
|
|
520
522
|
}, "Reconciliation: leaving terminal requested-changes issue escalated because the repair budget is exhausted");
|
|
521
523
|
return;
|
|
522
524
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { sanitizeOperatorFacingText } from "./presentation-text.js";
|
|
2
|
+
export function computeLinearActivityKey(content) {
|
|
3
|
+
if (content.type === "action") {
|
|
4
|
+
const action = sanitizeOperatorFacingText(content.action) ?? content.action;
|
|
5
|
+
const parameter = sanitizeOperatorFacingText(content.parameter) ?? content.parameter;
|
|
6
|
+
const result = sanitizeOperatorFacingText(content.result);
|
|
7
|
+
return `action:${action}:${parameter}:${result ?? ""}`;
|
|
8
|
+
}
|
|
9
|
+
const body = sanitizeOperatorFacingText(content.body) ?? content.body;
|
|
10
|
+
return `${content.type}:${body}`;
|
|
11
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildAgentSessionPlanForIssue } from "./agent-session-plan.js";
|
|
2
2
|
import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
|
|
3
|
+
import { computeLinearActivityKey } from "./linear-activity-key.js";
|
|
3
4
|
export class LinearAgentSessionClient {
|
|
4
5
|
config;
|
|
5
6
|
db;
|
|
@@ -36,11 +37,23 @@ export class LinearAgentSessionClient {
|
|
|
36
37
|
if (!linear)
|
|
37
38
|
return;
|
|
38
39
|
const allowEphemeral = content.type === "thought" || content.type === "action";
|
|
40
|
+
const ephemeral = options?.ephemeral && allowEphemeral;
|
|
41
|
+
const activityKey = ephemeral ? undefined : computeLinearActivityKey(content);
|
|
42
|
+
if (activityKey && syncedIssue.lastLinearActivityKey === activityKey) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
39
45
|
await linear.createAgentActivity({
|
|
40
46
|
agentSessionId: syncedIssue.agentSessionId,
|
|
41
47
|
content,
|
|
42
|
-
...(
|
|
48
|
+
...(ephemeral ? { ephemeral: true } : {}),
|
|
43
49
|
});
|
|
50
|
+
if (activityKey) {
|
|
51
|
+
this.db.issues.upsertIssue({
|
|
52
|
+
projectId: syncedIssue.projectId,
|
|
53
|
+
linearIssueId: syncedIssue.linearIssueId,
|
|
54
|
+
lastLinearActivityKey: activityKey,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
44
57
|
}
|
|
45
58
|
catch (error) {
|
|
46
59
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { sanitizeOperatorFacingText } from "./presentation-text.js";
|
|
2
|
+
export function deriveLinearProgressFact(notification, issue) {
|
|
3
|
+
switch (notification.method) {
|
|
4
|
+
case "item/completed":
|
|
5
|
+
return deriveProgressFactFromCompletedItem(notification.params.item, issue);
|
|
6
|
+
case "turn/plan/updated":
|
|
7
|
+
return deriveProgressFactFromPlan(notification.params.plan, issue);
|
|
8
|
+
default:
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function deriveProgressFactFromCompletedItem(rawItem, issue) {
|
|
13
|
+
void issue;
|
|
14
|
+
if (!rawItem || typeof rawItem !== "object") {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const item = rawItem;
|
|
18
|
+
if (item.type !== "agentMessage" || typeof item.text !== "string") {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const body = compactOperatorSentence(item.text);
|
|
22
|
+
if (!body) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
if (looksLikeVerification(body)) {
|
|
26
|
+
return {
|
|
27
|
+
kind: "verification_started",
|
|
28
|
+
meaningKey: `verification:${normalizeMeaningKey(body)}`,
|
|
29
|
+
content: { type: "thought", body },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (looksLikePublishing(body)) {
|
|
33
|
+
return {
|
|
34
|
+
kind: "publishing_started",
|
|
35
|
+
meaningKey: `publishing:${normalizeMeaningKey(body)}`,
|
|
36
|
+
content: { type: "thought", body },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (looksLikeRootCause(body)) {
|
|
40
|
+
return {
|
|
41
|
+
kind: "root_cause_found",
|
|
42
|
+
meaningKey: `finding:${normalizeMeaningKey(body)}`,
|
|
43
|
+
content: { type: "thought", body },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
function deriveProgressFactFromPlan(rawPlan, issue) {
|
|
49
|
+
if (!Array.isArray(rawPlan)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const activeStep = rawPlan
|
|
53
|
+
.map((entry) => normalizePlanEntry(entry))
|
|
54
|
+
.find((entry) => entry && entry.status === "in_progress");
|
|
55
|
+
if (!activeStep) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (looksLikeVerification(activeStep.step)) {
|
|
59
|
+
return {
|
|
60
|
+
kind: "verification_started",
|
|
61
|
+
meaningKey: `verification:${normalizeMeaningKey(activeStep.step)}`,
|
|
62
|
+
content: {
|
|
63
|
+
type: "action",
|
|
64
|
+
action: "Verifying",
|
|
65
|
+
parameter: summarizePlanStep(activeStep.step, "latest changes before publishing"),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (looksLikePublishing(activeStep.step)) {
|
|
70
|
+
const parameter = summarizePlanStep(activeStep.step, issue?.prNumber !== undefined ? `changes to PR #${issue.prNumber}` : "latest changes");
|
|
71
|
+
return {
|
|
72
|
+
kind: "publishing_started",
|
|
73
|
+
meaningKey: `publishing:${normalizeMeaningKey(activeStep.step)}`,
|
|
74
|
+
content: {
|
|
75
|
+
type: "action",
|
|
76
|
+
action: "Publishing",
|
|
77
|
+
parameter,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function normalizePlanEntry(rawEntry) {
|
|
84
|
+
if (!rawEntry || typeof rawEntry !== "object") {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const entry = rawEntry;
|
|
88
|
+
const rawStep = entry.step;
|
|
89
|
+
if (typeof rawStep !== "string" || !rawStep.trim()) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const rawStatus = typeof entry.status === "string" ? entry.status : "pending";
|
|
93
|
+
return {
|
|
94
|
+
step: rawStep.trim(),
|
|
95
|
+
status: rawStatus === "inProgress" ? "in_progress"
|
|
96
|
+
: rawStatus === "completed" ? "completed"
|
|
97
|
+
: rawStatus === "pending" ? "pending"
|
|
98
|
+
: rawStatus === "in_progress" ? "in_progress"
|
|
99
|
+
: "pending",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function looksLikeRootCause(text) {
|
|
103
|
+
const normalized = text.toLowerCase();
|
|
104
|
+
return /\b(narrowed|isolated|root cause)\b/.test(normalized)
|
|
105
|
+
|| normalized.startsWith("found that ")
|
|
106
|
+
|| normalized.startsWith("the failure is isolated")
|
|
107
|
+
|| normalized.startsWith("the issue is isolated");
|
|
108
|
+
}
|
|
109
|
+
function looksLikeVerification(text) {
|
|
110
|
+
const normalized = text.toLowerCase();
|
|
111
|
+
return /\b(verifying|verification|targeted verification|smoke)\b/.test(normalized);
|
|
112
|
+
}
|
|
113
|
+
function looksLikePublishing(text) {
|
|
114
|
+
const normalized = text.toLowerCase();
|
|
115
|
+
return /\b(publish|publishing|push|pushing)\b/.test(normalized)
|
|
116
|
+
|| normalized.includes("opening pr")
|
|
117
|
+
|| normalized.includes("opening the pr")
|
|
118
|
+
|| normalized.includes("opening pull request");
|
|
119
|
+
}
|
|
120
|
+
function compactOperatorSentence(text, maxLength = 160) {
|
|
121
|
+
const sanitized = sanitizeOperatorFacingText(text)?.replace(/\s+/g, " ").trim();
|
|
122
|
+
if (!sanitized) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
if (sanitized.length <= maxLength) {
|
|
126
|
+
return sanitized;
|
|
127
|
+
}
|
|
128
|
+
const punctuated = lastBoundaryWithinLimit(sanitized, maxLength, /[.;!?]/g);
|
|
129
|
+
if (punctuated !== undefined) {
|
|
130
|
+
return sanitized.slice(0, punctuated + 1).trim();
|
|
131
|
+
}
|
|
132
|
+
const spaced = sanitized.lastIndexOf(" ", maxLength);
|
|
133
|
+
if (spaced > 0) {
|
|
134
|
+
return `${sanitized.slice(0, spaced).trimEnd()}...`;
|
|
135
|
+
}
|
|
136
|
+
return `${sanitized.slice(0, maxLength).trimEnd()}...`;
|
|
137
|
+
}
|
|
138
|
+
function summarizePlanStep(step, fallback) {
|
|
139
|
+
const sanitized = sanitizeOperatorFacingText(step)?.replace(/\s+/g, " ").trim();
|
|
140
|
+
if (!sanitized) {
|
|
141
|
+
return fallback;
|
|
142
|
+
}
|
|
143
|
+
const stripped = sanitized
|
|
144
|
+
.replace(/^(run|running|start|starting)\s+/i, "")
|
|
145
|
+
.replace(/^(verify|verifying|verification of)\s+/i, "")
|
|
146
|
+
.replace(/^(publish|publishing|push|pushing|open|opening)\s+/i, "")
|
|
147
|
+
.trim()
|
|
148
|
+
.replace(/[.]+$/, "");
|
|
149
|
+
return stripped || fallback;
|
|
150
|
+
}
|
|
151
|
+
function normalizeMeaningKey(text) {
|
|
152
|
+
return text
|
|
153
|
+
.toLowerCase()
|
|
154
|
+
.replace(/\s+/g, " ")
|
|
155
|
+
.trim();
|
|
156
|
+
}
|
|
157
|
+
function lastBoundaryWithinLimit(text, maxLength, pattern) {
|
|
158
|
+
let last = -1;
|
|
159
|
+
for (;;) {
|
|
160
|
+
const match = pattern.exec(text);
|
|
161
|
+
if (!match) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
if (match.index >= maxLength) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
last = match.index;
|
|
168
|
+
}
|
|
169
|
+
return last >= 0 ? last : undefined;
|
|
170
|
+
}
|
|
@@ -1,185 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const PROGRESS_THROTTLE_MS = 5_000;
|
|
3
|
-
const MAX_PROGRESS_TEXT_LENGTH = 220;
|
|
1
|
+
import { deriveLinearProgressFact } from "./linear-progress-facts.js";
|
|
4
2
|
export class LinearProgressReporter {
|
|
5
3
|
db;
|
|
6
4
|
emitActivity;
|
|
7
|
-
|
|
8
|
-
workingOnPublishedRuns = new Set();
|
|
9
|
-
agentMessageBuffers = new Map();
|
|
10
|
-
agentMessageProgressPublished = new Set();
|
|
5
|
+
publicationsByRun = new Map();
|
|
11
6
|
constructor(db, emitActivity) {
|
|
12
7
|
this.db = db;
|
|
13
8
|
this.emitActivity = emitActivity;
|
|
14
9
|
}
|
|
15
10
|
maybeEmitProgress(notification, run) {
|
|
16
|
-
const issue = this.db.
|
|
17
|
-
if (!issue)
|
|
11
|
+
const issue = this.db.getIssue(run.projectId, run.linearIssueId);
|
|
12
|
+
if (!issue) {
|
|
18
13
|
return;
|
|
19
|
-
const agentSentence = this.consumeAgentMessageSentence(notification, run);
|
|
20
|
-
const workingOn = resolveWorkingOnActivity(notification, agentSentence?.sentence);
|
|
21
|
-
if (workingOn && !this.workingOnPublishedRuns.has(run.id)) {
|
|
22
|
-
this.workingOnPublishedRuns.add(run.id);
|
|
23
|
-
void this.emitActivity(issue, workingOn);
|
|
24
14
|
}
|
|
25
|
-
const
|
|
26
|
-
if (!
|
|
15
|
+
const fact = deriveLinearProgressFact(notification, issue);
|
|
16
|
+
if (!fact) {
|
|
27
17
|
return;
|
|
28
|
-
if (!progress.bypassThrottle) {
|
|
29
|
-
const now = Date.now();
|
|
30
|
-
const lastEmit = this.progressThrottle.get(run.id) ?? 0;
|
|
31
|
-
if (now - lastEmit < PROGRESS_THROTTLE_MS)
|
|
32
|
-
return;
|
|
33
|
-
this.progressThrottle.set(run.id, now);
|
|
34
18
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.progressThrottle.delete(runId);
|
|
39
|
-
this.workingOnPublishedRuns.delete(runId);
|
|
40
|
-
for (const key of this.agentMessageBuffers.keys()) {
|
|
41
|
-
if (key.startsWith(`${runId}:`)) {
|
|
42
|
-
this.agentMessageBuffers.delete(key);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
for (const key of this.agentMessageProgressPublished) {
|
|
46
|
-
if (key.startsWith(`${runId}:`)) {
|
|
47
|
-
this.agentMessageProgressPublished.delete(key);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
consumeAgentMessageSentence(notification, run) {
|
|
52
|
-
const messageKey = resolveAgentMessageKey(notification, run);
|
|
53
|
-
if (!messageKey)
|
|
54
|
-
return undefined;
|
|
55
|
-
if (this.agentMessageProgressPublished.has(messageKey))
|
|
56
|
-
return undefined;
|
|
57
|
-
const delta = resolveAgentMessageDelta(notification);
|
|
58
|
-
if (delta) {
|
|
59
|
-
const previous = this.agentMessageBuffers.get(messageKey) ?? "";
|
|
60
|
-
const next = `${previous}${delta}`;
|
|
61
|
-
this.agentMessageBuffers.set(messageKey, next);
|
|
62
|
-
const sentence = extractFirstCompletedSentence(next);
|
|
63
|
-
if (!sentence)
|
|
64
|
-
return undefined;
|
|
65
|
-
this.agentMessageProgressPublished.add(messageKey);
|
|
66
|
-
return { sentence };
|
|
67
|
-
}
|
|
68
|
-
const completedText = resolveCompletedAgentMessageText(notification);
|
|
69
|
-
if (!completedText)
|
|
70
|
-
return undefined;
|
|
71
|
-
const sentence = extractFirstSentence(completedText);
|
|
72
|
-
if (!sentence)
|
|
73
|
-
return undefined;
|
|
74
|
-
this.agentMessageProgressPublished.add(messageKey);
|
|
75
|
-
return { sentence };
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
function resolveWorkingOnActivity(notification, agentSentence) {
|
|
79
|
-
const summary = resolveWorkingOnSummary(notification) ?? agentSentence;
|
|
80
|
-
if (!summary)
|
|
81
|
-
return undefined;
|
|
82
|
-
return { type: "response", body: `Working on: ${summary}` };
|
|
83
|
-
}
|
|
84
|
-
function resolveEphemeralProgressActivity(notification, agentSentence) {
|
|
85
|
-
if (notification.method === "item/started") {
|
|
86
|
-
const item = notification.params.item;
|
|
87
|
-
if (!item)
|
|
88
|
-
return undefined;
|
|
89
|
-
const type = typeof item.type === "string" ? item.type : undefined;
|
|
90
|
-
if (type === "commandExecution") {
|
|
91
|
-
const cmd = item.command;
|
|
92
|
-
const cmdStr = Array.isArray(cmd)
|
|
93
|
-
? sanitizeOperatorFacingCommand(cmd.map((part) => String(part)).join(" "))
|
|
94
|
-
: sanitizeOperatorFacingCommand(typeof cmd === "string" ? cmd : undefined);
|
|
95
|
-
return { activity: { type: "action", action: "Running", parameter: truncateProgressText(cmdStr ?? "command", 120) } };
|
|
96
|
-
}
|
|
97
|
-
if (type === "mcpToolCall") {
|
|
98
|
-
const server = typeof item.server === "string" ? item.server : "";
|
|
99
|
-
const tool = typeof item.tool === "string" ? item.tool : "";
|
|
100
|
-
return { activity: { type: "action", action: "Using", parameter: `${server}/${tool}` } };
|
|
101
|
-
}
|
|
102
|
-
if (type === "dynamicToolCall") {
|
|
103
|
-
const tool = typeof item.tool === "string" ? item.tool : "tool";
|
|
104
|
-
return { activity: { type: "action", action: "Using", parameter: tool } };
|
|
19
|
+
const previous = this.publicationsByRun.get(run.id);
|
|
20
|
+
if (previous?.meaningKey === fact.meaningKey) {
|
|
21
|
+
return;
|
|
105
22
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
activity: { type: "thought", body: agentSentence },
|
|
110
|
-
bypassThrottle: true,
|
|
23
|
+
const publication = {
|
|
24
|
+
meaningKey: fact.meaningKey,
|
|
25
|
+
publishedAtMs: Date.now(),
|
|
111
26
|
};
|
|
27
|
+
this.publicationsByRun.set(run.id, publication);
|
|
28
|
+
void this.emitActivity(issue, fact.content, { ephemeral: true }).catch(() => {
|
|
29
|
+
const current = this.publicationsByRun.get(run.id);
|
|
30
|
+
if (current?.publishedAtMs === publication.publishedAtMs && current.meaningKey === publication.meaningKey) {
|
|
31
|
+
this.publicationsByRun.delete(run.id);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
112
34
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
function resolveWorkingOnSummary(notification) {
|
|
116
|
-
if (notification.method !== "turn/plan/updated") {
|
|
117
|
-
return undefined;
|
|
118
|
-
}
|
|
119
|
-
const plan = notification.params.plan;
|
|
120
|
-
if (!Array.isArray(plan))
|
|
121
|
-
return undefined;
|
|
122
|
-
const ranked = plan
|
|
123
|
-
.map((entry) => entry)
|
|
124
|
-
.filter((entry) => typeof entry.step === "string" && entry.step.trim().length > 0)
|
|
125
|
-
.sort((a, b) => rankPlanStatus(a.status) - rankPlanStatus(b.status));
|
|
126
|
-
const first = ranked[0];
|
|
127
|
-
return summarizeProgressSentence(typeof first?.step === "string" ? first.step : undefined);
|
|
128
|
-
}
|
|
129
|
-
function rankPlanStatus(status) {
|
|
130
|
-
return status === "inProgress" ? 0
|
|
131
|
-
: status === "pending" ? 1
|
|
132
|
-
: status === "completed" ? 2
|
|
133
|
-
: 3;
|
|
134
|
-
}
|
|
135
|
-
function resolveAgentMessageKey(notification, run) {
|
|
136
|
-
if (notification.method === "item/agentMessage/delta") {
|
|
137
|
-
const itemId = typeof notification.params.itemId === "string" ? notification.params.itemId : undefined;
|
|
138
|
-
return itemId ? `${run.id}:${itemId}` : undefined;
|
|
139
|
-
}
|
|
140
|
-
if (notification.method === "item/completed") {
|
|
141
|
-
const item = notification.params.item;
|
|
142
|
-
const itemId = typeof item?.id === "string" ? item.id : undefined;
|
|
143
|
-
const itemType = typeof item?.type === "string" ? item.type : undefined;
|
|
144
|
-
return itemId && itemType === "agentMessage" ? `${run.id}:${itemId}` : undefined;
|
|
145
|
-
}
|
|
146
|
-
return undefined;
|
|
147
|
-
}
|
|
148
|
-
function resolveAgentMessageDelta(notification) {
|
|
149
|
-
if (notification.method !== "item/agentMessage/delta") {
|
|
150
|
-
return undefined;
|
|
151
|
-
}
|
|
152
|
-
return typeof notification.params.delta === "string" ? notification.params.delta : undefined;
|
|
153
|
-
}
|
|
154
|
-
function resolveCompletedAgentMessageText(notification) {
|
|
155
|
-
if (notification.method !== "item/completed") {
|
|
156
|
-
return undefined;
|
|
35
|
+
clearProgress(runId) {
|
|
36
|
+
this.publicationsByRun.delete(runId);
|
|
157
37
|
}
|
|
158
|
-
const item = notification.params.item;
|
|
159
|
-
if (!item || item.type !== "agentMessage")
|
|
160
|
-
return undefined;
|
|
161
|
-
return typeof item.text === "string" ? item.text : undefined;
|
|
162
|
-
}
|
|
163
|
-
function extractFirstSentence(text) {
|
|
164
|
-
const sanitized = sanitizeOperatorFacingText(text)?.replace(/\s+/g, " ").trim();
|
|
165
|
-
if (!sanitized)
|
|
166
|
-
return undefined;
|
|
167
|
-
const match = sanitized.match(/^(.+?[.!?])(?:\s|$)/);
|
|
168
|
-
return truncateProgressText((match?.[1] ?? sanitized).trim(), MAX_PROGRESS_TEXT_LENGTH);
|
|
169
|
-
}
|
|
170
|
-
function extractFirstCompletedSentence(text) {
|
|
171
|
-
const sanitized = sanitizeOperatorFacingText(text)?.replace(/\s+/g, " ").trim();
|
|
172
|
-
if (!sanitized)
|
|
173
|
-
return undefined;
|
|
174
|
-
const match = sanitized.match(/^(.+?[.!?])(?:\s|$)/);
|
|
175
|
-
return match?.[1] ? truncateProgressText(match[1].trim(), MAX_PROGRESS_TEXT_LENGTH) : undefined;
|
|
176
|
-
}
|
|
177
|
-
function summarizeProgressSentence(text) {
|
|
178
|
-
const summary = extractFirstSentence(text);
|
|
179
|
-
if (!summary)
|
|
180
|
-
return undefined;
|
|
181
|
-
return summary.endsWith(".") || summary.endsWith("!") || summary.endsWith("?") ? summary : `${summary}.`;
|
|
182
|
-
}
|
|
183
|
-
function truncateProgressText(text, maxLength) {
|
|
184
|
-
return text.length <= maxLength ? text : `${text.slice(0, maxLength - 3).trimEnd()}...`;
|
|
185
38
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { extractCompletionCheck } from "./completion-check.js";
|
|
2
|
+
import { isClosedPrState } from "./pr-state.js";
|
|
2
3
|
import { deriveIssueStatusNote } from "./status-note.js";
|
|
3
4
|
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
4
|
-
import { isClosedPrState } from "./pr-state.js";
|
|
5
|
-
import { isUndelegatedPausedIssue } from "./paused-issue-state.js";
|
|
6
5
|
export async function syncVisibleStatusComment(params) {
|
|
7
6
|
const { db, issue, linear, logger, trackedIssue, options } = params;
|
|
8
7
|
try {
|
|
@@ -25,23 +24,8 @@ export async function syncVisibleStatusComment(params) {
|
|
|
25
24
|
logger.warn({ issueKey: issue.issueKey, error: msg }, "Failed to sync Linear status comment");
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
|
-
export function shouldSyncVisibleIssueComment(
|
|
29
|
-
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
if (issue.sessionState === "waiting_input" || issue.sessionState === "failed"
|
|
33
|
-
|| issue.factoryState === "awaiting_input" || issue.factoryState === "failed" || issue.factoryState === "escalated") {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
if (isUndelegatedPausedIssue(issue)) {
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
if ((issue.sessionState === "done" || issue.factoryState === "done")
|
|
40
|
-
&& ((issue.prNumber === undefined && !issue.prUrl)
|
|
41
|
-
|| isClosedPrState(issue.prState))) {
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
return false;
|
|
27
|
+
export function shouldSyncVisibleIssueComment(_issue, _hasAgentSession) {
|
|
28
|
+
return true;
|
|
45
29
|
}
|
|
46
30
|
function renderStatusComment(db, issue, trackedIssue, options) {
|
|
47
31
|
const activeRun = issue.activeRunId ? db.runs.getRunById(issue.activeRunId) : undefined;
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { resolvePreferredDeployingLinearState, resolvePreferredHumanNeededLinearState, resolvePreferredImplementingLinearState, resolvePreferredReviewLinearState, resolvePreferredReviewingLinearState, } from "./linear-workflow.js";
|
|
1
|
+
import { resolvePreferredCompletedLinearState, resolvePreferredDeployingLinearState, resolvePreferredHumanNeededLinearState, resolvePreferredImplementingLinearState, resolvePreferredReviewLinearState, resolvePreferredReviewingLinearState, } from "./linear-workflow.js";
|
|
2
|
+
import { isCompletedLinearState } from "./pr-state.js";
|
|
3
|
+
import { hasTrustedNoPrCompletion } from "./trusted-no-pr-completion.js";
|
|
2
4
|
export async function syncActiveWorkflowState(params) {
|
|
3
5
|
const { db, issue, linear, trackedIssue, options } = params;
|
|
4
|
-
if (!shouldAutoAdvanceLinearState(issue)) {
|
|
5
|
-
return;
|
|
6
|
-
}
|
|
7
6
|
const liveIssue = await linear.getIssue(issue.linearIssueId).catch(() => undefined);
|
|
8
7
|
if (!liveIssue)
|
|
9
8
|
return;
|
|
9
|
+
const latestRun = db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
10
|
+
if (hasTrustedNoPrCompletion(issue, latestRun)) {
|
|
11
|
+
await syncCompletedLinearState({ db, issue, linear, liveIssue });
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (!shouldAutoAdvanceLinearState(issue)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
10
17
|
if (!shouldAutoAdvanceLinearState({
|
|
11
18
|
currentLinearState: liveIssue.stateName,
|
|
12
19
|
currentLinearStateType: liveIssue.stateType,
|
|
13
20
|
})) {
|
|
14
|
-
db.
|
|
15
|
-
projectId: issue.projectId,
|
|
16
|
-
linearIssueId: issue.linearIssueId,
|
|
17
|
-
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
18
|
-
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
19
|
-
});
|
|
21
|
+
refreshCachedLinearState(db, issue, liveIssue.stateName, liveIssue.stateType);
|
|
20
22
|
return;
|
|
21
23
|
}
|
|
22
24
|
const targetState = resolveDesiredActiveWorkflowState(issue, trackedIssue, options, liveIssue);
|
|
@@ -24,20 +26,37 @@ export async function syncActiveWorkflowState(params) {
|
|
|
24
26
|
return;
|
|
25
27
|
const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
|
|
26
28
|
if (normalizedCurrent === targetState.trim().toLowerCase()) {
|
|
27
|
-
db.
|
|
28
|
-
projectId: issue.projectId,
|
|
29
|
-
linearIssueId: issue.linearIssueId,
|
|
30
|
-
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
31
|
-
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
32
|
-
});
|
|
29
|
+
refreshCachedLinearState(db, issue, liveIssue.stateName, liveIssue.stateType);
|
|
33
30
|
return;
|
|
34
31
|
}
|
|
35
32
|
const updated = await linear.setIssueState(issue.linearIssueId, targetState);
|
|
33
|
+
refreshCachedLinearState(db, issue, updated.stateName, updated.stateType);
|
|
34
|
+
}
|
|
35
|
+
async function syncCompletedLinearState(params) {
|
|
36
|
+
const { db, issue, linear, liveIssue } = params;
|
|
37
|
+
if (isCompletedLinearState(liveIssue.stateType, liveIssue.stateName)) {
|
|
38
|
+
refreshCachedLinearState(db, issue, liveIssue.stateName, liveIssue.stateType);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const targetState = resolvePreferredCompletedLinearState(liveIssue);
|
|
42
|
+
if (!targetState) {
|
|
43
|
+
refreshCachedLinearState(db, issue, liveIssue.stateName, liveIssue.stateType);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
|
|
47
|
+
if (normalizedCurrent === targetState.trim().toLowerCase()) {
|
|
48
|
+
refreshCachedLinearState(db, issue, liveIssue.stateName, liveIssue.stateType);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const updated = await linear.setIssueState(issue.linearIssueId, targetState);
|
|
52
|
+
refreshCachedLinearState(db, issue, updated.stateName, updated.stateType);
|
|
53
|
+
}
|
|
54
|
+
function refreshCachedLinearState(db, issue, stateName, stateType) {
|
|
36
55
|
db.issues.upsertIssue({
|
|
37
56
|
projectId: issue.projectId,
|
|
38
57
|
linearIssueId: issue.linearIssueId,
|
|
39
|
-
...(
|
|
40
|
-
...(
|
|
58
|
+
...(stateName ? { currentLinearState: stateName } : {}),
|
|
59
|
+
...(stateType ? { currentLinearStateType: stateType } : {}),
|
|
41
60
|
});
|
|
42
61
|
}
|
|
43
62
|
function shouldAutoAdvanceLinearState(issue) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { hasOpenPr } from "./pr-state.js";
|
|
2
|
+
export function resolveRetryTarget(params) {
|
|
3
|
+
if (params.prState === "merged") {
|
|
4
|
+
return { runType: "none", factoryState: "done" };
|
|
5
|
+
}
|
|
6
|
+
if (hasOpenPr(params.prNumber, params.prState) && params.lastGitHubFailureSource === "queue_eviction") {
|
|
7
|
+
return { runType: "queue_repair", factoryState: "repairing_queue" };
|
|
8
|
+
}
|
|
9
|
+
if (hasOpenPr(params.prNumber, params.prState)
|
|
10
|
+
&& (params.prCheckStatus === "failed" || params.prCheckStatus === "failure" || params.lastGitHubFailureSource === "branch_ci")) {
|
|
11
|
+
return { runType: "ci_repair", factoryState: "repairing_ci" };
|
|
12
|
+
}
|
|
13
|
+
if (hasOpenPr(params.prNumber, params.prState) && params.prReviewState === "changes_requested") {
|
|
14
|
+
return {
|
|
15
|
+
runType: params.pendingRunType === "branch_upkeep" || params.lastRunType === "branch_upkeep"
|
|
16
|
+
? "branch_upkeep"
|
|
17
|
+
: "review_fix",
|
|
18
|
+
factoryState: "changes_requested",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (hasOpenPr(params.prNumber, params.prState)) {
|
|
22
|
+
return { runType: "implementation", factoryState: "implementing" };
|
|
23
|
+
}
|
|
24
|
+
return { runType: "implementation", factoryState: "delegated" };
|
|
25
|
+
}
|
|
26
|
+
export function buildManualRetryAttemptReset(runType) {
|
|
27
|
+
if (runType === "ci_repair") {
|
|
28
|
+
return { ciRepairAttempts: 0 };
|
|
29
|
+
}
|
|
30
|
+
if (runType === "queue_repair") {
|
|
31
|
+
return { queueRepairAttempts: 0 };
|
|
32
|
+
}
|
|
33
|
+
if (runType === "review_fix" || runType === "branch_upkeep") {
|
|
34
|
+
return { reviewFixAttempts: 0 };
|
|
35
|
+
}
|
|
36
|
+
return {};
|
|
37
|
+
}
|