claude-dev-env 2.32.0 → 2.33.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 (96) hide show
  1. package/.agents/agents/clean-coder.md +8 -7
  2. package/.agents/agents/code-quality-agent.md +4 -2
  3. package/.agents/agents/pr-description-writer.md +8 -3
  4. package/.agents/agents/reference/pr-description-illustrative-voice.md +6 -1
  5. package/.agents/agents/test_agent_frontmatter.py +0 -9
  6. package/.agents/agents/test_pr_description_writer_style.py +24 -1
  7. package/.agents/skills/e-simplify/SKILL.md +1 -15
  8. package/.agents/skills/grok-spawn/reference/worker-briefs.md +4 -0
  9. package/.agents/skills/pr-refinement/SKILL.md +2 -2
  10. package/.agents/skills/pr-shared-extraction/SKILL.md +2 -2
  11. package/.agents/skills/pr-title-description/SKILL.md +6 -2
  12. package/.agents/skills/review-tier/SKILL.md +1 -1
  13. package/.agents/skills/review-tier/reference/tier-policy.json +2 -2
  14. package/.agents/skills/review-tier/scripts/review_tier.py +31 -4
  15. package/.agents/skills/review-tier/scripts/test_generation.py +7 -19
  16. package/.agents/skills/review-tier/scripts/test_review_tier.py +11 -11
  17. package/AGENTS.md +83 -0
  18. package/_shared/pr-loop/code-rules-gate.md +5 -2
  19. package/_shared/pr-loop/scripts/code_rules_gate.py +7 -3
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +13 -1
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +7 -0
  22. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -1
  23. package/audit-rubrics/audit-categories.json +2 -2
  24. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
  25. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +5 -3
  26. package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +6 -3
  27. package/audit-rubrics/prompts/category-e-dead-code.md +6 -6
  28. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +25 -19
  29. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +28 -25
  30. package/docs/CODE_RULES.md +7 -8
  31. package/hooks/blocking/code_rules_comments.py +118 -59
  32. package/hooks/blocking/code_rules_enforcer.py +19 -7
  33. package/hooks/blocking/code_rules_type_escape.py +16 -23
  34. package/hooks/blocking/javascript_comment_scanner.py +99 -0
  35. package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +24 -4
  36. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  37. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +190 -3
  38. package/hooks/blocking/test_code_rules_enforcer_javascript_comments.py +44 -0
  39. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +0 -1
  40. package/hooks/git-hooks/pull_request_handoff.py +1 -1
  41. package/hooks/git-hooks/test_post_commit.py +8 -0
  42. package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +3 -4
  43. package/hooks/hooks_constants/test_unused_module_import_constants.py +9 -1
  44. package/hooks/hooks_constants/unused_module_import_constants.py +1 -1
  45. package/hooks/validators/comment_checks.py +2 -2
  46. package/hooks/validators/fast_save_validators.py +0 -9
  47. package/hooks/validators/git_checks.py +0 -84
  48. package/hooks/validators/run_all_validators.py +2 -9
  49. package/hooks/validators/test_fast_save_validators.py +2 -8
  50. package/hooks/validators/test_git_checks.py +0 -249
  51. package/hooks/validators/test_todo_checks.py +1 -0
  52. package/hooks/validators/todo_checks.py +1 -1
  53. package/package.json +1 -1
  54. package/rules/git-workflow.md +0 -8
  55. package/scripts/check.ps1 +35 -1
  56. package/scripts/codex_compat_materializer.py +1 -0
  57. package/scripts/tests/test_rules.py +111 -0
  58. package/system-prompts/software-engineer.xml +6 -19
  59. package/.agents/skills-archived/split-pr/SKILL.md +0 -85
  60. package/.agents/skills-archived/split-pr/reference/path-layers.md +0 -16
  61. package/.agents/skills-archived/split-pr/reference/proposal-format.md +0 -15
  62. package/.agents/skills-archived/split-pr/reference/split-further-loop.md +0 -10
  63. package/.agents/skills-archived/split-pr/reference/splitting-principles.md +0 -26
  64. package/.agents/skills-archived/split-pr/scripts/analyze_pr.py +0 -279
  65. package/.agents/skills-archived/split-pr/scripts/categorize_files.py +0 -106
  66. package/.agents/skills-archived/split-pr/scripts/config/__init__.py +0 -1
  67. package/.agents/skills-archived/split-pr/scripts/config/dependency_constants.py +0 -14
  68. package/.agents/skills-archived/split-pr/scripts/config/git_operations_constants.py +0 -36
  69. package/.agents/skills-archived/split-pr/scripts/config/packing_constants.py +0 -61
  70. package/.agents/skills-archived/split-pr/scripts/config/plan_constants.py +0 -49
  71. package/.agents/skills-archived/split-pr/scripts/config/split_pr_constants.py +0 -110
  72. package/.agents/skills-archived/split-pr/scripts/execute_split_slices.py +0 -82
  73. package/.agents/skills-archived/split-pr/scripts/pack_files_into_slices.py +0 -212
  74. package/.agents/skills-archived/split-pr/scripts/split_pr_dependency_graph.py +0 -70
  75. package/.agents/skills-archived/split-pr/scripts/split_pr_git_operations.py +0 -184
  76. package/.agents/skills-archived/split-pr/scripts/split_pr_layer_order.py +0 -58
  77. package/.agents/skills-archived/split-pr/scripts/split_pr_paginate.py +0 -119
  78. package/.agents/skills-archived/split-pr/scripts/split_pr_process_runner.py +0 -52
  79. package/.agents/skills-archived/split-pr/scripts/split_pr_script_types.py +0 -126
  80. package/.agents/skills-archived/split-pr/scripts/split_pr_title.py +0 -41
  81. package/.agents/skills-archived/split-pr/scripts/test_analyze_pr.py +0 -228
  82. package/.agents/skills-archived/split-pr/scripts/test_categorize_files.py +0 -55
  83. package/.agents/skills-archived/split-pr/scripts/test_categorize_files_packing.py +0 -59
  84. package/.agents/skills-archived/split-pr/scripts/test_execute_split_slices.py +0 -99
  85. package/.agents/skills-archived/split-pr/scripts/test_split_pr_dependency_graph.py +0 -47
  86. package/.agents/skills-archived/split-pr/scripts/test_split_pr_git_operations.py +0 -125
  87. package/.agents/skills-archived/split-pr/scripts/test_split_pr_layer_order.py +0 -36
  88. package/.agents/skills-archived/split-pr/scripts/test_split_pr_paginate.py +0 -65
  89. package/.agents/skills-archived/split-pr/scripts/test_split_pr_script_types.py +0 -73
  90. package/.agents/skills-archived/split-pr/scripts/test_split_pr_title.py +0 -28
  91. package/.agents/skills-archived/split-pr/scripts/test_verify_dependency_graph.py +0 -46
  92. package/.agents/skills-archived/split-pr/scripts/test_verify_plan.py +0 -56
  93. package/.agents/skills-archived/split-pr/scripts/test_verify_plan_contract.py +0 -50
  94. package/.agents/skills-archived/split-pr/scripts/test_verify_plan_path_normalization.py +0 -45
  95. package/.agents/skills-archived/split-pr/scripts/verify_dependency_graph.py +0 -111
  96. package/.agents/skills-archived/split-pr/scripts/verify_plan.py +0 -139
@@ -600,9 +600,54 @@ def test_run_gate_detects_new_inline_comment_in_touched_file(
600
600
  )
601
601
  stage_file(temporary_git_repository, "module.py")
