claude-dev-env 2.9.0 → 2.10.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 (289) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/pr-loop/CLAUDE.md +1 -1
  3. package/_shared/pr-loop/audit-contract.md +17 -6
  4. package/_shared/pr-loop/audit-reply-template.md +4 -4
  5. package/_shared/pr-loop/code-rules-gate.md +3 -5
  6. package/_shared/pr-loop/fix-protocol.md +2 -3
  7. package/_shared/pr-loop/gh-payloads.md +1 -1
  8. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  9. package/_shared/pr-loop/scripts/README.md +1 -1
  10. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  13. package/_shared/pr-loop/scripts/preflight.py +9 -4
  14. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  15. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  17. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  18. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  19. package/_shared/pr-loop/state-schema.md +5 -14
  20. package/agents/CLAUDE.md +2 -2
  21. package/agents/clean-coder.md +58 -548
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +32 -40
  25. package/audit-rubrics/CLAUDE.md +2 -1
  26. package/audit-rubrics/audit-categories.json +704 -0
  27. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  28. package/bin/CLAUDE.md +16 -5
  29. package/bin/ever-shipped-skills.mjs +2 -0
  30. package/bin/install-plan.mjs +402 -0
  31. package/bin/install-transaction.mjs +455 -0
  32. package/bin/install.mjs +593 -147
  33. package/bin/install.plan.test.mjs +194 -0
  34. package/bin/install.profile-root.test.mjs +154 -0
  35. package/bin/install.profiles.test.mjs +253 -0
  36. package/bin/install.settings-defaults.test.mjs +200 -0
  37. package/bin/install.transaction.test.mjs +400 -0
  38. package/bin/install.uninstall-transaction.test.mjs +418 -0
  39. package/bin/merge_managed_permissions.mjs +130 -0
  40. package/bin/resolve-install-root.mjs +181 -0
  41. package/bin/select-install-targets.mjs +401 -0
  42. package/commands/CLAUDE.md +0 -2
  43. package/docs/references/CLAUDE.md +1 -0
  44. package/docs/references/advisor-tool.md +1 -1
  45. package/docs/references/prose-style-enforcement.md +25 -0
  46. package/hooks/blocking/CLAUDE.md +6 -6
  47. package/hooks/blocking/_path_setup.py +9 -5
  48. package/hooks/blocking/code_rules_docstrings.py +124 -30
  49. package/hooks/blocking/code_rules_enforcer.py +161 -16
  50. package/hooks/blocking/code_rules_shared.py +40 -23
  51. package/hooks/blocking/config/CLAUDE.md +3 -5
  52. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  53. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  54. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  55. package/hooks/blocking/hedging_language_blocker.py +103 -20
  56. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  57. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  58. package/hooks/blocking/plain_language_blocker.py +139 -20
  59. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  60. package/hooks/blocking/state_description_blocker.py +7 -1
  61. package/hooks/blocking/tdd_enforcer.py +8 -0
  62. package/hooks/blocking/test__path_setup.py +28 -0
  63. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  64. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  65. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  66. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  67. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  68. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  69. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  70. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  71. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  72. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  73. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  74. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  75. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  76. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  77. package/hooks/blocking/test_state_description_blocker.py +45 -2
  78. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  79. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  80. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  81. package/hooks/hooks.json +15 -0
  82. package/hooks/hooks_constants/CLAUDE.md +14 -3
  83. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  84. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  85. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  86. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  87. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  88. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  89. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  90. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  91. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  92. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  93. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  94. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  95. package/hooks/hooks_constants/session_start_injector.py +163 -0
  96. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  97. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  98. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  99. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  100. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  101. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  102. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  103. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  104. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  105. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  106. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  107. package/hooks/observability/CLAUDE.md +2 -0
  108. package/hooks/observability/prose_matcher_advisory.py +237 -0
  109. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  110. package/hooks/session/CLAUDE.md +9 -1
  111. package/hooks/session/_path_setup.py +13 -0
  112. package/hooks/session/issue_tracker_session_starter.py +135 -0
  113. package/hooks/session/orchestrator_auto_starter.py +100 -0
  114. package/hooks/session/test__path_setup.py +28 -0
  115. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  116. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  117. package/hooks/session/test_working_style_prompt.py +47 -0
  118. package/hooks/session/untracked_repo_detector.py +1 -24
  119. package/hooks/session/working_style_prompt.py +36 -0
  120. package/hooks/validators/_path_setup.py +19 -0
  121. package/hooks/validators/run_all_validators.py +8 -13
  122. package/installable-surfaces.manifest.json +21 -0
  123. package/output-styles/CLAUDE.md +1 -3
  124. package/package.json +4 -2
  125. package/rules/CLAUDE.md +1 -0
  126. package/rules/durable-post-artifacts.md +2 -2
  127. package/rules/eli11-replies.md +6 -1
  128. package/rules/hedging-claims.md +4 -2
  129. package/rules/long-horizon-autonomy.md +3 -1
  130. package/rules/opus5-communication-contract.md +45 -0
  131. package/rules/plain-language.md +2 -2
  132. package/rules/research-mode.md +1 -1
  133. package/scripts/CLAUDE.md +11 -0
  134. package/scripts/Sync-RepoMain.ps1 +215 -0
  135. package/scripts/active_capability_references.py +218 -0
  136. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  137. package/scripts/claude_chain_runner.py +394 -6
  138. package/scripts/claude_chain_usage.py +1 -1
  139. package/scripts/codex_compat_materializer.py +105 -85
  140. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  141. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  142. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  143. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  144. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  145. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  146. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  147. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  148. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  149. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  150. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  151. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  152. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  153. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  154. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  155. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  156. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  157. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  158. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  159. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  160. package/scripts/spawn_grok_batch.py +3 -0
  161. package/scripts/test_active_capability_references.py +108 -0
  162. package/scripts/test_claude_chain_runner.py +414 -82
  163. package/scripts/test_claude_chain_usage.py +12 -12
  164. package/scripts/test_resolve_worker_spawn.py +2 -2
  165. package/scripts/test_verify_installable_package.py +208 -0
  166. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  167. package/scripts/verify_installable_package.py +612 -0
  168. package/settings.json +10 -0
  169. package/skills/CLAUDE.md +2 -0
  170. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  171. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  172. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  173. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  174. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  175. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  176. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  177. package/skills/autoconverge/reference/convergence.md +2 -1
  178. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  179. package/skills/beat-sheet/SKILL.md +54 -0
  180. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  181. package/skills/bugteam/CONSTRAINTS.md +4 -4
  182. package/skills/bugteam/EXAMPLES.md +1 -1
  183. package/skills/bugteam/reference/README.md +1 -1
  184. package/skills/e-code-review/SKILL.md +26 -5
  185. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  186. package/skills/e-code-review/reference/medium.md +15 -4
  187. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  188. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  189. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  190. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  191. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  192. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  193. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  194. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  195. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  196. package/skills/e-code-review/scripts/grok_code_review.py +16 -7
  197. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  198. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  199. package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
  200. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  201. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  202. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  203. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  204. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  205. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  206. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  207. package/skills/pr-converge/scripts/conftest.py +35 -0
  208. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  209. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  210. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  211. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  212. package/skills/rebase/SKILL.md +15 -3
  213. package/skills/reviewer-gates/SKILL.md +2 -2
  214. package/skills/show/SKILL.md +51 -0
  215. package/skills/show/references/accessibility.md +7 -0
  216. package/skills/show/references/art.md +3 -0
  217. package/skills/show/references/charts.md +3 -0
  218. package/skills/show/references/core-design.md +14 -0
  219. package/skills/show/references/erds.md +3 -0
  220. package/skills/show/references/flowcharts.md +3 -0
  221. package/skills/show/references/host-and-html.md +3 -0
  222. package/skills/show/references/illustrative-diagrams.md +10 -0
  223. package/skills/show/references/interaction.md +3 -0
  224. package/skills/show/references/mockups.md +3 -0
  225. package/skills/show/references/quality-gates.md +7 -0
  226. package/skills/show/references/structural-diagrams.md +3 -0
  227. package/skills/show/references/subject-inventory.md +21 -0
  228. package/skills/show/references/svg-contract.md +22 -0
  229. package/skills/show/routing.yaml +30 -0
  230. package/skills/show/samples/pr1262-v2.svg +222 -0
  231. package/skills/show/scripts/README.md +6 -0
  232. package/skills/show/scripts/validate-artifact.py +91 -0
  233. package/skills/show/scripts/validate-package.py +18 -0
  234. package/skills/show/templates/html-widget.html +4 -0
  235. package/skills/show/templates/svg-base.svg +19 -0
  236. package/skills/show/tests/fixtures/css-var.svg +6 -0
  237. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  238. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  239. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  240. package/skills/show/tests/fixtures/invalid.svg +1 -0
  241. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  242. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  243. package/skills/show/tests/fixtures/valid.html +1 -0
  244. package/skills/show/tests/test_validate-artifact.py +74 -0
  245. package/skills/show/tests/test_validators.py +59 -0
  246. package/skills/show/workflows/create-visual.md +13 -0
  247. package/skills/show/workflows/review-visual.md +20 -0
  248. package/skills/split-pr/SKILL.md +85 -0
  249. package/skills/split-pr/reference/path-layers.md +16 -0
  250. package/skills/split-pr/reference/proposal-format.md +15 -0
  251. package/skills/split-pr/reference/split-further-loop.md +10 -0
  252. package/skills/split-pr/reference/splitting-principles.md +26 -0
  253. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  254. package/skills/split-pr/scripts/categorize_files.py +106 -0
  255. package/skills/split-pr/scripts/config/__init__.py +1 -0
  256. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  257. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  258. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  259. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  260. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  261. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  262. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  263. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  264. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  265. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  266. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  267. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  268. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  269. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  270. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  271. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  272. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  273. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  274. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  275. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  276. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  277. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  278. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  279. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  280. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  281. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  282. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  283. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  284. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  285. package/skills/split-pr/scripts/verify_plan.py +139 -0
  286. package/system-prompts/software-engineer.xml +11 -2
  287. package/commands/initialize.md +0 -90
  288. package/commands/stubcheck.md +0 -88
  289. package/output-styles/caveman-agent.md +0 -37
