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.
Files changed (115) hide show
  1. package/.agents/agents/test_agent_frontmatter.py +0 -72
  2. package/.agents/agents-archived/code-quality-agent.md +16 -16
  3. package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +5 -21
  4. package/.agents/skills/e-code-review/SKILL.md +1 -3
  5. package/.agents/skills/e-simplify/SKILL.md +6 -6
  6. package/.agents/skills/eli5/SKILL.md +4 -14
  7. package/.agents/skills/eli5/reference/task-seeds.md +3 -4
  8. package/.agents/skills/everything-search/SKILL.md +2 -2
  9. package/.agents/skills/fresh-branch/SKILL.md +2 -3
  10. package/.agents/skills/grok-spawn/SKILL.md +3 -8
  11. package/.agents/skills/issue-tracker/SKILL.md +4 -6
  12. package/.agents/skills/orchestrator-refresh/SKILL.md +7 -7
  13. package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +3 -3
  14. package/.agents/skills/privacy-hygiene/SKILL.md +6 -10
  15. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  16. package/.agents/skills/pull-request/SKILL.md +11 -14
  17. package/.agents/skills/pull-request/reference/publication-tasks.md +2 -4
  18. package/.agents/skills/recovering-codex-startup/SKILL.md +3 -6
  19. package/.agents/skills/recovering-codex-startup/reference/task-seeds.md +1 -1
  20. package/.agents/skills/repairing-hook-boundaries/SKILL.md +4 -7
  21. package/.agents/skills/repairing-hook-boundaries/reference/task-seeds.md +1 -2
  22. package/.agents/skills/second-account-workers/SKILL.md +26 -8
  23. package/.agents/skills/syncing-submodules/SKILL.md +7 -17
  24. package/.agents/skills/syncing-submodules/reference/run-tasks.md +1 -1
  25. package/.agents/skills/task-build/SKILL.md +1 -1
  26. package/.agents/skills/task-build/reference/tool-routing.md +1 -1
  27. package/.agents/skills/team-advisor/SKILL.md +2 -4
  28. package/.agents/skills/usage-pause/SKILL.md +1 -1
  29. package/.agents/skills/windows-scheduled-task/SKILL.md +1 -1
  30. package/AGENTS.md +1 -1
  31. package/_shared/advisor/advisor-protocol.md +4 -4
  32. package/_shared/advisor/reference/consult-format.md +1 -1
  33. package/_shared/advisor/reference/identity.md +2 -2
  34. package/_shared/advisor/reference/warm-up.md +1 -1
  35. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  36. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -16
  37. package/bin/install.mjs +1 -0
  38. package/bin/install.prompt-context.test.mjs +7 -3
  39. package/bin/install.test.mjs +3 -2
  40. package/commands/sr-loop.md +2 -2
  41. package/docs/CODE_RULES.md +2 -2
  42. package/docs/DJANGO_PATTERNS.md +3 -3
  43. package/docs/REACT_PATTERNS.md +6 -6
  44. package/docs/TEST_QUALITY.md +10 -16
  45. package/docs/agent-spawn-protocol.md +4 -4
  46. package/docs/codex-compatibility.md +4 -33
  47. package/docs/references/team-advisor-skill.md +0 -1
  48. package/docs/worker-completion-gate.md +0 -1
  49. package/hooks/advisory/migration_safety_advisor.py +3 -2
  50. package/hooks/advisory/refactor_guard.py +3 -3
  51. package/hooks/hooks.json +1 -1
  52. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +1 -2
  53. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +1 -2
  54. package/package.json +1 -1
  55. package/rules/explore-thoroughly.md +0 -1
  56. package/rules/filesystem-search.md +1 -1
  57. package/rules/git-workflow.md +5 -5
  58. package/rules/long-horizon-autonomy.md +1 -5
  59. package/rules/re-stage-before-commit.md +0 -2
  60. package/rules/shell-invocation.md +1 -1
  61. package/scripts/claude_account_choice.py +183 -52
  62. package/scripts/claude_account_profile.py +81 -49
  63. package/scripts/claude_account_worker.py +48 -15
  64. package/scripts/dev_env_scripts_constants/claude_account_constants.py +36 -1
  65. package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +2 -0
  66. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +8 -2
  67. package/scripts/policy_lint/config/constants.py +1 -0
  68. package/scripts/policy_lint/engine.py +5 -2
  69. package/scripts/repository_checks/config/constants.py +1 -1
  70. package/scripts/spawn_grok_batch.py +12 -1
  71. package/scripts/test_claude_account_choice.py +127 -17
  72. package/scripts/test_claude_account_profile.py +63 -16
  73. package/scripts/test_claude_account_worker.py +118 -0
  74. package/scripts/test_spawn_grok_batch.py +22 -0
  75. package/scripts/tests/test_banned_prose_words.py +1 -1
  76. package/scripts/tests/test_policy_lint_rules_archive.py +16 -0
  77. package/scripts/tests/test_policy_lint_rules_engine.py +2 -2
  78. package/scripts/tests/test_rules.py +9 -9
  79. package/system-prompts/software-engineer.xml +15 -30
  80. package/audit-rubrics/audit-categories.json +0 -700
  81. package/audit-rubrics/category_rubrics/category-a-api-contracts.md +0 -90
  82. package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +0 -36
  83. package/audit-rubrics/category_rubrics/category-c-resource-cleanup.md +0 -35
  84. package/audit-rubrics/category_rubrics/category-d-scoping-and-ordering.md +0 -35
  85. package/audit-rubrics/category_rubrics/category-e-dead-code.md +0 -40
  86. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +0 -41
  87. package/audit-rubrics/category_rubrics/category-g-bounds-and-overflow.md +0 -38
  88. package/audit-rubrics/category_rubrics/category-h-security-boundaries.md +0 -40
  89. package/audit-rubrics/category_rubrics/category-i-concurrency.md +0 -38
  90. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +0 -59
  91. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +0 -66
  92. package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +0 -48
  93. package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +0 -44
  94. package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +0 -54
  95. package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +0 -59
  96. package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +0 -46
  97. package/audit-rubrics/prompts/category-a-api-contracts.md +0 -407
  98. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +0 -401
  99. package/audit-rubrics/prompts/category-c-resource-cleanup.md +0 -420
  100. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +0 -414
  101. package/audit-rubrics/prompts/category-e-dead-code.md +0 -434
  102. package/audit-rubrics/prompts/category-f-silent-failures.md +0 -432
  103. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +0 -383
  104. package/audit-rubrics/prompts/category-h-security-boundaries.md +0 -423
  105. package/audit-rubrics/prompts/category-i-concurrency.md +0 -429
  106. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +0 -458
  107. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +0 -338
  108. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +0 -131
  109. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +0 -129
  110. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +0 -139
  111. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +0 -89
  112. package/audit-rubrics/prompts/category-q-cross-surface-claims.md +0 -51
  113. package/hooks/hooks_constants/style_reminder_prompt_constants.py +0 -9
  114. package/hooks/session/style_reminder_prompt.py +0 -55
  115. package/hooks/session/test_style_reminder_prompt.py +0 -86
