tade — a queue that holds, and says why it did

blog /

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.

The Tade project·4 min read·queueagentsdesign

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:

# 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-chain-in-the-queue · paneldrawn by Tade
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.

the-plan · paneldrawn by Tade
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:

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.

npm i -g tade-sh

The queue is one file — packages/core/src/queue.ts in the repository — and it is pure: facts in, answers out.

blog← oldernewer →The Tade project│2026-09-28│4 min│top