claude-dev-env 1.75.0 → 1.76.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 (39) hide show
  1. package/_shared/pr-loop/scripts/code_rules_gate.py +60 -5
  2. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/inline_duplicate_body_span_constants.py +22 -0
  4. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +147 -3
  5. package/docs/CODE_RULES.md +1 -1
  6. package/hooks/blocking/CLAUDE.md +2 -2
  7. package/hooks/blocking/claude_md_orphan_file_blocker.py +170 -20
  8. package/hooks/blocking/code_rules_duplicate_body.py +378 -26
  9. package/hooks/blocking/code_rules_enforcer.py +36 -5
  10. package/hooks/blocking/code_rules_imports_logging.py +679 -1
  11. package/hooks/blocking/code_rules_shared.py +8 -5
  12. package/hooks/blocking/code_rules_test_assertions.py +6 -7
  13. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +484 -0
  14. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  15. package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +157 -0
  16. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +466 -0
  17. package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +11 -9
  18. package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +758 -0
  19. package/hooks/blocking/test_code_rules_logging_printf_tokens.py +134 -0
  20. package/hooks/blocking/test_verification_verdict_store.py +66 -1
  21. package/hooks/blocking/test_verifier_verdict_minter.py +64 -5
  22. package/hooks/blocking/verification_verdict_store.py +19 -5
  23. package/hooks/blocking/verifier_verdict_minter.py +18 -15
  24. package/hooks/hooks_constants/blocking_check_limits.py +30 -1
  25. package/hooks/hooks_constants/claude_md_orphan_file_blocker_constants.py +52 -24
  26. package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -1
  27. package/hooks/hooks_constants/duplicate_function_body_constants.py +21 -5
  28. package/package.json +1 -1
  29. package/rules/claude-md-orphan-file.md +7 -8
  30. package/rules/docstring-prose-matches-implementation.md +1 -0
  31. package/rules/package-inventory-stale-entry.md +8 -0
  32. package/skills/anthropic-plan/CLAUDE.md +1 -1
  33. package/skills/anthropic-plan/SKILL.md +15 -2
  34. package/skills/autoconverge/workflow/converge.contract.test.mjs +12 -19
  35. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +71 -0
  36. package/skills/autoconverge/workflow/converge.mjs +86 -110
  37. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +58 -4
  38. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +9 -0
  39. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +42 -0