602
602
 
603
+ monkeypatch.chdir(temporary_git_repository)
604
+ exit_code = gate_module.main(["--staged", "--comment-policy"])
605
+
606
+ assert exit_code == 1
607
+
608
+
609
+ def test_main_staged_mode_omits_comments_without_opt_in(
610
+ temporary_git_repository: Path,
611
+ monkeypatch: pytest.MonkeyPatch,
612
+ ) -> None:
613
+ write_file(temporary_git_repository / "module.py", "total = 1\n")
614
+ commit_all_files(temporary_git_repository, "initial")
615
+ write_file(temporary_git_repository / "module.py", "total = 1 # added\n")
616
+ stage_file(temporary_git_repository, "module.py")
617
+
603
618
  monkeypatch.chdir(temporary_git_repository)
604
619
  exit_code = gate_module.main(["--staged"])
605
620
 
621
+ assert exit_code == 0
622
+
623
+
624
+ def test_run_gate_staged_mode_blocks_retained_comment_on_changed_line(
625
+ temporary_git_repository: Path,
626
+ monkeypatch: pytest.MonkeyPatch,
627
+ ) -> None:
628
+ write_file(temporary_git_repository / "module.py", "total = 1 # TODO #999: track\n")
629
+ commit_all_files(temporary_git_repository, "initial")
630
+ write_file(temporary_git_repository / "module.py", "total = 2 # TODO #999: track\n")
631
+ stage_file(temporary_git_repository, "module.py")
632
+
633
+ monkeypatch.chdir(temporary_git_repository)
634
+ exit_code = gate_module.main(["--staged", "--comment-policy"])
635
+
636
+ assert exit_code == 1
637
+
638
+
639
+ def test_run_gate_base_mode_blocks_retained_comment_on_changed_line(
640
+ temporary_git_repository: Path,
641
+ monkeypatch: pytest.MonkeyPatch,
642
+ ) -> None:
643
+ write_file(temporary_git_repository / "module.py", "total = 1 # noqa\n")
644
+ commit_all_files(temporary_git_repository, "initial")
645
+ write_file(temporary_git_repository / "module.py", "total = 2 # noqa\n")
646
+ commit_all_files(temporary_git_repository, "change")
647
+
648
+ monkeypatch.chdir(temporary_git_repository)
649
+ exit_code = gate_module.main(["--base", "HEAD~1", "--comment-policy"])
650
+
606
651
  assert exit_code == 1
607
652
 
608
653
 
@@ -619,7 +664,7 @@ def test_run_gate_treats_new_files_prior_content_as_empty(
619
664
  stage_file(temporary_git_repository, "brand_new.py")
620
665
 
621
666
  monkeypatch.chdir(temporary_git_repository)
622
- exit_code = gate_module.main(["--staged"])
667
+ exit_code = gate_module.main(["--staged", "--comment-policy"])
623
668
 
624
669
  assert exit_code == 1
625
670
 
@@ -2387,6 +2432,7 @@ def test_parse_arguments_applies_documented_defaults() -> None:
2387
2432
 
2388
2433
  assert parsed_arguments.staged is False
2389
2434
  assert parsed_arguments.immediate is False
2435
+ assert parsed_arguments.comment_policy is False
2390
2436
  assert parsed_arguments.base == "origin/main"
2391
2437
  assert parsed_arguments.repo_root is None
2392
2438
  assert parsed_arguments.only_under == []
@@ -192,7 +192,7 @@
192
192
  },
193
193
  {
194
194
  "id": "E8",
195
- "axis_name": "Stub / placeholder code without TODO"
195
+ "axis_name": "Stub / placeholder code without tracking"
196
196
  },
197
197
  {
198
198
  "id": "E9",
@@ -509,7 +509,7 @@
509
509
  },
510
510
  {
511
511
  "id": "L7",
512
- "axis_name": "Skipped-category exhaustion"
512
+ "axis_name": "Changed-comment handling"
513
513
  },
