From starter
Lint Python : ruff auto-fix (Pass 1) puis modèle LLM pour tout le reste — docstrings, renommages, sécurité, complexité. Fan-out parallèle. Zéro finding ignoré.
How this skill is triggered — by the user, by Claude, or both
Slash command
/starter:check-styleThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
- Argument: $ARGUMENTS
python "${CLAUDE_PLUGIN_ROOT}/tools/resolve_runner.py" 2>/dev/null || echo uvpython "${CLAUDE_PLUGIN_ROOT}/tools/list_changed.py" 2>/dev/nullpython "${CLAUDE_PLUGIN_ROOT}/tools/list_changed.py" --all 2>/dev/nullpython "${CLAUDE_PLUGIN_ROOT}/tools/resolve_runner.py" --probe ruff 2>/dev/nullTwo-pass architecture: ruff fixes everything it can (cheap, fast, no LLM tokens), then the model fixes ALL of what ruff left behind — no advisory bucket. Never halts — everything is either fixed or refused with a reason.
ruff --fix --unsafe-fixes with all enabled codes, then ruff format. One shell call, ruff parallelises internally.ruff --no-fix --output-format=json. ALL remaining findings go to the model.style-fixer subagent per impacted file, in a single message. Each agent handles ALL codes: docstrings, renames, imports, syntax, security, complexity refactoring, and any other code.N801 (class) and N802 (function) renames need Grep to find every caller, then MultiEdit per touched file. Subagents refuse these by design.These rules keep every Bash call below in the allowlist Claude Code can match statically — required for silent autopilot execution. Violating them triggers hardcoded safety prompts that no permission rule can bypass (cf. anthropics/claude-code#48762).
cd <path> && ... — the shell already runs from the project root. If you need a path, pass it inline as an absolute argument.for ... do ... done loops — use a python -c "..." one-liner instead (covered by Bash(python:*)).cmd1 && cmd2 2>/dev/null) — split into separate calls or wrap in Python.uv run ruff ..., python -c "...", git diff .... One binary, no chaining.$ARGUMENTS is non-empty AND not exactly all → output Unknown argument: <token>. Accepts no argument or 'all'. Stop.ruff: NOT INSTALLED → output ruff not installed. Run /starter:proj-init. Stop.<files>$ARGUMENTS == all → <files> = the All Python files list from Context (entire codebase).<files> = the Changed Python files list from Context (diff only).If <files> is empty → output No .py files to lint. Stop with success.
Replace <runner> below with the literal Runner value from the Context above (uv or poetry).
Ruff exits 1 when findings remain after auto-fix — that is expected, not an error. Do NOT mask with || true (creates a compound that breaks allowlist matching, cf. anthropics/claude-code#13607) and do NOT redirect stderr with 2>/dev/null (same bug — breaks Bash(<runner> run:*) matching even on simple commands).
--force-exclude is required so [tool.ruff].extend-exclude (set by proj-init) applies even when files are passed explicitly.
<runner> run ruff check <files> --force-exclude --fix --unsafe-fixes --silent
<runner> run ruff format <files> --force-exclude
This handles F401, F541, F841, E/W/I/UP, D-fixable, B007/B009/B010/B011, and every other code ruff knows how to fix — in one shot. No pre-classification needed; ruff's --fix --unsafe-fixes applies every fix it has.
<runner> run ruff check <files> --force-exclude --output-format=json --no-fix
The output is a JSON array. Each finding has fields code, filename, location.row, location.column, message, and fix (object or null). Everything ruff could fix is already gone. All remaining findings go to model_fixable[] — there is no advisory bucket. The model fixes everything or refuses with a reason.
Hold the list in memory. Do not print yet. Branch below.
Do NOT split findings into "model auto-fix" and "advisory" sections before the fan-out. All findings are displayed uniformly. Refused findings (if any) are surfaced only after the fan-out, in Step 5.
Render each finding as a block preceded by a separator. To get the snippet, Read the target file with offset = max(1, location.row - 1) and limit = 3. Format:
----------------------------------
<filename>:<row>:<col> <code> — <message>
<row-1> | <previous source line>
> <row> | <offending source line>
<row+1> | <next source line>
→ <action line>
If the file does not have a previous/next line (top/bottom), omit that side. Plain ASCII; no colors.
The → <action> line previews what the model will do:
| Code | Action line template |
|---|---|
D100 | → Insert a module-level docstring at line 1 (one-line summary derived from the file's name and imports). |
D101 | → Insert a Google-style class docstring under \class :`.` |
D102 | → Insert a Google-style method docstring under \def (...):`.` |
D103 | → Insert a Google-style function docstring under \def (...):`.` |
D104 | → Insert a package docstring at the top of \init.py`.` |
D105 | → Insert a one-line docstring under the magic method. |
D106 | → Insert a one-line docstring under the nested class. |
D107 | → Insert a one-line docstring under \init` describing what the constructor sets up.` |
N801 | → Rename class \` → `` (CapWords) across the project.` |
N802 | → Rename function \` → `` (lower_snake_case) across the project.` |
N803 | → Rename argument \` → `` (lower_snake_case) inside this function.` |
N806 | → Rename local variable \` → `` (lower_snake_case) inside this function.` |
F821 | → Add missing import for \` (inferred from usage context in this file).` |
E999 | → Fix syntax error: <ruff message>. Model will read the file and repair. |
S113 | → Add \timeout=30` to the requests call.` |
S301/S302 | → Replace pickle with \json` (if data is JSON-serializable).` |
S311 | → Replace \random.` with `secrets.`.` |
S324 | → Replace weak hash with \sha256` or add `usedforsecurity=False`.` |
S501–S503 | → Enable certificate verification: \verify=True`.` |
S506 | → Replace \yaml.load()` with `yaml.safe_load()`.` |
S602/S605/S607 | → Replace \shell=True` with arg list.` |
S608 | → Use parameterized query instead of f-string SQL. |
Other S* | → Fix security issue: <ruff message>. Model reads context and applies fix. |
C901 | → Refactor: extract helper functions to reduce cyclomatic complexity. |
C901 / PLR0911 / PLR0912 / PLR0915 | → Refactor: simplify function to reduce branches/returns/statements. |
PLR0913 | → Refactor: group parameters into a dataclass or TypedDict. |
PLR2004 | → Replace magic value with a named constant. |
PLW2901 | → Use a different variable name to avoid overwriting the loop variable. |
Other PL* / C* | → Fix: <ruff message>. Model reads context and refactors. |
| Any other code | → Fix: <ruff message>. Model reads context and applies fix. |
<old> (for N-codes) is extracted from the ruff message (it is usually quoted in backticks: Function name `getUser` should be lowercase). Compute <new>:
N801): split on _ and on lowercase→uppercase boundaries, capitalize each token, concat. myClass → MyClass, my_class → MyClass.N802/N803/N806): insert _ before every uppercase letter that follows a lowercase one, lowercase everything. getUser → get_user, MyVar → my_var, HTTPServer → http_server.<name> (for F821) is extracted from the ruff message (e.g., ``Undefined name os``` → os`).
If model_fixable is empty after Pass 1:
Style: no findings (ruff fixed everything).
Stop with success.
If len(model_fixable) > 0:
Print all findings with snippets and action lines:
Found <K> remaining finding(s) after ruff auto-fix — fixing with model:
----------------------------------
<first finding: snippet + → action line>
----------------------------------
<second finding: snippet + → action line>
...
Immediately run the fix sequence below. No AskUserQuestion.
style-fixer (parallel, per file)Group model_fixable[] by filename, excluding N801 and N802 items (the parent handles those in Step 2). Each group becomes one subagent invocation.
Issue ALL Task calls in a single message. For G groups ≤ 10, that's G Task tool calls in the same response. For G > 10, split into batches of 10 across consecutive messages (Claude Code's parallel limit is 10 per message).
Each Task call invokes subagent style-fixer with this JSON payload:
{
"file": "<source path>",
"model_fixable": [
{"code": "<any ruff code>", "row": <int>, "col": <int>, "message": "<ruff message>"},
...
]
}
Each subagent returns ONE line of JSON:
{"file":"<path>","docstrings":<N>,"renames_local":<N>,"code_fixes":<N>,"refused":[...],"errors":[...]}
Aggregate across all subagents:
docstrings_total = sum of docstringsrenames_local_total = sum of renames_localcode_fixes_total = sum of code_fixes (F821 imports added + E999 syntax fixes)agent_refused[] = flat union of every refused list (surfaced in final summary with reasons)agent_errors[] = flat union of every errors listIf agent_errors[] is non-empty, surface them in the final summary but do not halt — the user can re-run.
N801 / N802) handled by the parentClass (N801) and top-level function (N802) renames have project-wide reach: the symbol may be imported, called, mocked, or named in a fixture from any other file. Subagents cannot see beyond their own file, so the parent owns this step.
Group model_fixable[] rename findings whose code is N801 or N802 by (old_name, new_name). For each group:
Grep pattern=\b<old_name>\b output_mode=files_with_matches
Read it to confirm the matches are real references (not coincidental substrings inside an unrelated docstring or a string literal).MultiEdit:
old_string = <old_name>, new_string = <new_name>, replace_all = true.Edit calls with explicit context, one per real reference.If no N801/N802 items exist in model_fixable[], skip this step.
<runner> run ruff check <files> --force-exclude --output-format=json --no-fix
Compute remaining = count of findings from the new run (items the model could not fix end up in agent refused[]).
Stage only files actually modified (skip untouched files to avoid grabbing unrelated user edits). The set of touched files = the original <files> PLUS any extra files touched by N801/N802 cross-file renames.
Substitute <touched files> as a space-separated list of paths:
python -c "import subprocess, sys; [subprocess.run(['git','add','--',f], check=False) for f in sys.argv[1:] if subprocess.run(['git','diff','--quiet','--',f], check=False).returncode != 0]" <touched files>
If agent_refused[] is non-empty after Steps 1–4:
For each item in agent_refused[], Read the target file and compose a concrete proposed fix grounded in the actual source line (same quality as the action line table above).
Display with separators:
===============================================================================
Could not auto-fix — proposed fixes (<N>):
===============================================================================
----------------------------------
<filename>:<row>:<col> <code> — <message>
<row-1> | <previous source line>
> <row> | <offending source line>
<row+1> | <next source line>
→ Proposed fix: <concrete fix grounded in the actual code>
----------------------------------
...
Call AskUserQuestion once:
Apply refused fixesApply fixes for these <N> issue(s) that could not be handled automatically?falseYes, description Apply all proposed fixes aboveNo, description Skip — these will appear as remaining in the summaryOn Yes: apply each fix directly (parent, not a subagent):
Read the file to get the exact old_string.Edit or MultiEdit the file in-place.python -c "import subprocess, sys; [subprocess.run(['git','add','--',f], check=False) for f in sys.argv[1:] if subprocess.run(['git','diff','--quiet','--',f], check=False).returncode != 0]" <refused files>consent_fixed.On No: consent_fixed = 0.
If agent_refused[] is empty, skip this step entirely (consent_fixed = 0).
Totals:
auto_fixed = items fixed by ruff (Pass 1) + style-fixer subagents + parent cross-file renames (Steps 1–4)consent_fixed = items fixed in Step 5 after user approvedIf auto_fixed + consent_fixed == initial len(model_fixable):
Style: <auto_fixed> auto-fixed, <consent_fixed> consent-fixed, 0 remaining.
(Omit 0 consent-fixed segment when consent_fixed == 0: Style: <auto_fixed> fixed, 0 remaining.)
If items remain (user declined Step 5 or some fixes failed):
Style: <auto_fixed> auto-fixed, <consent_fixed> consent-fixed, <still_remaining> remaining.
Remaining:
- <filename>:<row> <code> — <reason>
...
Stop with success.
style-fixer. Refused findings appear only in Step 5, after the fan-out completes.AskUserQuestion. Refused findings get a consent prompt before the parent applies them directly.Edit/MultiEdit is preceded by a Read on the target file so old_string is grounded in real text, not paraphrased.|| true (creates a compound that breaks allowlist matching, cf. anthropics/claude-code#13607). NEVER redirect with 2>/dev/null (same bug — breaks Bash(<runner> run:*) matching even on simple commands).git add -A.Provides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub nasswiel/shapsha --plugin starter