@@ -0,0 +1,134 @@
1
+ """Tests for the printf-token check on str.format-logger (automation_logging) calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ from pathlib import Path
7
+ from types import ModuleType
8
+
9
+ ENFORCER_FILENAME = "code_rules_enforcer.py"
10
+ ENFORCER_MODULE_NAME = "code_rules_enforcer_printf_under_test"
11
+ PRODUCTION_FILE_PATH = "shared_utils/web_automation/sample.py"
12
+ TEST_FILE_PATH = "shared_utils/web_automation/tests/test_sample.py"
13
+ FORMAT_LOGGER_IMPORT = (
14
+ "from shared_utils.automation_logging import log_error, log_info, log_debug\n"
15
+ )
16
+
17
+
18
+ def load_enforcer_module() -> ModuleType:
19
+ enforcer_path = Path(__file__).parent / ENFORCER_FILENAME
20
+ module_spec = importlib.util.spec_from_file_location(ENFORCER_MODULE_NAME, enforcer_path)
21
+ assert module_spec is not None
22
+ assert module_spec.loader is not None
23
+ enforcer_module = importlib.util.module_from_spec(module_spec)
24
+ module_spec.loader.exec_module(enforcer_module)
25
+ return enforcer_module
26
+
27
+
28
+ enforcer = load_enforcer_module()
29
+
30
+
31
+ def test_should_flag_percent_s_in_format_logger_call() -> None:
32
+ source = FORMAT_LOGGER_IMPORT + 'log_error("Skipping %s after error: %s", name, err)\n'
33
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
34
+ assert len(issues) == 1
35
+ assert "printf token" in issues[0]
36
+
37
+
38
+ def test_should_flag_percent_d_in_format_logger_call() -> None:
39
+ source = FORMAT_LOGGER_IMPORT + 'log_debug("attempt %d of %d", index, total)\n'
40
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
41
+ assert len(issues) == 1
42
+
43
+
44
+ def test_should_allow_brace_placeholders_in_format_logger_call() -> None:
45
+ source = FORMAT_LOGGER_IMPORT + 'log_info("Processing {} of {}", index, total)\n'
46
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
47
+ assert issues == []
48
+
49
+
50
+ def test_should_ignore_percent_token_without_format_logger_import() -> None:
51
+ source = 'log_error("Skipping %s", name)\n'
52
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
53
+ assert issues == []
54
+
55
+
56
+ def test_should_ignore_attribute_style_logger_call() -> None:
57
+ source = FORMAT_LOGGER_IMPORT + 'logger.info("delivered %s", message_id)\n'
58
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
59
+ assert issues == []
60
+
61
+
62
+ def test_should_inspect_message_literal_not_argument_value() -> None:
63
+ source = FORMAT_LOGGER_IMPORT + 'log_info("status: {}", "100%s done")\n'
64
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
65
+ assert issues == []
66
+
67
+
68
+ def test_should_exempt_test_files() -> None:
69
+ source = FORMAT_LOGGER_IMPORT + 'log_error("Skipping %s", name)\n'
70
+ issues = enforcer.check_logging_printf_tokens(source, TEST_FILE_PATH)
71
+ assert issues == []
72
+
73
+
74
+ def test_should_resolve_aliased_format_logger_import() -> None:
75
+ source = (
76
+ "from shared_utils.automation_logging import log_error as report_error\n"
77
+ + 'report_error("failed %s", name)\n'
78
+ )
79
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
80
+ assert len(issues) == 1
81
+
82
+
83
+ def test_should_allow_token_bearing_message_with_no_format_args() -> None:
84
+ source = FORMAT_LOGGER_IMPORT + 'log_info("Use %s for string substitution")\n'
85
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
86
+ assert issues == []
87
+
88
+
89
+ def test_should_allow_documentation_style_token_message_with_no_args() -> None:
90
+ source = FORMAT_LOGGER_IMPORT + 'log_warning("avoid %s-style tokens here")\n'
91
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
92
+ assert issues == []
93
+
94
+
95
+ def test_should_allow_percent_adjacent_to_word_in_format_message() -> None:
96
+ source = FORMAT_LOGGER_IMPORT + 'log_info("memory 80%free now", host)\n'
97
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
98
+ assert issues == []
99
+
100
+
101
+ def test_should_allow_doubled_percent_in_format_message() -> None:
102
+ source = FORMAT_LOGGER_IMPORT + 'log_info("100%% done", host)\n'
103
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
104
+ assert issues == []
105
+
106
+
107
+ def test_should_allow_trailing_percent_in_format_message() -> None:
108
+ source = FORMAT_LOGGER_IMPORT + 'log_info("100% done", host)\n'
109
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
110
+ assert issues == []
111
+
112
+
113
+ def test_should_flag_width_token_in_format_logger_call() -> None:
114
+ source = FORMAT_LOGGER_IMPORT + 'log_error("item %5d", index)\n'
115
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
116
+ assert len(issues) == 1
117
+
118
+
119
+ def test_should_flag_precision_token_in_format_logger_call() -> None:
120
+ source = FORMAT_LOGGER_IMPORT + 'log_error("took %0.2f sec", elapsed)\n'
121
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
122
+ assert len(issues) == 1
123
+
124
+
125
+ def test_should_flag_float_general_token_in_format_logger_call() -> None:
126
+ source = FORMAT_LOGGER_IMPORT + 'log_error("ratio %g", ratio)\n'
127
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
128
+ assert len(issues) == 1
129
+
130
+
131
+ def test_should_flag_scientific_token_in_format_logger_call() -> None:
132
+ source = FORMAT_LOGGER_IMPORT + 'log_error("value %e", measurement)\n'
133
+ issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
134
+ assert len(issues) == 1
@@ -418,7 +418,7 @@ def test_workflow_verdict_covers_surface_false_for_non_verifier_sidecar(
418
418
  )
419
419
 
420
420
 
421
- def test_workflow_verdict_covers_surface_false_for_missing_sidecar(
421
+ def test_workflow_verdict_covers_surface_false_for_missing_sidecar_with_verdict(
422
422
  tmp_path: pathlib.Path,
423
423
  ) -> None:
424
424
  transcript_path = _session_transcript_path(tmp_path, "sess1")
@@ -438,6 +438,71 @@ def test_workflow_verdict_covers_surface_false_for_missing_sidecar(
438
438
  )
439
439
 
440
440
 
441
+ def test_workflow_verdict_covers_surface_false_for_corrupt_sidecar_with_verdict(
442
+ tmp_path: pathlib.Path,
443
+ ) -> None:
444
+ transcript_path = _session_transcript_path(tmp_path, "sess1")
445
+ subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
446
+ _write_agent_transcript(
447
+ subagents_dir,
448
+ "01",
449
+ VERIFIER_AGENT_TYPE,
450
+ _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
451
+ should_write_sidecar=False,
452
+ )
453
+ corrupt_sidecar = (
454
+ subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
455
+ )
456
+ corrupt_sidecar.write_text("{not valid json", encoding="utf-8")
457
+ assert (
458
+ workflow_verdict_covers_surface(
459
+ str(transcript_path), MATCHING_MANIFEST_SHA256
460
+ )
461
+ is False
462
+ )
463
+
464
+
465
+ def test_workflow_verdict_covers_surface_false_for_invalid_utf8_sidecar(
466
+ tmp_path: pathlib.Path,
467
+ ) -> None:
468
+ transcript_path = _session_transcript_path(tmp_path, "sess1")
469
+ subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
470
+ _write_agent_transcript(
471
+ subagents_dir,
472
+ "01",
473
+ VERIFIER_AGENT_TYPE,
474
+ _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
475
+ should_write_sidecar=False,
476
+ )
477
+ invalid_utf8_sidecar = subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
478
+ invalid_utf8_sidecar.write_bytes(b'{"agentType": "\xff\xfe bad"}')
479
+ assert (
480
+ workflow_verdict_covers_surface(
481
+ str(transcript_path), MATCHING_MANIFEST_SHA256
482
+ )
483
+ is False
484
+ )
485
+
486
+
487
+ def test_workflow_verdict_covers_surface_false_for_invalid_utf8_transcript(
488
+ tmp_path: pathlib.Path,
489
+ ) -> None:
490
+ transcript_path = _session_transcript_path(tmp_path, "sess1")
491
+ subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
492
+ workflow_dir = subagents_dir / "workflows" / "wf_x"
493
+ workflow_dir.mkdir(parents=True, exist_ok=True)
494
+ (workflow_dir / "agent-01.jsonl").write_bytes(b'{"type": "assistant"}\xff\xfe\n')
495
+ (workflow_dir / "agent-01.meta.json").write_text(
496
+ json.dumps({"agentType": VERIFIER_AGENT_TYPE}), encoding="utf-8"
497
+ )
498
+ assert (
499
+ workflow_verdict_covers_surface(
500
+ str(transcript_path), MATCHING_MANIFEST_SHA256
501
+ )
502
+ is False
503
+ )
504
+
505
+
441
506
  def test_workflow_verdict_covers_surface_false_for_missing_subagents_dir(
442
507
  tmp_path: pathlib.Path,
443
508
  ) -> None:
@@ -7,11 +7,13 @@ SubagentStop payload names the stopping subagent's own transcript
7
7
  tests build that sidecar and assert the minter gates on the resolved type and
8
8
  on the shared MINTING_AGENT_TYPE constant, so a rename in config propagates to
9
9
  the minter without a second edit. A malformed or non-string sidecar resolves
10
- nothing, and an absent sidecar mints nothing the main session writes neither
11
- the transcript nor the sidecar, so it cannot forge a passing verdict. A
12
- further test holds the shipped settings.json to the minter docstring's
13
- anti-forgery claim: the main session is denied writes to the verdict
14
- directory, so only this hook can mint a passing verdict.
10
+ nothing, and an absent sidecar mints nothing even when the transcript carries a
11
+ verdict fence the main session controls the prompt of any Agent-tool subagent
12
+ it spawns, so a verdict fence in the transcript proves nothing about the agent
13
+ type, and only the harness-written sidecar attests a code-verifier. A further
14
+ test holds the shipped settings.json to the minter docstring's anti-forgery
15
+ claim: the main session is denied writes to the verdict directory, so only this
16
+ hook can mint a passing verdict.
15
17
  """
