Problem
Task runners hide underlying commands from RTK's hook system.
When a user runs mise run lint, RTK's hook sees mise — not biome check --write. Result: 0% token savings for task-runner-heavy workflows. The filtering intelligence built into RTK never activates because the hook can't see through the abstraction layer.
Context
Community report from a contributor who wraps all commands in mise run <task>. Same gap applies to just, task (go-task), and any abstraction layer. make already has basic TOML filter support but the problem is identical.
This is a growing issue given mise adoption in AI-assisted coding workflows — where task runners are increasingly the standard entry point for dev operations.
Task Runner Introspection Capabilities
| Runner |
Config file |
Dry-run |
JSON dump |
Parse difficulty |
| mise |
mise.toml → [tasks.<name>].run |
No native flag |
mise tasks ls --json |
Easy (TOML) |
| just |
justfile |
just --dry-run <recipe> |
just --dump --dump-format json |
Easy (JSON) |
| task |
Taskfile.yml |
task --dry <task> |
task --list --json |
Medium (YAML) |
| make |
Makefile |
make -n <target> |
No |
Hard (not worth parsing) |
Implementation Plan
Phase 1 — TOML Filters + Registry (quick win, ~2-3h)
Strip task runner boilerplate from output via TOML filters. Register patterns in the rewrite registry so the hook intercepts task runner commands.
New files:
src/filters/mise.toml — strip [mise] banners, $ command echoes, blank lines
src/filters/just.toml — strip recipe echo lines, timing output
src/filters/taskfile.toml — strip task: [Name] headers, timing output
Modified files:
src/discover/rules.rs — add 3 patterns (^mise\s+run\b, ^just\s+, ^task\s+) + 3 rules
CHANGELOG.md
Impact: 30-50% savings from boilerplate removal. Hook rewrites mise run lint → rtk mise run lint. TOML filter matches in run_fallback().
Ships independently. Zero breaking changes.
Phase 2 — Dedicated Rust Module with Routing (core, ~4-6h)
Single src/task_runner_cmd.rs module that:
- Runs the task runner command, captures stdout/stderr
- Resolves the underlying tool via config file introspection
- Routes output through the specialized
filter_*() function (mirrors npx pattern)
- Falls back to TOML filter if no resolution possible
Architecture: Run the task runner (preserves env setup, hooks, dep ordering), then filter captured output through the right RTK filter function. RTK never bypasses the task runner.
Delegation logic:
underlying tool detected → delegate to specialized filter
biome/eslint → lint_cmd::filter_lint_output()
tsc → tsc_cmd::filter_tsc_output()
cargo test → runner test filter
vitest/jest → vitest_cmd filter
pytest → pytest_cmd filter
unknown → TOML filter fallback → summary heuristic → passthrough
New files:
src/task_runner_cmd.rs — unified module with TaskRunner enum (Mise/Just/Task)
tests/fixtures/mise_run_lint_raw.txt, just_lint_raw.txt
Modified files:
src/main.rs — add Mise, Just, Task to Commands enum + routing
src/lint_cmd.rs, src/tsc_cmd.rs, etc. — expose filter_*() functions as pub
src/toml_filter.rs — add mise/just/task to RUST_HANDLED_COMMANDS
Needs Phase 1. Ships independently after Phase 1.
Phase 3 — Config Introspection + User Mappings (smart, ~3-4h)
Auto-parse task runner config files to resolve task → command mappings. Add user-configurable overrides.
Resolution priority:
- User config
[task_runners.mappings] in config.toml (explicit override, always wins)
- Config file introspection (
mise.toml → tasks.lint.run, just --dump --dump-format json)
- TOML filter for boilerplate stripping
summary heuristic detection
- Raw passthrough
Config example (~/.config/rtk/config.toml):
[task_runners.mappings]
lint = "biome check"
test = "cargo test"
typecheck = "tsc --noEmit"
Modified files:
src/config.rs — add TaskRunnerConfig with mappings: HashMap<String, String>
src/task_runner_cmd.rs — add introspection functions per runner
Needs Phase 2. Ships independently.
Phase 4 — Discover Integration (polish, ~1h)
rtk discover reports missed savings from task runner commands in Claude Code sessions.
Already covered by Phase 1 registry entries — classify_command() picks up new patterns automatically. Needs verification + a test to confirm classification fires correctly.
Needs Phase 1. Ships independently.
Key Design Decisions
Run the task runner, not the underlying command. RTK executes mise run lint, not biome check --write directly. This preserves task runner semantics: env vars, pre/post hooks, dependency ordering. Bypassing the runner would break workflows that rely on these side effects.
One Rust module for all runners. task_runner_cmd.rs with a TaskRunner enum, not separate modules per runner. The introspection and delegation logic is shared — splitting it across files adds complexity without benefit.
Lazy parsing. Config file introspection happens only when the command is invoked, never on startup. The <10ms startup constraint is non-negotiable.
No persistent cache. In-memory resolution per invocation is sufficient. Parsing mise.toml costs <5ms. A persistent cache adds complexity and invalidation edge cases.
Hook stays dumb. mise run lint → rtk mise run lint. All intelligence lives in the Rust binary, not the hook script.
Acceptance Criteria
Priority and Effort
| Phase |
Effort |
Savings |
Ships independently |
| Phase 1 |
2-3h |
30-50% |
Yes |
| Phase 2 |
4-6h |
60-90% |
Yes (after Phase 1) |
| Phase 3 |
3-4h |
Auto-resolution |
Yes (after Phase 2) |
| Phase 4 |
1h |
Analytics |
Yes (after Phase 1) |
Phase 1 is a clear quick win — shippable standalone and unblocks all other phases.
Problem
Task runners hide underlying commands from RTK's hook system.
When a user runs
mise run lint, RTK's hook seesmise— notbiome check --write. Result: 0% token savings for task-runner-heavy workflows. The filtering intelligence built into RTK never activates because the hook can't see through the abstraction layer.Context
Community report from a contributor who wraps all commands in
mise run <task>. Same gap applies tojust,task(go-task), and any abstraction layer.makealready has basic TOML filter support but the problem is identical.This is a growing issue given mise adoption in AI-assisted coding workflows — where task runners are increasingly the standard entry point for dev operations.
Task Runner Introspection Capabilities
mise.toml→[tasks.<name>].runmise tasks ls --jsonjustfilejust --dry-run <recipe>just --dump --dump-format jsonTaskfile.ymltask --dry <task>task --list --jsonMakefilemake -n <target>Implementation Plan
Phase 1 — TOML Filters + Registry (quick win, ~2-3h)
Strip task runner boilerplate from output via TOML filters. Register patterns in the rewrite registry so the hook intercepts task runner commands.
New files:
src/filters/mise.toml— strip[mise]banners,$ commandechoes, blank linessrc/filters/just.toml— strip recipe echo lines, timing outputsrc/filters/taskfile.toml— striptask: [Name]headers, timing outputModified files:
src/discover/rules.rs— add 3 patterns (^mise\s+run\b,^just\s+,^task\s+) + 3 rulesCHANGELOG.mdImpact: 30-50% savings from boilerplate removal. Hook rewrites
mise run lint→rtk mise run lint. TOML filter matches inrun_fallback().Ships independently. Zero breaking changes.
Phase 2 — Dedicated Rust Module with Routing (core, ~4-6h)
Single
src/task_runner_cmd.rsmodule that:filter_*()function (mirrors npx pattern)Architecture: Run the task runner (preserves env setup, hooks, dep ordering), then filter captured output through the right RTK filter function. RTK never bypasses the task runner.
Delegation logic:
New files:
src/task_runner_cmd.rs— unified module withTaskRunnerenum (Mise/Just/Task)tests/fixtures/mise_run_lint_raw.txt,just_lint_raw.txtModified files:
src/main.rs— addMise,Just,Taskto Commands enum + routingsrc/lint_cmd.rs,src/tsc_cmd.rs, etc. — exposefilter_*()functions aspubsrc/toml_filter.rs— addmise/just/tasktoRUST_HANDLED_COMMANDSNeeds Phase 1. Ships independently after Phase 1.
Phase 3 — Config Introspection + User Mappings (smart, ~3-4h)
Auto-parse task runner config files to resolve task → command mappings. Add user-configurable overrides.
Resolution priority:
[task_runners.mappings]inconfig.toml(explicit override, always wins)mise.toml→tasks.lint.run,just --dump --dump-format json)summaryheuristic detectionConfig example (
~/.config/rtk/config.toml):Modified files:
src/config.rs— addTaskRunnerConfigwithmappings: HashMap<String, String>src/task_runner_cmd.rs— add introspection functions per runnerNeeds Phase 2. Ships independently.
Phase 4 — Discover Integration (polish, ~1h)
rtk discoverreports missed savings from task runner commands in Claude Code sessions.Already covered by Phase 1 registry entries —
classify_command()picks up new patterns automatically. Needs verification + a test to confirm classification fires correctly.Needs Phase 1. Ships independently.
Key Design Decisions
Run the task runner, not the underlying command. RTK executes
mise run lint, notbiome check --writedirectly. This preserves task runner semantics: env vars, pre/post hooks, dependency ordering. Bypassing the runner would break workflows that rely on these side effects.One Rust module for all runners.
task_runner_cmd.rswith aTaskRunnerenum, not separate modules per runner. The introspection and delegation logic is shared — splitting it across files adds complexity without benefit.Lazy parsing. Config file introspection happens only when the command is invoked, never on startup. The <10ms startup constraint is non-negotiable.
No persistent cache. In-memory resolution per invocation is sufficient. Parsing
mise.tomlcosts <5ms. A persistent cache adds complexity and invalidation edge cases.Hook stays dumb.
mise run lint→rtk mise run lint. All intelligence lives in the Rust binary, not the hook script.Acceptance Criteria
mise run lintrewritten by hook tortk mise run lintrtk discoverreports missed savings formise run/just/taskcommandsPriority and Effort
Phase 1 is a clear quick win — shippable standalone and unblocks all other phases.