DEV Community

yureki_lab
yureki_lab

Posted on

How I Taught My Autonomous Coding Agent What to Work On Next

TL;DR

I run an autonomous coding agent that works through a backlog of real engineering tasks — bug fixes, refactors, small features — with no one telling it what to do next. The hard part was never "can it write code," it was "how does it decide what to write code for." This post walks through the priority-scoring system I built so the agent picks its own next task, why my first three attempts at this failed in boring but expensive ways, and what I'd do differently if I started over.

The Problem

For the first few weeks of running this thing, I was the scheduler. I'd finish my coffee, glance at a list of open issues, and tell the agent "do this one next." It worked fine, but it defeated half the point of having an autonomous agent — I was still the bottleneck, just a bottleneck that occasionally stepped away from the keyboard.

So I tried the obvious fix: dump every open task into a flat list and let the agent pick whatever looked interesting. That lasted about four days before it turned into a mess:

  • It cherry-picked easy, satisfying tasks (rename this variable, add a docstring) and let a gnarly race-condition bug sit untouched for two weeks.
  • It occasionally re-opened a task it had "sort of" finished the day before, because nothing marked partial work as still-in-flight.
  • When the backlog ran dry, it just... stopped. No new work, no signal, just silence until I noticed and fed it something.

None of these are exotic problems. They're the same scheduling problems every task-tracker and CI queue has solved a dozen times. The difference is that a human running triage brings judgment to the table for free — "this bug is scary, do it now" — and an agent picking from a flat list has none of that judgment unless you build it in explicitly.

How I Solved It

I ended up building a small priority-scoring layer that sits between "list of candidate tasks" and "task the agent actually starts." It's not fancy — no ML, no learned ranking model — just a handful of signals combined into one number, plus rules for what happens at the edges.

The signals

Each candidate task gets scored on four axes:

def priority_score(task):
    urgency = task.urgency          # 0-10: is something broken/blocked right now?
    dependency = task.unblocks_count  # how many other tasks are waiting on this one?
    staleness = days_since(task.created_at)  # how long has this sat untouched?
    cost = task.estimated_effort    # rough size estimate, 1 (small) - 5 (large)

    return (
        urgency * 3.0
        + dependency * 2.0
        + min(staleness, 30) * 0.5   # capped so old-but-unimportant doesn't dominate
        - cost * 1.0                 # small bias toward finishing more, smaller things
    )
Enter fullscreen mode Exit fullscreen mode

The weights aren't principled — I hand-tuned them by watching a week of picks and nudging numbers when the agent made a call I disagreed with. That's the honest answer. Anyone telling you their priority weights came from first principles is probably rounding up.

Here's a realistic snapshot of what the backlog looks like on an average day, scored and ranked:

Task Urgency Unblocks Staleness (days) Cost Score
Fix flaky auth test 7 2 3 2 24.5
Refactor payment retry logic 4 0 21 4 22.5
Add docstring to utils module 1 0 45 1 16.5
Investigate slow query on dashboard 6 1 1 3 20.5

The flaky test wins narrowly over the payment refactor, which is roughly what I'd expect a decent human triager to pick too — not because it's more important in the abstract, but because it's cheap, urgent, and unblocks other work. The docstring task, despite being the oldest item on the board, stays near the bottom because staleness is capped and urgency is weighted three times as heavily.

Resolving conflicts

Scoring alone isn't enough, because ties and near-ties happen constantly, and re-scoring on every loop iteration causes a subtler problem: thrashing. If task A and task B have scores 8.1 and 8.0, and B's score creeps up slightly because it's gotten one day staler, the agent would swap mid-work. So I added two rules:

  1. Once a task is picked, it's pinned until it's done, blocked, or explicitly abandoned. Re-scoring only affects the next pick, never the current one.
  2. Ties within 10% of the top score are broken by smallest cost first. This is the "finish more things" bias — all else being equal, ship the small win.
flowchart LR
    A[Candidate tasks] --> B[Score each task]
    B --> C{Top score within 10% of others?}
    C -->|Yes| D[Pick smallest-cost among top group]
    C -->|No| E[Pick top score]
    D --> F[Pin task, start work]
    E --> F
    F --> G{Done, blocked, or abandoned?}
    G -->|Not yet| F
    G -->|Yes| A
Enter fullscreen mode Exit fullscreen mode

The empty-queue problem

The silence-when-idle failure turned out to be the most interesting one to fix. An empty backlog isn't actually empty — there's almost always something worth doing, it just isn't sitting in a ticket yet. So instead of treating an empty queue as "nothing to do," I treat it as a trigger for a self-scan pass:

  • Grep the codebase for TODO / FIXME comments older than 30 days
  • Check the last test run for flaky tests (passed on retry, failed on first attempt) and file those as candidates
  • Look for functions with no test coverage that were touched in the last week

Each of those becomes a low-urgency, low-cost candidate task, scored the same way as everything else. It doesn't guarantee good taste, but it guarantees the agent never just... stops.

Human override

I kept one escape hatch: a task can be flagged with a force-priority that skips scoring entirely and jumps the queue. I use this maybe once a week, usually for "actually, drop everything, this is on fire." Without it, I'd have had to explain "on fire" as a set of signals, which is more engineering than it's worth for something this rare.

Lessons Learned

  1. A priority score without a staleness term quietly starves your most important boring work. Urgent-and-recent tasks will always look better than important-but-unglamorous ones unless staleness has real weight. My first version had no staleness term at all, and a legitimately scary bug sat for two weeks while the agent polished docstrings.

  2. Cheap-first is a trap if it's the only rule. I initially had the agent always prefer the smallest task, on the theory that "ship more things" is good. It is good — right up until it becomes an excuse to never touch the big scary refactor. Cost should break ties, not set direction.

  3. Log the reasoning, not just the pick. I have the agent write one line explaining why it picked a task before starting — something like Picked "fix flaky auth test" — score 24.5 (urgency 7, unblocks 2, stale 3d). Runner-up: payment retry refactor at 22.5. This turned debugging bad picks from guesswork into a five-second read. When I disagree with a pick, I almost always disagree with a weight, not the logic, and the log makes that obvious immediately instead of after an hour of digging.

  4. Pin the task once it's started, or you'll get thrashing. Re-scoring on every loop felt more "reactive" and "responsive" on paper. In practice it meant the agent would abandon 80%-done work because something else's score ticked up half a point.

  5. An idle agent is a design bug, not a vacation. The instinct is to treat "queue is empty" as a success state. It's actually a sign your task-generation isn't pulling its weight yet — there is always more useful work (stale TODOs, uncovered code, flaky tests) if you build the self-scan to find it.

What's Next

The current scoring function is hand-tuned weights, which is honest but crude. The next thing I want to try is having the agent itself propose a weight adjustment after a batch of picks turn out badly (in a supervised, log-and-review way, not a fully closed loop — I'm not ready to let it rewrite its own priorities unsupervised). I'm also looking at richer dependency tracking — right now "unblocks_count" is a manually maintained number, and it should really be inferred from the codebase graph instead.

Wrap-up

If you're running any kind of autonomous or semi-autonomous coding agent past the toy-demo stage, the scheduling problem will find you faster than you expect — usually within the first couple weeks. Cherry-picking and idling aren't edge cases, they're the default behavior of any system that scores nothing and just reacts to what's in front of it.

If this was useful, follow me here on Dev.to — I'm writing this whole build-in-public series as I go. And if you haven't tried giving Claude Code's Plan Mode a spin as a review gate before handing off autonomy, that pairs really well with everything above.

Top comments (0)