claude-dev-env 1.92.1 → 1.93.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 +6 -2
- package/_shared/CLAUDE.md +2 -1
- package/_shared/advisor/CLAUDE.md +21 -0
- package/_shared/advisor/advisor-protocol.md +142 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
- package/_shared/advisor/scripts/pyproject.toml +3 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
- package/_shared/advisor/scripts/tier_model_ids.py +164 -0
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/precatch-rubric.md +65 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
- package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
- package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
- package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
- package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
- package/agents/CLAUDE.md +3 -2
- package/agents/clasp-deployment-orchestrator.md +2 -2
- package/agents/code-advisor.md +7 -5
- package/agents/code-verifier.md +6 -2
- package/agents/session-advisor.md +27 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
- package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
- package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
- package/bin/CLAUDE.md +1 -0
- package/bin/expand_home_directory_tokens.mjs +75 -0
- package/bin/install.mjs +21 -7
- package/bin/install.test.mjs +220 -10
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +4 -1
- package/hooks/blocking/_path_setup.py +13 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
- package/hooks/blocking/code_rules_enforcer.py +6 -0
- package/hooks/blocking/code_rules_imports_logging.py +34 -1
- package/hooks/blocking/code_rules_paths_syspath.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +272 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
- package/hooks/blocking/pii_prevention_blocker.py +781 -0
- package/hooks/blocking/pii_scanner.py +340 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/stop_dispatcher.py +84 -0
- package/hooks/blocking/test__path_setup.py +39 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
- package/hooks/blocking/test_destructive_command_blocker.py +29 -9
- package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
- package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
- package/hooks/blocking/test_pii_scanner.py +165 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
- package/hooks/blocking/test_stop_dispatcher.py +187 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
- package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
- package/hooks/diagnostic/migrations/README.md +25 -26
- package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
- package/hooks/hooks.json +10 -120
- package/hooks/hooks_constants/CLAUDE.md +12 -3
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
- package/hooks/hooks_constants/local_identity.py +182 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
- package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
- package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
- package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
- package/hooks/hooks_constants/test_layout_constants.py +28 -0
- package/hooks/hooks_constants/test_local_identity.py +88 -0
- package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
- package/hooks/validators/README.md +5 -6
- package/hooks/validators/__init__.py +2 -2
- package/hooks/validators/python_style_checks.py +298 -243
- package/hooks/validators/run_all_validators.py +6 -0
- package/hooks/validators/test_python_style_checks.py +278 -163
- package/package.json +2 -2
- package/rules/CLAUDE.md +2 -1
- package/rules/bdd.md +1 -1
- package/rules/nas-ssh-invocation.md +6 -4
- package/rules/no-justification-noise.md +61 -0
- package/scripts/test_setup_project_paths.py +1 -1
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +113 -35
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +44 -22
- package/skills/autoconverge/reference/gotchas.md +11 -0
- package/skills/autoconverge/reference/stop-conditions.md +23 -13
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
- package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +329 -68
- package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
- package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
- package/skills/bdd-protocol/SKILL.md +4 -5
- package/skills/bdd-protocol/references/anti-patterns.md +1 -1
- package/skills/bugteam/SKILL.md +8 -0
- package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
- package/skills/bugteam/reference/team-setup.md +7 -5
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/copilot-finding-triage/SKILL.md +124 -0
- package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
- package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
- package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
- package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
- package/skills/copilot-finding-triage/templates/notification.md +34 -0
- package/skills/copilot-review/SKILL.md +8 -6
- package/skills/findbugs/SKILL.md +4 -0
- package/skills/fixbugs/SKILL.md +8 -7
- package/skills/gotcha/CLAUDE.md +2 -2
- package/skills/gotcha/SKILL.md +4 -4
- package/skills/log-audit/SKILL.md +4 -6
- package/skills/monitor-open-prs/CLAUDE.md +1 -1
- package/skills/monitor-open-prs/SKILL.md +6 -1
- package/skills/orchestrator/SKILL.md +74 -78
- package/skills/orchestrator-refresh/SKILL.md +24 -12
- package/skills/post-audit-findings/SKILL.md +5 -9
- package/skills/pr-consistency-audit/SKILL.md +5 -1
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +86 -47
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +20 -17
- package/skills/pr-converge/reference/examples.md +63 -47
- package/skills/pr-converge/reference/fix-protocol.md +15 -15
- package/skills/pr-converge/reference/ground-rules.md +11 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
- package/skills/pr-converge/reference/per-tick.md +129 -107
- package/skills/pr-converge/reference/state-schema.md +15 -10
- package/skills/pr-converge/scripts/check_convergence.py +5 -4
- package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
- package/skills/pr-fix-protocol/SKILL.md +3 -8
- package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
- package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
- package/skills/pr-loop-lifecycle/SKILL.md +5 -9
- package/skills/pr-scope-resolve/SKILL.md +3 -6
- package/skills/privacy-hygiene/SKILL.md +114 -0
- package/skills/qbug/SKILL.md +8 -8
- package/skills/rebase/SKILL.md +5 -1
- package/skills/refine/SKILL.md +4 -5
- package/skills/reviewer-gates/SKILL.md +7 -11
- package/skills/session-log/SKILL.md +4 -1
- package/skills/skill-builder/SKILL.md +3 -6
- package/skills/structure-prompt/SKILL.md +4 -5
- package/skills/team-advisor/SKILL.md +56 -0
- package/skills/test_markdown_link_integrity.py +10 -6
- package/skills/update/SKILL.md +5 -1
- package/skills/usage-pause/SKILL.md +14 -5
- package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
- package/skills/verified-build/SKILL.md +4 -9
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Behavioral tests for the model-tier spawn-walk log validator."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _load_validator_module() -> ModuleType:
|
|
13
|
+
scripts_root = Path(__file__).parent.parent
|
|
14
|
+
constants_root = scripts_root / "config"
|
|
15
|
+
sys.path.insert(0, str(constants_root))
|
|
16
|
+
module_path = scripts_root / "model_tier_run_validator.py"
|
|
17
|
+
specification = importlib.util.spec_from_file_location(
|
|
18
|
+
"model_tier_run_validator", module_path
|
|
19
|
+
)
|
|
20
|
+
assert specification is not None
|
|
21
|
+
assert specification.loader is not None
|
|
22
|
+
module = importlib.util.module_from_spec(specification)
|
|
23
|
+
sys.modules[specification.name] = module
|
|
24
|
+
specification.loader.exec_module(module)
|
|
25
|
+
return module
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
model_tier_run_validator = _load_validator_module()
|
|
29
|
+
ModelTierRun = model_tier_run_validator.ModelTierRun
|
|
30
|
+
ModelTierRunError = model_tier_run_validator.ModelTierRunError
|
|
31
|
+
validate_model_tier_run = model_tier_run_validator.validate_model_tier_run
|
|
32
|
+
main = model_tier_run_validator.main
|
|
33
|
+
load_model_tier_run_from_json_path = (
|
|
34
|
+
model_tier_run_validator.load_model_tier_run_from_json_path
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_clean_single_spawn_at_top_of_slice_passes() -> None:
|
|
39
|
+
run = ModelTierRun(
|
|
40
|
+
own_tier="Opus",
|
|
41
|
+
candidate_tiers=["Fable", "Opus"],
|
|
42
|
+
attempts=[{"tier": "Fable", "result": "spawned"}],
|
|
43
|
+
selected_tier="Fable",
|
|
44
|
+
)
|
|
45
|
+
assert validate_model_tier_run(run) is None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_fallthrough_to_floor_tier_passes() -> None:
|
|
49
|
+
run = ModelTierRun(
|
|
50
|
+
own_tier="Opus",
|
|
51
|
+
candidate_tiers=["Fable", "Opus"],
|
|
52
|
+
attempts=[
|
|
53
|
+
{"tier": "Fable", "result": "unavailable"},
|
|
54
|
+
{"tier": "Opus", "result": "spawned"},
|
|
55
|
+
],
|
|
56
|
+
selected_tier="Opus",
|
|
57
|
+
)
|
|
58
|
+
assert validate_model_tier_run(run) is None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_fully_exhausted_walk_with_fallback_reason_passes() -> None:
|
|
62
|
+
run = ModelTierRun(
|
|
63
|
+
own_tier="Opus",
|
|
64
|
+
candidate_tiers=["Fable", "Opus"],
|
|
65
|
+
attempts=[
|
|
66
|
+
{"tier": "Fable", "result": "unavailable"},
|
|
67
|
+
{"tier": "Opus", "result": "unavailable"},
|
|
68
|
+
],
|
|
69
|
+
selected_tier=None,
|
|
70
|
+
fallback_reason="every candidate tier failed; CLI fallback took over",
|
|
71
|
+
)
|
|
72
|
+
assert validate_model_tier_run(run) is None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_candidate_tiers_shorter_than_ladder_slice_raises() -> None:
|
|
76
|
+
run = ModelTierRun(
|
|
77
|
+
own_tier="Opus",
|
|
78
|
+
candidate_tiers=["Fable"],
|
|
79
|
+
attempts=[{"tier": "Fable", "result": "spawned"}],
|
|
80
|
+
selected_tier="Fable",
|
|
81
|
+
)
|
|
82
|
+
with pytest.raises(ModelTierRunError):
|
|
83
|
+
validate_model_tier_run(run)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_attempt_tier_outside_candidate_slice_raises() -> None:
|
|
87
|
+
run = ModelTierRun(
|
|
88
|
+
own_tier="Opus",
|
|
89
|
+
candidate_tiers=["Fable", "Opus"],
|
|
90
|
+
attempts=[{"tier": "Haiku", "result": "spawned"}],
|
|
91
|
+
selected_tier="Haiku",
|
|
92
|
+
)
|
|
93
|
+
with pytest.raises(ModelTierRunError):
|
|
94
|
+
validate_model_tier_run(run)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_attempts_out_of_ladder_order_raises() -> None:
|
|
98
|
+
run = ModelTierRun(
|
|
99
|
+
own_tier="Opus",
|
|
100
|
+
candidate_tiers=["Fable", "Opus"],
|
|
101
|
+
attempts=[
|
|
102
|
+
{"tier": "Opus", "result": "unavailable"},
|
|
103
|
+
{"tier": "Fable", "result": "spawned"},
|
|
104
|
+
],
|
|
105
|
+
selected_tier="Fable",
|
|
106
|
+
)
|
|
107
|
+
with pytest.raises(ModelTierRunError):
|
|
108
|
+
validate_model_tier_run(run)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_selected_tier_not_first_spawned_attempt_raises() -> None:
|
|
112
|
+
run = ModelTierRun(
|
|
113
|
+
own_tier="Opus",
|
|
114
|
+
candidate_tiers=["Fable", "Opus"],
|
|
115
|
+
attempts=[
|
|
116
|
+
{"tier": "Fable", "result": "unavailable"},
|
|
117
|
+
{"tier": "Opus", "result": "spawned"},
|
|
118
|
+
],
|
|
119
|
+
selected_tier="Fable",
|
|
120
|
+
)
|
|
121
|
+
with pytest.raises(ModelTierRunError):
|
|
122
|
+
validate_model_tier_run(run)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_exhausted_walk_with_non_null_selected_tier_raises() -> None:
|
|
126
|
+
run = ModelTierRun(
|
|
127
|
+
own_tier="Opus",
|
|
128
|
+
candidate_tiers=["Fable", "Opus"],
|
|
129
|
+
attempts=[
|
|
130
|
+
{"tier": "Fable", "result": "unavailable"},
|
|
131
|
+
{"tier": "Opus", "result": "unavailable"},
|
|
132
|
+
],
|
|
133
|
+
selected_tier="Opus",
|
|
134
|
+
fallback_reason="every candidate tier failed",
|
|
135
|
+
)
|
|
136
|
+
with pytest.raises(ModelTierRunError):
|
|
137
|
+
validate_model_tier_run(run)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_exhausted_walk_missing_fallback_reason_raises() -> None:
|
|
141
|
+
run = ModelTierRun(
|
|
142
|
+
own_tier="Opus",
|
|
143
|
+
candidate_tiers=["Fable", "Opus"],
|
|
144
|
+
attempts=[
|
|
145
|
+
{"tier": "Fable", "result": "unavailable"},
|
|
146
|
+
{"tier": "Opus", "result": "unavailable"},
|
|
147
|
+
],
|
|
148
|
+
selected_tier=None,
|
|
149
|
+
)
|
|
150
|
+
with pytest.raises(ModelTierRunError):
|
|
151
|
+
validate_model_tier_run(run)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_unknown_own_tier_raises() -> None:
|
|
155
|
+
run = ModelTierRun(
|
|
156
|
+
own_tier="Titan",
|
|
157
|
+
candidate_tiers=["Titan"],
|
|
158
|
+
attempts=[{"tier": "Titan", "result": "spawned"}],
|
|
159
|
+
selected_tier="Titan",
|
|
160
|
+
)
|
|
161
|
+
with pytest.raises(ModelTierRunError):
|
|
162
|
+
validate_model_tier_run(run)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_empty_attempts_with_null_selected_tier_raises() -> None:
|
|
166
|
+
run = ModelTierRun(
|
|
167
|
+
own_tier="Opus",
|
|
168
|
+
candidate_tiers=["Fable", "Opus"],
|
|
169
|
+
attempts=[],
|
|
170
|
+
selected_tier=None,
|
|
171
|
+
fallback_reason="skipped straight to CLI fallback",
|
|
172
|
+
)
|
|
173
|
+
with pytest.raises(ModelTierRunError):
|
|
174
|
+
validate_model_tier_run(run)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_incomplete_fallback_walk_before_floor_raises() -> None:
|
|
178
|
+
run = ModelTierRun(
|
|
179
|
+
own_tier="Opus",
|
|
180
|
+
candidate_tiers=["Fable", "Opus"],
|
|
181
|
+
attempts=[{"tier": "Fable", "result": "unavailable"}],
|
|
182
|
+
selected_tier=None,
|
|
183
|
+
fallback_reason="stopped after Fable without trying Opus",
|
|
184
|
+
)
|
|
185
|
+
with pytest.raises(ModelTierRunError):
|
|
186
|
+
validate_model_tier_run(run)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_lowercase_own_tier_and_candidates_pass() -> None:
|
|
190
|
+
run = ModelTierRun(
|
|
191
|
+
own_tier="opus",
|
|
192
|
+
candidate_tiers=["fable", "opus"],
|
|
193
|
+
attempts=[
|
|
194
|
+
{"tier": "fable", "result": "unavailable"},
|
|
195
|
+
{"tier": "opus", "result": "spawned"},
|
|
196
|
+
],
|
|
197
|
+
selected_tier="opus",
|
|
198
|
+
)
|
|
199
|
+
assert validate_model_tier_run(run) is None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_cli_validates_json_log_file(tmp_path: Path) -> None:
|
|
203
|
+
log_path = tmp_path / "model-tier-run.json"
|
|
204
|
+
log_path.write_text(
|
|
205
|
+
json.dumps(
|
|
206
|
+
{
|
|
207
|
+
"own_tier": "Opus",
|
|
208
|
+
"candidate_tiers": ["Fable", "Opus"],
|
|
209
|
+
"attempts": [{"tier": "Fable", "result": "spawned"}],
|
|
210
|
+
"selected_tier": "Fable",
|
|
211
|
+
}
|
|
212
|
+
),
|
|
213
|
+
encoding="utf-8",
|
|
214
|
+
)
|
|
215
|
+
assert main([str(log_path)]) == 0
|
|
216
|
+
loaded_run = load_model_tier_run_from_json_path(from_path=log_path)
|
|
217
|
+
assert loaded_run.selected_tier == "Fable"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_cli_rejects_incomplete_fallback_log(tmp_path: Path) -> None:
|
|
221
|
+
log_path = tmp_path / "incomplete-walk.json"
|
|
222
|
+
log_path.write_text(
|
|
223
|
+
json.dumps(
|
|
224
|
+
{
|
|
225
|
+
"own_tier": "Opus",
|
|
226
|
+
"candidate_tiers": ["Fable", "Opus"],
|
|
227
|
+
"attempts": [{"tier": "Fable", "result": "unavailable"}],
|
|
228
|
+
"selected_tier": None,
|
|
229
|
+
"fallback_reason": "incomplete",
|
|
230
|
+
}
|
|
231
|
+
),
|
|
232
|
+
encoding="utf-8",
|
|
233
|
+
)
|
|
234
|
+
assert main([str(log_path)]) == 1
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_cli_missing_path_returns_usage_exit_code() -> None:
|
|
238
|
+
assert main([]) == 2
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_grok_host_single_tier_self_bind_passes() -> None:
|
|
242
|
+
run = ModelTierRun(
|
|
243
|
+
own_tier="Grok",
|
|
244
|
+
candidate_tiers=["Grok"],
|
|
245
|
+
attempts=[{"tier": "Grok", "result": "self"}],
|
|
246
|
+
selected_tier="Grok",
|
|
247
|
+
)
|
|
248
|
+
assert validate_model_tier_run(run) is None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_grok_host_lowercase_self_bind_passes() -> None:
|
|
252
|
+
run = ModelTierRun(
|
|
253
|
+
own_tier="grok",
|
|
254
|
+
candidate_tiers=["grok"],
|
|
255
|
+
attempts=[{"tier": "grok", "result": "self"}],
|
|
256
|
+
selected_tier="grok",
|
|
257
|
+
)
|
|
258
|
+
assert validate_model_tier_run(run) is None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_grok_host_multi_tier_candidate_list_raises() -> None:
|
|
262
|
+
run = ModelTierRun(
|
|
263
|
+
own_tier="Grok",
|
|
264
|
+
candidate_tiers=["Fable", "Opus", "Grok"],
|
|
265
|
+
attempts=[{"tier": "Grok", "result": "self"}],
|
|
266
|
+
selected_tier="Grok",
|
|
267
|
+
)
|
|
268
|
+
with pytest.raises(ModelTierRunError):
|
|
269
|
+
validate_model_tier_run(run)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_grok_host_spawned_token_is_not_self_bind_raises() -> None:
|
|
273
|
+
run = ModelTierRun(
|
|
274
|
+
own_tier="Grok",
|
|
275
|
+
candidate_tiers=["Grok"],
|
|
276
|
+
attempts=[{"tier": "Grok", "result": "spawned"}],
|
|
277
|
+
selected_tier="Grok",
|
|
278
|
+
)
|
|
279
|
+
with pytest.raises(ModelTierRunError):
|
|
280
|
+
validate_model_tier_run(run)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_grok_host_selected_tier_mismatch_raises() -> None:
|
|
284
|
+
run = ModelTierRun(
|
|
285
|
+
own_tier="Grok",
|
|
286
|
+
candidate_tiers=["Grok"],
|
|
287
|
+
attempts=[{"tier": "Grok", "result": "self"}],
|
|
288
|
+
selected_tier="Opus",
|
|
289
|
+
)
|
|
290
|
+
with pytest.raises(ModelTierRunError):
|
|
291
|
+
validate_model_tier_run(run)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_claude_host_self_token_is_not_spawn_success_raises() -> None:
|
|
295
|
+
run = ModelTierRun(
|
|
296
|
+
own_tier="Opus",
|
|
297
|
+
candidate_tiers=["Fable", "Opus"],
|
|
298
|
+
attempts=[{"tier": "Fable", "result": "self"}],
|
|
299
|
+
selected_tier="Fable",
|
|
300
|
+
)
|
|
301
|
+
with pytest.raises(ModelTierRunError):
|
|
302
|
+
validate_model_tier_run(run)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Behavioral tests for ladder-tier to CLI model-alias resolution."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load_tier_model_ids_module() -> ModuleType:
|
|
14
|
+
scripts_root = Path(__file__).parent.parent
|
|
15
|
+
module_path = scripts_root / "tier_model_ids.py"
|
|
16
|
+
specification = importlib.util.spec_from_file_location(
|
|
17
|
+
"tier_model_ids", module_path
|
|
18
|
+
)
|
|
19
|
+
assert specification is not None
|
|
20
|
+
assert specification.loader is not None
|
|
21
|
+
module = importlib.util.module_from_spec(specification)
|
|
22
|
+
sys.modules[specification.name] = module
|
|
23
|
+
specification.loader.exec_module(module)
|
|
24
|
+
return module
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
tier_model_ids = _load_tier_model_ids_module()
|
|
28
|
+
resolve_cli_model_id = tier_model_ids.resolve_cli_model_id
|
|
29
|
+
canonical_tier_name = tier_model_ids.canonical_tier_name
|
|
30
|
+
detect_host_profile = tier_model_ids.detect_host_profile
|
|
31
|
+
constants_root = Path(__file__).parent.parent / "config"
|
|
32
|
+
if str(constants_root) not in sys.path:
|
|
33
|
+
sys.path.insert(0, str(constants_root))
|
|
34
|
+
|
|
35
|
+
from advisor_scripts_constants.model_tier_run_validator_constants import ( # noqa: E402
|
|
36
|
+
ADVISOR_SENDMESSAGE_REPLY_WAIT_SECONDS,
|
|
37
|
+
ALL_CLI_MODEL_ID_BY_TIER,
|
|
38
|
+
ALL_KNOWN_TIER_NAMES,
|
|
39
|
+
ALL_MODEL_TIERS,
|
|
40
|
+
GROK_MODEL_TIER,
|
|
41
|
+
HOST_PROFILE_CLAUDE,
|
|
42
|
+
HOST_PROFILE_GROK,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
SCRIPTS_ROOT = Path(__file__).parent.parent
|
|
46
|
+
DOCUMENTED_RESOLVE_ONE_LINER = (
|
|
47
|
+
"from tier_model_ids import resolve_cli_model_id; "
|
|
48
|
+
"print(resolve_cli_model_id('Opus'))"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.parametrize(
|
|
53
|
+
("tier_name", "expected_model_alias"),
|
|
54
|
+
[
|
|
55
|
+
("Fable", "fable"),
|
|
56
|
+
("Opus", "opus"),
|
|
57
|
+
("Sonnet", "sonnet"),
|
|
58
|
+
("Haiku", "haiku"),
|
|
59
|
+
("Grok", "grok"),
|
|
60
|
+
("fable", "fable"),
|
|
61
|
+
("OPUS", "opus"),
|
|
62
|
+
("sonnet", "sonnet"),
|
|
63
|
+
("hAiKu", "haiku"),
|
|
64
|
+
("grok", "grok"),
|
|
65
|
+
(" GROK ", "grok"),
|
|
66
|
+
(" Opus ", "opus"),
|
|
67
|
+
("\thaiku\n", "haiku"),
|
|
68
|
+
],
|
|
69
|
+
)
|
|
70
|
+
def test_resolve_cli_model_id_maps_known_tiers(
|
|
71
|
+
tier_name: str,
|
|
72
|
+
expected_model_alias: str,
|
|
73
|
+
) -> None:
|
|
74
|
+
assert resolve_cli_model_id(tier_name) == expected_model_alias
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_resolve_cli_model_id_rejects_unknown_tier() -> None:
|
|
78
|
+
with pytest.raises(ValueError, match="not a known model tier"):
|
|
79
|
+
resolve_cli_model_id("Titan")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_resolve_cli_model_id_rejects_empty_string() -> None:
|
|
83
|
+
with pytest.raises(ValueError, match="not a known model tier"):
|
|
84
|
+
resolve_cli_model_id("")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_resolve_cli_model_id_rejects_whitespace_only() -> None:
|
|
88
|
+
with pytest.raises(ValueError, match="not a known model tier"):
|
|
89
|
+
resolve_cli_model_id(" ")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_sendmessage_reply_wait_is_positive_bound() -> None:
|
|
93
|
+
assert ADVISOR_SENDMESSAGE_REPLY_WAIT_SECONDS > 0
|
|
94
|
+
assert ADVISOR_SENDMESSAGE_REPLY_WAIT_SECONDS == 120
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_cli_model_alias_map_keys_match_known_tiers() -> None:
|
|
98
|
+
assert set(ALL_CLI_MODEL_ID_BY_TIER) == set(ALL_KNOWN_TIER_NAMES)
|
|
99
|
+
assert set(ALL_MODEL_TIERS).issubset(set(ALL_KNOWN_TIER_NAMES))
|
|
100
|
+
assert GROK_MODEL_TIER in ALL_KNOWN_TIER_NAMES
|
|
101
|
+
assert GROK_MODEL_TIER not in ALL_MODEL_TIERS
|
|
102
|
+
assert all(
|
|
103
|
+
ALL_CLI_MODEL_ID_BY_TIER[each_tier] for each_tier in ALL_KNOWN_TIER_NAMES
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_canonical_tier_name_strips_and_normalizes() -> None:
|
|
108
|
+
assert canonical_tier_name(" opus ") == "Opus"
|
|
109
|
+
assert canonical_tier_name("grok") == "Grok"
|
|
110
|
+
assert canonical_tier_name("") is None
|
|
111
|
+
assert canonical_tier_name("Titan") is None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_detect_host_profile_defaults_to_claude() -> None:
|
|
115
|
+
assert detect_host_profile(setting_by_name={}) == HOST_PROFILE_CLAUDE
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.mark.parametrize(
|
|
119
|
+
"truthy_value",
|
|
120
|
+
["1", "true", "TRUE", "yes", "YES", "on", "On"],
|
|
121
|
+
)
|
|
122
|
+
def test_detect_host_profile_reads_grok_build_truthy_values(
|
|
123
|
+
truthy_value: str,
|
|
124
|
+
) -> None:
|
|
125
|
+
assert (
|
|
126
|
+
detect_host_profile(setting_by_name={"GROK_BUILD": truthy_value})
|
|
127
|
+
== HOST_PROFILE_GROK
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_detect_host_profile_reads_grok_build_flag() -> None:
|
|
132
|
+
assert (
|
|
133
|
+
detect_host_profile(setting_by_name={"GROK_BUILD": "0"})
|
|
134
|
+
== HOST_PROFILE_CLAUDE
|
|
135
|
+
)
|
|
136
|
+
assert (
|
|
137
|
+
detect_host_profile(setting_by_name={"GROK_BUILD": "false"})
|
|
138
|
+
== HOST_PROFILE_CLAUDE
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_detect_host_profile_reads_explicit_override() -> None:
|
|
143
|
+
assert (
|
|
144
|
+
detect_host_profile(setting_by_name={"ADVISOR_HOST_PROFILE": "Grok"})
|
|
145
|
+
== HOST_PROFILE_GROK
|
|
146
|
+
)
|
|
147
|
+
assert (
|
|
148
|
+
detect_host_profile(setting_by_name={"ADVISOR_HOST_PROFILE": "claude"})
|
|
149
|
+
== HOST_PROFILE_CLAUDE
|
|
150
|
+
)
|
|
151
|
+
assert (
|
|
152
|
+
detect_host_profile(
|
|
153
|
+
setting_by_name={"ADVISOR_HOST_PROFILE": "Claude", "GROK_BUILD": "1"}
|
|
154
|
+
)
|
|
155
|
+
== HOST_PROFILE_CLAUDE
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_detect_host_profile_rejects_unknown_explicit_value() -> None:
|
|
160
|
+
with pytest.raises(ValueError, match="not a known profile"):
|
|
161
|
+
detect_host_profile(setting_by_name={"ADVISOR_HOST_PROFILE": "Titan"})
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_documented_resolve_one_liner_runs_without_prior_path_pollution() -> None:
|
|
165
|
+
clean_environment = {
|
|
166
|
+
each_key: each_value
|
|
167
|
+
for each_key, each_value in os.environ.items()
|
|
168
|
+
if each_key.upper() != "PYTHONPATH"
|
|
169
|
+
}
|
|
170
|
+
clean_environment["PYTHONPATH"] = ""
|
|
171
|
+
completed_process = subprocess.run(
|
|
172
|
+
[sys.executable, "-c", DOCUMENTED_RESOLVE_ONE_LINER],
|
|
173
|
+
cwd=str(SCRIPTS_ROOT),
|
|
174
|
+
capture_output=True,
|
|
175
|
+
text=True,
|
|
176
|
+
env=clean_environment,
|
|
177
|
+
check=False,
|
|
178
|
+
)
|
|
179
|
+
assert completed_process.returncode == 0, completed_process.stderr
|
|
180
|
+
assert completed_process.stdout.strip() == "opus"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Map ladder tier names to Claude Code CLI / Agent short model aliases.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
resolve_cli_model_id("Opus") # ok: "opus"
|
|
6
|
+
resolve_cli_model_id("fable") # ok: "fable" (any letter case)
|
|
7
|
+
resolve_cli_model_id("Grok") # ok: "grok" (Grok host single tier)
|
|
8
|
+
resolve_cli_model_id(" Opus ") # ok: "opus" (leading/trailing whitespace)
|
|
9
|
+
resolve_cli_model_id("Titan") # flag: ValueError
|
|
10
|
+
|
|
11
|
+
detect_host_profile(setting_by_name={"GROK_BUILD": "1"}) # ok: "Grok"
|
|
12
|
+
detect_host_profile(
|
|
13
|
+
setting_by_name={"ADVISOR_HOST_PROFILE": "Claude"}
|
|
14
|
+
) # ok: "Claude"
|
|
15
|
+
|
|
16
|
+
Values are stable short aliases (``opus``, ``sonnet``, ``grok``), not dated
|
|
17
|
+
full model IDs such as ``claude-opus-4-…``. The map lives in
|
|
18
|
+
``advisor_scripts_constants`` so protocol text, skills, and tests share one
|
|
19
|
+
source of truth for the aliases the Agent tool ``model:`` field and CLI
|
|
20
|
+
``--model`` flag already accept.
|
|
21
|
+
|
|
22
|
+
Args and returns for the public helpers are documented on each function.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
from collections.abc import Mapping
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
_config_directory = str(Path(__file__).resolve().parent / "config")
|
|
33
|
+
if _config_directory not in sys.path:
|
|
34
|
+
sys.path.insert(0, _config_directory)
|
|
35
|
+
|
|
36
|
+
from advisor_scripts_constants.model_tier_run_validator_constants import ( # noqa: E402
|
|
37
|
+
ALL_CLI_MODEL_ID_BY_TIER,
|
|
38
|
+
ALL_GROK_BUILD_TRUTHY_VALUES,
|
|
39
|
+
ALL_HOST_PROFILES,
|
|
40
|
+
ALL_KNOWN_TIER_NAMES,
|
|
41
|
+
GROK_BUILD_ENV_VAR,
|
|
42
|
+
HOST_PROFILE_CLAUDE,
|
|
43
|
+
HOST_PROFILE_ENV_VAR,
|
|
44
|
+
HOST_PROFILE_GROK,
|
|
45
|
+
UNKNOWN_HOST_PROFILE_ERROR,
|
|
46
|
+
UNKNOWN_LADDER_NAME_ERROR,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def canonical_tier_name(tier_name: str) -> str | None:
|
|
51
|
+
"""Return the Title Case ladder name for ``tier_name``, or ``None``.
|
|
52
|
+
|
|
53
|
+
::
|
|
54
|
+
|
|
55
|
+
canonical_tier_name("opus") # ok: "Opus"
|
|
56
|
+
canonical_tier_name("grok") # ok: "Grok"
|
|
57
|
+
canonical_tier_name(" Opus ") # ok: "Opus"
|
|
58
|
+
canonical_tier_name("") # ok: None
|
|
59
|
+
canonical_tier_name("Titan") # ok: None
|
|
60
|
+
|
|
61
|
+
Strips leading and trailing whitespace, then matches any letter case
|
|
62
|
+
against ``ALL_KNOWN_TIER_NAMES`` (Claude ladder plus the Grok host tier).
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
tier_name: Raw tier text from a spawn log, protocol walk, or caller.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The canonical Title Case ladder name, or ``None`` when unknown.
|
|
69
|
+
"""
|
|
70
|
+
stripped_tier_name = tier_name.strip()
|
|
71
|
+
if not stripped_tier_name:
|
|
72
|
+
return None
|
|
73
|
+
tier_by_lower_name = {
|
|
74
|
+
each_tier.lower(): each_tier for each_tier in ALL_KNOWN_TIER_NAMES
|
|
75
|
+
}
|
|
76
|
+
return tier_by_lower_name.get(stripped_tier_name.lower())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def resolve_cli_model_id(tier: str) -> str:
|
|
80
|
+
"""Return the CLI / Agent short model alias for a ladder tier name.
|
|
81
|
+
|
|
82
|
+
::
|
|
83
|
+
|
|
84
|
+
resolve_cli_model_id("Sonnet") # ok: "sonnet"
|
|
85
|
+
resolve_cli_model_id("HAIKU") # ok: "haiku"
|
|
86
|
+
resolve_cli_model_id("Grok") # ok: "grok"
|
|
87
|
+
resolve_cli_model_id(" Opus ") # ok: "opus"
|
|
88
|
+
resolve_cli_model_id("Titan") # flag: ValueError
|
|
89
|
+
|
|
90
|
+
Accepts any letter case and surrounding whitespace. The returned string is
|
|
91
|
+
the short alias used by the Agent tool ``model:`` field and the CLI
|
|
92
|
+
``--model`` flag — not a dated full model ID.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
tier: Ladder tier name (``Fable``, ``Opus``, ``Sonnet``, ``Haiku``,
|
|
96
|
+
or ``Grok``).
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
The short model alias for that tier (for example ``"opus"``).
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ValueError: When ``tier`` is not a known ladder tier, or the alias map
|
|
103
|
+
is missing that ladder name.
|
|
104
|
+
"""
|
|
105
|
+
maybe_canonical_tier = canonical_tier_name(tier)
|
|
106
|
+
if maybe_canonical_tier is None:
|
|
107
|
+
raise ValueError(UNKNOWN_LADDER_NAME_ERROR.format(tier))
|
|
108
|
+
maybe_model_alias = ALL_CLI_MODEL_ID_BY_TIER.get(maybe_canonical_tier)
|
|
109
|
+
if maybe_model_alias is None:
|
|
110
|
+
raise ValueError(UNKNOWN_LADDER_NAME_ERROR.format(tier))
|
|
111
|
+
return maybe_model_alias
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def detect_host_profile(
|
|
115
|
+
setting_by_name: Mapping[str, str] | None = None,
|
|
116
|
+
) -> str:
|
|
117
|
+
"""Return the advisor host profile from environment detection hints.
|
|
118
|
+
|
|
119
|
+
::
|
|
120
|
+
|
|
121
|
+
detect_host_profile(setting_by_name={"ADVISOR_HOST_PROFILE": "Grok"})
|
|
122
|
+
# ok: "Grok"
|
|
123
|
+
detect_host_profile(setting_by_name={"GROK_BUILD": "1"}) # ok: "Grok"
|
|
124
|
+
detect_host_profile(setting_by_name={}) # ok: "Claude"
|
|
125
|
+
detect_host_profile(setting_by_name={"ADVISOR_HOST_PROFILE": "X"})
|
|
126
|
+
# flag: ValueError
|
|
127
|
+
|
|
128
|
+
Order:
|
|
129
|
+
|
|
130
|
+
1. ``ADVISOR_HOST_PROFILE`` when set (must be a known profile name).
|
|
131
|
+
2. ``GROK_BUILD`` when truthy (``1``, ``true``, ``yes``, ``on``).
|
|
132
|
+
3. Default ``Claude``.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
setting_by_name: Env name → setting text (defaults to ``os.environ``).
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
``HOST_PROFILE_GROK`` or ``HOST_PROFILE_CLAUDE``.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: When ``ADVISOR_HOST_PROFILE`` is set to an unknown name.
|
|
142
|
+
"""
|
|
143
|
+
resolved_setting_by_name: Mapping[str, str] = (
|
|
144
|
+
os.environ if setting_by_name is None else setting_by_name
|
|
145
|
+
)
|
|
146
|
+
explicit_host_profile = resolved_setting_by_name.get(
|
|
147
|
+
HOST_PROFILE_ENV_VAR, ""
|
|
148
|
+
).strip()
|
|
149
|
+
if explicit_host_profile:
|
|
150
|
+
host_profile_by_lower_name = {
|
|
151
|
+
each_profile.lower(): each_profile for each_profile in ALL_HOST_PROFILES
|
|
152
|
+
}
|
|
153
|
+
maybe_canonical_host = host_profile_by_lower_name.get(
|
|
154
|
+
explicit_host_profile.lower()
|
|
155
|
+
)
|
|
156
|
+
if maybe_canonical_host is None:
|
|
157
|
+
raise ValueError(UNKNOWN_HOST_PROFILE_ERROR.format(explicit_host_profile))
|
|
158
|
+
return maybe_canonical_host
|
|
159
|
+
raw_harness_marker = resolved_setting_by_name.get(
|
|
160
|
+
GROK_BUILD_ENV_VAR, ""
|
|
161
|
+
).strip().lower()
|
|
162
|
+
if raw_harness_marker in ALL_GROK_BUILD_TRUTHY_VALUES:
|
|
163
|
+
return HOST_PROFILE_GROK
|
|
164
|
+
return HOST_PROFILE_CLAUDE
|
|
@@ -12,6 +12,7 @@ Runtime documents and scripts shared by every PR-loop skill. Changes here affect
|
|
|
12
12
|
| `gh-payloads.md` | How to build GitHub review and reply payloads via MCP tools; describes the one-review-per-loop pattern |
|
|
13
13
|
| `state-schema.md` | Fields each PR-loop workflow tracks across iterations; documents common fields and per-skill extensions |
|
|
14
14
|
| `code-rules-gate.md` | Reference for the CODE_RULES pre-commit gate check; describes what the gate blocks and when it runs |
|
|
15
|
+
| `precatch-rubric.md` | Shared pre-catch lane checklist — deterministic sweep, doc-vs-code parity, test-assertion completeness, PR-description parity, adversarial audit — that autoconverge's lenses and pr-converge's CODE_REVIEW step read on demand |
|
|
15
16
|
|
|
16
17
|
## Subdirectory
|
|
17
18
|
|