claude-dev-env 2.2.1 → 2.4.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/CLAUDE.md +3 -2
- package/_shared/advisor/advisor-protocol.md +2 -2
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -284
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +3 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -0
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +5 -0
- package/_shared/pr-loop/scripts/stale_worktree_rule_sweep.py +107 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +55 -73
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +9 -15
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +91 -1
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +88 -1
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -0
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -0
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +1 -0
- package/agents/code-verifier.md +4 -4
- package/agents/skill-writer-agent.md +84 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/docs/CLAUDE.md +1 -0
- package/docs/references/CLAUDE.md +1 -0
- package/docs/references/code-review-enforcement.md +97 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +8 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +194 -0
- package/hooks/blocking/code_review_push_gate.py +140 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
- package/hooks/blocking/config/verified_commit_constants.py +24 -9
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
- package/hooks/blocking/test_code_review_push_gate.py +189 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_code_verifier_tools_contract.py +28 -0
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
- package/hooks/blocking/test_verification_verdict_store.py +920 -810
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verification_verdict_store.py +118 -5
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +103 -0
- package/hooks/hooks.json +16 -1
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +211 -23
- package/hooks/validators/run_all_validators.py +21 -14
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_ruff_integration.py +68 -1
- package/hooks/validators/test_run_all_validators.py +25 -0
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +20 -43
- package/rules/durable-post-artifacts.md +7 -0
- package/scripts/CLAUDE.md +2 -1
- package/scripts/claude-chain.example.json +15 -3
- package/scripts/claude_chain_runner.py +130 -27
- package/scripts/claude_chain_usage.py +346 -0
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +8 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -3
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +57 -2
- package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +58 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/dev_env_scripts_constants/timing.py +1 -1
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +8 -1
- package/scripts/test_claude_chain_runner.py +412 -6
- package/scripts/test_claude_chain_usage.py +534 -0
- package/scripts/test_invoke_code_review.py +298 -4
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
- package/skills/autoconverge/workflow/converge.mjs +24 -8
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/orchestrator/SKILL.md +1 -20
- package/skills/orchestrator-refresh/SKILL.md +1 -1
- package/skills/pr-converge/SKILL.md +3 -3
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +3 -3
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/team-advisor/SKILL.md +5 -4
|
@@ -15,20 +15,19 @@ the spawn does not wait for interactive input. Result JSON on stdout only::
|
|
|
15
15
|
|
|
16
16
|
{"mode", "served_command", "returncode", "dirty_tree"}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
or config/host error leaves the tree clean but must not advance past
|
|
21
|
-
CODE_REVIEW — use ``is_code_review_clean_stamp_allowed``.
|
|
18
|
+
``--record-stamp`` forces chain mode, loops a capped number of review passes,
|
|
19
|
+
and mints a clean stamp only when a pass exits 0 with a stable surface hash.
|
|
22
20
|
|
|
23
21
|
Import ``invoke_code_review`` for the outcome object, or run as a CLI::
|
|
24
22
|
|
|
25
23
|
python invoke_code_review.py --cwd <dir> --session-model <alias>
|
|
26
|
-
[--timeout-seconds N]
|
|
24
|
+
[--timeout-seconds N] [--record-stamp] [effort]
|
|
27
25
|
"""
|
|
28
26
|
|
|
29
27
|
from __future__ import annotations
|
|
30
28
|
|
|
31
29
|
import argparse
|
|
30
|
+
import importlib.util
|
|
32
31
|
import json
|
|
33
32
|
import subprocess
|
|
34
33
|
import sys
|
|
@@ -36,6 +35,7 @@ import threading
|
|
|
36
35
|
from collections.abc import Callable, Sequence
|
|
37
36
|
from dataclasses import dataclass
|
|
38
37
|
from pathlib import Path
|
|
38
|
+
from types import ModuleType
|
|
39
39
|
|
|
40
40
|
if str(Path(__file__).resolve().parent) not in sys.path:
|
|
41
41
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
@@ -57,24 +57,47 @@ from claude_chain_runner import ( # noqa: E402
|
|
|
57
57
|
)
|
|
58
58
|
from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
59
59
|
CHAIN_CONFIG_ERROR_EXIT_CODE,
|
|
60
|
+
collect_forwarded_text_codec,
|
|
60
61
|
)
|
|
61
62
|
from dev_env_scripts_constants.code_review_constants import ( # noqa: E402
|
|
63
|
+
ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER,
|
|
64
|
+
CLI_EFFORT_HELP,
|
|
65
|
+
CLI_EFFORT_METAVAR,
|
|
66
|
+
CLI_RECORD_STAMP_HELP,
|
|
62
67
|
CLI_SESSION_MODEL_FLAG,
|
|
68
|
+
CODE_REVIEW_FIX_FLAG,
|
|
63
69
|
CODE_REVIEW_MODEL_ALIAS,
|
|
64
|
-
|
|
70
|
+
CODE_REVIEW_SLASH_COMMAND,
|
|
71
|
+
DEFAULT_CODE_REVIEW_EFFORT,
|
|
72
|
+
EFFORT_TOKEN_LIST_SEPARATOR,
|
|
65
73
|
GIT_BINARY,
|
|
66
74
|
GIT_PORCELAIN_FLAG,
|
|
67
75
|
GIT_STATUS_SUBCOMMAND,
|
|
68
76
|
HOST_PROFILE_ERROR_RETURNCODE,
|
|
69
77
|
IN_SESSION_RETURNCODE,
|
|
78
|
+
INVALID_EFFORT_MESSAGE,
|
|
79
|
+
INVALID_EFFORT_RETURNCODE,
|
|
80
|
+
MAXIMUM_STAMP_MINT_PASSES,
|
|
70
81
|
MODE_CHAIN,
|
|
71
82
|
MODE_IN_SESSION,
|
|
72
83
|
PERMISSION_MODE_BYPASS,
|
|
73
84
|
PERMISSION_MODE_FLAG,
|
|
85
|
+
RECORD_STAMP_FLAG,
|
|
86
|
+
RESULT_KEY_BOUND_HASH,
|
|
74
87
|
RESULT_KEY_DIRTY_TREE,
|
|
75
88
|
RESULT_KEY_MODE,
|
|
89
|
+
RESULT_KEY_PASS_COUNT,
|
|
76
90
|
RESULT_KEY_RETURNCODE,
|
|
77
91
|
RESULT_KEY_SERVED_COMMAND,
|
|
92
|
+
RESULT_KEY_STAMP_MINTED,
|
|
93
|
+
STAMP_DID_NOT_CONVERGE_MESSAGE,
|
|
94
|
+
STAMP_DID_NOT_CONVERGE_RETURNCODE,
|
|
95
|
+
STAMP_STORE_IMPORT_FAILURE_MESSAGE,
|
|
96
|
+
STAMP_STORE_LIVE_SURFACE_HASH_NAME,
|
|
97
|
+
STAMP_STORE_MODULE_FILE_NAME,
|
|
98
|
+
STAMP_STORE_MODULE_NAME,
|
|
99
|
+
STAMP_STORE_RECORD_CLEAN_STAMP_NAME,
|
|
100
|
+
STAMP_STORE_RESOLVE_REPO_ROOT_NAME,
|
|
78
101
|
SUCCESSFUL_REVIEW_RETURNCODE,
|
|
79
102
|
)
|
|
80
103
|
from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
@@ -115,6 +138,25 @@ class CodeReviewOutcome:
|
|
|
115
138
|
is_dirty_tree: bool
|
|
116
139
|
|
|
117
140
|
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class StampMintOutcome:
|
|
143
|
+
"""Outcome of a ``--record-stamp`` minting run.
|
|
144
|
+
|
|
145
|
+
::
|
|
146
|
+
|
|
147
|
+
stable rc 0 pass -> is_stamp_minted True, bound_hash set
|
|
148
|
+
cap hit / unstable -> is_stamp_minted False, non-zero returncode
|
|
149
|
+
|
|
150
|
+
The attributes carry the last review, whether a stamp was minted, the pass
|
|
151
|
+
count, and the surface hash the stamp was bound to when minted.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
review_outcome: CodeReviewOutcome
|
|
155
|
+
is_stamp_minted: bool
|
|
156
|
+
pass_count: int
|
|
157
|
+
bound_hash: str | None
|
|
158
|
+
|
|
159
|
+
|
|
118
160
|
review_claude_runner = run_claude
|
|
119
161
|
review_host_profile_detector = detect_host_profile
|
|
120
162
|
review_git_status_runner = subprocess.run
|
|
@@ -125,6 +167,65 @@ TextCapturingSubprocessRunner = Callable[
|
|
|
125
167
|
]
|
|
126
168
|
|
|
127
169
|
|
|
170
|
+
def _stamp_store_file_path() -> Path:
|
|
171
|
+
"""Return the stamp store module path in the sibling hooks/blocking tree."""
|
|
172
|
+
blocking_directory = Path(__file__).resolve().parent.parent / "hooks" / "blocking"
|
|
173
|
+
return blocking_directory / STAMP_STORE_MODULE_FILE_NAME
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _load_store_from_spec(store_file_path: Path) -> ModuleType:
|
|
177
|
+
"""Import the stamp store from *store_file_path*, re-raising a missing dep."""
|
|
178
|
+
blocking_directory_string = str(store_file_path.parent)
|
|
179
|
+
if blocking_directory_string not in sys.path:
|
|
180
|
+
sys.path.insert(0, blocking_directory_string)
|
|
181
|
+
module_spec = importlib.util.spec_from_file_location(
|
|
182
|
+
STAMP_STORE_MODULE_NAME, store_file_path
|
|
183
|
+
)
|
|
184
|
+
if module_spec is None or module_spec.loader is None:
|
|
185
|
+
raise ModuleNotFoundError(
|
|
186
|
+
f"could not create import spec for {store_file_path}",
|
|
187
|
+
name=STAMP_STORE_MODULE_NAME,
|
|
188
|
+
)
|
|
189
|
+
store_module = importlib.util.module_from_spec(module_spec)
|
|
190
|
+
sys.modules[STAMP_STORE_MODULE_NAME] = store_module
|
|
191
|
+
try:
|
|
192
|
+
module_spec.loader.exec_module(store_module)
|
|
193
|
+
except ModuleNotFoundError:
|
|
194
|
+
del sys.modules[STAMP_STORE_MODULE_NAME]
|
|
195
|
+
raise
|
|
196
|
+
return store_module
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def load_code_review_stamp_store() -> ModuleType:
|
|
200
|
+
"""Import the stamp store module from the installed or repo hooks tree.
|
|
201
|
+
|
|
202
|
+
::
|
|
203
|
+
|
|
204
|
+
code_review_stamp_store.py present -> module with record_clean_stamp
|
|
205
|
+
module file missing -> ModuleNotFoundError (loud)
|
|
206
|
+
|
|
207
|
+
A genuine missing dependency of the store still raises rather than being
|
|
208
|
+
swallowed, so ``--record-stamp`` fails loudly when it cannot mint.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
The loaded ``code_review_stamp_store`` module.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ModuleNotFoundError: When the store file is absent or a real import
|
|
215
|
+
dependency of the store is missing.
|
|
216
|
+
"""
|
|
217
|
+
store_file_path = _stamp_store_file_path()
|
|
218
|
+
if not store_file_path.is_file():
|
|
219
|
+
raise ModuleNotFoundError(
|
|
220
|
+
f"code review stamp store not found at {store_file_path}",
|
|
221
|
+
name=STAMP_STORE_MODULE_NAME,
|
|
222
|
+
)
|
|
223
|
+
cached_module = sys.modules.get(STAMP_STORE_MODULE_NAME)
|
|
224
|
+
if cached_module is not None:
|
|
225
|
+
return cached_module
|
|
226
|
+
return _load_store_from_spec(store_file_path)
|
|
227
|
+
|
|
228
|
+
|
|
128
229
|
def is_opus_session_model(session_model: str) -> bool:
|
|
129
230
|
"""Return True when *session_model* is the opus short alias (any letter case).
|
|
130
231
|
|
|
@@ -152,8 +253,6 @@ def decide_review_mode(*, host_profile: str, session_model: str) -> str:
|
|
|
152
253
|
# ok: "in_session"
|
|
153
254
|
decide_review_mode(host_profile="Claude", session_model="sonnet")
|
|
154
255
|
# ok: "chain"
|
|
155
|
-
decide_review_mode(host_profile="ThirdParty", session_model="opus")
|
|
156
|
-
# ok: "chain"
|
|
157
256
|
|
|
158
257
|
Args:
|
|
159
258
|
host_profile: Detected host profile (``Claude`` or ``ThirdParty``).
|
|
@@ -168,20 +267,42 @@ def decide_review_mode(*, host_profile: str, session_model: str) -> str:
|
|
|
168
267
|
return MODE_CHAIN
|
|
169
268
|
|
|
170
269
|
|
|
171
|
-
def
|
|
270
|
+
def build_code_review_prompt(effort: str) -> str:
|
|
271
|
+
"""Build the single-turn slash-command prompt for the given effort.
|
|
272
|
+
|
|
273
|
+
::
|
|
274
|
+
|
|
275
|
+
build_code_review_prompt("low") # ok: "/code-review low --fix"
|
|
276
|
+
build_code_review_prompt("xhigh") # ok: "/code-review xhigh --fix"
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
effort: A validated effort token from the ascending order tuple.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
The ``/code-review <effort> --fix`` prompt string.
|
|
283
|
+
"""
|
|
284
|
+
return f"{CODE_REVIEW_SLASH_COMMAND} {effort} {CODE_REVIEW_FIX_FLAG}"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def build_code_review_arguments(
|
|
288
|
+
effort: str = DEFAULT_CODE_REVIEW_EFFORT,
|
|
289
|
+
) -> list[str]:
|
|
172
290
|
"""Return the argv tokens passed to ``run_claude`` for a chain review.
|
|
173
291
|
|
|
174
292
|
::
|
|
175
293
|
|
|
176
|
-
build_code_review_arguments()
|
|
177
|
-
# ok: ["-p",
|
|
294
|
+
build_code_review_arguments("high")
|
|
295
|
+
# ok: ["-p", "/code-review high --fix", "--model", "opus", ...]
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
effort: Effort token embedded in the slash-command prompt.
|
|
178
299
|
|
|
179
300
|
Returns:
|
|
180
301
|
Ordered claude CLI arguments for the headless opus review slash command.
|
|
181
302
|
"""
|
|
182
303
|
return [
|
|
183
304
|
SINGLE_TURN_FLAG,
|
|
184
|
-
|
|
305
|
+
build_code_review_prompt(effort),
|
|
185
306
|
MODEL_FLAG,
|
|
186
307
|
CODE_REVIEW_MODEL_ALIAS,
|
|
187
308
|
OUTPUT_FORMAT_FLAG,
|
|
@@ -191,6 +312,29 @@ def build_code_review_arguments() -> list[str]:
|
|
|
191
312
|
]
|
|
192
313
|
|
|
193
314
|
|
|
315
|
+
def validate_effort_token(effort: str) -> str | None:
|
|
316
|
+
"""Return an error message when *effort* is unknown or unsupported.
|
|
317
|
+
|
|
318
|
+
::
|
|
319
|
+
|
|
320
|
+
validate_effort_token("low") # ok: None
|
|
321
|
+
validate_effort_token("ultra") # flag: error mentioning ultra
|
|
322
|
+
validate_effort_token("bogus") # flag: error listing allowed tokens
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
effort: Caller-supplied effort token.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
None when the token is allowed; otherwise a human-readable error.
|
|
329
|
+
"""
|
|
330
|
+
if effort in ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER:
|
|
331
|
+
return None
|
|
332
|
+
allowed_tokens = EFFORT_TOKEN_LIST_SEPARATOR.join(
|
|
333
|
+
ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER
|
|
334
|
+
)
|
|
335
|
+
return INVALID_EFFORT_MESSAGE.format(effort=effort, allowed=allowed_tokens)
|
|
336
|
+
|
|
337
|
+
|
|
194
338
|
def is_working_tree_dirty(working_directory: Path) -> bool:
|
|
195
339
|
"""Return True when the tree is dirty or ``git status`` fails.
|
|
196
340
|
|
|
@@ -282,7 +426,9 @@ def _run_claude_with_empty_stdin(
|
|
|
282
426
|
) -> ChainInvocationOutcome:
|
|
283
427
|
working_directory_path = str(working_directory)
|
|
284
428
|
with _CHAIN_RUNNER_LOCK:
|
|
285
|
-
previous_runner =
|
|
429
|
+
previous_runner: TextCapturingSubprocessRunner = (
|
|
430
|
+
chain_runner.chain_subprocess_runner
|
|
431
|
+
)
|
|
286
432
|
|
|
287
433
|
def _runner_with_empty_stdin(
|
|
288
434
|
all_invocation_tokens: Sequence[str],
|
|
@@ -296,6 +442,7 @@ def _run_claude_with_empty_stdin(
|
|
|
296
442
|
timeout_for_run = float(maybe_timeout)
|
|
297
443
|
else:
|
|
298
444
|
timeout_for_run = None
|
|
445
|
+
forwarded_text_codec = collect_forwarded_text_codec(all_keywords)
|
|
299
446
|
completed_process: subprocess.CompletedProcess[str] = previous_runner(
|
|
300
447
|
all_invocation_tokens,
|
|
301
448
|
capture_output=True,
|
|
@@ -304,6 +451,7 @@ def _run_claude_with_empty_stdin(
|
|
|
304
451
|
check=False,
|
|
305
452
|
stdin=subprocess.DEVNULL,
|
|
306
453
|
cwd=working_directory_path,
|
|
454
|
+
**forwarded_text_codec,
|
|
307
455
|
)
|
|
308
456
|
return completed_process
|
|
309
457
|
|
|
@@ -347,24 +495,55 @@ def _failure_code_review_outcome(returncode: int) -> CodeReviewOutcome:
|
|
|
347
495
|
is_dirty_tree=False,
|
|
348
496
|
)
|
|
349
497
|
|
|
498
|
+
def _run_chain_review(
|
|
499
|
+
*,
|
|
500
|
+
working_directory: Path,
|
|
501
|
+
timeout_seconds: int,
|
|
502
|
+
effort: str,
|
|
503
|
+
) -> CodeReviewOutcome:
|
|
504
|
+
all_claude_arguments = build_code_review_arguments(effort)
|
|
505
|
+
chain_outcome = _run_claude_with_empty_stdin(
|
|
506
|
+
all_claude_arguments,
|
|
507
|
+
timeout_seconds=timeout_seconds,
|
|
508
|
+
working_directory=working_directory,
|
|
509
|
+
)
|
|
510
|
+
return _chain_outcome(chain_outcome, working_directory=working_directory)
|
|
511
|
+
|
|
512
|
+
|
|
350
513
|
def invoke_code_review(
|
|
351
514
|
*,
|
|
352
515
|
working_directory: Path,
|
|
353
516
|
session_model: str,
|
|
354
517
|
timeout_seconds: int,
|
|
518
|
+
effort: str = DEFAULT_CODE_REVIEW_EFFORT,
|
|
519
|
+
is_force_chain: bool = False,
|
|
355
520
|
) -> CodeReviewOutcome:
|
|
356
521
|
"""Run or hand off ``/code-review`` based on host profile and session model.
|
|
357
522
|
|
|
523
|
+
::
|
|
524
|
+
|
|
525
|
+
Claude + opus, is_force_chain False -> in_session (no spawn)
|
|
526
|
+
Claude + opus, is_force_chain True -> chain (headless spawn)
|
|
527
|
+
any host with sonnet -> chain
|
|
528
|
+
|
|
358
529
|
Args:
|
|
359
530
|
working_directory: PR working tree used as cwd for the chain spawn.
|
|
360
|
-
session_model: Caller-stated session model short alias
|
|
361
|
-
``opus`` or ``sonnet``).
|
|
531
|
+
session_model: Caller-stated session model short alias.
|
|
362
532
|
timeout_seconds: Timeout applied to each chain binary invocation.
|
|
533
|
+
effort: Effort token embedded in the ``/code-review`` prompt.
|
|
534
|
+
is_force_chain: When True, always spawn chain mode (used by
|
|
535
|
+
``--record-stamp`` so the invoker observes the review).
|
|
363
536
|
|
|
364
537
|
Returns:
|
|
365
538
|
Structured outcome including mode, served binary, return code, and
|
|
366
539
|
whether the working tree is dirty after a chain run.
|
|
367
540
|
"""
|
|
541
|
+
if is_force_chain:
|
|
542
|
+
return _run_chain_review(
|
|
543
|
+
working_directory=working_directory,
|
|
544
|
+
timeout_seconds=timeout_seconds,
|
|
545
|
+
effort=effort,
|
|
546
|
+
)
|
|
368
547
|
host_profile = review_host_profile_detector()
|
|
369
548
|
review_mode = decide_review_mode(
|
|
370
549
|
host_profile=host_profile,
|
|
@@ -372,13 +551,193 @@ def invoke_code_review(
|
|
|
372
551
|
)
|
|
373
552
|
if review_mode == MODE_IN_SESSION:
|
|
374
553
|
return _in_session_outcome()
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
all_claude_arguments,
|
|
554
|
+
return _run_chain_review(
|
|
555
|
+
working_directory=working_directory,
|
|
378
556
|
timeout_seconds=timeout_seconds,
|
|
557
|
+
effort=effort,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _surface_hash_before_and_after_are_stable(
|
|
562
|
+
before_hash: str | None,
|
|
563
|
+
after_hash: str | None,
|
|
564
|
+
) -> bool:
|
|
565
|
+
if before_hash is None:
|
|
566
|
+
return False
|
|
567
|
+
if after_hash is None:
|
|
568
|
+
return False
|
|
569
|
+
return before_hash == after_hash
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _mint_stamp_for_stable_pass(
|
|
573
|
+
*,
|
|
574
|
+
store_module: ModuleType,
|
|
575
|
+
working_directory: Path,
|
|
576
|
+
surface_hash: str,
|
|
577
|
+
effort: str,
|
|
578
|
+
) -> bool:
|
|
579
|
+
resolve_repo_root = getattr(store_module, STAMP_STORE_RESOLVE_REPO_ROOT_NAME)
|
|
580
|
+
record_clean_stamp = getattr(store_module, STAMP_STORE_RECORD_CLEAN_STAMP_NAME)
|
|
581
|
+
repo_root = resolve_repo_root(str(working_directory))
|
|
582
|
+
if repo_root is None:
|
|
583
|
+
return False
|
|
584
|
+
record_clean_stamp(repo_root, surface_hash, effort)
|
|
585
|
+
return True
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _mint_outcome_when_stable_clean(
|
|
589
|
+
*,
|
|
590
|
+
store_module: ModuleType,
|
|
591
|
+
working_directory: Path,
|
|
592
|
+
effort: str,
|
|
593
|
+
pass_number: int,
|
|
594
|
+
before_hash: str,
|
|
595
|
+
review_outcome: CodeReviewOutcome,
|
|
596
|
+
) -> StampMintOutcome | None:
|
|
597
|
+
is_minted = _mint_stamp_for_stable_pass(
|
|
598
|
+
store_module=store_module,
|
|
379
599
|
working_directory=working_directory,
|
|
600
|
+
surface_hash=before_hash,
|
|
601
|
+
effort=effort,
|
|
602
|
+
)
|
|
603
|
+
if not is_minted:
|
|
604
|
+
return None
|
|
605
|
+
return StampMintOutcome(
|
|
606
|
+
review_outcome=review_outcome,
|
|
607
|
+
is_stamp_minted=True,
|
|
608
|
+
pass_count=pass_number,
|
|
609
|
+
bound_hash=before_hash,
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _unminted_pass_outcome(
|
|
614
|
+
review_outcome: CodeReviewOutcome, pass_number: int
|
|
615
|
+
) -> StampMintOutcome:
|
|
616
|
+
return StampMintOutcome(
|
|
617
|
+
review_outcome=review_outcome,
|
|
618
|
+
is_stamp_minted=False,
|
|
619
|
+
pass_count=pass_number,
|
|
620
|
+
bound_hash=None,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _stamp_outcome_for_pass(
|
|
625
|
+
*,
|
|
626
|
+
store_module: ModuleType,
|
|
627
|
+
working_directory: Path,
|
|
628
|
+
effort: str,
|
|
629
|
+
pass_number: int,
|
|
630
|
+
before_hash: str | None,
|
|
631
|
+
after_hash: str | None,
|
|
632
|
+
review_outcome: CodeReviewOutcome,
|
|
633
|
+
) -> StampMintOutcome | None:
|
|
634
|
+
is_stable = _surface_hash_before_and_after_are_stable(before_hash, after_hash)
|
|
635
|
+
is_successful = is_successful_code_review(review_outcome)
|
|
636
|
+
is_empty_surface = before_hash is None and after_hash is None
|
|
637
|
+
if is_successful and is_empty_surface:
|
|
638
|
+
return _unminted_pass_outcome(review_outcome, pass_number)
|
|
639
|
+
if is_successful and is_stable and before_hash is not None:
|
|
640
|
+
minted = _mint_outcome_when_stable_clean(
|
|
641
|
+
store_module=store_module,
|
|
642
|
+
working_directory=working_directory,
|
|
643
|
+
effort=effort,
|
|
644
|
+
pass_number=pass_number,
|
|
645
|
+
before_hash=before_hash,
|
|
646
|
+
review_outcome=review_outcome,
|
|
647
|
+
)
|
|
648
|
+
if minted is not None:
|
|
649
|
+
return minted
|
|
650
|
+
if is_stable:
|
|
651
|
+
return _unminted_pass_outcome(review_outcome, pass_number)
|
|
652
|
+
return None
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _run_one_stamp_mint_pass(
|
|
656
|
+
*,
|
|
657
|
+
store_module: ModuleType,
|
|
658
|
+
live_surface_hash: Callable[..., str | None],
|
|
659
|
+
working_directory: Path,
|
|
660
|
+
timeout_seconds: int,
|
|
661
|
+
effort: str,
|
|
662
|
+
pass_number: int,
|
|
663
|
+
) -> tuple[CodeReviewOutcome, StampMintOutcome | None]:
|
|
664
|
+
before_hash = live_surface_hash(str(working_directory))
|
|
665
|
+
review_outcome = invoke_code_review(
|
|
666
|
+
working_directory=working_directory,
|
|
667
|
+
session_model=CODE_REVIEW_MODEL_ALIAS,
|
|
668
|
+
timeout_seconds=timeout_seconds,
|
|
669
|
+
effort=effort,
|
|
670
|
+
is_force_chain=True,
|
|
671
|
+
)
|
|
672
|
+
after_hash = live_surface_hash(str(working_directory))
|
|
673
|
+
maybe_outcome = _stamp_outcome_for_pass(
|
|
674
|
+
store_module=store_module,
|
|
675
|
+
working_directory=working_directory,
|
|
676
|
+
effort=effort,
|
|
677
|
+
pass_number=pass_number,
|
|
678
|
+
before_hash=before_hash,
|
|
679
|
+
after_hash=after_hash,
|
|
680
|
+
review_outcome=review_outcome,
|
|
681
|
+
)
|
|
682
|
+
return review_outcome, maybe_outcome
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _iterate_stamp_mint_passes(
|
|
686
|
+
*,
|
|
687
|
+
store_module: ModuleType,
|
|
688
|
+
live_surface_hash: Callable[..., str | None],
|
|
689
|
+
working_directory: Path,
|
|
690
|
+
timeout_seconds: int,
|
|
691
|
+
effort: str,
|
|
692
|
+
maximum_passes: int,
|
|
693
|
+
) -> StampMintOutcome:
|
|
694
|
+
last_review_outcome = _failure_code_review_outcome(
|
|
695
|
+
STAMP_DID_NOT_CONVERGE_RETURNCODE
|
|
696
|
+
)
|
|
697
|
+
for each_pass_number in range(1, maximum_passes + 1):
|
|
698
|
+
last_review_outcome, maybe_mint_outcome = _run_one_stamp_mint_pass(
|
|
699
|
+
store_module=store_module,
|
|
700
|
+
live_surface_hash=live_surface_hash,
|
|
701
|
+
working_directory=working_directory,
|
|
702
|
+
timeout_seconds=timeout_seconds,
|
|
703
|
+
effort=effort,
|
|
704
|
+
pass_number=each_pass_number,
|
|
705
|
+
)
|
|
706
|
+
if maybe_mint_outcome is not None:
|
|
707
|
+
return maybe_mint_outcome
|
|
708
|
+
return _unminted_pass_outcome(last_review_outcome, maximum_passes)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def invoke_code_review_and_record_stamp(
|
|
712
|
+
*,
|
|
713
|
+
working_directory: Path,
|
|
714
|
+
session_model: str,
|
|
715
|
+
timeout_seconds: int,
|
|
716
|
+
effort: str,
|
|
717
|
+
maximum_passes: int = MAXIMUM_STAMP_MINT_PASSES,
|
|
718
|
+
) -> StampMintOutcome:
|
|
719
|
+
"""Force chain review passes until the surface is stable, then mint a stamp.
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
working_directory: PR working tree used as cwd for each chain spawn.
|
|
723
|
+
session_model: Session model short alias (unused; chain mode forced).
|
|
724
|
+
timeout_seconds: Timeout applied to each chain binary invocation.
|
|
725
|
+
effort: Effort token the clean review records on the stamp.
|
|
726
|
+
maximum_passes: Hard cap on review passes before giving up.
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
A StampMintOutcome for the last review and whether a stamp was minted.
|
|
730
|
+
"""
|
|
731
|
+
del session_model
|
|
732
|
+
store_module = load_code_review_stamp_store()
|
|
733
|
+
return _iterate_stamp_mint_passes(
|
|
734
|
+
store_module=store_module,
|
|
735
|
+
live_surface_hash=getattr(store_module, STAMP_STORE_LIVE_SURFACE_HASH_NAME),
|
|
736
|
+
working_directory=working_directory,
|
|
737
|
+
timeout_seconds=timeout_seconds,
|
|
738
|
+
effort=effort,
|
|
739
|
+
maximum_passes=maximum_passes,
|
|
380
740
|
)
|
|
381
|
-
return _chain_outcome(chain_outcome, working_directory=working_directory)
|
|
382
741
|
|
|
383
742
|
|
|
384
743
|
def encode_code_review_outcome(
|
|
@@ -400,12 +759,28 @@ def encode_code_review_outcome(
|
|
|
400
759
|
}
|
|
401
760
|
|
|
402
761
|
|
|
403
|
-
def
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
762
|
+
def encode_stamp_mint_outcome(
|
|
763
|
+
mint_outcome: StampMintOutcome,
|
|
764
|
+
) -> dict[str, object]:
|
|
765
|
+
"""Encode a stamp-mint outcome as the JSON-serializable payload.
|
|
766
|
+
|
|
767
|
+
Args:
|
|
768
|
+
mint_outcome: The ``--record-stamp`` outcome to encode.
|
|
769
|
+
|
|
770
|
+
Returns:
|
|
771
|
+
A plain dict with the review fields plus mint metadata.
|
|
772
|
+
"""
|
|
773
|
+
encoded_payload = encode_code_review_outcome(mint_outcome.review_outcome)
|
|
774
|
+
encoded_payload[RESULT_KEY_STAMP_MINTED] = mint_outcome.is_stamp_minted
|
|
775
|
+
encoded_payload[RESULT_KEY_PASS_COUNT] = mint_outcome.pass_count
|
|
776
|
+
encoded_payload[RESULT_KEY_BOUND_HASH] = mint_outcome.bound_hash
|
|
777
|
+
if not mint_outcome.is_stamp_minted:
|
|
778
|
+
if mint_outcome.pass_count >= MAXIMUM_STAMP_MINT_PASSES:
|
|
779
|
+
encoded_payload[RESULT_KEY_RETURNCODE] = STAMP_DID_NOT_CONVERGE_RETURNCODE
|
|
780
|
+
return encoded_payload
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _add_review_arguments(parser: argparse.ArgumentParser) -> None:
|
|
409
784
|
parser.add_argument(
|
|
410
785
|
CWD_FLAG,
|
|
411
786
|
dest="working_directory",
|
|
@@ -426,37 +801,174 @@ def _build_argument_parser() -> argparse.ArgumentParser:
|
|
|
426
801
|
default=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
427
802
|
help="Timeout in seconds applied to each chain binary invocation.",
|
|
428
803
|
)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _add_stamp_and_effort_arguments(parser: argparse.ArgumentParser) -> None:
|
|
807
|
+
parser.add_argument(
|
|
808
|
+
RECORD_STAMP_FLAG,
|
|
809
|
+
dest="is_record_stamp",
|
|
810
|
+
action="store_true",
|
|
811
|
+
help=CLI_RECORD_STAMP_HELP,
|
|
812
|
+
)
|
|
813
|
+
parser.add_argument(
|
|
814
|
+
CLI_EFFORT_METAVAR,
|
|
815
|
+
nargs="?",
|
|
816
|
+
default=DEFAULT_CODE_REVIEW_EFFORT,
|
|
817
|
+
help=CLI_EFFORT_HELP,
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _build_argument_parser() -> argparse.ArgumentParser:
|
|
822
|
+
parser = argparse.ArgumentParser(
|
|
823
|
+
description=(
|
|
824
|
+
"Run built-in /code-review at a chosen effort on opus, host-aware."
|
|
825
|
+
)
|
|
826
|
+
)
|
|
827
|
+
_add_review_arguments(parser)
|
|
828
|
+
_add_stamp_and_effort_arguments(parser)
|
|
429
829
|
return parser
|
|
430
830
|
|
|
431
831
|
|
|
832
|
+
def _emit_invalid_effort_and_exit_code(effort: str) -> int:
|
|
833
|
+
error_message = validate_effort_token(effort)
|
|
834
|
+
if error_message is None:
|
|
835
|
+
return SUCCESSFUL_REVIEW_RETURNCODE
|
|
836
|
+
sys.stderr.write(error_message + "\n")
|
|
837
|
+
return INVALID_EFFORT_RETURNCODE
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _import_failure_payload() -> dict[str, object]:
|
|
841
|
+
failure_outcome = _failure_code_review_outcome(INVALID_EFFORT_RETURNCODE)
|
|
842
|
+
encoded_payload = encode_code_review_outcome(failure_outcome)
|
|
843
|
+
encoded_payload[RESULT_KEY_STAMP_MINTED] = False
|
|
844
|
+
encoded_payload[RESULT_KEY_PASS_COUNT] = 0
|
|
845
|
+
encoded_payload[RESULT_KEY_BOUND_HASH] = None
|
|
846
|
+
return encoded_payload
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _no_mint_outcome(returncode: int) -> StampMintOutcome:
|
|
850
|
+
return StampMintOutcome(
|
|
851
|
+
review_outcome=_failure_code_review_outcome(returncode),
|
|
852
|
+
is_stamp_minted=False,
|
|
853
|
+
pass_count=0,
|
|
854
|
+
bound_hash=None,
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _mint_or_config_outcome(
|
|
859
|
+
*,
|
|
860
|
+
working_directory: Path,
|
|
861
|
+
session_model: str,
|
|
862
|
+
timeout_seconds: int,
|
|
863
|
+
effort: str,
|
|
864
|
+
) -> StampMintOutcome:
|
|
865
|
+
try:
|
|
866
|
+
return invoke_code_review_and_record_stamp(
|
|
867
|
+
working_directory=working_directory,
|
|
868
|
+
session_model=session_model,
|
|
869
|
+
timeout_seconds=timeout_seconds,
|
|
870
|
+
effort=effort,
|
|
871
|
+
)
|
|
872
|
+
except ChainConfigurationError:
|
|
873
|
+
return _no_mint_outcome(CHAIN_CONFIG_ERROR_EXIT_CODE)
|
|
874
|
+
except ValueError:
|
|
875
|
+
return _no_mint_outcome(HOST_PROFILE_ERROR_RETURNCODE)
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def _emit_import_failure(import_error: ModuleNotFoundError) -> int:
|
|
879
|
+
sys.stderr.write(
|
|
880
|
+
STAMP_STORE_IMPORT_FAILURE_MESSAGE.format(error=import_error) + "\n"
|
|
881
|
+
)
|
|
882
|
+
sys.stdout.write(json.dumps(_import_failure_payload()) + "\n")
|
|
883
|
+
return INVALID_EFFORT_RETURNCODE
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _record_stamp_exit_code(mint_outcome: StampMintOutcome) -> int:
|
|
887
|
+
if mint_outcome.is_stamp_minted:
|
|
888
|
+
return mint_outcome.review_outcome.returncode
|
|
889
|
+
if mint_outcome.pass_count >= MAXIMUM_STAMP_MINT_PASSES:
|
|
890
|
+
return STAMP_DID_NOT_CONVERGE_RETURNCODE
|
|
891
|
+
return mint_outcome.review_outcome.returncode
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def _emit_mint_outcome(mint_outcome: StampMintOutcome) -> int:
|
|
895
|
+
encoded_payload = encode_stamp_mint_outcome(mint_outcome)
|
|
896
|
+
did_not_converge = (
|
|
897
|
+
not mint_outcome.is_stamp_minted
|
|
898
|
+
and mint_outcome.pass_count >= MAXIMUM_STAMP_MINT_PASSES
|
|
899
|
+
)
|
|
900
|
+
if did_not_converge:
|
|
901
|
+
sys.stderr.write(
|
|
902
|
+
STAMP_DID_NOT_CONVERGE_MESSAGE.format(pass_count=mint_outcome.pass_count)
|
|
903
|
+
+ "\n"
|
|
904
|
+
)
|
|
905
|
+
sys.stdout.write(json.dumps(encoded_payload) + "\n")
|
|
906
|
+
return _record_stamp_exit_code(mint_outcome)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _run_record_stamp_cli(
|
|
910
|
+
*,
|
|
911
|
+
working_directory: Path,
|
|
912
|
+
session_model: str,
|
|
913
|
+
timeout_seconds: int,
|
|
914
|
+
effort: str,
|
|
915
|
+
) -> int:
|
|
916
|
+
try:
|
|
917
|
+
mint_outcome = _mint_or_config_outcome(
|
|
918
|
+
working_directory=working_directory,
|
|
919
|
+
session_model=session_model,
|
|
920
|
+
timeout_seconds=timeout_seconds,
|
|
921
|
+
effort=effort,
|
|
922
|
+
)
|
|
923
|
+
except ModuleNotFoundError as import_error:
|
|
924
|
+
return _emit_import_failure(import_error)
|
|
925
|
+
return _emit_mint_outcome(mint_outcome)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def _run_plain_review_cli(*, parsed_arguments: argparse.Namespace, effort: str) -> int:
|
|
929
|
+
try:
|
|
930
|
+
review_outcome = invoke_code_review(
|
|
931
|
+
working_directory=parsed_arguments.working_directory,
|
|
932
|
+
session_model=parsed_arguments.session_model,
|
|
933
|
+
timeout_seconds=parsed_arguments.timeout_seconds,
|
|
934
|
+
effort=effort,
|
|
935
|
+
)
|
|
936
|
+
except ChainConfigurationError:
|
|
937
|
+
review_outcome = _failure_code_review_outcome(CHAIN_CONFIG_ERROR_EXIT_CODE)
|
|
938
|
+
except ValueError:
|
|
939
|
+
review_outcome = _failure_code_review_outcome(HOST_PROFILE_ERROR_RETURNCODE)
|
|
940
|
+
sys.stdout.write(json.dumps(encode_code_review_outcome(review_outcome)) + "\n")
|
|
941
|
+
return review_outcome.returncode
|
|
942
|
+
|
|
943
|
+
|
|
432
944
|
def main(all_command_arguments: list[str]) -> int:
|
|
433
945
|
"""Run the invoker for CLI arguments and print the JSON outcome.
|
|
434
946
|
|
|
435
|
-
``
|
|
436
|
-
|
|
437
|
-
|
|
947
|
+
``--record-stamp`` forces chain mode and mints only on a surface-stable
|
|
948
|
+
returncode-0 pass; an unknown or ``ultra`` effort exits non-zero before any
|
|
949
|
+
review runs.
|
|
438
950
|
|
|
439
951
|
Args:
|
|
440
952
|
all_command_arguments: The argument vector after the program name.
|
|
441
953
|
|
|
442
954
|
Returns:
|
|
443
|
-
The outcome return code (``0`` for in-session; chain return code
|
|
955
|
+
The outcome return code (``0`` for in-session; chain return code
|
|
956
|
+
otherwise; non-zero when effort is invalid or minting fails to converge).
|
|
444
957
|
"""
|
|
445
958
|
parser = _build_argument_parser()
|
|
446
959
|
parsed_arguments = parser.parse_args(all_command_arguments)
|
|
447
|
-
|
|
448
|
-
|
|
960
|
+
effort_token = str(parsed_arguments.effort)
|
|
961
|
+
invalid_effort_exit_code = _emit_invalid_effort_and_exit_code(effort_token)
|
|
962
|
+
if invalid_effort_exit_code != SUCCESSFUL_REVIEW_RETURNCODE:
|
|
963
|
+
return invalid_effort_exit_code
|
|
964
|
+
if parsed_arguments.is_record_stamp:
|
|
965
|
+
return _run_record_stamp_cli(
|
|
449
966
|
working_directory=parsed_arguments.working_directory,
|
|
450
967
|
session_model=parsed_arguments.session_model,
|
|
451
968
|
timeout_seconds=parsed_arguments.timeout_seconds,
|
|
969
|
+
effort=effort_token,
|
|
452
970
|
)
|
|
453
|
-
|
|
454
|
-
review_outcome = _failure_code_review_outcome(CHAIN_CONFIG_ERROR_EXIT_CODE)
|
|
455
|
-
except ValueError:
|
|
456
|
-
review_outcome = _failure_code_review_outcome(HOST_PROFILE_ERROR_RETURNCODE)
|
|
457
|
-
encoded_payload = encode_code_review_outcome(review_outcome)
|
|
458
|
-
sys.stdout.write(json.dumps(encoded_payload) + "\n")
|
|
459
|
-
return review_outcome.returncode
|
|
971
|
+
return _run_plain_review_cli(parsed_arguments=parsed_arguments, effort=effort_token)
|
|
460
972
|
|
|
461
973
|
|
|
462
974
|
if __name__ == "__main__":
|