@@ -0,0 +1,180 @@
1
+ """Behavior tests for the effort evaluation harness."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ _SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
9
+ _CONSTANTS_PARENT = _SCRIPTS_DIRECTORY / "config"
10
+ for each_path in (_SCRIPTS_DIRECTORY, _CONSTANTS_PARENT):
11
+ path_text = str(each_path)
12
+ if path_text not in sys.path:
13
+ sys.path.insert(0, path_text)
14
+
15
+ from e_code_review_effort_constants import ( # noqa: E402
16
+ ALL_EFFORT_LEVELS,
17
+ ALL_FIXTURE_BANDS,
18
+ ALL_SKILL_EFFORT_LEVELS,
19
+ MINIMUM_QUALITY_HOLD_SCORE,
20
+ THINKING_ENABLED_DEFAULT,
21
+ WORKFLOW_FAMILY_E_CODE_REVIEW,
22
+ )
23
+ from effort_evaluation import ( # noqa: E402
24
+ build_synthetic_row,
25
+ evaluation_evidence_path,
26
+ fixtures_directory,
27
+ load_evaluation_evidence,
28
+ load_fixtures,
29
+ map_evaluation_effort_to_skill_level,
30
+ quality_holds,
31
+ recommend_effort_by_band,
32
+ resolve_skill_effort_for_band,
33
+ skill_defaults_from_recommendation,
34
+ validate_evaluation_row,
35
+ )
36
+
37
+
38
+ def test_fixtures_directory_points_at_frozen_json() -> None:
39
+ directory = fixtures_directory()
40
+ assert directory.is_dir()
41
+ assert directory.name == "fixtures"
42
+ assert (directory / "easy.json").is_file()
43
+
44
+
45
+ def test_loads_three_frozen_fixture_bands() -> None:
46
+ all_fixtures = load_fixtures()
47
+ assert len(all_fixtures) == len(ALL_FIXTURE_BANDS)
48
+ bands = {str(each_fixture["band"]) for each_fixture in all_fixtures}
49
+ assert bands == set(ALL_FIXTURE_BANDS)
50
+ for each_fixture in all_fixtures:
51
+ assert each_fixture["fixture_id"]
52
+ assert isinstance(each_fixture["seeded_findings"], list)
53
+ assert len(each_fixture["seeded_findings"]) >= 1
54
+
55
+
56
+ def test_validate_row_accepts_complete_row_and_rejects_thinking_off() -> None:
57
+ good_row = build_synthetic_row(
58
+ fixture_id="easy-comment-preservation",
59
+ fixture_band="easy",
60
+ effort="low",
61
+ quality_score=0.9,
62
+ finding_recall=1.0,
63
+ finding_precision=1.0,
64
+ visible_tokens=1200,
65
+ latency_ms=4000,
66
+ )
67
+ assert validate_evaluation_row(good_row) == []
68
+ assert good_row["thinking_enabled"] is THINKING_ENABLED_DEFAULT
69
+ bad_row = dict(good_row)
70
+ bad_row["thinking_enabled"] = False
71
+ problems = validate_evaluation_row(bad_row)
72
+ assert any("thinking_enabled" in each_problem for each_problem in problems)
73
+ bool_score_row = dict(good_row)
74
+ bool_score_row["quality_score"] = True
75
+ bool_problems = validate_evaluation_row(bool_score_row)
76
+ assert any("quality_score" in each_problem for each_problem in bool_problems)
77
+
78
+
79
+ def test_recommend_picks_lowest_effort_that_holds_quality() -> None:
80
+ all_rows = [
81
+ build_synthetic_row("easy-a", "easy", "low", 0.5, 0.5, 0.5, 100, 1000),
82
+ build_synthetic_row("easy-a", "easy", "medium", 0.95, 0.95, 0.95, 200, 2000),
83
+ build_synthetic_row("easy-a", "easy", "xhigh", 0.99, 0.99, 0.99, 400, 5000),
84
+ build_synthetic_row("medium-a", "medium", "high", 0.9, 0.9, 0.9, 300, 3000),
85
+ build_synthetic_row(
86
+ "demanding-a", "demanding", "max", 0.85, 0.85, 0.85, 800, 9000
87
+ ),
88
+ ]
89
+ for each_row in all_rows:
90
+ assert validate_evaluation_row(each_row) == []
91
+ recommendation = recommend_effort_by_band(all_rows)
92
+ by_band = recommendation["recommendation_by_band"]
93
+ assert isinstance(by_band, dict)
94
+ assert by_band["easy"]["recommended_effort"] == "medium"
95
+ assert by_band["easy"]["cited_row"]["effort"] == "medium"
96
+ assert by_band["medium"]["recommended_effort"] == "high"
97
+ assert by_band["demanding"]["recommended_effort"] == "max"
98
+ assert recommendation["thinking_enabled"] is True
99
+ assert recommendation["cost_latency_lever"] == "effort"
100
+ assert recommendation["defaults_unchanged"] is True
101
+
102
+
103
+ def test_recommend_blocks_band_without_holding_row() -> None:
104
+ all_rows = [
105
+ build_synthetic_row("easy-a", "easy", "low", 0.1, 0.1, 0.1, 50, 500),
106
+ ]
107
+ recommendation = recommend_effort_by_band(all_rows)
108
+ easy = recommendation["recommendation_by_band"]["easy"]
109
+ assert easy["recommended_effort"] is None
110
+ assert easy["cited_row"] is None
111
+ assert "quality floor" in str(easy["blocker"])
112
+
113
+
114
+ def test_quality_holds_uses_named_floor() -> None:
115
+ holding = build_synthetic_row(
116
+ "x", "easy", "low", MINIMUM_QUALITY_HOLD_SCORE, 1.0, 1.0, 1, 1
117
+ )
118
+ failing = build_synthetic_row(
119
+ "x", "easy", "low", MINIMUM_QUALITY_HOLD_SCORE - 0.01, 1.0, 1.0, 1, 1
120
+ )
121
+ assert quality_holds(holding) is True
122
+ assert quality_holds(failing) is False
123
+
124
+
125
+ def test_effort_levels_cover_cli_set() -> None:
126
+ assert "low" in ALL_EFFORT_LEVELS
127
+ assert "max" in ALL_EFFORT_LEVELS
128
+ assert len(ALL_EFFORT_LEVELS) == 5
129
+
130
+
131
+ def test_evidence_rows_validate_and_skill_defaults_cite_rows() -> None:
132
+ evidence_path = evaluation_evidence_path()
133
+ assert evidence_path.is_file()
134
+ assert evidence_path.name == "effort_defaults_evidence.json"
135
+ evidence = load_evaluation_evidence()
136
+ assert evidence["workflow_family"] == WORKFLOW_FAMILY_E_CODE_REVIEW
137
+ assert evidence["thinking_enabled"] is True
138
+ all_rows = evidence["all_rows"]
139
+ assert isinstance(all_rows, list)
140
+ for each_row in all_rows:
141
+ assert isinstance(each_row, dict)
142
+ assert validate_evaluation_row(each_row) == []
143
+ recommendation = recommend_effort_by_band(all_rows)
144
+ skill_defaults = skill_defaults_from_recommendation(recommendation)
145
+ assert skill_defaults["workflow_family"] == WORKFLOW_FAMILY_E_CODE_REVIEW
146
+ default_by_band = skill_defaults["default_by_band"]
147
+ assert isinstance(default_by_band, dict)
148
+ for each_band in ALL_FIXTURE_BANDS:
149
+ band_default = default_by_band[each_band]
150
+ assert band_default["skill_effort"] in ALL_SKILL_EFFORT_LEVELS
151
+ assert band_default["cited_row"] is not None
152
+ assert band_default["cited_row"]["thinking_enabled"] is True
153
+
154
+
155
+ def test_committed_skill_defaults_match_resolver() -> None:
156
+ assert resolve_skill_effort_for_band("easy") == "medium"
157
+ assert resolve_skill_effort_for_band("medium") == "xhigh"
158
+ assert resolve_skill_effort_for_band("demanding") == "xhigh"
159
+ evidence = load_evaluation_evidence()
160
+ committed = evidence["skill_defaults"]["default_by_band"]
161
+ for each_band in ALL_FIXTURE_BANDS:
162
+ assert committed[each_band]["skill_effort"] == resolve_skill_effort_for_band(
163
+ each_band
164
+ )
165
+ assert committed[each_band]["cited_row"]["effort"] is not None
166
+
167
+
168
+ def test_map_high_and_max_to_skill_xhigh() -> None:
169
+ assert map_evaluation_effort_to_skill_level("high") == "xhigh"
170
+ assert map_evaluation_effort_to_skill_level("max") == "xhigh"
171
+ assert map_evaluation_effort_to_skill_level("medium") == "medium"
172
+
173
+
174
+ def test_resolve_rejects_unknown_band() -> None:
175
+ try:
176
+ resolve_skill_effort_for_band("not-a-band")
177
+ except ValueError as error:
178
+ assert "unknown fixture band" in str(error)
179
+ else:
180
+ raise AssertionError("expected ValueError for unknown band")
@@ -0,0 +1,197 @@
1
+ """Red fixtures: low-severity findings survive collection; filter is separate."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
11
+ if str(_SCRIPTS_DIR) not in sys.path:
12
+ sys.path.insert(0, str(_SCRIPTS_DIR))
13
+
14
+ from e_code_review_scripts_constants.finding_pipeline_constants import (
15
+ ALL_COLLECTION_SEVERITIES,
16
+ COLLECTION_STAGE_NAME,
17
+ FILTER_STAGE_NAME,
18
+ FINDING_FIELD_CATEGORY,
19
+ FINDING_FIELD_EVIDENCE,
20
+ FINDING_FIELD_FILE,
21
+ FINDING_FIELD_LINE,
22
+ FINDING_FIELD_SEVERITY,
23
+ REPORT_EVERY_FINDING_INSTRUCTION,
24
+ SEVERITY_BLOCKER,
25
+ SEVERITY_HIGH,
26
+ SEVERITY_LOW,
27
+ SEVERITY_MEDIUM,
28
+ SEVERITY_NIT,
29
+ )
30
+ from finding_pipeline import (
31
+ CollectedFinding,
32
+ FindingCollection,
33
+ collect_findings,
34
+ filter_findings_by_severity,
35
+ )
36
+
37
+
38
+ def _seeded_findings() -> list[CollectedFinding]:
39
+ return [
40
+ CollectedFinding(
41
+ file_path="pkg/a.py",
42
+ line_number=10,
43
+ severity=SEVERITY_BLOCKER,
44
+ category="correctness",
45
+ evidence="raises on empty input",
46
+ ),
47
+ CollectedFinding(
48
+ file_path="pkg/b.py",
49
+ line_number=20,
50
+ severity=SEVERITY_HIGH,
51
+ category="security",
52
+ evidence="shell=True with user text",
53
+ ),
54
+ CollectedFinding(
55
+ file_path="pkg/c.py",
56
+ line_number=30,
57
+ severity=SEVERITY_MEDIUM,
58
+ category="api-contracts",
59
+ evidence="return type narrowed",
60
+ ),
61
+ CollectedFinding(
62
+ file_path="pkg/d.py",
63
+ line_number=40,
64
+ severity=SEVERITY_LOW,
65
+ category="simplification",
66
+ evidence="duplicated helper body",
67
+ ),
68
+ CollectedFinding(
69
+ file_path="pkg/e.py",
70
+ line_number=50,
71
+ severity=SEVERITY_NIT,
72
+ category="conventions",
73
+ evidence="typo in comment",
74
+ ),
75
+ ]
76
+
77
+
78
+ def test_collection_retains_every_seeded_severity_including_low_and_nit() -> None:
79
+ all_seeded = _seeded_findings()
80
+ collection = collect_findings(all_seeded)
81
+
82
+ assert collection.stage_name == COLLECTION_STAGE_NAME
83
+ all_severities = {
84
+ each.severity for each in collection.all_findings
85
+ }
86
+ assert all_severities == set(ALL_COLLECTION_SEVERITIES)
87
+ assert len(collection.all_findings) == len(all_seeded)
88
+ low_finding = next(
89
+ each for each in collection.all_findings if each.severity == SEVERITY_LOW
90
+ )
91
+ assert low_finding.file_path == "pkg/d.py"
92
+ assert low_finding.line_number == 40
93
+ assert low_finding.evidence == "duplicated helper body"
94
+ assert low_finding.category == "simplification"
95
+
96
+
97
+ def test_each_collected_finding_keeps_file_line_evidence_and_category() -> None:
98
+ collection = collect_findings(_seeded_findings())
99
+ for each_finding in collection.all_findings:
100
+ as_mapping = each_finding.as_mapping()
101
+ assert as_mapping[FINDING_FIELD_FILE]
102
+ assert as_mapping[FINDING_FIELD_LINE] > 0
103
+ assert as_mapping[FINDING_FIELD_EVIDENCE]
104
+ assert as_mapping[FINDING_FIELD_CATEGORY]
105
+ assert as_mapping[FINDING_FIELD_SEVERITY] in ALL_COLLECTION_SEVERITIES
106
+
107
+
108
+ def test_filter_is_explicit_later_stage_and_does_not_mutate_collection() -> None:
109
+ collection = collect_findings(_seeded_findings())
110
+ collected_snapshot = [
111
+ (
112
+ each.file_path,
113
+ each.line_number,
114
+ each.severity,
115
+ each.category,
116
+ each.evidence,
117
+ )
118
+ for each in collection.all_findings
119
+ ]
120
+
121
+ filtered = filter_findings_by_severity(
122
+ collection,
123
+ minimum_severity=SEVERITY_MEDIUM,
124
+ )
125
+
126
+ assert filtered.stage_name == FILTER_STAGE_NAME
127
+ all_filtered_severities = {each.severity for each in filtered.all_findings}
128
+ assert SEVERITY_LOW not in all_filtered_severities
129
+ assert SEVERITY_NIT not in all_filtered_severities
130
+ assert SEVERITY_MEDIUM in all_filtered_severities
131
+ assert SEVERITY_BLOCKER in all_filtered_severities
132
+
133
+ after_filter_snapshot = [
134
+ (
135
+ each.file_path,
136
+ each.line_number,
137
+ each.severity,
138
+ each.category,
139
+ each.evidence,
140
+ )
141
+ for each in collection.all_findings
142
+ ]
143
+ assert after_filter_snapshot == collected_snapshot
144
+ assert len(collection.all_findings) == 5
145
+
146
+
147
+ def test_filter_rejects_unknown_minimum_severity() -> None:
148
+ collection = collect_findings(_seeded_findings())
149
+ with pytest.raises(ValueError, match="unknown minimum severity"):
150
+ filter_findings_by_severity(collection, minimum_severity="P1")
151
+
152
+
153
+ def test_filter_rejects_unknown_finding_severity_with_value_error() -> None:
154
+ collection = FindingCollection(
155
+ stage_name=COLLECTION_STAGE_NAME,
156
+ all_findings=(
157
+ CollectedFinding(
158
+ file_path="pkg/x.py",
159
+ line_number=1,
160
+ severity="P0",
161
+ category="correctness",
162
+ evidence="bypass collect",
163
+ ),
164
+ ),
165
+ )
166
+ with pytest.raises(ValueError, match="unknown collection severity"):
167
+ filter_findings_by_severity(collection, minimum_severity=SEVERITY_LOW)
168
+
169
+
170
+ def test_collect_rejects_finding_missing_required_fields() -> None:
171
+ with pytest.raises(ValueError, match="file"):
172
+ collect_findings(
173
+ [
174
+ CollectedFinding(
175
+ file_path="",
176
+ line_number=1,
177
+ severity=SEVERITY_LOW,
178
+ category="correctness",
179
+ evidence="x",
180
+ )
181
+ ]
182
+ )
183
+
184
+
185
+ def test_report_every_finding_instruction_names_all_severities() -> None:
186
+ for each_severity in ALL_COLLECTION_SEVERITIES:
187
+ assert each_severity in REPORT_EVERY_FINDING_INSTRUCTION
188
+ assert "consumer stage" in REPORT_EVERY_FINDING_INSTRUCTION
189
+ assert "Do not drop findings by severity during collection" in (
190
+ REPORT_EVERY_FINDING_INSTRUCTION
191
+ )
192
+
193
+
194
+ def test_collection_record_type_is_immutable() -> None:
195
+ collection = collect_findings(_seeded_findings())
196
+ assert isinstance(collection, FindingCollection)
197
+ assert collection.stage_name == COLLECTION_STAGE_NAME
@@ -134,6 +134,83 @@ def test_run_medium_review_happy_path() -> None:
134
134
  assert len(batch.all_retained_findings) == MEDIUM_REVIEW_FINDER_COUNT - 1
