2026-08-29 · Updated 2026-08-29 · 10 min read
Task management for autonomous coding-agent workflows
The operator's discipline for agent work that runs while you are elsewhere: admit each batch from dependency-ready tasks, make claims visible, requeue stale work, stop on conditions the queue itself can evaluate, and close every iteration with recorded evidence.
By Juno AI INC · yylo-ledger · autonomous
Hand a repository's worth of small, checkable changes to agents that iterate while you are asleep, and the interesting question stops being whether the model is smart. It becomes whether the queue itself can answer three questions at any moment, without a human in the loop: which work is allowed to start now, which work is already claimed and by which pass, and what did each finished pass actually leave behind. A TODO file answers those questions to whoever edited it last. A hosted board answers them to whoever drags the cards. An autonomous workflow needs the answers to be computed, timestamped, and reviewable — because the operator arrives in the morning and has to trust a state they never watched change.
This page is the operations manual for that queue on YYLO Ledger: how to shape the work before launch, how each batch is admitted, how claims go stale and get recovered, what stops the run, and what every iteration must deposit. Two neighboring layers are already owned elsewhere and linked rather than retold — the case for commit-bound task memory and the four ledger semantics behind it belong to the durable task-memory guide, and the on-disk contract those semantics ride on — schema, lifecycle, merge — belongs to the storage-format guide. Every command below was executed against the yylo-ledger release this site's package facts pin, and every runner behavior was traced through the committed script templates, on 2026-08-29.
Shape the wave before you launch it
An autonomous queue digests tasks, not intentions. Each body needs a finish line a stranger can check — a suite that must pass, a route that must render, a document that must exist — because the worker deciding "done" at 3 a.m. is exactly that stranger. Declared edges carry the ordering: create the task with --blocked-by listing the IDs whose completion must come first (the create surface also auto-parses dependency markup written into the body), and the graph, not the prose, decides what may run. Two write-time guards keep the wave honest. Duplicate submissions are refused up front when you ask for it — the flag's own help text promises it will "Reject creation when an open task (backlog/todo/in_progress) already has the exact same body" — and a wave that would deadlock on itself is refused too, because the ledger's README states flatly that "Cycle detection prevents circular dependencies."
Size the wave for one review sitting, not for the quarter. A dozen tasks whose completions you can actually read beats a three-hundred-row backlog nobody trusts; queues that grow faster than they drain are plans, not work. Tags are the routing surface — --tags backend on the way in, --tag backend on the way out — so a docs lane, a security lane, and a refactor lane can share one board without sharing one dispatcher. When the wave drains, create the next one; the dependency graph makes admission a computed answer at every size.
Draw each batch from readiness, not from the backlog
The backlog is a wish; readiness is a query. ready returns tasks in an actionable status whose declared blockers all exist and have each reached a terminal state — and the subtlety that matters for unattended dispatch is that actionable includes in_progress. A bare ready therefore hands you back the tasks some worker already claimed. The dispatcher's form filters claims out explicitly, and the sort contract is stable across the query family, as the README documents: "--sort asc|desc uses one shared contract across list, search, and ready." The batch shape:
--sort asc puts the oldest-modified candidates first, so a wave admitted at dawn finishes the work that has been waiting longest rather than whatever was created last. When more tasks are simultaneously eligible than slots, order --scores breaks the tie structurally: each task carries a priority score counting everything that transitively depends on it, which is why the README can say "Priority scoring ranks tasks by how much downstream work they unblock" — work the queue is waiting on goes first. And when the batch omits a task you counted on, deps Ab3c4d is the diagnosis: it names the blocker still holding the task, with that blocker's current status, plus the dependents and the score. A missing task is a held task; the query tells you by whom.
Make claims visible and stale claims recoverable
A claim in this system is a status change with an account attached: mark in_progress with a response that says who is taking the work and from where. Two properties turn that into queue hygiene. Every mutation rewrites the task's last_modified timestamp, so a claim is dated by the tool, not by the claimant. And the same sort contract that orders admission orders the recovery sweep — list --status in_progress --sort asc is the staleness radar, oldest claims first, the tasks most likely to be abandoned or crashed on top.
What you find there gets one of three recoveries, and the ledger supports all three:
- Resume it. The claim is fresh and the worker's session survives; continue that conversation rather than restarting the work.
- Requeue it. The claim is stale or the worker died;
mark todoreturns the task to the pool, and the default transition map permits exactly that move fromin_progress— with a response explaining why the work came back, because a requeue without an account is just a second abandonment. - Retire it. The task was a dead end. Archiving is not cosmetic here: terminal means resolved, so archiving a stuck blocker immediately releases every task waiting on it — the dependent that could never start becomes ready with no other edit.
Claims can also be contested — two dispatchers, one stale cache of the board — and the guarded form of update refuses to lose that race: --expected-revision fails the write with a stale-revision error naming both the revision you expected and the revision actually on disk, before a byte moves. For queues that carry deadlines, the same discipline gets a date field: declare due_date as a typed custom field in the ledger config, set it per task with --field, and search --overdue returns exactly the open tasks whose date has passed — the README's phrase for the family is "Core/tag/body filters plus typed custom fields, date ranges, and --overdue". Run it unconfigured and the CLI refuses rather than guessing, with an error stating that --overdue requires the configured date field.
Stop on conditions the queue can evaluate
A run you leave unattended must know how to end without you, and the ending has to mean something. YYLO's loop driver is the worked example; its README describes the contract in one sentence: "Continuously runs yylo until all kanban tasks are completed. Uses a do-while loop: yylo runs at least once, then continues while tasks remain in backlog, todo, or in_progress status." Read that against the ledger and every stop is a query over task state. Exhaustion is the empty answer to the open-status listing. The ceiling is a number — the driver's maximum-iteration cap ends the run as a normal exit with the count in the log, a budget spent rather than an error thrown. And the tripwire is a diff: the driver snapshots the board between passes, and after a configurable run of passes that changed nothing it fires the stale hook and exits with a status code distinct from both success and crash, so a wrapper can separate a wedged run from a finished one.
Two details make those stops safe rather than decorative. The driver treats a failed pass as retryable state, not a reason to halt — the committed template's own comment on a nonzero inner exit is "Continue the loop even if yylo fails - it might succeed next iteration" — and its tripwire only counts passes that change nothing, which is exactly why the operator should add the one stop the driver does not own: a failure budget. A queue can churn expensively while mutating the board on every pass — claiming, failing, requeueing — and churn never trips the no-change wire; so count failed passes themselves, whatever they moved, and stop the queue when that count crosses a small threshold, then read the evidence instead of paying for the same failure in a new costume. And when the tripwire fires, the shipped default hook converts the stall into work — its command opens a new todo task on the board with the warning text "You haven't done anything on the kanban in the past run. You need to process a task, or if you find it unsuitable or unresolvable, you need to archive the task," so the stalled night leaves a task, not a scrollback. The general method behind these — pairing numeric stops with semantic ones, making staleness machine-detectable — is the bounded-failure guide's territory, and installing the three stops into a loop you already run is the hardened-loop guide's job; this page contributes only the queue-level reading: every stop your queue owns should be a query the ledger can answer, and every stop that fires should leave a task, a count, or a receipt behind.
Close every iteration with evidence
An iteration that ends without a record did not happen, and the ledger holds that line where it can. The response is a gate — a mark without one is refused as a usage error and no byte moves. The commit hash is a nudge: omitted on mark done, the CLI still stores the completion and prints a reminder to standard error, which is why a finished task missing its hash stays readable as exactly that in get and the listings. For iterations whose outcome must survive independently — batch runners, audited waves — the mutating commands also write a receipt: pass --receipt-file and you get the task's content hash before and after the change, the exact fields that moved, the ledger event's identifier, and the persisted path — a machine-checkable account of the mutation itself, not a summary anyone authored by hand.
The morning review then needs no terminal history. search --commit <hash> returns every task carrying that change's hash; history <id> replays one task's transitions as a hash-chained sequence, each event sealed to the one before it, so tampering shows as a broken link rather than a discrepancy you have to argue about. For batch runs, the same discipline lives in run artifacts: the batch runner atomically writes a machine-readable status file whose own docblock states "The status JSON is the single source of truth for run completion." — the wait helper parks on exactly that file instead of parsing human-readable logs — and each worker's structured result carries its session handle, so an iteration that needs continuing is continued exactly, not rerun from scratch. One stronger close exists for whole waves: multi-task finalization through umbrella-finalize, where the README states the boundary as "Umbrella child reconciliation is available only through umbrella-finalize" — sealed admission and evidence receipts, one shared commit, every child updated in one recoverable transaction.
Where this queue hands off
One worker draining one board is the smallest honest unit, and the discipline above is deliberately sized for it. When the wave is wide, the same admission rule fans out to concurrent lanes — quotas, isolation, and per-run artifacts are the safe parallel execution guide's ground. When steps must hand results to each other inside one bounded unit, you are choreographing, not queuing, and the runner-choice guide draws that line. When the question is why the shared Markdown TODO file this queue replaced fails under concurrency in the first place, the failure-anatomy guide takes that apart, mechanism by mechanism. Install the ledger from PyPI, initialize it in the repository the agents already work in, create one wave with finish lines and edges, and let the first unattended drain teach you the rest — the storage contract underneath is linked above, and the Ledger documentation carries the full command surface.