claude-dev-env 1.59.0 → 1.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CLAUDE.md +4 -0
  2. package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
  3. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
  4. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +1 -1
  5. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  6. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  7. package/audit-rubrics/prompts/category-e-dead-code.md +17 -4
  8. package/audit-rubrics/prompts/category-f-silent-failures.md +1 -0
  9. package/docs/CODE_RULES.md +2 -2
  10. package/hooks/blocking/code_rules_annotations_length.py +189 -10
  11. package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
  12. package/hooks/blocking/code_rules_duplicate_body.py +152 -0
  13. package/hooks/blocking/code_rules_enforcer.py +38 -15
  14. package/hooks/blocking/code_rules_orphan_css_class.py +196 -0
  15. package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
  16. package/hooks/blocking/config/__init__.py +5 -0
  17. package/hooks/blocking/config/verified_commit_constants.py +118 -0
  18. package/hooks/blocking/destructive_command_blocker.py +483 -61
  19. package/hooks/blocking/test_code_rules_enforcer_annotations.py +240 -0
  20. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  21. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
  22. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
  23. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +82 -0
  24. package/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py +196 -0
  25. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
  26. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
  27. package/hooks/blocking/test_destructive_command_blocker.py +213 -0
  28. package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
  29. package/hooks/blocking/test_verification_verdict_store.py +490 -0
  30. package/hooks/blocking/test_verified_commit_gate.py +495 -0
  31. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
  32. package/hooks/blocking/test_verifier_verdict_minter.py +193 -0
  33. package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
  34. package/hooks/blocking/verification_verdict_store.py +686 -0
  35. package/hooks/blocking/verified_commit_gate.py +535 -0
  36. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
  37. package/hooks/blocking/verifier_verdict_minter.py +221 -0
  38. package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
  39. package/hooks/hooks.json +43 -1
  40. package/hooks/hooks_constants/blocking_check_limits.py +1 -0
  41. package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
  42. package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
  43. package/hooks/hooks_constants/destructive_command_segment_constants.py +15 -0
  44. package/hooks/hooks_constants/duplicate_function_body_constants.py +22 -5
  45. package/hooks/hooks_constants/orphan_css_class_constants.py +40 -0
  46. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
  47. package/hooks/validation/mypy_validator.py +59 -7
  48. package/hooks/validation/test_mypy_validator.py +94 -0
  49. package/package.json +1 -1
  50. package/rules/file-global-constants.md +7 -1
  51. package/rules/no-cross-skill-duplicate-helpers.md +29 -0
  52. package/rules/orphan-css-class.md +23 -0
  53. package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
  54. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
  55. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
  56. package/skills/autoconverge/SKILL.md +54 -17
  57. package/skills/autoconverge/reference/closing-report.md +59 -17
  58. package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
  59. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +192 -76
  60. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
  61. package/skills/autoconverge/workflow/converge.contract.test.mjs +395 -206
  62. package/skills/autoconverge/workflow/converge.mjs +520 -57
  63. package/skills/autoconverge/workflow/convergence_summary.py +110 -0
  64. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
  65. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
  66. package/skills/autoconverge/workflow/render_report.py +488 -397
  67. package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
  68. package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
  69. package/skills/autoconverge/workflow/test_render_report.py +518 -259
  70. package/skills/pr-converge/reference/per-tick.md +28 -8
  71. package/skills/rebase/SKILL.md +2 -4
  72. package/system-prompts/software-engineer.xml +2 -6
  73. package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
  74. package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
  75. package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
  76. package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
  77. package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
  78. package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
  79. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
  80. package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
  81. package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
package/CLAUDE.md CHANGED
@@ -57,6 +57,10 @@ Repair agents run only on reported findings; the verifier re-checks after each r
57
57
  - **Tight edit scope:** Edit exactly what the task names — no whole-file rewrites, no renaming public method parameters, no changes beyond the stated task. When the user asks for a "lasting" or "reusable" fix, prefer the durable systemic fix over a one-off edit. When the task touches a pipeline, generator, or other repeated process, fix the process itself, not its individual outputs — even when the request does not say so; for one-off targets, a scoped patch remains the default.
58
58
  - **GitHub MCP first:** The GitHub MCP (`mcp__plugin_github_github__*`) is the primary path for PR and review-thread inspection; raw `gh api` is the fallback, not the default — MCP calls work the same from any worktree.
59
59
 
60
+ ## Destructive-command literals in Bash
61
+
62
+ Never put a destructive-command literal (`rm -rf`, `git reset --hard`, `dd`, `mkfs`) inside a Bash command string, even when the shell never runs it — a quoted `python -c` argument, a heredoc body, an echoed string, a commit or PR body. The `destructive_command_blocker` hook matches the raw text and asks for confirmation, which a background run cannot answer, so the call stalls. Run hook and deletion checks through the committed test suite (`python -m pytest <test_file>`), or a throwaway script under `$CLAUDE_JOB_DIR/tmp` run as `python <file>.py` — either way the command string carries no destructive text, so the hook stays silent. Group genuine cleanup deletions into one teardown step. See `~/.claude/rules/no-inline-destructive-literals.md`.
63
+
60
64
  ## Sub-agent Output Validation
61
65
 
