From 19d8ec573a24721d8071889821972445c721042e Mon Sep 17 00:00:00 2001 From: Sebastian Tramp Date: Sat, 17 Oct 2026 15:46:55 +0200 Subject: [PATCH 1/3] ignore .claude/ in the deptry and trivy checks, fixes #109 Claude Code creates git worktrees under .claude/worktrees/, which puts a complete second checkout inside the repository. Both checks that walk the tree rather than named targets then read it as if it were this project: $ git worktree add .claude/worktrees/wip $ poetry run deptry . .claude/worktrees/wip/tests/test_example.py:11:8: DEP004 'pytest' ... Found 4 dependency issues. deptry's default excludes cover a tests directory at the root but not a nested copy of one, so every test import in the second checkout is reported, and task check fails in the parent repository for as long as the worktree exists - while the same checks pass inside the worktree. trivy has the same exposure, and contrary to the report it fails too: check:trivy passes --exit-code 1, and with the lock file committed the scan picks up both copies (num=2), so a worktree on an older branch brings that branch's findings into the parent's check. check:ruff and check:mypy are unaffected: they are given tests and the package as explicit targets. Verified in a rendered plugin case - reproduced both, then confirmed --extend-exclude '\.claude' returns deptry to "Scanning 3 files... no dependency issues" and --skip-dirs .claude returns trivy to one target. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013pwym1cFV5jEhi7LdzKmEY --- CHANGELOG.md | 4 ++++ src/Taskfile.yaml | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f37dec7..31c4851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Changed - trivy: use 0.76.0.1 +- `check:deptry` and `check:trivy` ignore `.claude/`, so a git worktree created + there - a complete second checkout inside the repository - no longer fails + `task check` with findings from its own copy of the tests and its own lock + file (#109) ## [9.9.0] 2026-10-13 diff --git a/src/Taskfile.yaml b/src/Taskfile.yaml index aade49a..1c75d8c 100644 --- a/src/Taskfile.yaml +++ b/src/Taskfile.yaml @@ -164,10 +164,14 @@ tasks: deps: - prepare cmds: + # .claude/ is skipped because a git worktree created there is a second + # checkout inside the repository, whose lock file is scanned as if it + # were this one - on an older branch, with older findings. - > poetry run trivy fs --include-dev-deps --scanners vuln + --skip-dirs .claude --skip-files .poetry/plugins/poetry.lock --exit-code 1 . @@ -177,7 +181,12 @@ tasks: deps: - prepare cmds: - - poetry run deptry . + # deptry's default excludes cover a tests directory at the root but not + # a nested copy of one, so the worktree described above would be read as + # source and its test imports reported. + - > + poetry run deptry . + --extend-exclude '\.claude' check:ruff: desc: Complain about everything else From f25a7021fc3f05f8428297d7257a3f9a7f04af82 Mon Sep 17 00:00:00 2001 From: Sebastian Tramp Date: Sat, 17 Oct 2026 15:49:13 +0200 Subject: [PATCH 2/3] name ARG002 on a framework-fixed signature as settled, fixes #108 A task declaring FixedNumberOfInputs([]) never reads the inputs argument of execute(), but WorkflowPlugin decides the signature, so ARG002 cannot be answered by dropping it. The rules listed only PLR0913 and PLR0917 as settled, so every project had to escalate to a human the case the shipped example_workflow.py already resolves with a noqa. The other escape does not hold up. Renaming the argument to _inputs passes both linters - mypy treats an override parameter starting with an underscore as positional-only, and ruff stops reporting it - but the name is part of the API the caller uses: the shipped tests/test_example.py and the plugin-testing skill both call execute(inputs=(), context=...) by keyword, so the rename would raise TypeError against the template's own test, and nothing here can see how DataIntegration invokes it. The example also violated the rule it illustrates: the rules require the reason in a comment above a noqa, and it carried none. It does now. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013pwym1cFV5jEhi7LdzKmEY --- CHANGELOG.md | 6 +++++ src/{{ '.claude' }}/rules/copier-template.md | 8 ++++++ .../SKILL.md | 27 +++++++++++++++++++ ...in' %}example_workflow.py{% endif %}.jinja | 2 ++ 4 files changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31c4851..e90d9ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p there - a complete second checkout inside the repository - no longer fails `task check` with findings from its own copy of the tests and its own lock file (#109) +- `ARG002` on an argument whose presence the framework fixes - `inputs` in a + workflow task with no input ports - is a settled suppression in the shipped + rules, beside `PLR0913` and `PLR0917`, so it no longer needs a human decision + per project; `plugin-implementation` says why renaming it to `_inputs` is the + wrong answer, and the example plugin now carries the reason above its + `# noqa` (#108) ## [9.9.0] 2026-10-13 diff --git a/src/{{ '.claude' }}/rules/copier-template.md b/src/{{ '.claude' }}/rules/copier-template.md index 61277e6..05e41f2 100644 --- a/src/{{ '.claude' }}/rules/copier-template.md +++ b/src/{{ '.claude' }}/rules/copier-template.md @@ -60,6 +60,14 @@ reason above it. Neither escape ruff suggests is available, since keyword-only arguments answer `PLR0917` but not `PLR0913`, and folding the arguments into one object breaks the one-argument-per-option mapping the framework relies on. +`ARG002` is settled on the same grounds, one argument further in: a method the +framework calls has the signature the framework declares, so an argument the +implementation does not need cannot be dropped - a workflow task with no input +ports never reads `inputs`. Suppress it on the argument, again with the reason +above it. Renaming it to `_inputs` silences ruff as well, and is wrong here: +the name belongs to an API the caller may use by keyword, as this project's own +tests do. + ## Every user visible change gets a changelog entry `CHANGELOG.md` follows [Keep a Changelog](https://keepachangelog.com/). Add an diff --git a/src/{{ '.claude' }}/skills/{% if project_type == 'plugin' %}plugin-implementation{% endif %}/SKILL.md b/src/{{ '.claude' }}/skills/{% if project_type == 'plugin' %}plugin-implementation{% endif %}/SKILL.md index 7897ada..3903efa 100644 --- a/src/{{ '.claude' }}/skills/{% if project_type == 'plugin' %}plugin-implementation{% endif %}/SKILL.md +++ b/src/{{ '.claude' }}/skills/{% if project_type == 'plugin' %}plugin-implementation{% endif %}/SKILL.md @@ -350,6 +350,33 @@ start failing on it without anything in the task changing. Suppress it on the constructor only - it stays a real finding on an ordinary function, which is why it is not in the `ignore` list in `pyproject.toml`. +## An argument the task never reads + +`ARG002` complains about an unused method argument, and a task declaring +`FixedNumberOfInputs([])` generates its own entities - so `execute()` never +reads `inputs`, while `WorkflowPlugin` decides the signature all the same. A +`TransformPlugin` or a `PluginAction` ignoring an argument the framework +supplies is the same case. + +This is the standing exception the lint rules describe, so it needs no fresh +decision either. Suppress the code on the argument, with the reason above it: + +```python +def execute( + self, + # This task has no input ports, so it never reads inputs - but the + # signature is the one WorkflowPlugin declares. + inputs: Sequence[Entities], # noqa: ARG002 + context: ExecutionContext, +) -> Entities: +``` + +Do not rename it to `_inputs` instead. Ruff accepts the underscore and mypy +permits the rename, but the name is part of the API the caller uses: +`execute(inputs=(), context=TestExecutionContext())` is a keyword call, which is +how `tests/test_example.py` and the `plugin-testing` skill invoke a task, and it +raises `TypeError` against a renamed argument. + ## Parameters that carry secrets A password, token or API key is typed, never a plain string: diff --git a/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_workflow.py{% endif %}.jinja b/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_workflow.py{% endif %}.jinja index 4e5acbb..9c5ff5d 100644 --- a/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_workflow.py{% endif %}.jinja +++ b/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_workflow.py{% endif %}.jinja @@ -79,6 +79,8 @@ class DollyPlugin(WorkflowPlugin): def execute( self, + # This task has no input ports, so it never reads inputs - but the + # signature is the one WorkflowPlugin declares. inputs: Sequence[Entities], # noqa: ARG002 context: ExecutionContext, ) -> Entities: From 8a5829dddcf1637329a778ed6d871301d7d09c75 Mon Sep 17 00:00:00 2001 From: Sebastian Tramp Date: Sat, 17 Oct 2026 15:51:23 +0200 Subject: [PATCH 3/3] say the examples are replaced, not removed first, fixes #107 The example docstrings said "Remove this and other example files after bootstrapping your project", which reads as a step of its own. Doing it before the first real module exists leaves a package with no code, and task check then fails three times over - reproduced in a rendered plugin case: DEP002 'cmem-plugin-base' defined as a dependency but not used pytest: no tests ran ... exit status 5 genbadge: ValueError: Computed line rate (0) is different from the one in the file (1.0) None of the three can be answered without a suppression or a Taskfile edit, both of which the shipped rules forbid, so the instruction led somewhere the project is not allowed to leave. All five example files say the working thing instead: replace them with the first real task or module, and delete them in that same change. The report's second suggestion - skipping the coverage badge step on a package with no statements - is declined and recorded under Deliberate decisions in CLAUDE.md: pytest exits 5 before genbadge is reached and deptry is right as well, so silencing the badge alone changes nothing. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013pwym1cFV5jEhi7LdzKmEY --- CHANGELOG.md | 5 +++++ CLAUDE.md | 20 +++++++++++++++++++ ...eneric' %}test_example.py{% endif %}.jinja | 4 +++- ...plugin' %}test_example.py{% endif %}.jinja | 4 +++- ...== 'generic' %}example.py{% endif %}.jinja | 4 +++- ...n' %}example_transform.py{% endif %}.jinja | 4 +++- ...in' %}example_workflow.py{% endif %}.jinja | 4 +++- 7 files changed, 40 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e90d9ec..a4d3172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p per project; `plugin-implementation` says why renaming it to `_inputs` is the wrong answer, and the example plugin now carries the reason above its `# noqa` (#108) +- the example files say to replace them with the first real task in the same + change, rather than to remove them after bootstrapping: an emptied package + fails `task check` three times over - deptry on the now unused + `cmem-plugin-base`, pytest on collecting no tests, and the coverage badge on + a package with no statements (#107) ## [9.9.0] 2026-10-13 diff --git a/CLAUDE.md b/CLAUDE.md index 6c97a99..61e3b5f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -576,3 +576,23 @@ added. A project that wants the check can add a `check:links` task to its own `TaskfileCustom.yaml`, which is exactly what that file is for, and run it on a schedule rather than on every commit — which is the cadence link rot deserves. + +### The coverage badge is not made to tolerate a package with no code + +`check:pytest` ends by generating the coverage badge, and on a package with +zero statements that step fails: +`ValueError: Computed line rate (0) is different from the one in the file +(1.0)` — `coverage.xml` records `line-rate="1.0"` for nothing at all, genbadge +recomputes it as 0 and refuses the file. Report #107 asked for the badge step +to be skipped when the package has no lines, so that an emptied package fails +on deptry alone, which is the check that is actually right. + +Declined, because the state it would rescue is one the template now tells you +not to create. The example files say to replace them with the first real task +in the same change, precisely because a package emptied beforehand has no green +baseline to start from. And the badge is not the first thing to go: `pytest` +exits 5 on having collected no tests before genbadge runs at all, and deptry +reports `DEP002 'cmem-plugin-base' defined as a dependency but not used`. Three +correct signals say the same true thing, so silencing the third one changes +nothing a user would notice — while the shell conditional it needs would have +to hold on the Windows branch of that task too. diff --git a/src/tests/{% if project_type == 'generic' %}test_example.py{% endif %}.jinja b/src/tests/{% if project_type == 'generic' %}test_example.py{% endif %}.jinja index 4e9be1d..e1c0f21 100644 --- a/src/tests/{% if project_type == 'generic' %}test_example.py{% endif %}.jinja +++ b/src/tests/{% if project_type == 'generic' %}test_example.py{% endif %}.jinja @@ -1,6 +1,8 @@ """Example tests. -Remove this and other example files after bootstrapping your project. +Replace these with tests for your first real module and delete the example +files in the same change - a package emptied beforehand fails `task check` on +deptry and on the coverage badge. """ from {{ package_dir }}.example import double diff --git a/src/tests/{% if project_type == 'plugin' %}test_example.py{% endif %}.jinja b/src/tests/{% if project_type == 'plugin' %}test_example.py{% endif %}.jinja index f8c3cc2..b9f9de1 100644 --- a/src/tests/{% if project_type == 'plugin' %}test_example.py{% endif %}.jinja +++ b/src/tests/{% if project_type == 'plugin' %}test_example.py{% endif %}.jinja @@ -1,6 +1,8 @@ """Plugin tests. -Remove this and other example files after bootstrapping your project. +Replace these with tests for your first real task and delete the example +files in the same change - a package emptied beforehand fails `task check` on +deptry and on the coverage badge. """ import os diff --git a/src/{{ package_dir }}/{% if project_type == 'generic' %}example.py{% endif %}.jinja b/src/{{ package_dir }}/{% if project_type == 'generic' %}example.py{% endif %}.jinja index ce9466b..5c2f67b 100644 --- a/src/{{ package_dir }}/{% if project_type == 'generic' %}example.py{% endif %}.jinja +++ b/src/{{ package_dir }}/{% if project_type == 'generic' %}example.py{% endif %}.jinja @@ -1,6 +1,8 @@ """Example module -Remove this and other example files after bootstrapping your project. +Replace this with your first real module and delete the example files in the +same change - a package emptied beforehand fails `task check` on deptry and on +the coverage badge. """ diff --git a/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_transform.py{% endif %}.jinja b/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_transform.py{% endif %}.jinja index f75ed80..f30efab 100644 --- a/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_transform.py{% endif %}.jinja +++ b/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_transform.py{% endif %}.jinja @@ -1,6 +1,8 @@ """lifetime(age) transform plugin module -Remove this and other example files after bootstrapping your project. +Replace this with your first real task and delete the example files in the +same change - a package emptied beforehand fails `task check` on deptry and on +the coverage badge. """ import datetime diff --git a/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_workflow.py{% endif %}.jinja b/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_workflow.py{% endif %}.jinja index 9c5ff5d..c0d668d 100644 --- a/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_workflow.py{% endif %}.jinja +++ b/src/{{ package_dir }}/{% if project_type == 'plugin' %}example_workflow.py{% endif %}.jinja @@ -1,6 +1,8 @@ """Random values workflow plugin module -Remove this and other example files after bootstrapping your project. +Replace this with your first real task and delete the example files in the +same change - a package emptied beforehand fails `task check` on deptry and on +the coverage badge. """ import uuid