514
514
  {
515
515
  "id": "L8",
@@ -25,7 +25,7 @@
25
25
  | E5 | Unused parameters and locals | Parameters declared but never read inside the function body; local variables assigned but never read afterward in the same scope. |
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
- | E8 | Stub / placeholder code without TODO | `pass`, `...`, `raise NotImplementedError` left without explanation or tracking. |
28
+ | E8 | Stub / placeholder code without tracking | Judge `pass`, `...`, and `raise NotImplementedError` directly; track follow-up work outside source code. |
29
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. |
30
30
 
31
31
  ---
@@ -24,13 +24,15 @@
24
24
  | J5 | Abbreviations | `ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `elem`, `val`, `tmp`, `str`, `num`, `arr`, `obj`, `fn`, `cb`, `req`, `res`. (Loop counters `i`/`j`/`k` and `e` for exceptions are exempt.) |
25
25
  | J6 | Vague names | `result`, `data`, `output`, `response`, `value`, `item`, `temp`, `info`, `stuff`, `thing`. Vague prefixes: `handle`, `process`, `manage`, `do`. |
26
26
  | J7 | Type hints | Missing type annotation on a parameter or return; presence of `Any` or `# type: ignore`. |
27
- | J8 | New inline comments | New `#` or `//` comments in production code added by this diff. (Removing an existing comment is a Comment Preservation concern the hook flags with a stderr advisory rather than a block.) |
27
+ | J8 | New inline comments | New `#` or `//` comments in production or test code added by this diff; changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are also findings. |
28
28
  | J9 | Logging format | `log_*(f"...")` rather than `log_*("...", arg)`. |
29
29
  | J10 | Imports inside functions | `import` statements placed inside function bodies. |
30
30
  | J11 | sys.path.insert dedup | `sys.path.insert(0, X)` must be guarded by `if X not in sys.path:` (test files exempt). |
31
31
  | J12 | Hardcoded user paths | String literals naming a specific user's home directory (`C:/Users/example/...`, `/Users/alice/...`, `/home/bob/...`). Use `pathlib.Path.home()`. |
32
32
 
33
- The write-time hook (`code_rules_enforcer.py`) exempts test files (`test_*.py`, `*_test.py`, `*.test.*`, `*.spec.*`, `conftest.py`, paths under `/tests/`) from most Category J sub-buckets, and skips the naming, logging, annotation, and unused-import rules on `.mjs` / `.js` files. J11 (`sys.path.insert`) always applies. Read the next section for the sub-buckets this audit applies more widely than the hook.
33
+ The write-time hook (`code_rules_enforcer.py`) exempts test files (`test_*.py`, `*_test.py`, `*.test.*`, `*.spec.*`, `conftest.py`, paths under `/tests/`) from most Category J sub-buckets, while comment changes use one rule for production and tests. It skips the naming, logging, annotation, and unused-import rules on `.mjs` / `.js` files. J11 (`sys.path.insert`) always applies. Read the next section for the sub-buckets this audit applies more widely than the hook.
34
+
35
+ When a change touches code that an existing comment describes or is attached to, remove that comment in the same change and carry its meaning through clear names and structure. Leave comments tied to untouched code unchanged. Keep comment cleanup inside the requested task. Production and tests follow one rule. Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified.
34
36
 
35
37
  ## Write-time exemptions do not scope this audit
36
38
 
@@ -38,7 +40,7 @@ The write-time hook skips several rules on test files and on `.mjs` / `.js` file
38
40
 
39
41
  - **Naming (J5, J6)** — banned identifiers (`ctx`, `cfg`, `msg`, `result`, `data`, `output`, `value`, `item`, `temp`, `elem`, `val`, …) and banned function prefixes (`handle_`, `process_`, `manage_`, `do_`); boolean names prefixed `is_` / `has_` / `should_` / `can_` / `was_` / `did_`.
40
42
  - **Logging format (J9)** — the format string and its arguments pass as separate parameters (`log_*("...", arg)`), never an f-string.
41
- - **Type annotations (J7)** — every changed function parameter and return is typed; no `Any`, no bare `# type: ignore`.
43
+ - **Type annotations (J7)** — every changed function parameter and return is typed; no `Any`, no `# type: ignore` directives.
42
44
  - **Unused imports** — a module-level import a changed file does not read is a finding.
43
45
  - **Function length** — a changed function that runs past the length threshold splits into named helpers.
44
46
 
@@ -1,11 +1,14 @@
1
1
  # Category L — Behavior-equivalence for refactors
2
2
 
3
+ When a change touches code that an existing comment describes or is attached to, remove that comment in the same change and carry its meaning through clear names and structure. Leave comments tied to untouched code unchanged. Keep comment cleanup inside the requested task.
4
+ Production and tests follow one rule. Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified.
5
+
3
6
  **What this category audits:** rewrites of an existing function (especially an enforcement check, parser, classifier, or normalizer) where the new implementation must accept every input the old implementation accepted and reject every input the old implementation rejected. Common when a regex-based check is rewritten as a tokenize-based check, when a `str.startswith` chain is consolidated into a single regex, when a hand-rolled split is replaced with a library call, or when a multi-step pipeline is collapsed into one pass.
4
7
 
5
8
  **Why this category is its own bucket:** Categories A–K catch failure modes inside the rewrite itself (wrong signature, dead code, missed branch). Category L catches the failure mode that emerges between the rewrite and the *historically valid inputs* the original code accepted. The diff looks internally consistent and the new unit tests pass — but inputs the prior code accepted fall through under the new implementation, or inputs the prior code rejected slip past. The bug only surfaces against the corpus of canonical inputs the original implementation was tuned for.
6
9
 
7
10
  **Examples of Category L findings:**
8
- - A tokenize-based exempt-marker check drops `#noqa` (no space after `#`) when the original normalization-based check accepted it. (ccc#479 F1)
11
+ - A refactored comment check changes acceptance for marker-shaped input when code changes.
9
12
  - A new comment classifier misreads a bare `#` lookalike that the original regex correctly rejected. (ccc#479 F4)
10
13
  - A refactored shebang detector drops the inline `#!` variant the original handled. (ccc#479 F5)
11
14
  - An invariant the original loop enforced at the first match (early-exit) is dropped in the rewrite. (ccc#479 F6)
@@ -27,7 +30,7 @@ Decomposition is by the **kind of historically valid input** the rewrite must co
27
30
  | L4 | Empty / boundary inputs | Empty string, single character, single-line vs multi-line, EOF without newline retain their original classification. |
28
31
  | L5 | Invariant preservation | Early-exit guarantees, idempotence, ordering, stable iteration, "first match wins" semantics carry over. |
29
32
  | L6 | Implementation-tag parity | Token-based vs regex-based vs str-method-based: the new tag accepts every input shape the old tag accepted (no shape silently dropped). |
30
- | L7 | Skipped-category exhaustion | Inputs that the original explicitly skipped (e.g., shebang on line 1 only, exempt markers without trailing prose, `# type:` with a trailing justification) remain skipped. |
33
+ | L7 | Changed-comment handling | Shebangs on line 1 and docstrings retain their handling. Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are findings. |
31
34
  | L8 | Sibling-implementation comparison | When two parallel implementations exist (e.g., Python + PowerShell, regex + tokenize), the rewrite of one must still produce the same accept/reject decisions as the sibling for shared inputs. |
32
35
 
33
36
  Customize per-artifact: a parser refactor without an explicit sibling implementation reduces L8 to "verified clean — no parallel implementation"; a single-axis rewrite (whitespace handling only) may exhaust the per-sub-bucket checks against just L2 and L7.
@@ -42,4 +45,4 @@ The reusable Variant C template for Category L is in [`../prompts/category-l-beh
42
45
 
43
46
  Categories A–K describe failure modes that show up in the rewrite's own surface. Category L describes the failure mode that shows up only when the rewrite is compared against the inputs the original was tuned for. A reviewer walking only A–K reads the rewrite, finds it clean, and approves it — without re-running the original test inputs through the new code path. L forces the reviewer to pin the original's known-good inputs in a table and assert each still passes against the rewrite.
44
47
 
45
- The ccc#479 F1 case illustrates the cost of not running L. The refactor of `_is_exempt_python_comment` replaced a `comment_string[1:].lstrip()` normalization (which reduced both `# noqa` and `#noqa` to the body `noqa` before the membership test) with a tokenize-based recognizer that tested the raw `tokenize.COMMENT` token text against `startswith("# noqa")`. Production code carrying `#noqa: F401` (no space) silently stopped matching the exempt marker after the refactor, and the no-new-comments gate began blocking writes that the original implementation passed. The dropped no-space variant only surfaces under a KNOWN_GOOD_INPUTS table that enumerates spaced, no-space, tab-separated, and multi-space inputs — fixtures the rewrite's own tests would otherwise miss.
48
+ The ccc#479 F1 case illustrates the cost of not comparing changed-code marker handling with the original input corpus. Category L requires the reviewer to pin the original inputs and verify that shebangs and docstrings remain preserved while changed marker comments are rejected.
@@ -63,10 +63,10 @@ Inline the artifact under this section using the section types defined in the ch
63
63
  - Helpers defined inside a single test body that are never called within that body are dead.
64
64
  - Adversarial probes for proof-of-absence: (a) any test that defines a local helper (e.g., `def make_dir(...):`) and never calls it? (b) any imported test name from the production module that no test exercises? (c) any module-level constant whose call graph collapses to zero references after the diff?
65
65
 
66
- **E8. Stub / placeholder code without TODO**
66
+ **E8. Stub / placeholder code without tracking**
67
67
  - Distinguish real-behavior `pass` / `continue` / empty-handler bodies (intentional swallowing of expected exceptions, no-op branches that exist to satisfy a contract) from scaffolding stubs.
68
- - Real-behavior bodies do NOT require a TODO; the audit must state the rationale (e.g., "rmdir race with concurrent writer is intentionally swallowed").
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 `CODE_RULES.md` → "COMMENT PRESERVATION" rule.
68
+ - Real-behavior bodies need no source-code marker; the audit must state the rationale (e.g., "rmdir race with concurrent writer is intentionally swallowed").
69
+ - Scaffolding bodies (`pass`, `...`, `raise NotImplementedError`, empty `else { }`, single-statement `return None` placeholders) are Category E findings. Judge the stub itself; track follow-up work outside source code.
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
72
  **E9. Constants-module exports with no importer**
@@ -160,12 +160,12 @@ ID prefix: `find`.
160
160
  - No test data builders or mock factories are defined in this PR.
161
161
  - Adversarial probes for proof-of-absence: (a) does any of the five tests define a local helper inside its body that is then never called? (e.g., a `def make_dir(...):` defined but unused) — scan each test body. (b) does any test import a name from `sweep_empty_dirs` that it never uses? — only `sweep` is imported, and every test calls it. (c) does the `_SCRIPTS_DIR` block survive if pytest is invoked with `sweep_empty_dirs.py` already on `sys.path`? — yes, the membership-test guards the insert, so the constant is still meaningfully consumed even on the second run.
162
162
 
163
- **E8. Stub / placeholder code without TODO**
164
- - `sweep_empty_dirs.py` line 30: `except OSError: pass` — the `pass` is the entire handler body, intentionally swallowing rmdir failures (e.g., directory became non-empty between the walk and the rmdir, race with another writer). This is a real-behavior `pass`, not a stub. No TODO is required because the handler IS the intended behavior.
163
+ **E8. Stub / placeholder code without tracking**
164
+ - `sweep_empty_dirs.py` line 30: `except OSError: pass` — the `pass` is the entire handler body, intentionally swallowing rmdir failures (e.g., directory became non-empty between the walk and the rmdir, race with another writer). This is a real-behavior `pass`, not a stub. No source-code marker is needed because the handler IS the intended behavior.
165
165
  - `sweep_empty_dirs.py` line 28: the second `except OSError: continue` similarly is intended behavior (skip the directory whose ctime is unreadable), not a stub.
166
166
  - No `...` literal anywhere in the four files.
167
167
  - No `raise NotImplementedError` anywhere.
168
- - No `# TODO` markers in the diff — the project's own rule (`CODE_RULES.md` → "COMMENT PRESERVATION") requires TODOs only for scaffolding/placeholder code. The two `pass`/`continue` bodies above are production behavior, not scaffolding.
168
+ - The two `pass`/`continue` bodies above are production behavior, not scaffolding. No source-code marker is needed because the audit judges the body directly.
169
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.
170
170
 
171
171
  **E9. Constants-module exports with no importer**
@@ -5,7 +5,7 @@ Audit [REPO/ARTIFACT] [TARGET_ID] for **Category J only** (CODE_RULES.md complia
5
5
  - Head SHA / Revision: [SHA or revision identifier]
6
6
  - Scope: [files / line ranges / packages in scope]
7
7
  - Languages in scope: [e.g., Python, PowerShell, TypeScript]
8
- - Production vs test split: [explicit list of which files are production and which are test files; test files are exempt from most J sub-buckets]
8
+ - Production vs test split: [explicit list of which files are production and which are test files; both use the same comment rule]
9
9
 
10
10
  ID prefix: `find`.
11
11
 
@@ -17,11 +17,14 @@ Replace this paragraph with the chunked source material before issuing the promp
17
17
 
18
18
  ## Write-time exemptions do not scope this audit
19
19
 
20
- The write-time hook skips several rules on test files and on `.mjs` / `.js` files; this audit does not. Apply these rules to every changed line — production, test, and JavaScript files alike:
20
+ When a change touches code that an existing comment describes or is attached to, remove that comment in the same change and carry its meaning through clear names and structure. Leave comments tied to untouched code unchanged. Keep comment cleanup inside the requested task.
21
+ Production and tests follow one rule. Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified.
22
+
23
+ The write-time hook skips several rules on test files and on `.mjs` / `.js` files; comment changes use one rule for production and tests. This audit does not skip the other listed rules. Apply these rules to every changed line — production, test, and JavaScript files alike:
21
24
 
22
25
  - Naming (J5, J6) — banned identifiers and banned function prefixes; boolean names prefixed `is_` / `has_` / `should_` / `can_` / `was_` / `did_`.
23
26
  - Logging format (J9) — the format string and its arguments pass as separate parameters, never an f-string.
24
- - Type annotations (J7) — every changed parameter and return is typed; no `Any`, no bare `# type: ignore`.
27
+ - Type annotations (J7) — every changed parameter and return is typed; no `Any`, no `# type: ignore` directives.
25
28
  - Unused imports — a module-level import a changed file does not read.
26
29
  - Function length — a changed function past the length threshold splits into named helpers.
27
30
 
@@ -31,7 +34,8 @@ A `test_*.py` name or a `.mjs` extension takes the line out of the write-time ga
31
34
 
32
35
  **J1. Magic values in production function bodies**
33
36
  - Walk every numeric or non-trivial literal other than `0`, `1`, `-1` inside production function bodies.
34
- - Test files are exempt. Module-level declarations are J3-scope, not J1-scope.
37
+ - Test files follow the same no-new-comment policy.
38
+ - Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified. Module-level declarations are J3-scope, not J1-scope.
35
39
  - For each literal found, decide: structural value that belongs in `config/` (flag) vs. truly local arithmetic constant tied to the line's logic (defendable).
36
40
  - Adversarial probes must each verify a distinct angle: (a) does the literal duplicate an existing value already centralized in `config/`? (b) does the literal silently couple two languages (e.g., a Python config value and a hand-typed PowerShell / shell mirror)? (c) does the literal appear in user-facing help/doc text in a way that would silently lie if the canonical value changed?
37
41
 
@@ -67,10 +71,11 @@ A `test_*.py` name or a `.mjs` extension takes the line out of the write-time ga
67
71
  - Adversarial probes: (a) does any production function rely on inferred return type from a single `return` path? (b) does any parameter use a string-quoted forward reference that masks `Any`? (c) is there a `# type: ignore` anywhere? Grep the diff explicitly.
68
72
 
69
73
  **J8. New inline comments**
70
- - Every `#` or `//` comment line **added** by this diff in production code — flag, except for exempt markers (shebangs, `# type:`, `# noqa`, `# pylint:`, `# pragma:`, `// @ts-`, `// eslint-`, `// prettier-`, `/// `).
74
+ - Every `#` or `//` comment line **added** by this diff in any code — flag.
71
75
  - Module/function/class docstrings are always allowed.
72
- - Existing comments are NEVER removed (Comment Preservation rule); if the diff removes an existing comment, that is a separate violation outside J8 (the hook emits a stderr advisory for it rather than blocking).
73
- - Test files are exempt.
76
+ - Comments tied to untouched code remain unchanged; a changed comment is removed with the code it describes.
77
+ - Test files follow the same no-new-comment policy.
78
+ - Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified.
74
79
  - Adversarial probes: (a) is there any `# type:` or marker comment that is actually inert prose rather than a real type-checker / linter directive? (b) is any docstring carrying inline-comment content (line-level explanations rather than module/function description)? (c) does any newly-added blank line between code stanzas function as a comment substitute, suggesting the author wanted to add a comment but couldn't?
75
80
 
76
81
  **J9. Logging format**
@@ -81,7 +86,7 @@ A `test_*.py` name or a `.mjs` extension takes the line out of the write-time ga
81
86
 
82
87
  **J10. Imports inside functions**
83
88
  - Every `import` / `from ... import ...` statement — verify at module scope.
84
- - Test files: deferred imports after a `sys.path.insert(0, ...)` guard at module scope are allowed (documented circular-import-style workaround).
89
+ - Test file — lines 152-158 are module-scope imports, and line 164 is a deferred import at module scope after the sys.path guard. The documented import placement remains allowed.
85
90
  - Adversarial probes: (a) is there any lazy `import` inside a production function body? (b) does any conditional `import` (e.g., inside `if TYPE_CHECKING:`) escape into runtime accidentally? (c) does any non-Python language analog (e.g., `require(...)` inside a JS function, `Import-Module` inside a PowerShell `if` branch) appear in production code?
86
91
 
87
92
  **J11. sys.path.insert dedup**
@@ -90,9 +95,9 @@ A `test_*.py` name or a `.mjs` extension takes the line out of the write-time ga
90
95
  - Adversarial probes: (a) is the guard expression semantically equivalent to `if X not in sys.path:` where `X` is exactly the value being inserted (no string/Path mismatch)? (b) is there a second `sys.path` mutation elsewhere in the file that is not guarded? (c) would importing the module twice (e.g., via test collection re-runs) re-trigger the insert?
91
96
 
92
97
  **J12. Hardcoded user paths**
93
- - Any string literal containing `C:/Users/<name>/...`, `/Users/<name>/...`, `/home/<name>/...` in production code? Use `pathlib.Path.home()` or `os.path.expanduser('~')`.
98
+ - Any string literal containing a user-specific home path in production code? Use `pathlib.Path.home()` or `os.path.expanduser('~')`.
94
99
  - Exempt: test files, `config/` files, workflow registry paths (`/workflow/`, `_tab.py`, `/states.py`, `/modules.py`), Django migrations (`/migrations/`), and hook infrastructure.
95
- - Adversarial probes: (a) does any error message, help string, or docstring example embed a `C:/Users/...`, `/home/...`, or `/Users/...` example? Grep the diff. (b) does any scheduled-task / launcher / installer artifact hardcode a path that should be derived from `$PSCommandPath`, `__file__`, or `os.getcwd()`? (c) does any docstring example show a user-specific home, even if the runtime code itself is path-clean?
100
+ - Adversarial probes: (a) does any error message, help string, or docstring example embed a user-specific home path? Grep the diff. (b) does any scheduled-task / launcher / installer artifact hardcode a path that should be derived from `$PSCommandPath`, `__file__`, or `os.getcwd()`? (c) does any docstring example show a user-specific home, even if the runtime code itself is path-clean?
96
101
 
97
102
  ## Cross-bucket questions to answer at the end
98
103
 
@@ -167,7 +172,7 @@ ID prefix: `find`.
167
172
  - Flag any name from the vague list: `result`, `data`, `output`, `response`, `value`, `item`, `temp`, `info`, `stuff`, `thing`. Vague verb prefixes: `handle`, `process`, `manage`, `do`.
168
173
  - `sweep_empty_dirs.py` — variable `removed: list[str]` (line 81) is domain-specific (what was removed from disk), not vague. `now` (line 80) is a time concept, not on the vague list. `created` (line 87) is a domain timestamp, not vague.
169
174
  - Function names: `sweep`, `_log_walk_error`, `_build_parser`, `main` — none use the vague verb prefixes (`handle`, `process`, `manage`, `do`). `sweep` is concrete domain action.
170
- - Test file — exempt.
175
+ - Test file — the same no-new-comment rule applies.
171
176
  - Adversarial probes: (a) does `removed` (line 81) overlap with the vague-name list? It does not — the list is `result`, `data`, `output`, etc., and `removed` is none of those. (b) is `arguments` (line 115) on any vague list? No — `arguments` is the argparse-namespace canonical name. (c) does any callback parameter use a vague name? `_log_walk_error(os_error)` — `os_error` is concrete. Clean.
172
177
 
173
178
  **J7. Type hints**
@@ -181,24 +186,24 @@ ID prefix: `find`.
181
186
  - Adversarial probes: (a) does any production function rely on inferred return type from a single `return` path? All four production functions have explicit return annotations. (b) does any parameter use a string-quoted forward reference that masks `Any`? No — all annotations are direct. (c) is there a `# type: ignore` anywhere? Grep the diff: no occurrences.
182
187
 
183
188
  **J8. New inline comments**
184
- - Every `#` comment line added by this diff in production code — flag, except for exempt markers (shebangs, `# type:`, `# noqa`, `# pylint:`, `# pragma:`).
189
+ - Every `#` comment line added by this diff in production or test code — flag. Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are also findings.
185
190
  - `sweep_empty_dirs.py` — line 61 (`#!/usr/bin/env python3`) is a shebang, exempt. Line 62 is a module-level docstring, allowed. Lines 78 and 102 are function docstrings, allowed. No inline `#` comments. Clean.
186
191
  - `config/sweep_config.py` — line 140 is a module-level docstring. No inline comments. Clean.
187
- - Test file — exempt.
192
+ - Test file — the same no-new-comment rule applies.
188
193
  - `Install-SweepEmptyDirs.ps1` — line 227 (`#!/usr/bin/env pwsh`) is a shebang, exempt. No inline `#` comments added in the PowerShell file. Clean.
189
194
  - Adversarial probes: (a) is there any `# type:` comment that is actually inert noise rather than a type-checker directive? No occurrences in the diff. (b) is any docstring carrying inline-comment content as line-level explanations rather than module/function description? Module docstring on line 62 is one line ("Delete empty directories older than 2 minutes under a given root."); function docstrings on 78 and 102 are one-liners. Clean. (c) does any newly-added blank line between code stanzas function as a comment substitute? Visual whitespace is allowed.
190
195
 
191
196
  **J9. Logging format**
192
197
  - Walk every `log_*(...)` call. Must be `log_*("template with {}", arg)`, not `log_*(f"...")`.
193
198
  - `sweep_empty_dirs.py` uses `print()`, not a logger. The rule applies to `log_*` (the project's structured logger), not stdlib `print`. The print f-strings on lines 74, 93, 118, 125, 131 are J2-scope (string-template magic), not J9-scope.
194
- - Test file — exempt.
199
+ - Test file — the same no-new-comment rule applies.
195
200
  - Adversarial probes: (a) is there any imported `log_*` function in `sweep_empty_dirs.py` that uses an f-string? No — no logger import in the diff. (b) is `print(..., file=sys.stderr)` (lines 74, 118) a logger-equivalent call that should be subject to J9? No — `print` is stdlib stdout/stderr, not the structured-logger family. (c) does the PowerShell `Write-Host` / `Write-Error` family count? J9 is Python-specific (`log_*` callable convention).
196
201
 
197
202
  **J10. Imports inside functions**
198
203
  - Every `import` / `from ... import ...` statement — verify at module scope.
199
204
  - `sweep_empty_dirs.py` lines 64-70 — all imports at module top. Function bodies (lines 73-135) contain no `import` statements. Clean.
200
205
  - `config/sweep_config.py` — no imports. Clean.
201
- - Test file — exempt; nevertheless lines 152-158 are all module-scope imports, line 164 is a deferred import (`from sweep_empty_dirs import sweep # noqa: E402`) at module scope after the `sys.path.insert` guard. Documented circular-import-style workaround pattern; allowed.
206
+ - Test file — lines 152-158 are module-scope imports, and line 164 is a deferred import at module scope after the sys.path guard. The documented import placement remains allowed.
202
207
  - Adversarial probes: (a) is there any lazy `import` inside `sweep`, `main`, or `_build_parser` body? No occurrences. (b) is `argparse.ArgumentParser` accessed via `argparse.ArgumentParser` (line 102) using the module-scope import on line 64? Yes. (c) does the PowerShell file have any `Import-Module` calls inside `if` branches? No — the script imports nothing.
203
208
 
204
209
  **J11. sys.path.insert dedup**
@@ -208,12 +213,12 @@ ID prefix: `find`.
208
213
  - Adversarial probes: (a) is the guard expression `if str(_SCRIPTS_DIR) not in sys.path:` semantically equivalent to `if X not in sys.path:` where `X` is exactly the value being inserted? Yes — both use `str(_SCRIPTS_DIR)`. (b) is there a second `sys.path` mutation elsewhere in the test file that's not guarded? No — only one occurrence at line 162. (c) would importing the test module twice (e.g., via pytest's collection re-runs) re-trigger the insert? The guard prevents it. Clean.
209
214
 
210
215
  **J12. Hardcoded user paths**
211
- - Any string literal containing `C:/Users/<name>/...`, `/Users/<name>/...`, `/home/<name>/...`?
216
+ - Any string literal containing a user-specific home path?
212
217
  - `sweep_empty_dirs.py` — no hardcoded user paths. The `arguments.root` value is supplied at runtime via argparse positional argument (line 103). Clean.
213
218
  - `config/sweep_config.py` — no paths at all. Clean.
214
- - Test file — exempt; uses `tempfile.TemporaryDirectory()` (lines 178, 188, 197, 210, 216) which is the canonical safe pattern. Clean.
219
+ - Test file — uses `tempfile.TemporaryDirectory()` (lines 178, 188, 197, 210, 216), which is the canonical safe pattern. Clean.
215
220
  - `Install-SweepEmptyDirs.ps1` — `$ScriptDir = Split-Path -Parent $PSCommandPath` (line 272), `$ScriptPath = Join-Path $ScriptDir "sweep_empty_dirs.py"` (line 273). Both derive from `$PSCommandPath` (the script's own path), not a hardcoded user home. Clean. The `$Target` parameter is supplied at install time (line 230); not hardcoded.
216
- - Adversarial probes: (a) does any error message or help string embed a `C:/Users/...` example? Grep the diff for `C:/Users`, `/home/`, `/Users/` — no occurrences. (b) does the scheduled task `-Argument` string at line 296 hardcode a path? It interpolates `$ScriptPath`, `$AgeSeconds`, and `$Target` — all dynamic. Clean. (c) does any docstring example show a user-specific home? Module docstrings on lines 62 and 140 are short and contain no example paths. Clean.
221
+ - Adversarial probes: (a) does any error message or help string embed a user-specific home path? Grep the diff — no occurrences. (b) does the scheduled task `-Argument` string at line 296 hardcode a path? It interpolates `$ScriptPath`, `$AgeSeconds`, and `$Target` — all dynamic. Clean. (c) does any docstring example show a user-specific home? Module docstrings on lines 62 and 140 are short and contain no example paths. Clean.
217
222
 
218
223
  ## Cross-bucket questions to answer at the end
219
224
 
@@ -301,7 +306,8 @@ def main() -> None:
301
306
  sweep(arguments.root, arguments.age)
302
307
  time.sleep(arguments.interval)
303
308
  except KeyboardInterrupt:
304
- print("\nstopped.")
309
+ print("
310
+ stopped.")
305
311
 
306
312
 
307
313
  if __name__ == "__main__":
@@ -1,5 +1,8 @@
1
1
  Audit [REPO/ARTIFACT] [TARGET_ID] for **Category L only** (behavior-equivalence for refactors). Skip A–K, M–P. Sub-bucket forced-exhaustion mode: Category L 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.
2
2
 
3
+ When a change touches code that an existing comment describes or is attached to, remove that comment in the same change and carry its meaning through clear names and structure. Leave comments tied to untouched code unchanged. Keep comment cleanup inside the requested task.
4
+ Production and tests follow one rule. Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified.
5
+
3
6
  [ARTIFACT METADATA — include the BEFORE state of the rewritten function so the agent can compare BEFORE vs AFTER behavior on the same input corpus]
4
7
 
5
8
  - Title / one-line summary: [TITLE]
@@ -30,16 +33,16 @@ ID prefix: `find`.
30
33
 
31
34
  **L2. Whitespace / separator variants**
32
35
  - For every input the BEFORE implementation accepted, does the AFTER implementation also accept the variant with: no space where the BEFORE allowed space, leading whitespace, trailing whitespace, multiple internal spaces, tab vs single space, CRLF vs LF?
33
- - Adversarial probes: (a) construct inputs identical to KNOWN_GOOD_INPUTS but with the space stripped (`#noqa` vs `# noqa`) — does the AFTER state still accept? (b) construct inputs with trailing whitespace and CRLF — does the AFTER state strip them the same way the BEFORE state did? (c) construct inputs with a tab where the BEFORE allowed a space — does the AFTER state's tokenizer / regex treat them identically?
36
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
34
37
 
35
38
  **L3. Adjacent-form regressions**
36
39
  - Does the AFTER implementation use a looser pattern than the BEFORE (e.g., `startswith("## Problem")` where the BEFORE used `re.match(r"^## Problem\b")`)? A loose pattern accepts inputs the original rejected.
37
- - Does the AFTER implementation use a tighter pattern than the BEFORE (e.g., `re.match(r"^# noqa\b")` where the BEFORE used `startswith("# noqa")`)? A tight pattern rejects inputs the original accepted.
40
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
38
41
  - Adversarial probes: (a) construct inputs that satisfy the AFTER pattern but NOT the BEFORE — these are inputs the rewrite silently accepted; (b) construct inputs that satisfy the BEFORE pattern but NOT the AFTER — these are inputs the rewrite silently rejected; (c) walk the BEFORE pattern's anchors (`^`, `\b`, `\s`) and the AFTER pattern's anchors — does every BEFORE anchor have a semantic equivalent in the AFTER pattern?
39
42
 
40
43
  **L4. Empty / boundary inputs**
41
44
  - For empty string, single character, single-newline, single-line, EOF-without-newline — does the AFTER implementation produce the same accept/reject decision as the BEFORE?
42
- - Adversarial probes: (a) does the AFTER tokenizer raise on an empty input where the BEFORE returned False? (b) does the AFTER regex match on a single-newline input where the BEFORE skipped? (c) does the AFTER state handle the EOF-without-newline edge that the BEFORE state's `splitlines()` call did?
45
+ - Adversarial probes: compare empty and single-line inputs under the original and changed-code paths; both preserve the same handling.
43
46
 
44
47
  **L5. Invariant preservation**
45
48
  - Does the BEFORE implementation enforce an invariant (early-exit on first match, idempotence under repeated invocation, stable iteration order, ordering of returned items)? Does the AFTER preserve each invariant?
@@ -49,9 +52,9 @@ ID prefix: `find`.
49
52
  - The BEFORE implementation used [TAG_BEFORE] (regex / tokenize / str-method / library). The AFTER uses [TAG_AFTER]. For each input shape the BEFORE-tag accepted (e.g., a regex pattern accepted inline `#!` because the `re.MULTILINE` flag matched at any line start), does the AFTER-tag accept the same shape via a different mechanism?
50
53
  - Adversarial probes: (a) enumerate the BEFORE-tag's capabilities that the AFTER-tag does not natively have (e.g., regex `\b` boundaries vs tokenize stream events) — has the AFTER implementation added compensating logic? (b) enumerate the AFTER-tag's capabilities that the BEFORE-tag did not have — are any of them silently expanding the accept set? (c) construct an input shape that the BEFORE-tag rejected only because of its tag's limitations — does the AFTER accept now and is that intentional?
51
54
 
52
- **L7. Skipped-category exhaustion**
53
- - Inputs the BEFORE explicitly skipped — shebang on line 1 only, exempt markers without trailing prose, free-form `# type:` directives carrying a trailing justification — does the AFTER state continue to skip them?
54
- - Adversarial probes: (a) does the AFTER state's skip-list match the BEFORE state's skip-list literally? (b) for each skip rule, construct an input the BEFORE skipped — does the AFTER also skip? (c) for each skip rule, construct an input one character off from the skip pattern — does the AFTER fall through to the main check or also skip?
55
+ **L7. Changed-comment handling**
56
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
57
+ - Adversarial probes: compare shebang and docstring handling with the changed marker-comment result across the original and changed-code paths.
55
58
 
56
59
  **L8. Sibling-implementation comparison**
57
60
  - If a parallel implementation exists in another language or paradigm (Python + PowerShell hook, regex + tokenize, JavaScript + Go), does the AFTER implementation produce the same accept/reject decisions as the sibling for shared inputs?
@@ -75,53 +78,53 @@ Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket L1-L8, produce Shape A
75
78
 
76
79
  Audit jl-cmd/claude-dev-env PR #479 for **Category L only** (behavior-equivalence for refactors). Skip A–K, M, N. Sub-bucket forced-exhaustion mode: Category L is decomposed into 8 sub-buckets below.
77
80
 
78
- PR: refactor(hooks): tokenize-based exempt-marker recognition for the no-new-comments gate
81
+ PR: refactor(hooks): tokenize-based comment recognition
79
82
  Base SHA: (the commit before the tokenize-based rewrite landed)
80
83
  Head SHA at audit time: (the commit that landed the rewrite)
81
84
  ID prefix: `find`.
82
85
 
83
- The rewrite changed `_is_exempt_python_comment` from a normalization-based check (it ran `comment_string[1:].lstrip()` to strip the leading `#` and surrounding whitespace, then tested the body against the exempt-marker set) to a tokenize-based recognizer that tests each raw `tokenize.COMMENT` token string against `startswith("# noqa")`. The wider hook structure was left unchanged. The audit goal: identify any input shape the normalization-based BEFORE implementation accepted that the tokenize-based AFTER implementation now misclassifies as a non-exempt comment, OR any input the BEFORE rejected that the AFTER now accepts.
86
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
84
87
 
85
88
  ## Sub-buckets (each requires Shape A finding OR Shape B with ≥3 adversarial probes)
86
89
 
87
90
  **L1. KNOWN_GOOD_INPUTS table presence**
88
- - The PR ships `test_code_rules_enforcer_exempt_marker_chained.py` with 14 parametric inputs covering `# noqa`, `# pylint:`, `# pragma:`, `# type:`, `# TODO`, `# FIXME`, shebang at line 1, and a chained-comment variant. The table does NOT include the no-space variant `#noqa` — this is the first L1 gap.
89
- - Adversarial probes: scan the BEFORE implementation for the literal `"# noqa"` — production files at the base SHA carry `#noqa: F401` (no space) on at least three import lines. These are KNOWN_GOOD_INPUTS the table is missing.
91
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
92
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
90
93
 
91
94
  **L2. Whitespace / separator variants**
92
- - The BEFORE state stripped the leading `#` and surrounding whitespace from the comment text, then tested the body against the exempt-marker set — both `# noqa: F401` and `#noqa: F401` reduce to the body `noqa: F401`, which the set contains, so the BEFORE accepts both. The tokenize-based AFTER tests each raw `tokenize.COMMENT` token string against `startswith("# noqa")`, which matches only when a space separates `#` from `noqa`; `#noqa: F401` fails the prefix check. The AFTER therefore drops `#noqa` while the BEFORE accepts it — a behavior regression on the no-space axis.
93
- - Adversarial probe: construct `#noqa: F401` and trace through both. The BEFORE's strip-`#`-and-whitespace step yields body `noqa: F401`, which IS in the exempt-marker set, so the BEFORE returns True. The AFTER's `startswith("# noqa")` against the raw token `"#noqa: F401"` returns False (no space separating `#` from `noqa`) — the AFTER returns False. L2 detects a dropped accept on the no-space variant.
94
- - Per Category L's stated equivalence rule, a dropped accept is a regression: an exempt marker the BEFORE recognizes falls through to the main check under the AFTER, so the no-new-comments gate blocks writes carrying `#noqa: F401` that the BEFORE passed. Flag it P1 because the regression silently re-enables blocking on inputs production code carries.
95
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
96
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
97
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
95
98
  - The CRLF / tab variants pass through the AFTER tokenizer identically.
96
99
 
97
100
  **L3. Adjacent-form regressions**
98
- - The BEFORE pattern `startswith("# noqa")` is a 6-character prefix check. The AFTER's tokenize-based check strips `#` and surrounding whitespace, then tests the body against the exempt-marker set. The AFTER's pattern is therefore looser on the leading whitespace axis (accepts `# noqa` and `# noqa`) but no looser on the body content. Verified clean.
99
- - Adversarial probe: construct `# noqa-but-not-really: F401` — does the BEFORE startswith accept (yes, prefix match) and the AFTER's token-body check also accept (yes, the body starts with `noqa`)? Both accept; no regression.
101
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
102
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
100
103
 
101
104
  **L4. Empty / boundary inputs**
102
105
  - Empty input: BEFORE's `startswith` returns False on empty string. AFTER's `tokenize.COMMENT` token list is empty for an empty source; the iteration body never runs; the function returns False. Equivalent.
103
- - Single character `#`: BEFORE's startswith returns False (length 1 < 6 prefix); AFTER's tokenize emits a COMMENT token with string `"#"`, which the AFTER's strip-and-compare reduces to empty string, which fails the exempt-marker set membership test. Equivalent.
106
+ - Single character marker text remains a regular comment candidate under the changed-code rule. Equivalent.
104
107
 
105
108
  **L5. Invariant preservation**
106
- - BEFORE's chain `startswith("# noqa") or startswith("# pylint:") or ...` short-circuits on first match. AFTER's set-membership lookup is O(1); no iteration order. Both return True on first match. Verified clean.
109
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
107
110
 
108
111
  **L6. Implementation-tag parity**
109
- - BEFORE tag: `str.startswith` chain. AFTER tag: `tokenize.tokenize` + set-membership. The token-based AFTER picks up `# noqa` inside a string literal — wait, does it? The `tokenize.COMMENT` token type fires only for actual comment tokens, not for `#` characters inside string literals. So a string `"foo # noqa bar"` does NOT emit a COMMENT token. BEFORE's `startswith` would not have matched either (the line starts with a string literal). Verified clean.
110
- - Adversarial probe: construct an input where the comment is at end-of-line after a string literal (`x = "foo" # noqa: F401`). BEFORE's `startswith` operates on `comment.string` (the part after `#`), so it would have accepted. AFTER's tokenize emits a COMMENT token for the same trailing comment. Both accept.
112
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
113
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
111
114
 
112
- **L7. Skipped-category exhaustion**
113
- - BEFORE skipped: shebang at line 1 column 0, `# type:` with trailing justification. AFTER's skip logic must continue to apply these. The PR ships `_build_comment_token` test fixtures that exercise shebang-at-line-1 and shebang-elsewhere; the AFTER skip-list matches the BEFORE skip-list. Verified clean.
115
+ **L7. Changed-comment handling**
116
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
114
117
 
115
118
  **L8. Sibling-implementation comparison**
116
- - No sibling implementation of exempt-marker recognition exists at this SHA. L8 is verified clean — no parallel implementation.
119
+ PR: refactor(hooks): tokenize-based comment recognition
117
120
 
118
121
  ## Cross-bucket questions to answer at the end
119
122
 
120
- Q1: Yes — there is a single input class that the BEFORE accepted and the AFTER rejects: `#noqa: F401` (no space after the leading `#`). The BEFORE's `_is_exempt_python_comment` strips the leading `#` and surrounding whitespace from the comment text, yielding body `noqa: F401`, which the exempt-marker set contains, so the BEFORE returns True. The AFTER's `startswith("# noqa")` against the raw `tokenize.COMMENT` token `"#noqa: F401"` returns False (the literal prefix `"# noqa"` requires the space separating `#` from `noqa`), so the AFTER returns False. The divergence lives at the AFTER's prefix-only `startswith("# noqa")` check in `code_rules_enforcer.py::_is_exempt_python_comment` against the BEFORE's strip-and-compare step. The same L1 KNOWN_GOOD_INPUTS gap — the no-space variant absent from the table — masks this divergence at audit time, since the rewrite's parametric tests never probe it.
123
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
121
124
 
122
- Q2: Worst behavior-equivalence break candidate: the dropped accept for the no-space `#noqa` variant — the BEFORE's strip-`#`-and-whitespace logic reduces both `# noqa: F401` and `#noqa: F401` to the same body `noqa: F401` and accepts both, while the AFTER's `startswith("# noqa")` rejects `#noqa` (without the space separating `#` from `noqa`). Mark this P1 because production code at the audit SHA carries `#noqa: F401` on real import lines, so the regression silently re-enables the no-new-comments gate against writes the BEFORE passed; downgrade to P2 only when no production input carries the no-space variant.
125
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
123
126
 
124
- Q3: The next-likely behavior-equivalence break is the `# pylint:` family — the AFTER's set-membership test uses literal strings, but `pylint:` directives can carry comma-separated options that the BEFORE startswith would have accepted unchanged. Future tightening of the AFTER's set lookup could silently reject `pylint: disable=line-too-long,too-many-arguments`.
127
+ - Marker-shaped comments are findings when changed; verify whitespace variants against the changed-code rule.
125
128
 
126
129
  ## Output
127
130
 
@@ -8,9 +8,10 @@ The canonical review-criteria instruction set for every AI agent that audits pul
8
8
 
9
9
  ## COMMENT PRESERVATION
10
10
 
11
- **Keep existing comments in place.** Only evaluate comments on lines you are actively changing. Do not add new inline comments in production code — write self-documenting code. Docstrings (module/class/function) are always allowed. Test files are exempt. The hook treats the two directions differently: adding a new inline comment blocks the edit, while removing an existing comment prints a stderr advisory and lets the edit through.
11
+ Do not add code comments. Preserve existing comments. Docstrings remain allowed.
12
12
 
13
- Scaffolding and placeholder code carry a `TODO:` comment naming the permanent implementation and why the code is temporary — the documented exception to the no-new-comments rule.
13
+ When a change touches code that an existing comment describes or is attached to, remove that comment in the same change and carry its meaning through clear names and structure. Leave comments tied to untouched code unchanged. Keep comment cleanup inside the requested task.
14
+ Production and tests follow one rule. Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified.
14
15
 
15
16
  ---
16
17
 
@@ -30,7 +31,7 @@ Scaffolding and placeholder code carry a `TODO:` comment naming the permanent im
30
31
 
31
32
  no new comments · imports at top · logging format args (`log_*("...", arg)`) · no `%s`/`%d` printf tokens in a `str.format`-logger message (`log_*` imported from `automation_logging`; `str.format` drops the args — use `{}`) · no magic values in production bodies (0, 1, -1 exempt) · UPPER_SNAKE constants only in `config/` (exempt: `config/*`; `/migrations/`; Workflow registries: path contains any of these substrings — `/workflow/`, `_tab.py`, `/states.py`, or `/modules.py`, each matching independently as a substring, so `pkg/states.py` qualifies while a top-level `states.py` follows the standard `config/` rule; test files — path or filename matches `test_`, `_test.`, `.spec.`, `conftest`, or `/tests/`) · no hardcoded user home paths · guarded `sys.path.insert` · 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`, a parameter typed bare `object` whose body reads `param.attribute`) 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) · JavaScript/TypeScript boolean declarations (`const`/`let`/`var` bound to a boolean literal or negation) and `@param {boolean}` JSDoc names prefixed `is`/`has`/`should`/`can`/`was`/`did` (camelCase forms) · banned identifiers as `.mjs`/`.js` declaration names (`result`, `data`, `ctx`, `msg`, …), scoped to changed lines · in test files, banned identifiers fire on changed lines, and pytest-collectable `test_*` functions need a return annotation · unused module-level imports and unsorted import blocks are ruff's job (F401, isort I001), not this hook's · a `hooks/blocking/` command classifier anchors its multi-word command regex to the command start (`^`/`\A`) or tokenizes the first word (`shlex.split`), never matching a command as a bare substring
32
33
 
33
- 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).
34
+ Test files follow the comment policy above; other test-specific exemptions are listed here. 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).
34
35
 
35
36
  ---
36
37
 
@@ -46,17 +47,17 @@ Full words only (`context`, not `ctx`). Exceptions: `i`/`j`/`k` in loops, `e` fo
46
47
 
47
48
  ### Public compatibility definitions
48
49
 
49
- A public function may use the exact inline marker `# pragma: no-banned-noun` on its `def` line. The banned-noun check permits the function name and parameters on that definition. Body local bindings, other definitions, and similar names remain checked.
50
+ The banned-noun check applies to public function definitions, parameters, and body bindings. Use clear names instead of a directive marker.
50
51
 
51
52
  ---
52
53
 
53
54
  ## 6. COMPLETE TYPE HINTS
54
55
 
55
- ALL parameters typed, ALL returns typed. No `Any`. A `# type: ignore` is allowed only with a second trailing `#` justification of at least five characters (e.g. `# type: ignore[misc] # stubs missing in foo library`); bare ignores are blocked by `code_rules_enforcer.py`. Prefer fixing the type over an ignore when a real annotation is available.
56
+ ALL parameters typed, ALL returns typed. No `Any`. Avoid `# type: ignore`; remove it and use a typed boundary or real type. Prefer fixing the type over an ignore when a real annotation is available.
56
57
 
57
58
  ## 6.5 FILE LENGTH GUIDANCE
58
59
 
59
- Advisory only, never blocking: soft advisory at >= 400 lines, strong nudge at >= 1000 (pylint / SonarQube defaults). Split on cohesion (SRP, "Large Class" smell), not line count — run the readability rubric when an advisory fires.
60
+ Advisory only, never blocking: emit a stderr advisory at >= 400 lines and a stronger stderr advisory at >= 1000 (pylint / SonarQube defaults). Split on cohesion (SRP, "Large Class" smell), not line count — run the readability rubric when an advisory fires.
60
61
 
61
62
  ---
62
63
 
@@ -66,8 +67,6 @@ Advisory only, never blocking: soft advisory at >= 400 lines, strong nudge at >=
66
67
  Never: ABC for single impl, DI frameworks, factory for single type. Always: functions when no state, concrete classes, simple imports.
67
68
  Parameters follow YAGNI: add an optional parameter when a caller varies the value; when every call site passes the same value, make it required or inline the constant. Remove parameters no caller passes and no body reads.
68
69
 
69
- **Complexity budget — state it BEFORE implementation:** files (target 1-2, max 3), lines (~50-300). Checkpoints: Is this the MINIMUM? Can it be fewer files? Functions instead of classes?
70
-
71
70
  ## 7.5 SOLID PRINCIPLES
72
71
 
73
72
  **SRP always applies** — one reason to change per function/class/module. **OCP, LSP, ISP, DIP apply only where two or more concrete implementations already share a contract**; with a single concretion §7 wins (concrete classes, direct imports, YAGNI — introduce the abstraction at the commit that adds the second concretion). Misapplication signals: interface/ABC with exactly one implementation, SRP-splitting a cohesive class by size alone, abstract factories for one product, DI containers where every injected type has one concretion.