62
66
  After any sub-agent returns a PR description, file list, or counts, verify each claim against the actual diff and repo state before using it. Flag and correct any invented paths, fabricated counts, or out-of-scope changes before they land in commits or PR bodies.
@@ -22,7 +22,7 @@
22
22
  | B3 | Regex syntax vs engine flavor | Lookbehind / lookahead support; named groups (`(?P<…>)` vs `(?<…>)`); backreferences; Unicode character classes. |
23
23
  | B4 | Shell / CLI / cmdlet syntax vs runtime version | PowerShell 5.1 vs 7+; bash 3 vs 5; cmdlet parameters added in later versions; CLI flag deprecations. |
24
24
  | B5 | JSON path / XPath / structural query vs library | jq vs Python jsonpath-ng vs JavaScript jsonpath syntax; XPath 1.0 vs 2.0/3.0 functions. |
25
- | B6 | Search query DSL vs engine | Lucene / Elasticsearch / Zoekt / OpenSearch syntax; differences in escaping, fuzzy matching, multi-field queries. |
25
+ | B6 | Search query DSL vs engine | Lucene / Elasticsearch / OpenSearch syntax; differences in escaping, fuzzy matching, multi-field queries. |
26
26
  | B7 | ORM vs raw SQL semantic differences | SQLAlchemy `.filter()` vs `.filter_by()`; Django Q expressions vs raw SQL; lazy vs eager evaluation. |
27
27
 
28
28
  Use 5–10 sub-buckets for any single audit. For an audit that doesn't touch SQL or web frontends, drop B1 / B2 entirely and split B4 across the relevant runtimes.
@@ -26,6 +26,7 @@
26
26
  | E6 | Removed-but-not-deleted symbol references | Symbols renamed/removed elsewhere with stale import or call sites left behind. |
27
27
  | E7 | Test fixtures / helpers defined but never used | Pytest fixtures, test data builders, mock factories with no callers. |
28
28
  | E8 | Stub / placeholder code without TODO | `pass`, `...`, `raise NotImplementedError` left without explanation or tracking. |
29
+ | E9 | Constants-module exports with no importer | A module-level `UPPER_SNAKE` constant added to a `*_constants.py` / `config/` module that no module in the repo imports and that the constants file itself never references. The file-global use-count gate exempts a constants module because every name it exports legitimately carries zero in-file references, so a genuinely dead export slips past the write-time gate. Distinguish dead from live by grepping the whole repo for each constant name: a sibling such as `MEDIUM_TERMINAL` imported by a consumer module is live; a `MEDIUM_TEXT` that no `from ... import` line and no in-file reference names is dead (CODE_RULES 9.8). Remove the dead export. |
29
30
 
30
31
  ---
31
32
 
@@ -20,7 +20,7 @@
20
20
  |---|---|---|
21
21
  | F1 | Catch-all except clauses | `except:` (bare), `except Exception:`, `except BaseException:` followed by `pass` / `continue` / log-only. |
22
22
  | F2 | Errors logged then swallowed | `logger.error(...)` followed by `return None` / `return default` without re-raise. |
23
- | F3 | Default fallback values masking failure | `dict.get(key, default)` where the absence of the key is itself a bug; `or default` short-circuits hiding `None`. |
23
+ | F3 | Default fallback values masking failure | `dict.get(key, default)` where the absence of the key is itself a bug; `or default` short-circuits hiding `None`. Includes the stale-payload-key shape: a `payload.get("KEY", "")` (or `payload["KEY"]` wrapped in a fallback) read against an external-input dict whose contract the diff migrated — the rest of the module reads the payload through a different key set named in the docstring and bound to same-named variables, while this lone read targets a dropped key, so it resolves to the default on every real payload and silently records an empty value into the field it feeds. The audit teammate lists every string-literal key read from each `*_payload` / event / request dict, checks each key against the payload contract the module's docstring and other reads set up, and flags a key read at one site that no docstring, second read, or same-named binding anchors. A key consumed inline with a meaningful default (`resolve(payload.get("cwd", "."))`) is legitimate, not stale — the flag is the dropped key whose value the field needs but never receives. |
24
24
  | F4 | Async task error swallowing | `asyncio.create_task(...)` without exception observation; `gather(..., return_exceptions=True)` consumed loosely. |
25
25
  | F5 | Boolean / status returns identical on success and failure | A function returns `True` on the happy path and `True` on the catch-all error path. |
26
26
  | F6 | Ignored return values from fallible calls | `subprocess.run` without `check=True` and unchecked `returncode`; `os.write` return value discarded. |
@@ -25,7 +25,7 @@ Decomposition is by the **kind of docstring claim** that needs to be cross-check
25
25
  | O3 | Predicate-name and -docstring vs body breadth | A boolean helper's name and docstring promise a narrow predicate. Walk the body's branches: every branch's `return True` path is consistent with the promised name. Bodies that accept inputs broader than the name (`_dir_value_resolves_to_shared_temp` also accepting HOME/TMP env-derived paths) are O3 findings. |
26
26
  | O4 | Step-ordering narrative | A docstring describes processing as `A then B then C`. Walk the body and confirm the call order matches. Mismatched order is an O4 finding regardless of whether the final output is the same. |
