Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/route-names-every-linter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskless/cli": patch
---

`agent route` no longer reads as though a detected linter is missing. It now names every detected tool that can host a rule, and Oxlint gets its own `create-legacy-rule --tool=oxlint` guide.
2 changes: 1 addition & 1 deletion .github/workflows/legacy-linter-contract.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Pins what the `create-legacy-rule` recipes say about Ruff, pylint, flake8,
# and Biome, by asking each linter at a pinned version. See
# Biome, and Oxlint, by asking each linter at a pinned version. See
# `packages/cli/test/legacy-linter-vendor-contract.test.ts`.
#
# Separate from `Validate` on purpose: it downloads linters from PyPI and npm,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Why

`route` prints "The linters `detect` found here" and then lists only linters with a guide paragraph, so a repository running Oxlint and ESLint reads as ESLint-only. In a repository running only Oxlint, `route` says steps 1 and 2 of the legacy ladder do not apply, which contradicts the requirement that step 1 is taken whenever a detected linter could host the rule. Found on NoMercyLabs/nomnomzbot, where `detect` reports Oxlint correctly and `route` omits it.

## What Changes

- Add an Oxlint recipe, `create-legacy-rule --tool=oxlint`, and a paragraph for it in `route`.
- `route` names every detected tool that can host a rule. One with no paragraph is listed with the fetch command for the generic recipe, and the ladder still applies to it.
- Formatters and Taskless's own engines stay out of that list.
- A scanned `route` says `detect` recognizes a fixed list of tools, so the repository may run others.

## Capabilities

### New Capabilities

None.

### Modified Capabilities

- `cli-rule-routing`: `route` no longer implies a detected linter is absent.
- `cli-legacy-rule-recipes`: Oxlint joins the per-tool recipes.

## Impact

- `packages/cli/src/prompts/recipes.ts`, `src/agent/route.md` (topic v9), and the new `src/agent/create-legacy-rule.oxlint.md`.
- The custom-rule count from a repository root, and C# language detection, are separate work.
- Delivery shape: **single PR**. The recipe and the routing that points at it fit one review.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
## MODIFIED Requirements

### Requirement: Legacy recipes use what a tool ships before anything is written

Every `create-legacy-rule` recipe, generic or per-tool, SHALL direct the agent to first establish whether a rule the tool already ships solves the request, including the tool's configurable catch-all rules. A recipe SHALL name the catch-all rules for its tool explicitly, because a search for the request's subject does not surface a catch-all rule.

A per-tool recipe SHALL state the check its tool always performs first:

- Ruff: whether `banned-api` (TID251) covers the request.
- ESLint: whether a core rule covers it, with `no-restricted-syntax` and `no-restricted-imports` (and their `@typescript-eslint/` counterparts) named as the catch-alls, before any plugin.
- Biome: whether an existing rule makes the problem go away, with `noRestrictedImports` and `noRestrictedGlobals` named as the catch-alls.
- Oxlint: whether an existing rule makes the problem go away, with `no-restricted-imports` and `no-restricted-globals` named as the catch-alls. The recipe SHALL state that Oxlint has no `no-restricted-syntax`.
- pylint: whether a shipped message or the `deprecated-modules`, `preferred-modules`, or `bad-functions` options cover it.
- flake8: whether `banned-modules` from `flake8-tidy-imports` covers it. The flake8 recipe SHALL NOT include a discovery step.

#### Scenario: Ruff tries banned-api first

- **WHEN** the agent follows the Ruff recipe for a request to forbid an API
- **THEN** the recipe SHALL direct it to check `banned-api` before any other step

#### Scenario: ESLint tries a core rule before a plugin

- **WHEN** the agent follows the ESLint recipe
- **THEN** it SHALL check core rules and the named catch-alls before considering any plugin from the curated list

#### Scenario: flake8 goes straight to its catch-all

- **WHEN** the agent follows the flake8 recipe
- **THEN** it SHALL be directed to `banned-modules` without a rule-discovery step

### Requirement: Rule discovery is confirmed against the installed version

Where a tool's rule set is too large to match a request against locally, the recipe SHALL direct the agent to find candidate rules on the tool's rules page by web search or fetch, then confirm each candidate against the installed version with the tool's own command before enabling it. The Ruff recipe SHALL use `ruff rule <name or code>`, the Biome recipe `biome explain <rule>`, and the Oxlint recipe `oxlint --rules --format default`.

