claude-dev-env 7.5.0 → 7.6.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 (121) hide show
  1. package/.agents/skills/everything-search/scripts/everything_search.py +11 -0
  2. package/.agents/skills/everything-search/scripts/test_everything_search.py +1 -0
  3. package/.agents/skills/fresh-branch/SKILL.md +1 -0
  4. package/.agents/skills/fresh-branch/scripts/fresh_branch_git_commands.py +3 -0
  5. package/.agents/skills/fresh-branch/scripts/subprocess_window_access.py +43 -0
  6. package/.agents/skills/syncing-submodules/SKILL.md +1 -0
  7. package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
  8. package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
  9. package/.agents/skills/test-runner/SKILL.md +1 -0
  10. package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
  11. package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
  12. package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
  13. package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
  14. package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
  15. package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
  16. package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
  23. package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
  24. package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
  25. package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
  26. package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
  27. package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
  28. package/_shared/pr-loop/scripts/preflight.py +7 -0
  29. package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
  30. package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
  31. package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
  32. package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
  33. package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
  34. package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
  35. package/bin/install-constants.mjs +4 -2
  36. package/bin/install.cursor-rules.test.mjs +57 -15
  37. package/bin/install.mjs +10 -15
  38. package/bin/pstack-model-preferences.claude.json +23 -0
  39. package/hooks/advisory/pr_done_reminder.py +7 -0
  40. package/hooks/advisory/refactor_guard.py +8 -0
  41. package/hooks/advisory/test_pr_done_reminder.py +21 -0
  42. package/hooks/advisory/test_refactor_guard.py +40 -0
  43. package/hooks/blocking/block_main_commit.py +4 -0
  44. package/hooks/blocking/code_rules_comments.py +39 -5
  45. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
  46. package/hooks/blocking/precommit_code_rules_gate.py +3 -0
  47. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
  48. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
  49. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
  50. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
  51. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
  52. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
  53. package/hooks/git-hooks/pull_request_handoff.py +3 -0
  54. package/hooks/git-hooks/subprocess_window_access.py +41 -0
  55. package/hooks/git-hooks/test_verification_start.py +4 -0
  56. package/hooks/git-hooks/verification_notice_context.py +3 -0
  57. package/hooks/git-hooks/verification_start.py +7 -8
  58. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
  59. package/hooks/hooks_constants/subprocess_window.py +141 -0
  60. package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
  61. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
  62. package/hooks/hooks_constants/test_subprocess_window.py +128 -0
  63. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
  64. package/hooks/validation/mypy_validator.py +4 -0
  65. package/hooks/validators/README.md +1 -0
  66. package/hooks/validators/git_checks.py +4 -0
  67. package/hooks/validators/health_check.py +3 -0
  68. package/hooks/validators/hook_timing_harness.py +3 -0
  69. package/hooks/validators/mypy_integration.py +4 -0
  70. package/hooks/validators/ruff_integration.py +6 -0
  71. package/hooks/validators/run_all_validators.py +7 -0
  72. package/hooks/validators/subprocess_window_access.py +41 -0
  73. package/hooks/workflow/auto_formatter.py +8 -0
  74. package/hooks/workflow/test_auto_formatter.py +39 -0
  75. package/package.json +1 -1
  76. package/rules/pstack-models.md +4 -3
  77. package/scripts/automatic_advisory/checkout.py +8 -1
  78. package/scripts/automatic_advisory/cli.py +9 -3
  79. package/scripts/automatic_advisory/config/constants.py +0 -2
  80. package/scripts/automatic_advisory/execution.py +7 -1
  81. package/scripts/automatic_advisory/git.py +9 -1
  82. package/scripts/automatic_advisory/process_host.py +3 -0
  83. package/scripts/claude_chain_runner.py +3 -0
  84. package/scripts/gh_artifact_upload.py +3 -0
  85. package/scripts/grok_headless_runner.py +3 -0
  86. package/scripts/grok_patch_artifacts.py +4 -0
  87. package/scripts/grok_worker_preflight.py +3 -0
  88. package/scripts/invoke_code_review.py +3 -0
  89. package/scripts/local_verification/command_runner.py +3 -0
  90. package/scripts/local_verification/git_state.py +3 -0
  91. package/scripts/policy_lint/adapter_detectors.py +48 -2
  92. package/scripts/policy_lint/adapter_pairing.py +150 -2
  93. package/scripts/policy_lint/adapters.py +46 -4
  94. package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
  95. package/scripts/policy_lint/config/constants.py +3 -0
  96. package/scripts/policy_lint/registry.py +1 -1
  97. package/scripts/policy_lint/selection_git.py +11 -0
  98. package/scripts/repository_checks/config/constants.py +5 -0
  99. package/scripts/repository_checks/config/policy_document.py +19 -0
  100. package/scripts/repository_checks/email_exemptions.py +14 -71
  101. package/scripts/repository_checks/path_exemptions.py +35 -0
  102. package/scripts/repository_checks/policy_document.py +120 -0
  103. package/scripts/repository_checks/tracked_secrets.py +27 -4
  104. package/scripts/setup_project_paths.py +3 -0
  105. package/scripts/spawn_grok_batch.py +3 -0
  106. package/scripts/subprocess_window_access.py +41 -0
  107. package/scripts/test_dispatcher_profile_import.py +4 -0
  108. package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
  109. package/scripts/tests/test_email_exemptions.py +37 -0
  110. package/scripts/tests/test_path_exemptions.py +164 -0
  111. package/scripts/tests/test_policy_document.py +104 -0
  112. package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
  113. package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
  114. package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
  115. package/scripts/tests/test_policy_lint_selection.py +34 -0
  116. package/scripts/tests/test_subprocess_window_access.py +118 -0
  117. package/scripts/tests/test_tracked_secrets.py +79 -0
  118. package/scripts/validate_instruction_pairs.py +3 -0
  119. package/scripts/verify_installable_package.py +5 -0
  120. package/scripts/automatic_advisory/window_flags.py +0 -41
  121. package/scripts/repository_checks/config/email_exemptions.py +0 -10
