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.
- package/_shared/CLAUDE.md +1 -0
- package/_shared/advisor/advisor-protocol.md +19 -9
- package/_shared/pr-loop/audit-contract.md +4 -4
- package/_shared/pr-loop/precatch-rubric.md +2 -2
- package/_shared/pr-loop/worker-spawn.md +3 -1
- package/_shared/process-tree/CLAUDE.md +41 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
- package/_shared/process-tree/scripts/pyproject.toml +16 -0
- package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
- package/agents/code-quality-agent.md +6 -5
- package/agents/deep-research.md +7 -24
- package/agents/docs-agent.md +1 -27
- package/agents/issue-tracker.md +1 -7
- package/agents/skill-writer-agent.md +1 -2
- package/agents/test_agent_frontmatter.py +309 -12
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/fable_spawn_gate.py +187 -0
- package/hooks/blocking/piped_pytest_blocker.py +1223 -0
- package/hooks/blocking/plain_language_blocker.py +287 -15
- package/hooks/blocking/test_fable_spawn_gate.py +374 -0
- package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
- package/hooks/blocking/test_plain_language_blocker.py +277 -2
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
- package/hooks/git-hooks/CLAUDE.md +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
- package/hooks/git-hooks/pre_push.py +343 -54
- package/hooks/git-hooks/test_pre_push.py +852 -6
- package/hooks/hooks.json +9 -19
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
- package/hooks/hooks_constants/shell_command_segments.py +1 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/ask-user-question-required.md +26 -0
- package/rules/claims-as-quotes.md +65 -0
- package/scripts/CLAUDE.md +4 -4
- package/scripts/_code_review_test_support.py +6 -0
- package/scripts/check.ps1 +18 -5
- package/scripts/claude_chain_runner.py +203 -31
- package/scripts/codec_forwarding_test_support.py +2 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +83 -13
- package/scripts/grok_headless_runner.py +148 -18
- package/scripts/resolve_worker_spawn.py +56 -10
- package/scripts/spawn_grok_batch.py +81 -23
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +547 -10
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +185 -15
- package/scripts/test_spawn_grok_batch.py +326 -22
- package/scripts/tests/CLAUDE.md +1 -0
- package/scripts/tests/test_grok_worker_constants.py +59 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +133 -8
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -1
- package/skills/autoconverge/workflow/converge.mjs +113 -31
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
- package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
- package/skills/codex-review/scripts/run_codex_review.py +16 -64
- package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
- package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
- package/skills/e-code-review/SKILL.md +9 -8
- package/skills/e-code-review/reference/fix.md +29 -7
- package/skills/e-code-review/reference/loop.md +230 -14
- package/skills/e-code-review/reference/low.md +33 -15
- package/skills/e-code-review/reference/medium.md +55 -21
- package/skills/e-code-review/reference/xhigh.md +30 -12
- package/skills/fresh-branch/CLAUDE.md +5 -5
- package/skills/fresh-branch/SKILL.md +14 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
- package/skills/grok-spawn/SKILL.md +10 -3
- package/skills/grok-spawn/reference/flag-profiles.md +3 -1
- package/skills/orchestrator/SKILL.md +4 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- package/skills/team-advisor/SKILL.md +4 -1
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Run a ``claude`` invocation through a
|
|
2
|
+
"""Run a ``claude`` invocation through a fallback chain of account binaries.
|
|
3
3
|
|
|
4
4
|
An automation that shells out to a single ``claude -p ...`` fails outright when
|
|
5
5
|
that account hits a usage limit. Other logged-in installs sit idle meanwhile.
|
|
6
|
-
|
|
7
|
-
highest remaining first, and tries that order. It falls over to the
|
|
8
|
-
ranked binary only on a usage-limit failure. Every other outcome returns
|
|
9
|
-
the caller unchanged.
|
|
6
|
+
By default this module probes remaining weekly usage once per call, ranks chain
|
|
7
|
+
accounts highest remaining first, and tries that order. It falls over to the
|
|
8
|
+
next ranked binary only on a usage-limit failure. Every other outcome returns
|
|
9
|
+
to the caller unchanged.
|
|
10
|
+
|
|
11
|
+
Ordered-account mode (``--routing-mode ordered_account``) walks the chain in
|
|
12
|
+
config order instead, still falling over only on a usage-limit signature.
|
|
13
|
+
Authentication, timeout, and other non-usage failures stop immediately with
|
|
14
|
+
``terminal_status=advisor_blocked``.
|
|
10
15
|
|
|
11
16
|
The chain lives in ``~/.claude/claude-chain.json``. Copy the committed
|
|
12
17
|
``claude-chain.example.json`` template there and list your account binaries.
|
|
13
|
-
|
|
14
|
-
OAuth probe), not from list position alone::
|
|
18
|
+
Default try order comes from weekly remaining via ``claude_chain_usage``
|
|
19
|
+
(usage-pause OAuth probe), not from list position alone::
|
|
15
20
|
|
|
16
21
|
{"chain": [{"command": "claude", "extra_args": []},
|
|
17
22
|
{"command": "claude-ev", "extra_args": []}]}
|
|
@@ -29,7 +34,8 @@ binary in the walk::
|
|
|
29
34
|
|
|
30
35
|
Import ``run_claude`` for the outcome object, or run the module as a CLI::
|
|
31
36
|
|
|
32
|
-
python claude_chain_runner.py [--timeout-seconds N]
|
|
37
|
+
python claude_chain_runner.py [--timeout-seconds N]
|
|
38
|
+
[--routing-mode usage_ranked|ordered_account] -- <claude args...>
|
|
33
39
|
"""
|
|
34
40
|
|
|
35
41
|
from __future__ import annotations
|
|
@@ -51,6 +57,7 @@ if __name__ == "__main__":
|
|
|
51
57
|
sys.modules.setdefault("claude_chain_runner", sys.modules[__name__])
|
|
52
58
|
|
|
53
59
|
from dev_env_scripts_constants.claude_chain_constants import (
|
|
60
|
+
ALL_ROUTING_MODES,
|
|
54
61
|
ALL_USAGE_LIMIT_SIGNATURES,
|
|
55
62
|
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
56
63
|
ATTEMPT_STATUS_NONZERO_EXIT,
|
|
@@ -60,12 +67,14 @@ from dev_env_scripts_constants.claude_chain_constants import (
|
|
|
60
67
|
ATTEMPT_SUMMARY_ENTRY_TEMPLATE,
|
|
61
68
|
ATTEMPT_SUMMARY_JOIN_SEPARATOR,
|
|
62
69
|
CARRIAGE_RETURN,
|
|
70
|
+
CHAIN_ADVISOR_BLOCKED_EXIT_CODE,
|
|
63
71
|
CHAIN_CONFIG_ERROR_EXIT_CODE,
|
|
64
72
|
CHAIN_EXHAUSTED_EXIT_CODE,
|
|
65
73
|
CHAIN_EXHAUSTED_MESSAGE_TEMPLATE,
|
|
66
74
|
CHAIN_USAGE_MODULE_NAME,
|
|
67
75
|
CLAUDE_HOME_SUBDIRECTORY,
|
|
68
76
|
CLI_ARGUMENTS_SEPARATOR,
|
|
77
|
+
CLI_ROUTING_MODE_FLAG,
|
|
69
78
|
CLI_TIMEOUT_FLAG,
|
|
70
79
|
CODEC_ERROR_STRATEGY,
|
|
71
80
|
CONFIG_CHAIN_EMPTY_REASON,
|
|
@@ -85,10 +94,17 @@ from dev_env_scripts_constants.claude_chain_constants import (
|
|
|
85
94
|
CONFIG_NOT_OBJECT_REASON,
|
|
86
95
|
CONFIG_UNREADABLE_MESSAGE_TEMPLATE,
|
|
87
96
|
CRLF_NEWLINE,
|
|
97
|
+
DEFAULT_ROUTING_MODE,
|
|
88
98
|
DEFAULT_TIMEOUT_SECONDS,
|
|
89
99
|
EXAMPLE_CONFIG_FILENAME,
|
|
90
100
|
LINE_FEED,
|
|
91
101
|
NO_COMPLETED_PROCESS_RETURN_CODE,
|
|
102
|
+
ROUTING_MODE_ORDERED_ACCOUNT,
|
|
103
|
+
SESSION_ID_JSON_KEY,
|
|
104
|
+
TERMINAL_STATUS_ADVISOR_BLOCKED,
|
|
105
|
+
TERMINAL_STATUS_CHAIN_EXHAUSTED,
|
|
106
|
+
TERMINAL_STATUS_SERVED,
|
|
107
|
+
TERMINAL_STATUS_TIMEOUT,
|
|
92
108
|
UTF8_ENCODING,
|
|
93
109
|
)
|
|
94
110
|
|
|
@@ -257,12 +273,21 @@ class ChainAttempt:
|
|
|
257
273
|
|
|
258
274
|
@dataclass(frozen=True)
|
|
259
275
|
class ChainInvocationOutcome:
|
|
260
|
-
"""Outcome of
|
|
276
|
+
"""Outcome of one chain walk: who served, how it ended, optional session id.
|
|
277
|
+
|
|
278
|
+
::
|
|
261
279
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
280
|
+
zero exit with JSON session_id
|
|
281
|
+
-> served_command set, terminal_status=served, session_id filled
|
|
282
|
+
ordered_account auth/timeout/generic process error
|
|
283
|
+
-> served_command=None, terminal_status=advisor_blocked
|
|
284
|
+
usage_ranked TimeoutExpired mid-walk
|
|
285
|
+
-> served_command=None, terminal_status=timeout
|
|
286
|
+
every entry usage-limited or missing
|
|
287
|
+
-> served_command=None, terminal_status=chain_exhausted
|
|
288
|
+
|
|
289
|
+
``attempts`` lists every binary tried. Callers resume later consults with
|
|
290
|
+
``session_id`` when the bind returned one.
|
|
266
291
|
"""
|
|
267
292
|
|
|
268
293
|
served_command: str | None
|
|
@@ -270,6 +295,8 @@ class ChainInvocationOutcome:
|
|
|
270
295
|
stdout: str
|
|
271
296
|
stderr: str
|
|
272
297
|
attempts: tuple[ChainAttempt, ...]
|
|
298
|
+
terminal_status: str
|
|
299
|
+
session_id: str | None = None
|
|
273
300
|
|
|
274
301
|
|
|
275
302
|
class WeeklyUsageAccountReport(Protocol):
|
|
@@ -444,17 +471,70 @@ def _is_usage_limit_failure(completion: subprocess.CompletedProcess[str]) -> boo
|
|
|
444
471
|
)
|
|
445
472
|
|
|
446
473
|
|
|
474
|
+
def extract_session_id_from_stdout(stdout_text: str) -> str | None:
|
|
475
|
+
"""Return the first ``session_id`` found in Claude JSON stdout.
|
|
476
|
+
|
|
477
|
+
::
|
|
478
|
+
|
|
479
|
+
'{"type":"result","session_id":"abc","result":"ok"}'
|
|
480
|
+
-> "abc"
|
|
481
|
+
'not json'
|
|
482
|
+
-> None
|
|
483
|
+
|
|
484
|
+
Accepts a single JSON object or NDJSON event lines. The first non-empty
|
|
485
|
+
string value under the ``session_id`` key wins.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
stdout_text: Captured stdout from a Claude ``--output-format json`` run.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
The session id string, or ``None`` when none is present.
|
|
492
|
+
"""
|
|
493
|
+
stripped_stdout = stdout_text.strip()
|
|
494
|
+
if not stripped_stdout:
|
|
495
|
+
return None
|
|
496
|
+
maybe_session_id = _session_id_from_json_text(stripped_stdout)
|
|
497
|
+
if maybe_session_id is not None:
|
|
498
|
+
return maybe_session_id
|
|
499
|
+
for each_line in stripped_stdout.splitlines():
|
|
500
|
+
stripped_line = each_line.strip()
|
|
501
|
+
if not stripped_line:
|
|
502
|
+
continue
|
|
503
|
+
maybe_session_id = _session_id_from_json_text(stripped_line)
|
|
504
|
+
if maybe_session_id is not None:
|
|
505
|
+
return maybe_session_id
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _session_id_from_json_text(json_text: str) -> str | None:
|
|
510
|
+
try:
|
|
511
|
+
parsed_payload = json.loads(json_text)
|
|
512
|
+
except json.JSONDecodeError:
|
|
513
|
+
return None
|
|
514
|
+
if not isinstance(parsed_payload, dict):
|
|
515
|
+
return None
|
|
516
|
+
raw_session_id = parsed_payload.get(SESSION_ID_JSON_KEY)
|
|
517
|
+
if isinstance(raw_session_id, str) and raw_session_id:
|
|
518
|
+
return raw_session_id
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
|
|
447
522
|
def _served_outcome(
|
|
448
523
|
served_command: str,
|
|
449
524
|
completion: subprocess.CompletedProcess[str],
|
|
450
525
|
all_attempts: list[ChainAttempt],
|
|
451
526
|
) -> ChainInvocationOutcome:
|
|
527
|
+
maybe_session_id = None
|
|
528
|
+
if completion.returncode == 0:
|
|
529
|
+
maybe_session_id = extract_session_id_from_stdout(completion.stdout)
|
|
452
530
|
return ChainInvocationOutcome(
|
|
453
531
|
served_command=served_command,
|
|
454
532
|
returncode=completion.returncode,
|
|
455
533
|
stdout=completion.stdout,
|
|
456
534
|
stderr=completion.stderr,
|
|
457
535
|
attempts=tuple(all_attempts),
|
|
536
|
+
terminal_status=TERMINAL_STATUS_SERVED,
|
|
537
|
+
session_id=maybe_session_id,
|
|
458
538
|
)
|
|
459
539
|
|
|
460
540
|
|
|
@@ -475,6 +555,8 @@ def _timeout_streams(
|
|
|
475
555
|
def _no_process_outcome(
|
|
476
556
|
all_attempts: list[ChainAttempt],
|
|
477
557
|
timeout_error: subprocess.TimeoutExpired | None,
|
|
558
|
+
*,
|
|
559
|
+
terminal_status: str,
|
|
478
560
|
) -> ChainInvocationOutcome:
|
|
479
561
|
captured_stdout, captured_stderr = _timeout_streams(timeout_error)
|
|
480
562
|
return ChainInvocationOutcome(
|
|
@@ -483,6 +565,23 @@ def _no_process_outcome(
|
|
|
483
565
|
stdout=captured_stdout,
|
|
484
566
|
stderr=captured_stderr,
|
|
485
567
|
attempts=tuple(all_attempts),
|
|
568
|
+
terminal_status=terminal_status,
|
|
569
|
+
session_id=None,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _advisor_blocked_outcome(
|
|
574
|
+
completion: subprocess.CompletedProcess[str],
|
|
575
|
+
all_attempts: list[ChainAttempt],
|
|
576
|
+
) -> ChainInvocationOutcome:
|
|
577
|
+
return ChainInvocationOutcome(
|
|
578
|
+
served_command=None,
|
|
579
|
+
returncode=completion.returncode,
|
|
580
|
+
stdout=completion.stdout,
|
|
581
|
+
stderr=completion.stderr,
|
|
582
|
+
attempts=tuple(all_attempts),
|
|
583
|
+
terminal_status=TERMINAL_STATUS_ADVISOR_BLOCKED,
|
|
584
|
+
session_id=None,
|
|
486
585
|
)
|
|
487
586
|
|
|
488
587
|
|
|
@@ -491,13 +590,19 @@ def _exhausted_outcome(
|
|
|
491
590
|
last_usage_limited: subprocess.CompletedProcess[str] | None,
|
|
492
591
|
) -> ChainInvocationOutcome:
|
|
493
592
|
if last_usage_limited is None:
|
|
494
|
-
return _no_process_outcome(
|
|
593
|
+
return _no_process_outcome(
|
|
594
|
+
all_attempts,
|
|
595
|
+
None,
|
|
596
|
+
terminal_status=TERMINAL_STATUS_CHAIN_EXHAUSTED,
|
|
597
|
+
)
|
|
495
598
|
return ChainInvocationOutcome(
|
|
496
599
|
served_command=None,
|
|
497
600
|
returncode=last_usage_limited.returncode,
|
|
498
601
|
stdout=last_usage_limited.stdout,
|
|
499
602
|
stderr=last_usage_limited.stderr,
|
|
500
603
|
attempts=tuple(all_attempts),
|
|
604
|
+
terminal_status=TERMINAL_STATUS_CHAIN_EXHAUSTED,
|
|
605
|
+
session_id=None,
|
|
501
606
|
)
|
|
502
607
|
|
|
503
608
|
|
|
@@ -505,6 +610,8 @@ def _classify_completion(
|
|
|
505
610
|
entry: ChainEntry,
|
|
506
611
|
completion: subprocess.CompletedProcess[str],
|
|
507
612
|
all_attempts: list[ChainAttempt],
|
|
613
|
+
*,
|
|
614
|
+
routing_mode: str,
|
|
508
615
|
) -> ChainInvocationOutcome | None:
|
|
509
616
|
if completion.returncode == 0:
|
|
510
617
|
all_attempts.append(ChainAttempt(entry.command, ATTEMPT_STATUS_SERVED))
|
|
@@ -513,6 +620,8 @@ def _classify_completion(
|
|
|
513
620
|
all_attempts.append(ChainAttempt(entry.command, ATTEMPT_STATUS_USAGE_LIMITED))
|
|
514
621
|
return None
|
|
515
622
|
all_attempts.append(ChainAttempt(entry.command, ATTEMPT_STATUS_NONZERO_EXIT))
|
|
623
|
+
if routing_mode == ROUTING_MODE_ORDERED_ACCOUNT:
|
|
624
|
+
return _advisor_blocked_outcome(completion, all_attempts)
|
|
516
625
|
return _served_outcome(entry.command, completion, all_attempts)
|
|
517
626
|
|
|
518
627
|
|
|
@@ -529,27 +638,50 @@ def _ranked_entries_or_config_order(
|
|
|
529
638
|
return list(all_entries)
|
|
530
639
|
|
|
531
640
|
|
|
641
|
+
def _resolve_walk_entries(
|
|
642
|
+
all_entries: list[ChainEntry],
|
|
643
|
+
config_path: Path,
|
|
644
|
+
routing_mode: str,
|
|
645
|
+
) -> list[ChainEntry]:
|
|
646
|
+
if routing_mode == ROUTING_MODE_ORDERED_ACCOUNT:
|
|
647
|
+
return list(all_entries)
|
|
648
|
+
return _ranked_entries_or_config_order(all_entries, config_path)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _require_known_routing_mode(routing_mode: str) -> str:
|
|
652
|
+
if routing_mode not in ALL_ROUTING_MODES:
|
|
653
|
+
raise ValueError(
|
|
654
|
+
f"Unknown routing_mode {routing_mode!r}; "
|
|
655
|
+
f"expected one of {sorted(ALL_ROUTING_MODES)}"
|
|
656
|
+
)
|
|
657
|
+
return routing_mode
|
|
658
|
+
|
|
659
|
+
|
|
532
660
|
def run_claude(
|
|
533
661
|
all_claude_arguments: list[str],
|
|
534
662
|
*,
|
|
535
663
|
timeout_seconds: int,
|
|
536
664
|
stdin_text: str | None = None,
|
|
665
|
+
routing_mode: str = DEFAULT_ROUTING_MODE,
|
|
537
666
|
) -> ChainInvocationOutcome:
|
|
538
|
-
"""Run *all_claude_arguments* through the
|
|
667
|
+
"""Run *all_claude_arguments* through the fallback chain.
|
|
539
668
|
|
|
540
669
|
::
|
|
541
670
|
|
|
542
|
-
highest remaining
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
->
|
|
546
|
-
|
|
547
|
-
->
|
|
671
|
+
usage_ranked (default): highest remaining first
|
|
672
|
+
ordered_account: config order; usage-limit-only fallover
|
|
673
|
+
ordered_account + auth/timeout/generic process error
|
|
674
|
+
-> terminal_status=advisor_blocked (no fallover)
|
|
675
|
+
zero exit with JSON session_id
|
|
676
|
+
-> outcome.session_id set for later --resume
|
|
548
677
|
|
|
549
|
-
|
|
678
|
+
Default mode probes weekly remaining once, ranks highest first, then walks
|
|
679
|
+
that order. Ordered-account mode walks config order and never probes usage.
|
|
550
680
|
Only a usage-limit failure falls over. Missing binaries are skipped and the
|
|
551
|
-
walk continues; timeout and other nonzero exits stop.
|
|
552
|
-
|
|
681
|
+
walk continues; timeout and other nonzero exits stop. In ordered-account
|
|
682
|
+
mode those non-usage stops report ``advisor_blocked``. When usage ranking
|
|
683
|
+
infrastructure fails to load under usage-ranked mode, the walk uses config
|
|
684
|
+
order instead.
|
|
553
685
|
|
|
554
686
|
Args:
|
|
555
687
|
all_claude_arguments: Arguments passed after the binary name, such as
|
|
@@ -557,20 +689,25 @@ def run_claude(
|
|
|
557
689
|
timeout_seconds: Timeout applied to each binary invocation.
|
|
558
690
|
stdin_text: Optional UTF-8 text forwarded as stdin to every binary.
|
|
559
691
|
``None`` leaves the subprocess without a piped stdin body.
|
|
692
|
+
routing_mode: ``usage_ranked`` (default) or ``ordered_account``.
|
|
560
693
|
|
|
561
694
|
Returns:
|
|
562
|
-
The outcome of the walk, naming the serving binary
|
|
563
|
-
attempt trail.
|
|
695
|
+
The outcome of the walk, naming the serving binary, terminal status,
|
|
696
|
+
optional session id, and the full attempt trail.
|
|
564
697
|
|
|
565
698
|
Raises:
|
|
566
699
|
ChainConfigurationError: When the chain configuration cannot be loaded.
|
|
700
|
+
ValueError: When *routing_mode* is not a known mode.
|
|
567
701
|
"""
|
|
702
|
+
selected_routing_mode = _require_known_routing_mode(routing_mode)
|
|
568
703
|
config_path = chain_config_path()
|
|
569
704
|
all_entries = load_chain(config_path)
|
|
570
|
-
|
|
705
|
+
all_walk_entries = _resolve_walk_entries(
|
|
706
|
+
all_entries, config_path, selected_routing_mode
|
|
707
|
+
)
|
|
571
708
|
all_attempts: list[ChainAttempt] = []
|
|
572
709
|
last_usage_limited: subprocess.CompletedProcess[str] | None = None
|
|
573
|
-
for each_entry in
|
|
710
|
+
for each_entry in all_walk_entries:
|
|
574
711
|
try:
|
|
575
712
|
completion = chain_subprocess_runner(
|
|
576
713
|
_build_invocation(each_entry, all_claude_arguments),
|
|
@@ -586,13 +723,27 @@ def run_claude(
|
|
|
586
723
|
all_attempts.append(
|
|
587
724
|
ChainAttempt(each_entry.command, ATTEMPT_STATUS_TIMEOUT)
|
|
588
725
|
)
|
|
589
|
-
|
|
726
|
+
timeout_terminal_status = (
|
|
727
|
+
TERMINAL_STATUS_ADVISOR_BLOCKED
|
|
728
|
+
if selected_routing_mode == ROUTING_MODE_ORDERED_ACCOUNT
|
|
729
|
+
else TERMINAL_STATUS_TIMEOUT
|
|
730
|
+
)
|
|
731
|
+
return _no_process_outcome(
|
|
732
|
+
all_attempts,
|
|
733
|
+
timeout_error,
|
|
734
|
+
terminal_status=timeout_terminal_status,
|
|
735
|
+
)
|
|
590
736
|
except FileNotFoundError:
|
|
591
737
|
all_attempts.append(
|
|
592
738
|
ChainAttempt(each_entry.command, ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND)
|
|
593
739
|
)
|
|
594
740
|
continue
|
|
595
|
-
terminal_outcome = _classify_completion(
|
|
741
|
+
terminal_outcome = _classify_completion(
|
|
742
|
+
each_entry,
|
|
743
|
+
completion,
|
|
744
|
+
all_attempts,
|
|
745
|
+
routing_mode=selected_routing_mode,
|
|
746
|
+
)
|
|
596
747
|
if terminal_outcome is not None:
|
|
597
748
|
return terminal_outcome
|
|
598
749
|
last_usage_limited = completion
|
|
@@ -610,6 +761,16 @@ def _build_argument_parser() -> argparse.ArgumentParser:
|
|
|
610
761
|
default=DEFAULT_TIMEOUT_SECONDS,
|
|
611
762
|
help="Timeout in seconds applied to each binary invocation.",
|
|
612
763
|
)
|
|
764
|
+
parser.add_argument(
|
|
765
|
+
CLI_ROUTING_MODE_FLAG,
|
|
766
|
+
dest="routing_mode",
|
|
767
|
+
choices=sorted(ALL_ROUTING_MODES),
|
|
768
|
+
default=DEFAULT_ROUTING_MODE,
|
|
769
|
+
help=(
|
|
770
|
+
"Chain routing: usage_ranked (default) or ordered_account "
|
|
771
|
+
"(config order, usage-limit-only fallover)."
|
|
772
|
+
),
|
|
773
|
+
)
|
|
613
774
|
parser.add_argument("passthrough", nargs=argparse.REMAINDER)
|
|
614
775
|
return parser
|
|
615
776
|
|
|
@@ -639,12 +800,18 @@ def _read_piped_stdin_text() -> str | None:
|
|
|
639
800
|
def main(all_command_arguments: list[str]) -> int:
|
|
640
801
|
"""Walk the chain for CLI arguments and return the process exit code.
|
|
641
802
|
|
|
803
|
+
::
|
|
804
|
+
|
|
805
|
+
main(["--", "-p", "hi"])
|
|
806
|
+
main(["--routing-mode", "ordered_account", "--", "-p", "hi"])
|
|
807
|
+
|
|
642
808
|
Args:
|
|
643
809
|
all_command_arguments: The argument vector after the program name.
|
|
644
810
|
|
|
645
811
|
Returns:
|
|
646
812
|
The served binary's return code, a distinct code when the chain is
|
|
647
|
-
exhausted, or a distinct code when the configuration
|
|
813
|
+
exhausted or advisor-blocked, or a distinct code when the configuration
|
|
814
|
+
cannot be loaded.
|
|
648
815
|
"""
|
|
649
816
|
parser = _build_argument_parser()
|
|
650
817
|
parsed_arguments = parser.parse_args(all_command_arguments)
|
|
@@ -655,10 +822,15 @@ def main(all_command_arguments: list[str]) -> int:
|
|
|
655
822
|
all_claude_arguments,
|
|
656
823
|
timeout_seconds=parsed_arguments.timeout_seconds,
|
|
657
824
|
stdin_text=maybe_stdin_text,
|
|
825
|
+
routing_mode=parsed_arguments.routing_mode,
|
|
658
826
|
)
|
|
659
827
|
except ChainConfigurationError as configuration_error:
|
|
660
828
|
print(str(configuration_error), file=sys.stderr)
|
|
661
829
|
return CHAIN_CONFIG_ERROR_EXIT_CODE
|
|
830
|
+
if chain_outcome.terminal_status == TERMINAL_STATUS_ADVISOR_BLOCKED:
|
|
831
|
+
sys.stdout.write(chain_outcome.stdout)
|
|
832
|
+
sys.stderr.write(chain_outcome.stderr)
|
|
833
|
+
return CHAIN_ADVISOR_BLOCKED_EXIT_CODE
|
|
662
834
|
if chain_outcome.served_command is None:
|
|
663
835
|
print(_exhausted_message(chain_outcome.attempts), file=sys.stderr)
|
|
664
836
|
return CHAIN_EXHAUSTED_EXIT_CODE
|
|
@@ -8,6 +8,7 @@ from collections.abc import Sequence
|
|
|
8
8
|
import claude_chain_runner as chain_runner
|
|
9
9
|
import pytest
|
|
10
10
|
from claude_chain_runner import ChainAttempt, ChainInvocationOutcome
|
|
11
|
+
from dev_env_scripts_constants.claude_chain_constants import TERMINAL_STATUS_SERVED
|
|
11
12
|
|
|
12
13
|
FIXTURE_ENCODING_KEYWORD_NAME = "encoding"
|
|
13
14
|
FIXTURE_ERRORS_KEYWORD_NAME = "errors"
|
|
@@ -23,6 +24,7 @@ def build_served_outcome(*, chain_stdout: str) -> ChainInvocationOutcome:
|
|
|
23
24
|
stdout=chain_stdout,
|
|
24
25
|
stderr="",
|
|
25
26
|
attempts=(ChainAttempt(command=FIXTURE_SERVED_COMMAND, status="served"),),
|
|
27
|
+
terminal_status=TERMINAL_STATUS_SERVED,
|
|
26
28
|
)
|
|
27
29
|
|
|
28
30
|
|
|
@@ -8,12 +8,12 @@ Named constants for scripts in `scripts/`. Follows the project convention that t
|
|
|
8
8
|
|---|---|
|
|
9
9
|
| `timing.py` | `sweep_empty_dirs.py` - `DEFAULT_AGE_SECONDS` (smallest age before an empty directory is eligible for removal) and `DEFAULT_POLL_INTERVAL` (seconds between sweep passes in continuous-watch mode); `spawn_grok_batch.py` - `WORKER_STAGGER_SECONDS` (seconds between staggered headless grok worker starts); `invoke_code_review.py` - `DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS` (timeout for one headless `/code-review` chain run) |
|
|
10
10
|
| `gh_artifact_upload_constants.py` | `gh_artifact_upload.py` - the `artifacts` release tag, title, and notes body, the GitHub CLI binary name, the asset-name timestamp format and template, the asset download URL template, the notes-file suffix, and the text encoding |
|
|
11
|
-
| `claude_chain_constants.py` | `claude_chain_runner.py` - the chain config filename and home subdirectory, the usage-limit signature text, the per-binary status labels, the default timeout, CLI flag and separator tokens, config JSON keys (including optional `credentials_path`), invalid-shape reason text, config-error and exhausted-chain message templates, and CLI exit codes; `invoke_code_review.py` and `resolve_worker_spawn.py` - the text-codec keyword names and the `collect_forwarded_text_codec` helper that forwards them to the subprocess runner for text-mode capture |
|
|
11
|
+
| `claude_chain_constants.py` | `claude_chain_runner.py` - the chain config filename and home subdirectory, the usage-limit signature text, the per-binary status labels, routing-mode tokens (`usage_ranked` / `ordered_account`), outcome `terminal_status` values (`served` / `advisor_blocked` / `chain_exhausted` / `timeout`), the default timeout, CLI flag and separator tokens, config JSON keys (including optional `credentials_path`), invalid-shape reason text, config-error and exhausted-chain message templates, and CLI exit codes (including advisor-blocked); `invoke_code_review.py` and `resolve_worker_spawn.py` - the text-codec keyword names and the `collect_forwarded_text_codec` helper that forwards them to the subprocess runner for text-mode capture |
|
|
12
12
|
| `claude_chain_usage_constants.py` | `claude_chain_usage.py` - full weekly percent scale, usage-pause skill path segments, CLI config-path flag, JSON report keys, and probe error message templates |
|
|
13
|
-
| `grok_worker_constants.py` | `grok_worker_preflight.py`, `grok_headless_runner.py`, and `
|
|
14
|
-
| `code_review_constants.py` | `invoke_code_review.py` - the `/code-review ultra --fix` prompt, opus model alias, permission-mode flag and value, result mode and JSON keys, session-model CLI flag, git dirty-check tokens, and in-session return markers |
|
|
13
|
+
| `grok_worker_constants.py` | `grok_worker_preflight.py`, `grok_headless_runner.py`, `spawn_grok_batch.py`, and `resolve_worker_spawn.py` - the `grok` binary name and CLI flags, model and subcommand tokens, leader-socket and scratch-file name parts, auth and usage-limit signature lists, outcome classifications, fallthrough reasons, tool-profile names and prompt headers, worker timeouts (default, plus the floor and the `MAXIMUM_WORKER_TIMEOUT_SECONDS` ceiling that the batch spec parse, the headless runner, and the spawn dispatcher all enforce, each bound carrying one rejection template shared by those sites, plus launch-failure return code and post-kill grace), the process-tree kill attempt limit and kill-failed classification, the preflight ping's single-turn cap, ping-cache keys and TTL, batch-spec and summary JSON keys, the prompt-part and report-stream join separators, and the CLI launch-error stderr prefix |
|
|
14
|
+
| `code_review_constants.py` | `invoke_code_review.py` - the `/code-review ultra --fix` prompt, opus model alias, permission-mode flag and value, result mode and JSON keys, session-model CLI flag, git dirty-check tokens, and in-session return markers; finding severity tokens (`blocker` / `high` / `medium` / `low` / `nit`), verification verdict tokens, loop terminal tokens (`clean` / `nits_fixed` / `advisor_blocked`), and pure helpers for retained-finding checks, head recording, terminal resolution, and terminal encoding |
|
|
15
15
|
| `__init__.py` | Empty package marker |
|
|
16
16
|
|
|
17
17
|
## Convention
|
|
18
18
|
|
|
19
|
-
Every constant is `UPPER_SNAKE_CASE` with an explicit type annotation and a docstring. Scripts import from here rather than embedding literal values in their bodies.
|
|
19
|
+
Every constant is `UPPER_SNAKE_CASE` with an explicit type annotation and a docstring. Scripts import from here rather than embedding literal values in their bodies. `code_review_constants.py` also exports pure helpers that resolve review-loop terminals from those tokens.
|
|
@@ -126,9 +126,47 @@ CHAIN_CONFIG_ERROR_EXIT_CODE: int = 3
|
|
|
126
126
|
CLI_TIMEOUT_FLAG: str = "--timeout-seconds"
|
|
127
127
|
"""CLI flag that overrides the per-invocation timeout in seconds."""
|
|
128
128
|
|
|
129
|
+
CLI_ROUTING_MODE_FLAG: str = "--routing-mode"
|
|
130
|
+
"""CLI flag that selects usage-ranked or ordered-account chain routing."""
|
|
131
|
+
|
|
129
132
|
CLI_ARGUMENTS_SEPARATOR: str = "--"
|
|
130
133
|
"""CLI token separating runner flags from the passthrough claude arguments."""
|
|
131
134
|
|
|
135
|
+
ROUTING_MODE_USAGE_RANKED: str = "usage_ranked"
|
|
136
|
+
"""Default routing: probe weekly remaining and try highest remaining first."""
|
|
137
|
+
|
|
138
|
+
ROUTING_MODE_ORDERED_ACCOUNT: str = "ordered_account"
|
|
139
|
+
"""Explicit routing: walk chain entries in config order; usage-limit-only fallover."""
|
|
140
|
+
|
|
141
|
+
DEFAULT_ROUTING_MODE: str = ROUTING_MODE_USAGE_RANKED
|
|
142
|
+
"""Routing mode applied when the caller does not name one."""
|
|
143
|
+
|
|
144
|
+
ALL_ROUTING_MODES: frozenset[str] = frozenset(
|
|
145
|
+
{
|
|
146
|
+
ROUTING_MODE_USAGE_RANKED,
|
|
147
|
+
ROUTING_MODE_ORDERED_ACCOUNT,
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
"""Accepted values for the routing-mode parameter and CLI flag."""
|
|
151
|
+
|
|
152
|
+
TERMINAL_STATUS_SERVED: str = "served"
|
|
153
|
+
"""Outcome status when a binary served the call (zero or non-usage nonzero)."""
|
|
154
|
+
|
|
155
|
+
TERMINAL_STATUS_ADVISOR_BLOCKED: str = "advisor_blocked"
|
|
156
|
+
"""Outcome status when ordered-account mode stops on a non-usage failure."""
|
|
157
|
+
|
|
158
|
+
TERMINAL_STATUS_CHAIN_EXHAUSTED: str = "chain_exhausted"
|
|
159
|
+
"""Outcome status when every chain entry was usage-limited or missing."""
|
|
160
|
+
|
|
161
|
+
TERMINAL_STATUS_TIMEOUT: str = "timeout"
|
|
162
|
+
"""Outcome status when a usage-ranked walk stops on TimeoutExpired mid-walk."""
|
|
163
|
+
|
|
164
|
+
SESSION_ID_JSON_KEY: str = "session_id"
|
|
165
|
+
"""JSON key read from Claude ``--output-format json`` events for resume."""
|
|
166
|
+
|
|
167
|
+
CHAIN_ADVISOR_BLOCKED_EXIT_CODE: int = 4
|
|
168
|
+
"""CLI exit code when ordered-account mode stops with advisor_blocked."""
|
|
169
|
+
|
|
132
170
|
CONFIG_NOT_OBJECT_REASON: str = "the top-level value is not a JSON object"
|
|
133
171
|
"""Reason detail when the config root is not an object."""
|
|
134
172
|
|