patchrelay 0.68.5 → 0.68.7
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 +1 -0
- package/dist/db/issue-store.js +22 -4
- package/dist/db.js +3 -0
- package/dist/issue-class.js +26 -0
- package/dist/manual-issue-actions.js +5 -0
- package/dist/operator-retry-event.js +7 -0
- package/dist/orchestration-parent-wake.js +1 -1
- package/dist/prompting/patchrelay.js +13 -4
- package/dist/queue-health-monitor.js +16 -1
- package/dist/run-orchestrator.js +2 -2
- package/dist/service-issue-actions.js +1 -0
- package/dist/webhooks/comment-wake-handler.js +1 -1
- package/dist/webhooks/desired-stage-recorder.js +1 -1
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli/data.js
CHANGED
|
@@ -220,6 +220,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
220
220
|
prState: dbIssue.prState,
|
|
221
221
|
prReviewState: dbIssue.prReviewState,
|
|
222
222
|
prCheckStatus: dbIssue.prCheckStatus,
|
|
223
|
+
factoryState: dbIssue.factoryState,
|
|
223
224
|
pendingRunType: dbIssue.pendingRunType,
|
|
224
225
|
lastRunType: issueSession?.lastRunType,
|
|
225
226
|
lastGitHubFailureSource: issue.latestFailureSource,
|
package/dist/db/issue-store.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { buildInsertBindings, buildUpdateAssignments } from "./issue-upsert-columns.js";
|
|
2
2
|
import { isoNow } from "./shared.js";
|
|
3
|
+
const CANCELED_OR_DUPLICATE_CHILD_PREDICATE = `
|
|
4
|
+
LOWER(TRIM(COALESCE(child.current_linear_state_type, ''))) NOT IN ('canceled', 'cancelled')
|
|
5
|
+
AND LOWER(TRIM(COALESCE(child.current_linear_state, ''))) NOT IN ('duplicate', 'canceled', 'cancelled')
|
|
6
|
+
`;
|
|
7
|
+
const OPEN_CHILD_PREDICATE = `
|
|
8
|
+
LOWER(TRIM(COALESCE(child.current_linear_state_type, ''))) NOT IN ('completed', 'canceled', 'cancelled')
|
|
9
|
+
AND LOWER(TRIM(COALESCE(child.current_linear_state, ''))) NOT IN ('done', 'completed', 'duplicate', 'canceled', 'cancelled')
|
|
10
|
+
`;
|
|
3
11
|
export class IssueStore {
|
|
4
12
|
connection;
|
|
5
13
|
syncIssueSessionFromIssue;
|
|
@@ -292,6 +300,19 @@ export class IssueStore {
|
|
|
292
300
|
`).all(projectId, parentLinearIssueId);
|
|
293
301
|
return rows.map(mapIssueRow);
|
|
294
302
|
}
|
|
303
|
+
listCanonicalChildIssues(projectId, parentLinearIssueId) {
|
|
304
|
+
const rows = this.connection.prepare(`
|
|
305
|
+
SELECT child.*
|
|
306
|
+
FROM issue_children edges
|
|
307
|
+
JOIN issues child
|
|
308
|
+
ON child.project_id = edges.project_id
|
|
309
|
+
AND child.linear_issue_id = edges.child_linear_issue_id
|
|
310
|
+
WHERE edges.project_id = ? AND edges.parent_linear_issue_id = ?
|
|
311
|
+
AND ${CANCELED_OR_DUPLICATE_CHILD_PREDICATE}
|
|
312
|
+
ORDER BY COALESCE(child.issue_key, child.linear_issue_id) ASC
|
|
313
|
+
`).all(projectId, parentLinearIssueId);
|
|
314
|
+
return rows.map(mapIssueRow);
|
|
315
|
+
}
|
|
295
316
|
countOpenChildIssues(projectId, parentLinearIssueId) {
|
|
296
317
|
const row = this.connection.prepare(`
|
|
297
318
|
SELECT COUNT(*) AS count
|
|
@@ -302,10 +323,7 @@ export class IssueStore {
|
|
|
302
323
|
WHERE edges.project_id = ? AND edges.parent_linear_issue_id = ?
|
|
303
324
|
AND (
|
|
304
325
|
child.linear_issue_id IS NULL
|
|
305
|
-
OR (
|
|
306
|
-
COALESCE(child.current_linear_state_type, '') NOT IN ('completed', 'canceled')
|
|
307
|
-
AND LOWER(TRIM(COALESCE(child.current_linear_state, ''))) NOT IN ('done', 'duplicate', 'canceled')
|
|
308
|
-
)
|
|
326
|
+
OR (${OPEN_CHILD_PREDICATE})
|
|
309
327
|
)
|
|
310
328
|
`).get(projectId, parentLinearIssueId);
|
|
311
329
|
return Number(row?.count ?? 0);
|
package/dist/db.js
CHANGED
|
@@ -163,6 +163,9 @@ export class PatchRelayDatabase {
|
|
|
163
163
|
listChildIssues(projectId, parentLinearIssueId) {
|
|
164
164
|
return this.issues.listChildIssues(projectId, parentLinearIssueId);
|
|
165
165
|
}
|
|
166
|
+
listCanonicalChildIssues(projectId, parentLinearIssueId) {
|
|
167
|
+
return this.issues.listCanonicalChildIssues(projectId, parentLinearIssueId);
|
|
168
|
+
}
|
|
166
169
|
countOpenChildIssues(projectId, parentLinearIssueId) {
|
|
167
170
|
return this.issues.countOpenChildIssues(projectId, parentLinearIssueId);
|
|
168
171
|
}
|
package/dist/issue-class.js
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
function hasExplicitNoCodePlanningSplitIntent(issue) {
|
|
2
|
+
const text = [issue.title, issue.description].filter(Boolean).join("\n").toLowerCase();
|
|
3
|
+
if (!text.trim())
|
|
4
|
+
return false;
|
|
5
|
+
const noCodePlanning = [
|
|
6
|
+
/\bno code\b/,
|
|
7
|
+
/\bcode (?:is )?not (?:needed|required|part of this)\b/,
|
|
8
|
+
/\bplanning only\b/,
|
|
9
|
+
/\banalysis only\b/,
|
|
10
|
+
/код[^\n.]{0,80}не делаем/,
|
|
11
|
+
/без (?:изменени[яй]|правок) код[а]?/,
|
|
12
|
+
/только анализ/,
|
|
13
|
+
/только планирован/,
|
|
14
|
+
].some((pattern) => pattern.test(text));
|
|
15
|
+
if (!noCodePlanning)
|
|
16
|
+
return false;
|
|
17
|
+
return [
|
|
18
|
+
/\b(?:create|open|file|add|split|decompose|break down)[^\n.]{0,100}\b(?:child issues|follow-?up issues|issues|tickets|tasks)\b/,
|
|
19
|
+
/\b(?:child issues|follow-?up issues|issues|tickets|tasks)[^\n.]{0,100}\b(?:create|open|file|add|split|decompose|break down)\b/,
|
|
20
|
+
/(?:поставь|создай|заведи|добавь|разбей)[^\n.]{0,100}(?:задач|тикет|issue)/,
|
|
21
|
+
/(?:задач|тикет|issue)[^\n.]{0,100}(?:поставь|создай|заведи|добавь|разбей)/,
|
|
22
|
+
].some((pattern) => pattern.test(text));
|
|
23
|
+
}
|
|
1
24
|
export function classifyIssue(params) {
|
|
2
25
|
if (params.issue.parentLinearIssueId) {
|
|
3
26
|
return { issueClass: "implementation", issueClassSource: "hierarchy" };
|
|
@@ -14,5 +37,8 @@ export function classifyIssue(params) {
|
|
|
14
37
|
if (params.issue.issueClassSource === "triage" && params.issue.issueClass) {
|
|
15
38
|
return { issueClass: params.issue.issueClass, issueClassSource: "triage" };
|
|
16
39
|
}
|
|
40
|
+
if (hasExplicitNoCodePlanningSplitIntent(params.issue)) {
|
|
41
|
+
return { issueClass: "orchestration", issueClassSource: "heuristic" };
|
|
42
|
+
}
|
|
17
43
|
return { issueClass: "implementation", issueClassSource: "heuristic" };
|
|
18
44
|
}
|
|
@@ -6,6 +6,11 @@ export function resolveRetryTarget(params) {
|
|
|
6
6
|
if (hasOpenPr(params.prNumber, params.prState) && params.lastGitHubFailureSource === "queue_eviction") {
|
|
7
7
|
return { runType: "queue_repair", factoryState: "repairing_queue" };
|
|
8
8
|
}
|
|
9
|
+
if (hasOpenPr(params.prNumber, params.prState)
|
|
10
|
+
&& params.prReviewState === "approved"
|
|
11
|
+
&& (params.factoryState === "awaiting_queue" || params.lastRunType === "queue_repair")) {
|
|
12
|
+
return { runType: "queue_repair", factoryState: "repairing_queue" };
|
|
13
|
+
}
|
|
9
14
|
if (hasOpenPr(params.prNumber, params.prState)
|
|
10
15
|
&& (params.prCheckStatus === "failed" || params.prCheckStatus === "failure" || params.lastGitHubFailureSource === "branch_ci")) {
|
|
11
16
|
return { runType: "ci_repair", factoryState: "repairing_ci" };
|
|
@@ -19,6 +19,13 @@ export function buildOperatorRetryEvent(issue, runType, source = "operator_retry
|
|
|
19
19
|
...(queueIncident ?? {}),
|
|
20
20
|
...(failureContext ?? {}),
|
|
21
21
|
source,
|
|
22
|
+
requiresFreshHead: true,
|
|
23
|
+
promptContext: [
|
|
24
|
+
"Operator retry is recovering a merge queue rejection on an approved PR.",
|
|
25
|
+
"If the previous repair left the same head SHA in place, merge-steward may still consider it terminally evicted.",
|
|
26
|
+
"Preserve the approved diff, but publish a new head SHA on the existing PR branch before finishing.",
|
|
27
|
+
"If rebasing onto the current base produces no content change, create an empty queue-kick commit.",
|
|
28
|
+
].join(" "),
|
|
22
29
|
}),
|
|
23
30
|
dedupeKey: `${source}:queue_repair:${issue.linearIssueId}:${issue.prHeadSha ?? issue.lastGitHubFailureHeadSha ?? "unknown-sha"}`,
|
|
24
31
|
};
|
|
@@ -6,7 +6,7 @@ export function computeOrchestrationSettleUntil(now = Date.now()) {
|
|
|
6
6
|
function resolveOrchestrationIssueClass(db, issue) {
|
|
7
7
|
return classifyIssue({
|
|
8
8
|
issue,
|
|
9
|
-
childIssueCount: db.issues.
|
|
9
|
+
childIssueCount: db.issues.listCanonicalChildIssues(issue.projectId, issue.linearIssueId).length,
|
|
10
10
|
}).issueClass;
|
|
11
11
|
}
|
|
12
12
|
function unique(values) {
|
|
@@ -531,7 +531,7 @@ function buildPrePushSelfReviewSection(target, runType) {
|
|
|
531
531
|
}
|
|
532
532
|
return lines;
|
|
533
533
|
}
|
|
534
|
-
function buildPublicationContract(runType, issueClass) {
|
|
534
|
+
function buildPublicationContract(runType, issueClass, context) {
|
|
535
535
|
if (issueClass === "orchestration") {
|
|
536
536
|
return [
|
|
537
537
|
"## Publish",
|
|
@@ -554,6 +554,7 @@ function buildPublicationContract(runType, issueClass) {
|
|
|
554
554
|
...buildPrePushSelfReviewSection("new_pr", runType),
|
|
555
555
|
].join("\n");
|
|
556
556
|
}
|
|
557
|
+
const requiresFreshQueueHead = runType === "queue_repair" && context?.requiresFreshHead === true;
|
|
557
558
|
return [
|
|
558
559
|
"## Publish",
|
|
559
560
|
"",
|
|
@@ -561,8 +562,16 @@ function buildPublicationContract(runType, issueClass) {
|
|
|
561
562
|
"Do not open a new PR.",
|
|
562
563
|
"A PR-less stop is not a successful outcome for a repair run unless a genuine external blocker prevents any correct push.",
|
|
563
564
|
"",
|
|
564
|
-
|
|
565
|
-
|
|
565
|
+
...(requiresFreshQueueHead
|
|
566
|
+
? [
|
|
567
|
+
"This queue repair requires a fresh PR head SHA because the previous head was terminally evicted by merge-steward.",
|
|
568
|
+
"Before pushing, compute `git diff $(git merge-base origin/main HEAD)..HEAD | git patch-id --stable` and compare its first field to the `Last published patch-id` shown in the prompt header (if any).",
|
|
569
|
+
"If the patch-id matches, preserve the approved diff and still push a new head SHA on the existing PR branch. Prefer a real rebase onto current `origin/main`; if that produces no content change, create an empty queue-kick commit.",
|
|
570
|
+
]
|
|
571
|
+
: [
|
|
572
|
+
"Before pushing, compute `git diff $(git merge-base origin/main HEAD)..HEAD | git patch-id --stable` and compare its first field to the `Last published patch-id` shown in the prompt header (if any).",
|
|
573
|
+
"If they match, do not push — finish the run as a no-op. Edit the PR body via `gh pr edit` instead if a textual update is needed.",
|
|
574
|
+
]),
|
|
566
575
|
"",
|
|
567
576
|
...buildPrePushSelfReviewSection("existing_pr", runType),
|
|
568
577
|
].join("\n");
|
|
@@ -586,7 +595,7 @@ function buildSections(issue, runType, repoPath, context, followUp = false) {
|
|
|
586
595
|
if (workflow) {
|
|
587
596
|
sections.push({ id: "workflow-guidance", content: workflow });
|
|
588
597
|
}
|
|
589
|
-
sections.push({ id: "publication-contract", content: buildPublicationContract(runType, issueClass) });
|
|
598
|
+
sections.push({ id: "publication-contract", content: buildPublicationContract(runType, issueClass, context) });
|
|
590
599
|
return sections;
|
|
591
600
|
}
|
|
592
601
|
function filterAllowedReplacements(promptLayer) {
|
|
@@ -12,6 +12,8 @@ function isDuplicateProbe(issue, context) {
|
|
|
12
12
|
const headSha = typeof context?.failureHeadSha === "string" ? context.failureHeadSha : undefined;
|
|
13
13
|
if (!signature)
|
|
14
14
|
return false;
|
|
15
|
+
if (context?.requiresFreshHead === true)
|
|
16
|
+
return false;
|
|
15
17
|
return issue.lastAttemptedFailureSignature === signature
|
|
16
18
|
&& (headSha === undefined || issue.lastAttemptedFailureHeadSha === headSha);
|
|
17
19
|
}
|
|
@@ -133,12 +135,25 @@ export class QueueHealthMonitor {
|
|
|
133
135
|
if (isDirty || hasEvictionCheckRun) {
|
|
134
136
|
const headRefOid = pr.headRefOid ?? "unknown";
|
|
135
137
|
const reason = hasEvictionCheckRun ? "queue_eviction_missed" : "preemptive_conflict";
|
|
136
|
-
const signature =
|
|
138
|
+
const signature = hasEvictionCheckRun
|
|
139
|
+
? `same_head_queue_eviction:${headRefOid}`
|
|
140
|
+
: `preemptive_queue_conflict:${headRefOid}`;
|
|
137
141
|
const pendingRunContext = {
|
|
138
142
|
source: "queue_health_monitor",
|
|
139
143
|
failureReason: reason,
|
|
140
144
|
failureHeadSha: headRefOid,
|
|
141
145
|
failureSignature: signature,
|
|
146
|
+
...(hasEvictionCheckRun
|
|
147
|
+
? {
|
|
148
|
+
requiresFreshHead: true,
|
|
149
|
+
promptContext: [
|
|
150
|
+
`merge-steward/queue is already failed on PR #${issue.prNumber} at head ${headRefOid}.`,
|
|
151
|
+
"merge-steward will not re-admit the same evicted head SHA.",
|
|
152
|
+
"Preserve the approved diff, but publish a new head SHA on the existing PR branch before finishing.",
|
|
153
|
+
"If rebasing onto the current base produces no content change, create an empty queue-kick commit.",
|
|
154
|
+
].join(" "),
|
|
155
|
+
}
|
|
156
|
+
: {}),
|
|
142
157
|
};
|
|
143
158
|
if (isDuplicateProbe(issue, pendingRunContext)) {
|
|
144
159
|
return;
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -186,7 +186,7 @@ export class RunOrchestrator {
|
|
|
186
186
|
...(entry.blockerCurrentLinearStateType ? { stateType: entry.blockerCurrentLinearStateType } : {}),
|
|
187
187
|
}));
|
|
188
188
|
const childIssues = this.db.issues
|
|
189
|
-
.
|
|
189
|
+
.listCanonicalChildIssues(issue.projectId, issue.linearIssueId)
|
|
190
190
|
.map((entry) => ({
|
|
191
191
|
linearIssueId: entry.linearIssueId,
|
|
192
192
|
...(entry.issueKey ? { issueKey: entry.issueKey } : {}),
|
|
@@ -205,7 +205,7 @@ export class RunOrchestrator {
|
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
async classifyTrackedIssue(issue) {
|
|
208
|
-
const childIssues = this.db.issues.
|
|
208
|
+
const childIssues = this.db.issues.listCanonicalChildIssues(issue.projectId, issue.linearIssueId);
|
|
209
209
|
const classification = classifyIssue({ issue, childIssueCount: childIssues.length });
|
|
210
210
|
const triageHash = buildIssueTriageHash({ issue, childIssues });
|
|
211
211
|
const triageCacheFresh = issue.issueClassSource === "triage" && issue.issueTriageHash === triageHash;
|
|
@@ -108,6 +108,7 @@ export class ServiceIssueActions {
|
|
|
108
108
|
prState: issue.prState,
|
|
109
109
|
prReviewState: issue.prReviewState,
|
|
110
110
|
prCheckStatus: issue.prCheckStatus,
|
|
111
|
+
factoryState: issue.factoryState,
|
|
111
112
|
pendingRunType: issue.pendingRunType,
|
|
112
113
|
lastRunType: issueSession?.lastRunType,
|
|
113
114
|
lastGitHubFailureSource: issue.lastGitHubFailureSource,
|
|
@@ -30,7 +30,7 @@ export class CommentWakeHandler {
|
|
|
30
30
|
return;
|
|
31
31
|
const issueClass = classifyIssue({
|
|
32
32
|
issue,
|
|
33
|
-
childIssueCount: this.db.issues.
|
|
33
|
+
childIssueCount: this.db.issues.listCanonicalChildIssues(project.id, normalized.issue.id).length,
|
|
34
34
|
}).issueClass;
|
|
35
35
|
const trimmedBody = normalized.comment.body.trim();
|
|
36
36
|
const installation = this.db.linearInstallations.getLinearInstallationForProject(project.id);
|
|
@@ -75,7 +75,7 @@ export class DesiredStageRecorder {
|
|
|
75
75
|
terminal,
|
|
76
76
|
currentState: existingIssue?.factoryState,
|
|
77
77
|
});
|
|
78
|
-
const childIssueCount = this.db.issues.
|
|
78
|
+
const childIssueCount = this.db.issues.listCanonicalChildIssues(params.project.id, normalizedIssue.id).length;
|
|
79
79
|
const classification = classifyIssue({
|
|
80
80
|
issue: {
|
|
81
81
|
issueClass: existingIssue?.issueClass,
|