Skip to content

Commit fbe31b9

Browse files
authored
feat(work-implement): group a worktree drain's PRs behind one queue branch (#217)
1 parent 442c1d5 commit fbe31b9

6 files changed

Lines changed: 81 additions & 5 deletions

File tree

skills/work/work-implement-queue/SKILL.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ Before building the queue, reclaim issues an earlier implement-run crashed on: a
4343

4444
### 4. Announce the batch — then drain
4545

46-
**`ai: ready` is already the human's approval** to work an issue — the label means "scoped + approved for an AI agent to pick up". So the drain does **not** gate on a fresh confirmation: **announce** the ordered queue plus the cap, branch strategy and parallel mode — with the **concurrency** it will run at when that mode is `parallel` (call out any **dependency-forced order**, any **mutex-forced wave split**, plus issues **deferred** or **skipped**), then drain. Under `/loop` it runs unattended.
46+
**`ai: ready` is already the human's approval** to work an issue — the label means "scoped + approved for an AI agent to pick up". So the drain does **not** gate on a fresh confirmation: **announce** the ordered queue plus the cap, branch strategy and parallel mode — with the **concurrency** it will run at when that mode is `parallel` (call out any **dependency-forced order**, any **mutex-forced wave split**, plus issues **deferred** or **skipped**), then drain. Say so too when `work.queueBranch` is on, naming the `ai/queue-<hash>` the PRs will target and whether it is being reused or cut. Under `/loop` it runs unattended.
4747

4848
- **Plan-only triggers** ("just show me", "dry run", "nur den Plan", "don't run") still stop after the plan.
4949
- If the ready-gate is **widened** (`labels.ready: false`, so issues were never explicitly opted-in), confirm before working those — there is no per-issue approval to lean on.
5050

5151
### 5. Drain
5252

53+
**First, if `work.queueBranch` is on** (`worktree` only — inert under `branch:<name>`, which opens no per-issue PR to group): **reuse or cut the queue branch, and open its PR, before the first worker starts.** An `ai/queue-*` PR already **open** against `pr.base` → reuse that branch; none open → cut `ai/queue-<hash>` from `pr.base` and open the `ai/queue-<hash>``pr.base` PR. The hash only keeps concurrent drains apart and encodes nothing later read back. Hand the branch to every worker as its base — that is the whole of what the mode changes for them. Doing this **here** rather than at step 1 means an empty queue cuts nothing. **Cannot cut the branch or open that PR → stop and report**, having drained nothing: falling back to `pr.base`, or draining onto a branch carrying no PR, is how work is stranded where nobody looks for it. The drain **never merges or fast-forwards** that PR — the target repo's own workflow lands it. Rules: **Queue branch** in `work-implement`'s REFERENCE.
54+
5355
For each issue, up to `work.cap`, spawn a **fresh worker** that runs `work-implement` on exactly that issue:
5456

