claude-dev-env 1.94.0 → 2.0.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 (280) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +41 -33
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/CLAUDE.md +10 -1
  42. package/bin/ever-shipped-skills.mjs +73 -0
  43. package/bin/expand_home_directory_tokens.mjs +1 -1
  44. package/bin/install.mjs +140 -7
  45. package/bin/install.prune.test.mjs +457 -0
  46. package/docs/CODE_RULES.md +1 -1
  47. package/hooks/advisory/refactor_guard.py +3 -4
  48. package/hooks/blocking/CLAUDE.md +7 -1
  49. package/hooks/blocking/block_main_commit.py +2 -2
  50. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  61. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  62. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  63. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  64. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  65. package/hooks/blocking/code_rules_enforcer.py +4 -0
  66. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  67. package/hooks/blocking/code_rules_optional_params.py +2 -2
  68. package/hooks/blocking/code_rules_shared.py +83 -1
  69. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  70. package/hooks/blocking/code_rules_test_layout.py +9 -3
  71. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  72. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  73. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  74. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  75. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  76. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  77. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  78. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  79. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  80. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  85. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  86. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  87. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  88. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  89. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  90. package/hooks/blocking/pii_payload_scan.py +138 -42
  91. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  92. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  93. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  94. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  95. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  96. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  97. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  98. package/hooks/blocking/plain_language_blocker.py +138 -4
  99. package/hooks/blocking/pr_description_command_parser.py +8 -4
  100. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  101. package/hooks/blocking/sensitive_file_protector.py +114 -48
  102. package/hooks/blocking/tdd_enforcer.py +97 -601
  103. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  104. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  105. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  106. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  107. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  108. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  109. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  110. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  111. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  112. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  113. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  114. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  115. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  116. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  117. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  118. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  119. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  120. package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
  121. package/hooks/blocking/test_code_rules_shared.py +181 -0
  122. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  123. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  124. package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
  125. package/hooks/blocking/test_sensitive_file_protector.py +185 -0
  126. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  127. package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
  128. package/hooks/blocking/tests/conftest.py +10 -0
  129. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  130. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  131. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  132. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  133. package/hooks/git-hooks/post_commit.py +3 -4
  134. package/hooks/hooks_constants/CLAUDE.md +4 -2
  135. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  136. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  137. package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
  138. package/hooks/hooks_constants/local_identity.py +59 -8
  139. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  140. package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
  141. package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
  142. package/hooks/hooks_constants/test_local_identity.py +105 -3
  143. package/hooks/pyproject.toml +52 -4
  144. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  145. package/hooks/validation/mypy_validator.py +2 -2
  146. package/hooks/validators/CLAUDE.md +1 -1
  147. package/hooks/validators/README.md +2 -0
  148. package/hooks/validators/health_check.py +1 -0
  149. package/hooks/validators/mypy_integration.py +2 -0
  150. package/hooks/validators/python_style_checks.py +114 -136
  151. package/hooks/validators/python_style_helpers.py +95 -0
  152. package/hooks/validators/ruff_integration.py +3 -0
  153. package/hooks/validators/test_python_style_checks.py +0 -164
  154. package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
  155. package/hooks/validators/test_python_style_fixes.py +251 -0
  156. package/hooks/validators/test_python_style_helpers.py +125 -0
  157. package/hooks/workflow/auto_formatter.py +5 -4
  158. package/package.json +1 -1
  159. package/rules/CLAUDE.md +1 -0
  160. package/rules/anti-corollary-tests.md +69 -0
  161. package/rules/bdd.md +1 -3
  162. package/rules/code-reviews.md +1 -1
  163. package/rules/gh-paginate.md +1 -1
  164. package/rules/plain-language.md +2 -0
  165. package/scripts/CLAUDE.md +4 -0
  166. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  167. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  168. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  169. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  170. package/scripts/grok_headless_runner.py +294 -0
  171. package/scripts/grok_worker_preflight.py +410 -0
  172. package/scripts/invoke_code_review.py +463 -0
  173. package/scripts/resolve_worker_spawn.py +619 -0
  174. package/scripts/spawn_grok_batch.py +672 -0
  175. package/scripts/test_grok_headless_runner.py +626 -0
  176. package/scripts/test_grok_worker_preflight.py +1054 -0
  177. package/scripts/test_invoke_code_review.py +672 -0
  178. package/scripts/test_resolve_worker_spawn.py +1014 -0
  179. package/scripts/test_spawn_grok_batch.py +1017 -0
  180. package/skills/CLAUDE.md +6 -3
  181. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  182. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  183. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  184. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  185. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  186. package/skills/autoconverge/CLAUDE.md +3 -3
  187. package/skills/autoconverge/SKILL.md +9 -3
  188. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  189. package/skills/autoconverge/reference/convergence.md +33 -11
  190. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  191. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  192. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  193. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  194. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  195. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  196. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  197. package/skills/autoconverge/workflow/converge.mjs +202 -18
  198. package/skills/bugteam/CLAUDE.md +2 -2
  199. package/skills/bugteam/CONSTRAINTS.md +3 -2
  200. package/skills/bugteam/PROMPTS.md +7 -6
  201. package/skills/bugteam/SKILL.md +18 -13
  202. package/skills/bugteam/reference/README.md +2 -3
  203. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  204. package/skills/bugteam/reference/design-rationale.md +1 -1
  205. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  206. package/skills/bugteam/reference/team-setup.md +8 -2
  207. package/skills/closeout/SKILL.md +153 -0
  208. package/skills/closeout/reference/handoff-prompt-template.md +72 -0
  209. package/skills/closeout/reference/issue-body-templates.md +108 -0
  210. package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
  211. package/skills/codex-review/CLAUDE.md +46 -0
  212. package/skills/codex-review/SKILL.md +181 -0
  213. package/skills/codex-review/reference/CLAUDE.md +15 -0
  214. package/skills/codex-review/reference/cli-contract.md +253 -0
  215. package/skills/codex-review/reference/loop-integration.md +118 -0
  216. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  217. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  218. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  219. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  220. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  221. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  222. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  223. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  224. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  225. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  226. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  227. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  228. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  229. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  230. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  231. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  232. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  233. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  234. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  235. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  236. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  237. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  238. package/skills/codex-review/test_skill_scaffold.py +192 -0
  239. package/skills/grok-spawn/CLAUDE.md +28 -0
  240. package/skills/grok-spawn/SKILL.md +226 -0
  241. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  242. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  243. package/skills/grokify/SKILL.md +9 -1
  244. package/skills/grokify/capability-claims.test.mjs +28 -0
  245. package/skills/grokify/evals/README.md +72 -0
  246. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  247. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  248. package/skills/orchestrator/SKILL.md +32 -22
  249. package/skills/orchestrator-refresh/SKILL.md +12 -8
  250. package/skills/pr-converge/CLAUDE.md +1 -1
  251. package/skills/pr-converge/SKILL.md +34 -13
  252. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  253. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  254. package/skills/pr-converge/reference/ground-rules.md +1 -1
  255. package/skills/pr-converge/reference/per-tick.md +130 -42
  256. package/skills/pr-converge/reference/state-schema.md +10 -0
  257. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  258. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  259. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  260. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  261. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  262. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  263. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  264. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  265. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  266. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  267. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  268. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  269. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  270. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  271. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  272. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  273. package/skills/pr-fix-protocol/SKILL.md +65 -0
  274. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  275. package/skills/reviewer-gates/SKILL.md +7 -5
  276. package/skills/skill-builder/references/skill-modularity.md +1 -1
  277. package/skills/team-advisor/SKILL.md +16 -12
  278. package/system-prompts/software-engineer.xml +7 -6
  279. package/hooks/validators/test_verify_paths.py +0 -32
  280. package/hooks/validators/verify_paths.py +0 -57