27
27
  | O5 | Named-sentinel / filename references | A docstring names a sentinel marker, environment variable, filename, or magic string. Confirm the named token actually exists in the module body or in the repo's naming convention. |
28
- | O6 | Free-form `Args:`-adjacent claims | A docstring's `Returns:` / `Raises:` / `Note:` / `Example:` sections make claims (`returns shared-temp only`, `raises ValueError on missing key`). Verify each claim against the body. When a docstring enumerates the inputs a body counts (a "field counts as read when ..." list, a list of conditions treated as a match, a list of cases the body skips), list every union member and every suppressor the body applies (`read_names = a | b | c`, each early-return guard) and confirm each appears in the prose enumeration. A union member or suppressor the body applies but the prose omits is an O6 finding. See `../../rules/docstring-prose-matches-implementation.md`. |
28
+ | O6 | Free-form `Args:`-adjacent claims | A docstring's `Returns:` / `Raises:` / `Note:` / `Example:` sections make claims (`returns shared-temp only`, `raises ValueError on missing key`). Verify each claim against the body. When a docstring enumerates the inputs a body counts (a "field counts as read when ..." list, a list of conditions treated as a match, a list of cases the body skips), list every union member and every suppressor the body applies (`read_names = a | b | c`, each early-return guard) and confirm each appears in the prose enumeration. A union member or suppressor the body applies but the prose omits is an O6 finding. A `Returns:` that names the mechanism, tool, or output format the function produces (`instructing a StructuredOutput summary`, `returns a YAML document`, `emits a JSON object`) matches the artifact the body actually builds: a prompt body that asks the agent to "Return strictly a JSON object" while the docstring claims it "instruct[s] a StructuredOutput" summary is an O6 finding, because the named tool appears nowhere in the emitted text. See `../../rules/docstring-prose-matches-implementation.md`. |
29
29
  | O7 | Module-doc-vs-split-module after refactor | When a refactor moves a responsibility to a sibling module, the originating module's docstring and the receiving module's docstring both describe the home of that responsibility. A module docstring should describe only the responsibilities it owns. |
30
30
 
31
31
  ---
@@ -52,7 +52,7 @@ ID prefix: `find`.
52
52
  **B6. Search query DSL vs engine**
53
53
  - Every Lucene/Elasticsearch query string — verify field syntax (`field:value`), required/excluded operators (`+`, `-`), fuzzy (`term~2`), proximity (`"a b"~5`), and wildcard rules (`*`, `?`) match the engine version's parser.
54
54
  - Every Elasticsearch query DSL object (`match`, `bool`, `should`, `must`, `filter`, `term`, `terms`) — verify removed/renamed clauses across major versions (e.g. `query_string` defaults, `term` vs `match` for `text` fields, mapping-type removal in ES 7+).
55
- - Every Zoekt / Sourcegraph / OpenSearch / Solr query — verify dialect-specific operators and that the deployment has the relevant features enabled (e.g. ES `query_string` may be disabled for security).
55
+ - Every Sourcegraph / OpenSearch / Solr query — verify dialect-specific operators and that the deployment has the relevant features enabled (e.g. ES `query_string` may be disabled for security).
56
56
  - Every escaping rule for special characters in the DSL (`+ - && || ! ( ) { } [ ] ^ " ~ * ? : \ /`) — verify the producer escapes them before handing to the engine; flag any user-supplied input concatenated raw.
57
57
  - Every analyzer assumption (whitespace, standard, keyword, ngram) — verify the index mapping matches what the query string assumes.
58
58
 
@@ -375,7 +375,7 @@ Write-Host "$TaskName registered — runs every ${IntervalMinutes}min against '$
375
375
  - Probe B5.c: confirm no JSON-pointer (`/foo/bar`) string literals, no JsonPath-style `$.foo[?(@.bar)]` patterns, no XPath `/html/body//div[@class='x']` patterns in any string in the four files. Walk every f-string and string literal.
376
376
 
377
377
  **B6. Search query DSL vs engine**
378
- - The four PR #394 files contain no search-engine queries, no Lucene/Elasticsearch/Zoekt/OpenSearch DSL.
378
+ - The four PR #394 files contain no search-engine queries, no Lucene/Elasticsearch/OpenSearch DSL.
379
379
  - Shape B proof-of-absence expected. Adversarial probes must each verify a distinct search-DSL dimension:
380
380
  - Probe B6.a: confirm no HTTP calls to `/_search`, `/_msearch`, `/_count`, `/_analyze` endpoints — `sweep_empty_dirs.py` does not import `requests`, `urllib`, `httpx`, `aiohttp`. Pure stdlib + local config.
381
381
  - Probe B6.b: confirm no Lucene-syntax fragments — no `field:value`, no `+required -excluded`, no fuzzy `term~2`, no proximity `"a b"~5`. The only colon-bearing literals in the diff are PowerShell hash separators (`$($action.Execute) $($action.Arguments)` at `Install-SweepEmptyDirs.ps1:31`) and the time literal `"00:00"` at line 71 — neither is a search-DSL fragment.
