fyrer.yml, figures out the correct order, and runs each command with full log streaming.
Execution lifecycle
1
Parse config
fyrer reads
fyrer.yml from the current directory (or the path given to --config) and validates it: version check, unique package and task names, valid paths, valid glob patterns, and mutually exclusive flags like cache + persistent.2
Resolve graph
Every
depends_on declaration is resolved into edges of a directed acyclic graph (DAG). Each node in the graph is a fully-qualified package:task identifier. fyrer validates that no cycles exist and reports an error at startup if one is found.3
Level execution
Tasks are grouped into execution levels. Tasks with no unmet dependencies form level 0 and run immediately. Tasks whose dependencies are all in level 0 form level 1, and so on. All tasks within a level run concurrently; fyrer waits for the entire level to finish before advancing to the next.
4
Cache check
For tasks with
cache: true, fyrer computes a blake3 hash of the task’s ID, command, working directory, resolved environment, matched input file contents, and the cache keys of every dependency. If a matching cache entry exists, the task is skipped and reported as ⚡ Cached. Declared outputs are restored from the cache archive before the skip.5
Output streaming
For tasks that are not cached, their stdout and stderr are streamed in real time as the command runs. In plain mode (
-n), every line is prefixed with the package:task label and colorized so interleaved output from concurrent tasks remains easy to read. The default TUI mode routes logs into a per-task pane you can browse interactively.6
Completion
After all tasks have run, a summary is shown with counts for successful, failed, cached, and skipped tasks, plus the total wall-clock duration.
Concurrency model
Tasks within the same graph level run in parallel via Tokio’s async runtime. fyrer spawns each command as a child process and polls all of them concurrently. A level is considered complete only when every task in it has either succeeded, failed, or been skipped. Only then does fyrer advance to the next level. This model means you get maximum parallelism within each level without any task at a later level starting before its dependencies have finished.Failure propagation
If a task exits with a non-zero status, it is marked as failed. Any task that declares a (transitive) dependency on the failed task is skipped — not failed itself. This distinction matters in the summary: failed tasks are those that actually crashed; skipped tasks are those that couldn’t run because something upstream went wrong.Persistent tasks
Tasks markedpersistent: true are intended for long-running processes like development servers. Because they never exit on their own, they hold their graph level open indefinitely — which means any tasks scheduled at later levels will never start.
Keep persistent tasks on the leaves of your graph (no other tasks should depend on them). A typical pattern is to have build tasks run at early levels, and then start all dev servers at the final level once everything is built.
Plain mode
Pass-n / --no-tui to fyrer run to disable the interactive TUI and get prefixed, colorized log output instead:
package:task so the source is always clear even when multiple tasks are running at the same time.
