Read memory/MEMORY.md and memory/watched-repos.md for repos to target.
Read the last 2 days of memory/logs/ to avoid re-logging PRs already merged.
-
Bootstrap state — per-PR retry counter lives in memory/topics/auto-merge-state.json:
mkdir -p memory/topics
[ -f memory/topics/auto-merge-state.json ] || echo '{"prs":{},"last_run":null}' > memory/topics/auto-merge-state.json
Schema:
{
"last_run": "2026-05-23T08:00:00Z",
"prs": {
"owner/repo#123": {
"first_seen": "2026-05-21T10:00:00Z",
"last_attempt": "2026-05-23T08:00:00Z",
"attempts": 2,
"last_outcome": "merge_failed",
"last_error": "Pull Request is in unstable state"
}
}
}
PR keys are <owner>/<repo>#<number> so state survives multi-repo runs. Cap to 50 most-recent entries (LRU by last_attempt). Validate with jq empty after write; restore from .bak on failure.
-
List open PRs for each watched repo with the full field set:
gh pr list -R owner/repo --state open --json number,title,author,isDraft,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,autoMergeRequest,isCrossRepository,labels,additions,deletions,baseRefName
-
Handle UNKNOWN state — GitHub computes mergeStateStatus lazily. If a PR returns UNKNOWN, sleep 3 seconds and re-query once:
sleep 3 && gh pr view NUMBER -R owner/repo --json mergeStateStatus,mergeable,statusCheckRollup
If still UNKNOWN after the retry, skip the PR with reason UNKNOWN-persistent and let the next run retry.
-
Apply the safety policy to each PR. Record a verdict for every PR: either MERGE or SKIP:<specific-reason>. Reasons must name the failing gate — e.g. SKIP:author-not-allowlisted:contributor123, SKIP:size-cap:823-lines, SKIP:mergeStateStatus=BEHIND, SKIP:label:do-not-merge, SKIP:check-failed:lint, SKIP:retry-cap:3-attempts. Vague reasons like SKIP:not-ready are not acceptable.
-
Merge qualifying PRs, up to MAX_AUTO_MERGE (default 3):
-
Send a notification only when at least one real (non-dry-run) merge succeeded or at least one PR has hit the retry cap (5b below). No merges and no cap hits → no notification, just a log entry.
5a. At least one merge succeeded:
*Auto Merge — ${today}*
Merged N PR(s) on owner/repo:
- #123: PR title (+45/-12, by @author) — squash merged abc1234
Queue cleared. Self-improve cycle unblocked.
5b. Retry cap reached on ≥1 PR (AUTO_MERGE_RETRY_CAP) — include in the same message if both fire, otherwise stand-alone:
*Auto Merge — retry cap*
Hit retry cap (3 attempts) on:
- owner/repo#40 — last error: "Pull Request is in unstable state"
Stopping auto-merge attempts on this PR. Investigate manually.
Dedup: suppress re-notify if the exact same set of cap-hit PR keys already notified within the last 24h (grep memory/logs/ for prior AUTO_MERGE_RETRY_CAP entries).
-
Persist state — write the updated memory/topics/auto-merge-state.json. Update last_run to current timestamp. Validate with jq empty; on failure restore from a .bak written before this run.
-
Log to memory/logs/${today}.md under an ### auto-merge heading:
Mode: live | dry-run
Repo(s): list
Merged: #N title @author +A-D SHA per line
Skipped: #N SKIP:<reason> per line
Retry-capped: owner/repo#N — <last_error> per line (empty if none)
Totals: merged=X qualified=Y considered=Z retry_capped=R
- If zero qualified, include a verdict breakdown:
AUTO_MERGE_SKIP: 0/Z qualifying (behind=B blocked=L failing=F draft=D author-blocked=A size-blocked=S retry-capped=R)
Once allowlisted, agent PRs flow through the same safety policy as bot PRs and get auto-merged on green CI. The retry cap protects against runaway behavior on a stuck PR.