claude-dev-env 1.89.0 → 1.92.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 +0 -13
- package/_shared/pr-loop/scripts/code_rules_gate.py +106 -14
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +26 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +160 -0
- package/agents/clean-coder.md +2 -2
- package/agents/code-verifier.md +0 -1
- package/agents/test_agent_frontmatter.py +78 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -1
- package/bin/install.mjs +1 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/CLAUDE.md +6 -2
- package/hooks/blocking/code_rules_comments.py +2 -2
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +44 -0
- package/hooks/blocking/config/verified_commit_constants.py +2 -0
- package/hooks/blocking/conftest.py +115 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +191 -0
- package/hooks/blocking/pr_description_enforcer.py +46 -22
- package/hooks/blocking/pr_description_pr_number.py +5 -3
- package/hooks/blocking/pr_description_proof_of_work.py +367 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -1
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +8 -2
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +71 -0
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +168 -0
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +175 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +162 -0
- package/hooks/blocking/test_precommit_code_rules_gate.py +89 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -0
- package/hooks/blocking/test_verification_verdict_store.py +11 -0
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +49 -0
- package/hooks/blocking/test_verified_commit_gate.py +11 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +11 -0
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +210 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +6 -0
- package/hooks/blocking/verification_verdict_store.py +73 -5
- package/hooks/blocking/verified_commit_config_bootstrap.py +51 -0
- package/hooks/blocking/verified_commit_gate.py +6 -0
- package/hooks/blocking/verifier_verdict_minter.py +6 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +351 -0
- package/hooks/hooks.json +32 -2
- package/hooks/hooks_constants/CLAUDE.md +4 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +3 -0
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +59 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +111 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +48 -0
- package/hooks/lifecycle/CLAUDE.md +3 -1
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +146 -0
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +178 -0
- package/hooks/validators/run_all_validators.py +216 -4
- package/hooks/validators/test_run_all_validators_pretooluse.py +102 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +3 -0
- package/rules/durable-post-artifacts.md +35 -0
- package/rules/nas-ssh-invocation.md +21 -0
- package/rules/proof-of-work-pr-comments.md +26 -0
- package/scripts/CLAUDE.md +2 -0
- package/scripts/claude-chain.example.json +8 -0
- package/scripts/claude_chain_runner.py +400 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +124 -0
- package/scripts/dev_env_scripts_constants/gh_artifact_upload_constants.py +43 -0
- package/scripts/gh_artifact_upload.py +256 -0
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_claude_chain_runner.py +472 -0
- package/scripts/tests/test_gh_artifact_upload.py +205 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/team-advisor/SKILL.md +188 -0
- package/skills/team-advisor-refresh/SKILL.md +25 -0
- package/skills/usage-pause/SKILL.md +108 -0
- package/skills/usage-pause/scripts/resolve_usage_window.py +462 -0
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +278 -0
- package/skills/usage-pause/scripts/usage_pause_constants/__init__.py +1 -0
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +65 -0
- package/system-prompts/software-engineer.xml +3 -2
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""Behavioral tests for the claude fallback-chain runner."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
12
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
14
|
+
|
|
15
|
+
import claude_chain_runner as runner # noqa: E402
|
|
16
|
+
from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
17
|
+
ALL_USAGE_LIMIT_SIGNATURES,
|
|
18
|
+
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
19
|
+
ATTEMPT_STATUS_NONZERO_EXIT,
|
|
20
|
+
ATTEMPT_STATUS_SERVED,
|
|
21
|
+
ATTEMPT_STATUS_TIMEOUT,
|
|
22
|
+
ATTEMPT_STATUS_USAGE_LIMITED,
|
|
23
|
+
ATTEMPT_SUMMARY_ENTRY_TEMPLATE,
|
|
24
|
+
ATTEMPT_SUMMARY_JOIN_SEPARATOR,
|
|
25
|
+
CHAIN_CONFIG_ERROR_EXIT_CODE,
|
|
26
|
+
CHAIN_EXHAUSTED_EXIT_CODE,
|
|
27
|
+
CHAIN_EXHAUSTED_MESSAGE_TEMPLATE,
|
|
28
|
+
CLAUDE_HOME_SUBDIRECTORY,
|
|
29
|
+
CLI_ARGUMENTS_SEPARATOR,
|
|
30
|
+
CLI_TIMEOUT_FLAG,
|
|
31
|
+
CONFIG_CHAIN_EMPTY_REASON,
|
|
32
|
+
CONFIG_CHAIN_KEY,
|
|
33
|
+
CONFIG_CHAIN_NOT_LIST_REASON,
|
|
34
|
+
CONFIG_COMMAND_KEY,
|
|
35
|
+
CONFIG_ENTRY_COMMAND_MISSING_REASON,
|
|
36
|
+
CONFIG_ENTRY_EXTRA_ARGS_INVALID_REASON,
|
|
37
|
+
CONFIG_ENTRY_NOT_OBJECT_REASON,
|
|
38
|
+
CONFIG_EXTRA_ARGS_KEY,
|
|
39
|
+
CONFIG_FILENAME,
|
|
40
|
+
CONFIG_INVALID_SHAPE_MESSAGE_TEMPLATE,
|
|
41
|
+
CONFIG_MALFORMED_MESSAGE_TEMPLATE,
|
|
42
|
+
CONFIG_MISSING_MESSAGE_TEMPLATE,
|
|
43
|
+
CONFIG_NOT_OBJECT_REASON,
|
|
44
|
+
CONFIG_UNREADABLE_MESSAGE_TEMPLATE,
|
|
45
|
+
DEFAULT_TIMEOUT_SECONDS,
|
|
46
|
+
EXAMPLE_CONFIG_FILENAME,
|
|
47
|
+
NO_COMPLETED_PROCESS_RETURN_CODE,
|
|
48
|
+
UTF8_ENCODING,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_A_SIGNATURE = ALL_USAGE_LIMIT_SIGNATURES[0]
|
|
52
|
+
_PROMPT_ARGUMENTS = ["-p", "hello"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _completed(command, returncode, stdout="", stderr=""):
|
|
56
|
+
return subprocess.CompletedProcess(
|
|
57
|
+
args=[command], returncode=returncode, stdout=stdout, stderr=stderr
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _entry(command, extra_args=None):
|
|
62
|
+
return {
|
|
63
|
+
CONFIG_COMMAND_KEY: command,
|
|
64
|
+
CONFIG_EXTRA_ARGS_KEY: extra_args if extra_args is not None else [],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _write_chain_config(tmp_path, chain_entries):
|
|
69
|
+
config_file = tmp_path / CONFIG_FILENAME
|
|
70
|
+
config_file.write_text(
|
|
71
|
+
json.dumps({CONFIG_CHAIN_KEY: chain_entries}), encoding=UTF8_ENCODING
|
|
72
|
+
)
|
|
73
|
+
return config_file
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _Recorder:
|
|
77
|
+
def __init__(self, behavior_by_command):
|
|
78
|
+
self.behavior_by_command = behavior_by_command
|
|
79
|
+
self.invocations = []
|
|
80
|
+
self.timeouts = []
|
|
81
|
+
|
|
82
|
+
def __call__(self, invocation, **keyword_arguments):
|
|
83
|
+
self.invocations.append(invocation)
|
|
84
|
+
self.timeouts.append(keyword_arguments.get("timeout"))
|
|
85
|
+
behavior = self.behavior_by_command[invocation[0]]
|
|
86
|
+
if isinstance(behavior, BaseException):
|
|
87
|
+
raise behavior
|
|
88
|
+
return behavior
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _install(monkeypatch, config_file, behavior_by_command):
|
|
92
|
+
recorder = _Recorder(behavior_by_command)
|
|
93
|
+
monkeypatch.setattr(runner, "chain_config_path", lambda: config_file)
|
|
94
|
+
monkeypatch.setattr(runner, "chain_subprocess_runner", recorder)
|
|
95
|
+
return recorder
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_usage_limited_primary_falls_over_to_second(
|
|
99
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
100
|
+
) -> None:
|
|
101
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
102
|
+
_install(
|
|
103
|
+
monkeypatch,
|
|
104
|
+
config_file,
|
|
105
|
+
{
|
|
106
|
+
"claude": _completed("claude", 1, stderr=_A_SIGNATURE),
|
|
107
|
+
"claude-ev": _completed("claude-ev", 0, stdout="ok"),
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
111
|
+
assert chain_result.served_command == "claude-ev"
|
|
112
|
+
assert chain_result.returncode == 0
|
|
113
|
+
assert chain_result.stdout == "ok"
|
|
114
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
115
|
+
"claude",
|
|
116
|
+
"claude-ev",
|
|
117
|
+
]
|
|
118
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_USAGE_LIMITED
|
|
119
|
+
assert chain_result.attempts[1].status == ATTEMPT_STATUS_SERVED
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_nonzero_exit_without_signature_does_not_fall_over(
|
|
123
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
124
|
+
) -> None:
|
|
125
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
126
|
+
_install(
|
|
127
|
+
monkeypatch,
|
|
128
|
+
config_file,
|
|
129
|
+
{
|
|
130
|
+
"claude": _completed("claude", 2, stderr="unknown flag"),
|
|
131
|
+
"claude-ev": _completed("claude-ev", 0, stdout="should not run"),
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
135
|
+
assert chain_result.served_command == "claude"
|
|
136
|
+
assert chain_result.returncode == 2
|
|
137
|
+
assert len(chain_result.attempts) == 1
|
|
138
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_NONZERO_EXIT
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_timeout_on_primary_does_not_fall_over(
|
|
142
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
143
|
+
) -> None:
|
|
144
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
145
|
+
_install(
|
|
146
|
+
monkeypatch,
|
|
147
|
+
config_file,
|
|
148
|
+
{
|
|
149
|
+
"claude": subprocess.TimeoutExpired(cmd=["claude"], timeout=5),
|
|
150
|
+
"claude-ev": _completed("claude-ev", 0),
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
154
|
+
assert chain_result.served_command is None
|
|
155
|
+
assert chain_result.returncode == NO_COMPLETED_PROCESS_RETURN_CODE
|
|
156
|
+
assert len(chain_result.attempts) == 1
|
|
157
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_TIMEOUT
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_missing_primary_binary_does_not_fall_over(
|
|
161
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
162
|
+
) -> None:
|
|
163
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
164
|
+
_install(
|
|
165
|
+
monkeypatch,
|
|
166
|
+
config_file,
|
|
167
|
+
{
|
|
168
|
+
"claude": FileNotFoundError(),
|
|
169
|
+
"claude-ev": _completed("claude-ev", 0),
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
173
|
+
assert chain_result.served_command is None
|
|
174
|
+
assert len(chain_result.attempts) == 1
|
|
175
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_missing_fallback_binary_is_skipped_and_walk_continues(
|
|
179
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
180
|
+
) -> None:
|
|
181
|
+
config_file = _write_chain_config(
|
|
182
|
+
tmp_path, [_entry("claude"), _entry("claude-ev"), _entry("claude-editor")]
|
|
183
|
+
)
|
|
184
|
+
_install(
|
|
185
|
+
monkeypatch,
|
|
186
|
+
config_file,
|
|
187
|
+
{
|
|
188
|
+
"claude": _completed("claude", 1, stderr=_A_SIGNATURE),
|
|
189
|
+
"claude-ev": FileNotFoundError(),
|
|
190
|
+
"claude-editor": _completed("claude-editor", 0, stdout="done"),
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
194
|
+
assert chain_result.served_command == "claude-editor"
|
|
195
|
+
assert [each_attempt.status for each_attempt in chain_result.attempts] == [
|
|
196
|
+
ATTEMPT_STATUS_USAGE_LIMITED,
|
|
197
|
+
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
198
|
+
ATTEMPT_STATUS_SERVED,
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_zero_exit_mentioning_usage_limit_is_success(
|
|
203
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
204
|
+
) -> None:
|
|
205
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
206
|
+
_install(
|
|
207
|
+
monkeypatch,
|
|
208
|
+
config_file,
|
|
209
|
+
{
|
|
210
|
+
"claude": _completed("claude", 0, stdout=f"note: {_A_SIGNATURE} earlier"),
|
|
211
|
+
"claude-ev": _completed("claude-ev", 0),
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
215
|
+
assert chain_result.served_command == "claude"
|
|
216
|
+
assert chain_result.returncode == 0
|
|
217
|
+
assert len(chain_result.attempts) == 1
|
|
218
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_SERVED
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_exhausted_chain_records_every_attempt(
|
|
222
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
223
|
+
) -> None:
|
|
224
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
225
|
+
_install(
|
|
226
|
+
monkeypatch,
|
|
227
|
+
config_file,
|
|
228
|
+
{
|
|
229
|
+
"claude": _completed("claude", 1, stderr=_A_SIGNATURE),
|
|
230
|
+
"claude-ev": _completed("claude-ev", 1, stderr=_A_SIGNATURE),
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
234
|
+
assert chain_result.served_command is None
|
|
235
|
+
assert chain_result.returncode == 1
|
|
236
|
+
assert [each_attempt.status for each_attempt in chain_result.attempts] == [
|
|
237
|
+
ATTEMPT_STATUS_USAGE_LIMITED,
|
|
238
|
+
ATTEMPT_STATUS_USAGE_LIMITED,
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_reordering_config_changes_walk_order(
|
|
243
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
244
|
+
) -> None:
|
|
245
|
+
behavior_by_command = {
|
|
246
|
+
"claude": _completed("claude", 0, stdout="from-claude"),
|
|
247
|
+
"claude-ev": _completed("claude-ev", 0, stdout="from-ev"),
|
|
248
|
+
}
|
|
249
|
+
first_config = tmp_path / "first"
|
|
250
|
+
first_config.mkdir()
|
|
251
|
+
config_a = _write_chain_config(
|
|
252
|
+
first_config, [_entry("claude"), _entry("claude-ev")]
|
|
253
|
+
)
|
|
254
|
+
_install(monkeypatch, config_a, behavior_by_command)
|
|
255
|
+
result_a = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
256
|
+
assert result_a.served_command == "claude"
|
|
257
|
+
|
|
258
|
+
second_config = tmp_path / "second"
|
|
259
|
+
second_config.mkdir()
|
|
260
|
+
config_b = _write_chain_config(
|
|
261
|
+
second_config, [_entry("claude-ev"), _entry("claude")]
|
|
262
|
+
)
|
|
263
|
+
_install(monkeypatch, config_b, behavior_by_command)
|
|
264
|
+
result_b = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
265
|
+
assert result_b.served_command == "claude-ev"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_extra_args_are_appended_to_invocation(
|
|
269
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
270
|
+
) -> None:
|
|
271
|
+
config_file = _write_chain_config(
|
|
272
|
+
tmp_path, [_entry("claude", extra_args=["--account", "ev"])]
|
|
273
|
+
)
|
|
274
|
+
recorder = _install(monkeypatch, config_file, {"claude": _completed("claude", 0)})
|
|
275
|
+
runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
276
|
+
assert recorder.invocations[0] == ["claude", "-p", "hello", "--account", "ev"]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_signature_matching_is_case_insensitive(
|
|
280
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
281
|
+
) -> None:
|
|
282
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
283
|
+
_install(
|
|
284
|
+
monkeypatch,
|
|
285
|
+
config_file,
|
|
286
|
+
{
|
|
287
|
+
"claude": _completed("claude", 1, stderr=_A_SIGNATURE.upper()),
|
|
288
|
+
"claude-ev": _completed("claude-ev", 0),
|
|
289
|
+
},
|
|
290
|
+
)
|
|
291
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
292
|
+
assert chain_result.served_command == "claude-ev"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_cli_passthrough_builds_argument_list_and_timeout(
|
|
296
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
297
|
+
) -> None:
|
|
298
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude")])
|
|
299
|
+
recorder = _install(monkeypatch, config_file, {"claude": _completed("claude", 0)})
|
|
300
|
+
exit_code = runner.main(
|
|
301
|
+
[CLI_TIMEOUT_FLAG, "9", CLI_ARGUMENTS_SEPARATOR, "-p", "hi"]
|
|
302
|
+
)
|
|
303
|
+
assert exit_code == 0
|
|
304
|
+
assert recorder.invocations[0] == ["claude", "-p", "hi"]
|
|
305
|
+
assert recorder.timeouts[0] == 9
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_cli_uses_default_timeout_when_flag_omitted(
|
|
309
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
310
|
+
) -> None:
|
|
311
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude")])
|
|
312
|
+
recorder = _install(monkeypatch, config_file, {"claude": _completed("claude", 0)})
|
|
313
|
+
runner.main([CLI_ARGUMENTS_SEPARATOR, "-p", "hi"])
|
|
314
|
+
assert recorder.timeouts[0] == DEFAULT_TIMEOUT_SECONDS
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_cli_served_nonzero_exit_is_passed_through(
|
|
318
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
319
|
+
) -> None:
|
|
320
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude")])
|
|
321
|
+
_install(
|
|
322
|
+
monkeypatch,
|
|
323
|
+
config_file,
|
|
324
|
+
{"claude": _completed("claude", 7, stdout="out", stderr="err")},
|
|
325
|
+
)
|
|
326
|
+
exit_code = runner.main([CLI_ARGUMENTS_SEPARATOR, "-p", "hi"])
|
|
327
|
+
captured = capsys.readouterr()
|
|
328
|
+
assert exit_code == 7
|
|
329
|
+
assert captured.out == "out"
|
|
330
|
+
assert "err" in captured.err
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_cli_exhausted_chain_exits_nonzero_with_attempt_summary(
|
|
334
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
335
|
+
) -> None:
|
|
336
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
337
|
+
_install(
|
|
338
|
+
monkeypatch,
|
|
339
|
+
config_file,
|
|
340
|
+
{
|
|
341
|
+
"claude": _completed("claude", 1, stderr=_A_SIGNATURE),
|
|
342
|
+
"claude-ev": _completed("claude-ev", 1, stderr=_A_SIGNATURE),
|
|
343
|
+
},
|
|
344
|
+
)
|
|
345
|
+
exit_code = runner.main([CLI_ARGUMENTS_SEPARATOR, "-p", "hi"])
|
|
346
|
+
captured = capsys.readouterr()
|
|
347
|
+
assert exit_code == CHAIN_EXHAUSTED_EXIT_CODE
|
|
348
|
+
expected_summary = ATTEMPT_SUMMARY_JOIN_SEPARATOR.join(
|
|
349
|
+
ATTEMPT_SUMMARY_ENTRY_TEMPLATE.format(
|
|
350
|
+
command=each_command, status=ATTEMPT_STATUS_USAGE_LIMITED
|
|
351
|
+
)
|
|
352
|
+
for each_command in ("claude", "claude-ev")
|
|
353
|
+
)
|
|
354
|
+
assert (
|
|
355
|
+
CHAIN_EXHAUSTED_MESSAGE_TEMPLATE.format(attempt_summary=expected_summary)
|
|
356
|
+
in captured.err
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_cli_missing_config_exits_config_error(
|
|
361
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
362
|
+
) -> None:
|
|
363
|
+
missing_config = tmp_path / CONFIG_FILENAME
|
|
364
|
+
monkeypatch.setattr(runner, "chain_config_path", lambda: missing_config)
|
|
365
|
+
exit_code = runner.main([CLI_ARGUMENTS_SEPARATOR, "-p", "hi"])
|
|
366
|
+
captured = capsys.readouterr()
|
|
367
|
+
assert exit_code == CHAIN_CONFIG_ERROR_EXIT_CODE
|
|
368
|
+
assert EXAMPLE_CONFIG_FILENAME in captured.err
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_missing_config_raises_naming_path_and_example(tmp_path: Path) -> None:
|
|
372
|
+
missing_config = tmp_path / CONFIG_FILENAME
|
|
373
|
+
with pytest.raises(runner.ChainConfigurationError) as raised:
|
|
374
|
+
runner.load_chain(missing_config)
|
|
375
|
+
assert str(raised.value) == CONFIG_MISSING_MESSAGE_TEMPLATE.format(
|
|
376
|
+
config_path=missing_config, example_filename=EXAMPLE_CONFIG_FILENAME
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def test_malformed_json_raises_configuration_error(tmp_path: Path) -> None:
|
|
381
|
+
config_file = tmp_path / CONFIG_FILENAME
|
|
382
|
+
config_file.write_text("{ not valid json", encoding=UTF8_ENCODING)
|
|
383
|
+
with pytest.raises(runner.ChainConfigurationError) as raised:
|
|
384
|
+
runner.load_chain(config_file)
|
|
385
|
+
malformed_prefix = CONFIG_MALFORMED_MESSAGE_TEMPLATE.split("{config_path}")[0]
|
|
386
|
+
assert malformed_prefix in str(raised.value)
|
|
387
|
+
assert EXAMPLE_CONFIG_FILENAME in str(raised.value)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def test_unreadable_config_raises_configuration_error(tmp_path: Path) -> None:
|
|
391
|
+
config_file = tmp_path / CONFIG_FILENAME
|
|
392
|
+
config_file.write_text("{}", encoding=UTF8_ENCODING)
|
|
393
|
+
with patch.object(Path, "read_text", side_effect=OSError("locked")):
|
|
394
|
+
with pytest.raises(runner.ChainConfigurationError) as raised:
|
|
395
|
+
runner.load_chain(config_file)
|
|
396
|
+
unreadable_prefix = CONFIG_UNREADABLE_MESSAGE_TEMPLATE.split("{config_path}")[0]
|
|
397
|
+
assert unreadable_prefix in str(raised.value)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def test_top_level_not_object_raises_invalid_shape(tmp_path: Path) -> None:
|
|
401
|
+
config_file = tmp_path / CONFIG_FILENAME
|
|
402
|
+
config_file.write_text(json.dumps(["claude"]), encoding=UTF8_ENCODING)
|
|
403
|
+
with pytest.raises(runner.ChainConfigurationError) as raised:
|
|
404
|
+
runner.load_chain(config_file)
|
|
405
|
+
assert str(raised.value) == CONFIG_INVALID_SHAPE_MESSAGE_TEMPLATE.format(
|
|
406
|
+
config_path=config_file,
|
|
407
|
+
reason=CONFIG_NOT_OBJECT_REASON,
|
|
408
|
+
example_filename=EXAMPLE_CONFIG_FILENAME,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_chain_key_not_list_raises_invalid_shape(tmp_path: Path) -> None:
|
|
413
|
+
config_file = tmp_path / CONFIG_FILENAME
|
|
414
|
+
config_file.write_text(
|
|
415
|
+
json.dumps({CONFIG_CHAIN_KEY: "claude"}), encoding=UTF8_ENCODING
|
|
416
|
+
)
|
|
417
|
+
with pytest.raises(runner.ChainConfigurationError) as raised:
|
|
418
|
+
runner.load_chain(config_file)
|
|
419
|
+
assert CONFIG_CHAIN_NOT_LIST_REASON in str(raised.value)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def test_empty_chain_raises_invalid_shape(tmp_path: Path) -> None:
|
|
423
|
+
config_file = _write_chain_config(tmp_path, [])
|
|
424
|
+
with pytest.raises(runner.ChainConfigurationError) as raised:
|
|
425
|
+
runner.load_chain(config_file)
|
|
426
|
+
assert CONFIG_CHAIN_EMPTY_REASON in str(raised.value)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def test_entry_not_object_raises_invalid_shape(tmp_path: Path) -> None:
|
|
430
|
+
config_file = _write_chain_config(tmp_path, ["claude"])
|
|
431
|
+
with pytest.raises(runner.ChainConfigurationError) as raised:
|
|
432
|
+
runner.load_chain(config_file)
|
|
433
|
+
assert CONFIG_ENTRY_NOT_OBJECT_REASON in str(raised.value)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def test_entry_without_command_raises_invalid_shape(tmp_path: Path) -> None:
|
|
437
|
+
config_file = _write_chain_config(tmp_path, [{CONFIG_EXTRA_ARGS_KEY: []}])
|
|
438
|
+
with pytest.raises(runner.ChainConfigurationError) as raised:
|
|
439
|
+
runner.load_chain(config_file)
|
|
440
|
+
assert CONFIG_ENTRY_COMMAND_MISSING_REASON in str(raised.value)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def test_entry_with_non_list_extra_args_raises_invalid_shape(tmp_path: Path) -> None:
|
|
444
|
+
config_file = _write_chain_config(
|
|
445
|
+
tmp_path, [{CONFIG_COMMAND_KEY: "claude", CONFIG_EXTRA_ARGS_KEY: "nope"}]
|
|
446
|
+
)
|
|
447
|
+
with pytest.raises(runner.ChainConfigurationError) as raised:
|
|
448
|
+
runner.load_chain(config_file)
|
|
449
|
+
assert CONFIG_ENTRY_EXTRA_ARGS_INVALID_REASON in str(raised.value)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def test_extra_args_default_to_empty_when_omitted(tmp_path: Path) -> None:
|
|
453
|
+
config_file = _write_chain_config(tmp_path, [{CONFIG_COMMAND_KEY: "claude"}])
|
|
454
|
+
all_entries = runner.load_chain(config_file)
|
|
455
|
+
assert all_entries[0].command == "claude"
|
|
456
|
+
assert all_entries[0].extra_args == ()
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def test_load_chain_parses_command_and_extra_args(tmp_path: Path) -> None:
|
|
460
|
+
config_file = _write_chain_config(
|
|
461
|
+
tmp_path, [_entry("claude", extra_args=["--account", "ev"])]
|
|
462
|
+
)
|
|
463
|
+
all_entries = runner.load_chain(config_file)
|
|
464
|
+
assert all_entries == [
|
|
465
|
+
runner.ChainEntry(command="claude", extra_args=("--account", "ev"))
|
|
466
|
+
]
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def test_chain_config_path_points_at_home_config() -> None:
|
|
470
|
+
config_path = runner.chain_config_path()
|
|
471
|
+
assert config_path.name == CONFIG_FILENAME
|
|
472
|
+
assert CLAUDE_HOME_SUBDIRECTORY in config_path.parts
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Behavioral tests for gh_artifact_upload.py using a stubbed GitHub CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import types
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
14
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
16
|
+
|
|
17
|
+
import gh_artifact_upload as mod
|
|
18
|
+
|
|
19
|
+
_DEFAULT_READBACK_ASSET = {
|
|
20
|
+
"name": "20260707_140233_contact_sheet.png",
|
|
21
|
+
"url": (
|
|
22
|
+
"https://github.com/owner/repo/releases/download/artifacts/"
|
|
23
|
+
"20260707_140233_contact_sheet.png"
|
|
24
|
+
),
|
|
25
|
+
"createdAt": "2026-07-07T14:02:33Z",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _make_gh_stub(
|
|
30
|
+
recorded_calls: list[list[str]],
|
|
31
|
+
view_return_code: int,
|
|
32
|
+
all_readback_assets: list[dict[str, str]] | None = None,
|
|
33
|
+
) -> object:
|
|
34
|
+
readback_assets = (
|
|
35
|
+
[_DEFAULT_READBACK_ASSET] if all_readback_assets is None else all_readback_assets
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def fake_run(
|
|
39
|
+
all_command_arguments: list[str],
|
|
40
|
+
**_keyword_arguments: object,
|
|
41
|
+
) -> types.SimpleNamespace:
|
|
42
|
+
recorded_calls.append(all_command_arguments)
|
|
43
|
+
is_release_view = "view" in all_command_arguments
|
|
44
|
+
is_asset_read_back = is_release_view and "assets" in all_command_arguments
|
|
45
|
+
if is_asset_read_back:
|
|
46
|
+
return types.SimpleNamespace(
|
|
47
|
+
returncode=0,
|
|
48
|
+
stdout=json.dumps({"assets": readback_assets}),
|
|
49
|
+
stderr="",
|
|
50
|
+
)
|
|
51
|
+
return_code = view_return_code if is_release_view else 0
|
|
52
|
+
return types.SimpleNamespace(returncode=return_code, stdout="", stderr="")
|
|
53
|
+
|
|
54
|
+
return fake_run
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_timestamped_asset_name_prefixes_basename() -> None:
|
|
58
|
+
asset_name = mod.timestamped_asset_name(r"C:\stage\contact_sheet.png")
|
|
59
|
+
assert asset_name.endswith("_contact_sheet.png")
|
|
60
|
+
assert len(asset_name) > len("_contact_sheet.png")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_upload_artifact_returns_the_readback_url(
|
|
64
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
65
|
+
) -> None:
|
|
66
|
+
source_file = tmp_path / "contact_sheet.png"
|
|
67
|
+
source_file.write_bytes(b"binary")
|
|
68
|
+
recorded_calls: list[list[str]] = []
|
|
69
|
+
monkeypatch.setattr(
|
|
70
|
+
subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=0)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
asset_url = mod.upload_artifact(str(source_file), "owner/repo")
|
|
74
|
+
|
|
75
|
+
assert asset_url == _DEFAULT_READBACK_ASSET["url"]
|
|
76
|
+
assert any("assets" in call for call in recorded_calls)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_upload_artifact_prints_the_sanitized_url_github_serves(
|
|
80
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
81
|
+
) -> None:
|
|
82
|
+
source_file = tmp_path / "My Report.png"
|
|
83
|
+
source_file.write_bytes(b"binary")
|
|
84
|
+
sanitized_asset = {
|
|
85
|
+
"name": "20260707_140233_My.Report.png",
|
|
86
|
+
"url": (
|
|
87
|
+
"https://github.com/owner/repo/releases/download/artifacts/"
|
|
88
|
+
"20260707_140233_My.Report.png"
|
|
89
|
+
),
|
|
90
|
+
"createdAt": "2026-07-07T14:02:33Z",
|
|
91
|
+
}
|
|
92
|
+
monkeypatch.setattr(
|
|
93
|
+
subprocess,
|
|
94
|
+
"run",
|
|
95
|
+
_make_gh_stub([], view_return_code=0, all_readback_assets=[sanitized_asset]),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
asset_url = mod.upload_artifact(str(source_file), "owner/repo")
|
|
99
|
+
|
|
100
|
+
assert asset_url == sanitized_asset["url"]
|
|
101
|
+
assert " " not in asset_url
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_upload_artifact_returns_the_newest_asset_url(
|
|
105
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
106
|
+
) -> None:
|
|
107
|
+
source_file = tmp_path / "out.png"
|
|
108
|
+
source_file.write_bytes(b"binary")
|
|
109
|
+
older_asset = {
|
|
110
|
+
"name": "20260707_140000_out.png",
|
|
111
|
+
"url": "https://github.com/owner/repo/releases/download/artifacts/old.png",
|
|
112
|
+
"createdAt": "2026-07-07T14:00:00Z",
|
|
113
|
+
}
|
|
114
|
+
newest_asset = {
|
|
115
|
+
"name": "20260707_140233_out.png",
|
|
116
|
+
"url": "https://github.com/owner/repo/releases/download/artifacts/new.png",
|
|
117
|
+
"createdAt": "2026-07-07T14:02:33Z",
|
|
118
|
+
}
|
|
119
|
+
monkeypatch.setattr(
|
|
120
|
+
subprocess,
|
|
121
|
+
"run",
|
|
122
|
+
_make_gh_stub(
|
|
123
|
+
[], view_return_code=0, all_readback_assets=[older_asset, newest_asset]
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
asset_url = mod.upload_artifact(str(source_file), "owner/repo")
|
|
128
|
+
|
|
129
|
+
assert asset_url == newest_asset["url"]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_upload_artifact_raises_when_asset_missing_on_readback(
|
|
133
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
134
|
+
) -> None:
|
|
135
|
+
source_file = tmp_path / "out.png"
|
|
136
|
+
source_file.write_bytes(b"binary")
|
|
137
|
+
monkeypatch.setattr(
|
|
138
|
+
subprocess, "run", _make_gh_stub([], view_return_code=0, all_readback_assets=[])
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
with pytest.raises(mod.ArtifactUploadError):
|
|
142
|
+
mod.upload_artifact(str(source_file), "owner/repo")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_upload_artifact_uploads_to_tag_without_clobber(
|
|
146
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
147
|
+
) -> None:
|
|
148
|
+
source_file = tmp_path / "out.png"
|
|
149
|
+
source_file.write_bytes(b"binary")
|
|
150
|
+
recorded_calls: list[list[str]] = []
|
|
151
|
+
monkeypatch.setattr(
|
|
152
|
+
subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=0)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
mod.upload_artifact(str(source_file), "owner/repo")
|
|
156
|
+
|
|
157
|
+
upload_call = next(call for call in recorded_calls if "upload" in call)
|
|
158
|
+
assert "artifacts" in upload_call
|
|
159
|
+
assert "--clobber" not in upload_call
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_upload_artifact_creates_release_when_absent(
|
|
163
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
164
|
+
) -> None:
|
|
165
|
+
source_file = tmp_path / "out.png"
|
|
166
|
+
source_file.write_bytes(b"binary")
|
|
167
|
+
recorded_calls: list[list[str]] = []
|
|
168
|
+
monkeypatch.setattr(
|
|
169
|
+
subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=1)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
mod.upload_artifact(str(source_file), "owner/repo")
|
|
173
|
+
|
|
174
|
+
assert any("create" in call for call in recorded_calls)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_artifacts_release_exists_true_when_view_succeeds(
|
|
178
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
179
|
+
) -> None:
|
|
180
|
+
monkeypatch.setattr(subprocess, "run", _make_gh_stub([], view_return_code=0))
|
|
181
|
+
assert mod.artifacts_release_exists("owner/repo") is True
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_artifacts_release_exists_false_when_view_fails(
|
|
185
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
186
|
+
) -> None:
|
|
187
|
+
monkeypatch.setattr(subprocess, "run", _make_gh_stub([], view_return_code=1))
|
|
188
|
+
assert mod.artifacts_release_exists("owner/repo") is False
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_ensure_artifacts_release_skips_create_when_present(
|
|
192
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
193
|
+
) -> None:
|
|
194
|
+
recorded_calls: list[list[str]] = []
|
|
195
|
+
monkeypatch.setattr(
|
|
196
|
+
subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=0)
|
|
197
|
+
)
|
|
198
|
+
mod.ensure_artifacts_release("owner/repo")
|
|
199
|
+
assert not any("create" in call for call in recorded_calls)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_upload_artifact_missing_file_raises(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
203
|
+
monkeypatch.setattr(subprocess, "run", _make_gh_stub([], view_return_code=0))
|
|
204
|
+
with pytest.raises(mod.ArtifactUploadError):
|
|
205
|
+
mod.upload_artifact("does_not_exist_9f3a.png", "owner/repo")
|
package/skills/CLAUDE.md
CHANGED
|
@@ -26,6 +26,8 @@ Skills install to `~/.claude/skills/<skill-name>/` via `packages/claude-dev-env/
|
|
|
26
26
|
- `implement` — structured implementation from an existing plan packet
|
|
27
27
|
- `bdd-protocol` — BDD depth: Example Mapping, scenario quality, outside-in layout
|
|
28
28
|
- `verified-build` — build + test loop that gates on a verifier verdict
|
|
29
|
+
- `team-advisor` — turns the session into the advisor-orchestrator: it spawns executor subagents to do the code edits and test runs, and answers a blocked executor with a plan, correction, or stop
|
|
30
|
+
- `team-advisor-refresh` — sub-skill fired by the `/team-advisor` loop to re-assert the executor-advisor discipline mid-run
|
|
29
31
|
|
|
30
32
|
**PR review and convergence**
|
|
31
33
|
- `autoconverge` — autonomous single-run workflow that drives a PR to ready
|
|
@@ -66,6 +68,7 @@ Skills install to `~/.claude/skills/<skill-name>/` via `packages/claude-dev-env/
|
|
|
66
68
|
- `monitor-open-prs` — polls open PRs for status
|
|
67
69
|
- `pre-compact` — compact-safe session handoff
|
|
68
70
|
- `qbug` — required baseline PR audit; one clean-coder subagent loops audit → fix → commit → push until clean or stuck
|
|
71
|
+
- `usage-pause` — waits out the 5-hour usage window in ScheduleWakeup stages that keep agent contexts warm; probes the OAuth usage endpoint or takes a manual reset override
|
|
69
72
|
- `skill-builder` — complete skill-building lifecycle
|
|
70
73
|
- `auditing-claude-config` — audits a Claude Code setup for context-budget waste and produces a migration table with savings
|
|
71
74
|
- `log-audit` — background agent that audits this repo's own logs for recurring errors and timing regressions and files grouped fixes
|