@@ -0,0 +1,207 @@
1
+ """Parse Codex reviewer text into stable internal finding records.
2
+
3
+ ::
4
+
5
+ all_findings = parse_codex_findings(reviewer_text)
6
+ all_findings[0].title # structured or freeform title
7
+ all_findings[0].structured # True only for fenced JSON findings
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+ from dataclasses import dataclass
15
+
16
+ from codex_review_scripts_constants.findings_constants import (
17
+ EMPTY_STRING,
18
+ FENCED_JSON_BLOCK_PATTERN,
19
+ FINDING_KEY_BODY,
20
+ FINDING_KEY_FILE,
21
+ FINDING_KEY_LINE_RANGE,
22
+ FINDING_KEY_PRIORITY,
23
+ FINDING_KEY_TITLE,
24
+ FREEFORM_BULLET_PREFIX,
25
+ FREEFORM_FINDING_LINE_PATTERN,
26
+ NEWLINE,
27
+ )
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class CodexFinding:
32
+ """One review finding in the skill's stable internal shape.
33
+
34
+ ::
35
+
36
+ CodexFinding(
37
+ title="Restore empty-input handling",
38
+ priority="P1",
39
+ file="src/stats.py",
40
+ line_range="2-2",
41
+ body="Divides by zero on empty input.",
42
+ structured=True,
43
+ )
44
+
45
+ Attributes:
46
+ title: Short finding title.
47
+ priority: Priority tag such as ``P1``.
48
+ file: Path the finding points at.
49
+ line_range: Inclusive line span as ``start-end``.
50
+ body: Explanation text for the finding.
51
+ structured: True when the finding came from fenced JSON.
52
+ """
53
+
54
+ title: str
55
+ priority: str
56
+ file: str
57
+ line_range: str
58
+ body: str
59
+ structured: bool
60
+
61
+
62
+ def _string_field(all_finding_fields: dict[str, object], field_name: str) -> str:
63
+ field_payload = all_finding_fields.get(field_name, EMPTY_STRING)
64
+ if isinstance(field_payload, str):
65
+ return field_payload
66
+ return EMPTY_STRING
67
+
68
+
69
+ def _finding_from_structured_fields(
70
+ all_finding_fields: dict[str, object],
71
+ ) -> CodexFinding:
72
+ return CodexFinding(
73
+ title=_string_field(all_finding_fields, FINDING_KEY_TITLE),
74
+ priority=_string_field(all_finding_fields, FINDING_KEY_PRIORITY),
75
+ file=_string_field(all_finding_fields, FINDING_KEY_FILE),
76
+ line_range=_string_field(all_finding_fields, FINDING_KEY_LINE_RANGE),
77
+ body=_string_field(all_finding_fields, FINDING_KEY_BODY),
78
+ structured=True,
79
+ )
80
+
81
+
82
+ def _has_usable_structured_fields(finding: CodexFinding) -> bool:
83
+ has_title = bool(finding.title.strip())
84
+ has_body = bool(finding.body.strip())
85
+ return has_title or has_body
86
+
87
+
88
+ def _try_parse_structured_findings(reviewer_text: str) -> list[CodexFinding] | None:
89
+ fenced_json_block = re.compile(
90
+ FENCED_JSON_BLOCK_PATTERN,
91
+ re.DOTALL | re.IGNORECASE,
92
+ )
93
+ for each_match in fenced_json_block.finditer(reviewer_text):
94
+ block_body = each_match.group(1).strip()
95
+ try:
96
+ parsed_payload = json.loads(block_body)
97
+ except json.JSONDecodeError:
98
+ continue
99
+ if not isinstance(parsed_payload, list):
100
+ continue
101
+ if not parsed_payload:
102
+ return []
103
+ all_findings: list[CodexFinding] = []
104
+ for each_entry in parsed_payload:
105
+ if not isinstance(each_entry, dict):
106
+ continue
107
+ candidate_finding = _finding_from_structured_fields(each_entry)
108
+ if not _has_usable_structured_fields(candidate_finding):
109
+ continue
110
+ all_findings.append(candidate_finding)
111
+ if all_findings:
112
+ return all_findings
113
+ return None
114
+
115
+
116
+ def _collect_freeform_body(
117
+ all_lines: list[str],
118
+ *,
119
+ from_index: int,
120
+ ) -> tuple[str, int]:
121
+ all_body_lines: list[str] = []
122
+ current_index = from_index
123
+ while current_index < len(all_lines):
124
+ current_line = all_lines[current_index]
125
+ stripped_line = current_line.strip()
126
+ if stripped_line.startswith(FREEFORM_BULLET_PREFIX):
127
+ break
128
+ if stripped_line:
129
+ all_body_lines.append(stripped_line)
130
+ current_index += 1
131
+ return NEWLINE.join(all_body_lines), current_index
132
+
133
+
134
+ def _parse_freeform_findings(reviewer_text: str) -> list[CodexFinding]:
135
+ freeform_finding_line = re.compile(FREEFORM_FINDING_LINE_PATTERN)
136
+ all_lines = reviewer_text.splitlines()
137
+ all_findings: list[CodexFinding] = []
138
+ line_index = 0
139
+ while line_index < len(all_lines):
140
+ line_match = freeform_finding_line.match(all_lines[line_index].strip())
141
+ if line_match is None:
142
+ line_index += 1
143
+ continue
144
+ body_text, next_index = _collect_freeform_body(
145
+ all_lines,
146
+ from_index=line_index + 1,
147
+ )
148
+ all_findings.append(
149
+ CodexFinding(
150
+ title=line_match.group("title").strip(),
151
+ priority=line_match.group("priority"),
152
+ file=line_match.group("file_path").strip(),
153
+ line_range=line_match.group("line_range"),
154
+ body=body_text,
155
+ structured=False,
156
+ )
157
+ )
158
+ line_index = next_index
159
+ return all_findings
160
+
161
+
162
+ def _floor_finding(reviewer_text: str) -> list[CodexFinding]:
163
+ return [
164
+ CodexFinding(
165
+ title=EMPTY_STRING,
166
+ priority=EMPTY_STRING,
167
+ file=EMPTY_STRING,
168
+ line_range=EMPTY_STRING,
169
+ body=reviewer_text,
170
+ structured=False,
171
+ )
172
+ ]
173
+
174
+
175
+ def parse_codex_findings(reviewer_text: str) -> list[CodexFinding]:
176
+ """Parse reviewer text into findings; never drop non-empty text.
177
+
178
+ ::
179
+
180
+ parse_codex_findings("```json\\n[]\\n```") # ok: []
181
+ parse_codex_findings("loose note") # ok: one unstructured finding
182
+ parse_codex_findings('```json\\n["x"]\\n```\\n- [P1] T — a.py:1-1')
183
+ # ok: freeform finding (non-dict JSON list is not structured clean)
184
+
185
+ Tries fenced JSON first (the custom-instructions contract). An empty
186
+ array ``[]`` is structured clean. A non-empty array that yields no
187
+ usable object findings (non-dicts, empty shells, wrong keys) is not
188
+ accepted; freeform and floor paths run instead. Freeform is the
189
+ ``- [P1] title — path:start-end`` shape. Floor carries the raw text.
190
+
191
+ Args:
192
+ reviewer_text: Agent message text from a completed Codex review.
193
+
194
+ Returns:
195
+ Parsed findings. Empty only when the text is blank, or when a
196
+ fenced structured payload is exactly the empty array ``[]``.
197
+ Non-blank text never returns empty for any other structured shape.
198
+ """
199
+ if not reviewer_text.strip():
200
+ return []
201
+ maybe_structured = _try_parse_structured_findings(reviewer_text)
202
+ if maybe_structured is not None:
203
+ return maybe_structured
204
+ all_freeform_findings = _parse_freeform_findings(reviewer_text)
205
+ if all_freeform_findings:
206
+ return all_freeform_findings
207
+ return _floor_finding(reviewer_text)
@@ -0,0 +1,415 @@
1
+ """Headless wrapper that runs one Codex review and captures its outputs.
2
+
3
+ ::
4
+
5
+ review_outcome = run_codex_review(
6
+ repository_directory=Path("/path/to/repo"),
7
+ run_state_directory=Path("/path/to/run_state"),
8
+ is_uncommitted=True,
9
+ )
10
+ review_outcome.outcome_class # "completed" or "codex_down"
11
+ review_outcome.jsonl_path # path to captured JSONL stream
12
+ review_outcome.agent_message # final agent_message text
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import re
20
+ import shutil
21
+ import subprocess
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+
25
+ from codex_review_scripts_constants.codex_usage_probe_constants import (
26
+ ALL_WINDOWS_SCRIPT_SUFFIXES,
27
+ WINDOWS_COMMAND_SHELL,
28
+ WINDOWS_COMMAND_SHELL_RUN_FLAG,
29
+ WINDOWS_OS_NAME,
30
+ )
31
+ from codex_review_scripts_constants.run_constants import (
32
+ ALL_SHAPE_PROBE_REQUIRED_FLAGS,
33
+ BASE_TARGET_FLAG,
34
+ CODEX_BINARY_NAME,
35
+ CODEX_MODEL_PIN,
36
+ COMMIT_TARGET_FLAG,
37
+ CUSTOM_INSTRUCTIONS_PROMPT,
38
+ DEFAULT_TIMEOUT_SECONDS,
39
+ EXEC_SUBCOMMAND,
40
+ HELP_FLAG,
41
+ JSON_FLAG,
42
+ JSONL_AGENT_MESSAGE_TEXT_KEY,
43
+ JSONL_AGENT_MESSAGE_TYPE,
44
+ JSONL_CAPTURE_FILENAME,
45
+ JSONL_CAPTURE_NEWLINE,
46
+ JSONL_ENTRY_COMPLETED_TYPE,
47
+ JSONL_ENTRY_KEY,
48
+ JSONL_EVENT_TYPE_KEY,
49
+ MISSING_BINARY_EXIT_CODE,
50
+ MODEL_FLAG,
51
+ OUTCOME_CLASS_CODEX_DOWN,
52
+ OUTCOME_CLASS_COMPLETED,
53
+ REVIEW_SUBCOMMAND,
54
+ SHAPE_FLAG_TOKEN_TAIL_PATTERN,
55
+ SUBPROCESS_DECODE_EXIT_CODE,
56
+ TIMEOUT_EXIT_CODE,
57
+ UNCOMMITTED_TARGET_FLAG,
58
+ UTF8_ENCODING,
59
+ VERSION_FLAG,
60
+ VERSION_PROBE_PATTERN,
61
+ )
62
+
63
+ codex_subprocess_runner = subprocess.run
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class CodexReviewOutcome:
68
+ """Captured result of one probe-plus-review attempt.
69
+
70
+ ::
71
+
72
+ CodexReviewOutcome(
73
+ outcome_class="completed",
74
+ exit_code=0,
75
+ binary_version="0.144.3",
76
+ jsonl_path=Path("run_state/codex-review.jsonl"),
77
+ agent_message="No issues found.",
78
+ )
79
+
80
+ Attributes:
81
+ outcome_class: Capture-boundary class ``completed`` or ``codex_down``.
82
+ Callers treat this field as the success signal; ``exit_code`` alone
83
+ is not enough (a shape probe can exit 0 while flags are missing).
84
+ exit_code: Last process exit code, or a sentinel when none ran.
85
+ binary_version: Parsed ``codex --version`` string, or empty.
86
+ jsonl_path: Captured JSONL path, or None when review did not run.
87
+ agent_message: Last JSONL ``agent_message`` text when present; on a
88
+ non-zero review exit, falls back to stderr text when JSONL has no
89
+ agent message (config and argument errors land on stderr).
90
+ """
91
+
92
+ outcome_class: str
93
+ exit_code: int
94
+ binary_version: str
95
+ jsonl_path: Path | None
96
+ agent_message: str
97
+
98
+
99
+ def _down(*, exit_code: int, binary_version: str) -> CodexReviewOutcome:
100
+ return CodexReviewOutcome(
101
+ outcome_class=OUTCOME_CLASS_CODEX_DOWN,
102
+ exit_code=exit_code,
103
+ binary_version=binary_version,
104
+ jsonl_path=None,
105
+ agent_message="",
106
+ )
107
+
108
+
109
+ def _resolve_codex_command_prefix() -> list[str]:
110
+ """Resolve the codex launch prefix, wrapping a Windows ``.cmd``/``.bat`` shim.
111
+
112
+ ::
113
+
114
+ POSIX codex on PATH -> ["/usr/local/bin/codex"]
115
+ Windows codex.cmd -> ["cmd", "/c", "C:\\...\\codex.cmd"]
116
+ codex not on PATH -> ["codex"] (downstream FileNotFoundError)
117
+
118
+ ``CreateProcess`` resolves a bare name only against ``.exe``, so a bare
119
+ ``codex`` never finds the npm shim; the ``cmd /c`` wrap runs it.
120
+ """
121
+ codex_path = shutil.which(CODEX_BINARY_NAME)
122
+ if codex_path is None:
123
+ return [CODEX_BINARY_NAME]
124
+ if os.name == WINDOWS_OS_NAME and codex_path.lower().endswith(
125
+ ALL_WINDOWS_SCRIPT_SUFFIXES
126
+ ):
127
+ return [WINDOWS_COMMAND_SHELL, WINDOWS_COMMAND_SHELL_RUN_FLAG, codex_path]
128
+ return [codex_path]
129
+
130
+
131
+ def _run_command(
132
+ all_arguments: list[str],
133
+ *,
134
+ working_directory: Path | None,
135
+ timeout_seconds: int,
136
+ ) -> subprocess.CompletedProcess[str]:
137
+ return codex_subprocess_runner(
138
+ all_arguments,
139
+ cwd=str(working_directory) if working_directory is not None else None,
140
+ capture_output=True,
141
+ text=True,
142
+ encoding=UTF8_ENCODING,
143
+ check=False,
144
+ timeout=timeout_seconds,
145
+ env=dict(os.environ),
146
+ )
147
+
148
+
149
+ def _safe_run(
150
+ all_arguments: list[str],
151
+ *,
152
+ working_directory: Path | None,
153
+ timeout_seconds: int,
154
+ ) -> subprocess.CompletedProcess[str] | int:
155
+ try:
156
+ return _run_command(
157
+ all_arguments,
158
+ working_directory=working_directory,
159
+ timeout_seconds=timeout_seconds,
160
+ )
161
+ except FileNotFoundError:
162
+ if working_directory is not None and not working_directory.is_dir():
163
+ raise
164
+ return MISSING_BINARY_EXIT_CODE
165
+ except subprocess.TimeoutExpired:
166
+ return TIMEOUT_EXIT_CODE
167
+ except UnicodeDecodeError:
168
+ return SUBPROCESS_DECODE_EXIT_CODE
169
+
170
+
171
+ def _parse_binary_version(version_stdout: str) -> str:
172
+ version_match = re.search(VERSION_PROBE_PATTERN, version_stdout)
173
+ if version_match is None:
174
+ return version_stdout.strip()
175
+ return version_match.group(1)
176
+
177
+
178
+ def _probe_binary_version(timeout_seconds: int) -> tuple[str, int | None]:
179
+ completion_or_exit = _safe_run(
180
+ [*_resolve_codex_command_prefix(), VERSION_FLAG],
181
+ working_directory=None,
182
+ timeout_seconds=timeout_seconds,
183
+ )
184
+ if isinstance(completion_or_exit, int):
185
+ return "", completion_or_exit
186
+ if completion_or_exit.returncode != 0:
187
+ return "", completion_or_exit.returncode
188
+ return _parse_binary_version(completion_or_exit.stdout), None
189
+
190
+
191
+ def _probe_review_shape(timeout_seconds: int) -> tuple[bool, int]:
192
+ completion_or_exit = _safe_run(
193
+ [*_resolve_codex_command_prefix(), EXEC_SUBCOMMAND, REVIEW_SUBCOMMAND, HELP_FLAG],
194
+ working_directory=None,
195
+ timeout_seconds=timeout_seconds,
196
+ )
197
+ if isinstance(completion_or_exit, int):
198
+ return False, completion_or_exit
199
+ if completion_or_exit.returncode != 0:
200
+ return False, completion_or_exit.returncode
201
+ help_text = f"{completion_or_exit.stdout}{completion_or_exit.stderr}"
202
+ has_required_flags = all(
203
+ _help_text_contains_flag(help_text, each_flag)
204
+ for each_flag in ALL_SHAPE_PROBE_REQUIRED_FLAGS
205
+ )
206
+ return has_required_flags, completion_or_exit.returncode
207
+
208
+
209
+ def _help_text_contains_flag(help_text: str, flag_name: str) -> bool:
210
+ flag_token_pattern = re.compile(
211
+ re.escape(flag_name) + SHAPE_FLAG_TOKEN_TAIL_PATTERN
212
+ )
213
+ return flag_token_pattern.search(help_text) is not None
214
+
215
+
216
+ def _require_single_target(
217
+ *,
218
+ base_branch: str | None,
219
+ is_uncommitted: bool,
220
+ commit_sha: str | None,
221
+ is_prompt_target: bool,
222
+ ) -> None:
223
+ selected_target_count = sum(
224
+ [
225
+ base_branch is not None,
226
+ is_uncommitted,
227
+ commit_sha is not None,
228
+ is_prompt_target,
229
+ ]
230
+ )
231
+ if selected_target_count != 1:
232
+ raise ValueError(
233
+ "exactly one of base_branch, is_uncommitted, commit_sha, "
234
+ "or is_prompt_target must be selected"
235
+ )
236
+
237
+
238
+ def _require_existing_directory(directory_path: Path, parameter_name: str) -> None:
239
+ if not directory_path.is_dir():
240
+ raise ValueError(
241
+ f"{parameter_name} is not an existing directory: {directory_path}"
242
+ )
243
+
244
+
245
+ def _build_review_arguments(
246
+ *,
247
+ base_branch: str | None,
248
+ is_uncommitted: bool,
249
+ commit_sha: str | None,
250
+ is_prompt_target: bool,
251
+ ) -> list[str]:
252
+ all_arguments = [*_resolve_codex_command_prefix(), EXEC_SUBCOMMAND]
253
+ if CODEX_MODEL_PIN:
254
+ all_arguments.extend([MODEL_FLAG, CODEX_MODEL_PIN])
255
+ all_arguments.extend([REVIEW_SUBCOMMAND, JSON_FLAG])
256
+ if is_uncommitted:
257
+ all_arguments.append(UNCOMMITTED_TARGET_FLAG)
258
+ elif base_branch is not None:
259
+ all_arguments.extend([BASE_TARGET_FLAG, base_branch])
260
+ elif commit_sha is not None:
261
+ all_arguments.extend([COMMIT_TARGET_FLAG, commit_sha])
262
+ elif is_prompt_target:
263
+ all_arguments.append(CUSTOM_INSTRUCTIONS_PROMPT)
264
+ return all_arguments
265
+
266
+
267
+ def _extract_agent_message(jsonl_text: str) -> str:
268
+ agent_message_text = ""
269
+ for each_line in jsonl_text.splitlines():
270
+ stripped_line = each_line.strip()
271
+ if not stripped_line:
272
+ continue
273
+ try:
274
+ event_payload = json.loads(stripped_line)
275
+ except json.JSONDecodeError:
276
+ continue
277
+ if not isinstance(event_payload, dict):
278
+ continue
279
+ if event_payload.get(JSONL_EVENT_TYPE_KEY) != JSONL_ENTRY_COMPLETED_TYPE:
280
+ continue
281
+ entry_payload = event_payload.get(JSONL_ENTRY_KEY)
282
+ if not isinstance(entry_payload, dict):
283
+ continue
284
+ if entry_payload.get(JSONL_EVENT_TYPE_KEY) != JSONL_AGENT_MESSAGE_TYPE:
285
+ continue
286
+ message_text = entry_payload.get(JSONL_AGENT_MESSAGE_TEXT_KEY)
287
+ if isinstance(message_text, str):
288
+ agent_message_text = message_text
289
+ return agent_message_text
290
+
291
+
292
+ def _probe_or_down(timeout_seconds: int) -> CodexReviewOutcome | str:
293
+ binary_version, version_failure_exit_code = _probe_binary_version(timeout_seconds)
294
+ if version_failure_exit_code is not None:
295
+ return _down(
296
+ exit_code=version_failure_exit_code,
297
+ binary_version=binary_version,
298
+ )
299
+ is_shape_supported, shape_exit_code = _probe_review_shape(timeout_seconds)
300
+ if not is_shape_supported:
301
+ return _down(exit_code=shape_exit_code, binary_version=binary_version)
302
+ return binary_version
303
+
304
+
305
+ def _capture_review_run(
306
+ *,
307
+ repository_directory: Path,
308
+ run_state_directory: Path,
309
+ all_review_arguments: list[str],
310
+ timeout_seconds: int,
311
+ binary_version: str,
312
+ ) -> CodexReviewOutcome:
313
+ completion_or_exit = _safe_run(
314
+ all_review_arguments,
315
+ working_directory=repository_directory,
316
+ timeout_seconds=timeout_seconds,
317
+ )
318
+ if isinstance(completion_or_exit, int):
319
+ return _down(exit_code=completion_or_exit, binary_version=binary_version)
320
+ jsonl_text = completion_or_exit.stdout or ""
321
+ stderr_text = completion_or_exit.stderr or ""
322
+ jsonl_path = run_state_directory / JSONL_CAPTURE_FILENAME
323
+ run_state_directory.mkdir(parents=True, exist_ok=True)
324
+ jsonl_path.write_text(
325
+ jsonl_text,
326
+ encoding=UTF8_ENCODING,
327
+ newline=JSONL_CAPTURE_NEWLINE,
328
+ )
329
+ review_returncode = completion_or_exit.returncode
330
+ if review_returncode != 0:
331
+ return CodexReviewOutcome(
332
+ outcome_class=OUTCOME_CLASS_CODEX_DOWN,
333
+ exit_code=review_returncode,
334
+ binary_version=binary_version,
335
+ jsonl_path=jsonl_path,
336
+ agent_message=_extract_agent_message(jsonl_text) or stderr_text,
337
+ )
338
+ return CodexReviewOutcome(
339
+ outcome_class=OUTCOME_CLASS_COMPLETED,
340
+ exit_code=review_returncode,
341
+ binary_version=binary_version,
342
+ jsonl_path=jsonl_path,
343
+ agent_message=_extract_agent_message(jsonl_text),
344
+ )
345
+
346
+
347
+ def run_codex_review(
348
+ *,
349
+ repository_directory: Path,
350
+ run_state_directory: Path,
351
+ base_branch: str | None = None,
352
+ is_uncommitted: bool = False,
353
+ commit_sha: str | None = None,
354
+ is_prompt_target: bool = False,
355
+ timeout_seconds: int | None = None,
356
+ ) -> CodexReviewOutcome:
357
+ """Run one Codex review against a single target and capture its outputs.
358
+
359
+ Capture-only boundary: returns ``completed`` or ``codex_down`` plus raw
360
+ capture fields. Skill-level classes ``down`` / ``clean`` / ``findings``
361
+ come from a later classifier that reads this outcome.
362
+
363
+ ::
364
+
365
+ ok: outcome_class == "completed" and exit_code == 0
366
+ flag: outcome_class == "codex_down" even when exit_code == 0
367
+ (shape probe ran, required flags missing)
368
+
369
+ Args:
370
+ repository_directory: Repository root used as the process cwd.
371
+ run_state_directory: Existing directory that receives the JSONL stream
372
+ file. The caller creates this directory before the call.
373
+ base_branch: Base branch for ``--base``; exclusive with other targets.
374
+ is_uncommitted: When True, uses ``--uncommitted``.
375
+ commit_sha: Commit SHA for ``--commit``; exclusive with other targets.
376
+ is_prompt_target: When True, uses the custom-instructions PROMPT target.
377
+ timeout_seconds: Per-invocation timeout; defaults from constants.
378
+
379
+ Returns:
380
+ Capture outcome (``completed`` / ``codex_down``), process exit code,
381
+ binary version, JSONL path, and agent text (JSONL agent_message, or
382
+ stderr on a failed review when JSONL has no agent message).
383
+
384
+ Raises:
385
+ ValueError: When zero targets or more than one target is selected, or
386
+ when ``repository_directory`` or ``run_state_directory`` is not an
387
+ existing directory. Both directories are checked before any Codex
388
+ process starts, so a bad path costs no review run.
389
+ """
390
+ _require_single_target(
391
+ base_branch=base_branch,
392
+ is_uncommitted=is_uncommitted,
393
+ commit_sha=commit_sha,
394
+ is_prompt_target=is_prompt_target,
395
+ )
396
+ _require_existing_directory(repository_directory, "repository_directory")
397
+ _require_existing_directory(run_state_directory, "run_state_directory")
398
+ resolved_timeout_seconds = (
399
+ DEFAULT_TIMEOUT_SECONDS if timeout_seconds is None else timeout_seconds
400
+ )
401
+ probe_outcome = _probe_or_down(resolved_timeout_seconds)
402
+ if isinstance(probe_outcome, CodexReviewOutcome):
403
+ return probe_outcome
404
+ return _capture_review_run(
405
+ repository_directory=repository_directory,
406
+ run_state_directory=run_state_directory,
407
+ all_review_arguments=_build_review_arguments(
408
+ base_branch=base_branch,
409
+ is_uncommitted=is_uncommitted,
410
+ commit_sha=commit_sha,
411
+ is_prompt_target=is_prompt_target,
412
+ ),
413
+ timeout_seconds=resolved_timeout_seconds,
414
+ binary_version=probe_outcome,
415
+ )