claude-dev-env 1.92.1 → 1.93.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 (214) hide show
  1. package/CLAUDE.md +6 -2
  2. package/_shared/CLAUDE.md +2 -1
  3. package/_shared/advisor/CLAUDE.md +21 -0
  4. package/_shared/advisor/advisor-protocol.md +142 -0
  5. package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
  6. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
  7. package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
  8. package/_shared/advisor/scripts/pyproject.toml +3 -0
  9. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
  10. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
  11. package/_shared/advisor/scripts/tier_model_ids.py +164 -0
  12. package/_shared/pr-loop/CLAUDE.md +1 -0
  13. package/_shared/pr-loop/precatch-rubric.md +65 -0
  14. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  15. package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
  16. package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
  18. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
  19. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
  20. package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
  21. package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
  22. package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
  23. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
  24. package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
  25. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
  26. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
  27. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
  28. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
  29. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
  30. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
  31. package/agents/CLAUDE.md +3 -2
  32. package/agents/clasp-deployment-orchestrator.md +2 -2
  33. package/agents/code-advisor.md +7 -5
  34. package/agents/code-verifier.md +6 -2
  35. package/agents/session-advisor.md +27 -0
  36. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
  37. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
  38. package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
  39. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  40. package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
  41. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
  42. package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
  43. package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
  44. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
  45. package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
  46. package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
  47. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
  48. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
  49. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
  50. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
  51. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
  52. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  53. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
  54. package/bin/CLAUDE.md +1 -0
  55. package/bin/expand_home_directory_tokens.mjs +75 -0
  56. package/bin/install.mjs +21 -7
  57. package/bin/install.test.mjs +220 -10
  58. package/hooks/CLAUDE.md +1 -1
  59. package/hooks/blocking/CLAUDE.md +4 -1
  60. package/hooks/blocking/_path_setup.py +13 -0
  61. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
  62. package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
  63. package/hooks/blocking/code_rules_enforcer.py +6 -0
  64. package/hooks/blocking/code_rules_imports_logging.py +34 -1
  65. package/hooks/blocking/code_rules_paths_syspath.py +1 -1
  66. package/hooks/blocking/code_rules_test_layout.py +272 -0
  67. package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
  68. package/hooks/blocking/pii_prevention_blocker.py +781 -0
  69. package/hooks/blocking/pii_scanner.py +340 -0
  70. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  71. package/hooks/blocking/stop_dispatcher.py +84 -0
  72. package/hooks/blocking/test__path_setup.py +39 -0
  73. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
  74. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
  75. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
  76. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
  77. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
  78. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
  79. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
  80. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
  81. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
  82. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
  83. package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
  84. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
  85. package/hooks/blocking/test_destructive_command_blocker.py +29 -9
  86. package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
  87. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
  88. package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
  89. package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
  90. package/hooks/blocking/test_pii_scanner.py +165 -0
  91. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
  92. package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
  93. package/hooks/blocking/test_stop_dispatcher.py +187 -0
  94. package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
  95. package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
  96. package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
  97. package/hooks/diagnostic/migrations/README.md +25 -26
  98. package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
  99. package/hooks/hooks.json +10 -120
  100. package/hooks/hooks_constants/CLAUDE.md +12 -3
  101. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
  102. package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
  103. package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
  104. package/hooks/hooks_constants/local_identity.py +182 -0
  105. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
  106. package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
  107. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  108. package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
  109. package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
  110. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
  111. package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
  112. package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
  113. package/hooks/hooks_constants/test_layout_constants.py +28 -0
  114. package/hooks/hooks_constants/test_local_identity.py +88 -0
  115. package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
  116. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
  117. package/hooks/validators/README.md +5 -6
  118. package/hooks/validators/__init__.py +2 -2
  119. package/hooks/validators/python_style_checks.py +298 -243
  120. package/hooks/validators/run_all_validators.py +6 -0
  121. package/hooks/validators/test_python_style_checks.py +278 -163
  122. package/package.json +2 -2
  123. package/rules/CLAUDE.md +2 -1
  124. package/rules/bdd.md +1 -1
  125. package/rules/nas-ssh-invocation.md +6 -4
  126. package/rules/no-justification-noise.md +61 -0
  127. package/scripts/test_setup_project_paths.py +1 -1
  128. package/skills/CLAUDE.md +6 -3
  129. package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
  130. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
  131. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
  132. package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
  133. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
  134. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
  135. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
  136. package/skills/autoconverge/CLAUDE.md +3 -3
  137. package/skills/autoconverge/SKILL.md +113 -35
  138. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  139. package/skills/autoconverge/reference/convergence.md +44 -22
  140. package/skills/autoconverge/reference/gotchas.md +11 -0
  141. package/skills/autoconverge/reference/stop-conditions.md +23 -13
  142. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  143. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
  144. package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
  145. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
  146. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
  147. package/skills/autoconverge/workflow/converge.mjs +329 -68
  148. package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
  149. package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
  150. package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
  151. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
  152. package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
  153. package/skills/bdd-protocol/SKILL.md +4 -5
  154. package/skills/bdd-protocol/references/anti-patterns.md +1 -1
  155. package/skills/bugteam/SKILL.md +8 -0
  156. package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
  157. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
  158. package/skills/bugteam/reference/team-setup.md +7 -5
  159. package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
  160. package/skills/copilot-finding-triage/SKILL.md +124 -0
  161. package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
  162. package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
  163. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
  164. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
  165. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
  166. package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
  167. package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
  168. package/skills/copilot-finding-triage/templates/notification.md +34 -0
  169. package/skills/copilot-review/SKILL.md +8 -6
  170. package/skills/findbugs/SKILL.md +4 -0
  171. package/skills/fixbugs/SKILL.md +8 -7
  172. package/skills/gotcha/CLAUDE.md +2 -2
  173. package/skills/gotcha/SKILL.md +4 -4
  174. package/skills/log-audit/SKILL.md +4 -6
  175. package/skills/monitor-open-prs/CLAUDE.md +1 -1
  176. package/skills/monitor-open-prs/SKILL.md +6 -1
  177. package/skills/orchestrator/SKILL.md +74 -78
  178. package/skills/orchestrator-refresh/SKILL.md +24 -12
  179. package/skills/post-audit-findings/SKILL.md +5 -9
  180. package/skills/pr-consistency-audit/SKILL.md +5 -1
  181. package/skills/pr-converge/CLAUDE.md +1 -1
  182. package/skills/pr-converge/SKILL.md +86 -47
  183. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  184. package/skills/pr-converge/reference/convergence-gates.md +20 -17
  185. package/skills/pr-converge/reference/examples.md +63 -47
  186. package/skills/pr-converge/reference/fix-protocol.md +15 -15
  187. package/skills/pr-converge/reference/ground-rules.md +11 -7
  188. package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
  189. package/skills/pr-converge/reference/per-tick.md +129 -107
  190. package/skills/pr-converge/reference/state-schema.md +15 -10
  191. package/skills/pr-converge/scripts/check_convergence.py +5 -4
  192. package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
  193. package/skills/pr-fix-protocol/SKILL.md +3 -8
  194. package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
  195. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
  196. package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
  197. package/skills/pr-loop-lifecycle/SKILL.md +5 -9
  198. package/skills/pr-scope-resolve/SKILL.md +3 -6
  199. package/skills/privacy-hygiene/SKILL.md +114 -0
  200. package/skills/qbug/SKILL.md +8 -8
  201. package/skills/rebase/SKILL.md +5 -1
  202. package/skills/refine/SKILL.md +4 -5
  203. package/skills/reviewer-gates/SKILL.md +7 -11
  204. package/skills/session-log/SKILL.md +4 -1
  205. package/skills/skill-builder/SKILL.md +3 -6
  206. package/skills/structure-prompt/SKILL.md +4 -5
  207. package/skills/team-advisor/SKILL.md +56 -0
  208. package/skills/test_markdown_link_integrity.py +10 -6
  209. package/skills/update/SKILL.md +5 -1
  210. package/skills/usage-pause/SKILL.md +14 -5
  211. package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
  212. package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
  213. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
  214. package/skills/verified-build/SKILL.md +4 -9
