patchrelay 0.36.3 → 0.36.5
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/db.js +7 -0
- package/dist/idle-reconciliation.js +19 -0
- package/dist/issue-session.js +4 -1
- package/dist/run-orchestrator.js +83 -24
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/db.js
CHANGED
|
@@ -357,6 +357,7 @@ export class PatchRelayDatabase {
|
|
|
357
357
|
last_github_ci_snapshot_head_sha, last_github_ci_snapshot_gate_check_name, last_github_ci_snapshot_gate_check_status, last_github_ci_snapshot_json, last_github_ci_snapshot_settled_at,
|
|
358
358
|
last_queue_signal_at, last_queue_incident_json,
|
|
359
359
|
last_attempted_failure_head_sha, last_attempted_failure_signature,
|
|
360
|
+
ci_repair_attempts, queue_repair_attempts, review_fix_attempts, zombie_recovery_attempts, last_zombie_recovery_at,
|
|
360
361
|
updated_at
|
|
361
362
|
) VALUES (
|
|
362
363
|
@projectId, @linearIssueId, @issueKey, @title, @description, @url,
|
|
@@ -369,6 +370,7 @@ export class PatchRelayDatabase {
|
|
|
369
370
|
@lastGitHubCiSnapshotHeadSha, @lastGitHubCiSnapshotGateCheckName, @lastGitHubCiSnapshotGateCheckStatus, @lastGitHubCiSnapshotJson, @lastGitHubCiSnapshotSettledAt,
|
|
370
371
|
@lastQueueSignalAt, @lastQueueIncidentJson,
|
|
371
372
|
@lastAttemptedFailureHeadSha, @lastAttemptedFailureSignature,
|
|
373
|
+
@ciRepairAttempts, @queueRepairAttempts, @reviewFixAttempts, @zombieRecoveryAttempts, @lastZombieRecoveryAt,
|
|
372
374
|
@now
|
|
373
375
|
)
|
|
374
376
|
`).run({
|
|
@@ -415,6 +417,11 @@ export class PatchRelayDatabase {
|
|
|
415
417
|
lastQueueIncidentJson: params.lastQueueIncidentJson ?? null,
|
|
416
418
|
lastAttemptedFailureHeadSha: params.lastAttemptedFailureHeadSha ?? null,
|
|
417
419
|
lastAttemptedFailureSignature: params.lastAttemptedFailureSignature ?? null,
|
|
420
|
+
ciRepairAttempts: params.ciRepairAttempts ?? 0,
|
|
421
|
+
queueRepairAttempts: params.queueRepairAttempts ?? 0,
|
|
422
|
+
reviewFixAttempts: params.reviewFixAttempts ?? 0,
|
|
423
|
+
zombieRecoveryAttempts: params.zombieRecoveryAttempts ?? 0,
|
|
424
|
+
lastZombieRecoveryAt: params.lastZombieRecoveryAt ?? null,
|
|
418
425
|
now,
|
|
419
426
|
});
|
|
420
427
|
}
|
|
@@ -491,6 +491,25 @@ export class IdleIssueReconciler {
|
|
|
491
491
|
mergeConflictDetected,
|
|
492
492
|
downstreamOwned,
|
|
493
493
|
});
|
|
494
|
+
if ((issue.factoryState === "escalated" || issue.factoryState === "failed")
|
|
495
|
+
&& (reactiveIntent?.runType === "review_fix" || reactiveIntent?.runType === "branch_upkeep")) {
|
|
496
|
+
const pendingRunContext = reactiveIntent.runType === "branch_upkeep"
|
|
497
|
+
? buildBranchUpkeepContext(issue.prNumber, project.github?.baseBranch ?? "main", pr.mergeStateStatus, pr.headRefOid)
|
|
498
|
+
: undefined;
|
|
499
|
+
this.logger.info({
|
|
500
|
+
issueKey: issue.issueKey,
|
|
501
|
+
prNumber: issue.prNumber,
|
|
502
|
+
from: issue.factoryState,
|
|
503
|
+
runType: reactiveIntent.runType,
|
|
504
|
+
mergeStateStatus: pr.mergeStateStatus,
|
|
505
|
+
}, "Reconciliation: recovered terminal requested-changes issue from GitHub truth");
|
|
506
|
+
this.advanceIdleIssue(issue, reactiveIntent.compatibilityFactoryState, {
|
|
507
|
+
pendingRunType: reactiveIntent.runType,
|
|
508
|
+
...(pendingRunContext ? { pendingRunContext } : {}),
|
|
509
|
+
clearFailureProvenance: true,
|
|
510
|
+
});
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
494
513
|
if (reactiveIntent?.runType === "branch_upkeep" && mergeConflictDetected) {
|
|
495
514
|
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, mergeable: pr.mergeable, mergeStateStatus: pr.mergeStateStatus }, "Reconciliation: PR still needs branch upkeep after requested changes");
|
|
496
515
|
this.advanceIdleIssue(issue, reactiveIntent.compatibilityFactoryState, {
|
package/dist/issue-session.js
CHANGED
|
@@ -79,12 +79,15 @@ export function isIssueSessionReadyForExecution(params) {
|
|
|
79
79
|
return false;
|
|
80
80
|
if (params.blockedByCount > 0)
|
|
81
81
|
return false;
|
|
82
|
-
if (params.sessionState === "done" || params.sessionState === "
|
|
82
|
+
if (params.sessionState === "done" || params.sessionState === "waiting_input") {
|
|
83
83
|
return false;
|
|
84
84
|
}
|
|
85
85
|
if (params.hasPendingWake) {
|
|
86
86
|
return true;
|
|
87
87
|
}
|
|
88
|
+
if (params.sessionState === "failed") {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
88
91
|
if (!params.hasLegacyPendingRun) {
|
|
89
92
|
return false;
|
|
90
93
|
}
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -12,7 +12,7 @@ import { getThreadTurns } from "./codex-thread-utils.js";
|
|
|
12
12
|
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
13
13
|
const DEFAULT_CI_REPAIR_BUDGET = 3;
|
|
14
14
|
const DEFAULT_QUEUE_REPAIR_BUDGET = 3;
|
|
15
|
-
const DEFAULT_REVIEW_FIX_BUDGET =
|
|
15
|
+
const DEFAULT_REVIEW_FIX_BUDGET = 6;
|
|
16
16
|
const DEFAULT_ZOMBIE_RECOVERY_BUDGET = 5;
|
|
17
17
|
const ZOMBIE_RECOVERY_BASE_DELAY_MS = 15_000; // 15s, 30s, 60s, 120s, 240s
|
|
18
18
|
const ISSUE_SESSION_LEASE_MS = 10 * 60_000;
|
|
@@ -134,6 +134,34 @@ function appendTaskObjective(lines, issue) {
|
|
|
134
134
|
}
|
|
135
135
|
lines.push("");
|
|
136
136
|
}
|
|
137
|
+
function extractIssueSection(description, heading) {
|
|
138
|
+
if (!description)
|
|
139
|
+
return undefined;
|
|
140
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
141
|
+
const pattern = new RegExp(`^## ${escaped}\\s*$([\\s\\S]*?)(?=^##\\s+|$)`, "im");
|
|
142
|
+
const match = description.match(pattern);
|
|
143
|
+
const body = match?.[1]?.trim();
|
|
144
|
+
return body && body.length > 0 ? body : undefined;
|
|
145
|
+
}
|
|
146
|
+
function appendScopeDiscipline(lines, issue) {
|
|
147
|
+
const description = issue.description?.trim();
|
|
148
|
+
const scope = extractIssueSection(description, "Scope");
|
|
149
|
+
const acceptance = extractIssueSection(description, "Acceptance criteria")
|
|
150
|
+
?? extractIssueSection(description, "Success criteria");
|
|
151
|
+
const relevantCode = extractIssueSection(description, "Relevant code");
|
|
152
|
+
lines.push("## Scope Discipline", "");
|
|
153
|
+
lines.push("Stay inside the delegated task.", "Finish the issue completely enough to satisfy its stated scope and acceptance criteria, but do not widen it into unrelated product polish or follow-up cleanup.", "Only broaden to adjacent routes, copy, or supporting surfaces when the issue text or repository guidance explicitly says they are the same user flow.", "If you notice a worthwhile broader inconsistency that is not required to make this task correct, mention it in your summary as follow-up context instead of expanding the implementation.", "");
|
|
154
|
+
if (scope) {
|
|
155
|
+
lines.push("### In Scope", "", scope, "");
|
|
156
|
+
}
|
|
157
|
+
if (acceptance) {
|
|
158
|
+
lines.push("### Acceptance / Done", "", acceptance, "");
|
|
159
|
+
}
|
|
160
|
+
if (relevantCode) {
|
|
161
|
+
lines.push("### Relevant Code", "", relevantCode, "");
|
|
162
|
+
}
|
|
163
|
+
lines.push("### Likely Review Invariants", "", "- Check the surfaces explicitly named in the task before stopping.", "- If repository guidance says certain changed surfaces are one flow, verify that shared flow, but do not treat unrelated surrounding cleanup as part of this task.", "- A review repair should fix the concrete concern on the current head, not silently expand the Linear issue into a broader rewrite.", "");
|
|
164
|
+
}
|
|
137
165
|
function appendLinearContext(lines, context) {
|
|
138
166
|
const promptContext = typeof context?.promptContext === "string" ? context.promptContext.trim() : "";
|
|
139
167
|
const latestPrompt = typeof context?.promptBody === "string" ? context.promptBody.trim() : "";
|
|
@@ -404,6 +432,7 @@ function appendRequestedChangesInstructions(lines, runType, context) {
|
|
|
404
432
|
export function buildInitialRunPrompt(issue, runType, repoPath, context) {
|
|
405
433
|
const lines = buildPromptHeader(issue);
|
|
406
434
|
appendTaskObjective(lines, issue);
|
|
435
|
+
appendScopeDiscipline(lines, issue);
|
|
407
436
|
appendLinearContext(lines, context);
|
|
408
437
|
// Add run-type-specific context for reactive runs
|
|
409
438
|
switch (runType) {
|
|
@@ -440,6 +469,8 @@ export function buildInitialRunPrompt(issue, runType, repoPath, context) {
|
|
|
440
469
|
export function buildFollowUpRunPrompt(issue, runType, repoPath, context) {
|
|
441
470
|
const lines = buildPromptHeader(issue);
|
|
442
471
|
appendFollowUpPromptPrelude(lines, issue, runType, context);
|
|
472
|
+
appendTaskObjective(lines, issue);
|
|
473
|
+
appendScopeDiscipline(lines, issue);
|
|
443
474
|
// Add run-type-specific context for reactive runs
|
|
444
475
|
switch (runType) {
|
|
445
476
|
case "ci_repair": {
|
|
@@ -636,8 +667,8 @@ export class RunOrchestrator {
|
|
|
636
667
|
this.escalate(issue, runType, `Queue repair budget exhausted (${DEFAULT_QUEUE_REPAIR_BUDGET} attempts)`);
|
|
637
668
|
return;
|
|
638
669
|
}
|
|
639
|
-
if (runType
|
|
640
|
-
this.escalate(issue, runType, `
|
|
670
|
+
if (isRequestedChangesRunType(runType) && issue.reviewFixAttempts >= DEFAULT_REVIEW_FIX_BUDGET) {
|
|
671
|
+
this.escalate(issue, runType, `Requested-changes budget exhausted (${DEFAULT_REVIEW_FIX_BUDGET} attempts)`);
|
|
641
672
|
return;
|
|
642
673
|
}
|
|
643
674
|
// Increment repair counters
|
|
@@ -655,7 +686,7 @@ export class RunOrchestrator {
|
|
|
655
686
|
return;
|
|
656
687
|
}
|
|
657
688
|
}
|
|
658
|
-
if (runType
|
|
689
|
+
if (isRequestedChangesRunType(runType)) {
|
|
659
690
|
const updated = this.db.upsertIssueWithLease({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, leaseId }, { projectId: issue.projectId, linearIssueId: issue.linearIssueId, reviewFixAttempts: issue.reviewFixAttempts + 1 });
|
|
660
691
|
if (!updated) {
|
|
661
692
|
this.releaseIssueSessionLease(item.projectId, item.issueId);
|
|
@@ -1467,13 +1498,6 @@ export class RunOrchestrator {
|
|
|
1467
1498
|
queueRepairAttempts: issue.queueRepairAttempts - 1,
|
|
1468
1499
|
});
|
|
1469
1500
|
}
|
|
1470
|
-
else if (run.runType === "review_fix" && issue.reviewFixAttempts > 0) {
|
|
1471
|
-
this.db.upsertIssueWithLease(lease, {
|
|
1472
|
-
projectId: issue.projectId,
|
|
1473
|
-
linearIssueId: issue.linearIssueId,
|
|
1474
|
-
reviewFixAttempts: issue.reviewFixAttempts - 1,
|
|
1475
|
-
});
|
|
1476
|
-
}
|
|
1477
1501
|
if (run.runType === "ci_repair" || run.runType === "queue_repair") {
|
|
1478
1502
|
this.db.upsertIssueWithLease(lease, {
|
|
1479
1503
|
projectId: issue.projectId,
|
|
@@ -1490,21 +1514,56 @@ export class RunOrchestrator {
|
|
|
1490
1514
|
return;
|
|
1491
1515
|
}
|
|
1492
1516
|
if (isRequestedChangesRunType(run.runType)) {
|
|
1517
|
+
const refreshedIssue = await this.refreshIssueAfterReactivePublish(run, this.db.getIssue(run.projectId, run.linearIssueId) ?? issue);
|
|
1518
|
+
const project = this.config.projects.find((entry) => entry.id === run.projectId);
|
|
1519
|
+
const retryContext = project
|
|
1520
|
+
? await this.resolveRequestedChangesWakeContext(refreshedIssue, run.runType, run.runType === "branch_upkeep"
|
|
1521
|
+
? {
|
|
1522
|
+
branchUpkeepRequired: true,
|
|
1523
|
+
reviewFixMode: "branch_upkeep",
|
|
1524
|
+
wakeReason: "branch_upkeep",
|
|
1525
|
+
}
|
|
1526
|
+
: undefined, project)
|
|
1527
|
+
: undefined;
|
|
1528
|
+
const retryRunType = resolveRequestedChangesMode(run.runType, retryContext) === "branch_upkeep"
|
|
1529
|
+
? "branch_upkeep"
|
|
1530
|
+
: "review_fix";
|
|
1531
|
+
const recoveredState = resolveRecoverablePostRunState(refreshedIssue) ?? "failed";
|
|
1493
1532
|
const interruptedMessage = "Requested-changes run was interrupted before PatchRelay could verify that a new PR head was published";
|
|
1494
|
-
this.failRunAndClear(run, interruptedMessage,
|
|
1533
|
+
this.failRunAndClear(run, interruptedMessage, recoveredState);
|
|
1495
1534
|
await this.restoreIdleWorktree(issue);
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1535
|
+
const recoveredIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? refreshedIssue;
|
|
1536
|
+
if (recoveredState === "changes_requested") {
|
|
1537
|
+
this.db.upsertIssue({
|
|
1538
|
+
projectId: run.projectId,
|
|
1539
|
+
linearIssueId: run.linearIssueId,
|
|
1540
|
+
pendingRunType: retryRunType,
|
|
1541
|
+
pendingRunContextJson: retryContext ? JSON.stringify(retryContext) : null,
|
|
1542
|
+
});
|
|
1543
|
+
this.feed?.publish({
|
|
1544
|
+
level: "warn",
|
|
1545
|
+
kind: "workflow",
|
|
1546
|
+
issueKey: issue.issueKey,
|
|
1547
|
+
projectId: run.projectId,
|
|
1548
|
+
stage: run.runType,
|
|
1549
|
+
status: "retry_queued",
|
|
1550
|
+
summary: "Requested-changes run was interrupted; PatchRelay will retry from fresh GitHub truth",
|
|
1551
|
+
});
|
|
1552
|
+
this.enqueueIssue(run.projectId, run.linearIssueId);
|
|
1553
|
+
}
|
|
1554
|
+
else {
|
|
1555
|
+
this.feed?.publish({
|
|
1556
|
+
level: "error",
|
|
1557
|
+
kind: "workflow",
|
|
1558
|
+
issueKey: issue.issueKey,
|
|
1559
|
+
projectId: run.projectId,
|
|
1560
|
+
stage: run.runType,
|
|
1561
|
+
status: "escalated",
|
|
1562
|
+
summary: interruptedMessage,
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
void this.linearSync.emitActivity(recoveredIssue, buildRunFailureActivity(run.runType, interruptedMessage));
|
|
1566
|
+
void this.linearSync.syncSession(recoveredIssue, { activeRunType: run.runType });
|
|
1508
1567
|
this.releaseIssueSessionLease(run.projectId, run.linearIssueId);
|
|
1509
1568
|
return;
|
|
1510
1569
|
}
|