claude-dev-env 2.9.0 → 2.11.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/CLAUDE.md +2 -2
- package/_shared/advisor/CLAUDE.md +3 -2
- package/_shared/advisor/advisor-protocol.md +74 -108
- package/_shared/advisor/reference/advisor-block.md +37 -0
- package/_shared/advisor/reference/cli-chain.md +45 -0
- package/_shared/advisor/reference/consult-format.md +41 -0
- package/_shared/advisor/reference/lifecycle.md +21 -0
- package/_shared/advisor/reference/sol-rung.md +31 -0
- package/_shared/advisor/reference/spawn-walk-log.md +31 -0
- package/_shared/advisor/reference/third-party-bind.md +30 -0
- package/_shared/advisor/reference/warm-up.md +33 -0
- package/_shared/advisor/scripts/codex_sol_advisor.py +449 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +21 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +19 -17
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +28 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +32 -9
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +474 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +79 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +39 -17
- package/_shared/advisor/scripts/tier_model_ids.py +24 -0
- package/_shared/pr-loop/CLAUDE.md +1 -1
- package/_shared/pr-loop/audit-contract.md +17 -6
- package/_shared/pr-loop/audit-reply-template.md +4 -4
- package/_shared/pr-loop/code-rules-gate.md +3 -5
- package/_shared/pr-loop/fix-protocol.md +2 -3
- package/_shared/pr-loop/gh-payloads.md +1 -1
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/README.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
- package/_shared/pr-loop/scripts/preflight.py +9 -4
- package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
- package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
- package/_shared/pr-loop/state-schema.md +5 -14
- package/agents/CLAUDE.md +2 -2
- package/agents/clean-coder.md +58 -548
- package/agents/code-quality-agent.md +10 -2
- package/agents/code-verifier.md +1 -1
- package/agents/test_agent_frontmatter.py +32 -40
- package/audit-rubrics/CLAUDE.md +2 -1
- package/audit-rubrics/audit-categories.json +704 -0
- package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
- package/bin/CLAUDE.md +16 -5
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/install-plan.mjs +402 -0
- package/bin/install-transaction.mjs +455 -0
- package/bin/install.mjs +593 -147
- package/bin/install.plan.test.mjs +194 -0
- package/bin/install.profile-root.test.mjs +154 -0
- package/bin/install.profiles.test.mjs +253 -0
- package/bin/install.settings-defaults.test.mjs +200 -0
- package/bin/install.transaction.test.mjs +400 -0
- package/bin/install.uninstall-transaction.test.mjs +418 -0
- package/bin/merge_managed_permissions.mjs +130 -0
- package/bin/resolve-install-root.mjs +181 -0
- package/bin/select-install-targets.mjs +401 -0
- package/commands/CLAUDE.md +0 -2
- package/docs/references/CLAUDE.md +3 -1
- package/docs/references/advisor-tool.md +25 -7
- package/docs/references/prose-style-enforcement.md +25 -0
- package/docs/references/team-advisor-skill.md +3 -3
- package/docs/references/weak-executor-advisor.md +91 -0
- package/hooks/blocking/CLAUDE.md +6 -6
- package/hooks/blocking/_path_setup.py +9 -5
- package/hooks/blocking/code_rules_docstrings.py +124 -30
- package/hooks/blocking/code_rules_enforcer.py +161 -16
- package/hooks/blocking/code_rules_shared.py +40 -23
- package/hooks/blocking/config/CLAUDE.md +3 -5
- package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
- package/hooks/blocking/eli11_reply_enforcer.py +70 -113
- package/hooks/blocking/hedging_language_blocker.py +103 -20
- package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
- package/hooks/blocking/intent_only_ending_blocker.py +6 -0
- package/hooks/blocking/plain_language_blocker.py +139 -20
- package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
- package/hooks/blocking/state_description_blocker.py +7 -1
- package/hooks/blocking/tdd_enforcer.py +8 -0
- package/hooks/blocking/test__path_setup.py +28 -0
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
- package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
- package/hooks/blocking/test_fable_spawn_gate.py +18 -11
- package/hooks/blocking/test_hedging_language_blocker.py +120 -1
- package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
- package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
- package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
- package/hooks/blocking/test_plain_language_blocker.py +129 -19
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
- package/hooks/blocking/test_state_description_blocker.py +45 -2
- package/hooks/blocking/test_stop_dispatcher.py +11 -7
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
- package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
- package/hooks/hooks.json +15 -0
- package/hooks/hooks_constants/CLAUDE.md +14 -3
- package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
- package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
- package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
- package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/project_paths_reader.py +31 -4
- package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
- package/hooks/hooks_constants/pytest_invocation.py +354 -0
- package/hooks/hooks_constants/session_start_injector.py +163 -0
- package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
- package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
- package/hooks/hooks_constants/shell_command_segments.py +5 -0
- package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
- package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
- package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
- package/hooks/hooks_constants/test_session_start_injector.py +168 -0
- package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
- package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
- package/hooks/observability/CLAUDE.md +2 -0
- package/hooks/observability/prose_matcher_advisory.py +237 -0
- package/hooks/observability/test_prose_matcher_advisory.py +143 -0
- package/hooks/session/CLAUDE.md +9 -1
- package/hooks/session/_path_setup.py +13 -0
- package/hooks/session/issue_tracker_session_starter.py +135 -0
- package/hooks/session/orchestrator_auto_starter.py +100 -0
- package/hooks/session/test__path_setup.py +28 -0
- package/hooks/session/test_issue_tracker_session_starter.py +104 -0
- package/hooks/session/test_orchestrator_auto_starter.py +99 -0
- package/hooks/session/test_working_style_prompt.py +47 -0
- package/hooks/session/untracked_repo_detector.py +1 -24
- package/hooks/session/working_style_prompt.py +36 -0
- package/hooks/validators/_path_setup.py +19 -0
- package/hooks/validators/run_all_validators.py +8 -13
- package/installable-surfaces.manifest.json +21 -0
- package/output-styles/CLAUDE.md +1 -3
- package/package.json +4 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/durable-post-artifacts.md +2 -2
- package/rules/eli11-replies.md +6 -1
- package/rules/hedging-claims.md +4 -2
- package/rules/long-horizon-autonomy.md +3 -1
- package/rules/opus5-communication-contract.md +45 -0
- package/rules/plain-language.md +2 -2
- package/rules/research-mode.md +1 -1
- package/scripts/CLAUDE.md +11 -0
- package/scripts/Sync-RepoMain.ps1 +215 -0
- package/scripts/active_capability_references.py +218 -0
- package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
- package/scripts/claude_chain_runner.py +394 -6
- package/scripts/claude_chain_usage.py +1 -1
- package/scripts/codex_compat_materializer.py +105 -85
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
- package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
- package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
- package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
- package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
- package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
- package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
- package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
- package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
- package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
- package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
- package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
- package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
- package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
- package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
- package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
- package/scripts/spawn_grok_batch.py +3 -0
- package/scripts/test_active_capability_references.py +108 -0
- package/scripts/test_claude_chain_runner.py +414 -82
- package/scripts/test_claude_chain_usage.py +12 -12
- package/scripts/test_resolve_worker_spawn.py +2 -2
- package/scripts/test_verify_installable_package.py +208 -0
- package/scripts/tests/test_codex_compat_materializer.py +33 -0
- package/scripts/verify_installable_package.py +612 -0
- package/settings.json +10 -0
- package/skills/CLAUDE.md +2 -0
- package/skills/_shared/advisor/CLAUDE.md +1 -1
- package/skills/_shared/advisor/scripts/README.md +2 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
- package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/stop-conditions.md +5 -3
- package/skills/beat-sheet/SKILL.md +54 -0
- package/skills/beat-sheet/reference/visual-beats.md +29 -0
- package/skills/bugteam/CONSTRAINTS.md +4 -4
- package/skills/bugteam/EXAMPLES.md +1 -1
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/e-code-review/SKILL.md +26 -5
- package/skills/e-code-review/reference/effort-evaluation.md +35 -0
- package/skills/e-code-review/reference/medium.md +15 -4
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
- package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
- package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
- package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
- package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
- package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
- package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
- package/skills/e-code-review/scripts/grok_code_review.py +16 -7
- package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
- package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
- package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
- package/skills/grokify/SKILL.md +1 -1
- package/skills/grokify/templates/handoff-template.md +2 -2
- package/skills/orchestrator/SKILL.md +5 -4
- package/skills/plan-to-pr/scripts/create_packet.py +4 -4
- package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
- package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
- package/skills/plan-to-pr/scripts/validate_run.py +4 -1
- package/skills/pr-converge/scripts/check_convergence.py +21 -19
- package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
- package/skills/pr-converge/scripts/conftest.py +35 -0
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/rebase/SKILL.md +15 -3
- package/skills/reviewer-gates/SKILL.md +2 -2
- package/skills/show/SKILL.md +51 -0
- package/skills/show/references/accessibility.md +7 -0
- package/skills/show/references/art.md +3 -0
- package/skills/show/references/charts.md +3 -0
- package/skills/show/references/core-design.md +14 -0
- package/skills/show/references/erds.md +3 -0
- package/skills/show/references/flowcharts.md +3 -0
- package/skills/show/references/host-and-html.md +3 -0
- package/skills/show/references/illustrative-diagrams.md +10 -0
- package/skills/show/references/interaction.md +3 -0
- package/skills/show/references/mockups.md +3 -0
- package/skills/show/references/quality-gates.md +7 -0
- package/skills/show/references/structural-diagrams.md +3 -0
- package/skills/show/references/subject-inventory.md +21 -0
- package/skills/show/references/svg-contract.md +22 -0
- package/skills/show/routing.yaml +30 -0
- package/skills/show/samples/pr1262-v2.svg +222 -0
- package/skills/show/scripts/README.md +6 -0
- package/skills/show/scripts/validate-artifact.py +91 -0
- package/skills/show/scripts/validate-package.py +18 -0
- package/skills/show/templates/html-widget.html +4 -0
- package/skills/show/templates/svg-base.svg +19 -0
- package/skills/show/tests/fixtures/css-var.svg +6 -0
- package/skills/show/tests/fixtures/dead-ref.svg +7 -0
- package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
- package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
- package/skills/show/tests/fixtures/invalid.svg +1 -0
- package/skills/show/tests/fixtures/large-canvas.svg +21 -0
- package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
- package/skills/show/tests/fixtures/valid.html +1 -0
- package/skills/show/tests/test_validate-artifact.py +74 -0
- package/skills/show/tests/test_validators.py +59 -0
- package/skills/show/workflows/create-visual.md +13 -0
- package/skills/show/workflows/review-visual.md +20 -0
- package/skills/split-pr/SKILL.md +85 -0
- package/skills/split-pr/reference/path-layers.md +16 -0
- package/skills/split-pr/reference/proposal-format.md +15 -0
- package/skills/split-pr/reference/split-further-loop.md +10 -0
- package/skills/split-pr/reference/splitting-principles.md +26 -0
- package/skills/split-pr/scripts/analyze_pr.py +279 -0
- package/skills/split-pr/scripts/categorize_files.py +106 -0
- package/skills/split-pr/scripts/config/__init__.py +1 -0
- package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
- package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
- package/skills/split-pr/scripts/config/packing_constants.py +61 -0
- package/skills/split-pr/scripts/config/plan_constants.py +49 -0
- package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
- package/skills/split-pr/scripts/execute_split_slices.py +82 -0
- package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
- package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
- package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
- package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
- package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
- package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
- package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
- package/skills/split-pr/scripts/split_pr_title.py +41 -0
- package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
- package/skills/split-pr/scripts/test_categorize_files.py +55 -0
- package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
- package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
- package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
- package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
- package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
- package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
- package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
- package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
- package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
- package/skills/split-pr/scripts/test_verify_plan.py +56 -0
- package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
- package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
- package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
- package/skills/split-pr/scripts/verify_plan.py +139 -0
- package/skills/team-advisor/SKILL.md +7 -4
- package/skills/team-advisor/reference/advisor-docs-review.md +207 -0
- package/system-prompts/software-engineer.xml +11 -2
- package/commands/initialize.md +0 -90
- package/commands/stubcheck.md +0 -88
- package/output-styles/caveman-agent.md +0 -37
|
@@ -19,7 +19,7 @@ Default try order comes from weekly remaining via ``claude_chain_usage``
|
|
|
19
19
|
(usage-pause OAuth probe), not from list position alone::
|
|
20
20
|
|
|
21
21
|
{"chain": [{"command": "claude", "extra_args": []},
|
|
22
|
-
{"command": "claude-
|
|
22
|
+
{"command": "claude-profile-c", "extra_args": []}]}
|
|
23
23
|
|
|
24
24
|
A usage-limited first try falls over to the next ranked binary::
|
|
25
25
|
|
|
@@ -44,11 +44,12 @@ import argparse
|
|
|
44
44
|
import importlib
|
|
45
45
|
import io
|
|
46
46
|
import json
|
|
47
|
+
import os
|
|
47
48
|
import subprocess
|
|
48
49
|
import sys
|
|
49
50
|
import tempfile
|
|
50
51
|
from collections.abc import Callable, Sequence
|
|
51
|
-
from dataclasses import dataclass
|
|
52
|
+
from dataclasses import dataclass, field
|
|
52
53
|
from pathlib import Path
|
|
53
54
|
from types import ModuleType
|
|
54
55
|
from typing import Protocol, TextIO
|
|
@@ -57,7 +58,27 @@ if __name__ == "__main__":
|
|
|
57
58
|
sys.modules.setdefault("claude_chain_runner", sys.modules[__name__])
|
|
58
59
|
|
|
59
60
|
from dev_env_scripts_constants.claude_chain_constants import (
|
|
61
|
+
AFFINITY_BINDING_COMMAND_MISSING_REASON,
|
|
62
|
+
AFFINITY_BINDING_NOT_OBJECT_REASON,
|
|
63
|
+
AFFINITY_BINDING_SESSION_ID_MISSING_REASON,
|
|
64
|
+
AFFINITY_BINDINGS_MISSING_OR_NOT_LIST_REASON,
|
|
65
|
+
AFFINITY_CORRUPT_MESSAGE_TEMPLATE,
|
|
66
|
+
AFFINITY_JSON_INDENT_SPACES,
|
|
67
|
+
AFFINITY_KEY_ALL_BINDINGS,
|
|
68
|
+
AFFINITY_KEY_COMMAND,
|
|
69
|
+
AFFINITY_KEY_SCHEMA_VERSION,
|
|
70
|
+
AFFINITY_KEY_SESSION_ID,
|
|
71
|
+
AFFINITY_MAXIMUM_ENTRIES,
|
|
72
|
+
AFFINITY_MAXIMUM_ENTRIES_MINIMUM_MESSAGE,
|
|
73
|
+
AFFINITY_SESSION_ID_AND_COMMAND_REQUIRED_MESSAGE,
|
|
74
|
+
AFFINITY_STATE_FILENAME,
|
|
75
|
+
AFFINITY_STATE_SCHEMA_VERSION,
|
|
76
|
+
AFFINITY_TEMP_SUFFIX,
|
|
77
|
+
AFFINITY_TOP_LEVEL_NOT_OBJECT_REASON,
|
|
78
|
+
AFFINITY_UNSUPPORTED_SCHEMA_VERSION_REASON_TEMPLATE,
|
|
79
|
+
AFFINITY_WRITE_FAILED_MESSAGE_TEMPLATE,
|
|
60
80
|
ALL_ROUTING_MODES,
|
|
81
|
+
RESUME_SESSION_FLAG,
|
|
61
82
|
ALL_USAGE_LIMIT_SIGNATURES,
|
|
62
83
|
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
63
84
|
ATTEMPT_STATUS_NONZERO_EXIT,
|
|
@@ -519,6 +540,219 @@ def _session_id_from_json_text(json_text: str) -> str | None:
|
|
|
519
540
|
return None
|
|
520
541
|
|
|
521
542
|
|
|
543
|
+
@dataclass(frozen=True)
|
|
544
|
+
class AffinityBinding:
|
|
545
|
+
"""One session-id to chain-binary binding."""
|
|
546
|
+
|
|
547
|
+
session_id: str
|
|
548
|
+
command: str
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@dataclass(frozen=True)
|
|
552
|
+
class AffinityStore:
|
|
553
|
+
"""Versioned, bounded session-to-binary affinity document."""
|
|
554
|
+
|
|
555
|
+
schema_version: int = AFFINITY_STATE_SCHEMA_VERSION
|
|
556
|
+
all_bindings: list[AffinityBinding] = field(default_factory=list)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def default_affinity_state_path(claude_home_directory: Path) -> Path:
|
|
560
|
+
"""Return the default affinity state path under a Claude home directory.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
claude_home_directory: Claude configuration root (for example ``~/.claude``).
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
Path to the affinity state JSON file.
|
|
567
|
+
"""
|
|
568
|
+
return claude_home_directory / AFFINITY_STATE_FILENAME
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _affinity_corrupt_error(state_path: Path, error: object) -> ValueError:
|
|
572
|
+
return ValueError(
|
|
573
|
+
AFFINITY_CORRUPT_MESSAGE_TEMPLATE.format(
|
|
574
|
+
state_path=state_path,
|
|
575
|
+
error=error,
|
|
576
|
+
)
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _parse_affinity_binding(
|
|
581
|
+
each_binding: object,
|
|
582
|
+
*,
|
|
583
|
+
state_path: Path,
|
|
584
|
+
) -> AffinityBinding:
|
|
585
|
+
if not isinstance(each_binding, dict):
|
|
586
|
+
raise _affinity_corrupt_error(state_path, AFFINITY_BINDING_NOT_OBJECT_REASON)
|
|
587
|
+
session_id = each_binding.get(AFFINITY_KEY_SESSION_ID)
|
|
588
|
+
command = each_binding.get(AFFINITY_KEY_COMMAND)
|
|
589
|
+
if not isinstance(session_id, str) or not session_id:
|
|
590
|
+
raise _affinity_corrupt_error(
|
|
591
|
+
state_path, AFFINITY_BINDING_SESSION_ID_MISSING_REASON
|
|
592
|
+
)
|
|
593
|
+
if not isinstance(command, str) or not command:
|
|
594
|
+
raise _affinity_corrupt_error(
|
|
595
|
+
state_path, AFFINITY_BINDING_COMMAND_MISSING_REASON
|
|
596
|
+
)
|
|
597
|
+
return AffinityBinding(session_id=session_id, command=command)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _bindings_from_payload(
|
|
601
|
+
all_payload_fields: dict[str, object],
|
|
602
|
+
*,
|
|
603
|
+
state_path: Path,
|
|
604
|
+
) -> list[AffinityBinding]:
|
|
605
|
+
schema_version = all_payload_fields.get(AFFINITY_KEY_SCHEMA_VERSION)
|
|
606
|
+
if schema_version != AFFINITY_STATE_SCHEMA_VERSION:
|
|
607
|
+
raise _affinity_corrupt_error(
|
|
608
|
+
state_path,
|
|
609
|
+
AFFINITY_UNSUPPORTED_SCHEMA_VERSION_REASON_TEMPLATE.format(
|
|
610
|
+
schema_version=schema_version
|
|
611
|
+
),
|
|
612
|
+
)
|
|
613
|
+
raw_bindings = all_payload_fields.get(AFFINITY_KEY_ALL_BINDINGS)
|
|
614
|
+
if not isinstance(raw_bindings, list):
|
|
615
|
+
raise _affinity_corrupt_error(
|
|
616
|
+
state_path, AFFINITY_BINDINGS_MISSING_OR_NOT_LIST_REASON
|
|
617
|
+
)
|
|
618
|
+
return [
|
|
619
|
+
_parse_affinity_binding(each_binding, state_path=state_path)
|
|
620
|
+
for each_binding in raw_bindings
|
|
621
|
+
]
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def load_affinity_store(state_path: Path) -> AffinityStore:
|
|
625
|
+
"""Load a versioned affinity store, or an empty store when the file is absent.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
state_path: Path to the affinity state JSON file.
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
Parsed affinity store.
|
|
632
|
+
|
|
633
|
+
Raises:
|
|
634
|
+
ValueError: When the document is corrupt or uses an unsupported schema.
|
|
635
|
+
"""
|
|
636
|
+
if not state_path.is_file():
|
|
637
|
+
return AffinityStore()
|
|
638
|
+
try:
|
|
639
|
+
raw_text = state_path.read_text(encoding=UTF8_ENCODING)
|
|
640
|
+
parsed_payload = json.loads(raw_text)
|
|
641
|
+
except (OSError, json.JSONDecodeError, UnicodeError) as load_error:
|
|
642
|
+
raise _affinity_corrupt_error(state_path, load_error) from load_error
|
|
643
|
+
if not isinstance(parsed_payload, dict):
|
|
644
|
+
raise _affinity_corrupt_error(
|
|
645
|
+
state_path, AFFINITY_TOP_LEVEL_NOT_OBJECT_REASON
|
|
646
|
+
)
|
|
647
|
+
return AffinityStore(
|
|
648
|
+
schema_version=AFFINITY_STATE_SCHEMA_VERSION,
|
|
649
|
+
all_bindings=_bindings_from_payload(parsed_payload, state_path=state_path),
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def record_affinity_binding(
|
|
654
|
+
store: AffinityStore,
|
|
655
|
+
*,
|
|
656
|
+
session_id: str,
|
|
657
|
+
command: str,
|
|
658
|
+
maximum_entries: int = AFFINITY_MAXIMUM_ENTRIES,
|
|
659
|
+
) -> AffinityStore:
|
|
660
|
+
"""Return a new store with ``session_id`` bound to ``command``, bounded.
|
|
661
|
+
|
|
662
|
+
Re-binding an existing session moves it to the newest end. When the store
|
|
663
|
+
exceeds ``maximum_entries``, the oldest bindings drop first.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
store: Current affinity store.
|
|
667
|
+
session_id: Claude session id to bind.
|
|
668
|
+
command: Chain binary command that served the session.
|
|
669
|
+
maximum_entries: Hard cap on retained bindings.
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
Updated store (does not mutate ``store``).
|
|
673
|
+
|
|
674
|
+
Raises:
|
|
675
|
+
ValueError: When session_id, command, or maximum_entries is invalid.
|
|
676
|
+
"""
|
|
677
|
+
if not session_id or not command:
|
|
678
|
+
raise ValueError(AFFINITY_SESSION_ID_AND_COMMAND_REQUIRED_MESSAGE)
|
|
679
|
+
if maximum_entries < 1:
|
|
680
|
+
raise ValueError(AFFINITY_MAXIMUM_ENTRIES_MINIMUM_MESSAGE)
|
|
681
|
+
all_remaining = [
|
|
682
|
+
each_binding
|
|
683
|
+
for each_binding in store.all_bindings
|
|
684
|
+
if each_binding.session_id != session_id
|
|
685
|
+
]
|
|
686
|
+
all_remaining.append(AffinityBinding(session_id=session_id, command=command))
|
|
687
|
+
if len(all_remaining) > maximum_entries:
|
|
688
|
+
all_remaining = all_remaining[-maximum_entries:]
|
|
689
|
+
return AffinityStore(
|
|
690
|
+
schema_version=AFFINITY_STATE_SCHEMA_VERSION,
|
|
691
|
+
all_bindings=all_remaining,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def save_affinity_store_atomic(state_path: Path, store: AffinityStore) -> None:
|
|
696
|
+
"""Atomically replace the affinity state file with ``store``.
|
|
697
|
+
|
|
698
|
+
Creates a unique sibling temporary file via ``tempfile.mkstemp``, writes
|
|
699
|
+
and flushes the document, then uses ``os.replace`` so readers never
|
|
700
|
+
observe a partial document.
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
state_path: Destination affinity state path.
|
|
704
|
+
store: Store document to persist.
|
|
705
|
+
|
|
706
|
+
Raises:
|
|
707
|
+
OSError: When the write or replace fails (message is actionable).
|
|
708
|
+
"""
|
|
709
|
+
payload = {
|
|
710
|
+
AFFINITY_KEY_SCHEMA_VERSION: store.schema_version,
|
|
711
|
+
AFFINITY_KEY_ALL_BINDINGS: [
|
|
712
|
+
{
|
|
713
|
+
AFFINITY_KEY_SESSION_ID: each_binding.session_id,
|
|
714
|
+
AFFINITY_KEY_COMMAND: each_binding.command,
|
|
715
|
+
}
|
|
716
|
+
for each_binding in store.all_bindings
|
|
717
|
+
],
|
|
718
|
+
}
|
|
719
|
+
serialized_document = (
|
|
720
|
+
json.dumps(payload, indent=AFFINITY_JSON_INDENT_SPACES, sort_keys=True)
|
|
721
|
+
+ "\n"
|
|
722
|
+
)
|
|
723
|
+
state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
724
|
+
file_descriptor, temporary_name = tempfile.mkstemp(
|
|
725
|
+
prefix=f".{state_path.name}.",
|
|
726
|
+
suffix=AFFINITY_TEMP_SUFFIX,
|
|
727
|
+
dir=state_path.parent,
|
|
728
|
+
)
|
|
729
|
+
temporary_path = Path(temporary_name)
|
|
730
|
+
owned_descriptor = file_descriptor
|
|
731
|
+
try:
|
|
732
|
+
with os.fdopen(file_descriptor, "w", encoding=UTF8_ENCODING) as temporary_file:
|
|
733
|
+
owned_descriptor = -1
|
|
734
|
+
temporary_file.write(serialized_document)
|
|
735
|
+
temporary_file.flush()
|
|
736
|
+
os.fsync(temporary_file.fileno())
|
|
737
|
+
os.replace(temporary_path, state_path)
|
|
738
|
+
except OSError as write_error:
|
|
739
|
+
if owned_descriptor >= 0:
|
|
740
|
+
try:
|
|
741
|
+
os.close(owned_descriptor)
|
|
742
|
+
except OSError:
|
|
743
|
+
pass
|
|
744
|
+
try:
|
|
745
|
+
temporary_path.unlink(missing_ok=True)
|
|
746
|
+
except OSError:
|
|
747
|
+
pass
|
|
748
|
+
raise OSError(
|
|
749
|
+
AFFINITY_WRITE_FAILED_MESSAGE_TEMPLATE.format(
|
|
750
|
+
state_path=state_path,
|
|
751
|
+
error=write_error,
|
|
752
|
+
)
|
|
753
|
+
) from write_error
|
|
754
|
+
|
|
755
|
+
|
|
522
756
|
def _served_outcome(
|
|
523
757
|
served_command: str,
|
|
524
758
|
completion: subprocess.CompletedProcess[str],
|
|
@@ -638,6 +872,80 @@ def _ranked_entries_or_config_order(
|
|
|
638
872
|
return list(all_entries)
|
|
639
873
|
|
|
640
874
|
|
|
875
|
+
def extract_resume_session_id(
|
|
876
|
+
all_claude_arguments: Sequence[str],
|
|
877
|
+
) -> str | None:
|
|
878
|
+
"""Return the session id after ``--resume`` when present.
|
|
879
|
+
|
|
880
|
+
Args:
|
|
881
|
+
all_claude_arguments: Arguments passed after the binary name.
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
The non-empty session id, or ``None`` when the flag is absent.
|
|
885
|
+
"""
|
|
886
|
+
for each_index, each_argument in enumerate(all_claude_arguments):
|
|
887
|
+
if each_argument != RESUME_SESSION_FLAG:
|
|
888
|
+
continue
|
|
889
|
+
next_index = each_index + 1
|
|
890
|
+
if next_index >= len(all_claude_arguments):
|
|
891
|
+
return None
|
|
892
|
+
session_id = all_claude_arguments[next_index]
|
|
893
|
+
if session_id and not session_id.startswith("-"):
|
|
894
|
+
return session_id
|
|
895
|
+
return None
|
|
896
|
+
return None
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def lookup_affinity_command(
|
|
900
|
+
store: AffinityStore,
|
|
901
|
+
session_id: str,
|
|
902
|
+
) -> str | None:
|
|
903
|
+
"""Return the bound binary command for ``session_id``, if any.
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
store: Loaded affinity store.
|
|
907
|
+
session_id: Session id from a prior serve.
|
|
908
|
+
|
|
909
|
+
Returns:
|
|
910
|
+
The bound command string, or ``None`` when unbound.
|
|
911
|
+
"""
|
|
912
|
+
for each_binding in store.all_bindings:
|
|
913
|
+
if each_binding.session_id == session_id:
|
|
914
|
+
return each_binding.command
|
|
915
|
+
return None
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def order_entries_for_resume(
|
|
919
|
+
all_entries: list[ChainEntry],
|
|
920
|
+
*,
|
|
921
|
+
preferred_command: str | None,
|
|
922
|
+
) -> list[ChainEntry]:
|
|
923
|
+
"""Put the preferred originating binary first; keep relative order of the rest.
|
|
924
|
+
|
|
925
|
+
When ``preferred_command`` is missing from the chain or is ``None``, the
|
|
926
|
+
original list order is returned unchanged (documented fallback).
|
|
927
|
+
|
|
928
|
+
Args:
|
|
929
|
+
all_entries: Chain entries in the mode's base order.
|
|
930
|
+
preferred_command: Originating binary from the affinity store.
|
|
931
|
+
|
|
932
|
+
Returns:
|
|
933
|
+
Ordered walk list for this invocation.
|
|
934
|
+
"""
|
|
935
|
+
if preferred_command is None:
|
|
936
|
+
return list(all_entries)
|
|
937
|
+
preferred: list[ChainEntry] = []
|
|
938
|
+
remaining: list[ChainEntry] = []
|
|
939
|
+
for each_entry in all_entries:
|
|
940
|
+
if each_entry.command == preferred_command and not preferred:
|
|
941
|
+
preferred.append(each_entry)
|
|
942
|
+
else:
|
|
943
|
+
remaining.append(each_entry)
|
|
944
|
+
if not preferred:
|
|
945
|
+
return list(all_entries)
|
|
946
|
+
return preferred + remaining
|
|
947
|
+
|
|
948
|
+
|
|
641
949
|
def _resolve_walk_entries(
|
|
642
950
|
all_entries: list[ChainEntry],
|
|
643
951
|
config_path: Path,
|
|
@@ -648,6 +956,36 @@ def _resolve_walk_entries(
|
|
|
648
956
|
return _ranked_entries_or_config_order(all_entries, config_path)
|
|
649
957
|
|
|
650
958
|
|
|
959
|
+
def _affinity_preferred_command(
|
|
960
|
+
all_claude_arguments: Sequence[str],
|
|
961
|
+
affinity_path: Path,
|
|
962
|
+
) -> str | None:
|
|
963
|
+
resume_session_id = extract_resume_session_id(all_claude_arguments)
|
|
964
|
+
if resume_session_id is None:
|
|
965
|
+
return None
|
|
966
|
+
try:
|
|
967
|
+
affinity_store = load_affinity_store(affinity_path)
|
|
968
|
+
except ValueError:
|
|
969
|
+
return None
|
|
970
|
+
return lookup_affinity_command(affinity_store, resume_session_id)
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def _walk_entries_for_invocation(
|
|
974
|
+
all_entries: list[ChainEntry],
|
|
975
|
+
config_path: Path,
|
|
976
|
+
routing_mode: str,
|
|
977
|
+
all_claude_arguments: Sequence[str],
|
|
978
|
+
affinity_path: Path,
|
|
979
|
+
) -> list[ChainEntry]:
|
|
980
|
+
base_order = _resolve_walk_entries(all_entries, config_path, routing_mode)
|
|
981
|
+
preferred_command = _affinity_preferred_command(
|
|
982
|
+
all_claude_arguments, affinity_path
|
|
983
|
+
)
|
|
984
|
+
return order_entries_for_resume(
|
|
985
|
+
base_order, preferred_command=preferred_command
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
|
|
651
989
|
def _require_known_routing_mode(routing_mode: str) -> str:
|
|
652
990
|
if routing_mode not in ALL_ROUTING_MODES:
|
|
653
991
|
raise ValueError(
|
|
@@ -702,9 +1040,35 @@ def run_claude(
|
|
|
702
1040
|
selected_routing_mode = _require_known_routing_mode(routing_mode)
|
|
703
1041
|
config_path = chain_config_path()
|
|
704
1042
|
all_entries = load_chain(config_path)
|
|
705
|
-
|
|
706
|
-
|
|
1043
|
+
affinity_path = default_affinity_state_path(
|
|
1044
|
+
Path.home() / CLAUDE_HOME_SUBDIRECTORY
|
|
707
1045
|
)
|
|
1046
|
+
all_walk_entries = _walk_entries_for_invocation(
|
|
1047
|
+
all_entries,
|
|
1048
|
+
config_path,
|
|
1049
|
+
selected_routing_mode,
|
|
1050
|
+
all_claude_arguments,
|
|
1051
|
+
affinity_path,
|
|
1052
|
+
)
|
|
1053
|
+
return _walk_chain_attempts(
|
|
1054
|
+
all_walk_entries,
|
|
1055
|
+
all_claude_arguments=all_claude_arguments,
|
|
1056
|
+
timeout_seconds=timeout_seconds,
|
|
1057
|
+
stdin_text=stdin_text,
|
|
1058
|
+
routing_mode=selected_routing_mode,
|
|
1059
|
+
affinity_path=affinity_path,
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def _walk_chain_attempts(
|
|
1064
|
+
all_walk_entries: list[ChainEntry],
|
|
1065
|
+
*,
|
|
1066
|
+
all_claude_arguments: list[str],
|
|
1067
|
+
timeout_seconds: int,
|
|
1068
|
+
stdin_text: str | None,
|
|
1069
|
+
routing_mode: str,
|
|
1070
|
+
affinity_path: Path,
|
|
1071
|
+
) -> ChainInvocationOutcome:
|
|
708
1072
|
all_attempts: list[ChainAttempt] = []
|
|
709
1073
|
last_usage_limited: subprocess.CompletedProcess[str] | None = None
|
|
710
1074
|
for each_entry in all_walk_entries:
|
|
@@ -725,7 +1089,7 @@ def run_claude(
|
|
|
725
1089
|
)
|
|
726
1090
|
timeout_terminal_status = (
|
|
727
1091
|
TERMINAL_STATUS_ADVISOR_BLOCKED
|
|
728
|
-
if
|
|
1092
|
+
if routing_mode == ROUTING_MODE_ORDERED_ACCOUNT
|
|
729
1093
|
else TERMINAL_STATUS_TIMEOUT
|
|
730
1094
|
)
|
|
731
1095
|
return _no_process_outcome(
|
|
@@ -742,14 +1106,38 @@ def run_claude(
|
|
|
742
1106
|
each_entry,
|
|
743
1107
|
completion,
|
|
744
1108
|
all_attempts,
|
|
745
|
-
routing_mode=
|
|
1109
|
+
routing_mode=routing_mode,
|
|
746
1110
|
)
|
|
747
1111
|
if terminal_outcome is not None:
|
|
1112
|
+
_persist_served_affinity(
|
|
1113
|
+
affinity_path,
|
|
1114
|
+
served_command=each_entry.command,
|
|
1115
|
+
session_id=terminal_outcome.session_id,
|
|
1116
|
+
)
|
|
748
1117
|
return terminal_outcome
|
|
749
1118
|
last_usage_limited = completion
|
|
750
1119
|
return _exhausted_outcome(all_attempts, last_usage_limited)
|
|
751
1120
|
|
|
752
1121
|
|
|
1122
|
+
def _persist_served_affinity(
|
|
1123
|
+
affinity_path: Path,
|
|
1124
|
+
*,
|
|
1125
|
+
served_command: str,
|
|
1126
|
+
session_id: str | None,
|
|
1127
|
+
) -> None:
|
|
1128
|
+
"""Best-effort write of session-to-binary affinity after a successful serve."""
|
|
1129
|
+
if not session_id:
|
|
1130
|
+
return
|
|
1131
|
+
try:
|
|
1132
|
+
store = load_affinity_store(affinity_path)
|
|
1133
|
+
updated = record_affinity_binding(
|
|
1134
|
+
store, session_id=session_id, command=served_command
|
|
1135
|
+
)
|
|
1136
|
+
save_affinity_store_atomic(affinity_path, updated)
|
|
1137
|
+
except (OSError, ValueError):
|
|
1138
|
+
return
|
|
1139
|
+
|
|
1140
|
+
|
|
753
1141
|
def _build_argument_parser() -> argparse.ArgumentParser:
|
|
754
1142
|
parser = argparse.ArgumentParser(
|
|
755
1143
|
description="Run a claude invocation through the fallback chain."
|
|
@@ -11,7 +11,7 @@ consumes that ranking to choose try order.
|
|
|
11
11
|
python claude_chain_usage.py
|
|
12
12
|
{"accounts": [
|
|
13
13
|
{"command": "claude", "weekly_remaining_percent": 58.0},
|
|
14
|
-
{"command": "claude-
|
|
14
|
+
{"command": "claude-profile-c", "weekly_remaining_percent": null, "error": "..."}
|
|
15
15
|
]}
|
|
16
16
|
"""
|
|
17
17
|
|