diff --git a/docs/CATALOG.md b/docs/CATALOG.md index d220d94556..a9fba57313 100644 --- a/docs/CATALOG.md +++ b/docs/CATALOG.md @@ -38,7 +38,7 @@ plugin manifests and kept in sync by CI — never hand-edit it; the category voc - [`actionlint`](../plugins/actionlint) — Lint GitHub Actions workflow files on edit via actionlint, surfacing findings as advisory context. - [`source-control`](../plugins/source-control) — Git and GitHub delivery workflow: /commit (Conventional Commits + Co-authored-by trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop — safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and standing merge-rung raises binding from the team-tracked layer only — with one named exception, where an invocation line explicitly typing both the autopilot tier keyword and the dedicated raise argument --merge c3-this-run widens that single invocation's merge authority up to C3 behind a fresh independent frontier-tier resolver, while C4-structural and C5-untrusted-provenance stay unconditionally human-merge), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply — interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep — never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared. - [`implementation`](../plugins/implementation) — Disciplined implementation stage: execute approved plans inline (`/implementation:implement`) or via orchestrated worker subagents (`/implementation:implement-dispatch`) with incremental validation, TDD-by-default cadence, green-checkpoint commits, scope-fence drift detection, and divergence detection that routes back to planning. Build/test/lint, testing, and outcome verification live in the companion `toolchain`, `testing`, and `verification` plugins, invoked when installed. -- [`toolchain`](../plugins/toolchain) — Repo-agnostic polyglot verification toolchain: build + test + lint for changed files across .NET, Python, TypeScript, Bash, PowerShell, Markdown, Go, YAML, and cross-cutting surfaces (`/toolchain:check`, `/toolchain:lint`), plus a re-runnable `/toolchain:setup` with check (report the configured ecosystems and their command surface) and apply (interview, infer, and write the tracked per-ecosystem command config those skills resolve first). +- [`toolchain`](../plugins/toolchain) — Repo-agnostic polyglot verification toolchain: build + test + lint for changed files across .NET, Python, TypeScript, Bash, PowerShell, Markdown, Go, YAML, and cross-cutting surfaces (`/toolchain:check`, `/toolchain:lint` with format-only `--fix` and gated `--code-fix`), plus a re-runnable `/toolchain:setup` with check (report the configured ecosystems and their command surface) and apply (interview, infer, and write the tracked per-ecosystem command config those skills resolve first). - [`wizard`](../plugins/wizard) — Generate an interactive bash wizard that walks a human, step by step, through the manual procedures an agent cannot perform — provisioning infrastructure or credentials, setting CI secrets, clicking through third-party dashboards, one-off migrations and cutovers. One skill, generate (/wizard:generate): the agent scopes the stages from the repo (reading key NAMES only from a live .env, never values), authors them onto a fixed hardened library (TTY-only fail-closed prompts, https-only URL opening, hidden secret entry, single-quoted 0600 .env upserts with a gitignore check, repo-confirmed gh secret/variable writes over stdin, names-only summary), prints the full STAGES block for explicit human approval BEFORE the script is made executable, and never runs the wizard itself — the human does, in their own terminal. Ephemeral by default: built for one run, committed only when the setup path should live in the repo. The generated script requires bash (Windows: Git Bash or WSL); gh is optional — CI-secret stages degrade to a visible warning plus a closing-summary entry when it is absent. ## Testing diff --git a/docs/conventions/ecosystem-commands/CHANGELOG.md b/docs/conventions/ecosystem-commands/CHANGELOG.md index 26797995c5..4f9e4a7e1c 100644 --- a/docs/conventions/ecosystem-commands/CHANGELOG.md +++ b/docs/conventions/ecosystem-commands/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog — ecosystem-commands convention +## 1.3.0 — 2026-08-15 + +Additive schema change: new optional `code-fix-cmd` key for semantic/code-changing autofixes +(ruff `check --fix`, golangci-lint `--fix`, biome `check --write`, …). Clarifies that `fix-cmd` +is **format-only** (whitespace / import layout / style). `/toolchain:lint --fix` runs `fix-cmd`; +`--code-fix` runs `code-fix-cmd` behind that skill's confirmation / `--yes` gate. Null when +absent — tolerant readers ignore the unknown key until they upgrade. Closes +melodic-software/claude-code-plugins#2649. + ## 1.2.3 — 2026-07-26 Docs-only, no schema shape change: the task-runner deferral's "Revisit triggers" label becomes diff --git a/docs/conventions/ecosystem-commands/README.md b/docs/conventions/ecosystem-commands/README.md index 2634d087b2..a54cb1435a 100644 --- a/docs/conventions/ecosystem-commands/README.md +++ b/docs/conventions/ecosystem-commands/README.md @@ -110,6 +110,10 @@ documented placeholders: | `` | Each per-project root discovered via `project-discovery` | | `$REPO_ROOT` | Absolute repo root (`git rev-parse --show-toplevel`) | +`fix-cmd` is **format-only** (whitespace / import layout / style). Semantic/code-changing +autofixes belong in optional `code-fix-cmd` and are invoked only by `/toolchain:lint --code-fix` +behind that skill's confirmation / `--yes` gate — never by bare `--fix`. + Consumers are tolerant readers: unknown keys are inert, missing optional keys fall back to defaults. Consuming repos SHOULD validate their files against the schema in their own gates (a `check-jsonschema` hook or CI lane); plugins SHOULD fail soft — a malformed file degrades to rung 2 diff --git a/docs/conventions/ecosystem-commands/ecosystem.schema.json b/docs/conventions/ecosystem-commands/ecosystem.schema.json index d5e851e7a2..c9f0273867 100644 --- a/docs/conventions/ecosystem-commands/ecosystem.schema.json +++ b/docs/conventions/ecosystem-commands/ecosystem.schema.json @@ -41,7 +41,11 @@ }, "fix-cmd": { "type": ["string", "null"], - "description": "Auto-fix command. Null when the toolchain has no fix mode." + "description": "Format-only auto-fix command (whitespace, import layout, style). Must not apply semantic/code-changing lint autofixes. Null when the toolchain has no format-fix mode. Code-changing autofixes belong in code-fix-cmd." + }, + "code-fix-cmd": { + "type": ["string", "null"], + "description": "Code-changing auto-fix command (lint autofixes that rewrite logic, delete unused imports, apply safe/unsafe code edits). Null when absent. Invoked only by /toolchain:lint --code-fix behind that skill's confirmation / --yes gate — never by bare --fix." }, "opt-in": { "type": "string", diff --git a/docs/conventions/ecosystem-commands/examples/go.yaml b/docs/conventions/ecosystem-commands/examples/go.yaml index 580b5c1aa2..6148db8447 100644 --- a/docs/conventions/ecosystem-commands/examples/go.yaml +++ b/docs/conventions/ecosystem-commands/examples/go.yaml @@ -5,7 +5,8 @@ project-discovery: ["go.mod"] build-cmd: "go build ./..." test-cmd: "go test ./..." check-cmd: "golangci-lint run ./..." -fix-cmd: "golangci-lint run --fix ./..." +fix-cmd: "gofmt -w " # format-only; substitute only *.go from (never go.mod/go.sum) +code-fix-cmd: "golangci-lint run --fix " # *.go only; one package directory per invoke opt-in: ".golangci.yml, .golangci.yaml, .golangci.toml, or .golangci.json present (walked from the changed file up to the repo root) — otherwise golangci-lint applies its own unconfigured \"standard\" linter preset unconditionally" install-hint: "Install golangci-lint: https://golangci-lint.run/docs/welcome/install/ | Go toolchain: https://go.dev/dl/ | Buf CLI (needed by the proto-gen-freshness gate below): https://buf.build/docs/cli/installation/" gates: diff --git a/docs/conventions/ecosystem-commands/examples/python.yaml b/docs/conventions/ecosystem-commands/examples/python.yaml index 2877acd137..201f99482a 100644 --- a/docs/conventions/ecosystem-commands/examples/python.yaml +++ b/docs/conventions/ecosystem-commands/examples/python.yaml @@ -5,6 +5,7 @@ project-discovery: ["pyproject.toml"] build-cmd: null # no separate build step; lint/test cover verification test-cmd: "uv run pytest -x -q" check-cmd: "uv run ruff check . --no-fix && uv run ruff format . --check" -fix-cmd: "uv run ruff check . --fix && uv run ruff format ." +fix-cmd: "uv run ruff format " # format-only; code-changing autofixes live in code-fix-cmd +code-fix-cmd: "uv run ruff check --fix --no-unsafe-fixes --unfixable F401" opt-in: "ruff config (ruff.toml, .ruff.toml, or pyproject.toml [tool.ruff])" install-hint: "pip install ruff uv | brew install uv ruff | winget install astral-sh.uv" diff --git a/plugins/toolchain/.claude-plugin/plugin.json b/plugins/toolchain/.claude-plugin/plugin.json index 94540dd26b..6d4be97eb6 100644 --- a/plugins/toolchain/.claude-plugin/plugin.json +++ b/plugins/toolchain/.claude-plugin/plugin.json @@ -1,8 +1,8 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "toolchain", - "version": "0.12.1", - "description": "Repo-agnostic polyglot verification toolchain: build + test + lint for changed files across .NET, Python, TypeScript, Bash, PowerShell, Markdown, Go, YAML, and cross-cutting surfaces (`/toolchain:check`, `/toolchain:lint`), plus a re-runnable `/toolchain:setup` with check (report the configured ecosystems and their command surface) and apply (interview, infer, and write the tracked per-ecosystem command config those skills resolve first).", + "version": "0.13.0", + "description": "Repo-agnostic polyglot verification toolchain: build + test + lint for changed files across .NET, Python, TypeScript, Bash, PowerShell, Markdown, Go, YAML, and cross-cutting surfaces (`/toolchain:check`, `/toolchain:lint` with format-only `--fix` and gated `--code-fix`), plus a re-runnable `/toolchain:setup` with check (report the configured ecosystems and their command surface) and apply (interview, infer, and write the tracked per-ecosystem command config those skills resolve first).", "author": { "name": "Melodic Software", "email": "info@melodicsoftware.com" diff --git a/plugins/toolchain/CHANGELOG.md b/plugins/toolchain/CHANGELOG.md index b165c021b5..87ce7cb375 100644 --- a/plugins/toolchain/CHANGELOG.md +++ b/plugins/toolchain/CHANGELOG.md @@ -3,6 +3,31 @@ All notable changes to the `toolchain` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.13.0] + +### Added + +- **`/toolchain:lint --code-fix` — gated semantic lint autofixes.** New optional ecosystem key + `code-fix-cmd` (ecosystem-commands contract 1.3.0) holds code-changing autofixes. The skill emits + a plan, then requires interactive confirmation or `--yes` (non-interactive without `--yes` stops + after the plan). `--dry-run` always stops after the plan. A default **file-cap of 40** scoped files + stops over-broad applies unless `--all-files` is passed. After an apply, surface `git diff --stat`. + +### Changed + +- **`--fix` is format-only.** Bundled portable defaults split format from code-fix: + - python: `fix-cmd` → `ruff format `; `code-fix-cmd` → `ruff check --fix + --no-unsafe-fixes --unfixable F401` (matches the `ruff-format` hook's F401 guard) + - go: `fix-cmd` → `gofmt -w `; `code-fix-cmd` → `golangci-lint run --fix ` + Go format/code-fix substitute **only `*.go` paths** (never `go.mod`/`go.sum`) and invoke + `golangci-lint run --fix` once per package directory so multi-package changes do not hit + `named files must all be in one directory`. + - typescript: `fix-cmd` → `biome format --write `; `code-fix-cmd` → + `biome check --write ` + Ecosystems that were already format-only (dotnet, bash, markdown) are unchanged. Consumer + overrides that still put code-changing verbs in `fix-cmd` keep that behavior under `--fix`; + prefer migrating them to `code-fix-cmd`. Closes #2649. + ## [0.12.1] ### Changed diff --git a/plugins/toolchain/README.md b/plugins/toolchain/README.md index 60de4ee6ce..cfe3d33353 100644 --- a/plugins/toolchain/README.md +++ b/plugins/toolchain/README.md @@ -8,7 +8,7 @@ defaults. Three skills, one concern: mechanical verification of changed code. | Skill | What it does | |---|---| | `/toolchain:check` | Build + test + lint for changed files, auto-detecting affected ecosystems (.NET, Python, TypeScript, Bash, PowerShell, Markdown) from git status; resolves each ecosystem's commands through the shared four-rung ladder. Also the reference skill other plugins compose for ecosystem detection and command resolution. | -| `/toolchain:lint` | Lint + format checks only — faster than a build cycle, honors each tool's config-file opt-in, `--fix` mode where linters support it; also owns the `yaml` and `cross-cutting` lint surfaces. | +| `/toolchain:lint` | Lint + format checks only — faster than a build cycle, honors each tool's config-file opt-in; `--fix` is format-only, `--code-fix` runs semantic lint autofixes behind a confirmation / `--yes` gate; also owns the `yaml` and `cross-cutting` lint surfaces. | | `/toolchain:setup` | Configure the plugin for a repo. `check` (read-only, default) reports which ecosystems are configured and each one's resolved command surface, validating the tracked files against the contract schema; `apply` interviews + infers + writes the tracked `.claude/ecosystems/.yaml` files that `/toolchain:check` and `/toolchain:lint` resolve first. Re-runnable. | ## Works in any repo diff --git a/plugins/toolchain/reference/ecosystems/go.yaml b/plugins/toolchain/reference/ecosystems/go.yaml index 88e350bd3d..51c9459dd4 100644 --- a/plugins/toolchain/reference/ecosystems/go.yaml +++ b/plugins/toolchain/reference/ecosystems/go.yaml @@ -34,7 +34,8 @@ project-discovery: ["go.mod"] build-cmd: "go build ./..." test-cmd: "go test ./..." check-cmd: "golangci-lint run ./..." -fix-cmd: "golangci-lint run --fix ./..." +fix-cmd: "gofmt -w " # format-only; agent substitutes only *.go from (never go.mod/go.sum) +code-fix-cmd: "golangci-lint run --fix " # *.go only; invoke once per package directory (see lint SKILL.md) opt-in: ".golangci.yml, .golangci.yaml, .golangci.toml, or .golangci.json present (walked from the changed file up to the repo root) — otherwise golangci-lint applies its own unconfigured \"standard\" linter preset unconditionally" install-hint: "Install golangci-lint: https://golangci-lint.run/docs/welcome/install/ | Go toolchain: https://go.dev/dl/" gates: @@ -42,4 +43,4 @@ gates: cmd: "go mod tidy -diff" trigger-globs: ["go.mod", "go.sum", "*.go"] remediation: "Run go mod tidy and commit the updated go.mod/go.sum. (go mod tidy -diff requires Go 1.23+; an older toolchain rejects the flag, so the gate reports skip rather than drift.)" -notes: "govulncheck is intentionally not a rung-4 default (per the epic brief's \"optional\" framing) — add it as a consumer-local gate via .claude/ecosystems/go.local.yaml if desired." +notes: "gofmt / golangci-lint --fix receive only *.go paths from , grouped by package directory for golangci-lint. govulncheck is intentionally not a rung-4 default (per the epic brief's \"optional\" framing) — add it as a consumer-local gate via .claude/ecosystems/go.local.yaml if desired." diff --git a/plugins/toolchain/reference/ecosystems/python.yaml b/plugins/toolchain/reference/ecosystems/python.yaml index f06950ac69..b2286be9c4 100644 --- a/plugins/toolchain/reference/ecosystems/python.yaml +++ b/plugins/toolchain/reference/ecosystems/python.yaml @@ -1,11 +1,18 @@ # Bundled portable default — python. Rung-4 fallback ONLY (consumer # .claude/ecosystems/python.yaml overrides key-by-key). Contract + schema: # https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/conventions/ecosystem-commands/README.md +# +# fix-cmd is format-only (ruff format). Code-changing autofixes (ruff check +# --fix) live in code-fix-cmd and require /toolchain:lint --code-fix plus +# that skill's confirmation / --yes gate. --unfixable F401 matches the +# ruff-format hook: protects just-added imports during iterative editing; +# F401 still surfaces as a finding — only auto-deletion is suppressed. globs: ["*.py", "pyproject.toml", "uv.lock", "pyrightconfig.json"] project-discovery: ["pyproject.toml"] build-cmd: null # no separate build step; lint/test cover verification test-cmd: "uv run pytest -x -q" # plain pytest when the project doesn't use uv check-cmd: "uv run ruff check . --no-fix && uv run ruff format . --check && uv run pyright" -fix-cmd: "uv run ruff check . --fix && uv run ruff format ." # pyright has no fix mode +fix-cmd: "uv run ruff format " # format-only; pyright has no fix mode +code-fix-cmd: "uv run ruff check --fix --no-unsafe-fixes --unfixable F401" opt-in: "ruff config (ruff.toml, .ruff.toml, or pyproject.toml [tool.ruff])" install-hint: "pip install ruff pyright uv | brew install uv ruff pyright | winget install astral-sh.uv" diff --git a/plugins/toolchain/reference/ecosystems/typescript.yaml b/plugins/toolchain/reference/ecosystems/typescript.yaml index 30d8fd3859..3c5077af39 100644 --- a/plugins/toolchain/reference/ecosystems/typescript.yaml +++ b/plugins/toolchain/reference/ecosystems/typescript.yaml @@ -1,11 +1,15 @@ # Bundled portable default — typescript. Rung-4 fallback ONLY (consumer # .claude/ecosystems/typescript.yaml overrides key-by-key). Contract + schema: # https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/conventions/ecosystem-commands/README.md +# +# fix-cmd is format-only (biome format). biome check --write applies lint +# autofixes and lives in code-fix-cmd behind /toolchain:lint --code-fix. globs: ["*.ts", "*.tsx", "*.js", "*.jsx", "*.mjs", "package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock", "tsconfig*.json"] project-discovery: ["package.json"] build-cmd: "npx tsc --noEmit" # when the project has a tsconfig; otherwise the package.json build script test-cmd: "the package.json test script (npm test / npx vitest run / npx jest — use what the project configures)" check-cmd: "npx biome check ." # biome.json present; use eslint when the project configures it instead -fix-cmd: "npx biome check --write ." +fix-cmd: "npx biome format --write " +code-fix-cmd: "npx biome check --write " opt-in: "biome.json → Biome; eslint config → ESLint (use whichever the project configures)" install-hint: "project-local devDependencies preferred; npm i -g typescript @biomejs/biome" diff --git a/plugins/toolchain/reference/resolution-ladder.md b/plugins/toolchain/reference/resolution-ladder.md index a390d23b65..28afba9a9b 100644 --- a/plugins/toolchain/reference/resolution-ladder.md +++ b/plugins/toolchain/reference/resolution-ladder.md @@ -14,7 +14,11 @@ conforming to the contract's `ecosystem.schema.json`. Command keys are **opaque - `build-cmd` — build/compile verification; `null` when the ecosystem has no build step - `test-cmd` — test command; `null` when no test framework is wired - `check-cmd` — lint/format check, no file modification; `null` when lint does not apply -- `fix-cmd` — auto-fix; `null` when the toolchain has no fix mode +- `fix-cmd` — **format-only** auto-fix (whitespace / import layout / style); `null` when absent. + Must not apply semantic/code-changing lint autofixes. `/toolchain:lint --fix` runs this key. +- `code-fix-cmd` — **code-changing** auto-fix (lint autofixes that rewrite logic, delete unused + imports, apply safe/unsafe code edits); `null` when absent. `/toolchain:lint --code-fix` runs + this key behind that skill's confirmation / `--yes` gate — never bare `--fix`. Plus `globs` (required — classify changed files), and optional `enabled` (default `true`; a consumer sets `false` to disable an ecosystem without deleting its file), `anchor`, `project-discovery`, diff --git a/plugins/toolchain/skills/check/SKILL.md b/plugins/toolchain/skills/check/SKILL.md index 519c4cfe15..2627eac375 100644 --- a/plugins/toolchain/skills/check/SKILL.md +++ b/plugins/toolchain/skills/check/SKILL.md @@ -89,7 +89,7 @@ If neither path yields changes and no `$ARGUMENTS`: report "No changes found (wo ### 1.5 Resolve each ecosystem's command surface -For each affected ecosystem, resolve its command surface (`globs`, `build-cmd`, `test-cmd`, `check-cmd`, `fix-cmd`, `anchor`, `project-discovery`, `install-hint`, `gates`, `notes`) through the four-rung ladder in [`${CLAUDE_PLUGIN_ROOT}/reference/resolution-ladder.md`](${CLAUDE_PLUGIN_ROOT}/reference/resolution-ladder.md): +For each affected ecosystem, resolve its command surface (`globs`, `build-cmd`, `test-cmd`, `check-cmd`, `fix-cmd`, `code-fix-cmd`, `anchor`, `project-discovery`, `install-hint`, `gates`, `notes`) through the four-rung ladder in [`${CLAUDE_PLUGIN_ROOT}/reference/resolution-ladder.md`](${CLAUDE_PLUGIN_ROOT}/reference/resolution-ladder.md): 1. Consumer `.claude/ecosystems/.yaml` (+ `.local.yaml` overlay, `~/.claude/ecosystems/` user-global, additive per key) → authoritative. 2. Absent → infer from the repo's build files and offer to persist via `/toolchain:setup`. diff --git a/plugins/toolchain/skills/check/context/go.md b/plugins/toolchain/skills/check/context/go.md index 54b4b27d95..599a655a36 100644 --- a/plugins/toolchain/skills/check/context/go.md +++ b/plugins/toolchain/skills/check/context/go.md @@ -23,8 +23,11 @@ own unconfigured "standard" linter preset on a repo that never configured any. # Check (CI mode — fails on violations) cd "$PROJECT_DIR" && golangci-lint run ./... -# Fix -cd "$PROJECT_DIR" && golangci-lint run --fix ./... +# Format-only (/toolchain:lint --fix) +cd "$PROJECT_DIR" && gofmt -w + +# Code-fix (semantic lint autofixes — /toolchain:lint --code-fix only) +cd "$PROJECT_DIR" && golangci-lint run --fix ``` ## Gotchas @@ -58,3 +61,7 @@ Find all Go modules dynamically: ```bash find "$REPO_ROOT" -name "go.mod" -not -path "*/vendor/*" ``` + +## Format / code-fix file scope + +`gofmt` and `golangci-lint --fix` receive only `*.go` paths from `` (never `go.mod`/`go.sum`). `golangci-lint --fix` is invoked once per package directory when changed files span multiple packages. diff --git a/plugins/toolchain/skills/check/context/python.md b/plugins/toolchain/skills/check/context/python.md index 2788e15953..d6bae594b8 100644 --- a/plugins/toolchain/skills/check/context/python.md +++ b/plugins/toolchain/skills/check/context/python.md @@ -6,18 +6,21 @@ # Check (CI mode — no auto-fix) cd "$PROJECT_DIR" && uv run ruff check . --no-fix -# Fix -cd "$PROJECT_DIR" && uv run ruff check . --fix +# Code-fix (semantic lint autofixes — /toolchain:lint --code-fix only) +cd "$PROJECT_DIR" && uv run ruff check --fix --no-unsafe-fixes --unfixable F401 ``` +`--unfixable F401` matches the `ruff-format` hook: protects just-added imports during iterative +editing; F401 still surfaces as a finding — only auto-deletion is suppressed. + ## Format ```bash # Check (CI mode) cd "$PROJECT_DIR" && uv run ruff format . --check -# Fix -cd "$PROJECT_DIR" && uv run ruff format . +# Fix (format-only — /toolchain:lint --fix) +cd "$PROJECT_DIR" && uv run ruff format ``` ## Test @@ -28,7 +31,7 @@ cd "$PROJECT_DIR" && uv run pytest tests/ -x -q ## Type check -Part of `check-cmd` (no fix mode — `fix-cmd` only runs the two ruff commands): +Part of `check-cmd` (no fix mode — `fix-cmd` is format-only; `code-fix-cmd` is ruff check only): ```bash cd "$PROJECT_DIR" && uv run pyright diff --git a/plugins/toolchain/skills/check/context/typescript.md b/plugins/toolchain/skills/check/context/typescript.md index 40e72fec9d..22b50cb5d1 100644 --- a/plugins/toolchain/skills/check/context/typescript.md +++ b/plugins/toolchain/skills/check/context/typescript.md @@ -20,8 +20,11 @@ cd "$PROJECT_DIR" && npm test # Use the project's configured linter — biome.json → Biome; eslint config → ESLint cd "$PROJECT_DIR" && npx biome check . -# Auto-fix -cd "$PROJECT_DIR" && npx biome check --write . +# Format-only (/toolchain:lint --fix) +cd "$PROJECT_DIR" && npx biome format --write + +# Code-fix (lint autofixes — /toolchain:lint --code-fix only) +cd "$PROJECT_DIR" && npx biome check --write ``` ## Gotchas diff --git a/plugins/toolchain/skills/lint/SKILL.md b/plugins/toolchain/skills/lint/SKILL.md index 70fa9b6c6a..066737bb73 100644 --- a/plugins/toolchain/skills/lint/SKILL.md +++ b/plugins/toolchain/skills/lint/SKILL.md @@ -1,7 +1,7 @@ --- -description: "Run polyglot linters and format checks across all affected ecosystems without a full build cycle — auto-detects ecosystems from changed files, honors each tool's config-file opt-in, and supports --fix mode to auto-correct where linters allow. Use when: 'lint this', 'run the linter', 'format check', 'fix the formatting', 'is this formatted right', 'run prettier/ruff/eslint', or for quick lint/format feedback during development; for build+test use /toolchain:check, for full outcome verification use /verification:confirm." +description: "Run polyglot linters and format checks across all affected ecosystems without a full build cycle — auto-detects ecosystems from changed files, honors each tool's config-file opt-in, and supports --fix (format-only) plus a gated --code-fix mode for semantic lint autofixes. Use when: 'lint this', 'run the linter', 'format check', 'fix the formatting', 'is this formatted right', 'run prettier/ruff/eslint', or for quick lint/format feedback during development; for build+test use /toolchain:check, for full outcome verification use /verification:confirm." user-invocable: true -argument-hint: "[ecosystem] [--fix] (e.g., /toolchain:lint, /toolchain:lint dotnet, /toolchain:lint --fix, /toolchain:lint all)" +argument-hint: "[ecosystem] [--fix|--code-fix] [--yes] [--dry-run] [--all-files] (e.g., /toolchain:lint, /toolchain:lint dotnet, /toolchain:lint --fix, /toolchain:lint --code-fix --yes, /toolchain:lint all)" shell: bash metadata: workflow-stage: verify @@ -25,24 +25,30 @@ Run lint and format checks across affected ecosystems in one command. Fills the Use `/toolchain:lint` for quick feedback during development. Use `/verification:confirm` before committing. -**The command surface is resolved, not hardcoded.** `/toolchain:lint` resolves each ecosystem's `check-cmd`/`fix-cmd` through the shared four-rung ladder in [`${CLAUDE_PLUGIN_ROOT}/reference/resolution-ladder.md`](${CLAUDE_PLUGIN_ROOT}/reference/resolution-ladder.md) — shared with `/toolchain:check`: the consuming repo's tracked `.claude/ecosystems/.yaml` is authoritative when present; the plugin's bundled portable defaults at `${CLAUDE_PLUGIN_ROOT}/reference/ecosystems/` are the rung-4 fallback. The consumer's file always wins. +**The command surface is resolved, not hardcoded.** `/toolchain:lint` resolves each ecosystem's `check-cmd`/`fix-cmd`/`code-fix-cmd` through the shared four-rung ladder in [`${CLAUDE_PLUGIN_ROOT}/reference/resolution-ladder.md`](${CLAUDE_PLUGIN_ROOT}/reference/resolution-ladder.md) — shared with `/toolchain:check`: the consuming repo's tracked `.claude/ecosystems/.yaml` is authoritative when present; the plugin's bundled portable defaults at `${CLAUDE_PLUGIN_ROOT}/reference/ecosystems/` are the rung-4 fallback. The consumer's file always wins. + +**Two mutators, two gates.** Format-only and code-changing autofixes are separate keys and separate flags — bare `--fix` must never run semantic lint autofixes (ruff `check --fix`, golangci-lint `--fix`, biome `check --write`, …). That split matches the rest of the fleet's mutator pattern (`review:fanout fix` confirmation + `--yes`, `claude-memory:audit` never batch-applies without approval). ## Arguments -`$ARGUMENTS` — optional ecosystem filter and/or mode flag. +`$ARGUMENTS` — optional ecosystem filter and/or mode flags. **Ecosystem filters** (if omitted, auto-detect from changed files): `/toolchain:lint` covers `dotnet`, `python`, `typescript`, `bash`, `powershell`, `markdown`, `go`, `yaml`, and `cross-cutting` (each resolved per the ladder); any with matching files is exposed as a filter. Aliases: `py` → `python`; `ts`/`node` → `typescript`; `shell` → `bash`; `ps`/`pwsh` → `powershell`; `md` → `markdown`; `golang` → `go`; `xc`/`text` → `cross-cutting`. Literal `all` runs every applicable ecosystem. -**Mode flag:** +**Mode flags:** | Flag | Effect | |------|--------| | (default) | Check mode — report violations, no file modifications | -| `--fix` or `fix` | Fix mode — auto-correct where the linter supports it | +| `--fix` or `fix` | **Format-only** fix mode — run each ecosystem's non-null `fix-cmd` (whitespace / import layout / style). Does **not** run `code-fix-cmd`. | +| `--code-fix` | **Code-changing** fix mode — run each ecosystem's non-null `code-fix-cmd` behind the [confirmation gate](#code-fix-confirmation-gate). Mutually exclusive with `--fix` in one invocation; if both appear, prefer `--code-fix` and note that `--fix` was ignored. | +| `--yes` / `-y` | Skip the interactive confirmation prompt for `--code-fix`. Required for non-interactive / headless `--code-fix` applies. Inert for check mode and for `--fix`. | +| `--dry-run` | With `--code-fix`: emit the plan (commands + scoped files) and **stop** — mutate nothing. Implies the plan half of the confirmation gate. | +| `--all-files` | With `--code-fix`: allow applying when the scoped file count exceeds the [file-cap](#code-fix-scope-fence) (default 40). Without it, over-cap runs stop after the plan. | -**Combinable:** `/toolchain:lint dotnet --fix`, `/toolchain:lint --fix`, `/toolchain:lint all`, `/toolchain:lint md` +**Combinable:** `/toolchain:lint dotnet --fix`, `/toolchain:lint --fix`, `/toolchain:lint --code-fix`, `/toolchain:lint --code-fix --yes`, `/toolchain:lint all`, `/toolchain:lint md` ## Workflow @@ -55,7 +61,8 @@ REPO_ROOT=$(git rev-parse --show-toplevel) Parse `$ARGUMENTS` for: - **Ecosystem filter**: extract any ecosystem name. Default: auto-detect -- **Fix mode**: detect `--fix` or `fix` in arguments. Default: check mode +- **Fix mode**: detect `--fix` / `fix` (format-only) vs `--code-fix` (code-changing). Default: check mode +- **Consent / scope flags**: `--yes` / `-y`, `--dry-run`, `--all-files` (only meaningful with `--code-fix`) ### 1. Detect ecosystems @@ -88,16 +95,28 @@ Auto-detection algorithm: 1. Resolve each covered ecosystem's surface per [`${CLAUDE_PLUGIN_ROOT}/reference/resolution-ladder.md`](${CLAUDE_PLUGIN_ROOT}/reference/resolution-ladder.md) (consumer `.claude/ecosystems/.yaml` when present, else the bundled default; a malformed consumer file warns and degrades to inference, never a hard stop). Skip any ecosystem whose resolved `enabled` is `false` (a consumer opt-out) — excluded even under `all` 2. For each ecosystem, match its `globs` against the changed-files list -3. Run every ecosystem with ≥1 glob match whose `opt-in` condition holds, plus cross-cutting when any text file changed. This binary run/skip treatment applies cleanly when `opt-in` describes a SINGLE condition for the whole `check-cmd` (dotnet, python): unmet → excluded from the run but still reported (see sections 2 and 3 below, `skip (opt-in unmet: ...)`; never silently omitted). When `opt-in` instead describes MULTIPLE independent per-tool conditions bundled into one opaque command string (bash's shellcheck-always/shfmt-conditional split; cross-cutting's per-tool config-file list), this rule does not apply — `check-cmd`/`fix-cmd` is a single opaque string with no way to run one sub-tool's portion without the other, so run it as before (unchanged from prior behavior) and report its real output. See `/toolchain:check`'s Gotchas for the known atomicity limitation this leaves open. +3. Run every ecosystem with ≥1 glob match whose `opt-in` condition holds, plus cross-cutting when any text file changed. This binary run/skip treatment applies cleanly when `opt-in` describes a SINGLE condition for the whole `check-cmd` (dotnet, python): unmet → excluded from the run but still reported (see sections 2 and 3 below, `skip (opt-in unmet: ...)`; never silently omitted). When `opt-in` instead describes MULTIPLE independent per-tool conditions bundled into one opaque command string (bash's shellcheck-always/shfmt-conditional split; cross-cutting's per-tool config-file list), this rule does not apply — `check-cmd`/`fix-cmd`/`code-fix-cmd` is a single opaque string with no way to run one sub-tool's portion without the other, so run it as before (unchanged from prior behavior) and report its real output. See `/toolchain:check`'s Gotchas for the known atomicity limitation this leaves open. If neither detection path yields changes and no filter specified: report "No changes found (working tree clean, no branch diff vs the default branch). Use `/toolchain:lint all` to check the full repo, or `/toolchain:lint ` for a specific filter." and stop. ### 2. Run linters per ecosystem -Run each ecosystem's resolved `check-cmd` (or `fix-cmd` with `--fix`). Honor each ecosystem's `opt-in`: for a single-condition ecosystem (dotnet, python), an unmet condition skips the whole ecosystem, reporting `skip (opt-in unmet: )` visibly (never a silent omission) in every column that ecosystem's row has — this skip counts toward the table's total ecosystem count but never toward the FAIL count, the same precedent as a missing-tool skip. For a multi-tool ecosystem (bash, cross-cutting) whose `check-cmd` bundles multiple sub-tools into one opaque string, this binary treatment doesn't apply — run and report `check-cmd`/`fix-cmd` as before (unchanged from prior behavior); see `/toolchain:check`'s Gotchas for the known atomicity limitation. +**Command selection by mode:** + +| Mode | Command key | Gate | +|------|-------------|------| +| check (default) | `check-cmd` | none (read-only) | +| `--fix` | `fix-cmd` | none beyond the flag (format-only by contract) | +| `--code-fix` | `code-fix-cmd` | [confirmation gate](#code-fix-confirmation-gate) + [scope fence](#code-fix-scope-fence) | + +Honor each ecosystem's `opt-in`: for a single-condition ecosystem (dotnet, python), an unmet condition skips the whole ecosystem, reporting `skip (opt-in unmet: )` visibly (never a silent omission) in every column that ecosystem's row has — this skip counts toward the table's total ecosystem count but never toward the FAIL count, the same precedent as a missing-tool skip. For a multi-tool ecosystem (bash, cross-cutting) whose `check-cmd` bundles multiple sub-tools into one opaque string, this binary treatment doesn't apply — run and report `check-cmd`/`fix-cmd`/`code-fix-cmd` as before (unchanged from prior behavior); see `/toolchain:check`'s Gotchas for the known atomicity limitation. For ecosystem-specific gotchas, reference `/toolchain:check` — its `context/.md` files own the per-ecosystem prose detail. +**`` substitution:** expand to the ecosystem-scoped changed-file list (paths relative to the execution root). Prefer this scoped list over whole-tree `.` / `./...` whenever the command string contains ``. Under `/toolchain:lint all` with an empty detection set, expand to the matching files under each project root (or the repo root) rather than inventing a silent whole-tree rewrite for code-fix — and still apply the [file-cap](#code-fix-scope-fence) to that expanded set. + +**Go `*.go` filter for format/code-fix:** ecosystem `globs` include `go.mod` / `go.sum`, but `gofmt -w` and `golangci-lint run --fix` reject non-source inputs (`gofmt` exits 2 on `go.mod`; `golangci-lint` requires named files in one directory). When substituting `` into Go `format-cmd` / `code-fix-cmd`, drop every non-`.go` path first. For `golangci-lint run --fix`, further partition the remaining `.go` paths by parent directory and invoke once per directory (never pass a multi-directory file list in one process). If filtering leaves zero `.go` files, skip that Go format/code-fix command and report the skip rather than invoking the tool on module metadata alone. + Per-project walking (ecosystems with `project-discovery`): - python: walk each `pyproject.toml` directory and run check/fix from there @@ -118,6 +137,37 @@ elif command -v ec-windows-amd64 >/dev/null 2>&1; then EC_BIN=ec-windows-amd64 fi ``` +### Code-fix confirmation gate + +`--code-fix` MUTATES semantics, not just whitespace — the only `/toolchain:lint` path that does. ALWAYS emit the plan first: + +```text +Code-fix plan — ecosystems: ( files scoped) +- python: uv run ruff check --fix --no-unsafe-fixes --unfixable F401 ( files) +- go: golangci-lint run --fix ( files) +- typescript: (no code-fix-cmd) — skip +``` + +Then gate on session context and flags. Every side-effect path is explicitly gated — the gate never self-downgrades unattended: + +| Session | Flags | Gate | +|---|---|---| +| Interactive | `--code-fix` only | Confirm with the user before applying. Honor scope narrowing ("only python"). | +| Interactive | `--code-fix --yes` | Skip the confirmation prompt and apply (still honor the file-cap unless `--all-files`). | +| Interactive or any | `--code-fix --dry-run` | Emit the plan and **STOP** — mutate nothing. | +| Non-interactive (`CLAUDE_CODE_REMOTE`, `claude -p`, an autonomous loop) | `--code-fix` without `--yes` | **STOP after the plan — mutate nothing.** The plan IS the report. Re-run with `--yes` to apply. | +| Non-interactive | `--code-fix --yes` | Apply (still honor the file-cap unless `--all-files`). | + +`--code-fix` opts INTO code-changing fix mode; `--yes` is the separate, explicit consent to mutate a tree with no human watching. A non-interactive session with no `--yes` is never consent. Bare `--fix` does not enter this gate. + +### Code-fix scope fence + +Before applying `--code-fix`: + +1. **Changed-files allowlist.** Scope each `code-fix-cmd` to the ecosystem's changed-file list via `` (or the expanded `all` set). Do not silently retarget a whole-tree `.` / `./...` when a non-empty scoped list exists. +2. **File cap (default 40).** Sum the scoped files across ecosystems that will actually run a non-null `code-fix-cmd`. If the sum exceeds 40 and `--all-files` is absent, emit the plan, report `file-cap exceeded ( > 40; pass --all-files to override)`, and **STOP** — mutate nothing. +3. **No LOC auto-budget.** Do not invent a line-count ceiling; the confirmation plan (file list + commands) is the pre-apply review surface. After apply, surface `git diff --stat` so the operator can see blast radius. + ### 3. Present results ```text @@ -135,13 +185,18 @@ Overall: FAIL (1 of 4 ecosystems failed) Use `pass`, `FAIL`, `skip` (tool not installed) or `skip (opt-in unmet: ...)` (config condition not met), or `—` (not applicable). Split lint and format into separate columns where the ecosystem has both (dotnet, python, bash). Use a single "Lint" column for ecosystems with only one tool (markdown, yaml, powershell). An opt-in-unmet skip fills every column that ecosystem's row has. -If fix mode was used, note which ecosystems were auto-fixed vs which have no auto-fix. +If `--fix` was used, note which ecosystems were format-fixed vs which have no `fix-cmd`. + +If `--code-fix` was used (and applied), note which ecosystems ran `code-fix-cmd` vs which have none, and show `git diff --stat` for blast radius. Show failing output below the table — truncated to key error lines, not the full dump. -### 4. Fix mode note +### 4. Fix mode notes + +- **`--fix`:** auto-fix capability derives from a non-null `fix-cmd`. Report check-only ecosystems alongside format fixes: "Fixed formatting in dotnet, python, bash. markdownlint had no remaining auto-fixes." +- **`--code-fix`:** capability derives from a non-null `code-fix-cmd`. Ecosystems with only `fix-cmd` (format-only) are not code-fixed under this flag — tell the operator to use `--fix` for those. Example: "Applied code-fix in python, go. typescript has no code-fix-cmd. Use `--fix` for format-only ecosystems (dotnet, bash, …)." -When `--fix` is used, auto-fix capability is derived from the config: an ecosystem supports auto-fix when its `fix-cmd` is non-null. Report check-only ecosystems alongside fixes: "Fixed formatting in dotnet, python. ShellCheck violations require manual fix (2 issues)." +**Consumer overrides:** a consumer who still puts code-changing autofixes in `fix-cmd` keeps that behavior under `--fix` (their file wins). Prefer migrating those verbs into `code-fix-cmd` so bare `--fix` stays format-only. Bundled portable defaults already split the two. ## Edge cases @@ -151,10 +206,13 @@ When `--fix` is used, auto-fix capability is derived from the config: an ecosyst - **Multiple projects in same ecosystem**: run per-project (each `pyproject.toml`, each `package.json`) - **File outside any ecosystem**: silently skip (no noise for binary files, images, etc.) - **CWD drift**: always use absolute paths from `$REPO_ROOT` +- **`--code-fix` over file-cap without `--all-files`**: plan + stop, never mutate +- **`--code-fix` in non-interactive session without `--yes`**: plan + stop, never mutate +- **Both `--fix` and `--code-fix`**: prefer `--code-fix`, note `--fix` ignored ## Relationship to other skills - **Composes from `/toolchain:check`**: `/toolchain:check` owns the per-ecosystem prose gotchas — reference it rather than duplicating - **Composed by `/verification:confirm`** (the separate `verification` plugin, when installed): the lint leg of full verification - **After a simplify/cleanup pass**: run `/toolchain:lint` to catch formatting issues the cleanup introduced -- **Before commit**: `/toolchain:lint --fix` is a quick pre-commit cleanup without the overhead of a full build +- **Before commit**: `/toolchain:lint --fix` is a quick pre-commit **format** cleanup without the overhead of a full build. Use `/toolchain:lint --code-fix` (interactive confirm, or `--yes` when headless) only when you intentionally want semantic lint autofixes. diff --git a/plugins/toolchain/skills/lint/evals/evals.json b/plugins/toolchain/skills/lint/evals/evals.json index 7617de73f9..44e5d9651a 100644 --- a/plugins/toolchain/skills/lint/evals/evals.json +++ b/plugins/toolchain/skills/lint/evals/evals.json @@ -16,14 +16,14 @@ }, { "id": 2, - "name": "fix-mode-parsed-and-auto-corrects", - "prompt": "/toolchain:lint dotnet --fix", - "expected_output": "Parses the --fix flag as fix mode and the dotnet filter, runs the ecosystem's fix-cmd to auto-correct where the linter supports it, and notes which ecosystems were auto-fixed versus which have no auto-fix (fix capability derives from a non-null fix-cmd in the config).", + "name": "fix-mode-is-format-only", + "prompt": "/toolchain:lint python --fix", + "expected_output": "Parses --fix as format-only fix mode and python as the ecosystem filter, runs the resolved fix-cmd (ruff format) without running code-fix-cmd (ruff check --fix), and notes which ecosystems were format-fixed versus which have no fix-cmd.", "files": [], "expectations": [ - "Parses --fix as fix mode (auto-correct) rather than check mode, and dotnet as the ecosystem filter", - "Runs the resolved fix-cmd to auto-correct where the linter supports it", - "Reports which ecosystems were auto-fixed vs which are check-only (no non-null fix-cmd)" + "Parses --fix as format-only fix mode (not code-changing) and python as the ecosystem filter", + "Runs the resolved fix-cmd only — does NOT run code-fix-cmd / ruff check --fix / golangci-lint --fix / biome check --write", + "Reports which ecosystems were format-fixed vs which are check-only (no non-null fix-cmd)" ] }, { @@ -61,6 +61,55 @@ "Reports 'No changes found' and suggests `/toolchain:lint all` or `/toolchain:lint ` when both detection paths are empty and no filter was given", "Does not silently run every ecosystem when no changes and no filter are present" ] + }, + { + "id": 6, + "name": "code-fix-requires-confirmation-or-yes", + "prompt": "/toolchain:lint --code-fix — non-interactive session, a few Python and Go files changed.", + "expected_output": "Parses --code-fix, emits a code-fix plan listing each ecosystem's code-fix-cmd and scoped file counts, and STOPS without mutating because the session is non-interactive and --yes is absent. Does not run format-only fix-cmd under this flag.", + "files": [], + "expectations": [ + "Parses --code-fix as code-changing fix mode (distinct from --fix)", + "Emits a code-fix plan (ecosystems, commands, scoped file counts) before any mutation", + "In a non-interactive session without --yes, STOPS after the plan and mutates nothing", + "Does not run fix-cmd / format-only commands under --code-fix" + ] + }, + { + "id": 7, + "name": "code-fix-file-cap-stops-without-all-files", + "prompt": "/toolchain:lint --code-fix --yes — more than 40 changed Python files in scope.", + "expected_output": "Even with --yes, when the scoped file count exceeds the default file-cap of 40 and --all-files is absent, emits the plan, reports file-cap exceeded, and mutates nothing.", + "files": [], + "expectations": [ + "Counts scoped files for ecosystems with a non-null code-fix-cmd", + "When the count exceeds 40 without --all-files, reports file-cap exceeded and STOPS without mutating", + "--yes alone does not bypass the file-cap; --all-files is required for over-cap applies" + ] + }, + { + "id": 8, + "name": "go-fix-filters-non-go-files", + "prompt": "/toolchain:lint --fix go — changed files are pkg/a.go and go.mod (and maybe go.sum).", + "expected_output": "Substitutes only *.go paths into gofmt -w (never go.mod/go.sum). If filtering leaves zero .go files, skips gofmt rather than invoking it on module metadata.", + "files": [], + "expectations": [ + "When substituting into Go fix-cmd (gofmt -w), drops every non-.go path from the ecosystem-scoped list", + "Does not pass go.mod or go.sum to gofmt (gofmt exits 2 on module metadata)", + "If no *.go paths remain after filtering, skips the Go format command and reports the skip" + ] + }, + { + "id": 9, + "name": "go-code-fix-groups-by-package-directory", + "prompt": "/toolchain:lint --code-fix --yes go — changed files span pkg/a/a.go and pkg/b/b.go plus go.mod.", + "expected_output": "Filters to *.go only, partitions by parent directory, and invokes golangci-lint run --fix once per package directory (never a multi-directory named-file list in one process).", + "files": [], + "expectations": [ + "Drops non-.go paths before golangci-lint run --fix", + "Partitions remaining .go paths by parent directory and invokes once per directory", + "Does not pass a multi-directory file list in one golangci-lint process (avoids 'named files must all be in one directory')" + ] } ] } diff --git a/plugins/toolchain/skills/setup/SKILL.md b/plugins/toolchain/skills/setup/SKILL.md index 1c7dcd49b2..5bad945059 100644 --- a/plugins/toolchain/skills/setup/SKILL.md +++ b/plugins/toolchain/skills/setup/SKILL.md @@ -41,7 +41,7 @@ first**, then report a PASS/FAIL/INFO table; modify nothing. 1. **Configured ecosystems.** For each `$REPO_ROOT/.claude/ecosystems/.yaml` present, report the ecosystem and its resolved command surface - (`build-cmd`/`test-cmd`/`check-cmd`/`fix-cmd`). Validate each against the contract's + (`build-cmd`/`test-cmd`/`check-cmd`/`fix-cmd`/`code-fix-cmd`). Validate each against the contract's `ecosystem.schema.json`. **FAIL** a schema-invalid file (with the validation error in the remediation line), or a tracked ecosystem file excluded by `.gitignore` (teammates would never receive it — report the matching rule). Otherwise PASS. If `$ARGUMENTS` names one ecosystem, scope @@ -74,7 +74,8 @@ then specialize it to the repo: - **python** — `pyproject.toml`/`uv.lock`. Prefer `uv run …` when `uv.lock` exists, else plain `ruff`/`pytest`. Set `project-discovery` to the `pyproject.toml` roots. - **typescript** — `package.json`/`tsconfig*.json`. Read the `scripts` block for the real `test-cmd`; - pick `check-cmd`/`fix-cmd` from the configured linter (`biome.json` → Biome, eslint config → ESLint). + pick `check-cmd`/`fix-cmd` (format-only) and `code-fix-cmd` (semantic lint autofixes) from the + configured linter (`biome.json` → Biome format vs check --write; eslint config → ESLint). - **bash** / **powershell** — shell/PowerShell files present; keep the bundled check/fix commands unless the repo documents its own. - **markdown** — a markdownlint config present.