claude-dev-env 1.95.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +15 -11
- package/skills/orchestrator-refresh/SKILL.md +5 -5
- 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
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
enabled lists bugbot, disabled bugbot -> bugbot flag: off (opt-out wins)
|
|
8
8
|
disabled empty -> bugteam ok: runs
|
|
9
9
|
disabled lists copilot -> copilot flag: off
|
|
10
|
+
disabled lists codex -> codex flag: off
|
|
10
11
|
|
|
11
12
|
Bugbot is off by default and runs only when the enabled list names it.
|
|
12
|
-
Bugteam and
|
|
13
|
-
names them; both lists parse case-insensitively and tolerate whitespace.
|
|
13
|
+
Bugteam, copilot, and codex run by default and stop only when the disabled
|
|
14
|
+
list names them; both lists parse case-insensitively and tolerate whitespace.
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
from __future__ import annotations
|
|
@@ -22,6 +23,7 @@ import sys
|
|
|
22
23
|
from pr_loop_shared_constants.reviews_disabled_constants import (
|
|
23
24
|
CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN,
|
|
24
25
|
CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
|
|
26
|
+
CLAUDE_REVIEWS_DISABLED_CODEX_TOKEN,
|
|
25
27
|
CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
|
|
26
28
|
CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME,
|
|
27
29
|
CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR,
|
|
@@ -33,6 +35,7 @@ from pr_loop_shared_constants.reviews_disabled_constants import (
|
|
|
33
35
|
__all__ = [
|
|
34
36
|
"CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN",
|
|
35
37
|
"CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN",
|
|
38
|
+
"CLAUDE_REVIEWS_DISABLED_CODEX_TOKEN",
|
|
36
39
|
"CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN",
|
|
37
40
|
"CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME",
|
|
38
41
|
"CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR",
|
|
@@ -41,6 +44,7 @@ __all__ = [
|
|
|
41
44
|
"is_bugbot_disabled_via_env",
|
|
42
45
|
"is_bugbot_opted_out_via_env",
|
|
43
46
|
"is_bugteam_disabled_via_env",
|
|
47
|
+
"is_codex_disabled_via_env",
|
|
44
48
|
"is_copilot_disabled_via_env",
|
|
45
49
|
"main",
|
|
46
50
|
]
|
|
@@ -129,6 +133,17 @@ def is_copilot_disabled_via_env() -> bool:
|
|
|
129
133
|
)
|
|
130
134
|
|
|
131
135
|
|
|
136
|
+
def is_codex_disabled_via_env() -> bool:
|
|
137
|
+
"""Check whether CLAUDE_REVIEWS_DISABLED opts the Codex reviewer out.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True when the env var lists the ``codex`` token.
|
|
141
|
+
"""
|
|
142
|
+
return _is_reviewer_listed_in_env(
|
|
143
|
+
CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME, CLAUDE_REVIEWS_DISABLED_CODEX_TOKEN
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
132
147
|
def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
|
|
133
148
|
"""Parse command-line arguments for the reviewer opt-out and opt-in gate check.
|
|
134
149
|
|
|
@@ -148,6 +163,7 @@ def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
|
|
|
148
163
|
CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN,
|
|
149
164
|
CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
|
|
150
165
|
CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
|
|
166
|
+
CLAUDE_REVIEWS_DISABLED_CODEX_TOKEN,
|
|
151
167
|
],
|
|
152
168
|
help="Reviewer token to test against the CLAUDE_REVIEWS_DISABLED / CLAUDE_REVIEWS_ENABLED gates",
|
|
153
169
|
)
|
|
@@ -168,6 +184,7 @@ def main(all_arguments: list[str]) -> int:
|
|
|
168
184
|
CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN: is_bugbot_disabled_via_env,
|
|
169
185
|
CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN: is_bugteam_disabled_via_env,
|
|
170
186
|
CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN: is_copilot_disabled_via_env,
|
|
187
|
+
CLAUDE_REVIEWS_DISABLED_CODEX_TOKEN: is_codex_disabled_via_env,
|
|
171
188
|
}
|
|
172
189
|
return 0 if disabled_checker_by_reviewer[arguments.reviewer]() else 1
|
|
173
190
|
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Behavioral tests for the gate's empty-file-set loudness and inspected-count report.
|
|
2
|
+
|
|
3
|
+
Covers the three acceptance criteria of the loud-empty change:
|
|
4
|
+
- A run whose resolved file set is empty exits non-zero and says so; a set
|
|
5
|
+
emptied only by the ``--only-under`` scope exits clean with the loud count.
|
|
6
|
+
- A run over new, untracked modules inspects them.
|
|
7
|
+
- Every run reports how many files it inspected.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import importlib.util
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_gate_module() -> ModuleType:
|
|
20
|
+
module_path = Path(__file__).parent / "code_rules_gate.py"
|
|
21
|
+
spec = importlib.util.spec_from_file_location(
|
|
22
|
+
"code_rules_gate_empty_set_under_test", module_path
|
|
23
|
+
)
|
|
24
|
+
assert spec is not None
|
|
25
|
+
assert spec.loader is not None
|
|
26
|
+
module = importlib.util.module_from_spec(spec)
|
|
27
|
+
spec.loader.exec_module(module)
|
|
28
|
+
return module
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
gate_module = _load_gate_module()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _control_violation_module_content() -> str:
|
|
35
|
+
"""Return module content the enforcer must block.
|
|
36
|
+
|
|
37
|
+
The control violation is a formatted-string logging call. The source
|
|
38
|
+
fragment is assembled from pieces so this test file itself does not
|
|
39
|
+
carry the flagged pattern.
|
|
40
|
+
"""
|
|
41
|
+
formatted_call = "logger.info(" + 'f"announcing {name}")'
|
|
42
|
+
return (
|
|
43
|
+
'"""Fresh module with a logging violation."""\n'
|
|
44
|
+
"import logging\n\n"
|
|
45
|
+
'logger = logging.getLogger("fresh_violation")\n\n\n'
|
|
46
|
+
"def announce(name: str) -> None:\n"
|
|
47
|
+
f" {formatted_call}\n"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run_git_in_repository(repository_root: Path, *arguments: str) -> str:
|
|
52
|
+
completion = subprocess.run(
|
|
53
|
+
["git", *arguments],
|
|
54
|
+
cwd=str(repository_root),
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
encoding="utf-8",
|
|
58
|
+
errors="replace",
|
|
59
|
+
check=True,
|
|
60
|
+
env=gate_module.repository_environment(),
|
|
61
|
+
)
|
|
62
|
+
return completion.stdout
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def initialize_git_repository(repository_root: Path) -> None:
|
|
66
|
+
run_git_in_repository(repository_root, "init", "--initial-branch=main")
|
|
67
|
+
run_git_in_repository(repository_root, "config", "user.email", "test@example.com")
|
|
68
|
+
run_git_in_repository(repository_root, "config", "user.name", "Test")
|
|
69
|
+
run_git_in_repository(repository_root, "config", "commit.gpgsign", "false")
|
|
70
|
+
disabled_hooks_directory = repository_root / "disabled-git-hooks"
|
|
71
|
+
disabled_hooks_directory.mkdir()
|
|
72
|
+
run_git_in_repository(
|
|
73
|
+
repository_root, "config", "core.hooksPath", str(disabled_hooks_directory)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def commit_all_files(repository_root: Path, commit_message: str) -> None:
|
|
78
|
+
run_git_in_repository(repository_root, "add", "-A")
|
|
79
|
+
run_git_in_repository(repository_root, "commit", "-m", commit_message)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.fixture(autouse=True)
|
|
83
|
+
def isolated_git_environment(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
84
|
+
for each_variable_name in list(os.environ):
|
|
85
|
+
if each_variable_name.startswith("GIT_"):
|
|
86
|
+
monkeypatch.delenv(each_variable_name, raising=False)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.fixture()
|
|
90
|
+
def temporary_git_repository(tmp_path: Path) -> Path:
|
|
91
|
+
repository_root = tmp_path / "repository_under_test"
|
|
92
|
+
repository_root.mkdir()
|
|
93
|
+
initialize_git_repository(repository_root)
|
|
94
|
+
(repository_root / "seed.txt").write_text("seed\n", encoding="utf-8")
|
|
95
|
+
commit_all_files(repository_root, "seed commit")
|
|
96
|
+
return repository_root
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_diff_mode_with_empty_file_set_exits_non_zero_and_says_so(
|
|
100
|
+
temporary_git_repository: Path,
|
|
101
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
102
|
+
capsys: pytest.CaptureFixture[str],
|
|
103
|
+
) -> None:
|
|
104
|
+
"""HEAD == base and nothing untracked: the gate refuses to vouch for nothing.
|
|
105
|
+
|
|
106
|
+
A diff that resolves zero candidate files means the gate inspected
|
|
107
|
+
nothing — a bad merge base or a wrong directory looks exactly like this.
|
|
108
|
+
Issue #62's contract: that run exits non-zero and says so, because a
|
|
109
|
+
silent pass over zero files is trusted like a real pass.
|
|
110
|
+
"""
|
|
111
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
112
|
+
|
|
113
|
+
exit_code = gate_module.main(["--base", "HEAD"])
|
|
114
|
+
|
|
115
|
+
captured = capsys.readouterr()
|
|
116
|
+
assert exit_code != 0
|
|
117
|
+
assert "inspected 0 file(s)" in captured.err
|
|
118
|
+
assert "empty" in captured.err.lower()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_diff_mode_changes_outside_only_under_prefixes_exit_clean_and_report(
|
|
122
|
+
temporary_git_repository: Path,
|
|
123
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
124
|
+
capsys: pytest.CaptureFixture[str],
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Changes exist but none under the given prefixes: a scoped no-op exits 0.
|
|
127
|
+
|
|
128
|
+
The candidate set is non-empty, so the gate's wiring is proven live; the
|
|
129
|
+
``--only-under`` scope legitimately excludes every candidate. That run
|
|
130
|
+
reports the zero count loudly and exits clean, so scoped gate loops keep
|
|
131
|
+
converging on branches whose changes sit outside the scoped tree.
|
|
132
|
+
"""
|
|
133
|
+
(temporary_git_repository / "notes.txt").write_text("note\n", encoding="utf-8")
|
|
134
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
135
|
+
|
|
136
|
+
exit_code = gate_module.main(["--base", "HEAD", "--only-under", "src"])
|
|
137
|
+
|
|
138
|
+
captured = capsys.readouterr()
|
|
139
|
+
assert exit_code == 0
|
|
140
|
+
assert "inspected 0 file(s)" in captured.err
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_diff_mode_inspects_untracked_module_and_reports_count(
|
|
144
|
+
temporary_git_repository: Path,
|
|
145
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
146
|
+
capsys: pytest.CaptureFixture[str],
|
|
147
|
+
) -> None:
|
|
148
|
+
"""A brand-new untracked module joins the file set and is validated."""
|
|
149
|
+
untracked_module = temporary_git_repository / "fresh_module.py"
|
|
150
|
+
untracked_module.write_text(
|
|
151
|
+
'"""Fresh module."""\n\n\ndef greet() -> str:\n return "hello"\n',
|
|
152
|
+
encoding="utf-8",
|
|
153
|
+
)
|
|
154
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
155
|
+
|
|
156
|
+
exit_code = gate_module.main(["--base", "HEAD"])
|
|
157
|
+
|
|
158
|
+
captured = capsys.readouterr()
|
|
159
|
+
assert "inspected 1 file(s)" in captured.err
|
|
160
|
+
assert exit_code == 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_diff_mode_blocks_violation_in_untracked_module(
|
|
164
|
+
temporary_git_repository: Path,
|
|
165
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
166
|
+
capsys: pytest.CaptureFixture[str],
|
|
167
|
+
) -> None:
|
|
168
|
+
"""An untracked module whose content the enforcer rejects fails the gate."""
|
|
169
|
+
untracked_module = temporary_git_repository / "fresh_violation.py"
|
|
170
|
+
untracked_module.write_text(
|
|
171
|
+
_control_violation_module_content(),
|
|
172
|
+
encoding="utf-8",
|
|
173
|
+
)
|
|
174
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
175
|
+
|
|
176
|
+
exit_code = gate_module.main(["--base", "HEAD"])
|
|
177
|
+
|
|
178
|
+
captured = capsys.readouterr()
|
|
179
|
+
assert exit_code != 0
|
|
180
|
+
assert "inspected 1 file(s)" in captured.err
|
|
181
|
+
assert "fresh_violation.py" in captured.err
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_explicit_paths_run_reports_inspected_count(
|
|
185
|
+
temporary_git_repository: Path,
|
|
186
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
187
|
+
capsys: pytest.CaptureFixture[str],
|
|
188
|
+
) -> None:
|
|
189
|
+
"""The count line appears on explicit-path runs too, so a caller always sees it."""
|
|
190
|
+
module_path = temporary_git_repository / "explicit_module.py"
|
|
191
|
+
module_path.write_text(
|
|
192
|
+
'"""Explicit module."""\n\n\ndef ping() -> str:\n return "pong"\n',
|
|
193
|
+
encoding="utf-8",
|
|
194
|
+
)
|
|
195
|
+
commit_all_files(temporary_git_repository, "add explicit module")
|
|
196
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
197
|
+
|
|
198
|
+
exit_code = gate_module.main(["explicit_module.py"])
|
|
199
|
+
|
|
200
|
+
captured = capsys.readouterr()
|
|
201
|
+
assert exit_code == 0
|
|
202
|
+
assert "inspected 1 file(s)" in captured.err
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_staged_mode_with_nothing_staged_reports_zero_inspected(
|
|
206
|
+
temporary_git_repository: Path,
|
|
207
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
208
|
+
capsys: pytest.CaptureFixture[str],
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Staged mode keeps its exit contract for an empty commit but names the count."""
|
|
211
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
212
|
+
|
|
213
|
+
exit_code = gate_module.main(["--staged"])
|
|
214
|
+
|
|
215
|
+
captured = capsys.readouterr()
|
|
216
|
+
assert exit_code == 0
|
|
217
|
+
assert "inspected 0 file(s)" in captured.err
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_untracked_paths_helper_lists_only_untracked_files(
|
|
221
|
+
temporary_git_repository: Path,
|
|
222
|
+
) -> None:
|
|
223
|
+
tracked_path = temporary_git_repository / "tracked.py"
|
|
224
|
+
tracked_path.write_text('"""Tracked."""\n', encoding="utf-8")
|
|
225
|
+
commit_all_files(temporary_git_repository, "add tracked module")
|
|
226
|
+
untracked_path = temporary_git_repository / "untracked.py"
|
|
227
|
+
untracked_path.write_text('"""Untracked."""\n', encoding="utf-8")
|
|
228
|
+
|
|
229
|
+
resolved_paths = gate_module.paths_from_git_untracked(temporary_git_repository)
|
|
230
|
+
|
|
231
|
+
resolved_names = {each_path.name for each_path in resolved_paths}
|
|
232
|
+
assert "untracked.py" in resolved_names
|
|
233
|
+
assert "tracked.py" not in resolved_names
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _gate_import_names_in_source(module_source: str) -> set[str]:
|
|
237
|
+
"""Collect the names a source file imports from ``code_rules_gate``.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
module_source: Python source text to scan for the gate import block.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
The imported-name set from the ``from code_rules_gate import (...)``
|
|
244
|
+
statement in the source, empty when no such block exists.
|
|
245
|
+
"""
|
|
246
|
+
_, _, after_open = module_source.partition("from code_rules_gate import (")
|
|
247
|
+
import_block, _, _ = after_open.partition(")")
|
|
248
|
+
return {
|
|
249
|
+
each_line.strip().rstrip(",")
|
|
250
|
+
for each_line in import_block.splitlines()
|
|
251
|
+
if each_line.strip() and not each_line.strip().startswith("#")
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_entry_exposes_every_symbol_the_preflight_hook_imports() -> None:
|
|
256
|
+
"""The gate entry re-exports the whole surface the spawn-preflight hook uses.
|
|
257
|
+
|
|
258
|
+
The code-verifier spawn-preflight hook imports gate helpers by name at
|
|
259
|
+
runtime; a name the entry drops breaks that hook's import and silently
|
|
260
|
+
turns its deny paths into allows.
|
|
261
|
+
"""
|
|
262
|
+
hook_path = (
|
|
263
|
+
Path(__file__).resolve().parents[3]
|
|
264
|
+
/ "hooks"
|
|
265
|
+
/ "blocking"
|
|
266
|
+
/ "code_verifier_spawn_preflight_gate.py"
|
|
267
|
+
)
|
|
268
|
+
hook_imported_names = _gate_import_names_in_source(
|
|
269
|
+
hook_path.read_text(encoding="utf-8")
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
assert hook_imported_names, "hook no longer imports from code_rules_gate"
|
|
273
|
+
missing_names = sorted(
|
|
274
|
+
each_name
|
|
275
|
+
for each_name in hook_imported_names
|
|
276
|
+
if not hasattr(gate_module, each_name)
|
|
277
|
+
)
|
|
278
|
+
assert not missing_names, f"gate entry drops hook imports: {missing_names}"
|
|
@@ -7,9 +7,7 @@ from types import ModuleType
|
|
|
7
7
|
|
|
8
8
|
def _load_constants_module() -> ModuleType:
|
|
9
9
|
module_path = (
|
|
10
|
-
Path(__file__).parent.parent
|
|
11
|
-
/ "pr_loop_shared_constants"
|
|
12
|
-
/ "code_rules_gate_constants.py"
|
|
10
|
+
Path(__file__).parent.parent / "pr_loop_shared_constants" / "code_rules_gate_constants.py"
|
|
13
11
|
)
|
|
14
12
|
specification = importlib.util.spec_from_file_location(
|
|
15
13
|
"pr_loop_shared_constants.code_rules_gate_constants", module_path
|
|
@@ -29,29 +27,14 @@ def test_max_violations_per_check_is_typed_integer() -> None:
|
|
|
29
27
|
assert constants_module.MAX_VIOLATIONS_PER_CHECK == 3
|
|
30
28
|
|
|
31
29
|
|
|
32
|
-
def test_expected_tuple_pair_length_is_typed_integer() -> None:
|
|
33
|
-
assert isinstance(constants_module.EXPECTED_TUPLE_PAIR_LENGTH, int)
|
|
34
|
-
assert constants_module.EXPECTED_TUPLE_PAIR_LENGTH == 2
|
|
35
|
-
|
|
36
|
-
|
|
37
30
|
def test_all_code_file_extensions_is_frozenset() -> None:
|
|
38
31
|
assert isinstance(constants_module.ALL_CODE_FILE_EXTENSIONS, frozenset)
|
|
39
|
-
assert
|
|
40
|
-
{".py", ".js", ".ts", ".tsx", ".jsx"}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def test_all_literal_keyword_exemptions_is_frozenset() -> None:
|
|
45
|
-
assert isinstance(constants_module.ALL_LITERAL_KEYWORD_EXEMPTIONS, frozenset)
|
|
46
|
-
assert constants_module.ALL_LITERAL_KEYWORD_EXEMPTIONS == frozenset(
|
|
47
|
-
{"true", "false", "none", "null"}
|
|
32
|
+
assert (
|
|
33
|
+
frozenset({".py", ".js", ".ts", ".tsx", ".jsx"})
|
|
34
|
+
== constants_module.ALL_CODE_FILE_EXTENSIONS
|
|
48
35
|
)
|
|
49
36
|
|
|
50
37
|
|
|
51
|
-
def test_config_path_segment() -> None:
|
|
52
|
-
assert constants_module.CONFIG_PATH_SEGMENT == "/config/"
|
|
53
|
-
|
|
54
|
-
|
|
55
38
|
def test_tests_path_segment() -> None:
|
|
56
39
|
assert constants_module.TESTS_PATH_SEGMENT == "/tests/"
|
|
57
40
|
|
|
@@ -73,10 +56,6 @@ def test_test_filename_prefix() -> None:
|
|
|
73
56
|
assert constants_module.TEST_FILENAME_PREFIX == "test_"
|
|
74
57
|
|
|
75
58
|
|
|
76
|
-
def test_minimum_column_name_length_after_first_char() -> None:
|
|
77
|
-
assert constants_module.MINIMUM_COLUMN_NAME_LENGTH_AFTER_FIRST_CHAR == 2
|
|
78
|
-
|
|
79
|
-
|
|
80
59
|
def test_git_name_status_added_prefix() -> None:
|
|
81
60
|
assert constants_module.GIT_NAME_STATUS_ADDED_PREFIX == "A"
|
|
82
61
|
|
|
@@ -89,17 +68,8 @@ def test_expected_rename_column_count() -> None:
|
|
|
89
68
|
assert constants_module.EXPECTED_RENAME_COLUMN_COUNT == 3
|
|
90
69
|
|
|
91
70
|
|
|
92
|
-
def test_column_key_pattern_template_renders_with_minimum_length() -> None:
|
|
93
|
-
rendered_pattern = constants_module.COLUMN_KEY_PATTERN_TEMPLATE.format(
|
|
94
|
-
minimum_length=constants_module.MINIMUM_COLUMN_NAME_LENGTH_AFTER_FIRST_CHAR
|
|
95
|
-
)
|
|
96
|
-
assert rendered_pattern == r"^[a-z][a-z0-9_]{2,}$"
|
|
97
|
-
|
|
98
|
-
|
|
99
71
|
def test_git_diff_name_only_null_terminated_command_prefix_includes_dash_z() -> None:
|
|
100
|
-
command_prefix =
|
|
101
|
-
constants_module.ALL_GIT_DIFF_NAME_ONLY_NULL_TERMINATED_COMMAND_PREFIX
|
|
102
|
-
)
|
|
72
|
+
command_prefix = constants_module.ALL_GIT_DIFF_NAME_ONLY_NULL_TERMINATED_COMMAND_PREFIX
|
|
103
73
|
assert command_prefix == ("git", "diff", "--name-only", "-z")
|
|
104
74
|
|
|
105
75
|
|
|
@@ -110,10 +80,7 @@ def test_banned_noun_span_pattern_extracts_definition_line_and_span() -> None:
|
|
|
110
80
|
)
|
|
111
81
|
match = constants_module.BANNED_NOUN_VIOLATION_PATTERN.search(message)
|
|
112
82
|
assert match is not None
|
|
113
|
-
definition_line = int(
|
|
114
|
-
match.group(constants_module.BANNED_NOUN_DEFINITION_LINE_GROUP_INDEX)
|
|
115
|
-
)
|
|
83
|
+
definition_line = int(match.group(constants_module.BANNED_NOUN_DEFINITION_LINE_GROUP_INDEX))
|
|
116
84
|
line_span = int(match.group(constants_module.BANNED_NOUN_SPAN_GROUP_INDEX))
|
|
117
85
|
assert definition_line == 1
|
|
118
86
|
assert line_span == 3
|
|
119
|
-
|
|
@@ -143,6 +143,35 @@ def test_is_copilot_disabled_via_env_true_when_listed_among_other_tokens(
|
|
|
143
143
|
assert reviews_disabled.is_bugbot_disabled_via_env() is True
|
|
144
144
|
|
|
145
145
|
|
|
146
|
+
def test_is_codex_disabled_via_env_returns_true_when_env_lists_codex(
|
|
147
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
148
|
+
) -> None:
|
|
149
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
|
|
150
|
+
assert reviews_disabled.is_codex_disabled_via_env() is True
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_is_codex_disabled_via_env_returns_false_when_env_is_empty(
|
|
154
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
155
|
+
) -> None:
|
|
156
|
+
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
157
|
+
assert reviews_disabled.is_codex_disabled_via_env() is False
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_is_codex_disabled_via_env_returns_false_when_only_bugbot_listed(
|
|
161
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
162
|
+
) -> None:
|
|
163
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
|
|
164
|
+
assert reviews_disabled.is_codex_disabled_via_env() is False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_is_codex_disabled_via_env_true_when_listed_among_other_tokens(
|
|
168
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
169
|
+
) -> None:
|
|
170
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", " BugBot , CoDex ")
|
|
171
|
+
assert reviews_disabled.is_codex_disabled_via_env() is True
|
|
172
|
+
assert reviews_disabled.is_bugbot_disabled_via_env() is True
|
|
173
|
+
|
|
174
|
+
|
|
146
175
|
def test_cli_main_returns_zero_when_named_reviewer_disabled(
|
|
147
176
|
monkeypatch: pytest.MonkeyPatch,
|
|
148
177
|
) -> None:
|
|
@@ -185,3 +214,17 @@ def test_cli_main_returns_one_for_copilot_when_only_bugbot_disabled(
|
|
|
185
214
|
) -> None:
|
|
186
215
|
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
|
|
187
216
|
assert reviews_disabled.main(["--reviewer", "copilot"]) == 1
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_cli_main_supports_codex_reviewer(
|
|
220
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
221
|
+
) -> None:
|
|
222
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
|
|
223
|
+
assert reviews_disabled.main(["--reviewer", "codex"]) == 0
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_cli_main_returns_one_for_codex_when_only_bugbot_disabled(
|
|
227
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
228
|
+
) -> None:
|
|
229
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
|
|
230
|
+
assert reviews_disabled.main(["--reviewer", "codex"]) == 1
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Worker-spawn protocol
|
|
2
|
+
|
|
3
|
+
Single home for the worker-spawn dispatcher contract and for Claude-only
|
|
4
|
+
slash-step host routing. Skills that need a role-bound headless worker call
|
|
5
|
+
[`scripts/resolve_worker_spawn.py`](../../scripts/resolve_worker_spawn.py)
|
|
6
|
+
(or import `resolve_worker_spawn`) and read this page for the tier walk, inputs,
|
|
7
|
+
JSON result shape, host rules, and setup. Skills that run a Claude-only slash
|
|
8
|
+
step (for example `/code-review`) read the code-review host-routing section.
|
|
9
|
+
|
|
10
|
+
Composed helpers (preflight, headless runners, and the code-review invoker)
|
|
11
|
+
stay documented at their own modules; this page states how callers use them.
|
|
12
|
+
|
|
13
|
+
## Three tiers
|
|
14
|
+
|
|
15
|
+
Walk order is fixed: soft-gate preflight for tier 1, then tier 1, then host
|
|
16
|
+
branching for tiers 2 and 3.
|
|
17
|
+
|
|
18
|
+
| Tier | Name | Who runs it |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| 1 | Grok headless | Python helper: `grok_worker_preflight` then `grok_headless_runner.run_headless_worker` |
|
|
21
|
+
| 2 | `claude_agent_required` | **Not** the Python helper. A stop signal so the **calling skill / harness** runs the Claude Code Agent tool itself |
|
|
22
|
+
| 3 | Claude headless | Python helper: `claude_chain_runner.run_claude` with `-p --output-format json --agent <stem>`, prompt body on stdin from the prompt file, and subprocess `cwd` set to the caller's working directory |
|
|
23
|
+
|
|
24
|
+
### `claude_agent_required` boundary
|
|
25
|
+
|
|
26
|
+
A Python helper cannot spawn Claude Code Agent-tool subagents. On a **Claude**
|
|
27
|
+
host, when the claude headless tier is **not** enabled (`--enable-claude-tier`
|
|
28
|
+
absent / `is_claude_tier_enabled=False`), a grok fallthrough records tier 2 with
|
|
29
|
+
reason `claude_agent_required`, leaves `tier_used` null, and returns. The
|
|
30
|
+
calling skill must then run the Agent tool in-process.
|
|
31
|
+
|
|
32
|
+
On a **ThirdParty** host, or when the claude tier is enabled on a Claude host,
|
|
33
|
+
the dispatcher continues to tier 3.
|
|
34
|
+
|
|
35
|
+
## Tier-1 fleets via grok-spawn
|
|
36
|
+
|
|
37
|
+
For a fleet of tier-1 grok workers (batch spec in, reports out), use the
|
|
38
|
+
**grok-spawn** skill when that skill is installed on the host (it ships on a
|
|
39
|
+
sibling stack and is not part of this dispatcher page). When present, that
|
|
40
|
+
skill owns the batch playbook and flag profiles under its reference docs.
|
|
41
|
+
This page stays the single-worker dispatcher contract (`resolve_worker_spawn`).
|
|
42
|
+
|
|
43
|
+
The calling session owns verification and every commit, push, or GitHub post.
|
|
44
|
+
Workers never commit or post.
|
|
45
|
+
|
|
46
|
+
## Required inputs
|
|
47
|
+
|
|
48
|
+
### CLI
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
python resolve_worker_spawn.py \
|
|
52
|
+
--role <role> \
|
|
53
|
+
--prompt-file <path> \
|
|
54
|
+
--cwd <dir> \
|
|
55
|
+
--run-temp-dir <dir> \
|
|
56
|
+
[--timeout-seconds N] \
|
|
57
|
+
[--enable-claude-tier]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| Flag | Required | Meaning |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `--role` | no (default `bugteam`) | Role name for preflight agent-set checks; mapped to the primary agent stem for headless `--agent` (for example `bugteam` → `code-quality-agent`, `clean-coder` → `clean-coder`; unknown roles fall back to the raw role string) |
|
|
63
|
+
| `--prompt-file` | yes | Prompt file path for headless workers |
|
|
64
|
+
| `--cwd` | yes | Working directory for headless grok and claude workers |
|
|
65
|
+
| `--run-temp-dir` | yes | Run-scoped directory for leader sockets and preflight cache (created if missing) |
|
|
66
|
+
| `--timeout-seconds` | no (default `600`) | Per-tier timeout in seconds |
|
|
67
|
+
| `--enable-claude-tier` | no | Allow tier 3 on a Claude host |
|
|
68
|
+
|
|
69
|
+
There is no separate outcome-file flag. The result surface is **stdout JSON**
|
|
70
|
+
plus the process exit code.
|
|
71
|
+
|
|
72
|
+
### Import API
|
|
73
|
+
|
|
74
|
+
`resolve_worker_spawn(...)` takes keyword-only arguments:
|
|
75
|
+
`role`, `prompt_file`, `working_directory`, `timeout_seconds`,
|
|
76
|
+
`is_claude_tier_enabled`, `run_state_directory`, `max_turns`.
|
|
77
|
+
`encode_spawn_outcome` turns a `SpawnOutcome` into the same JSON shape the CLI
|
|
78
|
+
prints.
|
|
79
|
+
|
|
80
|
+
## JSON result shape
|
|
81
|
+
|
|
82
|
+
Stdout is one JSON object:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"tier_used": 1,
|
|
87
|
+
"ok": true,
|
|
88
|
+
"attempts": [
|
|
89
|
+
{"tier": 1, "ok": true, "reason": null}
|
|
90
|
+
],
|
|
91
|
+
"output": "<serving tier stdout>",
|
|
92
|
+
"returncode": 0
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| Key | Meaning |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `tier_used` | Serving tier number (`1` or `3`), or `null` when no tier served |
|
|
99
|
+
| `ok` | Whether the serving call completed successfully |
|
|
100
|
+
| `attempts` | Ordered list of tries: each `{tier, ok, reason}` |
|
|
101
|
+
| `output` | Captured stdout from the serving tier (empty when none) |
|
|
102
|
+
| `returncode` | Process return code from the last tier run (or config-error code) |
|
|
103
|
+
|
|
104
|
+
`attempts` is in tried order. A Claude-host stop records tier `2` with
|
|
105
|
+
`reason: "claude_agent_required"`. Preflight or grok fallthrough reasons use the
|
|
106
|
+
preflight/runner classification strings (for example `grok_binary_missing`,
|
|
107
|
+
`usage_limit`).
|
|
108
|
+
|
|
109
|
+
### Exit codes
|
|
110
|
+
|
|
111
|
+
| Code | Meaning |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `0` | A tier served (`tier_used` is not null) |
|
|
114
|
+
| `2` | Exhausted — no tier served |
|
|
115
|
+
| `3` | Config error: the `--prompt-file` path is not an existing file (`output` = `prompt_file_missing`) or claude chain configuration missing or invalid |
|
|
116
|
+
|
|
117
|
+
## Host detection
|
|
118
|
+
|
|
119
|
+
After a grok fallthrough the dispatcher calls `detect_host_profile()` from
|
|
120
|
+
`tier_model_ids` (advisor scripts). Detection order:
|
|
121
|
+
|
|
122
|
+
1. `ADVISOR_HOST_PROFILE` when set — must be `Claude` or `ThirdParty` (any letter case).
|
|
123
|
+
2. `THIRD_PARTY` when truthy — `1`, `true`, `yes`, or `on`.
|
|
124
|
+
3. Default: `Claude`.
|
|
125
|
+
|
|
126
|
+
Profiles are the real names `Claude` and `ThirdParty` (not a separate Grok host
|
|
127
|
+
profile). Grok is tier 1 on every host; host profile only chooses the post-grok
|
|
128
|
+
branch (Agent-tool handoff vs headless claude chain).
|
|
129
|
+
|
|
130
|
+
## Code-review host routing
|
|
131
|
+
|
|
132
|
+
Claude-only slash steps need a Claude Code harness. The built-in
|
|
133
|
+
`/code-review` command is the main case. The host-routing pair is:
|
|
134
|
+
|
|
135
|
+
- `detect_host_profile()` from `tier_model_ids` — same host detection as the
|
|
136
|
+
worker-spawn dispatcher above
|
|
137
|
+
- `invoke_code_review` — host-aware invoker for `/code-review` (lives next to
|
|
138
|
+
`resolve_worker_spawn` under the package scripts tree)
|
|
139
|
+
|
|
140
|
+
Mode decision:
|
|
141
|
+
|
|
142
|
+
| Host profile | Session model | Mode | Who runs `/code-review` |
|
|
143
|
+
|---|---|---|---|
|
|
144
|
+
| `Claude` | `opus` | `in_session` | The calling skill runs the slash command in-process |
|
|
145
|
+
| `Claude` | any other alias | `chain` | Headless spawn through `claude_chain_runner` |
|
|
146
|
+
| `ThirdParty` | any | `chain` | Headless spawn through `claude_chain_runner` |
|
|
147
|
+
|
|
148
|
+
The review always runs at effort **high** on model **opus**. Chain mode builds
|
|
149
|
+
a single-turn prompt `/code-review high --fix`, pins `--model opus`, sets
|
|
150
|
+
subprocess `cwd` to the PR working tree, and reads stdin from the empty stream
|
|
151
|
+
so the spawn does not wait for input.
|
|
152
|
+
|
|
153
|
+
After a chain run, a non-empty `git status --porcelain` means the review
|
|
154
|
+
applied fixes. The calling host commits and pushes those fixes per the fix
|
|
155
|
+
protocol. The chain process never commits or posts. A clean stamp requires
|
|
156
|
+
`returncode == 0` and a non-null `served_command` plus a clean tree —
|
|
157
|
+
`dirty_tree` false alone is not enough when the chain failed or never served.
|
|
158
|
+
|
|
159
|
+
GitHub transport (`pr-loop-cloud-transport`) is a separate axis from this
|
|
160
|
+
harness branch. That skill routes `gh` vs MCP; it does not change which host
|
|
161
|
+
can run Claude-only slash commands.
|
|
162
|
+
|
|
163
|
+
## Leader-socket rule
|
|
164
|
+
|
|
165
|
+
Every concurrent grok process gets its own `--leader-socket` path under the
|
|
166
|
+
caller-supplied run temp directory. The headless runner mints a unique
|
|
167
|
+
`grok-leader-<uuid>.sock` per invocation. Preflight auth and ping probes use
|
|
168
|
+
their own fixed socket filenames under the same directory so they never share a
|
|
169
|
+
socket with a live worker.
|
|
170
|
+
|
|
171
|
+
## Cloud setup
|
|
172
|
+
|
|
173
|
+
Unattended grok auth uses the `XAI_API_KEY` environment variable. Install the
|
|
174
|
+
CLI via the vendor curl installer or the `@xai-official/grok` package. Keep
|
|
175
|
+
agent definitions and hook config in parity with `npx claude-dev-env` so the
|
|
176
|
+
preflight role-agent check and local `~/.claude` layout match what the worker
|
|
177
|
+
expects.
|
|
178
|
+
|
|
179
|
+
Use placeholders for account-specific values; do not hardcode personal paths or
|
|
180
|
+
keys in skills or docs.
|
|
181
|
+
|
|
182
|
+
## GitHub transport
|
|
183
|
+
|
|
184
|
+
`pr-loop-cloud-transport` is a separate axis. This utility never posts reviews,
|
|
185
|
+
opens PRs, or substitutes `gh` for MCP. GitHub reachability and cloud session
|
|
186
|
+
routing stay outside the worker-spawn contract.
|