5557
- **sequential** (`parallel: false`) — one worker at a time; **re-fetch** the next eligible issue each iteration.
@@ -72,6 +74,8 @@ Release the lock. Summarise each issue and its outcome (handed to `reviewRequest
7274

7375
Issues now in `reviewRequested` are the drain's hand-off — the `work-review-queue` picks them up. Name the count.
7476

77+
**Under `work.queueBranch`, the queue PR is a hand-off artifact too** — report its url beside them, and say plainly that this drain does **not** land it: landing belongs to the target repo's fast-forward workflow, and that workflow can only act while the queue branch still contains `pr.base`'s tip. Where anything else has landed on `pr.base` since, say so too — the fast-forward is off the table until that repo puts the branch back on top, which is its call and not this drain's. A queue PR nobody knows is waiting is the mode's failure mode; naming it every run is what keeps it from becoming one.
78+
7579
**Then name the queue's state**, so a repeating driver (`/loop`, cron, a human) knows whether to run again, wait, or stop — instead of that rule living in whoever typed the loop prompt. **Query the tracker again first**: work that became eligible while the last issue was being implemented is already there, and waiting on input that exists wastes an interval. Then decide **in this order**:
7680

7781
1. **Stopped on `work.cap` with eligible issues left → `work remaining`.** Run again **immediately**, never wait: a cap-ended drain is not an empty queue.
@@ -136,6 +140,7 @@ of a file.
136140
- **Never run a declared mutex pair concurrently** — split them across waves of the same run, under either branch strategy. A mutex **delays**; it never defers, labels, or blocks, and the skill never writes a `mutex:` label.
137141
- **Never work an issue whose labels contradict each other**`needsTriage` beside a lifecycle label is withheld and reported, never resolved by obeying the more permissive of the two, and never written to the tracker.
138142
- **This loop never reviews.** It produces `reviewRequested`/`blocked` only; `done`/`changes-requested`/`needs human` are the review loop's and the human's.
143+
- **The queue branch is opt-in, and the drain never lands it** — off unless `work.queueBranch` says otherwise, because only the repo knows whether a workflow exists to fast-forward it; the drain opens that PR and stops there, holding no credential that could write to a protected branch. Cutting the branch or opening the PR fails → stop, never fall back to `pr.base`.
139144
- Inherits `work-implement`'s attribution-free, secret-free, only-this-issue guardrails.
140145

141146
## Reference

skills/work/work-implement/DESIGN.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,11 @@ The lifecycle is **symmetric**: each loop has a **waiting** state and an **in-fl
5454
- **One tracker per repo**, never both. If labels mirror GitHub↔Linear, that is the integration's job; the agent writes only the configured side.
5555
- **`REFERENCE.md` is one file, shaped by ownership rather than size.** Splitting it into per-task rule files behind an index was measured and turned down: most of what looked conditional was the **drain's** mechanics misfiled in the worker's reference, and the genuinely conditional remainder did not pay for a directory, an index and a second read per pass. Where a mechanic is _hosted_ is decided by the reader that cannot delegate — the queue declares and checks its worker, so it may assume the worker's `REFERENCE`; the worker runs standalone and declares no sibling, so the selection query, dependency ordering and the lease rules stay with it and the drain names them there. Rejected: **a `rules/` directory** (machinery against a remainder too small to pay for it); **deleting rationale to shrink the file** (the reason it is there is that an agent which knows why a rule exists does not route around it); **a `REFERENCE.md` per queue skill** (it moves prose into a skill whose siblings cannot read it). Recorded as [ADR-0025](https://github.com/TitusKirch/skills/blob/main/docs/99.adr/0025-keep-the-work-loops-reference-whole.md).
5656

57+
- **The queue branch groups PRs without the drain gaining the power to land them.** Under `worktree` a drain at cap opens up to ten PRs into `pr.base`, each triggering the repo's full CI for work that all lands on one branch anyway — cost scaling with queue length rather than with the result. `work.queueBranch` retargets them at one `ai/queue-<hash>`, closed by a single PR into `pr.base`. **What it deliberately does not do is merge that PR.** Landing it is the **target repo's** job, via a workflow that mints its own token in CI and **fast-forwards** the integration branch onto the queue PR's head — so the credential able to write to a protected branch never sits on the machine running the loop, and the mode needs strictly less privilege than `branch:dev`, which pushes to that branch directly. The fast-forward, rather than a merge commit, is what keeps **one CHANGELOG entry per issue**: it carries every commit across individually and unchanged, so release-please still sees them, and it leaves `allowed_merge_methods` on the base branch untouched. It is also **conditional**, and the skill states the condition rather than promising the landing: a fast-forward exists only while the queue branch still contains the integration branch's tip, so anything else landing there beside it — release-please, a human's squash, another clone's `branch:dev` run — closes the window, and reuse-before-cutting widens it by making the queue PR long-lived. Recovering from that is the **target repo's** job, because putting the branch back on top is the same protected-branch write the split just refused the drain; giving the loop that power back to rescue its own artifact would buy the recovery at the price of the whole reason the mode needs less privilege than `branch:dev`. Rejected: **squashing the queue PR** (discards the per-issue entries, the reason the merge method was ever in question); **a merge commit** (needs `merge` allowed on the integration branch, and rulesets match the **base** ref, not the head — so the permission cannot be scoped to `ai/queue-*` and would apply to every PR into `dev`); **GitHub Merge Queue**, the native fit, rejected on **availability** rather than on design — it needs Enterprise Cloud or an org-owned public repo, and neither this repo nor the private estate qualifies. **Opt-in, defaulting off**, because nothing in a drain can verify the landing workflow exists and its absence fails silently and completely: every issue PR green, reviewed, and stranded on a branch nothing merges. The saving is likewise the repo's to make and is **not promised** — it exists only where workflow triggers are scoped to the integration branches, and on a public repo, where minutes are free, it is tidiness rather than money.
58+
5759
## Deferred (v2)
5860

59-
- **Stacked branches for dependent issues (`worktree` mode)** — branch-off-parent, PR base retargeting and a revision cascade fight the stateless model. What `worktree` does instead is **decline to select** a dependent whose prerequisite has not landed (above), which needs none of that machinery: it waits for the prerequisite rather than building on it. Ordering, the other half, is implemented for `branch:<name>`, where accumulation makes it pure ordering.
61+
- **Stacked branches for dependent issues (`worktree` mode)** — branch-off-parent, PR base retargeting and a revision cascade fight the stateless model. What `worktree` does instead is **decline to select** a dependent whose prerequisite has not landed (above), which needs none of that machinery: it waits for the prerequisite rather than building on it. Ordering, the other half, is implemented for `branch:<name>`, where accumulation makes it pure ordering. The **queue branch** above is not this and does not revive it: every issue still branches off one common base and no PR is retargeted at another issue's branch — grouping the PRs says nothing about the dependencies between them, so the prerequisite gate applies there unchanged.
6062
- **`branch` extensions**`dev`/named-branch grammar is designed in (`branch:<name>`); richer targets later.
6163
- **Structured PR-feedback ingestion** (thread resolution, author trust, auto-detect) — only if conversational feedback proves insufficient.
6264

0 commit comments

Comments
 (0)