Skip to content

feat: task runner support (mise, just, task) — intelligent output filtering and delegation #607

@FlorianBruniaux

Description

@FlorianBruniaux

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 lintrtk 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:

  1. Runs the task runner command, captures stdout/stderr
  2. Resolves the underlying tool via config file introspection
  3. Routes output through the specialized filter_*() function (mirrors npx pattern)
  4. 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:

  1. User config [task_runners.mappings] in config.toml (explicit override, always wins)
  2. Config file introspection (mise.tomltasks.lint.run, just --dump --dump-format json)
  3. TOML filter for boilerplate stripping
  4. summary heuristic detection
  5. 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 lintrtk mise run lint. All intelligence lives in the Rust binary, not the hook script.


Acceptance Criteria

  • mise run lint rewritten by hook to rtk mise run lint
  • Output filtered with ≥60% token savings when underlying tool is recognized
  • Output filtered with ≥30% savings even when underlying tool is unknown (TOML boilerplate stripping)
  • rtk discover reports missed savings for mise run / just / task commands
  • Exit codes preserved (non-zero from task runner propagates correctly)
  • Fallback to raw output on any parsing or introspection failure
  • <10ms startup overhead (no config file parsing on startup)

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions