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
|
@@ -50,6 +50,9 @@ from claude_chain_runner import ( # noqa: E402
|
|
|
50
50
|
ChainInvocationOutcome,
|
|
51
51
|
run_claude,
|
|
52
52
|
)
|
|
53
|
+
from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
54
|
+
collect_forwarded_text_codec,
|
|
55
|
+
)
|
|
53
56
|
from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
54
57
|
AGENT_FLAG,
|
|
55
58
|
ALL_AGENT_FILENAMES_BY_ROLE,
|
|
@@ -177,7 +180,9 @@ def _run_claude_with_headless_overrides(
|
|
|
177
180
|
) -> ChainInvocationOutcome:
|
|
178
181
|
working_directory_path = str(working_directory)
|
|
179
182
|
with _HEADLESS_CHAIN_RUNNER_LOCK:
|
|
180
|
-
previous_runner =
|
|
183
|
+
previous_runner: TextCapturingSubprocessRunner = (
|
|
184
|
+
chain_runner.chain_subprocess_runner
|
|
185
|
+
)
|
|
181
186
|
|
|
182
187
|
def _runner_with_headless_overrides(
|
|
183
188
|
all_invocation_tokens: Sequence[str],
|
|
@@ -186,6 +191,7 @@ def _run_claude_with_headless_overrides(
|
|
|
186
191
|
) -> subprocess.CompletedProcess[str]:
|
|
187
192
|
del all_positionals
|
|
188
193
|
prompt_stdin.seek(0)
|
|
194
|
+
forwarded_text_codec = collect_forwarded_text_codec(all_keywords)
|
|
189
195
|
completed_process: subprocess.CompletedProcess[str] = previous_runner(
|
|
190
196
|
all_invocation_tokens,
|
|
191
197
|
capture_output=True,
|
|
@@ -194,6 +200,7 @@ def _run_claude_with_headless_overrides(
|
|
|
194
200
|
check=False,
|
|
195
201
|
stdin=prompt_stdin,
|
|
196
202
|
cwd=working_directory_path,
|
|
203
|
+
**forwarded_text_codec,
|
|
197
204
|
)
|
|
198
205
|
return completed_process
|
|
199
206
|
|
|
@@ -14,6 +14,7 @@ if str(_SCRIPTS_DIR) not in sys.path:
|
|
|
14
14
|
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
15
15
|
|
|
16
16
|
import claude_chain_runner as runner # noqa: E402
|
|
17
|
+
import claude_chain_usage as chain_usage # noqa: E402
|
|
17
18
|
from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
18
19
|
ALL_USAGE_LIMIT_SIGNATURES,
|
|
19
20
|
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
@@ -51,6 +52,10 @@ from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
|
51
52
|
|
|
52
53
|
_A_SIGNATURE = ALL_USAGE_LIMIT_SIGNATURES[0]
|
|
53
54
|
_PROMPT_ARGUMENTS = ["-p", "hello"]
|
|
55
|
+
_EQUAL_WEEKLY_REMAINING_PERCENT = 50.0
|
|
56
|
+
_HIGH_WEEKLY_REMAINING_PERCENT = 90.0
|
|
57
|
+
_MID_WEEKLY_REMAINING_PERCENT = 50.0
|
|
58
|
+
_LOW_WEEKLY_REMAINING_PERCENT = 10.0
|
|
54
59
|
|
|
55
60
|
|
|
56
61
|
def _completed(command, returncode, stdout="", stderr=""):
|
|
@@ -74,6 +79,43 @@ def _write_chain_config(tmp_path, chain_entries):
|
|
|
74
79
|
return config_file
|
|
75
80
|
|
|
76
81
|
|
|
82
|
+
def _config_order_usage_reporter(
|
|
83
|
+
*, config_path: Path
|
|
84
|
+
) -> list[chain_usage.AccountUsageReport]:
|
|
85
|
+
all_entries = runner.load_chain(config_path)
|
|
86
|
+
return [
|
|
87
|
+
chain_usage.AccountUsageReport(
|
|
88
|
+
command=each_entry.command,
|
|
89
|
+
weekly_remaining_percent=_EQUAL_WEEKLY_REMAINING_PERCENT,
|
|
90
|
+
)
|
|
91
|
+
for each_entry in all_entries
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _usage_reporter_from_remaining(
|
|
96
|
+
remaining_percent_by_command: dict[str, float | None],
|
|
97
|
+
):
|
|
98
|
+
call_count = {"count": 0}
|
|
99
|
+
|
|
100
|
+
def active_reporter(
|
|
101
|
+
*, config_path: Path
|
|
102
|
+
) -> list[chain_usage.AccountUsageReport]:
|
|
103
|
+
call_count["count"] += 1
|
|
104
|
+
all_entries = runner.load_chain(config_path)
|
|
105
|
+
return [
|
|
106
|
+
chain_usage.AccountUsageReport(
|
|
107
|
+
command=each_entry.command,
|
|
108
|
+
weekly_remaining_percent=remaining_percent_by_command[
|
|
109
|
+
each_entry.command
|
|
110
|
+
],
|
|
111
|
+
)
|
|
112
|
+
for each_entry in all_entries
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
active_reporter.call_count = call_count
|
|
116
|
+
return active_reporter
|
|
117
|
+
|
|
118
|
+
|
|
77
119
|
class _Recorder:
|
|
78
120
|
def __init__(self, behavior_by_command):
|
|
79
121
|
self.behavior_by_command = behavior_by_command
|
|
@@ -100,14 +142,364 @@ def _install_tty_stdin(monkeypatch):
|
|
|
100
142
|
monkeypatch.setattr(runner.sys, "stdin", _TtyStdin())
|
|
101
143
|
|
|
102
144
|
|
|
103
|
-
def _install(
|
|
145
|
+
def _install(
|
|
146
|
+
monkeypatch,
|
|
147
|
+
config_file,
|
|
148
|
+
behavior_by_command,
|
|
149
|
+
*,
|
|
150
|
+
weekly_usage_reporter=None,
|
|
151
|
+
):
|
|
104
152
|
recorder = _Recorder(behavior_by_command)
|
|
105
153
|
monkeypatch.setattr(runner, "chain_config_path", lambda: config_file)
|
|
106
154
|
monkeypatch.setattr(runner, "chain_subprocess_runner", recorder)
|
|
155
|
+
active_reporter = (
|
|
156
|
+
weekly_usage_reporter
|
|
157
|
+
if weekly_usage_reporter is not None
|
|
158
|
+
else _config_order_usage_reporter
|
|
159
|
+
)
|
|
160
|
+
monkeypatch.setattr(runner, "chain_weekly_usage_reporter", active_reporter)
|
|
107
161
|
_install_tty_stdin(monkeypatch)
|
|
108
162
|
return recorder
|
|
109
163
|
|
|
110
164
|
|
|
165
|
+
def test_highest_weekly_remaining_is_tried_first(
|
|
166
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
167
|
+
) -> None:
|
|
168
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
169
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
170
|
+
{
|
|
171
|
+
"claude": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
172
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
recorder = _install(
|
|
176
|
+
monkeypatch,
|
|
177
|
+
config_file,
|
|
178
|
+
{
|
|
179
|
+
"claude": _completed("claude", 0, stdout="from-claude"),
|
|
180
|
+
"claude-ev": _completed("claude-ev", 0, stdout="from-ev"),
|
|
181
|
+
},
|
|
182
|
+
weekly_usage_reporter=usage_reporter,
|
|
183
|
+
)
|
|
184
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
185
|
+
assert chain_result.served_command == "claude-ev"
|
|
186
|
+
assert chain_result.returncode == 0
|
|
187
|
+
assert chain_result.stdout == "from-ev"
|
|
188
|
+
assert recorder.invocations[0][0] == "claude-ev"
|
|
189
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
190
|
+
"claude-ev"
|
|
191
|
+
]
|
|
192
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_SERVED
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_usage_limit_failsover_remaining_ranked_accounts(
|
|
196
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
197
|
+
) -> None:
|
|
198
|
+
config_file = _write_chain_config(
|
|
199
|
+
tmp_path, [_entry("claude"), _entry("claude-ev"), _entry("claude-editor")]
|
|
200
|
+
)
|
|
201
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
202
|
+
{
|
|
203
|
+
"claude": _MID_WEEKLY_REMAINING_PERCENT,
|
|
204
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
205
|
+
"claude-editor": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
_install(
|
|
209
|
+
monkeypatch,
|
|
210
|
+
config_file,
|
|
211
|
+
{
|
|
212
|
+
"claude-ev": _completed("claude-ev", 1, stderr=_A_SIGNATURE),
|
|
213
|
+
"claude": _completed("claude", 0, stdout="ok"),
|
|
214
|
+
"claude-editor": _completed("claude-editor", 0, stdout="should not run"),
|
|
215
|
+
},
|
|
216
|
+
weekly_usage_reporter=usage_reporter,
|
|
217
|
+
)
|
|
218
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
219
|
+
assert chain_result.served_command == "claude"
|
|
220
|
+
assert chain_result.returncode == 0
|
|
221
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
222
|
+
"claude-ev",
|
|
223
|
+
"claude",
|
|
224
|
+
]
|
|
225
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_USAGE_LIMITED
|
|
226
|
+
assert chain_result.attempts[1].status == ATTEMPT_STATUS_SERVED
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_non_usage_error_on_highest_ranked_stops_without_rest(
|
|
230
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
231
|
+
) -> None:
|
|
232
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
233
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
234
|
+
{
|
|
235
|
+
"claude": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
236
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
_install(
|
|
240
|
+
monkeypatch,
|
|
241
|
+
config_file,
|
|
242
|
+
{
|
|
243
|
+
"claude-ev": _completed("claude-ev", 2, stderr="unknown flag"),
|
|
244
|
+
"claude": _completed("claude", 0, stdout="should not run"),
|
|
245
|
+
},
|
|
246
|
+
weekly_usage_reporter=usage_reporter,
|
|
247
|
+
)
|
|
248
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
249
|
+
assert chain_result.served_command == "claude-ev"
|
|
250
|
+
assert chain_result.returncode == 2
|
|
251
|
+
assert len(chain_result.attempts) == 1
|
|
252
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_NONZERO_EXIT
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_weekly_usage_probe_runs_once_per_run_claude(
|
|
256
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
257
|
+
) -> None:
|
|
258
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
259
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
260
|
+
{
|
|
261
|
+
"claude": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
262
|
+
"claude-ev": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
_install(
|
|
266
|
+
monkeypatch,
|
|
267
|
+
config_file,
|
|
268
|
+
{
|
|
269
|
+
"claude": _completed("claude", 1, stderr=_A_SIGNATURE),
|
|
270
|
+
"claude-ev": _completed("claude-ev", 0, stdout="ok"),
|
|
271
|
+
},
|
|
272
|
+
weekly_usage_reporter=usage_reporter,
|
|
273
|
+
)
|
|
274
|
+
runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
275
|
+
assert usage_reporter.call_count["count"] == 1
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_usage_reporter_import_failure_falls_back_to_config_order(
|
|
279
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
280
|
+
) -> None:
|
|
281
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
282
|
+
|
|
283
|
+
def broken_usage_reporter(
|
|
284
|
+
*, config_path: Path
|
|
285
|
+
) -> list[chain_usage.AccountUsageReport]:
|
|
286
|
+
raise ImportError("claude_chain_usage unavailable")
|
|
287
|
+
|
|
288
|
+
recorder = _install(
|
|
289
|
+
monkeypatch,
|
|
290
|
+
config_file,
|
|
291
|
+
{
|
|
292
|
+
"claude": _completed("claude", 0, stdout="from-claude"),
|
|
293
|
+
"claude-ev": _completed("claude-ev", 0, stdout="from-ev"),
|
|
294
|
+
},
|
|
295
|
+
weekly_usage_reporter=broken_usage_reporter,
|
|
296
|
+
)
|
|
297
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
298
|
+
assert chain_result.served_command == "claude"
|
|
299
|
+
assert chain_result.returncode == 0
|
|
300
|
+
assert chain_result.stdout == "from-claude"
|
|
301
|
+
assert recorder.invocations[0][0] == "claude"
|
|
302
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
303
|
+
"claude"
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_missing_high_remaining_binary_falls_through_to_lower_remaining(
|
|
308
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
309
|
+
) -> None:
|
|
310
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
311
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
312
|
+
{
|
|
313
|
+
"claude": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
314
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
_install(
|
|
318
|
+
monkeypatch,
|
|
319
|
+
config_file,
|
|
320
|
+
{
|
|
321
|
+
"claude-ev": FileNotFoundError(),
|
|
322
|
+
"claude": _completed("claude", 0, stdout="from-claude"),
|
|
323
|
+
},
|
|
324
|
+
weekly_usage_reporter=usage_reporter,
|
|
325
|
+
)
|
|
326
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
327
|
+
assert chain_result.served_command == "claude"
|
|
328
|
+
assert chain_result.returncode == 0
|
|
329
|
+
assert chain_result.stdout == "from-claude"
|
|
330
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
331
|
+
"claude-ev",
|
|
332
|
+
"claude",
|
|
333
|
+
]
|
|
334
|
+
assert [each_attempt.status for each_attempt in chain_result.attempts] == [
|
|
335
|
+
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
336
|
+
ATTEMPT_STATUS_SERVED,
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_all_missing_binaries_exhausts_chain(
|
|
341
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
342
|
+
) -> None:
|
|
343
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
344
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
345
|
+
{
|
|
346
|
+
"claude": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
347
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
_install(
|
|
351
|
+
monkeypatch,
|
|
352
|
+
config_file,
|
|
353
|
+
{
|
|
354
|
+
"claude-ev": FileNotFoundError(),
|
|
355
|
+
"claude": FileNotFoundError(),
|
|
356
|
+
},
|
|
357
|
+
weekly_usage_reporter=usage_reporter,
|
|
358
|
+
)
|
|
359
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
360
|
+
assert chain_result.served_command is None
|
|
361
|
+
assert chain_result.returncode == NO_COMPLETED_PROCESS_RETURN_CODE
|
|
362
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
363
|
+
"claude-ev",
|
|
364
|
+
"claude",
|
|
365
|
+
]
|
|
366
|
+
assert [each_attempt.status for each_attempt in chain_result.attempts] == [
|
|
367
|
+
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
368
|
+
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
369
|
+
]
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_missing_later_ranked_binary_is_skipped(
|
|
373
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
374
|
+
) -> None:
|
|
375
|
+
config_file = _write_chain_config(
|
|
376
|
+
tmp_path, [_entry("claude"), _entry("claude-ev"), _entry("claude-editor")]
|
|
377
|
+
)
|
|
378
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
379
|
+
{
|
|
380
|
+
"claude": _MID_WEEKLY_REMAINING_PERCENT,
|
|
381
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
382
|
+
"claude-editor": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
_install(
|
|
386
|
+
monkeypatch,
|
|
387
|
+
config_file,
|
|
388
|
+
{
|
|
389
|
+
"claude-ev": _completed("claude-ev", 1, stderr=_A_SIGNATURE),
|
|
390
|
+
"claude": FileNotFoundError(),
|
|
391
|
+
"claude-editor": _completed("claude-editor", 0, stdout="done"),
|
|
392
|
+
},
|
|
393
|
+
weekly_usage_reporter=usage_reporter,
|
|
394
|
+
)
|
|
395
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
396
|
+
assert chain_result.served_command == "claude-editor"
|
|
397
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
398
|
+
"claude-ev",
|
|
399
|
+
"claude",
|
|
400
|
+
"claude-editor",
|
|
401
|
+
]
|
|
402
|
+
assert [each_attempt.status for each_attempt in chain_result.attempts] == [
|
|
403
|
+
ATTEMPT_STATUS_USAGE_LIMITED,
|
|
404
|
+
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
405
|
+
ATTEMPT_STATUS_SERVED,
|
|
406
|
+
]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def test_ranked_walk_preserves_extra_args_for_mapped_entry(
|
|
410
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
411
|
+
) -> None:
|
|
412
|
+
config_file = _write_chain_config(
|
|
413
|
+
tmp_path,
|
|
414
|
+
[
|
|
415
|
+
_entry("claude", extra_args=["--account", "primary"]),
|
|
416
|
+
_entry("claude-ev", extra_args=["--account", "ev"]),
|
|
417
|
+
],
|
|
418
|
+
)
|
|
419
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
420
|
+
{
|
|
421
|
+
"claude": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
422
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
recorder = _install(
|
|
426
|
+
monkeypatch,
|
|
427
|
+
config_file,
|
|
428
|
+
{"claude-ev": _completed("claude-ev", 0)},
|
|
429
|
+
weekly_usage_reporter=usage_reporter,
|
|
430
|
+
)
|
|
431
|
+
runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
432
|
+
assert recorder.invocations[0] == [
|
|
433
|
+
"claude-ev",
|
|
434
|
+
"-p",
|
|
435
|
+
"hello",
|
|
436
|
+
"--account",
|
|
437
|
+
"ev",
|
|
438
|
+
]
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def test_duplicate_command_entries_are_both_walked(
|
|
442
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
443
|
+
) -> None:
|
|
444
|
+
config_file = _write_chain_config(
|
|
445
|
+
tmp_path,
|
|
446
|
+
[
|
|
447
|
+
_entry("claude", extra_args=["--account", "primary"]),
|
|
448
|
+
_entry("claude", extra_args=["--account", "secondary"]),
|
|
449
|
+
],
|
|
450
|
+
)
|
|
451
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
452
|
+
{"claude": _HIGH_WEEKLY_REMAINING_PERCENT}
|
|
453
|
+
)
|
|
454
|
+
recorder = _install(
|
|
455
|
+
monkeypatch,
|
|
456
|
+
config_file,
|
|
457
|
+
{"claude": _completed("claude", 1, stderr=_A_SIGNATURE)},
|
|
458
|
+
weekly_usage_reporter=usage_reporter,
|
|
459
|
+
)
|
|
460
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
461
|
+
assert [each_invocation[-1] for each_invocation in recorder.invocations] == [
|
|
462
|
+
"primary",
|
|
463
|
+
"secondary",
|
|
464
|
+
]
|
|
465
|
+
assert [each_attempt.status for each_attempt in chain_result.attempts] == [
|
|
466
|
+
ATTEMPT_STATUS_USAGE_LIMITED,
|
|
467
|
+
ATTEMPT_STATUS_USAGE_LIMITED,
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def test_entry_absent_from_usage_report_is_still_walked(
|
|
472
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
473
|
+
) -> None:
|
|
474
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
475
|
+
|
|
476
|
+
def partial_usage_reporter(
|
|
477
|
+
*, config_path: Path
|
|
478
|
+
) -> list[chain_usage.AccountUsageReport]:
|
|
479
|
+
return [
|
|
480
|
+
chain_usage.AccountUsageReport(
|
|
481
|
+
command="claude-ev",
|
|
482
|
+
weekly_remaining_percent=_HIGH_WEEKLY_REMAINING_PERCENT,
|
|
483
|
+
)
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
_install(
|
|
487
|
+
monkeypatch,
|
|
488
|
+
config_file,
|
|
489
|
+
{
|
|
490
|
+
"claude-ev": _completed("claude-ev", 1, stderr=_A_SIGNATURE),
|
|
491
|
+
"claude": _completed("claude", 0, stdout="ok"),
|
|
492
|
+
},
|
|
493
|
+
weekly_usage_reporter=partial_usage_reporter,
|
|
494
|
+
)
|
|
495
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
496
|
+
assert chain_result.served_command == "claude"
|
|
497
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
498
|
+
"claude-ev",
|
|
499
|
+
"claude",
|
|
500
|
+
]
|
|
501
|
+
|
|
502
|
+
|
|
111
503
|
def test_usage_limited_primary_falls_over_to_second(
|
|
112
504
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
113
505
|
) -> None:
|
|
@@ -170,7 +562,7 @@ def test_timeout_on_primary_does_not_fall_over(
|
|
|
170
562
|
assert chain_result.attempts[0].status == ATTEMPT_STATUS_TIMEOUT
|
|
171
563
|
|
|
172
564
|
|
|
173
|
-
def
|
|
565
|
+
def test_missing_primary_binary_falls_through_to_next(
|
|
174
566
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
175
567
|
) -> None:
|
|
176
568
|
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
@@ -179,13 +571,21 @@ def test_missing_primary_binary_does_not_fall_over(
|
|
|
179
571
|
config_file,
|
|
180
572
|
{
|
|
181
573
|
"claude": FileNotFoundError(),
|
|
182
|
-
"claude-ev": _completed("claude-ev", 0),
|
|
574
|
+
"claude-ev": _completed("claude-ev", 0, stdout="from-ev"),
|
|
183
575
|
},
|
|
184
576
|
)
|
|
185
577
|
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
186
|
-
assert chain_result.served_command
|
|
187
|
-
assert
|
|
188
|
-
assert chain_result.
|
|
578
|
+
assert chain_result.served_command == "claude-ev"
|
|
579
|
+
assert chain_result.returncode == 0
|
|
580
|
+
assert chain_result.stdout == "from-ev"
|
|
581
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
582
|
+
"claude",
|
|
583
|
+
"claude-ev",
|
|
584
|
+
]
|
|
585
|
+
assert [each_attempt.status for each_attempt in chain_result.attempts] == [
|
|
586
|
+
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
587
|
+
ATTEMPT_STATUS_SERVED,
|
|
588
|
+
]
|
|
189
589
|
|
|
190
590
|
|
|
191
591
|
def test_missing_fallback_binary_is_skipped_and_walk_continues(
|
|
@@ -530,6 +930,9 @@ def test_real_subprocess_capture_replaces_undecodable_bytes(
|
|
|
530
930
|
) -> None:
|
|
531
931
|
config_file = _write_chain_config(tmp_path, [_entry(sys.executable)])
|
|
532
932
|
monkeypatch.setattr(runner, "chain_config_path", lambda: config_file)
|
|
933
|
+
monkeypatch.setattr(
|
|
934
|
+
runner, "chain_weekly_usage_reporter", _config_order_usage_reporter
|
|
935
|
+
)
|
|
533
936
|
child_code = 'import sys; sys.stdout.buffer.write(b"ok \\x90 end")'
|
|
534
937
|
chain_result = runner.run_claude(["-c", child_code], timeout_seconds=60)
|
|
535
938
|
assert chain_result.served_command == sys.executable
|
|
@@ -542,6 +945,9 @@ def test_real_subprocess_capture_preserves_utf8_text(
|
|
|
542
945
|
) -> None:
|
|
543
946
|
config_file = _write_chain_config(tmp_path, [_entry(sys.executable)])
|
|
544
947
|
monkeypatch.setattr(runner, "chain_config_path", lambda: config_file)
|
|
948
|
+
monkeypatch.setattr(
|
|
949
|
+
runner, "chain_weekly_usage_reporter", _config_order_usage_reporter
|
|
950
|
+
)
|
|
545
951
|
child_code = 'import sys; sys.stdout.buffer.write("report ✅ done".encode("utf-8"))'
|
|
546
952
|
chain_result = runner.run_claude(["-c", child_code], timeout_seconds=60)
|
|
547
953
|
assert chain_result.served_command == sys.executable
|