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/detect-oxlint.md
Original file line number Diff line number Diff line change
@@ -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`.
32 changes: 32 additions & 0 deletions openspec/changes/archive/2026-09-20-detect-oxlint/proposal.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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)
15 changes: 15 additions & 0 deletions openspec/changes/archive/2026-09-20-detect-oxlint/tasks.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 10 additions & 1 deletion openspec/specs/cli-detect/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/src/detect/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
29 changes: 29 additions & 0 deletions packages/cli/test/detect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Loading