@@ -1,26 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- """Keep the second account's Claude profile linked to the main Claude home.
2
+ """Keep a named extra Claude profile linked to the main Claude home.
3
3
 
4
- The second account runs Claude with its own sign-in and history, and with
5
- everything else the main home holds: skills, rules, plugins, settings, docs::
6
-
7
- main home profile home (second account)
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
- LAUNCHER_FILE_NAME,
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 default_profile_home() -> Path:
73
- """Locate the second account's profile directory.
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 / SECOND_ACCOUNT_PROFILE_NAME
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"{LAUNCHER_FILE_NAME}{LAUNCHER_REPLACED_SUFFIX}{_stamp(now)}"
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
- *, launcher_directory: Path, profile_home: Path, now: datetime
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 the second account's profile.
323
+ """Write the launcher that runs Claude under a named profile.
307
324
 
308
325
  ::
309
326
 
310
- claude-ev -p "fix the test"
327
+ claude-NAME -p "fix the test"
311
328
  -> CLAUDE_CONFIG_DIR=<profile home>, then claude -p "fix the test"
312
- an older claude-ev.cmd -> claude-ev.cmd.replaced-<time>
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 second account's Claude home.
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
- launcher_path = launcher_directory / LAUNCHER_FILE_NAME
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(launcher_path, launcher_path.with_name(_moved_launcher_name(now)))
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-home", type=Path, default=default_profile_home())
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
- arguments = _build_argument_parser().parse_args(all_command_arguments)
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=arguments.profile_home, now=now
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=arguments.profile_home,
402
+ profile_home=profile_home,
367
403
  now=now,
404
+ profile_name=arguments.profile_name,
368
405
  )
369
- print(
370
- json.dumps(
371
- {
372
- JSON_LINKED_KEY: list(report.all_linked),
373
- JSON_MOVED_ASIDE_KEY: list(report.all_moved_aside),
374
- JSON_UNLINKED_KEY: list(report.all_unlinked),
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 choose_account, decision_payload, read_account_meters
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 account picker returns an incomplete decision.
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
- second_config_dir = default_profile_home()
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
- second_meters = read_account_meters(second_config_dir / CREDENTIALS_FILE_NAME)
124
- decision = choose_account(
125
- main_meters=main_meters, second_meters=second_meters, now=datetime.now().astimezone()
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("second account selection needs a config directory")
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
- LAUNCHER_FILE_NAME: str = "claude-ev.cmd"
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: ENDORSE this worker assignment if safe, else CORRECTION."
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 used for the pre-dispatch advisor bind."""
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 hooks.
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 or not document.changed_lines:
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(role_name=role_name)
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
  )