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.
Files changed (318) hide show
  1. package/CLAUDE.md +2 -2
  2. package/_shared/advisor/CLAUDE.md +3 -2
  3. package/_shared/advisor/advisor-protocol.md +74 -108
  4. package/_shared/advisor/reference/advisor-block.md +37 -0
  5. package/_shared/advisor/reference/cli-chain.md +45 -0
  6. package/_shared/advisor/reference/consult-format.md +41 -0
  7. package/_shared/advisor/reference/lifecycle.md +21 -0
  8. package/_shared/advisor/reference/sol-rung.md +31 -0
  9. package/_shared/advisor/reference/spawn-walk-log.md +31 -0
  10. package/_shared/advisor/reference/third-party-bind.md +30 -0
  11. package/_shared/advisor/reference/warm-up.md +33 -0
  12. package/_shared/advisor/scripts/codex_sol_advisor.py +449 -0
  13. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +21 -0
  14. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +19 -17
  15. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +28 -0
  16. package/_shared/advisor/scripts/model_tier_run_validator.py +32 -9
  17. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +474 -0
  18. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +79 -0
  19. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +39 -17
  20. package/_shared/advisor/scripts/tier_model_ids.py +24 -0
  21. package/_shared/pr-loop/CLAUDE.md +1 -1
  22. package/_shared/pr-loop/audit-contract.md +17 -6
  23. package/_shared/pr-loop/audit-reply-template.md +4 -4
  24. package/_shared/pr-loop/code-rules-gate.md +3 -5
  25. package/_shared/pr-loop/fix-protocol.md +2 -3
  26. package/_shared/pr-loop/gh-payloads.md +1 -1
  27. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  28. package/_shared/pr-loop/scripts/README.md +1 -1
  29. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  32. package/_shared/pr-loop/scripts/preflight.py +9 -4
  33. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  34. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  35. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  36. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  37. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  38. package/_shared/pr-loop/state-schema.md +5 -14
  39. package/agents/CLAUDE.md +2 -2
  40. package/agents/clean-coder.md +58 -548
  41. package/agents/code-quality-agent.md +10 -2
  42. package/agents/code-verifier.md +1 -1
  43. package/agents/test_agent_frontmatter.py +32 -40
  44. package/audit-rubrics/CLAUDE.md +2 -1
  45. package/audit-rubrics/audit-categories.json +704 -0
  46. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  47. package/bin/CLAUDE.md +16 -5
  48. package/bin/ever-shipped-skills.mjs +2 -0
  49. package/bin/install-plan.mjs +402 -0
  50. package/bin/install-transaction.mjs +455 -0
  51. package/bin/install.mjs +593 -147
  52. package/bin/install.plan.test.mjs +194 -0
  53. package/bin/install.profile-root.test.mjs +154 -0
  54. package/bin/install.profiles.test.mjs +253 -0
  55. package/bin/install.settings-defaults.test.mjs +200 -0
  56. package/bin/install.transaction.test.mjs +400 -0
  57. package/bin/install.uninstall-transaction.test.mjs +418 -0
  58. package/bin/merge_managed_permissions.mjs +130 -0
  59. package/bin/resolve-install-root.mjs +181 -0
  60. package/bin/select-install-targets.mjs +401 -0
  61. package/commands/CLAUDE.md +0 -2
  62. package/docs/references/CLAUDE.md +3 -1
  63. package/docs/references/advisor-tool.md +25 -7
  64. package/docs/references/prose-style-enforcement.md +25 -0
  65. package/docs/references/team-advisor-skill.md +3 -3
  66. package/docs/references/weak-executor-advisor.md +91 -0
  67. package/hooks/blocking/CLAUDE.md +6 -6
  68. package/hooks/blocking/_path_setup.py +9 -5
  69. package/hooks/blocking/code_rules_docstrings.py +124 -30
  70. package/hooks/blocking/code_rules_enforcer.py +161 -16
  71. package/hooks/blocking/code_rules_shared.py +40 -23
  72. package/hooks/blocking/config/CLAUDE.md +3 -5
  73. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  74. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  75. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  76. package/hooks/blocking/hedging_language_blocker.py +103 -20
  77. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  78. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  79. package/hooks/blocking/plain_language_blocker.py +139 -20
  80. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  81. package/hooks/blocking/state_description_blocker.py +7 -1
  82. package/hooks/blocking/tdd_enforcer.py +8 -0
  83. package/hooks/blocking/test__path_setup.py +28 -0
  84. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  85. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  86. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  87. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  88. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  89. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  90. package/hooks/blocking/test_fable_spawn_gate.py +18 -11
  91. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  92. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  93. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  94. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  95. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  96. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  97. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  98. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  99. package/hooks/blocking/test_state_description_blocker.py +45 -2
  100. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  101. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  102. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  103. package/hooks/hooks.json +15 -0
  104. package/hooks/hooks_constants/CLAUDE.md +14 -3
  105. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  106. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  107. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  108. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  109. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  110. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  111. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  112. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  113. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  114. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  115. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  116. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  117. package/hooks/hooks_constants/session_start_injector.py +163 -0
  118. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  119. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  120. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  121. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  122. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  123. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  124. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  125. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  126. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  127. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  128. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  129. package/hooks/observability/CLAUDE.md +2 -0
  130. package/hooks/observability/prose_matcher_advisory.py +237 -0
  131. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  132. package/hooks/session/CLAUDE.md +9 -1
  133. package/hooks/session/_path_setup.py +13 -0
  134. package/hooks/session/issue_tracker_session_starter.py +135 -0
  135. package/hooks/session/orchestrator_auto_starter.py +100 -0
  136. package/hooks/session/test__path_setup.py +28 -0
  137. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  138. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  139. package/hooks/session/test_working_style_prompt.py +47 -0
  140. package/hooks/session/untracked_repo_detector.py +1 -24
  141. package/hooks/session/working_style_prompt.py +36 -0
  142. package/hooks/validators/_path_setup.py +19 -0
  143. package/hooks/validators/run_all_validators.py +8 -13
  144. package/installable-surfaces.manifest.json +21 -0
  145. package/output-styles/CLAUDE.md +1 -3
  146. package/package.json +4 -2
  147. package/rules/CLAUDE.md +1 -0
  148. package/rules/durable-post-artifacts.md +2 -2
  149. package/rules/eli11-replies.md +6 -1
  150. package/rules/hedging-claims.md +4 -2
  151. package/rules/long-horizon-autonomy.md +3 -1
  152. package/rules/opus5-communication-contract.md +45 -0
  153. package/rules/plain-language.md +2 -2
  154. package/rules/research-mode.md +1 -1
  155. package/scripts/CLAUDE.md +11 -0
  156. package/scripts/Sync-RepoMain.ps1 +215 -0
  157. package/scripts/active_capability_references.py +218 -0
  158. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  159. package/scripts/claude_chain_runner.py +394 -6
  160. package/scripts/claude_chain_usage.py +1 -1
  161. package/scripts/codex_compat_materializer.py +105 -85
  162. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  163. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  164. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  165. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  166. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  167. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  168. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  169. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  170. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  171. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  172. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  173. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  174. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  175. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  176. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  177. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  178. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  179. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  180. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  181. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  182. package/scripts/spawn_grok_batch.py +3 -0
  183. package/scripts/test_active_capability_references.py +108 -0
  184. package/scripts/test_claude_chain_runner.py +414 -82
  185. package/scripts/test_claude_chain_usage.py +12 -12
  186. package/scripts/test_resolve_worker_spawn.py +2 -2
  187. package/scripts/test_verify_installable_package.py +208 -0
  188. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  189. package/scripts/verify_installable_package.py +612 -0
  190. package/settings.json +10 -0
  191. package/skills/CLAUDE.md +2 -0
  192. package/skills/_shared/advisor/CLAUDE.md +1 -1
  193. package/skills/_shared/advisor/scripts/README.md +2 -0
  194. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  195. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  196. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  197. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  198. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  199. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  200. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  201. package/skills/autoconverge/reference/convergence.md +2 -1
  202. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  203. package/skills/beat-sheet/SKILL.md +54 -0
  204. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  205. package/skills/bugteam/CONSTRAINTS.md +4 -4
  206. package/skills/bugteam/EXAMPLES.md +1 -1
  207. package/skills/bugteam/reference/README.md +1 -1
  208. package/skills/e-code-review/SKILL.md +26 -5
  209. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  210. package/skills/e-code-review/reference/medium.md +15 -4
  211. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  212. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  213. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  214. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  215. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  216. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  217. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  218. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  219. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  220. package/skills/e-code-review/scripts/grok_code_review.py +16 -7
  221. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  222. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  223. package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
  224. package/skills/grokify/SKILL.md +1 -1
  225. package/skills/grokify/templates/handoff-template.md +2 -2
  226. package/skills/orchestrator/SKILL.md +5 -4
  227. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  228. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  229. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  230. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  231. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  232. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  233. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  234. package/skills/pr-converge/scripts/conftest.py +35 -0
  235. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  237. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  238. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  239. package/skills/rebase/SKILL.md +15 -3
  240. package/skills/reviewer-gates/SKILL.md +2 -2
  241. package/skills/show/SKILL.md +51 -0
  242. package/skills/show/references/accessibility.md +7 -0
  243. package/skills/show/references/art.md +3 -0
  244. package/skills/show/references/charts.md +3 -0
  245. package/skills/show/references/core-design.md +14 -0
  246. package/skills/show/references/erds.md +3 -0
  247. package/skills/show/references/flowcharts.md +3 -0
  248. package/skills/show/references/host-and-html.md +3 -0
  249. package/skills/show/references/illustrative-diagrams.md +10 -0
  250. package/skills/show/references/interaction.md +3 -0
  251. package/skills/show/references/mockups.md +3 -0
  252. package/skills/show/references/quality-gates.md +7 -0
  253. package/skills/show/references/structural-diagrams.md +3 -0
  254. package/skills/show/references/subject-inventory.md +21 -0
  255. package/skills/show/references/svg-contract.md +22 -0
  256. package/skills/show/routing.yaml +30 -0
  257. package/skills/show/samples/pr1262-v2.svg +222 -0
  258. package/skills/show/scripts/README.md +6 -0
  259. package/skills/show/scripts/validate-artifact.py +91 -0
  260. package/skills/show/scripts/validate-package.py +18 -0
  261. package/skills/show/templates/html-widget.html +4 -0
  262. package/skills/show/templates/svg-base.svg +19 -0
  263. package/skills/show/tests/fixtures/css-var.svg +6 -0
  264. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  265. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  266. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  267. package/skills/show/tests/fixtures/invalid.svg +1 -0
  268. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  269. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  270. package/skills/show/tests/fixtures/valid.html +1 -0
  271. package/skills/show/tests/test_validate-artifact.py +74 -0
  272. package/skills/show/tests/test_validators.py +59 -0
  273. package/skills/show/workflows/create-visual.md +13 -0
  274. package/skills/show/workflows/review-visual.md +20 -0
  275. package/skills/split-pr/SKILL.md +85 -0
  276. package/skills/split-pr/reference/path-layers.md +16 -0
  277. package/skills/split-pr/reference/proposal-format.md +15 -0
  278. package/skills/split-pr/reference/split-further-loop.md +10 -0
  279. package/skills/split-pr/reference/splitting-principles.md +26 -0
  280. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  281. package/skills/split-pr/scripts/categorize_files.py +106 -0
  282. package/skills/split-pr/scripts/config/__init__.py +1 -0
  283. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  284. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  285. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  286. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  287. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  288. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  289. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  290. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  291. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  292. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  293. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  294. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  295. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  296. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  297. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  298. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  299. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  300. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  301. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  302. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  303. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  304. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  305. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  306. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  307. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  308. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  309. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  310. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  311. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  312. package/skills/split-pr/scripts/verify_plan.py +139 -0
  313. package/skills/team-advisor/SKILL.md +7 -4
  314. package/skills/team-advisor/reference/advisor-docs-review.md +207 -0
  315. package/system-prompts/software-engineer.xml +11 -2
  316. package/commands/initialize.md +0 -90
  317. package/commands/stubcheck.md +0 -88
  318. package/output-styles/caveman-agent.md +0 -37