135
135
 
136
136
 
137
+ def _one_candidate_per_angle(head: str) -> list[FinderCandidate]:
138
+ return [
139
+ FinderCandidate(
140
+ angle=each_angle,
141
+ file_path=f"{each_angle}.py",
142
+ line_number=1,
143
+ mechanism="m",
144
+ scenario=f"scenario {each_angle}",
145
+ worktree_path=f"/wt/{each_angle}",
146
+ leader_socket=f"sock-{each_angle}",
147
+ advisor_session_id=f"adv-{each_angle}",
148
+ reviewed_head=head,
149
+ )
150
+ for each_angle in ALL_MEDIUM_FINDER_ANGLES
151
+ ]
152
+
153
+
154
+ def test_a_finder_reporting_several_candidates_is_not_a_identity_clash() -> None:
155
+ head = "abc123"
156
+ all_finders = _one_candidate_per_angle(head)
157
+ first = all_finders[0]
158
+ second_from_first_finder = FinderCandidate(
159
+ angle=first.angle,
160
+ file_path="second.py",
161
+ line_number=9,
162
+ mechanism="m",
163
+ scenario="a second finding from the same finder",
164
+ worktree_path=first.worktree_path,
165
+ leader_socket=first.leader_socket,
166
+ advisor_session_id=first.advisor_session_id,
167
+ reviewed_head=head,
168
+ )
169
+ all_finders.append(second_from_first_finder)
170
+ all_keys = [
171
+ (each.file_path, each.line_number, each.mechanism) for each in all_finders
172
+ ]
173
+ batch = run_medium_review(
174
+ target_head=head,
175
+ diff_base="base",
176
+ all_finder_candidates=all_finders,
177
+ verdict_by_key={each_key: VERDICT_CONFIRMED for each_key in all_keys},
178
+ severity_by_key={each_key: "medium" for each_key in all_keys},
179
+ live_head=head,
180
+ )
181
+ assert batch.is_rejected is False
182
+ assert batch.rejection_reason is None
183
+ assert len(batch.all_retained_findings) == len(all_finders)
184
+
185
+
186
+ def test_two_finders_sharing_one_socket_reject_the_batch() -> None:
187
+ head = "abc123"
188
+ all_finders = _one_candidate_per_angle(head)
189
+ first = all_finders[0]
190
+ second = all_finders[1]
191
+ all_finders[1] = FinderCandidate(
192
+ angle=second.angle,
193
+ file_path=second.file_path,
194
+ line_number=second.line_number,
195
+ mechanism=second.mechanism,
196
+ scenario=second.scenario,
197
+ worktree_path=second.worktree_path,
198
+ leader_socket=first.leader_socket,
199
+ advisor_session_id=second.advisor_session_id,
200
+ reviewed_head=head,
201
+ )
202
+ batch = run_medium_review(
203
+ target_head=head,
204
+ diff_base="base",
205
+ all_finder_candidates=all_finders,
206
+ verdict_by_key={},
207
+ severity_by_key={},
208
+ live_head=head,
209
+ )
210
+ assert batch.is_rejected is True
211
+ assert batch.rejection_reason == "non_unique_finder_identity"
212
+
213
+
137
214
  def test_head_drift_rejects_batch() -> None:
