claude-dev-env 1.95.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +20 -20
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
  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/ever-shipped-skills.mjs +3 -0
  42. package/bin/expand_home_directory_tokens.mjs +1 -1
  43. package/bin/install.mjs +5 -2
  44. package/hooks/advisory/refactor_guard.py +3 -4
  45. package/hooks/blocking/CLAUDE.md +7 -1
  46. package/hooks/blocking/block_main_commit.py +2 -2
  47. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  48. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  49. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  50. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  61. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  62. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  63. package/hooks/blocking/code_rules_optional_params.py +2 -2
  64. package/hooks/blocking/code_rules_shared.py +1 -1
  65. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  66. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  67. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  69. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  70. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  71. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  72. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  73. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  74. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  75. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  76. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  77. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  78. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  79. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  80. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  85. package/hooks/blocking/pii_payload_scan.py +138 -42
  86. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  87. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  88. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  89. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  90. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  91. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  92. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  93. package/hooks/blocking/pr_description_command_parser.py +8 -4
  94. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  95. package/hooks/blocking/tdd_enforcer.py +97 -608
  96. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  97. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  98. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  99. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  100. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  101. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  102. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  103. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  104. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  105. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  106. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  107. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  108. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  109. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  110. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  111. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  112. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  113. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  114. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  115. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  116. package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
  117. package/hooks/blocking/tests/conftest.py +10 -0
  118. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  119. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  120. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  121. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  122. package/hooks/git-hooks/post_commit.py +3 -4
  123. package/hooks/hooks_constants/CLAUDE.md +2 -2
  124. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  125. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  126. package/hooks/hooks_constants/local_identity.py +59 -8
  127. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  128. package/hooks/hooks_constants/test_local_identity.py +105 -3
  129. package/hooks/pyproject.toml +13 -36
  130. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  131. package/hooks/validation/mypy_validator.py +2 -2
  132. package/hooks/validators/health_check.py +1 -0
  133. package/hooks/validators/mypy_integration.py +2 -0
  134. package/hooks/validators/ruff_integration.py +3 -0
  135. package/hooks/workflow/auto_formatter.py +5 -4
  136. package/package.json +1 -1
  137. package/scripts/CLAUDE.md +4 -0
  138. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  139. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  140. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  141. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  142. package/scripts/grok_headless_runner.py +294 -0
  143. package/scripts/grok_worker_preflight.py +410 -0
  144. package/scripts/invoke_code_review.py +463 -0
  145. package/scripts/resolve_worker_spawn.py +619 -0
  146. package/scripts/spawn_grok_batch.py +672 -0
  147. package/scripts/test_grok_headless_runner.py +626 -0
  148. package/scripts/test_grok_worker_preflight.py +1054 -0
  149. package/scripts/test_invoke_code_review.py +672 -0
  150. package/scripts/test_resolve_worker_spawn.py +1014 -0
  151. package/scripts/test_spawn_grok_batch.py +1017 -0
  152. package/skills/CLAUDE.md +5 -3
  153. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  154. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  155. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  156. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  157. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  158. package/skills/autoconverge/CLAUDE.md +3 -3
  159. package/skills/autoconverge/SKILL.md +9 -3
  160. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  161. package/skills/autoconverge/reference/convergence.md +33 -11
  162. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  163. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  164. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  165. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  166. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  167. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  168. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  169. package/skills/autoconverge/workflow/converge.mjs +200 -16
  170. package/skills/bugteam/CLAUDE.md +2 -2
  171. package/skills/bugteam/CONSTRAINTS.md +3 -2
  172. package/skills/bugteam/PROMPTS.md +7 -6
  173. package/skills/bugteam/SKILL.md +18 -13
  174. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  175. package/skills/bugteam/reference/design-rationale.md +1 -1
  176. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  177. package/skills/bugteam/reference/team-setup.md +8 -2
  178. package/skills/codex-review/CLAUDE.md +46 -0
  179. package/skills/codex-review/SKILL.md +181 -0
  180. package/skills/codex-review/reference/CLAUDE.md +15 -0
  181. package/skills/codex-review/reference/cli-contract.md +253 -0
  182. package/skills/codex-review/reference/loop-integration.md +118 -0
  183. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  184. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  185. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  186. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  189. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  190. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  191. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  192. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  193. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  194. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  195. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  196. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  197. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  198. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  199. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  200. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  201. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  202. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  203. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  204. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  205. package/skills/codex-review/test_skill_scaffold.py +192 -0
  206. package/skills/grok-spawn/CLAUDE.md +28 -0
  207. package/skills/grok-spawn/SKILL.md +226 -0
  208. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  209. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  210. package/skills/grokify/SKILL.md +9 -1
  211. package/skills/grokify/capability-claims.test.mjs +28 -0
  212. package/skills/grokify/evals/README.md +72 -0
  213. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  214. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  215. package/skills/orchestrator/SKILL.md +42 -29
  216. package/skills/orchestrator-refresh/SKILL.md +17 -9
  217. package/skills/pr-converge/SKILL.md +34 -13
  218. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  219. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  220. package/skills/pr-converge/reference/ground-rules.md +1 -1
  221. package/skills/pr-converge/reference/per-tick.md +130 -42
  222. package/skills/pr-converge/reference/state-schema.md +10 -0
  223. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  224. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  225. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  226. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  227. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  228. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  229. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  230. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  231. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  232. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  233. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  234. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  235. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  237. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  238. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  239. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  240. package/skills/reviewer-gates/SKILL.md +7 -5
  241. package/skills/team-advisor/SKILL.md +7 -7
