claude-dev-env 1.85.0 → 1.86.1

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 (33) hide show
  1. package/_shared/pr-loop/scripts/code_rules_gate.py +18 -1
  2. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +142 -7
  3. package/_shared/pr-loop/scripts/terminology_sweep.py +288 -92
  4. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +89 -0
  5. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +180 -91
  6. package/agents/code-quality-agent.md +1 -1
  7. package/hooks/blocking/CLAUDE.md +1 -0
  8. package/hooks/blocking/code_rules_dead_module_constant.py +7 -4
  9. package/hooks/blocking/session_edit_stage_gate.py +654 -0
  10. package/hooks/blocking/test_session_edit_stage_gate.py +537 -0
  11. package/hooks/hooks.json +30 -0
  12. package/hooks/hooks_constants/CLAUDE.md +2 -0
  13. package/hooks/hooks_constants/session_edit_stage_gate_constants.py +92 -0
  14. package/hooks/hooks_constants/task_list_loop_starter_constants.py +18 -0
  15. package/hooks/lifecycle/CLAUDE.md +1 -1
  16. package/hooks/observability/CLAUDE.md +2 -0
  17. package/hooks/observability/session_file_edit_tracker.py +224 -0
  18. package/hooks/observability/test_session_file_edit_tracker.py +174 -0
  19. package/hooks/session/CLAUDE.md +6 -2
  20. package/hooks/session/session_edit_tracker_cleanup.py +120 -0
  21. package/hooks/session/task_list_loop_starter.py +36 -0
  22. package/hooks/session/test_session_edit_tracker_cleanup.py +157 -0
  23. package/hooks/session/test_task_list_loop_starter.py +53 -0
  24. package/package.json +1 -1
  25. package/rules/CLAUDE.md +1 -0
  26. package/rules/re-stage-before-commit.md +31 -0
  27. package/skills/autoconverge/SKILL.md +4 -4
  28. package/skills/autoconverge/reference/gotchas.md +3 -2
  29. package/skills/autoconverge/reference/stop-conditions.md +22 -6
  30. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +528 -1
  31. package/skills/autoconverge/workflow/converge.contract.test.mjs +29 -21
  32. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +77 -8
  33. package/skills/autoconverge/workflow/converge.mjs +456 -59
@@ -64,6 +64,7 @@ def run_git_in_repository(repository_root: Path, *arguments: str) -> str:
64
64
  encoding="utf-8",
65
65
  errors="replace",
66
66
  check=True,
67
+ env=gate_module.repository_environment(),
67
68
  )
68
69
  return completion.stdout
69
70
 
@@ -2329,3 +2330,91 @@ def test_parse_arguments_applies_documented_defaults() -> None:
2329
2330
  assert parsed_arguments.repo_root is None
2330
2331
  assert parsed_arguments.only_under == []
2331
2332
  assert parsed_arguments.paths == []