@@ -1,22 +1,24 @@
1
1
  """Flag a prose term that names a code identifier a hair differently.
2
2
 
3
- A change adds the field ``premium_interactions`` while the same branch's docs
4
- call it the ``premium-request`` budget: one thing, two names, and a reader who
5
- searches one never finds the other.
3
+ A change adds the field ``premium_request_interactions`` while the same
4
+ branch's docs call it the ``premium-request-budget`` field: one thing, two
5
+ names, and a reader who searches one never finds the other.
6
6
 
7
7
  ::
8
8
 
9
- code adds: premium_interactions the identifier
10
- prose adds: the premium-request budget a hyphenated term
11
- flag: premium-request vs premium_interactions -- shared first word, tail diverges
12
- ok: premium-interactions -- exact hyphen form, agrees
13
- ok: read-only, test files, to a file -- English compound, plural, or stopword
9
+ code adds: premium_request_interactions the identifier
10
+ prose adds: the premium-request-budget field a hyphenated term
11
+ flag: premium-request-budget vs premium_request_interactions -- only the final word differs
12
+ ok: premium-request-interactions -- exact hyphen form, agrees
13
+ ok: premium-report-total vs premium_request_interactions -- an earlier word differs, ordinary prose
14
14
 
15
15
  The sweep reads a unified diff, collects the multi-word identifiers added on
16
16
  code lines, and scans each added prose line for a hyphen or space variant that
17
- shares an identifier's leading word but diverges after it. It spares ordinary
18
- prose: English compound tails, singular/plural forms, stopword windows, and
19
- words that are themselves tokens of another introduced identifier.
17
+ renames only the final word of an identifier: it agrees on every earlier word
18
+ and differs on the last. A term that diverges in any earlier word, or shares
19
+ only one leading word, is ordinary prose. It also spares English compound
20
+ tails, singular/plural forms, stopword windows, and words that are themselves
21
+ tokens of another introduced identifier.
20
22
  """
