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.
- package/_shared/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +20 -20
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
- package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
- package/_shared/advisor/scripts/tier_model_ids.py +18 -18
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
- package/_shared/pr-loop/worker-spawn.md +186 -0
- package/agents/code-verifier.md +1 -1
- package/bin/ever-shipped-skills.mjs +3 -0
- package/bin/expand_home_directory_tokens.mjs +1 -1
- package/bin/install.mjs +5 -2
- package/hooks/advisory/refactor_guard.py +3 -4
- package/hooks/blocking/CLAUDE.md +7 -1
- package/hooks/blocking/block_main_commit.py +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
- package/hooks/blocking/code_rules_mock_completeness.py +1 -1
- package/hooks/blocking/code_rules_optional_params.py +2 -2
- package/hooks/blocking/code_rules_shared.py +1 -1
- package/hooks/blocking/code_rules_test_assertions.py +1 -1
- package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
- package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
- package/hooks/blocking/inventory_intent_records/records.py +271 -0
- package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
- package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
- package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
- package/hooks/blocking/pii_payload_scan.py +138 -42
- package/hooks/blocking/pii_prevention_blocker.py +185 -291
- package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
- package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
- package/hooks/blocking/pr_description_command_parser.py +8 -4
- package/hooks/blocking/precommit_code_rules_gate.py +3 -3
- package/hooks/blocking/tdd_enforcer.py +97 -608
- package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
- package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
- package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
- package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
- package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
- package/hooks/blocking/test_pii_payload_scan.py +168 -0
- package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
- package/hooks/blocking/tests/conftest.py +10 -0
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
- package/hooks/blocking/tests/test_repository_exemption.py +105 -0
- package/hooks/blocking/tests/test_repository_resolution.py +108 -0
- package/hooks/diagnostic/hook_log_extractor.py +12 -10
- package/hooks/git-hooks/post_commit.py +3 -4
- package/hooks/hooks_constants/CLAUDE.md +2 -2
- package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/local_identity.py +59 -8
- package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
- package/hooks/hooks_constants/test_local_identity.py +105 -3
- package/hooks/pyproject.toml +13 -36
- package/hooks/session/plugin_data_dir_cleanup.py +0 -1
- package/hooks/validation/mypy_validator.py +2 -2
- package/hooks/validators/health_check.py +1 -0
- package/hooks/validators/mypy_integration.py +2 -0
- package/hooks/validators/ruff_integration.py +3 -0
- package/hooks/workflow/auto_formatter.py +5 -4
- package/package.json +1 -1
- package/scripts/CLAUDE.md +4 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
- package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
- package/scripts/dev_env_scripts_constants/timing.py +7 -1
- package/scripts/grok_headless_runner.py +294 -0
- package/scripts/grok_worker_preflight.py +410 -0
- package/scripts/invoke_code_review.py +463 -0
- package/scripts/resolve_worker_spawn.py +619 -0
- package/scripts/spawn_grok_batch.py +672 -0
- package/scripts/test_grok_headless_runner.py +626 -0
- package/scripts/test_grok_worker_preflight.py +1054 -0
- package/scripts/test_invoke_code_review.py +672 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -0
- package/scripts/test_spawn_grok_batch.py +1017 -0
- package/skills/CLAUDE.md +5 -3
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +33 -11
- package/skills/autoconverge/reference/stop-conditions.md +16 -5
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +200 -16
- package/skills/bugteam/CLAUDE.md +2 -2
- package/skills/bugteam/CONSTRAINTS.md +3 -2
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +18 -13
- package/skills/bugteam/reference/audit-and-teammates.md +215 -35
- package/skills/bugteam/reference/design-rationale.md +1 -1
- package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -2
- package/skills/codex-review/CLAUDE.md +46 -0
- package/skills/codex-review/SKILL.md +181 -0
- package/skills/codex-review/reference/CLAUDE.md +15 -0
- package/skills/codex-review/reference/cli-contract.md +253 -0
- package/skills/codex-review/reference/loop-integration.md +118 -0
- package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
- package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
- package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
- package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
- package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
- package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
- package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
- package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
- package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
- package/skills/codex-review/scripts/run_codex_review.py +415 -0
- package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
- package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
- package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
- package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
- package/skills/codex-review/test_skill_scaffold.py +192 -0
- package/skills/grok-spawn/CLAUDE.md +28 -0
- package/skills/grok-spawn/SKILL.md +226 -0
- package/skills/grok-spawn/reference/flag-profiles.md +132 -0
- package/skills/grok-spawn/reference/worker-briefs.md +152 -0
- package/skills/grokify/SKILL.md +9 -1
- package/skills/grokify/capability-claims.test.mjs +28 -0
- package/skills/grokify/evals/README.md +72 -0
- package/skills/grokify/evals/parse-payload.test.mjs +171 -0
- package/skills/grokify/evals/run-capability-evals.mjs +545 -0
- package/skills/orchestrator/SKILL.md +42 -29
- package/skills/orchestrator-refresh/SKILL.md +17 -9
- package/skills/pr-converge/SKILL.md +34 -13
- package/skills/pr-converge/reference/convergence-gates.md +42 -15
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +130 -42
- package/skills/pr-converge/reference/state-schema.md +10 -0
- package/skills/pr-converge/scripts/CLAUDE.md +2 -0
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
- package/skills/pr-converge/scripts/check_convergence.py +605 -29
- package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
- package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
- package/skills/pr-converge/test_step5_host_branch.py +106 -0
- package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
- package/skills/reviewer-gates/SKILL.md +7 -5
- 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,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.
|