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.
2 stars
0 votes
0 copies
3 views
Added October 13, 2026
toolsgoshellbashgitapi
Works with
api
Security analysis
B88/100
criticalSends environment variables or credentials to an external URL
Installs into .claude/skills of the current project.
Are you the author of Afk Merge?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/warshoow-afk-merge)
---
name: afk-merge
description: "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."
disable-model-invocation: true
---
# /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?**
It does not diagnose the reds — `/afk-debrief` does that, better, and without redoing it
here. This skill decides; the other one explains.
## 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.
```bash
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 |
|---|---|
| `GREEN` in the summary (not `OK` — `OK` contains the unproven greens) | `.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.
```bash
grep -h '^base_ref=' .afk/*.status # the chain, ticket by ticket
# 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 `CONTEXT.md` or `CONTEXT-MAP.md` only: 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**, the gate on `$base` (`git pull -q` first: the merge happened on GitHub):
```bash
<VERIFY_CMD>; echo "rc=$?"
```
Red → 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:
```bash
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`:
```bash
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"
done
```
## 4 — Checking off
For each unchecked criterion of the current milestone, its command, on `$base`, exactly as
written in the spec:
```bash
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:
```bash
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 empty
```
The `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
```bash
/afk-debrief
```
Do not redo its work. What 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-wave` will re-slice it smaller
```bash
gh issue edit <n> --add-label ready-for-human --remove-label ready-for-agent
```
And record in afk's `docs/defects.md` whatever would have broken the same way on any
repo — `/afk-debrief` says which ones. Nothing to record is the normal case. The next
number is the highest one in `docs/defects.md` **and** `docs/defects-fixed.md` plus one,
and a defect already there gets an "Again" paragraph, not a new number.
## 6 — The milestone
All of a milestone's criteria checked → close its milestone and say which one starts:
```bash
gh api -X PATCH repos/{owner}/{repo}/milestones/<id> -f state=closed
```
## 7 — 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`. Do not run it: the loop does.
## 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)
```
## What this skill does not do
- **It never merges into `master`.** The landing branch only. The last merge is a human decision, and
it is the only thing left to be one.
- **It checks off no criterion whose command it has not run itself.**
- **It does not modify the text of `docs/spec.md`** — only `[ ]` into `[x]`. A criterion
that turns out to be impossible is flagged in the report and stops the loop; it does
not get rewritten.
- It opens no ticket: that is `/afk-wave`.
- It does not review the greens' code. The gate and CI are what we have; widening them
is a `.afk.env` change, not an end-of-wave judgement.
- It does not relaunch `afk.sh`.