claude-dev-env 8.13.0 → 8.15.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/.agents/skills/everything-search/SKILL.md +1 -1
- package/.agents/skills/second-account-workers/.claude/CLAUDE.md +1 -0
- package/.agents/skills/second-account-workers/AGENTS.md +1 -0
- package/.agents/skills/second-account-workers/SKILL.md +65 -0
- package/.agents/skills/team-advisor/SKILL.md +1 -1
- package/_shared/advisor/advisor-protocol.md +19 -19
- package/_shared/advisor/reference/advisor-block.md +1 -1
- package/_shared/advisor/reference/astra-rung.md +6 -6
- package/_shared/advisor/reference/identity.md +4 -4
- package/_shared/advisor/reference/lifecycle.md +2 -2
- package/_shared/advisor/reference/spawn-walk-log.md +4 -4
- package/_shared/advisor/reference/third-party-bind.md +6 -6
- package/_shared/advisor/reference/warm-up.md +3 -3
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +2 -2
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -3
- package/_shared/advisor/scripts/model_tier_run_validator.py +8 -9
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +91 -91
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install.prompt-context.test.mjs +42 -0
- package/docs/DJANGO_PATTERNS.md +2 -2
- package/docs/TEST_QUALITY.md +1 -13
- package/docs/second-claude-account.md +4 -0
- package/hooks/hooks.json +1 -6
- package/hooks/session/style_reminder_prompt.py +18 -4
- package/hooks/session/test_style_reminder_prompt.py +57 -3
- package/package.json +1 -1
- package/rules/research-mode.md +1 -1
- package/scripts/claude_account_worker.py +347 -0
- package/scripts/claude_account_worker_process.py +169 -0
- package/scripts/claude_account_worker_report.py +170 -0
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +139 -0
- package/scripts/grok_headless_runner.py +2 -5
- package/scripts/subprocess_stream_text.py +28 -0
- package/scripts/task_usage_events.py +387 -0
- package/scripts/task_usage_report.py +391 -0
- package/scripts/test_claude_account_worker.py +331 -0
- package/scripts/test_task_usage_events.py +47 -0
- package/scripts/test_task_usage_report.py +603 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
import claude_account_worker
|
|
12
|
+
from claude_account_worker import (
|
|
13
|
+
AccountSelection,
|
|
14
|
+
WorkerDependencies,
|
|
15
|
+
build_argument_parser,
|
|
16
|
+
select_account,
|
|
17
|
+
run_worker,
|
|
18
|
+
)
|
|
19
|
+
from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
20
|
+
CHOICE_MAIN,
|
|
21
|
+
CHOICE_SECOND,
|
|
22
|
+
CHOICE_WAIT,
|
|
23
|
+
CLAUDE_CONFIG_DIR_ENV_VAR,
|
|
24
|
+
STDOUT_TAIL_CHARACTER_LIMIT,
|
|
25
|
+
TIMEOUT_EXIT_CODE,
|
|
26
|
+
WAIT_EXIT_CODE,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FakeProcess:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
stdout: str = '{"result":"ready","is_error":false}',
|
|
35
|
+
returncode: int = 0,
|
|
36
|
+
timeout_first: bool = False,
|
|
37
|
+
timeout_output: str = "",
|
|
38
|
+
drain_timeout_count: int = 0,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.stdout = stdout
|
|
41
|
+
self.returncode = returncode
|
|
42
|
+
self.timeout_first = timeout_first
|
|
43
|
+
self.timeout_output = timeout_output
|
|
44
|
+
self.drain_timeout_count = drain_timeout_count
|
|
45
|
+
self.pid = 42
|
|
46
|
+
self.communicate_calls: list[tuple[str | None, float | None]] = []
|
|
47
|
+
self.terminated = False
|
|
48
|
+
|
|
49
|
+
def communicate(
|
|
50
|
+
self,
|
|
51
|
+
input: str | None = None,
|
|
52
|
+
timeout: float | None = None,
|
|
53
|
+
) -> tuple[str, str]:
|
|
54
|
+
self.communicate_calls.append((input, timeout))
|
|
55
|
+
if self.timeout_first and len(self.communicate_calls) == 1:
|
|
56
|
+
raise subprocess.TimeoutExpired(
|
|
57
|
+
cmd="claude",
|
|
58
|
+
timeout=timeout,
|
|
59
|
+
output=self.timeout_output,
|
|
60
|
+
)
|
|
61
|
+
if self.drain_timeout_count:
|
|
62
|
+
self.drain_timeout_count -= 1
|
|
63
|
+
raise subprocess.TimeoutExpired(
|
|
64
|
+
cmd="claude",
|
|
65
|
+
timeout=timeout,
|
|
66
|
+
output=self.timeout_output,
|
|
67
|
+
)
|
|
68
|
+
return self.stdout, ""
|
|
69
|
+
|
|
70
|
+
def poll(self) -> int | None:
|
|
71
|
+
return self.returncode if self.terminated else None
|
|
72
|
+
|
|
73
|
+
def kill(self) -> None:
|
|
74
|
+
self.terminated = True
|
|
75
|
+
self.returncode = -9
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _selection(
|
|
79
|
+
account: str,
|
|
80
|
+
*,
|
|
81
|
+
config_dir: Path | None = None,
|
|
82
|
+
reason: str = "selected by test",
|
|
83
|
+
) -> AccountSelection:
|
|
84
|
+
return AccountSelection(account=account, config_dir=config_dir, reason=reason)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _make_runner(
|
|
88
|
+
process: FakeProcess,
|
|
89
|
+
captured: dict[str, Any],
|
|
90
|
+
) -> Callable[..., FakeProcess]:
|
|
91
|
+
def run(arguments: list[str], **options: Any) -> FakeProcess:
|
|
92
|
+
captured["arguments"] = arguments
|
|
93
|
+
captured.update(options)
|
|
94
|
+
return process
|
|
95
|
+
|
|
96
|
+
return run
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _run_worker(
|
|
100
|
+
tmp_path: Path,
|
|
101
|
+
*,
|
|
102
|
+
selection: AccountSelection,
|
|
103
|
+
process: FakeProcess,
|
|
104
|
+
parent_environment: dict[str, str] | None = None,
|
|
105
|
+
process_terminator: Callable[[FakeProcess], None] | None = None,
|
|
106
|
+
model: str | None = None,
|
|
107
|
+
permission_mode: str = "auto",
|
|
108
|
+
) -> tuple[int, dict[str, Any], dict[str, Any]]:
|
|
109
|
+
prompt_file = tmp_path / "brief.md"
|
|
110
|
+
prompt_file.write_text("standalone brief", encoding="utf-8")
|
|
111
|
+
report_file = tmp_path / "report.json"
|
|
112
|
+
captured: dict[str, Any] = {}
|
|
113
|
+
runner = _make_runner(process, captured)
|
|
114
|
+
dependencies = WorkerDependencies(
|
|
115
|
+
process_factory=runner,
|
|
116
|
+
all_parent_environment_variables=(
|
|
117
|
+
{"KEEP_ME": "present"} if parent_environment is None else parent_environment
|
|
118
|
+
),
|
|
119
|
+
binary_resolver=lambda _: "claude.exe",
|
|
120
|
+
process_terminator=(
|
|
121
|
+
process_terminator if process_terminator is not None else lambda _: None
|
|
122
|
+
),
|
|
123
|
+
monotonic_clock=lambda: 1.0,
|
|
124
|
+
)
|
|
125
|
+
exit_code = run_worker(
|
|
126
|
+
prompt_file=prompt_file,
|
|
127
|
+
cwd=tmp_path,
|
|
128
|
+
report_file=report_file,
|
|
129
|
+
model=model,
|
|
130
|
+
permission_mode=permission_mode,
|
|
131
|
+
timeout_minutes=60,
|
|
132
|
+
decision_selector=lambda: selection,
|
|
133
|
+
dependencies=dependencies,
|
|
134
|
+
)
|
|
135
|
+
return exit_code, json.loads(report_file.read_text(encoding="utf-8")), captured
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_should_set_second_profile_and_pass_parent_environment(
|
|
139
|
+
tmp_path: Path,
|
|
140
|
+
) -> None:
|
|
141
|
+
profile_dir = tmp_path / "second-profile"
|
|
142
|
+
process = FakeProcess()
|
|
143
|
+
exit_code, report, captured = _run_worker(
|
|
144
|
+
tmp_path,
|
|
145
|
+
selection=_selection(CHOICE_SECOND, config_dir=profile_dir),
|
|
146
|
+
process=process,
|
|
147
|
+
parent_environment={
|
|
148
|
+
CLAUDE_CONFIG_DIR_ENV_VAR: "inherited-profile",
|
|
149
|
+
"KEEP_ME": "present",
|
|
150
|
+
},
|
|
151
|
+
model="sonnet",
|
|
152
|
+
permission_mode="plan",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
assert exit_code == 0
|
|
156
|
+
assert report["account"] == CHOICE_SECOND
|
|
157
|
+
assert captured["env"][CLAUDE_CONFIG_DIR_ENV_VAR] == str(profile_dir)
|
|
158
|
+
assert captured["env"]["KEEP_ME"] == "present"
|
|
159
|
+
assert captured["arguments"] == [
|
|
160
|
+
"claude.exe",
|
|
161
|
+
"-p",
|
|
162
|
+
"--output-format",
|
|
163
|
+
"json",
|
|
164
|
+
"--permission-mode",
|
|
165
|
+
"plan",
|
|
166
|
+
"--model",
|
|
167
|
+
"sonnet",
|
|
168
|
+
]
|
|
169
|
+
assert captured["cwd"] == str(tmp_path)
|
|
170
|
+
assert process.communicate_calls[0][0] == "standalone brief"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_should_remove_inherited_profile_for_main_account(tmp_path: Path) -> None:
|
|
174
|
+
process = FakeProcess()
|
|
175
|
+
exit_code, report, captured = _run_worker(
|
|
176
|
+
tmp_path,
|
|
177
|
+
selection=_selection(CHOICE_MAIN, config_dir=tmp_path / "main-profile"),
|
|
178
|
+
process=process,
|
|
179
|
+
parent_environment={
|
|
180
|
+
CLAUDE_CONFIG_DIR_ENV_VAR: "inherited-profile",
|
|
181
|
+
"KEEP_ME": "present",
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
assert exit_code == 0
|
|
186
|
+
assert report["account"] == CHOICE_MAIN
|
|
187
|
+
assert CLAUDE_CONFIG_DIR_ENV_VAR not in captured["env"]
|
|
188
|
+
assert captured["env"]["KEEP_ME"] == "present"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_should_write_wait_reason_and_start_no_process(tmp_path: Path) -> None:
|
|
192
|
+
report_file = tmp_path / "wait-report.json"
|
|
193
|
+
process_started = False
|
|
194
|
+
|
|
195
|
+
def forbidden_runner(*_: Any, **__: Any) -> FakeProcess:
|
|
196
|
+
nonlocal process_started
|
|
197
|
+
process_started = True
|
|
198
|
+
raise AssertionError("wait must not start a process")
|
|
199
|
+
|
|
200
|
+
exit_code = run_worker(
|
|
201
|
+
prompt_file=tmp_path / "missing-brief.md",
|
|
202
|
+
cwd=tmp_path,
|
|
203
|
+
report_file=report_file,
|
|
204
|
+
model=None,
|
|
205
|
+
permission_mode="auto",
|
|
206
|
+
timeout_minutes=60,
|
|
207
|
+
decision_selector=lambda: _selection(
|
|
208
|
+
CHOICE_WAIT,
|
|
209
|
+
reason="second account resets at 2026-09-24T12:00:00Z",
|
|
210
|
+
),
|
|
211
|
+
dependencies=WorkerDependencies(
|
|
212
|
+
process_factory=forbidden_runner,
|
|
213
|
+
all_parent_environment_variables={},
|
|
214
|
+
binary_resolver=lambda _: (_ for _ in ()).throw(
|
|
215
|
+
AssertionError("wait must not resolve a binary")
|
|
216
|
+
),
|
|
217
|
+
process_terminator=lambda _: None,
|
|
218
|
+
monotonic_clock=lambda: 1.0,
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
report = json.loads(report_file.read_text(encoding="utf-8"))
|
|
222
|
+
|
|
223
|
+
assert exit_code == WAIT_EXIT_CODE
|
|
224
|
+
assert process_started is False
|
|
225
|
+
assert report["account"] == CHOICE_WAIT
|
|
226
|
+
assert report["reason"] == "second account resets at 2026-09-24T12:00:00Z"
|
|
227
|
+
assert report["is_error"] is True
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_should_kill_child_on_timeout_and_exit_124(tmp_path: Path) -> None:
|
|
231
|
+
process = FakeProcess(
|
|
232
|
+
stdout='{"result":"partial"}',
|
|
233
|
+
returncode=-9,
|
|
234
|
+
timeout_first=True,
|
|
235
|
+
timeout_output='{"result":"partial"}',
|
|
236
|
+
)
|
|
237
|
+
termination_calls: list[FakeProcess] = []
|
|
238
|
+
exit_code, report, _ = _run_worker(
|
|
239
|
+
tmp_path,
|
|
240
|
+
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
241
|
+
process=process,
|
|
242
|
+
process_terminator=lambda child: termination_calls.append(child),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
assert exit_code == TIMEOUT_EXIT_CODE
|
|
246
|
+
assert termination_calls == [process]
|
|
247
|
+
assert len(process.communicate_calls) == 2
|
|
248
|
+
assert report["exit_code"] == TIMEOUT_EXIT_CODE
|
|
249
|
+
assert report["is_error"] is True
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_should_copy_parsed_result_into_report(tmp_path: Path) -> None:
|
|
253
|
+
process = FakeProcess(stdout='{"result":"ready","is_error":false}')
|
|
254
|
+
exit_code, report, _ = _run_worker(
|
|
255
|
+
tmp_path,
|
|
256
|
+
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
257
|
+
process=process,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
assert exit_code == 0
|
|
261
|
+
assert report["result"] == "ready"
|
|
262
|
+
assert report["is_error"] is False
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_should_write_raw_stdout_tail_when_output_is_not_json(tmp_path: Path) -> None:
|
|
266
|
+
stdout_text = "x" * (STDOUT_TAIL_CHARACTER_LIMIT - 4) + "tail"
|
|
267
|
+
process = FakeProcess(stdout=stdout_text)
|
|
268
|
+
exit_code, report, _ = _run_worker(
|
|
269
|
+
tmp_path,
|
|
270
|
+
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
271
|
+
process=process,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
assert exit_code == 0
|
|
275
|
+
assert report["result"] == stdout_text[-STDOUT_TAIL_CHARACTER_LIMIT:]
|
|
276
|
+
assert report["is_error"] is True
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_should_preserve_child_exit_code_three_for_selected_account(
|
|
280
|
+
tmp_path: Path,
|
|
281
|
+
) -> None:
|
|
282
|
+
process = FakeProcess(returncode=3)
|
|
283
|
+
exit_code, report, _ = _run_worker(
|
|
284
|
+
tmp_path,
|
|
285
|
+
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
286
|
+
process=process,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
assert exit_code == 3
|
|
290
|
+
assert report["account"] == CHOICE_SECOND
|
|
291
|
+
assert report["exit_code"] == 3
|
|
292
|
+
assert report["is_error"] is True
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_should_keep_claude_json_error_flag(tmp_path: Path) -> None:
|
|
296
|
+
process = FakeProcess(stdout='{"result":"failed","is_error":true}')
|
|
297
|
+
exit_code, report, _ = _run_worker(
|
|
298
|
+
tmp_path,
|
|
299
|
+
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
300
|
+
process=process,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
assert exit_code == 0
|
|
304
|
+
assert report["result"] == "failed"
|
|
305
|
+
assert report["is_error"] is True
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_should_pick_second_account_when_neither_meter_is_readable(
|
|
309
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
310
|
+
) -> None:
|
|
311
|
+
main_home = tmp_path / "main-home"
|
|
312
|
+
second_home = tmp_path / "second-home"
|
|
313
|
+
monkeypatch.setattr(Path, "home", lambda: main_home)
|
|
314
|
+
monkeypatch.setattr(claude_account_worker, "default_profile_home", lambda: second_home)
|
|
315
|
+
|
|
316
|
+
selection = select_account()
|
|
317
|
+
|
|
318
|
+
assert selection.account == CHOICE_SECOND
|
|
319
|
+
assert selection.config_dir == second_home
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_should_require_prompt_file_and_report_file_flags() -> None:
|
|
323
|
+
parser = build_argument_parser()
|
|
324
|
+
|
|
325
|
+
arguments = parser.parse_args(
|
|
326
|
+
["--prompt-file", "brief.md", "--report-file", "report.json"]
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
assert arguments.prompt_file == Path("brief.md")
|
|
330
|
+
assert arguments.report_file == Path("report.json")
|
|
331
|
+
assert arguments.permission_mode == "auto"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Check vendor rate parsing through the task report command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
SCRIPT_PATH = Path(__file__).with_name("task_usage_report.py")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_rejects_rates_missing_a_token_category() -> None:
|
|
14
|
+
all_events = [
|
|
15
|
+
{"type": "thread.started", "thread_id": "thread-1"},
|
|
16
|
+
{"type": "turn.completed", "model": "gpt-x", "usage": {"input_tokens": 1}},
|
|
17
|
+
]
|
|
18
|
+
completed = subprocess.run(
|
|
19
|
+
[
|
|
20
|
+
sys.executable,
|
|
21
|
+
str(SCRIPT_PATH),
|
|
22
|
+
"run",
|
|
23
|
+
"--task-id",
|
|
24
|
+
"task-1",
|
|
25
|
+
"--run-id",
|
|
26
|
+
"run-1",
|
|
27
|
+
"--repository",
|
|
28
|
+
"team/project",
|
|
29
|
+
"--outcome",
|
|
30
|
+
"pass",
|
|
31
|
+
"--source-format",
|
|
32
|
+
"codex-json",
|
|
33
|
+
"--rate",
|
|
34
|
+
"gpt-x=1,2,3",
|
|
35
|
+
"--rate-source",
|
|
36
|
+
"vendor-price-card",
|
|
37
|
+
"--rate-effective-date",
|
|
38
|
+
"2026-09-24",
|
|
39
|
+
],
|
|
40
|
+
input="\n".join(map(json.dumps, all_events)),
|
|
41
|
+
capture_output=True,
|
|
42
|
+
text=True,
|
|
43
|
+
check=False,
|
|
44
|
+
)
|
|
45
|
+
assert completed.returncode == 2
|
|
46
|
+
assert completed.stdout == ""
|
|
47
|
+
assert "rate needs four prices per million tokens" in completed.stderr
|