2333
+
2334
+
2335
+ def test_staged_mode_resolves_scan_roots_from_outside_the_repository(
2336
+ temporary_git_repository: Path,
2337
+ monkeypatch: pytest.MonkeyPatch,
2338
+ tmp_path_factory: pytest.TempPathFactory,
2339
+ ) -> None:
2340
+ package_directory = temporary_git_repository / "pkg"
2341
+ config_directory = package_directory / "config"
2342
+ write_file(package_directory / "__init__.py", "")
2343
+ write_file(config_directory / "__init__.py", "")
2344
+ write_file(
2345
+ config_directory / "pkg_constants.py",
2346
+ '"""Frame constants."""\n\nFRAME_COUNT: int = 9\n',
2347
+ )
2348
+ write_file(
2349
+ package_directory / "consumer.py",
2350
+ '"""Consumer of the frame constants."""\n\n'
2351
+ "from pkg.config.pkg_constants import FRAME_COUNT\n\n"
2352
+ "frame_total = FRAME_COUNT\n",
2353
+ )
2354
+ commit_all_files(temporary_git_repository, "initial")
2355
+ write_file(
2356
+ config_directory / "pkg_constants.py",
2357
+ '"""Frame constants."""\n\nFRAME_COUNT: int = 9\nPLATE_COUNT: int = 4\n',
2358
+ )
2359
+ write_file(
2360
+ package_directory / "consumer.py",
2361
+ '"""Consumer of the frame constants."""\n\n'
2362
+ "from pkg.config.pkg_constants import FRAME_COUNT, PLATE_COUNT\n\n"
2363
+ "frame_total = FRAME_COUNT + PLATE_COUNT\n",
2364
+ )
2365
+ stage_file(temporary_git_repository, "pkg/config/pkg_constants.py")
2366
+ stage_file(temporary_git_repository, "pkg/consumer.py")
2367
+
2368
+ monkeypatch.chdir(tmp_path_factory.mktemp("elsewhere"))
2369
+ exit_code = gate_module.main(
2370
+ ["--repo-root", str(temporary_git_repository), "--staged"]
2371
+ )
2372
+
2373
+ assert exit_code == 0
2374
+
2375
+
2376
+ def test_paths_from_git_staged_ignores_inherited_git_environment(
2377
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
2378
+ ) -> None:
2379
+ target_repository = tmp_path / "target_repository"
2380
+ target_repository.mkdir()
2381
+ initialize_git_repository(target_repository)
2382
+ write_file(target_repository / "pkg" / "example_module.py", "EXAMPLE = 1\n")
2383
+ stage_file(target_repository, "pkg/example_module.py")
2384
+ victim_repository = tmp_path / "victim_repository"
2385
+ victim_repository.mkdir()
2386
+ initialize_git_repository(victim_repository)
2387
+ monkeypatch.setenv("GIT_DIR", str(victim_repository / ".git"))
2388
+ monkeypatch.setenv("GIT_WORK_TREE", str(victim_repository))
2389
+ monkeypatch.setenv("GIT_INDEX_FILE", str(victim_repository / ".git" / "index"))
2390
+
2391
+ all_staged_names = [
2392
+ each_path.name
2393
+ for each_path in gate_module.paths_from_git_staged(target_repository)
2394
+ ]
2395
+
2396
+ assert all_staged_names == ["example_module.py"]
2397
+
2398
+
2399
+ def test_run_staged_test_files_runs_pytest_without_git_environment(
2400
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
2401
+ ) -> None:
2402
+ repository_root = tmp_path / "repository_under_test"
2403
+ repository_root.mkdir()
2404
+ initialize_git_repository(repository_root)
2405
+ staged_test_body = (
2406
+ "import os\n"
2407
+ "\n"
2408
+ "\n"
2409
+ "def test_environment_carries_no_git_variables() -> None:\n"
2410
+ " all_git_names = sorted(\n"
2411
+ " each_name for each_name in os.environ"
2412
+ " if each_name.startswith('GIT_')\n"
2413
+ " )\n"
2414
+ " assert all_git_names == []\n"
2415
+ )
2416
+ write_file(repository_root / "test_environment_scrub.py", staged_test_body)
2417
+ stage_file(repository_root, "test_environment_scrub.py")
2418
+ monkeypatch.setenv("GIT_AUTHOR_NAME", "Hostile Hook Environment")
2419
+
2420
+ assert gate_module.run_staged_test_files(repository_root) == 0
@@ -5,6 +5,8 @@ import subprocess
5
5
  from pathlib import Path
6
6
  from types import ModuleType
7
7
 
8
+ import pytest
9
+
8
10
 
9
11
  def _load_sweep_module() -> ModuleType:
10
12
  module_path = Path(__file__).parent.parent / "terminology_sweep.py"
@@ -23,7 +25,7 @@ main = sweep_module.main
23
25
  parse_added_lines = sweep_module._parse_added_lines
24
26
 
25
27
 
26
- _git_environment = sweep_module._git_environment
28
+ _hermetic_git_environment = sweep_module.repository_environment
27
29
 
28
30
 
29
31
  def _init_git_repository(repository_path: Path) -> None:
@@ -37,7 +39,7 @@ def _init_git_repository(repository_path: Path) -> None:
37
39
  cwd=repository_path,
38
40
  check=True,
39
41
  capture_output=True,
40
- env=_git_environment(),
42
+ env=_hermetic_git_environment(),
41
43
  )
42
44
 
43
45
 
@@ -200,7 +202,7 @@ def test_does_not_flag_y_to_ies_plural_variant() -> None:
200
202
  assert sweep_diff(diff) == []
201
203
 
202
204
 
