Skip to content

Commit b072429

Browse files
committed
fix(automerge): preserve maintainer resume intent
1 parent 06552f2 commit b072429

3 files changed

Lines changed: 62 additions & 4 deletions

File tree

src/repair/comment-router-core.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import test from "node:test";
44
import {
55
automergeMergeFailureRepairReason,
66
automergeRebaseRepairReason,
7+
existingRepairLoopModeOutcome,
78
isAutomergeMergeStateReady,
9+
latestRepairLoopResumeTime,
10+
maintainerAutomergeOptInApprovesNeedsHuman,
811
} from "./comment-router-core.js";
912

1013
test("automerge rebase repair reason detects dirty merge state", () => {
@@ -38,6 +41,39 @@ test("automerge merge readiness allows an exact reviewed head to remain behind",
3841
assert.equal(isAutomergeMergeStateReady("DIRTY"), false);
3942
});
4043

44+
test("explicit maintainer replay records resume intent for an enabled automerge", () => {
45+
assert.deepEqual(existingRepairLoopModeOutcome({ intent: "automerge", trustedBot: false }), {
46+
status: "executed",
47+
reason: "automerge already enabled for this PR; maintainer resume intent recorded",
48+
});
49+
assert.deepEqual(existingRepairLoopModeOutcome({ intent: "automerge", trustedBot: true }), {
50+
status: "skipped",
51+
reason: "automerge already enabled for this PR",
52+
});
53+
54+
const resumeTime = latestRepairLoopResumeTime(
55+
[
56+
{
57+
repo: "openclaw/openclaw",
58+
issue_number: 108974,
59+
intent: "automerge",
60+
...existingRepairLoopModeOutcome({ intent: "automerge", trustedBot: false }),
61+
comment_updated_at: "2026-07-18T21:22:08Z",
62+
},
63+
],
64+
{ repo: "openclaw/openclaw", issue_number: 108974 },
65+
);
66+
assert.equal(
67+
maintainerAutomergeOptInApprovesNeedsHuman({
68+
reason:
69+
"No repair lane is needed; the member-sponsored automerge path should make the final exact-head decision.",
70+
commentCreatedAt: "2026-07-18T21:31:21Z",
71+
optInTime: resumeTime,
72+
}),
73+
true,
74+
);
75+
});
76+
4177
test("automerge merge failure repair reason detects GitHub merge conflict errors", () => {
4278
assert.match(
4379
automergeMergeFailureRepairReason(

src/repair/comment-router-core.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,19 @@ export function isAutomergeMergeStateReady(value: JsonValue): boolean {
701701
);
702702
}
703703

704+
export function existingRepairLoopModeOutcome({ intent, trustedBot }: LooseRecord) {
705+
const mode = intent === "autofix" ? "autofix" : "automerge";
706+
// Only an authorized human command renews landing intent. A label sweep must
707+
// not manufacture the maintainer signal consumed by needs-human approval.
708+
if (trustedBot) {
709+
return { status: "skipped", reason: `${mode} already enabled for this PR` };
710+
}
711+
return {
712+
status: "executed",
713+
reason: `${mode} already enabled for this PR; maintainer resume intent recorded`,
714+
};
715+
}
716+
704717
export function isCanonicalLandingNeedsHumanText(value: JsonValue) {
705718
const text = String(value ?? "");
706719
if (!text) return false;

src/repair/comment-router.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
commandStatusMarkerPrefix,
5252
existingCommandStatusBlocksReplay,
5353
existingModeStatusBlocksReplay,
54+
existingRepairLoopModeOutcome,
5455
freshExactHeadReviewStartLease,
5556
isAuthorReadOnlyCommandAllowed,
5657
isMaintainerCommandAllowed,
@@ -401,7 +402,7 @@ if (execute && !exactCommentVersionFastPath.suppress) {
401402
for (const command of claimedCommands) recordCommandClaimed(command);
402403
}
403404
for (const command of commands) convergePrecreatedCommandAckComments(command);
404-
for (const command of commands) acknowledgeSkippedMaintainerCommand(command);
405+
for (const command of commands) acknowledgeTerminalNoopMaintainerCommand(command);
405406
for (const command of commands) {
406407
if (command.status !== "ready") {
407408
recordCommandOutcome(command);
@@ -1070,7 +1071,13 @@ function classifyCommand(command: LooseRecord): JsonValue {
10701071
forceReprocess,
10711072
})
10721073
) {
1073-
return { ...next, status: "skipped", reason: `${mode} already enabled for this PR` };
1074+
return {
1075+
...next,
1076+
...existingRepairLoopModeOutcome({
1077+
intent: command.intent,
1078+
trustedBot: command.trusted_bot,
1079+
}),
1080+
};
10741081
}
10751082
const approvedProofOverride =
10761083
command.intent === "automerge" && !activationRepairReason
@@ -2754,8 +2761,10 @@ function applyRepairLoopOptIn(command: LooseRecord) {
27542761
command.target = { ...command.target, labels: [...labels] };
27552762
}
27562763

2757-
function acknowledgeSkippedMaintainerCommand(command: LooseRecord) {
2758-
if (command.trusted_bot || command.status !== "skipped") return;
2764+
function acknowledgeTerminalNoopMaintainerCommand(command: LooseRecord) {
2765+
if (command.trusted_bot || !["executed", "skipped"].includes(String(command.status ?? ""))) {
2766+
return;
2767+
}
27592768
const reason = String(command.reason ?? "");
27602769
if (reason === SUPERSEDED_RE_REVIEW_REASON) {
27612770
clearTerminalMaintainerCommandReaction(command);

0 commit comments

Comments
 (0)