claude-dev-env 2.9.0 → 2.10.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 (289) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/pr-loop/CLAUDE.md +1 -1
  3. package/_shared/pr-loop/audit-contract.md +17 -6
  4. package/_shared/pr-loop/audit-reply-template.md +4 -4
  5. package/_shared/pr-loop/code-rules-gate.md +3 -5
  6. package/_shared/pr-loop/fix-protocol.md +2 -3
  7. package/_shared/pr-loop/gh-payloads.md +1 -1
  8. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  9. package/_shared/pr-loop/scripts/README.md +1 -1
  10. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  13. package/_shared/pr-loop/scripts/preflight.py +9 -4
  14. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  15. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  17. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  18. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  19. package/_shared/pr-loop/state-schema.md +5 -14
  20. package/agents/CLAUDE.md +2 -2
  21. package/agents/clean-coder.md +58 -548
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +32 -40
  25. package/audit-rubrics/CLAUDE.md +2 -1
  26. package/audit-rubrics/audit-categories.json +704 -0
  27. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  28. package/bin/CLAUDE.md +16 -5
  29. package/bin/ever-shipped-skills.mjs +2 -0
  30. package/bin/install-plan.mjs +402 -0
  31. package/bin/install-transaction.mjs +455 -0
  32. package/bin/install.mjs +593 -147
  33. package/bin/install.plan.test.mjs +194 -0
  34. package/bin/install.profile-root.test.mjs +154 -0
  35. package/bin/install.profiles.test.mjs +253 -0
  36. package/bin/install.settings-defaults.test.mjs +200 -0
  37. package/bin/install.transaction.test.mjs +400 -0
  38. package/bin/install.uninstall-transaction.test.mjs +418 -0
  39. package/bin/merge_managed_permissions.mjs +130 -0
  40. package/bin/resolve-install-root.mjs +181 -0
  41. package/bin/select-install-targets.mjs +401 -0
  42. package/commands/CLAUDE.md +0 -2
  43. package/docs/references/CLAUDE.md +1 -0
  44. package/docs/references/advisor-tool.md +1 -1
  45. package/docs/references/prose-style-enforcement.md +25 -0
  46. package/hooks/blocking/CLAUDE.md +6 -6
  47. package/hooks/blocking/_path_setup.py +9 -5
  48. package/hooks/blocking/code_rules_docstrings.py +124 -30
  49. package/hooks/blocking/code_rules_enforcer.py +161 -16
  50. package/hooks/blocking/code_rules_shared.py +40 -23
  51. package/hooks/blocking/config/CLAUDE.md +3 -5
  52. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  53. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  54. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  55. package/hooks/blocking/hedging_language_blocker.py +103 -20
  56. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  57. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  58. package/hooks/blocking/plain_language_blocker.py +139 -20
  59. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  60. package/hooks/blocking/state_description_blocker.py +7 -1
  61. package/hooks/blocking/tdd_enforcer.py +8 -0
  62. package/hooks/blocking/test__path_setup.py +28 -0
  63. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  64. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  65. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  66. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  67. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  68. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  69. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  70. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  71. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  72. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  73. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  74. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  75. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  76. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  77. package/hooks/blocking/test_state_description_blocker.py +45 -2
  78. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  79. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  80. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  81. package/hooks/hooks.json +15 -0
  82. package/hooks/hooks_constants/CLAUDE.md +14 -3
  83. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  84. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  85. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  86. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  87. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  88. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  89. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  90. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  91. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  92. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  93. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  94. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  95. package/hooks/hooks_constants/session_start_injector.py +163 -0
  96. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  97. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  98. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  99. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  100. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  101. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  102. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  103. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  104. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  105. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  106. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  107. package/hooks/observability/CLAUDE.md +2 -0
  108. package/hooks/observability/prose_matcher_advisory.py +237 -0
  109. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  110. package/hooks/session/CLAUDE.md +9 -1
  111. package/hooks/session/_path_setup.py +13 -0
  112. package/hooks/session/issue_tracker_session_starter.py +135 -0
  113. package/hooks/session/orchestrator_auto_starter.py +100 -0
  114. package/hooks/session/test__path_setup.py +28 -0
  115. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  116. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  117. package/hooks/session/test_working_style_prompt.py +47 -0
  118. package/hooks/session/untracked_repo_detector.py +1 -24
  119. package/hooks/session/working_style_prompt.py +36 -0
  120. package/hooks/validators/_path_setup.py +19 -0
  121. package/hooks/validators/run_all_validators.py +8 -13
  122. package/installable-surfaces.manifest.json +21 -0
  123. package/output-styles/CLAUDE.md +1 -3
  124. package/package.json +4 -2
  125. package/rules/CLAUDE.md +1 -0
  126. package/rules/durable-post-artifacts.md +2 -2
  127. package/rules/eli11-replies.md +6 -1
  128. package/rules/hedging-claims.md +4 -2
  129. package/rules/long-horizon-autonomy.md +3 -1
  130. package/rules/opus5-communication-contract.md +45 -0
  131. package/rules/plain-language.md +2 -2
  132. package/rules/research-mode.md +1 -1
  133. package/scripts/CLAUDE.md +11 -0
  134. package/scripts/Sync-RepoMain.ps1 +215 -0
  135. package/scripts/active_capability_references.py +218 -0
  136. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  137. package/scripts/claude_chain_runner.py +394 -6
  138. package/scripts/claude_chain_usage.py +1 -1
  139. package/scripts/codex_compat_materializer.py +105 -85
  140. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  141. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  142. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  143. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  144. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  145. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  146. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  147. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  148. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  149. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  150. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  151. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  152. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  153. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  154. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  155. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  156. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  157. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  158. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  159. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  160. package/scripts/spawn_grok_batch.py +3 -0
  161. package/scripts/test_active_capability_references.py +108 -0
  162. package/scripts/test_claude_chain_runner.py +414 -82
  163. package/scripts/test_claude_chain_usage.py +12 -12
  164. package/scripts/test_resolve_worker_spawn.py +2 -2
  165. package/scripts/test_verify_installable_package.py +208 -0
  166. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  167. package/scripts/verify_installable_package.py +612 -0
  168. package/settings.json +10 -0
  169. package/skills/CLAUDE.md +2 -0
  170. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  171. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  172. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  173. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  174. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  175. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  176. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  177. package/skills/autoconverge/reference/convergence.md +2 -1
  178. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  179. package/skills/beat-sheet/SKILL.md +54 -0
  180. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  181. package/skills/bugteam/CONSTRAINTS.md +4 -4
  182. package/skills/bugteam/EXAMPLES.md +1 -1
  183. package/skills/bugteam/reference/README.md +1 -1
  184. package/skills/e-code-review/SKILL.md +26 -5
  185. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  186. package/skills/e-code-review/reference/medium.md +15 -4
  187. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  188. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  189. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  190. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  191. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  192. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  193. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  194. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  195. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  196. package/skills/e-code-review/scripts/grok_code_review.py +16 -7
  197. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  198. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  199. package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
  200. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  201. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  202. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  203. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  204. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  205. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  206. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  207. package/skills/pr-converge/scripts/conftest.py +35 -0
  208. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  209. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  210. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  211. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  212. package/skills/rebase/SKILL.md +15 -3
  213. package/skills/reviewer-gates/SKILL.md +2 -2
  214. package/skills/show/SKILL.md +51 -0
  215. package/skills/show/references/accessibility.md +7 -0
  216. package/skills/show/references/art.md +3 -0
  217. package/skills/show/references/charts.md +3 -0
  218. package/skills/show/references/core-design.md +14 -0
  219. package/skills/show/references/erds.md +3 -0
  220. package/skills/show/references/flowcharts.md +3 -0
  221. package/skills/show/references/host-and-html.md +3 -0
  222. package/skills/show/references/illustrative-diagrams.md +10 -0
  223. package/skills/show/references/interaction.md +3 -0
  224. package/skills/show/references/mockups.md +3 -0
  225. package/skills/show/references/quality-gates.md +7 -0
  226. package/skills/show/references/structural-diagrams.md +3 -0
  227. package/skills/show/references/subject-inventory.md +21 -0
  228. package/skills/show/references/svg-contract.md +22 -0
  229. package/skills/show/routing.yaml +30 -0
  230. package/skills/show/samples/pr1262-v2.svg +222 -0
  231. package/skills/show/scripts/README.md +6 -0
  232. package/skills/show/scripts/validate-artifact.py +91 -0
  233. package/skills/show/scripts/validate-package.py +18 -0
  234. package/skills/show/templates/html-widget.html +4 -0
  235. package/skills/show/templates/svg-base.svg +19 -0
  236. package/skills/show/tests/fixtures/css-var.svg +6 -0
  237. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  238. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  239. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  240. package/skills/show/tests/fixtures/invalid.svg +1 -0
  241. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  242. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  243. package/skills/show/tests/fixtures/valid.html +1 -0
  244. package/skills/show/tests/test_validate-artifact.py +74 -0
  245. package/skills/show/tests/test_validators.py +59 -0
  246. package/skills/show/workflows/create-visual.md +13 -0
  247. package/skills/show/workflows/review-visual.md +20 -0
  248. package/skills/split-pr/SKILL.md +85 -0
  249. package/skills/split-pr/reference/path-layers.md +16 -0
  250. package/skills/split-pr/reference/proposal-format.md +15 -0
  251. package/skills/split-pr/reference/split-further-loop.md +10 -0
  252. package/skills/split-pr/reference/splitting-principles.md +26 -0
  253. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  254. package/skills/split-pr/scripts/categorize_files.py +106 -0
  255. package/skills/split-pr/scripts/config/__init__.py +1 -0
  256. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  257. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  258. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  259. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  260. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  261. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  262. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  263. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  264. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  265. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  266. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  267. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  268. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  269. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  270. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  271. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  272. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  273. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  274. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  275. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  276. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  277. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  278. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  279. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  280. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  281. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  282. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  283. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  284. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  285. package/skills/split-pr/scripts/verify_plan.py +139 -0
  286. package/system-prompts/software-engineer.xml +11 -2
  287. package/commands/initialize.md +0 -90
  288. package/commands/stubcheck.md +0 -88
  289. package/output-styles/caveman-agent.md +0 -37
