From beccbbcf11795dc533c160144543e8ab1214ed2a Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Sun, 20 Sep 2026 14:27:46 -0700 Subject: [PATCH] feat(detect): recognize oxlint alongside eslint, biome and stylelint --- .changeset/detect-oxlint.md | 5 ++ .../2026-09-20-detect-oxlint/proposal.md | 32 +++++++++ .../specs/cli-detect/spec.md | 66 +++++++++++++++++++ .../archive/2026-09-20-detect-oxlint/tasks.md | 15 +++++ openspec/specs/cli-detect/spec.md | 11 +++- packages/cli/src/detect/scan.ts | 14 ++++ packages/cli/test/detect.test.ts | 29 ++++++++ 7 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 .changeset/detect-oxlint.md create mode 100644 openspec/changes/archive/2026-09-20-detect-oxlint/proposal.md create mode 100644 openspec/changes/archive/2026-09-20-detect-oxlint/specs/cli-detect/spec.md create mode 100644 openspec/changes/archive/2026-09-20-detect-oxlint/tasks.md diff --git a/.changeset/detect-oxlint.md b/.changeset/detect-oxlint.md new file mode 100644 index 00000000..ae91bc52 --- /dev/null +++ b/.changeset/detect-oxlint.md @@ -0,0 +1,5 @@ +--- +"@taskless/cli": patch +--- + +`taskless detect` recognizes oxlint alongside eslint, biome and stylelint, from `.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`, `oxlint.config.mts`, or an `oxlint` entry in `package.json`. diff --git a/openspec/changes/archive/2026-09-20-detect-oxlint/proposal.md b/openspec/changes/archive/2026-09-20-detect-oxlint/proposal.md new file mode 100644 index 00000000..dd8a919e --- /dev/null +++ b/openspec/changes/archive/2026-09-20-detect-oxlint/proposal.md @@ -0,0 +1,32 @@ +## Why + +`taskless detect` recognizes eslint, biome and stylelint for JavaScript and TypeScript, but not oxlint. A repo that has moved to oxlint reports no JS linter, so `detect` output and everything downstream that reads it (`init`, the `route` recipe) treats the project as unlinted. oxlint is where a growing share of JS repos are heading for the static tier, and its plugin architecture cannot do cross-file rules, which makes an oxlint repo exactly the kind of project where Taskless runtime rules add something the linter cannot. Detect needs to see it. + +## What Changes + +- A new `oxlint` entry in `LINTER_SIGNALS` (`packages/cli/src/detect/scan.ts`), shaped like `biome`: languages JavaScript + TypeScript, config files `.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`, `oxlint.config.mts`, dependency `oxlint` matched against `package.json` only. +- The config list is the set oxlint auto-discovers per https://oxc.rs/docs/guide/usage/linter/config.html ("Oxlint automatically looks for a `.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`, or `oxlint.config.mts` in the current working directory"). `oxlint.config.{js,mjs,cjs}` are accepted only through an explicit `-c` flag and are deliberately excluded. +- The `cli-detect` spec's example list for "Linter configs are detected from disk" names `.oxlintrc.json`, and a scenario covers oxlint from either signal. +- Tests: `.oxlintrc.json` alone detects oxlint; `oxlint` only in `devDependencies` detects oxlint; an eslint-only repo does not report oxlint. + +Nothing here is **BREAKING**. Pre-1.0, an added linter signal is a `patch`. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `cli-detect`: the "Detect scans deterministic repo signals only" requirement's example config list gains `.oxlintrc.json`, and a new scenario states that oxlint is detected from its config file or its `package.json` dependency. + +## Impact + +- `packages/cli/src/detect/scan.ts`: one new `LinterSignal` entry. +- `packages/cli/test/detect.test.ts`: three new cases. +- `packages/cli/src/agent/detect.md`: untouched; it does not enumerate the JS linters. + +## Delivery shape + +**Single PR.** The change is one signal entry, its tests, the spec delta and the archive; it fits one reviewable diff and is safe in production on its own. diff --git a/openspec/changes/archive/2026-09-20-detect-oxlint/specs/cli-detect/spec.md b/openspec/changes/archive/2026-09-20-detect-oxlint/specs/cli-detect/spec.md new file mode 100644 index 00000000..6c698262 --- /dev/null +++ b/openspec/changes/archive/2026-09-20-detect-oxlint/specs/cli-detect/spec.md @@ -0,0 +1,66 @@ +## MODIFIED Requirements + +### Requirement: Detect scans deterministic repo signals only + +The `detect` command SHALL emit only deterministic signals derived from files on +disk: configured linters, detected languages, and the styles of the repo's own +existing rules. It SHALL NOT perform any LLM inference and SHALL NOT match the +request against any catalog of known packaged linter rules. + +Detection follows a languages → linters flow: languages are inferred first, and +a linter's dependency evidence is then read from the manifest of that linter's +own language (a node dependency from `package.json`, a Python dependency from +`pyproject.toml`/`requirements.txt`) rather than conflating ecosystems. A +recognized linter config file on disk is honored regardless of the languages +inferred. + +#### Scenario: Linter configs are detected from disk + +- **WHEN** the working directory contains a recognized linter config (for + example `.eslintrc*`, `eslint.config.js`, `ruff.toml`, a `[tool.ruff]` block in + `pyproject.toml`, `.rubocop.yml`, `biome.json`, `.oxlintrc.json`, or + `stylelint` config) +- **THEN** `detect --json` SHALL report each configured linter it found + +#### Scenario: Languages are reported + +- **WHEN** `detect --json` runs in a repository +- **THEN** the output SHALL include the languages inferred from manifest and + marker files present on disk and from the linters detected + +#### Scenario: A linter dependency is sourced from its own language's manifest + +- **WHEN** a dependency-evidenced linter (for example `ruff`) is named only in a + manifest belonging to a different language (for example `package.json`) +- **THEN** `detect --json` SHALL NOT report that linter from the mismatched + manifest + +#### Scenario: oxlint is detected from its config or its dependency + +- **WHEN** the working directory contains one of the config files oxlint + discovers on its own (`.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`, + `oxlint.config.mts`), or names `oxlint` as a dependency in `package.json` +- **THEN** `detect --json` SHALL report `oxlint` as a JavaScript/TypeScript linter +- **AND** a repository configured only for eslint SHALL NOT report `oxlint` + +#### Scenario: Configs in monorepo sub-packages are detected + +- **WHEN** a linter config or language manifest lives in a sub-package rather + than the repository root (for example `packages/api/.eslintrc.json`) +- **THEN** `detect --json` SHALL detect it and SHALL carry the path it was found + at in the linter's evidence +- **AND** the scan SHALL prune a curated set of ignored directories (for example + `node_modules`, `.git`, build output) and SHALL bound traversal depth + +#### Scenario: The repo's own rule styles are surfaced + +- **WHEN** the working directory contains existing rule definitions (for example + custom linter rules or `.taskless/rules/`) +- **THEN** `detect --json` SHALL surface a description of those existing rule + styles for downstream authoring + +#### Scenario: No packaged-rule catalog matching + +- **WHEN** `detect --json` runs +- **THEN** the output SHALL NOT claim a request maps to a specific named packaged + rule (such matching is left to the authoring recipe, not the command) diff --git a/openspec/changes/archive/2026-09-20-detect-oxlint/tasks.md b/openspec/changes/archive/2026-09-20-detect-oxlint/tasks.md new file mode 100644 index 00000000..2f7d4e88 --- /dev/null +++ b/openspec/changes/archive/2026-09-20-detect-oxlint/tasks.md @@ -0,0 +1,15 @@ +## 1. Implementation + +- [x] 1.1 Verify against https://oxc.rs/docs/guide/usage/linter/config.html which config filenames oxlint discovers on its own; exclude any `oxlint.config.*` variant that is only reachable through `-c`. +- [x] 1.2 Add the `oxlint` entry to `LINTER_SIGNALS` in `packages/cli/src/detect/scan.ts`, shaped like `biome`, with a comment naming the excluded filenames and why. +- [x] 1.3 Add tests in `packages/cli/test/detect.test.ts`: `.oxlintrc.json` alone detects oxlint; `oxlint` in `devDependencies` alone detects oxlint; an eslint-only repo does not report oxlint. +- [x] 1.4 Add `.changeset/detect-oxlint.md` (`patch`). + +## 2. Spec + +- [x] 2.1 Write the `cli-detect` delta restating the full "Detect scans deterministic repo signals only" requirement with every existing scenario, the updated example list, and the new oxlint scenario. +- [x] 2.2 `pnpm openspec validate --strict`, then the dry-run archive check from CLAUDE.md (scenario count before vs after), then archive for real. + +## 3. Verification + +- [x] 3.1 `pnpm build`, `pnpm typecheck`, `pnpm lint`, `pnpm --filter @taskless/cli test` all pass. diff --git a/openspec/specs/cli-detect/spec.md b/openspec/specs/cli-detect/spec.md index 973be667..48a07c44 100644 --- a/openspec/specs/cli-detect/spec.md +++ b/openspec/specs/cli-detect/spec.md @@ -36,7 +36,8 @@ inferred. - **WHEN** the working directory contains a recognized linter config (for example `.eslintrc*`, `eslint.config.js`, `ruff.toml`, a `[tool.ruff]` block in - `pyproject.toml`, `.rubocop.yml`, `biome.json`, or `stylelint` config) + `pyproject.toml`, `.rubocop.yml`, `biome.json`, `.oxlintrc.json`, or + `stylelint` config) - **THEN** `detect --json` SHALL report each configured linter it found #### Scenario: Languages are reported @@ -52,6 +53,14 @@ inferred. - **THEN** `detect --json` SHALL NOT report that linter from the mismatched manifest +#### Scenario: oxlint is detected from its config or its dependency + +- **WHEN** the working directory contains one of the config files oxlint + discovers on its own (`.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`, + `oxlint.config.mts`), or names `oxlint` as a dependency in `package.json` +- **THEN** `detect --json` SHALL report `oxlint` as a JavaScript/TypeScript linter +- **AND** a repository configured only for eslint SHALL NOT report `oxlint` + #### Scenario: Configs in monorepo sub-packages are detected - **WHEN** a linter config or language manifest lives in a sub-package rather diff --git a/packages/cli/src/detect/scan.ts b/packages/cli/src/detect/scan.ts index 1de74565..5c0d1a26 100644 --- a/packages/cli/src/detect/scan.ts +++ b/packages/cli/src/detect/scan.ts @@ -120,6 +120,20 @@ const LINTER_SIGNALS: readonly LinterSignal[] = [ configFiles: ["biome.json", "biome.jsonc"], deps: ["@biomejs/biome"], }, + { + name: "oxlint", + languages: ["JavaScript", "TypeScript"], + // The four names oxlint discovers on its own (oxc.rs, "Configuration"). + // `oxlint.config.{js,mjs,cjs}` are accepted only via `-c`, never found by + // the walk, so listing them would be dead weight in the sweep. + configFiles: [ + ".oxlintrc.json", + ".oxlintrc.jsonc", + "oxlint.config.ts", + "oxlint.config.mts", + ], + deps: ["oxlint"], + }, { name: "stylelint", languages: ["JavaScript", "TypeScript"], diff --git a/packages/cli/test/detect.test.ts b/packages/cli/test/detect.test.ts index 7942e103..0ec72755 100644 --- a/packages/cli/test/detect.test.ts +++ b/packages/cli/test/detect.test.ts @@ -127,6 +127,35 @@ describe("taskless detect", () => { expect(linterNames(result)).toContain("biome"); }); + it("detects oxlint from .oxlintrc.json", async () => { + await writeFile(join(cwd, ".oxlintrc.json"), "{}", "utf8"); + const result = await detect(cwd); + expect(linterNames(result)).toContain("oxlint"); + expect(linterNames(result)).not.toContain("eslint"); + }); + + it("detects oxlint from a package.json devDependency", async () => { + await writeFile( + join(cwd, "package.json"), + JSON.stringify({ devDependencies: { oxlint: "^1.0.0" } }), + "utf8" + ); + const result = await detect(cwd); + expect(linterNames(result)).toContain("oxlint"); + }); + + it("does not report oxlint for an eslint-only repo", async () => { + await writeFile(join(cwd, ".eslintrc.json"), "{}", "utf8"); + await writeFile( + join(cwd, "package.json"), + JSON.stringify({ devDependencies: { eslint: "^9.0.0" } }), + "utf8" + ); + const result = await detect(cwd); + expect(linterNames(result)).toContain("eslint"); + expect(linterNames(result)).not.toContain("oxlint"); + }); + it("detects stylelint from a package.json devDependency", async () => { await writeFile( join(cwd, "package.json"),