21
23
 
22
24
  import argparse
@@ -345,6 +347,47 @@ def _tuples_match_ignoring_plural(
345
347
  )
346
348
 
347
349
 
350
+ def _diverges_only_in_final_token(
351
+ candidate_tuple: IdentifierTuple, identifier_tuple: IdentifierTuple
352
+ ) -> bool:
353
+ """Return whether two equal-length tuples agree on every token but the last.
354
+
355
+ ::
356
+
357
+ candidate: ("premium", "request", "budget")
358
+ identifier: ("premium", "request", "interactions")
359
+ flag: every earlier token agrees, so the prose renames the final word
360
+ ok: ("premium", "report", "total") -- an earlier token diverges too
361
+
362
+ A near-miss renames a single trailing word of an identifier, so every token
363
+ before the last agrees by an exact or singular/plural match while the final
364
+ token differs. A tuple no longer than the two-token identifier minimum
365
+ cannot carry two agreeing leading tokens, so it never qualifies.
366
+
367
+ Args:
368
+ candidate_tuple: The prose term's lowercase token tuple.
369
+ identifier_tuple: An introduced identifier's lowercase token tuple.
370
+
371
+ Returns:
372
+ True when both tuples run longer than the two-token identifier minimum,
373
+ share the same length, agree by exact or singular/plural match on every
374
+ token but the last, and differ on the final token.
375
+ """
376
+ if (
377
+ len(candidate_tuple) <= MINIMUM_IDENTIFIER_TOKEN_COUNT
378
+ or len(candidate_tuple) != len(identifier_tuple)
379
+ ):
380
+ return False
381
+ if _tokens_are_plural_variants(candidate_tuple[-1], identifier_tuple[-1]):
382
+ return False
383
+ return all(
384
+ _tokens_are_plural_variants(each_candidate_token, each_identifier_token)
385
+ for each_candidate_token, each_identifier_token in zip(
386
+ candidate_tuple[:-1], identifier_tuple[:-1]
387
+ )
388
+ )
389
+
390
+
348
391
  def _word_is_identifier_vocabulary(
349
392
  prose_word: str, all_identifier_tokens: frozenset[str]
350
393
  ) -> bool:
