# A queue that holds, and says why it did

> Three agents built this blog in order. Six times in September a fourth was held because another was already mid-edit in the files it planned to change.

Published 2026-09-28 · by The Tade project · tagged queue, agents, design.
Published at https://tade.sh/blog/a-queue-that-holds-and-says-why/ — part of https://tade.sh/blog/.

---

Ask for five things at once and a pile runs all five and hopes. Two of them
edit `package.json`, one of them installs while another reads the lockfile, and
the morning is spent working out which agent produced which half of the mess.

A queue is the alternative, and the interesting part of a queue is not the
order. It is what happens when the order turns out to have been written against
a repository that has since moved.

This page is the example. The blog you are reading was built by three agents in
a row, and the third of them wrote this sentence.

## The chain that made this blog

One request, four asks in it, and the orchestrator split it into three tasks
whose waits are in their own files. The `start` blocks from
`.tade/tasks/*/task.yaml` in this repository, trimmed to the waits and what
each said it would touch:

```yaml
# tade-web/blog-framework
start:
  after: []
  touches: [src, public, package.json, astro.config.mjs, scripts]

# tade-web/sentry-in-the-site
start:
  after:
    - task: tade-web/blog-framework
      why: both edit package.json and astro.config.mjs, and it must fit the
        site that task sets up
  touches: [package.json, src, astro.config.mjs, .github/workflows]

# tade-web/ten-posts
start:
  after:
    - task: tade-web/sentry-in-the-site
      why: so the posts land on the final site, after framework and analytics
  touches: [src/content, public/images]
```

Two things about that shape are the whole design.

**A wait carries its reason.** Not a priority number, not a position in a
list — the sentence whoever planned it would have said out loud. When the
second task starts, the reason it was waiting is still there to read, and when
it is held the reason is what a person is shown.

**A task says what it expects to touch.** `touches` is a reading of the code
somebody made at planning time. It is not enforcement and it is not a lock. It
is a claim, made early, that can be checked later against what has actually
happened.

> **A screen from Tade — `a-chain-in-the-queue`.** Queued work opened: the chain it is in drawn as boxes, why each link waits, and what its agent will be told.
>
> Queued work opened: the chain it is in, why each link waits, and what its
> agent will be told. Looking at queued work is never starting it.

## A column is a step, not a rank

`plan` on the queue's heading steps back from one piece of work to all of it.
Each column is a step: everything in it can run once the columns before have
finished, so two boxes in one column are two agents at the same time.

That is a layout of the waits rather than a decision about them. A task sits
in the column after the last thing it waits on, a wait that skips columns is
drawn as a line through the ones between, and nothing crosses a box. What the
picture cannot show — *why* — is written under it.

> **A screen from Tade — `the-plan`.** The whole plan at once: a column per step, a box per task, an arrow for every wait and the reason under it.
>
> The whole plan before any of it starts: a column per step, an arrow per wait,
> the reason under each one, and `bump-mailer` in the first column beside
> `fix-charge` because nothing makes it wait.

## The check at the moment of starting

Here is the case the plan cannot cover. Two hours pass. The work at the front
of the queue is ready, everything it waited on has finished — and an agent that
started in between has been changing files ever since.

So the tree is asked at the moment something is about to start, because that is
the moment it is true. `collidesNow` compares what the queued work said it
touches against two facts about the checkout right now: what each working agent
has **committed** since it started, and what is **changed and not committed**,
whoever changed it.

It has fired six times in the journal on this machine, all of them in
September. Two, in the words a person was shown:

```console
tade/terminal-buttons-always  held
  tade/settings-copy-paste, which is working, has already changed
  packages/app/src/wire/lanes.ts and packages/app/src/view/main.ts, which this
  was planned to change

tade/plan-button-unreachable  held
  packages/app/src/view.ts, packages/app/src/app.ts, packages/app/test and
  images, which this was planned to change, are changed and not committed,
  while tade/notes-two-line-rows works in the same checkout
```

The two sentences are worded differently on purpose. Git says whose a commit
is, so the first names the agent. Git says nothing about whose an uncommitted
change is, so the second does not guess — it says the true thing instead: the
file is being changed now, and these are the agents in the same checkout.

## Four things it refuses to do

**It ignores the changes of anything it waits on.** What a task waits on is
what it builds on; those changes are the reason it is starting at all.

**It never holds work that said nothing.** A task with an empty `touches`
cannot be checked against the tree, and holding everything that said nothing
would stop the queue rather than guard it.

**It does nothing in a worktree.** With a branch each, nothing is being changed
under anybody, and what two branches do to one file is a merge — which was
already said when the plan was checked. `agents.workspace` decides which world
a project lives in, and this check is only for the shared one.

**A hold heals on its own.** It is written down, and the next look that finds
the files settled says ready. Nothing waits on a person for a reason that has
gone.

## What it saved

A hold is not free: somebody eventually looks at it and decides. What it
replaces is worse.

Two agents editing `packages/app/src/wire/lanes.ts` in one checkout do not
produce a merge conflict, because there are no branches to merge. They produce
one file with both edits in it, attributed to whichever agent committed second,
and a test failure neither of them can explain. The cheapest moment to notice
that is before the second agent starts — which is the only moment a queue gets
to choose.

The six holds cost six decisions. What they bought cannot be measured, because
the thing they prevent is the thing that then does not happen — which is worth
saying plainly rather than converting into a number nobody can check.

## Looking is never starting

Opening a piece of queued work shows the chain, why each link waits, and the
prompt its agent will be given, word for word. None of that runs anything.
Where something upstream failed the work it feeds is **held** rather than lost,
and Tade says so and asks — wait for a retry, start anyway, remove.

`readyToStart` is a pure function: what can run now, as far as each project has
room, in the order given. Held work is not ready, so it never takes a slot from
work behind it, and nothing a person merely looked at can be started by the
looking.

```sh
npm i -g tade-sh
```

The queue is one file — `packages/core/src/queue.ts` in
[the repository](https://github.com/mujacica/tade) — and it is pure: facts in,
answers out.
