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
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Keep
|
|
2
|
+
"""Keep a named extra Claude profile linked to the main Claude home.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
skills/ <----- skills/ link
|
|
9
|
-
CLAUDE.md <----- CLAUDE.md link
|
|
10
|
-
.credentials.json .credentials.json its own sign-in
|
|
11
|
-
projects/ projects/ its own history
|
|
12
|
-
|
|
13
|
-
Each run converges the profile on that picture. A stale copy moves into
|
|
14
|
-
``.replaced/<time>/`` with its content kept, a link whose main entry is gone
|
|
15
|
-
is removed, a main link under an account-local name such as a credentials
|
|
16
|
-
backup is removed, and a second run changes nothing. The run also writes the
|
|
17
|
-
``claude-ev.cmd`` launcher that starts Claude on this profile.
|
|
18
|
-
|
|
19
|
-
::
|
|
20
|
-
|
|
21
|
-
python claude_account_profile.py
|
|
22
|
-
{"linked": ["CLAUDE.md"], "moved_aside": [], "unlinked": [],
|
|
23
|
-
"launcher": "C:/Users/me/.local/bin/claude-ev.cmd"}
|
|
4
|
+
Each profile keeps its own sign-in and history while sharing the main home's
|
|
5
|
+
skills, rules, plugins, settings, and docs. Stale shared copies move into
|
|
6
|
+
``.replaced/<time>/``. A second run with the same name changes nothing. The
|
|
7
|
+
launcher is named ``claude-<profile-name>.cmd``.
|
|
24
8
|
"""
|
|
25
9
|
|
|
26
10
|
from __future__ import annotations
|
|
@@ -29,6 +13,7 @@ import argparse
|
|
|
29
13
|
import filecmp
|
|
30
14
|
import json
|
|
31
15
|
import os
|
|
16
|
+
import re
|
|
32
17
|
import subprocess
|
|
33
18
|
import sys
|
|
34
19
|
from collections.abc import Callable
|
|
@@ -39,18 +24,22 @@ from pathlib import Path
|
|
|
39
24
|
from dev_env_scripts_constants.claude_account_constants import (
|
|
40
25
|
ALL_ACCOUNT_LOCAL_NAME_PREFIXES,
|
|
41
26
|
ALL_ACCOUNT_LOCAL_NAMES,
|
|
27
|
+
ALL_WINDOWS_RESERVED_PROFILE_NAMES,
|
|
42
28
|
ALL_WINDOWS_JUNCTION_COMMAND_PREFIX,
|
|
29
|
+
CHOICE_MAIN,
|
|
30
|
+
CHOICE_WAIT,
|
|
43
31
|
JSON_LAUNCHER_KEY,
|
|
44
32
|
JSON_LINKED_KEY,
|
|
45
33
|
JSON_MOVED_ASIDE_KEY,
|
|
46
34
|
JSON_UNLINKED_KEY,
|
|
47
35
|
ALL_LAUNCHER_DIRECTORY_RELATIVE_PARTS,
|
|
48
|
-
|
|
36
|
+
LAUNCHER_FILE_NAME_TEMPLATE,
|
|
49
37
|
LAUNCHER_REPLACED_SUFFIX,
|
|
50
38
|
LAUNCHER_TEXT_TEMPLATE,
|
|
51
39
|
MAIN_CLAUDE_HOME_DIRECTORY_NAME,
|
|
52
40
|
PROFILES_ROOT_DIRECTORY_NAME,
|
|
53
41
|
PROFILES_ROOT_ENVIRONMENT_VARIABLE,
|
|
42
|
+
PROFILE_NAME_PATTERN,
|
|
54
43
|
REPLACED_DIRECTORY_NAME,
|
|
55
44
|
REPLACED_STAMP_FORMAT,
|
|
56
45
|
SECOND_ACCOUNT_PROFILE_NAME,
|
|
@@ -69,8 +58,32 @@ class ProfileSyncReport:
|
|
|
69
58
|
all_unlinked: tuple[str, ...]
|
|
70
59
|
|
|
71
60
|
|
|
72
|
-
def
|
|
73
|
-
"""
|
|
61
|
+
def validate_profile_name(profile_name: str) -> str:
|
|
62
|
+
"""Accept a profile name safe for a directory and command launcher.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
profile_name: The name to check.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The same name.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: When the name has other characters or is reserved.
|
|
72
|
+
"""
|
|
73
|
+
reserved_names = ALL_WINDOWS_RESERVED_PROFILE_NAMES | {CHOICE_MAIN, CHOICE_WAIT}
|
|
74
|
+
if (
|
|
75
|
+
re.fullmatch(PROFILE_NAME_PATTERN, profile_name) is None
|
|
76
|
+
or profile_name.casefold() in reserved_names
|
|
77
|
+
):
|
|
78
|
+
raise ValueError("profile name must use letters, digits, hyphens, or underscores")
|
|
79
|
+
return profile_name
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def default_profile_home(profile_name: str = SECOND_ACCOUNT_PROFILE_NAME) -> Path:
|
|
83
|
+
"""Locate a named extra account's profile directory.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
profile_name: The profile's name under the profiles root.
|
|
74
87
|
|
|
75
88
|
Returns:
|
|
76
89
|
The profile under the profiles root the environment names, else under
|
|
@@ -82,7 +95,7 @@ def default_profile_home() -> Path:
|
|
|
82
95
|
if profiles_root
|
|
83
96
|
else Path.home() / PROFILES_ROOT_DIRECTORY_NAME
|
|
84
97
|
)
|
|
85
|
-
return root /
|
|
98
|
+
return root / validate_profile_name(profile_name)
|
|
86
99
|
|
|
87
100
|
|
|
88
101
|
def is_account_local(entry_name: str) -> bool:
|
|
@@ -296,36 +309,47 @@ def sync_profile(
|
|
|
296
309
|
)
|
|
297
310
|
|
|
298
311
|
|
|
299
|
-
def _moved_launcher_name(now: datetime) -> str:
|
|
300
|
-
return f"{
|
|
312
|
+
def _moved_launcher_name(launcher_file_name: str, now: datetime) -> str:
|
|
313
|
+
return f"{launcher_file_name}{LAUNCHER_REPLACED_SUFFIX}{_stamp(now)}"
|
|
301
314
|
|
|
302
315
|
|
|
303
316
|
def write_launcher(
|
|
304
|
-
*,
|
|
317
|
+
*,
|
|
318
|
+
launcher_directory: Path,
|
|
319
|
+
profile_home: Path,
|
|
320
|
+
now: datetime,
|
|
321
|
+
profile_name: str = SECOND_ACCOUNT_PROFILE_NAME,
|
|
305
322
|
) -> Path:
|
|
306
|
-
"""Write the launcher that runs Claude under
|
|
323
|
+
"""Write the launcher that runs Claude under a named profile.
|
|
307
324
|
|
|
308
325
|
::
|
|
309
326
|
|
|
310
|
-
claude-
|
|
327
|
+
claude-NAME -p "fix the test"
|
|
311
328
|
-> CLAUDE_CONFIG_DIR=<profile home>, then claude -p "fix the test"
|
|
312
|
-
an older claude-
|
|
329
|
+
an older claude-NAME.cmd -> claude-NAME.cmd.replaced-<time>
|
|
313
330
|
|
|
314
331
|
Args:
|
|
315
332
|
launcher_directory: The directory on PATH that holds the launcher.
|
|
316
|
-
profile_home: The
|
|
333
|
+
profile_home: The named account's Claude home.
|
|
317
334
|
now: The run time that names a moved older launcher.
|
|
335
|
+
profile_name: The name used in the launcher file name.
|
|
318
336
|
|
|
319
337
|
Returns:
|
|
320
338
|
The launcher path.
|
|
321
339
|
"""
|
|
322
|
-
|
|
340
|
+
launcher_file_name = LAUNCHER_FILE_NAME_TEMPLATE.format(
|
|
341
|
+
profile_name=validate_profile_name(profile_name)
|
|
342
|
+
)
|
|
343
|
+
launcher_path = launcher_directory / launcher_file_name
|
|
323
344
|
launcher_text = LAUNCHER_TEXT_TEMPLATE.format(profile_home=profile_home)
|
|
324
345
|
launcher_bytes = launcher_text.encode(TEXT_ENCODING)
|
|
325
346
|
if launcher_path.is_file() and launcher_path.read_bytes() == launcher_bytes:
|
|
326
347
|
return launcher_path
|
|
327
348
|
if launcher_path.is_file():
|
|
328
|
-
os.replace(
|
|
349
|
+
os.replace(
|
|
350
|
+
launcher_path,
|
|
351
|
+
launcher_path.with_name(_moved_launcher_name(launcher_file_name, now)),
|
|
352
|
+
)
|
|
329
353
|
launcher_directory.mkdir(parents=True, exist_ok=True)
|
|
330
354
|
launcher_path.write_bytes(launcher_bytes)
|
|
331
355
|
return launcher_path
|
|
@@ -338,7 +362,8 @@ def _build_argument_parser() -> argparse.ArgumentParser:
|
|
|
338
362
|
parser.add_argument(
|
|
339
363
|
"--main-home", type=Path, default=Path.home() / MAIN_CLAUDE_HOME_DIRECTORY_NAME
|
|
340
364
|
)
|
|
341
|
-
parser.add_argument("--profile-
|
|
365
|
+
parser.add_argument("--profile-name", default=SECOND_ACCOUNT_PROFILE_NAME)
|
|
366
|
+
parser.add_argument("--profile-home", type=Path)
|
|
342
367
|
parser.add_argument(
|
|
343
368
|
"--launcher-directory",
|
|
344
369
|
type=Path,
|
|
@@ -356,27 +381,34 @@ def main(all_command_arguments: list[str]) -> int:
|
|
|
356
381
|
Returns:
|
|
357
382
|
Zero once the profile and launcher match the main home.
|
|
358
383
|
"""
|
|
359
|
-
|
|
384
|
+
parser = _build_argument_parser()
|
|
385
|
+
arguments = parser.parse_args(all_command_arguments)
|
|
386
|
+
try:
|
|
387
|
+
validate_profile_name(arguments.profile_name)
|
|
388
|
+
except ValueError as error:
|
|
389
|
+
parser.error(str(error))
|
|
390
|
+
print(json.dumps(_sync_named_profile(arguments)))
|
|
391
|
+
return 0
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _sync_named_profile(arguments: argparse.Namespace) -> dict[str, object]:
|
|
395
|
+
profile_home = arguments.profile_home or default_profile_home(arguments.profile_name)
|
|
360
396
|
now = datetime.now(timezone.utc)
|
|
361
397
|
report = sync_profile(
|
|
362
|
-
main_home=arguments.main_home, profile_home=
|
|
398
|
+
main_home=arguments.main_home, profile_home=profile_home, now=now
|
|
363
399
|
)
|
|
364
400
|
launcher_path = write_launcher(
|
|
365
401
|
launcher_directory=arguments.launcher_directory,
|
|
366
|
-
profile_home=
|
|
402
|
+
profile_home=profile_home,
|
|
367
403
|
now=now,
|
|
404
|
+
profile_name=arguments.profile_name,
|
|
368
405
|
)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
JSON_LAUNCHER_KEY: str(launcher_path),
|
|
376
|
-
}
|
|
377
|
-
)
|
|
378
|
-
)
|
|
379
|
-
return 0
|
|
406
|
+
return {
|
|
407
|
+
JSON_LINKED_KEY: list(report.all_linked),
|
|
408
|
+
JSON_MOVED_ASIDE_KEY: list(report.all_moved_aside),
|
|
409
|
+
JSON_UNLINKED_KEY: list(report.all_unlinked),
|
|
410
|
+
JSON_LAUNCHER_KEY: str(launcher_path),
|
|
411
|
+
}
|
|
380
412
|
|
|
381
413
|
|
|
382
414
|
if __name__ == "__main__":
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import importlib
|
|
7
|
+
import json
|
|
7
8
|
import os
|
|
8
9
|
import shutil
|
|
9
10
|
import subprocess
|
|
@@ -14,7 +15,13 @@ from dataclasses import dataclass
|
|
|
14
15
|
from datetime import datetime
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
17
|
-
from claude_account_choice import
|
|
18
|
+
from claude_account_choice import (
|
|
19
|
+
choose_account_from_extras,
|
|
20
|
+
config_directory_for_decision,
|
|
21
|
+
decision_payload,
|
|
22
|
+
read_account_meters,
|
|
23
|
+
read_extra_accounts,
|
|
24
|
+
)
|
|
18
25
|
from claude_account_profile import default_profile_home
|
|
19
26
|
from claude_account_worker_process import WorkerProcess, invoke_worker
|
|
20
27
|
from claude_account_worker_report import (
|
|
@@ -26,7 +33,6 @@ from claude_account_worker_report import (
|
|
|
26
33
|
)
|
|
27
34
|
from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
28
35
|
CHOICE_MAIN,
|
|
29
|
-
CHOICE_SECOND,
|
|
30
36
|
CHOICE_WAIT,
|
|
31
37
|
CLAUDE_BINARY_NAME,
|
|
32
38
|
CLAUDE_CONFIG_DIR_ENV_VAR,
|
|
@@ -35,6 +41,7 @@ from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
|
35
41
|
CWD_FLAG,
|
|
36
42
|
DEFAULT_PERMISSION_MODE,
|
|
37
43
|
DEFAULT_TIMEOUT_MINUTES,
|
|
44
|
+
EXTRA_PROFILES_FILE_NAME,
|
|
38
45
|
INVALID_TIMEOUT_MESSAGE,
|
|
39
46
|
JSON_ACCOUNT_KEY,
|
|
40
47
|
JSON_CONFIG_DIRECTORY_KEY,
|
|
@@ -104,6 +111,33 @@ def _selection_from_payload(all_selection_payload: Mapping[str, str | None]) ->
|
|
|
104
111
|
)
|
|
105
112
|
|
|
106
113
|
|
|
114
|
+
def extra_config_directories(main_config_dir: Path) -> tuple[Path, ...]:
|
|
115
|
+
"""Read the ordered local profile list or use the legacy default.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
main_config_dir: The main Claude home that may hold the list file.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The extra profile homes in the order they are tried.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
ValueError: When the list is empty, holds a non-name, or repeats a name.
|
|
125
|
+
"""
|
|
126
|
+
profiles_file = main_config_dir / EXTRA_PROFILES_FILE_NAME
|
|
127
|
+
if not profiles_file.exists():
|
|
128
|
+
return (default_profile_home(),)
|
|
129
|
+
profile_names = json.loads(profiles_file.read_text(encoding=UTF8_ENCODING))
|
|
130
|
+
if (
|
|
131
|
+
not isinstance(profile_names, list)
|
|
132
|
+
or not profile_names
|
|
133
|
+
or any(not isinstance(name, str) for name in profile_names)
|
|
134
|
+
):
|
|
135
|
+
raise ValueError("extra profile list must be a nonempty JSON list of names")
|
|
136
|
+
if len({name.casefold() for name in profile_names}) != len(profile_names):
|
|
137
|
+
raise ValueError("extra profile names must be distinct")
|
|
138
|
+
return tuple(default_profile_home(name) for name in profile_names)
|
|
139
|
+
|
|
140
|
+
|
|
107
141
|
def select_account() -> AccountSelection:
|
|
108
142
|
"""Pick the account this worker runs on and its Claude home directory.
|
|
109
143
|
|
|
@@ -111,20 +145,19 @@ def select_account() -> AccountSelection:
|
|
|
111
145
|
The chosen account, its Claude config directory, and the reason.
|
|
112
146
|
|
|
113
147
|
Raises:
|
|
114
|
-
ValueError: When the
|
|
148
|
+
ValueError: When the profile list or account decision is invalid.
|
|
115
149
|
"""
|
|
116
150
|
main_config_dir = Path.home() / MAIN_CLAUDE_HOME_DIRECTORY_NAME
|
|
117
|
-
|
|
118
|
-
config_directory_by_account = {
|
|
119
|
-
CHOICE_MAIN: main_config_dir,
|
|
120
|
-
CHOICE_SECOND: second_config_dir,
|
|
121
|
-
}
|
|
151
|
+
extra_accounts = read_extra_accounts(extra_config_directories(main_config_dir))
|
|
122
152
|
main_meters = read_account_meters(main_config_dir / CREDENTIALS_FILE_NAME)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
decision = choose_account_from_extras(
|
|
154
|
+
main_meters=main_meters,
|
|
155
|
+
all_extra_accounts=extra_accounts,
|
|
156
|
+
now=datetime.now().astimezone(),
|
|
157
|
+
)
|
|
158
|
+
selected_config_dir = config_directory_for_decision(
|
|
159
|
+
decision, main_config_dir=main_config_dir, all_extra_accounts=extra_accounts
|
|
126
160
|
)
|
|
127
|
-
selected_config_dir = config_directory_by_account.get(decision.account)
|
|
128
161
|
payload = decision_payload(decision, config_directory=selected_config_dir)
|
|
129
162
|
return _selection_from_payload(payload)
|
|
130
163
|
|
|
@@ -163,14 +196,14 @@ def _child_environment(
|
|
|
163
196
|
selection: AccountSelection,
|
|
164
197
|
all_environment_variables: Mapping[str, str],
|
|
165
198
|
) -> dict[str, str]:
|
|
166
|
-
if selection.account not in (CHOICE_MAIN, CHOICE_SECOND):
|
|
167
|
-
raise ValueError(f"unsupported account selection: {selection.account}")
|
|
168
199
|
all_child_environment_variables = dict(all_environment_variables)
|
|
169
200
|
if selection.account == CHOICE_MAIN:
|
|
170
201
|
all_child_environment_variables.pop(CLAUDE_CONFIG_DIR_ENV_VAR, None)
|
|
171
202
|
return all_child_environment_variables
|
|
203
|
+
if selection.account == CHOICE_WAIT:
|
|
204
|
+
raise ValueError("wait selection cannot launch a worker")
|
|
172
205
|
if selection.config_dir is None:
|
|
173
|
-
raise ValueError("
|
|
206
|
+
raise ValueError("extra account selection needs a config directory")
|
|
174
207
|
all_child_environment_variables[CLAUDE_CONFIG_DIR_ENV_VAR] = str(selection.config_dir)
|
|
175
208
|
return all_child_environment_variables
|
|
176
209
|
|
|
@@ -22,10 +22,28 @@ PROFILES_ROOT_ENVIRONMENT_VARIABLE: str = "LLM_SETTINGS_PROFILES_ROOT"
|
|
|
22
22
|
SECOND_ACCOUNT_PROFILE_NAME: str = "ev"
|
|
23
23
|
"""Profile directory name the second account signs in under."""
|
|
24
24
|
|
|
25
|
+
EXTRA_PROFILES_FILE_NAME: str = "extra-profiles.json"
|
|
26
|
+
"""Local main-home file listing extra profile names in selection order."""
|
|
27
|
+
|
|
28
|
+
PROFILE_NAME_PATTERN: str = r"[A-Za-z0-9][A-Za-z0-9_-]*"
|
|
29
|
+
"""Allowed profile names for directories and command launchers."""
|
|
30
|
+
|
|
31
|
+
ALL_WINDOWS_RESERVED_PROFILE_NAMES: frozenset[str] = frozenset(
|
|
32
|
+
{"con", "prn", "aux", "nul"}
|
|
33
|
+
| {f"com{number}" for number in range(1, 10)}
|
|
34
|
+
| {f"lpt{number}" for number in range(1, 10)}
|
|
35
|
+
)
|
|
36
|
+
"""Windows device names that cannot be used as profile directories."""
|
|
37
|
+
|
|
25
38
|
ALL_LAUNCHER_DIRECTORY_RELATIVE_PARTS: tuple[str, ...] = (".local", "bin")
|
|
26
39
|
"""Path parts under the user home of the directory on PATH that holds the launcher."""
|
|
27
40
|
|
|
28
|
-
|
|
41
|
+
LAUNCHER_FILE_NAME_TEMPLATE: str = "claude-{profile_name}.cmd"
|
|
42
|
+
"""File name pattern for a named profile launcher."""
|
|
43
|
+
|
|
44
|
+
LAUNCHER_FILE_NAME: str = LAUNCHER_FILE_NAME_TEMPLATE.format(
|
|
45
|
+
profile_name=SECOND_ACCOUNT_PROFILE_NAME
|
|
46
|
+
)
|
|
29
47
|
"""File name of the launcher that runs Claude under the second account."""
|
|
30
48
|
|
|
31
49
|
LAUNCHER_TEXT_TEMPLATE: str = (
|
|
@@ -67,6 +85,7 @@ ALL_ACCOUNT_LOCAL_NAMES: frozenset[str] = frozenset(
|
|
|
67
85
|
"cache",
|
|
68
86
|
"paste-cache",
|
|
69
87
|
"stats-cache.json",
|
|
88
|
+
EXTRA_PROFILES_FILE_NAME,
|
|
70
89
|
}
|
|
71
90
|
)
|
|
72
91
|
"""Top-level Claude home entries that belong to one account and are never linked."""
|
|
@@ -135,6 +154,22 @@ REASON_SECOND_UNREAD: str = (
|
|
|
135
154
|
)
|
|
136
155
|
"""Reason when the second account's meter cannot be read and it takes the job anyway."""
|
|
137
156
|
|
|
157
|
+
REASON_EXTRA_HAS_ROOM_TEMPLATE: str = (
|
|
158
|
+
"{account} has {weekly_remaining_percent:.0f}% of its week"
|
|
159
|
+
" and {session_remaining_percent:.0f}% of its 5-hour window left"
|
|
160
|
+
)
|
|
161
|
+
"""Reason when a later extra profile takes the job."""
|
|
162
|
+
|
|
163
|
+
REASON_EXTRA_UNREAD_TEMPLATE: str = (
|
|
164
|
+
"{account} meter unreadable; its own run refreshes the sign-in"
|
|
165
|
+
)
|
|
166
|
+
"""Reason when a later extra profile's meter cannot be read."""
|
|
167
|
+
|
|
168
|
+
REASON_ALL_EXTRAS_WAIT_TEMPLATE: str = (
|
|
169
|
+
"all extra accounts are full; next known reset {next_reset}"
|
|
170
|
+
)
|
|
171
|
+
"""Reason when every configured extra profile is blocked."""
|
|
172
|
+
|
|
138
173
|
REASON_WAIT_TEMPLATE: str = (
|
|
139
174
|
"second account is at {weekly_used_percent:.0f}% of its week"
|
|
140
175
|
" and {session_used_percent:.0f}% of its 5-hour window; next reset {next_reset}"
|
|
@@ -7,6 +7,7 @@ from dev_env_scripts_constants.claude_account_constants import (
|
|
|
7
7
|
CHOICE_SECOND,
|
|
8
8
|
CHOICE_WAIT,
|
|
9
9
|
CREDENTIALS_FILE_NAME,
|
|
10
|
+
EXTRA_PROFILES_FILE_NAME,
|
|
10
11
|
JSON_ACCOUNT_KEY,
|
|
11
12
|
JSON_CONFIG_DIRECTORY_KEY,
|
|
12
13
|
JSON_REASON_KEY,
|
|
@@ -103,6 +104,7 @@ __all__ = [
|
|
|
103
104
|
"DRAIN_ATTEMPT_LIMIT",
|
|
104
105
|
"DRAIN_GRACE_TIMEOUT_SECONDS",
|
|
105
106
|
"DURATION_DECIMAL_PLACES",
|
|
107
|
+
"EXTRA_PROFILES_FILE_NAME",
|
|
106
108
|
"INVALID_TIMEOUT_MESSAGE",
|
|
107
109
|
"JSON_ACCOUNT_KEY",
|
|
108
110
|
"JSON_CONFIG_DIRECTORY_KEY",
|
|
@@ -616,9 +616,15 @@ ADVISOR_CLI_RESUME_FLAG: str = "--resume"
|
|
|
616
616
|
ADVISOR_BIND_PROMPT_TEMPLATE: str = (
|
|
617
617
|
"You are the unique worker advisor for role {role_name}. "
|
|
618
618
|
"Answer only. Open with exactly one of: ENDORSE | CORRECTION | PLAN | STOP. "
|
|
619
|
-
"Pre-dispatch:
|
|
619
|
+
"Pre-dispatch consult: the full worker assignment follows. "
|
|
620
|
+
"Hunt for missing requirements, wrong assumptions, unsafe actions, and unclear "
|
|
621
|
+
"completion criteria. ENDORSE when the worker can start as written; "
|
|
622
|
+
"otherwise open with CORRECTION, PLAN, or STOP and name the fix.\n"
|
|
623
|
+
"(Advisor: please keep your guidance under 80 words, I need a focused "
|
|
624
|
+
"starting point.)\n\n"
|
|
625
|
+
"Worker assignment:\n{assignment_text}"
|
|
620
626
|
)
|
|
621
|
-
"""Prompt body
|
|
627
|
+
"""Prompt body for the pre-dispatch advisor consult; carries the assembled worker prompt."""
|
|
622
628
|
|
|
623
629
|
ADVISOR_VERDICT_PROMPT_TEMPLATE: str = (
|
|
624
630
|
"You are the unique worker advisor for role {role_name}, session {session_id}. "
|
|
@@ -7,6 +7,7 @@ UTF8_ENCODING = "utf-8"
|
|
|
7
7
|
PATH_SEPARATOR = "/"
|
|
8
8
|
ARCHIVED_SKILLS_DIRECTORY_NAME = "skill-archive"
|
|
9
9
|
ARCHIVED_HOOKS_DIRECTORY_NAME = "hooks-archived"
|
|
10
|
+
ARCHIVED_AUDIT_RUBRICS_DIRECTORY_NAME = "audit-rubrics-archived"
|
|
10
11
|
NUL_BYTE = b"\0"
|
|
11
12
|
ALL_GIT_ROOT_ARGUMENTS = ("rev-parse", "--show-toplevel")
|
|
12
13
|
ALL_GIT_HEAD_ARGUMENTS = ("rev-parse", "--verify", "HEAD")
|
|
@@ -4,6 +4,7 @@ from collections.abc import Iterable, Sequence
|
|
|
4
4
|
from pathlib import Path, PurePosixPath
|
|
5
5
|
|
|
6
6
|
from .config.constants import (
|
|
7
|
+
ARCHIVED_AUDIT_RUBRICS_DIRECTORY_NAME,
|
|
7
8
|
ARCHIVED_HOOKS_DIRECTORY_NAME,
|
|
8
9
|
ARCHIVED_SKILLS_DIRECTORY_NAME,
|
|
9
10
|
)
|
|
@@ -30,7 +31,8 @@ def lint(
|
|
|
30
31
|
"""Run the selected rules on one source selection.
|
|
31
32
|
|
|
32
33
|
Repository and diff selections check active code. File and editor
|
|
33
|
-
selections include archived skills and archived
|
|
34
|
+
selections include archived skills, archived hooks, and archived
|
|
35
|
+
audit rubrics.
|
|
34
36
|
|
|
35
37
|
Args:
|
|
36
38
|
request: Repository root, source, and rule sets.
|
|
@@ -61,6 +63,7 @@ def _is_runtime_path(path: PurePosixPath) -> bool:
|
|
|
61
63
|
return (
|
|
62
64
|
path.parts[0] != ARCHIVED_SKILLS_DIRECTORY_NAME
|
|
63
65
|
and ARCHIVED_HOOKS_DIRECTORY_NAME not in path.parts
|
|
66
|
+
and ARCHIVED_AUDIT_RUBRICS_DIRECTORY_NAME not in path.parts
|
|
64
67
|
)
|
|
65
68
|
|
|
66
69
|
|
|
@@ -252,7 +255,7 @@ def _scoped_diagnostics(
|
|
|
252
255
|
all_diagnostics: Iterable[Diagnostic],
|
|
253
256
|
document: Document,
|
|
254
257
|
) -> tuple[Diagnostic, ...]:
|
|
255
|
-
if document.changed_lines is None
|
|
258
|
+
if document.changed_lines is None:
|
|
256
259
|
return tuple(all_diagnostics)
|
|
257
260
|
return tuple(
|
|
258
261
|
each_diagnostic
|
|
@@ -118,7 +118,7 @@ ALL_TRACKED_SECRET_EXACT_EXEMPTIONS: frozenset[tuple[str, str, str]] = frozenset
|
|
|
118
118
|
"f9e409c5c16ecfdd5883da4f3ad045b4af5677740c9e703ad2d47ba231718350",
|
|
119
119
|
),
|
|
120
120
|
(
|
|
121
|
-
"packages/claude-dev-env/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md",
|
|
121
|
+
"packages/claude-dev-env/audit-rubrics-archived/prompts/category-n-test-name-scenario-verifier.md",
|
|
122
122
|
"home-path",
|
|
123
123
|
"6b98ff7f0398f06452f19c4a93f3d69186401dab40aaa7077b394ef59b3e730e",
|
|
124
124
|
),
|
|
@@ -290,6 +290,7 @@ def bind_unique_worker_advisor(
|
|
|
290
290
|
*,
|
|
291
291
|
advisor_spec: AdvisorSpec,
|
|
292
292
|
role_name: str,
|
|
293
|
+
assignment_text: str,
|
|
293
294
|
all_used_session_ids: set[str],
|
|
294
295
|
) -> tuple[str, str]:
|
|
295
296
|
"""Bind one unique advisor session for a worker; refuse duplicates and sentinels.
|
|
@@ -303,6 +304,7 @@ def bind_unique_worker_advisor(
|
|
|
303
304
|
Args:
|
|
304
305
|
advisor_spec: Lead-supplied launcher/model/effort.
|
|
305
306
|
role_name: Worker role used in the bind prompt.
|
|
307
|
+
assignment_text: Assembled worker prompt the advisor reviews before dispatch.
|
|
306
308
|
all_used_session_ids: Sessions already issued in this batch.
|
|
307
309
|
|
|
308
310
|
Returns:
|
|
@@ -317,7 +319,9 @@ def bind_unique_worker_advisor(
|
|
|
317
319
|
)
|
|
318
320
|
if advisor_spec.launcher == PENDING_BIND_SENTINEL:
|
|
319
321
|
raise ValueError(f"advisor.launcher is {PENDING_BIND_SENTINEL}")
|
|
320
|
-
prompt_text = ADVISOR_BIND_PROMPT_TEMPLATE.format(
|
|
322
|
+
prompt_text = ADVISOR_BIND_PROMPT_TEMPLATE.format(
|
|
323
|
+
role_name=role_name, assignment_text=assignment_text
|
|
324
|
+
)
|
|
321
325
|
session_id, advisor_body_text, returncode = batch_invoke_advisor(
|
|
322
326
|
launcher=advisor_spec.launcher,
|
|
323
327
|
model=advisor_spec.model,
|
|
@@ -898,6 +902,7 @@ def _bind_worker_advisor_session(
|
|
|
898
902
|
*,
|
|
899
903
|
advisor_spec: AdvisorSpec,
|
|
900
904
|
role_name: str,
|
|
905
|
+
assignment_text: str,
|
|
901
906
|
all_used_session_ids: set[str],
|
|
902
907
|
session_id_lock: object | None,
|
|
903
908
|
) -> tuple[str, str]:
|
|
@@ -906,11 +911,13 @@ def _bind_worker_advisor_session(
|
|
|
906
911
|
return bind_unique_worker_advisor(
|
|
907
912
|
advisor_spec=advisor_spec,
|
|
908
913
|
role_name=role_name,
|
|
914
|
+
assignment_text=assignment_text,
|
|
909
915
|
all_used_session_ids=all_used_session_ids,
|
|
910
916
|
)
|
|
911
917
|
return bind_unique_worker_advisor(
|
|
912
918
|
advisor_spec=advisor_spec,
|
|
913
919
|
role_name=role_name,
|
|
920
|
+
assignment_text=assignment_text,
|
|
914
921
|
all_used_session_ids=all_used_session_ids,
|
|
915
922
|
)
|
|
916
923
|
|
|
@@ -990,6 +997,10 @@ def _maybe_bind_advisor_for_worker(
|
|
|
990
997
|
advisor_session_id, pre_signal = _bind_worker_advisor_session(
|
|
991
998
|
advisor_spec=advisor_spec,
|
|
992
999
|
role_name=worker_spec.role_name,
|
|
1000
|
+
assignment_text=assemble_worker_prompt(
|
|
1001
|
+
all_prompt_part_paths=worker_spec.all_prompt_part_paths,
|
|
1002
|
+
tool_profile=worker_spec.tool_profile,
|
|
1003
|
+
),
|
|
993
1004
|
all_used_session_ids=used_ids,
|
|
994
1005
|
session_id_lock=session_id_lock,
|
|
995
1006
|
)
|