Implemented providers: evaluations#
The evaluation each shipped provider was accepted on, kept in the repository so
it outlives the issue it came from. These entries record the evaluation at
acceptance. The provider module docstring and its tests are authoritative for
current behavior, which can have grown since (for example VS Code inheritance and
Ant extension points); when they disagree, fix the entry. The run command shown
is what nur delegates to. For every provider, discovery reads files and never executes
project code or calls the runner to list.
Entries follow the template, condensed. Where a provider has its own ADR, that ADR is the contract and the entry only links to it.
Providers that predate the register#
npm, make, pdm, poe, just, Taskfile, mise and xc were selected
under ADR 0001 before per-format evaluations were written down. Their entries
below were backfilled from the provider modules and tests, not from issue text,
and are checked against the three gates of
ADR 0005. Adoption: no figure for
these eight is recorded in the repository, so each entry says so rather than
quoting one. Observed gaps are recorded as they are, not fixed here.
npm#
Format:
scriptsof a Node package. Source:package.json(strict JSON). Docs: https://docs.npmjs.com/cli/using-npm/scripts.Run:
<pm> run <name>, where the package manager comes from the lockfile (first match ofbun.lock,bun.lockb,pnpm-lock.yaml,yarn.lock,package-lock.json;npmwhen none exists). Extra arguments follow--. Never use:npm runwith no argument to list.Gate check: (1) safe: the file is parsed as JSON and no runner is called. (2) faithful: the script runs under the package manager the lockfile names. (3) honest subset: names and command strings are read; nothing is evaluated.
Mapping:
prefixis alwaysnpm, even when the package manager is pnpm, yarn or bun;nameis the script key;argv_baseis(pm, "run", name);definitionis the script string (str()of the value); nodescription;source_fileispackage.json;passthrough_prefixis("--",).Edge cases: a missing, null or empty
scriptslists nothing, and so does a falsyscriptssuch asfalseor0, silently; a truthyscriptsthat is not an object, malformed JSON or an unreadable file logs a warning and lists nothing. A JSON document whose top level is not an object, and a file that is not valid UTF-8, are not guarded.Opaque:
pre*/post*hooks are listed as ordinary scripts and not run specially bynur;packageManager,enginesand workspaces (seeworkspacein ADR 0004) are not read.Adoption (information only): no figure known in the repository.
make#
Format: Make rules. Source:
Makefilein the current directory only (notmakefileorGNUmakefile). Contract: ADR 0003. Docs: https://www.gnu.org/software/make/manual/.Run:
make <target>. Never use:make -pRrqor any invocation that makes Make read the file: it evaluates$(shell ...)and!=while parsing.Gate check: (1) safe: tree-sitter-make syntax tree plus a line fallback; tests prove
$(shell ...)and!=never run. (2) faithful:make <target>is what the file declares. (3) honest subset: syntactic candidates only, as ADR 0003 states.Mapping:
prefixmake;namethe target;argv_base("make", name);descriptionfrom a##comment on the rule’s own header line (first non-empty one wins; a multi-target rule gives each target the text); nodefinition;source_fileMakefile.Edge cases: names match
[A-Za-z0-9][A-Za-z0-9._/-]*. Kept: multi-target,::and static-pattern rules, backslash-continued headers, rules inside conditionals (no branch is evaluated), target-specific variable targets. Skipped:.-prefixed specials such as.PHONY,%pattern rules, computed names ($(X)), a target literally namedMakefile(excluded even though it matches the name pattern), assignments, recipe lines,definebodies,includedirectives (never followed). Non-ASCII name fragments are rejected, not truncated. A BOM is stripped and line endings normalised. Input the grammar rejects logs a warning and goes through the line fallback, so valid rules around the damage are still listed. An unreadableMakefilelists nothing.Opaque: variable expansion, included files, the active branch of a conditional, prerequisites, recipe bodies and the default goal. Known limits (nested
define, targets named like directive keywords) are in ADR 0003.Adoption (information only): no figure known in the repository.
pdm#
Format: PDM user scripts. Source:
[tool.pdm.scripts]inpyproject.toml. Docs: https://pdm-project.org/latest/usage/scripts/.Run:
pdm run <name>. Never use:pdm run --list.Gate check: (1) safe:
tomllibonly;pdmis never called. (2) faithful:pdm run <name>is the documented way to run a script. (3) honest subset: names, a description and a flat command are read; composite behaviour is not.Mapping:
prefixpdm;argv_base("pdm", "run", name);descriptionfromhelpwhen it is a string;definitionfrom a string value, or a table’scmd, elseshell, elsecall(the first present wins; a list body is joined with spaces);source_filepyproject.toml; nopassthrough_prefix.Edge cases: detection needs a non-empty scripts table. The reserved
_table (shared options) is skipped. A bare list value (a composite) is listed, withdefinitionthe Pythonreprof the list, so treat it as opaque. A table with onlycompositehas an empty definition. Malformed TOML logs a warning and lists nothing. Thecallform is handled but has no test.Opaque:
compositeexpansion,env,env_file,working_dir,site_packages,{args}placeholders andpre_/post_hooks.Adoption (information only): no figure known in the repository.
poe#
Format: Poe the Poet tasks. Source:
[tool.poe.tasks]inpyproject.toml(nopoe_tasks.toml). Docs: https://poethepoet.natn.io/.Run:
poe <name>, the bare binary (notpoe run, notuv run poe). Never use:poewith no arguments, which prints the task list.Gate check: (1) safe:
tomllibonly. (2) faithful:poe <name>runs the declared task. (3) honest subset: as for pdm.Mapping:
prefixpoe;argv_base("poe", name);descriptionfromhelp;definitionfrom a string, or a table’scmd, elseshell, elsescript, a list body joined with spaces;source_filepyproject.toml; nopassthrough_prefix.Edge cases: detection needs a non-empty tasks table. A sequence task (a TOML array) is listed with the
reprof the list asdefinition. A table with onlysequence,switch,exprorrefhas an empty definition. Names starting with_are listed; the tests do not pin that. Malformed TOML logs a warning and lists nothing. A project that also has[tool.pdm]does not leak pdm scripts into poe (tested). Theshellandscriptforms are handled but untested.Opaque:
args,env,cwd,[tool.poe]options such asexecutor.Adoption (information only): no figure known in the repository.
just#
Format: just recipes. Source:
justfile, elseJustfile(not.justfile). Docs: https://just.systems/man/en/. Issue: #91.Run:
just <name>. Never use:just --dumporjust --list. Before #91 the provider shelled out tojust --dump; it now needs nojustbinary.Gate check: (1) safe: a text scan, no subprocess. (2) faithful:
just <name>. (3) honest subset: recipe names and doc text only.Mapping:
prefixjust;argv_base("just", name); a leading@is dropped from the name;descriptionfrom a[doc("...")]attribute, else the#comment directly above; nodefinition;source_filethe file found; nopassthrough_prefix.Edge cases: a header is an unindented
name args: depsline whose:is not followed by=, which excludesalias,export X :=andset. Parameters and dependencies are skipped. Indented lines are bodies. A blank line or an unindented non-comment line clears a pending comment; attribute lines between comment and recipe keep it, and so do indented lines, so a comment can still reach a later unindented recipe past an indented body line.doc(...)inside another attribute’s string is not mistaken for a description. Triple-quoted and triple-backtick literals are skipped so unindented text inside them is not read as a recipe (the toggle is naive about quote escaping). The first of a duplicate name wins.[private]and_-prefixed recipes are listed, althoughjust --listhides them. An unreadable or non-UTF-8 file logs a warning and lists nothing.Opaque:
alias,import,mod, parameters, dependencies, settings,[group]and[private]semantics. A move to tree-sitter is open as #294, see research.Adoption (information only): no figure known in the repository.
Taskfile#
Format: go-task. Source:
Taskfile.yml, elseTaskfile.yaml(other spellings andincludes:are not read). Docs: https://taskfile.dev/.Run:
task <name>. Never use:task --list: go-task evaluates dynamicsh:variables while loading, which can run repository-controlled commands. A test plants avars.<X>.shsentinel and proves it never runs.Gate check: (1) safe:
yaml.safe_loadof the top-leveltasksmapping. (2) faithful:task <name>. (3) honest subset: names anddesconly.Mapping:
prefixtask;argv_base("task", name);descriptionfromdescwhen it is a string (an empty string stays empty); nodefinition;source_filethe file found; nopassthrough_prefix.Edge cases: a shorthand string or list task is listed with no description.
internal: truetasks are skipped (the literal boolean only). A root ortasksthat is not a mapping lists nothing. Names are listed as written, including:namespaces and_or.prefixes. Malformed YAML and read errors log a warning and list nothing. A file that is not valid UTF-8 is not caught.Opaque:
includes:and the namespaced tasks they bring,aliases,summary,deps,cmds,vars,dir,platforms.Adoption (information only): no figure known in the repository.
mise#
Format: mise tasks. Source: the first existing of
mise.local.toml,mise.toml,.mise.local.toml,.mise.toml,.config/mise.toml. Docs: https://mise.jdx.dev/tasks/. Issue: #32.Run:
mise run <name>. Never use:mise tasks ls.Gate check: (1) safe:
tomllibonly. (2) faithful:mise run <name>. (3) honest subset: one file’s[tasks]table.Mapping:
prefixmise;argv_base("mise", "run", name);descriptionfrom a table’sdescription;definitionfromrun(a list joined with&&);source_filethe file chosen; nopassthrough_prefix.Edge cases: only the first file found is read and its siblings are ignored, so mise’s own merge of local and shared config is not replicated (a
mise.local.tomlhidesmise.toml). Detection needs a non-empty[tasks]. String and list shorthands work.hide = truetasks are skipped, matchingmise tasks ls. A table withoutrun(for instance afiletask) has an empty definition. Colon names such as"build:fast"pass through; nested tables are not flattened. Malformed or non-UTF-8 TOML logs a warning and lists nothing, without falling back to another file.Opaque: file tasks (
mise_tasks/,.mise/tasks/),alias,depends,dir,env,sources,outputs,tools, parent and global config.Adoption (information only): no figure known in the repository.
xc#
Format: xc, tasks written as Markdown headings. Source:
README.mdin the current directory only. Docs: https://xcfile.dev/task-syntax/task-list/. Issue: #36.Run:
xc <name>. Never use:xc -sto list. It does not run task scripts (the Markdown parser evaluation usesxc -shortas the reference listing), but ADR 0005 gate 1 forbids shelling out to a runner to enumerate, and a file scan needs noxcbinary. The module docstring says xc “executes task scripts”; that wording is imprecise.Gate check: (1) safe: a Markdown scan; xc is never called. (2) faithful:
xc <name>. (3) honest subset: names, description and first script only.Mapping:
prefixxc;argv_base("xc", name);descriptionthe prose between the heading and the first fence, attribute lines removed;definitionthe first fenced block’s contents;source_fileREADME.md; nopassthrough_prefix(extra arguments are appended positionally).Edge cases: the task section is the heading preceded by
<!-- xc-heading -->(which wins), else the first heading namedTasks(case-insensitive, emphasis stripped). It ends at the next heading of the same or higher level. Tasks are headings exactly one level deeper; deeper headings are body. Headings inside fences, indented code (4+ spaces) and bare##lines are not headings. A name that is empty or has whitespace is skipped. Tilde, nested and unclosed fences are handled. A task with no script is still listed. Detection needs at least one task. Non-UTF-8 or unreadable files log a warning and list nothing.Opaque: attribute values (
requires,dir,env,inputs,run,rundeps,interactive), script language, further fences, files other thanREADME.md. Measured behavior against the real runner: Markdown parser evaluation.Adoption (information only): no figure known in the repository.
deno#
Source:
deno.json, thendeno.jsonc(first existing). Issue: #71. Shipped: 0.3.0.Run:
deno task <name>. Never use:deno taskto list.Reads: top-level
tasks; a value is a command string or an object with acommand.descriptionfrom the object form. JSONC-tolerant (comments, trailing commas), so strictjson.loadsis wrong.Left out: the fallback to
package.jsonscripts (thenpmprovider already lists them, avoiding double listing);dependencies,env,cwd,--filtersemantics. Deno does not auto-runpre/posthooks, so none are synthesized.Adoption (information only): about 188 repositories (2025 Task Runner Census (aleyan.com), as recorded in #78).
composer#
Source:
composer.json. Issue: #72. Shipped: 0.3.0.Run:
composer run-script <name>. Never use:composer run-script --list.Reads:
scripts(string or array; arrays joined with&&);scripts-descriptionssupplies descriptions.Left out: PHP static-method callbacks and
@other-scriptreferences are listed by name with an opaquedefinition;@php,@composer,@putenv,@additional_argsdirectives are opaque. Reserved lifecycle events (pre/post-install-cmd,pre/post-update-cmd,post-autoload-dump, and the package, installer and plugin events) are filtered so the picker shows only user-defined scripts.Adoption (information only): about 3,135 repositories (2025 Task Runner Census (aleyan.com), as recorded in #78).
pre-commit#
Source:
.pre-commit-config.yaml. Issue: #73. Shipped: 0.5.0.Run:
pre-commit run <id> --all-files. Never use:pre-committo enumerate hooks.Reads:
repos[].hooks[]only; sibling top-level keys (ci,default_stages,files,exclude,fail_fast, …) are configuration and hold no hooks. Three repo kinds: local (entryis in this file, sodefinitionis available), remote (entrylives in the upstream.pre-commit-hooks.yaml, sodefinitionstays empty).metarepos (the built-inscheck-hooks-apply,check-useless-excludes,identity) are not special-cased: they are listed like remote hooks. Hooks without anidare skipped.Left out: resolving remote hooks’
entry,languageandargs(needs a clone);stagesfiltering.Resolved at implementation: plain
pre-commit run <id>runs on staged files and no-ops on a clean tree, so the provider delegates with--all-files. Still unconfirmed against primary sources: thenameversusaliasrule (the provider keys onid).Adoption (information only): about 3,632 repositories (2025 Task Runner Census (aleyan.com), as recorded in #78).
tox#
Source:
tox.ini,setup.cfg([tox:tox]section),pyproject.toml([tool.tox], includinglegacy_tox_ini),tox.toml, in that order. Issue: #74. Shipped: 0.5.0.Run:
tox -e <name>. Never use:tox -lortox -a.Reads: environments as the union of the expanded
env_list/envlistand the explicit[testenv:NAME](INI) or[tool.tox.env.NAME](TOML) sections, excluding the[testenv]base. INIenvlistis brace- and range-expanded ({py27,py36}-django{15,16}is a hyphen-joined Cartesian product;py3{10-14}expands too). TOMLenv_listlist items are literal (a top-level stringenv_listis split and brace-expanded;{replace = "env", ...}items are resolved). With noenvlistthe default env ispy. Missing fields are inherited from[testenv]orenv_run_base.Config discovery is version dependent. tox 3 uses
pyproject.toml,tox.ini,setup.cfg; tox 4 adds nativepyproject.tomlandtox.toml, and its real precedence was buggy until tox PR #3742 (merged Feb 2026, issue #3645). Pick the first existing file by a documented order and do not over-trust cross-version precedence.Also read: generative section headers
[testenv:{a,b}], factor-conditional commands (filtered by the current environment’s factors) andlabels(INI and TOML).Left out: TOML structured matrices (
range,labeled,product) and provisioning and package environments (.pkg,.tox, and the names given byprovision_tox_env,package_env,isolated_build_env,wheel_build_env), treated as non-runnable.Adoption (information only): about 1,869 repositories (2025 Task Runner Census (aleyan.com), as recorded in #78).
cargo-make#
Source:
Makefile.toml. Issue: #76. Shipped: 0.3.0.Run:
cargo make <name>. Never use:cargo make --list-all-steps.Reads:
[tasks.<name>]tables;description;commandplusargs, orscript(a list joined with&&) asdefinition.Skipped: tasks with
private = trueordisabled = true, and entries that are not tables.aliasis not resolved.Left out:
extends,env,dependencies,condition, profile-specificenv, cargo-make’s built-in predefined tasks (only locally declared tasks appear), and themakersalias.
moon#
Source:
moon.ymlin the current directory. Issue: #77. Shipped: 0.4.0.Run:
moon run <name>. Never use:moon query tasks.Reads: top-level
tasks:;description;commandplusargs, orscript.Left out: inherited tasks from
.moon/tasks.ymland.moon/tasks/**,.moon/workspace.yml(project locations, not tasks), project-qualified<project>:<task>addressing, anddeps,inputs,outputs,options.Skipped: tasks with
options.internal: true, which cannot be run from the CLI.
VS Code tasks#
Source:
.vscode/tasks.json. Issue: #139. Shipped: 0.5.0. Spec: https://code.visualstudio.com/docs/editor/tasks, schema appendix https://code.visualstudio.com/docs/editor/tasks-appendix.Run: the task’s own
commandandargs;definitionis the command and its args joined with spaces. Nopassthrough_prefix.Reads: schema
version"2.0.0", JSONC. Tasklabelis the name,detailthe description.shell,processand untyped tasks with acommandare runnable. A task or the document may carrywindows,linuxandosxoverride objects, merged for the current platform;commandandargsitems may be strings or{ "value", "quoting" }.Also read: a document-level
commandis inherited by commandless tasks, with its globalargsand thesuppressTaskName/taskSelectorrules.Left out: the legacy
"0.1.0"schema; any task whoseoptionssetenvorshell, or whosecwdresolves anywhere other than the project root (the whole task is skipped); extension-contributed task types (npm,typescript,gulp,grunt,cargo, …), which the extension resolves at runtime; any task with a non-emptydependsOn(even one that also has its owncommand); editor-only variables (${file},${selectedText},${input:...}): a task whose command, args or working directory still contains an unresolved variable is skipped, not listed (${workspaceFolder},${workspaceRoot},${workspaceFolderBasename},${pathSeparator},${/}and${env:X}resolve). Tasks withhide: trueare skipped. Document-leveloptions(includingenv) andtypemerge into each task. For duplicate labels the last definition wins.Not read:
groupandisDefault(a possible later mapping onto grouping) anddependsOrder.Adoption (information only): the most widely used statically parseable candidate when it was filed, and unlike GitHub Actions its tasks run locally. No census figure.
Sibling: Zed
.zed/tasks.json, see candidates.
invoke#
Source:
tasks.py. Issue: #140. Shipped: 0.6.0. Docs: https://www.pyinvoke.org/, https://docs.pyinvoke.org/en/stable/.Run:
invoke <name>. Never use:inv -l(imports and executes the module).Reads: AST of
@task-decorated functions, including@task(...),@invoke.taskand aliased imports; explicitname=andaliases=; first docstring line asdescription. The module is never imported.Notes: only top-level
defs with one decorator are read; underscores in names become dashes. A task with a non-emptypre=/post=is dropped (empty collections are accepted), and invalid task metadata or collidingdefault=Truetasks make the whole file list nothing.Left out: command bodies (
c.run(...)), parameters,pre/postgraphs,Collectionnamespacing (so namespaced setups are under-covered), and tasks built dynamically or imported from other modules.History: first parked “low priority” on reliability and value; accepted once value stopped being a gate (ADR 0004 and 0005). The parking reasons were that an opaque definition adds little over telling users to run
inv -l, and that parameter-driven tasks may not run frominvoke <name>alone (function arguments become CLI flags such as--repository; the first parameter is theContext, not a user argument).Adoption (information only): the runner behind Fabric and a common Makefile alternative in Python projects. No census figure. nox is its natural AST sibling.
nox#
Source:
noxfile.py. Issue: #142. Shipped: 0.5.0. Docs: https://nox.thea.codes/, https://nox.thea.codes/en/stable/.Run:
nox -s <name>. Never use:nox -l.Reads: AST of
@nox.session,@nox.session(...)and@session(aliased import); explicitname=; docstring first line.Left out:
python=[...]and@nox.parametrizeexpansion (concrete names such astests-3.11are runtime-generated, so only the base session name is listed, andnox -s testsstill runs all its variants), dynamically registered sessions, command bodies. If the module is not straight-line (loops,with,match, unrecognized star imports, namespace mutation;from nox import *is modeled), the provider lists nothing for the file. Only top-leveldefs register sessions;nox_uv.sessionis accepted.Notes: the first parameter is the
Session, not a user argument. Barenoxruns the default sessions;nox -s <name>selects a session name, which may cover several variants.nox -limports and executesnoxfile.py, so it is never used; displayed names may also differ from its output. nox is the imperative sibling of tox (#74).Adoption (information only): common as a tox alternative. No census figure.
Apache Ant#
Source:
build.xml. Issue: #141. Docs: https://ant.apache.org/, https://ant.apache.org/manual/, targets: https://ant.apache.org/manual/targets.html.Run:
ant <target>. Never use:ant -p.Reads:
<target name=...>, direct-child<extension-point name=...>elements (both run asant <name>) anddescription.dependsordering and<project default>are not read (an evaluation idea, not shipped). Declarative XML, so the safest of the imperative-adjacent set. Undescribed targets are listed even though Ant hides them from-p. Only direct children of<project>are read; names starting with-, files with a<!DOCTYPE>and non-<project>roots are skipped.Left out: targets from
<import>and<include>,<macrodef>and<presetdef>output,${property}interpolation (kept literal), non-default file names (ant -f foo.xml).if/unlesstargets are still named, so still listed.Notes:
definitionis empty: a target’s body is declarative XML tasks. Bareantruns the<project default>target. The attribute itself is not read, though the target it names is listed like any other.Adoption (information only): a legacy JVM tool, superseded by Maven and Gradle but still present in older enterprise code. No census figure. It was parked on demand, never on parseability.
Rake#
Contract: ADR 0002. Issue: #143. Shipped: 0.7.0. Docs: https://ruby.github.io/rake/, https://github.com/ruby/rake.
Run:
rake <qualified-name>. Never use:rake -T.Notes from the evaluation: namespacing determines the runnable name (
rake db:migrate, notrake migrate), so nestednamespaceblocks must be tracked;descbinds to the nexttask. First implementation grew into a partial Ruby interpreter and was reset to the bounded syntactic contract.Left out:
rakelib/*.rakeandrequired files (a possible follow-up pass), file tasks, rules and loop-generated tasks. A wrong namespaced name yields arake <name>that does not run.rake -Tloads and evaluates the Rakefile, so it is never used.Adoption (information only): ships with Ruby and underpins Rails. No census figure.
mask#
Source:
maskfile.md. Issue: #144. Shipped: 0.5.0. Docs: https://github.com/jacobdeichert/mask.Run:
mask <path...>(for examplemask test lint).nameis the heading path shell-quoted and space-joined (test lint; a single headingdeploy prodis named'deploy prod', distinct from nesteddeploy/prod) andargv_baseis("mask", *parts). Nested headings are emitted as full paths.Reads: the Markdown heading tree as the command tree; the
>blockquote as description (its last paragraph); the last fenced block under the heading asdefinition; heading args(pattern)or[optional]stripped from the name. Untagged or empty fences, andpowershell,batchandcmdfences on non-Windows, make a command unrunnable. A top-level title or prose heading with no code block or subcommands is not a task.Left out:
**OPTIONS**flag sections, interpolation of positional arguments and option variables intodefinition, the//include and--maskfilealternate file. The fence info string selects the interpreter and the body stays verbatim.Adoption (information only): a niche but liked single-binary runner. No census figure.
Overlap: it shares machinery with
xc; that was the reason it was first parked, and it no longer is one.Measured behavior: Markdown parser evaluation.
Grunt, Gulp, Turborepo, Nx, workspaces, npm-run-all#
ADR 0004 holds the full evaluation table, per-provider read boundaries and consequences. Summary:
Grunt: literal
grunt.registerTaskandregisterMultiTasknames via tree-sitter; rungrunt <name>.Gulp: literal
gulp.task(name, ...)and CommonJSexports.name; rungulp <name>.Turborepo:
turbo.jsontaskskeys (legacypipeline); runturbo run <task>.Nx:
nx.jsontargetDefaultskeys; runnx run-many -t <name>.workspace: members from
pnpm-workspace.yaml,package.jsonworkspaces,lerna.json; tasks named<package>:<script>, run through the lockfile-resolved package manager. Rooted traversal, never escapingcwd.npm-run-all, run-p, run-s: no provider; they compose
package.jsonscripts thenpmprovider already lists.