@@ -1,4 +1,4 @@
1
- Audit [REPO/ARTIFACT] [TARGET_ID] for **Category E only** (dead code and unused imports). Skip A–D, F–P. Sub-bucket forced-exhaustion mode: Category E is decomposed into 8 sub-buckets below. Each sub-bucket REQUIRES at least one Shape A finding OR exactly one Shape B proof-of-absence with **at least 3 adversarial probes** specific to that sub-bucket. A sub-bucket returning neither is a protocol gap.
1
+ Audit [REPO/ARTIFACT] [TARGET_ID] for **Category E only** (dead code and unused imports). Skip A–D, F–P. Sub-bucket forced-exhaustion mode: Category E is decomposed into 9 sub-buckets below. Each sub-bucket REQUIRES at least one Shape A finding OR exactly one Shape B proof-of-absence with **at least 3 adversarial probes** specific to that sub-bucket. A sub-bucket returning neither is a protocol gap.
2
2
 
3
3
  [ARTIFACT METADATA]
4
4
  - Repo / artifact: [REPO_OR_ARTIFACT_NAME]
@@ -69,6 +69,13 @@ Inline the artifact under this section using the section types defined in the ch
69
69
  - Scaffolding bodies (`pass`, `...`, `raise NotImplementedError`, empty `else { }`, single-statement `return None` placeholders) without a `# TODO` comment ARE Category E findings under the project's "Document Temporary Code" rule.
70
70
  - Adversarial probes for proof-of-absence: (a) any empty brace block in PowerShell / TypeScript / Go (`{ }` with no statements)? (b) any function whose entire body is `pass` / `return` / `return None`? (c) any branch that exits cleanly only because the surrounding loop is no-op for an empty input — is the no-op intentional or a placeholder?
71
71
 
72
+ **E9. Constants-module exports with no importer**
73
+ - For every module-level `UPPER_SNAKE` constant the artifact adds to a `*_constants.py` or `config/` module, grep the whole repo for the constant name and locate at least one importer (`from <module> import <NAME>`) or in-file reference.
74
+ - The file-global use-count gate exempts a constants module because every name it exports carries zero in-file references by design, so a genuinely dead export slips past the write-time gate; this sub-bucket is the audit-time backstop for that exemption.
75
+ - A sibling that a consumer module imports is live; a constant that no `from ... import` line and no in-file reference names anywhere in the repo is dead and must be removed (CODE_RULES 9.8).
76
+ - Constants reached only by string-form lookup (`getattr(config, name)`, settings registries) are live; name the dynamic consumer when you mark such a constant referenced.
77
+ - Adversarial probes for proof-of-absence: (a) does the artifact add any constant to a `*_constants.py` / `config/` module whose name returns zero hits outside its own definition line? (b) is any newly added constant shadowed by a same-named constant in a sibling module so the importer resolves the other one? (c) does any constant exist only as an `__all__` re-export with no downstream importer of that re-export?
78
+
72
79
  ## Cross-bucket questions to answer at the end
73
80
 
74
81
  Q1: Are there imports unused locally but consumed by a re-export pattern in another file? Cite the cross-file pair if found, or state the hypothesis "none — neither file declares `__all__`" with the supporting evidence.
@@ -79,7 +86,7 @@ Q3: Which symbol most likely will *become* dead code after a near-future refacto
79
86
 
80
87
  ## Output
81
88
 
82
- Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket E1-E8, produce Shape A or Shape B (with ≥3 probes). Cross-bucket Q1-Q3 answers after the per-sub-bucket walk. Adversarial second pass: "assume your first pass missed at least 3 P2 dead-code instances across these 8 sub-buckets — find them." Open Questions section for ambiguities. Read-only. No edits, no commits.
89
+ Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket E1-E9, produce Shape A or Shape B (with ≥3 probes). Cross-bucket Q1-Q3 answers after the per-sub-bucket walk. Adversarial second pass: "assume your first pass missed at least 3 P2 dead-code instances across these 9 sub-buckets — find them." Open Questions section for ambiguities. Read-only. No edits, no commits.
83
90
 
84
91
  Note: most Category E findings are P2 (style / cleanup) unless the dead code masks an actual bug; the adversarial-pass quota uses P2 here.
85
92
 
@@ -87,7 +94,7 @@ Note: most Category E findings are P2 (style / cleanup) unless the dead code mas
87
94
 
88
95
  # Worked example: jl-cmd/claude-code-config PR #394
89
96
 
90
- Audit jl-cmd/claude-code-config PR #394 for **Category E only** (dead code and unused imports). Skip A–D, F–N. Sub-bucket forced-exhaustion mode: Category E is decomposed into 8 sub-buckets below. Each sub-bucket REQUIRES at least one Shape A finding OR exactly one Shape B proof-of-absence with **at least 3 adversarial probes** specific to that sub-bucket. A sub-bucket returning neither is a protocol gap.
97
+ Audit jl-cmd/claude-code-config PR #394 for **Category E only** (dead code and unused imports). Skip A–D, F–N. Sub-bucket forced-exhaustion mode: Category E is decomposed into 9 sub-buckets below. Each sub-bucket REQUIRES at least one Shape A finding OR exactly one Shape B proof-of-absence with **at least 3 adversarial probes** specific to that sub-bucket. A sub-bucket returning neither is a protocol gap.
91
98
 