@@ -15,7 +15,25 @@ if str(_SCRIPTS_DIR) not in sys.path:
15
15
 
16
16
  import claude_chain_runner as runner # noqa: E402
17
17
  import claude_chain_usage as chain_usage # noqa: E402
18
+ from claude_chain_runner import ( # noqa: E402
19
+ ChainEntry,
20
+ default_affinity_state_path,
21
+ extract_resume_session_id,
22
+ load_affinity_store,
23
+ lookup_affinity_command,
24
+ order_entries_for_resume,
25
+ record_affinity_binding,
26
+ save_affinity_store_atomic,
27
+ )
18
28
  from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
29
+ AFFINITY_KEY_ALL_BINDINGS,
30
+ AFFINITY_KEY_COMMAND,
31
+ AFFINITY_KEY_SCHEMA_VERSION,
32
+ AFFINITY_KEY_SESSION_ID,
33
+ AFFINITY_MAXIMUM_ENTRIES,
34
+ AFFINITY_STATE_FILENAME,
35
+ AFFINITY_STATE_SCHEMA_VERSION,
36
+ RESUME_SESSION_FLAG,
19
37
  ALL_USAGE_LIMIT_SIGNATURES,
20
38
  ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
21
39
  ATTEMPT_STATUS_NONZERO_EXIT,
@@ -183,6 +201,12 @@ def _install(
183
201
  else _config_order_usage_reporter
184
202
  )
185
203
  monkeypatch.setattr(runner, "chain_weekly_usage_reporter", active_reporter)
204
+ affinity_path = Path(config_file).parent / AFFINITY_STATE_FILENAME
205
+ monkeypatch.setattr(
206
+ runner,
207
+ "default_affinity_state_path",
208
+ lambda _home, _affinity_path=affinity_path: _affinity_path,
209
+ )
186
210
  _install_tty_stdin(monkeypatch)
187
211
  return recorder
188
212
 
