You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(work)!: let the repo own the queue branch, not the drain
The drain no longer cuts ai/queue-<hash> or opens its PR. Cutting, retargeting
and landing are all the target repo's workflow's — the side that already held
the landing credential — so the loop now writes only its own issue branches.
work.queueBranch stays the opt-in and only changes meaning: a worker resolves
its PR base per issue at the push, taking an open ai/queue-* where exactly one
is there and pr.base otherwise. Per issue rather than per drain, because the
workflow cuts the branch in response to the first worker PR and it can appear
mid-run. None open is no longer a stop — a PR into pr.base is one the repo
already knows how to land, so there is no stranding left to guard against. A
base the workflow moves afterwards is reported, never corrected.
BREAKING CHANGE: a repo setting work.queueBranch: true now needs a workflow
that cuts the queue branch and opens its PR. The drain previously did both and
stopped the whole run when it could not.
Copy file name to clipboardExpand all lines: skills/work/work-implement-queue/SKILL.md
+4-4Lines changed: 4 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -43,14 +43,14 @@ Before building the queue, reclaim issues an earlier implement-run crashed on: a
43
43
44
44
### 4. Announce the batch — then drain
45
45
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.
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 open `ai/queue-<hash>` the PRs will target — or that none is open yet, so the first PRs go to `pr.base` until the repo's workflow cuts one. Under `/loop` it runs unattended.
47
47
48
48
-**Plan-only triggers** ("just show me", "dry run", "nur den Plan", "don't run") still stop after the plan.
49
49
- 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.
50
50
51
51
### 5. Drain
52
52
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.
53
+
**`work.queueBranch`needs nothing from the drain** (`worktree` only — inert under `branch:<name>`, which opens no per-issue PR to group). The queue branch and its PR are the **target repo's workflow's** to cut, retarget and land; each worker simply aims its own PR at an open `ai/queue-*` when there is one, resolved per issue at its own step 8 because the branch can appear mid-drain. Nothing to prepare here, and nothingthat can fail here. Rules: **Queue branch** in `work-implement`'s REFERENCE.
54
54
55
55
For each issue, up to `work.cap`, spawn a **fresh worker** that runs `work-implement` on exactly that issue:
56
56
@@ -78,7 +78,7 @@ Summarise each issue and its outcome (handed to `reviewRequested` / `blocked` re
78
78
79
79
Issues now in `reviewRequested` are the drain's hand-off — the `work-review-queue` picks them up. Name the count.
80
80
81
-
**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.
81
+
**Under `work.queueBranch`, name the base each PR was actually opened against** — the open `ai/queue-<hash>`, or `pr.base` where none was open yet. Say plainly that the repo's workflow may **move** those bases afterwards, and that doing so is expected rather than drift: this drain neither cuts that branch, nor opens its PR, nor lands it. Where more than one `ai/queue-*` PR was open, report that too — the run took `pr.base` and left the ambiguity to the repo.
82
82
83
83
**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**:
84
84
@@ -184,7 +184,7 @@ opening for the summaries it writes on request; one house frame, reached two way
184
184
-**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.
185
185
-**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.
186
186
-**This loop never reviews.** It produces `reviewRequested`/`blocked` only; `done`/`changes-requested`/`needs human` are the review loop's and the human's.
187
-
-**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`.
187
+
-**The queue branch is opt-in, and the drain neither makes nor lands it** — off unless `work.queueBranch` says otherwise, because only the repo knows whether the workflow that cuts and fast-forwards it exists. A worker **aims** at an open `ai/queue-*`and does nothing else with it; none open is not a failure, it opens against `pr.base` as usual. The loop holds no credential that could write to a protected branch, and now none that writes to `ai/queue-*` either.
Copy file name to clipboardExpand all lines: skills/work/work-implement/DESIGN.md
+5-1Lines changed: 5 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -54,7 +54,11 @@ The lifecycle is **symmetric**: each loop has a **waiting** state and an **in-fl
54
54
-**One tracker per repo**, never both. If labels mirror GitHub↔Linear, that is the integration's job; the agent writes only the configured side.
55
55
- **`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).
56
56
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.
57
+
- **The queue branch groups PRs, and the repo — not the drain — owns every write that makes that happen.** 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`. **Cutting that branch, opening its PR and landing it are all the target repo's**, via a workflow that mints its own token in CI and **fast-forwards** the integration branch onto the queue PR's head. 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. 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.
58
+
59
+
- **The drain was the wrong side to cut the branch from, and moving it fixed the failure mode rather than mitigating it.** The mode first had the drain reuse-or-cut `ai/queue-<hash>` and open its PR before the first worker, stopping the whole run when it could not. Two things were wrong with that. The **stranding risk it guarded against was one it created**: a branch the drain cut, carrying PRs nothing would merge if the landing workflow turned out absent — so the guard had to be a loud stop, and the mode's own opt-in was the only thing standing between a misconfigured repo and ten stranded PRs. And it put a **write to `ai/queue-*` on the machine running the loop**, in a design whose whole argument was that the loop holds no branch-writing credential. Handing creation to the same workflow that already held the landing credential removes both: the loop now writes **only** its own issue branches, and a run with nothing to aim at opens against `pr.base` — a PR the repo already knows how to land, so there is no stranding left to guard. The base is therefore resolved **per issue at the push**, not once per drain, because the workflow cuts the branch in response to the first worker PR and it can appear mid-run; the workflow retargets whatever preceded it. Rejected: **keeping the pre-flight cut and merely softening the stop to a fallback** — that leaves the loop holding the `ai/queue-*` write for no remaining benefit. Rejected: **dropping `work.queueBranch` entirely** now that the repo decides whether a queue branch exists — a drain that hunts for one unasked would retarget PRs in repos that never opted in, and the key is what keeps the lookup deliberate. **A base a workflow moves afterwards is never corrected**, and the run reports the base it set rather than the base the PR now carries: re-reading would make a race out of the one interaction the split exists to keep one-directional.
60
+
61
+
-**The CI saving is the repo's to make and is not promised.** It exists only where workflow triggers are scoped to the integration branches — a `ci.yml` on `pull_request.branches: [main, dev]` runs nothing for a PR against `ai/queue-*`, while a repo without that filter runs everything exactly as before and gains one merge into `pr.base` instead of n. On a **public** repo, where Actions minutes are free, it is tidiness rather than money.
0 commit comments