@@ -391,16 +434,19 @@ def _near_miss_identifier(
391
434
  lines, used to spare prose built from real code vocabulary.
392
435
 
393
436
  Returns:
394
- The first identifier sharing the candidate's leading token and length
395
- but differing in a later token. None marks the candidate ordinary
396
- prose instead, on any of these grounds: it matches an identifier
397
- exactly; it shares no prefix with one; it ends in a common English
398
- compound tail word (``read-only``, ``data-driven``); it contains an
399
- English stopword (``to a``, ``each image``); it differs from the
400
- identifier only by a singular/plural form of one or more tokens
401
- (``test files`` against ``test_file``); or every diverging word is
402
- itself a token of some introduced identifier (``target box`` when
403
- ``box_height`` is also in the diff).
437
+ The first identifier the candidate renames in only its final token. Such a
438
+ candidate agrees on every earlier token, matches the identifier's
439
+ length, and differs on the last token. None marks the candidate
440
+ ordinary prose instead, on any of these grounds: it matches an
441
+ identifier exactly; it diverges in a token before the last
442
+ (``premium report total`` against ``premium_request_interactions``);
443
+ it holds no more than the two-token identifier minimum; it ends in a
444
+ common English compound tail word (``read-only``, ``data-driven``); it
445
+ contains an English stopword (``to a``, ``each image``); it differs
446
+ from the identifier only by a singular/plural form of one or more
447
+ tokens (``test files`` against ``test_file``); or every diverging word
448
+ is itself a token of some introduced identifier (``target width box``
449
+ when ``box_height`` is also in the diff).
404
450
  """
405
451
  if not candidate_tuple or candidate_tuple in all_identifier_tuples:
406
452
  return None
@@ -415,6 +461,8 @@ def _near_miss_identifier(
415
461
  continue
416
462
  if _tuples_match_ignoring_plural(each_identifier, candidate_tuple):
417
463
  continue
464
+ if not _diverges_only_in_final_token(candidate_tuple, each_identifier):
465
+ continue
418
466
  diverging_words = [
419
467
  each_word
420
468
  for each_word, each_token in zip(candidate_tuple, each_identifier)
@@ -32,7 +32,7 @@ pytest suite for the scripts and constants in `_shared/pr-loop/scripts/`. Each t
32
32
 
33
33
  ## Fixtures
34
34
 
35
- `fixtures/copilot_internal_user_jonecho.json` — a captured `gh api
35
+ `fixtures/copilot_internal_user_example.json` — a captured `gh api
36
36
  copilot_internal/user` response driving `test_copilot_quota.py`.
37
37
 
38
38
  ## Running
@@ -1,5 +1,5 @@
1
1
  {
2
- "login": "jonecho",
2
+ "login": "example-user",
3
3
  "access_type_sku": "plus_monthly_subscriber_quota",
4
4
  "assigned_date": "2026-05-09T14:08:29-04:00",
5
5
  "can_signup_for_limited": false,
@@ -336,7 +336,7 @@ def test_is_trust_entry_rejects_cross_project_path_suffix_collision() -> None:
336
336
  short_project_path = "/projects/foo"
337
337
  trust_prefix = "Trusted local workspace:"
338
338
  longer_unrelated_path_entry = (
339
- "Trusted local workspace: /Users/jon/projects/foo/.claude/** unrelated path"
339
+ "Trusted local workspace: /Users/example/projects/foo/.claude/** unrelated path"
340
340
  )
341
341
  assert (
342
342
  common_module.is_trust_entry_for_project(
@@ -337,11 +337,11 @@ def test_main_staged_mode_passes_when_only_terminology_findings_exist(
337
337
  commit_all_files(temporary_git_repository, "initial")
338
338
  write_file(
339
339
  temporary_git_repository / "quota.py",
340
- "first_count = 1\npremium_interactions = 1\n",
340
+ "first_count = 1\npremium_request_interactions = 1\n",
341
341
  )
342
342
  write_file(
343
343
  temporary_git_repository / "README.md",
344
- "The premium-request budget gates the run.\n",
344
+ "The premium-request-budget field gates the run.\n",
345
345
  )
346
346
  stage_file(temporary_git_repository, "quota.py")
347
347
  stage_file(temporary_git_repository, "README.md")
@@ -2043,6 +2043,88 @@ def test_run_staged_test_files_zero_when_staged_conftest_collects_no_tests(
2043
2043
  assert gate_module.run_staged_test_files(temporary_git_repository) == 0
2044
2044
 
2045
2045
 
2046
+ def test_staged_pytest_skipped_below_minimum_python(
2047
+ temporary_git_repository: Path,
2048
+ monkeypatch: pytest.MonkeyPatch,
2049
+ capsys: pytest.CaptureFixture[str],
2050
+ ) -> None:
2051
+ """Below the minimum Python, the staged pytest step is skipped, not run.
2052
+
2053
+ A staged test that would fail returns a zero exit code because the older
2054
+ interpreter cannot run its newer-API calls, so the environment gap does not
2055
+ block the commit.
2056
+ """
2057
+ write_file(
2058
+ temporary_git_repository / "test_needs_new_python.py",
2059
+ "def test_intentionally_fails() -> None:\n assert False\n",
2060
+ )
2061
+ stage_file(temporary_git_repository, "test_needs_new_python.py")
2062
+ monkeypatch.setattr(gate_module, "MINIMUM_STAGED_PYTEST_PYTHON_MAJOR", 99)
2063
+
2064
+ exit_code = gate_module._staged_pytest_exit_code_for_current_python(
2065
+ temporary_git_repository
2066
+ )
2067
+
2068
+ captured = capsys.readouterr()
2069
+ assert exit_code == 0
2070
+ assert "skipping the staged pytest step" in captured.err
2071
+
2072
+
2073
+ def test_staged_pytest_runs_at_or_above_minimum_python(
2074
+ temporary_git_repository: Path,
2075
+ monkeypatch: pytest.MonkeyPatch,
2076
+ ) -> None:
2077
+ """At or above the minimum Python, the staged pytest step runs and a failing
2078
+ staged test blocks with a non-zero exit code.
2079
+ """
2080
+ write_file(
2081
+ temporary_git_repository / "test_fails.py",
2082
+ "def test_intentionally_fails() -> None:\n assert False\n",
2083
+ )
2084
+ stage_file(temporary_git_repository, "test_fails.py")
2085
+ monkeypatch.setattr(gate_module, "MINIMUM_STAGED_PYTEST_PYTHON_MAJOR", 2)
2086
+
2087
+ exit_code = gate_module._staged_pytest_exit_code_for_current_python(
2088
+ temporary_git_repository
2089
+ )
2090
+
2091
+ assert exit_code != 0
2092
+
2093
+
2094
+ def test_main_staged_skips_pytest_but_runs_code_rules_below_minimum_python(
2095
+ temporary_git_repository: Path,
2096
+ monkeypatch: pytest.MonkeyPatch,
2097
+ capsys: pytest.CaptureFixture[str],
2098
+ ) -> None:
2099
+ """The version guard skips only the staged pytest step. A staged code-rules
2100
+ violation still blocks the commit, so the two checks stay independent.
2101
+ """
2102
+ write_file(temporary_git_repository / "module.py", "first_count = 1\n")
2103
+ commit_all_files(temporary_git_repository, "initial")
2104
+ write_file(
2105
+ temporary_git_repository / "module.py",
2106
+ "first_count = 1\n"
2107
+ "def compute_total(operand: int) -> int:\n"
2108
+ " result = operand + 1\n"
2109
+ " return result\n",
2110
+ )
2111
+ write_file(
2112
+ temporary_git_repository / "test_fails.py",
2113
+ "def test_intentionally_fails() -> None:\n assert False\n",
2114
+ )
2115
+ stage_file(temporary_git_repository, "module.py")
2116
+ stage_file(temporary_git_repository, "test_fails.py")
2117
+ monkeypatch.setattr(gate_module, "MINIMUM_STAGED_PYTEST_PYTHON_MAJOR", 99)
2118
+ monkeypatch.chdir(temporary_git_repository)
2119
+
2120
+ exit_code = gate_module.main(["--staged"])
2121
+
2122
+ captured = capsys.readouterr()
2123
+ assert exit_code == 1
2124
+ assert "skipping the staged pytest step" in captured.err
2125
+ assert "result" in captured.err
2126
+
2127
+
2046
2128
  def test_hunk_header_pattern_captures_new_start_and_count() -> None:
2047
2129
  header_match = gate_module.hunk_header_pattern().match("@@ -12,3 +45,6 @@ def scope")
2048
2130
  assert header_match is not None
@@ -2578,3 +2660,65 @@ def test_run_staged_test_files_fails_when_one_group_fails(
2578
2660
  assert exit_code != 0
2579
2661
  captured_error = capsys.readouterr().err
2580
2662
  assert "failing_skill" in captured_error
2663
+
2664
+
2665
+ def test_resolve_gate_python_executable_prefers_env_var_over_venv(
2666
+ tmp_path: Path,
2667
+ monkeypatch: pytest.MonkeyPatch,
2668
+ ) -> None:
2669
+ relative_segments = (
2670
+ gate_module.ALL_WINDOWS_VENV_PYTHON_RELATIVE_PATH_SEGMENTS
2671
+ if os.name == "nt"
2672
+ else gate_module.ALL_POSIX_VENV_PYTHON_RELATIVE_PATH_SEGMENTS
2673
+ )
2674
+ venv_python = tmp_path.joinpath(".venv", *relative_segments)
2675
+ venv_python.parent.mkdir(parents=True)
2676
+ venv_python.write_text("", encoding="utf-8")
2677
+ monkeypatch.setenv("CODE_RULES_GATE_PYTHON", "/configured/python")
2678
+
2679
+ resolved = gate_module._resolve_gate_python_executable(tmp_path)
2680
+
2681
+ assert resolved == "/configured/python"
2682
+
2683
+
2684
+ def test_resolve_gate_python_executable_falls_back_to_project_venv(
2685
+ tmp_path: Path,
2686
+ monkeypatch: pytest.MonkeyPatch,
2687
+ ) -> None:
2688
+ monkeypatch.delenv("CODE_RULES_GATE_PYTHON", raising=False)
2689
+ relative_segments = (
2690
+ gate_module.ALL_WINDOWS_VENV_PYTHON_RELATIVE_PATH_SEGMENTS
2691
+ if os.name == "nt"
2692
+ else gate_module.ALL_POSIX_VENV_PYTHON_RELATIVE_PATH_SEGMENTS
2693
+ )
2694
+ venv_python = tmp_path.joinpath(".venv", *relative_segments)
2695
+ venv_python.parent.mkdir(parents=True)
2696
+ venv_python.write_text("", encoding="utf-8")
2697
+
2698
+ resolved = gate_module._resolve_gate_python_executable(tmp_path)
2699
+
2700
+ assert resolved == str(venv_python)
2701
+
2702
+
2703
+ def test_resolve_gate_python_executable_falls_back_to_sys_executable(
2704
+ tmp_path: Path,
2705
+ monkeypatch: pytest.MonkeyPatch,
2706
+ ) -> None:
2707
+ monkeypatch.delenv("CODE_RULES_GATE_PYTHON", raising=False)
2708
+
2709
+ resolved = gate_module._resolve_gate_python_executable(tmp_path)
2710
+
2711
+ assert resolved == sys.executable
2712
+
2713
+
2714
+ def test_staged_pytest_environment_prepends_configured_pythonpath(
2715
+ monkeypatch: pytest.MonkeyPatch,
2716
+ ) -> None:
2717
+ monkeypatch.setenv("CODE_RULES_GATE_PYTHONPATH", "/configured/site-packages")
2718
+ monkeypatch.setenv("PYTHONPATH", "/already/on/path")
2719
+
2720
+ environment = gate_module._staged_pytest_environment()
2721
+
2722
+ assert environment["PYTHONPATH"] == os.pathsep.join(
2723
+ ["/configured/site-packages", "/already/on/path"]
2724
+ )
@@ -2,7 +2,7 @@
2
2
 
3
3
  Every case drives the production ``main`` / ``evaluate_copilot_quota`` path with
4
4
  only the ``gh`` subprocess boundary (``_run_gh``) stubbed, fed a captured
5
- ``copilot_internal/user`` JSON fixture in the jonecho shape.
5
+ ``copilot_internal/user`` JSON fixture in the example-account shape.
6
6
  """
7
7
 
8
8
  import importlib.util
@@ -30,7 +30,7 @@ def _load_copilot_quota_module() -> ModuleType:
30
30
 
31
31
  copilot_quota = _load_copilot_quota_module()
32
32
 
33
- FIXTURE_PATH = Path(__file__).parent / "fixtures" / "copilot_internal_user_jonecho.json"
33
+ FIXTURE_PATH = Path(__file__).parent / "fixtures" / "copilot_internal_user_example.json"
34
34
  AVAILABLE_USER_JSON = FIXTURE_PATH.read_text(encoding="utf-8")
35
35
  FAKE_TOKEN_RESULT = (0, "ghp_faketoken_value\n")
36
36
 
@@ -75,7 +75,7 @@ def _isolate_account_sources(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) ->
75
75
  def test_main_runs_copilot_when_premium_quota_available(
76
76
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
77
77
  ) -> None:
78
- monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
78
+ monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "example-account")
79
79
  monkeypatch.setattr(
80
80
  copilot_quota,
81
81
  "_run_gh",
@@ -91,7 +91,7 @@ def test_main_runs_copilot_when_premium_quota_available(
91
91
  def test_main_exits_out_of_quota_when_premium_exhausted(
92
92
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
93
93
  ) -> None:
94
- monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
94
+ monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "example-account")
95
95
  monkeypatch.setattr(
96
96
  copilot_quota,
97
97
  "_run_gh",
@@ -106,7 +106,7 @@ def test_main_exits_out_of_quota_when_premium_exhausted(
106
106
  def test_main_exits_api_down_on_non_json_response(
107
107
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
108
108
  ) -> None:
109
- monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
109
+ monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "example-account")
110
110
  monkeypatch.setattr(
111
111
  copilot_quota,
112
112
  "_run_gh",
@@ -121,7 +121,7 @@ def test_main_exits_api_down_on_non_json_response(
121
121
  def test_main_exits_api_down_on_missing_premium_snapshot(
122
122
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
123
123
  ) -> None:
124
- monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
124
+ monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "example-account")
125
125
  monkeypatch.setattr(
126
126
  copilot_quota,
127
127
  "_run_gh",
@@ -136,7 +136,7 @@ def test_main_exits_api_down_on_missing_premium_snapshot(
136
136
  def test_main_exits_api_down_when_gh_token_unresolved(
137
137
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
138
138
  ) -> None:
139
- monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
139
+ monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "example-account")
140
140
  monkeypatch.setattr(
141
141
  copilot_quota,
142
142
  "_run_gh",
@@ -180,7 +180,7 @@ def test_every_gh_backed_skip_writes_a_log_line_naming_the_scenario(
180
180
  expected_exit_code: int,
181
181
  expected_scenario: str,
182
182
  ) -> None:
183
- monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
183
+ monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "example-account")
184
184
  monkeypatch.setattr(
185
185
  copilot_quota, "_run_gh", _gh_stub(FAKE_TOKEN_RESULT, api_result)
186
186
  )
@@ -33,7 +33,7 @@ def _load_module(module_name: str) -> ModuleType:
33
33
  copilot_quota = _load_module("copilot_quota")
34
34
  reviewer_availability = _load_module("reviewer_availability")
35
35
 
36
- FIXTURE_PATH = Path(__file__).parent / "fixtures" / "copilot_internal_user_jonecho.json"
36
+ FIXTURE_PATH = Path(__file__).parent / "fixtures" / "copilot_internal_user_example.json"
37
37
  AVAILABLE_USER_JSON = FIXTURE_PATH.read_text(encoding="utf-8")
38
38
  FAKE_TOKEN_RESULT = (0, "ghp_faketoken_value\n")
39
39
 
@@ -67,6 +67,7 @@ def _isolate_reviewer_environment(
67
67
  ) -> None:
68
68
  monkeypatch.delenv("COPILOT_QUOTA_ACCOUNT", raising=False)
69
69
  monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
70
+ monkeypatch.delenv("CLAUDE_REVIEWS_ENABLED", raising=False)
70
71
  monkeypatch.setattr(
71
72
  reviewer_availability, "COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH", tmp_path / ".env"
72
73
  )
@@ -75,7 +76,7 @@ def _isolate_reviewer_environment(
75
76
  def test_main_reports_copilot_available_when_quota_confirmed_available(
76
77
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
77
78
  ) -> None:
78
- monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
79
+ monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "example-account")
79
80
  monkeypatch.setattr(
80
81
  copilot_quota, "_run_gh", _gh_stub(FAKE_TOKEN_RESULT, (0, AVAILABLE_USER_JSON))
81
82
  )
@@ -106,7 +107,7 @@ def test_main_reports_copilot_down_when_opted_out_via_env(
106
107
  def test_main_reports_copilot_down_when_out_of_quota(
107
108
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
108
109
  ) -> None:
109
- monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
110
+ monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "example-account")
110
111
  monkeypatch.setattr(
111
112
  copilot_quota,
112
113
  "_run_gh",
@@ -121,7 +122,7 @@ def test_main_reports_copilot_down_when_out_of_quota(
121
122
  def test_main_reports_copilot_down_when_quota_api_is_down(
122
123
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
123
124
  ) -> None:
124
- monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
125
+ monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "example-account")
125
126
  monkeypatch.setattr(
126
127
  copilot_quota, "_run_gh", _gh_stub(FAKE_TOKEN_RESULT, (0, "not json at all"))
127
128
  )
@@ -140,9 +141,20 @@ def test_main_reports_copilot_down_when_no_account_configured(
140
141
  assert "scenario C" in captured.err
141
142
 
142
143
 
143
- def test_main_reports_bugbot_available_when_not_opted_out(
144
+ def test_main_reports_bugbot_down_when_no_env_vars_set(
144
145
  capsys: pytest.CaptureFixture[str],
145
146
  ) -> None:
147
+ exit_code = reviewer_availability.main(["--reviewer", "bugbot"])
148
+ captured = capsys.readouterr()
149
+ assert exit_code == reviewer_availability.EXIT_CODE_REVIEWER_DOWN
150
+ assert "CLAUDE_REVIEWS_ENABLED" in captured.err
151
+ assert "CLAUDE_REVIEWS_DISABLED" not in captured.err
152
+
153
+
154
+ def test_main_reports_bugbot_available_when_enabled_via_env(
155
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
156
+ ) -> None:
157
+ monkeypatch.setenv("CLAUDE_REVIEWS_ENABLED", "bugbot")
146
158
  exit_code = reviewer_availability.main(["--reviewer", "bugbot"])
147
159
  captured = capsys.readouterr()
148
160
  assert exit_code == reviewer_availability.EXIT_CODE_REVIEWER_AVAILABLE
@@ -153,6 +165,7 @@ def test_main_reports_bugbot_down_when_opted_out_via_env(
153
165
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
154
166
  ) -> None:
155
167
  monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
168
+ monkeypatch.setenv("CLAUDE_REVIEWS_ENABLED", "bugbot")
156
169
  exit_code = reviewer_availability.main(["--reviewer", "bugbot"])
157
170
  captured = capsys.readouterr()
158
171
  assert exit_code == reviewer_availability.EXIT_CODE_REVIEWER_DOWN
@@ -43,20 +43,46 @@ def test_is_bugbot_disabled_via_env_returns_true_when_env_lists_bugbot(
43
43
  assert reviews_disabled.is_bugbot_disabled_via_env() is True
44
44
 
45
45
 
46
- def test_is_bugbot_disabled_via_env_returns_false_when_env_is_empty(
46
+ def test_is_bugbot_disabled_via_env_returns_true_when_no_env_vars_set(
47
47
  monkeypatch: pytest.MonkeyPatch,
48
48
  ) -> None:
49
49
  monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
50
+ monkeypatch.delenv("CLAUDE_REVIEWS_ENABLED", raising=False)
51
+ assert reviews_disabled.is_bugbot_disabled_via_env() is True
52
+
53
+
54
+ def test_is_bugbot_disabled_via_env_returns_false_when_enabled_lists_bugbot(
55
+ monkeypatch: pytest.MonkeyPatch,
56
+ ) -> None:
57
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
58
+ monkeypatch.setenv("CLAUDE_REVIEWS_ENABLED", "bugbot")
50
59
  assert reviews_disabled.is_bugbot_disabled_via_env() is False
51
60
 
52
61
 
53
- def test_is_bugbot_disabled_via_env_returns_false_when_only_bugteam_listed(
62
+ def test_is_bugbot_disabled_via_env_enabled_parse_is_case_and_space_tolerant(
54
63
  monkeypatch: pytest.MonkeyPatch,
55
64
  ) -> None:
56
- monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
65
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
66
+ monkeypatch.setenv("CLAUDE_REVIEWS_ENABLED", " BugBot ")
57
67
  assert reviews_disabled.is_bugbot_disabled_via_env() is False
58
68
 
59
69
 
70
+ def test_is_bugbot_disabled_via_env_true_when_disabled_overrides_enabled(
71
+ monkeypatch: pytest.MonkeyPatch,
72
+ ) -> None:
73
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
74
+ monkeypatch.setenv("CLAUDE_REVIEWS_ENABLED", "bugbot")
75
+ assert reviews_disabled.is_bugbot_disabled_via_env() is True
76
+
77
+
78
+ def test_is_bugbot_disabled_via_env_true_when_bugteam_listed_and_not_enabled(
79
+ monkeypatch: pytest.MonkeyPatch,
80
+ ) -> None:
81
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
82
+ monkeypatch.delenv("CLAUDE_REVIEWS_ENABLED", raising=False)
83
+ assert reviews_disabled.is_bugbot_disabled_via_env() is True
84
+
85
+
60
86
  def test_is_bugbot_disabled_via_env_true_when_both_tokens_listed_mixed_case(
61
87
  monkeypatch: pytest.MonkeyPatch,
62
88
  ) -> None:
@@ -65,6 +91,29 @@ def test_is_bugbot_disabled_via_env_true_when_both_tokens_listed_mixed_case(
65
91
  assert reviews_disabled.is_bugteam_disabled_via_env() is True
66
92
 
67
93
 
94
+ def test_is_bugbot_opted_out_via_env_returns_true_when_disabled_lists_bugbot(
95
+ monkeypatch: pytest.MonkeyPatch,
96
+ ) -> None:
97
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
98
+ assert reviews_disabled.is_bugbot_opted_out_via_env() is True
99
+
100
+
101
+ def test_is_bugbot_opted_out_via_env_returns_false_when_only_enabled_lists_bugbot(
102
+ monkeypatch: pytest.MonkeyPatch,
103
+ ) -> None:
104
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
105
+ monkeypatch.setenv("CLAUDE_REVIEWS_ENABLED", "bugbot")
106
+ assert reviews_disabled.is_bugbot_opted_out_via_env() is False
107
+
108
+
109
+ def test_is_bugbot_opted_out_via_env_returns_false_when_no_env_vars_set(
110
+ monkeypatch: pytest.MonkeyPatch,
111
+ ) -> None:
112
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
113
+ monkeypatch.delenv("CLAUDE_REVIEWS_ENABLED", raising=False)
114
+ assert reviews_disabled.is_bugbot_opted_out_via_env() is False
115
+
116
+
68
117
  def test_is_copilot_disabled_via_env_returns_true_when_env_lists_copilot(
69
118
  monkeypatch: pytest.MonkeyPatch,
70
119
  ) -> None:
@@ -101,17 +150,19 @@ def test_cli_main_returns_zero_when_named_reviewer_disabled(
101
150
  assert reviews_disabled.main(["--reviewer", "bugbot"]) == 0
102
151
 
103
152
 
104
- def test_cli_main_returns_one_when_named_reviewer_not_disabled(
153
+ def test_cli_main_returns_one_when_bugbot_enabled_via_env(
105
154
  monkeypatch: pytest.MonkeyPatch,
106
155
  ) -> None:
107
156
  monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
157
+ monkeypatch.setenv("CLAUDE_REVIEWS_ENABLED", "bugbot")
108
158
  assert reviews_disabled.main(["--reviewer", "bugbot"]) == 1
109
159
 
110
160
 
111
- def test_cli_main_returns_one_when_other_reviewer_disabled(
161
+ def test_cli_main_returns_one_for_bugbot_when_enabled_despite_bugteam_optout(
112
162
  monkeypatch: pytest.MonkeyPatch,
113
163
  ) -> None:
114
164
  monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
165
+ monkeypatch.setenv("CLAUDE_REVIEWS_ENABLED", "bugbot")
115
166
  assert reviews_disabled.main(["--reviewer", "bugbot"]) == 1
116
167
 
117
168