@@ -190,11 +214,11 @@ def _install(
190
214
  def test_highest_weekly_remaining_is_tried_first(
191
215
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
192
216
  ) -> None:
193
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
217
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
194
218
  usage_reporter = _usage_reporter_from_remaining(
195
219
  {
196
220
  "claude": _LOW_WEEKLY_REMAINING_PERCENT,
197
- "claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
221
+ "claude-profile-c": _HIGH_WEEKLY_REMAINING_PERCENT,
198
222
  }
199
223
  )
200
224
  recorder = _install(
@@ -202,17 +226,17 @@ def test_highest_weekly_remaining_is_tried_first(
202
226
  config_file,
203
227
  {
204
228
  "claude": _completed("claude", 0, stdout="from-claude"),
205
- "claude-ev": _completed("claude-ev", 0, stdout="from-ev"),
229
+ "claude-profile-c": _completed("claude-profile-c", 0, stdout="from-profile-c"),
206
230
  },
207
231
  weekly_usage_reporter=usage_reporter,
208
232
  )
209
233
  chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
210
- assert chain_result.served_command == "claude-ev"
234
+ assert chain_result.served_command == "claude-profile-c"
211
235
  assert chain_result.returncode == 0
212
- assert chain_result.stdout == "from-ev"
213
- assert recorder.invocations[0][0] == "claude-ev"
236
+ assert chain_result.stdout == "from-profile-c"
237
+ assert recorder.invocations[0][0] == "claude-profile-c"
214
238
  assert [each_attempt.command for each_attempt in chain_result.attempts] == [
215
- "claude-ev"
239
+ "claude-profile-c"
216
240
  ]
217
241
  assert chain_result.attempts[0].status == ATTEMPT_STATUS_SERVED
218
242
 
@@ -221,22 +245,22 @@ def test_usage_limit_failsover_remaining_ranked_accounts(
221
245
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
222
246
  ) -> None:
223
247
  config_file = _write_chain_config(
224
- tmp_path, [_entry("claude"), _entry("claude-ev"), _entry("claude-editor")]
248
+ tmp_path, [_entry("claude"), _entry("claude-profile-c"), _entry("claude-profile-a")]
225
249
  )
226
250
  usage_reporter = _usage_reporter_from_remaining(
227
251
  {
228
252
  "claude": _MID_WEEKLY_REMAINING_PERCENT,
229
- "claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
230
- "claude-editor": _LOW_WEEKLY_REMAINING_PERCENT,
253
+ "claude-profile-c": _HIGH_WEEKLY_REMAINING_PERCENT,
254
+ "claude-profile-a": _LOW_WEEKLY_REMAINING_PERCENT,
231
255
  }
232
256
  )
233
257
  _install(
234
258
  monkeypatch,
235
259
  config_file,
236
260
  {
237
- "claude-ev": _completed("claude-ev", 1, stderr=_A_SIGNATURE),
261
+ "claude-profile-c": _completed("claude-profile-c", 1, stderr=_A_SIGNATURE),
238
262
  "claude": _completed("claude", 0, stdout="ok"),
239
- "claude-editor": _completed("claude-editor", 0, stdout="should not run"),
263
+ "claude-profile-a": _completed("claude-profile-a", 0, stdout="should not run"),
240
264
  },
241
265
  weekly_usage_reporter=usage_reporter,
242
266
  )
@@ -244,7 +268,7 @@ def test_usage_limit_failsover_remaining_ranked_accounts(
244
268
  assert chain_result.served_command == "claude"
245
269
  assert chain_result.returncode == 0
246
270
  assert [each_attempt.command for each_attempt in chain_result.attempts] == [
247
- "claude-ev",
271
+ "claude-profile-c",
248
272
  "claude",
249
273
  ]
250
274
  assert chain_result.attempts[0].status == ATTEMPT_STATUS_USAGE_LIMITED
@@ -254,24 +278,24 @@ def test_usage_limit_failsover_remaining_ranked_accounts(
254
278
  def test_non_usage_error_on_highest_ranked_stops_without_rest(
255
279
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
256
280
  ) -> None:
257
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
281
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
258
282
  usage_reporter = _usage_reporter_from_remaining(
259
283
  {
260
284
  "claude": _LOW_WEEKLY_REMAINING_PERCENT,
261
- "claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
285
+ "claude-profile-c": _HIGH_WEEKLY_REMAINING_PERCENT,
262
286
  }
263
287
  )
264
288
  _install(
265
289
  monkeypatch,
266
290
  config_file,
267
291
  {
268
- "claude-ev": _completed("claude-ev", 2, stderr="unknown flag"),
292
+ "claude-profile-c": _completed("claude-profile-c", 2, stderr="unknown flag"),
269
293
  "claude": _completed("claude", 0, stdout="should not run"),
270
294
  },
271
295
  weekly_usage_reporter=usage_reporter,
272
296
  )
273
297
  chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
274
- assert chain_result.served_command == "claude-ev"
298
+ assert chain_result.served_command == "claude-profile-c"
275
299
  assert chain_result.returncode == 2
276
300
  assert len(chain_result.attempts) == 1
277
301
  assert chain_result.attempts[0].status == ATTEMPT_STATUS_NONZERO_EXIT
@@ -280,11 +304,11 @@ def test_non_usage_error_on_highest_ranked_stops_without_rest(
280
304
  def test_weekly_usage_probe_runs_once_per_run_claude(
281
305
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
282
306
  ) -> None:
283
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
307
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
284
308
  usage_reporter = _usage_reporter_from_remaining(
285
309
  {
286
310
  "claude": _HIGH_WEEKLY_REMAINING_PERCENT,
287
- "claude-ev": _LOW_WEEKLY_REMAINING_PERCENT,
311
+ "claude-profile-c": _LOW_WEEKLY_REMAINING_PERCENT,
288
312
  }
289
313
  )
290
314
  _install(
@@ -292,7 +316,7 @@ def test_weekly_usage_probe_runs_once_per_run_claude(
292
316
  config_file,
293
317
  {
294
318
  "claude": _completed("claude", 1, stderr=_A_SIGNATURE),
295
- "claude-ev": _completed("claude-ev", 0, stdout="ok"),
319
+ "claude-profile-c": _completed("claude-profile-c", 0, stdout="ok"),
296
320
  },
297
321
  weekly_usage_reporter=usage_reporter,
298
322
  )
@@ -303,7 +327,7 @@ def test_weekly_usage_probe_runs_once_per_run_claude(
303
327
  def test_usage_reporter_import_failure_falls_back_to_config_order(
304
328
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
305
329
  ) -> None:
306
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
330
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
307
331
 
308
332
  def broken_usage_reporter(
309
333
  *, config_path: Path
@@ -315,7 +339,7 @@ def test_usage_reporter_import_failure_falls_back_to_config_order(
315
339
  config_file,
316
340
  {
317
341
  "claude": _completed("claude", 0, stdout="from-claude"),
318
- "claude-ev": _completed("claude-ev", 0, stdout="from-ev"),
342
+ "claude-profile-c": _completed("claude-profile-c", 0, stdout="from-profile-c"),
319
343
  },
320
344
  weekly_usage_reporter=broken_usage_reporter,
321
345
  )
@@ -332,18 +356,18 @@ def test_usage_reporter_import_failure_falls_back_to_config_order(
332
356
  def test_missing_high_remaining_binary_falls_through_to_lower_remaining(
333
357
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
334
358
  ) -> None:
335
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
359
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
336
360
  usage_reporter = _usage_reporter_from_remaining(
337
361
  {
338
362
  "claude": _LOW_WEEKLY_REMAINING_PERCENT,
339
- "claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
363
+ "claude-profile-c": _HIGH_WEEKLY_REMAINING_PERCENT,
340
364
  }
341
365
  )
342
366
  _install(
343
367
  monkeypatch,
344
368
  config_file,
345
369
  {
346
- "claude-ev": FileNotFoundError(),
370
+ "claude-profile-c": FileNotFoundError(),
347
371
  "claude": _completed("claude", 0, stdout="from-claude"),
348
372
  },
349
373
  weekly_usage_reporter=usage_reporter,
@@ -353,7 +377,7 @@ def test_missing_high_remaining_binary_falls_through_to_lower_remaining(
353
377
  assert chain_result.returncode == 0
354
378
  assert chain_result.stdout == "from-claude"
355
379
  assert [each_attempt.command for each_attempt in chain_result.attempts] == [
356
- "claude-ev",
380
+ "claude-profile-c",
357
381
  "claude",
358
382
  ]
359
383
  assert [each_attempt.status for each_attempt in chain_result.attempts] == [
@@ -365,18 +389,18 @@ def test_missing_high_remaining_binary_falls_through_to_lower_remaining(
365
389
  def test_all_missing_binaries_exhausts_chain(
366
390
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
367
391
  ) -> None:
368
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
392
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
369
393
  usage_reporter = _usage_reporter_from_remaining(
370
394
  {
371
395
  "claude": _LOW_WEEKLY_REMAINING_PERCENT,
372
- "claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
396
+ "claude-profile-c": _HIGH_WEEKLY_REMAINING_PERCENT,
373
397
  }
374
398
  )
375
399
  _install(
376
400
  monkeypatch,
377
401
  config_file,
378
402
  {
379
- "claude-ev": FileNotFoundError(),
403
+ "claude-profile-c": FileNotFoundError(),
380
404
  "claude": FileNotFoundError(),
381
405
  },
382
406
  weekly_usage_reporter=usage_reporter,
@@ -385,7 +409,7 @@ def test_all_missing_binaries_exhausts_chain(
385
409
  assert chain_result.served_command is None
386
410
  assert chain_result.returncode == NO_COMPLETED_PROCESS_RETURN_CODE
387
411
  assert [each_attempt.command for each_attempt in chain_result.attempts] == [
388
- "claude-ev",
412
+ "claude-profile-c",
389
413
  "claude",
390
414
  ]
391
415
  assert [each_attempt.status for each_attempt in chain_result.attempts] == [
@@ -398,31 +422,31 @@ def test_missing_later_ranked_binary_is_skipped(
398
422
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
399
423
  ) -> None:
400
424
  config_file = _write_chain_config(
401
- tmp_path, [_entry("claude"), _entry("claude-ev"), _entry("claude-editor")]
425
+ tmp_path, [_entry("claude"), _entry("claude-profile-c"), _entry("claude-profile-a")]
402
426
  )
403
427
  usage_reporter = _usage_reporter_from_remaining(
404
428
  {
405
429
  "claude": _MID_WEEKLY_REMAINING_PERCENT,
406
- "claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
407
- "claude-editor": _LOW_WEEKLY_REMAINING_PERCENT,
430
+ "claude-profile-c": _HIGH_WEEKLY_REMAINING_PERCENT,
431
+ "claude-profile-a": _LOW_WEEKLY_REMAINING_PERCENT,
408
432
  }
409
433
  )
410
434
  _install(
411
435
  monkeypatch,
412
436
  config_file,
413
437
  {
414
- "claude-ev": _completed("claude-ev", 1, stderr=_A_SIGNATURE),
438
+ "claude-profile-c": _completed("claude-profile-c", 1, stderr=_A_SIGNATURE),
415
439
  "claude": FileNotFoundError(),
416
- "claude-editor": _completed("claude-editor", 0, stdout="done"),
440
+ "claude-profile-a": _completed("claude-profile-a", 0, stdout="done"),
417
441
  },
418
442
  weekly_usage_reporter=usage_reporter,
419
443
  )
420
444
  chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
421
- assert chain_result.served_command == "claude-editor"
445
+ assert chain_result.served_command == "claude-profile-a"
422
446
  assert [each_attempt.command for each_attempt in chain_result.attempts] == [
423
- "claude-ev",
447
+ "claude-profile-c",
424
448
  "claude",
425
- "claude-editor",
449
+ "claude-profile-a",
426
450
  ]
427
451
  assert [each_attempt.status for each_attempt in chain_result.attempts] == [
428
452
  ATTEMPT_STATUS_USAGE_LIMITED,
@@ -438,28 +462,28 @@ def test_ranked_walk_preserves_extra_args_for_mapped_entry(
438
462
  tmp_path,
439
463
  [
440
464
  _entry("claude", extra_args=["--account", "primary"]),
441
- _entry("claude-ev", extra_args=["--account", "ev"]),
465
+ _entry("claude-profile-c", extra_args=["--account", "profile-c"]),
442
466
  ],
443
467
  )
444
468
  usage_reporter = _usage_reporter_from_remaining(
445
469
  {
446
470
  "claude": _LOW_WEEKLY_REMAINING_PERCENT,
447
- "claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
471
+ "claude-profile-c": _HIGH_WEEKLY_REMAINING_PERCENT,
448
472
  }
449
473
  )
450
474
  recorder = _install(
451
475
  monkeypatch,
452
476
  config_file,
453
- {"claude-ev": _completed("claude-ev", 0)},
477
+ {"claude-profile-c": _completed("claude-profile-c", 0)},
454
478
  weekly_usage_reporter=usage_reporter,
455
479
  )
456
480
  runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
457
481
  assert recorder.invocations[0] == [
458
- "claude-ev",
482
+ "claude-profile-c",
459
483
  "-p",
460
484
  "hello",
461
485
  "--account",
462
- "ev",
486
+ "profile-c",
463
487
  ]
464
488
 
465
489
 
@@ -496,14 +520,14 @@ def test_duplicate_command_entries_are_both_walked(
496
520
  def test_entry_absent_from_usage_report_is_still_walked(
497
521
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
498
522
  ) -> None:
499
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
523
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
500
524
 
501
525
  def partial_usage_reporter(
502
526
  *, config_path: Path
503
527
  ) -> list[chain_usage.AccountUsageReport]:
504
528
  return [
505
529
  chain_usage.AccountUsageReport(
506
- command="claude-ev",
530
+ command="claude-profile-c",
507
531
  weekly_remaining_percent=_HIGH_WEEKLY_REMAINING_PERCENT,
508
532
  )
509
533
  ]
@@ -512,7 +536,7 @@ def test_entry_absent_from_usage_report_is_still_walked(
512
536
  monkeypatch,
513
537
  config_file,
514
538
  {
515
- "claude-ev": _completed("claude-ev", 1, stderr=_A_SIGNATURE),
539
+ "claude-profile-c": _completed("claude-profile-c", 1, stderr=_A_SIGNATURE),
516
540
  "claude": _completed("claude", 0, stdout="ok"),
517
541
  },
518
542
  weekly_usage_reporter=partial_usage_reporter,
@@ -520,7 +544,7 @@ def test_entry_absent_from_usage_report_is_still_walked(
520
544
  chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
521
545
  assert chain_result.served_command == "claude"
522
546
  assert [each_attempt.command for each_attempt in chain_result.attempts] == [
523
- "claude-ev",
547
+ "claude-profile-c",
524
548
  "claude",
525
549
  ]
526
550
 
@@ -528,22 +552,22 @@ def test_entry_absent_from_usage_report_is_still_walked(
528
552
  def test_usage_limited_primary_falls_over_to_second(
529
553
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
530
554
  ) -> None:
531
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
555
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
532
556
  _install(
533
557
  monkeypatch,
534
558
  config_file,
535
559
  {
536
560
  "claude": _completed("claude", 1, stderr=_A_SIGNATURE),
537
- "claude-ev": _completed("claude-ev", 0, stdout="ok"),
561
+ "claude-profile-c": _completed("claude-profile-c", 0, stdout="ok"),
538
562
  },
539
563
  )
540
564
  chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
541
- assert chain_result.served_command == "claude-ev"
565
+ assert chain_result.served_command == "claude-profile-c"
542
566
  assert chain_result.returncode == 0
543
567
  assert chain_result.stdout == "ok"
544
568
  assert [each_attempt.command for each_attempt in chain_result.attempts] == [
545
569
  "claude",
546
- "claude-ev",
570
+ "claude-profile-c",
547
571
  ]
548
572
  assert chain_result.attempts[0].status == ATTEMPT_STATUS_USAGE_LIMITED
549
573
  assert chain_result.attempts[1].status == ATTEMPT_STATUS_SERVED
@@ -552,13 +576,13 @@ def test_usage_limited_primary_falls_over_to_second(
552
576
  def test_nonzero_exit_without_signature_does_not_fall_over(
553
577
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
554
578
  ) -> None:
555
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
579
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
556
580
  _install(
557
581
  monkeypatch,
558
582
  config_file,
559
583
  {
560
584
  "claude": _completed("claude", 2, stderr="unknown flag"),
561
- "claude-ev": _completed("claude-ev", 0, stdout="should not run"),
585
+ "claude-profile-c": _completed("claude-profile-c", 0, stdout="should not run"),
562
586
  },
563
587
  )
564
588
  chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
@@ -571,13 +595,13 @@ def test_nonzero_exit_without_signature_does_not_fall_over(
571
595
  def test_timeout_on_primary_does_not_fall_over(
572
596
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
573
597
  ) -> None:
574
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
598
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
575
599
  _install(
576
600
  monkeypatch,
577
601
  config_file,
578
602
  {
579
603
  "claude": subprocess.TimeoutExpired(cmd=["claude"], timeout=5),
580
- "claude-ev": _completed("claude-ev", 0),
604
+ "claude-profile-c": _completed("claude-profile-c", 0),
581
605
  },
582
606
  )
583
607
  chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
@@ -591,22 +615,22 @@ def test_timeout_on_primary_does_not_fall_over(
591
615
  def test_missing_primary_binary_falls_through_to_next(
592
616
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
593
617
  ) -> None:
594
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
618
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
595
619
  _install(
596
620
  monkeypatch,
597
621
  config_file,
598
622
  {
599
623
  "claude": FileNotFoundError(),
600
- "claude-ev": _completed("claude-ev", 0, stdout="from-ev"),
624
+ "claude-profile-c": _completed("claude-profile-c", 0, stdout="from-profile-c"),
601
625
  },
602
626
  )
603
627
  chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
604
- assert chain_result.served_command == "claude-ev"
628
+ assert chain_result.served_command == "claude-profile-c"
605
629
  assert chain_result.returncode == 0
606
- assert chain_result.stdout == "from-ev"
630
+ assert chain_result.stdout == "from-profile-c"
607
631
  assert [each_attempt.command for each_attempt in chain_result.attempts] == [
608
632
  "claude",
609
- "claude-ev",
633
+ "claude-profile-c",
610
634
  ]
611
635
  assert [each_attempt.status for each_attempt in chain_result.attempts] == [
612
636
  ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
@@ -618,19 +642,19 @@ def test_missing_fallback_binary_is_skipped_and_walk_continues(
618
642
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
619
643
  ) -> None:
620
644
  config_file = _write_chain_config(
621
- tmp_path, [_entry("claude"), _entry("claude-ev"), _entry("claude-editor")]
645
+ tmp_path, [_entry("claude"), _entry("claude-profile-c"), _entry("claude-profile-a")]
622
646
  )
623
647
  _install(
624
648
  monkeypatch,
625
649
  config_file,
626
650
  {
627
651
  "claude": _completed("claude", 1, stderr=_A_SIGNATURE),
628
- "claude-ev": FileNotFoundError(),
629
- "claude-editor": _completed("claude-editor", 0, stdout="done"),
652
+ "claude-profile-c": FileNotFoundError(),
653
+ "claude-profile-a": _completed("claude-profile-a", 0, stdout="done"),
630
654
  },
631
655
  )
632
656
  chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
633
- assert chain_result.served_command == "claude-editor"
657
+ assert chain_result.served_command == "claude-profile-a"
634
658
  assert [each_attempt.status for each_attempt in chain_result.attempts] == [
635
659
  ATTEMPT_STATUS_USAGE_LIMITED,
636
660
  ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
@@ -641,13 +665,13 @@ def test_missing_fallback_binary_is_skipped_and_walk_continues(
641
665
  def test_zero_exit_mentioning_usage_limit_is_success(
642
666
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
643
667
  ) -> None:
644
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
668
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
645
669
  _install(
646
670
  monkeypatch,
647
671
  config_file,
648
672
  {
649
673
  "claude": _completed("claude", 0, stdout=f"note: {_A_SIGNATURE} earlier"),
650
- "claude-ev": _completed("claude-ev", 0),
674
+ "claude-profile-c": _completed("claude-profile-c", 0),
651
675
  },
652
676
  )
653
677
  chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
@@ -660,13 +684,13 @@ def test_zero_exit_mentioning_usage_limit_is_success(
660
684
  def test_exhausted_chain_records_every_attempt(
661
685
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
662
686
  ) -> None:
663
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
687
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
664
688
  _install(
665
689
  monkeypatch,
666
690
  config_file,
667
691
  {
668
692
  "claude": _completed("claude", 1, stderr=_A_SIGNATURE),
669
- "claude-ev": _completed("claude-ev", 1, stderr=_A_SIGNATURE),
693
+ "claude-profile-c": _completed("claude-profile-c", 1, stderr=_A_SIGNATURE),
670
694
  },
671
695
  )
672
696
  chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
@@ -683,12 +707,12 @@ def test_reordering_config_changes_walk_order(
683
707
  ) -> None:
684
708
  behavior_by_command = {
685
709
  "claude": _completed("claude", 0, stdout="from-claude"),
686
- "claude-ev": _completed("claude-ev", 0, stdout="from-ev"),
710
+ "claude-profile-c": _completed("claude-profile-c", 0, stdout="from-profile-c"),
687
711
  }
688
712
  first_config = tmp_path / "first"
689
713
  first_config.mkdir()
690
714
  config_a = _write_chain_config(
691
- first_config, [_entry("claude"), _entry("claude-ev")]
715
+ first_config, [_entry("claude"), _entry("claude-profile-c")]
692
716
  )
693
717
  _install(monkeypatch, config_a, behavior_by_command)
694
718
  result_a = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
@@ -697,22 +721,22 @@ def test_reordering_config_changes_walk_order(
697
721
  second_config = tmp_path / "second"
698
722
  second_config.mkdir()
699
723
  config_b = _write_chain_config(
700
- second_config, [_entry("claude-ev"), _entry("claude")]
724
+ second_config, [_entry("claude-profile-c"), _entry("claude")]
701
725
  )
702
726
  _install(monkeypatch, config_b, behavior_by_command)
703
727
  result_b = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
704
- assert result_b.served_command == "claude-ev"
728
+ assert result_b.served_command == "claude-profile-c"
705
729
 
706
730
 
707
731
  def test_extra_args_are_appended_to_invocation(
708
732
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
709
733
  ) -> None:
710
734
  config_file = _write_chain_config(
711
- tmp_path, [_entry("claude", extra_args=["--account", "ev"])]
735
+ tmp_path, [_entry("claude", extra_args=["--account", "profile-c"])]
712
736
  )
713
737
  recorder = _install(monkeypatch, config_file, {"claude": _completed("claude", 0)})
714
738
  runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
715
- assert recorder.invocations[0] == ["claude", "-p", "hello", "--account", "ev"]
739
+ assert recorder.invocations[0] == ["claude", "-p", "hello", "--account", "profile-c"]
716
740
 
717
741
 
718
742
  def test_run_claude_forwards_stdin_text_to_subprocess(
@@ -757,17 +781,17 @@ def test_cli_forwards_piped_stdin_to_invocation(
757
781
  def test_signature_matching_is_case_insensitive(
758
782
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
759
783
  ) -> None:
760
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
784
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
761
785
  _install(
762
786
  monkeypatch,
763
787
  config_file,
764
788
  {
765
789
  "claude": _completed("claude", 1, stderr=_A_SIGNATURE.upper()),
766
- "claude-ev": _completed("claude-ev", 0),
790
+ "claude-profile-c": _completed("claude-profile-c", 0),
767
791
  },
768
792
  )
769
793
  chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
770
- assert chain_result.served_command == "claude-ev"
794
+ assert chain_result.served_command == "claude-profile-c"
771
795
 
772
796
 
773
797
  def test_cli_passthrough_builds_argument_list_and_timeout(
@@ -811,13 +835,13 @@ def test_cli_served_nonzero_exit_is_passed_through(
811
835
  def test_cli_exhausted_chain_exits_nonzero_with_attempt_summary(
812
836
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
813
837
  ) -> None:
814
- config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
838
+ config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-profile-c")])
815
839
  _install(
816
840
  monkeypatch,
817
841
  config_file,
818
842
  {
819
843
  "claude": _completed("claude", 1, stderr=_A_SIGNATURE),
820
- "claude-ev": _completed("claude-ev", 1, stderr=_A_SIGNATURE),
844
+ "claude-profile-c": _completed("claude-profile-c", 1, stderr=_A_SIGNATURE),
821
845
  },
822
846
  )
823
847
  exit_code = runner.main([CLI_ARGUMENTS_SEPARATOR, "-p", "hi"])
@@ -827,7 +851,7 @@ def test_cli_exhausted_chain_exits_nonzero_with_attempt_summary(
827
851
  ATTEMPT_SUMMARY_ENTRY_TEMPLATE.format(
828
852
  command=each_command, status=ATTEMPT_STATUS_USAGE_LIMITED
829
853
  )
830
- for each_command in ("claude", "claude-ev")
854
+ for each_command in ("claude", "claude-profile-c")
831
855
  )
832
856
  assert (
833
857
  CHAIN_EXHAUSTED_MESSAGE_TEMPLATE.format(attempt_summary=expected_summary)
@@ -937,11 +961,11 @@ def test_extra_args_default_to_empty_when_omitted(tmp_path: Path) -> None:
937
961
 
938
962
  def test_load_chain_parses_command_and_extra_args(tmp_path: Path) -> None:
939
963
  config_file = _write_chain_config(
940
- tmp_path, [_entry("claude", extra_args=["--account", "ev"])]
964
+ tmp_path, [_entry("claude", extra_args=["--account", "profile-c"])]
941
965
  )
942
966
  all_entries = runner.load_chain(config_file)
943
967
  assert all_entries == [
944
- runner.ChainEntry(command="claude", extra_args=("--account", "ev"))
968
+ runner.ChainEntry(command="claude", extra_args=("--account", "profile-c"))
945
969
  ]
946
970
 
947
971
 
@@ -1477,3 +1501,311 @@ def test_cli_ordered_account_advisor_blocked_exit_code(
1477
1501
  assert exit_code == CHAIN_ADVISOR_BLOCKED_EXIT_CODE
1478
1502
 
1479
1503
 
1504
+ def test_default_affinity_state_path_joins_filename() -> None:
1505
+ claude_home = Path("/tmp/claude-home")
1506
+ resolved = default_affinity_state_path(claude_home)
1507
+ assert resolved == claude_home / AFFINITY_STATE_FILENAME
1508
+
1509
+
1510
+ def test_affinity_store_round_trip_is_versioned_and_atomic(tmp_path: Path) -> None:
1511
+ state_path = tmp_path / AFFINITY_STATE_FILENAME
1512
+ empty_store = load_affinity_store(state_path)
1513
+ assert empty_store.schema_version == AFFINITY_STATE_SCHEMA_VERSION
1514
+ assert empty_store.all_bindings == []
1515
+
1516
+ updated = record_affinity_binding(
1517
+ empty_store,
1518
+ session_id=_BOUND_SESSION_ID,
1519
+ command=_PRIMARY_LAUNCHER,
1520
+ )
1521
+ save_affinity_store_atomic(state_path, updated)
1522
+
1523
+ reloaded = load_affinity_store(state_path)
1524
+ assert reloaded.schema_version == AFFINITY_STATE_SCHEMA_VERSION
1525
+ assert len(reloaded.all_bindings) == 1
1526
+ assert reloaded.all_bindings[0].session_id == _BOUND_SESSION_ID
1527
+ assert reloaded.all_bindings[0].command == _PRIMARY_LAUNCHER
1528
+ on_disk = json.loads(state_path.read_text(encoding=UTF8_ENCODING))
1529
+ assert on_disk[AFFINITY_KEY_SCHEMA_VERSION] == AFFINITY_STATE_SCHEMA_VERSION
1530
+ assert on_disk[AFFINITY_KEY_ALL_BINDINGS][0][AFFINITY_KEY_SESSION_ID] == (
1531
+ _BOUND_SESSION_ID
1532
+ )
1533
+ assert on_disk[AFFINITY_KEY_ALL_BINDINGS][0][AFFINITY_KEY_COMMAND] == (
1534
+ _PRIMARY_LAUNCHER
1535
+ )
1536
+
1537
+
1538
+ def test_affinity_store_is_bounded_and_drops_oldest() -> None:
1539
+ store = runner.AffinityStore()
1540
+ for each_index in range(AFFINITY_MAXIMUM_ENTRIES + 3):
1541
+ store = record_affinity_binding(
1542
+ store,
1543
+ session_id=f"session-{each_index}",
1544
+ command=_PRIMARY_LAUNCHER,
1545
+ maximum_entries=AFFINITY_MAXIMUM_ENTRIES,
1546
+ )
1547
+ assert len(store.all_bindings) == AFFINITY_MAXIMUM_ENTRIES
1548
+ all_session_ids = [each.session_id for each in store.all_bindings]
1549
+ assert "session-0" not in all_session_ids
1550
+ assert "session-1" not in all_session_ids
1551
+ assert "session-2" not in all_session_ids
1552
+ assert f"session-{AFFINITY_MAXIMUM_ENTRIES + 2}" in all_session_ids
1553
+
1554
+
1555
+ def test_affinity_rebind_moves_session_to_newest_end() -> None:
1556
+ store = runner.AffinityStore()
1557
+ store = record_affinity_binding(
1558
+ store, session_id="a", command=_PRIMARY_LAUNCHER
1559
+ )
1560
+ store = record_affinity_binding(
1561
+ store, session_id="b", command=_SECONDARY_LAUNCHER
1562
+ )
1563
+ store = record_affinity_binding(
1564
+ store, session_id="a", command=_SECONDARY_LAUNCHER
1565
+ )
1566
+ assert [each.session_id for each in store.all_bindings] == ["b", "a"]
1567
+ assert store.all_bindings[-1].command == _SECONDARY_LAUNCHER
1568
+
1569
+
1570
+ def test_affinity_corrupt_state_raises_actionable_diagnostic(tmp_path: Path) -> None:
1571
+ state_path = tmp_path / AFFINITY_STATE_FILENAME
1572
+ state_path.write_text("{not-json", encoding=UTF8_ENCODING)
1573
+ with pytest.raises(ValueError, match="corrupt or unreadable") as raised:
1574
+ load_affinity_store(state_path)
1575
+ assert str(state_path) in str(raised.value)
1576
+
1577
+
1578
+ def test_affinity_unsupported_schema_version_raises_actionable_diagnostic(
1579
+ tmp_path: Path,
1580
+ ) -> None:
1581
+ state_path = tmp_path / AFFINITY_STATE_FILENAME
1582
+ state_path.write_text(
1583
+ json.dumps(
1584
+ {
1585
+ AFFINITY_KEY_SCHEMA_VERSION: AFFINITY_STATE_SCHEMA_VERSION + 1,
1586
+ AFFINITY_KEY_ALL_BINDINGS: [],
1587
+ }
1588
+ ),
1589
+ encoding=UTF8_ENCODING,
1590
+ )
1591
+ with pytest.raises(ValueError, match="unsupported schema_version") as raised:
1592
+ load_affinity_store(state_path)
1593
+ assert str(state_path) in str(raised.value)
1594
+
1595
+
1596
+ def test_affinity_binding_not_object_raises_actionable_diagnostic(
1597
+ tmp_path: Path,
1598
+ ) -> None:
1599
+ state_path = tmp_path / AFFINITY_STATE_FILENAME
1600
+ state_path.write_text(
1601
+ json.dumps(
1602
+ {
1603
+ AFFINITY_KEY_SCHEMA_VERSION: AFFINITY_STATE_SCHEMA_VERSION,
1604
+ AFFINITY_KEY_ALL_BINDINGS: ["not-an-object"],
1605
+ }
1606
+ ),
1607
+ encoding=UTF8_ENCODING,
1608
+ )
1609
+ with pytest.raises(ValueError, match="not an object") as raised:
1610
+ load_affinity_store(state_path)
1611
+ assert str(state_path) in str(raised.value)
1612
+
1613
+
1614
+ def test_record_affinity_binding_rejects_empty_session_or_command() -> None:
1615
+ store = runner.AffinityStore()
1616
+ with pytest.raises(ValueError, match="session_id and command"):
1617
+ record_affinity_binding(store, session_id="", command=_PRIMARY_LAUNCHER)
1618
+ with pytest.raises(ValueError, match="session_id and command"):
1619
+ record_affinity_binding(store, session_id=_BOUND_SESSION_ID, command="")
1620
+
1621
+
1622
+ def test_record_affinity_binding_rejects_non_positive_maximum_entries() -> None:
1623
+ store = runner.AffinityStore()
1624
+ with pytest.raises(ValueError, match="maximum_entries must be at least 1"):
1625
+ record_affinity_binding(
1626
+ store,
1627
+ session_id=_BOUND_SESSION_ID,
1628
+ command=_PRIMARY_LAUNCHER,
1629
+ maximum_entries=0,
1630
+ )
1631
+
1632
+
1633
+ def test_affinity_write_failure_raises_actionable_diagnostic(
1634
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1635
+ ) -> None:
1636
+ state_path = tmp_path / AFFINITY_STATE_FILENAME
1637
+ store = record_affinity_binding(
1638
+ runner.AffinityStore(),
1639
+ session_id=_BOUND_SESSION_ID,
1640
+ command=_PRIMARY_LAUNCHER,
1641
+ )
1642
+
1643
+ def _raise_replace(_source: object, _destination: object) -> None:
1644
+ raise OSError("disk full")
1645
+
1646
+ monkeypatch.setattr(runner.os, "replace", _raise_replace)
1647
+ with pytest.raises(OSError, match="Failed to write affinity state") as raised:
1648
+ save_affinity_store_atomic(state_path, store)
1649
+ assert str(state_path) in str(raised.value)
1650
+
1651
+
1652
+ def test_extract_resume_session_id_reads_flag() -> None:
1653
+ assert extract_resume_session_id(
1654
+ [RESUME_SESSION_FLAG, _BOUND_SESSION_ID, "-p", "hi"]
1655
+ ) == _BOUND_SESSION_ID
1656
+ assert extract_resume_session_id(["-p", "hi"]) is None
1657
+ assert extract_resume_session_id([RESUME_SESSION_FLAG]) is None
1658
+
1659
+
1660
+ def test_order_entries_for_resume_puts_originating_binary_first() -> None:
1661
+ all_entries = [
1662
+ ChainEntry(command=_PRIMARY_LAUNCHER, extra_args=()),
1663
+ ChainEntry(command=_SECONDARY_LAUNCHER, extra_args=()),
1664
+ ]
1665
+ ordered = order_entries_for_resume(
1666
+ all_entries, preferred_command=_SECONDARY_LAUNCHER
1667
+ )
1668
+ assert [each.command for each in ordered] == [
1669
+ _SECONDARY_LAUNCHER,
1670
+ _PRIMARY_LAUNCHER,
1671
+ ]
1672
+
1673
+
1674
+ def test_order_entries_for_resume_falls_back_when_command_missing() -> None:
1675
+ all_entries = [
1676
+ ChainEntry(command=_PRIMARY_LAUNCHER, extra_args=()),
1677
+ ChainEntry(command=_SECONDARY_LAUNCHER, extra_args=()),
1678
+ ]
1679
+ ordered = order_entries_for_resume(
1680
+ all_entries, preferred_command="missing-binary"
1681
+ )
1682
+ assert [each.command for each in ordered] == [
1683
+ _PRIMARY_LAUNCHER,
1684
+ _SECONDARY_LAUNCHER,
1685
+ ]
1686
+ ordered_none = order_entries_for_resume(all_entries, preferred_command=None)
1687
+ assert [each.command for each in ordered_none] == [
1688
+ _PRIMARY_LAUNCHER,
1689
+ _SECONDARY_LAUNCHER,
1690
+ ]
1691
+
1692
+
1693
+ def test_lookup_affinity_command_returns_bound_binary() -> None:
1694
+ store = record_affinity_binding(
1695
+ runner.AffinityStore(),
1696
+ session_id=_BOUND_SESSION_ID,
1697
+ command=_SECONDARY_LAUNCHER,
1698
+ )
1699
+ assert lookup_affinity_command(store, _BOUND_SESSION_ID) == _SECONDARY_LAUNCHER
1700
+ assert lookup_affinity_command(store, "unknown") is None
1701
+
1702
+
1703
+ def test_run_claude_resume_routes_through_originating_binary(
1704
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1705
+ ) -> None:
1706
+ config_file = _write_chain_config(
1707
+ tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
1708
+ )
1709
+ affinity_path = tmp_path / "claude-chain-affinity.json"
1710
+ store = record_affinity_binding(
1711
+ runner.AffinityStore(),
1712
+ session_id=_BOUND_SESSION_ID,
1713
+ command=_SECONDARY_LAUNCHER,
1714
+ )
1715
+ save_affinity_store_atomic(affinity_path, store)
1716
+
1717
+ monkeypatch.setattr(runner, "chain_config_path", lambda: config_file)
1718
+ monkeypatch.setattr(
1719
+ runner,
1720
+ "default_affinity_state_path",
1721
+ lambda _home: affinity_path,
1722
+ )
1723
+ monkeypatch.setattr(
1724
+ runner,
1725
+ "chain_weekly_usage_reporter",
1726
+ _usage_reporter_from_remaining(
1727
+ {
1728
+ _PRIMARY_LAUNCHER: _HIGH_WEEKLY_REMAINING_PERCENT,
1729
+ _SECONDARY_LAUNCHER: _LOW_WEEKLY_REMAINING_PERCENT,
1730
+ }
1731
+ ),
1732
+ )
1733
+ all_called: list[str] = []
1734
+
1735
+ def _runner(command, **_kwargs):
1736
+ all_called.append(command[0])
1737
+ return _completed(command[0], 0, stdout=_JSON_BIND_STDOUT_WITH_SESSION)
1738
+
1739
+ monkeypatch.setattr(runner, "chain_subprocess_runner", _runner)
1740
+ outcome = runner.run_claude(
1741
+ [RESUME_SESSION_FLAG, _BOUND_SESSION_ID, "-p", "hi"],
1742
+ timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
1743
+ )
1744
+ assert outcome.served_command == _SECONDARY_LAUNCHER
1745
+ assert all_called[0] == _SECONDARY_LAUNCHER
1746
+
1747
+
1748
+ def test_run_claude_success_persists_session_affinity(
1749
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1750
+ ) -> None:
1751
+ config_file = _write_chain_config(
1752
+ tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
1753
+ )
1754
+ _install(
1755
+ monkeypatch,
1756
+ config_file,
1757
+ {
1758
+ _PRIMARY_LAUNCHER: _completed(
1759
+ _PRIMARY_LAUNCHER, 0, stdout=_JSON_BIND_STDOUT_WITH_SESSION
1760
+ ),
1761
+ _SECONDARY_LAUNCHER: _completed(_SECONDARY_LAUNCHER, 0),
1762
+ },
1763
+ )
1764
+ chain_result = runner.run_claude(
1765
+ _PROMPT_ARGUMENTS,
1766
+ timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
1767
+ routing_mode=ROUTING_MODE_ORDERED_ACCOUNT,
1768
+ )
1769
+ assert chain_result.served_command == _PRIMARY_LAUNCHER
1770
+ assert chain_result.session_id == _BOUND_SESSION_ID
1771
+ affinity_path = Path(config_file).parent / AFFINITY_STATE_FILENAME
1772
+ reloaded = load_affinity_store(affinity_path)
1773
+ assert len(reloaded.all_bindings) == 1
1774
+ assert reloaded.all_bindings[0].session_id == _BOUND_SESSION_ID
1775
+ assert reloaded.all_bindings[0].command == _PRIMARY_LAUNCHER
1776
+
1777
+
1778
+ def test_run_claude_resume_falls_back_when_affinity_corrupt(
1779
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1780
+ ) -> None:
1781
+ config_file = _write_chain_config(
1782
+ tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
1783
+ )
1784
+ affinity_path = Path(config_file).parent / AFFINITY_STATE_FILENAME
1785
+ affinity_path.write_text("{not-json", encoding=UTF8_ENCODING)
1786
+ usage_reporter = _usage_reporter_from_remaining(
1787
+ {
1788
+ _PRIMARY_LAUNCHER: _HIGH_WEEKLY_REMAINING_PERCENT,
1789
+ _SECONDARY_LAUNCHER: _LOW_WEEKLY_REMAINING_PERCENT,
1790
+ }
1791
+ )
1792
+ all_called: list[str] = []
1793
+
1794
+ def _runner(command, **_kwargs):
1795
+ all_called.append(command[0])
1796
+ return _completed(command[0], 0, stdout=_JSON_BIND_STDOUT_WITH_SESSION)
1797
+
1798
+ monkeypatch.setattr(runner, "chain_config_path", lambda: config_file)
1799
+ monkeypatch.setattr(
1800
+ runner,
1801
+ "default_affinity_state_path",
1802
+ lambda _home, _affinity_path=affinity_path: _affinity_path,
1803
+ )
1804
+ monkeypatch.setattr(runner, "chain_weekly_usage_reporter", usage_reporter)
1805
+ monkeypatch.setattr(runner, "chain_subprocess_runner", _runner)
1806
+ outcome = runner.run_claude(
1807
+ [RESUME_SESSION_FLAG, _BOUND_SESSION_ID, "-p", "hi"],
1808
+ timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
1809
+ )
1810
+ assert outcome.served_command == _PRIMARY_LAUNCHER
1811
+ assert all_called[0] == _PRIMARY_LAUNCHER