The Ruff recipe SHALL state that a preview-only rule does not fire unless preview mode is enabled, and SHALL direct the agent to check the rule's preview status before enabling it. The pylint recipe SHALL state that `--help-msg` reports a message from an optional extension as nonexistent unless that extension is passed with `--load-plugins`.

#### Scenario: A Ruff rule is confirmed locally

- **WHEN** the agent finds a candidate Ruff rule on the web
- **THEN** it SHALL confirm it with `ruff rule` against the installed version before enabling it

#### Scenario: A preview-only Ruff rule is not enabled silently

- **WHEN** the confirmed Ruff rule is preview-only
- **THEN** the agent SHALL either enable preview mode for it with the user's knowledge or choose another rule, and SHALL NOT enable it in a configuration where it cannot fire

#### Scenario: A pylint extension message is looked up with its extension loaded

- **WHEN** the agent looks up a message that belongs to a pylint optional extension
- **THEN** it SHALL pass that extension with `--load-plugins` before concluding the message does not exist

### Requirement: A new legacy rule is written only from the repository's own examples

A legacy recipe SHALL permit the agent to write a new custom rule in a linter only when the repository already has at least five custom rules for that linter, as counted by `detect --json`. The agent SHALL use those rules as its reference for structure and conventions.

The recipes SHALL NOT teach custom-rule authoring for any tool. They SHALL NOT carry instructions for writing an ESLint rule, a pylint checker, a flake8 plugin, a Biome plugin, or an Oxlint plugin of any kind. Below the threshold, a request no shipped rule solves SHALL return to `route`, which sends it to a Taskless rule.

#### Scenario: Five examples permit a new rule

- **WHEN** no shipped rule solves the request AND `detect` counts five or more custom rules for the linter
- **THEN** the agent MAY write a new custom rule modeled on those rules

#### Scenario: Fewer examples send the request back to Taskless

- **WHEN** no shipped rule solves the request AND `detect` counts fewer than five custom rules for the linter
- **THEN** the agent SHALL NOT write a custom rule in that linter
- **AND** it SHALL continue down the `route` ladder to a Taskless rule
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## MODIFIED Requirements

### Requirement: Route describes each detected linter

The `route` recipe SHALL include, for each linter `detect` reports that can host a legacy rule, one paragraph saying what the tool is for, what the agent always checks first in it, and the recipe command to fetch for it. The paragraphs SHALL be rendered when the recipe is served, and SHALL appear only for linters detected in the repository.

A detected tool that can host a legacy rule and has no paragraph SHALL be named in the recipe with the command to fetch the generic recipe for it, and the recipe SHALL state that steps 1 and 2 of the legacy ladder still apply to it. `route` SHALL NOT state or imply that a linter `detect` reported is absent. Formatters and Taskless's own engines are not legacy hosts and SHALL NOT be named this way.

When the recipe is rendered from a scan, it SHALL state that `detect` recognizes a fixed list of tools, so the repository may run others.

#### Scenario: Only detected linters are described

- **WHEN** `detect` reports Ruff and no other linter
- **THEN** the served `route` recipe SHALL contain the Ruff paragraph
- **AND** SHALL NOT contain a paragraph for any other linter

#### Scenario: A tool with no legacy recipe gets no paragraph

- **WHEN** `detect` reports only `prettier`
- **THEN** the served `route` recipe SHALL contain no linter paragraph

#### Scenario: Oxlint is described when detected

- **WHEN** `detect` reports `oxlint` and no other linter
- **THEN** the served `route` recipe SHALL contain the Oxlint paragraph
- **AND** SHALL NOT say that no linter was found

#### Scenario: A detected linter with no paragraph is still named

- **WHEN** `detect` reports `eslint` and `stylelint`
- **THEN** the served `route` recipe SHALL contain the ESLint paragraph
- **AND** SHALL name `stylelint` with the command to fetch `create-legacy-rule` for it

#### Scenario: A formatter is not offered as a legacy destination

- **WHEN** `detect` reports `eslint` and `prettier`
- **THEN** the served `route` recipe SHALL NOT name `prettier` as a linter to fetch a recipe for
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## 1. Recipe

- [x] 1.1 Add `create-legacy-rule.oxlint.md`, with facts taken from the Oxlint docs
- [x] 1.2 Pin its catch-all claims in the legacy linter vendor contract

## 2. Routing

