claude-dev-env 2.7.0 → 2.8.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 (88) hide show
  1. package/_shared/CLAUDE.md +1 -0
  2. package/_shared/advisor/advisor-protocol.md +19 -9
  3. package/_shared/pr-loop/audit-contract.md +4 -4
  4. package/_shared/pr-loop/precatch-rubric.md +2 -2
  5. package/_shared/pr-loop/worker-spawn.md +3 -1
  6. package/_shared/process-tree/CLAUDE.md +41 -0
  7. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
  8. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
  9. package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
  10. package/_shared/process-tree/scripts/pyproject.toml +16 -0
  11. package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
  12. package/agents/code-quality-agent.md +6 -5
  13. package/agents/deep-research.md +7 -24
  14. package/agents/docs-agent.md +1 -27
  15. package/agents/issue-tracker.md +1 -7
  16. package/agents/skill-writer-agent.md +1 -2
  17. package/agents/test_agent_frontmatter.py +309 -12
  18. package/hooks/blocking/CLAUDE.md +2 -0
  19. package/hooks/blocking/fable_spawn_gate.py +187 -0
  20. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  21. package/hooks/blocking/plain_language_blocker.py +287 -15
  22. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  23. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  24. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  25. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  26. package/hooks/git-hooks/CLAUDE.md +2 -2
  27. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  28. package/hooks/git-hooks/pre_push.py +343 -54
  29. package/hooks/git-hooks/test_pre_push.py +852 -6
  30. package/hooks/hooks.json +9 -19
  31. package/hooks/hooks_constants/CLAUDE.md +2 -0
  32. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  33. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  34. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  35. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  36. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  37. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  38. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  39. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  40. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  41. package/package.json +1 -1
  42. package/rules/CLAUDE.md +1 -0
  43. package/rules/ask-user-question-required.md +26 -0
  44. package/rules/claims-as-quotes.md +65 -0
  45. package/scripts/CLAUDE.md +4 -4
  46. package/scripts/_code_review_test_support.py +6 -0
  47. package/scripts/check.ps1 +18 -5
  48. package/scripts/claude_chain_runner.py +203 -31
  49. package/scripts/codec_forwarding_test_support.py +2 -0
  50. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  51. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  52. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  53. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +83 -13
  54. package/scripts/grok_headless_runner.py +148 -18
  55. package/scripts/resolve_worker_spawn.py +56 -10
  56. package/scripts/spawn_grok_batch.py +81 -23
  57. package/scripts/test_claude_chain_runner.py +358 -0
  58. package/scripts/test_grok_headless_runner.py +547 -10
  59. package/scripts/test_invoke_code_review.py +298 -0
  60. package/scripts/test_resolve_worker_spawn.py +185 -15
  61. package/scripts/test_spawn_grok_batch.py +326 -22
  62. package/scripts/tests/CLAUDE.md +1 -0
  63. package/scripts/tests/test_grok_worker_constants.py +59 -0
  64. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  65. package/skills/autoconverge/workflow/converge.contract.test.mjs +133 -8
  66. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -1
  67. package/skills/autoconverge/workflow/converge.mjs +113 -31
  68. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  69. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  70. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  71. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  72. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  73. package/skills/e-code-review/SKILL.md +9 -8
  74. package/skills/e-code-review/reference/fix.md +29 -7
  75. package/skills/e-code-review/reference/loop.md +230 -14
  76. package/skills/e-code-review/reference/low.md +33 -15
  77. package/skills/e-code-review/reference/medium.md +55 -21
  78. package/skills/e-code-review/reference/xhigh.md +30 -12
  79. package/skills/fresh-branch/CLAUDE.md +5 -5
  80. package/skills/fresh-branch/SKILL.md +14 -6
  81. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  82. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  83. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  84. package/skills/grok-spawn/SKILL.md +10 -3
  85. package/skills/grok-spawn/reference/flag-profiles.md +3 -1
  86. package/skills/orchestrator/SKILL.md +4 -1
  87. package/skills/orchestrator-refresh/SKILL.md +5 -1
  88. package/skills/team-advisor/SKILL.md +4 -1
@@ -24,11 +24,13 @@ from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
24
24
  ATTEMPT_STATUS_USAGE_LIMITED,
25
25
  ATTEMPT_SUMMARY_ENTRY_TEMPLATE,
26
26
  ATTEMPT_SUMMARY_JOIN_SEPARATOR,
27
+ CHAIN_ADVISOR_BLOCKED_EXIT_CODE,
27
28
  CHAIN_CONFIG_ERROR_EXIT_CODE,
28
29
  CHAIN_EXHAUSTED_EXIT_CODE,
29
30
  CHAIN_EXHAUSTED_MESSAGE_TEMPLATE,
30
31
  CLAUDE_HOME_SUBDIRECTORY,
31
32
  CLI_ARGUMENTS_SEPARATOR,
33
+ CLI_ROUTING_MODE_FLAG,
32
34
  CLI_TIMEOUT_FLAG,
33
35
  CODEC_ERROR_STRATEGY,
34
36
  CONFIG_CHAIN_EMPTY_REASON,
@@ -48,6 +50,11 @@ from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
48
50
  DEFAULT_TIMEOUT_SECONDS,
49
51
  EXAMPLE_CONFIG_FILENAME,
50
52
  NO_COMPLETED_PROCESS_RETURN_CODE,
53
+ ROUTING_MODE_ORDERED_ACCOUNT,
54
+ SESSION_ID_JSON_KEY,
55
+ TERMINAL_STATUS_ADVISOR_BLOCKED,
56
+ TERMINAL_STATUS_SERVED,
57
+ TERMINAL_STATUS_TIMEOUT,
51
58
  UTF8_ENCODING,
52
59
  )
53
60
 
@@ -65,6 +72,15 @@ _EQUAL_WEEKLY_REMAINING_PERCENT = 50.0
65
72
  _HIGH_WEEKLY_REMAINING_PERCENT = 90.0
66
73
  _MID_WEEKLY_REMAINING_PERCENT = 50.0
67
74
  _LOW_WEEKLY_REMAINING_PERCENT = 10.0
75
+ _PRIMARY_LAUNCHER = "primary-launcher"
76
+ _SECONDARY_LAUNCHER = "secondary-launcher"
77
+ _BOUND_SESSION_ID = "session-bound-abc-123"
78
+ _AUTHENTICATION_ERROR_STDERR = "authentication_error: invalid API key"
79
+ _GENERIC_PROCESS_ERROR_STDERR = "unknown flag: --not-a-real-flag"
80
+ _JSON_BIND_STDOUT_WITH_SESSION = (
81
+ f'{{"type":"result","{SESSION_ID_JSON_KEY}":"{_BOUND_SESSION_ID}",'
82
+ f'"result":"ENDORSE"}}\n'
83
+ )
68
84
 
69
85
 
70
86
  def _completed(command, returncode, stdout="", stderr=""):
