Why "AI Task Generation" Is Easy, and "AI Dependency Graphs" Are Not

Breaking a spec into tasks is mostly solved. Figuring out what actually blocks what is not, and one wrong edge quietly wrecks a sprint.


In the last post, I wrote about why we stopped using Linear. Short version: a login system isn't a line of tickets, it's a graph. Backend and frontend run in parallel, both blocked on a shared API contract, both blocking QA. Linear made us fake that graph with disconnected tickets. Ravel's whole premise is that AI should build the actual graph instead.

What I didn't get into there: that's a harder problem than it sounds, and it took us a few embarrassing failures to see why.

The easy 80%

Ask an LLM to turn "build a login system with email/password and OAuth" into a task list, and it will do a good job. Backend auth service, frontend login form, OAuth provider integration, session handling, password reset flow — AI decomposition is close to solved. Any reasonably capable model handles it out of the box.

The trouble starts one step later: figuring out how those tasks actually relate to each other.

Where it falls apart

Early on we fed the model a fairly ordinary spec for that same login system. It came back with a dependency chain that looked almost right and was wrong in a way that would have quietly broken a real sprint.

It decided the frontend login form depended on OAuth integration being finished first. Both were blocked on the same thing, a shared API contract, but nothing in the graph pointed there. Instead one task was chained behind the other for no real reason. Two engineers who could have started the same day would have been told, confidently, to work in sequence.

The model wasn't hallucinating. It was doing something harder to catch: the spec mentioned OAuth setup as a step, and it mentioned it before the login form, so the model inferred an ordering dependency from document order. That's a reasonable thing for a language model to do with text. It is not how software dependencies work.

That mistake pointed us at what we now think is the real problem. It's not really about intelligence. It's that "dependency" is an overloaded word.

One kind is mention order: thing A shows up earlier in the doc than thing B. Not a dependency at all.

Another is logical relation: two tasks touch the same feature. Related, but not necessarily blocking.

The third is what actually matters for scheduling: one task cannot start, or cannot be verified, until another finishes.

A list-based tool doesn't have to tell these apart. A human just orders the backlog by feel. If you want AI to output a dependency graph a real schedule can be built on, only the third kind counts, and the model has no built-in reason to know the difference. It has to be taught to keep asking: does this actually block that, or did I just read about it in that order?

We've hit the same overloading in other places since then.

Task granularity that doesn't match reality. The model might split "build login form" and "add form validation" into two tasks when no engineer would hand one off without the other. They're one unit of work wearing two labels.

Milestones dressed up as tasks. "Launch beta" isn't something anyone executes directly. It's a state the project reaches when a set of real tasks finish. Treat it like any other node in the graph and everything downstream gets confused about what it's waiting on.

Constraints that never make it into the spec. Nothing says "don't run the database migration during a release freeze." Everyone on the team just knows. The model, reading only the spec, doesn't.

None of these are edge cases you can shrug off. They're the ordinary texture of real projects. Get them wrong and you don't get a slightly-off diagram. You get a schedule people can't trust, which is worse than no schedule at all.

What actually helped

The single biggest improvement wasn't a smarter model. It was refusing to ask for the whole graph in one shot.

Our first version was one long prompt: here's the spec, give me back the full task graph. Most natural way to ask, and exactly what produces the OAuth-ordering mistake above. You're asking the model to extract tasks, judge granularity, infer every dependency, and catch missing constraints, all in one pass, with no room to check its own work.

What worked better was breaking that into narrower stages, each doing one job. Extract candidate tasks. Check whether the granularity is sane. Flag real ambiguity back to the user instead of guessing. Only then reason about blocking relationships. Each stage gets a smaller question, and a smaller question is one a model can get right more often. I'm deliberately vague about the exact pipeline here. That architecture is one of the things we've spent the most time tuning, and it's still evolving. The output feeds dependency-aware sprint planning instead of a one-shot guess.

We also stopped treating "the model produced an answer" as "the model is confident." When the spec doesn't say whether something is a hard dependency or just a mention, it's better for the system to surface that ambiguity, even if that means asking a clarifying question, than to silently pick one interpretation and hand back a graph that looks authoritative but is quietly wrong. A dependency graph that admits what it doesn't know beats one that guesses and hides it.

Where this still isn't solved

I'd rather be honest about this than oversell it. Dependency inference is the hardest part of what we're building, and it's not done.

It's better at spotting genuine blocking relationships than it was six months ago. It's still mostly blind to unstated tribal knowledge, the "everyone on the team just knows" category, because there's nothing in the spec to read.

The direction we're building toward is treating those gaps as first-class: flag "I'm not confident this is a real dependency" the same way a careful engineer would in a design review, instead of quietly picking an answer.

If you've built anything that tries to reason about task relationships from natural language, or if you've got a dependency-graph horror story of your own, I'd like to hear it. I don't think anyone has solved this well yet.

We're building this in the open at theravel.app.