- [x] 2.1 Add the Oxlint paragraph to `route`
- [x] 2.2 Name detected tools that have no paragraph, excluding formatters and Taskless's engines
- [x] 2.3 State that `detect` recognizes a fixed list of tools
- [x] 2.4 Bump `route` to topic v9
5 changes: 3 additions & 2 deletions openspec/specs/cli-legacy-rule-recipes/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ A per-tool recipe SHALL state the check its tool always performs first:
- Ruff: whether `banned-api` (TID251) covers the request.
- ESLint: whether a core rule covers it, with `no-restricted-syntax` and `no-restricted-imports` (and their `@typescript-eslint/` counterparts) named as the catch-alls, before any plugin.
- Biome: whether an existing rule makes the problem go away, with `noRestrictedImports` and `noRestrictedGlobals` named as the catch-alls.
- Oxlint: whether an existing rule makes the problem go away, with `no-restricted-imports` and `no-restricted-globals` named as the catch-alls. The recipe SHALL state that Oxlint has no `no-restricted-syntax`.
- pylint: whether a shipped message or the `deprecated-modules`, `preferred-modules`, or `bad-functions` options cover it.
- flake8: whether `banned-modules` from `flake8-tidy-imports` covers it. The flake8 recipe SHALL NOT include a discovery step.

Expand All @@ -35,7 +36,7 @@ A per-tool recipe SHALL state the check its tool always performs first:

### Requirement: Rule discovery is confirmed against the installed version

Where a tool's rule set is too large to match a request against locally, the recipe SHALL direct the agent to find candidate rules on the tool's rules page by web search or fetch, then confirm each candidate against the installed version with the tool's own command before enabling it. The Ruff recipe SHALL use `ruff rule <name or code>` and the Biome recipe `biome explain <rule>`.
Where a tool's rule set is too large to match a request against locally, the recipe SHALL direct the agent to find candidate rules on the tool's rules page by web search or fetch, then confirm each candidate against the installed version with the tool's own command before enabling it. The Ruff recipe SHALL use `ruff rule <name or code>`, the Biome recipe `biome explain <rule>`, and the Oxlint recipe `oxlint --rules --format default`.

The Ruff recipe SHALL state that a preview-only rule does not fire unless preview mode is enabled, and SHALL direct the agent to check the rule's preview status before enabling it. The pylint recipe SHALL state that `--help-msg` reports a message from an optional extension as nonexistent unless that extension is passed with `--load-plugins`.

Expand Down Expand Up @@ -76,7 +77,7 @@ The list names rule sources, not individual rules. The recipe SHALL NOT embed a

A legacy recipe SHALL permit the agent to write a new custom rule in a linter only when the repository already has at least five custom rules for that linter, as counted by `detect --json`. The agent SHALL use those rules as its reference for structure and conventions.

The recipes SHALL NOT teach custom-rule authoring for any tool. They SHALL NOT carry instructions for writing an ESLint rule, a pylint checker, a flake8 plugin, or a Biome plugin of any kind. Below the threshold, a request no shipped rule solves SHALL return to `route`, which sends it to a Taskless rule.
The recipes SHALL NOT teach custom-rule authoring for any tool. They SHALL NOT carry instructions for writing an ESLint rule, a pylint checker, a flake8 plugin, a Biome plugin, or an Oxlint plugin of any kind. Below the threshold, a request no shipped rule solves SHALL return to `route`, which sends it to a Taskless rule.

#### Scenario: Five examples permit a new rule

Expand Down
21 changes: 21 additions & 0 deletions openspec/specs/cli-rule-routing/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,10 @@ Step 1 SHALL be taken whenever a detected linter could host the rule, regardless

The `route` recipe SHALL include, for each linter `detect` reports that can host a legacy rule, one paragraph saying what the tool is for, what the agent always checks first in it, and the recipe command to fetch for it. The paragraphs SHALL be rendered when the recipe is served, and SHALL appear only for linters detected in the repository.

A detected tool that can host a legacy rule and has no paragraph SHALL be named in the recipe with the command to fetch the generic recipe for it, and the recipe SHALL state that steps 1 and 2 of the legacy ladder still apply to it. `route` SHALL NOT state or imply that a linter `detect` reported is absent. Formatters and Taskless's own engines are not legacy hosts and SHALL NOT be named this way.

When the recipe is rendered from a scan, it SHALL state that `detect` recognizes a fixed list of tools, so the repository may run others.

#### Scenario: Only detected linters are described

- **WHEN** `detect` reports Ruff and no other linter
Expand All @@ -352,6 +356,23 @@ The `route` recipe SHALL include, for each linter `detect` reports that can host
- **WHEN** `detect` reports only `prettier`
- **THEN** the served `route` recipe SHALL contain no linter paragraph

#### Scenario: Oxlint is described when detected