92
99
  PR: feat(scripts): add sweep-empty-dirs utility and scheduled-task installer
93
100
  Head SHA: 62c9c169ee7a44824e5da25c4cf8b74fdca08a53
@@ -161,6 +168,12 @@ ID prefix: `find`.
161
168
  - No `# TODO` markers in the diff — the project's own rule (`code-standards.md` → "Document Temporary Code") requires TODOs only for scaffolding/placeholder code. The two `pass`/`continue` bodies above are production behavior, not scaffolding.
162
169
  - Adversarial probes for proof-of-absence: (a) does the PowerShell script have an empty `else { }` or empty branch body? — scan lines 14-71 for any `{ }` with no statements between the braces. (b) does any function body consist of a single `pass` or `return` with no work done? — every function body in this PR performs at least one statement. (c) does the `Status` branch (lines 14-31) exit cleanly even when `$task.Triggers` is empty? — the `foreach` loop at line 26 is a no-op for an empty collection, which is correct behavior, not a stub.
163
170
 
171
+ **E9. Constants-module exports with no importer**
172
+ - `config/sweep_config.py` is the only constants module this PR adds; it declares `DEFAULT_AGE_SECONDS` and `DEFAULT_POLL_INTERVAL` and imports nothing.
173
+ - `DEFAULT_AGE_SECONDS` — imported by `sweep_empty_dirs.py` line 10 (`from config.sweep_config import DEFAULT_AGE_SECONDS`) and read in `_build_parser`'s `--age` default. Live.
174
+ - `DEFAULT_POLL_INTERVAL` — imported by `sweep_empty_dirs.py` line 11 and read in `_build_parser`'s `--interval` default. Live.
175
+ - Adversarial probes for proof-of-absence: (a) does either constant return zero importers when grepped across the repo? — each has exactly one importer (`sweep_empty_dirs.py`), so neither is dead. (b) is either name shadowed by a same-named constant in a sibling module? — `sweep_config.py` is the only module that declares them. (c) does either constant exist only as an `__all__` re-export with no downstream consumer? — `sweep_config.py` declares no `__all__`; both are imported directly.
176
+
164
177
  ## Cross-bucket questions to answer at the end
165
178
 
166
179
  Q1: Are there imports unused locally but consumed by a re-export pattern in another file? Cite the cross-file pair if found. (Hypothesis: none — neither `sweep_empty_dirs.py` nor `test_sweep_empty_dirs.py` defines `__all__`, so re-export is not in play. `config/sweep_config.py` declares two constants that ARE consumed by `sweep_empty_dirs.py` lines 10-11; this is normal cross-file consumption, not a re-export.)
@@ -169,7 +182,7 @@ Q3: Which symbol most likely will *become* dead code after a near-future refacto
169
182
 
170
183
  ## Output
171
184
 
172
- Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket E1-E8, produce Shape A or Shape B (with ≥3 probes). Cross-bucket Q1-Q3 answers after the per-sub-bucket walk. Adversarial second pass: "assume your first pass missed at least 3 P2 dead-code instances across these 8 sub-buckets — find them." Open Questions section for ambiguities. Read-only. No edits, no commits.
185
+ Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket E1-E9, produce Shape A or Shape B (with ≥3 probes). Cross-bucket Q1-Q3 answers after the per-sub-bucket walk. Adversarial second pass: "assume your first pass missed at least 3 P2 dead-code instances across these 9 sub-buckets — find them." Open Questions section for ambiguities. Read-only. No edits, no commits.
173
186
 
174
187
  Note: most Category E findings are P2 (style / cleanup) unless the dead code masks an actual bug; the adversarial-pass quota uses P2 here.
175
188
 
@@ -37,6 +37,7 @@ Repeat for every section in scope.
37
37
 
38
38
  **F3. Default fallback values masking failure**
39
39
  - `dict.get(key, default)` where the absence of the key is itself a bug.
40
+ - Stale-payload-key shape: for each external-input dict (`*_payload`, event, request, parsed-JSON body), list every string-literal key read from it. Check each key against the contract the module's docstring and its other reads set up — a payload whose other reads bind to same-named variables and whose docstring names a key set is an established contract. Flag a lone read of a key outside that contract: it resolves to the `.get` default on every real payload and silently records an empty value into the field it feeds. A key consumed inline with a meaningful default (`resolve(payload.get("cwd", "."))`) is legitimate; the flag is the dropped key whose value the consuming field needs but never receives.
40
41
  - `or default` short-circuits hiding `None` returns from fallible calls.
41
42
  - `getattr(obj, attr, default)` masking `AttributeError` from the wrong object type.
42
43
  - argparse `default=...` for values that should fail-loud when absent.
