patchrelay 0.50.4 → 0.50.6
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/watch/issue-token.js +15 -4
- package/dist/cli/watch/state-visualization.js +23 -3
- package/dist/config.js +2 -0
- package/dist/delegation-linked-pr.js +2 -2
- package/dist/linear-session-reporting.js +14 -0
- package/dist/linear-status-comment-sync.js +21 -1
- package/dist/pr-display-context.js +21 -0
- package/dist/prompting/patchrelay.js +44 -8
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isUndelegatedPausedIssue } from "../../paused-issue-state.js";
|
|
2
|
+
import { derivePrDisplayContext } from "../../pr-display-context.js";
|
|
2
3
|
import { hasFailedPrChecks, hasPendingPrChecks, isApprovedReviewState, isAwaitingReviewState, isChangesRequestedReviewState, prChecksFact, } from "./pr-status.js";
|
|
3
4
|
const GLYPH = {
|
|
4
5
|
running: "\u25cf",
|
|
@@ -89,9 +90,14 @@ export function prTokenFor(issue) {
|
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
92
|
function prKind(issue) {
|
|
92
|
-
|
|
93
|
+
const prContext = derivePrDisplayContext(issue);
|
|
94
|
+
if (prContext.kind === "merged_pr")
|
|
93
95
|
return "approved";
|
|
94
|
-
if (
|
|
96
|
+
if (prContext.kind === "closed_replacement_pending")
|
|
97
|
+
return "queued";
|
|
98
|
+
if (prContext.kind === "closed_pr_paused")
|
|
99
|
+
return "attention";
|
|
100
|
+
if (prContext.kind === "closed_historical_pr")
|
|
95
101
|
return "declined";
|
|
96
102
|
if (issue.prReviewState === "approved")
|
|
97
103
|
return "approved";
|
|
@@ -104,9 +110,14 @@ function prKind(issue) {
|
|
|
104
110
|
return "running";
|
|
105
111
|
}
|
|
106
112
|
function prPhraseFor(issue) {
|
|
107
|
-
|
|
113
|
+
const prContext = derivePrDisplayContext(issue);
|
|
114
|
+
if (prContext.kind === "merged_pr")
|
|
108
115
|
return "merged";
|
|
109
|
-
if (
|
|
116
|
+
if (prContext.kind === "closed_replacement_pending")
|
|
117
|
+
return "replace pr";
|
|
118
|
+
if (prContext.kind === "closed_pr_paused")
|
|
119
|
+
return "redelegate";
|
|
120
|
+
if (prContext.kind === "closed_historical_pr")
|
|
110
121
|
return "closed";
|
|
111
122
|
if (isChangesRequestedReviewState(issue.prReviewState))
|
|
112
123
|
return "changes req";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasOpenPr } from "../../pr-state.js";
|
|
2
|
+
import { derivePrDisplayContext } from "../../pr-display-context.js";
|
|
2
3
|
const STATE_LABELS = {
|
|
3
4
|
delegated: "delegated",
|
|
4
5
|
implementing: "implementing",
|
|
@@ -164,9 +165,28 @@ export function buildPatchRelayQueueObservations(issue, feedEvents) {
|
|
|
164
165
|
});
|
|
165
166
|
}
|
|
166
167
|
if (issue.prNumber !== undefined) {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
const prContext = derivePrDisplayContext(issue);
|
|
169
|
+
let prLabel;
|
|
170
|
+
switch (prContext.kind) {
|
|
171
|
+
case "active_pr":
|
|
172
|
+
case "merged_pr":
|
|
173
|
+
prLabel = hasOpenPr(issue.prNumber, issue.prState)
|
|
174
|
+
? `Tracked PR: #${issue.prNumber}`
|
|
175
|
+
: `Tracked PR: #${issue.prNumber}${issue.prState ? ` (${issue.prState})` : ""}`;
|
|
176
|
+
break;
|
|
177
|
+
case "closed_historical_pr":
|
|
178
|
+
prLabel = `Previous PR: #${prContext.prNumber} (closed)`;
|
|
179
|
+
break;
|
|
180
|
+
case "closed_replacement_pending":
|
|
181
|
+
prLabel = `Previous PR: #${prContext.prNumber} (closed; replacement pending)`;
|
|
182
|
+
break;
|
|
183
|
+
case "closed_pr_paused":
|
|
184
|
+
prLabel = `Previous PR: #${prContext.prNumber} (closed; redelegate to replace)`;
|
|
185
|
+
break;
|
|
186
|
+
case "no_pr":
|
|
187
|
+
prLabel = "PR context unavailable";
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
170
190
|
observations.push({
|
|
171
191
|
tone: "info",
|
|
172
192
|
text: `${prLabel}${issue.prReviewState ? ` (${issue.prReviewState})` : ""}`,
|
package/dist/config.js
CHANGED
|
@@ -21,6 +21,8 @@ const DEFAULT_PATCHRELAY_DEVELOPER_INSTRUCTIONS = [
|
|
|
21
21
|
"- If you change files for an implementation run, commit, push the issue branch, and open or update the PR.",
|
|
22
22
|
"- For repair runs, work on the existing PR branch and do not open a new PR.",
|
|
23
23
|
"- A requested-changes repair is only complete after a newer PR head is pushed, unless a genuine external blocker prevents correct publication.",
|
|
24
|
+
"- If you change schema, enums, shared vocabulary, normalization helpers, or compatibility mappings, inspect the main read/write paths that can bypass the new abstraction and fix or cover any mismatch before publishing.",
|
|
25
|
+
"- For CI repair, do not change code or config until you either reproduce the failure on the exact failing head or can point to a concrete log signature that justifies the fix. If you cannot reproduce it, prefer a rerun-only repair over speculative changes.",
|
|
24
26
|
"- If a broader inconsistency is not required to make this task correct, mention it briefly instead of expanding scope.",
|
|
25
27
|
"- Before publishing, do one brief reviewer-minded pass on the current head and fix likely in-scope blockers.",
|
|
26
28
|
].join("\n");
|
|
@@ -45,8 +45,8 @@ export function deriveLinkedPrAdoptionOutcome(project, prNumber, remote) {
|
|
|
45
45
|
}
|
|
46
46
|
if (prState === "closed") {
|
|
47
47
|
return {
|
|
48
|
-
factoryState: "
|
|
49
|
-
pendingRunType:
|
|
48
|
+
factoryState: "delegated",
|
|
49
|
+
pendingRunType: "implementation",
|
|
50
50
|
issueUpdates: {
|
|
51
51
|
...issueUpdates,
|
|
52
52
|
prIsDraft: false,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { formatRunTypeLabel } from "./agent-session-plan.js";
|
|
2
2
|
import { sanitizeOperatorFacingText } from "./presentation-text.js";
|
|
3
3
|
import { isClosedPrState } from "./pr-state.js";
|
|
4
|
+
import { derivePrDisplayContext } from "./pr-display-context.js";
|
|
4
5
|
function lowerRunTypeLabel(runType) {
|
|
5
6
|
return formatRunTypeLabel(runType).toLowerCase();
|
|
6
7
|
}
|
|
@@ -187,6 +188,7 @@ export function buildMergePrepEscalationActivity(attempts) {
|
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
190
|
export function summarizeIssueStateForLinear(issue) {
|
|
191
|
+
const prContext = derivePrDisplayContext(issue);
|
|
190
192
|
switch (issue.sessionState) {
|
|
191
193
|
case "waiting_input":
|
|
192
194
|
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} is waiting for input.` : "Waiting for input.");
|
|
@@ -208,11 +210,23 @@ export function summarizeIssueStateForLinear(issue) {
|
|
|
208
210
|
}
|
|
209
211
|
switch (issue.factoryState) {
|
|
210
212
|
case "delegated":
|
|
213
|
+
if (prContext.kind === "closed_replacement_pending") {
|
|
214
|
+
return `Queued to replace closed PR #${prContext.prNumber}.`;
|
|
215
|
+
}
|
|
216
|
+
if (prContext.kind === "closed_pr_paused") {
|
|
217
|
+
return `Closed PR #${prContext.prNumber} needs redelegation before replacement.`;
|
|
218
|
+
}
|
|
211
219
|
if (!issue.delegatedToPatchRelay) {
|
|
212
220
|
return "PatchRelay is queued to start work, but automation is paused.";
|
|
213
221
|
}
|
|
214
222
|
return "Queued to start work.";
|
|
215
223
|
case "implementing":
|
|
224
|
+
if (prContext.kind === "closed_replacement_pending") {
|
|
225
|
+
return `Replacing closed PR #${prContext.prNumber} with a fresh PR.`;
|
|
226
|
+
}
|
|
227
|
+
if (prContext.kind === "closed_pr_paused") {
|
|
228
|
+
return `Closed PR #${prContext.prNumber} needs redelegation before replacement.`;
|
|
229
|
+
}
|
|
216
230
|
if (!issue.delegatedToPatchRelay) {
|
|
217
231
|
return "Implementation is paused because the issue is undelegated.";
|
|
218
232
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { extractCompletionCheck } from "./completion-check.js";
|
|
2
2
|
import { isClosedPrState } from "./pr-state.js";
|
|
3
|
+
import { derivePrDisplayContext } from "./pr-display-context.js";
|
|
3
4
|
import { deriveIssueStatusNote } from "./status-note.js";
|
|
4
5
|
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
5
6
|
export async function syncVisibleStatusComment(params) {
|
|
@@ -84,8 +85,17 @@ function renderStatusComment(db, issue, trackedIssue, options) {
|
|
|
84
85
|
lines.push("", `Reply in a Linear comment to continue, or run \`patchrelay issue prompt ${issueRef} "..."\`.`);
|
|
85
86
|
}
|
|
86
87
|
if (issue.prNumber !== undefined || issue.prUrl) {
|
|
88
|
+
const prContext = derivePrDisplayContext(issue);
|
|
87
89
|
const prLabel = issue.prNumber !== undefined ? `#${issue.prNumber}` : "open";
|
|
88
|
-
|
|
90
|
+
const linkedLabel = issue.prUrl ? `[${prLabel}](${issue.prUrl})` : prLabel;
|
|
91
|
+
const prLine = prContext.kind === "closed_historical_pr"
|
|
92
|
+
? `Previous PR: ${linkedLabel} (closed)`
|
|
93
|
+
: prContext.kind === "closed_replacement_pending"
|
|
94
|
+
? `Previous PR: ${linkedLabel} (closed; replacement PR needed)`
|
|
95
|
+
: prContext.kind === "closed_pr_paused"
|
|
96
|
+
? `Previous PR: ${linkedLabel} (closed; redelegate to replace it)`
|
|
97
|
+
: `PR: ${linkedLabel}`;
|
|
98
|
+
lines.push("", prLine);
|
|
89
99
|
}
|
|
90
100
|
if (latestRun) {
|
|
91
101
|
lines.push("", `Latest run: ${formatLatestRun(latestRun)}`);
|
|
@@ -103,6 +113,7 @@ function renderStatusComment(db, issue, trackedIssue, options) {
|
|
|
103
113
|
return lines.join("\n");
|
|
104
114
|
}
|
|
105
115
|
function statusHeadline(issue, activeRunType) {
|
|
116
|
+
const prContext = derivePrDisplayContext(issue);
|
|
106
117
|
if (activeRunType) {
|
|
107
118
|
return `Running ${humanize(activeRunType)}`;
|
|
108
119
|
}
|
|
@@ -123,6 +134,9 @@ function statusHeadline(issue, activeRunType) {
|
|
|
123
134
|
break;
|
|
124
135
|
}
|
|
125
136
|
if (!issue.delegatedToPatchRelay && issue.prNumber !== undefined) {
|
|
137
|
+
if (prContext.kind === "closed_pr_paused") {
|
|
138
|
+
return `Closed PR #${prContext.prNumber} is waiting for redelegation before replacement`;
|
|
139
|
+
}
|
|
126
140
|
if (issue.factoryState === "awaiting_queue" || issue.prReviewState === "approved") {
|
|
127
141
|
return `PR #${issue.prNumber} is awaiting downstream merge while PatchRelay is paused`;
|
|
128
142
|
}
|
|
@@ -144,8 +158,14 @@ function statusHeadline(issue, activeRunType) {
|
|
|
144
158
|
}
|
|
145
159
|
switch (issue.factoryState) {
|
|
146
160
|
case "delegated":
|
|
161
|
+
if (prContext.kind === "closed_replacement_pending") {
|
|
162
|
+
return `Queued to replace closed PR #${prContext.prNumber}`;
|
|
163
|
+
}
|
|
147
164
|
return "Queued to start work";
|
|
148
165
|
case "implementing":
|
|
166
|
+
if (prContext.kind === "closed_replacement_pending") {
|
|
167
|
+
return `Replacing closed PR #${prContext.prNumber} with a fresh PR`;
|
|
168
|
+
}
|
|
149
169
|
return "Implementing requested change";
|
|
150
170
|
case "pr_open":
|
|
151
171
|
return issue.prNumber !== undefined ? `PR #${issue.prNumber} opened` : "PR opened";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function isTerminalFactoryState(factoryState) {
|
|
2
|
+
return factoryState === "done" || factoryState === "failed" || factoryState === "escalated";
|
|
3
|
+
}
|
|
4
|
+
export function derivePrDisplayContext(issue) {
|
|
5
|
+
if (issue.prNumber === undefined) {
|
|
6
|
+
return { kind: "no_pr" };
|
|
7
|
+
}
|
|
8
|
+
if (issue.prState === "merged") {
|
|
9
|
+
return { kind: "merged_pr", prNumber: issue.prNumber };
|
|
10
|
+
}
|
|
11
|
+
if (issue.prState === "closed") {
|
|
12
|
+
if (isTerminalFactoryState(issue.factoryState)) {
|
|
13
|
+
return { kind: "closed_historical_pr", prNumber: issue.prNumber };
|
|
14
|
+
}
|
|
15
|
+
if (issue.delegatedToPatchRelay === false) {
|
|
16
|
+
return { kind: "closed_pr_paused", prNumber: issue.prNumber };
|
|
17
|
+
}
|
|
18
|
+
return { kind: "closed_replacement_pending", prNumber: issue.prNumber };
|
|
19
|
+
}
|
|
20
|
+
return { kind: "active_pr", prNumber: issue.prNumber };
|
|
21
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { derivePrDisplayContext } from "../pr-display-context.js";
|
|
3
4
|
const WORKFLOW_FILES = {
|
|
4
5
|
implementation: "IMPLEMENTATION_WORKFLOW.md",
|
|
5
6
|
review_fix: "REVIEW_WORKFLOW.md",
|
|
@@ -28,11 +29,23 @@ function hasWorkflowFile(repoPath, runType) {
|
|
|
28
29
|
return existsSync(filePath);
|
|
29
30
|
}
|
|
30
31
|
function buildPromptHeader(issue) {
|
|
32
|
+
const prContext = derivePrDisplayContext(issue);
|
|
33
|
+
const prLine = prContext.kind === "active_pr"
|
|
34
|
+
? `PR: #${prContext.prNumber}`
|
|
35
|
+
: prContext.kind === "merged_pr"
|
|
36
|
+
? `Merged PR: #${prContext.prNumber}`
|
|
37
|
+
: prContext.kind === "closed_historical_pr"
|
|
38
|
+
? `Previous PR: #${prContext.prNumber} (closed)`
|
|
39
|
+
: prContext.kind === "closed_replacement_pending"
|
|
40
|
+
? `Previous PR: #${prContext.prNumber} (closed; replacement PR needed)`
|
|
41
|
+
: prContext.kind === "closed_pr_paused"
|
|
42
|
+
? `Previous PR: #${prContext.prNumber} (closed; redelegate to replace it)`
|
|
43
|
+
: undefined;
|
|
31
44
|
return [
|
|
32
45
|
`Issue: ${issue.issueKey ?? issue.linearIssueId}`,
|
|
33
46
|
issue.title ? `Title: ${issue.title}` : undefined,
|
|
34
47
|
issue.branchName ? `Branch: ${issue.branchName}` : undefined,
|
|
35
|
-
|
|
48
|
+
prLine,
|
|
36
49
|
].filter(Boolean).join("\n");
|
|
37
50
|
}
|
|
38
51
|
function extractIssueSection(description, heading) {
|
|
@@ -290,6 +303,8 @@ function buildCiRepairContext(context) {
|
|
|
290
303
|
return [
|
|
291
304
|
"Settled CI failure on the existing PR branch.",
|
|
292
305
|
"Goal: restore CI readiness and push a branch that is likely to pass the next full CI run.",
|
|
306
|
+
"Before changing code or config, reproduce the failure on the exact failing head or identify the concrete log signature that justifies the fix.",
|
|
307
|
+
"If the exact failing head does not reproduce locally and the logs do not support a scoped fix, prefer a rerun-only repair over speculative branch changes.",
|
|
293
308
|
snapshot?.gateCheckName ? `Gate check: ${String(snapshot.gateCheckName)}` : "",
|
|
294
309
|
snapshot?.gateCheckStatus ? `Gate status: ${String(snapshot.gateCheckStatus)}` : "",
|
|
295
310
|
snapshot?.settledAt ? `Settled at: ${String(snapshot.settledAt)}` : "",
|
|
@@ -352,6 +367,7 @@ function buildQueueRepairContext(context) {
|
|
|
352
367
|
return lines.filter(Boolean).join("\n");
|
|
353
368
|
}
|
|
354
369
|
function buildFollowUpContextLines(issue, runType, context) {
|
|
370
|
+
const prContext = derivePrDisplayContext(issue);
|
|
355
371
|
const wakeReason = typeof context?.wakeReason === "string" ? context.wakeReason : undefined;
|
|
356
372
|
const followUps = Array.isArray(context?.followUps) ? context.followUps : [];
|
|
357
373
|
const followUpLines = followUps
|
|
@@ -387,9 +403,25 @@ function buildFollowUpContextLines(issue, runType, context) {
|
|
|
387
403
|
followUpLines.forEach((line) => lines.push(`- ${line}`));
|
|
388
404
|
}
|
|
389
405
|
if (issue.prNumber || issue.prHeadSha || issue.prReviewState || context?.mergeStateStatus) {
|
|
390
|
-
|
|
406
|
+
const prHeading = prContext.kind === "closed_historical_pr"
|
|
407
|
+
|| prContext.kind === "closed_replacement_pending"
|
|
408
|
+
|| prContext.kind === "closed_pr_paused"
|
|
409
|
+
? "Previous PR facts:"
|
|
410
|
+
: "Current PR facts:";
|
|
411
|
+
const prLine = prContext.kind === "active_pr"
|
|
412
|
+
? `Current PR: #${prContext.prNumber}`
|
|
413
|
+
: prContext.kind === "merged_pr"
|
|
414
|
+
? `Merged PR: #${prContext.prNumber}`
|
|
415
|
+
: prContext.kind === "closed_historical_pr"
|
|
416
|
+
? `Previous PR: #${prContext.prNumber} (closed)`
|
|
417
|
+
: prContext.kind === "closed_replacement_pending"
|
|
418
|
+
? `Previous PR: #${prContext.prNumber} (closed; replacement PR needed)`
|
|
419
|
+
: prContext.kind === "closed_pr_paused"
|
|
420
|
+
? `Previous PR: #${prContext.prNumber} (closed; redelegate to replace it)`
|
|
421
|
+
: "";
|
|
422
|
+
lines.push("", prHeading, `Fact freshness: ${context?.githubFactsFresh === true
|
|
391
423
|
? "refreshed immediately before this turn was created."
|
|
392
|
-
: "may now be stale; refresh before making irreversible decisions."}`,
|
|
424
|
+
: "may now be stale; refresh before making irreversible decisions."}`, prLine, issue.prHeadSha ? `Current relevant head SHA: ${issue.prHeadSha}` : "", issue.prReviewState ? `Current review state: ${issue.prReviewState}` : "", typeof context?.mergeStateStatus === "string" ? `Merge state against ${String(context?.baseBranch ?? "main")}: ${String(context.mergeStateStatus)}` : "");
|
|
393
425
|
}
|
|
394
426
|
return lines.filter(Boolean);
|
|
395
427
|
}
|
|
@@ -443,17 +475,21 @@ function buildOrchestrationWorkflowGuidance() {
|
|
|
443
475
|
"Keep outputs concise and observable in Linear.",
|
|
444
476
|
].join("\n");
|
|
445
477
|
}
|
|
446
|
-
function buildPrePushSelfReviewSection(target) {
|
|
478
|
+
function buildPrePushSelfReviewSection(target, runType) {
|
|
447
479
|
const publishTarget = target === "new_pr"
|
|
448
480
|
? "open or update the PR"
|
|
449
481
|
: "push the existing PR branch";
|
|
450
|
-
|
|
482
|
+
const lines = [
|
|
451
483
|
"## Final Self-Review Before Push",
|
|
452
484
|
"",
|
|
453
485
|
`Before you ${publishTarget}, do one brief reviewer-minded pass on the current head.`,
|
|
454
486
|
"Fix any likely in-scope blocker you can see now: missing edge-case handling, broken adjacent invariant in the touched flow, mismatch between the PR explanation and the code, or an obviously unreviewable half-finished branch.",
|
|
455
|
-
"Do not widen scope for optional cleanup. If the issue explicitly allows a non-PR outcome, complete that outcome clearly; otherwise publish before stopping.",
|
|
456
487
|
];
|
|
488
|
+
if (runType === "implementation") {
|
|
489
|
+
lines.push("Name 2-4 concrete invariants most likely to regress in the touched flow, confirm which file or path enforces each one, and verify at least one adjacent path you did not edit directly.", "If you changed schema, enums, shared vocabulary, normalization helpers, or compatibility mappings, inspect the main read/write paths that can bypass the new abstraction and verify one legacy-flow and one new-flow case before publishing.");
|
|
490
|
+
}
|
|
491
|
+
lines.push("Do not widen scope for optional cleanup. If the issue explicitly allows a non-PR outcome, complete that outcome clearly; otherwise publish before stopping.");
|
|
492
|
+
return lines;
|
|
457
493
|
}
|
|
458
494
|
function buildPublicationContract(runType, issueClass) {
|
|
459
495
|
if (issueClass === "orchestration") {
|
|
@@ -471,7 +507,7 @@ function buildPublicationContract(runType, issueClass) {
|
|
|
471
507
|
"If this is code-delivery work, publish before stopping: commit, push the issue branch, and open or update the PR.",
|
|
472
508
|
"If the issue explicitly allows a non-PR outcome, complete that outcome clearly instead of inventing a PR.",
|
|
473
509
|
"",
|
|
474
|
-
...buildPrePushSelfReviewSection("new_pr"),
|
|
510
|
+
...buildPrePushSelfReviewSection("new_pr", runType),
|
|
475
511
|
].join("\n");
|
|
476
512
|
}
|
|
477
513
|
return [
|
|
@@ -481,7 +517,7 @@ function buildPublicationContract(runType, issueClass) {
|
|
|
481
517
|
"Do not open a new PR.",
|
|
482
518
|
"A PR-less stop is not a successful outcome for a repair run unless a genuine external blocker prevents any correct push.",
|
|
483
519
|
"",
|
|
484
|
-
...buildPrePushSelfReviewSection("existing_pr"),
|
|
520
|
+
...buildPrePushSelfReviewSection("existing_pr", runType),
|
|
485
521
|
].join("\n");
|
|
486
522
|
}
|
|
487
523
|
function buildSections(issue, runType, repoPath, context, followUp = false) {
|