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,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
|
+
)
|