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.
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 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.
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.