203
- def test_does_not_flag_spaced_prose_sharing_identifier_prefix() -> None:
205
+ def test_still_flags_non_plural_divergent_tail() -> None:
204
206
  diff = (
205
207
  "diff --git a/api/quota.py b/api/quota.py\n"
206
208
  "--- a/api/quota.py\n"
@@ -213,102 +215,23 @@ def test_does_not_flag_spaced_prose_sharing_identifier_prefix() -> None:
213
215
  "@@ -0,0 +1,1 @@\n"
214
216
  "+The premium request budget gates the run.\n"
215
217
  )
216
- assert sweep_diff(diff) == []
217
-
218
-
219
- def test_does_not_flag_ordinary_sentence_sharing_loop_variable_prefix() -> None:
220
- diff = (
221
- "diff --git a/api/scan.py b/api/scan.py\n"
222
- "--- a/api/scan.py\n"
223
- "+++ b/api/scan.py\n"
224
- "@@ -0,0 +1,2 @@\n"
225
- "+each_node = walk(tree)\n"
226
- '+DESCRIPTION = "Each attempt polls the review endpoint once."\n'
227
- )
228
- assert sweep_diff(diff) == []
229
-
230
-
231
- def test_flags_near_miss_inside_code_comment() -> None:
232
- diff = (
233
- "diff --git a/api/quota.mjs b/api/quota.mjs\n"
234
- "--- a/api/quota.mjs\n"
235
- "+++ b/api/quota.mjs\n"
236
- "@@ -0,0 +1,2 @@\n"
237
- "+const premium_interactions = 5\n"
238
- "+// the premium-request path resets the counter\n"
239
- )
240
218
  findings = sweep_diff(diff)
241
219
  assert len(findings) == 1
242
- assert "premium-request" in findings[0]
243
-
220
+ assert "premium request" in findings[0]
244
221
 
245
- def test_flags_near_miss_inside_code_string_literal() -> None:
246
- diff = (
247
- "diff --git a/api/quota.mjs b/api/quota.mjs\n"
248
- "--- a/api/quota.mjs\n"
249
- "+++ b/api/quota.mjs\n"
250
- "@@ -0,0 +1,2 @@\n"
251
- "+const premium_interactions = 5\n"
252
- '+throw new Error("the premium-request budget")\n'
253
- )
254
- findings = sweep_diff(diff)
255
- assert len(findings) == 1
256
- assert "premium-request" in findings[0]
257
222
 
258
-
259
- def test_does_not_flag_prose_inside_code_string_literal() -> None:
260
- diff = (
261
- "diff --git a/api/quota.py b/api/quota.py\n"
262
- "--- a/api/quota.py\n"
263
- "+++ b/api/quota.py\n"
264
- "@@ -0,0 +1,2 @@\n"
265
- "+premium_interactions = 5\n"
266
- '+message = "the premium-request budget"\n'
267
- )
268
- assert sweep_diff(diff) == []
269
-
270
-
271
- def test_does_not_flag_prose_inside_code_docstring() -> None:
223
+ def test_flags_near_miss_inside_code_comment() -> None:
272
224
  diff = (
273
225
  "diff --git a/api/quota.py b/api/quota.py\n"
274
226
  "--- a/api/quota.py\n"
275
227
  "+++ b/api/quota.py\n"
276
228
  "@@ -0,0 +1,2 @@\n"
277
229
  "+premium_interactions = 5\n"
278
- '+ """Gate the run on the premium-request budget."""\n'
279
- )
280
- assert sweep_diff(diff) == []
281
-
282
-
283
- def test_does_not_flag_prose_in_test_file() -> None:
284
- diff = (
285
- "diff --git a/api/quota.test.mjs b/api/quota.test.mjs\n"
286
- "--- a/api/quota.test.mjs\n"
287
- "+++ b/api/quota.test.mjs\n"
288
- "@@ -0,0 +1,2 @@\n"
289
- "+const premium_interactions = 5\n"
290
- "+// the premium-request path resets the counter\n"
291
- )
292
- assert sweep_diff(diff) == []
293
-
294
-
295
- def test_flags_prose_in_test_named_markdown_doc() -> None:
296
- diff = (
297
- "diff --git a/api/quota.py b/api/quota.py\n"
298
- "--- a/api/quota.py\n"
299
- "+++ b/api/quota.py\n"
300
- "@@ -0,0 +1,1 @@\n"
301
- "+premium_interactions = 5\n"
302
- "diff --git a/docs/test_plan.md b/docs/test_plan.md\n"
303
- "--- a/docs/test_plan.md\n"
304
- "+++ b/docs/test_plan.md\n"
305
- "@@ -0,0 +1,1 @@\n"
306
- "+The premium-request budget gates the run.\n"
230
+ "+# the premium-request path resets the counter\n"
307
231
  )