@@ -567,6 +583,7 @@ def test_timeout_on_primary_does_not_fall_over(
567
583
  chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
568
584
  assert chain_result.served_command is None
569
585
  assert chain_result.returncode == NO_COMPLETED_PROCESS_RETURN_CODE
586
+ assert chain_result.terminal_status == TERMINAL_STATUS_TIMEOUT
570
587
  assert len(chain_result.attempts) == 1
571
588
  assert chain_result.attempts[0].status == ATTEMPT_STATUS_TIMEOUT
572
589
 
@@ -1119,3 +1136,344 @@ def test_cli_emits_utf8_when_console_encoding_is_legacy(tmp_path: Path) -> None:
1119
1136
  assert completed.returncode == 0
1120
1137
  assert b"Traceback" not in completed.stderr
1121
1138
  assert "report ✅" in completed.stdout.decode(UTF8_ENCODING)
1139
+
1140
+
1141
+ def test_usage_limit_classifier_keys_on_exact_signature_constants() -> None:
1142
+ assert ALL_USAGE_LIMIT_SIGNATURES == (
1143
+ "hit your session limit",
1144
+ "usage limit reached",
1145
+ "out of usage",
1146
+ "usage quota exceeded",
1147
+ )
1148
+ assert _A_SIGNATURE in ALL_USAGE_LIMIT_SIGNATURES
1149
+ assert runner._is_usage_limit_failure(
1150
+ _completed("primary-launcher", 1, stderr=_A_SIGNATURE)
1151
+ )
1152
+ assert not runner._is_usage_limit_failure(
1153
+ _completed("primary-launcher", 1, stderr=_GENERIC_PROCESS_ERROR_STDERR)
1154
+ )
1155
+
1156
+
1157
+ def test_extract_session_id_from_stdout_reads_json_object_and_ndjson() -> None:
1158
+ single_object = (
1159
+ f'{{"type":"result","{SESSION_ID_JSON_KEY}":"{_BOUND_SESSION_ID}",'
1160
+ f'"result":"ENDORSE"}}'
1161
+ )
1162
+ assert runner.extract_session_id_from_stdout(single_object) == _BOUND_SESSION_ID
1163
+ ndjson_stream = (
1164
+ f'{{"type":"system","{SESSION_ID_JSON_KEY}":"{_BOUND_SESSION_ID}"}}\n'
1165
+ f'{{"type":"result","{SESSION_ID_JSON_KEY}":"{_BOUND_SESSION_ID}",'
1166
+ f'"result":"ok"}}\n'
1167
+ )
1168
+ assert runner.extract_session_id_from_stdout(ndjson_stream) == _BOUND_SESSION_ID
1169
+ assert runner.extract_session_id_from_stdout("not json at all") is None
1170
+ assert runner.extract_session_id_from_stdout("") is None
1171
+
1172
+
1173
+ def test_ordered_account_mode_tries_primary_launcher_first(
1174
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1175
+ ) -> None:
1176
+ config_file = _write_chain_config(
1177
+ tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
1178
+ )
1179
+ usage_reporter = _usage_reporter_from_remaining(
1180
+ {
1181
+ _PRIMARY_LAUNCHER: _LOW_WEEKLY_REMAINING_PERCENT,
1182
+ _SECONDARY_LAUNCHER: _HIGH_WEEKLY_REMAINING_PERCENT,
1183
+ }
1184
+ )
1185
+ recorder = _install(
1186
+ monkeypatch,
1187
+ config_file,
1188
+ {
1189
+ _PRIMARY_LAUNCHER: _completed(
1190
+ _PRIMARY_LAUNCHER, 0, stdout=_JSON_BIND_STDOUT_WITH_SESSION
1191
+ ),
1192
+ _SECONDARY_LAUNCHER: _completed(
1193
+ _SECONDARY_LAUNCHER, 0, stdout="should-not-run"
1194
+ ),
1195
+ },
1196
+ weekly_usage_reporter=usage_reporter,
1197
+ )
1198
+ chain_result = runner.run_claude(
1199
+ _PROMPT_ARGUMENTS,
1200
+ timeout_seconds=5,
1201
+ routing_mode=ROUTING_MODE_ORDERED_ACCOUNT,
1202
+ )
1203
+ assert chain_result.served_command == _PRIMARY_LAUNCHER
1204
+ assert chain_result.terminal_status == TERMINAL_STATUS_SERVED
1205
+ assert recorder.invocations[0][0] == _PRIMARY_LAUNCHER
1206
+ assert usage_reporter.call_count["count"] == 0
1207
+ assert [each_attempt.command for each_attempt in chain_result.attempts] == [
1208
+ _PRIMARY_LAUNCHER
1209
+ ]
1210
+
1211
+
1212
+ def test_ordered_account_mode_usage_limit_falls_over_to_secondary(
1213
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1214
+ ) -> None:
1215
+ config_file = _write_chain_config(
1216
+ tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
1217
+ )
1218
+ _install(
1219
+ monkeypatch,
1220
+ config_file,
1221
+ {
1222
+ _PRIMARY_LAUNCHER: _completed(
1223
+ _PRIMARY_LAUNCHER, 1, stderr=_A_SIGNATURE
1224
+ ),
1225
+ _SECONDARY_LAUNCHER: _completed(
1226
+ _SECONDARY_LAUNCHER, 0, stdout=_JSON_BIND_STDOUT_WITH_SESSION
1227
+ ),
1228
+ },
1229
+ )
1230
+ chain_result = runner.run_claude(
1231
+ _PROMPT_ARGUMENTS,
1232
+ timeout_seconds=5,
1233
+ routing_mode=ROUTING_MODE_ORDERED_ACCOUNT,
1234
+ )
1235
+ assert chain_result.served_command == _SECONDARY_LAUNCHER
1236
+ assert chain_result.returncode == 0
1237
+ assert chain_result.terminal_status == TERMINAL_STATUS_SERVED
1238
+ assert [each_attempt.command for each_attempt in chain_result.attempts] == [
1239
+ _PRIMARY_LAUNCHER,
1240
+ _SECONDARY_LAUNCHER,
1241
+ ]
1242
+ assert chain_result.attempts[0].status == ATTEMPT_STATUS_USAGE_LIMITED
1243
+ assert chain_result.attempts[1].status == ATTEMPT_STATUS_SERVED
1244
+
1245
+
1246
+ def test_ordered_account_mode_authentication_error_is_advisor_blocked(
1247
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1248
+ ) -> None:
1249
+ config_file = _write_chain_config(
1250
+ tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
1251
+ )
1252
+ recorder = _install(
1253
+ monkeypatch,
1254
+ config_file,
1255
+ {
1256
+ _PRIMARY_LAUNCHER: _completed(
1257
+ _PRIMARY_LAUNCHER, 1, stderr=_AUTHENTICATION_ERROR_STDERR
1258
+ ),
1259
+ _SECONDARY_LAUNCHER: _completed(
1260
+ _SECONDARY_LAUNCHER, 0, stdout="should-not-run"
1261
+ ),
1262
+ },
1263
+ )
1264
+ chain_result = runner.run_claude(
1265
+ _PROMPT_ARGUMENTS,
1266
+ timeout_seconds=5,
1267
+ routing_mode=ROUTING_MODE_ORDERED_ACCOUNT,
1268
+ )
1269
+ assert chain_result.terminal_status == TERMINAL_STATUS_ADVISOR_BLOCKED
1270
+ assert chain_result.served_command is None
1271
+ assert chain_result.returncode == 1
1272
+ assert _AUTHENTICATION_ERROR_STDERR in chain_result.stderr
1273
+ assert len(recorder.invocations) == 1
1274
+ assert chain_result.attempts[0].status == ATTEMPT_STATUS_NONZERO_EXIT
1275
+
1276
+
1277
+ def test_ordered_account_mode_timeout_is_advisor_blocked(
1278
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1279
+ ) -> None:
1280
+ config_file = _write_chain_config(
1281
+ tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
1282
+ )
1283
+ recorder = _install(
1284
+ monkeypatch,
1285
+ config_file,
1286
+ {
1287
+ _PRIMARY_LAUNCHER: subprocess.TimeoutExpired(
1288
+ cmd=[_PRIMARY_LAUNCHER], timeout=5
1289
+ ),
1290
+ _SECONDARY_LAUNCHER: _completed(_SECONDARY_LAUNCHER, 0),
1291
+ },
1292
+ )
1293
+ chain_result = runner.run_claude(
1294
+ _PROMPT_ARGUMENTS,
1295
+ timeout_seconds=5,
1296
+ routing_mode=ROUTING_MODE_ORDERED_ACCOUNT,
1297
+ )
1298
+ assert chain_result.terminal_status == TERMINAL_STATUS_ADVISOR_BLOCKED
1299
+ assert chain_result.served_command is None
1300
+ assert chain_result.returncode == NO_COMPLETED_PROCESS_RETURN_CODE
1301
+ assert len(recorder.invocations) == 1
1302
+ assert chain_result.attempts[0].status == ATTEMPT_STATUS_TIMEOUT
1303
+
1304
+
1305
+ def test_ordered_account_mode_generic_process_error_is_advisor_blocked(
1306
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1307
+ ) -> None:
1308
+ config_file = _write_chain_config(
1309
+ tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
1310
+ )
1311
+ recorder = _install(
1312
+ monkeypatch,
1313
+ config_file,
1314
+ {
1315
+ _PRIMARY_LAUNCHER: _completed(
1316
+ _PRIMARY_LAUNCHER, 2, stderr=_GENERIC_PROCESS_ERROR_STDERR
1317
+ ),
1318
+ _SECONDARY_LAUNCHER: _completed(
1319
+ _SECONDARY_LAUNCHER, 0, stdout="should-not-run"
1320
+ ),
1321
+ },
1322
+ )
1323
+ chain_result = runner.run_claude(
1324
+ _PROMPT_ARGUMENTS,
1325
+ timeout_seconds=5,
1326
+ routing_mode=ROUTING_MODE_ORDERED_ACCOUNT,
1327
+ )
1328
+ assert chain_result.terminal_status == TERMINAL_STATUS_ADVISOR_BLOCKED
1329
+ assert chain_result.served_command is None
1330
+ assert chain_result.returncode == 2
1331
+ assert len(recorder.invocations) == 1
1332
+ assert chain_result.attempts[0].status == ATTEMPT_STATUS_NONZERO_EXIT
1333
+
1334
+
1335
+ def test_ordered_account_mode_configuration_error_does_not_fall_over(
1336
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
1337
+ ) -> None:
1338
+ missing_config = tmp_path / CONFIG_FILENAME
1339
+ monkeypatch.setattr(runner, "chain_config_path", lambda: missing_config)
1340
+ _install_tty_stdin(monkeypatch)
1341
+ exit_code = runner.main(
1342
+ [
1343
+ CLI_ROUTING_MODE_FLAG,
1344
+ ROUTING_MODE_ORDERED_ACCOUNT,
1345
+ CLI_ARGUMENTS_SEPARATOR,
1346
+ "-p",
1347
+ "hi",
1348
+ ]
1349
+ )
1350
+ captured = capsys.readouterr()
1351
+ assert exit_code == CHAIN_CONFIG_ERROR_EXIT_CODE
1352
+ assert EXAMPLE_CONFIG_FILENAME in captured.err
1353
+
1354
+
1355
+ def test_ordered_account_mode_preserves_session_id_on_success(
1356
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1357
+ ) -> None:
1358
+ config_file = _write_chain_config(
1359
+ tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
1360
+ )
1361
+ _install(
1362
+ monkeypatch,
1363
+ config_file,
1364
+ {
1365
+ _PRIMARY_LAUNCHER: _completed(
1366
+ _PRIMARY_LAUNCHER, 0, stdout=_JSON_BIND_STDOUT_WITH_SESSION
1367
+ ),
1368
+ _SECONDARY_LAUNCHER: _completed(_SECONDARY_LAUNCHER, 0),
1369
+ },
1370
+ )
1371
+ chain_result = runner.run_claude(
1372
+ _PROMPT_ARGUMENTS,
1373
+ timeout_seconds=5,
1374
+ routing_mode=ROUTING_MODE_ORDERED_ACCOUNT,
1375
+ )
1376
+ assert chain_result.terminal_status == TERMINAL_STATUS_SERVED
1377
+ assert chain_result.session_id == _BOUND_SESSION_ID
1378
+ assert chain_result.served_command == _PRIMARY_LAUNCHER
1379
+
1380
+
1381
+ def test_default_routing_mode_remains_usage_ranked(
1382
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1383
+ ) -> None:
1384
+ config_file = _write_chain_config(
1385
+ tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
1386
+ )
1387
+ usage_reporter = _usage_reporter_from_remaining(
1388
+ {
1389
+ _PRIMARY_LAUNCHER: _LOW_WEEKLY_REMAINING_PERCENT,
1390
+ _SECONDARY_LAUNCHER: _HIGH_WEEKLY_REMAINING_PERCENT,
1391
+ }
1392
+ )
1393
+ recorder = _install(
1394
+ monkeypatch,
1395
+ config_file,
1396
+ {
1397
+ _PRIMARY_LAUNCHER: _completed(
1398
+ _PRIMARY_LAUNCHER, 0, stdout="from-primary"
1399
+ ),
1400
+ _SECONDARY_LAUNCHER: _completed(
1401
+ _SECONDARY_LAUNCHER, 0, stdout="from-secondary"
1402
+ ),
1403
+ },
1404
+ weekly_usage_reporter=usage_reporter,
1405
+ )
1406
+ chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
1407
+ assert chain_result.served_command == _SECONDARY_LAUNCHER
1408
+ assert chain_result.terminal_status == TERMINAL_STATUS_SERVED
1409
+ assert recorder.invocations[0][0] == _SECONDARY_LAUNCHER
1410
+ assert usage_reporter.call_count["count"] == 1
1411
+
1412
+
1413
+ def test_cli_ordered_account_routing_mode_flag(
1414
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1415
+ ) -> None:
1416
+ config_file = _write_chain_config(
1417
+ tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
1418
+ )
1419
+ usage_reporter = _usage_reporter_from_remaining(
1420
+ {
1421
+ _PRIMARY_LAUNCHER: _LOW_WEEKLY_REMAINING_PERCENT,
1422
+ _SECONDARY_LAUNCHER: _HIGH_WEEKLY_REMAINING_PERCENT,
1423
+ }
1424
+ )
1425
+ recorder = _install(
1426
+ monkeypatch,
1427
+ config_file,
1428
+ {
1429
+ _PRIMARY_LAUNCHER: _completed(
1430
+ _PRIMARY_LAUNCHER, 0, stdout=_JSON_BIND_STDOUT_WITH_SESSION
1431
+ ),
1432
+ _SECONDARY_LAUNCHER: _completed(
1433
+ _SECONDARY_LAUNCHER, 0, stdout="should-not-run"
1434
+ ),
1435
+ },
1436
+ weekly_usage_reporter=usage_reporter,
1437
+ )
1438
+ exit_code = runner.main(
1439
+ [
1440
+ CLI_ROUTING_MODE_FLAG,
1441
+ ROUTING_MODE_ORDERED_ACCOUNT,
1442
+ CLI_ARGUMENTS_SEPARATOR,
1443
+ "-p",
1444
+ "hi",
1445
+ ]
1446
+ )
1447
+ assert exit_code == 0
1448
+ assert recorder.invocations[0][0] == _PRIMARY_LAUNCHER
1449
+ assert usage_reporter.call_count["count"] == 0
1450
+
1451
+
1452
+ def test_cli_ordered_account_advisor_blocked_exit_code(
1453
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1454
+ ) -> None:
1455
+ config_file = _write_chain_config(
1456
+ tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
1457
+ )
1458
+ _install(
1459
+ monkeypatch,
1460
+ config_file,
1461
+ {
1462
+ _PRIMARY_LAUNCHER: _completed(
1463
+ _PRIMARY_LAUNCHER, 2, stderr=_GENERIC_PROCESS_ERROR_STDERR
1464
+ ),
1465
+ _SECONDARY_LAUNCHER: _completed(_SECONDARY_LAUNCHER, 0),
1466
+ },
1467
+ )
1468
+ exit_code = runner.main(
1469
+ [
1470
+ CLI_ROUTING_MODE_FLAG,
1471
+ ROUTING_MODE_ORDERED_ACCOUNT,
1472
+ CLI_ARGUMENTS_SEPARATOR,
1473
+ "-p",
1474
+ "hi",
1475
+ ]
1476
+ )
1477
+ assert exit_code == CHAIN_ADVISOR_BLOCKED_EXIT_CODE
1478
+
1479
+