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
2 changes: 1 addition & 1 deletion docs/conventions/ecosystem-commands/ecosystem.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
},
"opt-in": {
"type": "string",
"description": "The config file or condition whose presence signals the repo uses this toolchain (e.g. 'biome.json'). Informational for resolvers deciding applicability."
"description": "A condition resolvers evaluate to decide whether to run this ecosystem's lint phase — either a single condition governing the whole check-cmd (e.g. 'a ruff config (ruff.toml, .ruff.toml, or pyproject.toml [tool.ruff])'), or a multi-clause value where each clause names a sub-tool and its own condition (e.g. 'shellcheck always applies to shell files; shfmt only when .editorconfig declares shell style'). Unmet -> resolvers report a visible skip rather than silently omitting the ecosystem, and never impose the tool's built-in defaults on a repo that hasn't configured it."
},
"install-hint": {
"type": "string",
Expand Down
1 change: 1 addition & 0 deletions docs/conventions/ecosystem-commands/examples/dotnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ build-cmd: 'dotnet build "<solution-or-project-file>"'
test-cmd: 'dotnet test "<solution-or-project-file>" --no-build'
check-cmd: 'dotnet format "<solution-or-project-file>" --verify-no-changes'
fix-cmd: 'dotnet format "<solution-or-project-file>"'
opt-in: ".editorconfig with a [*] or C#-glob section, walked from the changed file up to the repo root or a `root = true` marker (whichever comes first) — otherwise dotnet format applies Roslyn's built-in defaults unconditionally"
install-hint: "Install .NET SDK from https://dot.net"
gates:
- name: nuget-lockfile-drift
Expand Down
2 changes: 1 addition & 1 deletion plugins/toolchain/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "toolchain",
"version": "0.5.2",
"version": "0.6.0",
"description": "Repo-agnostic polyglot verification toolchain: build + test + lint for changed files across .NET, Python, TypeScript, Bash, PowerShell, Markdown, 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).",
"author": {
"name": "Melodic Software",
Expand Down
35 changes: 35 additions & 0 deletions plugins/toolchain/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,41 @@
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.6.0]

### Added

- **dotnet ecosystem `opt-in` key** (`.editorconfig` with a `[*]` or C#-glob
section, walked from the changed file up to the repo root or a
`root = true` marker, whichever comes first — empirically verified against
dotnet SDK 10.0.302 that a universal `[*]` section governs `dotnet format`'s
output on `.cs` files just as a `[*.cs]` section would, that a
`.editorconfig` with only unrelated globs has zero effect, and that a
nested `root = true` marker genuinely stops EditorConfig discovery before
it reaches an outer section) — closes the one lint-bearing ecosystem gap
where a config-presence opt-in didn't exist.
- **`/toolchain:check` now honors `opt-in`** for the lint phase (it never did
before — `dotnet format --verify-no-changes` ran unconditionally whenever
`.cs`/`.csproj`/etc. files changed, regardless of whether the repo
configured any style/analyzer preferences). Build and test are unaffected;
only the lint phase is gated. This binary run/skip treatment applies to
single-condition ecosystems (dotnet, python); multi-tool ecosystems whose
`opt-in` bundles several sub-tools into one opaque command string (bash,
cross-cutting) are unchanged from prior behavior — a bundled command
cannot be partially suppressed, a known limitation documented in
`check/SKILL.md`'s Gotchas.
- **Visible `skip (opt-in unmet: ...)` status** in both `/toolchain:check`
and `/toolchain:lint` results tables — a single-condition ecosystem whose
`opt-in` isn't met is now reported, not silently dropped from output as
it previously was in `/toolchain:lint` for every opt-in-bearing ecosystem.

### Fixed

- dotnet's lint/format check no longer imposes Roslyn's built-in formatting
defaults on a repo that never configured `.editorconfig`/analyzer
preferences — matching the same "never impose an unconfigured opinion"
posture already applied at the hook layer by `ruff-format`/`typos-format`.

## [0.5.2]

### Changed
Expand Down
24 changes: 24 additions & 0 deletions plugins/toolchain/reference/ecosystems/dotnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,34 @@
# .claude/ecosystems/dotnet.yaml overrides this key-by-key; this file is never
# written into a consumer repo). Ecosystem-commands contract + schema:
# https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/conventions/ecosystem-commands/README.md
#
# opt-in rationale: dotnet format's own docs state preferences are read from
# .editorconfig "if present, otherwise a default set of preferences will be
# used" (https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format) —
# verified empirically (dotnet SDK 10.0.302) that a project with NO
# .editorconfig still gets `dotnet format --verify-no-changes` failures
# (Roslyn's built-in whitespace conventions apply unconditionally). Gated on
# a section that actually reaches .cs files (a universal `[*]`, or a
# C#-specific glob like `[*.cs]`/`[*.{cs,vb}]`), not bare file presence,
# mirroring bash.yaml's "shfmt only when .editorconfig declares shell style"
# precision — a .editorconfig with ONLY unrelated globs (e.g. `[*.md]`, no
# `[*]`) verifiably has zero effect on dotnet format's output (empirically
# confirmed: exit 0, no diagnostics) and shouldn't count as configured .NET
# style. EnableNETAnalyzers was considered and rejected as a signal: it
# defaults to true for .NET 5+, so its presence is not meaningful.
#
# The walk stops at a `root = true` marker even when that file has no
# C#-relevant section — EditorConfig discovery itself stops there (verified
# empirically: an outer repo-root `[*]` section has zero effect on a nested
# .cs file once a `root = true` .editorconfig sits between them), so a
# parent section beyond that marker is never actually consulted by dotnet
# format and must not count toward this opt-in either. Same discipline as
# bash-format.sh's shell_editorconfig_opt_in() walk elsewhere in this repo.
globs: ["*.cs", "*.csproj", "*.sln", "*.slnx", "*.props", "*.targets"]
anchor: "the solution file at repo root (*.slnx or *.sln); when none exists, the nearest *.csproj above the changed files"
build-cmd: 'dotnet build "<solution-or-project-file>"'
test-cmd: 'dotnet test "<solution-or-project-file>" --no-build'
check-cmd: 'dotnet format "<solution-or-project-file>" --verify-no-changes'
fix-cmd: 'dotnet format "<solution-or-project-file>"'
opt-in: ".editorconfig with a [*] or C#-glob section, walked from the changed file up to the repo root or a `root = true` marker (whichever comes first) — otherwise dotnet format applies Roslyn's built-in defaults unconditionally"
install-hint: "Install .NET SDK from https://dot.net"
12 changes: 11 additions & 1 deletion plugins/toolchain/skills/check/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ Run build → test → lint in order per ecosystem. Stop that ecosystem on first

Tool presence: before each ecosystem runs, verify the tool is on `PATH`. If missing, report `skip` with the ecosystem's `install-hint` from the ecosystem config — never report `FAIL` for a missing tool.

**Opt-in gate (lint phase only)**: before running an ecosystem's `check-cmd`, evaluate its resolved `opt-in` condition (if present) against the repo. Build and test always run regardless of `opt-in` — only the lint phase is gated, since compiling and testing don't depend on style configuration.

This binary gate applies cleanly when `opt-in` describes ONE condition governing the whole `check-cmd` (e.g. dotnet, python): unmet → report the ecosystem's Lint column as `skip (opt-in unmet: <condition, ≤10 words>)` — visible, not silently omitted — and do not run `check-cmd`. Met → run `check-cmd` normally.

When `opt-in` instead describes MULTIPLE independent per-tool conditions bundled into one opaque command string (e.g. bash's `"shellcheck always applies to shell files; shfmt only when .editorconfig declares shell style"`, where `check-cmd` is `shellcheck ... && shfmt -d <files>`), this gate does NOT apply — `check-cmd` is a single opaque string (per the ecosystem-commands contract) with no way to run one sub-tool's portion without the other. Run `check-cmd` as before (unchanged from prior behavior) and report its real output; do not attempt a partial skip. See Gotchas below for the known atomicity limitation this leaves open.

An opt-in-unmet skip (single-condition case) counts toward the table's total ecosystem count but never toward the FAIL count, the same precedent as a missing-tool skip. This is ecosystem-generic (reads the resolved `opt-in` key), not dotnet-specific — it applies to every current and future single-condition opt-in-bearing ecosystem `/toolchain:check` covers. Project-declared CI-parity gates (below) are unaffected — they already run independent of `check-cmd`.

**Project-declared CI-parity gates** — when the consuming project documents extra local checks that mirror CI gates plain build / test / lint don't catch (lockfile drift, generated-artifact freshness, schema regeneration), run the ones whose trigger files changed. These live in the consumer's own conventions (its `CLAUDE.md` / rules / commands reference) — this plugin ships none of its own.

For ecosystem-specific gotchas (xUnit `--nologo` trap, `dotnet test --project`, etc.), read the corresponding `context/<ecosystem>.md` file.
Expand All @@ -126,7 +134,7 @@ For ecosystem-specific gotchas (xUnit `--nologo` trap, `dotnet test --project`,
Overall: FAIL (1 of 2 ecosystems failed)
```

Use `pass`, `FAIL`, `skip`, or `—` (not applicable — for ecosystems where the corresponding command is null in the ecosystem config). Show failing command output below the table.
Use `pass`, `FAIL`, `skip` (tool missing) or `skip (opt-in unmet: ...)` (config condition not met), or `—` (not applicable — for ecosystems where the corresponding command is null in the ecosystem config). Show failing command output below the table.

If any project-declared CI-parity gates fired, summarize each by name + outcome below the per-ecosystem block, with the remediation pointer on failure.

Expand All @@ -143,4 +151,6 @@ When composing `/toolchain:check` from another skill (like `/verification:confir

- **CWD drift** — the #1 source of false failures. Always use absolute paths
- **Missing tools** — report as `skip` with reason, not as failure (e.g., `uv` not installed)
- **Opt-in unmet** — report as `skip (opt-in unmet: ...)` with the condition, not as failure and not silently omitted (e.g., dotnet with no C#-relevant `.editorconfig`)
- **Multi-tool `check-cmd` atomicity** — when a multi-tool ecosystem's `check-cmd` bundles a gated sub-tool and an unconditional sub-tool in one shell string (e.g. bash's `shellcheck ... && shfmt -d <files>`), the opt-in gate cannot suppress just the gated sub-tool's contribution — both run whenever the unconditional sub-tool's condition holds, per the ecosystem-commands contract's own "opaque shell string" rule. Splitting a multi-tool `check-cmd` into separately gateable ecosystem keys would need a schema change; not addressed here
- **Multiple projects in same ecosystem** — ecosystems with an `anchor` use that as the scoping anchor; ecosystems with `project-discovery` patterns walk each discovered project root
6 changes: 6 additions & 0 deletions plugins/toolchain/skills/check/context/dotnet.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ dotnet test --project "$REPO_ROOT/path/to/Project.Tests.csproj"

## Lint / Format

Opt-in gated: only runs when a governing `.editorconfig` is present (see the
`opt-in` key and its header-comment rationale in
`reference/ecosystems/dotnet.yaml`) — otherwise skipped visibly rather than
imposing Roslyn's built-in formatting defaults on a repo that never
configured any.

```bash
# Check formatting (CI mode — fails on violations)
dotnet format "$REPO_ROOT/<solution>" --verify-no-changes
Expand Down
7 changes: 4 additions & 3 deletions plugins/toolchain/skills/lint/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ 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/<ecosystem>.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
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.

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 <ecosystem>` 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`skip tools the project hasn't configured.
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: <condition, ≤10 words>)` 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply opt-in skip reporting beyond dotnet/python

When /toolchain:lint sees a changed Markdown/PowerShell/YAML file whose configured opt-in is unmet, this new rule only defines the visible skip (opt-in unmet: ...) behavior for dotnet and python. Those ecosystems also have single-condition opt-ins in the bundled defaults, so this leaves the old silent omission path in place for them despite the updated results-table contract saying unmet opt-ins should be reported. Please treat every single-condition opt-in ecosystem, not just dotnet/python, as a visible skip.

Useful? React with 👍 / 👎.


For ecosystem-specific gotchas, reference `/toolchain:check` — its `context/<ecosystem>.md` files own the per-ecosystem prose detail.

Expand Down Expand Up @@ -130,7 +130,7 @@ fi
Overall: FAIL (1 of 4 ecosystems failed)
```

Use `pass`, `FAIL`, `skip` (tool not installed), 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).
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.

Expand All @@ -144,6 +144,7 @@ When `--fix` is used, auto-fix capability is derived from the config: an ecosyst

- **No git changes but `/toolchain:lint all`**: run all applicable ecosystems (useful after rebase or pull)
- **Missing tools**: report as `skip` with tool name and install hint, not as failure
- **Opt-in unmet**: report as `skip (opt-in unmet: ...)` with the condition, not as failure and not a silent omission (e.g., dotnet with no C#-relevant `.editorconfig`)
- **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`
Expand Down
Loading