mise (Polyglot)
A single-binary, mise.toml-configured polyglot dev-tool manager, environment loader, and task runner (the successor to rtx, itself an asdf reimagining) that — since the 2026 experimental_monorepo_root feature — also discovers sub-projects across a tree, prefixes their tasks as //path:task, and runs them through a petgraph DAG with sources/outputs change detection.
| Field | Value |
|---|---|
| Language | Rust (the binary); workspace config in TOML (mise.toml) + standalone task scripts |
| License | MIT (Copyright Jeff Dickey @jdx and contributors) |
| Repository | jdx/mise |
| Documentation | mise.jdx.dev · Tasks · Monorepo Tasks · Task architecture |
| Category | Generic Task Runner |
| Workspace model | No package graph; two layers — (a) hierarchical mise.toml config that layers tools/env/tasks per directory, and (b) opt-in monorepo mode (experimental_monorepo_root = true) that discovers config_roots and namespaces tasks as //project:task |
| First released | As rtx (2023); renamed to mise in 2024. CalVer release line |
| Latest release | 2026.6.0 (June 3, 2026) |
Latest release:
2026.6.0, published June 3, 2026 (releases). mise uses a CalVerYYYY.M.PATCHscheme and ships extremely frequently (211 releases across 2025). The monorepo tasks feature analysed here is still gated behindexperimental_monorepo_root = trueand the globalMISE_EXPERIMENTAL=1flag (monorepo docs). Source citations below are againstmainat commitebf4795and the official docs as of June 5, 2026.
Overview
What it solves
mise's positioning is broader than the rest of the Generic Task Runner family — it is three tools in one binary. From the README (README.md):
"
miseprepares your development environment before each command runs. It keeps project tools, environment variables, and tasks in onemise.tomlfile so new shells, checkouts, and CI jobs all start from the same setup."
The three pillars, also stated in the README, are: install and switch between dev tools (node, python, terraform, … — an asdf replacement), load environment variables per project directory (a direnv replacement), and define and run tasks (a make/just replacement). It is the only tool in this survey that bundles a version manager with the task runner, which is exactly what makes its monorepo story distinctive: a member's [tools] and [env] are layered automatically alongside its [tasks].
For a polyglot monorepo, mise is the glue layer plus toolchain layer. Like Task and just it orchestrates commands, not packages — it has no dependency resolver for the project graph, no unified lockfile across members, no package store, and no remote-execution backend. The actual build of each member is delegated to that member's native tool (cargo build, npm ci, uv sync, dub build), invoked as a task run command. What mise adds over Task/just is (1) automatic per-directory tool + env layering and (2) — new in 2026 — first-class monorepo task discovery with //...:build wildcards.
mise itself draws the comparison explicitly (docs/tasks/monorepo.md):
"mise's advantage: Simplicity through non-hermetic builds. mise doesn't try to control your entire build environment in isolation — instead, it manages tools and tasks in a flexible, practical way. … You get powerful monorepo task management with simple TOML configuration — enough power for most teams without the enterprise-level complexity that hermetic builds require."
Design philosophy
Three principles shape the tool, all visible in the source and docs:
Layered configuration, not a workspace manifest. mise's core data model is a stack of
mise.tomlfiles discovered by walking from the CWD up to the root (and~/.config/mise/config.tomlglobally). Each file contributes[tools],[env], and[tasks]; child files override parents. There is nomembers = [...]array in base mode — the "workspace" is implicit in the directory tree. Monorepo mode (below) is an opt-in overlay on top of this.Non-hermetic by design. Unlike Bazel/Buck2, mise makes no attempt to sandbox a task or capture undeclared inputs (the per-task
--deny-read/--deny-net/--deny-writesandbox flags are an opt-in, experimental, security feature, not a correctness/hermeticity guarantee). Change detection is best-effortsources/outputscomparison — the same correctness ceiling as Task and moon, well below a sandboxed action graph.One static Rust binary, batteries included. No Node, JVM, or plugin runtime to bootstrap; the tool manager, env loader, task DAG, and a
blake3content cache all ship in the singlemiseexecutable. This is the ergonomic axis on which it competes with Task/just while doing strictly more.
Within this survey mise is the canonical "task runner that is also a version manager, with an opt-in monorepo overlay" data point: compare it against Task (file-fingerprinting runner, no tool management, hand-enumerated includes), just (command runner, no change detection at all), and moon/Turborepo (which add the project graph, content- addressed cache, and --affected detection mise omits).
How it works
The configuration stack
A mise project is anchored by one or more mise.toml files. The config-root resolver (config/config_file/config_root.rs) recognises a wide family of filenames — mise.toml, .mise.toml, .config/mise.toml, mise/config.toml, .mise/config.toml, .config/mise/config.toml, .config/mise/conf.d/*.toml, plus .tool-versions (asdf compat) — and maps each back to its config root (the directory the config governs). A single file declares all three pillars:
# mise.toml
[tools]
node = "20"
python = "3.12"
[env]
DATABASE_URL = "postgres://localhost/dev"
[tasks.build]
run = "npm run build"
sources = ["src/**/*.ts"]
outputs = ["dist/**/*.js"]
depends = ["lint"]
[tasks.lint]
run = "eslint src"Running mise run build activates node@20/python@3.12, exports the [env] vars, then resolves the task DAG. Tasks can equally live as standalone scripts in mise-tasks/, .mise/tasks/, mise/tasks/, or .config/mise/tasks/ — the filename becomes the task name (cli/run.rs):
$ cat .mise/tasks/build <<EOF
#!/usr/bin/env bash
npm run build
EOF
$ mise run buildThe task graph
When mise run is invoked, mise builds a petgraph::DiGraph<Task, ()> over the requested tasks and their transitive dependencies (task/deps.rs). Three dependency edges exist, deserialised from the task config (task/task_dep.rs, docs/tasks/task-configuration.md):
| Key | Semantics (verbatim from docs) |
|---|---|
depends | "Tasks that must be run before this task." |
depends_post | "Like depends but these tasks run after this task and its dependencies complete." |
wait_for | "It will wait for these tasks to complete before running however they won't be added to the list of tasks to run." (soft ordering) |
A dependency entry is flexible: a bare string ("lint"), an array (["build", "arg1"] → task + args), or a table ({ task = "build", env = { … } }). Shell-style FOO=bar taskname arg is also parsed. The graph nodes are keyed by (name, args, env) so the same task with different args/env is a distinct node (task_key).
Workspace declaration & topology
mise has two topology mechanisms, and distinguishing them is the most important fact about it for this survey.
Base mode: hierarchical config discovery (no explicit members)
By default there is no workspace declaration at all. mise walks the directory tree from the CWD upward, loading every mise.toml it finds and layering them — parent tools/env/tasks are inherited by children, children override parents. A "monorepo" in base mode is simply a tree of directories that each carry a mise.toml; tasks from the current directory's hierarchy are the ones visible. There is no glob array of members, no project metadata, and no cross-directory task discovery — a task in packages/api/mise.toml is invisible from the repo root unless you cd into packages/api.
Monorepo mode: opt-in discovery + // namespacing
The 2026 monorepo tasks feature (discussion #6564, docs/tasks/monorepo.md) adds an explicit overlay. It is enabled by a flag in the root mise.toml, and requires the global experimental gate:
# repo-root mise.toml
experimental_monorepo_root = true
[monorepo]
config_roots = [
"packages/frontend",
"packages/backend",
"services/*",
]The root-marking is detected by experimental_monorepo_root() and find_monorepo_root (config/mod.rs); the [monorepo] section deserialises to a tiny struct (config/config_file/mise_toml.rs):
// src/config/config_file/mise_toml.rs
pub struct MonorepoConfig {
/// Explicit list of config roots for monorepo task discovery.
/// Supports single-level glob patterns (*).
#[serde(default)]
pub config_roots: Vec<String>,
}IMPORTANT
Discovery globs are single-level only. config_roots accepts services/* but rejects recursive ** — expand*config_roots (config/mod.rs) errors with *"recursive glob '**' not supported … use single-level '*' instead"_, and rejects absolute or out-of-tree paths. If config_roots is omitted entirely, mise auto-discovers any subdirectory containing a recognised config file. This is more automatic than Task's hand-enumerated includes, but less expressive than Cargo's members = ["crates/*"] (which **-globs) or pnpm's recursive packages: globs.
Once in monorepo mode, every sub-project's tasks are prefixed by their root-relative path (prefix_monorepo_task_names):
// src/config/mod.rs — prefix_monorepo_task_names
const MONOREPO_PATH_PREFIX: &str = "//";
const MONOREPO_TASK_SEPARATOR: &str = ":";
// "packages/frontend" + "build" => "//packages/frontend:build"So a task build in packages/frontend/mise.toml is addressed globally as //packages/frontend:build. The //-from-root / :-current-dir convention is borrowed straight from Bazel/Buck2 label syntax (//pkg:target), and extract_monorepo_path (task/mod.rs) parses it back out.
NOTE
mise loads sub-project tasks lazily. By default (should_load_subdirs == false) only the current directory hierarchy is loaded; subdirectories are walked only when a wildcard (//...:build) or path hint (//packages/...) or --all is present (config/mod.rs load_local_tasks_with_context). This keeps the common single-project mise run build fast even in a large repo.
Dependency handling & isolation
This dimension splits cleanly between mise's two roles.
Project dependencies — delegated (no resolver, no unified lockfile)
For the project graph, mise behaves like every other generic runner: it does not resolve packages, hoist, symlink, maintain a virtual store, or produce a unified lockfile across members. Each member's real dependencies are installed by that member's native package manager, invoked as a task run command. Cross-member ordering is a task edge, not a package edge: depends = ["//libs/core:build"] guarantees the library builds first, but linking the dependent against the library's artifact remains the language toolchain's concern (a path = in Cargo.toml, a replace in go.mod, a path= in dub.sdl). There is no workspace:-protocol equivalent for one member to depend on a sibling's package.
NOTE
mise has a real but separate "deps" subsystem (src/deps/) with providers for npm, pnpm, yarn, bun, pip, uv, poetry, go, composer, bundler, dart, and git_submodule. These detect and install a single project's language dependencies (the mise install/deps flow) — they are not a cross-member workspace resolver. mise orchestrates these per project; it does not unify them into one lockfile the way Cargo/uv /pnpm do.
Tool isolation — mise's distinctive layer
Where mise diverges from Task/just is per-task and per-project tool/env isolation, which it owns natively because it is also a version manager:
Per-directory tool layering. Each
mise.tomlin the config stack contributes[tools]; a sub-project can pinnode = "18"while the root pinsnode = "20", and the nearest config wins for commands in that directory. In monorepo mode eachconfig_root's tools and env are layered automatically when its tasks run.Per-task tools. A task can declare its own
[tools]table — the field istools: IndexMap<String, TaskToolValue>on theTaskstruct (task/mod.rs) — to install/activate a specific tool version for just that task (docs: "Tools to install and activate before running the task.").mise run --tool node@20 buildadds tools ad-hoc from the CLI (cli/run.rs).Per-task env, not inherited by deps. A task's
[env]is scoped to that task; the docs are explicit that these "will not be passed todependstasks." This is a deliberate isolation boundary between a task and its prerequisites.
This tool/env layering is the closest mise comes to "dependency isolation," and it is genuinely more than the pure runners offer — but it isolates toolchains and environment, not package dependency trees.
Task orchestration & scheduling
This is where mise earns its keep as a build-ish tool.
The DAG and the leaf-emitting scheduler
Deps::new (task/deps.rs) builds the petgraph::DiGraph, walking each task's resolved depends/depends_post/wait_for edges, detecting cycles, and recording dep_edges and post_dep_parents maps. Execution is a streaming leaf-emission model rather than a precomputed topological order: emit_leaves finds all graph nodes with no outstanding outgoing edges (graph.externals( Direction::Outgoing)) and pushes them to the scheduler; as each task completes it is removed from the graph, which re-emits any newly-freed leaves. This naturally maximises parallelism — every currently-runnable task is dispatched at once.
The Scheduler (task/task_scheduler.rs) is a Tokio construct: a Semaphore caps concurrency, a JoinSet tracks in-flight work, and an mpsc::UnboundedSender<SchedMsg> feeds leaves into a run_loop. On a task failure without --continue-on-error, it sends SIGTERM to every sibling (CmdLineRunner::kill_all(SIGTERM)) so the run aborts promptly:
// src/task/task_scheduler.rs — Scheduler::new
pub fn new(jobs: usize) -> Self {
let (sched_tx, sched_rx) = mpsc::unbounded_channel::<SchedMsg>();
Self {
semaphore: Arc::new(Semaphore::new(jobs)), // parallelism cap
jset: Arc::new(Mutex::new(JoinSet::new())),
// ...
}
}Concurrency defaults to 4 jobs, set by --jobs/-j (or MISE_JOBS, or jobs in config); -j 1 serialises. The DAG is the unit of parallelism — there is no separate "parallel vs sequential deps" split as in Task; everything that is concurrently runnable runs concurrently up to the job cap.
Change detection — sources/outputs, mtime or blake3
A task with sources declared is skipped when fresh (task/task_source_checker.rs, docs):
"if this and
outputsis defined, mise will skip executing tasks where the modification time of the oldest output file is newer than the modification time of the newest source file."
sources_are_fresh implements two staleness strategies, selected by settings:
| Mode | Mechanism |
|---|---|
| metadata hash (default) | Hash file metadata (size/mtime) of all sources into a digest stored beside the task; mismatch ⇒ stale. Then compare newest-source mtime vs oldest-output mtime. |
content hash (source_freshness_hash_contents) | Hash sources contents with blake3 (cached per file) instead of metadata — robust to mtime churn (e.g. fresh git clone, tarball extraction). |
sources/outputs accept gitignore-style globs (with !-exclusions), parsed via ignore::overrides::Override (build_source_matcher). Two escape hatches mirror the auto convention: outputs = { auto = true } lets mise "touch an internally tracked file based on the hash of the task definition" (docs) instead of you listing outputs; and save_checksum touches those auto-outputs after a successful run. Files with a UNIX-epoch mtime (timestamp-stripped tarball extracts) are conservatively treated as stale.
IMPORTANT
Dependency-driven cascade re-run. mise tracks, per task, whether each dependency actually ran (mark_ran / any_dep_ran in task/deps.rs). The documented contract (docs): "When a task depends on another task that also has sources defined, and the dependency runs because its sources changed, the dependent task will also re-run — even if the dependent's own sources haven't changed." This is the monorepo-critical behaviour Task lacks: a downstream member rebuilds when an upstream member it depends on was rebuilt, propagating change through the DAG.
WARNING
No Git-aware affected-detection. There is no --affected <ref> / --since main that computes "which members changed since a branch," the way moon / Turborepo / Nx do. mise's only change detection is the per-task sources fingerprint (plus the dependency cascade). Restricting a run to "what changed across the repo" must be hand-built, or approximated with //...:build (which runs every match and relies on each task's sources skip to no-op the unchanged ones).
Caching & remote execution
mise's caching is local-only and skip-only, the same shape as Task's and fundamentally different from the artifact-replay caches of Turborepo/Bazel:
What is cached is a fingerprint, not an artifact. mise stores the
sourceshash (metadata- orblake3-content-based) and, forautooutputs, a touched marker file — under the mise cache directory, not the build outputs themselves. On a "hit," mise skips re-running the command and leaves existing outputs in place; on a miss it re-runs and rewrites the hash. It never archives, restores, or transports build outputs. Consequence: a freshgit clonehas no outputs and (with the default metadata strategy) churned mtimes, so everything re-runs — thesource_freshness_hash_contents(blake3) mode exists precisely to make the skip survive mtime resets, but it still does not replay a missing artifact.No remote cache, no REAPI, no remote execution. There is no content- addressable artifact store, no Bazel Remote-Execution-API client, no shared-team cache. mise's own monorepo docs draw the line explicitly (
docs/tasks/monorepo.md): Bazel "offers incredible features like distributed caching, remote execution, and hermetic builds," and mise positions itself as the deliberately simpler, non-hermetic alternative that forgoes them. The docs' own "when not to use mise" guidance: "You need advanced task caching → Nx, Turborepo, or Bazel offer sophisticated caching systems."Remote tasks ≠ remote cache. mise can fetch a task definition from an
http(s)://orgit::URL (thetask_file_providers—remote_task_http.rs,remote_task_git.rs), with--no-cache/MISE_TASK_REMOTE_NO_CACHEto bypass the local copy. As in Task, this caches the configuration, not build results.
NOTE
The practical consequence for a monorepo: mise's incrementality is real on a warm checkout (don't re-run a task whose sources are unchanged, and propagate re-runs through depends), but there is no cross-machine cache. Teams that want shared/remote caching pair mise with an external CI cache keyed on the same globs, or reach for moon / Turborepo / Bazel.
CLI / UX ergonomics
mise's command boundary is mise run [flags] <task...> (aliased mise r, and a bare mise <task> works for non-ambiguous names). The monorepo addressing scheme is the ergonomic headline.
| Invocation | Meaning |
|---|---|
mise run build | Run build (and its depends) from the current config hierarchy |
mise //packages/frontend:build | Run a specific sub-project's task by absolute //-from-root path |
mise :build | Run build in the current config_root (the : prefix) |
mise //...:test | Path wildcard — run test in every project at any depth (...) |
mise //packages/...:build | Run build in every project under packages/ |
mise '//projects/frontend:*' | Task-name wildcard (*) — run all tasks in one project |
mise '//...:test*' | Combined path + name wildcards |
mise run -j 8 test | Cap parallelism at 8 jobs (--jobs; default 4) |
mise run -f build | --force — run even if sources/outputs say up to date |
mise run -n build | --dry-run — print tasks in execution order without running |
mise run -c a b | --continue-on-error — don't SIGTERM siblings on a failure |
mise run -o prefix|interleave|keep-order | Output mode: per-line task-labelled, raw interleaved, or ordered |
mise tasks deps [--dot] | Print the dependency graph (DOT format for Graphviz) |
The //path:task / ... / * wildcard grammar is mise's answer to the filter-flag problem. Where Task forces explicit enumeration and pnpm/Turborepo use a --filter flag, mise folds project selection into the task address itself — Bazel-style. //...:test is the "test everything" broadcast; //services/...:build is a sub-tree slice; //api:lint is a single target. There is, however, no --since/--affected Git slicing — the wildcard runs every match and leans on per-task sources skips to avoid redundant work.
NOTE
mise warns that it "will never define commands with a // or : prefix" (monorepo docs) — the namespace grammar is reserved so it can never collide with a built-in subcommand. The trade-off is that the powerful addressing only exists inside an experimental_monorepo_root repo behind MISE_EXPERIMENTAL=1; outside monorepo mode you get plain mise run <name>.
Strengths
- Three tools in one binary. A version manager (
asdf), env loader (direnv), and task runner (make/just) unified in one static Rust executable with no runtime to bootstrap — uniquely, the toolchain and the tasks live in the samemise.toml. - Automatic per-directory tool + env layering. Each member's
[tools]/[env]activate when its tasks run, with child-overrides-parent inheritance — isolation the pure runners (Task, just) cannot offer. - A real
petgraphDAG with maximal-parallelism leaf scheduling, a configurable job cap, and three edge types (depends,depends_post,wait_for). - Content-aware change detection with a
blake3option.sources/outputsskips, plus ablake3content-hash mode that survivesgit clonemtime churn, plus a dependency-cascade re-run that Task lacks. - Bazel-style
//path:taskaddressing with.../*wildcards — project selection folded into the task name, no separate--filterflag to learn. - Auto project discovery (
config_rootssingle-level globs, or zero-config subdir discovery) — more automatic than Task's hand-enumeratedincludes.
Weaknesses
- Monorepo mode is experimental and double-gated (
experimental_monorepo_rootandMISE_EXPERIMENTAL=1); the rich//path:taskergonomics don't exist in plain mode. - No package resolver, no unified lockfile, no
workspace:protocol. Cross- member linkage is a task edge, not a package edge; version unification across members is the user's problem. config_rootsglobs are single-level only (*, never**) — deep hierarchies need every level enumerated or rely on auto-discovery.- Cache is skip-only and local-only. It stores fingerprints, not artifacts; no remote/shared cache, no REAPI, no remote execution — incrementality is lost on a fresh CI checkout (mitigated, not solved, by
blake3content hashing). - No Git-aware affected-detection (
--since/--affected); "run only what changed" is approximated by//...:task+ per-tasksourcesskips. - Non-hermetic. Undeclared inputs cause stale "up to date"; the
--deny-*sandbox is a security feature, not a correctness guarantee.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| Bundle version manager + env loader + task runner in one binary | One mise.toml, one tool to install; toolchain and tasks co-located per project | Larger scope/surface than a pure runner; task features compete for roadmap with tool management |
Layered per-directory mise.toml instead of a workspace manifest | Zero-ceremony; nearest-config-wins gives natural per-member tool/env overrides | No explicit member list/metadata in base mode; cross-dir task discovery needs monorepo overlay |
Opt-in monorepo mode (experimental_monorepo_root + config_roots) | Keeps the common single-project path fast; namespaced //path:task discovery when asked | Experimental + double-gated; single-level globs only; lazy subdir loading has edge cases |
Bazel-style //path:task + .../* addressing (no --filter) | Project selection folded into the task name; reserved grammar can't collide with subcommands | Only available inside monorepo mode; no --since/--affected graph slicing |
petgraph DAG with streaming leaf emission | Maximises parallelism automatically; newly-freed leaves dispatch immediately | All-or-nothing concurrency model; ordering nuance pushed into depends_post/wait_for |
sources/outputs skip with metadata or blake3 content hash | Content mode survives mtime churn (clones, tarballs); metadata mode is cheap by default | Not hermetic — undeclared inputs cause stale hits; content hashing adds per-file hash cost |
Dependency-cascade re-run (any_dep_ran) | A downstream member rebuilds when an upstream depends member rebuilt — correct for monorepos | Coarser than fine-grained input tracking; a dep that re-ran but produced no change still cascades |
| Cache stores fingerprints, not artifacts; local only | Tiny, server-less, no cache infra to run | No artifact replay; no remote/shared cache; incrementality lost on fresh CI checkouts |
| Non-hermetic, delegate real builds to native managers | Polyglot by construction; no resolver to maintain; no codebase restructuring | No unified lockfile/isolation; correctness ceiling below sandboxed engines (Bazel) |
Sources
- jdx/mise — GitHub repository (Rust, MIT; source for the README positioning quote, formerly
rtx) - mise.jdx.dev — official documentation · Tasks · Task configuration
- Monorepo Tasks documentation —
experimental_monorepo_root,config_roots,//path:task,.../*wildcards, Bazel/Nx/Turborepo comparison - Task System Architecture — DAG,
depends/depends_post/wait_for,--jobs, sources/outputs freshness src/config/config_file/config_root.rs— config-file family + config-root resolutionsrc/config/config_file/mise_toml.rs—MonorepoConfig { config_roots }structsrc/config/mod.rs—find_monorepo_root,prefix_monorepo_task_names(///:),expand_config_roots(single-level glob), lazy subdir loadingsrc/task/deps.rs—petgraphDAG,emit_leaves,task_key,mark_ran/any_dep_rancascadesrc/task/task_scheduler.rs— TokioSemaphore/JoinSetscheduler,SIGTERMsibling killsrc/task/task_source_checker.rs—sources_are_fresh, metadata vsblake3content hash,autooutputssrc/task/task_dep.rs·src/task/mod.rs—TaskDepparsing,extract_monorepo_path, per-tasktoolssrc/cli/run.rs—mise runflags (--jobs,--force,--dry-run,--continue-on-error,--output,--tool,--no-cache)- Releases —
2026.6.0(June 3, 2026); Introducing Monorepo Tasks (discussion #6564) - Related: Task · just · make · moon · Turborepo · Nx · Cargo · uv · pnpm · Bazel · Buck2 · D landscape