patchrelay 0.23.1 → 0.23.3
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
CHANGED
|
@@ -19,14 +19,14 @@ const STATE_SHORT = {
|
|
|
19
19
|
preparing: "prep",
|
|
20
20
|
implementing: "impl",
|
|
21
21
|
pr_open: "pr open",
|
|
22
|
-
changes_requested: "
|
|
22
|
+
changes_requested: "review fix",
|
|
23
23
|
repairing_ci: "ci fix",
|
|
24
24
|
awaiting_queue: "merging",
|
|
25
25
|
repairing_queue: "merge fix",
|
|
26
26
|
done: "done",
|
|
27
27
|
failed: "failed",
|
|
28
28
|
escalated: "escalated",
|
|
29
|
-
awaiting_input: "
|
|
29
|
+
awaiting_input: "paused",
|
|
30
30
|
};
|
|
31
31
|
const RUN_SHORT = {
|
|
32
32
|
implementation: "impl",
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -56,7 +56,7 @@ function buildRunPrompt(issue, runType, repoPath, context) {
|
|
|
56
56
|
lines.push("## CI Repair", "", "A CI check has failed on your PR. Fix the failure and push.", context?.checkName ? `Failed check: ${String(context.checkName)}` : "", context?.checkUrl ? `Check URL: ${String(context.checkUrl)}` : "", "", "Read the CI failure logs, fix the code issue, run verification, commit and push.", "Do not change test expectations unless the test is genuinely wrong.", "");
|
|
57
57
|
break;
|
|
58
58
|
case "review_fix":
|
|
59
|
-
lines.push("## Review Changes Requested", "", "A reviewer has requested changes on your PR. Address the feedback and push.", context?.reviewerName ? `Reviewer: ${String(context.reviewerName)}` : "", context?.reviewBody ? `\n## Review comment\n\n${String(context.reviewBody)}` : "", "", "Read the review feedback and PR comments (`gh pr view --comments`),
|
|
59
|
+
lines.push("## Review Changes Requested", "", "A reviewer has requested changes on your PR. Address the feedback and push.", context?.reviewerName ? `Reviewer: ${String(context.reviewerName)}` : "", context?.reviewBody ? `\n## Review comment\n\n${String(context.reviewBody)}` : "", "", "Steps:", "1. Read the review feedback and PR comments (`gh pr view --comments`).", "2. Check the current diff (`git diff origin/main`) — a prior rebase may have already resolved some concerns (e.g., scope-bundling from stale commits).", "3. For each review point: if already resolved, note why. If not, fix it.", "4. Run verification, commit and push.", "5. If you believe all concerns are resolved, request a re-review: `gh pr edit <PR#> --add-reviewer <reviewer>`.", " Do NOT just post a comment saying \"resolved\" — the reviewer must re-review to dismiss the CHANGES_REQUESTED state.", "");
|
|
60
60
|
break;
|
|
61
61
|
case "queue_repair":
|
|
62
62
|
lines.push("## Merge Queue Failure", "", "The merge queue rejected this PR. Rebase onto latest main and fix conflicts.", context?.failureReason ? `Failure reason: ${String(context.failureReason)}` : "", "", "Fetch and rebase onto latest main, resolve conflicts, run verification, push.", "If the conflict is a semantic contradiction, explain and stop.", "");
|
|
@@ -188,6 +188,10 @@ export class RunOrchestrator {
|
|
|
188
188
|
await execCommand(gitBin, ["-C", worktreePath, "config", "user.name", this.botIdentity.name], { timeoutMs: 5_000 });
|
|
189
189
|
await execCommand(gitBin, ["-C", worktreePath, "config", "user.email", this.botIdentity.email], { timeoutMs: 5_000 });
|
|
190
190
|
}
|
|
191
|
+
// Freshen the worktree: fetch + rebase onto latest base branch.
|
|
192
|
+
// This prevents branch contamination when local main has drifted
|
|
193
|
+
// and avoids scope-bundling review rejections from stale commits.
|
|
194
|
+
await this.freshenWorktree(worktreePath, project, issue);
|
|
191
195
|
// Run prepare-worktree hook
|
|
192
196
|
const hookEnv = buildHookEnv(issue.issueKey ?? issue.linearIssueId, branchName, runType, worktreePath);
|
|
193
197
|
const prepareResult = await runProjectHook(project.repoPath, "prepare-worktree", { cwd: worktreePath, env: hookEnv });
|
|
@@ -246,6 +250,60 @@ export class RunOrchestrator {
|
|
|
246
250
|
void this.emitLinearActivity(freshIssue, buildRunStartedActivity(runType));
|
|
247
251
|
void this.syncLinearSession(freshIssue, { activeRunType: runType });
|
|
248
252
|
}
|
|
253
|
+
// ─── Pre-run branch freshening ────────────────────────────────────
|
|
254
|
+
/**
|
|
255
|
+
* Fetch origin and rebase the worktree onto the latest base branch.
|
|
256
|
+
*
|
|
257
|
+
* Risks mitigated:
|
|
258
|
+
* - Dirty worktree from interrupted run → stash before, pop after
|
|
259
|
+
* - Conflicts → abort rebase, throw so the run fails with a clear reason
|
|
260
|
+
* - Already up-to-date → no-op, no force-push needed
|
|
261
|
+
* - Force-push invalidates reviews → only push if rebase actually moved commits
|
|
262
|
+
*/
|
|
263
|
+
async freshenWorktree(worktreePath, project, issue) {
|
|
264
|
+
const gitBin = this.config.runner.gitBin;
|
|
265
|
+
const baseBranch = project.github?.baseBranch ?? "main";
|
|
266
|
+
// Stash any uncommitted changes from a previous interrupted run
|
|
267
|
+
const stashResult = await execCommand(gitBin, ["-C", worktreePath, "stash"], { timeoutMs: 30_000 });
|
|
268
|
+
const didStash = stashResult.exitCode === 0 && !stashResult.stdout?.includes("No local changes");
|
|
269
|
+
// Fetch latest base
|
|
270
|
+
const fetchResult = await execCommand(gitBin, ["-C", worktreePath, "fetch", "origin", baseBranch], { timeoutMs: 60_000 });
|
|
271
|
+
if (fetchResult.exitCode !== 0) {
|
|
272
|
+
this.logger.warn({ issueKey: issue.issueKey, stderr: fetchResult.stderr?.slice(0, 300) }, "Pre-run fetch failed, proceeding with current base");
|
|
273
|
+
if (didStash)
|
|
274
|
+
await execCommand(gitBin, ["-C", worktreePath, "stash", "pop"], { timeoutMs: 10_000 });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// Check if rebase is needed: is HEAD already on top of origin/baseBranch?
|
|
278
|
+
const mergeBaseResult = await execCommand(gitBin, ["-C", worktreePath, "merge-base", "--is-ancestor", `origin/${baseBranch}`, "HEAD"], { timeoutMs: 10_000 });
|
|
279
|
+
if (mergeBaseResult.exitCode === 0) {
|
|
280
|
+
// Already up-to-date — no rebase needed
|
|
281
|
+
this.logger.debug({ issueKey: issue.issueKey }, "Pre-run freshen: branch already up to date");
|
|
282
|
+
if (didStash)
|
|
283
|
+
await execCommand(gitBin, ["-C", worktreePath, "stash", "pop"], { timeoutMs: 10_000 });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// Rebase onto latest base
|
|
287
|
+
const rebaseResult = await execCommand(gitBin, ["-C", worktreePath, "rebase", `origin/${baseBranch}`], { timeoutMs: 120_000 });
|
|
288
|
+
if (rebaseResult.exitCode !== 0) {
|
|
289
|
+
// Abort the failed rebase and restore state
|
|
290
|
+
await execCommand(gitBin, ["-C", worktreePath, "rebase", "--abort"], { timeoutMs: 10_000 });
|
|
291
|
+
if (didStash)
|
|
292
|
+
await execCommand(gitBin, ["-C", worktreePath, "stash", "pop"], { timeoutMs: 10_000 });
|
|
293
|
+
throw new Error(`Pre-run rebase onto origin/${baseBranch} failed with conflicts — escalate or resolve manually`);
|
|
294
|
+
}
|
|
295
|
+
// Push the rebased branch (force-with-lease to protect against concurrent pushes)
|
|
296
|
+
const pushResult = await execCommand(gitBin, ["-C", worktreePath, "push", "--force-with-lease"], { timeoutMs: 60_000 });
|
|
297
|
+
if (pushResult.exitCode !== 0) {
|
|
298
|
+
this.logger.warn({ issueKey: issue.issueKey, stderr: pushResult.stderr?.slice(0, 300) }, "Pre-run rebase push failed, proceeding anyway");
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
this.logger.info({ issueKey: issue.issueKey, baseBranch }, "Pre-run freshen: rebased and pushed onto latest base");
|
|
302
|
+
}
|
|
303
|
+
// Restore stashed changes
|
|
304
|
+
if (didStash)
|
|
305
|
+
await execCommand(gitBin, ["-C", worktreePath, "stash", "pop"], { timeoutMs: 10_000 });
|
|
306
|
+
}
|
|
249
307
|
// ─── Notification handler ─────────────────────────────────────────
|
|
250
308
|
async handleCodexNotification(notification) {
|
|
251
309
|
// threadId is present on turn-level notifications but NOT on item-level ones.
|
|
@@ -492,6 +550,15 @@ export class RunOrchestrator {
|
|
|
492
550
|
...(newState === "awaiting_queue" ? { pendingMergePrep: true } : {}),
|
|
493
551
|
...(pendingRunType ? { pendingRunType: pendingRunType } : {}),
|
|
494
552
|
});
|
|
553
|
+
this.feed?.publish({
|
|
554
|
+
level: "info",
|
|
555
|
+
kind: "stage",
|
|
556
|
+
issueKey: issue.issueKey,
|
|
557
|
+
projectId: issue.projectId,
|
|
558
|
+
stage: newState,
|
|
559
|
+
status: "reconciled",
|
|
560
|
+
summary: `Reconciliation: ${issue.factoryState} \u2192 ${newState}`,
|
|
561
|
+
});
|
|
495
562
|
if (newState === "awaiting_queue" || pendingRunType) {
|
|
496
563
|
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
497
564
|
}
|
|
@@ -611,11 +678,15 @@ export class RunOrchestrator {
|
|
|
611
678
|
// ─── Internal helpers ─────────────────────────────────────────────
|
|
612
679
|
escalate(issue, runType, reason) {
|
|
613
680
|
this.logger.warn({ issueKey: issue.issueKey, runType, reason }, "Escalating to human");
|
|
681
|
+
if (issue.activeRunId) {
|
|
682
|
+
this.db.finishRun(issue.activeRunId, { status: "released" });
|
|
683
|
+
}
|
|
614
684
|
this.db.upsertIssue({
|
|
615
685
|
projectId: issue.projectId,
|
|
616
686
|
linearIssueId: issue.linearIssueId,
|
|
617
687
|
pendingRunType: null,
|
|
618
688
|
pendingRunContextJson: null,
|
|
689
|
+
activeRunId: null,
|
|
619
690
|
factoryState: "escalated",
|
|
620
691
|
});
|
|
621
692
|
this.feed?.publish({
|
|
@@ -752,10 +823,12 @@ export class RunOrchestrator {
|
|
|
752
823
|
*/
|
|
753
824
|
function resolvePostRunState(issue) {
|
|
754
825
|
if (ACTIVE_RUN_STATES.has(issue.factoryState) && issue.prNumber) {
|
|
755
|
-
|
|
756
|
-
|
|
826
|
+
// Check merged first — a merged PR is both approved and merged,
|
|
827
|
+
// and "done" must take priority over "awaiting_queue".
|
|
757
828
|
if (issue.prState === "merged")
|
|
758
829
|
return "done";
|
|
830
|
+
if (issue.prReviewState === "approved")
|
|
831
|
+
return "awaiting_queue";
|
|
759
832
|
return "pr_open";
|
|
760
833
|
}
|
|
761
834
|
return undefined;
|