@@ -23,9 +23,9 @@ Compact reference for agents. ⚡ marks rules enforced by `code_rules_enforcer.p
23
23
 
24
24
  `code_rules_enforcer.py` blocks each of these at Write/Edit and explains the specific violation when it fires; exact patterns and exemption lists live in the hook:
25
25
 
26
- no new comments · imports at top · logging format args (`log_*("...", arg)`) · no magic values in production bodies (0, 1, -1 exempt) · UPPER_SNAKE constants only in `config/` (exempt: `config/*`, `/migrations/`, workflow registries `/workflow/` + `_tab.py` + `/states.py` + `/modules.py`, test files) · no hardcoded user home paths · guarded `sys.path.insert` · no unused module-level imports · banned identifiers (`ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `tmp`, `elem`, `val`) · banned function prefixes (`handle_`, `process_`, `manage_`, `do_`) · no type escape hatches (`Any` import, `cast()`, inline `Any`) outside boundary files · no bare/broad `except` · no `Any` in signatures or class attributes · no stub bodies (`pass`/`...`/`raise NotImplementedError`) outside abstract/Protocol · TypedDict `_encode_*`/`_decode_*` companions in the same module · no test-mode branching in production (use dependency injection) · no thin wrapper modules · Google-style docstrings on public functions with `Args:` matching the signature · boolean names prefixed `is_`/`has_`/`should_`/`can_`/`was_`/`did_` (assignments AND bool-typed parameters) · must-check returns (`find_and_click`, `write_outcome`) assigned and checked · known pytest fixture parameters in test files annotated with their single documented type (`tmp_path: Path`, `monkeypatch: pytest.MonkeyPatch`, `capsys`, `caplog`, `request`, …)
26
+ no new comments · imports at top · logging format args (`log_*("...", arg)`) · no magic values in production bodies (0, 1, -1 exempt) · UPPER_SNAKE constants only in `config/` (exempt: `config/*`, `/migrations/`, workflow registries `/workflow/` + `_tab.py` + `/states.py` + `/modules.py`, test files) · no hardcoded user home paths · guarded `sys.path.insert` · no unused module-level imports · banned identifiers (`ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `tmp`, `elem`, `val`) · banned function prefixes (`handle_`, `process_`, `manage_`, `do_`) · no type escape hatches (`Any` import, `cast()`, inline `Any`) outside boundary files · no bare/broad `except` · no `Any` in signatures or class attributes · no stub bodies (`pass`/`...`/`raise NotImplementedError`) outside abstract/Protocol · TypedDict `_encode_*`/`_decode_*` companions in the same module · no test-mode branching in production (use dependency injection) · no thin wrapper modules · Google-style docstrings on public functions with `Args:` matching the signature · boolean names prefixed `is_`/`has_`/`should_`/`can_`/`was_`/`did_` (assignments AND bool-typed parameters) · must-check returns (`find_and_click`, `write_outcome`) assigned and checked · known pytest fixture parameters in test files annotated with their single documented type (`tmp_path: Path`, `monkeypatch: pytest.MonkeyPatch`, `capsys`, `caplog`, `request`, …) · known pytest fixture parameters a test function declares but never references (drop the unused parameter — pytest still pays its setup cost)
27
27
 
28
- Test files are exempt from most checks. The one annotation the test-file exemption does NOT cover is a known pytest builtin fixture parameter: `tmp_path`, `monkeypatch`, `capsys`, `capfd`, `caplog`, `request`, and `tmp_path_factory` each have a single documented injected type, so the gate requires that annotation (`tmp_path: Path`) even inside a test file. Ordinary test parameters stay exempt. See also the file-global constants use-count rule: [`rules/file-global-constants.md`](../rules/file-global-constants.md).
28
+ Test files are exempt from most checks. The one annotation the test-file exemption does NOT cover is a known pytest builtin fixture parameter: `tmp_path`, `monkeypatch`, `capsys`, `capfd`, `caplog`, `request`, and `tmp_path_factory` each have a single documented injected type, so the gate requires that annotation (`tmp_path: Path`) even inside a test file. The same set of fixtures is also subject to a use check: a pytest-collected test function that declares one of these parameters and never references it in its body fails the gate, because pytest materializes the fixture's setup (the temp directory, the monkeypatch context, the output capture) on every run whether or not the body reads the value — drop the unused parameter. A parameter counts as referenced when its name is read, augmented-assigned, or deleted anywhere in the body, including inside a nested function or comprehension. Only pytest-collectable functions are inspected — those at module top level or defined directly in a class body; a function nested inside another function's body is a local helper pytest never collects, so its fixture-named parameter is exempt. A `@pytest.fixture`-decorated function is exempt from the use check, since injecting one fixture into another purely to order its setup is intentional. Ordinary test parameters stay exempt from both checks. See also the file-global constants use-count rule: [`rules/file-global-constants.md`](../rules/file-global-constants.md).
29
29
 
30
30
  ---
31
31
 
@@ -29,6 +29,7 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
29
29
  FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX,
30
30
  FUNCTION_LENGTH_BLOCKING_THRESHOLD,
31
31
  KNOWN_PYTEST_FIXTURE_ANNOTATION_MESSAGE_SUFFIX,
32
+ UNUSED_PYTEST_FIXTURE_PARAMETER_MESSAGE_SUFFIX,
32
33
  )
33
34
 
34
35
 
@@ -55,6 +56,36 @@ def check_parameter_annotations(content: str, file_path: str) -> list[str]:
55
56
  return issues
56
57
 
57
58
 
59
+ def _has_pytest_fixture_decorator(
60
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
61
+ ) -> bool:
62
+ """Return True when a function carries an ``@pytest.fixture`` decorator.
63
+
64
+ The decorator is recognized whether it is written as a dotted
65
+ ``@pytest.fixture`` attribute or a bare ``@fixture`` name, and whether or not
66
+ it is called with arguments (``@pytest.fixture(scope="module")``). The call
67
+ form is unwrapped to its callee before the name is matched.
68
+
69
+ Args:
70
+ node: The function definition AST node to inspect.
71
+
72
+ Returns:
73
+ True when any decorator on the node is a call or bare reference whose
74
+ final name is ``fixture``; False otherwise.
75
+ """
76
+ for each_decorator in node.decorator_list:
77
+ unwrapped = (
78
+ each_decorator.func
79
+ if isinstance(each_decorator, ast.Call)
80
+ else each_decorator
81
+ )
82
+ if isinstance(unwrapped, ast.Name) and unwrapped.id == "fixture":
83
+ return True
84
+ if isinstance(unwrapped, ast.Attribute) and unwrapped.attr == "fixture":
85
+ return True
86
+ return False
87
+
88
+
58
89
  def _is_pytest_fixture_injection_site(
59
90
  node: ast.FunctionDef | ast.AsyncFunctionDef,
60
91
  ) -> bool:
@@ -77,13 +108,7 @@ def _is_pytest_fixture_injection_site(
77
108
  """
78
109
  if node.name.startswith("test"):
79
110
  return True
80
- for each_decorator in node.decorator_list:
81
- unwrapped = each_decorator.func if isinstance(each_decorator, ast.Call) else each_decorator
82
- if isinstance(unwrapped, ast.Name) and unwrapped.id == "fixture":
83
- return True
84
- if isinstance(unwrapped, ast.Attribute) and unwrapped.attr == "fixture":
85
- return True
86
- return False
111
+ return _has_pytest_fixture_decorator(node)
87
112
 
88
113
 
89
114
  def _normalize_fixture_annotation_text(annotation_text: str) -> str:
@@ -159,6 +184,11 @@ def check_known_pytest_fixture_annotations(content: str, file_path: str) -> list
159
184
  fixture; and a ``*args`` or ``**kwargs`` parameter that happens to share a
160
185
  fixture name is never a fixture injection.
161
186
 
187
+ Only pytest-collectable functions are inspected: functions at module top
188
+ level and methods defined directly in a class body. A fixture-named
189
+ parameter on a function nested inside another function's body is exempt,
190
+ because pytest never injects a fixture into a function-nested definition.
191
+
162
192
  Args:
163
193
  content: The Python source to analyze.
164
194
  file_path: The path of the file being checked.
@@ -177,9 +207,7 @@ def check_known_pytest_fixture_annotations(content: str, file_path: str) -> list
177
207
  except SyntaxError:
178
208
  return []
179
209
  issues: list[str] = []
180
- for each_node in ast.walk(tree):
181
- if not isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
182
- continue
210
+ for each_node in _collect_pytest_collectable_functions(tree):
183
211
  if not _is_pytest_fixture_injection_site(each_node):
184
212
  continue
185
213
  for each_arg in _collect_fixture_injection_arguments(each_node):
@@ -205,6 +233,157 @@ def check_known_pytest_fixture_annotations(content: str, file_path: str) -> list
205
233
  return issues
206
234
 
207
235
 
236
+ def _names_referenced_in_subtree(node: ast.AST) -> set[str]:
237
+ """Return every identifier referenced anywhere within an AST subtree.
238
+
239
+ A name counts as referenced when it is read (an ``ast.Name`` in load
240
+ context), deleted (an ``ast.Name`` in delete context), or augmented-assigned
241
+ (the ``ast.Name`` target of an ``ast.AugAssign``, which reads the prior value
242
+ before storing). The walk reaches into nested function and comprehension
243
+ bodies, so a parameter referenced only inside an inner function still counts.
244
+ A name appearing solely as a plain assignment target — ``ast.Store`` context
245
+ without augmentation — is absent, because rebinding the name without reading
246
+ it leaves the fixture's setup genuinely unused.
247
+
248
+ Args:
249
+ node: The AST node whose subtree is scanned for referenced identifiers.
250
+
251
+ Returns:
252
+ The set of identifier strings read, deleted, or augmented-assigned at
253
+ least once within the subtree.
254
+ """
255
+ referenced_names: set[str] = set()
256
+ for each_descendant in ast.walk(node):
257
+ if isinstance(each_descendant, ast.Name) and isinstance(
258
+ each_descendant.ctx, (ast.Load, ast.Del)
259
+ ):
260
+ referenced_names.add(each_descendant.id)
261
+ if isinstance(each_descendant, ast.AugAssign) and isinstance(
262
+ each_descendant.target, ast.Name
263
+ ):
264
+ referenced_names.add(each_descendant.target.id)
265
+ return referenced_names
266
+
267
+
268
+ def _is_pytest_test_function(
269
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
270
+ ) -> bool:
271
+ """Return True when a function is a pytest-collected test function.
272
+
273
+ A test function is one whose name begins with the ``test`` prefix, matching
274
+ pytest's default ``python_functions = test*`` collection rule. A
275
+ ``@pytest.fixture``-decorated function is deliberately excluded regardless of
276
+ its name: a fixture that injects another fixture only to compose its setup,
277
+ without reading the value, is an intentional pattern this check must not flag.
278
+ The decorator is recognized written as ``@pytest.fixture`` or a bare
279
+ ``@fixture``, with or without call arguments.
280
+
281
+ Args:
282
+ node: The function definition AST node to inspect.
283
+
284
+ Returns:
285
+ True when the node's name begins with ``test`` and the node carries no
286
+ ``@pytest.fixture`` decorator; False otherwise.
287
+ """
288
+ if _has_pytest_fixture_decorator(node):
289
+ return False
290
+ return node.name.startswith("test")
291
+
292
+
293
+ def _collect_pytest_collectable_functions(
294
+ tree: ast.AST,
295
+ ) -> "list[ast.FunctionDef | ast.AsyncFunctionDef]":
296
+ """Return the function nodes pytest can collect as tests, in source order.
297
+
298
+ pytest collects a test function only at module top level or as a method
299
+ defined directly in a class body; a function nested inside another
300
+ function's body is never collected, so its parameters are ordinary local
301
+ arguments rather than injected fixtures. The walk descends through the
302
+ module body and through class bodies (including nested classes, so methods
303
+ of a nested class are reached), collects each ``FunctionDef`` /
304
+ ``AsyncFunctionDef`` it encounters, and never descends into a function's own
305
+ body — function-nested definitions are excluded.
306
+
307
+ Args:
308
+ tree: The parsed module (or any node whose body holds the candidates).
309
+
310
+ Returns:
311
+ Module-level and class-method function definitions in source order;
312
+ never a function-nested definition.
313
+ """
314
+ collectable_functions: list[ast.FunctionDef | ast.AsyncFunctionDef] = []
315
+ for each_statement in getattr(tree, "body", []):
316
+ if isinstance(each_statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
317
+ collectable_functions.append(each_statement)
318
+ continue
319
+ if isinstance(each_statement, ast.ClassDef):
320
+ collectable_functions.extend(
321
+ _collect_pytest_collectable_functions(each_statement)
322
+ )
323
+ return collectable_functions
324
+
325
+
326
+ def check_unused_known_pytest_fixture_parameters(
327
+ content: str, file_path: str
328
+ ) -> list[str]:
329
+ """Flag well-known pytest fixture parameters a test declares but never reads.
330
+
331
+ A pytest test function that names a builtin fixture from
332
+ ``ANNOTATION_BY_PYTEST_FIXTURE`` — ``tmp_path``, ``monkeypatch``,
333
+ ``capsys``, and the rest — pays the fixture's setup cost on every run:
334
+ pytest materializes the temp directory, installs the monkeypatch context,
335
+ or captures output even when the body never touches the value. A parameter
336
+ the body never references is therefore dead weight, and most often a
337
+ copy-paste remnant from a sibling test that did use it. This check flags
338
+ each such parameter so the author drops it.
339
+
340
+ Only pytest-collected test functions are inspected: functions at module top
341
+ level and methods defined directly in a class body. A function nested inside
342
+ another function's body is excluded — pytest never collects it, so its
343
+ fixture-named parameter is an ordinary local argument. A
344
+ ``@pytest.fixture``-decorated function is exempt because injecting a fixture
345
+ into another fixture purely to order its setup is an intentional pattern. A
346
+ parameter counts as used when its name is referenced anywhere in the function
347
+ body — read, augmented-assigned, or deleted — including inside a nested
348
+ function or comprehension; an attribute access such as
349
+ ``monkeypatch.setenv(...)`` reads the name and so counts. Only the named
350
+ injection slots pytest fills — undefaulted positional-or-keyword and
351
+ keyword-only parameters — are considered, matching
352
+ ``check_known_pytest_fixture_annotations``.
353
+
354
+ Args:
355
+ content: The Python source to analyze.
356
+ file_path: The path of the file being checked.
357
+
358
+ Returns:
359
+ One blocking issue per known fixture parameter declared on a test
360
+ function whose body never references it, naming the parameter.
361
+ """
362
+ if not is_test_file(file_path):
363
+ return []
364
+ if is_workflow_registry_file(file_path) or is_migration_file(file_path):
365
+ return []
366
+ try:
367
+ tree = ast.parse(content)
368
+ except SyntaxError:
369
+ return []
370
+ issues: list[str] = []
371
+ for each_node in _collect_pytest_collectable_functions(tree):
372
+ if not _is_pytest_test_function(each_node):
373
+ continue
374
+ referenced_names = _names_referenced_in_subtree(each_node)
375
+ for each_arg in _collect_fixture_injection_arguments(each_node):
376
+ if each_arg.arg not in ANNOTATION_BY_PYTEST_FIXTURE:
377
+ continue
378
+ if each_arg.arg in referenced_names:
379
+ continue
380
+ issues.append(
381
+ f"Line {each_arg.lineno}: parameter {each_arg.arg!r} on "
382
+ f"{each_node.name!r} - {UNUSED_PYTEST_FIXTURE_PARAMETER_MESSAGE_SUFFIX}"
383
+ )
384
+ return issues
385
+
386
+
208
387
  def check_return_annotations(content: str, file_path: str) -> list[str]:
209
388
  if is_test_file(file_path):
210
389
  return []