@tm/core (packages/tm-core/src/modules/dependencies/), and taskgraph graph, taskgraph why, taskgraph next and the dependency_analysis MCP tool only display it. Some older code paths still do their own thing; see Open issues.
How next orders tasks
taskgraph next (and the recommendation in taskgraph list) considers tasks that are pending or in-progress and whose dependencies are all done. Subtasks of an in-progress parent are considered first, when both their own dependencies and their parent’s are done (see Parents and subtasks). A subtask whose parent is not in-progress is not offered on its own: next offers the parent, and once you start it, next offers its subtasks. Among the candidates it picks by:
- Priority:
critical, thenhigh,medium,low. - Unblocks the most: the task with the most unfinished tasks waiting on it, directly or through other tasks. Between two subtasks that tie, the one more of its own task waits on.
- Fewer dependencies: the task that lists fewer dependencies.
- ID: the lowest ID.
deferred, blocked and review ones, because they still wait on their dependencies. A task is “unfinished” unless it is done or cancelled.
It counts top-level tasks: a parent and its subtasks are one piece of work, however finely the task was expanded. A task in front of a parent with ten subtasks counts that parent once, the same as before the parent was expanded, and subtasks that wait through their parent count toward that parent. A subtask doesn’t count its own parent or siblings, so one that holds up only its own task counts 0 (step 2 then compares how many of its siblings wait on it).
How a subtask’s dependencies are read
A subtask’sdependencies can name a sibling, another task’s subtask or a top-level task. The stored value decides which:
- A number below 100 (
3) is a sibling subtask:3under task 2 is2.3. - A dotted string (
"4.1") is that subtask. - Anything else (a string without a dot, like
"3", or a number of 100 or more) is a top-level task.
add-dependency and add-subtask --dependencies (and their MCP tools) take the IDs you see in list: 3 is task 3 and 2.1 is that subtask. They store a task as a string ("3"), so it is never read as a sibling. To depend on a sibling, give its dotted ID (2.1 under task 2). add-subtask rejects an entry that is not a task or subtask ID.
When expand, update-task or update has the AI write subtasks, the AI uses the same forms: a number for an earlier subtask in its list, a string for a task ("3") or another task’s subtask ("4.1"). Entries that are none of these, and a dependency on the subtask’s own parent, are dropped. update-task also drops a sibling number that is not earlier than the subtask itself.
On a top-level task there is no sibling to name: a number or a plain string is a task, and a dotted string is a subtask. add-dependency and add-task --dependencies store a task there as "3" and a subtask as "3.2", the form tm-core saves, and add-dependency rewrites a list that mixes 3 and "3" into that form when it adds to it. next, why, graph, the dependency commands and the MCP tools all read dependencies by this rule (resolveSubtaskDependency in @tm/core).
Parents and subtasks
A subtask is part of its parent, so the analysis links them both ways:- A subtask waits on what its parent waits on. If task 3 depends on task 2, subtask 3.1 can’t start before task 2 is done either, even though 3.1 lists nothing.
why,next, the critical path, bottleneck counts and loop checks all follow this. A parent’s dependency on one of its own subtasks is not passed down. - A parent isn’t finished until its subtasks are. The critical path orders a parent after its open subtasks, so a chain can run
1 → 3.1 → 3.2 → 3 → 5. A subtask that lists its own parent keeps that dependency, and the parent is then not ordered after it.
validate-dependencies and fix-dependencies check only the dependencies written on each task and subtask, so they don’t report a loop that only exists through a parent (task 2 needs 4, and 4 needs 2.1). why and the cycles of dependency_analysis do.
taskgraph graph
needs 4). The task next would pick is marked ← next; when next picks a subtask, its parent is marked ← next: 3.2. Tasks in a dependency loop and dependency IDs that match no task are called out.
blocks Nmarks a bottleneck: an unfinished task that N unfinished top-level tasks wait on, directly, through other tasks, or through a subtask. A parent and its subtasks count as one, as innext. The list shows the top five, most first, ties to the lowest ID. Tasks that block nothing are left out.wave Nis the task’s parallel wave. Wave 1 is every task that can run now:pendingorin-progresswith all its dependencies finished. Wave N holds the tasks whose dependencies are all finished or in earlier waves, so everything in one wave can run side by side. The line under the critical path counts the waves and names wave 1;N can't be scheduledcounts the unfinished tasks with no wave (see Parallel waves).◆marks the critical path: the longest chain of unfinished work, dependencies first, through parents and subtasks as described above. Ties go to the lowest IDs.needs 2.1: a dependency on a subtask places the task under the subtask’s parent (task 2), and the subtask is always listed after the title, since the parent is not what the task waits on.*marks a dependency the AI chose rather than you (see Dependency provenance). After an ID in aneedslist, it marks that dependency. After a task’s own ID (2*), it marks that task’s edge to the task drawn above it.
JSON output
--format json returns the tree plus the analysis:
Known limits
- Tasks in a dependency cycle, and tasks that depend on one, are left out of the critical path, since they can’t be started. Use
taskgraph whyto find and break the loop. - The tree shows top-level tasks only.
bottlenecksandcriticalPathcan name subtasks such as2.1; they appear in those lists but not as tree nodes. - The tree reads only top-level tasks’ dependencies, so a loop formed by a subtask’s own dependencies (task 4 needs 2.1, and 2.1 needs 4) is not called out in the tree;
why,validate-dependenciesand thecyclesofdependency_analysisreport it. A loop through a parent’s dependencies (task 2 needs 4, and 4 needs 2.1) is called out. - A loop that only exists because a parent finishes after its subtasks (subtask 2.1 needs task 3, and 3 needs task 2) keeps those tasks off the critical path, but is not reported as a cycle: each task can still start.
- A dependency on an archived task (see
taskgraph archive) counts as done and isn’t drawn: the task sits at the top level, or under its other dependencies. --iddraws the task and the tasks placed under it. Each task is drawn under only one dependency (the one it waits on longest), so a task that also needs the chosen task, but is drawn under another dependency, does not appear. The chosen task’s own parent edge is not shown or marked, and bottlenecks and the critical path still cover the whole tag.
Parallel waves
parallelWaves(tasks) in @tm/core (parallel-waves.ts) groups unfinished tasks into waves. It is what graph shows and what a runner that starts several tasks at once should read.
- Top-level tasks only. A task is the unit of parallel work; its subtasks run inside it. This matches the tree
graphdraws. - Finished tasks (
done,cancelled) get no wave and satisfy whatever waits on them. - A dependency on a subtask (
2.1) is satisfied once that subtask or its parent is finished. Until then the task goes in a wave after the parent (task 2). - Unfinished tasks with no wave are listed in
unscheduledwith areason:cycle: in a dependency loop (a task that lists itself counts).blockersnames the loop members it depends on.missing: names a dependency ID that matches no task.nextnever picks such a task, so it gets no wave either.blockerslists the IDs.held:review,blockedordeferred. Onlypendingandin-progresstasks go in a wave.waiting: depends on a task that has no wave.blockerslists those tasks.
taskgraph why
why follows a task’s unfinished dependencies down to the root causes and prints the tree of blockers, then what to do:
ready (unfinished, nothing in its way), stalled (deferred or blocked), missing (the ID matches no task) or cycle (it depends on itself through the chain). The JSON has id, title, status, blocked, tag, a nested blockers tree and rootCauses; see why for the full shape.
A subtask also waits on what its parent waits on. why draws those blockers under the parent, marked ← parent:
parentOf set to the subtask’s ID. The parent itself is not something to finish first; only its blockers are.
Exit codes
why and graph exit 0 on success, whether or not the task is blocked. They exit 1 for a missing or malformed ID (why), an unknown task or tag, or an unsupported --format.
Dependency provenance
Every dependency is either explicit (you added it, or it was already there) or inferred (the AI chose it inparse-prd, add-task, expand, update, update-task, or scope-up / scope-down). A dependency you asked for in an update prompt is still recorded as inferred, because the model decides which IDs to write. The other AI commands (research, analyze-complexity, update-subtask) never write dependencies.
Where it is stored
Provenance lives in the task’smetadata.dependencySources, a map from dependency ID to "inferred":
- Only inferred dependencies are stored. A dependency with no entry is explicit, so existing files need no migration.
- Keys are full IDs: the ID the dependency names by the rule above. A task’s keys are plain IDs (
"4"). On a subtask of task 2, a sibling is keyed"2.1"whether it is stored as1or"2.1", and a task stored as"3"is keyed"3". Code reads marks throughgetDependencySourcein@tm/core, never from raw keys. dependencySourcesis a reserved metadata key. Themetadataparameter of the MCPupdate_taskandupdate_subtasktools (available only withTASKGRAPH_ALLOW_METADATA_UPDATES=true) drops it, so a hand-written value can’t overwrite provenance.- Marks for a dependency that is removed are dropped with it, and a moved task carries its marks to the new ID.
- Running
add-dependencyfor a dependency that is already inferred makes it explicit. - Known limit: when
update-taskregenerates subtasks and the AI reorders them, subtasks are renumbered without remapping their sibling dependencies, so a sibling dependency can change source.
Links that don’t block
A dependency means “wait for this task”. Sometimes two tasks are only connected: they touch the same code, or you found one while doing the other. Link them without making either wait:
Only
blocks counts anywhere a task waits: next, why, graph (blocked counts and the critical path), loop checks, and validate-dependencies / fix-dependencies. show lists the other two after what the task depends on:
- Changing the type. Running
add-dependency --type=relatedon a task that already depends on the other turns the dependency into a link, so the task stops waiting. Running plainadd-dependencyon a linked pair turns the link back into a dependency. - Removing a link.
remove-dependencyremoves a link the same way it removes a dependency. - Cleanup. Removing a task removes the links to it (and to its subtasks), as it does dependencies. Moving a task within a tag carries the links to its new ID. Moving it to another tag drops links that would cross tags.
validate-dependenciesreports a link to a task that doesn’t exist andfix-dependenciesremoves it. - MCP.
add_dependencytakestype(blocks,relatedordiscovered-from) andadd_tasktakesdiscoveredFrom.
Where links are stored
Non-blocking links live in the task’smetadata.taskLinks, a map from full ID to link type, not in dependencies:
dependencies is a blocks link, so files written before link types existed load unchanged. Keeping the other links out of dependencies means no code that reads dependencies can mistake them for blockers. Keys are always full IDs ("2.1" for a subtask), on a task or a subtask. If the same task is both a dependency and a link, the dependency wins. Like dependencySources, taskLinks is a reserved metadata key that the metadata parameter of update_task and update_subtask drops.
Schema version
Each tag in.taskgraph/tasks/tasks.json records metadata.schemaVersion (currently 1), stamped whenever TaskGraph saves the tag, from any command, and in the empty file init creates. A missing or malformed value is read as version 1. A tag written by a newer version, or with a malformed schemaVersion, prints a warning to stderr once per file and tag, and is still read. The stamp never lowers a version already on disk.
MCP
Thedependency_analysis tool, in the standard and all tiers, returns the bottlenecks, critical path and cycles for a tag. Pass id to also get an explanation with the blockers and root causes for one task (what taskgraph why shows). limit sets how many bottlenecks come back (default 5). A tag the project doesn’t have is an error, not an empty result. See MCP tool loading for the tiers.
Open issues
These are known and not yet fixed:- What counts as finished differs for subtasks.
next,whyandgraphall treatdoneandcancelledas finished, butnextonly looks for finished work under parents that arepending,in-progress,doneorcancelled. Adonesubtask under a parent inreview,blockedordeferredsatisfies a dependency inwhyandgraph, but never innext. - Older writers don’t stamp a schema version. Tags created by legacy code paths have no
schemaVersionuntil the next@tm/coresave; a missing value means version 1. - Graph limits: cycle members and tasks behind a loop are left out of the critical path, and the tree doesn’t call out a loop formed only by a subtask’s own dependencies (see
taskgraph graph).

