Graph & parallelism
The dependency graph
deps lists the tasks that must complete before a task runs — these edges form a DAG. Each task runs its deps first, then itself.
[tasks.ci] deps = ["lint", "test", "build"] [tasks."web#build"] run = "vite build" dir = "apps/web" deps = ["ui#build"] # explicit cross-package edge
Explicit cross-package edges (
pkg#task, e.g.ui#build) ship in v1 — they require no graph inference.Shared dependencies are de-duplicated: in a diamond (
ci → a → base,ci → b → base),baseruns once.A dependency cycle is a config error, caught before execution, exit
64.
The upstream marker ^task ("run this task in the package's dependencies first") is deferred to v1.1, because it requires reading each package's manifest to build the package dependency graph.
Sequential by default
Execution is sequential by default. Concurrency is opt-in via parallel = true, and the rule is uniform:
a
depslist runs one at a time unlessparallel = true;a
packagesfan-out runs one at a time unlessparallel = true.
Nothing runs concurrently unless a task explicitly sets parallel = true. This keeps default behaviour predictable and race-free.
[tasks.ci] deps = ["lint", "test", "build"] parallel = true # opt into concurrency
Fail-fast
On any failure within a task's batch, tsr fails fast: it stops launching new work and kills still-running siblings, then prints a summary and exits.
✗ ci failed ✓ lint ok 1.2s ✗ test exit 1 3.4s ← failed ⊘ build skipped (killed: sibling failed) exit code: 1
In a parallel batch, "the failure" is whichever child exits non-zero first in wall-clock time; this is non-deterministic across runs and is expected. Fail-fast guarantees at most one failing child's code is reported — and that exact code is what tsr propagates.