@@ -0,0 +1,474 @@
1
+ """Behavioral tests for the executable Codex Sol advisor path."""
2
+
3
+ import io
4
+ import importlib.util
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from collections.abc import Callable
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+
12
+ import pytest
13
+
14
+ _ProcessRunner = Callable[..., subprocess.CompletedProcess[str]]
15
+
16
+
17
+ def _load_sol_module() -> ModuleType:
18
+ scripts_root = Path(__file__).parent.parent
19
+ config_root = scripts_root / "config"
20
+ sys.path.insert(0, str(config_root))
21
+ specification = importlib.util.spec_from_file_location(
22
+ "codex_sol_advisor", scripts_root / "codex_sol_advisor.py"
23
+ )
24
+ assert specification is not None and specification.loader is not None
25
+ module = importlib.util.module_from_spec(specification)
26
+ sys.modules[specification.name] = module
27
+ specification.loader.exec_module(module)
28
+ return module
29
+
30
+
31
+ sol_advisor = _load_sol_module()
32
+ SCRIPTS_ROOT = Path(__file__).parent.parent
33
+ USAGE_PROBE_PATH = (
34
+ SCRIPTS_ROOT.parents[2]
35
+ / "skills"
36
+ / "codex-review"
37
+ / "scripts"
38
+ / "codex_usage_probe.py"
39
+ )
40
+
41
+
42
+ def _probe_process(
43
+ payload: object,
44
+ returncode: int = 0,
45
+ ) -> subprocess.CompletedProcess[str]:
46
+ return subprocess.CompletedProcess(
47
+ [sys.executable, str(USAGE_PROBE_PATH)],
48
+ returncode,
49
+ json.dumps(payload),
50
+ "",
51
+ )
52
+
53
+
54
+ def _event_stream(
55
+ session_id: str = "thread-1",
56
+ guidance: str = "PLAN\ninspect the change",
57
+ ) -> str:
58
+ return "\n".join(
59
+ [
60
+ json.dumps({"type": "thread.started", "thread_id": session_id}),
61
+ json.dumps(
62
+ {
63
+ "type": "item.completed",
64
+ "item": {
65
+ "type": "agent_message",
66
+ "text": guidance,
67
+ },
68
+ }
69
+ ),
70
+ ]
71
+ )
72
+
73
+
74
+ def _two_step_process_runner(calls: list[list[str]], guidance: str) -> _ProcessRunner:
75
+ def process_runner(
76
+ arguments: list[str], **kwargs: object
77
+ ) -> subprocess.CompletedProcess[str]:
78
+ calls.append(arguments)
79
+ if len(calls) == 1:
80
+ return _probe_process({"percent_left": 90})
81
+ return subprocess.CompletedProcess(
82
+ arguments, 0, _event_stream(guidance=guidance), ""
83
+ )
84
+
85
+ return process_runner
86
+
87
+
88
+ def test_sol_flag_accepts_documented_truthy_values() -> None:
89
+ assert sol_advisor.is_sol_advisor_enabled({"ADVISOR_SOL_XHIGH": "yes"})
90
+ assert not sol_advisor.is_sol_advisor_enabled({"ADVISOR_SOL_XHIGH": "0"})
91
+
92
+
93
+ def test_resolve_usage_probe_path_uses_supplied_home_directory(tmp_path: Path) -> None:
94
+ probe_path = sol_advisor.resolve_usage_probe_path(tmp_path)
95
+
96
+ assert probe_path == (
97
+ tmp_path
98
+ / ".claude"
99
+ / "skills"
100
+ / "codex-review"
101
+ / "scripts"
102
+ / "codex_usage_probe.py"
103
+ )
104
+
105
+
106
+ def test_argument_parser_accepts_bind_and_resume_modes() -> None:
107
+ parser = sol_advisor.build_argument_parser()
108
+
109
+ bind_arguments = parser.parse_args(["--bind", "--cwd", "."])
110
+ resume_arguments = parser.parse_args(
111
+ ["--resume", "thread-1", "--cwd", "."]
112
+ )
113
+
114
+ assert bind_arguments.bind
115
+ assert bind_arguments.resume is None
116
+ assert resume_arguments.resume == "thread-1"
117
+
118
+
119
+ def test_main_serializes_stable_result_field(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None:
120
+ monkeypatch.setattr(
121
+ sol_advisor,
122
+ "run_codex_sol_advisor",
123
+ lambda **kwargs: sol_advisor.CodexSolAdvisorReply(
124
+ session_id="thread-1",
125
+ guidance="PLAN\ninspect",
126
+ successful=True,
127
+ reason=None,
128
+ is_fallback=False,
129
+ signal="PLAN",
130
+ sol_enabled=True,
131
+ selected_tier=sol_advisor.ADVISOR_MODEL_TIER,
132
+ outcome=sol_advisor.CODEX_BIND_SUCCESS_TOKEN,
133
+ ),
134
+ )
135
+ monkeypatch.setattr(sys, "stdin", io.StringIO("first consult"))
136
+
137
+ exit_code = sol_advisor.main(["--bind", "--cwd", "."])
138
+ payload = json.loads(capsys.readouterr().out)
139
+
140
+ assert exit_code == 0
141
+ assert payload["result"] == "codex"
142
+ assert "outcome" not in payload
143
+ assert payload["sol_enabled"] is True
144
+
145
+
146
+ def test_bind_and_resume_arguments_match_installed_codex_interface() -> None:
147
+ expected_common_arguments = [
148
+ "codex",
149
+ "exec",
150
+ "--model",
151
+ sol_advisor.ADVISOR_CODEX_MODEL_ID,
152
+ "--config",
153
+ 'model_reasoning_effort="xhigh"',
154
+ "--sandbox",
155
+ "read-only",
156
+ "--json",
157
+ ]
158
+ assert sol_advisor.build_codex_arguments() == [
159
+ *expected_common_arguments,
160
+ "-",
161
+ ]
162
+ assert sol_advisor.build_codex_arguments("thread-1") == [
163
+ *expected_common_arguments,
164
+ "resume",
165
+ "thread-1",
166
+ "-",
167
+ ]
168
+
169
+
170
+ def test_successful_probe_requires_finite_meter_above_configured_gate() -> None:
171
+ def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
172
+ return _probe_process({"percent_left": 90})
173
+
174
+ preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
175
+
176
+ assert preflight.eligible
177
+ assert preflight.percent_left == 90
178
+
179
+
180
+ @pytest.mark.parametrize("percent_left", [10, 10.0, 10.000])
181
+ def test_meter_at_configured_threshold_falls_back(
182
+ percent_left: float,
183
+ ) -> None:
184
+ def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
185
+ return _probe_process({"percent_left": percent_left})
186
+
187
+ preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
188
+
189
+ assert not preflight.eligible
190
+ assert preflight.percent_left == percent_left
191
+
192
+
193
+ @pytest.mark.parametrize(
194
+ "payload",
195
+ [
196
+ {"percent_left": None},
197
+ {"percent_left": "90"},
198
+ {"percent_left": True},
199
+ {"percent_left": float("nan")},
200
+ ["percent_left", 90],
201
+ ],
202
+ )
203
+ def test_unknown_and_malformed_meter_falls_back(payload: object) -> None:
204
+ def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
205
+ return _probe_process(payload)
206
+
207
+ preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
208
+
209
+ assert not preflight.eligible
210
+ assert preflight.percent_left is None
211
+
212
+
213
+ def test_malformed_probe_json_falls_back() -> None:
214
+ def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
215
+ return subprocess.CompletedProcess(arguments, 0, "{broken", "")
216
+
217
+ preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
218
+
219
+ assert not preflight.eligible
220
+
221
+
222
+ def test_probe_nonzero_exit_falls_back() -> None:
223
+ def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
224
+ return _probe_process({"percent_left": 90}, returncode=2)
225
+
226
+ preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
227
+
228
+ assert not preflight.eligible
229
+
230
+
231
+ def test_probe_timeout_falls_back() -> None:
232
+ def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
233
+ raise subprocess.TimeoutExpired(arguments, 30)
234
+
235
+ preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
236
+
237
+ assert not preflight.eligible
238
+ assert "timed out" in preflight.reason
239
+
240
+
241
+ @pytest.mark.parametrize("signal", ["ENDORSE", "CORRECTION", "PLAN", "STOP"])
242
+ def test_jsonl_parser_accepts_each_exact_guidance_signal(signal: str) -> None:
243
+ reply = sol_advisor.parse_codex_jsonl_reply(
244
+ _event_stream(guidance=f"{signal}\nadditional guidance"),
245
+ existing_session_id=None,
246
+ is_sol_enabled=True,
247
+ )
248
+
249
+ assert reply.successful
250
+ assert not reply.is_fallback
251
+ assert reply.session_id == "thread-1"
252
+ assert reply.guidance == f"{signal}\nadditional guidance"
253
+
254
+
255
+ @pytest.mark.parametrize(
256
+ "jsonl_text",
257
+ [
258
+ "{broken",
259
+ json.dumps({"type": "thread.started", "thread_id": "thread-1"}),
260
+ _event_stream(session_id="", guidance="PLAN\ninspect"),
261
+ _event_stream(guidance="ENDORSE: ready"),
262
+ _event_stream(guidance="\n unknown signal\nmore"),
263
+ json.dumps(
264
+ {
265
+ "type": "item.completed",
266
+ "item": {"type": "agent_message", "text": "PLAN"},
267
+ }
268
+ ),
269
+ ],
270
+ )
271
+ def test_jsonl_parser_returns_typed_fallback_for_invalid_reply(
272
+ jsonl_text: str,
273
+ ) -> None:
274
+ reply = sol_advisor.parse_codex_jsonl_reply(
275
+ jsonl_text,
276
+ existing_session_id=None,
277
+ is_sol_enabled=True,
278
+ )
279
+
280
+ assert not reply.successful
281
+ assert reply.is_fallback
282
+ assert reply.session_id is None
283
+ assert reply.guidance is None
284
+ assert reply.reason
285
+
286
+
287
+ def test_bind_runs_probe_then_codex_with_read_only_xhigh_settings() -> None:
288
+ calls: list[tuple[list[str], dict[str, object]]] = []
289
+
290
+ def process_runner(
291
+ arguments: list[str], **kwargs: object
292
+ ) -> subprocess.CompletedProcess[str]:
293
+ calls.append((arguments, kwargs))
294
+ if len(calls) == 1:
295
+ return _probe_process({"percent_left": 90})
296
+ return subprocess.CompletedProcess(
297
+ arguments, 0, _event_stream(guidance="PLAN\ninspect"), ""
298
+ )
299
+
300
+ reply = sol_advisor.run_codex_sol_advisor(
301
+ prompt="reply only",
302
+ working_directory=Path("."),
303
+ preflight=None,
304
+ probe_path=USAGE_PROBE_PATH,
305
+ session_id=None,
306
+ process_runner=process_runner,
307
+ setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
308
+ )
309
+
310
+ assert reply.successful
311
+ assert len(calls) == 2
312
+ assert calls[1][0] == sol_advisor.build_codex_arguments()
313
+ assert calls[1][1]["cwd"] == "."
314
+ assert calls[1][1]["shell"] is False
315
+ assert calls[1][1]["timeout"]
316
+
317
+
318
+ def test_team_advisor_path_preserves_sol_routing_fields() -> None:
319
+ team_advisor_path = SCRIPTS_ROOT.parents[2] / "skills" / "team-advisor" / "SKILL.md"
320
+ sol_rung_path = SCRIPTS_ROOT.parent / "reference" / "sol-rung.md"
321
+ assert "advisor-protocol.md" in team_advisor_path.read_text(encoding="utf-8")
322
+ sol_rung = sol_rung_path.read_text(encoding="utf-8")
323
+ assert "codex_sol_advisor.py" in sol_rung
324
+ assert "--resume <session_id>" in sol_rung
325
+ calls: list[list[str]] = []
326
+
327
+ reply = sol_advisor.run_codex_sol_advisor(
328
+ prompt="first consult",
329
+ working_directory=Path("."),
330
+ preflight=None,
331
+ probe_path=USAGE_PROBE_PATH,
332
+ session_id=None,
333
+ process_runner=_two_step_process_runner(calls, guidance="PLAN\ninspect"),
334
+ setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
335
+ )
336
+
337
+ assert reply.successful
338
+ assert reply.sol_enabled
339
+ assert reply.selected_tier == sol_advisor.ADVISOR_MODEL_TIER
340
+ assert reply.outcome == sol_advisor.CODEX_BIND_SUCCESS_TOKEN
341
+ assert reply.signal == "PLAN"
342
+ assert reply.guidance == "PLAN\ninspect"
343
+
344
+
345
+ def test_team_advisor_path_uses_fable_result_when_sol_gate_is_closed() -> None:
346
+ calls: list[list[str]] = []
347
+
348
+ def process_runner(
349
+ arguments: list[str], **kwargs: object
350
+ ) -> subprocess.CompletedProcess[str]:
351
+ calls.append(arguments)
352
+ return _probe_process({"percent_left": 10})
353
+
354
+ reply = sol_advisor.run_codex_sol_advisor(
355
+ prompt="first consult",
356
+ working_directory=Path("."),
357
+ preflight=None,
358
+ probe_path=USAGE_PROBE_PATH,
359
+ session_id=None,
360
+ process_runner=process_runner,
361
+ setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
362
+ )
363
+
364
+ assert not reply.successful
365
+ assert reply.is_fallback
366
+ assert reply.sol_enabled
367
+ assert reply.selected_tier == sol_advisor.ADVISOR_FALLBACK_TIER
368
+ assert reply.outcome == sol_advisor.ADVISOR_FALLBACK_RESULT
369
+ assert len(calls) == 1
370
+
371
+
372
+ def test_disabled_flag_uses_default_optional_routing_inputs() -> None:
373
+ reply = sol_advisor.run_codex_sol_advisor(
374
+ prompt="first consult",
375
+ working_directory=Path("."),
376
+ preflight=sol_advisor.SolPreflight(
377
+ eligible=True,
378
+ percent_left=90,
379
+ reason="test preflight",
380
+ ),
381
+ probe_path=None,
382
+ setting_by_name=None,
383
+ session_id=None,
384
+ process_runner=subprocess.run,
385
+ )
386
+
387
+ assert reply.is_fallback
388
+ assert reply.reason == "Sol advisor flag is disabled"
389
+
390
+
391
+ def test_resume_runs_the_usage_gate_before_codex() -> None:
392
+ calls: list[list[str]] = []
393
+
394
+ reply = sol_advisor.run_codex_sol_advisor(
395
+ prompt="resume",
396
+ working_directory=Path("."),
397
+ preflight=None,
398
+ session_id="thread-1",
399
+ probe_path=USAGE_PROBE_PATH,
400
+ process_runner=_two_step_process_runner(calls, guidance="ENDORSE\nready"),
401
+ setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
402
+ )
403
+
404
+ assert reply.successful
405
+ assert calls == [
406
+ [sys.executable, str(USAGE_PROBE_PATH)],
407
+ sol_advisor.build_codex_arguments("thread-1"),
408
+ ]
409
+
410
+
411
+ @pytest.mark.parametrize(
412
+ "codex_failure",
413
+ [
414
+ "nonzero",
415
+ "timeout",
416
+ "malformed_jsonl",
417
+ "missing_session",
418
+ "missing_guidance",
419
+ "invalid_signal",
420
+ ],
421
+ )
422
+ def test_codex_failure_modes_always_return_fallback(
423
+ codex_failure: str,
424
+ ) -> None:
425
+ def process_runner(
426
+ arguments: list[str], **kwargs: object
427
+ ) -> subprocess.CompletedProcess[str]:
428
+ if len(arguments) == 2:
429
+ return _probe_process({"percent_left": 90})
430
+ if codex_failure == "timeout":
431
+ raise subprocess.TimeoutExpired(arguments, 30)
432
+ if codex_failure == "nonzero":
433
+ return subprocess.CompletedProcess(
434
+ arguments, 3, _event_stream(guidance="PLAN"), ""
435
+ )
436
+ if codex_failure == "malformed_jsonl":
437
+ return subprocess.CompletedProcess(arguments, 0, "{broken", "")
438
+ if codex_failure == "missing_session":
439
+ return subprocess.CompletedProcess(
440
+ arguments,
441
+ 0,
442
+ json.dumps(
443
+ {
444
+ "type": "item.completed",
445
+ "item": {"type": "agent_message", "text": "PLAN"},
446
+ }
447
+ ),
448
+ "",
449
+ )
450
+ if codex_failure == "missing_guidance":
451
+ return subprocess.CompletedProcess(
452
+ arguments,
453
+ 0,
454
+ json.dumps({"type": "thread.started", "thread_id": "thread-1"}),
455
+ "",
456
+ )
457
+ return subprocess.CompletedProcess(
458
+ arguments, 0, _event_stream(guidance="PLAN: inspect"), ""
459
+ )
460
+
461
+ reply = sol_advisor.run_codex_sol_advisor(
462
+ prompt="bind",
463
+ working_directory=Path("."),
464
+ preflight=None,
465
+ probe_path=USAGE_PROBE_PATH,
466
+ session_id=None,
467
+ process_runner=process_runner,
468
+ setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
469
+ )
470
+
471
+ assert not reply.successful
472
+ assert reply.is_fallback
473
+ assert reply.session_id is None
474
+ assert reply.guidance is None
@@ -33,6 +33,8 @@ main = model_tier_run_validator.main
33
33
  load_model_tier_run_from_json_path = (
34
34
  model_tier_run_validator.load_model_tier_run_from_json_path
35
35
  )
36
+ ADVISOR_MODEL_TIER = model_tier_run_validator.ADVISOR_MODEL_TIER
37
+ CODEX_BIND_SUCCESS_TOKEN = model_tier_run_validator.CODEX_BIND_SUCCESS_TOKEN
36
38
 
37
39
 
38
40
  def test_clean_single_spawn_at_top_of_slice_passes() -> None:
@@ -45,6 +47,66 @@ def test_clean_single_spawn_at_top_of_slice_passes() -> None:
45
47
  assert validate_model_tier_run(run) is None
46
48
 
47
49
 
50
+ def test_sol_codex_bind_is_first_success_when_enabled() -> None:
51
+ run = ModelTierRun(
52
+ own_tier="Opus",
53
+ candidate_tiers=[ADVISOR_MODEL_TIER, "Fable", "Opus"],
54
+ attempts=[{"tier": ADVISOR_MODEL_TIER, "result": CODEX_BIND_SUCCESS_TOKEN}],
55
+ selected_tier=ADVISOR_MODEL_TIER,
56
+ is_sol_enabled=True,
57
+ )
58
+ assert validate_model_tier_run(run) is None
59
+
60
+
61
+ def test_sol_failure_falls_through_to_fable_when_enabled() -> None:
62
+ run = ModelTierRun(
63
+ own_tier="Opus",
64
+ candidate_tiers=[ADVISOR_MODEL_TIER, "Fable", "Opus"],
65
+ attempts=[
66
+ {"tier": ADVISOR_MODEL_TIER, "result": "unavailable"},
67
+ {"tier": "Fable", "result": "spawned"},
68
+ ],
69
+ selected_tier="Fable",
70
+ is_sol_enabled=True,
71
+ )
72
+ assert validate_model_tier_run(run) is None
73
+
74
+
75
+ def test_sol_rung_precedes_third_party_cli_floor_when_enabled() -> None:
76
+ run = ModelTierRun(
77
+ own_tier="ThirdParty",
78
+ candidate_tiers=[ADVISOR_MODEL_TIER, "Fable", "Opus"],
79
+ attempts=[{"tier": ADVISOR_MODEL_TIER, "result": CODEX_BIND_SUCCESS_TOKEN}],
80
+ selected_tier=ADVISOR_MODEL_TIER,
81
+ is_sol_enabled=True,
82
+ )
83
+
84
+ assert validate_model_tier_run(run) is None
85
+
86
+
87
+ def test_sol_codex_result_requires_sol_candidate() -> None:
88
+ run = ModelTierRun(
89
+ own_tier="Opus",
90
+ candidate_tiers=["Fable", "Opus"],
91
+ attempts=[{"tier": "Fable", "result": CODEX_BIND_SUCCESS_TOKEN}],
92
+ selected_tier="Fable",
93
+ )
94
+ with pytest.raises(ModelTierRunError):
95
+ validate_model_tier_run(run)
96
+
97
+
98
+ def test_sol_spawned_result_does_not_count_as_codex_success() -> None:
99
+ run = ModelTierRun(
100
+ own_tier="Opus",
101
+ candidate_tiers=[ADVISOR_MODEL_TIER, "Fable", "Opus"],
102
+ attempts=[{"tier": ADVISOR_MODEL_TIER, "result": "spawned"}],
103
+ selected_tier=ADVISOR_MODEL_TIER,
104
+ is_sol_enabled=True,
105
+ )
106
+ with pytest.raises(ModelTierRunError):
107
+ validate_model_tier_run(run)
108
+
109
+
48
110
  def test_fallthrough_to_floor_tier_passes() -> None:
49
111
  run = ModelTierRun(
50
112
  own_tier="Opus",
@@ -223,6 +285,23 @@ def test_cli_validates_json_log_file(tmp_path: Path) -> None:
223
285
  assert loaded_run.selected_tier == "Fable"
224
286
 
225
287
 
288
+ def test_cli_rejects_non_boolean_sol_enabled(tmp_path: Path) -> None:
289
+ log_path = tmp_path / "invalid-sol-enabled.json"
290
+ log_path.write_text(
291
+ json.dumps(
292
+ {
293
+ "own_tier": "Opus",
294
+ "candidate_tiers": ["Fable", "Opus"],
295
+ "attempts": [{"tier": "Fable", "result": "spawned"}],
296
+ "selected_tier": "Fable",
297
+ "sol_enabled": "false",
298
+ }
299
+ ),
300
+ encoding="utf-8",
301
+ )
302
+ assert main([str(log_path)]) == 2
303
+
304
+
226
305
  def test_cli_rejects_incomplete_fallback_log(tmp_path: Path) -> None:
227
306
  log_path = tmp_path / "incomplete-walk.json"
228
307
  log_path.write_text(
@@ -10,6 +10,26 @@ from types import ModuleType
10
10
  import pytest
11
11
 
12
12
 
13
+ constants_root = Path(__file__).parent.parent / "config"
14
+ if str(constants_root) not in sys.path:
15
+ sys.path.insert(0, str(constants_root))
16
+
17
+ from advisor_scripts_constants.model_tier_run_validator_constants import ( # noqa: E402
18
+ ADVISOR_SENDMESSAGE_REPLY_WAIT_SECONDS,
19
+ ALL_CLI_MODEL_ID_BY_TIER,
20
+ ALL_KNOWN_TIER_NAMES,
21
+ ALL_MODEL_TIERS,
22
+ HOST_PROFILE_CLAUDE,
23
+ HOST_PROFILE_THIRD_PARTY,
24
+ THIRD_PARTY_MODEL_TIER,
25
+ )
26
+ from advisor_scripts_constants.advisor_route_constants import ( # noqa: E402
27
+ ADVISOR_CODEX_MODEL_ID,
28
+ ADVISOR_MODEL_TIER,
29
+ ALL_CODEX_MODEL_ID_BY_TIER,
30
+ )
31
+
32
+
13
33
  def _load_tier_model_ids_module() -> ModuleType:
14
34
  scripts_root = Path(__file__).parent.parent
15
35
  module_path = scripts_root / "tier_model_ids.py"
@@ -26,21 +46,9 @@ def _load_tier_model_ids_module() -> ModuleType:
26
46
 
27
47
  tier_model_ids = _load_tier_model_ids_module()
28
48
  resolve_cli_model_id = tier_model_ids.resolve_cli_model_id
49
+ resolve_codex_model_id = tier_model_ids.resolve_codex_model_id
29
50
  canonical_tier_name = tier_model_ids.canonical_tier_name
30
51
  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
- HOST_PROFILE_CLAUDE,
41
- HOST_PROFILE_THIRD_PARTY,
42
- THIRD_PARTY_MODEL_TIER,
43
- )
44
52
 
45
53
  SCRIPTS_ROOT = Path(__file__).parent.parent
46
54
  DOCUMENTED_RESOLVE_ONE_LINER = (
@@ -94,14 +102,28 @@ def test_sendmessage_reply_wait_is_positive_bound() -> None:
94
102
  assert ADVISOR_SENDMESSAGE_REPLY_WAIT_SECONDS == 120
95
103
 
96
104
 
105
+ def test_resolve_codex_model_id_maps_sol() -> None:
106
+ assert resolve_codex_model_id(f" {ADVISOR_MODEL_TIER.lower()} ") == (
107
+ ADVISOR_CODEX_MODEL_ID
108
+ )
109
+
110
+
111
+ def test_resolve_codex_model_id_rejects_claude_tier() -> None:
112
+ with pytest.raises(ValueError, match="not a known model tier"):
113
+ resolve_codex_model_id("Opus")
114
+
115
+
97
116
  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)
117
+ all_cli_tiers = (*ALL_MODEL_TIERS, THIRD_PARTY_MODEL_TIER)
118
+ assert set(ALL_CLI_MODEL_ID_BY_TIER) == set(all_cli_tiers)
99
119
  assert set(ALL_MODEL_TIERS).issubset(set(ALL_KNOWN_TIER_NAMES))
120
+ assert ADVISOR_MODEL_TIER in ALL_KNOWN_TIER_NAMES
100
121
  assert THIRD_PARTY_MODEL_TIER in ALL_KNOWN_TIER_NAMES
101
122
  assert THIRD_PARTY_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
- )
123
+ assert all(ALL_CLI_MODEL_ID_BY_TIER[each_tier] for each_tier in all_cli_tiers)
124
+ assert ALL_CODEX_MODEL_ID_BY_TIER == {
125
+ ADVISOR_MODEL_TIER: ADVISOR_CODEX_MODEL_ID,
126
+ }
105
127
 
106
128
 
107
129
  def test_canonical_tier_name_strips_and_normalizes() -> None:
@@ -45,6 +45,9 @@ from advisor_scripts_constants.model_tier_run_validator_constants import ( # no
45
45
  UNKNOWN_HOST_PROFILE_ERROR,
46
46
  UNKNOWN_LADDER_NAME_ERROR,
47
47
  )
48
+ from advisor_scripts_constants.advisor_route_constants import ( # noqa: E402
49
+ ALL_CODEX_MODEL_ID_BY_TIER,
50
+ )
48
51
 
49
52
 
50
53
  def canonical_tier_name(tier_name: str) -> str | None:
@@ -111,6 +114,27 @@ def resolve_cli_model_id(tier: str) -> str:
111
114
  return maybe_model_alias
112
115
 
113
116
 
117
+ def resolve_codex_model_id(tier: str) -> str:
118
+ """Return the dated Codex model id for a Codex-backed advisor tier.
119
+
120
+ Args:
121
+ tier: Ladder tier name whose Codex model id should be resolved.
122
+
123
+ Returns:
124
+ The configured Codex model id.
125
+
126
+ Raises:
127
+ ValueError: When ``tier`` is not a known Codex-backed tier.
128
+ """
129
+ maybe_canonical_tier = canonical_tier_name(tier)
130
+ if maybe_canonical_tier is None:
131
+ raise ValueError(UNKNOWN_LADDER_NAME_ERROR.format(tier))
132
+ maybe_model_id = ALL_CODEX_MODEL_ID_BY_TIER.get(maybe_canonical_tier)
133
+ if maybe_model_id is None:
134
+ raise ValueError(UNKNOWN_LADDER_NAME_ERROR.format(tier))
135
+ return maybe_model_id
136
+
137
+
114
138
  def detect_host_profile(
115
139
  setting_by_name: Mapping[str, str] | None = None,
116
140
  ) -> str:
@@ -1,6 +1,6 @@
1
1
  # _shared/pr-loop
2
2
 
3
- Runtime documents and scripts shared by every PR-loop skill. Changes here affect `bugteam`, `pr-converge`, `findbugs`, `fixbugs`, and `qbug` simultaneously — treat this as a breaking-change surface.
3
+ Runtime documents and scripts shared by every PR-loop skill. Changes here affect `bugteam`, `pr-converge`, `clean-room audit`, and `pr-fix-protocol` simultaneously — treat this as a breaking-change surface.
4
4
 
5
5
  ## Key documents
6
6