claude-dev-env 8.15.0 → 8.16.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/agents/test_agent_frontmatter.py +0 -72
- package/.agents/agents-archived/code-quality-agent.md +16 -16
- package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +5 -21
- package/.agents/skills/e-code-review/SKILL.md +1 -3
- package/.agents/skills/e-simplify/SKILL.md +6 -6
- package/.agents/skills/eli5/SKILL.md +4 -14
- package/.agents/skills/eli5/reference/task-seeds.md +3 -4
- package/.agents/skills/everything-search/SKILL.md +2 -2
- package/.agents/skills/fresh-branch/SKILL.md +2 -3
- package/.agents/skills/grok-spawn/SKILL.md +3 -8
- package/.agents/skills/issue-tracker/SKILL.md +4 -6
- package/.agents/skills/orchestrator-refresh/SKILL.md +7 -7
- package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +3 -3
- package/.agents/skills/privacy-hygiene/SKILL.md +6 -10
- package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/.agents/skills/pull-request/SKILL.md +11 -14
- package/.agents/skills/pull-request/reference/publication-tasks.md +2 -4
- package/.agents/skills/recovering-codex-startup/SKILL.md +3 -6
- package/.agents/skills/recovering-codex-startup/reference/task-seeds.md +1 -1
- package/.agents/skills/repairing-hook-boundaries/SKILL.md +4 -7
- package/.agents/skills/repairing-hook-boundaries/reference/task-seeds.md +1 -2
- package/.agents/skills/second-account-workers/SKILL.md +26 -8
- package/.agents/skills/syncing-submodules/SKILL.md +7 -17
- package/.agents/skills/syncing-submodules/reference/run-tasks.md +1 -1
- package/.agents/skills/task-build/SKILL.md +1 -1
- package/.agents/skills/task-build/reference/tool-routing.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +2 -4
- package/.agents/skills/usage-pause/SKILL.md +1 -1
- package/.agents/skills/windows-scheduled-task/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/_shared/advisor/advisor-protocol.md +4 -4
- package/_shared/advisor/reference/consult-format.md +1 -1
- package/_shared/advisor/reference/identity.md +2 -2
- package/_shared/advisor/reference/warm-up.md +1 -1
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -16
- package/bin/install.mjs +1 -0
- package/bin/install.prompt-context.test.mjs +7 -3
- package/bin/install.test.mjs +3 -2
- package/commands/sr-loop.md +2 -2
- package/docs/CODE_RULES.md +2 -2
- package/docs/DJANGO_PATTERNS.md +3 -3
- package/docs/REACT_PATTERNS.md +6 -6
- package/docs/TEST_QUALITY.md +10 -16
- package/docs/agent-spawn-protocol.md +4 -4
- package/docs/codex-compatibility.md +4 -33
- package/docs/references/team-advisor-skill.md +0 -1
- package/docs/worker-completion-gate.md +0 -1
- package/hooks/advisory/migration_safety_advisor.py +3 -2
- package/hooks/advisory/refactor_guard.py +3 -3
- package/hooks/hooks.json +1 -1
- package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +1 -2
- package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +1 -2
- package/package.json +1 -1
- package/rules/explore-thoroughly.md +0 -1
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +5 -5
- package/rules/long-horizon-autonomy.md +1 -5
- package/rules/re-stage-before-commit.md +0 -2
- package/rules/shell-invocation.md +1 -1
- package/scripts/claude_account_choice.py +183 -52
- package/scripts/claude_account_profile.py +81 -49
- package/scripts/claude_account_worker.py +48 -15
- package/scripts/dev_env_scripts_constants/claude_account_constants.py +36 -1
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +2 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +8 -2
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/policy_lint/engine.py +5 -2
- package/scripts/repository_checks/config/constants.py +1 -1
- package/scripts/spawn_grok_batch.py +12 -1
- package/scripts/test_claude_account_choice.py +127 -17
- package/scripts/test_claude_account_profile.py +63 -16
- package/scripts/test_claude_account_worker.py +118 -0
- package/scripts/test_spawn_grok_batch.py +22 -0
- package/scripts/tests/test_banned_prose_words.py +1 -1
- package/scripts/tests/test_policy_lint_rules_archive.py +16 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +2 -2
- package/scripts/tests/test_rules.py +9 -9
- package/system-prompts/software-engineer.xml +15 -30
- package/audit-rubrics/audit-categories.json +0 -700
- package/audit-rubrics/category_rubrics/category-a-api-contracts.md +0 -90
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +0 -36
- package/audit-rubrics/category_rubrics/category-c-resource-cleanup.md +0 -35
- package/audit-rubrics/category_rubrics/category-d-scoping-and-ordering.md +0 -35
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +0 -40
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +0 -41
- package/audit-rubrics/category_rubrics/category-g-bounds-and-overflow.md +0 -38
- package/audit-rubrics/category_rubrics/category-h-security-boundaries.md +0 -40
- package/audit-rubrics/category_rubrics/category-i-concurrency.md +0 -38
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +0 -59
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +0 -66
- package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +0 -48
- package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +0 -44
- package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +0 -54
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +0 -59
- package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +0 -46
- package/audit-rubrics/prompts/category-a-api-contracts.md +0 -407
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +0 -401
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +0 -420
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +0 -414
- package/audit-rubrics/prompts/category-e-dead-code.md +0 -434
- package/audit-rubrics/prompts/category-f-silent-failures.md +0 -432
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +0 -383
- package/audit-rubrics/prompts/category-h-security-boundaries.md +0 -423
- package/audit-rubrics/prompts/category-i-concurrency.md +0 -429
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +0 -458
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +0 -338
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +0 -131
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +0 -129
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +0 -139
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +0 -89
- package/audit-rubrics/prompts/category-q-cross-surface-claims.md +0 -51
- package/hooks/hooks_constants/style_reminder_prompt_constants.py +0 -9
- package/hooks/session/style_reminder_prompt.py +0 -55
- package/hooks/session/test_style_reminder_prompt.py +0 -86
|
@@ -30,7 +30,7 @@ def meters(
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class TestChooseAccount:
|
|
33
|
-
def
|
|
33
|
+
def test_should_pick_main_when_week_resets_soon(self) -> None:
|
|
34
34
|
main_meters = meters(
|
|
35
35
|
session_used=10.0, weekly_used=80.0, weekly_resets_in=timedelta(hours=5)
|
|
36
36
|
)
|
|
@@ -42,7 +42,7 @@ class TestChooseAccount:
|
|
|
42
42
|
)
|
|
43
43
|
assert decision.account == choice.CHOICE_MAIN
|
|
44
44
|
|
|
45
|
-
def
|
|
45
|
+
def test_should_pick_second_when_main_week_resets_later_than_a_day(self) -> None:
|
|
46
46
|
main_meters = meters(
|
|
47
47
|
session_used=0.0, weekly_used=5.0, weekly_resets_in=timedelta(hours=30)
|
|
48
48
|
)
|
|
@@ -54,7 +54,7 @@ class TestChooseAccount:
|
|
|
54
54
|
)
|
|
55
55
|
assert decision.account == choice.CHOICE_SECOND
|
|
56
56
|
|
|
57
|
-
def
|
|
57
|
+
def test_should_keep_main_once_it_nears_its_weekly_ceiling(self) -> None:
|
|
58
58
|
main_meters = meters(
|
|
59
59
|
session_used=0.0, weekly_used=90.0, weekly_resets_in=timedelta(hours=3)
|
|
60
60
|
)
|
|
@@ -66,7 +66,7 @@ class TestChooseAccount:
|
|
|
66
66
|
)
|
|
67
67
|
assert decision.account == choice.CHOICE_SECOND
|
|
68
68
|
|
|
69
|
-
def
|
|
69
|
+
def test_should_keep_main_when_its_five_hour_window_is_half_spent(self) -> None:
|
|
70
70
|
main_meters = meters(
|
|
71
71
|
session_used=50.0, weekly_used=20.0, weekly_resets_in=timedelta(hours=3)
|
|
72
72
|
)
|
|
@@ -78,7 +78,7 @@ class TestChooseAccount:
|
|
|
78
78
|
)
|
|
79
79
|
assert decision.account == choice.CHOICE_SECOND
|
|
80
80
|
|
|
81
|
-
def
|
|
81
|
+
def test_should_never_pick_main_when_its_meter_is_unread(self) -> None:
|
|
82
82
|
second_meters = meters(
|
|
83
83
|
session_used=95.0, weekly_used=99.0, weekly_resets_in=timedelta(days=4)
|
|
84
84
|
)
|
|
@@ -87,7 +87,7 @@ class TestChooseAccount:
|
|
|
87
87
|
)
|
|
88
88
|
assert decision.account == choice.CHOICE_WAIT
|
|
89
89
|
|
|
90
|
-
def
|
|
90
|
+
def test_should_never_pick_main_when_its_reset_time_is_missing(self) -> None:
|
|
91
91
|
main_meters = meters(session_used=0.0, weekly_used=0.0, weekly_resets_in=None)
|
|
92
92
|
second_meters = meters(
|
|
93
93
|
session_used=95.0, weekly_used=99.0, weekly_resets_in=timedelta(days=4)
|
|
@@ -97,12 +97,12 @@ class TestChooseAccount:
|
|
|
97
97
|
)
|
|
98
98
|
assert decision.account == choice.CHOICE_WAIT
|
|
99
99
|
|
|
100
|
-
def
|
|
100
|
+
def test_should_pick_second_when_its_meter_is_unread(self) -> None:
|
|
101
101
|
decision = choice.choose_account(main_meters=None, second_meters=None, now=NOW)
|
|
102
102
|
assert decision.account == choice.CHOICE_SECOND
|
|
103
103
|
assert decision.reason == choice.REASON_SECOND_UNREAD
|
|
104
104
|
|
|
105
|
-
def
|
|
105
|
+
def test_should_wait_and_name_the_session_reset_when_the_five_hour_window_blocks(
|
|
106
106
|
self,
|
|
107
107
|
) -> None:
|
|
108
108
|
second_meters = meters(
|
|
@@ -117,7 +117,7 @@ class TestChooseAccount:
|
|
|
117
117
|
assert decision.account == choice.CHOICE_WAIT
|
|
118
118
|
assert (NOW + timedelta(hours=1)).isoformat() in decision.reason
|
|
119
119
|
|
|
120
|
-
def
|
|
120
|
+
def test_should_wait_and_name_the_weekly_reset_when_the_week_blocks(self) -> None:
|
|
121
121
|
second_meters = meters(
|
|
122
122
|
session_used=10.0, weekly_used=95.0, weekly_resets_in=timedelta(days=2)
|
|
123
123
|
)
|
|
@@ -129,7 +129,7 @@ class TestChooseAccount:
|
|
|
129
129
|
|
|
130
130
|
|
|
131
131
|
class TestReadAccountMeters:
|
|
132
|
-
def
|
|
132
|
+
def test_should_return_none_when_the_probe_cannot_read_the_account(
|
|
133
133
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
134
134
|
) -> None:
|
|
135
135
|
def failing_probe(credentials_path: Path) -> AccountUsageMeters:
|
|
@@ -138,7 +138,7 @@ class TestReadAccountMeters:
|
|
|
138
138
|
monkeypatch.setattr(choice, "probe_account_meters", failing_probe)
|
|
139
139
|
assert choice.read_account_meters(tmp_path / ".credentials.json") is None
|
|
140
140
|
|
|
141
|
-
def
|
|
141
|
+
def test_should_return_the_probed_meters(
|
|
142
142
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
143
143
|
) -> None:
|
|
144
144
|
probed = meters(
|
|
@@ -149,7 +149,7 @@ class TestReadAccountMeters:
|
|
|
149
149
|
|
|
150
150
|
|
|
151
151
|
class TestDecisionPayload:
|
|
152
|
-
def
|
|
152
|
+
def test_should_name_no_config_directory_for_a_wait(self) -> None:
|
|
153
153
|
decision = choice.AccountDecision(account=choice.CHOICE_WAIT, reason="full")
|
|
154
154
|
assert choice.decision_payload(decision, config_directory=None) == {
|
|
155
155
|
"account": "wait",
|
|
@@ -159,10 +159,10 @@ class TestDecisionPayload:
|
|
|
159
159
|
|
|
160
160
|
|
|
161
161
|
class TestMetersPayload:
|
|
162
|
-
def
|
|
162
|
+
def test_should_name_no_meters_for_an_unread_account(self) -> None:
|
|
163
163
|
assert choice.meters_payload(None) is None
|
|
164
164
|
|
|
165
|
-
def
|
|
165
|
+
def test_should_carry_each_used_percent_and_reset_time(self) -> None:
|
|
166
166
|
read_meters = meters(
|
|
167
167
|
session_used=12.0,
|
|
168
168
|
weekly_used=34.0,
|
|
@@ -176,7 +176,7 @@ class TestMetersPayload:
|
|
|
176
176
|
"weekly_resets_at": "2026-09-25T21:00:00+00:00",
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
def
|
|
179
|
+
def test_should_name_no_reset_time_the_meter_left_out(self) -> None:
|
|
180
180
|
read_meters = meters(session_used=1.0, weekly_used=None, weekly_resets_in=None)
|
|
181
181
|
payload = choice.meters_payload(read_meters)
|
|
182
182
|
assert payload is not None
|
|
@@ -185,7 +185,7 @@ class TestMetersPayload:
|
|
|
185
185
|
|
|
186
186
|
|
|
187
187
|
class TestMain:
|
|
188
|
-
def
|
|
188
|
+
def test_should_print_the_choice_and_the_config_directory_of_the_chosen_account(
|
|
189
189
|
self,
|
|
190
190
|
tmp_path: Path,
|
|
191
191
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -219,7 +219,7 @@ class TestMain:
|
|
|
219
219
|
assert printed["meters"]["main"] is None
|
|
220
220
|
assert printed["meters"]["second"]["weekly_used_percent"] == 10.0
|
|
221
221
|
|
|
222
|
-
def
|
|
222
|
+
def test_should_print_no_config_directory_when_the_job_waits(
|
|
223
223
|
self,
|
|
224
224
|
tmp_path: Path,
|
|
225
225
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -243,3 +243,113 @@ class TestMain:
|
|
|
243
243
|
assert exit_code == 0
|
|
244
244
|
assert printed["account"] == choice.CHOICE_WAIT
|
|
245
245
|
assert printed["config_dir"] is None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_should_pick_second_extra_in_order_and_print_every_meter(
|
|
249
|
+
tmp_path: Path,
|
|
250
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
251
|
+
capsys: pytest.CaptureFixture[str],
|
|
252
|
+
) -> None:
|
|
253
|
+
main_home = tmp_path / "main"
|
|
254
|
+
first_extra = tmp_path / "first"
|
|
255
|
+
second_extra = tmp_path / "next"
|
|
256
|
+
third_extra = tmp_path / "last"
|
|
257
|
+
readings = {
|
|
258
|
+
main_home: None,
|
|
259
|
+
first_extra: meters(
|
|
260
|
+
session_used=90.0, weekly_used=95.0, weekly_resets_in=timedelta(days=4)
|
|
261
|
+
),
|
|
262
|
+
second_extra: meters(
|
|
263
|
+
session_used=10.0, weekly_used=20.0, weekly_resets_in=timedelta(days=4)
|
|
264
|
+
),
|
|
265
|
+
third_extra: meters(
|
|
266
|
+
session_used=30.0, weekly_used=40.0, weekly_resets_in=timedelta(days=4)
|
|
267
|
+
),
|
|
268
|
+
}
|
|
269
|
+
monkeypatch.setattr(
|
|
270
|
+
choice, "read_account_meters", lambda path: readings[path.parent]
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
assert choice.main(
|
|
274
|
+
[
|
|
275
|
+
"--main-config-dir",
|
|
276
|
+
str(main_home),
|
|
277
|
+
"--second-config-dir",
|
|
278
|
+
str(first_extra),
|
|
279
|
+
"--extra-config-dir",
|
|
280
|
+
str(second_extra),
|
|
281
|
+
"--extra-config-dir",
|
|
282
|
+
str(third_extra),
|
|
283
|
+
]
|
|
284
|
+
) == 0
|
|
285
|
+
printed = json.loads(capsys.readouterr().out)
|
|
286
|
+
|
|
287
|
+
assert printed["account"] == "extra_2"
|
|
288
|
+
assert printed["config_dir"] == str(second_extra)
|
|
289
|
+
assert list(printed["meters"]) == ["main", "second", "extra_2", "extra_3"]
|
|
290
|
+
assert printed["meters"]["main"] is None
|
|
291
|
+
assert printed["meters"]["second"]["weekly_used_percent"] == 95.0
|
|
292
|
+
assert printed["meters"]["extra_2"]["weekly_used_percent"] == 20.0
|
|
293
|
+
assert printed["meters"]["extra_3"]["weekly_used_percent"] == 40.0
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_should_wait_for_first_usable_reset_when_all_extras_are_full(
|
|
297
|
+
tmp_path: Path,
|
|
298
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
299
|
+
capsys: pytest.CaptureFixture[str],
|
|
300
|
+
) -> None:
|
|
301
|
+
first_extra = tmp_path / "first"
|
|
302
|
+
second_extra = tmp_path / "next"
|
|
303
|
+
third_extra = tmp_path / "last"
|
|
304
|
+
readings = {
|
|
305
|
+
first_extra: meters(
|
|
306
|
+
session_used=90.0,
|
|
307
|
+
weekly_used=95.0,
|
|
308
|
+
weekly_resets_in=timedelta(hours=1),
|
|
309
|
+
session_resets_in=timedelta(hours=3),
|
|
310
|
+
),
|
|
311
|
+
second_extra: meters(
|
|
312
|
+
session_used=10.0, weekly_used=95.0, weekly_resets_in=timedelta(hours=2)
|
|
313
|
+
),
|
|
314
|
+
third_extra: meters(
|
|
315
|
+
session_used=90.0, weekly_used=10.0, weekly_resets_in=timedelta(days=4),
|
|
316
|
+
session_resets_in=timedelta(hours=4),
|
|
317
|
+
),
|
|
318
|
+
}
|
|
319
|
+
monkeypatch.setattr(
|
|
320
|
+
choice, "read_account_meters", lambda path: readings.get(path.parent)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
assert choice.main(
|
|
324
|
+
[
|
|
325
|
+
"--main-config-dir",
|
|
326
|
+
str(tmp_path / "main"),
|
|
327
|
+
"--second-config-dir",
|
|
328
|
+
str(first_extra),
|
|
329
|
+
"--extra-config-dir",
|
|
330
|
+
str(second_extra),
|
|
331
|
+
"--extra-config-dir",
|
|
332
|
+
str(third_extra),
|
|
333
|
+
]
|
|
334
|
+
) == 0
|
|
335
|
+
printed = json.loads(capsys.readouterr().out)
|
|
336
|
+
|
|
337
|
+
assert printed["account"] == choice.CHOICE_WAIT
|
|
338
|
+
assert printed["config_dir"] is None
|
|
339
|
+
assert (NOW + timedelta(hours=2)).isoformat() in printed["reason"]
|
|
340
|
+
assert (NOW + timedelta(hours=1)).isoformat() not in printed["reason"]
|
|
341
|
+
assert list(printed["meters"]) == ["main", "second", "extra_2", "extra_3"]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def test_should_reject_duplicate_extra_config_directories(tmp_path: Path) -> None:
|
|
345
|
+
with pytest.raises(SystemExit) as error:
|
|
346
|
+
choice.main(
|
|
347
|
+
[
|
|
348
|
+
"--second-config-dir",
|
|
349
|
+
str(tmp_path / "first"),
|
|
350
|
+
"--extra-config-dir",
|
|
351
|
+
str(tmp_path / "first"),
|
|
352
|
+
]
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
assert error.value.code == 2
|
|
@@ -32,13 +32,13 @@ def sync(main_home: Path, profile_home: Path) -> profile.ProfileSyncReport:
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
class TestDefaultProfileHome:
|
|
35
|
-
def
|
|
35
|
+
def test_should_use_the_profiles_root_the_environment_names(
|
|
36
36
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
37
37
|
) -> None:
|
|
38
38
|
monkeypatch.setenv("LLM_SETTINGS_PROFILES_ROOT", str(tmp_path))
|
|
39
39
|
assert profile.default_profile_home() == tmp_path / "ev"
|
|
40
40
|
|
|
41
|
-
def
|
|
41
|
+
def test_should_fall_back_to_the_profiles_root_in_the_user_home(
|
|
42
42
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
43
43
|
) -> None:
|
|
44
44
|
monkeypatch.delenv("LLM_SETTINGS_PROFILES_ROOT", raising=False)
|
|
@@ -47,23 +47,24 @@ class TestDefaultProfileHome:
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
class TestIsAccountLocal:
|
|
50
|
-
def
|
|
50
|
+
def test_should_keep_sign_in_state_and_history_local(self) -> None:
|
|
51
51
|
for each_name in (
|
|
52
52
|
".credentials.json",
|
|
53
53
|
".credentials.json.bak-session-dashboard",
|
|
54
54
|
".claude.json",
|
|
55
55
|
".claude.json.backup",
|
|
56
|
+
"extra-profiles.json",
|
|
56
57
|
"projects",
|
|
57
58
|
):
|
|
58
59
|
assert profile.is_account_local(each_name)
|
|
59
60
|
|
|
60
|
-
def
|
|
61
|
+
def test_should_share_skills_rules_and_settings(self) -> None:
|
|
61
62
|
for each_name in ("skills", "rules", "CLAUDE.md", "settings.json", "plugins"):
|
|
62
63
|
assert not profile.is_account_local(each_name)
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
class TestSyncProfile:
|
|
66
|
-
def
|
|
67
|
+
def test_should_link_every_shared_entry_to_the_main_home(self, tmp_path: Path) -> None:
|
|
67
68
|
main_home = build_main_home(tmp_path)
|
|
68
69
|
profile_home = tmp_path / "ev"
|
|
69
70
|
sync(main_home, profile_home)
|
|
@@ -76,7 +77,7 @@ class TestSyncProfile:
|
|
|
76
77
|
profile_home / "settings.json", main_home / "settings.json"
|
|
77
78
|
)
|
|
78
79
|
|
|
79
|
-
def
|
|
80
|
+
def test_should_link_only_what_the_callers_rule_shares(self, tmp_path: Path) -> None:
|
|
80
81
|
main_home = build_main_home(tmp_path)
|
|
81
82
|
profile_home = tmp_path / "codex-1"
|
|
82
83
|
report = profile.sync_profile(
|
|
@@ -88,7 +89,7 @@ class TestSyncProfile:
|
|
|
88
89
|
assert report.all_linked == ("rules",)
|
|
89
90
|
assert not (profile_home / "skills").exists()
|
|
90
91
|
|
|
91
|
-
def
|
|
92
|
+
def test_should_keep_the_accounts_own_sign_in_state_and_history_apart(
|
|
92
93
|
self, tmp_path: Path
|
|
93
94
|
) -> None:
|
|
94
95
|
main_home = build_main_home(tmp_path)
|
|
@@ -105,7 +106,7 @@ class TestSyncProfile:
|
|
|
105
106
|
assert not (profile_home / ".claude.json.backup").exists()
|
|
106
107
|
assert not (profile_home / "projects").exists()
|
|
107
108
|
|
|
108
|
-
def
|
|
109
|
+
def test_should_move_a_stale_copy_aside_and_keep_its_content(
|
|
109
110
|
self, tmp_path: Path
|
|
110
111
|
) -> None:
|
|
111
112
|
main_home = build_main_home(tmp_path)
|
|
@@ -120,7 +121,7 @@ class TestSyncProfile:
|
|
|
120
121
|
assert profile.links_to(profile_home / "CLAUDE.md", main_home / "CLAUDE.md")
|
|
121
122
|
assert report.all_moved_aside == ("CLAUDE.md",)
|
|
122
123
|
|
|
123
|
-
def
|
|
124
|
+
def test_should_remove_a_same_content_copy_without_moving_it_aside(
|
|
124
125
|
self, tmp_path: Path
|
|
125
126
|
) -> None:
|
|
126
127
|
main_home = build_main_home(tmp_path)
|
|
@@ -132,7 +133,7 @@ class TestSyncProfile:
|
|
|
132
133
|
assert not (profile_home / ".replaced").exists()
|
|
133
134
|
assert profile.links_to(profile_home / "CLAUDE.md", main_home / "CLAUDE.md")
|
|
134
135
|
|
|
135
|
-
def
|
|
136
|
+
def test_should_move_aside_a_link_that_points_somewhere_else(
|
|
136
137
|
self, tmp_path: Path
|
|
137
138
|
) -> None:
|
|
138
139
|
main_home = build_main_home(tmp_path)
|
|
@@ -147,7 +148,7 @@ class TestSyncProfile:
|
|
|
147
148
|
assert profile.links_to(profile_home / "rules", main_home / "rules")
|
|
148
149
|
assert report.all_moved_aside == ("rules",)
|
|
149
150
|
|
|
150
|
-
def
|
|
151
|
+
def test_should_change_nothing_on_a_second_run(self, tmp_path: Path) -> None:
|
|
151
152
|
main_home = build_main_home(tmp_path)
|
|
152
153
|
profile_home = tmp_path / "ev"
|
|
153
154
|
sync(main_home, profile_home)
|
|
@@ -156,7 +157,7 @@ class TestSyncProfile:
|
|
|
156
157
|
assert second_report.all_moved_aside == ()
|
|
157
158
|
assert second_report.all_unlinked == ()
|
|
158
159
|
|
|
159
|
-
def
|
|
160
|
+
def test_should_unlink_an_entry_the_main_home_no_longer_has(
|
|
160
161
|
self, tmp_path: Path
|
|
161
162
|
) -> None:
|
|
162
163
|
main_home = build_main_home(tmp_path)
|
|
@@ -168,7 +169,7 @@ class TestSyncProfile:
|
|
|
168
169
|
assert report.all_unlinked == ("rules",)
|
|
169
170
|
|
|
170
171
|
|
|
171
|
-
def
|
|
172
|
+
def test_should_remove_a_main_link_left_under_an_account_local_name(
|
|
172
173
|
self, tmp_path: Path
|
|
173
174
|
) -> None:
|
|
174
175
|
main_home = build_main_home(tmp_path)
|
|
@@ -185,7 +186,7 @@ class TestSyncProfile:
|
|
|
185
186
|
|
|
186
187
|
|
|
187
188
|
class TestWriteLauncher:
|
|
188
|
-
def
|
|
189
|
+
def test_should_point_claude_at_the_profile_and_pass_every_argument(
|
|
189
190
|
self, tmp_path: Path
|
|
190
191
|
) -> None:
|
|
191
192
|
profile_home = tmp_path / "ev"
|
|
@@ -196,7 +197,7 @@ class TestWriteLauncher:
|
|
|
196
197
|
assert f'set "CLAUDE_CONFIG_DIR={profile_home}"' in launcher_text
|
|
197
198
|
assert "claude %*" in launcher_text
|
|
198
199
|
|
|
199
|
-
def
|
|
200
|
+
def test_should_move_an_older_launcher_aside(self, tmp_path: Path) -> None:
|
|
200
201
|
launcher_directory = tmp_path / "bin"
|
|
201
202
|
launcher_directory.mkdir()
|
|
202
203
|
(launcher_directory / "claude-ev.cmd").write_text(
|
|
@@ -212,7 +213,7 @@ class TestWriteLauncher:
|
|
|
212
213
|
|
|
213
214
|
|
|
214
215
|
class TestMain:
|
|
215
|
-
def
|
|
216
|
+
def test_should_print_what_the_sync_changed(
|
|
216
217
|
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
217
218
|
) -> None:
|
|
218
219
|
main_home = build_main_home(tmp_path)
|
|
@@ -230,3 +231,49 @@ class TestMain:
|
|
|
230
231
|
assert exit_code == 0
|
|
231
232
|
assert "CLAUDE.md" in printed["linked"]
|
|
232
233
|
assert printed["launcher"] == str(tmp_path / "bin" / "claude-ev.cmd")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_should_create_named_profile_and_launcher_without_changes_on_repeat(
|
|
237
|
+
tmp_path: Path,
|
|
238
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
239
|
+
capsys: pytest.CaptureFixture[str],
|
|
240
|
+
) -> None:
|
|
241
|
+
main_home = build_main_home(tmp_path)
|
|
242
|
+
(main_home / "extra-profiles.json").write_text("[]", encoding="utf-8")
|
|
243
|
+
profiles_root = tmp_path / "profiles"
|
|
244
|
+
launcher_directory = tmp_path / "bin"
|
|
245
|
+
monkeypatch.setenv("LLM_SETTINGS_PROFILES_ROOT", str(profiles_root))
|
|
246
|
+
arguments = [
|
|
247
|
+
"--main-home",
|
|
248
|
+
str(main_home),
|
|
249
|
+
"--profile-name",
|
|
250
|
+
"profile-3",
|
|
251
|
+
"--launcher-directory",
|
|
252
|
+
str(launcher_directory),
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
assert profile.main(arguments) == 0
|
|
256
|
+
first_report = json.loads(capsys.readouterr().out)
|
|
257
|
+
profile_home = profiles_root / "profile-3"
|
|
258
|
+
launcher_path = launcher_directory / "claude-profile-3.cmd"
|
|
259
|
+
assert profile.links_to(profile_home / "CLAUDE.md", main_home / "CLAUDE.md")
|
|
260
|
+
assert not (profile_home / "extra-profiles.json").exists()
|
|
261
|
+
assert first_report["launcher"] == str(launcher_path)
|
|
262
|
+
assert f'set "CLAUDE_CONFIG_DIR={profile_home}"' in launcher_path.read_text(
|
|
263
|
+
encoding="utf-8"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
assert profile.main(arguments) == 0
|
|
267
|
+
second_report = json.loads(capsys.readouterr().out)
|
|
268
|
+
assert second_report["linked"] == []
|
|
269
|
+
assert second_report["moved_aside"] == []
|
|
270
|
+
assert second_report["unlinked"] == []
|
|
271
|
+
assert list(launcher_directory.glob("*.replaced-*")) == []
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@pytest.mark.parametrize(
|
|
275
|
+
"profile_name", ["../outside", "bad name", "CON", "main", "wait"]
|
|
276
|
+
)
|
|
277
|
+
def test_should_reject_unsafe_profile_names(profile_name: str) -> None:
|
|
278
|
+
with pytest.raises(ValueError):
|
|
279
|
+
profile.default_profile_home(profile_name)
|
|
@@ -3,12 +3,15 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import subprocess
|
|
5
5
|
from collections.abc import Callable
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
import pytest
|
|
10
11
|
|
|
12
|
+
import claude_account_choice
|
|
11
13
|
import claude_account_worker
|
|
14
|
+
from claude_chain_usage import AccountUsageMeters
|
|
12
15
|
from claude_account_worker import (
|
|
13
16
|
AccountSelection,
|
|
14
17
|
WorkerDependencies,
|
|
@@ -26,6 +29,8 @@ from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
|
26
29
|
WAIT_EXIT_CODE,
|
|
27
30
|
)
|
|
28
31
|
|
|
32
|
+
NOW = datetime(2026, 9, 22, 21, 0, tzinfo=timezone.utc)
|
|
33
|
+
|
|
29
34
|
|
|
30
35
|
class FakeProcess:
|
|
31
36
|
def __init__(
|
|
@@ -319,6 +324,119 @@ def test_should_pick_second_account_when_neither_meter_is_readable(
|
|
|
319
324
|
assert selection.config_dir == second_home
|
|
320
325
|
|
|
321
326
|
|
|
327
|
+
def test_should_read_profile_list_and_pick_later_extra(
|
|
328
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
329
|
+
) -> None:
|
|
330
|
+
main_home = tmp_path / ".claude"
|
|
331
|
+
main_home.mkdir()
|
|
332
|
+
(main_home / "extra-profiles.json").write_text(
|
|
333
|
+
json.dumps(["profile_a", "profile_b"]), encoding="utf-8"
|
|
334
|
+
)
|
|
335
|
+
profiles_root = tmp_path / "profiles"
|
|
336
|
+
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
337
|
+
monkeypatch.setenv("LLM_SETTINGS_PROFILES_ROOT", str(profiles_root))
|
|
338
|
+
readings = {
|
|
339
|
+
profiles_root / "profile_a": AccountUsageMeters(
|
|
340
|
+
session_utilization=90.0,
|
|
341
|
+
session_resets_at=NOW + timedelta(hours=2),
|
|
342
|
+
weekly_utilization=95.0,
|
|
343
|
+
weekly_resets_at=NOW + timedelta(days=2),
|
|
344
|
+
),
|
|
345
|
+
profiles_root / "profile_b": AccountUsageMeters(
|
|
346
|
+
session_utilization=10.0,
|
|
347
|
+
session_resets_at=NOW + timedelta(hours=2),
|
|
348
|
+
weekly_utilization=20.0,
|
|
349
|
+
weekly_resets_at=NOW + timedelta(days=2),
|
|
350
|
+
),
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
def read_meters(path: Path) -> AccountUsageMeters | None:
|
|
354
|
+
return readings.get(path.parent)
|
|
355
|
+
|
|
356
|
+
monkeypatch.setattr(claude_account_choice, "read_account_meters", read_meters)
|
|
357
|
+
monkeypatch.setattr(claude_account_worker, "read_account_meters", read_meters)
|
|
358
|
+
|
|
359
|
+
selection = select_account()
|
|
360
|
+
|
|
361
|
+
assert selection.account == "extra_2"
|
|
362
|
+
assert selection.config_dir == profiles_root / "profile_b"
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def test_should_exit_three_when_every_listed_profile_is_full(
|
|
366
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
367
|
+
) -> None:
|
|
368
|
+
main_home = tmp_path / ".claude"
|
|
369
|
+
main_home.mkdir()
|
|
370
|
+
(main_home / "extra-profiles.json").write_text(
|
|
371
|
+
json.dumps(["profile_a", "profile_b"]), encoding="utf-8"
|
|
372
|
+
)
|
|
373
|
+
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
374
|
+
monkeypatch.setenv("LLM_SETTINGS_PROFILES_ROOT", str(tmp_path / "profiles"))
|
|
375
|
+
blocked = AccountUsageMeters(
|
|
376
|
+
session_utilization=90.0,
|
|
377
|
+
session_resets_at=NOW + timedelta(hours=2),
|
|
378
|
+
weekly_utilization=95.0,
|
|
379
|
+
weekly_resets_at=NOW + timedelta(days=2),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def read_meters(path: Path) -> AccountUsageMeters | None:
|
|
383
|
+
return None if path.parent == main_home else blocked
|
|
384
|
+
|
|
385
|
+
monkeypatch.setattr(claude_account_choice, "read_account_meters", read_meters)
|
|
386
|
+
monkeypatch.setattr(claude_account_worker, "read_account_meters", read_meters)
|
|
387
|
+
report_file = tmp_path / "report.json"
|
|
388
|
+
|
|
389
|
+
exit_code = run_worker(
|
|
390
|
+
prompt_file=tmp_path / "unused.md",
|
|
391
|
+
cwd=tmp_path,
|
|
392
|
+
report_file=report_file,
|
|
393
|
+
model=None,
|
|
394
|
+
permission_mode="auto",
|
|
395
|
+
timeout_minutes=60,
|
|
396
|
+
decision_selector=select_account,
|
|
397
|
+
dependencies=WorkerDependencies(
|
|
398
|
+
process_factory=lambda *args, **kwargs: (_ for _ in ()).throw(
|
|
399
|
+
AssertionError("wait must not start a process")
|
|
400
|
+
),
|
|
401
|
+
all_parent_environment_variables={},
|
|
402
|
+
binary_resolver=lambda _: None,
|
|
403
|
+
process_terminator=lambda _: None,
|
|
404
|
+
monotonic_clock=lambda: 1.0,
|
|
405
|
+
),
|
|
406
|
+
)
|
|
407
|
+
report = json.loads(report_file.read_text(encoding="utf-8"))
|
|
408
|
+
|
|
409
|
+
assert exit_code == WAIT_EXIT_CODE
|
|
410
|
+
assert report["account"] == CHOICE_WAIT
|
|
411
|
+
assert (NOW + timedelta(days=2)).isoformat() in report["reason"]
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def test_should_set_later_extra_profile_environment(tmp_path: Path) -> None:
|
|
415
|
+
profile_dir = tmp_path / "profile_b"
|
|
416
|
+
exit_code, report, captured = _run_worker(
|
|
417
|
+
tmp_path,
|
|
418
|
+
selection=_selection("extra_2", config_dir=profile_dir),
|
|
419
|
+
process=FakeProcess(),
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
assert exit_code == 0
|
|
423
|
+
assert report["account"] == "extra_2"
|
|
424
|
+
assert captured["env"][CLAUDE_CONFIG_DIR_ENV_VAR] == str(profile_dir)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@pytest.mark.parametrize(
|
|
428
|
+
"contents",
|
|
429
|
+
['{', '{}', '[]', '["profile_a", "PROFILE_A"]', '["../outside"]', '["main"]'],
|
|
430
|
+
)
|
|
431
|
+
def test_should_reject_invalid_profile_lists(tmp_path: Path, contents: str) -> None:
|
|
432
|
+
main_home = tmp_path / ".claude"
|
|
433
|
+
main_home.mkdir()
|
|
434
|
+
(main_home / "extra-profiles.json").write_text(contents, encoding="utf-8")
|
|
435
|
+
|
|
436
|
+
with pytest.raises(ValueError):
|
|
437
|
+
claude_account_worker.extra_config_directories(main_home)
|
|
438
|
+
|
|
439
|
+
|
|
322
440
|
def test_should_require_prompt_file_and_report_file_flags() -> None:
|
|
323
441
|
parser = build_argument_parser()
|
|
324
442
|
|
|
@@ -1514,6 +1514,7 @@ def test_bind_unique_worker_advisor_rejects_placeholder(
|
|
|
1514
1514
|
batch.bind_unique_worker_advisor(
|
|
1515
1515
|
advisor_spec=batch.AdvisorSpec(launcher=batch.DEFAULT_ADVISOR_LAUNCHER_PLACEHOLDER),
|
|
1516
1516
|
role_name="lens",
|
|
1517
|
+
assignment_text="assignment",
|
|
1517
1518
|
all_used_session_ids=set(),
|
|
1518
1519
|
)
|
|
1519
1520
|
|
|
@@ -1528,12 +1529,33 @@ def test_bind_unique_worker_advisor_returns_session(
|
|
|
1528
1529
|
session_id, signal = batch.bind_unique_worker_advisor(
|
|
1529
1530
|
advisor_spec=batch.AdvisorSpec(launcher="fixture-advisor-launcher"),
|
|
1530
1531
|
role_name="lens",
|
|
1532
|
+
assignment_text="assignment",
|
|
1531
1533
|
all_used_session_ids=set(),
|
|
1532
1534
|
)
|
|
1533
1535
|
assert session_id == "sess-unique-1"
|
|
1534
1536
|
assert signal == "ENDORSE"
|
|
1535
1537
|
|
|
1536
1538
|
|
|
1539
|
+
def test_bind_unique_worker_advisor_prompt_carries_assignment_text(
|
|
1540
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1541
|
+
) -> None:
|
|
1542
|
+
all_captured_prompts: list[object] = []
|
|
1543
|
+
|
|
1544
|
+
def fake(**kwargs: object) -> tuple[str | None, str, int]:
|
|
1545
|
+
all_captured_prompts.append(kwargs["prompt_text"])
|
|
1546
|
+
return "sess-assignment-1", "ENDORSE\nok", 0
|
|
1547
|
+
|
|
1548
|
+
monkeypatch.setattr(batch, "batch_invoke_advisor", fake)
|
|
1549
|
+
batch.bind_unique_worker_advisor(
|
|
1550
|
+
advisor_spec=batch.AdvisorSpec(launcher="fixture-advisor-launcher"),
|
|
1551
|
+
role_name="lens",
|
|
1552
|
+
assignment_text="Audit module alpha for unused imports.",
|
|
1553
|
+
all_used_session_ids=set(),
|
|
1554
|
+
)
|
|
1555
|
+
assert len(all_captured_prompts) == 1
|
|
1556
|
+
assert "Audit module alpha for unused imports." in str(all_captured_prompts[0])
|
|
1557
|
+
|
|
1558
|
+
|
|
1537
1559
|
def test_obtain_advisor_completion_verdict_endorses(
|
|
1538
1560
|
monkeypatch: pytest.MonkeyPatch,
|
|
1539
1561
|
) -> None:
|
|
@@ -86,7 +86,7 @@ def test_governed_paths_lists_the_shipped_surfaces(tmp_path: Path) -> None:
|
|
|
86
86
|
def test_governed_paths_cover_instruction_surfaces_and_skip_archives() -> None:
|
|
87
87
|
assert governs_path("rules/git-workflow.md")
|
|
88
88
|
assert governs_path(".agents/skills/eli5/SKILL.md")
|
|
89
|
-
assert governs_path("audit-rubrics/prompts/category-
|
|
89
|
+
assert governs_path("audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md")
|
|
90
90
|
assert governs_path("system-prompts/software-engineer.xml")
|
|
91
91
|
assert not governs_path("rules-archived/manifest.md")
|
|
92
92
|
assert not governs_path("scripts/banned_prose_words.py")
|
|
@@ -134,3 +134,19 @@ def test_archived_hook_moves_leave_the_automatic_checks(tmp_path: Path) -> None:
|
|
|
134
134
|
assert runtime_set.documents == ()
|
|
135
135
|
assert runtime_set.deleted_paths == (live_hook_path,)
|
|
136
136
|
assert runtime_set.renamed_paths == ()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_archived_audit_rubric_moves_leave_the_automatic_checks(tmp_path: Path) -> None:
|
|
140
|
+
live_rubric_path = PurePosixPath("packages/tool/audit-rubrics/prompts/category-k.md")
|
|
141
|
+
archived_rubric_path = PurePosixPath("packages/tool/audit-rubrics-archived/prompts/category-k.md")
|
|
142
|
+
document_set = DocumentSet(
|
|
143
|
+
(Document.from_text(archived_rubric_path, "archived"),),
|
|
144
|
+
SelectionKind.BASE,
|
|
145
|
+
tmp_path,
|
|
146
|
+
(),
|
|
147
|
+
((live_rubric_path, archived_rubric_path),),
|
|
148
|
+
)
|
|
149
|
+
runtime_set = _runtime_document_set(document_set)
|
|
150
|
+
assert runtime_set.documents == ()
|
|
151
|
+
assert runtime_set.deleted_paths == (live_rubric_path,)
|
|
152
|
+
assert runtime_set.renamed_paths == ()
|
|
@@ -209,7 +209,7 @@ def test_changed_line_filter_drops_other_located_line(tmp_path: Path) -> None:
|
|
|
209
209
|
assert lint_report.diagnostics == ()
|
|
210
210
|
|
|
211
211
|
|
|
212
|
-
def
|
|
212
|
+
def test_lint_drops_located_findings_when_a_change_only_deletes_lines(
|
|
213
213
|
tmp_path: Path,
|
|
214
214
|
) -> None:
|
|
215
215
|
repository_root = tmp_path.resolve()
|
|
@@ -233,7 +233,7 @@ def test_lint_keeps_located_findings_when_changed_lines_are_empty(
|
|
|
233
233
|
(document,),
|
|
234
234
|
(_changed_document_rule("probe", collect_diagnostics),),
|
|
235
235
|
)
|
|
236
|
-
assert
|
|
236
|
+
assert lint_report.diagnostics == ()
|
|
237
237
|
|
|
238
238
|
|
|
239
239
|
def test_code_rule_diagnostics_includes_comment_policy_without_prior_text(
|