We are live on DevHunt: tool of the week contest

Check it out

Taskless v0.13.4: Linters First and Fixtures as Folders

Jakob Heuser

0.13.4 is a patch release with six changesets. Most of it changes what your agent does when you ask for a rule in a repository that already runs a linter. It finds that linter more reliably, and it reaches for the rules the linter already ships before writing anything new. Rule fixtures also move to a layout that respects each rule's scope, and check gets quieter in CI.

npx @taskless/cli

Your linter's own rules come first

When you ask for a rule and the repository already runs a linter, the agent now follows a fixed order:

  1. Look for a rule the linter already ships. It checks each tool's catch-all rules by name, like ESLint's no-restricted-syntax, Ruff's banned-api, and Biome's noRestrictedImports.
  2. Write a custom rule in that linter, if the repository already has five or more of its own to model it on.
  3. Otherwise, write a Taskless rule.

The agent stops asking you to choose between a linter rule and a Taskless rule. It describes each linter detect found in the repository and makes the call from there. During onboarding, it only enables rules that your installed packages already ship.

Each linter also gets its own recipe. taskless agent create-legacy-rule --tool=<name> serves one for ruff, eslint (typescript-eslint included), biome, pylint, or flake8. Each recipe names the tool's catch-all rules, says how to confirm a rule exists in the version you have installed (ruff rule, biome explain, pylint --help-msg), and covers the traps that leave a rule loaded and silent, like Ruff preview rules and ESLint 10 removing .eslintrc. The ESLint recipe also lists eight well-known plugins with links to their docs.

detect --json reports customRules for ESLint, Biome, pylint, flake8, and Ruff: how many rules the repository wrote itself for that linter, and where they live. When counting them would mean running a JavaScript config, it reports "unknown".

detect finds linters that run on defaults

A repository that lints on a tool's defaults, with no config file of its own, used to read as having no linter at all. detect now looks where each ecosystem usually records one:

  • Dev dependencies in every manifest: pyproject.toml (PEP 735 [dependency-groups], which uv add --dev writes, plus [tool.uv], PDM, Hatch environments, and Poetry groups), pip requirements files like requirements-dev.txt and requirements/*.txt, Gemfile, composer.json require-dev, go.mod tool directives, and Package.swift.
  • Config inside a manifest: [lints.clippy] in Cargo.toml, a [flake8] section in setup.cfg or tox.ini, and the prettier and stylelint keys in package.json.
  • A root CI workflow or hook config that runs the linter, such as a workflow calling cargo clippy or a pre-commit hook from astral-sh/ruff-pre-commit. A Rust repository with none of these doesn't count as running clippy.

It also recognizes standard, pint, and php-cs-fixer, and knows the TypeScript config filenames for ESLint, Prettier, and stylelint.

Fixtures are case directories

Every rule's fixtures now live in case directories under tests/, for every engine:

tests/
pass/<case>/
fail/<case>/

taskless verify runs each case through the engine's own check, with the case directory as its root. ast-grep's files: and ignores: and a Vale rule's .vale.ini matchers apply to fixtures the same way they apply to your code. The old runner ignored scope, so a fixture could pass for a rule that would never run on that path. Those fixtures fail now, and the failure says the scope excluded them. ast-grep's valid:/invalid: test YAML and sg test are no longer used.

The next init your agent runs converts .tests/ into the new layout. Each ast-grep snippet becomes its own case, at a path the rule's scope admits, and each Vale document becomes a case named after it. Anything the migration can't convert stays where it is, and it tells you which files those are. Commit the result.

The scaffold version moves to 12, so an older CLI pinned in CI or a hook refuses a migrated project with SCAFFOLD_VERSION_MISMATCH until you upgrade it. A rule the service hands back in the old layout is converted as it's written.

check shows only errors in CI

When CI is true or 1, taskless check prints only error-severity findings, and ends with a count of the warnings, infos, and hints it left out. Pass --include-warnings to print them too. --json output and the exit code are unchanged.

Rule updates get flagged on every run

init now says when the project's rules need update --rules every time it runs. It used to check only when the CLI version moved, so going between two nightlies of the same release that bumped ast-grep or Vale reported nothing, and then check and verify refused with RULES_ENGINE_MISMATCH. The same answer, { required, engines, ruleMigrations }, is on init --json and update --json, and on info --json as rules.reconcile.

Nightlies and releases stay out of each other's way

A nightly install now sits beside the release. It no longer offers to move @taskless/cli pins in your scripts, CI, or hooks onto @taskless/cli-nightly, and it keeps the release's recorded version so .taskless/bin/cli.mjs still has something to run.

Going back to a release retires the nightly. Its launcher at .taskless/bin/cli-nightly.mjs, its lockfile, its recorded version, and its permission rules are removed, and any @taskless/cli-nightly pin left in scripts, CI, or hooks is named so you can change it.

A package.json dependency on the CLI is also flagged for removal now, in favor of the project launcher.

The full changelog is on taskless/cli#573.

Getting it

npx @taskless/cli