308
232
  findings = sweep_diff(diff)
309
233
  assert len(findings) == 1
310
234
  assert "premium-request" in findings[0]
311
- assert "test_plan.md" in findings[0]
312
235
 
313
236
 
314
237
  def test_no_findings_when_no_multiword_identifier_introduced() -> None:
@@ -345,9 +268,7 @@ def test_main_exits_zero_when_clean(tmp_path: Path) -> None:
345
268
 
346
269
  def test_staged_terminology_findings_flags_staged_prose(tmp_path: Path) -> None:
347
270
  _init_git_repository(tmp_path)
348
- (tmp_path / "quota.py").write_text(
349
- "premium_interactions = 5\n", encoding="utf-8"
350
- )
271
+ (tmp_path / "quota.py").write_text("premium_interactions = 5\n", encoding="utf-8")
351
272
  (tmp_path / "README.md").write_text(
352
273
  "The premium-request budget gates the run.\n", encoding="utf-8"
353
274
  )
@@ -356,7 +277,7 @@ def test_staged_terminology_findings_flags_staged_prose(tmp_path: Path) -> None:
356
277
  cwd=tmp_path,
357
278
  check=True,
358
279
  capture_output=True,
359
- env=_git_environment(),
280
+ env=_hermetic_git_environment(),
360
281
  )
361
282
  findings = staged_terminology_findings(tmp_path)
362
283
  assert any("premium-request" in each_finding for each_finding in findings)
@@ -370,7 +291,7 @@ def test_staged_terminology_findings_empty_when_clean(tmp_path: Path) -> None:
370
291
  cwd=tmp_path,
371
292
  check=True,
372
293
  capture_output=True,
373
- env=_git_environment(),
294
+ env=_hermetic_git_environment(),
374
295
  )
375
296
  assert staged_terminology_findings(tmp_path) == []
376
297
 
@@ -389,9 +310,177 @@ def test_parse_added_lines_counts_pre_increment_content_as_added_line() -> None:
389
310
  all_added_lines = parse_added_lines(diff_with_pre_increment)
390
311
 
391
312
  assert [
392
- (each_line_number, each_text) for _, each_line_number, each_text in all_added_lines
313
+ (each_line_number, each_text)
314
+ for _, each_line_number, each_text in all_added_lines
393
315
  ] == [
394
316
  (1, "++counter;"),
395
317
  (2, "let first_total = read_first_value();"),
396
318
  (3, "let second_total = read_second_value();"),
397
319
  ]