@@ -70,6 +70,37 @@ def test_approved_pairs_name_files_that_exist() -> None:
70
70
  assert (repository_root / each_test_path).is_file(), each_test_path
71
71
 
72
72
 
73
+ def test_every_approved_pair_actually_waives_its_production_file(tmp_path: Path) -> None:
74
+ """Each mapping entry must silence the rule when its production file changes.
75
+
76
+ File existence alone leaves an entry inert when its key carries a suffix the
77
+ rule skips, or when its value is not a name the rule reads as a test.
78
+ """
79
+ for (
80
+ production_path,
81
+ all_test_paths,
82
+ ) in APPROVED_TEST_PATHS_BY_PRODUCTION_PATH.items():
83
+ production_document = Document(
84
+ production_path,
85
+ _BODY_AFTER,
86
+ _BODY_BEFORE,
87
+ frozenset({1}),
88
+ ContentOrigin.REVISION_DIFF,
89
+ )
90
+ for each_test_path in all_test_paths:
91
+ test_document = Document(
92
+ each_test_path,
93
+ _BODY_AFTER,
94
+ _BODY_BEFORE,
95
+ frozenset({1}),
96
+ ContentOrigin.REVISION_DIFF,
97
+ )
98
+ document_set = DocumentSet(
99
+ (production_document, test_document), SelectionKind.BASE, tmp_path
100
+ )
101
+ assert adapters.test_pairing_diagnostics(document_set) == (), production_path
102
+
103
+
73
104
  def test_pairing_ignores_python_docstring_only_changes(tmp_path: Path) -> None:
74
105
  all_diagnostics = _pairing_diagnostics(
75
106
  tmp_path,
@@ -161,3 +192,347 @@ def test_pairing_rejects_an_unrelated_module_beside_a_changed_approved_suite(
161
192
  _body_change_at(_APPROVED_TEST_PATH),
162
193
  )
163
194
  assert all_paths == (_UNRELATED_PRODUCTION_PATH,)
195
+
196
+
197
+ _MECHANICAL_BEFORE = '''"""Before."""
198
+
199
+ import subprocess
200
+
201
+ TIMEOUT_SECONDS = 5
202
+
203
+
204
+ def work(command: list[str]) -> int:
205
+ """Run one command."""
206
+ if TIMEOUT_SECONDS > 0:
207
+ completed = subprocess.run(command, timeout=TIMEOUT_SECONDS, check=False)
208
+ return completed.returncode
209
+ return 1
210
+ '''
211
+ _KEYWORD_ADDED = '''"""Before."""
212
+
213
+ import subprocess
214
+
215
+ TIMEOUT_SECONDS = 5
216
+
217
+
218
+ def work(command: list[str]) -> int:
219
+ """Run one command."""
220
+ if TIMEOUT_SECONDS > 0:
221
+ completed = subprocess.run(
222
+ command, timeout=TIMEOUT_SECONDS, check=False, creationflags=0
223
+ )
224
+ return completed.returncode
225
+ return 1
226
+ '''
227
+ _IMPORT_AND_KEYWORD_ADDED = '''"""Before."""
228
+
229
+ import os
230
+ import subprocess
231
+
232
+ TIMEOUT_SECONDS = 5
233
+
234
+
235
+ def work(command: list[str]) -> int:
236
+ """Run one command."""
237
+ if TIMEOUT_SECONDS > 0:
238
+ completed = subprocess.run(
239
+ command, timeout=TIMEOUT_SECONDS, check=False, creationflags=os.O_RDONLY
240
+ )
241
+ return completed.returncode
242
+ return 1
243
+ '''
244
+ _DOCSTRING_AND_KEYWORD_CHANGED = '''"""After."""
245
+
246
+ import subprocess
247
+
248
+ TIMEOUT_SECONDS = 5
249
+
250
+
251
+ def work(command: list[str]) -> int:
252
+ """Run one command and report its exit status."""
253
+ if TIMEOUT_SECONDS > 0:
254
+ completed = subprocess.run(
255
+ command, timeout=TIMEOUT_SECONDS, check=False, creationflags=0
256
+ )
257
+ return completed.returncode
258
+ return 1
259
+ '''
260
+ _KEYWORD_ADDED_AND_LITERAL_CHANGED = '''"""Before."""
261
+
262
+ import subprocess
263
+
264
+ TIMEOUT_SECONDS = 6
265
+
266
+
267
+ def work(command: list[str]) -> int:
268
+ """Run one command."""
269
+ if TIMEOUT_SECONDS > 0:
270
+ completed = subprocess.run(
271
+ command, timeout=TIMEOUT_SECONDS, check=False, creationflags=0
272
+ )
273
+ return completed.returncode
274
+ return 1
275
+ '''
276
+ _KEYWORD_VALUE_CHANGED = '''"""Before."""
277
+
278
+ import subprocess
279
+
280
+ TIMEOUT_SECONDS = 5
281
+
282
+
283
+ def work(command: list[str]) -> int:
284
+ """Run one command."""
285
+ if TIMEOUT_SECONDS > 0:
286
+ completed = subprocess.run(command, timeout=TIMEOUT_SECONDS, check=True)
287
+ return completed.returncode
288
+ return 1
289
+ '''
290
+ _KEYWORD_VALUE_TYPE_CHANGED = '''"""Before."""
291
+
292
+ import subprocess
293
+
294
+ TIMEOUT_SECONDS = 5
295
+
296
+
297
+ def work(command: list[str]) -> int:
298
+ """Run one command."""
299
+ if TIMEOUT_SECONDS > 0:
300
+ completed = subprocess.run(command, timeout=TIMEOUT_SECONDS, check=0)
301
+ return completed.returncode
302
+ return 1
303
+ '''
304
+ _KEYWORD_REMOVED = '''"""Before."""
305
+
306
+ import subprocess
307
+
308
+ TIMEOUT_SECONDS = 5
309
+
310
+
311
+ def work(command: list[str]) -> int:
312
+ """Run one command."""
313
+ if TIMEOUT_SECONDS > 0:
314
+ completed = subprocess.run(command, timeout=TIMEOUT_SECONDS)
315
+ return completed.returncode
316
+ return 1
317
+ '''
318
+ _POSITIONAL_CHANGED = '''"""Before."""
319
+
320
+ import subprocess
321
+
322
+ TIMEOUT_SECONDS = 5
323
+
324
+
325
+ def work(command: list[str]) -> int:
326
+ """Run one command."""
327
+ if TIMEOUT_SECONDS > 0:
328
+ completed = subprocess.run(
329
+ command[:1], timeout=TIMEOUT_SECONDS, check=False, creationflags=0
330
+ )
331
+ return completed.returncode
332
+ return 1
333
+ '''
334
+ _STATEMENT_ADDED_IN_BODY = '''"""Before."""
335
+
336
+ import subprocess
337
+
338
+ TIMEOUT_SECONDS = 5
339
+
340
+
341
+ def work(command: list[str]) -> int:
342
+ """Run one command."""
343
+ if TIMEOUT_SECONDS > 0:
344
+ completed = subprocess.run(
345
+ command, timeout=TIMEOUT_SECONDS, check=False, creationflags=0
346
+ )
347
+ completed.check_returncode()
348
+ return completed.returncode
349
+ return 1
350
+ '''
351
+ _CONDITION_CHANGED = '''"""Before."""
352
+
353
+ import subprocess
354
+
355
+ TIMEOUT_SECONDS = 5
356
+
357
+
358
+ def work(command: list[str]) -> int:
359
+ """Run one command."""
360
+ if TIMEOUT_SECONDS > 1:
361
+ completed = subprocess.run(
362
+ command, timeout=TIMEOUT_SECONDS, check=False, creationflags=0
363
+ )
364
+ return completed.returncode
365
+ return 1
366
+ '''
367
+ _IMPORT_REMOVED = '''"""Before."""
368
+
369
+ TIMEOUT_SECONDS = 5
370
+
371
+
372
+ def work(command: list[str]) -> int:
373
+ """Run one command."""
374
+ if TIMEOUT_SECONDS > 0:
375
+ completed = subprocess.run(
376
+ command, timeout=TIMEOUT_SECONDS, check=False, creationflags=0
377
+ )
378
+ return completed.returncode
379
+ return 1
380
+ '''
381
+ _UNPARSEABLE = "def work(:\n return None\n"
382
+
383
+
384
+ def _mechanical_diagnostics(
385
+ temporary_path: Path, current_source: str, suffix: str = ".py"
386
+ ) -> tuple[Diagnostic, ...]:
387
+ return _pairing_diagnostics(
388
+ temporary_path,
389
+ _changed_document(current_source, _MECHANICAL_BEFORE, suffix),
390
+ )
391
+
392
+
393
+ def test_pairing_waives_one_added_keyword_argument(tmp_path: Path) -> None:
394
+ assert _mechanical_diagnostics(tmp_path, _KEYWORD_ADDED) == ()
395
+
396
+
397
+ def test_pairing_waives_an_added_import_beside_an_added_keyword(
398
+ tmp_path: Path,
399
+ ) -> None:
400
+ assert _mechanical_diagnostics(tmp_path, _IMPORT_AND_KEYWORD_ADDED) == ()
401
+
402
+
403
+ def test_pairing_waives_a_docstring_edit_beside_an_added_keyword(
404
+ tmp_path: Path,
405
+ ) -> None:
406
+ assert _mechanical_diagnostics(tmp_path, _DOCSTRING_AND_KEYWORD_CHANGED) == ()
407
+
408
+
409
+ def test_pairing_reports_an_added_keyword_beside_a_changed_literal(
410
+ tmp_path: Path,
411
+ ) -> None:
412
+ assert (
413
+ len(_mechanical_diagnostics(tmp_path, _KEYWORD_ADDED_AND_LITERAL_CHANGED)) == 1
414
+ )
415
+
416
+
417
+ def test_pairing_reports_a_changed_keyword_value(tmp_path: Path) -> None:
418
+ assert len(_mechanical_diagnostics(tmp_path, _KEYWORD_VALUE_CHANGED)) == 1
419
+
420
+
421
+ def test_pairing_reports_a_keyword_value_that_changed_literal_type(
422
+ tmp_path: Path,
423
+ ) -> None:
424
+ assert len(_mechanical_diagnostics(tmp_path, _KEYWORD_VALUE_TYPE_CHANGED)) == 1
425
+
426
+
427
+ def test_pairing_reports_a_removed_keyword(tmp_path: Path) -> None:
428
+ assert len(_mechanical_diagnostics(tmp_path, _KEYWORD_REMOVED)) == 1
429
+
430
+
431
+ def test_pairing_reports_a_changed_positional_argument(tmp_path: Path) -> None:
432
+ assert len(_mechanical_diagnostics(tmp_path, _POSITIONAL_CHANGED)) == 1
433
+
434
+
435
+ def test_pairing_reports_a_statement_added_inside_a_function(tmp_path: Path) -> None:
436
+ assert len(_mechanical_diagnostics(tmp_path, _STATEMENT_ADDED_IN_BODY)) == 1
437
+
438
+
439
+ def test_pairing_reports_a_changed_if_condition(tmp_path: Path) -> None:
440
+ assert len(_mechanical_diagnostics(tmp_path, _CONDITION_CHANGED)) == 1
441
+
442
+
443
+ def test_pairing_reports_a_removed_import(tmp_path: Path) -> None:
444
+ assert len(_mechanical_diagnostics(tmp_path, _IMPORT_REMOVED)) == 1
445
+
446
+
447
+ def test_pairing_reports_a_non_python_document_with_the_same_shape(
448
+ tmp_path: Path,
449
+ ) -> None:
450
+ assert len(_mechanical_diagnostics(tmp_path, _KEYWORD_ADDED, ".ts")) == 1
451
+
452
+
453
+ def test_pairing_reports_unparseable_current_source(tmp_path: Path) -> None:
454
+ assert len(_mechanical_diagnostics(tmp_path, _UNPARSEABLE)) == 1
455
+
456
+
457
+ def test_pairing_reports_unparseable_prior_source(tmp_path: Path) -> None:
458
+ all_diagnostics = _pairing_diagnostics(
459
+ tmp_path, _changed_document(_KEYWORD_ADDED, _UNPARSEABLE)
460
+ )
461
+ assert len(all_diagnostics) == 1
462
+
463
+
464
+ def test_pairing_reports_an_added_document_without_prior_text(tmp_path: Path) -> None:
465
+ added_document = Document(
466
+ PurePosixPath("src/feature.py"),
467
+ _KEYWORD_ADDED,
468
+ None,
469
+ frozenset({1}),
470
+ ContentOrigin.REVISION_DIFF,
471
+ )
472
+ assert len(_pairing_diagnostics(tmp_path, added_document)) == 1
473
+
474
+
475
+ _MAPPING_UNPACKING_ADDED = '''"""Before."""
476
+
477
+ import subprocess
478
+
479
+ TIMEOUT_SECONDS = 5
480
+
481
+
482
+ def work(command: list[str]) -> int:
483
+ """Run one command."""
484
+ if TIMEOUT_SECONDS > 0:
485
+ completed = subprocess.run(
486
+ command, timeout=TIMEOUT_SECONDS, check=False, **{"creationflags": 0}
487
+ )
488
+ return completed.returncode
489
+ return 1
490
+ '''
491
+
492
+
493
+ def test_pairing_reports_added_mapping_unpacking_on_a_call(tmp_path: Path) -> None:
494
+ assert len(_mechanical_diagnostics(tmp_path, _MAPPING_UNPACKING_ADDED)) == 1
495
+ _INSTALLER_PRODUCTION_PATHS = (
496
+ PurePosixPath("packages/claude-dev-env/bin/install-constants.mjs"),
497
+ PurePosixPath("packages/claude-dev-env/bin/install.mjs"),
498
+ )
499
+ _INSTALLER_SUITE_PATH = PurePosixPath(
500
+ "packages/claude-dev-env/bin/install.cursor-rules.test.mjs"
501
+ )
502
+
503
+
504
+ def test_pairing_accepts_installer_modules_with_the_cursor_rules_suite(
505
+ tmp_path: Path,
506
+ ) -> None:
507
+ all_paths = _diagnostic_paths(
508
+ tmp_path,
509
+ *(_body_change_at(each_path) for each_path in _INSTALLER_PRODUCTION_PATHS),
510
+ _body_change_at(_INSTALLER_SUITE_PATH),
511
+ )
512
+ assert all_paths == ()
513
+
514
+
515
+ _COMMENT_RULES_PRODUCTION_PATH = PurePosixPath(
516
+ "packages/claude-dev-env/hooks/blocking/code_rules_comments.py"
517
+ )
518
+ _COMMENT_RULES_SUITE_PATH = PurePosixPath(
519
+ "packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py"
520
+ )
521
+
522
+
523
+ def test_pairing_accepts_comment_rules_module_with_one_approved_suite(
524
+ tmp_path: Path,
525
+ ) -> None:
526
+ all_paths = _diagnostic_paths(
527
+ tmp_path,
528
+ _body_change_at(_COMMENT_RULES_PRODUCTION_PATH),
529
+ _body_change_at(_COMMENT_RULES_SUITE_PATH),
530
+ )
531
+ assert all_paths == ()
532
+
533
+
534
+ def test_pairing_rejects_comment_rules_module_without_any_approved_suite(
535
+ tmp_path: Path,
536
+ ) -> None:
537
+ all_paths = _diagnostic_paths(tmp_path, _body_change_at(_COMMENT_RULES_PRODUCTION_PATH))
538
+ assert all_paths == (_COMMENT_RULES_PRODUCTION_PATH,)
@@ -22,6 +22,27 @@ from policy_lint.model import (
22
22
 
23
23
  _FUNCTION_SOURCE = "def work() -> None:\n pass\n"
24
24
  _CHANGED_RULE_SETS = frozenset({"changed"})
25
+ _UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT = (
26
+ "import subprocess\n"
27
+ "PYTHON_FORMAT_TIMEOUT_SECONDS = 12\n"
28
+ "GIT_CHECK_TIMEOUT_SECONDS = 5\n"
29
+ "def worst_case_python_format_seconds() -> int:\n"
30
+ " return PYTHON_FORMAT_TIMEOUT_SECONDS + PYTHON_FORMAT_TIMEOUT_SECONDS\n"
31
+ "def is_untracked_in_git(file_path: str) -> bool:\n"
32
+ " git_check = subprocess.run(['git', 'ls-files', file_path], "
33
+ "timeout=GIT_CHECK_TIMEOUT_SECONDS)\n"
34
+ " return git_check.returncode != 0\n"
35
+ "def run_format(file_path: str) -> None:\n"
36
+ " subprocess.run(['ruff', 'format', file_path], "
37
+ "timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)\n"
38
+ "def main(file_path: str) -> None:\n"
39
+ " if is_untracked_in_git(file_path):\n"
40
+ " return\n"
41
+ " run_format(file_path)\n"
42
+ )
43
+ _GENERATED_CHANGELOG_TEXT = (
44
+ "## 7.5.0\n\n* **rules:** correct hook claims for gates that " + "no" + " longer run\n"
45
+ )
25
46
 
26
47
 
27
48
  def _rule_named(rule_id: str) -> DocumentRule:
@@ -266,6 +287,51 @@ def test_hook_format_accepts_claude_settings() -> None:
266
287
  assert configuration_rule.accepts(settings_document) is False
267
288
 
268
289
 
290
+ def test_prose_rules_skip_a_generated_changelog() -> None:
291
+ changelog_document = Document.from_text(
292
+ "packages/claude-dev-env/CHANGELOG.md", _GENERATED_CHANGELOG_TEXT
293
+ )
294
+ handwritten_document = Document.from_text("docs/notes.md", _GENERATED_CHANGELOG_TEXT)
295
+ stored_prompt_changelog = Document.from_text("rules/CHANGELOG.md", _GENERATED_CHANGELOG_TEXT)
296
+ stored_prompt_document = Document.from_text("rules/notes.md", _GENERATED_CHANGELOG_TEXT)
297
+ state_rule = _rule_named("state-description")
298
+ plain_language_rule = _rule_named("plain-language")
299
+ assert state_rule.accepts(changelog_document) is False
300
+ assert state_rule.accepts(handwritten_document) is True
301
+ assert plain_language_rule.accepts(stored_prompt_changelog) is False
302
+ assert plain_language_rule.accepts(stored_prompt_document) is True
303
+
304
+
305
+ def test_lint_reports_the_changelog_phrase_only_in_the_handwritten_document(
306
+ tmp_path: Path,
307
+ ) -> None:
308
+ _ensure_git_repository(tmp_path)
309
+ document_set = DocumentSet(
310
+ (
311
+ Document.from_text("packages/claude-dev-env/CHANGELOG.md", _GENERATED_CHANGELOG_TEXT),
312
+ Document.from_text("docs/notes.md", _GENERATED_CHANGELOG_TEXT),
313
+ ),
314
+ SelectionKind.TEXT,
315
+ tmp_path,
316
+ )
317
+ lint_report = lint(
318
+ LintRequest(tmp_path, document_set), all_registry=(_rule_named("state-description"),)
319
+ )
320
+ all_reported_paths = {
321
+ each_diagnostic.location.path.as_posix()
322
+ for each_diagnostic in lint_report.diagnostics
323
+ if each_diagnostic.location is not None
324
+ }
325
+ assert all_reported_paths == {"docs/notes.md"}
326
+
327
+
328
+ def test_non_prose_markdown_rules_still_run_on_a_generated_changelog() -> None:
329
+ changelog_document = Document.from_text(
330
+ "packages/claude-dev-env/CHANGELOG.md", _GENERATED_CHANGELOG_TEXT
331
+ )
332
+ assert _rule_named("docstring-gate-count").accepts(changelog_document) is True
333
+
334
+
269
335
  def test_workflow_substitution_accepts_workflow_js_suffix() -> None:
270
336
  workflow_document = Document.from_text("packages/foo/bar.workflow.js", "loop")
271
337
  directory_document = Document.from_text("docs/workflow/readme.md", "loop")
@@ -581,3 +647,53 @@ def test_lint_keeps_skipped_rules_out_of_executed_rules(tmp_path: Path) -> None:
581
647
  all_rules_report = lint(request, all_registry=(skipped_rule,))
582
648
  assert all_rules_report.executed_rules == ()
583
649
  assert all_rules_report.skipped_rules == ("skipped",)
650
+
651
+
652
+ def test_subprocess_budget_skips_a_test_module_and_keeps_production() -> None:
653
+ subprocess_budget_rule = _rule_named("subprocess-budget")
654
+ prefixed_test_document = Document.from_text(
655
+ "packages/claude-dev-env/hooks/workflow/test_auto_formatter.py",
656
+ _UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT,
657
+ )
658
+ repository_root_test_document = Document.from_text(
659
+ "tests/timing.py", _UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT
660
+ )
661
+ production_document = Document.from_text(
662
+ "src/timing.py", _UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT
663
+ )
664
+ assert subprocess_budget_rule.accepts(prefixed_test_document) is False
665
+ assert subprocess_budget_rule.accepts(repository_root_test_document) is False
666
+ assert subprocess_budget_rule.accepts(production_document) is True
667
+
668
+
669
+ def _subprocess_budget_rule_identifiers(
670
+ repository_root: Path, document_path: str
671
+ ) -> tuple[str, ...]:
672
+ document_set = DocumentSet(
673
+ (Document.from_text(document_path, _UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT),),
674
+ SelectionKind.TEXT,
675
+ repository_root,
676
+ )
677
+ lint_report = lint(
678
+ LintRequest(repository_root, document_set),
679
+ all_registry=(_rule_named("subprocess-budget"),),
680
+ )
681
+ return tuple(
682
+ each_diagnostic.rule_id for each_diagnostic in lint_report.diagnostics
683
+ )
684
+
685
+
686
+ def test_lint_reports_an_undercounted_budget_only_in_the_production_module(
687
+ tmp_path: Path,
688
+ ) -> None:
689
+ _ensure_git_repository(tmp_path)
690
+ assert _subprocess_budget_rule_identifiers(tmp_path, "src/timing.py") == (
691
+ "subprocess-budget",
692
+ )
693
+ assert (
694
+ _subprocess_budget_rule_identifiers(
695
+ tmp_path, "packages/claude-dev-env/hooks/workflow/test_auto_formatter.py"
696
+ )
697
+ == ()
698
+ )
699
+ assert _subprocess_budget_rule_identifiers(tmp_path, "tests/timing.py") == ()
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import subprocess
7
+ import sys
7
8
  from pathlib import Path, PurePosixPath
8
9
 
9
10
  import pytest
@@ -19,6 +20,9 @@ from policy_lint.model import (
19
20
  )
20
21
  from policy_lint.selection import SelectionRunFatal, select_documents
21
22
 
23
+ WINDOWS_NO_WINDOW_FLAG = 0x08000000
24
+ EXPECTED_HIDDEN_WINDOW_FLAGS = WINDOWS_NO_WINDOW_FLAG if sys.platform == "win32" else 0
25
+
22
26
  NON_UTF8_PNG_BYTES = bytes(
23
27
  (0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0xFF, 0xFE, 0xFF)
24
28
  )
@@ -501,3 +505,33 @@ def test_files_missing_path_should_still_raise(tmp_path: Path) -> None:
501
505
 
502
506
  with pytest.raises(SelectionRunFatal, match="does not exist"):
503
507
  select_documents(LintRequest.files(repository_root, [Path("missing.py")]))
508
+
509
+
510
+ def _record_git_creation_flags(monkeypatch: pytest.MonkeyPatch) -> dict[str, object]:
511
+ all_recorded_keyword_arguments: dict[str, object] = {}
512
+
513
+ def _record(*args: object, **kwargs: object) -> subprocess.CompletedProcess[bytes]:
514
+ del args
515
+ all_recorded_keyword_arguments.update(kwargs)
516
+ return subprocess.CompletedProcess([], 0, stdout=b"abc123\n", stderr=b"")
517
+
518
+ monkeypatch.setattr(selection_git.subprocess, "run", _record)
519
+ return all_recorded_keyword_arguments
520
+
521
+
522
+ def test_git_bytes_for_should_start_without_a_console_window(
523
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
524
+ ) -> None:
525
+ all_recorded_keyword_arguments = _record_git_creation_flags(monkeypatch)
526
+
527
+ assert selection_git.git_bytes_for(tmp_path, ("status",)) == b"abc123\n"
528
+ assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
529
+
530
+
531
+ def test_head_revision_should_start_without_a_console_window(
532
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
533
+ ) -> None:
534
+ all_recorded_keyword_arguments = _record_git_creation_flags(monkeypatch)
535
+
536
+ assert selection_git.head_revision(tmp_path) == "abc123"
537
+ assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
@@ -0,0 +1,118 @@
1
+ """Prove every deployed subprocess-window shim reaches the packaged helpers.
2
+
3
+ Each shim sits beside the scripts of one deployed directory. A deployed
4
+ profile carries no installed Python distribution, and the repository runs its
5
+ scripts under ``python -S -E``, which hides installed packages from the child.
6
+ So each check below drives a real ``-S -E`` child, imports the shim by the name
7
+ its callers use, and reads back where the helper came from.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ _PACKAGE_ROOT = Path(__file__).resolve().parents[2]
20
+ _SHIM_MODULE_NAME = "subprocess_window_access"
21
+ _SHIM_FILE_NAME = f"{_SHIM_MODULE_NAME}.py"
22
+ _PACKAGED_HELPER_PATH = (
23
+ _PACKAGE_ROOT / "hooks" / "hooks_constants" / "subprocess_window.py"
24
+ )
25
+ _ALL_EXPORTED_HELPER_NAMES = (
26
+ "HiddenWindowStartupInfo",
27
+ "detached_hidden_window_creation_flags",
28
+ "hidden_window_creation_flags",
29
+ "inherited_stream_startup_info",
30
+ )
31
+ _ALL_SHIM_DIRECTORY_PATHS = (
32
+ "scripts",
33
+ "_shared/pr-loop/scripts",
34
+ "_shared/process-tree/scripts",
35
+ "hooks/git-hooks",
36
+ "hooks/validators",
37
+ ".agents/skills/fresh-branch/scripts",
38
+ ".agents/skills/syncing-submodules/scripts",
39
+ ".agents/skills/test-runner/scripts",
40
+ )
41
+
42
+ _IMPORT_PROBE_SOURCE = "\n".join(
43
+ (
44
+ "import json",
45
+ "import sys",
46
+ "shim_directory = sys.argv[1]",
47
+ "sys.path.insert(0, shim_directory)",
48
+ "head_before_import = sys.path[0]",
49
+ f"import {_SHIM_MODULE_NAME} as shim",
50
+ "print(json.dumps({",
51
+ " 'helper_path': sys.modules[",
52
+ " shim.hidden_window_creation_flags.__module__",
53
+ " ].__file__,",
54
+ " 'head_after_import': sys.path[0],",
55
+ " 'head_before_import': head_before_import,",
56
+ " 'exported_names': sorted(shim.__all__),",
57
+ "}))",
58
+ )
59
+ )
60
+
61
+
62
+ def _import_probe_results(shim_directory: Path) -> dict[str, object]:
63
+ """Import one shim in a ``-S -E`` child and read back what it resolved."""
64
+ completed_process = subprocess.run(
65
+ [
66
+ sys.executable,
67
+ "-S",
68
+ "-E",
69
+ "-c",
70
+ _IMPORT_PROBE_SOURCE,
71
+ str(shim_directory),
72
+ ],
73
+ cwd=str(_PACKAGE_ROOT),
74
+ capture_output=True,
75
+ text=True,
76
+ check=False,
77
+ )
78
+ assert completed_process.returncode == 0, completed_process.stderr
79
+ return json.loads(completed_process.stdout)
80
+
81
+
82
+ @pytest.mark.parametrize("shim_directory_path", _ALL_SHIM_DIRECTORY_PATHS)
83
+ def test_every_deployed_directory_carries_a_shim(shim_directory_path: str) -> None:
84
+ """Each directory whose scripts import the shim also ships one."""
85
+ assert (_PACKAGE_ROOT / shim_directory_path / _SHIM_FILE_NAME).is_file()
86
+
87
+
88
+ @pytest.mark.parametrize("shim_directory_path", _ALL_SHIM_DIRECTORY_PATHS)
89
+ def test_shim_resolves_the_packaged_helper_without_an_installed_package(
90
+ shim_directory_path: str,
91
+ ) -> None:
92
+ """A ``-S -E`` child reaches the packaged helper through the shim alone."""
93
+ all_probe_results = _import_probe_results(_PACKAGE_ROOT / shim_directory_path)
94
+
95
+ assert Path(str(all_probe_results["helper_path"])) == _PACKAGED_HELPER_PATH
96
+
97
+
98
+ @pytest.mark.parametrize("shim_directory_path", _ALL_SHIM_DIRECTORY_PATHS)
99
+ def test_shim_leaves_the_head_of_the_search_path_alone(
100
+ shim_directory_path: str,
101
+ ) -> None:
102
+ """A caller that prepended its own roots keeps them ahead of the hooks tree."""
103
+ all_probe_results = _import_probe_results(_PACKAGE_ROOT / shim_directory_path)
104
+
105
+ assert (
106
+ all_probe_results["head_after_import"]
107
+ == (all_probe_results["head_before_import"])
108
+ )
109
+
110
+
111
+ @pytest.mark.parametrize("shim_directory_path", _ALL_SHIM_DIRECTORY_PATHS)
112
+ def test_shim_re_exports_every_hidden_window_helper(
113
+ shim_directory_path: str,
114
+ ) -> None:
115
+ """Every caller finds the name it needs on the shim it imports."""
116
+ all_probe_results = _import_probe_results(_PACKAGE_ROOT / shim_directory_path)
117
+
118
+ assert all_probe_results["exported_names"] == sorted(_ALL_EXPORTED_HELPER_NAMES)