16
18
 
17
19
  import importlib.util
@@ -290,6 +292,63 @@ def test_minter_refuses_when_recomputed_surface_is_empty(
290
292
  assert mint_for_payload(payload) is None
291
293
 
292
294
 
295
+ def test_resolves_none_when_sidecar_absent_even_with_verdict_fence(
296
+ tmp_path: pathlib.Path,
297
+ ) -> None:
298
+ agent_transcript = tmp_path / "agent-7.jsonl"
299
+ agent_transcript.write_text(
300
+ json.dumps(
301
+ {
302
+ "type": "assistant",
303
+ "message": {
304
+ "content": [
305
+ {
306
+ "type": "text",
307
+ "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
308
+ }
309
+ ]
310
+ },
311
+ }
312
+ )
313
+ + "\n",
314
+ encoding="utf-8",
315
+ )
316
+ payload = {"agent_transcript_path": str(agent_transcript)}
317
+ assert resolved_subagent_type(payload) is None
318
+
319
+
320
+ def test_does_not_mint_when_sidecar_absent_but_transcript_has_verdict(
321
+ tmp_path: pathlib.Path,
322
+ ) -> None:
323
+ repo_root = tmp_path / "repo"
324
+ repo_root.mkdir()
325
+ _init_repo_with_upstream_and_edit(repo_root)
326
+ agent_transcript = tmp_path / "agent-7.jsonl"
327
+ agent_transcript.write_text(
328
+ json.dumps(
329
+ {
330
+ "type": "assistant",
331
+ "message": {
332
+ "content": [
333
+ {
334
+ "type": "text",
335
+ "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
336
+ }
337
+ ]
338
+ },
339
+ }
340
+ )
341
+ + "\n",
342
+ encoding="utf-8",
343
+ )
344
+ payload = {
345
+ "agent_transcript_path": str(agent_transcript),
346
+ "cwd": str(repo_root),
347
+ "agent_id": "no-sidecar-1",
348
+ }
349
+ assert mint_for_payload(payload) is None
350
+
351
+
293
352
  def test_attested_manifest_hash_binds_over_cwd_surface(tmp_path: pathlib.Path) -> None:
294
353
  repo_root = tmp_path / "repo"
295
354
  repo_root.mkdir()
@@ -423,17 +423,29 @@ def _subagents_directory_for_transcript(transcript_path: str) -> Path | None:
423
423
  def _agent_type_for_transcript(transcript_file: Path) -> str | None:
424
424
  """Read an agent transcript's sidecar to learn the agent type it ran as.
425
425
 
426
+ The agent type comes solely from the harness-written
427
+ ``agent-<id>.meta.json`` sidecar, which only the harness writes — the main
428
+ session can neither write the sidecar nor forge the agentType it records. A
429
+ verdict fence in the transcript is never used to infer the type, because the
430
+ main session controls the prompt of any Agent-tool subagent it spawns and
431
+ could instruct one to print such a fence.
432
+
426
433
  Args:
427
434
  transcript_file: An ``agent-*.jsonl`` transcript path.
428
435
 
429
436
  Returns:
430
- The ``agentType`` recorded in the ``<stem>.meta.json`` sidecar, or
431
- None when the sidecar is missing, unreadable, or carries no type.
437
+ The ``agentType`` recorded in the ``<stem>.meta.json`` sidecar, or None
438
+ when the sidecar is absent, unreadable, or unparseable, it does not hold
439
+ a JSON object, or it names no string ``agentType``.
432
440
  """
433
441
  sidecar_file = transcript_file.with_suffix(AGENT_META_SIDECAR_SUFFIX)
434
442
  try:
435
- sidecar_record = json.loads(sidecar_file.read_text(encoding="utf-8"))
436
- except (OSError, json.JSONDecodeError):
443
+ sidecar_text = sidecar_file.read_text(encoding="utf-8")
444
+ except (OSError, UnicodeDecodeError):
445
+ return None
446
+ try:
447
+ sidecar_record = json.loads(sidecar_text)
448
+ except json.JSONDecodeError:
437
449
  return None
438
450
  if not isinstance(sidecar_record, dict):
439
451
  return None
@@ -452,7 +464,9 @@ def _assistant_text_blocks(transcript_file: Path) -> list[str]:
452
464
  when the file is missing, unreadable, or holds no assistant text.
453
465
  """
454
466
  try:
455
- transcript_lines = transcript_file.read_text(encoding="utf-8").splitlines()
467
+ transcript_lines = (
468
+ transcript_file.read_text(encoding="utf-8", errors="replace").splitlines()
469
+ )
456
470
  except OSError:
457
471
  return []
458
472
  all_text_blocks: list[str] = []
@@ -6,14 +6,14 @@ SubagentStop payload names the stopping subagent's own transcript
6
6
  (``agent_transcript_path``), which sits beside a harness-written
7
7
  ``agent-<id>.meta.json`` sidecar naming the spawning ``agentType``. The hook
8
8
  reads that type from the sidecar, so it resolves identically in interactive,
9
- background, and worktree-switched sessions. When that type is
9
+ background, and worktree-switched sessions; only the harness writes the
10
+ sidecar, so the type it records cannot be forged. When that type is
10
11
  ``code-verifier``, the hook pulls the verdict block out of the agent's own
11
- transcript (``agent_transcript_path``); the main session writes neither that
12
- transcript nor the sidecar, so text it prints can never mint — recomputes the
13
- live change-surface hash for the session repository, and writes the verdict
14
- bound to that hash. The companion ``verified_commit_gate.py`` (PreToolUse)
15
- then allows ``git commit`` / ``git push`` only while the work tree still
16
- matches the verified state.
12
+ transcript (``agent_transcript_path``), recomputes the live change-surface
13
+ hash for the session repository, and writes the verdict bound to that hash.
14
+ The companion ``verified_commit_gate.py`` (PreToolUse) then allows ``git
15
+ commit`` / ``git push`` only while the work tree still matches the verified
16
+ state.
17
17
 
18
18
  The verifier's final message must end with a fenced block::
19
19
 
@@ -125,7 +125,8 @@ def _agent_type_from_meta_sidecar(agent_transcript_path: str) -> str | None:
125
125
  ``agent-<id>.meta.json`` naming the spawning ``agentType``. Reading the type
126
126
  from this sidecar binds it to the stopping subagent itself, so it resolves
127
127
  identically in interactive, background, and worktree-switched sessions and
128
- needs no parent-transcript scan or flush retry.
128
+ needs no parent-transcript scan or flush retry. Only the harness writes this
129
+ sidecar, so a session cannot forge the type it reports.
129
130
 
130
131
  Args:
131
132
  agent_transcript_path: The stopping subagent's own transcript path from
@@ -133,7 +134,7 @@ def _agent_type_from_meta_sidecar(agent_transcript_path: str) -> str | None:
133
134
 
134
135
  Returns:
135
136
  The recorded ``agentType``, or None when the path is empty, the sidecar
136
- is absent or cannot be read or parsed, it does not hold a JSON object,
137
+ is absent, unreadable, or unparseable, it does not hold a JSON object,
137
138
  or it names no string ``agentType``.
138
139
  """
139
140
  if not agent_transcript_path:
@@ -155,17 +156,19 @@ def resolved_subagent_type(subagent_stop_payload: dict) -> str | None:
155
156
 
156
157
  The stopping subagent's own transcript (``agent_transcript_path``) sits
157
158
  beside a harness-written ``agent-<id>.meta.json`` sidecar naming its
158
- ``agentType``. Reading the type from that sidecar binds it to the subagent
159
- itself, so it resolves the same across interactive, background, and
160
- worktree-switched sessions.
159
+ ``agentType``. The type comes solely from that sidecar, which only the
160
+ harness writes the main session can neither write the sidecar nor forge
161
+ the agentType it records. A verdict fence in the transcript is never used to
162
+ infer the type, because the main session controls the prompt of any
163
+ Agent-tool subagent it spawns and could instruct one to print such a fence.
161
164
 
162
165
  Args:
163
166
  subagent_stop_payload: The SubagentStop hook payload.
164
167
 
165
168
  Returns:
166
- The agent type this subagent was spawned with, or None when the
167
- ``agent_transcript_path`` is empty, the sidecar is absent or cannot be
168
- read or parsed, it does not hold a JSON object, or it names no string
169
+ The agent type the sidecar records, or None when the
170
+ ``agent_transcript_path`` is empty, the sidecar is absent, unreadable,
171
+ or unparseable, it does not hold a JSON object, or it names no string
169
172
  ``agentType``.
170
173
  """
171
174
  return _agent_type_from_meta_sidecar(
@@ -7,7 +7,6 @@ under the file-global-constants use-count rule (CODE_RULES §file-global-constan
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
-
11
10
  MAX_BANNED_PREFIX_ISSUES: int = 3
12
11
  MAX_STUB_IMPLEMENTATION_ISSUES: int = 3
13
12
  MAX_TYPED_DICT_PAIR_ISSUES: int = 3
@@ -24,8 +23,27 @@ MAX_TYPE_ESCAPE_HATCH_ISSUES: int = 5
24
23
  MAX_THIN_WRAPPER_ISSUES: int = 1
25
24
  MAX_ZERO_PAYLOAD_ALIAS_ISSUES: int = 3
26
25
  MAX_LOGGING_FSTRING_ISSUES: int = 3
26
+ MAX_LOGGING_PRINTF_TOKEN_ISSUES: int = 3
27
27
  MAX_WINDOWS_API_NONE_ISSUES: int = 3
28
28
  MAX_E2E_TEST_NAMING_ISSUES: int = 3
29
+ MAX_IMPORT_BLOCK_SORT_ISSUES: int = 1
30
+ IMPORT_BLOCK_SORT_RUFF_TIMEOUT_SECONDS: int = 15
31
+ IMPORT_BLOCK_SORT_RULE_CODE: str = "I001"
32
+ RUFF_STDIN_ENCODING: str = "utf-8"
33
+ RUFF_PYPROJECT_CONFIG_FILENAME: str = "pyproject.toml"
34
+ RUFF_PYPROJECT_TOOL_TABLE_MARKER: str = "[tool.ruff"
35
+ ALL_RUFF_STANDALONE_CONFIG_FILENAMES: tuple[str, ...] = ("ruff.toml", ".ruff.toml")
36
+ ALL_IMPORT_BLOCK_SORT_RUFF_COMMAND_PREFIX: tuple[str, ...] = (
37
+ "ruff",
38
+ "check",
39
+ "--select",
40
+ "I001",
41
+ "--no-cache",
42
+ "--output-format",
43
+ "json",
44
+ )
45
+ MAX_JS_RESUME_TASK_ENUMERATION_ISSUES: int = 5
46
+ MINIMUM_RESUME_TASK_ENUMERATION_ITEMS: int = 2
29
47
  DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT: int = 3
30
48
  MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES: int = 3
31
49
  DOCSTRING_FALLBACK_BRANCH_MINIMUM_ROUTE_COUNT: int = 2
@@ -85,6 +103,17 @@ DOCSTRING_REFERENCE_MARKER_WINDOW: int = 2
85
103
  ALL_GENERIC_CHECK_NAME_TOKENS: frozenset[str] = frozenset(
86
104
  {"check", "checks", "test", "tests", "in", "for", "and", "the"}
87
105
  )
106
+ ALL_FORMAT_LOGGER_FUNCTION_NAMES: frozenset[str] = frozenset(
107
+ {
108
+ "log_debug",
109
+ "log_info",
110
+ "log_ok",
111
+ "log_error",
112
+ "log_warning",
113
+ "log_batch",
114
+ "log_background",
115
+ }
116
+ )
88
117
 
89
118
  ALL_DOCSTRING_NO_CONSUMER_CLAIM_PHRASES: tuple[str, ...] = (
90
119
  "no consumer reads",
@@ -1,15 +1,18 @@
1
1
  """Constants for the per-directory CLAUDE.md orphan-file-reference blocker.
2
2
 
3
3
  A per-directory ``CLAUDE.md`` documents the files reachable from its own
4
- directory in a markdown table whose first column names each file in backticks.
5
- When a first-column cell names a bare filename that exists nowhere under the scan
4
+ directory in a markdown table whose first column names each file in backticks,
5
+ and shows run commands inside fenced code blocks that invoke those files. When a
6
+ first-column cell, or an interpreter invocation inside a run-command fence
7
+ (``python script.py``), names a bare filename that exists nowhere under the scan
6
8
  root (the CLAUDE.md directory's parent, covering the directory, its
7
- subdirectories, and its siblings), the table points a reader at a file that is
8
- not there. This module holds the patterns that find those cells, the filename
9
- extensions that mark a cell as a file reference, the region-boundary marker that
10
- scopes a prose region to one section, the relative-path marker that exempts a
11
- cross-directory table block, the directory names the subtree walk prunes, the
12
- subtree scan budget, and the block-message text the hook emits.
9
+ subdirectories, and its siblings), the doc points a reader at a file that is not
10
+ there. This module holds the patterns that find those cells and run-command
11
+ invocations, the filename extensions that mark a cell or invocation as a file
12
+ reference, the region-boundary marker that scopes a prose region to one section,
13
+ the relative-path marker that exempts a cross-directory table block, the
14
+ directory names the subtree walk prunes, the subtree scan budget, and the
15
+ block-message text the hook emits.
13
16
  """
14
17
 
15
18
  import re
@@ -22,7 +25,9 @@ __all__ = [
22
25
  "SEPARATOR_CELL_PATTERN",
23
26
  "REGION_BOUNDARY_PATTERN",
24
27
  "RELATIVE_PATH_SOURCE_PATTERN",
28
+ "RUN_COMMAND_SCRIPT_PATTERN",
25
29
  "ALL_REFERENCED_FILE_EXTENSIONS",
30
+ "ALL_RUN_COMMAND_SCRIPT_EXTENSIONS",
26
31
  "ALL_NOISE_DIRECTORY_NAMES",
27
32
  "MAX_SUBTREE_FILES_SCANNED",
28
33
  "MAX_ORPHAN_FILE_ISSUES",
@@ -45,6 +50,13 @@ REGION_BOUNDARY_PATTERN: re.Pattern[str] = re.compile(r"^\s*#")
45
50
 
46
51
  RELATIVE_PATH_SOURCE_PATTERN: re.Pattern[str] = re.compile(r"\.\.[\\/]")
47
52
 
53
+ RUN_COMMAND_SCRIPT_PATTERN: re.Pattern[str] = re.compile(
54
+ r"(?<![\w.])(?:python(?:\.exe)?|python3|node|pwsh|powershell(?:\.exe)?|bash|sh|ruby|perl)"
55
+ r"\b(?:\s+-\S+(?:\s+[\w./\\][\w./\\-]*(?=\s+\S))?)*"
56
+ r"\s+[\"']?"
57
+ r"([\w./\\-]+\.(?:py|mjs|js|ts|ps1|sh|rb|pl))\b"
58
+ )
59
+
48
60
  ALL_REFERENCED_FILE_EXTENSIONS: frozenset[str] = frozenset(
49
61
  {
50
62
  ".py",
@@ -66,6 +78,19 @@ ALL_REFERENCED_FILE_EXTENSIONS: frozenset[str] = frozenset(
66
78
  }
67
79
  )
68
80
 
81
+ ALL_RUN_COMMAND_SCRIPT_EXTENSIONS: frozenset[str] = frozenset(
82
+ {
83
+ ".py",
84
+ ".mjs",
85
+ ".js",
86
+ ".ts",
87
+ ".ps1",
88
+ ".sh",
89
+ ".rb",
90
+ ".pl",
91
+ }
92
+ )
93
+
69
94
  ALL_NOISE_DIRECTORY_NAMES: frozenset[str] = frozenset(
70
95
  {
71
96
  ".git",
@@ -81,27 +106,30 @@ MAX_SUBTREE_FILES_SCANNED: int = 5000
81
106
  MAX_ORPHAN_FILE_ISSUES: int = 20
82
107
 
83
108
  ORPHAN_FILE_MESSAGE_TEMPLATE: str = (
84
- "CLAUDE.md table references files that exist nowhere under {directory}: "
85
- "{missing}. A per-directory CLAUDE.md table names files in its own directory "
86
- "subtree; a first-column cell naming a file absent from that subtree points a "
87
- "reader at something that is not there. Drop the row, or correct the cell to "
88
- "name a file that exists in this directory, a subdirectory of it, or a sibling "
89
- "directory under its parent."
109
+ "CLAUDE.md references files that exist nowhere under {directory}: "
110
+ "{missing}. A per-directory CLAUDE.md names files in its own directory "
111
+ "subtree, both in its table cells and in the run commands its fenced code "
112
+ "blocks show; a cell or a run command naming a file absent from that subtree "
113
+ "points a reader at something that is not there. Drop the row or run command, "
114
+ "or correct it to name a file that exists in this directory, a subdirectory of "
115
+ "it, or a sibling directory under its parent."
90
116
  )
91
117
 
92
118
  ORPHAN_FILE_SYSTEM_MESSAGE: str = (
93
- "CLAUDE.md table names a file that does not exist in its directory subtree - "
94
- "drop the row or name an existing file"
119
+ "CLAUDE.md names a file that does not exist in its directory subtree - "
120
+ "drop the row or run command, or name an existing file"
95
121
  )
96
122
 
97
123
  ORPHAN_FILE_ADDITIONAL_CONTEXT: str = (
98
124
  "Each first-column table cell wrapped in backticks that ends in a known file "
99
- "extension must name a file present under the scan root: this CLAUDE.md's own "
100
- "directory, a subdirectory of it, or a sibling directory under its parent. "
101
- "Cells holding a path with a slash, a subdirectory ending in '/', or a "
102
- "slash-command are out of scope. A table whose own block names an explicit "
103
- "relative-path source (a '../' token) documents files outside the subtree and "
104
- "is out of scope. For each missing file:\n"
105
- " - delete the table row, or\n"
106
- " - rename the cell to an existing file under the scan root."
125
+ "extension, and each script an interpreter invocation inside a fenced run "
126
+ "command names (such as 'python script.py'), must name a file present under "
127
+ "the scan root: this CLAUDE.md's own directory, a subdirectory of it, or a "
128
+ "sibling directory under its parent. Cells holding a path with a slash, a "
129
+ "subdirectory ending in '/', or a slash-command are out of scope. A table "
130
+ "whose own block names an explicit relative-path source (a '../' token) "
131
+ "documents files outside the subtree and is out of scope. For each missing "
132
+ "file:\n"
133
+ " - delete the table row or the run command, or\n"
134
+ " - rename it to an existing file under the scan root."
107
135
  )
@@ -13,7 +13,7 @@ ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS: tuple[type[BaseException], ...] = (
13
13
  )
14
14
 
15
15
  ALL_PYTHON_EXTENSIONS = {".py"}
16
- ALL_JAVASCRIPT_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx"}
16
+ ALL_JAVASCRIPT_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs", ".mts", ".cts"}
17
17
  ALL_CODE_EXTENSIONS = ALL_PYTHON_EXTENSIONS | ALL_JAVASCRIPT_EXTENSIONS
18
18
 
19
19
  ALL_TEST_PATH_PATTERNS = {"test_", "_test.", ".test.", ".spec.", "/tests/", "\\tests\\", "/tests.py", "\\tests.py"}
@@ -119,6 +119,46 @@ LOGGING_FSTRING_PATTERN = re.compile(
119
119
  r'|(?:logger|logging|log)\.(?:debug|info|warning|error|critical|exception))'
120
120
  r'\s*\(\s*(?:[rR][fF]|[fF][rR]?)["\']'
121
121
  )
122
+ LOGGING_PRINTF_TOKEN_PATTERN: re.Pattern[str] = re.compile(
123
+ r"(?<!%)%[#0\- +]?[0-9.*]*[sdrixfgeEcoX](?![a-zA-Z])"
124
+ )
125
+ MINIMUM_FORMAT_LOGGER_ARGUMENT_COUNT = 2
126
+ SPAWN_AGENT_WITH_JSDOC_PATTERN: re.Pattern[str] = re.compile(
127
+ r"/\*\*(?P<jsdoc>(?:(?!\*/).)*?)\*/\s*"
128
+ r"(?:async\s+)?function\s+spawn(?P<role>\w+?)Agent\s*\(",
129
+ re.DOTALL,
130
+ )
131
+ RESUME_TASK_ENUMERATION_PATTERN: re.Pattern[str] = re.compile(
132
+ r"(?<![A-Za-z])resume\s*\((?P<enumeration>[^)]*?)\)",
133
+ re.DOTALL,
134
+ )
135
+ TASK_DISPATCH_NAME_PATTERN: re.Pattern[str] = re.compile(
136
+ r"""(?<![A-Za-z0-9_])task\s*===\s*['"](?P<task>[a-z0-9-]+)['"]"""
137
+ )
138
+ ENUMERATION_LIST_ITEM_SEPARATOR_PATTERN: re.Pattern[str] = re.compile(
139
+ r"\s*,\s*|\s+and\s+"
140
+ )
141
+ ENUMERATION_LEADING_CONJUNCTION_PATTERN: re.Pattern[str] = re.compile(
142
+ r"^and\s+"
143
+ )
144
+ ALL_JAVASCRIPT_STRING_DELIMITERS: frozenset[str] = frozenset({"'", '"', "`"})
145
+ JAVASCRIPT_STRING_ESCAPE_CHARACTER: str = "\\"
146
+ JAVASCRIPT_LINE_COMMENT_OPENER: str = "//"
147
+ JAVASCRIPT_BLOCK_COMMENT_OPENER: str = "/*"
148
+ JAVASCRIPT_BLOCK_COMMENT_CLOSER: str = "*/"
149
+ JAVASCRIPT_REGEX_DELIMITER: str = "/"
150
+ ALL_JAVASCRIPT_REGEX_PRECEDING_CHARACTERS: frozenset[str] = frozenset(
151
+ {"(", ",", "=", ":", "[", "{", "}", ";", "!", "&", "|", "?", "+", "-", "*", "%", "<", ">", "~", "^", "\n"}
152
+ )
153
+ ALL_JAVASCRIPT_REGEX_PRECEDING_KEYWORDS: frozenset[str] = frozenset(
154
+ {"return", "typeof", "case", "in", "of", "do", "else", "void", "delete", "instanceof", "new", "yield", "await", "throw"}
155
+ )
156
+ ENUMERATION_TASK_ITEM_PATTERN: re.Pattern[str] = re.compile(
157
+ r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
158
+ )
159
+ HYPHENATED_TASK_ITEM_PATTERN: re.Pattern[str] = re.compile(
160
+ r"^[a-z0-9]+(?:-[a-z0-9]+)+$"
161
+ )
122
162
  ALL_BUILTIN_DICT_METHOD_NAMES: frozenset[str] = frozenset({
123
163
  "get", "items", "keys", "values", "update", "pop",
124
164
  "setdefault", "copy", "clear",