320
+
321
+
322
+ def test_does_not_flag_window_containing_a_stopword() -> None:
323
+ diff = (
324
+ "diff --git a/api/quota.py b/api/quota.py\n"
325
+ "--- a/api/quota.py\n"
326
+ "+++ b/api/quota.py\n"
327
+ "@@ -0,0 +1,1 @@\n"
328
+ "+to_path = destination\n"
329
+ "diff --git a/docs/README.md b/docs/README.md\n"
330
+ "--- a/docs/README.md\n"
331
+ "+++ b/docs/README.md\n"
332
+ "@@ -0,0 +1,1 @@\n"
333
+ "+Writes the report to a fresh location.\n"
334
+ )
335
+ assert sweep_diff(diff) == []
336
+
337
+
338
+ def test_does_not_flag_tail_word_that_is_identifier_vocabulary() -> None:
339
+ diff = (
340
+ "diff --git a/api/layout.py b/api/layout.py\n"
341
+ "--- a/api/layout.py\n"
342
+ "+++ b/api/layout.py\n"
343
+ "@@ -0,0 +1,1 @@\n"
344
+ "+target_width = box_height\n"
345
+ "diff --git a/docs/README.md b/docs/README.md\n"
346
+ "--- a/docs/README.md\n"
347
+ "+++ b/docs/README.md\n"
348
+ "@@ -0,0 +1,1 @@\n"
349
+ "+Scale the art until it covers the target box.\n"
350
+ )
351
+ assert sweep_diff(diff) == []
352
+
353
+
354
+ def test_staged_skips_identifier_already_in_base_tree(tmp_path: Path) -> None:
355
+ _init_git_repository(tmp_path)
356
+ (tmp_path / "api.py").write_text("box_height = 4\n", encoding="utf-8")
357
+ subprocess.run(
358
+ ["git", "add", "."],
359
+ cwd=tmp_path,
360
+ check=True,
361
+ capture_output=True,
362
+ env=_hermetic_git_environment(),
363
+ )
364
+ subprocess.run(
365
+ ["git", "commit", "-m", "base"],
366
+ cwd=tmp_path,
367
+ check=True,
368
+ capture_output=True,
369
+ env=_hermetic_git_environment(),
370
+ )
371
+ all_api_lines = ("box_height = 4", "box_frame_total = box_height")
372
+ (tmp_path / "api.py").write_text(chr(10).join(all_api_lines), encoding="utf-8")
373
+ (tmp_path / "README.md").write_text(
374
+ "The box is read from the capture.", encoding="utf-8"
375
+ )
376
+ subprocess.run(
377
+ ["git", "add", "."],
378
+ cwd=tmp_path,
379
+ check=True,
380
+ capture_output=True,
381
+ env=_hermetic_git_environment(),
382
+ )
383
+ assert staged_terminology_findings(tmp_path) == []
384
+
385
+
386
+ def test_does_not_flag_escape_sequence_letter_as_prose() -> None:
387
+ diff = (
388
+ "diff --git a/api/quota.py b/api/quota.py\n"
389
+ "--- a/api/quota.py\n"
390
+ "+++ b/api/quota.py\n"
391
+ "@@ -0,0 +1,2 @@\n"
392
+ "+box_frame_total = 5\n"
393
+ '+content = "box_height = 4\\nbox_frame_total = 5\\n"\n'
394
+ )
395
+ assert sweep_diff(diff) == []
396
+
397
+
398
+ def test_skips_string_literal_fragments_in_test_files() -> None:
399
+ diff = (
400
+ "diff --git a/api/quota.py b/api/quota.py\n"
401
+ "--- a/api/quota.py\n"
402
+ "+++ b/api/quota.py\n"
403
+ "@@ -0,0 +1,1 @@\n"
404
+ "+premium_interactions = 5\n"
405
+ "diff --git a/tests/test_quota.py b/tests/test_quota.py\n"
406
+ "--- a/tests/test_quota.py\n"
407
+ "+++ b/tests/test_quota.py\n"
408
+ "@@ -0,0 +1,2 @@\n"
409
+ '+fixture_text = "the premium-request budget gates the run"\n'
410
+ "+# the premium-request path resets the counter\n"
411
+ )
412
+ findings = sweep_diff(diff)
413
+ assert len(findings) == 1
414
+ assert "test_quota.py:2" in findings[0]
415
+
416
+
417
+ def test_repository_environment_strips_every_git_variable(
418
+ monkeypatch: pytest.MonkeyPatch,
419
+ ) -> None:
420
+ monkeypatch.setenv("GIT_DIR", "hostile-git-dir")
421
+ monkeypatch.setenv("PRESERVED_SETTING", "kept")
422
+
423
+ scrubbed_environment = sweep_module.repository_environment()
424
+
425
+ assert "GIT_DIR" not in scrubbed_environment
426
+ assert scrubbed_environment["PRESERVED_SETTING"] == "kept"
427
+
428
+
429
+ def test_skips_markdown_inline_code_spans() -> None:
430
+ diff = (
431
+ "diff --git a/api/quota.py b/api/quota.py\n"
432
+ "--- a/api/quota.py\n"
433
+ "+++ b/api/quota.py\n"
434
+ "@@ -0,0 +1,1 @@\n"
435
+ "+scale_expression_reference = 1\n"
436
+ "diff --git a/docs/guide.md b/docs/guide.md\n"
437
+ "--- a/docs/guide.md\n"
438
+ "+++ b/docs/guide.md\n"
439
+ "@@ -0,0 +1,1 @@\n"
440
+ "+Its `scale_expression.py` evaluates the layer's `scale` prop here.\n"
441
+ )
442
+ assert sweep_diff(diff) == []
443
+
444
+
445
+ def test_skips_identifier_shaped_string_literals() -> None:
446
+ diff = (
447
+ "diff --git a/api/quota.py b/api/quota.py\n"
448
+ "--- a/api/quota.py\n"
449
+ "+++ b/api/quota.py\n"
450
+ "@@ -0,0 +1,2 @@\n"
451
+ "+icon_entry = 1\n"
452
+ '+MASTER_UID = "HOMESCREEN_APPICONS_APP_ICON_IMAGE"\n'
453
+ )
454
+ assert sweep_diff(diff) == []
455
+
456
+
457
+ def test_skips_string_literal_fragments_in_dot_test_named_files() -> None:
458
+ diff = (
459
+ "diff --git a/api/quota.py b/api/quota.py\n"
460
+ "--- a/api/quota.py\n"
461
+ "+++ b/api/quota.py\n"
462
+ "@@ -0,0 +1,1 @@\n"
463
+ "+premium_interactions = 5\n"
464
+ "diff --git a/api/quota.test.mjs b/api/quota.test.mjs\n"
465
+ "--- a/api/quota.test.mjs\n"
466
+ "+++ b/api/quota.test.mjs\n"
467
+ "@@ -0,0 +1,1 @@\n"
468
+ '+const fixture_line = "the premium-request budget gates the run"\n'
469
+ )
470
+ assert sweep_diff(diff) == []
471
+
472
+
473
+ def test_skips_string_literal_fragments_in_tests_directory_files() -> None:
474
+ diff = (
475
+ "diff --git a/api/quota.py b/api/quota.py\n"
476
+ "--- a/api/quota.py\n"
477
+ "+++ b/api/quota.py\n"
478
+ "@@ -0,0 +1,1 @@\n"
479
+ "+premium_interactions = 5\n"
480
+ "diff --git a/tests/fixtures.py b/tests/fixtures.py\n"
481
+ "--- a/tests/fixtures.py\n"
482
+ "+++ b/tests/fixtures.py\n"
483
+ "@@ -0,0 +1,1 @@\n"
484
+ '+sample_line = "the premium-request budget gates the run"\n'
485
+ )
486
+ assert sweep_diff(diff) == []
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: code-quality-agent
3
3
  description: Use this agent for comprehensive code quality reviews across multiple files.
