claude-dev-env 2.2.0 → 2.3.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 +1 -1
- 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/pyproject.toml +22 -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/code-verifier.md +1 -1
- package/hooks/blocking/code_rules_shared.py +4 -2
- package/hooks/blocking/config/verified_commit_constants.py +11 -0
- package/hooks/blocking/pii_payload_scan.py +78 -20
- package/hooks/blocking/pii_prevention_blocker.py +3 -1
- package/hooks/blocking/test_code_verifier_tools_contract.py +28 -0
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +221 -0
- package/hooks/blocking/test_verification_verdict_store.py +93 -0
- package/hooks/blocking/verification_verdict_store.py +91 -0
- package/hooks/validators/ruff_integration.py +2 -1
- package/hooks/validators/run_all_validators.py +460 -24
- package/hooks/validators/test_ruff_integration.py +21 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +223 -3
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/cleanup-command-forms.md +23 -0
- package/scripts/CLAUDE.md +2 -1
- package/scripts/check.ps1 +32 -6
- 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/dev_env_scripts_constants/CLAUDE.md +4 -3
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +13 -1
- package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +58 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +1 -1
- package/scripts/dev_env_scripts_constants/timing.py +1 -1
- package/scripts/test_claude_chain_runner.py +412 -6
- package/scripts/test_claude_chain_usage.py +534 -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/team-advisor/SKILL.md +3 -2
- package/skills/usage-pause/SKILL.md +5 -4
- package/skills/usage-pause/scripts/resolve_usage_window.py +68 -13
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +121 -9
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +7 -3
|
@@ -8,12 +8,19 @@ when that content violates a validator, rather than grading the whole branch.
|
|
|
8
8
|
import json
|
|
9
9
|
import subprocess
|
|
10
10
|
import sys
|
|
11
|
+
from collections import Counter
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from unittest.mock import patch
|
|
13
14
|
|
|
14
15
|
import pytest
|
|
15
16
|
|
|
16
|
-
from .run_all_validators import
|
|
17
|
+
from .run_all_validators import (
|
|
18
|
+
ValidatorResult,
|
|
19
|
+
_scope_new_and_preexisting,
|
|
20
|
+
_violation_line_number,
|
|
21
|
+
main,
|
|
22
|
+
run_validators_entrypoint_subprocess,
|
|
23
|
+
)
|
|
17
24
|
|
|
18
25
|
CLEAN_PYTHON_SOURCE = (
|
|
19
26
|
"def add_two_numbers(first_number: int, second_number: int) -> int:\n"
|
|
@@ -26,7 +33,9 @@ VIOLATING_PYTHON_SOURCE = (
|
|
|
26
33
|
|
|
27
34
|
|
|
28
35
|
def run_gate(payload: dict[str, object]) -> "subprocess.CompletedProcess[str]":
|
|
29
|
-
return run_validators_entrypoint_subprocess(
|
|
36
|
+
return run_validators_entrypoint_subprocess(
|
|
37
|
+
["--pre-tool-use"], stdin_text=json.dumps(payload)
|
|
38
|
+
)
|
|
30
39
|
|
|
31
40
|
|
|
32
41
|
class TestPreToolUseGate:
|
|
@@ -73,7 +82,9 @@ class TestPreToolUseGate:
|
|
|
73
82
|
assert completed.returncode == 0, completed.stderr
|
|
74
83
|
assert "deny" not in completed.stdout
|
|
75
84
|
|
|
76
|
-
def test_edit_validates_reconstructed_post_edit_content(
|
|
85
|
+
def test_edit_validates_reconstructed_post_edit_content(
|
|
86
|
+
self, tmp_path: Path
|
|
87
|
+
) -> None:
|
|
77
88
|
target_file = tmp_path / "calculate.py"
|
|
78
89
|
target_file.write_text(CLEAN_PYTHON_SOURCE, encoding="utf-8")
|
|
79
90
|
completed = run_gate(
|
|
@@ -116,3 +127,212 @@ class TestCliModeRegression:
|
|
|
116
127
|
captured = capsys.readouterr()
|
|
117
128
|
assert exit_code == 1
|
|
118
129
|
assert "PRE-PUSH VALIDATOR RESULTS" in captured.out
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _over_long_function_source(function_name: str) -> str:
|
|
133
|
+
"""Return a syntactically clean function whose only defect is exceeding 30 lines."""
|
|
134
|
+
body_lines = "".join(" running_total = running_total + 1\n" for _ in range(35))
|
|
135
|
+
return (
|
|
136
|
+
f"def {function_name}(running_total: int) -> int:\n"
|
|
137
|
+
f"{body_lines}"
|
|
138
|
+
" return running_total\n"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
CLEAN_MARKER_FUNCTION = "def clean_marker() -> int:\n return 1\n"
|
|
143
|
+
CLEAN_MARKER_EDITED = (
|
|
144
|
+
"def clean_marker() -> int:\n computed_value = 1\n return computed_value\n"
|
|
145
|
+
)
|
|
146
|
+
MAGIC_VALUE_MARKER_FUNCTION = "def clean_marker() -> int:\n return 199\n"
|
|
147
|
+
PRE_EXISTING_VIOLATION_SOURCE = (
|
|
148
|
+
_over_long_function_source("over_long_function") + "\n" + CLEAN_MARKER_FUNCTION
|
|
149
|
+
)
|
|
150
|
+
MAGIC_VALUE_FUNCTION_SOURCE = "def compute_total() -> int:\n return 199\n"
|
|
151
|
+
RUFF_DIRTY_FUNCTION_SOURCE = (
|
|
152
|
+
"def compute_total() -> int:\n unused_intermediate = 1\n return 1\n"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TestBaselineScopedGate:
|
|
157
|
+
def test_clean_edit_to_violating_file_is_allowed_with_warning(
|
|
158
|
+
self, tmp_path: Path
|
|
159
|
+
) -> None:
|
|
160
|
+
target_file = tmp_path / "legacy_module.py"
|
|
161
|
+
target_file.write_text(PRE_EXISTING_VIOLATION_SOURCE, encoding="utf-8")
|
|
162
|
+
completed = run_gate(
|
|
163
|
+
{
|
|
164
|
+
"tool_name": "Edit",
|
|
165
|
+
"tool_input": {
|
|
166
|
+
"file_path": str(target_file),
|
|
167
|
+
"old_string": CLEAN_MARKER_FUNCTION,
|
|
168
|
+
"new_string": CLEAN_MARKER_EDITED,
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
assert completed.returncode == 0, completed.stderr
|
|
173
|
+
assert '"permissionDecision": "deny"' not in completed.stdout
|
|
174
|
+
assert "Code Quality" in completed.stderr
|
|
175
|
+
|
|
176
|
+
def test_edit_introducing_new_violation_denies_naming_only_the_new_one(
|
|
177
|
+
self, tmp_path: Path
|
|
178
|
+
) -> None:
|
|
179
|
+
target_file = tmp_path / "legacy_module.py"
|
|
180
|
+
target_file.write_text(PRE_EXISTING_VIOLATION_SOURCE, encoding="utf-8")
|
|
181
|
+
completed = run_gate(
|
|
182
|
+
{
|
|
183
|
+
"tool_name": "Edit",
|
|
184
|
+
"tool_input": {
|
|
185
|
+
"file_path": str(target_file),
|
|
186
|
+
"old_string": CLEAN_MARKER_FUNCTION,
|
|
187
|
+
"new_string": MAGIC_VALUE_MARKER_FUNCTION,
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
assert completed.returncode == 0, completed.stderr
|
|
192
|
+
assert '"permissionDecision": "deny"' in completed.stdout
|
|
193
|
+
assert "Magic Values" in completed.stdout
|
|
194
|
+
assert "Code Quality" not in completed.stdout
|
|
195
|
+
|
|
196
|
+
def test_new_violation_in_clean_file_denies(self, tmp_path: Path) -> None:
|
|
197
|
+
target_file = tmp_path / "clean_module.py"
|
|
198
|
+
target_file.write_text(CLEAN_MARKER_FUNCTION, encoding="utf-8")
|
|
199
|
+
completed = run_gate(
|
|
200
|
+
{
|
|
201
|
+
"tool_name": "Edit",
|
|
202
|
+
"tool_input": {
|
|
203
|
+
"file_path": str(target_file),
|
|
204
|
+
"old_string": " return 1\n",
|
|
205
|
+
"new_string": " return 199\n",
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
assert completed.returncode == 0, completed.stderr
|
|
210
|
+
assert '"permissionDecision": "deny"' in completed.stdout
|
|
211
|
+
assert "Magic Values" in completed.stdout
|
|
212
|
+
|
|
213
|
+
def test_clean_edit_to_clean_file_is_allowed(self, tmp_path: Path) -> None:
|
|
214
|
+
target_file = tmp_path / "clean_module.py"
|
|
215
|
+
target_file.write_text(CLEAN_MARKER_FUNCTION, encoding="utf-8")
|
|
216
|
+
completed = run_gate(
|
|
217
|
+
{
|
|
218
|
+
"tool_name": "Edit",
|
|
219
|
+
"tool_input": {
|
|
220
|
+
"file_path": str(target_file),
|
|
221
|
+
"old_string": " return 1\n",
|
|
222
|
+
"new_string": " computed_value = 1\n return computed_value\n",
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
assert completed.returncode == 0, completed.stderr
|
|
227
|
+
assert "deny" not in completed.stdout
|
|
228
|
+
|
|
229
|
+
def test_growing_a_function_with_existing_length_violation_is_allowed_with_warning(
|
|
230
|
+
self, tmp_path: Path
|
|
231
|
+
) -> None:
|
|
232
|
+
target_file = tmp_path / "legacy_module.py"
|
|
233
|
+
target_file.write_text(
|
|
234
|
+
_over_long_function_source("over_long_function"), encoding="utf-8"
|
|
235
|
+
)
|
|
236
|
+
completed = run_gate(
|
|
237
|
+
{
|
|
238
|
+
"tool_name": "Edit",
|
|
239
|
+
"tool_input": {
|
|
240
|
+
"file_path": str(target_file),
|
|
241
|
+
"old_string": " return running_total\n",
|
|
242
|
+
"new_string": (
|
|
243
|
+
" running_total = running_total + 1\n"
|
|
244
|
+
" running_total = running_total + 1\n"
|
|
245
|
+
" return running_total\n"
|
|
246
|
+
),
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
assert completed.returncode == 0, completed.stderr
|
|
251
|
+
assert '"permissionDecision": "deny"' not in completed.stdout
|
|
252
|
+
assert "Code Quality" in completed.stderr
|
|
253
|
+
|
|
254
|
+
def test_new_same_validator_violation_in_dirty_function_denies(
|
|
255
|
+
self, tmp_path: Path
|
|
256
|
+
) -> None:
|
|
257
|
+
target_file = tmp_path / "legacy_module.py"
|
|
258
|
+
target_file.write_text(MAGIC_VALUE_FUNCTION_SOURCE, encoding="utf-8")
|
|
259
|
+
completed = run_gate(
|
|
260
|
+
{
|
|
261
|
+
"tool_name": "Edit",
|
|
262
|
+
"tool_input": {
|
|
263
|
+
"file_path": str(target_file),
|
|
264
|
+
"old_string": " return 199\n",
|
|
265
|
+
"new_string": " threshold = 42\n return 199 + threshold\n",
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
assert completed.returncode == 0, completed.stderr
|
|
270
|
+
assert '"permissionDecision": "deny"' in completed.stdout
|
|
271
|
+
assert "Magic Values" in completed.stdout
|
|
272
|
+
|
|
273
|
+
def test_new_module_scope_ruff_violation_with_dirty_function_denies(
|
|
274
|
+
self, tmp_path: Path
|
|
275
|
+
) -> None:
|
|
276
|
+
target_file = tmp_path / "legacy_module.py"
|
|
277
|
+
target_file.write_text(RUFF_DIRTY_FUNCTION_SOURCE, encoding="utf-8")
|
|
278
|
+
completed = run_gate(
|
|
279
|
+
{
|
|
280
|
+
"tool_name": "Edit",
|
|
281
|
+
"tool_input": {
|
|
282
|
+
"file_path": str(target_file),
|
|
283
|
+
"old_string": "def compute_total",
|
|
284
|
+
"new_string": "import os\n\n\ndef compute_total",
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
assert completed.returncode == 0, completed.stderr
|
|
289
|
+
assert '"permissionDecision": "deny"' in completed.stdout
|
|
290
|
+
assert "F401" in completed.stdout
|
|
291
|
+
assert "F841" not in completed.stdout
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class TestScopeNewAndPreexisting:
|
|
295
|
+
def test_failed_result_without_located_lines_is_treated_as_new(self) -> None:
|
|
296
|
+
summary_only_result = ValidatorResult(
|
|
297
|
+
name="Ruff", checks="37", passed=False, output="Found 1 error."
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
new_results, preexisting_results = _scope_new_and_preexisting(
|
|
301
|
+
[summary_only_result], "import os\n", Counter()
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
assert [each_result.name for each_result in new_results] == ["Ruff"]
|
|
305
|
+
assert preexisting_results == []
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class TestViolationLineNumber:
|
|
309
|
+
def test_ruff_line_col_prefix_returns_line_not_column(self) -> None:
|
|
310
|
+
ruff_shaped_line = "/pkg/legacy_module.py:37:5: F401 `os` imported but unused"
|
|
311
|
+
|
|
312
|
+
assert _violation_line_number(ruff_shaped_line) == 37
|
|
313
|
+
|
|
314
|
+
def test_windows_drive_prefix_returns_line(self) -> None:
|
|
315
|
+
windows_shaped_line = r"C:\repo\tmp\legacy_module.py:37:5: F401 unused import"
|
|
316
|
+
|
|
317
|
+
assert _violation_line_number(windows_shaped_line) == 37
|
|
318
|
+
|
|
319
|
+
def test_check_module_line_prefix_returns_line(self) -> None:
|
|
320
|
+
check_module_line = "/pkg/legacy_module.py:37: magic number 199"
|
|
321
|
+
|
|
322
|
+
assert _violation_line_number(check_module_line) == 37
|
|
323
|
+
|
|
324
|
+
def test_summary_line_without_location_returns_zero(self) -> None:
|
|
325
|
+
assert _violation_line_number("Found 3 errors.") == 0
|
|
326
|
+
|
|
327
|
+
def test_code_frame_line_quoting_colon_digits_returns_zero(self) -> None:
|
|
328
|
+
frame_line = '4 | message = "err:37: bad"'
|
|
329
|
+
|
|
330
|
+
assert _violation_line_number(frame_line) == 0
|
|
331
|
+
|
|
332
|
+
def test_code_frame_source_line_returns_zero(self) -> None:
|
|
333
|
+
assert _violation_line_number("17 | import os") == 0
|
|
334
|
+
|
|
335
|
+
def test_path_with_spaces_returns_line(self) -> None:
|
|
336
|
+
spaced_path_line = "/tmp/my dir/legacy module.py:37: magic number 199"
|
|
337
|
+
|
|
338
|
+
assert _violation_line_number(spaced_path_line) == 37
|
package/package.json
CHANGED
package/rules/CLAUDE.md
CHANGED
|
@@ -16,6 +16,7 @@ Rule files installed into `~/.claude/rules/` by `bin/install.mjs`. A rule withou
|
|
|
16
16
|
| `ask-user-question-required.md` | Every user-directed question goes through the `AskUserQuestion` tool — no plain-text questions |
|
|
17
17
|
| `bdd.md` | BDD discovery-driven development workflow and Example Mapping reference |
|
|
18
18
|
| `claude-md-orphan-file.md` | Every backticked bare filename in a per-directory `CLAUDE.md` table's first column names a file in that directory's subtree |
|
|
19
|
+
| `cleanup-command-forms.md` | Never use bash `rm` to clean up; use the PowerShell `Remove-Item` and `git worktree remove --force` forms the `destructive_command_blocker` hook never prompts on, and carry the rule into every subagent prompt |
|
|
19
20
|
| `cleanup-temp-files.md` | Remove temporary files created during a task when the task is complete |
|
|
20
21
|
| `code-reviews.md` | Mandatory protocol for responding to GitHub PR review feedback |
|
|
21
22
|
| `code-standards.md` | Pointer to `CODE_RULES.md` as the single source of truth |
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Cleanup Command Forms
|
|
2
|
+
|
|
3
|
+
Never use bash `rm` in any form to clean up. The `destructive_command_blocker` hook watches every Bash-tool command and matches `rm -rf` (and the rest of the destructive patterns) as raw text. It allows an `rm` without a prompt only for a narrow set of shapes it can prove safe: an `rm` whose every target is an absolute path under the OS temp root, `/tmp`, `/temp`, or a worktrees directory — standalone, or in a chain whose other segments are plain reporting commands such as `echo` or `cat`; an `rm` run from an ephemeral working directory; and an `rm` whose every target sits inside `~/.claude`. It falls through to a permission prompt on anything outside that set: a `$`, `$(...)`, or backtick expansion whose value it cannot resolve, a target it cannot place in a safe directory, a glob basename, or a string-executing wrapper (`bash -c 'rm -rf …'`). In a background or auto-mode run no human can answer that prompt, so the call stalls.
|
|
4
|
+
|
|
5
|
+
Remove files with these forms, which the hook never prompts on:
|
|
6
|
+
|
|
7
|
+
- **Scratch and probe files:** the PowerShell tool — `Remove-Item -Recurse -Force -Confirm:$false <absolute path>`. The hook watches only the Bash tool, so a PowerShell removal never reaches it. A file left in the OS temp dir or `$CLAUDE_JOB_DIR/tmp` is ephemeral and needs no explicit removal.
|
|
8
|
+
- **Worktrees:** `git worktree remove --force <path>`. This matches no destructive pattern.
|
|
9
|
+
- **When bash `rm` is unavoidable:** one standalone `rm` command, an absolute literal path under the OS temp root or a worktrees directory, no chaining, no variables, no globs. The hook auto-allows this shape without a prompt.
|
|
10
|
+
|
|
11
|
+
## Every subagent prompt carries the rule
|
|
12
|
+
|
|
13
|
+
A prompt-delivered directive reaches only the agent that gets it. An agent that spawns its own workers — review lenses, fix agents, verifiers — copies this line into every subagent prompt it issues, so a grandchild cleaning up its own probe file uses an allowed form:
|
|
14
|
+
|
|
15
|
+
> Never use bash rm in any form. Delete scratch/probe files with the PowerShell tool (Remove-Item -Recurse -Force -Confirm:$false <absolute path>), or leave them in the OS temp dir; remove worktrees only via git worktree remove --force.
|
|
16
|
+
|
|
17
|
+
Prefer that a child leaves its scratch files in place for the parent to remove at teardown with `Remove-Item`.
|
|
18
|
+
|
|
19
|
+
## Sibling rules
|
|
20
|
+
|
|
21
|
+
- [`no-inline-destructive-literals`](no-inline-destructive-literals.md) — keep a destructive literal out of the Bash command string even when it rides only as data.
|
|
22
|
+
- [`cleanup-temp-files`](cleanup-temp-files.md) — remove the scratch files a task created once the task is done.
|
|
23
|
+
- [`windows-filesystem-safe`](windows-filesystem-safe.md) — the safe `rmtree` / `force_rmtree` patterns for read-only Windows files.
|
package/scripts/CLAUDE.md
CHANGED
|
@@ -6,7 +6,8 @@ Utility scripts installed into `~/.claude/scripts/` by `bin/install.mjs`. Each s
|
|
|
6
6
|
|
|
7
7
|
| File | Purpose |
|
|
8
8
|
|---|---|
|
|
9
|
-
| `claude_chain_runner.py` | Runs a `claude` invocation through a
|
|
9
|
+
| `claude_chain_runner.py` | Runs a `claude` invocation through a usage-ranked fallback chain (`~/.claude/claude-chain.json`): probes weekly remaining once via `claude_chain_usage` / the usage-pause OAuth probe, tries the highest-remaining account first, and falls over to the next ranked binary only on a usage-limit failure; usable as an imported module (`run_claude`) or a CLI. Copy `claude-chain.example.json` to `~/.claude/claude-chain.json` and list your account binaries. Optional per-entry `credentials_path` names that account's OAuth credentials file for the usage probe |
|
|
10
|
+
| `claude_chain_usage.py` | Reports remaining weekly usage for every account in `~/.claude/claude-chain.json` via the usage-pause OAuth probe; prints JSON (`accounts` with `weekly_remaining_percent` or null plus `error`); importable `report_chain_weekly_usage` and `rank_accounts_by_weekly_remaining` (highest remaining first, ties keep config order, unmeasurable last). The chain runner consumes this ranking for try order |
|
|
10
11
|
| `gh_artifact_upload.py` | Uploads a file to a repo's durable `artifacts` prerelease under a timestamped asset name and prints the permanent download URL a GitHub post can link |
|
|
11
12
|
| `grok_headless_runner.py` | Runs one worker as headless `grok`: builds argv, mints a unique leader socket, captures streams, kills on timeout with grace, classifies ok/usage_limit/auth_failure/timeout/error; imported by `spawn_grok_batch.py` |
|
|
12
13
|
| `grok_worker_preflight.py` | Soft gate for the headless grok tier: binary on PATH, `grok models` auth, install manifest + role agents, opt-in cached live ping; non-zero exit is fallthrough, not failure |
|
package/scripts/check.ps1
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env pwsh
|
|
2
2
|
<#
|
|
3
3
|
.SYNOPSIS
|
|
4
|
-
One-shot quality gate
|
|
4
|
+
One-shot quality gate — runs ruff, hooks mypy, pr-loop mypy, and the
|
|
5
5
|
blocking pytest suite from a single entry point.
|
|
6
6
|
|
|
7
7
|
.DESCRIPTION
|
|
8
8
|
Resolves paths relative to $PSScriptRoot so the script works from any CWD
|
|
9
9
|
and from both the worktree (packages/claude-dev-env/scripts/check.ps1)
|
|
10
10
|
and the installed runtime (~/.claude/scripts/check.ps1, after install.mjs
|
|
11
|
-
propagates this file).
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
propagates this file). Tools: ruff over hooks/, mypy over hooks blocking
|
|
12
|
+
and validators, mypy-pr-loop over _shared/pr-loop/scripts production
|
|
13
|
+
modules, and optional pytest over the blocking enforcer suite. Each tool
|
|
14
|
+
runs sequentially; the first non-zero exit code is preserved as the
|
|
15
|
+
script's exit code so CI/pre-commit can short-circuit on the first failure.
|
|
14
16
|
|
|
15
17
|
.PARAMETER SkipTests
|
|
16
18
|
Skip the pytest run. Useful during local iteration when you want only the
|
|
17
19
|
static-analysis gates.
|
|
18
20
|
|
|
19
21
|
.PARAMETER SkipMypy
|
|
20
|
-
Skip
|
|
22
|
+
Skip both mypy runs (hooks mypy and mypy-pr-loop).
|
|
21
23
|
|
|
22
24
|
.PARAMETER SkipRuff
|
|
23
25
|
Skip the ruff run.
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
.OUTPUTS
|
|
26
28
|
Per-tool status lines on stdout. Final summary line:
|
|
27
29
|
CHECK: OK
|
|
28
|
-
CHECK: FAILED tools=ruff,mypy,pytest
|
|
30
|
+
CHECK: FAILED tools=ruff,mypy,mypy-pr-loop,pytest
|
|
29
31
|
#>
|
|
30
32
|
[CmdletBinding()]
|
|
31
33
|
param(
|
|
@@ -38,6 +40,7 @@ $ErrorActionPreference = 'Stop'
|
|
|
38
40
|
|
|
39
41
|
$hooksRoot = Resolve-Path (Join-Path $PSScriptRoot '..' 'hooks')
|
|
40
42
|
$blockingRoot = Join-Path $hooksRoot 'blocking'
|
|
43
|
+
$prLoopScriptsRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '_shared' 'pr-loop' 'scripts')
|
|
41
44
|
|
|
42
45
|
$failedTools = @()
|
|
43
46
|
$firstNonZeroExitCode = 0
|
|
@@ -82,6 +85,29 @@ if (-not $SkipMypy) {
|
|
|
82
85
|
Pop-Location
|
|
83
86
|
}
|
|
84
87
|
}
|
|
88
|
+
|
|
89
|
+
Invoke-Tool -Label 'mypy-pr-loop' -Action {
|
|
90
|
+
Push-Location $prLoopScriptsRoot
|
|
91
|
+
try {
|
|
92
|
+
mypy --config-file (Join-Path $prLoopScriptsRoot 'pyproject.toml') `
|
|
93
|
+
_claude_permissions_common.py `
|
|
94
|
+
code_rules_gate.py `
|
|
95
|
+
copilot_quota.py `
|
|
96
|
+
fix_hookspath.py `
|
|
97
|
+
grant_project_claude_permissions.py `
|
|
98
|
+
post_audit_thread.py `
|
|
99
|
+
preflight_self_heal.py `
|
|
100
|
+
preflight.py `
|
|
101
|
+
reviewer_availability.py `
|
|
102
|
+
reviews_disabled.py `
|
|
103
|
+
revoke_project_claude_permissions.py `
|
|
104
|
+
terminology_sweep.py `
|
|
105
|
+
code_rules_gate_parts `
|
|
106
|
+
pr_loop_shared_constants
|
|
107
|
+
} finally {
|
|
108
|
+
Pop-Location
|
|
109
|
+
}
|
|
110
|
+
}
|
|
85
111
|
}
|
|
86
112
|
|
|
87
113
|
if (-not $SkipTests) {
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"chain": [
|
|
3
3
|
{ "command": "claude", "extra_args": [] },
|
|
4
|
-
{
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
{
|
|
5
|
+
"command": "claude-1",
|
|
6
|
+
"extra_args": [],
|
|
7
|
+
"credentials_path": "/path/to/account-1/.credentials.json"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"command": "claude-2",
|
|
11
|
+
"extra_args": [],
|
|
12
|
+
"credentials_path": "/path/to/account-2/.credentials.json"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"command": "claude-3",
|
|
16
|
+
"extra_args": [],
|
|
17
|
+
"credentials_path": "/path/to/account-3/.credentials.json"
|
|
18
|
+
}
|
|
7
19
|
]
|
|
8
20
|
}
|