138
215
  head = "h1"
139
216
  all_finders = [
@@ -8,10 +8,10 @@ import subprocess
8
8
  import sys
9
9
  from pathlib import Path
10
10
 
11
- if str(Path(__file__).parent) not in sys.path:
12
- sys.path.insert(0, str(Path(__file__).parent))
13
-
14
- from config.constants import (
11
+ _scripts_directory = str(Path(__file__).resolve().parent)
12
+ if _scripts_directory not in sys.path:
13
+ sys.path.insert(0, _scripts_directory)
14
+ from load_skill_constants import (
15
15
  PACKET_CREATION_ERROR_EXIT_CODE,
16
16
  PACKET_JSON_INDENT_LEVEL,
17
17
  SLUG_PATTERN,
@@ -0,0 +1,41 @@
1
+ """Load plan-to-pr skill constants without colliding with repo-root config.
2
+
3
+ Repo pytest sets ``pythonpath = .``, so a bare ``from config.constants import …``
4
+ can bind the monorepo root ``config`` package instead of this skill's
5
+ ``scripts/config/constants.py``. Load the skill file under a unique module name
6
+ and re-export its public names for script imports.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.util
12
+ import sys
13
+ from pathlib import Path
14
+ from types import ModuleType
15
+
16
+
17
+ def _load_skill_constants_module() -> ModuleType:
18
+ """Return the skill-local constants module, loading it once per process."""
19
+ skill_constants_module_name = "plan_to_pr_scripts_config_constants"
20
+ skill_constants_path = Path(__file__).resolve().parent / "config" / "constants.py"
21
+ existing_module = sys.modules.get(skill_constants_module_name)
22
+ if existing_module is not None:
23
+ return existing_module
24
+ module_spec = importlib.util.spec_from_file_location(
25
+ skill_constants_module_name, skill_constants_path
26
+ )
27
+ if module_spec is None or module_spec.loader is None:
28
+ raise ImportError(
29
+ f"unable to load plan-to-pr skill constants from {skill_constants_path}"
30
+ )
31
+ loaded_module = importlib.util.module_from_spec(module_spec)
32
+ sys.modules[skill_constants_module_name] = loaded_module
33
+ module_spec.loader.exec_module(loaded_module)
34
+ return loaded_module
35
+
36
+
37
+ _skill_constants = _load_skill_constants_module()
38
+ for each_constant_name, each_constant_binding in _skill_constants.__dict__.items():
39
+ if each_constant_name.startswith("_"):
40
+ continue
41
+ globals()[each_constant_name] = each_constant_binding
@@ -9,10 +9,10 @@ import shlex
9
9
  import sys
10
10
  from pathlib import Path
11
11
 
12
- if str(Path(__file__).parent) not in sys.path:
13
- sys.path.insert(0, str(Path(__file__).parent))
14
-
15
- from config.constants import (
12
+ _scripts_directory = str(Path(__file__).resolve().parent)
13
+ if _scripts_directory not in sys.path:
14
+ sys.path.insert(0, _scripts_directory)
15
+ from load_skill_constants import (
16
16
  COMMAND_SEPARATOR,
17
17
  HANDOFF_ACCEPTANCE_PREFIX,
18
18
  HANDOFF_ALLOWED_FILES_PREFIX,
@@ -10,7 +10,10 @@ import sys
10
10
  from collections.abc import Mapping, Sequence
11
11
  from pathlib import Path
12
12
 
13
- from config.constants import (
13
+ _scripts_directory = str(Path(__file__).resolve().parent)
14
+ if _scripts_directory not in sys.path:
15
+ sys.path.insert(0, _scripts_directory)
16
+ from load_skill_constants import (
14
17
  ALL_ALLOWED_RECORD_FIELDS,
15
18
  ALL_RECORD_FIELDS,
16
19
  ALL_REPAIR_FIELDS,
@@ -8,7 +8,10 @@ import sys
8
8
  from collections.abc import Mapping, Sequence
9
9
  from pathlib import Path
10
10
 
11
- from config.constants import (
11
+ _scripts_directory = str(Path(__file__).resolve().parent)
12
+ if _scripts_directory not in sys.path:
13
+ sys.path.insert(0, _scripts_directory)
14
+ from load_skill_constants import (
12
15
  COMMIT_RANGE_ARGUMENT_INDEX,
13
16
  COMMIT_RANGE_SEPARATOR,
14
17
  EXIT_CODE_INVALID_SET,
@@ -16,17 +16,16 @@ Each bypass flag closes one reviewer gate when that reviewer is unavailable.
16
16
  the Copilot review and pending-review gates. ``--bugteam-post-blocked`` skips
17
17
  the bugteam CLEAN-review gate. ``--codex-down`` skips the Codex review gate.
18
18
 
19
- Each flag also honors its own reviews-disabled token: "bugbot", "copilot",
20
- "bugteam", or "codex". A caller that cannot pass the flag reaches the same
21
- bypass by exporting the token. The mark-ready blocker hook re-runs this script
22
- with no flags, so its convergence re-check reads the bypass from the exported
23
- token instead. For Codex, sticky job-dir state ``codex_down: true`` is a third
24
- bypass source when the original run could not re-pass ``--codex-down``.
25
-
26
- Copilot and Bugbot waivers are disk-authoritative.
19
+ Bugbot, bugteam, and codex are off by default. Each gate runs only when
20
+ ``CLAUDE_REVIEWS_ENABLED`` lists its token, and a token in
21
+ ``CLAUDE_REVIEWS_DISABLED`` forces one off even when the opt-in lists it.
22
+ Copilot runs by default and stops only when the disabled list names it. For
23
+ Codex, sticky job-dir state ``codex_down: true`` is a further bypass source
24
+ when the original run could not re-pass ``--codex-down``.
25
+
26
+ Every reviewer waiver is disk-authoritative.
27
27
  When ``~/.claude/settings.json`` is readable, its env block is the single source.
28
28
  The frozen process env is only a fallback when that disk read fails (logged once).
29
- Bugteam's opt-out is env-only via ``_resolve_bugteam_post_blocked``.
30
29
  A probe error does not waive the gate; the live GitHub checks still run.
31
30
 
32
31
  The Codex gate is conditional-required: it demands
@@ -105,11 +104,11 @@ from pr_converge_skill_constants.constants import (
105
104
  )
106
105
  from reviews_disabled import (
107
106
  is_bugbot_disabled_via_env,
108
- is_bugteam_disabled_via_env,
109
- is_codex_disabled_via_env,
110
107
  is_copilot_disabled_via_env,
111
108
  )
112
109
  from check_convergence_availability import (
110
+ _is_bugteam_disabled_via_resolved_settings,
111
+ _is_codex_disabled_via_resolved_settings,
113
112
  _resolve_bugbot_waiver,
114
113
  _resolve_copilot_waiver,
115
114
  _waiver_from_cli_flag,
@@ -815,18 +814,21 @@ def _resolve_copilot_down(is_copilot_down_flag: bool) -> bool:
815
814
 
816
815
 
817
816
  def _resolve_bugteam_post_blocked(is_bugteam_post_blocked_flag: bool) -> bool:
818
- """Combine the --bugteam-post-blocked flag with the bugteam env opt-out.
817
+ """Combine the --bugteam-post-blocked flag with the resolved bugteam opt-in.
819
818
 
820
819
  ::
821
820
 
822
- flag True, env unset -> True (caller passed the flag)
823
- flag False, reviews-disabled lists bugteam -> True (exported token)
824
- flag False, env unset -> False (gate runs)
821
+ flag True -> True (caller passed the flag)
822
+ flag False, reviews-enabled lists bugteam -> False (gate runs)
823
+ flag False, neither list names bugteam -> True (off by default)
824
+ flag False, reviews-disabled lists bugteam -> True (opt-out wins)
825
825
 
826
- The mark-ready blocker hook re-runs this script with no flags, so the env
827
- fallback lets an exported bugteam token carry the bypass into that re-check.
826
+ Bugteam is off by default and runs only on an explicit opt-in. Settings on
827
+ disk are the source when readable, so a settings.json opt-in reaches the
828
+ mark-ready blocker hook, which re-runs this script with no flags. The
829
+ process env is the fallback when that disk read fails.
828
830
  """
829
- return is_bugteam_post_blocked_flag or is_bugteam_disabled_via_env()
831
+ return is_bugteam_post_blocked_flag or _is_bugteam_disabled_via_resolved_settings()
830
832
 
831
833
 
832
834
  def _resolve_codex_down(
@@ -854,7 +856,7 @@ def _resolve_codex_down(
854
856
  """
855
857
  return (
856
858
  is_codex_down_flag
857
- or is_codex_disabled_via_env()
859
+ or _is_codex_disabled_via_resolved_settings()
858
860
  or _read_codex_down_from_job_state(all_job_state_fields)
859
861
  )
860
862