4
- model: inherit
4
+ model: opus
5
5
  color: red
6
6
  ---
7
7
 
@@ -88,6 +88,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
88
88
  | `reviewer_spawn_gate.py` | PreToolUse (Bash) | A sentinel-marked autoconverge reviewer-spawn command (Copilot review request, Bugbot rerun comment) run while `reviewer_availability.py` reports that reviewer down or out of quota |
89
89
  | `send_user_file_open_locally_blocker.py` | PreToolUse (SendUserFile) | A desk-side file attach (`SendUserFile` with `status` not `proactive`); points to opening the file locally via `Show-Asset.ps1` |
90
90
  | `sensitive_file_protector.py` | PreToolUse (Write/Edit) | Writes to sensitive credential or config files |
91
+ | `session_edit_stage_gate.py` | PreToolUse (Bash) | A `git commit` that would drop files edited this session because they are tracked but left unstaged |
91
92
  | `session_handoff_blocker.py` | Stop | Responses suggesting a new session mid-task |
92
93
  | `stale_comment_reference_blocker.py` | PreToolUse (Edit) | An Edit that rewrites a Python code line while keeping the standalone comment directly above it, when that comment names an identifier the rewrite removes from the line |
93
94
  | `state_description_blocker.py` | PreToolUse (Write/Edit) | Historical/comparative language in documentation |
@@ -501,13 +501,16 @@ def check_dead_module_constants(
501
501
 
502
502
  Returns:
503
503
  One violation message per dead module-level constant, capped at the
504
- configured maximum. Returns an empty list when the file is exempt, no
505
- constant is in scope (none defined, or none exported when ``__all__`` is
506
- declared), the scan exceeds the file cap, or a SyntaxError prevents
507
- parsing.
504
+ configured maximum. Returns an empty list when the file is exempt, the
505
+ path is relative (the scan root cannot be resolved against a known
506
+ base, so the check cannot prove a constant dead), no constant is in
507
+ scope (none defined, or none exported when ``__all__`` is declared),
508
+ the scan exceeds the file cap, or a SyntaxError prevents parsing.
508
509
  """
509
510
  if _module_is_exempt_from_constant_check(file_path):
510
511
  return []
512
+ if not Path(file_path).is_absolute():
513
+ return []
511
514
  effective_content = content if full_file_content is None else full_file_content
512
515
  try:
513
516
  tree = ast.parse(effective_content)