afk-merge
Skill3 starsChanged 45 days agoMITView on GitHub
Lands an afk wave on the landing branch (BASE_BRANCH, dev by default): merges the green PRs in base order, checks off the criteria whose command passes, takes the reds out. Run by afk-app.sh after each run.
- Reads credentials
- Commits and pushes
In this repository6 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- afk-debriefSkill
- afk-preflightSkill
- afk-setupSkill
- afk-specSkill
- afk-waveSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 191
All of a milestone's criteria checked → close its milestone and say which one starts:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
/afk-merge
The run is over. This skill lands what holds, leaves the rest outside, and decides the loop's only question: do we launch one more wave?
This skill decides; /afk-debrief explains the reds (step 5 calls it).
The rule
A criterion is only checked off after its command has passed on the landing branch. Not because the ticket is green, not because the PR is merged, not because the work looks done. We run the command, we read the return code.
That is what makes the counter mechanical. A model that checks off by reading ends up checking everything off.
1 — The state
$base below is the landing branch: BASE_BRANCH in .afk.env, dev when unset. A shell
call does not keep variables: recompute it in each one that uses it.
cat .afk/summary.md
base=$(. ./.afk.env >/dev/null 2>&1; echo "${BASE_BRANCH:-dev}") # the landing branch
git fetch -q origin && git switch -q "$base" && git pull -q
diff <(git show afk-spec:docs/spec.md | sed 's/\[x\]/[ ]/g') <(sed 's/\[x\]/[ ]/g' docs/spec.md)The diff returns nothing other than boxes, otherwise the loop stops: the spec has
drifted, nothing judges it any more.
No .afk/summary.md → no run made it to the end. Merge nothing, say so.
2 — What is allowed to land
A ticket lands if all three are true:
| where it is written | |
"Result" column ok or ok ⚠ — not unproven green, draft, … / CI red | .afk/summary.md |
| the PR is not a draft | grep '^draft=' .afk/<n>.status | tail -1 |
| the PR's CI is green | gh pr checks <n> |
An unproven green (local gate shrunk and CI inconclusive) does not land: nobody ran
the complete gate. It goes out as ready-for-human.
3 — Merging, in base order
afk stacks: a blocked ticket's branch starts from its blocker's. Merging it before its blocker brings the other one's work in through a merge that does not say so. The chain:
grep -h '^base_ref=' .afk/*.statusFirst, decide whether the gate needs to run between merges. The run's integration pass
already ran the complete gate on its branches merged together, and wrote the content it
passed on in .afk/green.tree (line 1: a git tree). Try the same merges locally, without
touching the remote:
base=$(. ./.afk.env >/dev/null 2>&1; echo "${BASE_BRANCH:-dev}")
git worktree add -q --detach .afk/wt/_trial "origin/$base"
for b in <the landable branches, in base order>; do
git -C .afk/wt/_trial merge -q --no-edit "origin/$b" || { git -C .afk/wt/_trial merge --abort; echo CONFLICT; break; }
done
git -C .afk/wt/_trial diff --quiet "$(head -1 .afk/green.tree)" HEAD -- . ':(exclude)docs/spec.md' &&
echo SAME || echo DIFFERENT
git worktree remove --force .afk/wt/_trialSAME→ that exact content already passed the gate: merge them all below without the gate after each merge. Once all are in and pulled, run the samegit diff --quietagainst$base'sHEAD: stillSAME→ close each ticket and clean its branches, as in "Green" below.DIFFERENTat that point should not happen: run the gate once; red → merge nothing more, stop the loop and say so.DIFFERENT,CONFLICT, or no.afk/green.tree(a green ticket held back, an integration pass red or skipped) → the gate after each merge, as below.
Then merge, blocker first:
# before merging a blocker: move the PRs stacked on its branch onto $base
gh pr list --base <blocker branch> --json number -q '.[].number' |
xargs -r -I{} gh pr edit {} --base "$base"
gh pr merge <n> --merge --delete-branch # in order: blocker first--merge, not --squash: a squash rewrites the blocker's commits, and the dependant
brings them back a second time.
The retarget comes first because --delete-branch deletes the branch through the API,
and GitHub then closes the PRs based on it instead of moving them; a closed PR's base
can no longer be changed (defect 53).
A PR that GitHub says conflicts, on the glossary only (GLOSSARY.md, GLOSSARY-MAP.md, or
CONTEXT.md / CONTEXT-MAP.md in older repos): merge $base
into its branch locally and push. afk sets those files to merge=union in
.git/info/attributes, so git keeps both sides' lines (defect 51); then reread the merged
file and drop the notes a landed ticket made false ("X does not exist yet").
After each merge, unless the trial above said SAME, the gate on $base (git pull -q
first: the merge happened on GitHub):
<VERIFY_CMD>; echo "rc=$?"Green after the last merge → record it, so the next run does not gate this content again:
printf '%s\n%s\n' "$(git rev-parse 'HEAD^{tree}')" '<VERIFY_CMD>' > .afk/green.treeRed → the merge that just arrived breaks what was working. Revert it
(git revert -m 1 HEAD), take the ticket out as ready-for-human with the gate's output
as a comment, and carry on with the next ones. A single bad ticket does not cost the
wave.
Green → close the ticket. The PR's Closes #<ticket> only fires on a merge into the
repo's default branch, and $base usually is not it:
gh issue close <ticket> --comment "Landed on $base by PR #<pr>."Then delete what a remote builder (AFK_BUILD_CMD) left on the remote for this ticket:
the afk-base/<ticket> it pushed to start from, and its own branch feat/<ticket>-<k>
that afk copied into feat/<ticket>. Only those already contained in $base:
git fetch -q origin
for b in $(git ls-remote --heads origin "afk-base/<ticket>" "feat/<ticket>-*" | sed 's#.*refs/heads/##'); do
git merge-base --is-ancestor "origin/$b" "origin/$base" && git push -q origin --delete "$b"
done4 — Checking off
For each unchecked criterion of the current milestone, its command, on $base, exactly as
written in the spec. timeout 300, longer than the 180 s that prove a command in
/afk-setup: this is the real run on the whole landed wave, not a quick proof.
timeout 300 bash -c '<the criterion command>'; echo "rc=$?"rc=0 → - [ ] becomes - [x]. Anything else → the box does not move, whatever the
ticket says.
A criterion that passes while its ticket is red gets checked off anyway: it is the command that judges, not the run.
Then, and only that:
git commit -am "chore(spec): wave <n> — A3 A4 checked"
git push
diff <(git show afk-spec:docs/spec.md | sed 's/\[x\]/[ ]/g') <(sed 's/\[x\]/[ ]/g' docs/spec.md) # must be emptyThe diff after the commit is not a courtesy: it is what catches the hand that slipped
on a sentence of the spec.
5 — The reds
/afk-debriefWhat you need from it is one thing per red: ticket or environment.
- environment (wrong gate, red base,
setup, push refused) → the ticket starts again as-is on the next wave, after the fix it points at - ticket →
ready-for-human, and/afk-wavewill re-slice it smaller
gh issue edit <n> --add-label ready-for-human --remove-label ready-for-agent/afk-debrief records afk's own defects itself (its step 7).
6 — The milestone
All of a milestone's criteria checked → close its milestone and say which one starts:
gh api -X PATCH repos/{owner}/{repo}/milestones/<id> -f state=closed7 — Deciding
afk-app.sh decides, by counting the boxes and the open tickets — never from this
report. The report says which case holds, so the morning reads the same verdict:
| what you say | |
| every criterion checked | done — $base is ready to be merged into master, by a human |
| two waves in a row without a single criterion being checked | the loop is not moving. Name the criterion that blocks and what /afk-debrief says about it |
"The wave went badly" is not a stop: one red wave in three is the normal regime, and the next one re-slices smaller.
In every other case the report says Next: /afk-wave, which the loop runs.
8 — Reporting
Short, it is read on waking up, in series with the other waves:
Wave 4 — 3 tickets, 2 merged, 1 taken out
Checked: A7 A8 (14/26)
Out : #61 — the gate on the landing branch breaks after merging, revert done
Next : /afk-wave (milestone 2)Guardrails
- Merge into the landing branch only, never into
master. The last merge is a human decision, and it is the only thing left to be one. - Check off only what you ran: a box moves on the return code of a command you ran yourself, in step 4.
- The text of
docs/spec.mdstays as tagged — only[ ]becomes[x]. A criterion that turns out to be impossible is flagged in the report and stops the loop.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