- **WHEN** `detect` reports `oxlint` and no other linter
- **THEN** the served `route` recipe SHALL contain the Oxlint paragraph
- **AND** SHALL NOT say that no linter was found

#### Scenario: A detected linter with no paragraph is still named

- **WHEN** `detect` reports `eslint` and `stylelint`
- **THEN** the served `route` recipe SHALL contain the ESLint paragraph
- **AND** SHALL name `stylelint` with the command to fetch `create-legacy-rule` for it

#### Scenario: A formatter is not offered as a legacy destination

- **WHEN** `detect` reports `eslint` and `prettier`
- **THEN** the served `route` recipe SHALL NOT name `prettier` as a linter to fetch a recipe for

### Requirement: Route asks the user only when the ladder does not decide

The `route` recipe SHALL present the viable options to the user with their
Expand Down
80 changes: 80 additions & 0 deletions packages/cli/src/agent/create-legacy-rule.oxlint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Topic: create-legacy-rule (oxlint) (CLI v%(CLI_VERSION)s / topic v1)

%(ONE_COMMAND_PER_CALL)s

## You are here
This is `create-legacy-rule` for Oxlint. It solves a request by enabling
or configuring a rule Oxlint already ships. When nothing here fits,
re-run `%(TASKLESS_CLI)s agent route` and continue from step 3 of its legacy
ladder.

## Goal
Find the Oxlint rule that makes the problem go away, confirm it in the
installed version, and enable it in the repository's Oxlint configuration.

## Preconditions
- `%(TASKLESS_CLI)s detect --json` reports `oxlint`.
- No auth required.

## Steps

1. **Always check the catch-all rules first.** They take your own list,
and a search for the request's subject will not surface them:
- `no-restricted-imports`: bans modules or named imports, with a
message, through `paths` and `patterns`.
- `no-restricted-globals`: bans global names, such as `event`. Its
documented form is a plain list of names, with no custom message.
Both do nothing until they are given a list, so a repo that has not
configured them is not using them.

Oxlint has no `no-restricted-syntax`. A request for an AST selector
is not solved here, so go back to `route`.

2. **Otherwise, find a shipped rule.** Search the rules page,
https://oxc.rs/docs/guide/usage/linter/rules.html, for the behavior
the request describes. Many rules port a well-known ESLint rule under
the same name. The docs site also has an index at
https://oxc.rs/llms.txt.

3. **Confirm the rule against the installed version.** Run
`oxlint --rules --format default`, which lists the rules the installed
build has. Keep `--format default`: without it, Oxlint prints nothing
when it detects an agent or GitHub Actions, and still exits 0, so an
empty list says nothing about the rule. A rule belonging to a plugin
only fires when that plugin is on, and setting `plugins` replaces the
default set, so read the config's `plugins` before relying on one.

4. **Enable it where the repo configures Oxlint.** Oxlint reads
`.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`, or
`oxlint.config.mts`. Each file uses the nearest config, and a nested
config replaces its parent rather than merging with it, unless it
says `extends`. So change the config nearest the files in question,
not necessarily the one at the repository root. Rules go under
`rules`, as a severity or a `[severity, options]` pair:
```json
"rules": {
"no-restricted-imports": ["error", {
"paths": [{ "name": "lodash", "message": "Use the helpers in src/util." }]
}]
}
```
Use `overrides` when the rule should apply to some paths only.

5. **Report, and say who runs it.** Show the file you changed and the
command that shows the rule firing, e.g. `oxlint <path>`.
`%(TASKLESS_CLI)s check` does NOT run Oxlint.

## Important Notes

- Do not write an Oxlint plugin. JavaScript plugins exist, but they are
in alpha and Taskless does not teach authoring them. A request no
shipped rule solves is a Taskless rule.
- A repo running ESLint beside Oxlint may load `eslint-plugin-oxlint`,
whose `buildFromOxlintConfigFile` turns off in ESLint the rules the
Oxlint config enables. When a rule exists in both, enable it in
Oxlint. Enabling it in ESLint there has no effect.

## See Also

- `%(TASKLESS_CLI)s agent route`: continue down the legacy ladder when no Oxlint rule fits
- `%(TASKLESS_CLI)s agent create-sg-rule`: ast-grep parses JavaScript and TypeScript
2 changes: 1 addition & 1 deletion packages/cli/src/agent/route.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Topic: route (CLI v%(CLI_VERSION)s / topic v8)
# Topic: route (CLI v%(CLI_VERSION)s / topic v9)

%(ONE_COMMAND_PER_CALL)s

Expand Down
Loading
Loading