@@ -0,0 +1,45 @@
1
+ """Named constants for the codex-review headless wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ CODEX_BINARY_NAME = "codex"
6
+ EXEC_SUBCOMMAND = "exec"
7
+ REVIEW_SUBCOMMAND = "review"
8
+ HELP_FLAG = "--help"
9
+ VERSION_FLAG = "--version"
10
+ JSON_FLAG = "--json"
11
+ BASE_TARGET_FLAG = "--base"
12
+ UNCOMMITTED_TARGET_FLAG = "--uncommitted"
13
+ COMMIT_TARGET_FLAG = "--commit"
14
+ MODEL_FLAG = "-m"
15
+ CODEX_MODEL_PIN = ""
16
+ CODEX_HOME_ENV_VAR = "CODEX_HOME"
17
+ DEFAULT_TIMEOUT_SECONDS = 600
18
+ JSONL_CAPTURE_FILENAME = "codex-review.jsonl"
19
+ JSONL_CAPTURE_NEWLINE = ""
20
+ UTF8_ENCODING = "utf-8"
21
+ VERSION_PROBE_PATTERN = r"codex(?:-cli)?\s+(\d+\.\d+\.\d+)"
22
+ OUTCOME_CLASS_CODEX_DOWN = "codex_down"
23
+ OUTCOME_CLASS_COMPLETED = "completed"
24
+ MISSING_BINARY_EXIT_CODE = 127
25
+ TIMEOUT_EXIT_CODE = 124
26
+ SUBPROCESS_DECODE_EXIT_CODE = 70
27
+ SHAPE_FLAG_TOKEN_TAIL_PATTERN = r"(?![\w-])"
28
+ JSONL_EVENT_TYPE_KEY = "type"
29
+ JSONL_ENTRY_KEY = "item"
30
+ JSONL_ENTRY_COMPLETED_TYPE = "item.completed"
31
+ JSONL_AGENT_MESSAGE_TYPE = "agent_message"
32
+ JSONL_AGENT_MESSAGE_TEXT_KEY = "text"
33
+ CUSTOM_INSTRUCTIONS_PROMPT = (
34
+ "Write a one-line review summary, then the heading Review comment:, then "
35
+ "zero or more finding bullets. Each bullet must use the form "
36
+ "- [P1] <title> — <path>:<start>-<end> "
37
+ "followed by an explanation paragraph. Use the same [P#] priority tags. "
38
+ "When there are no findings, omit finding bullets after the summary."
39
+ )
40
+ ALL_SHAPE_PROBE_REQUIRED_FLAGS = (
41
+ BASE_TARGET_FLAG,
42
+ UNCOMMITTED_TARGET_FLAG,
43
+ COMMIT_TARGET_FLAG,
44
+ JSON_FLAG,
45
+ )
@@ -0,0 +1,573 @@
1
+ #!/usr/bin/env python3
2
+ """Report remaining weekly Codex usage as machine-readable JSON.
3
+
4
+ Discovery (codex-cli 0.144.3): no dedicated ``codex usage`` subcommand.
5
+ ``codex login status`` prints auth only. Weekly meters come from the
6
+ app-server JSON-RPC method ``account/rateLimits/read`` via
7
+ ``codex app-server --listen stdio://``. The weekly window is the rate-limit
8
+ window whose ``windowDurationMins`` is 10080 (seven days), preferring
9
+ ``secondary`` when both primary and secondary carry that duration.
10
+
11
+ stdout is one JSON object ``{percent_left, window_reset, source}``.
12
+ ``percent_left`` is 0-100, or null when the binary offers no usage data.
13
+ Exit 0 when the probe completed against a reachable CLI (including null
14
+ data). Exit non-zero when the Codex CLI is missing or broken, or when the
15
+ probe itself crashes — consumers must not treat that as a usage-threshold
16
+ skip.
17
+
18
+ Gate rule for consumers: null or unknown ``percent_left`` on exit 0 counts
19
+ as at-or-below ``WEEKLY_USAGE_GATE_THRESHOLD_PERCENT`` — the gate may skip
20
+ and never blocks on missing meters. A non-zero probe exit is CLI failure
21
+ (classify ``codex_down`` or fail closed), not a threshold skip. Require
22
+ review only when exit 0 and ``is_codex_review_required(percent_left)``.
23
+
24
+ ::
25
+
26
+ python codex_usage_probe.py
27
+ {"percent_left": 5, "window_reset": "2026-07-19T23:49:08+00:00",
28
+ "source": "codex app-server account/rateLimits/read"}
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ import math
35
+ import os
36
+ import re
37
+ import shutil
38
+ import subprocess
39
+ import sys
40
+ import threading
41
+ from collections.abc import Mapping, Sequence
42
+ from datetime import datetime, timezone
43
+ from typing import IO, Protocol
44
+
45
+ from codex_review_scripts_constants.codex_usage_probe_constants import (
46
+ ALL_APP_SERVER_COMMAND_PARTS,
47
+ ALL_WINDOWS_SCRIPT_SUFFIXES,
48
+ APP_SERVER_TIMEOUT_SECONDS,
49
+ CAPABILITIES_KEY,
50
+ CLIENT_INFO_KEY,
51
+ CLIENT_INFO_NAME,
52
+ CLIENT_INFO_NAME_KEY,
53
+ CLIENT_INFO_VERSION,
54
+ CLIENT_INFO_VERSION_KEY,
55
+ CODEX_BINARY_NAME,
56
+ EXIT_CODE_CRASH,
57
+ EXIT_CODE_SUCCESS,
58
+ EXPERIMENTAL_API_KEY,
59
+ INITIALIZE_REQUEST_ID,
60
+ JSONRPC_KEY_ERROR,
61
+ JSONRPC_KEY_ID,
62
+ JSONRPC_KEY_METHOD,
63
+ JSONRPC_KEY_PARAMS,
64
+ JSONRPC_KEY_REPLY_BODY,
65
+ JSONRPC_KEY_VERSION,
66
+ JSONRPC_VERSION,
67
+ METHOD_INITIALIZE,
68
+ METHOD_INITIALIZED,
69
+ METHOD_RATE_LIMITS_READ,
70
+ NEWLINE,
71
+ PERCENT_EMPTY,
72
+ PERCENT_FULL,
73
+ PRIMARY_WINDOW_KEY,
74
+ PROCESS_TREE_WAIT_TIMEOUT_SECONDS,
75
+ RATE_LIMITS_KEY,
76
+ RATE_LIMITS_REQUEST_ID,
77
+ READER_THREAD_JOIN_TIMEOUT_SECONDS,
78
+ RESETS_AT_KEY,
79
+ SECONDARY_WINDOW_KEY,
80
+ SOURCE_APP_SERVER_RATE_LIMITS,
81
+ SOURCE_NO_USAGE_SURFACE,
82
+ SOURCE_TEXT_STATUS,
83
+ TEXT_WEEKLY_PERCENT_LEFT_PATTERN,
84
+ TEXT_WEEKLY_USED_PERCENT_PATTERN,
85
+ TEXT_WINDOW_RESET_PATTERN,
86
+ USAGE_REPORT_KEY_PERCENT_LEFT,
87
+ USAGE_REPORT_KEY_SOURCE,
88
+ USAGE_REPORT_KEY_WINDOW_RESET,
89
+ USED_PERCENT_KEY,
90
+ UTF8_ENCODING,
91
+ WEEKLY_USAGE_GATE_THRESHOLD_PERCENT,
92
+ WEEKLY_WINDOW_DURATION_MINUTES,
93
+ WINDOW_DURATION_MINS_KEY,
94
+ WINDOWS_COMMAND_SHELL,
95
+ WINDOWS_COMMAND_SHELL_RUN_FLAG,
96
+ WINDOWS_OS_NAME,
97
+ WINDOWS_TASKKILL_COMMAND,
98
+ WINDOWS_TASKKILL_FORCE_FLAG,
99
+ WINDOWS_TASKKILL_PID_FLAG,
100
+ WINDOWS_TASKKILL_TREE_FLAG,
101
+ )
102
+
103
+
104
+ class AppServerExchange(Protocol):
105
+ """Callable that sends JSON-RPC request objects and returns stdout lines."""
106
+
107
+ def __call__(
108
+ self, all_request_messages: list[dict[str, object]]
109
+ ) -> list[str]:
110
+ """Exchange request messages for server stdout lines.
111
+
112
+ Args:
113
+ all_request_messages: JSON-RPC request/notification objects to send.
114
+
115
+ Returns:
116
+ Raw stdout lines from the app-server process.
117
+ """
118
+
119
+
120
+ UsageReport = dict[str, float | str | None]
121
+
122
+
123
+ def is_codex_review_required(percent_left: float | None) -> bool:
124
+ """Return whether the weekly gate should require a Codex review pass.
125
+
126
+ ::
127
+
128
+ None -> False # missing data never blocks
129
+ 10.0 -> False # at threshold: skip
130
+ 10.1 -> True # above threshold: require
131
+
132
+ Args:
133
+ percent_left: Weekly percent remaining, or None when unknown.
134
+
135
+ Returns:
136
+ True only when percent_left is known and strictly greater than
137
+ WEEKLY_USAGE_GATE_THRESHOLD_PERCENT.
138
+ """
139
+ if percent_left is None:
140
+ return False
141
+ return percent_left > WEEKLY_USAGE_GATE_THRESHOLD_PERCENT
142
+
143
+
144
+ def _null_usage_report() -> UsageReport:
145
+ return {
146
+ USAGE_REPORT_KEY_PERCENT_LEFT: None,
147
+ USAGE_REPORT_KEY_WINDOW_RESET: None,
148
+ USAGE_REPORT_KEY_SOURCE: SOURCE_NO_USAGE_SURFACE,
149
+ }
150
+
151
+
152
+ def _clamp_percent(raw_percent: float) -> float:
153
+ return max(float(PERCENT_EMPTY), min(float(PERCENT_FULL), raw_percent))
154
+
155
+
156
+ def _format_resets_at(raw_resets_at: object) -> str | None:
157
+ if isinstance(raw_resets_at, bool):
158
+ return None
159
+ if isinstance(raw_resets_at, (int, float)):
160
+ if not math.isfinite(raw_resets_at):
161
+ return None
162
+ try:
163
+ return datetime.fromtimestamp(
164
+ float(raw_resets_at), tz=timezone.utc
165
+ ).isoformat()
166
+ except (OverflowError, OSError, ValueError):
167
+ return None
168
+ if isinstance(raw_resets_at, str):
169
+ stripped = raw_resets_at.strip()
170
+ return stripped or None
171
+ return None
172
+
173
+
174
+ def _window_used_percent(all_window_fields: Mapping[str, object]) -> float | None:
175
+ raw_used_percent = all_window_fields.get(USED_PERCENT_KEY)
176
+ if isinstance(raw_used_percent, bool):
177
+ return None
178
+ if isinstance(raw_used_percent, (int, float)):
179
+ if not math.isfinite(raw_used_percent):
180
+ return None
181
+ return float(raw_used_percent)
182
+ return None
183
+
184
+
185
+ def _window_duration_minutes(
186
+ all_window_fields: Mapping[str, object],
187
+ ) -> int | None:
188
+ raw_duration = all_window_fields.get(WINDOW_DURATION_MINS_KEY)
189
+ if isinstance(raw_duration, bool):
190
+ return None
191
+ if isinstance(raw_duration, (int, float)):
192
+ if not math.isfinite(raw_duration):
193
+ return None
194
+ return int(raw_duration)
195
+ return None
196
+
197
+
198
+ def _select_weekly_window(
199
+ all_rate_limit_fields: Mapping[str, object],
200
+ ) -> Mapping[str, object] | None:
201
+ primary_payload = all_rate_limit_fields.get(PRIMARY_WINDOW_KEY)
202
+ secondary_payload = all_rate_limit_fields.get(SECONDARY_WINDOW_KEY)
203
+ all_candidate_windows: list[Mapping[str, object]] = []
204
+ if isinstance(secondary_payload, Mapping):
205
+ all_candidate_windows.append(secondary_payload)
206
+ if isinstance(primary_payload, Mapping):
207
+ all_candidate_windows.append(primary_payload)
208
+ if not all_candidate_windows:
209
+ return None
210
+ for each_window in all_candidate_windows:
211
+ if _window_duration_minutes(each_window) == WEEKLY_WINDOW_DURATION_MINUTES:
212
+ return each_window
213
+ return None
214
+
215
+
216
+ def _usage_report_from_window(
217
+ all_weekly_window_fields: Mapping[str, object],
218
+ source: str,
219
+ ) -> UsageReport:
220
+ used_percent = _window_used_percent(all_weekly_window_fields)
221
+ if used_percent is None:
222
+ return _null_usage_report()
223
+ percent_left = _clamp_percent(float(PERCENT_FULL) - used_percent)
224
+ return {
225
+ USAGE_REPORT_KEY_PERCENT_LEFT: percent_left,
226
+ USAGE_REPORT_KEY_WINDOW_RESET: _format_resets_at(
227
+ all_weekly_window_fields.get(RESETS_AT_KEY)
228
+ ),
229
+ USAGE_REPORT_KEY_SOURCE: source,
230
+ }
231
+
232
+
233
+ def _parse_rate_limits_snapshot(
234
+ all_rate_limit_fields: Mapping[str, object],
235
+ ) -> UsageReport:
236
+ weekly_window = _select_weekly_window(all_rate_limit_fields)
237
+ if weekly_window is None:
238
+ return _null_usage_report()
239
+ return _usage_report_from_window(weekly_window, SOURCE_APP_SERVER_RATE_LIMITS)
240
+
241
+
242
+ def parse_rate_limits_message(message_text: str) -> UsageReport:
243
+ """Parse one JSON-RPC reply line that may carry rateLimits.
244
+
245
+ Args:
246
+ message_text: A single stdout line from ``codex app-server``.
247
+
248
+ Returns:
249
+ The usage report, or a null report when the line has no usable meters.
250
+ """
251
+ try:
252
+ message_payload = json.loads(message_text)
253
+ except json.JSONDecodeError:
254
+ return _null_usage_report()
255
+ if not isinstance(message_payload, dict):
256
+ return _null_usage_report()
257
+ if message_payload.get(JSONRPC_KEY_ERROR) is not None:
258
+ return _null_usage_report()
259
+ reply_body = message_payload.get(JSONRPC_KEY_REPLY_BODY)
260
+ if not isinstance(reply_body, dict):
261
+ return _null_usage_report()
262
+ all_rate_limit_fields = reply_body.get(RATE_LIMITS_KEY)
263
+ if not isinstance(all_rate_limit_fields, dict):
264
+ return _null_usage_report()
265
+ return _parse_rate_limits_snapshot(all_rate_limit_fields)
266
+
267
+
268
+ def _text_status_report(
269
+ percent_left: float,
270
+ status_text: str,
271
+ search_from_index: int,
272
+ ) -> UsageReport:
273
+ weekly_scoped_text = status_text[search_from_index:]
274
+ reset_match = re.search(TEXT_WINDOW_RESET_PATTERN, weekly_scoped_text)
275
+ window_reset = (
276
+ reset_match.group("reset").strip() if reset_match is not None else None
277
+ )
278
+ return {
279
+ USAGE_REPORT_KEY_PERCENT_LEFT: percent_left,
280
+ USAGE_REPORT_KEY_WINDOW_RESET: window_reset,
281
+ USAGE_REPORT_KEY_SOURCE: SOURCE_TEXT_STATUS,
282
+ }
283
+
284
+
285
+ def parse_text_usage_status(status_text: str) -> UsageReport:
286
+ """Parse human status text for a weekly percent remaining.
287
+
288
+ Args:
289
+ status_text: Captured CLI or TUI status text.
290
+
291
+ Returns:
292
+ The usage report when a weekly percent pattern matches, else null.
293
+ """
294
+ left_match = re.search(TEXT_WEEKLY_PERCENT_LEFT_PATTERN, status_text)
295
+ if left_match is not None:
296
+ percent_left = _clamp_percent(float(left_match.group("percent")))
297
+ return _text_status_report(
298
+ percent_left,
299
+ status_text,
300
+ search_from_index=left_match.start(),
301
+ )
302
+ used_match = re.search(TEXT_WEEKLY_USED_PERCENT_PATTERN, status_text)
303
+ if used_match is not None:
304
+ percent_left = _clamp_percent(
305
+ float(PERCENT_FULL) - float(used_match.group("percent"))
306
+ )
307
+ return _text_status_report(
308
+ percent_left,
309
+ status_text,
310
+ search_from_index=used_match.start(),
311
+ )
312
+ return _null_usage_report()
313
+
314
+
315
+ def _build_app_server_request_messages() -> list[dict[str, object]]:
316
+ initialize_request: dict[str, object] = {
317
+ JSONRPC_KEY_VERSION: JSONRPC_VERSION,
318
+ JSONRPC_KEY_ID: INITIALIZE_REQUEST_ID,
319
+ JSONRPC_KEY_METHOD: METHOD_INITIALIZE,
320
+ JSONRPC_KEY_PARAMS: {
321
+ CLIENT_INFO_KEY: {
322
+ CLIENT_INFO_NAME_KEY: CLIENT_INFO_NAME,
323
+ CLIENT_INFO_VERSION_KEY: CLIENT_INFO_VERSION,
324
+ },
325
+ CAPABILITIES_KEY: {EXPERIMENTAL_API_KEY: True},
326
+ },
327
+ }
328
+ initialized_notification: dict[str, object] = {
329
+ JSONRPC_KEY_VERSION: JSONRPC_VERSION,
330
+ JSONRPC_KEY_METHOD: METHOD_INITIALIZED,
331
+ JSONRPC_KEY_PARAMS: {},
332
+ }
333
+ rate_limits_request: dict[str, object] = {
334
+ JSONRPC_KEY_VERSION: JSONRPC_VERSION,
335
+ JSONRPC_KEY_ID: RATE_LIMITS_REQUEST_ID,
336
+ JSONRPC_KEY_METHOD: METHOD_RATE_LIMITS_READ,
337
+ JSONRPC_KEY_PARAMS: {},
338
+ }
339
+ return [initialize_request, initialized_notification, rate_limits_request]
340
+
341
+
342
+ def _resolve_codex_command() -> list[str]:
343
+ codex_path = shutil.which(CODEX_BINARY_NAME)
344
+ if codex_path is None:
345
+ raise FileNotFoundError(f"{CODEX_BINARY_NAME} binary not found on PATH")
346
+ lower_path = codex_path.lower()
347
+ if os.name == WINDOWS_OS_NAME and lower_path.endswith(
348
+ ALL_WINDOWS_SCRIPT_SUFFIXES
349
+ ):
350
+ return [
351
+ WINDOWS_COMMAND_SHELL,
352
+ WINDOWS_COMMAND_SHELL_RUN_FLAG,
353
+ codex_path,
354
+ *ALL_APP_SERVER_COMMAND_PARTS,
355
+ ]
356
+ return [codex_path, *ALL_APP_SERVER_COMMAND_PARTS]
357
+
358
+
359
+ def _is_rate_limits_reply(message_text: str) -> bool:
360
+ """Return whether a stdout line is the reply to our rate-limits request."""
361
+ try:
362
+ message_payload = json.loads(message_text)
363
+ except json.JSONDecodeError:
364
+ return False
365
+ if not isinstance(message_payload, dict):
366
+ return False
367
+ return message_payload.get(JSONRPC_KEY_ID) == RATE_LIMITS_REQUEST_ID
368
+
369
+
370
+ def _collect_server_lines(
371
+ server_stdout: IO[str],
372
+ all_server_lines: list[str],
373
+ is_exchange_over: threading.Event,
374
+ ) -> None:
375
+ """Append server stdout lines until the rate-limits reply lands or stdout ends."""
376
+ try:
377
+ for each_line in server_stdout:
378
+ stripped_line = each_line.strip()
379
+ if not stripped_line:
380
+ continue
381
+ all_server_lines.append(stripped_line)
382
+ if _is_rate_limits_reply(stripped_line):
383
+ break
384
+ except (OSError, ValueError):
385
+ pass
386
+ is_exchange_over.set()
387
+
388
+
389
+ def _terminate_process_tree(server_process: subprocess.Popen[str]) -> None:
390
+ """Stop the app-server process and any children it launched.
391
+
392
+ On Windows the probe may wrap a ``.cmd``/``.bat`` shim as ``cmd /c``, so
393
+ ``Popen.pid`` is ``cmd.exe``. Killing only that process leaves the real
394
+ Codex app-server grandchild alive and holding stdout. ``taskkill /T``
395
+ tears down the whole tree; other platforms use ``kill()``.
396
+ """
397
+ if server_process.poll() is not None:
398
+ return
399
+ process_id = server_process.pid
400
+ if process_id is None:
401
+ return
402
+ if os.name == WINDOWS_OS_NAME:
403
+ subprocess.run(
404
+ [
405
+ WINDOWS_TASKKILL_COMMAND,
406
+ WINDOWS_TASKKILL_FORCE_FLAG,
407
+ WINDOWS_TASKKILL_TREE_FLAG,
408
+ WINDOWS_TASKKILL_PID_FLAG,
409
+ str(process_id),
410
+ ],
411
+ check=False,
412
+ stdout=subprocess.DEVNULL,
413
+ stderr=subprocess.DEVNULL,
414
+ )
415
+ try:
416
+ server_process.wait(timeout=PROCESS_TREE_WAIT_TIMEOUT_SECONDS)
417
+ except subprocess.TimeoutExpired:
418
+ server_process.kill()
419
+ return
420
+ server_process.kill()
421
+ try:
422
+ server_process.wait(timeout=PROCESS_TREE_WAIT_TIMEOUT_SECONDS)
423
+ except subprocess.TimeoutExpired:
424
+ pass
425
+
426
+
427
+ def _teardown_app_server_exchange(
428
+ server_process: subprocess.Popen[str],
429
+ reader_thread: threading.Thread,
430
+ ) -> None:
431
+ """Terminate the process tree, then join the stdout reader before return."""
432
+ _terminate_process_tree(server_process)
433
+ reader_thread.join(timeout=READER_THREAD_JOIN_TIMEOUT_SECONDS)
434
+
435
+
436
+ def _exchange_app_server_messages_via_subprocess(
437
+ all_request_messages: list[dict[str, object]],
438
+ ) -> list[str]:
439
+ """Run ``codex app-server`` and collect its replies to the probe's requests.
440
+
441
+ ::
442
+
443
+ write requests -> stdin stays OPEN -> server answers -> read reply -> close
444
+
445
+ The server treats end-of-input on stdin as a shutdown signal, so it exits
446
+ without answering when stdin closes right after the requests are written.
447
+ Stdin is held open until the rate-limits reply arrives or the timeout runs
448
+ out, then the process tree is torn down and the stdout reader is joined.
449
+
450
+ Args:
451
+ all_request_messages: JSON-RPC request/notification objects to send.
452
+
453
+ Returns:
454
+ The non-empty stdout lines the server emitted before the exchange ended.
455
+ """
456
+ all_command_parts = _resolve_codex_command()
457
+ stdin_text = NEWLINE.join(
458
+ json.dumps(each_message) for each_message in all_request_messages
459
+ ) + NEWLINE
460
+ all_server_lines: list[str] = []
461
+ is_exchange_over = threading.Event()
462
+ with subprocess.Popen(
463
+ all_command_parts,
464
+ stdin=subprocess.PIPE,
465
+ stdout=subprocess.PIPE,
466
+ stderr=subprocess.DEVNULL,
467
+ text=True,
468
+ encoding=UTF8_ENCODING,
469
+ shell=False,
470
+ ) as server_process:
471
+ server_stdin = server_process.stdin
472
+ server_stdout = server_process.stdout
473
+ if server_stdin is None or server_stdout is None:
474
+ _terminate_process_tree(server_process)
475
+ raise OSError("codex app-server offered no stdio pipes")
476
+ reader_thread = threading.Thread(
477
+ target=_collect_server_lines,
478
+ args=(server_stdout, all_server_lines, is_exchange_over),
479
+ daemon=True,
480
+ )
481
+ reader_thread.start()
482
+ try:
483
+ server_stdin.write(stdin_text)
484
+ server_stdin.flush()
485
+ is_exchange_over.wait(timeout=APP_SERVER_TIMEOUT_SECONDS)
486
+ finally:
487
+ _teardown_app_server_exchange(
488
+ server_process=server_process,
489
+ reader_thread=reader_thread,
490
+ )
491
+ return list(all_server_lines)
492
+
493
+
494
+ def _usage_report_from_server_lines(all_server_lines: Sequence[str]) -> UsageReport:
495
+ for each_line in all_server_lines:
496
+ usage_report = parse_rate_limits_message(each_line)
497
+ if usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] is not None:
498
+ return usage_report
499
+ return _null_usage_report()
500
+
501
+
502
+ def probe_weekly_usage(
503
+ exchange_app_server_messages: AppServerExchange,
504
+ ) -> UsageReport:
505
+ """Probe weekly Codex usage through the app-server rate-limits surface.
506
+
507
+ Args:
508
+ exchange_app_server_messages: JSON-RPC exchange seam. Production passes
509
+ the real subprocess exchange; tests inject a fake.
510
+
511
+ Returns:
512
+ The usage report with percent_left, window_reset, and source.
513
+ """
514
+ all_request_messages = _build_app_server_request_messages()
515
+ all_server_lines = exchange_app_server_messages(all_request_messages)
516
+ return _usage_report_from_server_lines(all_server_lines)
517
+
518
+
519
+ def probe_weekly_usage_via_subprocess() -> UsageReport:
520
+ """Probe weekly usage against the real ``codex app-server`` subprocess.
521
+
522
+ ::
523
+
524
+ probe_weekly_usage_via_subprocess()
525
+ # ok: {"percent_left": 62.0, "window_reset": ..., "source": ...}
526
+
527
+ The production entry point for every caller that has no exchange seam of
528
+ its own to inject.
529
+
530
+ Returns:
531
+ The usage report with percent_left, window_reset, and source.
532
+ """
533
+ return probe_weekly_usage(
534
+ exchange_app_server_messages=_exchange_app_server_messages_via_subprocess
535
+ )
536
+
537
+
538
+ def main() -> int:
539
+ """Run the probe and print one JSON object on stdout.
540
+
541
+ ::
542
+
543
+ reachable CLI, meters present -> exit 0, percent_left number
544
+ reachable CLI, no meters -> exit 0, percent_left null (usage skip ok)
545
+ missing or broken CLI -> exit non-zero (not a usage skip)
546
+
547
+ Returns:
548
+ EXIT_CODE_SUCCESS on a completed probe against a reachable CLI
549
+ (including null data), or EXIT_CODE_CRASH when the Codex CLI is
550
+ missing or broken or the probe itself fails.
551
+ """
552
+ try:
553
+ usage_report = probe_weekly_usage(
554
+ exchange_app_server_messages=_exchange_app_server_messages_via_subprocess
555
+ )
556
+ except (
557
+ FileNotFoundError,
558
+ OSError,
559
+ subprocess.TimeoutExpired,
560
+ subprocess.SubprocessError,
561
+ UnicodeDecodeError,
562
+ ):
563
+ print(json.dumps(_null_usage_report()))
564
+ return EXIT_CODE_CRASH
565
+ except (json.JSONDecodeError, ValueError, TypeError, KeyError):
566
+ print(json.dumps(_null_usage_report()))
567
+ return EXIT_CODE_CRASH
568
+ print(json.dumps(usage_report))
569
+ return EXIT_CODE_SUCCESS
570
+
571
+
572
+ if __name__ == "__main__":
573
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ Error: unauthorized (401). Login required before running Codex review.
@@ -0,0 +1 @@
1
+ Error loading config.toml: unknown variant 'default', expected 'fast' or 'flex' in 'service_tier'
@@ -0,0 +1,6 @@
1
+ The change removes required empty-input handling, causing a deterministic exception and breaking an existing test.
2
+
3
+ Review comment:
4
+
5
+ - [P1] Restore empty-input handling — src/stats.py:2-2
6
+ When average([]) is called, including by the existing test_empty_input_returns_zero, this line divides by zero and raises ZeroDivisionError instead of returning 0.0. This directly breaks the established contract and test suite.
@@ -0,0 +1,5 @@
1
+ {"type":"thread.started","thread_id":"thread-fixture-model"}
2
+ {"type":"turn.started"}
3
+ {"type":"error","message":"{\"error\":{\"type\":\"invalid_request_error\",\"code\":null,\"message\":\"The 'o3' model is not supported when using Codex with a ChatGPT account.\",\"param\":null},\"status\":400}"}
4
+ {"type":"item.completed","item":{"type":"agent_message","text":"Review was interrupted. Please try again."}}
5
+ {"type":"turn.failed","error":{"message":"The 'o3' model is not supported when using Codex with a ChatGPT account. The 'o3' model requires a newer version of Codex."}}
@@ -0,0 +1,13 @@
1
+ Review complete with one finding.
2
+
3
+ ```json
4
+ [
5
+ {
6
+ "title": "Restore empty-input handling",
7
+ "priority": "P1",
8
+ "file": "src/stats.py",
9
+ "line_range": "2-2",
10
+ "body": "Divides by zero on empty input instead of returning 0.0."
11
+ }
12
+ ]
13
+ ```
@@ -0,0 +1,6 @@
1
+ {"type":"thread.started","thread_id":"thread-fixture-success"}
2
+ {"type":"turn.started"}
3
+ {"type":"item.started","item":{"id":"item_0","type":"command_execution","command":"git status --short","status":"in_progress"}}
4
+ {"type":"item.completed","item":{"id":"item_0","type":"command_execution","command":"git status --short","exit_code":0,"status":"completed"}}
5
+ {"type":"item.completed","item":{"type":"agent_message","text":"No issues found.\n\n```json\n[]\n```"}}
6
+ {"type":"turn.completed","usage":{"input_tokens":12,"cached_input_tokens":0,"output_tokens":6,"reasoning_output_tokens":0}}
@@ -0,0 +1 @@
1
+ Error: unexpected internal failure while running review.
@@ -0,0 +1 @@
1
+ Error: rate limit exceeded. Too many requests (429). Quota and credits exhausted; retry later.